feat(dm): more stats
This commit is contained in:
parent
143219ef56
commit
38091f2c1a
19 changed files with 798 additions and 1106 deletions
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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
89
src/lib/messages.ts
Normal 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;
|
||||
});
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue