From 9dba26fc817bb5a525be65f8a327ff08b2e01b3b Mon Sep 17 00:00:00 2001 From: Vali98 Date: Mon, 23 Sep 2024 00:16:51 +0800 Subject: [PATCH] feat: implemented chat drawer --- app/components/ChatMenu/ChatMenu.tsx | 9 +- app/components/ChatMenu/ChatsDrawer.tsx | 211 ++++++++++++++++++------ app/components/ChatMenu/OptionsMenu.tsx | 5 +- app/constants/Chat.ts | 33 +++- 4 files changed, 193 insertions(+), 65 deletions(-) diff --git a/app/components/ChatMenu/ChatMenu.tsx b/app/components/ChatMenu/ChatMenu.tsx index 28d4e67..44ca847 100644 --- a/app/components/ChatMenu/ChatMenu.tsx +++ b/app/components/ChatMenu/ChatMenu.tsx @@ -164,19 +164,18 @@ const ChatMenu = () => { }} /> - {!chat ? ( - - ) : ( + {!chat && } + {chat && ( - + )} - + {showChats && } diff --git a/app/components/ChatMenu/ChatsDrawer.tsx b/app/components/ChatMenu/ChatsDrawer.tsx index 97bf1f0..8215b9b 100644 --- a/app/components/ChatMenu/ChatsDrawer.tsx +++ b/app/components/ChatMenu/ChatsDrawer.tsx @@ -1,12 +1,15 @@ -import { Style } from '@globals' -import { SetStateAction, useEffect } from 'react' +import { Ionicons, AntDesign } from '@expo/vector-icons' +import { Characters, Chats, Style } from '@globals' +import { useLiveQuery } from 'drizzle-orm/expo-sqlite' +import { SetStateAction } from 'react' import { Text, GestureResponderEvent, TouchableOpacity, StyleSheet, View, - BackHandler, + FlatList, + Alert, } from 'react-native' import Animated, { Easing, @@ -20,33 +23,126 @@ type ChatsDrawerProps = { booleans: [boolean, (b: boolean | SetStateAction) => void] } +type ListItem = { + id: number + character_id: number + create_date: Date + name: string + last_modified: null | number + entryCount: number +} + const ChatsDrawer: React.FC = ({ booleans: [showModal, setShowModal] }) => { + const { charId } = Characters.useCharacterCard((state) => ({ charId: state.id })) + + const { data } = useLiveQuery(Chats.db.query.chatListQuery(charId ?? 0)) + + const { deleteChat, loadChat, currentChatId } = Chats.useChat((state) => ({ + deleteChat: state.delete, + loadChat: state.load, + currentChatId: state.data?.id, + })) + const handleOverlayClick = (e: GestureResponderEvent) => { if (e.target === e.currentTarget) setShowModal(false) } - if (showModal) + const handleLoadChat = async (chatId: number) => { + await loadChat(chatId) + setShowModal(false) + } + + const handleCreateChat = async () => { + if (charId) + Chats.db.mutate.createChat(charId).then((chatId) => { + if (chatId) handleLoadChat(chatId) + }) + } + + const handleDeleteChat = (item: ListItem) => { + Alert.alert( + `Delete Chat`, + `Are you sure you want to delete this chat file: '${item.name}'?`, + [ + { + text: 'Cancel', + onPress: () => {}, + style: 'cancel', + }, + { + text: 'Confirm', + onPress: async () => { + await deleteChat(item.id) + if (charId && currentChatId === item.id) { + const returnedChatId = await Chats.db.query.chatNewest(charId) + const chatId = returnedChatId + ? returnedChatId + : await Chats.db.mutate.createChat(charId) + chatId && (await loadChat(chatId)) + } + }, + style: 'destructive', + }, + ] + ) + } + + const renderChat = (item: ListItem, index: number) => { + const date = new Date(item.last_modified ?? 0) return ( - - - - - - - Chat History - + + handleLoadChat(item.id)}> + {item.name} + + + {item.entryCount} + {date.toLocaleDateString()} + {date.toLocaleTimeString()} + + + handleDeleteChat(item)}> + + ) + } + + return ( + + + + + + + Chats + item.id.toString()} + renderItem={({ item, index }) => renderChat(item, index)} + /> + + New Chat + + + + ) } export default ChatsDrawer @@ -75,55 +171,60 @@ const styles = StyleSheet.create({ elevation: 20, position: 'absolute', height: '100%', - padding: 32, + paddingHorizontal: 16, + paddingTop: 16, + paddingBottom: 32, }, - userContainer: { - flexDirection: 'row', - paddingBottom: 24, - paddingTop: 40, - padding: 16, + drawerTitle: { + color: Style.getColor('primary-text2'), + fontSize: 18, + paddingLeft: 16, }, - buttonContainer: { - flexDirection: 'row', - marginLeft: 12, + title: { + color: Style.getColor('primary-text1'), + fontSize: 16, }, - button: { - borderColor: Style.getColor('primary-surface3'), - borderWidth: 2, - marginRight: 10, - borderRadius: 4, - padding: 8, + listContainer: { + flex: 1, + marginTop: 16, + marginBottom: 8, + borderRadius: 8, }, - userName: { - fontSize: 20, - marginTop: 4, + chatItem: { + paddingHorizontal: 8, + flexDirection: 'row', + flex: 1, marginBottom: 8, - marginLeft: 12, - color: Style.getColor('primary-text1'), + borderRadius: 8, + borderWidth: 2, + borderColor: Style.getColor('primary-surface1'), }, - userImage: { - width: 80, - height: 80, - borderRadius: 20, - borderColor: Style.getColor('primary-brand'), + chatItemActive: { + paddingHorizontal: 8, + flexDirection: 'row', + flex: 1, + marginBottom: 8, + borderRadius: 8, borderWidth: 2, + borderColor: Style.getColor('primary-brand'), }, + smallText: { color: Style.getColor('primary-text2'), marginLeft: 12 }, + smallTextChat: { color: Style.getColor('primary-text2'), marginLeft: 4 }, - largeButtonText: { - fontSize: 18, - paddingVertical: 12, - paddingLeft: 15, - color: Style.getColor('primary-text1'), + editButton: { + paddingHorizontal: 8, + justifyContent: 'center', }, - largeButton: { - paddingLeft: 15, - flexDirection: 'row', + newButton: { + backgroundColor: Style.getColor('primary-brand'), alignItems: 'center', + paddingVertical: 8, + borderRadius: 12, }, }) diff --git a/app/components/ChatMenu/OptionsMenu.tsx b/app/components/ChatMenu/OptionsMenu.tsx index ffafc1f..34ec15c 100644 --- a/app/components/ChatMenu/OptionsMenu.tsx +++ b/app/components/ChatMenu/OptionsMenu.tsx @@ -21,9 +21,10 @@ type MenuData = { type OptionsMenuProps = { menuRef: React.MutableRefObject + showChats: (b: boolean) => void } -const OptionsMenu: React.FC = ({ menuRef }) => { +const OptionsMenu: React.FC = ({ menuRef, showChats }) => { const router = useRouter() const { unloadCharacter } = Characters.useCharacterCard((state) => ({ unloadCharacter: state.unloadCard, @@ -49,7 +50,7 @@ const OptionsMenu: React.FC = ({ menuRef }) => { }, { callback: () => { - router.push('/ChatSelector') + showChats(true) }, text: 'Chat History', button: 'paperclip', diff --git a/app/constants/Chat.ts b/app/constants/Chat.ts index faf721d..876d2a1 100644 --- a/app/constants/Chat.ts +++ b/app/constants/Chat.ts @@ -1,6 +1,6 @@ import { db as database } from '@db' import { chatEntries, chatSwipes, chats } from 'db/schema' -import { eq } from 'drizzle-orm' +import { count, desc, eq, getTableColumns } from 'drizzle-orm' import { create } from 'zustand' import { API } from './API' @@ -37,8 +37,9 @@ export type ChatEntry = { export type ChatData = { id: number - createDate: Date + create_date: Date character_id: number + name: string messages: ChatEntry[] | undefined } @@ -317,13 +318,39 @@ export namespace Chats { return chatIds?.[chatIds?.length - 1]?.id } - export const chatList = async (charId: number) => { + export const chatListOld = async (charId: number) => { const chatIds = await database.query.chats.findMany({ where: eq(chats.character_id, charId), }) return chatIds } + export const chatList = async (charId: number) => { + const result = await database + .select({ + ...getTableColumns(chats), + entryCount: count(chatEntries.id), + }) + .from(chats) + .leftJoin(chatEntries, eq(chats.id, chatEntries.chat_id)) + .groupBy(chats.id) + .where(eq(chats.character_id, charId)) + return result + } + + export const chatListQuery = (charId: number) => { + return database + .select({ + ...getTableColumns(chats), + entryCount: count(chatEntries.id), + }) + .from(chats) + .leftJoin(chatEntries, eq(chats.id, chatEntries.chat_id)) + .groupBy(chats.id) + .where(eq(chats.character_id, charId)) + .orderBy(desc(chats.last_modified)) + } + export const chatExists = async (chatId: number) => { return await database.query.chats.findFirst({ where: eq(chats.id, chatId) }) }