feat(dm): more stats

This commit is contained in:
Samuel 2024-12-18 16:52:46 +01:00
parent 143219ef56
commit 38091f2c1a
19 changed files with 798 additions and 1106 deletions

View file

@ -8,3 +8,86 @@ export const getDistanceBetweenDatesInDays = (a: Date, b: Date) => {
return Math.floor((utc2 - utc1) / _MS_PER_DAY);
};
// https://dev.to/pretaporter/how-to-get-month-list-in-your-language-4lfb
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));
return monthList.map(getMonthName);
};
export 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;
};
export const getHourList = (
locales?: string | string[],
format: "numeric" | "2-digit" = "numeric"
): string[] => {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
const day = now.getDate();
const hourList = [...Array(24).keys()]; // [0, 1, 2, 3, 4, 5, 6, ..., 23]
const formatter = new Intl.DateTimeFormat(locales, {
hour: format,
hourCycle: "h11",
});
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[] => {
const monday = new Date();
// set day to monday (w/o +1 it would be sunday)
monday.setDate(monday.getDate() - monday.getDay() + 1);
const year = monday.getFullYear();
const month = monday.getMonth();
const mondayDate = monday.getDate();
const hourList = [...Array(7).keys()]; // [0, 1, 2, 3, 4, 5, 6]
const formatter = new Intl.DateTimeFormat(locales, {
weekday: format,
});
const getWeekDayName = (weekDayIndex: number) =>
formatter.format(new Date(year, month, mondayDate + weekDayIndex));
return hourList.map(getWeekDayName);
};

View file

@ -1,4 +1,4 @@
import { createEffect, createRoot, on } from "solid-js";
import { createRoot, on, createDeferred } from "solid-js";
const DATABASE_HASH_PREFIX = "database";
@ -27,35 +27,36 @@ const hashString = (str: string) => {
return hash;
};
const HASH_STORE_KEY = "database_hash";
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
queueMicrotask(() => {
createRoot(() => {
void import("~/db").then(({ db }) => {
createEffect(
on(db, (currentDb) => {
if (currentDb) {
const newHash = hashString(new TextDecoder().decode(currentDb.export())).toString();
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 oldHash = localStorage.getItem(HASH_STORE_KEY);
const oldHash = localStorage.getItem(HASH_STORE_KEY);
console.log(newHash, oldHash);
if (newHash !== oldHash) {
clearDbCache();
if (newHash !== oldHash) {
clearDbCache();
localStorage.setItem(HASH_STORE_KEY, newHash);
}
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 {
@ -66,7 +67,16 @@ class LocalStorageCacheAdapter {
const fullKey = this.#createKey(cacheName, key);
this.keys.add(fullKey);
localStorage.setItem(fullKey, JSON.stringify(value));
try {
localStorage.setItem(fullKey, JSON.stringify(value));
} catch (error: unknown) {
if (
error instanceof DOMException &&
error.name === "QUOTA_EXCEEDED_ERR"
) {
console.error("Storage quota exceeded, not caching new function calls");
}
}
}
has(cacheName: string, key: string): boolean {
@ -84,14 +94,44 @@ class LocalStorageCacheAdapter {
const cache = new LocalStorageCacheAdapter();
export const cached = <T extends unknown[], R, TT>(fn: (...args: T) => R, self?: ThisType<TT>): ((...args: T) => R) => {
const createHashKey = (...args: unknown[]) => {
let stringToHash = "";
for (const arg of args) {
switch (typeof arg) {
case "string":
stringToHash += arg;
break;
case "number":
case "bigint":
case "symbol":
case "function":
stringToHash += arg.toString();
break;
case "boolean":
case "undefined":
stringToHash += String(arg);
break;
case "object":
stringToHash += JSON.stringify(arg);
break;
}
}
return hashString(stringToHash);
};
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
let isPromise: boolean;
return (...args: T) => {
const cacheKey = JSON.stringify(args);
const cacheKey = createHashKey(...args).toString();
const cachedValue = cache.get<R>(cacheName, cacheKey);

89
src/lib/messages.ts Normal file
View file

@ -0,0 +1,89 @@
import { createEffect, createMemo, on, 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";
export const hourNames = getHourList();
const initialHoursMap = [...hourNames.keys()];
export const monthNames = getMonthList();
const initialMonthMap = [...monthNames.keys()];
export const weekdayNames = getWeekdayList();
const initialWeekdayMap = [...weekdayNames.keys()];
export const createMessageStatsSources = (
messageOverview: Accessor<MessageOverview>,
recipients: Accessor<Recipients>
) => {
const initialRecipientMap = () =>
Object.fromEntries(recipients().map(({ recipientId }) => [recipientId, 0]));
const dateList = () => {
const currentDmMessagesOverview = messageOverview();
const firstDate = currentDmMessagesOverview?.at(0)?.messageDate;
const lastDate = currentDmMessagesOverview?.at(-1)?.messageDate;
if (firstDate && lastDate) {
return getDateList(firstDate, lastDate).map((date) => ({
totalMessages: 0,
date,
...initialRecipientMap(),
}));
}
};
return createMemo(() => {
const currentMessageOverview = messageOverview();
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 (currentMessageOverview && currentDateList) {
const { person, month, date, weekday, daytime } = messageStats;
for (const message of currentMessageOverview) {
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
// months are an array from 0 - 11
month[messageDate.getMonth() - 1][message.fromRecipientId] += 1;
// biome-ignore lint/style/noNonNullAssertion: <explanation>
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;
daytime[messageDate.getHours()][message.fromRecipientId] += 1;
}
}
return messageStats;
});
};