diff --git a/src/app/personal/components/relayCard.tsx b/src/app/settings/components/relayCard.tsx
similarity index 100%
rename from src/app/personal/components/relayCard.tsx
rename to src/app/settings/components/relayCard.tsx
diff --git a/src/app/personal/components/zapCard.tsx b/src/app/settings/components/zapCard.tsx
similarity index 100%
rename from src/app/personal/components/zapCard.tsx
rename to src/app/settings/components/zapCard.tsx
diff --git a/src/app/settings/editContact.tsx b/src/app/settings/editContact.tsx
new file mode 100644
index 00000000..30dcd118
--- /dev/null
+++ b/src/app/settings/editContact.tsx
@@ -0,0 +1,41 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { useNDK } from '@libs/ndk/provider';
+import { useStorage } from '@libs/storage/provider';
+
+import { LoaderIcon } from '@shared/icons';
+import { User } from '@shared/user';
+
+export function EditContactScreen() {
+ const { db } = useStorage();
+ const { ndk } = useNDK();
+ const { status, data } = useQuery({
+ queryKey: ['contacts'],
+ queryFn: async () => {
+ const user = ndk.getUser({ pubkey: db.account.pubkey });
+
+ const follows = await user.follows();
+ return [...follows];
+ },
+ refetchOnWindowFocus: false,
+ });
+
+ return (
+
+ {status === 'pending' ? (
+
+
+
+ ) : (
+ data.map((item) => (
+
+
+
+ ))
+ )}
+
+ );
+}
diff --git a/src/app/settings/editProfile.tsx b/src/app/settings/editProfile.tsx
new file mode 100644
index 00000000..90e784a0
--- /dev/null
+++ b/src/app/settings/editProfile.tsx
@@ -0,0 +1,303 @@
+import { NDKEvent, NDKKind, NDKUserProfile } from '@nostr-dev-kit/ndk';
+import { useQueryClient } from '@tanstack/react-query';
+import { message } from '@tauri-apps/plugin-dialog';
+import { useState } from 'react';
+import { useForm } from 'react-hook-form';
+
+import { useNDK } from '@libs/ndk/provider';
+import { useStorage } from '@libs/storage/provider';
+
+import { CheckCircleIcon, LoaderIcon, PlusIcon, UnverifiedIcon } from '@shared/icons';
+
+import { useNostr } from '@utils/hooks/useNostr';
+
+export function EditProfileScreen() {
+ const queryClient = useQueryClient();
+
+ const [loading, setLoading] = useState(false);
+ const [picture, setPicture] = useState('');
+ const [banner, setBanner] = useState('');
+ const [nip05, setNIP05] = useState({ verified: true, text: '' });
+
+ const { db } = useStorage();
+ const { ndk } = useNDK();
+ const { upload } = useNostr();
+ const {
+ register,
+ handleSubmit,
+ reset,
+ setError,
+ formState: { isValid, errors },
+ } = useForm({
+ defaultValues: async () => {
+ const res: NDKUserProfile = queryClient.getQueryData(['user', db.account.pubkey]);
+ if (res.image) {
+ setPicture(res.image);
+ }
+ if (res.banner) {
+ setBanner(res.banner);
+ }
+ if (res.nip05) {
+ setNIP05((prev) => ({ ...prev, text: res.nip05 }));
+ }
+ return res;
+ },
+ });
+
+ const uploadAvatar = async () => {
+ try {
+ setLoading(true);
+
+ const image = await upload();
+
+ if (image) {
+ setPicture(image);
+ setLoading(false);
+ }
+ } catch (e) {
+ setLoading(false);
+ await message(`Upload failed, error: ${e}`, { title: 'Lume', type: 'error' });
+ }
+ };
+
+ const uploadBanner = async () => {
+ try {
+ setLoading(true);
+
+ const image = await upload();
+
+ if (image) {
+ setBanner(image);
+ setLoading(false);
+ }
+ } catch (e) {
+ setLoading(false);
+ await message(`Upload failed, error: ${e}`, { title: 'Lume', type: 'error' });
+ }
+ };
+
+ const onSubmit = async (data: NDKUserProfile) => {
+ // start loading
+ setLoading(true);
+
+ const content = {
+ ...data,
+ username: data.name,
+ display_name: data.name,
+ bio: data.about,
+ image: data.picture,
+ };
+
+ const event = new NDKEvent(ndk);
+ event.kind = NDKKind.Metadata;
+ event.tags = [];
+
+ if (data.nip05) {
+ const user = ndk.getUser({ pubkey: db.account.pubkey });
+ const verify = await user.validateNip05(data.nip05);
+ if (verify) {
+ event.content = JSON.stringify({ ...content, nip05: data.nip05 });
+ } else {
+ setNIP05((prev) => ({ ...prev, verified: false }));
+ setError('nip05', {
+ type: 'manual',
+ message: "Can't verify your Lume ID / NIP-05, please check again",
+ });
+ }
+ } else {
+ event.content = JSON.stringify(content);
+ }
+
+ const publishedRelays = await event.publish();
+
+ if (publishedRelays) {
+ // invalid cache
+ queryClient.invalidateQueries({
+ queryKey: ['user', db.account.pubkey],
+ });
+ // reset form
+ reset();
+ // reset state
+ setLoading(false);
+ setPicture(null);
+ setBanner(null);
+ } else {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/settings/general.tsx b/src/app/settings/general.tsx
new file mode 100644
index 00000000..b6f089a9
--- /dev/null
+++ b/src/app/settings/general.tsx
@@ -0,0 +1,3 @@
+export function GeneralSettingScreen() {
+ return
;
+}
diff --git a/src/app/settings/index.tsx b/src/app/settings/index.tsx
index 93ea91e9..fb272c15 100644
--- a/src/app/settings/index.tsx
+++ b/src/app/settings/index.tsx
@@ -1,3 +1,19 @@
-export function SettingsScreen() {
- return
;
+import { ContactCard } from '@app/settings/components/contactCard';
+import { PostCard } from '@app/settings/components/postCard';
+import { ProfileCard } from '@app/settings/components/profileCard';
+import { RelayCard } from '@app/settings/components/relayCard';
+import { ZapCard } from '@app/settings/components/zapCard';
+
+export function UserSettingScreen() {
+ return (
+
+ );
}
diff --git a/src/shared/accounts/active.tsx b/src/shared/accounts/active.tsx
index 3c734827..23d97d3a 100644
--- a/src/shared/accounts/active.tsx
+++ b/src/shared/accounts/active.tsx
@@ -25,7 +25,7 @@ export function ActiveAccount() {
return (
-
+
- Dashboard
-
-
-
-
Settings
-
-
-
+
diff --git a/src/shared/layouts/settings.tsx b/src/shared/layouts/settings.tsx
index 1c3e5d57..7280d4bb 100644
--- a/src/shared/layouts/settings.tsx
+++ b/src/shared/layouts/settings.tsx
@@ -1,8 +1,11 @@
-import { Outlet, ScrollRestoration } from 'react-router-dom';
+import { NavLink, Outlet, ScrollRestoration } from 'react-router-dom';
+import { twMerge } from 'tailwind-merge';
import { WindowTitlebar } from 'tauri-controls';
import { useStorage } from '@libs/storage/provider';
+import { SecureIcon, SettingsIcon } from '@shared/icons';
+
export function SettingsLayout() {
const { db } = useStorage();
@@ -13,7 +16,79 @@ export function SettingsLayout() {
) : (
)}
-
+
+
+
+ twMerge(
+ '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
+ ? 'bg-neutral-100 text-blue-500 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800'
+ : ''
+ )
+ }
+ >
+
+ User
+
+
+ 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'
+ : ''
+ )
+ }
+ >
+
+ General
+
+
+ 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'
+ : ''
+ )
+ }
+ >
+
+ Backup
+
+
+ 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'
+ : ''
+ )
+ }
+ >
+
+ Advanced
+
+
+ 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'
+ : ''
+ )
+ }
+ >
+
+ About
+
+