diff --git a/app/CharInfo.tsx b/app/CharInfo.tsx index 9575992..2f83e9f 100644 --- a/app/CharInfo.tsx +++ b/app/CharInfo.tsx @@ -1,12 +1,12 @@ import AnimatedView from '@components/AnimatedView' import { FontAwesome } from '@expo/vector-icons' -import { Characters, Logger, Style } from '@globals' +import { Characters, Chats, Logger, Style } from '@globals' import { CharacterCardV2 } from 'app/constants/Characters' import { RecentMessages } from 'app/constants/RecentMessages' import { Tokenizer } from 'app/constants/Tokenizer' import * as DocumentPicker from 'expo-document-picker' import { Stack, useRouter } from 'expo-router' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { useAutosave } from 'react-autosave' import { View, @@ -18,28 +18,41 @@ import { Alert, ScrollView, TextInput, + BackHandler, } from 'react-native' import { useShallow } from 'zustand/react/shallow' const CharInfo = () => { const router = useRouter() - const { currentCard, setCurrentCard, charId, charName } = Characters.useCharacterCard( - useShallow((state) => ({ - charId: state.id, - currentCard: state.card, - setCurrentCard: state.setCard, - charName: state.card?.data.name, - })) - ) + const { currentCard, setCurrentCard, charId, charName, unloadCharacter } = + Characters.useCharacterCard( + useShallow((state) => ({ + charId: state.id, + currentCard: state.card, + setCurrentCard: state.setCard, + charName: state.card?.data.name, + unloadCharacter: state.unloadCard, + })) + ) const getTokenCount = Tokenizer.useTokenizer((state) => state.getTokenCount) const [characterCard, setCharacterCard] = useState(currentCard) const imageDir = Characters.getImageDir(currentCard?.data.image_id ?? -1) + const chat = Chats.useChat((state) => state.data) const [imageSource, setImageSource] = useState({ uri: imageDir, }) + useEffect(() => { + const backAction = () => { + if (!chat) unloadCharacter() + return false + } + const handler = BackHandler.addEventListener('hardwareBackPress', backAction) + return () => handler.remove() + }, []) + const handleImageError = () => { setImageSource(require('@assets/user.png')) } diff --git a/app/_layout.tsx b/app/_layout.tsx index 0cd4c16..5a7c83f 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -58,15 +58,6 @@ const Layout = () => { }}> - - { - const router = useRouter() +enum SortType { + RECENT, + ALPHABETICAL, +} + +const sortModified = (item1: CharInfo, item2: CharInfo) => { + return item2.last_modified - item1.last_modified +} + +const sortAlphabetical = (item1: CharInfo, item2: CharInfo) => { + return -item2.name.localeCompare(item1.name) +} + +type SortButtonProps = { + sortType: SortType + currentSortType: SortType + label: string + onPress: () => void | Promise +} + +const SortButton: React.FC = ({ sortType, currentSortType, label, onPress }) => { + return ( + + + {label} + + + ) +} +type CharacterListProps = { + showHeader: boolean +} + +const CharacterList: React.FC = ({ showHeader }) => { + 'use no memo' const [characterList, setCharacterList] = useState([]) const [nowLoading, setNowLoading] = useState(false) - const goBack = () => router.back() - - const gesture = Gesture.Fling() - .direction(1) - .onEnd(() => { - runOnJS(goBack)() - }) + const [sortType, setSortType] = useState(SortType.RECENT) const getCharacterList = async () => { try { - const list = await Characters.db.query.cardList('character') + const list = await Characters.db.query.cardList('character', 'modified') setCharacterList(list) } catch (error) { Logger.log(`Could not retrieve characters.\n${error}`, true) @@ -44,39 +83,121 @@ const CharacterList = () => { }, [usePathname()]) return ( - - - ( - - ), - }} - /> + + ( + + ), + } + : {}), + }} + /> + + + + Sort By + + { + setSortType(SortType.RECENT) + setCharacterList(characterList.sort(sortModified)) + }} + /> + { + setSortType(SortType.ALPHABETICAL) + setCharacterList(characterList.sort(sortAlphabetical)) + }} + /> + + + + + + + + {characterList.length === 0 && } - {characterList.length === 0 && } - - {characterList.length !== 0 && ( - - {characterList.map((character, index) => ( - - ))} - - )} - - + {characterList.length !== 0 && ( + /* + {characterList.map((character, index) => ( + + ))} + */ + item.id.toString()} + renderItem={({ item, index }) => ( + + )} + /> + )} + ) } export default CharacterList + +const styles = StyleSheet.create({ + sortButton: { + paddingHorizontal: 16, + paddingVertical: 8, + backgroundColor: Style.getColor('primary-surface2'), + borderRadius: 16, + }, + + sortButtonText: { + color: Style.getColor('primary-text2'), + }, + + sortButtonActive: { + paddingHorizontal: 16, + paddingVertical: 8, + backgroundColor: Style.getColor('primary-surface3'), + borderRadius: 16, + }, + + sortButtonTextActive: { + color: Style.getColor('primary-text1'), + }, +}) diff --git a/app/components/CharacterMenu/CharacterListing.tsx b/app/components/CharacterMenu/CharacterListing.tsx index 9ea03d9..09fe9a0 100644 --- a/app/components/CharacterMenu/CharacterListing.tsx +++ b/app/components/CharacterMenu/CharacterListing.tsx @@ -1,4 +1,3 @@ -import AnimatedView from '@components/AnimatedView' import { AntDesign } from '@expo/vector-icons' import { Characters, Chats, Logger, Style } from '@globals' import { router } from 'expo-router' @@ -8,6 +7,7 @@ type CharacterListingProps = { index: number character: CharInfo nowLoading: boolean + showTags: boolean setNowLoading: (b: boolean) => void } @@ -15,13 +15,28 @@ type CharInfo = { name: string id: number image_id: number + last_modified: number tags: string[] + latestSwipe?: string + latestName?: string + latestChat?: number +} + +const day_ms = 86400000 +const day2_ms = 172800000 +const getTimeStamp = (oldtime: number) => { + const now = new Date().getTime() + const delta = now - oldtime + if (delta < now % day_ms) return new Date(oldtime).toLocaleTimeString() + if (delta < (now % day_ms) + day_ms) return 'Yesterday' + return new Date(oldtime).toLocaleDateString() } const CharacterListing: React.FC = ({ index, character, nowLoading, + showTags, setNowLoading, }) => { const { loadedCharId, setCurrentCard } = Characters.useCharacterCard((state) => ({ @@ -35,40 +50,45 @@ const CharacterListing: React.FC = ({ if (nowLoading) return try { - await setCurrentCard(charId) setNowLoading(true) - const returnedChatId = await Chats.db.query.chatNewest(charId) - let chatId = returnedChatId - if (!chatId) { - chatId = await Chats.db.mutate.createChat(charId) - } - if (!chatId) { - Logger.log('Chat creation backup has failed! Please report.', true) - return - } + await setCurrentCard(charId) - await loadChat(chatId) + if (edit) { + router.push('/CharInfo') + } else { + let chatId = character.latestChat + if (!chatId) { + chatId = await Chats.db.mutate.createChat(charId) + } + if (!chatId) { + Logger.log('Chat creation backup has failed! Please report.', true) + return + } + await loadChat(chatId) + } setNowLoading(false) - if (edit) router.push('/CharInfo') - else router.back() } catch (error) { Logger.log(`Couldn't load character: ${error}`, true) setNowLoading(false) } } + const getPreviewText = () => { + if (!character.latestSwipe || !character.latestName) return '' + const previewText = + (character.latestName + ': ' + character.latestSwipe).substring(0, 80) + + (character.latestSwipe.length > 80 ? '...' : '') + return previewText + } + return ( - + }> = ({ }} style={styles.avatar} /> - - {character.name} + + + {character.name} + + {' '} + {getTimeStamp(character.last_modified)} + + + {character.latestSwipe && ( + {getPreviewText()} + )} {character.tags.map((item, index) => ( @@ -110,15 +141,11 @@ const CharacterListing: React.FC = ({ setCurrentCharacter(character.id, true) }} disabled={nowLoading}> - + )} - + ) } @@ -170,10 +197,20 @@ const styles = StyleSheet.create({ nametag: { fontSize: 16, - marginLeft: 20, color: Style.getColor('primary-text1'), }, + timestamp: { + fontSize: 12, + color: Style.getColor('primary-text2'), + }, + + previewText: { + marginTop: 4, + fontSize: 12, + color: Style.getColor('primary-text3'), + }, + tag: { color: Style.getColor('primary-text2'), fontSize: 12, diff --git a/app/components/CharacterMenu/CharacterNewMenu.tsx b/app/components/CharacterMenu/CharacterNewMenu.tsx index 6e59a28..f72e142 100644 --- a/app/components/CharacterMenu/CharacterNewMenu.tsx +++ b/app/components/CharacterMenu/CharacterNewMenu.tsx @@ -4,6 +4,7 @@ import { Characters, Chats, Logger, Style } from '@globals' import { useRouter } from 'expo-router' import { useState } from 'react' import { View, StyleSheet, TouchableOpacity } from 'react-native' +import Animated, { Easing, SlideInRight, SlideOutRight } from 'react-native-reanimated' import { useShallow } from 'zustand/react/shallow' type CharacterNewMenuProps = { @@ -67,7 +68,13 @@ const CharacterNewMenu: React.FC = ({ } return ( - + = ({ }}> - + ) } diff --git a/app/components/ChatMenu/ChatMenu.tsx b/app/components/ChatMenu/ChatMenu.tsx index fee25d4..d22c0f5 100644 --- a/app/components/ChatMenu/ChatMenu.tsx +++ b/app/components/ChatMenu/ChatMenu.tsx @@ -1,28 +1,32 @@ +import CharacterList from '@components/CharacterMenu/CharacterList' import { Ionicons, FontAwesome } from '@expo/vector-icons' -import { Logger, Style, Characters } from '@globals' +import { Logger, Style, Characters, Chats } from '@globals' import { Stack, useFocusEffect, useRouter } from 'expo-router' import { useCallback, useRef, useState } from 'react' import { View, SafeAreaView, TouchableOpacity, StyleSheet, BackHandler } from 'react-native' import { Gesture, GestureDetector } from 'react-native-gesture-handler' import { Menu } from 'react-native-popup-menu' -import Animated, { SlideInRight, runOnJS, Easing } from 'react-native-reanimated' +import Animated, { SlideInRight, runOnJS, Easing, SlideOutRight } from 'react-native-reanimated' import { useShallow } from 'zustand/react/shallow' import ChatInput from './ChatInput' import { ChatWindow } from './ChatWindow/ChatWindow' import OptionsMenu from './OptionsMenu' -import Recents from './Recents' import SettingsDrawer from './SettingsDrawer' const ChatMenu = () => { const router = useRouter() - const { charName, unloadCharacter } = Characters.useCharacterCard( + const { unloadCharacter } = Characters.useCharacterCard( useShallow((state) => ({ - charName: state?.card?.data.name, unloadCharacter: state.unloadCard, })) ) + const { chat, unloadChat } = Chats.useChat((state) => ({ + chat: state.data, + unloadChat: state.reset, + })) + const [showDrawer, setDrawer] = useState(false) const menuRef = useRef(null) @@ -46,7 +50,8 @@ const ChatMenu = () => { return true } - if (charName) { + if (chat) { + unloadChat() unloadCharacter() Logger.debug('Returning to primary Menu') return true @@ -58,19 +63,13 @@ const ChatMenu = () => { useCallback(() => { BackHandler.removeEventListener('hardwareBackPress', backAction) BackHandler.addEventListener('hardwareBackPress', backAction) - return () => { BackHandler.removeEventListener('hardwareBackPress', backAction) } // eslint-disable-next-line react-compiler/react-compiler - }, [charName, showDrawer, menuRef.current?.isOpen()]) + }, [chat, showDrawer, menuRef.current?.isOpen()]) ) - const goToChars = () => { - if (showDrawer) setShowDrawer(false) - else router.push('/components/CharacterMenu/CharacterList') - } - const swipeDrawer = Gesture.Fling() .direction(1) .onEnd(() => { @@ -80,16 +79,18 @@ const ChatMenu = () => { const swipeChar = Gesture.Fling() .direction(3) .onEnd(() => { - runOnJS(goToChars)() + // runOnJS(goToChars)() }) const gesture = Gesture.Exclusive(swipeDrawer, swipeChar) const headerViewRightSettings = ( + .easing(Easing.out(Easing.ease)) + .duration(300)} + exiting={SlideOutRight.duration(500).easing(Easing.out(Easing.linear))}> { @@ -103,15 +104,13 @@ const ChatMenu = () => { const headerViewRight = ( - { - goToChars() - }}> - + .easing(Easing.out(Easing.ease)) + .duration(300)} + exiting={SlideOutRight.duration(500).easing(Easing.out(Easing.linear))}> + {}}> + @@ -137,13 +136,17 @@ const ChatMenu = () => { (showDrawer ? headerViewRightSettings : headerViewRight), headerLeft: () => headerViewLeft, + + headerRight: () => { + if (showDrawer) return headerViewRightSettings + if (chat) return headerViewRight + }, }} /> - {!charName ? ( - + {!chat ? ( + ) : ( diff --git a/app/constants/Characters.ts b/app/constants/Characters.ts index 682b4a9..82c32ba 100644 --- a/app/constants/Characters.ts +++ b/app/constants/Characters.ts @@ -1,7 +1,15 @@ import { db as database, rawdb } from '@db' import { copyFileRes, writeFile } from '@dr.pogodin/react-native-fs' -import { characterGreetings, characterTags, characters, tags } from 'db/schema' -import { eq, inArray, notInArray } from 'drizzle-orm' +import { + characterGreetings, + characterTags, + characters, + chatEntries, + chatSwipes, + chats, + tags, +} from 'db/schema' +import { desc, eq, inArray, notInArray } from 'drizzle-orm' import { randomUUID } from 'expo-crypto' import * as DocumentPicker from 'expo-document-picker' import * as FS from 'expo-file-system' @@ -40,12 +48,12 @@ export namespace Characters { card: undefined, tokenCache: undefined, setCard: async (id: number) => { - let start = performance.now() + //let start = performance.now() const card = await db.query.card(id) - Logger.debug(`[User] time for database query: ${performance.now() - start}`) - start = performance.now() + //Logger.debug(`[User] time for database query: ${performance.now() - start}`) + //start = performance.now() set((state) => ({ ...state, card: card, id: id, tokenCache: undefined })) - Logger.debug(`[User] time for zustand set: ${performance.now() - start}`) + //Logger.debug(`[User] time for zustand set: ${performance.now() - start}`) mmkv.set(Global.UserID, id) return card?.data.name }, @@ -109,15 +117,15 @@ export namespace Characters { card: undefined, tokenCache: undefined, setCard: async (id: number) => { - let start = performance.now() + //let start = performance.now() const card = await db.query.card(id) db.mutate.updateModified(id) - Logger.debug(`[Characters] time for database query: ${performance.now() - start}`) - start = performance.now() + //Logger.debug(`[Characters] time for database query: ${performance.now() - start}`) + //start = performance.now() set((state) => { return { ...state, card: card, id: id, tokenCache: undefined } }) - Logger.debug(`[Characters] time for zustand set: ${performance.now() - start}`) + //Logger.debug(`[Characters] time for zustand set: ${performance.now() - start}`) return card?.data.name }, unloadCard: () => { @@ -205,6 +213,7 @@ export namespace Characters { id: true, name: true, image_id: true, + last_modified: true, }, with: { tags: { @@ -215,12 +224,43 @@ export namespace Characters { tag: true, }, }, + chats: { + columns: { + id: true, + }, + limit: 1, + orderBy: desc(chats.last_modified), + with: { + messages: { + columns: { + id: true, + name: true, + }, + limit: 1, + orderBy: desc(chatEntries.id), + with: { + swipes: { + columns: { + swipe: true, + }, + orderBy: desc(chatSwipes.id), + limit: 1, + }, + }, + }, + }, + }, }, where: (characters, { eq }) => eq(characters.type, type), - orderBy: orderBy === 'id' ? characters.id : characters.last_modified, + orderBy: orderBy === 'id' ? characters.id : desc(characters.last_modified), }) + return query.map((item) => ({ ...item, + latestChat: item.chats[0]?.id, + latestSwipe: item.chats[0]?.messages[0]?.swipes[0]?.swipe, + latestName: item.chats[0]?.messages[0]?.name, + last_modified: item.last_modified ?? 0, tags: item.tags.map((item) => item.tag.tag), })) } diff --git a/app/constants/Chat.ts b/app/constants/Chat.ts index 06f3b25..faf721d 100644 --- a/app/constants/Chat.ts +++ b/app/constants/Chat.ts @@ -117,15 +117,15 @@ export namespace Chats { abortFunction: undefined, setAbortFunction: (fn) => (get().abortFunction = fn), load: async (chatId: number) => { - let start = performance.now() + //let start = performance.now() const data = await db.query.chat(chatId) - Logger.debug(`[Chats] time for database query: ${performance.now() - start}`) - start = performance.now() + //Logger.debug(`[Chats] time for database query: ${performance.now() - start}`) + //start = performance.now() set((state: ChatState) => ({ ...state, data: data, })) - Logger.debug(`[Chats] time for zustand set: ${performance.now() - start}`) + //Logger.debug(`[Chats] time for zustand set: ${performance.now() - start}`) db.mutate.updateChatModified(chatId) const charName = Characters.useCharacterCard.getState().card?.data.name const charId = Characters.useCharacterCard.getState().id @@ -431,22 +431,7 @@ export namespace Chats { where: eq(chatSwipes.id, swipeId), }) } - /* - export const readChat = async (chatId: number): Promise => { - const chat = await database.query.chats.findFirst({ - where: eq(chats.id, chatId), - with: { - messages: { - orderBy: chatEntries.order, - with: { - swipes: true, - }, - }, - }, - }) - if (chat) return { ...chat } - } -*/ + export const updateEntrySwipeId = async (entryId: number, swipeId: number) => { await updateEntryModified(entryId) await database diff --git a/db/schema.ts b/db/schema.ts index 43135be..eaea1af 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -61,6 +61,7 @@ export const characterRelations = relations(characters, ({ many }) => ({ alternate_greetings: many(characterGreetings), tags: many(characterTags), lorebooks: many(characterLorebooks), + chats: many(chats), })) export const greetingsRelations = relations(characterGreetings, ({ one }) => ({ @@ -133,8 +134,12 @@ export const chatSwipes = sqliteTable('chat_swipes', { .$defaultFn(() => new Date()), }) -export const chatsRelations = relations(chats, ({ many }) => ({ +export const chatsRelations = relations(chats, ({ many, one }) => ({ messages: many(chatEntries), + character: one(characters, { + fields: [chats.character_id], + references: [characters.id], + }), })) export const chatEntriesRelations = relations(chatEntries, ({ one, many }) => ({