Initial commit
This commit is contained in:
commit
28ec24b2c2
26 changed files with 4372 additions and 0 deletions
33
src/App.module.css
Normal file
33
src/App.module.css
Normal file
|
@ -0,0 +1,33 @@
|
|||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #b318f0;
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
20
src/App.tsx
Normal file
20
src/App.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { type Component } from "solid-js";
|
||||
import { Route } from "@solidjs/router";
|
||||
import { Home, Overview } from "./pages";
|
||||
|
||||
const App: Component = () => {
|
||||
return (
|
||||
<>
|
||||
<Route
|
||||
path="/"
|
||||
component={Home}
|
||||
/>
|
||||
<Route
|
||||
path="/overview"
|
||||
component={Overview}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
141
src/app.css
Normal file
141
src/app.css
Normal file
|
@ -0,0 +1,141 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--info: 204 94% 94%;
|
||||
--info-foreground: 199 89% 48%;
|
||||
|
||||
--success: 149 80% 90%;
|
||||
--success-foreground: 160 84% 39%;
|
||||
|
||||
--warning: 48 96% 89%;
|
||||
--warning-foreground: 25 95% 53%;
|
||||
|
||||
--error: 0 93% 94%;
|
||||
--error-foreground: 0 84% 60%;
|
||||
|
||||
--ring: 240 5.9% 10%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark,
|
||||
[data-kb-theme="dark"] {
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--info: 204 94% 94%;
|
||||
--info-foreground: 199 89% 48%;
|
||||
|
||||
--success: 149 80% 90%;
|
||||
--success-foreground: 160 84% 39%;
|
||||
|
||||
--warning: 48 96% 89%;
|
||||
--warning-foreground: 25 95% 53%;
|
||||
|
||||
--error: 0 93% 94%;
|
||||
--error-foreground: 0 84% 60%;
|
||||
|
||||
--ring: 240 4.9% 83.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-feature-settings:
|
||||
"rlig" 1,
|
||||
"calt" 1;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.step {
|
||||
counter-increment: step;
|
||||
}
|
||||
|
||||
.step:before {
|
||||
@apply absolute w-9 h-9 bg-muted rounded-full font-mono font-medium text-center text-base inline-flex items-center justify-center -indent-px border-4 border-background;
|
||||
@apply ml-[-50px] mt-[-4px];
|
||||
content: counter(step);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.container {
|
||||
@apply px-4;
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 9999px;
|
||||
border: 4px solid transparent;
|
||||
background-clip: content-box;
|
||||
@apply bg-accent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
display: none;
|
||||
}
|
292
src/components/ui/charts.tsx
Normal file
292
src/components/ui/charts.tsx
Normal file
|
@ -0,0 +1,292 @@
|
|||
import type { Component } from "solid-js"
|
||||
import { createEffect, createSignal, mergeProps, on, onCleanup, onMount } from "solid-js"
|
||||
import { unwrap } from "solid-js/store"
|
||||
|
||||
import type { Ref } from "@solid-primitives/refs"
|
||||
import { mergeRefs } from "@solid-primitives/refs"
|
||||
import type {
|
||||
ChartComponent,
|
||||
ChartData,
|
||||
ChartItem,
|
||||
ChartOptions,
|
||||
Plugin as ChartPlugin,
|
||||
ChartType,
|
||||
ChartTypeRegistry,
|
||||
TooltipModel
|
||||
} from "chart.js"
|
||||
import {
|
||||
ArcElement,
|
||||
BarController,
|
||||
BarElement,
|
||||
BubbleController,
|
||||
CategoryScale,
|
||||
Chart,
|
||||
Colors,
|
||||
DoughnutController,
|
||||
Filler,
|
||||
Legend,
|
||||
LinearScale,
|
||||
LineController,
|
||||
LineElement,
|
||||
PieController,
|
||||
PointElement,
|
||||
PolarAreaController,
|
||||
RadarController,
|
||||
RadialLinearScale,
|
||||
ScatterController,
|
||||
Tooltip
|
||||
} from "chart.js"
|
||||
|
||||
type TypedChartProps = {
|
||||
data: ChartData
|
||||
options?: ChartOptions
|
||||
plugins?: ChartPlugin[]
|
||||
ref?: Ref<HTMLCanvasElement | null>
|
||||
width?: number | undefined
|
||||
height?: number | undefined
|
||||
}
|
||||
|
||||
type ChartProps = TypedChartProps & {
|
||||
type: ChartType
|
||||
}
|
||||
|
||||
type ChartContext = {
|
||||
chart: Chart
|
||||
tooltip: TooltipModel<keyof ChartTypeRegistry>
|
||||
}
|
||||
|
||||
const BaseChart: Component<ChartProps> = (rawProps) => {
|
||||
const [canvasRef, setCanvasRef] = createSignal<HTMLCanvasElement | null>()
|
||||
const [chart, setChart] = createSignal<Chart>()
|
||||
|
||||
const props = mergeProps(
|
||||
{
|
||||
width: 512,
|
||||
height: 512,
|
||||
options: { responsive: true } as ChartOptions,
|
||||
plugins: [] as ChartPlugin[]
|
||||
},
|
||||
rawProps
|
||||
)
|
||||
|
||||
const init = () => {
|
||||
const ctx = canvasRef()?.getContext("2d") as ChartItem
|
||||
const config = unwrap(props)
|
||||
const chart = new Chart(ctx, {
|
||||
type: config.type,
|
||||
data: config.data,
|
||||
options: config.options,
|
||||
plugins: config.plugins
|
||||
})
|
||||
setChart(chart)
|
||||
}
|
||||
|
||||
onMount(() => init())
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => props.data,
|
||||
() => {
|
||||
chart()!.data = props.data
|
||||
chart()!.update()
|
||||
},
|
||||
{ defer: true }
|
||||
)
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => props.options,
|
||||
() => {
|
||||
chart()!.options = props.options
|
||||
chart()!.update()
|
||||
},
|
||||
{ defer: true }
|
||||
)
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
[() => props.width, () => props.height],
|
||||
() => {
|
||||
chart()!.resize(props.width, props.height)
|
||||
},
|
||||
{ defer: true }
|
||||
)
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => props.type,
|
||||
() => {
|
||||
const dimensions = [chart()!.width, chart()!.height]
|
||||
chart()!.destroy()
|
||||
init()
|
||||
chart()!.resize(...dimensions)
|
||||
},
|
||||
{ defer: true }
|
||||
)
|
||||
)
|
||||
|
||||
onCleanup(() => {
|
||||
chart()?.destroy()
|
||||
mergeRefs(props.ref, null)
|
||||
})
|
||||
|
||||
Chart.register(Colors, Filler, Legend, Tooltip)
|
||||
return (
|
||||
<canvas
|
||||
ref={mergeRefs(props.ref, (el) => setCanvasRef(el))}
|
||||
height={props.height}
|
||||
width={props.width}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function showTooltip(context: ChartContext) {
|
||||
let el = document.getElementById("chartjs-tooltip")
|
||||
if (!el) {
|
||||
el = document.createElement("div")
|
||||
el.id = "chartjs-tooltip"
|
||||
document.body.appendChild(el)
|
||||
}
|
||||
|
||||
const model = context.tooltip
|
||||
if (model.opacity === 0 || !model.body) {
|
||||
el.style.opacity = "0"
|
||||
return
|
||||
}
|
||||
|
||||
el.className = `p-2 bg-card text-card-foreground rounded-lg border shadow-sm text-sm ${
|
||||
model.yAlign ?? `no-transform`
|
||||
}`
|
||||
|
||||
let content = ""
|
||||
|
||||
model.title.forEach((title) => {
|
||||
content += `<h3 class="font-semibold leading-none tracking-tight">${title}</h3>`
|
||||
})
|
||||
|
||||
content += `<div class="mt-1 text-muted-foreground">`
|
||||
const body = model.body.flatMap((body) => body.lines)
|
||||
body.forEach((line, i) => {
|
||||
const colors = model.labelColors[i]
|
||||
content += `
|
||||
<div class="flex items-center">
|
||||
<span class="inline-block h-2 w-2 mr-1 rounded-full border" style="background: ${colors.backgroundColor}; border-color: ${colors.borderColor}"></span>
|
||||
${line}
|
||||
</div>`
|
||||
})
|
||||
content += `</div>`
|
||||
|
||||
el.innerHTML = content
|
||||
|
||||
const pos = context.chart.canvas.getBoundingClientRect()
|
||||
el.style.opacity = "1"
|
||||
el.style.position = "absolute"
|
||||
el.style.left = `${pos.left + window.scrollX + model.caretX}px`
|
||||
el.style.top = `${pos.top + window.scrollY + model.caretY}px`
|
||||
el.style.pointerEvents = "none"
|
||||
}
|
||||
|
||||
function createTypedChart(
|
||||
type: ChartType,
|
||||
components: ChartComponent[]
|
||||
): Component<TypedChartProps> {
|
||||
const chartsWithScales: ChartType[] = ["bar", "line", "scatter"]
|
||||
const chartsWithLegends: ChartType[] = ["bar", "line"]
|
||||
|
||||
const options: ChartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: chartsWithScales.includes(type)
|
||||
? {
|
||||
x: {
|
||||
border: { display: false },
|
||||
grid: { display: false }
|
||||
},
|
||||
y: {
|
||||
border: {
|
||||
dash: [3],
|
||||
dashOffset: 3,
|
||||
display: false
|
||||
},
|
||||
grid: {
|
||||
color: "hsla(240, 3.8%, 46.1%, 0.4)"
|
||||
}
|
||||
}
|
||||
}
|
||||
: {},
|
||||
plugins: {
|
||||
legend: chartsWithLegends.includes(type)
|
||||
? {
|
||||
display: true,
|
||||
align: "end",
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
boxWidth: 6,
|
||||
boxHeight: 6,
|
||||
color: "hsl(240, 3.8%, 46.1%)",
|
||||
font: { size: 14 }
|
||||
}
|
||||
}
|
||||
: { display: false },
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
external: (context) => showTooltip(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Chart.register(...components)
|
||||
return (props) => <BaseChart type={type} options={options} {...props} />
|
||||
}
|
||||
|
||||
const BarChart = /* #__PURE__ */ createTypedChart("bar", [
|
||||
BarController,
|
||||
BarElement,
|
||||
CategoryScale,
|
||||
LinearScale
|
||||
])
|
||||
const BubbleChart = /* #__PURE__ */ createTypedChart("bubble", [
|
||||
BubbleController,
|
||||
PointElement,
|
||||
LinearScale
|
||||
])
|
||||
const DonutChart = /* #__PURE__ */ createTypedChart("doughnut", [DoughnutController, ArcElement])
|
||||
const LineChart = /* #__PURE__ */ createTypedChart("line", [
|
||||
LineController,
|
||||
LineElement,
|
||||
PointElement,
|
||||
CategoryScale,
|
||||
LinearScale
|
||||
])
|
||||
const PieChart = /* #__PURE__ */ createTypedChart("pie", [PieController, ArcElement])
|
||||
const PolarAreaChart = /* #__PURE__ */ createTypedChart("polarArea", [
|
||||
PolarAreaController,
|
||||
ArcElement,
|
||||
RadialLinearScale
|
||||
])
|
||||
const RadarChart = /* #__PURE__ */ createTypedChart("radar", [
|
||||
RadarController,
|
||||
LineElement,
|
||||
PointElement,
|
||||
RadialLinearScale
|
||||
])
|
||||
const ScatterChart = /* #__PURE__ */ createTypedChart("scatter", [
|
||||
ScatterController,
|
||||
PointElement,
|
||||
LinearScale
|
||||
])
|
||||
|
||||
export {
|
||||
BaseChart as Chart,
|
||||
BarChart,
|
||||
BubbleChart,
|
||||
DonutChart,
|
||||
LineChart,
|
||||
PieChart,
|
||||
PolarAreaChart,
|
||||
RadarChart,
|
||||
ScatterChart
|
||||
}
|
89
src/db.ts
Normal file
89
src/db.ts
Normal file
|
@ -0,0 +1,89 @@
|
|||
import initSqlJS, { type Database } from "sql.js";
|
||||
|
||||
import wasmURL from "./assets/sql-wasm.wasm?url";
|
||||
import dbURL from "./assets/database.sqlite?url";
|
||||
import { createMemo, createSignal } from "solid-js";
|
||||
|
||||
export const SELF_ID = 2;
|
||||
|
||||
export const SQL = await initSqlJS({
|
||||
locateFile: () => wasmURL,
|
||||
});
|
||||
|
||||
const file = await fetch(dbURL).then((res) => res.arrayBuffer());
|
||||
|
||||
const testDb = new SQL.Database(new Uint8Array(file));
|
||||
|
||||
export const [db, setDb] = createSignal<Database>(testDb);
|
||||
|
||||
const createStatement = (sql: string) => {
|
||||
return createMemo(() => {
|
||||
return db().prepare(sql);
|
||||
});
|
||||
};
|
||||
|
||||
export const roomOverviewStmt = createStatement(`
|
||||
SELECT
|
||||
thread.recipient_id,
|
||||
thread.active,
|
||||
thread.archived,
|
||||
recipient.profile_joined_name,
|
||||
recipient.system_joined_name,
|
||||
groups.title,
|
||||
message_count,
|
||||
last_message_date
|
||||
FROM
|
||||
thread
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
thread_id,
|
||||
COUNT(*) AS message_count
|
||||
FROM
|
||||
message
|
||||
WHERE
|
||||
message.body IS NOT NULL
|
||||
AND message.body != ''
|
||||
GROUP BY
|
||||
thread_id
|
||||
) message_counts ON message_counts.thread_id = thread._id
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
thread_id,
|
||||
max(date_sent) AS last_message_date
|
||||
FROM
|
||||
message
|
||||
GROUP BY
|
||||
thread_id
|
||||
) last_messages ON last_messages.thread_id = thread._id
|
||||
JOIN recipient ON thread.recipient_id = recipient._id
|
||||
LEFT JOIN groups ON recipient._id = groups.recipient_id
|
||||
WHERE
|
||||
message_count > 0
|
||||
`);
|
||||
|
||||
export type RoomOverviewColumn = {
|
||||
recipient_id: number;
|
||||
active: 0 | 1;
|
||||
archived: 0 | 1;
|
||||
message_count: number;
|
||||
last_message_date: number;
|
||||
} & (
|
||||
| {
|
||||
profile_joined_name: string;
|
||||
system_joined_name: string | null;
|
||||
title: null;
|
||||
}
|
||||
| {
|
||||
profile_joined_name: null;
|
||||
system_joined_name: null;
|
||||
title: string;
|
||||
}
|
||||
);
|
||||
|
||||
export const overallSentMessagesStmt = createStatement(`
|
||||
SELECT
|
||||
COUNT(*) as message_count
|
||||
FROM
|
||||
message
|
||||
WHERE (message.from_recipient_id = :recipient_id AND message.body IS NOT NULL AND message.body != '')
|
||||
`);
|
13
src/index.css
Normal file
13
src/index.css
Normal file
|
@ -0,0 +1,13 @@
|
|||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
|
||||
'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
|
||||
'Helvetica Neue', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
23
src/index.tsx
Normal file
23
src/index.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
/* @refresh reload */
|
||||
import { render } from "solid-js/web";
|
||||
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
import { Router } from "@solidjs/router";
|
||||
|
||||
const root = document.getElementById("root");
|
||||
|
||||
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||
throw new Error(
|
||||
"Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?"
|
||||
);
|
||||
}
|
||||
|
||||
render(
|
||||
() => (
|
||||
<Router>
|
||||
<App />
|
||||
</Router>
|
||||
),
|
||||
root!
|
||||
);
|
53
src/lib/database.ts
Normal file
53
src/lib/database.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
const TABLES = [
|
||||
"attachment",
|
||||
"sqlite_sequence",
|
||||
"avatar_picker",
|
||||
"recipient",
|
||||
"thread",
|
||||
"message",
|
||||
"call",
|
||||
"call_link",
|
||||
"cds",
|
||||
"chat_colors",
|
||||
"chat_folder",
|
||||
"chat_folder_membership",
|
||||
"distribution_list",
|
||||
"distribution_list_member",
|
||||
"donation_receipt",
|
||||
"drafts",
|
||||
"emoji_search",
|
||||
"groups",
|
||||
"group_membership",
|
||||
"group_receipts",
|
||||
"identities",
|
||||
"in_app_payment",
|
||||
"in_app_payment_subscriber",
|
||||
"kyber_prekey",
|
||||
"mention",
|
||||
"message_fts",
|
||||
"message_fts_data",
|
||||
"message_fts_idx",
|
||||
"message_fts_docsize",
|
||||
"message_fts_config",
|
||||
"msl_payload",
|
||||
"msl_message",
|
||||
"msl_recipient",
|
||||
"name_collision",
|
||||
"name_collision_membership",
|
||||
"notification_profile",
|
||||
"notification_profile_allowed_members",
|
||||
"notification_profile_schedule",
|
||||
"one_time_prekeys",
|
||||
"payments",
|
||||
"pending_pni_signature_message",
|
||||
"pending_retry_receipts",
|
||||
"reaction",
|
||||
"remapped_recipients",
|
||||
"remapped_threads",
|
||||
"remote_megaphone",
|
||||
"sender_key_shared",
|
||||
"sender_keys",
|
||||
"sessions",
|
||||
"storage_key",
|
||||
"story_sends",
|
||||
];
|
44
src/lib/sql-queries.ts
Normal file
44
src/lib/sql-queries.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
// select messages from one chat / group
|
||||
`SELECT
|
||||
message.body,
|
||||
message.from_recipient_id,
|
||||
-- message."type" AS message_type,
|
||||
FROM
|
||||
message
|
||||
JOIN thread ON message.thread_id = thread._id
|
||||
JOIN recipient ON thread.recipient_id = recipient._id
|
||||
WHERE
|
||||
recipient._id = 1433
|
||||
AND
|
||||
message.body IS NOT NULL`;
|
||||
|
||||
// select messages from one chat / group with details of sender
|
||||
`SELECT
|
||||
message.body,
|
||||
message.from_recipient_id,
|
||||
-- message."type" AS message_type,
|
||||
sender.system_joined_name,
|
||||
sender.profile_joined_name
|
||||
FROM
|
||||
message
|
||||
JOIN thread ON message.thread_id = thread._id
|
||||
JOIN recipient AS r ON thread.recipient_id = r._id
|
||||
JOIN recipient AS sender ON message.from_recipient_id = sender._id
|
||||
WHERE
|
||||
r._id = 4
|
||||
AND message.body IS NOT NULL
|
||||
AND message.body != ''`;
|
||||
|
||||
// select all chats / groups with details
|
||||
`SELECT
|
||||
thread.recipient_id,
|
||||
thread.active,
|
||||
recipient.profile_joined_name,
|
||||
recipient.system_joined_name,
|
||||
groups.title
|
||||
FROM
|
||||
thread
|
||||
JOIN recipient ON thread.recipient_id = recipient._id
|
||||
LEFT JOIN groups ON recipient._id = groups.recipient_id
|
||||
WHERE (SELECT 1 FROM message WHERE message.thread_id = thread._id AND message.body IS NOT NULL)
|
||||
`;
|
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
32
src/pages/home.tsx
Normal file
32
src/pages/home.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { redirect, useNavigate, type RouteSectionProps } from "@solidjs/router";
|
||||
import { type Component, type JSX } from "solid-js";
|
||||
import { setDb, SQL } from "~/db";
|
||||
|
||||
export const Home: Component<RouteSectionProps> = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onFileChange: JSX.ChangeEventHandler<HTMLInputElement, Event> = (event) => {
|
||||
const file = event.currentTarget.files![0];
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.addEventListener("load", () => {
|
||||
const Uints = new Uint8Array(reader.result as ArrayBuffer);
|
||||
setDb(new SQL.Database(Uints));
|
||||
navigate("/overview");
|
||||
});
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
accept=".sqlite"
|
||||
onChange={onFileChange}
|
||||
></input>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
5
src/pages/index.tsx
Normal file
5
src/pages/index.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { lazy } from "solid-js";
|
||||
|
||||
export { Home } from "./home";
|
||||
|
||||
export const Overview = lazy(() => import("./overview"));
|
47
src/pages/overview.tsx
Normal file
47
src/pages/overview.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
import type { RouteSectionProps } from "@solidjs/router";
|
||||
import type { Component } from "solid-js";
|
||||
import { overallSentMessagesStmt, roomOverviewStmt, SELF_ID, type RoomOverviewColumn } from "~/db";
|
||||
|
||||
type RoomOverview = {
|
||||
recipientId: number;
|
||||
active: boolean;
|
||||
archived: boolean;
|
||||
messageCount: number;
|
||||
lastMessageDate: number;
|
||||
title: string;
|
||||
isGroup: boolean;
|
||||
}[];
|
||||
|
||||
export const Overview: Component<RouteSectionProps> = () => {
|
||||
const allSelfSentMessagesCount = overallSentMessagesStmt().getAsObject({
|
||||
":recipient_id": SELF_ID,
|
||||
});
|
||||
|
||||
const roomOverviewRaw: RoomOverviewColumn[] = [];
|
||||
|
||||
while (roomOverviewStmt().step()) {
|
||||
roomOverviewRaw.push(roomOverviewStmt().getAsObject() as RoomOverviewColumn);
|
||||
}
|
||||
|
||||
roomOverviewStmt().free();
|
||||
|
||||
const roomOverview: RoomOverview = roomOverviewRaw.map((column) => {
|
||||
const isGroup = Boolean(column.title);
|
||||
|
||||
return {
|
||||
recipientId: column.recipient_id,
|
||||
active: Boolean(column.active),
|
||||
archived: Boolean(column.archived),
|
||||
messageCount: column.message_count,
|
||||
lastMessageDate: column.last_message_date,
|
||||
title: isGroup ? column.title! : (column.system_joined_name ?? column.profile_joined_name)!,
|
||||
isGroup,
|
||||
};
|
||||
});
|
||||
|
||||
console.log(roomOverview);
|
||||
|
||||
return <p>All messages: {allSelfSentMessagesCount.message_count as number}</p>;
|
||||
};
|
||||
|
||||
export default Overview;
|
Loading…
Add table
Add a link
Reference in a new issue