-
+ {
+ const navigate = useNavigate();
+ const { pathname } = props.location;
+
+ createEffect(() => {
+ if (!db() && pathname !== "/") {
+ navigate("/");
+ }
+ });
+
+ return props.children;
+ }}
+ >
diff --git a/src/lib/db-cache.ts b/src/lib/db-cache.ts
index 888a330..d42e206 100644
--- a/src/lib/db-cache.ts
+++ b/src/lib/db-cache.ts
@@ -1,5 +1,7 @@
-import { createRoot, on, createDeferred } from "solid-js";
+import { on, createSignal, createEffect, createRoot, createMemo } from "solid-js";
import { serialize, deserialize } from "seroval";
+import { createSignaledWorker } from "@solid-primitives/workers";
+import { db } from "~/db";
const DATABASE_HASH_PREFIX = "database";
@@ -30,31 +32,48 @@ const hashString = (str: string) => {
const HASH_STORE_KEY = `${DATABASE_HASH_PREFIX}_hash`;
-// cannot import `db` the normal way because this file is imported in ~/db.ts before the initialisation of `db` has happened
createRoot(() => {
- void import("~/db").then(({ db }) => {
- // we use create deferred because hasing can take very long and we don't want to block the mainthread
- createDeferred(
- on(db, (currentDb) => {
- if (currentDb) {
- const newHash = hashString(new TextDecoder().decode(currentDb.export())).toString();
+ const [dbHash, setDbHash] = createSignal(localStorage.getItem(HASH_STORE_KEY));
- const oldHash = localStorage.getItem(HASH_STORE_KEY);
-
- if (newHash !== oldHash) {
- clearDbCache();
-
- localStorage.setItem(HASH_STORE_KEY, newHash);
- }
+ // offloaded because this can take a long time (>1s easily) and would block the mainthread
+ createSignaledWorker({
+ input: db,
+ output: setDbHash,
+ func: function hashDb(currentDb: ReturnType
) {
+ const hashString = (str: string) => {
+ let hash = 0,
+ i,
+ chr;
+ if (str.length === 0) return hash;
+ for (i = 0; i < str.length; i++) {
+ chr = str.charCodeAt(i);
+ hash = (hash << 5) - hash + chr;
+ hash |= 0; // Convert to 32bit integer
}
- }),
- );
+ return hash;
+ };
+
+ if (currentDb?.export) {
+ return hashString(new TextDecoder().decode(currentDb.export())).toString();
+ }
+ },
+ });
+
+ createEffect(() => {
+ on(dbHash, (currentDbHash) => {
+ if (currentDbHash) {
+ clearDbCache();
+
+ localStorage.setItem(HASH_STORE_KEY, currentDbHash);
+ }
+ });
});
});
class LocalStorageCacheAdapter {
keys = new Set(Object.keys(localStorage).filter((key) => key.startsWith(this.prefix)));
prefix = "database";
+ #dbLoaded = createMemo(() => !!db());
#createKey(cacheName: string, key: string): string {
return `${this.prefix}-${cacheName}-${key}`;
@@ -76,14 +95,24 @@ class LocalStorageCacheAdapter {
}
has(cacheName: string, key: string): boolean {
- return this.keys.has(this.#createKey(cacheName, key));
+ if (this.#dbLoaded()) {
+ return this.keys.has(this.#createKey(cacheName, key));
+ }
+
+ console.info("No database loaded");
+
+ return false;
}
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
get(cacheName: string, key: string): R | undefined {
- const item = localStorage.getItem(this.#createKey(cacheName, key));
- if (item) {
- return deserialize(item) as R;
+ if (this.#dbLoaded()) {
+ const item = localStorage.getItem(this.#createKey(cacheName, key));
+
+ if (item) {
+ return deserialize(item) as R;
+ }
+ } else {
+ console.info("No database loaded");
}
}
}
diff --git a/src/lib/messages-worker.ts b/src/lib/messages-worker.ts
new file mode 100644
index 0000000..d4cb339
--- /dev/null
+++ b/src/lib/messages-worker.ts
@@ -0,0 +1,109 @@
+import { getHourList, getMonthList, getWeekdayList } from "./date";
+import type { MessageOverview, MessageStats, Recipients } from "~/types";
+
+const hourNames = getHourList();
+
+const initialHourMap = [...hourNames.keys()];
+
+const monthNames = getMonthList();
+
+const initialMonthMap = [...monthNames.keys()];
+
+const weekdayNames = getWeekdayList();
+
+const initialWeekdayMap = [...weekdayNames.keys()];
+
+function createMessageStatsSourcesRaw(messageOverview: MessageOverview, recipients: Recipients) {
+ const getDateList = (startDate: Date, endDate: Date): Date[] => {
+ const dateArray = new Array();
+
+ // end date for loop has to be one date after because we increment after adding the date in the loop
+ const endDateForLoop = new Date(endDate);
+
+ endDateForLoop.setDate(endDateForLoop.getDate() + 1);
+
+ let currentDate = startDate;
+
+ while (currentDate <= endDateForLoop) {
+ dateArray.push(new Date(currentDate));
+
+ const newDate = new Date(currentDate);
+ newDate.setDate(newDate.getDate() + 1);
+
+ currentDate = newDate;
+ }
+
+ return dateArray;
+ };
+
+ const initialRecipientMap = () => {
+ return Object.fromEntries(recipients.map(({ recipientId }) => [recipientId, 0]));
+ };
+
+ const dateList = () => {
+ const firstDate = messageOverview?.at(0)?.messageDate;
+ const lastDate = messageOverview?.at(-1)?.messageDate;
+ if (firstDate && lastDate) {
+ return getDateList(firstDate, lastDate).map((date) => ({
+ totalMessages: 0,
+ date,
+ ...initialRecipientMap(),
+ }));
+ }
+ };
+
+ const currentDateList = dateList();
+ const currentInitialRecipientMap = initialRecipientMap();
+
+ const messageStats: MessageStats = {
+ person: { ...currentInitialRecipientMap },
+ month: initialMonthMap.map(() => ({ ...currentInitialRecipientMap })),
+ date: currentDateList ?? [],
+ weekday: initialWeekdayMap.map(() => ({ ...currentInitialRecipientMap })),
+ daytime: initialHourMap.map(() => ({ ...currentInitialRecipientMap })),
+ };
+
+ if (currentDateList) {
+ const { person, month, date, weekday, daytime } = messageStats;
+
+ for (const message of messageOverview) {
+ const { messageDate } = message;
+
+ // increment overall message count of a person
+ person[message.fromRecipientId] += 1;
+
+ // increment the message count of the message's month for this recipient
+ month[messageDate.getMonth()][message.fromRecipientId] += 1;
+
+ // biome-ignore lint/style/noNonNullAssertion:
+ const dateStatsEntry = date.find(({ date }) => date.toDateString() === messageDate.toDateString())!;
+
+ // increment the message count of the message's date for this recipient
+ dateStatsEntry[message.fromRecipientId] += 1;
+
+ // increment the overall message count of the message's date
+ dateStatsEntry.totalMessages += 1;
+
+ const weekdayOfDate = messageDate.getDay();
+ // we index starting with monday while the `Date` object indexes starting with Sunday
+ const weekdayIndex = weekdayOfDate === 0 ? 6 : weekdayOfDate - 1;
+
+ // increment the message count of the message's weekday for this recipient
+ weekday[weekdayIndex][message.fromRecipientId] += 1;
+
+ // increment the message count of the message's daytime for this recipient
+ daytime[messageDate.getHours()][message.fromRecipientId] += 1;
+ }
+ }
+
+ return messageStats;
+}
+
+self.onmessage = (event: MessageEvent<{ dmId: number; messageOverview: MessageOverview; recipients: Recipients }>) => {
+ const result = createMessageStatsSourcesRaw(event.data.messageOverview, event.data.recipients);
+
+ postMessage({
+ dmId: event.data.dmId,
+ messageStatsSources: result,
+ });
+};
diff --git a/src/lib/messages.ts b/src/lib/messages.ts
index 782581b..51c1539 100644
--- a/src/lib/messages.ts
+++ b/src/lib/messages.ts
@@ -1,82 +1,37 @@
-import { getDateList, getHourList, getMonthList, getWeekdayList } from "./date";
import type { MessageOverview, MessageStats, Recipients } from "~/types";
-import { isSameDay } from "date-fns";
import { cached } from "./db-cache";
+import { getHourList, getMonthList, getWeekdayList } from "./date";
+import MessageStatsWorker from "./messages-worker?worker";
export const hourNames = getHourList();
-const initialHoursMap = [...hourNames.keys()];
-
export const monthNames = getMonthList();
-const initialMonthMap = [...monthNames.keys()];
-
export const weekdayNames = getWeekdayList();
-const initialWeekdayMap = [...weekdayNames.keys()];
+const messageStatsWorker = new MessageStatsWorker();
-const createMessageStatsSourcesRaw = (messageOverview: MessageOverview, recipients: Recipients) => {
- const initialRecipientMap = () => {
- return Object.fromEntries(recipients.map(({ recipientId }) => [recipientId, 0]));
- };
+export const createMessageStatsSources = cached(
+ (dmId: number, messageOverview: MessageOverview, recipients: Recipients) => {
+ messageStatsWorker.postMessage({
+ dmId,
+ messageOverview,
+ recipients,
+ });
- const dateList = () => {
- const firstDate = messageOverview?.at(0)?.messageDate;
- const lastDate = messageOverview?.at(-1)?.messageDate;
- if (firstDate && lastDate) {
- return getDateList(firstDate, lastDate).map((date) => ({
- totalMessages: 0,
- date,
- ...initialRecipientMap(),
- }));
- }
- };
+ return new Promise((resolve) => {
+ const listener = (
+ event: MessageEvent<{
+ dmId: number;
+ messageStatsSources: MessageStats;
+ }>,
+ ) => {
+ if (event.data.dmId === dmId) {
+ resolve(event.data.messageStatsSources);
+ }
+ };
- const currentDateList = dateList();
- const currentInitialRecipientMap = initialRecipientMap();
-
- const messageStats: MessageStats = {
- person: { ...currentInitialRecipientMap },
- month: initialMonthMap.map(() => ({ ...currentInitialRecipientMap })),
- date: currentDateList ?? [],
- weekday: initialWeekdayMap.map(() => ({ ...currentInitialRecipientMap })),
- daytime: initialHoursMap.map(() => ({ ...currentInitialRecipientMap })),
- };
-
- if (currentDateList) {
- const { person, month, date, weekday, daytime } = messageStats;
-
- for (const message of messageOverview) {
- const { messageDate } = message;
-
- // increment overall message count of a person
- person[message.fromRecipientId] += 1;
-
- // increment the message count of the message's month for this recipient
- month[messageDate.getMonth()][message.fromRecipientId] += 1;
-
- // biome-ignore lint/style/noNonNullAssertion:
- const dateStatsEntry = date.find(({ date }) => isSameDay(date, messageDate))!;
-
- // increment the message count of the message's date for this recipient
- dateStatsEntry[message.fromRecipientId] += 1;
-
- // increment the overall message count of the message's date
- dateStatsEntry.totalMessages += 1;
-
- const weekdayOfDate = messageDate.getDay();
- // we index starting with monday while the `Date` object indexes starting with Sunday
- const weekdayIndex = weekdayOfDate === 0 ? 6 : weekdayOfDate - 1;
-
- // increment the message count of the message's weekday for this recipient
- weekday[weekdayIndex][message.fromRecipientId] += 1;
-
- // increment the message count of the message's daytime for this recipient
- daytime[messageDate.getHours()][message.fromRecipientId] += 1;
- }
- }
-
- return messageStats;
-};
-
-export const createMessageStatsSources = cached(createMessageStatsSourcesRaw);
+ messageStatsWorker.addEventListener("message", listener);
+ });
+ },
+);
diff --git a/src/pages/dm/dm-id.tsx b/src/pages/dm/dm-id.tsx
index e7f2e52..e10c445 100644
--- a/src/pages/dm/dm-id.tsx
+++ b/src/pages/dm/dm-id.tsx
@@ -1,7 +1,7 @@
-import { type Component, createMemo, createResource } from "solid-js";
-import type { RouteSectionProps } from "@solidjs/router";
+import { Suspense, type Component } from "solid-js";
+import { createAsync, type RoutePreloadFunc, type RouteSectionProps } from "@solidjs/router";
-import { dmPartnerRecipientQuery, SELF_ID, threadMostUsedWordsQuery, threadSentMessagesOverviewQuery } from "~/db";
+import { dmPartnerRecipientQuery, threadMostUsedWordsQuery, threadSentMessagesOverviewQuery } from "~/db-queries";
import { getNameFromRecipient } from "~/lib/get-name-from-recipient";
import { Heading } from "~/components/ui/heading";
import { Grid } from "~/components/ui/grid";
@@ -15,13 +15,13 @@ import { DmMessagesPerRecipient } from "./dm-messages-per-recipients";
import { DmMessagesPerWeekday } from "./dm-messages-per-weekday";
import type { MessageOverview } from "~/types";
import { createMessageStatsSources } from "~/lib/messages";
+import { SELF_ID } from "~/db";
+import { Flex } from "~/components/ui/flex";
-export const DmId: Component = (props) => {
- const dmId = () => Number(props.params.dmid);
-
+const getDmIdData = (dmId: number) => {
// the other person in the chat with name and id
- const [dmPartner] = createResource(async () => {
- const dmPartner = await dmPartnerRecipientQuery(dmId());
+ const dmPartner = createAsync(async () => {
+ const dmPartner = await dmPartnerRecipientQuery(dmId);
if (dmPartner) {
return {
@@ -35,8 +35,8 @@ export const DmId: Component = (props) => {
}
});
- const [dmMessagesOverview] = createResource(async () => {
- const dmMessageOverview = await threadSentMessagesOverviewQuery(dmId());
+ const dmMessagesOverview = createAsync(async () => {
+ const dmMessageOverview = await threadSentMessagesOverviewQuery(dmId);
if (dmMessageOverview) {
return dmMessageOverview.map((row) => {
return {
@@ -47,7 +47,7 @@ export const DmId: Component = (props) => {
}
});
- const [mostUsedWordCounts] = createResource(async () => threadMostUsedWordsQuery(dmId(), 300));
+ const mostUsedWordCounts = createAsync(async () => threadMostUsedWordsQuery(dmId, 300));
const recipients = () => {
const currentDmPartner = dmPartner();
@@ -63,45 +63,87 @@ export const DmId: Component = (props) => {
}
};
- const dmMessageStats = createMemo(() => {
+ const dmMessageStats = createAsync(async () => {
const currentDmMessagesOverview = dmMessagesOverview();
const currentRecipients = recipients();
if (currentDmMessagesOverview && currentRecipients) {
- return createMessageStatsSources(currentDmMessagesOverview, currentRecipients);
+ return await createMessageStatsSources(dmId, currentDmMessagesOverview, currentRecipients);
}
});
+ return {
+ dmPartner,
+ dmMessagesOverview,
+ mostUsedWordCounts,
+ recipients,
+ dmMessageStats,
+ };
+};
+
+export const preloadDmId: RoutePreloadFunc = (props) => {
+ void getDmIdData(Number(props.params.dmid));
+};
+
+export const DmId: Component = (props) => {
+ const { dmPartner, dmMessagesOverview, mostUsedWordCounts, recipients, dmMessageStats } = getDmIdData(
+ Number(props.params.dmid),
+ );
+
return (
<>
Dm with {dmPartner()?.name}
DM with {dmPartner()?.name}
Chat timeline
-
+
+ Loading...
+
+ }
+ >
+
+
Messages per
-
-
- Person
-
-
-
- Daytime
-
-
-
- Month
-
-
-
- Weekday
-
-
-
+
+ Loading...
+
+ }
+ >
+
+
+ Person
+
+
+
+ Daytime
+
+
+
+ Month
+
+
+
+ Weekday
+
+
+
+
Word cloud
-
+
+ Loading...
+
+ }
+ >
+
+
>
);
diff --git a/src/pages/dm/dm-wordcloud.tsx b/src/pages/dm/dm-wordcloud.tsx
index bd24624..5ec2b1e 100644
--- a/src/pages/dm/dm-wordcloud.tsx
+++ b/src/pages/dm/dm-wordcloud.tsx
@@ -1,7 +1,7 @@
import type { ChartData } from "chart.js";
import { Show, type Accessor, type Component } from "solid-js";
import { WordCloudChart } from "~/components/ui/charts";
-import type { threadMostUsedWordsQuery } from "~/db";
+import type { threadMostUsedWordsQuery } from "~/db-queries";
const maxWordSize = 100;
diff --git a/src/pages/home.tsx b/src/pages/home.tsx
index df3898f..3c5b066 100644
--- a/src/pages/home.tsx
+++ b/src/pages/home.tsx
@@ -1,10 +1,10 @@
import { createSignal, Show, type Component, type JSX } from "solid-js";
import { type RouteSectionProps, useNavigate } from "@solidjs/router";
-import { setDb, SQL } from "~/db";
import { Portal } from "solid-js/web";
import { Flex } from "~/components/ui/flex";
import { Title } from "@solidjs/meta";
+import { setDb, SQL } from "~/db";
export const Home: Component = () => {
const [isLoadingDb, setIsLoadingDb] = createSignal(false);
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index 82c820d..37439ba 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -2,6 +2,7 @@ import { lazy } from "solid-js";
export { Home } from "./home";
+export { preloadDmId } from "./dm/dm-id";
export const GroupId = lazy(() => import("./group/group-id"));
export const DmId = lazy(() => import("./dm/dm-id"));
diff --git a/src/pages/overview/index.tsx b/src/pages/overview/index.tsx
index e87bc14..b117891 100644
--- a/src/pages/overview/index.tsx
+++ b/src/pages/overview/index.tsx
@@ -1,11 +1,12 @@
import { type Component, createResource, Show } from "solid-js";
import type { RouteSectionProps } from "@solidjs/router";
-import { allThreadsOverviewQuery, overallSentMessagesQuery, SELF_ID } from "~/db";
+import { allThreadsOverviewQuery, overallSentMessagesQuery } from "~/db-queries";
import { OverviewTable, type RoomOverview } from "./overview-table";
import { getNameFromRecipient } from "~/lib/get-name-from-recipient";
import { Title } from "@solidjs/meta";
+import { SELF_ID } from "~/db";
export const Overview: Component = () => {
const [allSelfSentMessagesCount] = createResource(() => overallSentMessagesQuery(SELF_ID));
diff --git a/src/pages/overview/overview-table.tsx b/src/pages/overview/overview-table.tsx
index 5e896fb..2d72261 100644
--- a/src/pages/overview/overview-table.tsx
+++ b/src/pages/overview/overview-table.tsx
@@ -1,5 +1,5 @@
import { type Component, createSignal, For, Match, Show, Switch } from "solid-js";
-import { useNavigate } from "@solidjs/router";
+import { useNavigate, usePreloadRoute } from "@solidjs/router";
import {
type ColumnFiltersState,
@@ -191,6 +191,8 @@ interface OverviewTableProps {
}
export const OverviewTable = (props: OverviewTableProps) => {
+ const preload = usePreloadRoute();
+
const [sorting, setSorting] = createSignal([
{
id: "messageCount",
@@ -314,6 +316,26 @@ export const OverviewTable = (props: OverviewTableProps) => {
{
+ const threadId = row.original.threadId;
+ const isGroup = row.original.isGroup;
+
+ const preloadTimeout = setTimeout(() => {
+ preload(`/${isGroup ? "group" : "dm"}/${threadId.toString()}`, {
+ preloadData: true,
+ });
+ }, 20);
+
+ event.currentTarget.addEventListener(
+ "pointerout",
+ () => {
+ clearTimeout(preloadTimeout);
+ },
+ {
+ once: true,
+ },
+ );
+ }}
onClick={() => {
const threadId = row.original.threadId;
const isGroup = row.original.isGroup;
diff --git a/tsconfig.json b/tsconfig.json
index 8281cfe..059106e 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -13,6 +13,7 @@
"isolatedModules": true,
"verbatimModuleSyntax": true,
"baseUrl": ".",
+ "lib": ["ESNext", "DOM", "WebWorker"],
"paths": {
"~/*": ["./src/*"]
}
diff --git a/vite.config.ts b/vite.config.ts
index cbe9833..bf84311 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -15,4 +15,7 @@ export default defineConfig({
"~": path.resolve(__dirname, "./src"),
},
},
+ worker: {
+ format: "es",
+ },
});