diff --git a/src-tauri/prisma/migrations/20230410071123_add_chat/migration.sql b/src-tauri/prisma/migrations/20230410071123_add_chat/migration.sql new file mode 100644 index 00000000..83fd87fb --- /dev/null +++ b/src-tauri/prisma/migrations/20230410071123_add_chat/migration.sql @@ -0,0 +1,12 @@ +-- CreateTable +CREATE TABLE "Chat" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "pubkey" TEXT NOT NULL, + "createdAt" INTEGER NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "Chat_pubkey_key" ON "Chat"("pubkey"); + +-- CreateIndex +CREATE INDEX "Chat_pubkey_idx" ON "Chat"("pubkey"); diff --git a/src-tauri/prisma/migrations/20230410071606_add_account_related_to_chat_and_channel/migration.sql b/src-tauri/prisma/migrations/20230410071606_add_account_related_to_chat_and_channel/migration.sql new file mode 100644 index 00000000..cf191a1b --- /dev/null +++ b/src-tauri/prisma/migrations/20230410071606_add_account_related_to_chat_and_channel/migration.sql @@ -0,0 +1,35 @@ +/* + Warnings: + + - Added the required column `accountId` to the `Chat` table without a default value. This is not possible if the table is not empty. + - Added the required column `accountId` to the `Channel` table without a default value. This is not possible if the table is not empty. + +*/ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Chat" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "pubkey" TEXT NOT NULL, + "createdAt" INTEGER NOT NULL, + "accountId" INTEGER NOT NULL, + CONSTRAINT "Chat_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_Chat" ("createdAt", "id", "pubkey") SELECT "createdAt", "id", "pubkey" FROM "Chat"; +DROP TABLE "Chat"; +ALTER TABLE "new_Chat" RENAME TO "Chat"; +CREATE UNIQUE INDEX "Chat_pubkey_key" ON "Chat"("pubkey"); +CREATE INDEX "Chat_pubkey_idx" ON "Chat"("pubkey"); +CREATE TABLE "new_Channel" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "eventId" TEXT NOT NULL, + "content" TEXT NOT NULL, + "accountId" INTEGER NOT NULL, + CONSTRAINT "Channel_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_Channel" ("content", "eventId", "id") SELECT "content", "eventId", "id" FROM "Channel"; +DROP TABLE "Channel"; +ALTER TABLE "new_Channel" RENAME TO "Channel"; +CREATE UNIQUE INDEX "Channel_eventId_key" ON "Channel"("eventId"); +CREATE INDEX "Channel_eventId_idx" ON "Channel"("eventId"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/src-tauri/prisma/schema.prisma b/src-tauri/prisma/schema.prisma index 99456c2e..c098b9d9 100644 --- a/src-tauri/prisma/schema.prisma +++ b/src-tauri/prisma/schema.prisma @@ -21,6 +21,8 @@ model Account { plebs Pleb[] messages Message[] notes Note[] + chats Chat[] + channels Channel[] @@index([pubkey]) } @@ -66,11 +68,25 @@ model Message { @@index([pubkey, createdAt]) } +model Chat { + id Int @id @default(autoincrement()) + pubkey String @unique + createdAt Int + + Account Account @relation(fields: [accountId], references: [id]) + accountId Int + + @@index([pubkey]) +} + model Channel { id Int @id @default(autoincrement()) eventId String @unique content String + Account Account @relation(fields: [accountId], references: [id]) + accountId Int + @@index([eventId]) } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index bc017558..29e2f461 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -81,10 +81,23 @@ struct GetLatestNoteData { date: i32, } +#[derive(Deserialize, Type)] +struct CreateChatData { + pubkey: String, + created_at: i32, + account_id: i32, +} + +#[derive(Deserialize, Type)] +struct GetChatData { + account_id: i32, +} + #[derive(Deserialize, Type)] struct CreateChannelData { event_id: String, content: String, + account_id: i32, } #[tauri::command] @@ -225,7 +238,45 @@ async fn count_total_notes(db: DbState<'_>) -> Result { #[specta::specta] async fn create_channel(db: DbState<'_>, data: CreateChannelData) -> Result { db.channel() - .create(data.event_id, data.content, vec![]) + .upsert( + channel::event_id::equals(data.event_id.clone()), + channel::create( + data.event_id, + data.content, + account::id::equals(data.account_id), + vec![], + ), + vec![], + ) + .exec() + .await + .map_err(|_| ()) +} + +#[tauri::command] +#[specta::specta] +async fn create_chat(db: DbState<'_>, data: CreateChatData) -> Result { + db.chat() + .upsert( + chat::pubkey::equals(data.pubkey.clone()), + chat::create( + data.pubkey, + data.created_at, + account::id::equals(data.account_id), + vec![], + ), + vec![], + ) + .exec() + .await + .map_err(|_| ()) +} + +#[tauri::command] +#[specta::specta] +async fn get_chats(db: DbState<'_>, data: GetChatData) -> Result, ()> { + db.chat() + .find_many(vec![chat::account_id::equals(data.account_id)]) .exec() .await .map_err(|_| ()) @@ -247,7 +298,9 @@ async fn main() { get_notes, get_latest_notes, get_note_by_id, - create_channel + create_channel, + create_chat, + get_chats ], "../src/utils/bindings.ts", ) @@ -290,7 +343,9 @@ async fn main() { get_latest_notes, get_note_by_id, count_total_notes, - create_channel + create_channel, + create_chat, + get_chats ]) .manage(Arc::new(db)) .run(tauri::generate_context!()) diff --git a/src/components/appHeader/index.tsx b/src/components/appHeader/index.tsx index b4e3df47..1a8c0314 100644 --- a/src/components/appHeader/index.tsx +++ b/src/components/appHeader/index.tsx @@ -4,7 +4,7 @@ const AppActions = dynamic(() => import('@components/appHeader/actions'), { ssr: false, }); -const NoteConnector = dynamic(() => import('@components/note/connector'), { +const EventCollector = dynamic(() => import('@components/eventCollector'), { ssr: false, }); @@ -15,7 +15,7 @@ export default function AppHeader() {
- +
diff --git a/src/components/chats/chatList.tsx b/src/components/chats/chatList.tsx index 2a929c1f..b9dac892 100644 --- a/src/components/chats/chatList.tsx +++ b/src/components/chats/chatList.tsx @@ -1,23 +1,21 @@ import { ChatListItem } from '@components/chats/chatListItem'; import { ChatModal } from '@components/chats/chatModal'; import { ImageWithFallback } from '@components/imageWithFallback'; -import { RelayContext } from '@components/relaysProvider'; import { activeAccountAtom } from '@stores/account'; import { DEFAULT_AVATAR } from '@stores/constants'; import { useAtomValue } from 'jotai'; import { useRouter } from 'next/router'; -import { useContext, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; export default function ChatList() { - const [pool, relays]: any = useContext(RelayContext); const router = useRouter(); const activeAccount: any = useAtomValue(activeAccountAtom); const accountProfile = JSON.parse(activeAccount.metadata); - const [list, setList] = useState(new Set()); + const [list, setList] = useState([]); const openSelfChat = () => { router.push({ @@ -27,26 +25,15 @@ export default function ChatList() { }; useEffect(() => { - const unsubscribe = pool.subscribe( - [ - { - kinds: [4], - '#p': [activeAccount.pubkey], - since: 0, - }, - ], - relays, - (event: any) => { - if (event.pubkey !== activeAccount.pubkey) { - setList((list) => new Set(list).add(event.pubkey)); - } - } - ); - - return () => { - unsubscribe; + const fetchChats = async () => { + const { getChats } = await import('@utils/bindings'); + return await getChats({ account_id: activeAccount.id }); }; - }, [pool, relays, activeAccount.pubkey]); + + fetchChats() + .then((res) => setList(res)) + .catch(console.error); + }, [activeAccount.id]); return (
@@ -68,8 +55,8 @@ export default function ChatList() {
- {[...list].map((item: string, index) => ( - + {list.map((item) => ( + ))} diff --git a/src/components/note/connector.tsx b/src/components/eventCollector.tsx similarity index 60% rename from src/components/note/connector.tsx rename to src/components/eventCollector.tsx index 36b140f3..4ac7971e 100644 --- a/src/components/note/connector.tsx +++ b/src/components/eventCollector.tsx @@ -11,7 +11,7 @@ import { appWindow, getCurrent } from '@tauri-apps/api/window'; import { useSetAtom } from 'jotai'; import { useCallback, useContext, useEffect, useRef, useState } from 'react'; -export default function NoteConnector() { +export default function EventCollector() { const [pool, relays]: any = useContext(RelayContext); const setLastLoginAtom = useSetAtom(lastLoginAtom); @@ -24,6 +24,9 @@ export default function NoteConnector() { const subscribe = useCallback(async () => { const { createNote } = await import('@utils/bindings'); + const { createChat } = await import('@utils/bindings'); + const { createChannel } = await import('@utils/bindings'); + const activeAccount = JSON.parse(localStorage.getItem('activeAccount')); const follows = JSON.parse(localStorage.getItem('activeAccountFollows')); @@ -34,29 +37,47 @@ export default function NoteConnector() { authors: pubkeyArray(follows), since: dateToUnix(now.current), }, + { + kinds: [4], + '#p': [activeAccount.pubkey], + since: 0, + }, + { + kinds: [40], + since: 0, + }, ], 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, - }) - .then(() => - // notify user reload to get newer note - setHasNewerNote(true) - ) - .catch(console.error); - }, - 10000 + 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: activeAccount.id, + }) + .then(() => + // notify user reload to get newer note + setHasNewerNote(true) + ) + .catch(console.error); + } else if (event.kind === 4) { + if (event.pubkey !== activeAccount.pubkey) { + createChat({ pubkey: event.pubkey, created_at: event.created_at, account_id: activeAccount.id }); + } + } else if (event.kind === 40) { + createChannel({ event_id: event.id, content: event.content, account_id: activeAccount.id }); + } else { + console.error; + } + } ); }, [pool, relays, setHasNewerNote]); diff --git a/src/pages/channels/index.tsx b/src/pages/channels/index.tsx index f37272fc..263a918c 100644 --- a/src/pages/channels/index.tsx +++ b/src/pages/channels/index.tsx @@ -1,10 +1,48 @@ import BaseLayout from '@layouts/base'; import WithSidebarLayout from '@layouts/withSidebar'; -import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal } from 'react'; +import { RelayContext } from '@components/relaysProvider'; + +import { + JSXElementConstructor, + ReactElement, + ReactFragment, + ReactPortal, + useContext, + useEffect, + useState, +} from 'react'; export default function Page() { - return <>; + const [pool, relays]: any = useContext(RelayContext); + const [list, setList] = useState([]); + + useEffect(() => { + const unsubscribe = pool.subscribe( + [ + { + kinds: [40], + since: 0, + }, + ], + relays, + (event: any) => { + setList((list) => [event, ...list]); + } + ); + + return () => { + unsubscribe; + }; + }, [pool, relays]); + + return ( +
+ {list.map((channel) => ( +
{channel.content}
+ ))} +
+ ); } Page.getLayout = function getLayout( diff --git a/src/utils/bindings.ts b/src/utils/bindings.ts index 2d20b1e2..2cf44154 100644 --- a/src/utils/bindings.ts +++ b/src/utils/bindings.ts @@ -48,6 +48,14 @@ export function createChannel(data: CreateChannelData) { return invoke('create_channel', { data }); } +export function createChat(data: CreateChatData) { + return invoke('create_chat', { data }); +} + +export function getChats(data: GetChatData) { + return invoke('get_chats', { data }); +} + export type CreateNoteData = { event_id: string; pubkey: string; @@ -59,10 +67,7 @@ export type CreateNoteData = { created_at: number; account_id: number; }; -export type CreateChannelData = { event_id: string; content: string }; export type CreatePlebData = { pleb_id: string; pubkey: string; kind: number; metadata: string; account_id: number }; -export type GetNoteByIdData = { event_id: string }; -export type Pleb = { id: number; plebId: string; pubkey: string; kind: number; metadata: string; accountId: number }; export type Note = { id: number; eventId: string; @@ -75,10 +80,16 @@ export type Note = { createdAt: number; accountId: number; }; +export type CreateChatData = { pubkey: string; created_at: number; account_id: number }; +export type GetNoteByIdData = { event_id: string }; +export type Chat = { id: number; pubkey: string; createdAt: number; accountId: number }; export type Account = { id: number; pubkey: string; privkey: string; active: boolean; metadata: string }; -export type Channel = { id: number; eventId: string; content: string }; +export type GetChatData = { account_id: number }; +export type CreateChannelData = { event_id: string; content: string; account_id: number }; export type GetPlebPubkeyData = { pubkey: string }; +export type Channel = { id: number; eventId: string; content: string; accountId: number }; export type GetPlebData = { account_id: number }; export type CreateAccountData = { pubkey: string; privkey: string; metadata: string }; export type GetLatestNoteData = { date: number }; +export type Pleb = { id: number; plebId: string; pubkey: string; kind: number; metadata: string; accountId: number }; export type GetNoteData = { date: number; limit: number; offset: number };