feat: show message when loading the database

This commit is contained in:
Samuel 2024-12-18 17:13:34 +01:00
parent 38091f2c1a
commit 3d49a25cae
12 changed files with 99 additions and 150 deletions

View file

@ -44,6 +44,7 @@
"noUnreachable": "error",
"noUnreachableSuper": "error",
"noUnsafeFinally": "error",
"noUnusedImports": "error",
"noUnsafeOptionalChaining": "error",
"noUnusedLabels": "error",
"noUnusedPrivateClassMembers": "error",
@ -107,13 +108,7 @@
}
}
},
"ignore": [
"dist/**/*.ts",
"dist/**",
"**/*.mjs",
"eslint.config.js",
"**/*.js"
]
"ignore": ["dist/**/*.ts", "dist/**", "**/*.mjs", "eslint.config.js", "**/*.js"]
},
"javascript": {
"formatter": {

View file

@ -1,7 +1,5 @@
import { type Component } from "solid-js";
import { Route } from "@solidjs/router";
import { allThreadsOverviewQuery } from "./db";
import { DmId, GroupId, Home, Overview } from "./pages";
import "./app.css";

View file

@ -1,21 +1,21 @@
import type { Component, ComponentProps } from "solid-js"
import { mergeProps, splitProps } from "solid-js"
import type { Component, ComponentProps } from "solid-js";
import { mergeProps, splitProps } from "solid-js";
import { cn } from "~/lib/utils"
import { cn } from "~/lib/utils";
type Cols = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
type Span = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13
type Cols = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
type Span = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13;
type GridProps = ComponentProps<"div"> & {
cols?: Cols
colsSm?: Cols
colsMd?: Cols
colsLg?: Cols
}
cols?: Cols;
colsSm?: Cols;
colsMd?: Cols;
colsLg?: Cols;
};
const Grid: Component<GridProps> = (rawProps) => {
const props = mergeProps({ cols: 1 } satisfies GridProps, rawProps)
const [local, others] = splitProps(props, ["cols", "colsSm", "colsMd", "colsLg", "class"])
const props = mergeProps({ cols: 1 } satisfies GridProps, rawProps);
const [local, others] = splitProps(props, ["cols", "colsSm", "colsMd", "colsLg", "class"]);
return (
<div
@ -25,23 +25,23 @@ const Grid: Component<GridProps> = (rawProps) => {
local.colsSm && gridColsSm[local.colsSm],
local.colsMd && gridColsMd[local.colsMd],
local.colsLg && gridColsLg[local.colsLg],
local.class
local.class,
)}
{...others}
/>
)
}
);
};
type ColProps = ComponentProps<"div"> & {
span?: Span
spanSm?: Span
spanMd?: Span
spanLg?: Span
}
span?: Span;
spanSm?: Span;
spanMd?: Span;
spanLg?: Span;
};
const Col: Component<ColProps> = (rawProps) => {
const props = mergeProps({ span: 1 as Span }, rawProps)
const [local, others] = splitProps(props, ["span", "spanSm", "spanMd", "spanLg", "class"])
const props = mergeProps({ span: 1 as Span }, rawProps);
const [local, others] = splitProps(props, ["span", "spanSm", "spanMd", "spanLg", "class"]);
return (
<div
@ -50,14 +50,14 @@ const Col: Component<ColProps> = (rawProps) => {
local.spanSm && colSpanSm[local.spanSm],
local.spanMd && colSpanMd[local.spanMd],
local.spanLg && colSpanLg[local.spanLg],
local.class
local.class,
)}
{...others}
/>
)
}
);
};
export { Grid, Col }
export { Grid, Col };
const gridCols: { [key in Cols]: string } = {
0: "grid-cols-none",
@ -72,8 +72,8 @@ const gridCols: { [key in Cols]: string } = {
9: "grid-cols-9",
10: "grid-cols-10",
11: "grid-cols-11",
12: "grid-cols-12"
}
12: "grid-cols-12",
};
const gridColsSm: { [key in Cols]: string } = {
0: "sm:grid-cols-none",
@ -88,8 +88,8 @@ const gridColsSm: { [key in Cols]: string } = {
9: "sm:grid-cols-9",
10: "sm:grid-cols-10",
11: "sm:grid-cols-11",
12: "sm:grid-cols-12"
}
12: "sm:grid-cols-12",
};
const gridColsMd: { [key in Cols]: string } = {
0: "md:grid-cols-none",
@ -104,8 +104,8 @@ const gridColsMd: { [key in Cols]: string } = {
9: "md:grid-cols-9",
10: "md:grid-cols-10",
11: "md:grid-cols-11",
12: "md:grid-cols-12"
}
12: "md:grid-cols-12",
};
const gridColsLg: { [key in Cols]: string } = {
0: "lg:grid-cols-none",
@ -120,8 +120,8 @@ const gridColsLg: { [key in Cols]: string } = {
9: "lg:grid-cols-9",
10: "lg:grid-cols-10",
11: "lg:grid-cols-11",
12: "lg:grid-cols-12"
}
12: "lg:grid-cols-12",
};
const colSpan: { [key in Span]: string } = {
1: "col-span-1",
@ -136,8 +136,8 @@ const colSpan: { [key in Span]: string } = {
10: "col-span-10",
11: "col-span-11",
12: "col-span-12",
13: "col-span-13"
}
13: "col-span-13",
};
const colSpanSm: { [key in Span]: string } = {
1: "sm:col-span-1",
@ -152,8 +152,8 @@ const colSpanSm: { [key in Span]: string } = {
10: "sm:col-span-10",
11: "sm:col-span-11",
12: "sm:col-span-12",
13: "sm:col-span-13"
}
13: "sm:col-span-13",
};
const colSpanMd: { [key in Span]: string } = {
1: "md:col-span-1",
@ -168,8 +168,8 @@ const colSpanMd: { [key in Span]: string } = {
10: "md:col-span-10",
11: "md:col-span-11",
12: "md:col-span-12",
13: "md:col-span-13"
}
13: "md:col-span-13",
};
const colSpanLg: { [key in Span]: string } = {
1: "lg:col-span-1",
@ -184,5 +184,5 @@ const colSpanLg: { [key in Span]: string } = {
10: "lg:col-span-10",
11: "lg:col-span-11",
12: "lg:col-span-12",
13: "lg:col-span-13"
}
13: "lg:col-span-13",
};

