diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 261ef5f6..01f86325 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -255,6 +255,16 @@ async fn count_total_notes(db: DbState<'_>) -> Result { db.note().count(vec![]).exec().await.map_err(|_| ()) } +#[tauri::command] +async fn count_total_channels(db: DbState<'_>) -> Result { + db.channel().count(vec![]).exec().await.map_err(|_| ()) +} + +#[tauri::command] +async fn count_total_chats(db: DbState<'_>) -> Result { + db.chat().count(vec![]).exec().await.map_err(|_| ()) +} + #[tauri::command] #[specta::specta] async fn create_channel(db: DbState<'_>, data: CreateChannelData) -> Result { @@ -405,6 +415,8 @@ async fn main() { get_latest_notes, get_note_by_id, count_total_notes, + count_total_channels, + count_total_chats, create_channel, update_channel, get_channels, diff --git a/src/components/eventCollector.tsx b/src/components/eventCollector.tsx index ed9e2591..24fd3548 100644 --- a/src/components/eventCollector.tsx +++ b/src/components/eventCollector.tsx @@ -3,11 +3,12 @@ import { RelayContext } from '@components/relaysProvider'; import { hasNewerNoteAtom } from '@stores/note'; import { dateToUnix } from '@utils/getDate'; +import { fetchMetadata } from '@utils/metadata'; import { getParentID, pubkeyArray } from '@utils/transform'; import useLocalStorage, { writeStorage } from '@rehooks/local-storage'; +import { window } from '@tauri-apps/api'; import { TauriEvent } from '@tauri-apps/api/event'; -import { appWindow, getCurrent } from '@tauri-apps/api/window'; import { useSetAtom } from 'jotai'; import { useCallback, useContext, useEffect, useRef, useState } from 'react'; @@ -22,6 +23,26 @@ export default function EventCollector() { const now = useRef(new Date()); const unsubscribe = useRef(null); + const unlisten = useRef(null); + + const createFollowingPlebs = useCallback( + async (tags) => { + const { createPleb } = await import('@utils/bindings'); + for (const tag of tags) { + const pubkey = tag[1]; + const metadata: any = await fetchMetadata(pubkey); + + createPleb({ + pleb_id: pubkey + '-lume' + activeAccount.id.toString(), + pubkey: pubkey, + kind: 0, + metadata: metadata.content, + account_id: activeAccount.id, + }).catch(console.error); + } + }, + [activeAccount.id] + ); const subscribe = useCallback(async () => { const { createNote } = await import('@utils/bindings'); @@ -35,14 +56,18 @@ export default function EventCollector() { authors: pubkeyArray(follows), since: dateToUnix(now.current), }, + { + kinds: [3], + authors: [activeAccount.pubkey], + }, { kinds: [4], '#p': [activeAccount.pubkey], - since: 0, + since: dateToUnix(now.current), }, { kinds: [40], - since: 0, + since: dateToUnix(now.current), }, ], relays, @@ -66,30 +91,43 @@ export default function EventCollector() { setHasNewerNote(true) ) .catch(console.error); + } else if (event.kind === 3) { + createFollowingPlebs(event.tags); } else if (event.kind === 4) { if (event.pubkey !== activeAccount.pubkey) { - createChat({ pubkey: event.pubkey, created_at: event.created_at, account_id: activeAccount.id }); + createChat({ pubkey: event.pubkey, created_at: event.created_at, account_id: activeAccount.id }).catch( + console.error + ); } } else if (event.kind === 40) { - createChannel({ event_id: event.id, content: event.content, account_id: activeAccount.id }); + createChannel({ event_id: event.id, content: event.content, account_id: activeAccount.id }).catch( + console.error + ); } else { console.error; } } ); - }, [activeAccount.id, activeAccount.pubkey, follows, pool, relays, setHasNewerNote]); + }, [pool, relays, activeAccount.id, activeAccount.pubkey, follows, setHasNewerNote, createFollowingPlebs]); + + const listenWindowClose = useCallback(async () => { + unlisten.current = window.getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, () => { + writeStorage('lastLogin', now.current); + window.getCurrent().close(); + }); + }, []); useEffect(() => { subscribe(); - getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, () => { - writeStorage('lastLogin', now.current); - appWindow.close(); - }); + listenWindowClose(); return () => { - unsubscribe.current; + if (unsubscribe.current) { + unsubscribe.current(); + } + unlisten.current(); }; - }, [setHasNewerNote, subscribe]); + }, [setHasNewerNote, subscribe, listenWindowClose]); return (
diff --git a/src/components/multiAccounts/activeAccount.tsx b/src/components/multiAccounts/activeAccount.tsx index 0e8bdffd..a6c01475 100644 --- a/src/components/multiAccounts/activeAccount.tsx +++ b/src/components/multiAccounts/activeAccount.tsx @@ -1,20 +1,13 @@ -import { RelayContext } from '@components/relaysProvider'; - import { DEFAULT_AVATAR } from '@stores/constants'; -import { fetchMetadata } from '@utils/metadata'; - import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; import { AvatarIcon, ExitIcon, GearIcon } from '@radix-ui/react-icons'; import { writeText } from '@tauri-apps/api/clipboard'; import Image from 'next/image'; import { useRouter } from 'next/router'; import { nip19 } from 'nostr-tools'; -import { memo, useCallback, useContext, useEffect } from 'react'; - -export const ActiveAccount = memo(function ActiveAccount({ user }: { user: any }) { - const [pool, relays]: any = useContext(RelayContext); +export const ActiveAccount = ({ user }: { user: any }) => { const router = useRouter(); const userData = JSON.parse(user.metadata); @@ -26,50 +19,6 @@ export const ActiveAccount = memo(function ActiveAccount({ user }: { user: any } await writeText(nip19.npubEncode(user.pubkey)); }; - const insertFollowsToStorage = useCallback( - async (tags) => { - const { createPleb } = await import('@utils/bindings'); - - for (const tag of tags) { - const metadata: any = await fetchMetadata(tag[1]); - createPleb({ - pleb_id: tag[1] + '-lume' + user.id.toString(), - pubkey: tag[1], - kind: 0, - metadata: metadata.content, - account_id: user.id, - }).catch(console.error); - } - }, - [user.id] - ); - - useEffect(() => { - const unsubscribe = pool.subscribe( - [ - { - kinds: [3], - authors: [user.pubkey], - }, - ], - relays, - (event: any) => { - if (event.tags.length > 0) { - //insertFollowsToStorage(event.tags); - } - }, - 20000, - undefined, - { - unsubscribeOnEose: true, - } - ); - - return () => { - unsubscribe; - }; - }, [insertFollowsToStorage, pool, relays, user.pubkey]); - return ( @@ -125,4 +74,4 @@ export const ActiveAccount = memo(function ActiveAccount({ user }: { user: any } ); -}); +}; diff --git a/src/components/note/metadata.tsx b/src/components/note/metadata.tsx index c7bf6784..ae46bf60 100644 --- a/src/components/note/metadata.tsx +++ b/src/components/note/metadata.tsx @@ -52,7 +52,7 @@ export default function NoteMetadata({ ); return () => { - unsubscribe; + unsubscribe(); }; }, [eventID, eventTime, pool, relays]); diff --git a/src/components/note/parent.tsx b/src/components/note/parent.tsx index 4cd2a011..4661142b 100644 --- a/src/components/note/parent.tsx +++ b/src/components/note/parent.tsx @@ -50,7 +50,7 @@ export const NoteParent = memo(function NoteParent({ id }: { id: string }) { account_id: activeAccount.id, }).catch(console.error); }, - undefined, + 100, undefined, { unsubscribeOnEose: true, @@ -75,7 +75,9 @@ export const NoteParent = memo(function NoteParent({ id }: { id: string }) { checkNoteExist(); return () => { - unsubscribe.current; + if (unsubscribe.current) { + unsubscribe.current(); + } }; }, [checkNoteExist]); diff --git a/src/components/note/repost.tsx b/src/components/note/repost.tsx index 3472cba3..b115a809 100644 --- a/src/components/note/repost.tsx +++ b/src/components/note/repost.tsx @@ -46,7 +46,7 @@ export const NoteRepost = memo(function NoteRepost({ id }: { id: string }) { account_id: activeAccount.id, }).catch(console.error); }, - undefined, + 100, undefined, { unsubscribeOnEose: true, @@ -71,7 +71,9 @@ export const NoteRepost = memo(function NoteRepost({ id }: { id: string }) { checkNoteExist(); return () => { - unsubscribe.current; + if (unsubscribe.current) { + unsubscribe.current(); + } }; }, [checkNoteExist]); diff --git a/src/components/relaysProvider.tsx b/src/components/relaysProvider.tsx index be0f323c..777030cf 100644 --- a/src/components/relaysProvider.tsx +++ b/src/components/relaysProvider.tsx @@ -6,7 +6,7 @@ export const RelayContext = createContext({}); const relays = [ 'wss://relay.damus.io', 'wss://nostr-pub.wellorder.net', - //'wss://nostr.bongbong.com', + 'wss://nostr.bongbong.com', 'wss://nostr.zebedee.cloud', 'wss://nostr.fmt.wiz.biz', 'wss://relay.snort.social', diff --git a/src/pages/index.tsx b/src/pages/index.tsx index f9b2f6a1..fe58caef 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,42 +1,177 @@ import BaseLayout from '@layouts/base'; +import { RelayContext } from '@components/relaysProvider'; + +import { dateToUnix, hoursAgo } from '@utils/getDate'; +import { getParentID, pubkeyArray } from '@utils/transform'; + import LumeSymbol from '@assets/icons/Lume'; -import { writeStorage } from '@rehooks/local-storage'; +import useLocalStorage, { writeStorage } from '@rehooks/local-storage'; import { useRouter } from 'next/router'; -import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useCallback, useEffect } from 'react'; +import { + JSXElementConstructor, + ReactElement, + ReactFragment, + ReactPortal, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from 'react'; export default function Page() { + const [pool, relays]: any = useContext(RelayContext); const router = useRouter(); + const [lastLogin] = useLocalStorage('lastLogin', new Date()); + + const now = useRef(new Date()); + const eose = useRef(0); + const unsubscribe = useRef(null); + const fetchActiveAccount = useCallback(async () => { const { getAccounts } = await import('@utils/bindings'); return await getAccounts(); }, []); - const fetchFollowsByAccount = useCallback(async (id) => { + const fetchPlebsByAccount = useCallback(async (id: number, kind: number) => { const { getPlebs } = await import('@utils/bindings'); - return await getPlebs({ account_id: id, kind: 0 }); + return await getPlebs({ account_id: id, kind: kind }); }, []); + const totalNotes = useCallback(async () => { + const { countTotalNotes } = await import('@utils/commands'); + return countTotalNotes(); + }, []); + + const totalChannels = useCallback(async () => { + const { countTotalChannels } = await import('@utils/commands'); + return countTotalChannels(); + }, []); + + const totalChats = useCallback(async () => { + const { countTotalChats } = await import('@utils/commands'); + return countTotalChats(); + }, []); + + const fetchData = useCallback( + async (account, follows) => { + const { createNote } = await import('@utils/bindings'); + const { createChat } = await import('@utils/bindings'); + const { createChannel } = await import('@utils/bindings'); + + const notes = await totalNotes(); + const channels = await totalChannels(); + const chats = await totalChats(); + + const query = []; + let since: number; + + // kind 1 (notes) query + if (notes === 0) { + since = dateToUnix(hoursAgo(24, now.current)); + } else { + since = dateToUnix(new Date(lastLogin)); + } + query.push({ + kinds: [1], + authors: follows, + since: since, + until: dateToUnix(now.current), + }); + // kind 4 (chats) query + if (chats === 0) { + query.push({ + kinds: [4], + '#p': [account.pubkey], + since: 0, + until: dateToUnix(now.current), + }); + } + // kind 40 (channels) query + if (channels === 0) { + query.push({ + kinds: [40], + since: 0, + until: dateToUnix(now.current), + }); + } + // subscribe relays + unsubscribe.current = pool.subscribe( + query, + relays, + (event) => { + if (event.kind === 1) { + const parentID = getParentID(event.tags, event.id); + // insert event to local database + createNote({ + event_id: event.id, + pubkey: event.pubkey, + kind: event.kind, + tags: JSON.stringify(event.tags), + content: event.content, + parent_id: parentID, + parent_comment_id: '', + created_at: event.created_at, + account_id: account.id, + }).catch(console.error); + } else if (event.kind === 4) { + if (event.pubkey !== account.pubkey) { + createChat({ + pubkey: event.pubkey, + created_at: event.created_at, + account_id: account.id, + }).catch(console.error); + } + } else if (event.kind === 40) { + createChannel({ event_id: event.id, content: event.content, account_id: account.id }).catch(console.error); + } else { + console.error; + } + }, + undefined, + () => { + eose.current += 1; + if (eose.current > relays.length / 2) { + router.replace('/newsfeed/following'); + } + } + ); + }, + [router, pool, relays, lastLogin, totalChannels, totalChats, totalNotes] + ); + useEffect(() => { + let account; + let follows; + fetchActiveAccount() .then((res: any) => { if (res.length > 0) { - // fetch follows - fetchFollowsByAccount(res[0].id).then((follows) => { - writeStorage('activeAccountFollows', follows); - }); + account = res[0]; // update local storage writeStorage('activeAccount', res[0]); - // redirect - router.replace('/init'); + // fetch plebs, kind 0 = following + fetchPlebsByAccount(res[0].id, 0).then((res) => { + follows = pubkeyArray(res); + writeStorage('activeAccountFollows', res); + // fetch data + fetchData(account, follows); + }); } else { router.replace('/onboarding'); } }) .catch(console.error); - }, [fetchActiveAccount, fetchFollowsByAccount, router]); + + return () => { + if (unsubscribe.current) { + unsubscribe.current(); + } + }; + }, [fetchActiveAccount, fetchPlebsByAccount, totalNotes, fetchData, router]); return (
diff --git a/src/pages/init.tsx b/src/pages/init.tsx deleted file mode 100644 index 3710f497..00000000 --- a/src/pages/init.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import BaseLayout from '@layouts/base'; - -import { RelayContext } from '@components/relaysProvider'; - -import { dateToUnix, hoursAgo } from '@utils/getDate'; -import { getParentID, pubkeyArray } from '@utils/transform'; - -import LumeSymbol from '@assets/icons/Lume'; - -import { useLocalStorage } from '@rehooks/local-storage'; -import { invoke } from '@tauri-apps/api/tauri'; -import { useRouter } from 'next/router'; -import { - JSXElementConstructor, - ReactElement, - ReactFragment, - ReactPortal, - useCallback, - useContext, - useEffect, - useRef, - useState, -} from 'react'; - -export default function Page() { - const router = useRouter(); - const [pool, relays]: any = useContext(RelayContext); - - const now = useRef(new Date()); - const unsubscribe = useRef(null); - - const [eose, setEose] = useState(false); - - const [lastLogin] = useLocalStorage('lastLogin', ''); - const [activeAccount]: any = useLocalStorage('activeAccount', {}); - const [follows] = useLocalStorage('activeAccountFollows', []); - - const fetchData = useCallback( - async (since: Date) => { - const { createNote } = await import('@utils/bindings'); - - unsubscribe.current = pool.subscribe( - [ - { - kinds: [1], - authors: pubkeyArray(follows), - since: dateToUnix(since), - until: dateToUnix(now.current), - }, - ], - relays, - (event) => { - const parentID = getParentID(event.tags, event.id); - // insert event to local database - createNote({ - event_id: event.id, - pubkey: event.pubkey, - kind: event.kind, - tags: JSON.stringify(event.tags), - content: event.content, - parent_id: parentID, - parent_comment_id: '', - created_at: event.created_at, - account_id: activeAccount.id, - }).catch(console.error); - }, - undefined, - () => { - setEose(true); - } - ); - }, - [activeAccount.id, follows, pool, relays] - ); - - const isNoteExist = useCallback(async () => { - invoke('count_total_notes').then((res: number) => { - if (res > 0) { - const parseDate = new Date(lastLogin); - fetchData(parseDate); - } else { - fetchData(hoursAgo(24, now.current)); - } - }); - }, [fetchData, lastLogin]); - - useEffect(() => { - if (eose === false) { - isNoteExist(); - } else { - router.replace('/newsfeed/following'); - } - - return () => { - unsubscribe.current; - }; - }, [router, eose, isNoteExist]); - - return ( -
- {/* dragging area */} -
- {/* end dragging area */} -
-
- -
-

Loading...

-

- Keep calm as Lume fetches events... 🤙 -

-
-
-
- - - - -
-
-
- ); -} - -Page.getLayout = function getLayout( - page: - | string - | number - | boolean - | ReactElement> - | ReactFragment - | ReactPortal -) { - return {page}; -}; diff --git a/src/utils/commands.tsx b/src/utils/commands.tsx new file mode 100644 index 00000000..4fa858db --- /dev/null +++ b/src/utils/commands.tsx @@ -0,0 +1,13 @@ +import { invoke } from '@tauri-apps/api'; + +export function countTotalNotes() { + return invoke('count_total_notes'); +} + +export function countTotalChannels() { + return invoke('count_total_channels'); +} + +export function countTotalChats() { + return invoke('count_total_chats'); +} diff --git a/src/utils/metadata.tsx b/src/utils/metadata.tsx index d72aee2a..2257d6a4 100644 --- a/src/utils/metadata.tsx +++ b/src/utils/metadata.tsx @@ -12,35 +12,20 @@ export const fetchMetadata = async (pubkey: string) => { export const useMetadata = (pubkey) => { const [profile, setProfile] = useState(null); - /* - const insertPlebToDB = useCallback(async (account, pubkey, metadata) => { - const { createPleb } = await import('@utils/bindings'); - return await createPleb({ - pleb_id: pubkey + '-lume' + account.toString(), - pubkey: pubkey, - kind: 1, - metadata: metadata, - account_id: account, - }).catch(console.error); - }, []); - */ - const getCachedMetadata = useCallback(async () => { const { getPlebByPubkey } = await import('@utils/bindings'); getPlebByPubkey({ pubkey: pubkey }) .then((res) => { - if (res) { + if (res && res.metadata.length > 0) { const metadata = JSON.parse(res.metadata); // update state setProfile(metadata); } else { fetchMetadata(pubkey).then((res: any) => { - if (res.content) { + if (res) { const metadata = JSON.parse(res.content); // update state setProfile(metadata); - // save to database - // insertPlebToDB(activeAccount.id, pubkey, metadata); } }); }