@@ -91,7 +102,7 @@ export function CreateStep3Screen() {
Name *
@@ -102,26 +113,26 @@ export function CreateStep3Screen() {
minLength: 4,
})}
spellCheck={false}
- className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
+ className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 text-white !outline-none placeholder:text-white/50"
/>
Bio
Website
@@ -131,18 +142,26 @@ export function CreateStep3Screen() {
required: false,
})}
spellCheck={false}
- className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
+ className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 text-white !outline-none placeholder:text-white/50"
/>
{loading ? (
-
+ <>
+
+ Creating...
+
+ >
) : (
- 'Continue →'
+ <>
+
+ Continue
+
+ >
)}
diff --git a/src/app/auth/create/step-4.tsx b/src/app/auth/create/step-4.tsx
deleted file mode 100644
index 21356b8a4..000000000
--- a/src/app/auth/create/step-4.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import { Body, fetch } from '@tauri-apps/api/http';
-import { useState } from 'react';
-import { useNavigate } from 'react-router-dom';
-
-import { Button } from '@shared/button';
-import { LoaderIcon } from '@shared/icons';
-
-import { useOnboarding } from '@stores/onboarding';
-
-import { useAccount } from '@utils/hooks/useAccount';
-import { usePublish } from '@utils/hooks/usePublish';
-
-export function CreateStep4Screen() {
- const navigate = useNavigate();
- const profile = useOnboarding((state) => state.profile);
-
- const { publish } = usePublish();
- const { account } = useAccount();
-
- const [username, setUsername] = useState('');
- const [loading, setLoading] = useState(false);
-
- const createNIP05 = async () => {
- try {
- setLoading(true);
-
- const response = await fetch('https://lume.nu/api/user-create', {
- method: 'POST',
- timeout: 30,
- headers: {
- 'Content-Type': 'application/json; charset=utf-8',
- },
- body: Body.json({
- username: username,
- pubkey: account.pubkey,
- lightningAddress: '',
- }),
- });
-
- if (response.ok) {
- const data = { ...profile, nip05: `${username}@lume.nu` };
- publish({ content: JSON.stringify(data), kind: 0, tags: [] });
-
- // redirect to step 4
- navigate('/auth/create/step-5', { replace: true });
- }
- } catch (error) {
- setLoading(false);
- console.error('Error:', error);
- }
- };
-
- return (
-
-
-
Create your Lume ID
-
-
-
- setUsername(e.target.value)}
- autoCapitalize="false"
- autoCorrect="none"
- spellCheck="false"
- placeholder="satoshi"
- className="relative w-full bg-transparent py-3 pl-3.5 text-zinc-100 !outline-none placeholder:text-zinc-500"
- />
- @lume.nu
-
-
createNIP05()}
- disabled={username.length === 0}
- >
- {loading ? (
-
- ) : (
- 'Continue →'
- )}
-
-
-
- );
-}
diff --git a/src/app/auth/create/step-5.tsx b/src/app/auth/create/step-5.tsx
deleted file mode 100644
index 85abb199f..000000000
--- a/src/app/auth/create/step-5.tsx
+++ /dev/null
@@ -1,224 +0,0 @@
-import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-import { useState } from 'react';
-import { useNavigate } from 'react-router-dom';
-
-import { User } from '@app/auth/components/user';
-
-import { updateAccount } from '@libs/storage';
-
-import { CheckCircleIcon, LoaderIcon } from '@shared/icons';
-
-import { useAccount } from '@utils/hooks/useAccount';
-import { usePublish } from '@utils/hooks/usePublish';
-import { arrayToNIP02 } from '@utils/transform';
-
-const INITIAL_LIST = [
- {
- pubkey: '82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2',
- },
- {
- pubkey: 'a341f45ff9758f570a21b000c17d4e53a3a497c8397f26c0e6d61e5acffc7a98',
- },
- {
- pubkey: '04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9',
- },
- {
- pubkey: 'c4eabae1be3cf657bc1855ee05e69de9f059cb7a059227168b80b89761cbc4e0',
- },
- {
- pubkey: '6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93',
- },
- {
- pubkey: 'e88a691e98d9987c964521dff60025f60700378a4879180dcbbb4a5027850411',
- },
- {
- pubkey: '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d',
- },
- {
- pubkey: 'c49d52a573366792b9a6e4851587c28042fb24fa5625c6d67b8c95c8751aca15',
- },
- {
- pubkey: 'e33fe65f1fde44c6dc17eeb38fdad0fceaf1cae8722084332ed1e32496291d42',
- },
- {
- pubkey: '84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240',
- },
- {
- pubkey: '703e26b4f8bc0fa57f99d815dbb75b086012acc24fc557befa310f5aa08d1898',
- },
- {
- pubkey: 'bf2376e17ba4ec269d10fcc996a4746b451152be9031fa48e74553dde5526bce',
- },
- {
- pubkey: '4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0',
- },
- {
- pubkey: 'c9b19ffcd43e6a5f23b3d27106ce19e4ad2df89ba1031dd4617f1b591e108965',
- },
- {
- pubkey: 'c7dccba4fe4426a7b1ea239a5637ba40fab9862c8c86b3330fe65e9f667435f6',
- },
- {
- pubkey: '6e1534f56fc9e937e06237c8ba4b5662bcacc4e1a3cfab9c16d89390bec4fca3',
- },
- {
- pubkey: '50d94fc2d8580c682b071a542f8b1e31a200b0508bab95a33bef0855df281d63',
- },
- {
- pubkey: '3d2e51508699f98f0f2bdbe7a45b673c687fe6420f466dc296d90b908d51d594',
- },
- {
- pubkey: '6e3f51664e19e082df5217fd4492bb96907405a0b27028671dd7f297b688608c',
- },
- {
- pubkey: '2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884',
- },
- {
- pubkey: '3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24',
- },
- {
- pubkey: 'eab0e756d32b80bcd464f3d844b8040303075a13eabc3599a762c9ac7ab91f4f',
- },
- {
- pubkey: 'be1d89794bf92de5dd64c1e60f6a2c70c140abac9932418fee30c5c637fe9479',
- },
- {
- pubkey: 'a5e93aef8e820cbc7ab7b6205f854b87aed4b48c5f6b30fbbeba5c99e40dcf3f',
- },
- {
- pubkey: '1989034e56b8f606c724f45a12ce84a11841621aaf7182a1f6564380b9c4276b',
- },
- {
- pubkey: 'c48b5cced5ada74db078df6b00fa53fc1139d73bf0ed16de325d52220211dbd5',
- },
- {
- pubkey: '460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c',
- },
- {
- pubkey: '7f3b464b9ff3623630485060cbda3a7790131c5339a7803bde8feb79a5e1b06a',
- },
- {
- pubkey: 'b99dbca0184a32ce55904cb267b22e434823c97f418f36daf5d2dff0dd7b5c27',
- },
- {
- pubkey: 'e9e4276490374a0daf7759fd5f475deff6ffb9b0fc5fa98c902b5f4b2fe3bba2',
- },
- {
- pubkey: 'ea2e3c814d08a378f8a5b8faecb2884d05855975c5ca4b5c25e2d6f936286f14',
- },
- {
- pubkey: 'ff04a0e6cd80c141b0b55825fed127d4532a6eecdb7e743a38a3c28bf9f44609',
- },
-];
-
-export function CreateStep5Screen() {
- const queryClient = useQueryClient();
- const navigate = useNavigate();
-
- const [loading, setLoading] = useState(false);
- const [follows, setFollows] = useState([]);
-
- const { publish } = usePublish();
- const { account } = useAccount();
- const { status, data } = useQuery(['trending-profiles'], async () => {
- const res = await fetch('https://api.nostr.band/v0/trending/profiles');
- if (!res.ok) {
- throw new Error('Error');
- }
- return res.json();
- });
-
- // toggle follow state
- const toggleFollow = (pubkey: string) => {
- const arr = follows.includes(pubkey)
- ? follows.filter((i) => i !== pubkey)
- : [...follows, pubkey];
- setFollows(arr);
- };
-
- const update = useMutation({
- mutationFn: (follows: string[]) => {
- return updateAccount('follows', follows, account.pubkey);
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['currentAccount'] });
- },
- });
-
- // save follows to database then broadcast
- const submit = async () => {
- try {
- setLoading(true);
-
- const tags = arrayToNIP02([...follows, account.pubkey]);
- publish({ content: '', kind: 3, tags: tags });
-
- // update
- update.mutate([...follows, account.pubkey]);
-
- // redirect to next step
- setTimeout(() => navigate('/auth/onboarding', { replace: true }), 1200);
- } catch {
- console.log('error');
- }
- };
-
- const list = data ? data.profiles.concat(INITIAL_LIST) : INITIAL_LIST;
-
- return (
-
-
-
- Personalized your newsfeed
-
-
-
-
-
- Follow at least
-
- {follows.length}/10
- {' '}
- plebs
-
- {status === 'loading' ? (
-
-
-
- ) : (
-
- {list.map((item: { pubkey: string; profile: { content: string } }) => (
-
toggleFollow(item.pubkey)}
- className="inline-flex transform items-center justify-between bg-zinc-900 px-4 py-2 hover:bg-zinc-800 active:translate-y-1"
- >
-
- {follows.includes(item.pubkey) && (
-
-
-
- )}
-
- ))}
-
- )}
-
- {follows.length >= 10 && (
-
submit()}
- className="inline-flex h-11 w-full items-center justify-center rounded-md bg-fuchsia-500 font-medium text-zinc-100 hover:bg-fuchsia-600"
- >
- {loading ? (
-
- ) : (
- 'Finish →'
- )}
-
- )}
-
-
- );
-}
diff --git a/src/app/auth/import/index.tsx b/src/app/auth/import/index.tsx
index bf8983ffc..37aebf6bf 100644
--- a/src/app/auth/import/index.tsx
+++ b/src/app/auth/import/index.tsx
@@ -1,6 +1,19 @@
+import { useEffect } from 'react';
import { Outlet } from 'react-router-dom';
+import { useOnboarding } from '@stores/onboarding';
+import { useStronghold } from '@stores/stronghold';
+
export function AuthImportScreen() {
+ const [step, tmpPrivkey] = useOnboarding((state) => [state.step, state.tempPrivkey]);
+ const setPrivkey = useStronghold((state) => state.setPrivkey);
+
+ useEffect(() => {
+ if (step) {
+ setPrivkey(tmpPrivkey);
+ }
+ }, [tmpPrivkey]);
+
return (
diff --git a/src/app/auth/import/step-1.tsx b/src/app/auth/import/step-1.tsx
index 3b4e6e636..33565e4e5 100644
--- a/src/app/auth/import/step-1.tsx
+++ b/src/app/auth/import/step-1.tsx
@@ -1,12 +1,13 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { getPublicKey, nip19 } from 'nostr-tools';
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
import { Resolver, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { createAccount } from '@libs/storage';
import { LoaderIcon } from '@shared/icons';
+import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
import { useOnboarding } from '@stores/onboarding';
import { useStronghold } from '@stores/stronghold';
@@ -33,7 +34,9 @@ export function ImportStep1Screen() {
const queryClient = useQueryClient();
const navigate = useNavigate();
const setPrivkey = useStronghold((state) => state.setPrivkey);
+ const setTempPubkey = useOnboarding((state) => state.setTempPrivkey);
const setPubkey = useOnboarding((state) => state.setPubkey);
+ const setStep = useOnboarding((state) => state.setStep);
const [loading, setLoading] = useState(false);
@@ -71,10 +74,9 @@ export function ImportStep1Screen() {
const pubkey = getPublicKey(privkey);
const npub = nip19.npubEncode(pubkey);
- // use for onboarding process only
- setPubkey(pubkey);
- // add stronghold state
setPrivkey(privkey);
+ setTempPubkey(privkey); // only use if user close app and reopen it
+ setPubkey(pubkey);
// add account to local database
account.mutate({
@@ -90,25 +92,30 @@ export function ImportStep1Screen() {
} catch (error) {
setError('privkey', {
type: 'custom',
- message: 'Private Key is invalid, please check again',
+ message: 'Private key is invalid, please check again',
});
}
};
+ useEffect(() => {
+ // save current step, if user close app and reopen it
+ setStep('/auth/import/step-1');
+ }, []);
+
return (
-
Import your key
+ Import your key
diff --git a/src/app/auth/import/step-3.tsx b/src/app/auth/import/step-3.tsx
index 3940fbb5f..93513a58b 100644
--- a/src/app/auth/import/step-3.tsx
+++ b/src/app/auth/import/step-3.tsx
@@ -1,85 +1,92 @@
-import { useMutation, useQueryClient } from '@tanstack/react-query';
-import { useState } from 'react';
+import { useQueryClient } from '@tanstack/react-query';
+import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { User } from '@app/auth/components/user';
-import { useNDK } from '@libs/ndk/provider';
-import { updateAccount } from '@libs/storage';
+import { updateLastLogin } from '@libs/storage';
-import { Button } from '@shared/button';
-import { LoaderIcon } from '@shared/icons';
+import { ArrowRightCircleIcon, LoaderIcon } from '@shared/icons';
+
+import { useOnboarding } from '@stores/onboarding';
import { useAccount } from '@utils/hooks/useAccount';
-import { setToArray } from '@utils/transform';
+import { useNostr } from '@utils/hooks/useNostr';
export function ImportStep3Screen() {
const queryClient = useQueryClient();
const navigate = useNavigate();
+ const setStep = useOnboarding((state) => state.setStep);
const [loading, setLoading] = useState(false);
- const { ndk } = useNDK();
const { status, account } = useAccount();
-
- const update = useMutation({
- mutationFn: (follows: string[]) => {
- return updateAccount('follows', follows, account.pubkey);
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['currentAccount'] });
- },
- });
+ const { fetchNotes, fetchChats } = useNostr();
const submit = async () => {
try {
// show loading indicator
setLoading(true);
- const user = ndk.getUser({ hexpubkey: account.pubkey });
- const follows = await user.follows();
+ const now = Math.floor(Date.now() / 1000);
+ await fetchNotes();
+ await fetchChats();
+ await updateLastLogin(now);
- // follows as list
- const followsList = setToArray(follows);
+ queryClient.invalidateQueries(['currentAccount']);
- // update
- update.mutate([...followsList, account.pubkey]);
-
- // redirect to next step
- setTimeout(() => navigate('/auth/onboarding', { replace: true }), 1200);
- } catch {
- console.log('error');
+ navigate('/auth/onboarding/step-2', { replace: true });
+ } catch (e) {
+ console.log('error: ', e);
+ setLoading(false);
}
};
+ useEffect(() => {
+ // save current step, if user close app and reopen it
+ setStep('/auth/import/step-3');
+ }, []);
+
return (
- {loading ? 'Creating...' : 'Continue with'}
+ {loading ? 'Prefetching data...' : 'Continue with'}
-
+
{status === 'loading' ? (
) : (
-
submit()}>
+ submit()}
+ >
{loading ? (
-
+ <>
+
+ It might take a bit, please patient...
+
+ >
) : (
- 'Continue →'
+ <>
+
+ Continue
+
+ >
)}
-
+
)}
diff --git a/src/app/auth/migrate.tsx b/src/app/auth/migrate.tsx
index 034f90c3d..a2928b1ac 100644
--- a/src/app/auth/migrate.tsx
+++ b/src/app/auth/migrate.tsx
@@ -92,7 +92,7 @@ export function MigrateScreen() {
-
+
Upgrade security for your account
@@ -100,15 +100,15 @@ export function MigrateScreen() {
-
+
You're using old Lume version which store your private key as
plaintext in database, this is huge security risk.
-
+
To secure your private key, please set a password and Lume will put your
private key in secure storage.
-
+
It is not possible to start the app without applying this step, it is
easy and fast!
@@ -124,7 +124,7 @@ export function MigrateScreen() {
{...register('password', { required: true })}
type={passwordInput}
placeholder="min. 4 characters"
- className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-zinc-100 !outline-none placeholder:text-zinc-400"
+ className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-white !outline-none placeholder:text-white/50"
/>
) : (
)}
@@ -154,10 +154,10 @@ export function MigrateScreen() {
{loading ? (
-
+
) : (
'Continue →'
)}
diff --git a/src/app/auth/onboarding.tsx b/src/app/auth/onboarding.tsx
deleted file mode 100644
index dc53955d3..000000000
--- a/src/app/auth/onboarding.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-import { useState } from 'react';
-import { Link, useNavigate } from 'react-router-dom';
-
-import { LoaderIcon } from '@shared/icons';
-import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
-import { User } from '@shared/user';
-
-import { useAccount } from '@utils/hooks/useAccount';
-import { usePublish } from '@utils/hooks/usePublish';
-
-export function OnboardingScreen() {
- const navigate = useNavigate();
-
- const { publish } = usePublish();
- const { status, account } = useAccount();
-
- const [loading, setLoading] = useState(false);
-
- const submit = async () => {
- try {
- setLoading(true);
-
- // publish event
- publish({
- content: 'Running Lume, join with me #nostr #lume : https://lume.nu',
- kind: 1,
- tags: [],
- });
-
- // redirect to home
- setTimeout(() => navigate('/', { replace: true }), 1200);
- } catch (error) {
- console.log(error);
- }
- };
-
- return (
-
-
-
-
- 👋 Hello, welcome you to Lume
-
-
- You're a part of Nostr community now
-
-
- If Lume gets your attention, please help us spread it and don't forget
- invite your friend join with you, we can have fun togother
-
-
-
-
- {status === 'success' && (
-
- )}
-
-
-
-
-
submit()}
- className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium text-zinc-100 hover:bg-fuchsia-600"
- >
- {loading ? (
- <>
-
-
-
- >
- ) : (
- <>
-
- Spread
-
- >
- )}
-
-
- Skip
-
-
-
-
- );
-}
diff --git a/src/app/auth/onboarding/index.tsx b/src/app/auth/onboarding/index.tsx
new file mode 100644
index 000000000..ddf8d4b89
--- /dev/null
+++ b/src/app/auth/onboarding/index.tsx
@@ -0,0 +1,22 @@
+import { useEffect } from 'react';
+import { Outlet } from 'react-router-dom';
+
+import { useOnboarding } from '@stores/onboarding';
+import { useStronghold } from '@stores/stronghold';
+
+export function OnboardingScreen() {
+ const [step, tmpPrivkey] = useOnboarding((state) => [state.step, state.tempPrivkey]);
+ const setPrivkey = useStronghold((state) => state.setPrivkey);
+
+ useEffect(() => {
+ if (step) {
+ setPrivkey(tmpPrivkey);
+ }
+ }, [tmpPrivkey]);
+
+ return (
+
+
+
+ );
+}
diff --git a/src/app/auth/onboarding/step-1.tsx b/src/app/auth/onboarding/step-1.tsx
new file mode 100644
index 000000000..79e64a00e
--- /dev/null
+++ b/src/app/auth/onboarding/step-1.tsx
@@ -0,0 +1,136 @@
+import { useQuery, useQueryClient } from '@tanstack/react-query';
+import { useEffect, useState } from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+
+import { User } from '@app/auth/components/user';
+
+import { updateAccount } from '@libs/storage';
+
+import { ArrowRightCircleIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
+
+import { useOnboarding } from '@stores/onboarding';
+
+import { useAccount } from '@utils/hooks/useAccount';
+import { useNostr } from '@utils/hooks/useNostr';
+import { arrayToNIP02 } from '@utils/transform';
+
+export function OnboardStep1Screen() {
+ const queryClient = useQueryClient();
+ const navigate = useNavigate();
+ const setStep = useOnboarding((state) => state.setStep);
+
+ const { publish, fetchNotes } = useNostr();
+ const { account } = useAccount();
+ const { status, data } = useQuery(['trending-profiles'], async () => {
+ const res = await fetch('https://api.nostr.band/v0/trending/profiles');
+ if (!res.ok) {
+ throw new Error('Error');
+ }
+ return res.json();
+ });
+
+ const [loading, setLoading] = useState(false);
+ const [follows, setFollows] = useState([]);
+
+ // toggle follow state
+ const toggleFollow = (pubkey: string) => {
+ const arr = follows.includes(pubkey)
+ ? follows.filter((i) => i !== pubkey)
+ : [...follows, pubkey];
+ setFollows(arr);
+ };
+
+ const submit = async () => {
+ try {
+ setLoading(true);
+
+ const tags = arrayToNIP02([...follows, account.pubkey]);
+ const event = await publish({ content: '', kind: 3, tags: tags });
+ await updateAccount('follows', follows);
+
+ // prefetch notes with current follows
+ const notes = await fetchNotes(follows);
+
+ // redirect to next step
+ if (event && notes) {
+ setTimeout(() => {
+ queryClient.invalidateQueries(['currentAccount']);
+ navigate('/auth/onboarding/step-2', { replace: true });
+ }, 1000);
+ }
+ } catch {
+ console.log('error');
+ }
+ };
+
+ useEffect(() => {
+ // save current step, if user close app and reopen it
+ setStep('/auth/onboarding');
+ }, []);
+
+ return (
+
+
+
+ {loading ? 'Prefetching data...' : 'Enrich your network'}
+
+
Choose account you want to follow
+
+
+
+ {status === 'loading' ? (
+
+
+
+ ) : (
+ data?.profiles.map(
+ (item: { pubkey: string; profile: { content: string } }) => (
+
toggleFollow(item.pubkey)}
+ className="inline-flex transform items-center justify-between bg-white/10 px-4 py-2 hover:bg-white/20"
+ >
+
+ {follows.includes(item.pubkey) && (
+
+
+
+ )}
+
+ )
+ )
+ )}
+
+
+
+ {loading ? (
+ <>
+
+ It might take a bit, please patient...
+
+ >
+ ) : (
+ <>
+
+ Follow {follows.length} accounts & Continue
+
+ >
+ )}
+
+
+ Skip, you can add later
+
+
+
+
+ );
+}
diff --git a/src/app/auth/onboarding/step-2.tsx b/src/app/auth/onboarding/step-2.tsx
new file mode 100644
index 000000000..6ef68d620
--- /dev/null
+++ b/src/app/auth/onboarding/step-2.tsx
@@ -0,0 +1,124 @@
+import { useEffect, useState } from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+
+import { createWidget } from '@libs/storage';
+
+import { ArrowRightCircleIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
+
+import { BLOCK_KINDS } from '@stores/constants';
+import { useOnboarding } from '@stores/onboarding';
+
+const data = [
+ { hashtag: '#bitcoin' },
+ { hashtag: '#nostr' },
+ { hashtag: '#zap' },
+ { hashtag: '#LFG' },
+ { hashtag: '#zapchain' },
+ { hashtag: '#plebchain' },
+ { hashtag: '#nodes' },
+ { hashtag: '#hodl' },
+ { hashtag: '#stacksats' },
+ { hashtag: '#nokyc' },
+ { hashtag: '#anime' },
+ { hashtag: '#waifu' },
+ { hashtag: '#manga' },
+ { hashtag: '#nostriches' },
+ { hashtag: '#dev' },
+];
+
+export function OnboardStep2Screen() {
+ const navigate = useNavigate();
+ const setStep = useOnboarding((state) => state.setStep);
+
+ const [loading, setLoading] = useState(false);
+ const [tags, setTags] = useState(new Set());
+
+ const toggleTag = (tag: string) => {
+ if (tags.has(tag)) {
+ setTags((prev) => {
+ prev.delete(tag);
+ return new Set(prev);
+ });
+ } else {
+ if (tags.size >= 3) return;
+ setTags((prev) => new Set(prev.add(tag)));
+ }
+ };
+
+ const submit = async () => {
+ try {
+ setLoading(true);
+
+ for (const tag of tags) {
+ await createWidget(BLOCK_KINDS.hashtag, tag, tag.replace('#', ''));
+ }
+
+ setTimeout(() => navigate('/auth/onboarding/step-3', { replace: true }), 1000);
+ } catch {
+ console.log('error');
+ }
+ };
+
+ useEffect(() => {
+ // save current step, if user close app and reopen it
+ setStep('/auth/onboarding/step-2');
+ }, []);
+
+ return (
+
+
+
+ Choose {tags.size}/3 your favorite tags
+
+
Customize your space which hashtag widget
+
+
+
+ {data.map((item: { hashtag: string }) => (
+
toggleTag(item.hashtag)}
+ className="inline-flex transform items-center justify-between bg-white/10 px-4 py-2 hover:bg-white/20"
+ >
+ {item.hashtag}
+ {tags.has(item.hashtag) && (
+
+
+
+ )}
+
+ ))}
+
+
+
3}
+ 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 disabled:opacity-50"
+ >
+ {loading ? (
+ <>
+
+ Creating...
+
+ >
+ ) : (
+ <>
+
+ Add {tags.size} tags & Continue
+
+ >
+ )}
+
+
+ Skip, you can add later
+
+
+
+
+ );
+}
diff --git a/src/app/auth/onboarding/step-3.tsx b/src/app/auth/onboarding/step-3.tsx
new file mode 100644
index 000000000..dea51bcc2
--- /dev/null
+++ b/src/app/auth/onboarding/step-3.tsx
@@ -0,0 +1,183 @@
+import { useQuery } from '@tanstack/react-query';
+import { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+import { UserRelay } from '@app/auth/components/userRelay';
+
+import { useNDK } from '@libs/ndk/provider';
+import { createRelay } from '@libs/storage';
+
+import { ArrowRightCircleIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
+
+import { FULL_RELAYS } from '@stores/constants';
+import { useOnboarding } from '@stores/onboarding';
+
+import { useAccount } from '@utils/hooks/useAccount';
+import { useNostr } from '@utils/hooks/useNostr';
+
+export function OnboardStep3Screen() {
+ const navigate = useNavigate();
+
+ const [setStep, clearStep] = useOnboarding((state) => [state.setStep, state.clearStep]);
+ const [loading, setLoading] = useState(false);
+ const [relays, setRelays] = useState(new Set());
+
+ const { publish } = useNostr();
+ const { account } = useAccount();
+ const { ndk } = useNDK();
+ const { status, data } = useQuery(
+ ['relays'],
+ async () => {
+ const tmp = new Map();
+ const events = await ndk.fetchEvents({ kinds: [10002], authors: account.follows });
+
+ if (events) {
+ events.forEach((event) => {
+ event.tags.forEach((tag) => {
+ tmp.set(tag[1], event.pubkey);
+ });
+ });
+ }
+
+ return tmp;
+ },
+ {
+ enabled: account ? true : false,
+ }
+ );
+
+ const toggleRelay = (relay: string) => {
+ if (relays.has(relay)) {
+ setRelays((prev) => {
+ prev.delete(relay);
+ return new Set(prev);
+ });
+ } else {
+ setRelays((prev) => new Set(prev.add(relay)));
+ }
+ };
+
+ const submit = async (skip?: boolean) => {
+ setLoading(true);
+ try {
+ if (!skip) {
+ for (const relay of relays) {
+ await createRelay(relay);
+ }
+
+ const tags = Array.from(relays).map((relay) => ['r', relay.replace(/\/+$/, '')]);
+ await publish({ content: '', kind: 10002, tags: tags });
+ } else {
+ for (const relay of FULL_RELAYS) {
+ await createRelay(relay);
+ }
+ }
+
+ setTimeout(() => {
+ clearStep();
+ navigate('/', { replace: true });
+ }, 1000);
+ } catch (e) {
+ setLoading(false);
+ console.log('error: ', e);
+ }
+ };
+
+ const relaysAsArray = Array.from(data?.keys() || []);
+
+ useEffect(() => {
+ // save current step, if user close app and reopen it
+ setStep('/auth/onboarding/step-3');
+ }, []);
+
+ return (
+
+
+
Relay discovery
+
+ You can add relay which is using by who you're following to easier reach
+ their content. Learn more about relay{' '}
+
+ here (nostr.com)
+
+
+
+
+
+ {status === 'loading' ? (
+
+
+
+ ) : relaysAsArray.length === 0 ? (
+
+
+ Can't found any relays, you can skip this step and use default relays
+ instead
+
+
+ ) : (
+ relaysAsArray.map((item, index) => (
+
toggleRelay(item)}
+ className="inline-flex transform items-start justify-between bg-white/10 px-4 py-2 hover:bg-white/20"
+ >
+
+
{item.replace(/\/+$/, '')}
+
+
+ {relays.has(item) && (
+
+
+
+ )}
+
+ ))
+ )}
+ {relays.size > 5 && (
+
+
+ Using too much relay can cause high resource usage
+
+
+ )}
+
+
+
submit()}
+ 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 disabled:opacity-50"
+ >
+ {loading ? (
+ <>
+
+ Creating...
+
+ >
+ ) : (
+ <>
+
+ Add {relays.size} relays & Continue
+
+ >
+ )}
+
+
submit(true)}
+ className="inline-flex h-11 w-full items-center justify-center rounded-lg px-6 font-medium leading-none text-white hover:bg-white/10 focus:outline-none"
+ >
+ Skip, use default relays
+
+
+
+
+ );
+}
diff --git a/src/app/auth/reset.tsx b/src/app/auth/reset.tsx
index e3ba0417a..36691c139 100644
--- a/src/app/auth/reset.tsx
+++ b/src/app/auth/reset.tsx
@@ -102,74 +102,64 @@ export function ResetScreen() {
return (
-
-
Reset unlock password
+
+
Reset unlock password
-
);
diff --git a/src/app/auth/unlock.tsx b/src/app/auth/unlock.tsx
index 64f5f4266..0dd45ad3b 100644
--- a/src/app/auth/unlock.tsx
+++ b/src/app/auth/unlock.tsx
@@ -81,65 +81,53 @@ export function UnlockScreen() {
return (
-
-
- Enter password to unlock
-
+
+
Enter password to unlock
-
-
-
-
-
- showPassword()}
- className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
- >
- {passwordInput === 'password' ? (
-
- ) : (
-
- )}
-
-
-
- {errors.password && {errors.password.message}
}
-
-
-
+
+
+
+
showPassword()}
+ className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-white/10"
>
- {loading ? (
-
+ {passwordInput === 'password' ? (
+
) : (
- 'Continue →'
+
)}
-
- Reset password
-
-
-
+
+ {errors.password && {errors.password.message}
}
+
+
+
+
+ {loading ? (
+
+ ) : (
+ 'Continue →'
+ )}
+
+
+ Reset password
+
+
+
);
diff --git a/src/app/auth/welcome.tsx b/src/app/auth/welcome.tsx
index cfbf9fa6f..f741251ed 100644
--- a/src/app/auth/welcome.tsx
+++ b/src/app/auth/welcome.tsx
@@ -1,29 +1,41 @@
+import { LogicalSize, appWindow } from '@tauri-apps/plugin-window';
+import { useEffect } from 'react';
import { Link } from 'react-router-dom';
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
export function WelcomeScreen() {
+ useEffect(() => {
+ async function setWindow() {
+ await appWindow.setSize(new LogicalSize(400, 500));
+ await appWindow.setResizable(false);
+ await appWindow.center();
+ }
+
+ setWindow();
+
+ return () => {
+ appWindow.setSize(new LogicalSize(1080, 800)).then(() => {
+ appWindow.setResizable(false);
+ appWindow.center();
+ });
+ };
+ }, []);
+
return (
-
-
-
-
- Preserve your freedom
-
-
- Protect your future
-
-
- Stack bitcoin
-
-
- Use nostr
+
+
+
+
Welcome to Lume
+
+ Let's get you up and connecting with all peoples around the world on
+ Nostr
-
+
Login with private key
@@ -31,24 +43,15 @@ export function WelcomeScreen() {
Create new key
-
-
+
+
+
);
}
diff --git a/src/app/channel/components/blacklist.tsx b/src/app/channel/components/blacklist.tsx
index c53a539d3..798d898e4 100644
--- a/src/app/channel/components/blacklist.tsx
+++ b/src/app/channel/components/blacklist.tsx
@@ -18,7 +18,7 @@ export function ChannelBlackList({ blacklist }: { blacklist: any }) {
Your muted list
-
+
Currently, unmute only affect locally, when you move to new client,
muted list will loaded again
diff --git a/src/app/channel/components/createModal.tsx b/src/app/channel/components/createModal.tsx
index c5e9d8977..62dec65ef 100644
--- a/src/app/channel/components/createModal.tsx
+++ b/src/app/channel/components/createModal.tsx
@@ -93,7 +93,7 @@ export function ChannelCreateModal() {
// close modal
setIsOpen(false);
// redirect to channel page
- navigate(`/app/channel/${event.id}`);
+ navigate(`/channel/${event.id}`);
}, 1000);
} catch (e) {
console.log('error: ', e);
@@ -112,10 +112,10 @@ export function ChannelCreateModal() {
className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5"
>
-
Create channel
+ Create channel
@@ -147,7 +147,7 @@ export function ChannelCreateModal() {
Create channel
@@ -159,7 +159,7 @@ export function ChannelCreateModal() {
-
+
Channels are freedom square, everyone can speech freely, no one can
stop you or deceive what to speech
@@ -174,10 +174,10 @@ export function ChannelCreateModal() {
type={'hidden'}
{...register('picture')}
value={image}
- className="shadow-input relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
+ className="shadow-input relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-white/50 dark:bg-zinc-800 dark:text-white dark:shadow-black/10 dark:placeholder:text-white/50"
/>
-
+
Picture
@@ -195,7 +195,7 @@ export function ChannelCreateModal() {
Channel name *
@@ -206,28 +206,28 @@ export function ChannelCreateModal() {
minLength: 4,
})}
spellCheck={false}
- className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
+ className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-white/50"
/>
Description
-
+
Encrypted
-
+
All messages are encrypted and only invited members can view and
send message
@@ -248,10 +248,10 @@ export function ChannelCreateModal() {
{loading ? (
-
+
) : (
'Create channel →'
)}
diff --git a/src/app/channel/components/item.tsx b/src/app/channel/components/item.tsx
index 71c7c4169..f75f1a09d 100644
--- a/src/app/channel/components/item.tsx
+++ b/src/app/channel/components/item.tsx
@@ -7,17 +7,17 @@ export function ChannelsListItem({ data }: { data: any }) {
const channel = useChannelProfile(data.event_id);
return (
twMerge(
'inline-flex h-9 items-center gap-2.5 rounded-md px-2.5',
- isActive ? 'bg-zinc-900/50 text-zinc-100' : ''
+ isActive ? 'bg-zinc-900/50 text-white' : ''
)
}
>
- #
+ #
{channel?.name}
diff --git a/src/app/channel/components/messages/form.tsx b/src/app/channel/components/messages/form.tsx
index 25ff345db..234067f15 100644
--- a/src/app/channel/components/messages/form.tsx
+++ b/src/app/channel/components/messages/form.tsx
@@ -74,7 +74,7 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
-
{replyTo.content}
+
{replyTo.content}
stopReply()}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-800"
>
-
+
@@ -95,10 +95,10 @@ export function ChannelMessageForm({ channelID }: { channelID: string }) {
placeholder="Message"
className={`relative ${
replyTo.id ? 'h-36 pt-16' : 'h-24 pt-3'
- } w-full resize-none rounded-md bg-zinc-800 px-5 !outline-none placeholder:text-zinc-500`}
+ } w-full resize-none rounded-md bg-zinc-800 px-5 !outline-none placeholder:text-white/50`}
/>
-
+
-
+
This message will be hidden from your feed.
@@ -109,14 +109,14 @@ export function MessageHideButton({ id }: { id: string }) {
Cancel
hideMessage()}
- className="inline-flex h-9 items-center justify-center rounded-md bg-red-500 px-2 text-base font-medium text-zinc-100 hover:bg-red-600"
+ className="inline-flex h-9 items-center justify-center rounded-md bg-red-500 px-2 text-base font-medium text-white hover:bg-red-600"
>
Confirm
diff --git a/src/app/channel/components/messages/item.tsx b/src/app/channel/components/messages/item.tsx
index cecac4f5f..0d61b9040 100644
--- a/src/app/channel/components/messages/item.tsx
+++ b/src/app/channel/components/messages/item.tsx
@@ -19,7 +19,7 @@ export function ChannelMessageItem({ data }: { data: LumeEvent }) {
-
+
{content.parsed}
{Array.isArray(content.images) && content.images.length ? (
diff --git a/src/app/channel/components/messages/muteButton.tsx b/src/app/channel/components/messages/muteButton.tsx
index 57aa9a47d..fb6ebf7a1 100644
--- a/src/app/channel/components/messages/muteButton.tsx
+++ b/src/app/channel/components/messages/muteButton.tsx
@@ -99,7 +99,7 @@ export function MessageMuteButton({ pubkey }: { pubkey: string }) {
-
+
You will no longer see messages from this user.
@@ -109,14 +109,14 @@ export function MessageMuteButton({ pubkey }: { pubkey: string }) {
Cancel
muteUser()}
- className="inline-flex h-9 items-center justify-center rounded-md bg-red-500 px-2 text-base font-medium text-zinc-100 hover:bg-red-600"
+ className="inline-flex h-9 items-center justify-center rounded-md bg-red-500 px-2 text-base font-medium text-white hover:bg-red-600"
>
Confirm
diff --git a/src/app/channel/components/messages/userReply.tsx b/src/app/channel/components/messages/userReply.tsx
index 3488e0c70..2ef3b200b 100644
--- a/src/app/channel/components/messages/userReply.tsx
+++ b/src/app/channel/components/messages/userReply.tsx
@@ -13,7 +13,7 @@ export function UserReply({ pubkey }: { pubkey: string }) {
{isError || isLoading ? (
<>
-
+
>
) : (
<>
@@ -25,7 +25,7 @@ export function UserReply({ pubkey }: { pubkey: string }) {
className="h-9 w-9 rounded object-cover"
/>
-
+
Replying to {user?.name || shortenKey(pubkey)}
>
diff --git a/src/app/channel/components/metadata.tsx b/src/app/channel/components/metadata.tsx
index 16fd591a9..01bce9916 100644
--- a/src/app/channel/components/metadata.tsx
+++ b/src/app/channel/components/metadata.tsx
@@ -12,7 +12,7 @@ export function ChannelMetadata({ id }: { id: string }) {
const noteID = id ? nip19.noteEncode(id) : null;
const copyNoteID = async () => {
- const { writeText } = await import('@tauri-apps/api/clipboard');
+ const { writeText } = await import('@tauri-apps/plugin-clipboard-manager');
if (noteID) {
await writeText(noteID);
}
@@ -32,10 +32,10 @@ export function ChannelMetadata({ id }: { id: string }) {
{metadata?.name}
copyNoteID()}>
-
+
-
+
{metadata?.about || (noteID && `${noteID.substring(0, 24)}...`)}
diff --git a/src/app/channel/components/mutedItem.tsx b/src/app/channel/components/mutedItem.tsx
index 0662e7d1d..0ae950756 100644
--- a/src/app/channel/components/mutedItem.tsx
+++ b/src/app/channel/components/mutedItem.tsx
@@ -51,10 +51,10 @@ export function MutedItem({ data }: { data: any }) {
/>
-
+
{user?.displayName || user?.name || 'Pleb'}
-
+
{shortenKey(data.content)}
@@ -64,7 +64,7 @@ export function MutedItem({ data }: { data: any }) {
unmute()}
- className="inline-flex h-6 w-min items-center justify-center rounded px-1.5 text-base font-medium leading-none text-zinc-400 hover:bg-zinc-800 hover:text-fuchsia-500"
+ className="inline-flex h-6 w-min items-center justify-center rounded px-1.5 text-base font-medium leading-none text-white/50 hover:bg-zinc-800 hover:text-fuchsia-500"
>
Unmute
@@ -72,7 +72,7 @@ export function MutedItem({ data }: { data: any }) {
mute()}
- className="inline-flex h-6 w-min items-center justify-center rounded px-1.5 text-base font-medium leading-none text-zinc-400 hover:bg-zinc-800 hover:text-fuchsia-500"
+ className="inline-flex h-6 w-min items-center justify-center rounded px-1.5 text-base font-medium leading-none text-white/50 hover:bg-zinc-800 hover:text-fuchsia-500"
>
Mute
diff --git a/src/app/channel/index.tsx b/src/app/channel/index.tsx
index 83f771b9c..6ffe92986 100644
--- a/src/app/channel/index.tsx
+++ b/src/app/channel/index.tsx
@@ -23,7 +23,7 @@ const Header = (
-
+
{getHourAgo(24, now).toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
@@ -40,7 +40,7 @@ const Empty = (
Nothing to see here yet
-
+
Be the first to share a message in this channel.
@@ -102,7 +102,7 @@ export function ChannelScreen() {
data-tauri-drag-region
className="inline-flex h-11 w-full shrink-0 items-center justify-center border-b border-zinc-900"
>
-
Public Channel
+
Public Channel
diff --git a/src/app/chats/components/item.tsx b/src/app/chats/components/item.tsx
index f1fb48485..bf2442ff5 100644
--- a/src/app/chats/components/item.tsx
+++ b/src/app/chats/components/item.tsx
@@ -6,48 +6,45 @@ import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile';
-import { shortenKey } from '@utils/shortenKey';
+import { displayNpub } from '@utils/shortenKey';
+import { Chats } from '@utils/types';
-export function ChatsListItem({ data }: { data: any }) {
+export function ChatsListItem({ data }: { data: Chats }) {
const { status, user } = useProfile(data.sender_pubkey);
if (status === 'loading') {
return (
-
-
-
+
);
}
return (
twMerge(
- 'inline-flex h-9 items-center gap-2.5 rounded-md px-2.5',
- isActive ? 'bg-zinc-900/50 text-zinc-100' : ''
+ 'inline-flex h-9 items-center gap-2.5 rounded-md px-2',
+ isActive ? 'bg-white/10 text-white' : 'text-white/80'
)
}
>
-
-
-
-
-
-
- {user?.nip05 ||
- user?.name ||
- user?.displayName ||
- shortenKey(data.sender_pubkey)}
-
-
+
+
+
+ {user?.nip05 ||
+ user?.name ||
+ user?.display_name ||
+ displayNpub(data.sender_pubkey, 16)}
+
{data.new_messages > 0 && (
diff --git a/src/app/chats/components/list.tsx b/src/app/chats/components/list.tsx
index 14b7dbd62..72e6800a9 100644
--- a/src/app/chats/components/list.tsx
+++ b/src/app/chats/components/list.tsx
@@ -23,7 +23,7 @@ export function ChatsList() {
const renderItem = useCallback(
(item: Chats) => {
- if (account.pubkey !== item.sender_pubkey) {
+ if (account?.pubkey !== item.sender_pubkey) {
return ;
}
},
@@ -34,12 +34,12 @@ export function ChatsList() {
return (
);
@@ -50,20 +50,20 @@ export function ChatsList() {
{account ? (
) : (
-
-
-
+
)}
{chats.follows.map((item) => renderItem(item))}
- {chats.unknowns.length > 0 &&
}
-
{isFetching && (
-
-
-
+
)}
+ {chats.unknowns.length > 0 &&
}
+
);
}
diff --git a/src/app/chats/components/messages/form.tsx b/src/app/chats/components/messages/form.tsx
index 570fdb896..182f94463 100644
--- a/src/app/chats/components/messages/form.tsx
+++ b/src/app/chats/components/messages/form.tsx
@@ -4,7 +4,7 @@ import { useCallback, useState } from 'react';
import { EnterIcon } from '@shared/icons';
import { MediaUploader } from '@shared/mediaUploader';
-import { usePublish } from '@utils/hooks/usePublish';
+import { useNostr } from '@utils/hooks/useNostr';
export function ChatMessageForm({
receiverPubkey,
@@ -14,7 +14,7 @@ export function ChatMessageForm({
userPubkey: string;
userPrivkey: string;
}) {
- const { publish } = usePublish();
+ const { publish } = useNostr();
const [value, setValue] = useState('');
const encryptMessage = useCallback(async () => {
@@ -51,10 +51,10 @@ export function ChatMessageForm({
onKeyDown={handleEnterPress}
spellCheck={false}
placeholder="Message"
- className="relative h-11 w-full resize-none rounded-md bg-zinc-800 px-5 !outline-none placeholder:text-zinc-500"
+ className="relative h-11 w-full resize-none rounded-md bg-white/10 px-5 text-white !outline-none placeholder:text-white/50"
/>
-
+
+
-
- {content.parsed}
+
+ {data.content}
- {content.images.length > 0 &&
}
- {content.videos.length > 0 &&
}
- {content.links.length > 0 &&
}
- {content.notes.length > 0 &&
- content.notes.map((note: string) =>
)}
diff --git a/src/app/chats/components/modal.tsx b/src/app/chats/components/modal.tsx
index 3e3ba19c0..c4fada22b 100644
--- a/src/app/chats/components/modal.tsx
+++ b/src/app/chats/components/modal.tsx
@@ -1,5 +1,5 @@
-import { Dialog, Transition } from '@headlessui/react';
-import { Fragment, useState } from 'react';
+import * as Dialog from '@radix-ui/react-dialog';
+import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { User } from '@app/auth/components/user';
@@ -10,114 +10,77 @@ import { useAccount } from '@utils/hooks/useAccount';
export function NewMessageModal() {
const navigate = useNavigate();
- const [isOpen, setIsOpen] = useState(false);
+ const [open, setOpen] = useState(false);
const { status, account } = useAccount();
- const follows = account ? JSON.parse(account.follows as string) : [];
-
- const closeModal = () => {
- setIsOpen(false);
- };
-
- const openModal = () => {
- setIsOpen(true);
- };
const openChat = (pubkey: string) => {
- closeModal();
- navigate(`/app/chats/${pubkey}`);
+ setOpen(false);
+ navigate(`/chats/${pubkey}`);
};
return (
- <>
- openModal()}
- className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5"
- >
-
-
-
New chat
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- New chat
-
+
+
+
+
+
+
New chat
+
+
+
+
+
+
+
+
+
+
+
+ New chat
+
+
+
+
+
+
+ All messages will be encrypted, but anyone can see who you chat
+
+
+
+
+ {status === 'loading' ? (
+
+
+
+ ) : (
+ account?.follows?.map((follow) => (
+
+
+
openChat(follow)}
+ className="hidden w-max rounded bg-white/10 px-3 py-1 text-sm font-medium hover:bg-fuchsia-500 group-hover:inline-flex"
>
-
+ Chat
-
- All messages will be encrypted, but anyone can see who you chat
-
-
-
- {status === 'loading' ? (
-
-
-
- ) : (
- follows.map((follow) => (
-
-
-
- openChat(follow)}
- className="hidden w-max rounded-md bg-zinc-700 px-3 py-1 text-sm font-medium hover:bg-fuchsia-500 group-hover:inline-flex"
- >
- Chat
-
-
-
- ))
- )}
-
-
-
+ ))
+ )}
+
-
-
- >
+
+
+
);
}
diff --git a/src/app/chats/components/self.tsx b/src/app/chats/components/self.tsx
index 54f9a834d..f56336f22 100644
--- a/src/app/chats/components/self.tsx
+++ b/src/app/chats/components/self.tsx
@@ -6,46 +6,42 @@ import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile';
-import { shortenKey } from '@utils/shortenKey';
+import { displayNpub } from '@utils/shortenKey';
export function ChatsListSelfItem({ data }: { data: { pubkey: string } }) {
const { status, user } = useProfile(data.pubkey);
if (status === 'loading') {
return (
-
-
-
+
);
}
return (
twMerge(
- 'inline-flex h-9 items-center gap-2.5 rounded-md px-2.5',
- isActive ? 'bg-zinc-900/50 text-zinc-100' : ''
+ 'inline-flex h-9 items-center gap-2.5 rounded-md px-2',
+ isActive ? 'bg-white/10 text-white' : 'text-white/80'
)
}
>
-
-
-
+
-
- {user?.nip05 || user?.name || shortenKey(data.pubkey)}
+
+ {user?.nip05 || user?.name || displayNpub(data.pubkey, 16)}
- (you)
+ (you)
);
diff --git a/src/app/chats/components/sidebar.tsx b/src/app/chats/components/sidebar.tsx
index d417a1f14..f85ba93b9 100644
--- a/src/app/chats/components/sidebar.tsx
+++ b/src/app/chats/components/sidebar.tsx
@@ -5,7 +5,7 @@ import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useProfile } from '@utils/hooks/useProfile';
-import { shortenKey } from '@utils/shortenKey';
+import { displayNpub } from '@utils/shortenKey';
export function ChatSidebar({ pubkey }: { pubkey: string }) {
const { user } = useProfile(pubkey);
@@ -24,17 +24,17 @@ export function ChatSidebar({ pubkey }: { pubkey: string }) {
- {user?.displayName || user?.name}
+ {user?.display_name || user?.name}
-
- {user?.nip05 || shortenKey(pubkey)}
+
+ {user?.nip05 || displayNpub(pubkey, 16)}
{user?.bio || user?.about}
View full profile
diff --git a/src/app/chats/components/unknowns.tsx b/src/app/chats/components/unknowns.tsx
index 0747e28b9..1b3af4694 100644
--- a/src/app/chats/components/unknowns.tsx
+++ b/src/app/chats/components/unknowns.tsx
@@ -1,117 +1,81 @@
-import { Dialog, Transition } from '@headlessui/react';
-import { Fragment, useState } from 'react';
+import * as Dialog from '@radix-ui/react-dialog';
+import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { User } from '@app/auth/components/user';
-import { CancelIcon, StrangersIcon } from '@shared/icons';
+import { CancelIcon, PlusIcon } from '@shared/icons';
import { compactNumber } from '@utils/number';
import { Chats } from '@utils/types';
export function UnknownsModal({ data }: { data: Chats[] }) {
const navigate = useNavigate();
- const [isOpen, setIsOpen] = useState(false);
-
- const closeModal = () => {
- setIsOpen(false);
- };
-
- const openModal = () => {
- setIsOpen(true);
- };
+ const [open, setOpen] = useState(false);
const openChat = (pubkey: string) => {
- closeModal();
- navigate(`/app/chats/${pubkey}`);
+ setOpen(false);
+ navigate(`/chats/${pubkey}`);
};
return (
- <>
-
openModal()}
- className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5"
- >
-
-
-
-
-
- {compactNumber.format(data.length)} unknowns
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {data.length} unknowns
-
-
-
-
-
-
- All messages from people you not follow
-
-
+
+
+
+
+
+
+ {compactNumber.format(data.length)} unknowns
+
+
+
+
+
+
+
+
+
+
+
+
+ {data.length} unknowns
+
+
+
+
-
- {data.map((user) => (
-
+ All messages from people you not follow
+
+
+
+
+ {data.map((user) => (
+
+
+
+
openChat(user.sender_pubkey)}
+ className="hidden w-max rounded bg-white/10 px-3 py-1 text-sm font-medium hover:bg-fuchsia-500 group-hover:inline-flex"
>
-
-
- openChat(user.sender_pubkey)}
- className="hidden w-max rounded-md bg-zinc-700 px-3 py-1 text-sm font-medium hover:bg-fuchsia-500 group-hover:inline-flex"
- >
- Chat
-
-
-
- ))}
+ Chat
+
+
-
-
+ ))}
+
-
-
- >
+
+
+
);
}
diff --git a/src/app/chats/hooks/useDecryptMessage.tsx b/src/app/chats/hooks/useDecryptMessage.tsx
index 7036b9751..634fe3437 100644
--- a/src/app/chats/hooks/useDecryptMessage.tsx
+++ b/src/app/chats/hooks/useDecryptMessage.tsx
@@ -1,7 +1,9 @@
import { nip04 } from 'nostr-tools';
import { useEffect, useState } from 'react';
-export function useDecryptMessage(data: any, userPubkey: string, userPriv: string) {
+import { Chats } from '@utils/types';
+
+export function useDecryptMessage(data: Chats, userPubkey: string, userPriv: string) {
const [content, setContent] = useState(data.content);
useEffect(() => {
diff --git a/src/app/chats/index.tsx b/src/app/chats/index.tsx
index 9cfa84366..000f7c5a8 100644
--- a/src/app/chats/index.tsx
+++ b/src/app/chats/index.tsx
@@ -14,6 +14,7 @@ import { createChat, getChatMessages } from '@libs/storage';
import { useStronghold } from '@stores/stronghold';
import { useAccount } from '@utils/hooks/useAccount';
+import { Chats } from '@utils/types';
export function ChatScreen() {
const queryClient = useQueryClient();
@@ -34,7 +35,7 @@ export function ChatScreen() {
const userPrivkey = useStronghold((state) => state.privkey);
- const itemContent: any = useCallback(
+ const itemContent = useCallback(
(index: string | number) => {
return (
{
+ mutationFn: (data: Chats) => {
return createChat(
data.id,
data.receiver_pubkey,
@@ -100,16 +101,10 @@ export function ChatScreen() {
}, [pubkey]);
return (
-
-
-
-
Encrypted Chat
-
+
+
-
+
{status === 'loading' ? (
Loading...
@@ -131,7 +126,7 @@ export function ChatScreen() {
/>
)}
-
-
@@ -155,7 +146,7 @@ export function ChatScreen() {
const Empty = (
🙌
-
+
You two didn't talk yet, let's send first message
diff --git a/src/app/events/index.tsx b/src/app/events/index.tsx
new file mode 100644
index 000000000..48db0500e
--- /dev/null
+++ b/src/app/events/index.tsx
@@ -0,0 +1,51 @@
+import { useParams } from 'react-router-dom';
+
+import {
+ NoteActions,
+ NoteContent,
+ NoteReplyForm,
+ NoteStats,
+ ThreadUser,
+} from '@shared/notes';
+import { RepliesList } from '@shared/notes/replies/list';
+import { NoteSkeleton } from '@shared/notes/skeleton';
+
+import { useAccount } from '@utils/hooks/useAccount';
+import { useEvent } from '@utils/hooks/useEvent';
+
+export function EventScreen() {
+ const { id } = useParams();
+ const { account } = useAccount();
+ const { status, data } = useEvent(id);
+
+ return (
+
+
+ {status === 'loading' ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/note/index.tsx b/src/app/note/index.tsx
deleted file mode 100644
index bd0199342..000000000
--- a/src/app/note/index.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { useParams } from 'react-router-dom';
-
-import { useLiveThread } from '@app/space/hooks/useLiveThread';
-
-import { NoteMetadata } from '@shared/notes/metadata';
-import { NoteReplyForm } from '@shared/notes/replies/form';
-import { RepliesList } from '@shared/notes/replies/list';
-import { NoteSkeleton } from '@shared/notes/skeleton';
-import { User } from '@shared/user';
-
-import { useAccount } from '@utils/hooks/useAccount';
-import { useEvent } from '@utils/hooks/useEvent';
-
-export function NoteScreen() {
- const { id } = useParams();
- const { account } = useAccount();
- const { status, data } = useEvent(id);
-
- useLiveThread(id);
-
- return (
-
-
- {status === 'loading' ? (
-
- ) : (
-
- )}
-
-
-
-
-
- );
-}
diff --git a/src/app/root.tsx b/src/app/root.tsx
deleted file mode 100644
index dc389bf92..000000000
--- a/src/app/root.tsx
+++ /dev/null
@@ -1,200 +0,0 @@
-import { NDKUser } from '@nostr-dev-kit/ndk';
-import { nip19 } from 'nostr-tools';
-import { useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
-
-import { useNDK } from '@libs/ndk/provider';
-import {
- countTotalNotes,
- createChat,
- createNote,
- getLastLogin,
- updateAccount,
- updateLastLogin,
-} from '@libs/storage';
-
-import { LoaderIcon } from '@shared/icons';
-
-import { nHoursAgo } from '@utils/date';
-import { useAccount } from '@utils/hooks/useAccount';
-
-const totalNotes = await countTotalNotes();
-const lastLogin = await getLastLogin();
-
-export function Root() {
- const navigate = useNavigate();
-
- const { ndk, relayUrls, fetcher } = useNDK();
- const { status, account } = useAccount();
-
- async function getFollows() {
- const authors: string[] = [];
-
- const user = ndk.getUser({ hexpubkey: account.pubkey });
- const follows = await user.follows();
-
- follows.forEach((follow: NDKUser) => {
- authors.push(nip19.decode(follow.npub).data as string);
- });
-
- // update follows in db
- await updateAccount('follows', authors, account.pubkey);
-
- return authors;
- }
-
- async function fetchNotes() {
- try {
- const follows = await getFollows();
-
- if (follows.length > 0) {
- let since: number;
- if (totalNotes === 0 || lastLogin === 0) {
- since = nHoursAgo(48);
- } else {
- since = lastLogin;
- }
-
- const events = fetcher.allEventsIterator(
- relayUrls,
- { kinds: [1], authors: follows },
- { since: since },
- { skipVerification: true }
- );
- for await (const event of events) {
- await createNote(
- event.id,
- event.pubkey,
- event.kind,
- event.tags,
- event.content,
- event.created_at
- );
- }
- }
-
- return true;
- } catch (e) {
- console.log('error: ', e);
- }
- }
-
- async function fetchChats() {
- try {
- const sendMessages = await fetcher.fetchAllEvents(
- relayUrls,
- {
- kinds: [4],
- authors: [account.pubkey],
- },
- { since: lastLogin }
- );
-
- const receiveMessages = await fetcher.fetchAllEvents(
- relayUrls,
- {
- kinds: [4],
- '#p': [account.pubkey],
- },
- { since: lastLogin }
- );
-
- const events = [...sendMessages, ...receiveMessages];
- for (const event of events) {
- const receiverPubkey = event.tags.find((t) => t[0] === 'p')[1] || account.pubkey;
- await createChat(
- event.id,
- receiverPubkey,
- event.pubkey,
- event.content,
- event.tags,
- event.created_at
- );
- }
-
- return true;
- } catch (e) {
- console.log('error: ', e);
- }
- }
-
- /*
- async function fetchChannelMessages() {
- try {
- const ids = [];
- const channels: any = await getChannels();
- channels.forEach((channel) => {
- ids.push(channel.event_id);
- });
-
- const since = lastLogin === 0 ? dateToUnix(getHourAgo(48, now.current)) : lastLogin;
-
- const filter: NDKFilter = {
- '#e': ids,
- kinds: [42],
- since: since,
- };
-
- const events = await prefetchEvents(ndk, filter);
- events.forEach((event) => {
- const channel_id = event.tags[0][1];
- if (channel_id) {
- createChannelMessage(
- channel_id,
- event.id,
- event.pubkey,
- event.kind,
- event.content,
- event.tags,
- event.created_at
- );
- }
- });
-
- return true;
- } catch (e) {
- console.log('error: ', e);
- }
- }
- */
-
- useEffect(() => {
- async function prefetch() {
- const notes = await fetchNotes();
- const chats = await fetchChats();
- if (notes && chats) {
- const now = Math.floor(Date.now() / 1000);
- await updateLastLogin(now);
- navigate('/app/space', { replace: true });
- }
- }
-
- if (status === 'success' && account) {
- prefetch();
- }
- }, [status]);
-
- return (
-
-
-
-
-
-
-
-
- Prefetching data...
-
-
- This may take a few seconds, please don't close app.
-
-
-
-
-
-
- );
-}
diff --git a/src/app/settings/account.tsx b/src/app/settings/account.tsx
index a2b82b633..d91af1e77 100644
--- a/src/app/settings/account.tsx
+++ b/src/app/settings/account.tsx
@@ -23,36 +23,36 @@ export function AccountSettingsScreen() {
return (
-
Account
+
Account
{status === 'loading' ? (
Loading...
) : (
-
+
Public Key
-
+
Npub
Private Key
@@ -61,7 +61,7 @@ export function AccountSettingsScreen() {
readOnly
type={type}
value={privkey}
- className="relative w-full rounded-lg bg-zinc-800 py-3 pl-3.5 pr-11 text-zinc-100 !outline-none placeholder:text-zinc-400"
+ className="relative w-full rounded-lg bg-white/10 py-3 pl-3.5 pr-11 text-white !outline-none placeholder:text-white/50"
/>
) : (
)}
diff --git a/src/app/settings/components/autoStart.tsx b/src/app/settings/components/autoStart.tsx
index da370111e..38f87e770 100644
--- a/src/app/settings/components/autoStart.tsx
+++ b/src/app/settings/components/autoStart.tsx
@@ -1,7 +1,7 @@
import { Switch } from '@headlessui/react';
+import { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart';
import { useEffect, useState } from 'react';
import { twMerge } from 'tailwind-merge';
-import { disable, enable, isEnabled } from 'tauri-plugin-autostart-api';
import { getSetting, updateSetting } from '@libs/storage';
@@ -36,7 +36,7 @@ export function AutoStartSetting() {
Auto start
- Auto start at login
+ Auto start at login
Cache time (milliseconds)
-
+
The length of time before inactive data gets removed from the cache
@@ -37,7 +37,7 @@ export function CacheTimeSetting() {
onClick={() => update()}
className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-800 font-medium hover:bg-fuchsia-500"
>
-
+
diff --git a/src/app/settings/components/version.tsx b/src/app/settings/components/version.tsx
index ee0c0a206..0aed8bcfa 100644
--- a/src/app/settings/components/version.tsx
+++ b/src/app/settings/components/version.tsx
@@ -1,25 +1,34 @@
-import { getVersion } from '@tauri-apps/api/app';
+import { getVersion } from '@tauri-apps/plugin-app';
+import { useEffect, useState } from 'react';
import { RefreshIcon } from '@shared/icons';
-const appVersion = await getVersion();
-
export function VersionSetting() {
+ const [version, setVersion] = useState
('');
+
+ useEffect(() => {
+ async function checkVersion() {
+ const appVersion = await getVersion();
+ setVersion(appVersion);
+ }
+ checkVersion();
+ }, []);
+
return (
Version
-
+
You're using latest version
- {appVersion}
+ {version}
-
+
diff --git a/src/app/settings/general.tsx b/src/app/settings/general.tsx
index e535679ef..7226c1325 100644
--- a/src/app/settings/general.tsx
+++ b/src/app/settings/general.tsx
@@ -6,9 +6,9 @@ export function GeneralSettingsScreen() {
return (
-
General
-
-
+
General
+
+
diff --git a/src/app/settings/shortcuts.tsx b/src/app/settings/shortcuts.tsx
index f7d5daa7d..b5ab0c862 100644
--- a/src/app/settings/shortcuts.tsx
+++ b/src/app/settings/shortcuts.tsx
@@ -4,81 +4,79 @@ export function ShortcutsSettingsScreen() {
return (
-
Shortcuts
-
-
+
Shortcuts
+
+
-
- Open composer
-
+ Open composer
-
-
+
Add image block
-
-
+
Add newsfeed block
-
-
+
Open personal page
-
-
+
Open notification
-
diff --git a/src/app/space/components/add.tsx b/src/app/space/components/add.tsx
deleted file mode 100644
index 557e36286..000000000
--- a/src/app/space/components/add.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { AddFeedBlock } from '@app/space/components/addFeed';
-import { AddHashTagBlock } from '@app/space/components/addHashtag';
-import { AddImageBlock } from '@app/space/components/addImage';
-
-export function AddBlock() {
- return (
-
- );
-}
diff --git a/src/app/space/components/addFeed.tsx b/src/app/space/components/addFeed.tsx
deleted file mode 100644
index 62073538f..000000000
--- a/src/app/space/components/addFeed.tsx
+++ /dev/null
@@ -1,252 +0,0 @@
-import { Dialog, Transition } from '@headlessui/react';
-import { Combobox } from '@headlessui/react';
-import { useMutation, useQueryClient } from '@tanstack/react-query';
-import { nip19 } from 'nostr-tools';
-import { Fragment, useState } from 'react';
-import { useForm } from 'react-hook-form';
-import { useHotkeys } from 'react-hotkeys-hook';
-
-import { User } from '@app/auth/components/user';
-
-import { createBlock } from '@libs/storage';
-
-import { CancelIcon, CheckCircleIcon, CommandIcon, LoaderIcon } from '@shared/icons';
-
-import { BLOCK_KINDS, DEFAULT_AVATAR } from '@stores/constants';
-import { ADD_FEEDBLOCK_SHORTCUT } from '@stores/shortcuts';
-
-import { useAccount } from '@utils/hooks/useAccount';
-
-export function AddFeedBlock() {
- const queryClient = useQueryClient();
-
- const [loading, setLoading] = useState(false);
- const [isOpen, setIsOpen] = useState(false);
- const [selected, setSelected] = useState([]);
- const [query, setQuery] = useState('');
-
- const { status, account } = useAccount();
-
- const openModal = () => {
- setIsOpen(true);
- };
-
- const closeModal = () => {
- setIsOpen(false);
- };
-
- useHotkeys(ADD_FEEDBLOCK_SHORTCUT, () => openModal());
-
- const block = useMutation({
- mutationFn: (data: { kind: number; title: string; content: string }) => {
- return createBlock(data.kind, data.title, data.content);
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['blocks'] });
- },
- });
-
- const {
- register,
- handleSubmit,
- reset,
- formState: { isDirty, isValid },
- } = useForm();
-
- const onSubmit = (data: { kind: number; title: string; content: string }) => {
- setLoading(true);
-
- selected.forEach((item, index) => {
- if (item.substring(0, 4) === 'npub') {
- selected[index] = nip19.decode(item).data;
- }
- });
-
- // insert to database
- block.mutate({
- kind: BLOCK_KINDS.feed,
- title: data.title,
- content: JSON.stringify(selected),
- });
-
- setLoading(false);
- // reset form
- reset();
- // close modal
- closeModal();
- };
-
- return (
- <>
-
openModal()}
- className="inline-flex h-9 w-72 items-center justify-start gap-2.5 rounded-md px-2.5"
- >
-
-
-
New feed block
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Create feed block
-
-
-
-
-
-
- Specific newsfeed space for people you want to keep up to date
-
-
-
-
-
-
-
- Title *
-
-
-
-
-
- Choose at least 1 user *
-
-
-
-
- setQuery(event.target.value)}
- spellCheck={false}
- placeholder="Enter pubkey or npub..."
- className="relative mb-2 h-10 w-full rounded-md bg-zinc-700 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
- />
-
- {query.length > 0 && (
-
- {({ selected }) => (
- <>
-
-
-
-
- {query}
-
-
-
- {selected && (
-
- )}
- >
- )}
-
- )}
- {status === 'loading' ? (
- Loading...
- ) : (
- JSON.parse(account.follows as string).map((follow) => (
-
- {({ selected }) => (
- <>
-
- {selected && (
-
- )}
- >
- )}
-
- ))
- )}
-
-
-
-
-
-
-
- {loading ? (
-
- ) : (
- 'Confirm'
- )}
-
-
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/src/app/space/components/addHashtag.tsx b/src/app/space/components/addHashtag.tsx
deleted file mode 100644
index 50ca5e2ef..000000000
--- a/src/app/space/components/addHashtag.tsx
+++ /dev/null
@@ -1,174 +0,0 @@
-import { Dialog, Transition } from '@headlessui/react';
-import { useMutation, useQueryClient } from '@tanstack/react-query';
-import { Fragment, useState } from 'react';
-import { useForm } from 'react-hook-form';
-import { useHotkeys } from 'react-hotkeys-hook';
-
-import { createBlock } from '@libs/storage';
-
-import { CancelIcon, CommandIcon, LoaderIcon } from '@shared/icons';
-
-import { BLOCK_KINDS } from '@stores/constants';
-import { ADD_HASHTAGBLOCK_SHORTCUT } from '@stores/shortcuts';
-
-export function AddHashTagBlock() {
- const queryClient = useQueryClient();
-
- const [loading, setLoading] = useState(false);
- const [isOpen, setIsOpen] = useState(false);
-
- const openModal = () => {
- setIsOpen(true);
- };
-
- const closeModal = () => {
- setIsOpen(false);
- };
-
- useHotkeys(ADD_HASHTAGBLOCK_SHORTCUT, () => openModal());
-
- const {
- register,
- handleSubmit,
- reset,
- formState: { isDirty, isValid },
- } = useForm();
-
- const block = useMutation({
- mutationFn: (data: { kind: number; title: string; content: string }) => {
- return createBlock(data.kind, data.title, data.content);
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['blocks'] });
- },
- });
-
- const onSubmit = async (data: { hashtag: string }) => {
- setLoading(true);
-
- // mutate
- block.mutate({
- kind: BLOCK_KINDS.hashtag,
- title: data.hashtag,
- content: data.hashtag.replace('#', ''),
- });
-
- setLoading(false);
- // reset form
- reset();
- // close modal
- closeModal();
- };
-
- return (
- <>
-
openModal()}
- className="inline-flex h-9 w-72 items-center justify-start gap-2.5 rounded-md px-2.5"
- >
-
-
-
New hashtag block
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Create hashtag block
-
-
-
-
-
-
- Pin the hashtag you want to keep follow up
-
-
-
-
-
-
-
-
- {loading ? (
-
- ) : (
- 'Confirm'
- )}
-
-
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/src/app/space/components/addImage.tsx b/src/app/space/components/addImage.tsx
deleted file mode 100644
index 069673816..000000000
--- a/src/app/space/components/addImage.tsx
+++ /dev/null
@@ -1,226 +0,0 @@
-import { Dialog, Transition } from '@headlessui/react';
-import { useMutation, useQueryClient } from '@tanstack/react-query';
-import { Fragment, useEffect, useRef, useState } from 'react';
-import { useForm } from 'react-hook-form';
-import { useHotkeys } from 'react-hotkeys-hook';
-
-import { createBlock } from '@libs/storage';
-
-import { CancelIcon, CommandIcon, LoaderIcon } from '@shared/icons';
-import { Image } from '@shared/image';
-
-import { BLOCK_KINDS, DEFAULT_AVATAR } from '@stores/constants';
-import { ADD_IMAGEBLOCK_SHORTCUT } from '@stores/shortcuts';
-
-import { usePublish } from '@utils/hooks/usePublish';
-import { useImageUploader } from '@utils/hooks/useUploader';
-
-export function AddImageBlock() {
- const queryClient = useQueryClient();
- const upload = useImageUploader();
-
- const { publish } = usePublish();
-
- const [loading, setLoading] = useState(false);
- const [isOpen, setIsOpen] = useState(false);
- const [image, setImage] = useState('');
-
- const tags = useRef(null);
-
- const openModal = () => {
- setIsOpen(true);
- };
-
- const closeModal = () => {
- setIsOpen(false);
- };
-
- useHotkeys(ADD_IMAGEBLOCK_SHORTCUT, () => openModal());
-
- const {
- register,
- handleSubmit,
- reset,
- setValue,
- formState: { isDirty, isValid },
- } = useForm();
-
- const block = useMutation({
- mutationFn: (data: { kind: number; title: string; content: string }) => {
- return createBlock(data.kind, data.title, data.content);
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['blocks'] });
- },
- });
-
- const uploadImage = async () => {
- const image = await upload(null);
- if (image.url) {
- setImage(image.url);
- }
- };
-
- const onSubmit = async (data: { kind: number; title: string; content: string }) => {
- setLoading(true);
-
- // publish file metedata
- await publish({ content: data.title, kind: 1063, tags: tags.current });
-
- // mutate
- block.mutate({ kind: BLOCK_KINDS.image, title: data.title, content: data.content });
-
- setLoading(false);
- // reset form
- reset();
- // close modal
- closeModal();
- };
-
- useEffect(() => {
- setValue('content', image);
- }, [setValue, image]);
-
- return (
- <>
-
openModal()}
- className="inline-flex h-9 w-72 items-center justify-start gap-2.5 rounded-md px-2.5"
- >
-
-
-
New image block
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Create image block
-
-
-
-
-
-
- Pin your favorite image to Space then you can view every time that
- you use Lume, your image will be broadcast to Nostr Relay as well
-
-
-
-
-
-
-
-
-
- Picture
-
-
-
-
- uploadImage()}
- type="button"
- className="inline-flex h-6 items-center justify-center rounded bg-zinc-900 px-3 text-sm font-medium text-zinc-300 ring-1 ring-zinc-800 hover:bg-zinc-800"
- >
- Upload
-
-
-
-
-
-
- {loading ? (
-
- ) : (
- 'Confirm'
- )}
-
-
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/src/app/space/components/blocks/feed.tsx b/src/app/space/components/blocks/feed.tsx
index fa0d31791..e529fdaf8 100644
--- a/src/app/space/components/blocks/feed.tsx
+++ b/src/app/space/components/blocks/feed.tsx
@@ -9,11 +9,11 @@ import { NoteKindUnsupport } from '@shared/notes/kinds/unsupport';
import { NoteSkeleton } from '@shared/notes/skeleton';
import { TitleBar } from '@shared/titleBar';
-import { Block, LumeEvent } from '@utils/types';
+import { LumeEvent, Widget } from '@utils/types';
const ITEM_PER_PAGE = 10;
-export function FeedBlock({ params }: { params: Block }) {
+export function FeedBlock({ params }: { params: Widget }) {
const { status, data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery({
queryKey: ['newsfeed', params.content],
@@ -34,6 +34,7 @@ export function FeedBlock({ params }: { params: Block }) {
});
const itemsVirtualizer = rowVirtualizer.getVirtualItems();
+ const totalSize = rowVirtualizer.getTotalSize();
useEffect(() => {
const [lastItem] = [...rowVirtualizer.getVirtualItems()].reverse();
@@ -113,24 +114,20 @@ export function FeedBlock({ params }: { params: Block }) {
);
return (
-
+
-
+
{status === 'loading' ? (
-
) : itemsVirtualizer.length === 0 ? (
-
+
-
+
Not found any posts from last 48 hours
@@ -140,7 +137,7 @@ export function FeedBlock({ params }: { params: Block }) {
-
diff --git a/src/app/space/components/blocks/hashtag.tsx b/src/app/space/components/blocks/hashtag.tsx
index 33e14fa79..20504c955 100644
--- a/src/app/space/components/blocks/hashtag.tsx
+++ b/src/app/space/components/blocks/hashtag.tsx
@@ -8,17 +8,17 @@ import { NoteKind_1, NoteSkeleton } from '@shared/notes';
import { TitleBar } from '@shared/titleBar';
import { nHoursAgo } from '@utils/date';
-import { Block, LumeEvent } from '@utils/types';
+import { LumeEvent, Widget } from '@utils/types';
-export function HashtagBlock({ params }: { params: Block }) {
- const { relayUrls, fetcher } = useNDK();
+export function HashtagBlock({ params }: { params: Widget }) {
+ const { ndk } = useNDK();
const { status, data } = useQuery(['hashtag', params.content], async () => {
- const events = (await fetcher.fetchAllEvents(
- relayUrls,
- { kinds: [1], '#t': [params.content] },
- { since: nHoursAgo(48) }
- )) as unknown as LumeEvent[];
- return events;
+ const events = await ndk.fetchEvents({
+ kinds: [1],
+ '#t': [params.content],
+ since: nHoursAgo(24),
+ });
+ return [...events] as unknown as LumeEvent[];
});
const parentRef = useRef();
@@ -29,27 +29,24 @@ export function HashtagBlock({ params }: { params: Block }) {
});
const itemsVirtualizer = rowVirtualizer.getVirtualItems();
+ const totalSize = rowVirtualizer.getTotalSize();
return (
-
-
-
+
+
+
{status === 'loading' ? (
-
) : itemsVirtualizer.length === 0 ? (
-
+
-
- No new posts about this hashtag in 48 hours ago
+
+ No new posts about this hashtag in 24 hours ago
@@ -58,7 +55,7 @@ export function HashtagBlock({ params }: { params: Block }) {
state.removeWidget);
return (
-
+
{params.title}
remove.mutate(params.id)}
+ onClick={() => remove(params.id)}
className="inline-flex h-7 w-7 items-center justify-center rounded-md bg-white/30 backdrop-blur-lg"
>
@@ -28,7 +28,7 @@ export function ImageBlock({ params }: { params: Block }) {
src={params.content}
fallback={DEFAULT_AVATAR}
alt={params.title}
- className="h-full w-full rounded-xl border-t border-zinc-800/50 object-cover"
+ className="h-full w-full rounded-xl object-cover"
/>
diff --git a/src/app/space/components/blocks/following.tsx b/src/app/space/components/blocks/network.tsx
similarity index 84%
rename from src/app/space/components/blocks/following.tsx
rename to src/app/space/components/blocks/network.tsx
index 513bc8c0c..2eb9e5107 100644
--- a/src/app/space/components/blocks/following.tsx
+++ b/src/app/space/components/blocks/network.tsx
@@ -16,7 +16,7 @@ import { LumeEvent } from '@utils/types';
const ITEM_PER_PAGE = 10;
-export function FollowingBlock() {
+export function NetworkBlock() {
// subscribe for live update
useNewsfeed();
@@ -40,9 +40,10 @@ export function FollowingBlock() {
});
const itemsVirtualizer = rowVirtualizer.getVirtualItems();
+ const totalSize = rowVirtualizer.getTotalSize();
useEffect(() => {
- const [lastItem] = [...rowVirtualizer.getVirtualItems()].reverse();
+ const [lastItem] = [...itemsVirtualizer].reverse();
if (!lastItem) {
return;
@@ -51,7 +52,7 @@ export function FollowingBlock() {
if (lastItem.index >= notes.length - 1 && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
- }, [notes.length, fetchNextPage, rowVirtualizer.getVirtualItems()]);
+ }, [notes.length, fetchNextPage, itemsVirtualizer]);
const renderItem = useCallback(
(index: string | number) => {
@@ -125,30 +126,26 @@ export function FollowingBlock() {
);
return (
-
-
-
+
+
+
{status === 'loading' ? (
-
) : itemsVirtualizer.length === 0 ? (
-
+
-
+
You not have any posts to see yet
Follow more people to have more fun.
Trending
@@ -160,7 +157,7 @@ export function FollowingBlock() {
-
diff --git a/src/app/space/components/blocks/thread.tsx b/src/app/space/components/blocks/thread.tsx
index 0946cd06e..8056b2464 100644
--- a/src/app/space/components/blocks/thread.tsx
+++ b/src/app/space/components/blocks/thread.tsx
@@ -12,9 +12,9 @@ import { TitleBar } from '@shared/titleBar';
import { useAccount } from '@utils/hooks/useAccount';
import { useEvent } from '@utils/hooks/useEvent';
-import { Block } from '@utils/types';
+import { Widget } from '@utils/types';
-export function ThreadBlock({ params }: { params: Block }) {
+export function ThreadBlock({ params }: { params: Widget }) {
const { status, data } = useEvent(params.content);
const { account } = useAccount();
@@ -22,31 +22,35 @@ export function ThreadBlock({ params }: { params: Block }) {
// useLiveThread(params.content);
return (
-
+
-
+
{status === 'loading' ? (
-
) : (
diff --git a/src/app/space/components/blocks/user.tsx b/src/app/space/components/blocks/user.tsx
index dd572c57e..bc09dfb1d 100644
--- a/src/app/space/components/blocks/user.tsx
+++ b/src/app/space/components/blocks/user.tsx
@@ -9,20 +9,19 @@ import { TitleBar } from '@shared/titleBar';
import { UserProfile } from '@shared/userProfile';
import { nHoursAgo } from '@utils/date';
-import { Block, LumeEvent } from '@utils/types';
+import { LumeEvent, Widget } from '@utils/types';
-export function UserBlock({ params }: { params: Block }) {
+export function UserBlock({ params }: { params: Widget }) {
const parentRef = useRef
(null);
- const { fetcher, relayUrls } = useNDK();
+ const { ndk } = useNDK();
const { status, data } = useQuery(['user-feed', params.content], async () => {
- const events = await fetcher.fetchAllEvents(
- relayUrls,
- { kinds: [1], authors: [params.content] },
- { since: nHoursAgo(48) },
- { sort: true }
- );
- return events as unknown as LumeEvent[];
+ const events = await ndk.fetchEvents({
+ kinds: [1],
+ authors: [params.content],
+ since: nHoursAgo(48),
+ });
+ return [...events] as unknown as LumeEvent[];
});
const rowVirtualizer = useVirtualizer({
@@ -35,32 +34,27 @@ export function UserBlock({ params }: { params: Block }) {
const itemsVirtualizer = rowVirtualizer.getVirtualItems();
return (
-
+
-
+
-
- Latest activities
-
+
Latest postrs
{status === 'loading' ? (
-
) : itemsVirtualizer.length === 0 ? (
-
+
-
- No new posts about this hashtag in 48 hours ago
+
+ No new posts from this user in 48 hours ago
diff --git a/src/app/space/components/modals/feed.tsx b/src/app/space/components/modals/feed.tsx
new file mode 100644
index 000000000..aa91ec569
--- /dev/null
+++ b/src/app/space/components/modals/feed.tsx
@@ -0,0 +1,201 @@
+import { Combobox } from '@headlessui/react';
+import * as Dialog from '@radix-ui/react-dialog';
+import { nip19 } from 'nostr-tools';
+import { useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { useHotkeys } from 'react-hotkeys-hook';
+
+import { User } from '@app/auth/components/user';
+
+import { CancelIcon, CheckCircleIcon, CommandIcon, LoaderIcon } from '@shared/icons';
+
+import { BLOCK_KINDS, DEFAULT_AVATAR } from '@stores/constants';
+import { ADD_FEEDBLOCK_SHORTCUT } from '@stores/shortcuts';
+import { useWidgets } from '@stores/widgets';
+
+import { useAccount } from '@utils/hooks/useAccount';
+
+export function FeedModal() {
+ const [open, setOpen] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [selected, setSelected] = useState([]);
+ const [query, setQuery] = useState('');
+
+ const { status, account } = useAccount();
+
+ const setWidget = useWidgets((state) => state.setWidget);
+
+ useHotkeys(ADD_FEEDBLOCK_SHORTCUT, () => setOpen(true));
+
+ const {
+ register,
+ handleSubmit,
+ reset,
+ formState: { isDirty, isValid },
+ } = useForm();
+
+ const onSubmit = (data: { kind: number; title: string; content: string }) => {
+ setLoading(true);
+
+ selected.forEach((item, index) => {
+ if (item.substring(0, 4) === 'npub') {
+ selected[index] = nip19.decode(item).data;
+ }
+ });
+
+ // insert to database
+ setWidget({
+ kind: BLOCK_KINDS.feed,
+ title: data.title,
+ content: JSON.stringify(selected),
+ });
+
+ setLoading(false);
+ // reset form
+ reset();
+ // close modal
+ setOpen(false);
+ };
+
+ return (
+
+
+
+
+ Add newsfeed widget
+
+
+
+
+
+
+
+
+
+
+ Create feed block
+
+
+
+
+
+
+ Specific newsfeed space for people you want to keep up to date
+
+
+
+
+
+
+
+ Title *
+
+
+
+
+
+ Choose at least 1 user *
+
+
+
+
+ setQuery(event.target.value)}
+ spellCheck={false}
+ placeholder="Enter pubkey or npub..."
+ className="relative mb-2 h-10 w-full rounded-md bg-white/10 px-3 py-2 text-white !outline-none placeholder:text-white/50"
+ />
+
+ {query.length > 0 && (
+
+ {({ selected }) => (
+ <>
+
+
+
+
+ {query}
+
+
+
+ {selected && (
+
+ )}
+ >
+ )}
+
+ )}
+ {status === 'loading' ? (
+ Loading...
+ ) : (
+ account?.follows?.map((follow) => (
+
+ {({ selected }) => (
+ <>
+
+ {selected && (
+
+ )}
+ >
+ )}
+
+ ))
+ )}
+
+
+
+
+
+
+ {loading ? (
+
+ ) : (
+ 'Confirm'
+ )}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/space/components/modals/hashtag.tsx b/src/app/space/components/modals/hashtag.tsx
new file mode 100644
index 000000000..96e7ebfa7
--- /dev/null
+++ b/src/app/space/components/modals/hashtag.tsx
@@ -0,0 +1,121 @@
+import * as Dialog from '@radix-ui/react-dialog';
+import { useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { useHotkeys } from 'react-hotkeys-hook';
+
+import { CancelIcon, CommandIcon, LoaderIcon } from '@shared/icons';
+
+import { BLOCK_KINDS } from '@stores/constants';
+import { ADD_HASHTAGBLOCK_SHORTCUT } from '@stores/shortcuts';
+import { useWidgets } from '@stores/widgets';
+
+export function HashtagModal() {
+ const [loading, setLoading] = useState(false);
+ const [open, setOpen] = useState(false);
+
+ const setWidget = useWidgets((state) => state.setWidget);
+
+ useHotkeys(ADD_HASHTAGBLOCK_SHORTCUT, () => setOpen(false));
+
+ const {
+ register,
+ handleSubmit,
+ reset,
+ formState: { isDirty, isValid },
+ } = useForm();
+
+ const onSubmit = async (data: { hashtag: string }) => {
+ setLoading(true);
+
+ // mutate
+ setWidget({
+ kind: BLOCK_KINDS.hashtag,
+ title: data.hashtag,
+ content: data.hashtag.replace('#', ''),
+ });
+
+ setLoading(false);
+ // reset form
+ reset();
+ // close modal
+ setOpen(false);
+ };
+
+ return (
+
+
+
+
+ Add hashtag widget
+
+
+
+
+
+
+
+
+
+
+ Create hashtag block
+
+
+
+
+
+
+ Pin the hashtag you want to keep follow up
+
+
+
+
+
+
+
+ Hashtag *
+
+
+
+
+ {loading ? (
+
+ ) : (
+ 'Confirm'
+ )}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/space/components/modals/image.tsx b/src/app/space/components/modals/image.tsx
new file mode 100644
index 000000000..e51cbdc39
--- /dev/null
+++ b/src/app/space/components/modals/image.tsx
@@ -0,0 +1,160 @@
+import * as Dialog from '@radix-ui/react-dialog';
+import { useEffect, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { useHotkeys } from 'react-hotkeys-hook';
+
+import { CancelIcon, CommandIcon, LoaderIcon } from '@shared/icons';
+import { Image } from '@shared/image';
+
+import { BLOCK_KINDS, DEFAULT_AVATAR } from '@stores/constants';
+import { ADD_IMAGEBLOCK_SHORTCUT } from '@stores/shortcuts';
+import { useWidgets } from '@stores/widgets';
+
+import { useImageUploader } from '@utils/hooks/useUploader';
+
+export function ImageModal() {
+ const upload = useImageUploader();
+ const setWidget = useWidgets((state) => state.setWidget);
+
+ const [open, setOpen] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [image, setImage] = useState('');
+
+ useHotkeys(ADD_IMAGEBLOCK_SHORTCUT, () => setOpen(false));
+
+ const {
+ register,
+ handleSubmit,
+ reset,
+ setValue,
+ formState: { isDirty, isValid },
+ } = useForm();
+
+ const uploadImage = async () => {
+ const image = await upload(null, true);
+ if (image.url) {
+ setImage(image.url);
+ }
+ };
+
+ const onSubmit = async (data: { kind: number; title: string; content: string }) => {
+ setLoading(true);
+
+ // mutate
+ setWidget({ kind: BLOCK_KINDS.image, title: data.title, content: data.content });
+
+ setLoading(false);
+ // reset form
+ reset();
+ // close modal
+ setOpen(false);
+ };
+
+ useEffect(() => {
+ setValue('content', image);
+ }, [setValue, image]);
+
+ return (
+
+
+
+
+ Add image widget
+
+
+
+
+
+
+
+
+
+
+ Create image block
+
+
+
+
+
+
+ Pin your favorite image to Space then you can view every time that you
+ use Lume, your image will be broadcast to Nostr Relay as well
+
+
+
+
+
+
+
+
+ Title *
+
+
+
+
+
+ Picture
+
+
+
+
+ uploadImage()}
+ type="button"
+ className="inline-flex h-6 items-center justify-center rounded bg-white/10 px-3 text-sm font-medium text-white hover:bg-fuchsia-500"
+ >
+ Upload
+
+
+
+
+
+ {loading ? (
+
+ ) : (
+ 'Confirm'
+ )}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/space/hooks/useNewsfeed.tsx b/src/app/space/hooks/useNewsfeed.tsx
index 40209e73d..d8e37a236 100644
--- a/src/app/space/hooks/useNewsfeed.tsx
+++ b/src/app/space/hooks/useNewsfeed.tsx
@@ -4,8 +4,6 @@ import { useEffect, useRef } from 'react';
import { useNDK } from '@libs/ndk/provider';
import { createNote } from '@libs/storage';
-import { useNote } from '@stores/note';
-
import { useAccount } from '@utils/hooks/useAccount';
export function useNewsfeed() {
@@ -15,15 +13,11 @@ export function useNewsfeed() {
const { ndk } = useNDK();
const { status, account } = useAccount();
- const toggleHasNewNote = useNote((state) => state.toggleHasNewNote);
-
useEffect(() => {
if (status === 'success' && account) {
- const follows = account ? JSON.parse(account.follows as string) : [];
-
const filter: NDKFilter = {
kinds: [1, 6],
- authors: follows,
+ authors: account.follows,
since: now.current,
};
@@ -39,8 +33,6 @@ export function useNewsfeed() {
event.content,
event.created_at
);
- // notify user about created note
- toggleHasNewNote(true);
});
}
diff --git a/src/app/space/index.tsx b/src/app/space/index.tsx
index 8492ea126..e1b3580d5 100644
--- a/src/app/space/index.tsx
+++ b/src/app/space/index.tsx
@@ -1,93 +1,71 @@
-import { useQuery } from '@tanstack/react-query';
-import { useCallback } from 'react';
+import { useCallback, useEffect } from 'react';
-import { AddBlock } from '@app/space/components/add';
import { FeedBlock } from '@app/space/components/blocks/feed';
-import { FollowingBlock } from '@app/space/components/blocks/following';
import { HashtagBlock } from '@app/space/components/blocks/hashtag';
import { ImageBlock } from '@app/space/components/blocks/image';
+import { NetworkBlock } from '@app/space/components/blocks/network';
import { ThreadBlock } from '@app/space/components/blocks/thread';
import { UserBlock } from '@app/space/components/blocks/user';
-
-import { getBlocks } from '@libs/storage';
+import { FeedModal } from '@app/space/components/modals/feed';
+import { HashtagModal } from '@app/space/components/modals/hashtag';
+import { ImageModal } from '@app/space/components/modals/image';
import { LoaderIcon } from '@shared/icons';
-import { Block } from '@utils/types';
+import { useWidgets } from '@stores/widgets';
+
+import { Widget } from '@utils/types';
export function SpaceScreen() {
- const {
- status,
- data: blocks,
- isFetching,
- } = useQuery(
- ['blocks'],
- async () => {
- return await getBlocks();
- },
- {
- staleTime: Infinity,
- refetchOnMount: false,
- refetchOnReconnect: false,
- refetchOnWindowFocus: false,
- }
- );
+ const [widgets, fetchWidgets] = useWidgets((state) => [
+ state.widgets,
+ state.fetchWidgets,
+ ]);
- const renderBlock = useCallback(
- (block: Block) => {
- switch (block.kind) {
+ const renderItem = useCallback(
+ (widget: Widget) => {
+ switch (widget.kind) {
case 0:
- return
;
+ return
;
case 1:
- return
;
+ return
;
case 2:
- return
;
+ return
;
case 3:
- return
;
+ return
;
case 5:
- return
;
+ return
;
default:
break;
}
},
- [blocks]
+ [widgets]
);
- return (
-
-
- {status === 'loading' ? (
-
-
+ useEffect(() => {
+ fetchWidgets();
+ }, [fetchWidgets]);
+ return (
+
+
+ {!widgets ? (
+
) : (
- blocks.map((block: Block) => renderBlock(block))
- )}
- {isFetching && (
-
+ widgets.map((widget) => renderItem(widget))
)}
-
-
);
}
diff --git a/src/app/splash.tsx b/src/app/splash.tsx
new file mode 100644
index 000000000..3e45d749d
--- /dev/null
+++ b/src/app/splash.tsx
@@ -0,0 +1,91 @@
+import { invoke } from '@tauri-apps/api/tauri';
+import { useEffect, useState } from 'react';
+
+import { useNDK } from '@libs/ndk/provider';
+import { updateLastLogin } from '@libs/storage';
+
+import { LoaderIcon } from '@shared/icons';
+
+import { useAccount } from '@utils/hooks/useAccount';
+import { useNostr } from '@utils/hooks/useNostr';
+
+export function SplashScreen() {
+ const { ndk, relayUrls } = useNDK();
+ const { status, account } = useAccount();
+ const { fetchChats, fetchNotes } = useNostr();
+
+ const [loading, setLoading] = useState
(true);
+ const [error, setError] = useState(null);
+
+ const skip = async () => {
+ await invoke('close_splashscreen');
+ };
+
+ const prefetch = async () => {
+ const onboarding = localStorage.getItem('onboarding');
+ const step = JSON.parse(onboarding).state.step || null;
+ if (step) await invoke('close_splashscreen');
+
+ const notes = await fetchNotes();
+ const chats = await fetchChats();
+
+ if (notes.status === 'ok' && chats.status === 'ok') {
+ const now = Math.floor(Date.now() / 1000);
+ await updateLastLogin(now);
+ invoke('close_splashscreen');
+ } else {
+ setLoading(false);
+ setError(notes.message || chats.message);
+ console.log('fetch notes failed, error: ', notes.message);
+ console.log('fetch chats failed, error: ', chats.message);
+ }
+ };
+
+ useEffect(() => {
+ if (status === 'success' && !account) {
+ invoke('close_splashscreen');
+ }
+
+ if (ndk && account) {
+ console.log('prefetching...');
+ prefetch();
+ }
+ }, [ndk, account]);
+
+ return (
+
+
+
+
+
+ {loading ? (
+
+
+ {!ndk
+ ? 'Connecting to relay...'
+ : `Connected to ${relayUrls.length} relays`}
+
+
+ This may take a few seconds, please don't close app.
+
+
+ ) : (
+
+
+ Something wrong!
+
+
{error}
+
+ Skip
+
+
+ )}
+
+
+
+ );
+}
diff --git a/src/app/trending/components/profile.tsx b/src/app/trending/components/profile.tsx
index d5f9da25a..5148d2034 100644
--- a/src/app/trending/components/profile.tsx
+++ b/src/app/trending/components/profile.tsx
@@ -1,4 +1,5 @@
import { useQuery } from '@tanstack/react-query';
+import { fetch } from '@tauri-apps/plugin-http';
import { useEffect, useState } from 'react';
import { FollowIcon, LoaderIcon, UnfollowIcon } from '@shared/icons';
@@ -11,14 +12,23 @@ import { compactNumber } from '@utils/number';
import { shortenKey } from '@utils/shortenKey';
export function Profile({ data }: { data: any }) {
- const { status, data: userStats } = useQuery(['user-stats', data.pubkey], async () => {
- const res = await fetch(`https://api.nostr.band/v0/stats/profile/${data.pubkey}`);
- return res.json();
- });
+ const { status: socialStatus, userFollows, follow, unfollow } = useSocial();
+ const { status, data: userStats } = useQuery(
+ ['user-stats', data.pubkey],
+ async () => {
+ const res = await fetch(`https://api.nostr.band/v0/stats/profile/${data.pubkey}`);
+ return res.json();
+ },
+ {
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ refetchOnWindowFocus: false,
+ staleTime: Infinity,
+ }
+ );
const embedProfile = data.profile ? JSON.parse(data.profile.content) : null;
const profile = embedProfile;
- const { status: socialStatus, userFollows, follow, unfollow } = useSocial();
const [followed, setFollowed] = useState(false);
@@ -50,29 +60,28 @@ export function Profile({ data }: { data: any }) {
}
}, [status]);
- if (!profile)
+ if (!profile) {
return (
-
+
);
+ }
return (
-
+
-
-
-
+
-
+
{profile.display_name || profile.name}
-
+
{profile.nip05 || shortenKey(data.pubkey)}
@@ -81,15 +90,15 @@ export function Profile({ data }: { data: any }) {
{socialStatus === 'loading' ? (
-
+
) : followed ? (
unfollowUser(data.pubkey)}
- className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-800 text-zinc-400 hover:bg-fuchsia-500 hover:text-white"
+ className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-white/10 text-white hover:bg-fuchsia-500 hover:text-white"
>
@@ -97,7 +106,7 @@ export function Profile({ data }: { data: any }) {
followUser(data.pubkey)}
- className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-zinc-800 text-zinc-400 hover:bg-fuchsia-500 hover:text-white"
+ className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-white/10 text-white hover:bg-fuchsia-500 hover:text-white"
>
@@ -105,7 +114,7 @@ export function Profile({ data }: { data: any }) {
-
+
{profile.about || profile.bio}
@@ -115,26 +124,26 @@ export function Profile({ data }: { data: any }) {
) : (
-
+
{userStats.stats[data.pubkey].followers_pubkey_count ?? 0}
- Followers
+ Followers
-
+
{userStats.stats[data.pubkey].pub_following_pubkey_count ?? 0}
- Following
+ Following
-
+
{userStats.stats[data.pubkey].zaps_received
? compactNumber.format(
userStats.stats[data.pubkey].zaps_received.msats / 1000
)
: 0}
- Zaps received
+ Zaps received
)}
diff --git a/src/app/trending/components/trendingNotes.tsx b/src/app/trending/components/trendingNotes.tsx
index b41e6c8ca..7fd30e397 100644
--- a/src/app/trending/components/trendingNotes.tsx
+++ b/src/app/trending/components/trendingNotes.tsx
@@ -1,33 +1,52 @@
import { useQuery } from '@tanstack/react-query';
+import { fetch } from '@tauri-apps/plugin-http';
import { NoteKind_1 } from '@shared/notes';
import { NoteSkeleton } from '@shared/notes/skeleton';
import { TitleBar } from '@shared/titleBar';
+import { LumeEvent } from '@utils/types';
+
+interface Response {
+ notes: Array<{ event: LumeEvent }>;
+}
+
export function TrendingNotes() {
- const { status, data, error } = useQuery(['trending-notes'], async () => {
- const res = await fetch('https://api.nostr.band/v0/trending/notes');
- if (!res.ok) {
- throw new Error('Error');
+ const { status, data, error } = useQuery(
+ ['trending-notes'],
+ async () => {
+ const res = await fetch('https://api.nostr.band/v0/trending/notes');
+ if (!res.ok) {
+ throw new Error('Error');
+ }
+ const json: Response = await res.json();
+ return json.notes;
+ },
+ {
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ refetchOnWindowFocus: false,
+ staleTime: Infinity,
}
- return res.json();
- });
+ );
+
+ console.log('notes: ', data);
return (
-
+
-
+
{error &&
Failed to fetch
}
{status === 'loading' ? (
-
) : (
-
- {data.notes.map((item) => (
-
+
+ {data.map((item) => (
+
))}
)}
diff --git a/src/app/trending/components/trendingProfiles.tsx b/src/app/trending/components/trendingProfiles.tsx
index f66f83078..d65a778e4 100644
--- a/src/app/trending/components/trendingProfiles.tsx
+++ b/src/app/trending/components/trendingProfiles.tsx
@@ -1,33 +1,50 @@
import { useQuery } from '@tanstack/react-query';
+import { fetch } from '@tauri-apps/plugin-http';
import { Profile } from '@app/trending/components/profile';
import { NoteSkeleton } from '@shared/notes/skeleton';
import { TitleBar } from '@shared/titleBar';
+interface Response {
+ profiles: Array<{ pubkey: string }>;
+}
+
export function TrendingProfiles() {
- const { status, data, error } = useQuery(['trending-profiles'], async () => {
- const res = await fetch('https://api.nostr.band/v0/trending/profiles');
- if (!res.ok) {
- throw new Error('Error');
+ const { status, data, error } = useQuery(
+ ['trending-profiles'],
+ async () => {
+ const res = await fetch('https://api.nostr.band/v0/trending/profiles');
+ if (!res.ok) {
+ throw new Error('Error');
+ }
+ const json: Response = await res.json();
+ return json.profiles;
+ },
+ {
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ refetchOnWindowFocus: false,
+ staleTime: Infinity,
}
- return res.json();
- });
+ );
+
+ console.log('profiles: ', data);
return (
-
+
-
+
{error &&
Failed to fetch
}
{status === 'loading' ? (
-
) : (
-
- {data.profiles.map((item) => (
+
+ {data?.map((item) => (
))}
diff --git a/src/app/trending/index.tsx b/src/app/trending/index.tsx
index 284e9ea6a..52dc4905f 100644
--- a/src/app/trending/index.tsx
+++ b/src/app/trending/index.tsx
@@ -3,7 +3,7 @@ import { TrendingProfiles } from '@app/trending/components/trendingProfiles';
export function TrendingScreen() {
return (
-
+
diff --git a/src/app/users/components/feed.tsx b/src/app/users/components/feed.tsx
index 49e47b4fd..57814896a 100644
--- a/src/app/users/components/feed.tsx
+++ b/src/app/users/components/feed.tsx
@@ -12,15 +12,14 @@ import { LumeEvent } from '@utils/types';
export function UserFeed({ pubkey }: { pubkey: string }) {
const parentRef = useRef();
- const { fetcher, relayUrls } = useNDK();
+ const { ndk } = useNDK();
const { status, data } = useQuery(['user-feed', pubkey], async () => {
- const events = await fetcher.fetchAllEvents(
- relayUrls,
- { kinds: [1], authors: [pubkey] },
- { since: nHoursAgo(48) },
- { sort: true }
- );
- return events as unknown as LumeEvent[];
+ const events = await ndk.fetchEvents({
+ kinds: [1],
+ authors: [pubkey],
+ since: nHoursAgo(48),
+ });
+ return [...events] as unknown as LumeEvent[];
});
const rowVirtualizer = useVirtualizer({
diff --git a/src/app/users/components/metadata.tsx b/src/app/users/components/metadata.tsx
index a471e7e94..c72ad0436 100644
--- a/src/app/users/components/metadata.tsx
+++ b/src/app/users/components/metadata.tsx
@@ -18,32 +18,32 @@ export function UserMetadata({ pubkey }: { pubkey: string }) {
return (
-
+
{compactNumber.format(data.stats[pubkey].followers_pubkey_count) ?? 0}
- Followers
+ Followers
-
+
{compactNumber.format(data.stats[pubkey].pub_following_pubkey_count) ?? 0}
- Following
+ Following
-
+
{data.stats[pubkey].zaps_received
? compactNumber.format(data.stats[pubkey].zaps_received.msats / 1000)
: 0}
- Zaps received
+ Zaps received
-
+
{data.stats[pubkey].zaps_sent
? compactNumber.format(data.stats[pubkey].zaps_sent.msats / 1000)
: 0}
- Zaps sent
+ Zaps sent
);
diff --git a/src/app/users/components/profile.tsx b/src/app/users/components/profile.tsx
index 916ce9b2d..48d9425b1 100644
--- a/src/app/users/components/profile.tsx
+++ b/src/app/users/components/profile.tsx
@@ -52,7 +52,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
return (
<>
-
+
@@ -73,7 +73,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
{user?.displayName || user?.name || 'No name'}
-
+
{user?.nip05 || shortenKey(pubkey)}
@@ -81,7 +81,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
{status === 'loading' ? (
Loading...
@@ -89,7 +89,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
unfollowUser(pubkey)}
- className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-zinc-900 text-sm font-medium hover:bg-fuchsia-500"
+ className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-white/10 text-sm font-medium hover:bg-fuchsia-500"
>
Unfollow
@@ -97,23 +97,23 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
followUser(pubkey)}
- className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-zinc-900 text-sm font-medium hover:bg-fuchsia-500"
+ className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-white/10 text-sm font-medium hover:bg-fuchsia-500"
>
Follow
)}
Message
-
+
{account && account.pubkey === pubkey &&
}
-
+
{user?.about || user?.bio}
diff --git a/src/app/users/index.tsx b/src/app/users/index.tsx
index ba38f4462..19fef9111 100644
--- a/src/app/users/index.tsx
+++ b/src/app/users/index.tsx
@@ -16,15 +16,14 @@ export function UserScreen() {
const parentRef = useRef();
const { pubkey } = useParams();
- const { fetcher, relayUrls } = useNDK();
+ const { ndk } = useNDK();
const { status, data } = useQuery(['user-feed', pubkey], async () => {
- const events = await fetcher.fetchAllEvents(
- relayUrls,
- { kinds: [1], authors: [pubkey] },
- { since: nHoursAgo(48) },
- { sort: true }
- );
- return events as unknown as LumeEvent[];
+ const events = await ndk.fetchEvents({
+ kinds: [1],
+ authors: [pubkey],
+ since: nHoursAgo(48),
+ });
+ return [...events] as unknown as LumeEvent[];
});
const rowVirtualizer = useVirtualizer({
@@ -36,30 +35,30 @@ export function UserScreen() {
const itemsVirtualizer = rowVirtualizer.getVirtualItems();
return (
-
-
+
+
-
+
-
Latest posts
-
48 hours ago
+
Latest posts
+
48 hours ago
{status === 'loading' ? (
-
) : itemsVirtualizer.length === 0 ? (
-
+
-
- No new posts about this hashtag in 48 hours ago
+
+ No new posts in 48 hours ago
diff --git a/src/index.css b/src/index.css
index dfcc6b3b0..87d8b506f 100644
--- a/src/index.css
+++ b/src/index.css
@@ -15,11 +15,11 @@ button {
}
.markdown {
- @apply prose prose-zinc max-w-none select-text hyphens-auto dark:prose-invert 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 prose-a:leading-tight prose-a:text-fuchsia-400 prose-a:after:content-['_↗'] hover:prose-a:text-fuchsia-500 prose-blockquote:m-0 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-li:leading-tight 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-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 prose-a:leading-tight prose-a:after:content-['_↗'] hover:prose-a:text-fuchsia-500 prose-blockquote:m-0 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-li:leading-tight prose-img:mb-2 prose-img:mt-3 prose-hr:mx-0 prose-hr:my-2;
}
.ProseMirror p.is-empty::before {
- @apply text-zinc-500;
+ @apply text-white/50;
content: attr(data-placeholder);
float: left;
height: 0;
diff --git a/src/libs/ndk/cache.tsx b/src/libs/ndk/cache.tsx
new file mode 100644
index 000000000..cdbe4993c
--- /dev/null
+++ b/src/libs/ndk/cache.tsx
@@ -0,0 +1,57 @@
+import { NDKCacheAdapter } from '@nostr-dev-kit/ndk';
+import { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk';
+import { Store } from '@tauri-apps/plugin-store';
+
+export default class TauriAdapter implements NDKCacheAdapter {
+ public store: Store;
+ readonly locking: boolean;
+
+ constructor() {
+ this.store = new Store('.ndkcache.dat');
+ this.locking = true;
+ }
+
+ public async query(subscription: NDKSubscription): Promise
{
+ const { filter } = subscription;
+
+ if (filter.authors && filter.kinds) {
+ const promises = [];
+
+ for (const author of filter.authors) {
+ for (const kind of filter.kinds) {
+ const key = `${author}:${kind}`;
+ promises.push(this.store.get(key));
+ }
+ }
+
+ const results = await Promise.all(promises);
+
+ for (const result of results) {
+ if (result) {
+ const event = await this.store.get(result as string);
+
+ if (event) {
+ const ndkEvent = new NDKEvent(subscription.ndk, JSON.parse(event as string));
+ subscription.eventReceived(ndkEvent, undefined, true);
+ }
+ }
+ }
+ }
+ }
+
+ public async setEvent(event: NDKEvent): Promise {
+ const nostrEvent = await event.toNostrEvent();
+ const key = `${nostrEvent.pubkey}:${nostrEvent.kind}`;
+
+ return new Promise((resolve) => {
+ Promise.all([
+ this.store.set(event.id, JSON.stringify(nostrEvent)),
+ this.store.set(key, event.id),
+ ]).then(() => resolve());
+ });
+ }
+
+ public save() {
+ return this.store.save();
+ }
+}
diff --git a/src/libs/ndk/instance.ts b/src/libs/ndk/instance.ts
index d73572ad6..7b5a334b0 100644
--- a/src/libs/ndk/instance.ts
+++ b/src/libs/ndk/instance.ts
@@ -1,41 +1,84 @@
-// source: https://github.com/nostr-dev-kit/ndk-react/
+// inspire by: https://github.com/nostr-dev-kit/ndk-react/
import NDK from '@nostr-dev-kit/ndk';
-import { ndkAdapter } from '@nostr-fetch/adapter-ndk';
-import { NostrFetcher, normalizeRelayUrlSet } from 'nostr-fetch';
-import { useEffect, useState } from 'react';
+import { fetch } from '@tauri-apps/plugin-http';
+import { useEffect, useMemo, useState } from 'react';
-import { getSetting } from '@libs/storage';
+import TauriAdapter from '@libs/ndk/cache';
+import { getExplicitRelayUrls } from '@libs/storage';
-const setting = await getSetting('relays');
-const relays = normalizeRelayUrlSet(JSON.parse(setting));
+import { FULL_RELAYS } from '@stores/constants';
export const NDKInstance = () => {
const [ndk, setNDK] = useState(undefined);
- const [relayUrls, setRelayUrls] = useState(relays);
- const [fetcher, setFetcher] = useState(undefined);
+ const [relayUrls, setRelayUrls] = useState([]);
- useEffect(() => {
- loadNdk(relays);
- }, []);
+ const cacheAdapter = useMemo(() => new TauriAdapter(), []);
+
+ // TODO: fully support NIP-11
+ async function verifyRelays(relays: string[]) {
+ const verifiedRelays: string[] = [];
+
+ for (const relay of relays) {
+ let url: string;
+
+ if (relay.startsWith('ws')) {
+ url = relay.replace('ws', 'http');
+ }
+
+ if (relay.startsWith('wss')) {
+ url = relay.replace('wss', 'https');
+ }
+
+ try {
+ const res = await fetch(url, {
+ headers: { Accept: 'application/nostr+json' },
+ });
+
+ if (res.ok) {
+ const data = await res.json();
+ console.log('relay information: ', data);
+ verifiedRelays.push(relay);
+ }
+ } catch (e) {
+ console.log('fetch error', e);
+ }
+ }
- async function loadNdk(explicitRelayUrls: string[]) {
- const ndkInstance = new NDK({ explicitRelayUrls });
+ return verifiedRelays;
+ }
+
+ async function initNDK() {
+ let explicitRelayUrls: string[];
+ const explicitRelayUrlsFromDB = await getExplicitRelayUrls();
+
+ if (explicitRelayUrlsFromDB) {
+ explicitRelayUrls = await verifyRelays(explicitRelayUrlsFromDB);
+ } else {
+ explicitRelayUrls = await verifyRelays(FULL_RELAYS);
+ }
+
+ const instance = new NDK({ explicitRelayUrls, cacheAdapter });
try {
- await ndkInstance.connect();
+ await instance.connect(10000);
} catch (error) {
- console.error('ERROR loading NDK NDKInstance', error);
+ throw new Error('NDK instance init failed: ', error);
}
- setNDK(ndkInstance);
+ setNDK(instance);
setRelayUrls(explicitRelayUrls);
- setFetcher(NostrFetcher.withCustomPool(ndkAdapter(ndkInstance)));
}
+ useEffect(() => {
+ if (!ndk) initNDK();
+
+ return () => {
+ cacheAdapter.save();
+ };
+ }, []);
+
return {
ndk,
relayUrls,
- fetcher,
- loadNdk,
};
};
diff --git a/src/libs/ndk/provider.tsx b/src/libs/ndk/provider.tsx
index 6ebef78fd..58011bb8c 100644
--- a/src/libs/ndk/provider.tsx
+++ b/src/libs/ndk/provider.tsx
@@ -1,6 +1,5 @@
// source: https://github.com/nostr-dev-kit/ndk-react/
import NDK from '@nostr-dev-kit/ndk';
-import { NostrFetcher } from 'nostr-fetch';
import { PropsWithChildren, createContext, useContext } from 'react';
import { NDKInstance } from '@libs/ndk/instance';
@@ -8,33 +7,26 @@ import { NDKInstance } from '@libs/ndk/instance';
interface NDKContext {
ndk: NDK;
relayUrls: string[];
- fetcher: NostrFetcher;
- loadNdk: (_: string[]) => void;
}
const NDKContext = createContext({
ndk: new NDK({}),
relayUrls: [],
- fetcher: undefined,
- loadNdk: undefined,
});
const NDKProvider = ({ children }: PropsWithChildren) => {
- const { ndk, relayUrls, fetcher, loadNdk } = NDKInstance();
+ const { ndk, relayUrls } = NDKInstance();
- if (ndk)
- return (
-
- {children}
-
- );
+ return (
+
+ {children}
+
+ );
};
const useNDK = () => {
diff --git a/src/libs/openGraph.tsx b/src/libs/openGraph.tsx
index 528db73e3..3ef87cdf4 100644
--- a/src/libs/openGraph.tsx
+++ b/src/libs/openGraph.tsx
@@ -1,4 +1,4 @@
-import { FetchOptions, ResponseType, fetch } from '@tauri-apps/api/http';
+import { fetch } from '@tauri-apps/plugin-http';
import * as cheerio from 'cheerio';
import { OPENGRAPH } from '@stores/constants';
@@ -19,7 +19,13 @@ interface IPreFetchedResource {
imagesPropertyType?: string;
proxyUrl?: string;
url: string;
- data: any;
+ data: string;
+}
+
+function throwOnLoopback(address: string) {
+ if (OPENGRAPH.REGEX_LOOPBACK.test(address)) {
+ throw new Error('SSRF request detected, trying to query host');
+ }
}
function metaTag(doc: cheerio.CheerioAPI, type: string, attr: string) {
@@ -28,42 +34,42 @@ function metaTag(doc: cheerio.CheerioAPI, type: string, attr: string) {
}
function metaTagContent(doc: cheerio.CheerioAPI, type: string, attr: string) {
- return doc(`meta[${attr}='${type}']`).attr('content');
+ return doc(`meta[${attr}='${type}']`).attr(`content`);
}
function getTitle(doc: cheerio.CheerioAPI) {
let title =
- metaTagContent(doc, 'og:title', 'property') ||
- metaTagContent(doc, 'og:title', 'name');
+ metaTagContent(doc, `og:title`, `property`) ||
+ metaTagContent(doc, `og:title`, `name`);
if (!title) {
- title = doc('title').text();
+ title = doc(`title`).text();
}
return title;
}
function getSiteName(doc: cheerio.CheerioAPI) {
const siteName =
- metaTagContent(doc, 'og:site_name', 'property') ||
- metaTagContent(doc, 'og:site_name', 'name');
+ metaTagContent(doc, `og:site_name`, `property`) ||
+ metaTagContent(doc, `og:site_name`, `name`);
return siteName;
}
function getDescription(doc: cheerio.CheerioAPI) {
const description =
- metaTagContent(doc, 'description', 'name') ||
- metaTagContent(doc, 'Description', 'name') ||
- metaTagContent(doc, 'og:description', 'property');
+ metaTagContent(doc, `description`, `name`) ||
+ metaTagContent(doc, `Description`, `name`) ||
+ metaTagContent(doc, `og:description`, `property`);
return description;
}
function getMediaType(doc: cheerio.CheerioAPI) {
- const node = metaTag(doc, 'medium', 'name');
+ const node = metaTag(doc, `medium`, `name`);
if (node) {
- const content = node.attr('content');
- return content === 'image' ? 'photo' : content;
+ const content = node.attr(`content`);
+ return content === `image` ? `photo` : content;
}
return (
- metaTagContent(doc, 'og:type', 'property') || metaTagContent(doc, 'og:type', 'name')
+ metaTagContent(doc, `og:type`, `property`) || metaTagContent(doc, `og:type`, `name`)
);
}
@@ -77,14 +83,14 @@ function getImages(
let src: string | undefined;
let dic: Record = {};
- const imagePropertyType = imagesPropertyType ?? 'og';
+ const imagePropertyType = imagesPropertyType ?? `og`;
nodes =
- metaTag(doc, `${imagePropertyType}:image`, 'property') ||
- metaTag(doc, `${imagePropertyType}:image`, 'name');
+ metaTag(doc, `${imagePropertyType}:image`, `property`) ||
+ metaTag(doc, `${imagePropertyType}:image`, `name`);
if (nodes) {
nodes.each((_: number, node: cheerio.Element) => {
- if (node.type === 'tag') {
+ if (node.type === `tag`) {
src = node.attribs.content;
if (src) {
src = new URL(src, rootUrl).href;
@@ -95,18 +101,18 @@ function getImages(
}
if (images.length <= 0 && !imagesPropertyType) {
- src = doc('link[rel=image_src]').attr('href');
+ src = doc(`link[rel=image_src]`).attr(`href`);
if (src) {
src = new URL(src, rootUrl).href;
images = [src];
} else {
- nodes = doc('img');
+ nodes = doc(`img`);
if (nodes?.length) {
dic = {};
images = [];
nodes.each((_: number, node: cheerio.Element) => {
- if (node.type === 'tag') src = node.attribs.src;
+ if (node.type === `tag`) src = node.attribs.src;
if (src && !dic[src]) {
dic[src] = true;
// width = node.attribs.width;
@@ -135,32 +141,32 @@ function getVideos(doc: cheerio.CheerioAPI) {
let videoObj;
let index;
- const nodes = metaTag(doc, 'og:video', 'property') || metaTag(doc, 'og:video', 'name');
+ const nodes = metaTag(doc, `og:video`, `property`) || metaTag(doc, `og:video`, `name`);
if (nodes?.length) {
nodeTypes =
- metaTag(doc, 'og:video:type', 'property') || metaTag(doc, 'og:video:type', 'name');
+ metaTag(doc, `og:video:type`, `property`) || metaTag(doc, `og:video:type`, `name`);
nodeSecureUrls =
- metaTag(doc, 'og:video:secure_url', 'property') ||
- metaTag(doc, 'og:video:secure_url', 'name');
+ metaTag(doc, `og:video:secure_url`, `property`) ||
+ metaTag(doc, `og:video:secure_url`, `name`);
width =
- metaTagContent(doc, 'og:video:width', 'property') ||
- metaTagContent(doc, 'og:video:width', 'name');
+ metaTagContent(doc, `og:video:width`, `property`) ||
+ metaTagContent(doc, `og:video:width`, `name`);
height =
- metaTagContent(doc, 'og:video:height', 'property') ||
- metaTagContent(doc, 'og:video:height', 'name');
+ metaTagContent(doc, `og:video:height`, `property`) ||
+ metaTagContent(doc, `og:video:height`, `name`);
for (index = 0; index < nodes.length; index += 1) {
const node = nodes[index];
- if (node.type === 'tag') video = node.attribs.content;
+ if (node.type === `tag`) video = node.attribs.content;
nodeType = nodeTypes?.[index];
- if (nodeType?.type === 'tag') {
+ if (nodeType?.type === `tag`) {
videoType = nodeType ? nodeType.attribs.content : null;
}
nodeSecureUrl = nodeSecureUrls?.[index];
- if (nodeSecureUrl?.type === 'tag') {
+ if (nodeSecureUrl?.type === `tag`) {
videoSecureUrl = nodeSecureUrl ? nodeSecureUrl.attribs.content : null;
}
@@ -171,7 +177,7 @@ function getVideos(doc: cheerio.CheerioAPI) {
width,
height,
};
- if (videoType && videoType.indexOf('video/') === 0) {
+ if (videoType && videoType.indexOf(`video/`) === 0) {
videos.splice(0, 0, videoObj);
} else {
videos.push(videoObj);
@@ -193,7 +199,7 @@ function getFavicons(doc: cheerio.CheerioAPI, rootUrl: string) {
let nodes: cheerio.Cheerio | never[] = [];
let src: string | undefined;
- const relSelectors = ['rel=icon', `rel="shortcut icon"`, 'rel=apple-touch-icon'];
+ const relSelectors = [`rel=icon`, `rel="shortcut icon"`, `rel=apple-touch-icon`];
relSelectors.forEach((relSelector) => {
// look for all icon tags
@@ -202,9 +208,9 @@ function getFavicons(doc: cheerio.CheerioAPI, rootUrl: string) {
// collect all images from icon tags
if (nodes.length) {
nodes.each((_: number, node: cheerio.Element) => {
- if (node.type === 'tag') src = node.attribs.href;
+ if (node.type === `tag`) src = node.attribs.href;
if (src) {
- src = new URL(rootUrl).href;
+ src = new URL(src, rootUrl).href;
images.push(src);
}
});
@@ -222,7 +228,7 @@ function getFavicons(doc: cheerio.CheerioAPI, rootUrl: string) {
function parseImageResponse(url: string, contentType: string) {
return {
url,
- mediaType: 'image',
+ mediaType: `image`,
contentType,
favicons: [getDefaultFavicon(url)],
};
@@ -231,7 +237,7 @@ function parseImageResponse(url: string, contentType: string) {
function parseAudioResponse(url: string, contentType: string) {
return {
url,
- mediaType: 'audio',
+ mediaType: `audio`,
contentType,
favicons: [getDefaultFavicon(url)],
};
@@ -240,7 +246,7 @@ function parseAudioResponse(url: string, contentType: string) {
function parseVideoResponse(url: string, contentType: string) {
return {
url,
- mediaType: 'video',
+ mediaType: `video`,
contentType,
favicons: [getDefaultFavicon(url)],
};
@@ -249,7 +255,7 @@ function parseVideoResponse(url: string, contentType: string) {
function parseApplicationResponse(url: string, contentType: string) {
return {
url,
- mediaType: 'application',
+ mediaType: `application`,
contentType,
favicons: [getDefaultFavicon(url)],
};
@@ -268,7 +274,7 @@ function parseTextResponse(
title: getTitle(doc),
siteName: getSiteName(doc),
description: getDescription(doc),
- mediaType: getMediaType(doc) || 'website',
+ mediaType: getMediaType(doc) || `website`,
contentType,
images: getImages(doc, url, options.imagesPropertyType),
videos: getVideos(doc),
@@ -287,11 +293,11 @@ function parseUnknownResponse(
function parseResponse(response: IPreFetchedResource, options?: ILinkPreviewOptions) {
try {
- let contentType = response.headers['content-type'];
+ let contentType = response.headers[`content-type`];
// console.warn(`original content type`, contentType);
- if (contentType?.indexOf(';')) {
+ if (contentType?.indexOf(`;`)) {
// eslint-disable-next-line prefer-destructuring
- contentType = contentType.split(';')[0];
+ contentType = contentType.split(`;`)[0];
// console.warn(`splitting content type`, contentType);
}
@@ -330,20 +336,117 @@ function parseResponse(response: IPreFetchedResource, options?: ILinkPreviewOpti
}
}
-export async function getLinkPreview(text: string) {
- const fetchUrl = text;
- const options: FetchOptions = {
- method: 'GET',
- timeout: 5,
- responseType: ResponseType.Text,
+/**
+ * Parses the text, extracts the first link it finds and does a HTTP request
+ * to fetch the website content, afterwards it tries to parse the internal HTML
+ * and extract the information via meta tags
+ * @param text string, text to be parsed
+ * @param options ILinkPreviewOptions
+ */
+export async function getLinkPreview(text: string, options?: ILinkPreviewOptions) {
+ if (!text || typeof text !== `string`) {
+ throw new Error(`link-preview-js did not receive a valid url or text`);
+ }
+
+ const detectedUrl = text
+ .replace(/\n/g, ` `)
+ .split(` `)
+ .find((token) => OPENGRAPH.REGEX_VALID_URL.test(token));
+
+ if (!detectedUrl) {
+ throw new Error(`link-preview-js did not receive a valid a url or text`);
+ }
+
+ if (options?.followRedirects === `manual` && !options?.handleRedirects) {
+ throw new Error(
+ `link-preview-js followRedirects is set to manual, but no handleRedirects function was provided`
+ );
+ }
+
+ if (options?.resolveDNSHost) {
+ const resolvedUrl = await options.resolveDNSHost(detectedUrl);
+
+ throwOnLoopback(resolvedUrl);
+ }
+
+ const timeout = options?.timeout ?? 3000; // 3 second timeout default
+ const controller = new AbortController();
+ const timeoutCounter = setTimeout(() => controller.abort(), timeout);
+
+ const fetchOptions = {
+ headers: options?.headers ?? {},
+ redirect: options?.followRedirects ?? `error`,
+ signal: controller.signal,
};
- let response = await fetch(fetchUrl, options);
+ const fetchUrl = options?.proxyUrl ? options.proxyUrl.concat(detectedUrl) : detectedUrl;
+
+ // Seems like fetchOptions type definition is out of date
+ // https://github.com/node-fetch/node-fetch/issues/741
+ let response = await fetch(fetchUrl, fetchOptions as any).catch((e) => {
+ if (e.name === `AbortError`) {
+ throw new Error(`Request timeout`);
+ }
+
+ clearTimeout(timeoutCounter);
+ throw e;
+ });
+
+ if (
+ response.status > 300 &&
+ response.status < 309 &&
+ fetchOptions.redirect === `manual` &&
+ options?.handleRedirects
+ ) {
+ const forwardedUrl = response.headers.get(`location`) || ``;
+
+ if (!options.handleRedirects(fetchUrl, forwardedUrl)) {
+ throw new Error(`link-preview-js could not handle redirect`);
+ }
+
+ if (options?.resolveDNSHost) {
+ const resolvedUrl = await options.resolveDNSHost(forwardedUrl);
+
+ throwOnLoopback(resolvedUrl);
+ }
+
+ response = await fetch(forwardedUrl, fetchOptions as any);
+ }
+
+ clearTimeout(timeoutCounter);
+
+ const headers: Record = {};
+ response.headers.forEach((header, key) => {
+ headers[key] = header;
+ });
+
+ const normalizedResponse: IPreFetchedResource = {
+ url: options?.proxyUrl ? response.url.replace(options.proxyUrl, ``) : response.url,
+ headers,
+ data: await response.text(),
+ };
+
+ return parseResponse(normalizedResponse, options);
+}
+
+/**
+ * Skip the library fetching the website for you, instead pass a response object
+ * from whatever source you get and use the internal parsing of the HTML to return
+ * the necessary information
+ * @param response Preview Response
+ * @param options IPreviewLinkOptions
+ */
+export async function getPreviewFromContent(
+ response: IPreFetchedResource,
+ options?: ILinkPreviewOptions
+) {
+ if (!response || typeof response !== `object`) {
+ throw new Error(`link-preview-js did not receive a valid response object`);
+ }
- if (response.status > 300 && response.status < 309) {
- const forwardedUrl = response.headers.location || '';
- response = await fetch(forwardedUrl, options);
+ if (!response.url) {
+ throw new Error(`link-preview-js did not receive a valid response object`);
}
- return parseResponse(response);
+ return parseResponse(response, options);
}
diff --git a/src/libs/storage.tsx b/src/libs/storage.tsx
index 69dd8dab6..ad747e88c 100644
--- a/src/libs/storage.tsx
+++ b/src/libs/storage.tsx
@@ -1,9 +1,17 @@
+import Database from '@tauri-apps/plugin-sql';
import destr from 'destr';
-import Database from 'tauri-plugin-sql-api';
import { parser } from '@utils/parser';
import { getParentID } from '@utils/transform';
-import { Account, Block, Chats, LumeEvent, Profile, Settings } from '@utils/types';
+import {
+ Account,
+ Chats,
+ LumeEvent,
+ Profile,
+ Relays,
+ Settings,
+ Widget,
+} from '@utils/types';
let db: null | Database = null;
@@ -24,6 +32,12 @@ export async function getActiveAccount() {
'SELECT * FROM accounts WHERE is_active = 1;'
);
if (result.length > 0) {
+ result[0]['follows'] = result[0].follows
+ ? JSON.parse(result[0].follows as unknown as string)
+ : null;
+ result[0]['network'] = result[0].network
+ ? JSON.parse(result[0].network as unknown as string)
+ : null;
return result[0];
} else {
return null;
@@ -52,7 +66,7 @@ export async function createAccount(
[npub, pubkey, 'privkey is stored in secure storage', follows || '', is_active || 0]
);
if (res) {
- await createBlock(
+ await createWidget(
0,
'Have fun together!',
'https://void.cat/d/N5KUHEQCVg7SywXUPiJ7yq.jpg'
@@ -63,15 +77,12 @@ export async function createAccount(
}
// update account
-export async function updateAccount(
- column: string,
- value: string | string[],
- pubkey: string
-) {
+export async function updateAccount(column: string, value: string | string[]) {
const db = await connect();
- return await db.execute(`UPDATE accounts SET ${column} = ? WHERE pubkey = ?;`, [
+ const account = await getActiveAccount();
+ return await db.execute(`UPDATE accounts SET ${column} = ? WHERE id = ?;`, [
value,
- pubkey,
+ account.id,
]);
}
@@ -305,8 +316,6 @@ export async function getChannelUsers(channel_id: string) {
export async function getChats() {
const db = await connect();
const account = await getActiveAccount();
- const follows =
- typeof account.follows === 'string' ? JSON.parse(account.follows) : account.follows;
const chats: { follows: Array | null; unknowns: Array | null } = {
follows: [],
@@ -321,7 +330,7 @@ export async function getChats() {
result = result.sort((a, b) => a.new_messages - b.new_messages);
chats.follows = result.filter((el) => {
- return follows.some((i) => {
+ return account.follows.some((i) => {
return i === el.sender_pubkey;
});
});
@@ -408,34 +417,43 @@ export async function updateLastLogin(value: number) {
);
}
-// get all blocks
-export async function getBlocks() {
+// get all widgets
+export async function getWidgets() {
const db = await connect();
const account = await getActiveAccount();
- const result: Array = await db.select(
- `SELECT * FROM blocks WHERE account_id = "${account.id}" ORDER BY created_at DESC;`
+ const result: Array = await db.select(
+ `SELECT * FROM widgets WHERE account_id = "${account.id}" ORDER BY created_at DESC;`
);
return result;
}
// create block
-export async function createBlock(
+export async function createWidget(
kind: number,
title: string,
content: string | string[]
) {
const db = await connect();
const activeAccount = await getActiveAccount();
- return await db.execute(
- 'INSERT OR IGNORE INTO blocks (account_id, kind, title, content) VALUES (?, ?, ?, ?);',
+ const insert = await db.execute(
+ 'INSERT OR IGNORE INTO widgets (account_id, kind, title, content) VALUES (?, ?, ?, ?);',
[activeAccount.id, kind, title, content]
);
+
+ if (insert) {
+ const record: Widget = await db.select(
+ 'SELECT * FROM widgets ORDER BY id DESC LIMIT 1;'
+ );
+ return record[0];
+ } else {
+ return null;
+ }
}
// remove block
-export async function removeBlock(id: string) {
+export async function removeWidget(id: string) {
const db = await connect();
- return await db.execute(`DELETE FROM blocks WHERE id = "${id}";`);
+ return await db.execute(`DELETE FROM widgets WHERE id = "${id}";`);
}
// logout
@@ -444,8 +462,7 @@ export async function removeAll() {
await db.execute(`UPDATE settings SET value = "0" WHERE key = "last_login";`);
await db.execute('DELETE FROM replies;');
await db.execute('DELETE FROM notes;');
- await db.execute('DELETE FROM blacklist;');
- await db.execute('DELETE FROM blocks;');
+ await db.execute('DELETE FROM widgets;');
await db.execute('DELETE FROM chats;');
await db.execute('DELETE FROM accounts;');
return true;
@@ -497,3 +514,44 @@ export async function removePrivkey() {
`UPDATE accounts SET privkey = "privkey is stored in secure storage" WHERE id = "${activeAccount.id}";`
);
}
+
+// get relays
+export async function getRelays() {
+ const db = await connect();
+ const activeAccount = await getActiveAccount();
+ return (await db.select(
+ `SELECT * FROM relays WHERE account_id = "${activeAccount.id}";`
+ )) as Relays[];
+}
+
+// get relays
+export async function getExplicitRelayUrls() {
+ const db = await connect();
+ const activeAccount = await getActiveAccount();
+
+ if (!activeAccount) return null;
+
+ const result: Relays[] = await db.select(
+ `SELECT * FROM relays WHERE account_id = "${activeAccount.id}";`
+ );
+
+ if (result.length > 0) return result.map((el) => el.relay);
+
+ return null;
+}
+
+// create relay
+export async function createRelay(relay: string, purpose?: string) {
+ const db = await connect();
+ const activeAccount = await getActiveAccount();
+ return await db.execute(
+ 'INSERT OR IGNORE INTO relays (account_id, relay, purpose) VALUES (?, ?, ?);',
+ [activeAccount.id, relay, purpose || '']
+ );
+}
+
+// remove relay
+export async function removeRelay(relay: string) {
+ const db = await connect();
+ return await db.execute(`DELETE FROM relays WHERE relay = "${relay}";`);
+}
diff --git a/src/shared/accounts/active.tsx b/src/shared/accounts/active.tsx
index 60eb1d7fc..c2e459a7b 100644
--- a/src/shared/accounts/active.tsx
+++ b/src/shared/accounts/active.tsx
@@ -16,7 +16,7 @@ import { sendNativeNotification } from '@utils/notification';
const lastLogin = await getLastLogin();
-export function ActiveAccount({ data }: { data: any }) {
+export function ActiveAccount({ data }: { data: { pubkey: string; npub: string } }) {
const queryClient = useQueryClient();
const { ndk } = useNDK();
@@ -88,11 +88,11 @@ export function ActiveAccount({ data }: { data: any }) {
}, []);
if (status === 'loading') {
- return
;
+ return
;
}
return (
-
+
{
- navigate(-1);
- };
-
- const goForward = () => {
- navigate(1);
- };
-
- return (
-
-
-
goBack()}
- className="group inline-flex h-6 w-6 items-center justify-center rounded hover:bg-zinc-900"
- >
-
-
-
goForward()}
- className="group inline-flex h-6 w-6 items-center justify-center rounded hover:bg-zinc-900"
- >
-
-
-
-
- );
-}
diff --git a/src/shared/appLayout.tsx b/src/shared/appLayout.tsx
index 535686bac..295a51f43 100644
--- a/src/shared/appLayout.tsx
+++ b/src/shared/appLayout.tsx
@@ -5,12 +5,16 @@ import { Navigation } from '@shared/navigation';
export function AppLayout() {
return (
-
+
-
+
-
+ {
+ return location.pathname;
+ }}
+ />
);
diff --git a/src/shared/authLayout.tsx b/src/shared/authLayout.tsx
index b217de1b1..13e69e0e4 100644
--- a/src/shared/authLayout.tsx
+++ b/src/shared/authLayout.tsx
@@ -1,66 +1,10 @@
-import { platform } from '@tauri-apps/api/os';
-import { Outlet, useNavigate } from 'react-router-dom';
-
-import { ArrowLeftIcon, ArrowRightIcon } from '@shared/icons';
-
-const platformName = await platform();
+import { Outlet } from 'react-router-dom';
export function AuthLayout() {
- const navigate = useNavigate();
-
- const goBack = () => {
- navigate(-1);
- };
-
- const goForward = () => {
- navigate(1);
- };
-
return (
-
-
-
-
-
-
goBack()}
- className="group inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-zinc-900"
- >
-
-
-
goForward()}
- className="group inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-zinc-900"
- >
-
-
-
-
-
-
-
-
-
+
);
}
diff --git a/src/shared/avatarUploader.tsx b/src/shared/avatarUploader.tsx
index f77e5c908..6e73b8253 100644
--- a/src/shared/avatarUploader.tsx
+++ b/src/shared/avatarUploader.tsx
@@ -9,26 +9,24 @@ export function AvatarUploader({ setPicture }: { setPicture: any }) {
const [loading, setLoading] = useState(false);
const uploadAvatar = async () => {
+ setLoading(true);
const image = await upload(null);
if (image.url) {
- // update parent state
setPicture(image.url);
-
- // disable loader
- setLoading(false);
}
+ setLoading(false);
};
return (
uploadAvatar()}
- className="inline-flex h-full w-full items-center justify-center bg-zinc-900/40"
+ className="inline-flex h-full w-full items-center justify-center rounded-lg bg-black/50 hover:bg-black/60"
>
{loading ? (
-
+
) : (
-
+
)}
);
diff --git a/src/shared/bannerUploader.tsx b/src/shared/bannerUploader.tsx
index 57644ec0e..2a0483811 100644
--- a/src/shared/bannerUploader.tsx
+++ b/src/shared/bannerUploader.tsx
@@ -9,26 +9,24 @@ export function BannerUploader({ setBanner }: { setBanner: any }) {
const [loading, setLoading] = useState(false);
const uploadBanner = async () => {
+ setLoading(true);
const image = await upload(null);
if (image.url) {
- // update parent state
setBanner(image);
-
- // disable loader
- setLoading(false);
}
+ setLoading(false);
};
return (
uploadBanner()}
- className="inline-flex h-full w-full items-center justify-center bg-zinc-900/40"
+ className="inline-flex h-full w-full items-center justify-center bg-black/40 hover:bg-black/50"
>
{loading ? (
-
+
) : (
-
+
)}
);
diff --git a/src/shared/button.tsx b/src/shared/button.tsx
index 5dd4ea6db..538f73fcd 100644
--- a/src/shared/button.tsx
+++ b/src/shared/button.tsx
@@ -16,19 +16,19 @@ export function Button({
switch (preset) {
case 'small':
preClass =
- 'w-min h-9 px-4 bg-fuchsia-500 rounded-md text-sm font-medium text-zinc-100 hover:bg-fuchsia-600';
+ 'w-min h-9 px-4 bg-white/10 rounded-md text-sm font-medium text-white hover:bg-fuchsia-500';
break;
case 'publish':
preClass =
- 'w-min h-9 px-4 bg-fuchsia-500 rounded-md text-sm font-medium text-zinc-100 hover:bg-fuchsia-600';
+ 'w-min h-9 px-4 bg-fuchsia-500 rounded-md text-sm font-medium text-white hover:bg-fuchsia-600';
break;
case 'large':
preClass =
- 'h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600';
+ 'h-11 w-full bg-fuchsia-500 rounded-lg font-medium text-white hover:bg-fuchsia-600';
break;
case 'large-alt':
preClass =
- 'h-11 w-full bg-zinc-800 rounded-md font-medium text-zinc-300 border-t border-zinc-700/50 hover:bg-zinc-900';
+ 'h-11 w-full bg-white/10 rounded-lg font-medium text-white hover:bg-white/20';
break;
default:
break;
@@ -40,7 +40,7 @@ export function Button({
onClick={onClick}
disabled={disabled}
className={twMerge(
- 'inline-flex transform items-center justify-center gap-1 focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50',
+ 'inline-flex transform items-center justify-center gap-1 leading-none focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50',
preClass
)}
>
diff --git a/src/shared/composer/composer.tsx b/src/shared/composer/composer.tsx
index 7ba47db70..5fe92f78c 100644
--- a/src/shared/composer/composer.tsx
+++ b/src/shared/composer/composer.tsx
@@ -14,14 +14,13 @@ import { CancelIcon, LoaderIcon, PlusCircleIcon } from '@shared/icons';
import { MentionNote } from '@shared/notes';
import { useComposer } from '@stores/composer';
-import { FULL_RELAYS } from '@stores/constants';
-import { usePublish } from '@utils/hooks/usePublish';
+import { useNostr } from '@utils/hooks/useNostr';
import { useImageUploader } from '@utils/hooks/useUploader';
import { sendNativeNotification } from '@utils/notification';
export function Composer() {
- const { publish } = usePublish();
+ const { publish } = useNostr();
const [status, setStatus] = useState
(null);
const [reply, clearReply] = useComposer((state) => [state.reply, state.clearReply]);
@@ -45,7 +44,7 @@ export function Composer() {
Image.configure({
HTMLAttributes: {
class:
- 'rounded-lg w-2/3 h-auto border border-zinc-800 outline outline-2 outline-offset-0 outline-zinc-700 ml-1',
+ 'rounded-lg w-2/3 h-auto border border-white/10 outline outline-2 outline-offset-0 outline-white/20 ml-1',
},
}),
],
@@ -54,14 +53,14 @@ export function Composer() {
attributes: {
class: twMerge(
'scrollbar-hide markdown break-all max-h-[500px] overflow-y-auto outline-none pr-2',
- `${reply.id ? '!min-h-42' : '!min-h-[100px]'}`
+ `${reply.id ? '!min-h-42' : '!min-h-[120px]'}`
),
},
},
});
const uploadImage = async (file?: string) => {
- const image = await upload(file);
+ const image = await upload(file, true);
if (image.url) {
editor.commands.setImage({ src: image.url });
editor.commands.createParagraphNear();
@@ -88,13 +87,13 @@ export function Composer() {
if (reply.id && reply.pubkey) {
if (reply.root && reply.root.length > 1) {
tags = [
- ['e', reply.root, FULL_RELAYS[0], 'root'],
- ['e', reply.id, FULL_RELAYS[0], 'reply'],
+ ['e', reply.root, 'wss://relayable.org', 'root'],
+ ['e', reply.id, 'wss://relayable.org', 'reply'],
['p', reply.pubkey],
];
} else {
tags = [
- ['e', reply.id, FULL_RELAYS[0], 'reply'],
+ ['e', reply.id, 'wss://relayable.org', 'reply'],
['p', reply.pubkey],
];
}
@@ -112,7 +111,7 @@ export function Composer() {
await publish({ content: serializedContent, kind: 1, tags });
// send native notifiation
- await sendNativeNotification('Publish post successfully');
+ await sendNativeNotification('Publish postr successfully');
// update state
setStatus('done');
@@ -131,7 +130,7 @@ export function Composer() {
clearReply()}
- className="absolute right-3 top-3 inline-flex h-6 w-6 items-center justify-center gap-2 rounded bg-zinc-800 px-2 hover:bg-zinc-700"
+ className="absolute right-3 top-3 inline-flex h-6 w-6 items-center justify-center rounded bg-white/10 px-2"
>
-
+
)}
@@ -159,15 +158,15 @@ export function Composer() {
uploadImage()}
- className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-zinc-800"
+ className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-white/10"
>
-
+
submit()} preset="publish">
{status === 'loading' ? (
-
+
) : (
- 'Publish'
+ 'Postr'
)}
diff --git a/src/shared/composer/mention/item.tsx b/src/shared/composer/mention/item.tsx
index adea39fcb..a97a52a39 100644
--- a/src/shared/composer/mention/item.tsx
+++ b/src/shared/composer/mention/item.tsx
@@ -17,12 +17,12 @@ export function MentionItem({ profile }: { profile: Profile }) {
/>
-
+
{profile.ident || (
)}
-
+
{displayNpub(profile.pubkey, 16)}
diff --git a/src/shared/composer/modal.tsx b/src/shared/composer/modal.tsx
index 341942ad0..3cf0b35e6 100644
--- a/src/shared/composer/modal.tsx
+++ b/src/shared/composer/modal.tsx
@@ -1,8 +1,6 @@
-import { Dialog, Transition } from '@headlessui/react';
-import { Fragment } from 'react';
+import * as Dialog from '@radix-ui/react-dialog';
import { useHotkeys } from 'react-hotkeys-hook';
-import { Button } from '@shared/button';
import { Composer, ComposerUser } from '@shared/composer';
import {
CancelIcon,
@@ -20,71 +18,45 @@ export function ComposerModal() {
const { account } = useAccount();
const [toggle, open] = useComposer((state) => [state.toggleModal, state.open]);
- const closeModal = () => {
- toggle(false);
- };
-
useHotkeys(COMPOSE_SHORTCUT, () => toggle(true));
return (
- <>
- toggle(true)} preset="small">
-
- Compose
-
-
-
-
-
-
-
-
-
-
-
- {account &&
}
-
-
-
-
- New Post
-
-
-
-
-
-
+
+
+
+
+ Postr
+
+
+
+
+
+
+
+
+ {account &&
}
+
+
+
+
+ New Postr
+
-
-
-
+
+
toggle(false)}
+ className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-zinc-800"
+ >
+
+
+
+
-
-
- >
+
+
+
);
}
diff --git a/src/shared/composer/user.tsx b/src/shared/composer/user.tsx
index 75cea6275..a85afd748 100644
--- a/src/shared/composer/user.tsx
+++ b/src/shared/composer/user.tsx
@@ -9,15 +9,13 @@ export function ComposerUser({ pubkey }: { pubkey: string }) {
return (
-
-
-
-
+
+
{user?.nip05 || user?.name || (
)}
diff --git a/src/shared/editProfileModal.tsx b/src/shared/editProfileModal.tsx
index 8a8e98fc6..adf33cc1e 100644
--- a/src/shared/editProfileModal.tsx
+++ b/src/shared/editProfileModal.tsx
@@ -1,7 +1,7 @@
import { Dialog, Transition } from '@headlessui/react';
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useQueryClient } from '@tanstack/react-query';
-import { fetch } from '@tauri-apps/api/http';
+import { fetch } from '@tauri-apps/plugin-http';
import { Fragment, useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
@@ -13,7 +13,7 @@ import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from '@stores/constants';
import { useAccount } from '@utils/hooks/useAccount';
-import { usePublish } from '@utils/hooks/usePublish';
+import { useNostr } from '@utils/hooks/useNostr';
export function EditProfileModal() {
const queryClient = useQueryClient();
@@ -24,7 +24,7 @@ export function EditProfileModal() {
const [banner, setBanner] = useState('');
const [nip05, setNIP05] = useState({ verified: false, text: '' });
- const { publish } = usePublish();
+ const { publish } = useNostr();
const { account } = useAccount();
const {
register,
@@ -65,7 +65,6 @@ export function EditProfileModal() {
const res: any = await fetch(verifyURL, {
method: 'GET',
- timeout: 30,
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
@@ -146,7 +145,7 @@ export function EditProfileModal() {
openModal()}
- className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-zinc-900 text-sm font-medium hover:bg-fuchsia-500"
+ className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-white/10 text-sm font-medium hover:bg-fuchsia-500"
>
Edit profile
@@ -178,7 +177,7 @@ export function EditProfileModal() {
Edit profile
@@ -197,13 +196,13 @@ export function EditProfileModal() {
type={'hidden'}
{...register('picture')}
value={picture}
- className="shadow-input relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
+ className="shadow-input relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-white/50 dark:bg-zinc-800 dark:text-white dark:shadow-black/10 dark:placeholder:text-white/50"
/>
@@ -235,7 +234,7 @@ export function EditProfileModal() {
Name
@@ -246,13 +245,13 @@ export function EditProfileModal() {
minLength: 4,
})}
spellCheck={false}
- className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
+ className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-white/50"
/>
Lume ID / NIP-05
@@ -263,7 +262,7 @@ export function EditProfileModal() {
minLength: 4,
})}
spellCheck={false}
- className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
+ className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-white/50"
/>
{nip05.verified ? (
@@ -288,20 +287,20 @@ export function EditProfileModal() {
Bio
Website
@@ -309,13 +308,13 @@ export function EditProfileModal() {
type={'text'}
{...register('website', { required: false })}
spellCheck={false}
- className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
+ className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-white/50"
/>
Lightning address
@@ -323,17 +322,17 @@ export function EditProfileModal() {
type={'text'}
{...register('lud16', { required: false })}
spellCheck={false}
- className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
+ className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-white/50"
/>
{loading ? (
-
+
) : (
'Update'
)}
diff --git a/src/shared/icons/horizontalDots.tsx b/src/shared/icons/horizontalDots.tsx
new file mode 100644
index 000000000..d7c8adf29
--- /dev/null
+++ b/src/shared/icons/horizontalDots.tsx
@@ -0,0 +1,28 @@
+import { SVGProps } from 'react';
+
+export function HorizontalDotsIcon(
+ props: JSX.IntrinsicAttributes & SVGProps
+) {
+ return (
+
+
+
+
+ );
+}
diff --git a/src/shared/icons/index.tsx b/src/shared/icons/index.tsx
index 7bbdecb46..c0dd75a01 100644
--- a/src/shared/icons/index.tsx
+++ b/src/shared/icons/index.tsx
@@ -47,4 +47,6 @@ export * from './reaction';
export * from './thread';
export * from './strangers';
export * from './download';
+export * from './horizontalDots';
+export * from './arrowRightCircle';
// @endindex
diff --git a/src/shared/logout.tsx b/src/shared/logout.tsx
index 28471c249..a1056189c 100644
--- a/src/shared/logout.tsx
+++ b/src/shared/logout.tsx
@@ -1,6 +1,6 @@
-import { Dialog, Transition } from '@headlessui/react';
+import * as Dialog from '@radix-ui/react-dialog';
import { useQueryClient } from '@tanstack/react-query';
-import { relaunch } from '@tauri-apps/api/process';
+import { relaunch } from '@tauri-apps/plugin-process';
import { Fragment, useState } from 'react';
import { removeAll } from '@libs/storage';
@@ -29,91 +29,63 @@ export function Logout() {
};
return (
- <>
- openModal()}
- aria-label="Logout"
- className="inline-flex h-9 w-9 transform items-center justify-center rounded-md border-t border-zinc-700/50 bg-zinc-800 active:translate-y-1"
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Are you sure!
-
-
-
-
-
-
-
- When logout, all local data will be wiped, and restart app then
- you need to start onboarding process again when you log in.
-
-
- In the next version, Lume will support multi account, then you can
- switch between all account s instead of logout
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Are you sure!
+
+
+
+
-
-
-
- Cancel
-
- logout()}
- className="inline-flex h-9 items-center justify-center rounded-md bg-red-500 px-3 text-sm font-medium text-zinc-100 hover:bg-red-600"
- >
- Confirm
-
-
-
-
-
+
+
+ When logout, all local data will be wiped, and restart app then you
+ need to start onboarding process again when you log in.
+
+
+ In the next version, Lume will support multi account, then you can
+ switch between all account s instead of logout
+
+
+
+
+
+
+
+ Cancel
+
+ logout()}
+ className="inline-flex h-9 items-center justify-center rounded-md bg-red-500 px-3 text-sm font-medium text-white hover:bg-red-600"
+ >
+ Confirm
+
+
+
-
-
- >
+
+
+
);
}
diff --git a/src/shared/lumeBar.tsx b/src/shared/lumeBar.tsx
index ff14d2ed2..2641c729c 100644
--- a/src/shared/lumeBar.tsx
+++ b/src/shared/lumeBar.tsx
@@ -11,7 +11,7 @@ export function LumeBar() {
const { status, account } = useAccount();
return (
-
+
{status === 'loading' ? (
<>
@@ -26,9 +26,9 @@ export function LumeBar() {
)}
-
+
diff --git a/src/shared/mediaUploader.tsx b/src/shared/mediaUploader.tsx
index aed008c18..ae63bf6b9 100644
--- a/src/shared/mediaUploader.tsx
+++ b/src/shared/mediaUploader.tsx
@@ -1,5 +1,4 @@
import * as Tooltip from '@radix-ui/react-tooltip';
-import { open } from '@tauri-apps/api/dialog';
import { useState } from 'react';
import { LoaderIcon, MediaIcon } from '@shared/icons';
@@ -27,26 +26,22 @@ export function MediaUploader({ setState }: { setState: any }) {
uploadMedia()}
- className="group inline-flex h-6 w-6 items-center justify-center rounded bg-zinc-700 hover:bg-zinc-600"
+ className="group inline-flex h-8 w-8 items-center justify-center rounded hover:bg-white/10"
>
{loading ? (
-
+
) : (
-
+
)}
Upload media
-
+
diff --git a/src/shared/navigation.tsx b/src/shared/navigation.tsx
index f5e37a628..de6002e21 100644
--- a/src/shared/navigation.tsx
+++ b/src/shared/navigation.tsx
@@ -1,62 +1,124 @@
-import { Disclosure } from '@headlessui/react';
-import { NavLink } from 'react-router-dom';
+import * as Collapsible from '@radix-ui/react-collapsible';
+import { useState } from 'react';
+import { NavLink, useNavigate } from 'react-router-dom';
import { twMerge } from 'tailwind-merge';
import { ChatsList } from '@app/chats/components/list';
-import { AppHeader } from '@shared/appHeader';
import { ComposerModal } from '@shared/composer/modal';
-import { NavArrowDownIcon, SpaceIcon, TrendingIcon } from '@shared/icons';
+import {
+ ArrowLeftIcon,
+ ArrowRightIcon,
+ NavArrowDownIcon,
+ SpaceIcon,
+ TrendingIcon,
+} from '@shared/icons';
import { LumeBar } from '@shared/lumeBar';
export function Navigation() {
+ const navigate = useNavigate();
+
+ const [feeds, setFeeds] = useState(true);
+ const [chats, setChats] = useState(true);
+
return (
-
-
+
+
-
+
-
- {/* Newsfeed */}
-
-
-
- Feeds
-
-
-
-
- twMerge(
- 'flex h-9 items-center gap-2.5 rounded-md px-2.5 text-zinc-200',
- isActive ? 'bg-zinc-900/50' : ''
- )
- }
+
+
navigate(-1)}
+ className="group inline-flex h-8 w-8 items-center justify-center rounded hover:bg-white/10"
>
-
-
-
- Spaces
-
-
- twMerge(
- 'flex h-9 items-center gap-2.5 rounded-md px-2.5 text-zinc-200',
- isActive ? 'bg-zinc-900/50' : ''
- )
- }
+
+
+
navigate(1)}
+ className="group inline-flex h-8 w-8 items-center justify-center rounded hover:bg-white/10"
>
-
-
-
- Trending
-
+
+
+
+
+
+
+
+
+
+
+ Feeds
+
+
+
+
+
+
+ twMerge(
+ 'flex h-9 items-center gap-2.5 rounded-md px-2',
+ isActive ? 'bg-white/10 text-white' : 'text-white/80'
+ )
+ }
+ >
+
+
+
+ Spaces
+
+
+ twMerge(
+ 'flex h-9 items-center gap-2.5 rounded-md px-2 ',
+ isActive ? 'bg-white/10 text-white' : 'text-white/80'
+ )
+ }
+ >
+
+
+
+ Trending
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Chats
+
+
+
+
+
+
+
+
{/* Channels
{({ open }) => (
@@ -84,28 +146,6 @@ export function Navigation() {
)}
*/}
- {/* Chats */}
-
- {({ open }) => (
-
-
-
-
-
-
- Chats
-
-
-
-
-
-
- )}
-
diff --git a/src/shared/notes/actions.tsx b/src/shared/notes/actions.tsx
index dc9ce1321..d78d5264a 100644
--- a/src/shared/notes/actions.tsx
+++ b/src/shared/notes/actions.tsx
@@ -1,15 +1,16 @@
import * as Tooltip from '@radix-ui/react-tooltip';
import { ThreadIcon } from '@shared/icons';
+import { MoreActions } from '@shared/notes/actions/more';
import { NoteReaction } from '@shared/notes/actions/reaction';
import { NoteReply } from '@shared/notes/actions/reply';
import { NoteRepost } from '@shared/notes/actions/repost';
import { NoteZap } from '@shared/notes/actions/zap';
import { BLOCK_KINDS } from '@stores/constants';
+import { useWidgets } from '@stores/widgets';
import { useAccount } from '@utils/hooks/useAccount';
-import { useBlock } from '@utils/hooks/useBlock';
export function NoteActions({
id,
@@ -22,8 +23,8 @@ export function NoteActions({
noOpenThread?: boolean;
root?: string;
}) {
- const { add } = useBlock();
const { account } = useAccount();
+ const setWidget = useWidgets((state) => state.setWidget);
return (
@@ -36,13 +37,13 @@ export function NoteActions({
{!noOpenThread && (
<>
-
+
- add.mutate({
+ setWidget({
kind: BLOCK_KINDS.thread,
title: 'Thread',
content: id,
@@ -50,18 +51,19 @@ export function NoteActions({
}
className="group inline-flex h-7 w-7 items-center justify-center"
>
-
+
-
+
Open thread
-
+
>
)}
+
);
diff --git a/src/shared/notes/actions/more.tsx b/src/shared/notes/actions/more.tsx
new file mode 100644
index 000000000..6e3521395
--- /dev/null
+++ b/src/shared/notes/actions/more.tsx
@@ -0,0 +1,81 @@
+import * as Popover from '@radix-ui/react-popover';
+import * as Tooltip from '@radix-ui/react-tooltip';
+import { writeText } from '@tauri-apps/plugin-clipboard-manager';
+import { nip19 } from 'nostr-tools';
+import { EventPointer } from 'nostr-tools/lib/nip19';
+import { useState } from 'react';
+import { Link } from 'react-router-dom';
+
+import { HorizontalDotsIcon } from '@shared/icons';
+
+export function MoreActions({ id, pubkey }: { id: string; pubkey: string }) {
+ const [open, setOpen] = useState(false);
+
+ const copyID = async () => {
+ await writeText(nip19.neventEncode({ id: id, author: pubkey } as EventPointer));
+ setOpen(false);
+ };
+
+ const copyLink = async () => {
+ await writeText(
+ 'https://nostr.com/' +
+ nip19.neventEncode({ id: id, author: pubkey } as EventPointer)
+ );
+ setOpen(false);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ More
+
+
+
+
+
+
+
+
+ Open as new screen
+
+ copyLink()}
+ className="inline-flex h-10 items-center rounded-md px-2 text-sm font-medium text-white hover:bg-white/10"
+ >
+ Copy shareable link
+
+ copyID()}
+ className="inline-flex h-10 items-center rounded-md px-2 text-sm font-medium text-white hover:bg-white/10"
+ >
+ Copy ID
+
+
+ View profile
+
+
+
+
+
+ );
+}
diff --git a/src/shared/notes/actions/reaction.tsx b/src/shared/notes/actions/reaction.tsx
index 4fa7d31c4..a854ddccc 100644
--- a/src/shared/notes/actions/reaction.tsx
+++ b/src/shared/notes/actions/reaction.tsx
@@ -1,30 +1,30 @@
import * as Popover from '@radix-ui/react-popover';
-import { useCallback, useEffect, useState } from 'react';
+import { useState } from 'react';
import { ReactionIcon } from '@shared/icons';
-import { usePublish } from '@utils/hooks/usePublish';
+import { useNostr } from '@utils/hooks/useNostr';
const REACTIONS = [
{
content: '👏',
- img: 'https://raw.githubusercontent.com/Tarikul-Islam-Anik/Animated-Fluent-Emojis/master/Emojis/Hand%20gestures/Clapping%20Hands.png',
+ img: '/clapping_hands.png',
},
{
content: '🤪',
- img: 'https://raw.githubusercontent.com/Tarikul-Islam-Anik/Animated-Fluent-Emojis/master/Emojis/Smilies/Face%20with%20Tongue.png',
+ img: '/face_with_tongue.png',
},
{
content: '😮',
- img: 'https://raw.githubusercontent.com/Tarikul-Islam-Anik/Animated-Fluent-Emojis/master/Emojis/Smilies/Face%20with%20Open%20Mouth.png',
+ img: '/face_with_open_mouth.png',
},
{
content: '😢',
- img: 'https://raw.githubusercontent.com/Tarikul-Islam-Anik/Animated-Fluent-Emojis/master/Emojis/Smilies/Crying%20Face.png',
+ img: '/crying_face.png',
},
{
content: '🤡',
- img: 'https://raw.githubusercontent.com/Tarikul-Islam-Anik/Animated-Fluent-Emojis/master/Emojis/Smilies/Clown%20Face.png',
+ img: '/clown_face.png',
},
];
@@ -32,7 +32,7 @@ export function NoteReaction({ id, pubkey }: { id: string; pubkey: string }) {
const [open, setOpen] = useState(false);
const [reaction, setReaction] = useState
(null);
- const { publish } = usePublish();
+ const { publish } = useNostr();
const getReactionImage = (content: string) => {
const reaction: { img: string } = REACTIONS.find((el) => el.content === content);
@@ -66,13 +66,13 @@ export function NoteReaction({ id, pubkey }: { id: string; pubkey: string }) {
{reaction ? (
) : (
-
+
)}
@@ -80,21 +80,17 @@ export function NoteReaction({ id, pubkey }: { id: string; pubkey: string }) {
react('👏')}
- className="inline-flex h-8 w-8 items-center justify-center rounded hover:bg-zinc-600"
+ className="inline-flex h-8 w-8 items-center justify-center rounded hover:bg-white/10"
>
-
+
react('🤪')}
- className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-zinc-600"
+ className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-white/10"
>
@@ -102,10 +98,10 @@ export function NoteReaction({ id, pubkey }: { id: string; pubkey: string }) {
react('😮')}
- className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-zinc-600"
+ className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-white/10"
>
@@ -113,27 +109,19 @@ export function NoteReaction({ id, pubkey }: { id: string; pubkey: string }) {
react('😢')}
- className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-zinc-600"
+ className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-white/10"
>
-
+
react('🤡')}
- className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-zinc-600"
+ className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-white/10"
>
-
+
-
+
diff --git a/src/shared/notes/actions/reply.tsx b/src/shared/notes/actions/reply.tsx
index 3962ca354..d5bf9c335 100644
--- a/src/shared/notes/actions/reply.tsx
+++ b/src/shared/notes/actions/reply.tsx
@@ -23,13 +23,13 @@ export function NoteReply({
onClick={() => setReply(id, pubkey, root)}
className="group inline-flex h-7 w-7 items-center justify-center"
>
-
+
-
+
Quick reply
-
+
diff --git a/src/shared/notes/actions/repost.tsx b/src/shared/notes/actions/repost.tsx
index 0a08324a9..ef1413c2f 100644
--- a/src/shared/notes/actions/repost.tsx
+++ b/src/shared/notes/actions/repost.tsx
@@ -1,39 +1,78 @@
+import * as AlertDialog from '@radix-ui/react-alert-dialog';
import * as Tooltip from '@radix-ui/react-tooltip';
+import { useState } from 'react';
import { RepostIcon } from '@shared/icons';
-import { FULL_RELAYS } from '@stores/constants';
-
-import { usePublish } from '@utils/hooks/usePublish';
+import { useNostr } from '@utils/hooks/useNostr';
export function NoteRepost({ id, pubkey }: { id: string; pubkey: string }) {
- const { publish } = usePublish();
+ const { publish } = useNostr();
+ const [open, setOpen] = useState(false);
const submit = async () => {
const tags = [
- ['e', id, FULL_RELAYS[0], 'root'],
+ ['e', id, 'wss://relayable.org', 'root'],
['p', pubkey],
];
- await publish({ content: '', kind: 6, tags: tags });
+ const event = await publish({ content: '', kind: 6, tags: tags });
+ if (event) {
+ setOpen(false);
+ } else {
+ console.log('failed reposting');
+ }
};
return (
-
-
- submit()}
- className="group inline-flex h-7 w-7 items-center justify-center"
- >
-
-
-
-
-
- Repost
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+ Repostr
+
+
+
+
+
+
+
+
+
+
+ Confirm repostr this postr?
+
+
+ Repostred postr will be visible to your followers, and you cannot undo
+ this action.
+
+
+
+
+
+ Cancel
+
+
+
submit()}
+ className="inline-flex h-11 items-center justify-center rounded-lg bg-fuchsia-500 px-4 font-medium leading-none text-white outline-none"
+ >
+ Yes, repostr
+
+
+
+
+
+
);
}
diff --git a/src/shared/notes/actions/zap.tsx b/src/shared/notes/actions/zap.tsx
index 8a0cc7080..937df8a00 100644
--- a/src/shared/notes/actions/zap.tsx
+++ b/src/shared/notes/actions/zap.tsx
@@ -8,10 +8,10 @@ import { Button } from '@shared/button';
import { CancelIcon, ZapIcon } from '@shared/icons';
import { useEvent } from '@utils/hooks/useEvent';
-import { usePublish } from '@utils/hooks/usePublish';
+import { useNostr } from '@utils/hooks/useNostr';
export function NoteZap({ id }: { id: string }) {
- const { createZap } = usePublish();
+ const { createZap } = useNostr();
const { data: event } = useEvent(id);
const [amount, setAmount] = useState
(null);
@@ -23,6 +23,7 @@ export function NoteZap({ id }: { id: string }) {
};
const createZapRequest = async () => {
+ // @ts-expect-error, todo: fix this
const res = await createZap(event as unknown as NostrEvent, amount);
if (res) setInvoice(res);
};
@@ -43,10 +44,10 @@ export function NoteZap({ id }: { id: string }) {
-
+
Zap (Beta)
-
+
Send tip with Bitcoin via Lightning
@@ -178,10 +179,10 @@ export function NoteZap({ id }: { id: string }) {
-
+
Scan to pay
-
+
You must use Bitcoin wallet which support Lightning
such as: Blue Wallet, Bitkit, Phoenix,...
diff --git a/src/shared/notes/content.tsx b/src/shared/notes/content.tsx
index 9b315961d..c874f3a8b 100644
--- a/src/shared/notes/content.tsx
+++ b/src/shared/notes/content.tsx
@@ -12,7 +12,15 @@ import {
import { Content } from '@utils/types';
-export function NoteContent({ content }: { content: Content }) {
+export function NoteContent({ content, long }: { content: Content; long?: boolean }) {
+ if (long) {
+ return (
+
+ {content as unknown as string}
+
+ );
+ }
+
return (
<>
state.setWidget);
return (
- add.mutate({
+ setWidget({
kind: BLOCK_KINDS.hashtag,
title: tag,
content: tag.replace('#', ''),
diff --git a/src/shared/notes/kinds/kind1.tsx b/src/shared/notes/kinds/kind1.tsx
index f6bc0c0ca..2e11beba7 100644
--- a/src/shared/notes/kinds/kind1.tsx
+++ b/src/shared/notes/kinds/kind1.tsx
@@ -17,12 +17,12 @@ export function NoteKind_1({
return (