View file

@ -1,12 +1,4 @@
import {
type Accessor,
createEffect,
createMemo,
createRoot,
createSignal,
DEV,
type Setter,
} from "solid-js";
import { type Accessor, createEffect, createMemo, createRoot, createSignal, DEV, type Setter } from "solid-js";
import { Kysely, type NotNull, sql } from "kysely";
import type { DB } from "kysely-codegen";
@ -79,19 +71,13 @@ const allThreadsOverviewQueryRaw = kyselyDb()
(eb) =>
eb
.selectFrom("message")
.select((eb) => [
"message.thread_id",
eb.fn.countAll().as("message_count"),
])
.select((eb) => ["message.thread_id", eb.fn.countAll().as("message_count")])
.where((eb) => {
return eb.and([
eb("message.body", "is not", null),
eb("message.body", "is not", ""),
]);
return eb.and([eb("message.body", "is not", null), eb("message.body", "is not", "")]);
})
.groupBy("message.thread_id")
.as("message"),
(join) => join.onRef("message.thread_id", "=", "thread._id")
(join) => join.onRef("message.thread_id", "=", "thread._id"),
)
.innerJoin("recipient", "thread.recipient_id", "recipient._id")
.leftJoin("groups", "recipient._id", "groups.recipient_id")
@ -114,9 +100,7 @@ const allThreadsOverviewQueryRaw = kyselyDb()
}>()
.compile();
export const allThreadsOverviewQuery = cached(() =>
kyselyDb().executeQuery(allThreadsOverviewQueryRaw)
);
export const allThreadsOverviewQuery = cached(() => kyselyDb().executeQuery(allThreadsOverviewQueryRaw));
const overallSentMessagesQueryRaw = (recipientId: number) =>
kyselyDb()
@ -127,7 +111,7 @@ const overallSentMessagesQueryRaw = (recipientId: number) =>
eb("message.from_recipient_id", "=", recipientId),
eb("message.body", "is not", null),
eb("message.body", "!=", ""),
])
]),
)
.executeTakeFirst();
@ -143,9 +127,7 @@ const dmPartnerRecipientQueryRaw = (dmId: number) =>
"recipient.nickname_joined_name",
])
.innerJoin("thread", "recipient._id", "thread.recipient_id")
.where((eb) =>
eb.and([eb("thread._id", "=", dmId), eb("recipient._id", "!=", SELF_ID)])
)
.where((eb) => eb.and([eb("thread._id", "=", dmId), eb("recipient._id", "!=", SELF_ID)]))
.$narrowType<{
_id: number;
}>()
@ -156,25 +138,12 @@ export const dmPartnerRecipientQuery = cached(dmPartnerRecipientQueryRaw);
const threadSentMessagesOverviewQueryRaw = (threadId: number) =>
kyselyDb()
.selectFrom("message")
.select([
"from_recipient_id",
sql<string>`datetime(date_sent / 1000, 'unixepoch')`.as(
"message_datetime"
),
])
.select(["from_recipient_id", sql<string>`datetime(date_sent / 1000, 'unixepoch')`.as("message_datetime")])
.orderBy(["message_datetime"])
.where((eb) =>
eb.and([
eb("body", "is not", null),
eb("body", "!=", ""),
eb("thread_id", "=", threadId),
])
)
.where((eb) => eb.and([eb("body", "is not", null), eb("body", "!=", ""), eb("thread_id", "=", threadId)]))
.execute();
export const threadSentMessagesOverviewQuery = cached(
threadSentMessagesOverviewQueryRaw
);
export const threadSentMessagesOverviewQuery = cached(threadSentMessagesOverviewQueryRaw);
const threadMostUsedWordsQueryRaw = (threadId: number, limit = 10) =>
kyselyDb()
@ -185,16 +154,12 @@ const threadMostUsedWordsQueryRaw = (threadId: number, limit = 10) =>
sql`LOWER(substr(body, 1, instr(body || " ", " ") - 1))`.as("word"),
sql`(substr(body, instr(body || " ", " ") + 1))`.as("rest"),
])
.where((eb) =>
eb.and([eb("body", "is not", null), eb("thread_id", "=", threadId)])
)
.where((eb) => eb.and([eb("body", "is not", null), eb("thread_id", "=", threadId)]))
.unionAll((ebInner) => {
return ebInner
.selectFrom("words")
.select([
sql`LOWER(substr(rest, 1, instr(rest || " ", " ") - 1))`.as(
"word"
),
sql`LOWER(substr(rest, 1, instr(rest || " ", " ") - 1))`.as("word"),
sql`(substr(rest, instr(rest || " ", " ") + 1))`.as("rest"),
])
.where("rest", "<>", "");

View file

@ -10,18 +10,14 @@ export const getDistanceBetweenDatesInDays = (a: Date, b: Date) => {
};
// https://dev.to/pretaporter/how-to-get-month-list-in-your-language-4lfb
export const getMonthList = (
locales?: string | string[],
format: "long" | "short" = "long"
): string[] => {
export const getMonthList = (locales?: string | string[], format: "long" | "short" = "long"): string[] => {
const year = new Date().getFullYear(); // 2020
const monthList = [...Array(12).keys()]; // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
const formatter = new Intl.DateTimeFormat(locales, {
month: format,
});
const getMonthName = (monthIndex: number) =>
formatter.format(new Date(year, monthIndex));
const getMonthName = (monthIndex: number) => formatter.format(new Date(year, monthIndex));
return monthList.map(getMonthName);
};
@ -48,10 +44,7 @@ export const getDateList = (startDate: Date, endDate: Date): Date[] => {
return dateArray;
};
export const getHourList = (
locales?: string | string[],
format: "numeric" | "2-digit" = "numeric"
): string[] => {
export const getHourList = (locales?: string | string[], format: "numeric" | "2-digit" = "numeric"): string[] => {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
@ -63,16 +56,12 @@ export const getHourList = (
hourCycle: "h11",
});
const getHourName = (hourIndex: number) =>
formatter.format(new Date(year, month, day, hourIndex));
const getHourName = (hourIndex: number) => formatter.format(new Date(year, month, day, hourIndex));
return hourList.map(getHourName);
};
export const getWeekdayList = (
locales?: string | string[],
format: "long" | "short" | "narrow" = "long"
): string[] => {
export const getWeekdayList = (locales?: string | string[], format: "long" | "short" | "narrow" = "long"): string[] => {
const monday = new Date();
// set day to monday (w/o +1 it would be sunday)
monday.setDate(monday.getDate() - monday.getDay() + 1);
@ -86,8 +75,7 @@ export const getWeekdayList = (
weekday: format,
});
const getWeekDayName = (weekDayIndex: number) =>
formatter.format(new Date(year, month, mondayDate + weekDayIndex));
const getWeekDayName = (weekDayIndex: number) => formatter.format(new Date(year, month, mondayDate + weekDayIndex));
return hourList.map(getWeekDayName);
};

View file

@ -36,9 +36,7 @@ createRoot(() => {
createDeferred(
on(db, (currentDb) => {
if (currentDb) {
const newHash = hashString(
new TextDecoder().decode(currentDb.export())
).toString();
const newHash = hashString(new TextDecoder().decode(currentDb.export())).toString();
const oldHash = localStorage.getItem(HASH_STORE_KEY);
@ -48,15 +46,13 @@ createRoot(() => {
localStorage.setItem(HASH_STORE_KEY, newHash);
}
}
})
}),
);
});
});
class LocalStorageCacheAdapter {
keys = new Set<string>(
Object.keys(localStorage).filter((key) => key.startsWith(this.prefix))
);
keys = new Set<string>(Object.keys(localStorage).filter((key) => key.startsWith(this.prefix)));
prefix = "database";
#createKey(cacheName: string, key: string): string {
@ -70,10 +66,7 @@ class LocalStorageCacheAdapter {
try {
localStorage.setItem(fullKey, JSON.stringify(value));
} catch (error: unknown) {
if (
error instanceof DOMException &&
error.name === "QUOTA_EXCEEDED_ERR"
) {
if (error instanceof DOMException && error.name === "QUOTA_EXCEEDED_ERR") {
console.error("Storage quota exceeded, not caching new function calls");
}
}
@ -121,10 +114,7 @@ const createHashKey = (...args: unknown[]) => {
return hashString(stringToHash);
};
export const cached = <T extends unknown[], R, TT>(
fn: (...args: T) => R,
self?: ThisType<TT>
): ((...args: T) => R) => {
export const cached = <T extends unknown[], R, TT>(fn: (...args: T) => R, self?: ThisType<TT>): ((...args: T) => R) => {
const cacheName = hashString(fn.toString()).toString();
// important to return a promise on follow-up calls even if the data is immediately available

View file

@ -1,7 +1,7 @@
export const getNameFromRecipient = (
joinedNickname: string | null,
joinedSystemName: string | null,
joinedProfileName: string | null
joinedProfileName: string | null,
) => {
let name = "Could not determine name";

View file

@ -1,6 +1,5 @@
import { createEffect, createMemo, on, type Accessor } from "solid-js";
import { createMemo, type Accessor } from "solid-js";
import { getDateList, getHourList, getMonthList, getWeekdayList } from "./date";
import { cached } from "./db-cache";
import type { MessageOverview, MessageStats, Recipients } from "~/types";
import { isSameDay } from "date-fns";
@ -18,10 +17,9 @@ const initialWeekdayMap = [...weekdayNames.keys()];
export const createMessageStatsSources = (
messageOverview: Accessor<MessageOverview>,
recipients: Accessor<Recipients>
recipients: Accessor<Recipients>,
) => {
const initialRecipientMap = () =>
Object.fromEntries(recipients().map(({ recipientId }) => [recipientId, 0]));
const initialRecipientMap = () => Object.fromEntries(recipients().map(({ recipientId }) => [recipientId, 0]));
const dateList = () => {
const currentDmMessagesOverview = messageOverview();
@ -63,9 +61,7 @@ export const createMessageStatsSources = (
month[messageDate.getMonth() - 1][message.fromRecipientId] += 1;
// biome-ignore lint/style/noNonNullAssertion: <explanation>
const dateStatsEntry = date.find(({ date }) =>
isSameDay(date, messageDate)
)!;
const dateStatsEntry = date.find(({ date }) => isSameDay(date, messageDate))!;
// increment the message count of the message's date for this recipient
dateStatsEntry[message.fromRecipientId] += 1;

View file

@ -1,4 +1,4 @@
import { type Component, createResource, Show } from "solid-js";
import { type Component, createResource } from "solid-js";
import type { RouteSectionProps } from "@solidjs/router";
import { dmPartnerRecipientQuery, SELF_ID, threadMostUsedWordsQuery, threadSentMessagesOverviewQuery } from "~/db";

View file

@ -1,4 +1,4 @@
import { createEffect, Show, type Accessor, type Component } from "solid-js";
import { Show, type Accessor, type Component } from "solid-js";
import type { ChartData } from "chart.js";
import { LineChart } from "~/components/ui/charts";
import type { MessageStats, Recipients } from "~/types";

View file

@ -1,9 +1,12 @@
import { type Component, type JSX } from "solid-js";
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";
export const Home: Component<RouteSectionProps> = () => {
const [isLoadingDb, setIsLoadingDb] = createSignal(false);
const navigate = useNavigate();
const onFileChange: JSX.ChangeEventHandler<HTMLInputElement, Event> = (event) => {
@ -12,9 +15,14 @@ export const Home: Component<RouteSectionProps> = () => {
const reader = new FileReader();
reader.addEventListener("load", () => {
setIsLoadingDb(true);
setTimeout(() => {
const Uints = new Uint8Array(reader.result as ArrayBuffer);
setDb(new SQL.Database(Uints));
setIsLoadingDb(false);
navigate("/overview");
}, 10);
});
reader.readAsArrayBuffer(file);
@ -22,9 +30,18 @@ export const Home: Component<RouteSectionProps> = () => {
};
return (
<>
<Portal>
<Show when={isLoadingDb()}>
<Flex alignItems="center" justifyContent="center" class="fixed inset-0 backdrop-blur-lg backdrop-filter">
<p class="font-bold text-2xl">Loading database</p>
</Flex>
</Show>
</Portal>
<div>
<input type="file" accept=".sqlite" onChange={onFileChange}></input>
</div>
</>
);
};