From 84b5ef975554b6e5a73d34986bdf5573f7e80d4b Mon Sep 17 00:00:00 2001 From: Samuel Date: Wed, 18 Dec 2024 17:13:34 +0100 Subject: [PATCH] feat: show message when loading the database --- .vscode/settings.json | 2 +- biome.json | 9 +-- src/App.tsx | 2 - src/components/ui/grid.tsx | 84 +++++++++++++-------------- src/db.ts | 59 ++++--------------- src/lib/date.ts | 24 ++------ src/lib/db-cache.ts | 20 ++----- src/lib/get-name-from-recipient.ts | 2 +- src/lib/messages.ts | 12 ++-- src/pages/dm/dm-id.tsx | 2 +- src/pages/dm/dm-messages-per-date.tsx | 2 +- src/pages/home.tsx | 31 +++++++--- 12 files changed, 99 insertions(+), 150 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index a451915..d40f611 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,4 +7,4 @@ "editor.formatOnSave": true }, "typescript.inlayHints.parameterNames.enabled": "all" -} \ No newline at end of file +} diff --git a/biome.json b/biome.json index 2ef9167..1c4089f 100644 --- a/biome.json +++ b/biome.json @@ -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": { diff --git a/src/App.tsx b/src/App.tsx index 32ba558..980dd12 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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"; diff --git a/src/components/ui/grid.tsx b/src/components/ui/grid.tsx index 9ecd599..8e19352 100644 --- a/src/components/ui/grid.tsx +++ b/src/components/ui/grid.tsx @@ -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 = (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 (
= (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 = (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 (
= (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", +}; diff --git a/src/db.ts b/src/db.ts index 4dea490..f59d4b8 100644 --- a/src/db.ts +++ b/src/db.ts @@ -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`datetime(date_sent / 1000, 'unixepoch')`.as( - "message_datetime" - ), - ]) + .select(["from_recipient_id", sql`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", "<>", ""); diff --git a/src/lib/date.ts b/src/lib/date.ts index 6019974..d314a3c 100644 --- a/src/lib/date.ts +++ b/src/lib/date.ts @@ -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); }; diff --git a/src/lib/db-cache.ts b/src/lib/db-cache.ts index a4dd86e..bd92f6d 100644 --- a/src/lib/db-cache.ts +++ b/src/lib/db-cache.ts @@ -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( - Object.keys(localStorage).filter((key) => key.startsWith(this.prefix)) - ); + keys = new Set(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 = ( - fn: (...args: T) => R, - self?: ThisType -): ((...args: T) => R) => { +export const cached = (fn: (...args: T) => R, self?: ThisType): ((...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 diff --git a/src/lib/get-name-from-recipient.ts b/src/lib/get-name-from-recipient.ts index b3b6eee..ca3847e 100644 --- a/src/lib/get-name-from-recipient.ts +++ b/src/lib/get-name-from-recipient.ts @@ -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"; diff --git a/src/lib/messages.ts b/src/lib/messages.ts index f6d211d..95437df 100644 --- a/src/lib/messages.ts +++ b/src/lib/messages.ts @@ -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, - recipients: Accessor + recipients: Accessor, ) => { - 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: - 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; diff --git a/src/pages/dm/dm-id.tsx b/src/pages/dm/dm-id.tsx index bc39a25..235c51e 100644 --- a/src/pages/dm/dm-id.tsx +++ b/src/pages/dm/dm-id.tsx @@ -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"; diff --git a/src/pages/dm/dm-messages-per-date.tsx b/src/pages/dm/dm-messages-per-date.tsx index 4f3932c..4cfff39 100644 --- a/src/pages/dm/dm-messages-per-date.tsx +++ b/src/pages/dm/dm-messages-per-date.tsx @@ -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"; diff --git a/src/pages/home.tsx b/src/pages/home.tsx index 58100f9..3427c1a 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -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 = () => { + const [isLoadingDb, setIsLoadingDb] = createSignal(false); const navigate = useNavigate(); const onFileChange: JSX.ChangeEventHandler = (event) => { @@ -12,9 +15,14 @@ export const Home: Component = () => { const reader = new FileReader(); reader.addEventListener("load", () => { - const Uints = new Uint8Array(reader.result as ArrayBuffer); - setDb(new SQL.Database(Uints)); - navigate("/overview"); + 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 = () => { }; return ( -
- -
+ <> + + + +

Loading database

+
+
+
+
+ +
+ ); };