diff --git a/src/lib/commands/components/ChatMessage.svelte b/src/lib/commands/components/ChatMessage.svelte index 95698cd..3a2df09 100644 --- a/src/lib/commands/components/ChatMessage.svelte +++ b/src/lib/commands/components/ChatMessage.svelte @@ -9,11 +9,10 @@ import { fly } from 'svelte/transition'; import { decryptMessage } from '$lib/encryption'; - import { firestore } from '$lib/firebase'; - import { deleteDoc, doc } from 'firebase/firestore'; - import { userStore } from '$lib/firebase-store'; - import { db } from '../../db'; - import { addToast } from '../../toasts'; + import { db } from '$lib/db'; + import { addToast } from '$lib/toasts'; + import { userStore } from '$lib/stores/firebase-store'; + import { deleteMessage } from '$lib/stores/messages'; export let message: Message; export let components: Record>; @@ -91,7 +90,7 @@ closeMenu(); } - async function deleteMessage() { + async function deleteChatMessage() { closeMenu(); if (!confirm('Are you sure you want to delete this message?')) { @@ -100,7 +99,7 @@ if ($user) { try { - await deleteDoc(doc(firestore, `users/${$user!.uid}/messages/${message.id}`)); + await deleteMessage($user, message.id); } catch (e) { console.error(e); } @@ -215,7 +214,7 @@ class="dropdown-content z-[1] menu m-1 shadow bg-base-100 rounded-box text-sm" >
  • -
  • +
  • diff --git a/src/lib/commands/components/Login.svelte b/src/lib/commands/components/Login.svelte index 0351208..57b2df7 100644 --- a/src/lib/commands/components/Login.svelte +++ b/src/lib/commands/components/Login.svelte @@ -3,11 +3,11 @@ import { sha256 } from '@noble/hashes/sha256'; import { decryptMessage, encodeBase64, encryptMessage } from '$lib/encryption'; import { auth, firestore } from '$lib/firebase'; - import { collectionStore, userStore } from '$lib/firebase-store'; import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth'; import { db } from '$lib/db'; import type { Log } from '~/src/routes/+page.svelte'; - import { Timestamp, collection, doc, getDocs, updateDoc, writeBatch } from 'firebase/firestore'; + import { Timestamp, collection, doc, getDocs, writeBatch } from 'firebase/firestore'; + import { userStore } from '$lib/stores/firebase-store'; const provider = new GoogleAuthProvider(); const user = userStore(); diff --git a/src/lib/firebase-store.ts b/src/lib/stores/firebase-store.ts similarity index 98% rename from src/lib/firebase-store.ts rename to src/lib/stores/firebase-store.ts index 96447f0..0fe8746 100644 --- a/src/lib/firebase-store.ts +++ b/src/lib/stores/firebase-store.ts @@ -10,7 +10,7 @@ import { } from 'firebase/firestore'; import { writable } from 'svelte/store'; -import { auth } from './firebase'; +import { auth } from '../firebase'; /** * The userStore is updated whenever the authentication state changes. diff --git a/src/lib/stores/messages.ts b/src/lib/stores/messages.ts new file mode 100644 index 0000000..5642a22 --- /dev/null +++ b/src/lib/stores/messages.ts @@ -0,0 +1,97 @@ +import type { User } from 'firebase/auth'; +import { + type DocumentData, + type QueryDocumentSnapshot, + addDoc, + collection, + deleteDoc, + doc, + getDocs, + limit, + onSnapshot, + orderBy, + query, + startAfter +} from 'firebase/firestore'; +import { writable } from 'svelte/store'; +import type { Log } from '~/src/routes/+page.svelte'; + +import { firestore } from '../firebase'; + +const CHAT_MESSAGES_INITIAL_LIMIT = 15; + +export const firebaseChatMessages = writable(); + +async function loadLatestMessages(user: User) { + const recentMessagesQuery = query( + collection(firestore, `users/${user.uid}/messages`), + orderBy('time', 'desc'), + limit(CHAT_MESSAGES_INITIAL_LIMIT) + ); + + const querySnapshot = await getDocs(recentMessagesQuery); + + firebaseChatMessages.set( + querySnapshot.docs + .map((doc) => { + return { + ...(doc.data() as Log), + id: doc.id + }; + }) + .reverse() + ); + + const newestDoc = querySnapshot.docs[0]; + const oldestDoc = querySnapshot.docs[querySnapshot.docs.length - 1]; + + return { newestDoc, oldestDoc }; +} + +function listenToNewMessages( + user: User, + newestDoc: QueryDocumentSnapshot +) { + const queryOptions = newestDoc ? [orderBy('time'), startAfter(newestDoc)] : [orderBy('time')]; + + const newMessagesQuery = query( + collection(firestore, `users/${user.uid}/messages`), + ...queryOptions + ); + + const processedNewMessageIds = new Set(); + + onSnapshot(newMessagesQuery, (snapshot) => { + let newMessages: Log[] = []; + + snapshot.docs.forEach((s) => { + if (!processedNewMessageIds.has(s.id)) { + processedNewMessageIds.add(s.id); + newMessages.push({ ...(s.data() as Log), id: s.id }); + } + }); + + firebaseChatMessages.update((existingMessages) => [...existingMessages, ...newMessages]); + }); +} + +// TODO: implement +function loadMoreOlderMessages(count: number) {} + +export async function loadMessages(user: User) { + const { newestDoc, oldestDoc } = await loadLatestMessages(user); + + listenToNewMessages(user, newestDoc); +} + +export async function addMessage(user: User, doc: any) { + return await addDoc(collection(firestore, `users/${user.uid}/messages`), doc); +} + +export async function deleteMessage(user: User, docId: string) { + firebaseChatMessages.update((existingMessages) => + existingMessages.filter((message) => message.id !== docId) + ); + + await deleteDoc(doc(firestore, `users/${user.uid}/messages/${docId}`)); +} diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index 655f9e4..669fbb7 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -1,4 +1,4 @@ -import { userStore } from '../lib/firebase-store'; +import { userStore } from '../lib/stores/firebase-store'; import type { LayoutLoad } from './$types'; export const load: LayoutLoad = async (event) => { diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 5ad7f30..c41207e 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -21,13 +21,12 @@ import { onDestroy, tick } from 'svelte'; import ChatMessage from '$lib/commands/components/ChatMessage.svelte'; import type { BotMessageCallback, Components } from '$lib/commands'; - import { firestore } from '$lib/firebase'; - import { collectionStore } from '$lib/firebase-store'; - import { Timestamp, collection, orderBy, query, type DocumentData } from 'firebase/firestore'; + import { Timestamp } from 'firebase/firestore'; import type { PageData } from './$types'; import { liveQuery, type Observable } from 'dexie'; import { db } from '$lib/db'; import { fade } from 'svelte/transition'; + import { addMessage, firebaseChatMessages, loadMessages } from '$lib/stores/messages'; export let data: PageData; let { user } = data; @@ -70,26 +69,22 @@ return messages; }) satisfies Observable; - let messagesQuery: ReturnType>; - let messages: ReturnType>; - let messagesCollection: ReturnType>; - $: if ($user) { - messagesQuery = query(collection(firestore, `users/${$user.uid}/messages`), orderBy('time')); - messages = collectionStore(firestore, messagesQuery); - messagesCollection = collectionStore(firestore, `users/${$user.uid}/messages`); + loadMessages($user); } $: combinedMessages = $user - ? [...($messages || []), ...$guestMessages].sort( + ? [...($firebaseChatMessages || []), ...$guestMessages].sort( (a, b) => a.time?.toDate().valueOf() - b.time?.toDate().valueOf() ) : $guestMessages; $: dbReady = - $user !== undefined && ($user ? $messages !== undefined : $guestMessages !== undefined); + $user !== undefined && + ($user ? $firebaseChatMessages !== undefined : $guestMessages !== undefined); - $: newLoggedInUser = $user && $messages !== undefined && $messages?.length === 0; + $: newLoggedInUser = + $user && $firebaseChatMessages !== undefined && $firebaseChatMessages?.length === 0; $: newGuestUser = $user == null && $guestMessages !== undefined && $guestMessages?.length === 0; let greeted = false; @@ -107,7 +102,7 @@ greeted = true; if ($user) { - messagesCollection.add({ + addMessage($user, { self: false, message: `Hello ${$user.displayName}!`, time: Timestamp.now(), @@ -143,10 +138,10 @@ const debouncedScrollToBottom = debounce(scrollToBottom, 100); - $: if (chatDiv && ($messages?.length || $guestMessages?.length)) { + $: if (chatDiv && ($firebaseChatMessages?.length || $guestMessages?.length)) { tick().then(() => { if ( - $messages?.[$messages.length - 1]?.self || + $firebaseChatMessages?.[$firebaseChatMessages.length - 1]?.self || $guestMessages?.[$guestMessages.length - 1]?.self ) { scrollToBottom(chatDiv, 'auto'); @@ -180,7 +175,7 @@ if (encryptionKey) { const encryptedMessage = encryptMessage(message, encryptionKey); - await messagesCollection.add({ + await addMessage($user, { self: true, message: encryptedMessage, time: Timestamp.now(), @@ -188,7 +183,7 @@ encrypted: true }); } else { - await messagesCollection.add({ + await addMessage($user, { self: true, message, time: Timestamp.now(), @@ -219,7 +214,7 @@ const encryptedMessage = encryptMessage(message, encryptionKey); const encryptedOptions = encryptMessage(options, encryptionKey); - await messagesCollection.add({ + await addMessage($user, { self: false, message: encryptedMessage, time: Timestamp.now(), @@ -228,7 +223,7 @@ encrypted: true }); } else { - await messagesCollection.add({ + await addMessage($user, { self: false, message, time: Timestamp.now(), diff --git a/src/routes/test/+page.svelte b/src/routes/test/+page.svelte index ca46799..0f5f643 100644 --- a/src/routes/test/+page.svelte +++ b/src/routes/test/+page.svelte @@ -1,7 +1,6 @@