diff --git a/.gitignore b/.gitignore index 4094a70b..7afdbe21 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,8 @@ out .next .vscode pnpm-lock.yaml +*.db +*.db-journal # Editor directories and files .vscode/* diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore index 9f4929f3..0c1d05c7 100644 --- a/src-tauri/.gitignore +++ b/src-tauri/.gitignore @@ -4,4 +4,3 @@ # prisma src/db.rs -lume.db diff --git a/src-tauri/prisma/migrations/20230403043730_initial/migration.sql b/src-tauri/prisma/migrations/20230403043730_initial/migration.sql new file mode 100644 index 00000000..e86009dc --- /dev/null +++ b/src-tauri/prisma/migrations/20230403043730_initial/migration.sql @@ -0,0 +1,73 @@ +-- CreateTable +CREATE TABLE "Account" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "pubkey" TEXT NOT NULL, + "privkey" TEXT NOT NULL, + "active" BOOLEAN NOT NULL DEFAULT false, + "metadata" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "Follow" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "pubkey" TEXT NOT NULL, + "kind" INTEGER NOT NULL, + "metadata" TEXT NOT NULL, + "accountId" INTEGER NOT NULL, + CONSTRAINT "Follow_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Note" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "eventId" TEXT NOT NULL, + "pubkey" TEXT NOT NULL, + "kind" INTEGER NOT NULL, + "tags" TEXT NOT NULL, + "content" TEXT NOT NULL, + "parent_id" TEXT NOT NULL, + "parent_comment_id" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "accountId" INTEGER NOT NULL, + CONSTRAINT "Note_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Message" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "pubkey" TEXT NOT NULL, + "content" TEXT NOT NULL, + "tags" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "accountId" INTEGER NOT NULL, + CONSTRAINT "Message_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Relay" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "url" TEXT NOT NULL, + "active" BOOLEAN NOT NULL DEFAULT true +); + +-- CreateTable +CREATE TABLE "Setting" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "key" TEXT NOT NULL, + "value" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "Account_privkey_key" ON "Account"("privkey"); + +-- CreateIndex +CREATE INDEX "Account_pubkey_idx" ON "Account"("pubkey"); + +-- CreateIndex +CREATE UNIQUE INDEX "Note_eventId_key" ON "Note"("eventId"); + +-- CreateIndex +CREATE INDEX "Note_eventId_idx" ON "Note"("eventId"); + +-- CreateIndex +CREATE INDEX "Message_pubkey_idx" ON "Message"("pubkey"); diff --git a/src-tauri/prisma/migrations/migration_lock.toml b/src-tauri/prisma/migrations/migration_lock.toml new file mode 100644 index 00000000..e5e5c470 --- /dev/null +++ b/src-tauri/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/src-tauri/prisma/schema.prisma b/src-tauri/prisma/schema.prisma index 3d8f7c78..44f8fae3 100644 --- a/src-tauri/prisma/schema.prisma +++ b/src-tauri/prisma/schema.prisma @@ -1,6 +1,6 @@ datasource db { provider = "sqlite" - url = "file:../lume.db" + url = "file:../../lume.db" } generator client { @@ -37,6 +37,7 @@ model Follow { model Note { id Int @id @default(autoincrement()) + eventId String @unique pubkey String kind Int tags String @@ -47,6 +48,8 @@ model Note { Account Account @relation(fields: [accountId], references: [id]) accountId Int + + @@index([eventId]) } model Message { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 3a431503..c6bde5af 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -7,6 +7,7 @@ #[macro_use] extern crate objc; +use prisma_client_rust::raw; use tauri::{Manager, WindowEvent}; #[cfg(target_os = "macos")] use window_ext::WindowExt; @@ -31,6 +32,11 @@ struct CreateAccountData { metadata: String, } +#[derive(Deserialize, Type)] +struct GetFollowData { + account_id: i32, +} + #[derive(Deserialize, Type)] struct CreateFollowData { pubkey: String, @@ -39,6 +45,18 @@ struct CreateFollowData { account_id: i32, } +#[derive(Deserialize, Type)] +struct CreateNoteData { + event_id: String, + pubkey: String, + kind: i32, + tags: String, + content: String, + parent_id: String, + parent_comment_id: String, + account_id: i32, +} + #[tauri::command] #[specta::specta] async fn get_account(db: DbState<'_>) -> Result, ()> { @@ -59,6 +77,16 @@ async fn create_account(db: DbState<'_>, data: CreateAccountData) -> Result, data: GetFollowData) -> Result, ()> { + db.follow() + .find_many(vec![follow::account_id::equals(data.account_id)]) + .exec() + .await + .map_err(|_| ()) +} + #[tauri::command] #[specta::specta] async fn create_follow(db: DbState<'_>, data: CreateFollowData) -> Result { @@ -75,13 +103,68 @@ async fn create_follow(db: DbState<'_>, data: CreateFollowData) -> Result, data: CreateNoteData) -> Result { + let event_id = data.event_id.clone(); + let content = data.content.clone(); + + db.note() + .upsert( + note::event_id::equals(event_id), + note::create( + data.event_id, + data.pubkey, + data.kind, + data.tags, + data.content, + data.parent_id, + data.parent_comment_id, + account::id::equals(data.account_id), + vec![], + ), + vec![note::content::set(content)], + ) + .exec() + .await + .map_err(|_| ()) +} + +#[tauri::command] +#[specta::specta] +async fn get_notes(db: DbState<'_>) -> Result, ()> { + db._query_raw(raw!("SELECT * FROM Note")) + .exec() + .await + .map_err(|_| ()) +} + +#[tauri::command] +#[specta::specta] +async fn check_note(db: DbState<'_>) -> Result, ()> { + db.note() + .find_many(vec![]) + .take(5) + .exec() + .await + .map_err(|_| ()) +} + #[tokio::main] async fn main() { let db = PrismaClient::_builder().build().await.unwrap(); #[cfg(debug_assertions)] ts::export( - collect_types![get_account, create_account, create_follow], + collect_types![ + get_account, + create_account, + get_follows, + create_follow, + create_note, + get_notes, + check_note + ], "../src/utils/bindings.ts", ) .unwrap(); @@ -115,7 +198,11 @@ async fn main() { .invoke_handler(tauri::generate_handler![ get_account, create_account, - create_follow + get_follows, + create_follow, + create_note, + get_notes, + check_note ]) .manage(Arc::new(db)) .run(tauri::generate_context!()) diff --git a/src/components/note/connector.tsx b/src/components/note/connector.tsx index 12ef9ac7..49852348 100644 --- a/src/components/note/connector.tsx +++ b/src/components/note/connector.tsx @@ -1,10 +1,10 @@ import { RelayContext } from '@components/relaysProvider'; -import { activeAccountAtom } from '@stores/account'; +import { activeAccountAtom, lastLoginAtom } from '@stores/account'; import { hasNewerNoteAtom } from '@stores/note'; import { dateToUnix } from '@utils/getDate'; -import { createCacheNote, getAllFollowsByID, updateLastLoginTime } from '@utils/storage'; +import { createCacheNote, getAllFollowsByID } from '@utils/storage'; import { pubkeyArray } from '@utils/transform'; import { TauriEvent } from '@tauri-apps/api/event'; @@ -15,6 +15,7 @@ import { useCallback, useContext, useEffect, useRef, useState } from 'react'; export default function NoteConnector() { const [pool, relays]: any = useContext(RelayContext); + const setLastLoginAtom = useSetAtom(lastLoginAtom); const setHasNewerNote = useSetAtom(hasNewerNoteAtom); const activeAccount: any = useAtomValue(activeAccountAtom); @@ -44,7 +45,7 @@ export default function NoteConnector() { useEffect(() => { subscribe(); getCurrent().listen(TauriEvent.WINDOW_CLOSE_REQUESTED, () => { - updateLastLoginTime(now.current); + setLastLoginAtom(now.current); appWindow.close(); }); }, [activeAccount.id, pool, relays, setHasNewerNote, subscribe]); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index c9581d5a..4b0af815 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,6 +1,6 @@ import BaseLayout from '@layouts/base'; -import { activeAccountAtom } from '@stores/account'; +import { activeAccountAtom, activeAccountFollowsAtom } from '@stores/account'; import LumeSymbol from '@assets/icons/Lume'; @@ -11,16 +11,26 @@ import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useCal export default function Page() { const router = useRouter(); const setActiveAccount = useSetAtom(activeAccountAtom); + const setActiveAccountFollows = useSetAtom(activeAccountFollowsAtom); const fetchActiveAccount = useCallback(async () => { const { getAccount } = await import('@utils/bindings'); return await getAccount(); }, []); + const fetchFollowsByAccount = useCallback(async (id) => { + const { getFollows } = await import('@utils/bindings'); + return await getFollows({ account_id: id }); + }, []); + useEffect(() => { fetchActiveAccount() .then((res: any) => { if (res.length > 0) { + // fetch follows + fetchFollowsByAccount(res[0].id).then((follows) => { + setActiveAccountFollows(follows); + }); // update local storage setActiveAccount(res[0]); // redirect @@ -30,7 +40,7 @@ export default function Page() { } }) .catch(console.error); - }, [fetchActiveAccount, setActiveAccount, router]); + }, [fetchActiveAccount, setActiveAccount, fetchFollowsByAccount, setActiveAccountFollows, router]); return (
diff --git a/src/pages/init.tsx b/src/pages/init.tsx index 562318e4..4ab32b69 100644 --- a/src/pages/init.tsx +++ b/src/pages/init.tsx @@ -2,12 +2,10 @@ import BaseLayout from '@layouts/base'; import { RelayContext } from '@components/relaysProvider'; -import { activeAccountAtom } from '@stores/account'; -import { relaysAtom } from '@stores/relays'; +import { activeAccountAtom, activeAccountFollowsAtom, lastLoginAtom } from '@stores/account'; import { dateToUnix, hoursAgo } from '@utils/getDate'; -import { countTotalNotes, createCacheNote, getAllFollowsByID, getLastLoginTime } from '@utils/storage'; -import { pubkeyArray } from '@utils/transform'; +import { getParentID, pubkeyArray } from '@utils/transform'; import LumeSymbol from '@assets/icons/Lume'; @@ -30,64 +28,75 @@ export default function Page() { const [pool, relays]: any = useContext(RelayContext); const activeAccount: any = useAtomValue(activeAccountAtom); - const [done, setDone] = useState(false); + const activeAccountFollows: any = useAtomValue(activeAccountFollowsAtom); + const lastLogin: any = useAtomValue(lastLoginAtom); const now = useRef(new Date()); const unsubscribe = useRef(null); - const timer = useRef(null); + + const [eose, setEose] = useState(false); const fetchData = useCallback( - (since) => { - getAllFollowsByID(activeAccount.id).then((follows) => { - unsubscribe.current = pool.subscribe( - [ - { - kinds: [1], - authors: pubkeyArray(follows), - since: dateToUnix(since), - until: dateToUnix(now.current), - }, - ], - relays, - (event) => { - // insert event to local database - createCacheNote(event); - }, - undefined, - () => { - // wait for 8 seconds - timer.current = setTimeout(() => setDone(true), 8000); - }, + async (since) => { + const { createNote } = await import('@utils/bindings'); + unsubscribe.current = pool.subscribe( + [ { - unsubscribeOnEose: true, - } - ); - }); + kinds: [1], + authors: pubkeyArray(activeAccountFollows), + 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: 'aaa', + account_id: activeAccount.id, + }).catch(console.error); + }, + undefined, + () => { + setEose(true); + } + ); }, - [activeAccount.id, pool, relays] + [activeAccount.id, activeAccountFollows, pool, relays] ); - useEffect(() => { - if (!done) { - countTotalNotes().then((count) => { - if (count.total === 0) { - fetchData(hoursAgo(24, now.current)); + const isNoteExist = useCallback(async () => { + const { checkNote } = await import('@utils/bindings'); + checkNote() + .then((res) => { + if (res.length === 5) { + const parseDate = new Date(lastLogin); + fetchData(parseDate); } else { - getLastLoginTime().then((time) => { - const parseDate = new Date(time.setting_value); - fetchData(parseDate); - }); + fetchData(hoursAgo(24, now.current)); } - }); + }) + .catch(console.error); + }, [fetchData, lastLogin]); + + useEffect(() => { + if (eose === false) { + isNoteExist(); } else { router.replace('/newsfeed/following'); } return () => { unsubscribe.current; - clearTimeout(timer.current); }; - }, [activeAccount.id, done, pool, relays, router, fetchData]); + }, [router, eose, isNoteExist]); return (
diff --git a/src/pages/onboarding/create/index.tsx b/src/pages/onboarding/create/index.tsx index 9e9d7880..b445e068 100644 --- a/src/pages/onboarding/create/index.tsx +++ b/src/pages/onboarding/create/index.tsx @@ -41,7 +41,7 @@ export default function Page() { }; // auto-generated profile metadata - const metadata = useMemo( + const metadata: any = useMemo( () => ({ display_name: name, name: name, @@ -77,7 +77,7 @@ export default function Page() { event.sig = signEvent(event, privKey); // insert to database then broadcast - createAccount({ pubkey: pubKey, privkey: privKey, metadata: JSON.stringify(metadata) }) + createAccount({ pubkey: pubKey, privkey: privKey, metadata: metadata }) .then((res) => { pool.publish(event, relays); router.push({ diff --git a/src/pages/onboarding/create/step-2.tsx b/src/pages/onboarding/create/step-2.tsx index 4831a6e4..63b5e7c8 100644 --- a/src/pages/onboarding/create/step-2.tsx +++ b/src/pages/onboarding/create/step-2.tsx @@ -85,7 +85,9 @@ export default function Page() { for (const follow of follows) { const metadata: any = await fetchMetadata(follow, pool, relays); - createFollow({ pubkey: follow, kind: 0, metadata: metadata.content, account_id: parseInt(id) }); + createFollow({ pubkey: follow, kind: 0, metadata: metadata.content, account_id: parseInt(id) }).catch( + console.error + ); } // build event diff --git a/src/pages/onboarding/login/step-2.tsx b/src/pages/onboarding/login/step-2.tsx index fee12df2..c9d16c7b 100644 --- a/src/pages/onboarding/login/step-2.tsx +++ b/src/pages/onboarding/login/step-2.tsx @@ -2,6 +2,8 @@ import BaseLayout from '@layouts/base'; import { RelayContext } from '@components/relaysProvider'; +import { DEFAULT_AVATAR } from '@stores/constants'; + import { fetchMetadata } from '@utils/metadata'; import { truncate } from '@utils/truncate'; @@ -27,29 +29,34 @@ export default function Page() { const privkey: any = router.query.privkey || null; const pubkey = privkey ? getPublicKey(privkey) : null; - const [profile, setProfile] = useState(null); + const [profile, setProfile] = useState({ id: null, metadata: null }); const [done, setDone] = useState(false); - const accountId = useRef(null); - const insertAccountToStorage = useCallback(async (pubkey, privkey, metadata) => { const { createAccount } = await import('@utils/bindings'); - createAccount({ pubkey: pubkey, privkey: privkey, metadata: JSON.stringify(metadata) }).then( - (res) => (accountId.current = res.id) - ); + createAccount({ pubkey: pubkey, privkey: privkey, metadata: metadata }) + .then((res) => + setProfile({ + id: res.id, + metadata: JSON.parse(res.metadata), + }) + ) + .catch(console.error); }, []); const insertFollowsToStorage = useCallback( async (tags) => { const { createFollow } = await import('@utils/bindings'); - if (accountId.current !== null) { + if (profile?.id !== null) { for (const tag of tags) { const metadata: any = await fetchMetadata(tag[1], pool, relays); - createFollow({ pubkey: tag[1], kind: 0, metadata: metadata.content, account_id: accountId.current }); + createFollow({ pubkey: tag[1], kind: 0, metadata: metadata.content, account_id: profile.id }).catch( + console.error + ); } } }, - [pool, relays] + [pool, profile.id, relays] ); useEffect(() => { @@ -64,7 +71,6 @@ export default function Page() { relays, (event: any) => { if (event.kind === 0) { - setProfile(JSON.parse(event.content)); insertAccountToStorage(pubkey, privkey, event.content); } else { if (event.tags.length > 0) { @@ -75,9 +81,6 @@ export default function Page() { undefined, () => { setDone(true); - }, - { - unsubscribeOnEose: true, } ); @@ -106,13 +109,20 @@ export default function Page() {
- +
-

{profile?.display_name || profile?.name}

+

{profile.metadata?.display_name || profile.metadata?.name}

ยท -

@{profile?.username || (pubkey && truncate(pubkey, 16, ' .... '))}

+

+ @{profile.metadata?.username || (pubkey && truncate(pubkey, 16, ' .... '))} +

diff --git a/src/stores/account.tsx b/src/stores/account.tsx index f5568ba4..7c70b99e 100644 --- a/src/stores/account.tsx +++ b/src/stores/account.tsx @@ -10,3 +10,5 @@ const createMyJsonStorage = () => { }; export const activeAccountAtom = atomWithStorage('activeAccount', {}, createMyJsonStorage()); +export const activeAccountFollowsAtom = atomWithStorage('activeAccountFollows', [], createMyJsonStorage()); +export const lastLoginAtom = atomWithStorage('lastLoginAtom', [], createMyJsonStorage()); diff --git a/src/utils/bindings.ts b/src/utils/bindings.ts index 28b56dab..b483e75c 100644 --- a/src/utils/bindings.ts +++ b/src/utils/bindings.ts @@ -16,11 +16,50 @@ export function createAccount(data: CreateAccountData) { return invoke('create_account', { data }); } +export function getFollows(data: GetFollowData) { + return invoke('get_follows', { data }); +} + export function createFollow(data: CreateFollowData) { return invoke('create_follow', { data }); } -export type Account = { id: number; pubkey: string; privkey: string; active: boolean; metadata: string }; -export type Follow = { id: number; pubkey: string; kind: number; metadata: string; accountId: number }; +export function createNote(data: CreateNoteData) { + return invoke('create_note', { data }); +} + +export function getNotes() { + return invoke('get_notes'); +} + +export function checkNote() { + return invoke('check_note'); +} + +export type GetFollowData = { account_id: number }; +export type Note = { + id: number; + eventId: string; + pubkey: string; + kind: number; + tags: string; + content: string; + parent_id: string; + parent_comment_id: string; + createdAt: string; + accountId: number; +}; export type CreateFollowData = { pubkey: string; kind: number; metadata: string; account_id: number }; +export type Account = { id: number; pubkey: string; privkey: string; active: boolean; metadata: string }; +export type CreateNoteData = { + event_id: string; + pubkey: string; + kind: number; + tags: string; + content: string; + parent_id: string; + parent_comment_id: string; + account_id: number; +}; export type CreateAccountData = { pubkey: string; privkey: string; metadata: string }; +export type Follow = { id: number; pubkey: string; kind: number; metadata: string; accountId: number };