feat: cache stats sources
This commit is contained in:
parent
ec1ea5cfe8
commit
cad305aa66
14 changed files with 98 additions and 92 deletions
26
index.html
26
index.html
|
@ -1,16 +1,18 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="theme-color" content="#000000" />
|
|
||||||
<link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.ico" />
|
|
||||||
<title>Solid App</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
|
|
||||||
<script src="/src/index.tsx" type="module"></script>
|
<head>
|
||||||
</body>
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<!-- <link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.ico" /> -->
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
|
||||||
|
<script src="/src/index.tsx" type="module"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
|
@ -47,6 +47,7 @@
|
||||||
"kysely": "^0.27.5",
|
"kysely": "^0.27.5",
|
||||||
"kysely-wasm": "^0.7.0",
|
"kysely-wasm": "^0.7.0",
|
||||||
"lucide-solid": "^0.468.0",
|
"lucide-solid": "^0.468.0",
|
||||||
|
"seroval": "^1.1.1",
|
||||||
"solid-js": "^1.9.3",
|
"solid-js": "^1.9.3",
|
||||||
"sql.js": "^1.12.0",
|
"sql.js": "^1.12.0",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
|
|
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
|
@ -56,6 +56,9 @@ importers:
|
||||||
lucide-solid:
|
lucide-solid:
|
||||||
specifier: ^0.468.0
|
specifier: ^0.468.0
|
||||||
version: 0.468.0(solid-js@1.9.3)
|
version: 0.468.0(solid-js@1.9.3)
|
||||||
|
seroval:
|
||||||
|
specifier: ^1.1.1
|
||||||
|
version: 1.1.1
|
||||||
solid-js:
|
solid-js:
|
||||||
specifier: ^1.9.3
|
specifier: ^1.9.3
|
||||||
version: 1.9.3
|
version: 1.9.3
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { createRoot, on, createDeferred } from "solid-js";
|
import { createRoot, on, createDeferred } from "solid-js";
|
||||||
|
import { serialize, deserialize } from "seroval";
|
||||||
|
|
||||||
const DATABASE_HASH_PREFIX = "database";
|
const DATABASE_HASH_PREFIX = "database";
|
||||||
|
|
||||||
|
@ -64,10 +65,12 @@ class LocalStorageCacheAdapter {
|
||||||
this.keys.add(fullKey);
|
this.keys.add(fullKey);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(fullKey, JSON.stringify(value));
|
localStorage.setItem(fullKey, serialize(value));
|
||||||
} catch (error: unknown) {
|
} 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");
|
console.error("Storage quota exceeded, not caching new function calls");
|
||||||
|
} else {
|
||||||
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,7 +83,7 @@ class LocalStorageCacheAdapter {
|
||||||
get<R>(cacheName: string, key: string): R | undefined {
|
get<R>(cacheName: string, key: string): R | undefined {
|
||||||
const item = localStorage.getItem(this.#createKey(cacheName, key));
|
const item = localStorage.getItem(this.#createKey(cacheName, key));
|
||||||
if (item) {
|
if (item) {
|
||||||
return JSON.parse(item) as R;
|
return deserialize(item) as R;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { createMemo, type Accessor } from "solid-js";
|
|
||||||
import { getDateList, getHourList, getMonthList, getWeekdayList } from "./date";
|
import { getDateList, getHourList, getMonthList, getWeekdayList } from "./date";
|
||||||
import type { MessageOverview, MessageStats, Recipients } from "~/types";
|
import type { MessageOverview, MessageStats, Recipients } from "~/types";
|
||||||
import { isSameDay } from "date-fns";
|
import { isSameDay } from "date-fns";
|
||||||
|
import { cached } from "./db-cache";
|
||||||
|
|
||||||
export const hourNames = getHourList();
|
export const hourNames = getHourList();
|
||||||
|
|
||||||
|
@ -15,16 +15,14 @@ export const weekdayNames = getWeekdayList();
|
||||||
|
|
||||||
const initialWeekdayMap = [...weekdayNames.keys()];
|
const initialWeekdayMap = [...weekdayNames.keys()];
|
||||||
|
|
||||||
export const createMessageStatsSources = (
|
const createMessageStatsSourcesRaw = (messageOverview: MessageOverview, recipients: Recipients) => {
|
||||||
messageOverview: Accessor<MessageOverview>,
|
const initialRecipientMap = () => {
|
||||||
recipients: Accessor<Recipients>,
|
return Object.fromEntries(recipients.map(({ recipientId }) => [recipientId, 0]));
|
||||||
) => {
|
};
|
||||||
const initialRecipientMap = () => Object.fromEntries(recipients().map(({ recipientId }) => [recipientId, 0]));
|
|
||||||
|
|
||||||
const dateList = () => {
|
const dateList = () => {
|
||||||
const currentDmMessagesOverview = messageOverview();
|
const firstDate = messageOverview?.at(0)?.messageDate;
|
||||||
const firstDate = currentDmMessagesOverview?.at(0)?.messageDate;
|
const lastDate = messageOverview?.at(-1)?.messageDate;
|
||||||
const lastDate = currentDmMessagesOverview?.at(-1)?.messageDate;
|
|
||||||
if (firstDate && lastDate) {
|
if (firstDate && lastDate) {
|
||||||
return getDateList(firstDate, lastDate).map((date) => ({
|
return getDateList(firstDate, lastDate).map((date) => ({
|
||||||
totalMessages: 0,
|
totalMessages: 0,
|
||||||
|
@ -34,52 +32,51 @@ export const createMessageStatsSources = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return createMemo(() => {
|
const currentDateList = dateList();
|
||||||
const currentMessageOverview = messageOverview();
|
const currentInitialRecipientMap = initialRecipientMap();
|
||||||
const currentDateList = dateList();
|
|
||||||
const currentInitialRecipientMap = initialRecipientMap();
|
|
||||||
|
|
||||||
const messageStats: MessageStats = {
|
const messageStats: MessageStats = {
|
||||||
person: { ...currentInitialRecipientMap },
|
person: { ...currentInitialRecipientMap },
|
||||||
month: initialMonthMap.map(() => ({ ...currentInitialRecipientMap })),
|
month: initialMonthMap.map(() => ({ ...currentInitialRecipientMap })),
|
||||||
date: currentDateList ?? [],
|
date: currentDateList ?? [],
|
||||||
weekday: initialWeekdayMap.map(() => ({ ...currentInitialRecipientMap })),
|
weekday: initialWeekdayMap.map(() => ({ ...currentInitialRecipientMap })),
|
||||||
daytime: initialHoursMap.map(() => ({ ...currentInitialRecipientMap })),
|
daytime: initialHoursMap.map(() => ({ ...currentInitialRecipientMap })),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (currentMessageOverview && currentDateList) {
|
if (currentDateList) {
|
||||||
const { person, month, date, weekday, daytime } = messageStats;
|
const { person, month, date, weekday, daytime } = messageStats;
|
||||||
|
|
||||||
for (const message of currentMessageOverview) {
|
for (const message of messageOverview) {
|
||||||
const { messageDate } = message;
|
const { messageDate } = message;
|
||||||
|
|
||||||
// increment overall message count of a person
|
// increment overall message count of a person
|
||||||
person[message.fromRecipientId] += 1;
|
person[message.fromRecipientId] += 1;
|
||||||
|
|
||||||
// increment the message count of the message's month for this recipient
|
// increment the message count of the message's month for this recipient
|
||||||
month[messageDate.getMonth()][message.fromRecipientId] += 1;
|
month[messageDate.getMonth()][message.fromRecipientId] += 1;
|
||||||
|
|
||||||
// biome-ignore lint/style/noNonNullAssertion: <explanation>
|
// 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
|
// increment the message count of the message's date for this recipient
|
||||||
dateStatsEntry[message.fromRecipientId] += 1;
|
dateStatsEntry[message.fromRecipientId] += 1;
|
||||||
|
|
||||||
// increment the overall message count of the message's date
|
// increment the overall message count of the message's date
|
||||||
dateStatsEntry.totalMessages += 1;
|
dateStatsEntry.totalMessages += 1;
|
||||||
|
|
||||||
const weekdayOfDate = messageDate.getDay();
|
const weekdayOfDate = messageDate.getDay();
|
||||||
// we index starting with monday while the `Date` object indexes starting with Sunday
|
// we index starting with monday while the `Date` object indexes starting with Sunday
|
||||||
const weekdayIndex = weekdayOfDate === 0 ? 6 : weekdayOfDate - 1;
|
const weekdayIndex = weekdayOfDate === 0 ? 6 : weekdayOfDate - 1;
|
||||||
|
|
||||||
// increment the message count of the message's weekday for this recipient
|
// increment the message count of the message's weekday for this recipient
|
||||||
weekday[weekdayIndex][message.fromRecipientId] += 1;
|
weekday[weekdayIndex][message.fromRecipientId] += 1;
|
||||||
|
|
||||||
// increment the message count of the message's daytime for this recipient
|
// increment the message count of the message's daytime for this recipient
|
||||||
daytime[messageDate.getHours()][message.fromRecipientId] += 1;
|
daytime[messageDate.getHours()][message.fromRecipientId] += 1;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return messageStats;
|
return messageStats;
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createMessageStatsSources = cached(createMessageStatsSourcesRaw);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { type Component, createResource } from "solid-js";
|
import { type Component, createMemo, createResource } from "solid-js";
|
||||||
import type { RouteSectionProps } from "@solidjs/router";
|
import type { RouteSectionProps } from "@solidjs/router";
|
||||||
|
|
||||||
import { dmPartnerRecipientQuery, SELF_ID, threadMostUsedWordsQuery, threadSentMessagesOverviewQuery } from "~/db";
|
import { dmPartnerRecipientQuery, SELF_ID, threadMostUsedWordsQuery, threadSentMessagesOverviewQuery } from "~/db";
|
||||||
|
@ -35,7 +35,7 @@ export const DmId: Component<RouteSectionProps> = (props) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const [dmMessagesOverview] = createResource<MessageOverview>(async () => {
|
const [dmMessagesOverview] = createResource<MessageOverview | undefined>(async () => {
|
||||||
const dmMessageOverview = await threadSentMessagesOverviewQuery(dmId());
|
const dmMessageOverview = await threadSentMessagesOverviewQuery(dmId());
|
||||||
if (dmMessageOverview) {
|
if (dmMessageOverview) {
|
||||||
return dmMessageOverview.map((row) => {
|
return dmMessageOverview.map((row) => {
|
||||||
|
@ -61,16 +61,16 @@ export const DmId: Component<RouteSectionProps> = (props) => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
recipientId: SELF_ID,
|
|
||||||
name: "You",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const dmMessageStats = createMessageStatsSources(dmMessagesOverview, recipients);
|
const dmMessageStats = createMemo(() => {
|
||||||
|
const currentDmMessagesOverview = dmMessagesOverview();
|
||||||
|
const currentRecipients = recipients();
|
||||||
|
|
||||||
|
if (currentDmMessagesOverview && currentRecipients) {
|
||||||
|
return createMessageStatsSources(currentDmMessagesOverview, currentRecipients);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -78,26 +78,26 @@ export const DmId: Component<RouteSectionProps> = (props) => {
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<Heading level={1}>DM with {dmPartner()?.name}</Heading>
|
<Heading level={1}>DM with {dmPartner()?.name}</Heading>
|
||||||
<Heading level={2}>Chat timeline</Heading>
|
<Heading level={2}>Chat timeline</Heading>
|
||||||
<DmMessagesPerDate dateStats={dmMessageStats().date} recipients={recipients()} />
|
<DmMessagesPerDate dateStats={dmMessageStats()?.date} recipients={recipients()} />
|
||||||
<DmOverview messages={dmMessagesOverview()} />
|
<DmOverview messages={dmMessagesOverview()} />
|
||||||
<Heading level={2}>Messages per</Heading>
|
<Heading level={2}>Messages per</Heading>
|
||||||
|
|
||||||
<Grid cols={1} colsMd={2} class="gap-x-16 gap-y-16">
|
<Grid cols={1} colsMd={2} class="gap-x-16 gap-y-16">
|
||||||
<div>
|
<div>
|
||||||
<Heading level={3}>Person</Heading>
|
<Heading level={3}>Person</Heading>
|
||||||
<DmMessagesPerRecipient personStats={dmMessageStats().person} recipients={recipients()} />
|
<DmMessagesPerRecipient personStats={dmMessageStats()?.person} recipients={recipients()} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Heading level={3}>Daytime</Heading>
|
<Heading level={3}>Daytime</Heading>
|
||||||
<DmMessagesPerDaytime daytimeStats={dmMessageStats().daytime} recipients={recipients()} />
|
<DmMessagesPerDaytime daytimeStats={dmMessageStats()?.daytime} recipients={recipients()} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Heading level={3}>Month</Heading>
|
<Heading level={3}>Month</Heading>
|
||||||
<DmMessagesPerMonth monthStats={dmMessageStats().month} recipients={recipients()} />
|
<DmMessagesPerMonth monthStats={dmMessageStats()?.month} recipients={recipients()} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Heading level={3}>Weekday</Heading>
|
<Heading level={3}>Weekday</Heading>
|
||||||
<DmMessagesPerWeekday weekdayStats={dmMessageStats().weekday} recipients={recipients()} />
|
<DmMessagesPerWeekday weekdayStats={dmMessageStats()?.weekday} recipients={recipients()} />
|
||||||
</div>
|
</div>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Heading level={2}>Word cloud</Heading>
|
<Heading level={2}>Word cloud</Heading>
|
||||||
|
|
|
@ -4,14 +4,14 @@ import { LineChart } from "~/components/ui/charts";
|
||||||
import type { MessageStats, Recipients } from "~/types";
|
import type { MessageStats, Recipients } from "~/types";
|
||||||
|
|
||||||
export const DmMessagesPerDate: Component<{
|
export const DmMessagesPerDate: Component<{
|
||||||
dateStats: MessageStats["date"];
|
dateStats: MessageStats["date"] | undefined;
|
||||||
recipients: Recipients;
|
recipients: Recipients | undefined;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const dateChartData: Accessor<ChartData<"line"> | undefined> = () => {
|
const dateChartData: Accessor<ChartData<"line"> | undefined> = () => {
|
||||||
const currentDmMessages = props.dateStats;
|
const currentDmMessages = props.dateStats;
|
||||||
const currentRecipients = props.recipients;
|
const currentRecipients = props.recipients;
|
||||||
|
|
||||||
if (currentDmMessages) {
|
if (currentDmMessages && currentRecipients) {
|
||||||
const currentDmMessagesValues = Object.values(currentDmMessages);
|
const currentDmMessagesValues = Object.values(currentDmMessages);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -5,8 +5,8 @@ import type { MessageStats, Recipients } from "~/types";
|
||||||
import { hourNames } from "~/lib/messages";
|
import { hourNames } from "~/lib/messages";
|
||||||
|
|
||||||
export const DmMessagesPerDaytime: Component<{
|
export const DmMessagesPerDaytime: Component<{
|
||||||
daytimeStats: MessageStats["daytime"];
|
daytimeStats: MessageStats["daytime"] | undefined;
|
||||||
recipients: Recipients;
|
recipients: Recipients | undefined;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const daytimeChartData: Accessor<ChartData<"bar"> | undefined> = () => {
|
const daytimeChartData: Accessor<ChartData<"bar"> | undefined> = () => {
|
||||||
const currentMessagesPerHour = props.daytimeStats;
|
const currentMessagesPerHour = props.daytimeStats;
|
||||||
|
|
|
@ -5,8 +5,8 @@ import { monthNames } from "~/lib/messages";
|
||||||
import type { MessageStats, Recipients } from "~/types";
|
import type { MessageStats, Recipients } from "~/types";
|
||||||
|
|
||||||
export const DmMessagesPerMonth: Component<{
|
export const DmMessagesPerMonth: Component<{
|
||||||
monthStats: MessageStats["month"];
|
monthStats: MessageStats["month"] | undefined;
|
||||||
recipients: Recipients;
|
recipients: Recipients | undefined;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const monthChartData: Accessor<ChartData<"radar"> | undefined> = () => {
|
const monthChartData: Accessor<ChartData<"radar"> | undefined> = () => {
|
||||||
const currentMessagesPerMonth = props.monthStats;
|
const currentMessagesPerMonth = props.monthStats;
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { PieChart } from "~/components/ui/charts";
|
||||||
import type { MessageStats, Recipients } from "~/types";
|
import type { MessageStats, Recipients } from "~/types";
|
||||||
|
|
||||||
export const DmMessagesPerRecipient: Component<{
|
export const DmMessagesPerRecipient: Component<{
|
||||||
personStats: MessageStats["person"];
|
personStats: MessageStats["person"] | undefined;
|
||||||
recipients: Recipients;
|
recipients: Recipients | undefined;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const recipientChartData: Accessor<ChartData<"pie"> | undefined> = () => {
|
const recipientChartData: Accessor<ChartData<"pie"> | undefined> = () => {
|
||||||
const currentMessagesPerRecipient = props.personStats;
|
const currentMessagesPerRecipient = props.personStats;
|
||||||
|
|
|
@ -5,8 +5,8 @@ import { weekdayNames } from "~/lib/messages";
|
||||||
import type { MessageStats, Recipients } from "~/types";
|
import type { MessageStats, Recipients } from "~/types";
|
||||||
|
|
||||||
export const DmMessagesPerWeekday: Component<{
|
export const DmMessagesPerWeekday: Component<{
|
||||||
weekdayStats: MessageStats["weekday"];
|
weekdayStats: MessageStats["weekday"] | undefined;
|
||||||
recipients: Recipients;
|
recipients: Recipients | undefined;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const weekdayChartData: Accessor<ChartData<"radar"> | undefined> = () => {
|
const weekdayChartData: Accessor<ChartData<"radar"> | undefined> = () => {
|
||||||
const currentMessagesPerWeekday = props.weekdayStats;
|
const currentMessagesPerWeekday = props.weekdayStats;
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { getDistanceBetweenDatesInDays } from "~/lib/date";
|
||||||
import type { MessageOverview } from "~/types";
|
import type { MessageOverview } from "~/types";
|
||||||
|
|
||||||
export const DmOverview: Component<{
|
export const DmOverview: Component<{
|
||||||
messages: MessageOverview;
|
messages: MessageOverview | undefined;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const dmOverview = () => {
|
const dmOverview = () => {
|
||||||
const firstMessageDate = props.messages?.at(0)?.messageDate;
|
const firstMessageDate = props.messages?.at(0)?.messageDate;
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { type RouteSectionProps, useNavigate } from "@solidjs/router";
|
||||||
import { setDb, SQL } from "~/db";
|
import { setDb, SQL } from "~/db";
|
||||||
import { Portal } from "solid-js/web";
|
import { Portal } from "solid-js/web";
|
||||||
import { Flex } from "~/components/ui/flex";
|
import { Flex } from "~/components/ui/flex";
|
||||||
|
import { Title } from "@solidjs/meta";
|
||||||
|
|
||||||
export const Home: Component<RouteSectionProps> = () => {
|
export const Home: Component<RouteSectionProps> = () => {
|
||||||
const [isLoadingDb, setIsLoadingDb] = createSignal(false);
|
const [isLoadingDb, setIsLoadingDb] = createSignal(false);
|
||||||
|
@ -38,6 +39,7 @@ export const Home: Component<RouteSectionProps> = () => {
|
||||||
</Flex>
|
</Flex>
|
||||||
</Show>
|
</Show>
|
||||||
</Portal>
|
</Portal>
|
||||||
|
<Title>Signal stats</Title>
|
||||||
<div>
|
<div>
|
||||||
<input type="file" accept=".sqlite" onChange={onFileChange}></input>
|
<input type="file" accept=".sqlite" onChange={onFileChange}></input>
|
||||||
</div>
|
</div>
|
||||||
|
|
10
src/types.ts
10
src/types.ts
|
@ -1,9 +1,7 @@
|
||||||
export type MessageOverview =
|
export type MessageOverview = {
|
||||||
| {
|
messageDate: Date;
|
||||||
messageDate: Date;
|
fromRecipientId: number;
|
||||||
fromRecipientId: number;
|
}[];
|
||||||
}[]
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
export type Recipients = {
|
export type Recipients = {
|
||||||
recipientId: number;
|
recipientId: number;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue