Compare commits

...

26 Commits

Author SHA1 Message Date
Ren Amamiya
a896300f23 Merge pull request #118 from luminous-devs/v2.1.2
v2.1.2
2023-11-22 16:18:02 +07:00
d3cf1200ba bump version 2023-11-22 16:13:06 +07:00
b5ac3df090 fix package 2023-11-22 16:10:20 +07:00
3b40dd6903 update dependencies 2023-11-22 15:27:19 +07:00
Ren Amamiya
efba6b20ea Merge pull request #117 from luminous-devs/feat/optional-updater
Make auto update is optional
2023-11-22 10:34:49 +07:00
Ren Amamiya
05fb56e5fc Merge pull request #116 from vivganes/patch-1
Little grammar corrections
2023-11-22 10:32:47 +07:00
Vivek Ganesan
59d9646e9f Little grammar corrections 2023-11-22 08:43:31 +05:30
b73d84fccb update storage provider 2023-11-22 09:05:10 +07:00
1929ceb72d add toast message 2023-11-22 08:31:58 +07:00
a1d22c1daf make auto update is optional 2023-11-22 08:30:43 +07:00
cf7af1ba64 fix ci again again 2023-11-20 08:47:16 +07:00
933ca758ee fix ci again 2023-11-20 08:27:37 +07:00
f537209b92 fix ci 2023-11-20 08:16:39 +07:00
6777610b07 update dependencies 2023-11-20 08:02:09 +07:00
88803cd3cd bump version 2023-11-19 19:51:00 +07:00
Ren Amamiya
6adf5933b0 Merge pull request #113 from luminous-devs/hotfix/themes
Add support dark mode for toaster
2023-11-19 19:49:59 +07:00
9521a49fff support dark mode for toaster 2023-11-19 19:49:26 +07:00
Ren Amamiya
5789a105f5 Merge pull request #112 from luminous-devs/hotfix/settings
Fix settings screen
2023-11-19 15:15:11 +07:00
b7a18bea34 respect user settings 2023-11-19 14:50:59 +07:00
7117ed05a9 update settings screen 2023-11-19 08:48:01 +07:00
c53bdb68e5 add change theme function 2023-11-18 21:17:37 +07:00
6725dca807 fix all dms bugs 2023-11-17 16:03:12 +07:00
Ren Amamiya
077712cf43 Merge pull request #105 from luminous-devs/feat/v2.1.0
v2.1.0
2023-11-17 10:06:58 +07:00
2794c78ee1 add new private key screen 2023-11-17 09:24:21 +07:00
21574023db update dark mode 2023-11-17 08:16:25 +07:00
954b729dc9 wip: settings screen 2023-11-16 17:48:29 +07:00
49 changed files with 2185 additions and 1132 deletions

View File

@@ -2,7 +2,7 @@
"name": "lume", "name": "lume",
"description": "the communication app", "description": "the communication app",
"private": true, "private": true,
"version": "2.1.0", "version": "2.1.2",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
@@ -19,7 +19,7 @@
}, },
"dependencies": { "dependencies": {
"@evilmartians/harmony": "^1.1.0", "@evilmartians/harmony": "^1.1.0",
"@getalby/sdk": "^2.5.0", "@getalby/sdk": "^2.6.0",
"@nostr-dev-kit/ndk": "^2.0.5", "@nostr-dev-kit/ndk": "^2.0.5",
"@nostr-fetch/adapter-ndk": "^0.13.1", "@nostr-fetch/adapter-ndk": "^0.13.1",
"@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-alert-dialog": "^1.0.5",
@@ -29,11 +29,13 @@
"@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-toolbar": "^1.0.4", "@radix-ui/react-toolbar": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^5.8.1", "@tanstack/react-query": "^5.8.4",
"@tauri-apps/api": "2.0.0-alpha.11", "@tauri-apps/api": "2.0.0-alpha.11",
"@tauri-apps/cli": "2.0.0-alpha.17", "@tauri-apps/cli": "2.0.0-alpha.17",
"@tauri-apps/plugin-autostart": "2.0.0-alpha.3",
"@tauri-apps/plugin-clipboard-manager": "2.0.0-alpha.3", "@tauri-apps/plugin-clipboard-manager": "2.0.0-alpha.3",
"@tauri-apps/plugin-dialog": "2.0.0-alpha.3", "@tauri-apps/plugin-dialog": "2.0.0-alpha.3",
"@tauri-apps/plugin-fs": "2.0.0-alpha.3", "@tauri-apps/plugin-fs": "2.0.0-alpha.3",
@@ -57,13 +59,13 @@
"@tiptap/starter-kit": "^2.1.12", "@tiptap/starter-kit": "^2.1.12",
"@tiptap/suggestion": "^2.1.12", "@tiptap/suggestion": "^2.1.12",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"framer-motion": "^10.16.4", "framer-motion": "^10.16.5",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"idb-keyval": "^6.2.1", "idb-keyval": "^6.2.1",
"light-bolt11-decoder": "^3.0.0", "light-bolt11-decoder": "^3.0.0",
"lru-cache": "^10.0.2", "lru-cache": "^10.0.3",
"markdown-to-jsx": "^7.3.2", "markdown-to-jsx": "^7.3.2",
"media-chrome": "^1.5.2", "media-chrome": "^1.5.3",
"minidenticons": "^4.2.0", "minidenticons": "^4.2.0",
"nanoid": "^5.0.3", "nanoid": "^5.0.3",
"nostr-fetch": "^0.13.1", "nostr-fetch": "^0.13.1",
@@ -75,47 +77,47 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.48.2", "react-hook-form": "^7.48.2",
"react-hotkeys-hook": "^4.4.1", "react-hotkeys-hook": "^4.4.1",
"react-router-dom": "^6.18.0", "react-router-dom": "^6.19.0",
"react-string-replace": "^1.1.1", "react-string-replace": "^1.1.1",
"reactflow": "^11.10.1", "reactflow": "^11.10.1",
"sonner": "^1.2.0", "sonner": "^1.2.2",
"tailwind-scrollbar": "^3.0.5", "tailwind-scrollbar": "^3.0.5",
"tauri-controls": "github:reyamir/tauri-controls", "tauri-controls": "github:reyamir/tauri-controls",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"tiptap-markdown": "^0.8.4", "tiptap-markdown": "^0.8.4",
"virtua": "^0.16.2", "virtua": "^0.16.5",
"zustand": "^4.4.6" "zustand": "^4.4.6"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.10",
"@trivago/prettier-plugin-sort-imports": "^4.3.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/html-to-text": "^9.0.4", "@types/html-to-text": "^9.0.4",
"@types/node": "^20.9.0", "@types/node": "^20.9.4",
"@types/react": "^18.2.37", "@types/react": "^18.2.38",
"@types/react-dom": "^18.2.15", "@types/react-dom": "^18.2.17",
"@types/youtube-player": "^5.5.10", "@types/youtube-player": "^5.5.11",
"@typescript-eslint/eslint-plugin": "^6.10.0", "@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.10.0", "@typescript-eslint/parser": "^6.12.0",
"@vitejs/plugin-react-swc": "^3.4.1", "@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"csstype": "^3.1.2", "csstype": "^3.1.2",
"encoding": "^0.1.13", "encoding": "^0.1.13",
"eslint": "^8.53.0", "eslint": "^8.54.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-react": "^7.33.2", "eslint-plugin-react": "^7.33.2",
"eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-simple-import-sort": "^10.0.0",
"husky": "^8.0.3", "husky": "^8.0.3",
"lint-staged": "^15.0.2", "lint-staged": "^15.1.0",
"postcss": "^8.4.31", "postcss": "^8.4.31",
"prettier": "^3.0.3", "prettier": "^3.1.0",
"prettier-plugin-tailwindcss": "^0.5.7", "prettier-plugin-tailwindcss": "^0.5.7",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"tailwind-merge": "^1.14.0", "tailwind-merge": "^1.14.0",
"tailwindcss": "^3.3.5", "tailwindcss": "^3.3.5",
"typescript": "^5.2.2", "typescript": "^5.3.2",
"vite": "^4.5.0", "vite": "^4.5.0",
"vite-tsconfig-paths": "^4.2.1" "vite-tsconfig-paths": "^4.2.1"
} }

1291
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

655
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lume" name = "lume"
version = "2.1.0" version = "2.1.2"
description = "the communication app" description = "the communication app"
authors = ["Ren Amamiya"] authors = ["Ren Amamiya"]
license = "GPL-3.0" license = "GPL-3.0"
@@ -33,6 +33,7 @@ tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspac
tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-upload = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } tauri-plugin-upload = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-theme = { git = "https://github.com/wyhaya/tauri-plugin-theme" }
tauri-plugin-sql = { git = "hhttps://github.com/tauri-apps/plugins-workspace", branch = "v2", features = [ tauri-plugin-sql = { git = "hhttps://github.com/tauri-apps/plugins-workspace", branch = "v2", features = [
"sqlite", "sqlite",
] } ] }

View File

@@ -7,6 +7,7 @@ use keyring::Entry;
use std::time::Duration; use std::time::Duration;
use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_sql::{Migration, MigrationKind}; use tauri_plugin_sql::{Migration, MigrationKind};
use tauri_plugin_theme::ThemePlugin;
use webpage::{Webpage, WebpageOptions}; use webpage::{Webpage, WebpageOptions};
#[derive(Clone, serde::Serialize)] #[derive(Clone, serde::Serialize)]
@@ -105,6 +106,7 @@ fn secure_remove(key: String) -> Result<(), ()> {
} }
fn main() { fn main() {
let mut ctx = tauri::generate_context!();
tauri::Builder::default() tauri::Builder::default()
.setup(|app| { .setup(|app| {
#[cfg(desktop)] #[cfg(desktop)]
@@ -113,6 +115,7 @@ fn main() {
.plugin(tauri_plugin_updater::Builder::new().build())?; .plugin(tauri_plugin_updater::Builder::new().build())?;
Ok(()) Ok(())
}) })
.plugin(ThemePlugin::init(ctx.config_mut()))
.plugin( .plugin(
tauri_plugin_sql::Builder::default() tauri_plugin_sql::Builder::default()
.add_migrations( .add_migrations(
@@ -134,10 +137,6 @@ fn main() {
) )
.build(), .build(),
) )
.plugin(tauri_plugin_autostart::init(
MacosLauncher::LaunchAgent,
Some(vec!["--flag1", "--flag2"]),
))
.plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
@@ -148,12 +147,16 @@ fn main() {
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_upload::init()) .plugin(tauri_plugin_upload::init())
.plugin(tauri_plugin_window_state::Builder::default().build()) .plugin(tauri_plugin_window_state::Builder::default().build())
.plugin(tauri_plugin_autostart::init(
MacosLauncher::LaunchAgent,
Some(vec![]),
))
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
opengraph, opengraph,
secure_save, secure_save,
secure_load, secure_load,
secure_remove secure_remove
]) ])
.run(tauri::generate_context!()) .run(ctx)
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@@ -9,7 +9,7 @@
}, },
"package": { "package": {
"productName": "Lume", "productName": "Lume",
"version": "2.1.0" "version": "2.1.2"
}, },
"plugins": { "plugins": {
"fs": { "fs": {

View File

@@ -7,13 +7,13 @@ import { OnboardingScreen } from '@app/auth/onboarding';
import { ChatsScreen } from '@app/chats'; import { ChatsScreen } from '@app/chats';
import { ErrorScreen } from '@app/error'; import { ErrorScreen } from '@app/error';
import { ExploreScreen } from '@app/explore'; import { ExploreScreen } from '@app/explore';
import { NewScreen } from '@app/new';
import { useStorage } from '@libs/storage/provider'; import { useStorage } from '@libs/storage/provider';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { AppLayout } from '@shared/layouts/app'; import { AppLayout } from '@shared/layouts/app';
import { AuthLayout } from '@shared/layouts/auth'; import { AuthLayout } from '@shared/layouts/auth';
import { NewLayout } from '@shared/layouts/new';
import { NoteLayout } from '@shared/layouts/note'; import { NoteLayout } from '@shared/layouts/note';
import { SettingsLayout } from '@shared/layouts/settings'; import { SettingsLayout } from '@shared/layouts/settings';
@@ -115,7 +115,7 @@ export default function App() {
}, },
{ {
path: '/new', path: '/new',
element: <NewScreen />, element: <NewLayout />,
errorElement: <ErrorScreen />, errorElement: <ErrorScreen />,
children: [ children: [
{ {
@@ -139,6 +139,13 @@ export default function App() {
return { Component: NewFileScreen }; return { Component: NewFileScreen };
}, },
}, },
{
path: 'privkey',
async lazy() {
const { NewPrivkeyScreen } = await import('@app/new/privkey');
return { Component: NewPrivkeyScreen };
},
},
], ],
}, },
{ {

View File

@@ -45,6 +45,8 @@ export function CreateAccountScreen() {
const onSubmit = async (data: { name: string; about: string }) => { const onSubmit = async (data: { name: string; about: string }) => {
try { try {
if (!ndk.signer) return navigate('/new/privkey');
setLoading(true); setLoading(true);
const profile = { const profile = {

View File

@@ -47,7 +47,6 @@ export function OnboardEnrichScreen() {
setLoading(true); setLoading(true);
const tags = arrayToNIP02(follows); const tags = arrayToNIP02(follows);
const event = new NDKEvent(ndk); const event = new NDKEvent(ndk);
event.content = ''; event.content = '';
event.kind = NDKKind.Contacts; event.kind = NDKKind.Contacts;

View File

@@ -1,5 +1,5 @@
import { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk'; import { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { VList, VListHandle } from 'virtua'; import { VList, VListHandle } from 'virtua';
@@ -16,8 +16,6 @@ import { User } from '@shared/user';
import { useNostr } from '@utils/hooks/useNostr'; import { useNostr } from '@utils/hooks/useNostr';
export function ChatScreen() { export function ChatScreen() {
const listRef = useRef<VListHandle>(null);
const { db } = useStorage(); const { db } = useStorage();
const { ndk } = useNDK(); const { ndk } = useNDK();
const { pubkey } = useParams(); const { pubkey } = useParams();
@@ -30,10 +28,39 @@ export function ChatScreen() {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}); });
const queryClient = useQueryClient();
const listRef = useRef<VListHandle>(null);
const newMessage = useMutation({
mutationFn: async (event: NDKEvent) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: ['nip04-dm', pubkey] });
// Snapshot the previous value
const prevMessages = queryClient.getQueryData(['nip04-dm', pubkey]);
// Optimistically update to the new value
queryClient.setQueryData(['nip04-dm', pubkey], (prev: NDKEvent[]) => [
...prev,
event,
]);
// Return a context object with the snapshotted value
return { prevMessages };
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['nip04-dm', pubkey] });
},
});
const renderItem = useCallback( const renderItem = useCallback(
(message: NDKEvent) => { (message: NDKEvent) => {
return ( return (
<ChatMessage message={message} self={message.pubkey === db.account.pubkey} /> <ChatMessage
key={message.id}
message={message}
isSelf={message.pubkey === db.account.pubkey}
/>
); );
}, },
[data] [data]
@@ -57,7 +84,7 @@ export function ChatScreen() {
); );
sub.addListener('event', (event) => { sub.addListener('event', (event) => {
console.log(event); newMessage.mutate(event);
}); });
return () => { return () => {
@@ -96,11 +123,7 @@ export function ChatScreen() {
)} )}
</div> </div>
<div className="shrink-0 rounded-b-lg border-t border-neutral-300 bg-neutral-200 p-3 dark:border-neutral-700 dark:bg-neutral-800"> <div className="shrink-0 rounded-b-lg border-t border-neutral-300 bg-neutral-200 p-3 dark:border-neutral-700 dark:bg-neutral-800">
<ChatForm <ChatForm receiverPubkey={pubkey} />
receiverPubkey={pubkey}
userPubkey={db.account.pubkey}
userPrivkey={''}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; import { NDKEvent, NDKKind, NDKUser } from '@nostr-dev-kit/ndk';
import { nip04 } from 'nostr-tools'; import { useState } from 'react';
import { useCallback, useState } from 'react'; import { toast } from 'sonner';
import { MediaUploader } from '@app/chats/components/mediaUploader'; import { MediaUploader } from '@app/chats/components/mediaUploader';
@@ -8,34 +8,26 @@ import { useNDK } from '@libs/ndk/provider';
import { EnterIcon } from '@shared/icons'; import { EnterIcon } from '@shared/icons';
export function ChatForm({ export function ChatForm({ receiverPubkey }: { receiverPubkey: string }) {
receiverPubkey,
userPrivkey,
}: {
receiverPubkey: string;
userPubkey: string;
userPrivkey: string;
}) {
const { ndk } = useNDK(); const { ndk } = useNDK();
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const encryptMessage = useCallback(async () => {
return await nip04.encrypt(userPrivkey, receiverPubkey, value);
}, [receiverPubkey, value]);
const submit = async () => { const submit = async () => {
const message = await encryptMessage(); try {
const tags = [['p', receiverPubkey]]; const recipient = new NDKUser({ pubkey: receiverPubkey });
const message = await ndk.signer.encrypt(recipient, value);
const event = new NDKEvent(ndk); const event = new NDKEvent(ndk);
event.content = message; event.content = message;
event.kind = NDKKind.EncryptedDirectMessage; event.kind = NDKKind.EncryptedDirectMessage;
event.tags = tags; event.tag(recipient);
await event.publish(); const publish = await event.publish();
// reset state if (publish) setValue('');
setValue(''); } catch (e) {
toast.error(e);
}
}; };
const handleEnterPress = (e: { const handleEnterPress = (e: {
@@ -61,7 +53,7 @@ export function ChatForm({
autoComplete="off" autoComplete="off"
autoCorrect="off" autoCorrect="off"
autoCapitalize="off" autoCapitalize="off"
placeholder="Message" placeholder="Message..."
className="h-10 flex-1 resize-none bg-transparent px-3 text-neutral-900 placeholder:text-neutral-600 focus:outline-none dark:text-neutral-100 dark:placeholder:text-neutral-300" className="h-10 flex-1 resize-none bg-transparent px-3 text-neutral-900 placeholder:text-neutral-600 focus:outline-none dark:text-neutral-100 dark:placeholder:text-neutral-300"
/> />
<button <button

View File

@@ -3,14 +3,14 @@ import { twMerge } from 'tailwind-merge';
import { useDecryptMessage } from '@app/chats/hooks/useDecryptMessage'; import { useDecryptMessage } from '@app/chats/hooks/useDecryptMessage';
export function ChatMessage({ message, self }: { message: NDKEvent; self: boolean }) { export function ChatMessage({ message, isSelf }: { message: NDKEvent; isSelf: boolean }) {
const decryptedContent = useDecryptMessage(message); const decryptedContent = useDecryptMessage(message);
return ( return (
<div <div
className={twMerge( className={twMerge(
'my-2 w-max max-w-[400px] rounded-t-xl px-3 py-3', 'my-2 w-max max-w-[400px] rounded-t-xl px-3 py-3',
self isSelf
? 'ml-auto rounded-l-xl bg-blue-500 text-white' ? 'ml-auto rounded-l-xl bg-blue-500 text-white'
: 'rounded-r-xl bg-neutral-200 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100' : 'rounded-r-xl bg-neutral-200 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100'
)} )}
@@ -18,9 +18,7 @@ export function ChatMessage({ message, self }: { message: NDKEvent; self: boolea
{!decryptedContent ? ( {!decryptedContent ? (
<p>Decrypting...</p> <p>Decrypting...</p>
) : ( ) : (
<div> <p className="select-text whitespace-pre-line break-all">{decryptedContent}</p>
<p className="select-text whitespace-pre-line">{decryptedContent}</p>
</div>
)} )}
</div> </div>
); );

View File

@@ -1,15 +1,20 @@
import { NDKEvent } from '@nostr-dev-kit/ndk'; import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useCallback } from 'react'; import { useCallback, useEffect } from 'react';
import { Outlet } from 'react-router-dom'; import { Outlet, useNavigate } from 'react-router-dom';
import { ChatListItem } from '@app/chats/components/chatListItem'; import { ChatListItem } from '@app/chats/components/chatListItem';
import { useNDK } from '@libs/ndk/provider';
import { LoaderIcon } from '@shared/icons'; import { LoaderIcon } from '@shared/icons';
import { useNostr } from '@utils/hooks/useNostr'; import { useNostr } from '@utils/hooks/useNostr';
export function ChatsScreen() { export function ChatsScreen() {
const navigate = useNavigate();
const { ndk } = useNDK();
const { getAllNIP04Chats } = useNostr(); const { getAllNIP04Chats } = useNostr();
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: ['nip04-chats'], queryKey: ['nip04-chats'],
@@ -29,6 +34,10 @@ export function ChatsScreen() {
[data] [data]
); );
useEffect(() => {
if (!ndk.signer) navigate('/new/privkey');
}, []);
return ( return (
<div className="grid h-full w-full grid-cols-3"> <div className="grid h-full w-full grid-cols-3">
<div className="col-span-1 h-full overflow-y-auto border-r border-neutral-200 scrollbar-none dark:border-neutral-800"> <div className="col-span-1 h-full overflow-y-auto border-r border-neutral-200 scrollbar-none dark:border-neutral-800">

View File

@@ -57,7 +57,7 @@ export function ErrorScreen() {
Sorry, an unexpected error has occurred. Sorry, an unexpected error has occurred.
</h1> </h1>
<h3 className="text-3xl font-semibold leading-snug text-white"> <h3 className="text-3xl font-semibold leading-snug text-white">
Don&apos;t be panic, your account is safe. Don&apos;t panic, your account is safe.
<br /> <br />
Here are what things you can do: Here are what things you can do:
</h3> </h3>
@@ -65,7 +65,7 @@ export function ErrorScreen() {
<div className="flex w-full flex-col gap-3"> <div className="flex w-full flex-col gap-3">
<div className="flex items-center justify-between rounded-xl bg-blue-700 px-3 py-4"> <div className="flex items-center justify-between rounded-xl bg-blue-700 px-3 py-4">
<div className="text-xl font-semibold text-white"> <div className="text-xl font-semibold text-white">
1. Try close and re-open app 1. Try to close and re-open the app
</div> </div>
<button <button
type="button" type="button"
@@ -112,12 +112,12 @@ export function ErrorScreen() {
<div className="rounded-xl bg-blue-700 px-3 py-4"> <div className="rounded-xl bg-blue-700 px-3 py-4">
<div className="flex w-full flex-col gap-1.5"> <div className="flex w-full flex-col gap-1.5">
<div className="text-xl font-semibold text-white"> <div className="text-xl font-semibold text-white">
4. Use other Nostr client 4. Use another Nostr client
</div> </div>
<div className="select-text text-lg font-medium text-blue-300"> <div className="select-text text-lg font-medium text-blue-300">
<p> <p>
While waiting Lume&apos;s Devs release the bug fixes, you always can use While waiting for Lume&apos;s Devs to release the bug fixes, you always can use
other Nostr client with your account: other Nostr clients with your account:
</p> </p>
<div className="mt-2 flex flex-col gap-1 text-white"> <div className="mt-2 flex flex-col gap-1 text-white">
<a href="https://snort.social" className="hover:!underline"> <a href="https://snort.social" className="hover:!underline">

View File

@@ -5,6 +5,7 @@ import Placeholder from '@tiptap/extension-placeholder';
import { EditorContent, FloatingMenu, useEditor } from '@tiptap/react'; import { EditorContent, FloatingMenu, useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit'; import StarterKit from '@tiptap/starter-kit';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { Markdown } from 'tiptap-markdown'; import { Markdown } from 'tiptap-markdown';
@@ -31,6 +32,7 @@ export function NewArticleScreen() {
const [summary, setSummary] = useState({ open: false, content: '' }); const [summary, setSummary] = useState({ open: false, content: '' });
const [cover, setCover] = useState(''); const [cover, setCover] = useState('');
const navigate = useNavigate();
const ident = useMemo(() => String(Date.now()), []); const ident = useMemo(() => String(Date.now()), []);
const editor = useEditor({ const editor = useEditor({
extensions: [ extensions: [
@@ -65,6 +67,8 @@ export function NewArticleScreen() {
const submit = async () => { const submit = async () => {
try { try {
if (!ndk.signer) return navigate('/new/privkey');
setLoading(true); setLoading(true);
// get markdown content // get markdown content

View File

@@ -2,6 +2,7 @@ import { NDKEvent } from '@nostr-dev-kit/ndk';
import { message, open } from '@tauri-apps/plugin-dialog'; import { message, open } from '@tauri-apps/plugin-dialog';
import { readBinaryFile } from '@tauri-apps/plugin-fs'; import { readBinaryFile } from '@tauri-apps/plugin-fs';
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useNDK } from '@libs/ndk/provider'; import { useNDK } from '@libs/ndk/provider';
@@ -10,6 +11,7 @@ import { LoaderIcon } from '@shared/icons';
export function NewFileScreen() { export function NewFileScreen() {
const { ndk } = useNDK(); const { ndk } = useNDK();
const navigate = useNavigate();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isPublish, setIsPublish] = useState(false); const [isPublish, setIsPublish] = useState(false);
@@ -84,6 +86,8 @@ export function NewFileScreen() {
const submit = async () => { const submit = async () => {
try { try {
if (!ndk.signer) return navigate('/new/privkey');
setIsPublish(true); setIsPublish(true);
const event = new NDKEvent(ndk); const event = new NDKEvent(ndk);

View File

@@ -1,77 +0,0 @@
import { Link, NavLink, Outlet } from 'react-router-dom';
import { twMerge } from 'tailwind-merge';
import { WindowTitlebar } from 'tauri-controls';
import { useStorage } from '@libs/storage/provider';
import { ArrowLeftIcon } from '@shared/icons';
export function NewScreen() {
const { db } = useStorage();
return (
<div className="flex h-screen w-screen flex-col bg-neutral-50 dark:bg-neutral-950">
{db.platform !== 'macos' ? (
<WindowTitlebar />
) : (
<div data-tauri-drag-region className="h-9" />
)}
<div data-tauri-drag-region className="h-6" />
<div className="flex h-full min-h-0 w-full">
<div className="container mx-auto grid grid-cols-8 px-4">
<div className="col-span-1">
<Link
to="/"
className="inline-flex h-10 w-10 items-center justify-center rounded-lg bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-900"
>
<ArrowLeftIcon className="h-5 w-5" />
</Link>
</div>
<div className="relative col-span-6 flex flex-col">
<div className="mb-8 flex h-10 shrink-0 items-center gap-3">
<div className="flex h-10 items-center gap-2 rounded-lg bg-neutral-100 px-0.5 dark:bg-neutral-800">
<NavLink
to="/new/"
className={({ isActive }) =>
twMerge(
'inline-flex h-9 w-20 items-center justify-center rounded-lg text-sm font-medium',
isActive ? 'bg-white shadow dark:bg-black' : 'bg-transparent'
)
}
>
Post
</NavLink>
<NavLink
to="/new/article"
className={({ isActive }) =>
twMerge(
'inline-flex h-9 w-20 items-center justify-center rounded-lg text-sm font-medium',
isActive ? 'bg-white shadow dark:bg-black' : 'bg-transparent'
)
}
>
Article
</NavLink>
<NavLink
to="/new/file"
className={({ isActive }) =>
twMerge(
'inline-flex h-9 w-28 items-center justify-center rounded-lg text-sm font-medium',
isActive ? 'bg-white shadow dark:bg-black' : 'bg-transparent'
)
}
>
File Sharing
</NavLink>
</div>
</div>
<div className="h-full min-h-0 w-full">
<Outlet />
</div>
</div>
<div className="col-span-1" />
</div>
</div>
</div>
);
}

View File

@@ -6,7 +6,7 @@ import { EditorContent, useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit'; import StarterKit from '@tiptap/starter-kit';
import { convert } from 'html-to-text'; import { convert } from 'html-to-text';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { MediaUploader, MentionPopup } from '@app/new/components'; import { MediaUploader, MentionPopup } from '@app/new/components';
@@ -27,6 +27,7 @@ export function NewPostScreen() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate();
const editor = useEditor({ const editor = useEditor({
extensions: [ extensions: [
StarterKit.configure(), StarterKit.configure(),
@@ -54,6 +55,8 @@ export function NewPostScreen() {
const submit = async () => { const submit = async () => {
try { try {
if (!ndk.signer) return navigate('/new/privkey');
setLoading(true); setLoading(true);
// get plaintext content // get plaintext content

86
src/app/new/privkey.tsx Normal file
View File

@@ -0,0 +1,86 @@
import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
import { getPublicKey, nip19 } from 'nostr-tools';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider';
export function NewPrivkeyScreen() {
const { db } = useStorage();
const { ndk } = useNDK();
const [nsec, setNsec] = useState('');
const navigate = useNavigate();
const save = async (content: string) => {
return await db.secureSave(db.account.pubkey, content);
};
const submit = async (isSave?: boolean) => {
try {
if (!nsec.startsWith('nsec1'))
return toast.info('You must enter a private key starts with nsec');
const decoded = nip19.decode(nsec);
if (decoded.type !== 'nsec') return toast.info('You must enter a valid nsec');
const privkey = decoded.data;
const pubkey = getPublicKey(privkey);
if (pubkey !== db.account.pubkey)
return toast.info(
'Your nsec is not match your current public key, please make sure you enter right nsec'
);
const signer = new NDKPrivateKeySigner(privkey);
ndk.signer = signer;
if (isSave) await save(privkey);
navigate(-1);
} catch (e) {
toast.error(e);
}
};
return (
<div className="flex h-full w-full items-center justify-center">
<div className="mb-16 flex flex-col gap-3">
<h1 className="text-center font-semibold text-neutral-900 dark:text-neutral-100">
You need to provide private key to sign nostr event.
</h1>
<input
name="privkey"
placeholder="nsec..."
type="password"
value={nsec}
onChange={(e) => setNsec(e.target.value)}
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
className="h-11 w-full rounded-lg bg-neutral-100 px-3 py-2 placeholder:text-neutral-500 dark:bg-neutral-900 dark:placeholder:text-neutral-400"
/>
<div className="mt-2 flex flex-col gap-2">
<button
type="button"
onClick={() => submit()}
className="inline-flex h-9 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
>
Submit
</button>
<button
type="button"
onClick={() => submit(true)}
className="inline-flex h-9 w-full shrink-0 items-center justify-center rounded-lg bg-neutral-100 font-medium text-neutral-900 hover:bg-neutral-200 dark:bg-neutral-900 dark:text-neutral-100 dark:hover:bg-neutral-800"
>
Submit and Save
</button>
</div>
</div>
</div>
);
}

View File

@@ -1,3 +1,76 @@
import { getVersion } from '@tauri-apps/api/app';
import { relaunch } from '@tauri-apps/plugin-process';
import { Update, check } from '@tauri-apps/plugin-updater';
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { toast } from 'sonner';
export function AboutScreen() { export function AboutScreen() {
return <div></div>; const [version, setVersion] = useState('');
const [newUpdate, setNewUpdate] = useState<Update>(null);
const checkUpdate = async () => {
const update = await check();
if (!update) toast.info('There is no update available');
setNewUpdate(update);
};
const installUpdate = async () => {
await newUpdate.downloadAndInstall();
await relaunch();
};
useEffect(() => {
async function loadVersion() {
const appVersion = await getVersion();
setVersion(appVersion);
}
loadVersion();
}, []);
return (
<div className="mx-auto w-full max-w-lg">
<div className="flex items-center justify-center gap-2">
<img src="/icon.png" alt="Lume's logo" className="w-16 shrink-0" />
<div>
<h1 className="text-xl font-semibold">Lume</h1>
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
Version {version}
</p>
</div>
</div>
<div className="mx-auto mt-4 flex w-full max-w-xs flex-col gap-2">
{!newUpdate ? (
<button
type="button"
onClick={() => checkUpdate()}
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 text-sm font-medium text-white hover:bg-blue-600"
>
Check for update
</button>
) : (
<button
type="button"
onClick={() => installUpdate()}
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 text-sm font-medium text-white hover:bg-blue-600"
>
Install {newUpdate.version}
</button>
)}
<Link
to="https://lume.nu"
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-neutral-100 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
Website
</Link>
<Link
to="https://github.com/luminous-devs/lume/issues"
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-neutral-100 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
Report a issue
</Link>
</div>
</div>
);
} }

View File

@@ -1,3 +1,29 @@
import { useStorage } from '@libs/storage/provider';
export function AdvancedSettingScreen() { export function AdvancedSettingScreen() {
return <div></div>; const { db } = useStorage();
const clearCache = async () => {
await db.clearCache();
};
return (
<div className="mx-auto w-full max-w-lg">
<div className="flex flex-col gap-6">
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-8">
<div className="w-24 shrink-0 text-end text-sm font-semibold">Caches</div>
<div className="text-sm">Use for boost up NDK</div>
</div>
<button
type="button"
onClick={() => clearCache()}
className="h-8 w-max rounded-lg bg-blue-500 px-3 text-sm font-medium text-white hover:bg-blue-600"
>
Clear
</button>
</div>
</div>
</div>
);
} }

View File

@@ -1,3 +1,64 @@
import { nip19 } from 'nostr-tools';
import { useEffect, useState } from 'react';
import { useStorage } from '@libs/storage/provider';
import { EyeOffIcon } from '@shared/icons';
export function BackupSettingScreen() { export function BackupSettingScreen() {
return <div></div>; const { db } = useStorage();
const [privkey, setPrivkey] = useState(null);
const [showPassword, setShowPassword] = useState(false);
const removePrivkey = async () => {
await db.secureRemove(db.account.pubkey);
};
useEffect(() => {
async function loadPrivkey() {
const key = await db.secureLoad(db.account.pubkey);
if (key) setPrivkey(key);
}
loadPrivkey();
}, []);
return (
<div className="mx-auto w-full max-w-lg">
<div className="mb-2 text-sm font-semibold">Private key</div>
<div>
{!privkey ? (
<div className="inline-flex h-24 w-full items-center justify-center rounded-lg bg-neutral-100 dark:bg-neutral-900">
You&apos;ve stored private key on Lume
</div>
) : (
<>
<div className="relative">
<input
readOnly
type={showPassword ? 'text' : 'password'}
value={nip19.nsecEncode(privkey)}
className="relative h-11 w-full resize-none rounded-lg bg-neutral-200 py-1 pl-3 pr-11 text-neutral-900 !outline-none placeholder:text-neutral-600 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-400"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-1.5 top-1/2 inline-flex h-8 w-8 -translate-y-1/2 transform items-center justify-center rounded-lg bg-neutral-50 dark:bg-neutral-950"
>
<EyeOffIcon className="h-4 w-4" />
</button>
</div>
<button
type="button"
onClick={() => removePrivkey()}
className="mt-2 inline-flex h-9 w-full items-center justify-center gap-2 rounded-lg bg-red-200 px-6 font-medium text-red-500 hover:bg-red-500 hover:text-white focus:outline-none dark:hover:text-white"
>
Remove private key
</button>
</>
)}
</div>
</div>
);
} }

View File

@@ -3,6 +3,7 @@ import { useQueryClient } from '@tanstack/react-query';
import { message } from '@tauri-apps/plugin-dialog'; import { message } from '@tauri-apps/plugin-dialog';
import { useState } from 'react'; import { useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { useNDK } from '@libs/ndk/provider'; import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider'; import { useStorage } from '@libs/storage/provider';
@@ -13,6 +14,7 @@ import { useNostr } from '@utils/hooks/useNostr';
export function EditProfileScreen() { export function EditProfileScreen() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const navigate = useNavigate();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [picture, setPicture] = useState(''); const [picture, setPicture] = useState('');
@@ -46,10 +48,11 @@ export function EditProfileScreen() {
const uploadAvatar = async () => { const uploadAvatar = async () => {
try { try {
if (!ndk.signer) return navigate('/new/privkey');
setLoading(true); setLoading(true);
const image = await upload(); const image = await upload();
if (image) { if (image) {
setPicture(image); setPicture(image);
setLoading(false); setLoading(false);

View File

@@ -1,3 +1,277 @@
import * as Switch from '@radix-ui/react-switch';
import { invoke } from '@tauri-apps/api/primitives';
import { getCurrent } from '@tauri-apps/api/window';
import { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart';
import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notification';
import { useEffect, useState } from 'react';
import { twMerge } from 'tailwind-merge';
import { useStorage } from '@libs/storage/provider';
import { DarkIcon, LightIcon, SystemModeIcon } from '@shared/icons';
export function GeneralSettingScreen() { export function GeneralSettingScreen() {
return <div></div>; const { db } = useStorage();
const [settings, setSettings] = useState({
autoupdate: false,
autolaunch: false,
outbox: false,
media: true,
hashtag: true,
notification: true,
appearance: 'system',
});
const changeTheme = async (theme: 'light' | 'dark' | 'auto') => {
await invoke('plugin:theme|set_theme', { theme });
// update state
setSettings((prev) => ({ ...prev, appearance: theme }));
};
const toggleAutolaunch = async () => {
if (!settings.autolaunch) {
await enable();
// update state
setSettings((prev) => ({ ...prev, autolaunch: true }));
} else {
await disable();
// update state
setSettings((prev) => ({ ...prev, autolaunch: false }));
}
};
const toggleOutbox = async () => {
await db.createSetting('outbox', String(+!settings.outbox));
// update state
setSettings((prev) => ({ ...prev, outbox: !settings.outbox }));
};
const toggleMedia = async () => {
await db.createSetting('media', String(+!settings.media));
db.settings.media = !settings.media;
// update state
setSettings((prev) => ({ ...prev, media: !settings.media }));
};
const toggleHashtag = async () => {
await db.createSetting('hashtag', String(+!settings.hashtag));
db.settings.hashtag = !settings.hashtag;
// update state
setSettings((prev) => ({ ...prev, hashtag: !settings.hashtag }));
};
const toggleAutoupdate = async () => {
await db.createSetting('autoupdate', String(+!settings.autoupdate));
db.settings.autoupdate = !settings.autoupdate;
// update state
setSettings((prev) => ({ ...prev, autoupdate: !settings.autoupdate }));
};
const toggleNofitication = async () => {
if (settings.notification) return;
await requestPermission();
// update state
setSettings((prev) => ({ ...prev, notification: !settings.notification }));
};
useEffect(() => {
async function loadSettings() {
const theme = await getCurrent().theme();
setSettings((prev) => ({ ...prev, appearance: theme }));
const autostart = await isEnabled();
setSettings((prev) => ({ ...prev, autolaunch: autostart }));
const permissionGranted = await isPermissionGranted();
setSettings((prev) => ({ ...prev, notification: permissionGranted }));
const data = await db.getAllSettings();
if (!data) return;
data.forEach((item) => {
if (item.key === 'autoupdate')
setSettings((prev) => ({
...prev,
autoupdate: !!parseInt(item.value),
}));
if (item.key === 'outbox')
setSettings((prev) => ({
...prev,
outbox: !!parseInt(item.value),
}));
if (item.key === 'media')
setSettings((prev) => ({
...prev,
media: !!parseInt(item.value),
}));
if (item.key === 'hashtag')
setSettings((prev) => ({
...prev,
hashtag: !!parseInt(item.value),
}));
if (item.key === 'notification')
setSettings((prev) => ({
...prev,
notification: !!parseInt(item.value),
}));
});
}
loadSettings();
}, []);
return (
<div className="mx-auto w-full max-w-lg">
<div className="flex flex-col gap-6">
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-8">
<div className="w-24 shrink-0 text-end text-sm font-semibold">Updater</div>
<div className="text-sm">Auto download new update at Login</div>
</div>
<Switch.Root
checked={settings.autoupdate}
onClick={() => toggleAutoupdate()}
className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
>
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
</div>
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-8">
<div className="w-24 shrink-0 text-end text-sm font-semibold">Startup</div>
<div className="text-sm">Launch Lume at Login</div>
</div>
<Switch.Root
checked={settings.autolaunch}
onClick={() => toggleAutolaunch()}
className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
>
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
</div>
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-8">
<div className="w-24 shrink-0 text-end text-sm font-semibold">Gossip</div>
<div className="text-sm">Use Outbox model</div>
</div>
<Switch.Root
checked={settings.outbox}
onClick={() => toggleOutbox()}
className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
>
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
</div>
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-8">
<div className="w-24 shrink-0 text-end text-sm font-semibold">Media</div>
<div className="text-sm">Automatically load media</div>
</div>
<Switch.Root
checked={settings.media}
onClick={() => toggleMedia()}
className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
>
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
</div>
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-8">
<div className="w-24 shrink-0 text-end text-sm font-semibold">Hashtag</div>
<div className="text-sm">Hide all hashtags in content</div>
</div>
<Switch.Root
checked={settings.hashtag}
onClick={() => toggleHashtag()}
className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
>
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
</div>
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-8">
<div className="w-24 shrink-0 text-end text-sm font-semibold">
Notification
</div>
<div className="text-sm">Automatically send notification</div>
</div>
<Switch.Root
checked={settings.notification}
disabled={settings.notification}
onClick={() => toggleNofitication()}
className="relative h-7 w-12 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-neutral-800"
>
<Switch.Thumb className="block h-6 w-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
</div>
<div className="flex w-full items-start gap-8">
<div className="w-24 shrink-0 text-end text-sm font-semibold">Appearance</div>
<div className="flex flex-1 gap-6">
<button
type="button"
onClick={() => changeTheme('light')}
className="flex flex-col items-center justify-center gap-0.5"
>
<div
className={twMerge(
'inline-flex h-11 w-11 items-center justify-center rounded-lg',
settings.appearance === 'light'
? 'bg-blue-500 text-white'
: 'bg-neutral-100 dark:bg-neutral-900'
)}
>
<LightIcon className="h-5 w-5" />
</div>
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
Light
</p>
</button>
<button
type="button"
onClick={() => changeTheme('dark')}
className="flex flex-col items-center justify-center gap-0.5"
>
<div
className={twMerge(
'inline-flex h-11 w-11 items-center justify-center rounded-lg',
settings.appearance === 'dark'
? 'bg-blue-500 text-white'
: 'bg-neutral-100 dark:bg-neutral-900'
)}
>
<DarkIcon className="h-5 w-5" />
</div>
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
Dark
</p>
</button>
<button
type="button"
onClick={() => changeTheme('auto')}
className="flex flex-col items-center justify-center gap-0.5"
>
<div
className={twMerge(
'inline-flex h-11 w-11 items-center justify-center rounded-lg',
settings.appearance === 'auto'
? 'bg-blue-500 text-white'
: 'bg-neutral-100 dark:bg-neutral-900'
)}
>
<SystemModeIcon className="h-5 w-5" />
</div>
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
System
</p>
</button>
</div>
</div>
</div>
</div>
);
} }

View File

@@ -2,7 +2,7 @@ import { NDKEvent, NDKKind, NDKUser } from '@nostr-dev-kit/ndk';
import * as Avatar from '@radix-ui/react-avatar'; import * as Avatar from '@radix-ui/react-avatar';
import { minidenticon } from 'minidenticons'; import { minidenticon } from 'minidenticons';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { UserStats } from '@app/users/components/stats'; import { UserStats } from '@app/users/components/stats';
@@ -21,6 +21,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
const { user } = useProfile(pubkey); const { user } = useProfile(pubkey);
const [followed, setFollowed] = useState(false); const [followed, setFollowed] = useState(false);
const navigate = useNavigate();
const svgURI = const svgURI =
'data:image/svg+xml;utf8,' + encodeURIComponent(minidenticon(pubkey, 90, 50)); 'data:image/svg+xml;utf8,' + encodeURIComponent(minidenticon(pubkey, 90, 50));
@@ -43,6 +44,8 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
const unfollow = async (pubkey: string) => { const unfollow = async (pubkey: string) => {
try { try {
if (!ndk.signer) return navigate('/new/privkey');
const user = ndk.getUser({ pubkey: db.account.pubkey }); const user = ndk.getUser({ pubkey: db.account.pubkey });
const contacts = await user.follows(); const contacts = await user.follows();
contacts.delete(new NDKUser({ pubkey: pubkey })); contacts.delete(new NDKUser({ pubkey: pubkey }));

View File

@@ -20,11 +20,18 @@ export class LumeStorage {
public db: Database; public db: Database;
public account: Account | null; public account: Account | null;
public platform: Platform | null; public platform: Platform | null;
public settings: {
autoupdate: boolean;
outbox: boolean;
media: boolean;
hashtag: boolean;
};
constructor(sqlite: Database, platform: Platform) { constructor(sqlite: Database, platform: Platform) {
this.db = sqlite; this.db = sqlite;
this.account = null; this.account = null;
this.platform = platform; this.platform = platform;
this.settings = { autoupdate: false, outbox: false, media: true, hashtag: true };
} }
public async secureSave(key: string, value: string) { public async secureSave(key: string, value: string) {
@@ -429,10 +436,28 @@ export class LumeStorage {
} }
public async createSetting(key: string, value: string) { public async createSetting(key: string, value: string) {
return await this.db.execute( const currentSetting = await this.getSettingValue(key);
'INSERT OR IGNORE INTO settings (key, value) VALUES ($1, $2);',
[key, value] if (!currentSetting)
return await this.db.execute(
'INSERT OR IGNORE INTO settings (key, value) VALUES ($1, $2);',
[key, value]
);
const currentValue = !!parseInt(currentSetting);
return await this.db.execute('UPDATE settings SET value = $1 WHERE key = $2;', [
+!currentValue,
key,
]);
}
public async getAllSettings() {
const results: { key: string; value: string }[] = await this.db.select(
'SELECT * FROM settings ORDER BY id DESC;'
); );
if (results.length < 1) return null;
return results;
} }
public async getSettingValue(key: string) { public async getSettingValue(key: string) {
@@ -444,6 +469,12 @@ export class LumeStorage {
return results[0].value; return results[0].value;
} }
public async clearCache() {
await this.db.execute('DELETE FROM ndk_events;');
await this.db.execute('DELETE FROM ndk_eventtags;');
await this.db.execute('DELETE FROM ndk_users;');
}
public async accountLogout() { public async accountLogout() {
// update current account status // update current account status
await this.db.execute("UPDATE accounts SET is_active = '0' WHERE id = $1;", [ await this.db.execute("UPDATE accounts SET is_active = '0' WHERE id = $1;", [

View File

@@ -1,4 +1,3 @@
import { appConfigDir } from '@tauri-apps/api/path';
import { message } from '@tauri-apps/plugin-dialog'; import { message } from '@tauri-apps/plugin-dialog';
import { platform } from '@tauri-apps/plugin-os'; import { platform } from '@tauri-apps/plugin-os';
import { relaunch } from '@tauri-apps/plugin-process'; import { relaunch } from '@tauri-apps/plugin-process';
@@ -21,7 +20,7 @@ const StorageContext = createContext<StorageContext>({
db: undefined, db: undefined,
}); });
const StorageProvider = ({ children }: PropsWithChildren<object>) => { const StorageInstance = () => {
const [db, setDB] = useState<LumeStorage>(undefined); const [db, setDB] = useState<LumeStorage>(undefined);
const [isNewVersion, setIsNewVersion] = useState(false); const [isNewVersion, setIsNewVersion] = useState(false);
@@ -29,22 +28,41 @@ const StorageProvider = ({ children }: PropsWithChildren<object>) => {
try { try {
const sqlite = await Database.load('sqlite:lume_v2.db'); const sqlite = await Database.load('sqlite:lume_v2.db');
const platformName = await platform(); const platformName = await platform();
const dir = await appConfigDir();
const lumeStorage = new LumeStorage(sqlite, platformName); const lumeStorage = new LumeStorage(sqlite, platformName);
if (!lumeStorage.account) await lumeStorage.getActiveAccount(); if (!lumeStorage.account) await lumeStorage.getActiveAccount();
// check update const settings = await lumeStorage.getAllSettings();
const update = await check(); let autoUpdater = false;
if (update) {
setIsNewVersion(true);
await update.downloadAndInstall(); if (settings) {
await relaunch(); settings.forEach((item) => {
if (item.key === 'outbox') lumeStorage.settings.outbox = !!parseInt(item.value);
if (item.key === 'media') lumeStorage.settings.media = !!parseInt(item.value);
if (item.key === 'hashtag')
lumeStorage.settings.hashtag = !!parseInt(item.value);
if (item.key === 'autoupdate') {
if (parseInt(item.value)) autoUpdater = true;
}
});
}
if (autoUpdater) {
// check update
const update = await check();
// install new version
if (update) {
setIsNewVersion(true);
await update.downloadAndInstall();
await relaunch();
}
} }
setDB(lumeStorage); setDB(lumeStorage);
console.info(dir);
} catch (e) { } catch (e) {
await message(`Cannot initialize database: ${e}`, { await message(`Cannot initialize database: ${e}`, {
title: 'Lume', title: 'Lume',
@@ -57,6 +75,12 @@ const StorageProvider = ({ children }: PropsWithChildren<object>) => {
if (!db) initLumeStorage(); if (!db) initLumeStorage();
}, []); }, []);
return { db, isNewVersion };
};
const StorageProvider = ({ children }: PropsWithChildren<object>) => {
const { db, isNewVersion } = StorageInstance();
if (!db) if (!db)
return ( return (
<div <div
@@ -84,7 +108,7 @@ const StorageProvider = ({ children }: PropsWithChildren<object>) => {
<div className="absolute bottom-5 right-5 inline-flex items-center gap-2.5"> <div className="absolute bottom-5 right-5 inline-flex items-center gap-2.5">
<LoaderIcon className="h-6 w-6 animate-spin text-blue-500" /> <LoaderIcon className="h-6 w-6 animate-spin text-blue-500" />
<p className="font-semibold"> <p className="font-semibold">
{isNewVersion ? 'Found a new version, updating' : 'Checking for updates...'} {isNewVersion ? 'Found a new version, updating...' : 'Starting...'}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -20,7 +20,7 @@ const root = createRoot(container);
root.render( root.render(
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<Toaster position="top-center" closeButton /> <Toaster position="top-center" closeButton theme="system" />
<StorageProvider> <StorageProvider>
<NDKProvider> <NDKProvider>
<App /> <App />

View File

@@ -0,0 +1,24 @@
import { SVGProps } from 'react';
export function AdvancedSettingsIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>
) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M13.75 7h-10m10 0a3.25 3.25 0 116.5 0 3.25 3.25 0 11-6.5 0zm6.5 10h-8m0 0a3.25 3.25 0 11-6.5 0m6.5 0a3.25 3.25 0 10-6.5 0m0 0h-2"
></path>
</svg>
);
}

22
src/shared/icons/dark.tsx Normal file
View File

@@ -0,0 +1,22 @@
import { SVGProps } from 'react';
export function DarkIcon(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="M21.248 11.811a6.5 6.5 0 01-9.06-9.06 9.25 9.25 0 109.06 9.06z"
></path>
</svg>
);
}

View File

@@ -78,3 +78,9 @@ export * from './heading2';
export * from './heading3'; export * from './heading3';
export * from './bold'; export * from './bold';
export * from './italic'; export * from './italic';
export * from './user';
export * from './advancedSettings';
export * from './info';
export * from './light';
export * from './dark';
export * from './system';

32
src/shared/icons/info.tsx Normal file
View File

@@ -0,0 +1,32 @@
import { SVGProps } from 'react';
export function InfoIcon(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="M10.75 11H12v5.25M21.25 12a9.25 9.25 0 11-18.5 0 9.25 9.25 0 0118.5 0z"
></path>
<rect
width="1.25"
height="1.25"
x="11.375"
y="7.375"
fill="currentColor"
stroke="currentColor"
strokeWidth="0.25"
rx="0.625"
></rect>
</svg>
);
}

View File

@@ -0,0 +1,22 @@
import { SVGProps } from 'react';
export function LightIcon(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.998 3.29V1.769M5.84 18.158l-1.077 1.078m7.235 2.997v-1.524m7.235-15.944l-1.077 1.077M20.707 12h1.523m-4.074 6.159l1.077 1.077M1.766 12h1.523m1.474-7.235L5.84 5.842m9.87 2.446a5.25 5.25 0 11-7.424 7.424 5.25 5.25 0 017.424-7.424z"
></path>
</svg>
);
}

View File

@@ -0,0 +1,22 @@
import { SVGProps } from 'react';
export function SystemModeIcon(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="M3.75 12.25V12a8.25 8.25 0 1116.5 0v.25m-18.5 4h20.5m-15.5 4h10.5"
></path>
</svg>
);
}

21
src/shared/icons/user.tsx Normal file
View File

@@ -0,0 +1,21 @@
import { SVGProps } from 'react';
export function UserIcon(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"
strokeLinejoin="round"
strokeWidth="1.5"
d="M5.857 18.916C7.171 16.996 9.332 15.75 12 15.75c2.668 0 4.83 1.247 6.143 3.166m-12.286 0A9.215 9.215 0 0012 21.25c2.358 0 4.51-.882 6.143-2.334m-12.286 0a9.25 9.25 0 1112.286 0M15.25 10a3.25 3.25 0 11-6.5 0 3.25 3.25 0 016.5 0z"
></path>
</svg>
);
}

View File

@@ -0,0 +1,80 @@
import { Link, NavLink, Outlet, useLocation } from 'react-router-dom';
import { twMerge } from 'tailwind-merge';
import { WindowTitlebar } from 'tauri-controls';
import { useStorage } from '@libs/storage/provider';
import { ArrowLeftIcon } from '@shared/icons';
export function NewLayout() {
const { db } = useStorage();
const location = useLocation();
return (
<div className="flex h-screen w-screen flex-col bg-neutral-50 dark:bg-neutral-950">
{db.platform !== 'macos' ? (
<WindowTitlebar />
) : (
<div data-tauri-drag-region className="h-9" />
)}
<div data-tauri-drag-region className="h-6" />
<div className="flex h-full min-h-0 w-full">
<div className="container mx-auto grid grid-cols-8 px-4">
<div className="col-span-1">
<Link
to="/"
className="inline-flex h-10 w-10 items-center justify-center rounded-lg bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-900"
>
<ArrowLeftIcon className="h-5 w-5" />
</Link>
</div>
<div className="relative col-span-6 flex flex-col">
<div className="mb-8 flex h-10 shrink-0 items-center gap-3">
{location.pathname !== '/new/privkey' ? (
<div className="flex h-10 items-center gap-2 rounded-lg bg-neutral-100 px-0.5 dark:bg-neutral-800">
<NavLink
to="/new/"
className={({ isActive }) =>
twMerge(
'inline-flex h-9 w-20 items-center justify-center rounded-lg text-sm font-medium',
isActive ? 'bg-white shadow dark:bg-black' : 'bg-transparent'
)
}
>
Post
</NavLink>
<NavLink
to="/new/article"
className={({ isActive }) =>
twMerge(
'inline-flex h-9 w-20 items-center justify-center rounded-lg text-sm font-medium',
isActive ? 'bg-white shadow dark:bg-black' : 'bg-transparent'
)
}
>
Article
</NavLink>
<NavLink
to="/new/file"
className={({ isActive }) =>
twMerge(
'inline-flex h-9 w-28 items-center justify-center rounded-lg text-sm font-medium',
isActive ? 'bg-white shadow dark:bg-black' : 'bg-transparent'
)
}
>
File Sharing
</NavLink>
</div>
) : null}
</div>
<div className="h-full min-h-0 w-full">
<Outlet />
</div>
</div>
<div className="col-span-1" />
</div>
</div>
</div>
);
}

View File

@@ -1,10 +1,17 @@
import { NavLink, Outlet, ScrollRestoration } from 'react-router-dom'; import { Link, NavLink, Outlet, ScrollRestoration } from 'react-router-dom';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { WindowTitlebar } from 'tauri-controls'; import { WindowTitlebar } from 'tauri-controls';
import { useStorage } from '@libs/storage/provider'; import { useStorage } from '@libs/storage/provider';
import { SecureIcon, SettingsIcon } from '@shared/icons'; import {
AdvancedSettingsIcon,
ArrowLeftIcon,
InfoIcon,
SecureIcon,
SettingsIcon,
UserIcon,
} from '@shared/icons';
export function SettingsLayout() { export function SettingsLayout() {
const { db } = useStorage(); const { db } = useStorage();
@@ -17,77 +24,89 @@ export function SettingsLayout() {
<div data-tauri-drag-region className="h-9" /> <div data-tauri-drag-region className="h-9" />
)} )}
<div className="flex h-full min-h-0 w-full flex-col gap-8 overflow-y-auto pb-10"> <div className="flex h-full min-h-0 w-full flex-col gap-8 overflow-y-auto pb-10">
<div className="flex h-20 w-full items-end justify-center gap-0.5 border-b border-neutral-200 pb-2 dark:border-neutral-800"> <div className="flex h-20 w-full items-center justify-between border-b border-neutral-200 px-2 pb-2 dark:border-neutral-900">
<NavLink <div>
to="/settings/" <Link
className={({ isActive }) => to="/"
twMerge( className="inline-flex h-12 w-12 items-center justify-center rounded-xl bg-neutral-100 dark:bg-neutral-900"
'flex w-20 shrink-0 flex-col items-center justify-center rounded-lg px-2 py-2 hover:bg-neutral-100 dark:hover:bg-neutral-900', >
isActive <ArrowLeftIcon className="h-5 w-5" />
? 'bg-neutral-100 text-blue-500 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800' </Link>
: '' </div>
) <div className="flex items-center gap-0.5">
} <NavLink
> to="/settings"
<SettingsIcon className="h-6 w-6" /> end
<p className="text-sm font-medium">User</p> className={({ isActive }) =>
</NavLink> twMerge(
<NavLink 'flex w-20 shrink-0 flex-col items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-900',
to="/settings/general" isActive
className={({ isActive }) => ? 'bg-neutral-100 text-blue-500 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800'
twMerge( : ''
'flex w-20 shrink-0 flex-col items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-900', )
isActive }
? 'bg-neutral-100 text-blue-500 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800' >
: '' <UserIcon className="h-6 w-6" />
) <p className="text-sm font-medium">User</p>
} </NavLink>
> <NavLink
<SettingsIcon className="h-6 w-6" /> to="/settings/general"
<p className="text-sm font-medium">General</p> className={({ isActive }) =>
</NavLink> twMerge(
<NavLink 'flex w-20 shrink-0 flex-col items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-900',
to="/settings/backup" isActive
className={({ isActive }) => ? 'bg-neutral-100 text-blue-500 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800'
twMerge( : ''
'flex w-20 shrink-0 flex-col items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-900', )
isActive }
? 'bg-neutral-100 text-blue-500 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800' >
: '' <SettingsIcon className="h-6 w-6" />
) <p className="text-sm font-medium">General</p>
} </NavLink>
> <NavLink
<SecureIcon className="h-6 w-6" /> to="/settings/backup"
<p className="text-sm font-medium">Backup</p> className={({ isActive }) =>
</NavLink> twMerge(
<NavLink 'flex w-20 shrink-0 flex-col items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-900',
to="/settings/advanced" isActive
className={({ isActive }) => ? 'bg-neutral-100 text-blue-500 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800'
twMerge( : ''
'flex w-20 shrink-0 flex-col items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-900', )
isActive }
? 'bg-neutral-100 text-blue-500 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800' >
: '' <SecureIcon className="h-6 w-6" />
) <p className="text-sm font-medium">Backup</p>
} </NavLink>
> <NavLink
<SettingsIcon className="h-6 w-6" /> to="/settings/advanced"
<p className="text-sm font-medium">Advanced</p> className={({ isActive }) =>
</NavLink> twMerge(
<NavLink 'flex w-20 shrink-0 flex-col items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-900',
to="/settings/about" isActive
className={({ isActive }) => ? 'bg-neutral-100 text-blue-500 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800'
twMerge( : ''
'flex w-20 shrink-0 flex-col items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-900', )
isActive }
? 'bg-neutral-100 text-blue-500 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800' >
: '' <AdvancedSettingsIcon className="h-6 w-6" />
) <p className="text-sm font-medium">Advanced</p>
} </NavLink>
> <NavLink
<SettingsIcon className="h-6 w-6" /> to="/settings/about"
<p className="text-sm font-medium">About</p> className={({ isActive }) =>
</NavLink> twMerge(
'flex w-20 shrink-0 flex-col items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-900',
isActive
? 'bg-neutral-100 text-blue-500 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800'
: ''
)
}
>
<InfoIcon className="h-6 w-6" />
<p className="text-sm font-medium">About</p>
</NavLink>
</div>
<div />
</div> </div>
<Outlet /> <Outlet />
<ScrollRestoration /> <ScrollRestoration />

View File

@@ -1,8 +1,11 @@
import { NDKEvent } from '@nostr-dev-kit/ndk'; import { NDKEvent } from '@nostr-dev-kit/ndk';
import * as Popover from '@radix-ui/react-popover'; import * as Popover from '@radix-ui/react-popover';
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useNDK } from '@libs/ndk/provider';
import { ReactionIcon } from '@shared/icons'; import { ReactionIcon } from '@shared/icons';
const REACTIONS = [ const REACTIONS = [
@@ -32,6 +35,9 @@ export function NoteReaction({ event }: { event: NDKEvent }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [reaction, setReaction] = useState<string | null>(null); const [reaction, setReaction] = useState<string | null>(null);
const { ndk } = useNDK();
const navigate = useNavigate();
const getReactionImage = (content: string) => { const getReactionImage = (content: string) => {
const reaction: { img: string } = REACTIONS.find((el) => el.content === content); const reaction: { img: string } = REACTIONS.find((el) => el.content === content);
return reaction.img; return reaction.img;
@@ -39,6 +45,8 @@ export function NoteReaction({ event }: { event: NDKEvent }) {
const react = async (content: string) => { const react = async (content: string) => {
try { try {
if (!ndk.signer) return navigate('/new/privkey');
setReaction(content); setReaction(content);
// react // react

View File

@@ -2,9 +2,12 @@ import { NDKEvent } from '@nostr-dev-kit/ndk';
import * as AlertDialog from '@radix-ui/react-alert-dialog'; import * as AlertDialog from '@radix-ui/react-alert-dialog';
import * as Tooltip from '@radix-ui/react-tooltip'; import * as Tooltip from '@radix-ui/react-tooltip';
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { useNDK } from '@libs/ndk/provider';
import { LoaderIcon, RepostIcon } from '@shared/icons'; import { LoaderIcon, RepostIcon } from '@shared/icons';
export function NoteRepost({ event }: { event: NDKEvent }) { export function NoteRepost({ event }: { event: NDKEvent }) {
@@ -12,8 +15,13 @@ export function NoteRepost({ event }: { event: NDKEvent }) {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isRepost, setIsRepost] = useState(false); const [isRepost, setIsRepost] = useState(false);
const { ndk } = useNDK();
const navigate = useNavigate();
const submit = async () => { const submit = async () => {
try { try {
if (!ndk.signer) return navigate('/new/privkey');
setIsLoading(true); setIsLoading(true);
// repsot // repsot

View File

@@ -7,6 +7,9 @@ import { message } from '@tauri-apps/plugin-dialog';
import { QRCodeSVG } from 'qrcode.react'; import { QRCodeSVG } from 'qrcode.react';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import CurrencyInput from 'react-currency-input-field'; import CurrencyInput from 'react-currency-input-field';
import { useNavigate } from 'react-router-dom';
import { useNDK } from '@libs/ndk/provider';
import { CancelIcon, ZapIcon } from '@shared/icons'; import { CancelIcon, ZapIcon } from '@shared/icons';
@@ -16,6 +19,9 @@ import { compactNumber } from '@utils/number';
export function NoteZap({ event }: { event: NDKEvent }) { export function NoteZap({ event }: { event: NDKEvent }) {
const nwc = useRef(null); const nwc = useRef(null);
const navigate = useNavigate();
const { ndk } = useNDK();
const { user } = useProfile(event.pubkey); const { user } = useProfile(event.pubkey);
const [walletConnectURL, setWalletConnectURL] = useState<string>(null); const [walletConnectURL, setWalletConnectURL] = useState<string>(null);
@@ -28,6 +34,8 @@ export function NoteZap({ event }: { event: NDKEvent }) {
const createZapRequest = async () => { const createZapRequest = async () => {
try { try {
if (!ndk.signer) return navigate('/new/privkey');
const zapAmount = parseInt(amount) * 1000; const zapAmount = parseInt(amount) * 1000;
const res = await event.zap(zapAmount, zapMessage); const res = await event.zap(zapAmount, zapMessage);

View File

@@ -1,5 +1,6 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useNDK } from '@libs/ndk/provider'; import { useNDK } from '@libs/ndk/provider';
@@ -9,12 +10,15 @@ import { ReplyMediaUploader } from '@shared/notes';
export function NoteReplyForm({ rootEvent }: { rootEvent: NDKEvent }) { export function NoteReplyForm({ rootEvent }: { rootEvent: NDKEvent }) {
const { ndk } = useNDK(); const { ndk } = useNDK();
const navigate = useNavigate();
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const submit = async () => { const submit = async () => {
try { try {
if (!ndk.signer) return navigate('/new/privkey');
setLoading(true); setLoading(true);
const event = new NDKEvent(ndk); const event = new NDKEvent(ndk);

View File

@@ -541,7 +541,7 @@ export const User = memo(function User({
</Avatar.Fallback> </Avatar.Fallback>
</Avatar.Root> </Avatar.Root>
<div className="flex flex-1 flex-col gap-2"> <div className="flex flex-1 flex-col gap-2">
<div className="inline-flex flex-col gap-1"> <div className="inline-flex flex-col">
<h5 className="text-sm font-semibold"> <h5 className="text-sm font-semibold">
{user?.name || {user?.name ||
user?.display_name || user?.display_name ||

View File

@@ -1,6 +1,6 @@
import { NDKEvent, NDKKind, NDKUser } from '@nostr-dev-kit/ndk'; import { NDKEvent, NDKKind, NDKUser } from '@nostr-dev-kit/ndk';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useNDK } from '@libs/ndk/provider'; import { useNDK } from '@libs/ndk/provider';
@@ -17,6 +17,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
const { user } = useProfile(pubkey); const { user } = useProfile(pubkey);
const [followed, setFollowed] = useState(false); const [followed, setFollowed] = useState(false);
const navigate = useNavigate();
const follow = async (pubkey: string) => { const follow = async (pubkey: string) => {
try { try {
@@ -36,6 +37,8 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
const unfollow = async (pubkey: string) => { const unfollow = async (pubkey: string) => {
try { try {
if (!ndk.signer) return navigate('/new/privkey');
const user = ndk.getUser({ pubkey: db.account.pubkey }); const user = ndk.getUser({ pubkey: db.account.pubkey });
const contacts = await user.follows(); const contacts = await user.follows();
contacts.delete(new NDKUser({ pubkey: pubkey })); contacts.delete(new NDKUser({ pubkey: pubkey }));

View File

@@ -44,9 +44,9 @@ export function AddGroupFeeds({ currentWidgetId }: { currentWidgetId: string })
return ( return (
<Dialog.Root> <Dialog.Root>
<div className="inline-flex h-14 w-full items-center justify-between rounded-lg bg-white px-3 hover:shadow-md hover:shadow-neutral-200/50 dark:hover:shadow-neutral-800/50"> <div className="inline-flex h-14 w-full items-center justify-between rounded-lg bg-neutral-50 px-3 hover:shadow-md hover:shadow-neutral-200/50 dark:bg-neutral-950 dark:hover:shadow-neutral-800/50">
<div className="inline-flex items-center gap-2.5"> <div className="inline-flex items-center gap-2.5">
<div className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-neutral-100"> <div className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-neutral-100 dark:bg-neutral-900">
<GroupFeedsIcon className="h-4 w-4" /> <GroupFeedsIcon className="h-4 w-4" />
</div> </div>
<p className="font-medium">Group feeds</p> <p className="font-medium">Group feeds</p>

View File

@@ -55,9 +55,9 @@ export function AddHashtagFeeds({ currentWidgetId }: { currentWidgetId: string }
return ( return (
<Dialog.Root> <Dialog.Root>
<div className="inline-flex h-14 w-full items-center justify-between rounded-lg bg-white px-3 hover:shadow-md hover:shadow-neutral-200/50 dark:hover:shadow-neutral-800/50"> <div className="inline-flex h-14 w-full items-center justify-between rounded-lg bg-neutral-50 px-3 hover:shadow-md hover:shadow-neutral-200/50 dark:bg-neutral-950 dark:hover:shadow-neutral-800/50">
<div className="inline-flex items-center gap-2.5"> <div className="inline-flex items-center gap-2.5">
<div className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-neutral-100"> <div className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-neutral-100 dark:bg-neutral-900">
<GroupFeedsIcon className="h-4 w-4" /> <GroupFeedsIcon className="h-4 w-4" />
</div> </div>
<p className="font-medium">Hashtag</p> <p className="font-medium">Hashtag</p>

View File

@@ -1,5 +1,6 @@
import { NDKEvent, NDKKind, NDKUser } from '@nostr-dev-kit/ndk'; import { NDKEvent, NDKKind, NDKUser } from '@nostr-dev-kit/ndk';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useNDK } from '@libs/ndk/provider'; import { useNDK } from '@libs/ndk/provider';
@@ -22,6 +23,7 @@ export function NostrBandUserProfile({ data }: { data: Profile }) {
const { ndk } = useNDK(); const { ndk } = useNDK();
const [followed, setFollowed] = useState(false); const [followed, setFollowed] = useState(false);
const navigate = useNavigate();
const follow = async (pubkey: string) => { const follow = async (pubkey: string) => {
try { try {
@@ -41,6 +43,8 @@ export function NostrBandUserProfile({ data }: { data: Profile }) {
const unfollow = async (pubkey: string) => { const unfollow = async (pubkey: string) => {
try { try {
if (!ndk.signer) return navigate('/new/privkey');
const user = ndk.getUser({ pubkey: db.account.pubkey }); const user = ndk.getUser({ pubkey: db.account.pubkey });
const contacts = await user.follows(); const contacts = await user.follows();
contacts.delete(new NDKUser({ pubkey: pubkey })); contacts.delete(new NDKUser({ pubkey: pubkey }));

View File

@@ -16,7 +16,7 @@ export function ToggleWidgetList() {
onClick={() => onClick={() =>
addWidget.mutate({ kind: WIDGET_KIND.list, title: '', content: '' }) addWidget.mutate({ kind: WIDGET_KIND.list, title: '', content: '' })
} }
className="inline-flex h-14 w-14 items-center justify-center rounded-full bg-neutral-100 text-neutral-900 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800" className="inline-flex h-14 w-14 items-center justify-center rounded-full bg-neutral-100 text-neutral-900 hover:bg-neutral-200 dark:bg-neutral-900 dark:text-neutral-100 dark:hover:bg-neutral-800"
> >
<PlusIcon className="h-5 w-5" /> <PlusIcon className="h-5 w-5" />
</button> </button>

View File

@@ -65,9 +65,9 @@ export function WidgetList({ widget }: { widget: Widget }) {
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<AddGroupFeeds currentWidgetId={widget.id} /> <AddGroupFeeds currentWidgetId={widget.id} />
<AddHashtagFeeds currentWidgetId={widget.id} /> <AddHashtagFeeds currentWidgetId={widget.id} />
<div className="inline-flex h-14 w-full items-center justify-between rounded-lg bg-white px-3 hover:shadow-md hover:shadow-neutral-200/50 dark:hover:shadow-neutral-800/50"> <div className="inline-flex h-14 w-full items-center justify-between rounded-lg bg-neutral-50 px-3 hover:shadow-md hover:shadow-neutral-200/50 dark:bg-neutral-950 dark:hover:shadow-neutral-800/50">
<div className="inline-flex items-center gap-2.5"> <div className="inline-flex items-center gap-2.5">
<div className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-neutral-100"> <div className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-neutral-100 dark:bg-neutral-900">
<ArticleIcon className="h-4 w-4" /> <ArticleIcon className="h-4 w-4" />
</div> </div>
<p className="font-medium">Articles</p> <p className="font-medium">Articles</p>
@@ -90,9 +90,9 @@ export function WidgetList({ widget }: { widget: Widget }) {
Add Add
</button> </button>
</div> </div>
<div className="inline-flex h-14 w-full items-center justify-between rounded-lg bg-white px-3 hover:shadow-md hover:shadow-neutral-200/50 dark:hover:shadow-neutral-800/50"> <div className="inline-flex h-14 w-full items-center justify-between rounded-lg bg-neutral-50 px-3 hover:shadow-md hover:shadow-neutral-200/50 dark:bg-neutral-950 dark:hover:shadow-neutral-800/50">
<div className="inline-flex items-center gap-2.5"> <div className="inline-flex items-center gap-2.5">
<div className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-neutral-100"> <div className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-neutral-100 dark:bg-neutral-900">
<MediaIcon className="h-4 w-4" /> <MediaIcon className="h-4 w-4" />
</div> </div>
<p className="font-medium">Media</p> <p className="font-medium">Media</p>

View File

@@ -4,6 +4,8 @@ import { ReactNode } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import reactStringReplace from 'react-string-replace'; import reactStringReplace from 'react-string-replace';
import { useStorage } from '@libs/storage/provider';
import { import {
Hashtag, Hashtag,
ImagePreview, ImagePreview,
@@ -44,6 +46,8 @@ const VIDEOS = [
]; ];
export function useRichContent(content: string, textmode: boolean = false) { export function useRichContent(content: string, textmode: boolean = false) {
const { db } = useStorage();
let parsedContent: string | ReactNode[] = content.replace(/\n+/g, '\n'); let parsedContent: string | ReactNode[] = content.replace(/\n+/g, '\n');
let linkPreview: string; let linkPreview: string;
let images: string[] = []; let images: string[] = [];
@@ -54,8 +58,10 @@ export function useRichContent(content: string, textmode: boolean = false) {
const words = text.split(/( |\n)/); const words = text.split(/( |\n)/);
if (!textmode) { if (!textmode) {
images = words.filter((word) => IMAGES.some((el) => word.endsWith(el))); if (db.settings.media) {
videos = words.filter((word) => VIDEOS.some((el) => word.endsWith(el))); images = words.filter((word) => IMAGES.some((el) => word.endsWith(el)));
videos = words.filter((word) => VIDEOS.some((el) => word.endsWith(el)));
}
events = words.filter((word) => NOSTR_EVENTS.some((el) => word.startsWith(el))); events = words.filter((word) => NOSTR_EVENTS.some((el) => word.startsWith(el)));
} }
@@ -83,9 +89,10 @@ export function useRichContent(content: string, textmode: boolean = false) {
if (hashtags.length) { if (hashtags.length) {
hashtags.forEach((hashtag) => { hashtags.forEach((hashtag) => {
parsedContent = reactStringReplace(parsedContent, hashtag, (match, i) => ( parsedContent = reactStringReplace(parsedContent, hashtag, (match, i) => {
<Hashtag key={match + i} tag={hashtag} /> if (db.settings.hashtag) return <Hashtag key={match + i} tag={hashtag} />;
)); return null;
});
}); });
} }