messages screen improvement

This commit is contained in:
onysd 2026-09-08 01:19:33 +03:00
parent 280321b902
commit ae2cc3ba90
12 changed files with 438 additions and 111 deletions

View file

@ -1,6 +1,7 @@
import { ArrowLeft } from "lucide-react";
import { useEffect, useState } from "react";
import { api, errorMessage } from "../api";
import { MessageView } from "../components/MessageView";
import { Alert, Badge, EmptyRow, JsonBlock, LoadingSurface, PageFrame, SectionHead, Summary } from "../components/ui";
import { formatUnix } from "../lib/format";
import type { Navigate } from "../routing";
@ -38,32 +39,38 @@ export function GroupMessageDetailPage({ channelID, msgID, navigate }: { channel
actions={<button className="btn icon-text" onClick={() => navigate("/messages/groups")}><ArrowLeft size={15} /> {"Back to group messages"}</button>}
>
<div className="stacked-sections">
<section className="entity-head">
<div>
<div className="entity-title">{`Channel / Group ${msg.ChannelID}`}</div>
<div className="entity-subtitle">{`Sender ${msg.SenderUserID} · ${formatUnix(msg.Date)}`}</div>
</div>
<div className="entity-badges">
{msg.Deleted ? <Badge tone="danger">{"Deleted"}</Badge> : <Badge>{"Live"}</Badge>}
{msg.Pinned && <Badge tone="warn">{"Pinned"}</Badge>}
{msg.Post && <Badge>{"Channel post"}</Badge>}
<Badge>pts {msg.PTS}</Badge>
</div>
</section>
<MessageView
body={msg.Body}
media={msg.Media}
sender={msg.Post ? `Channel post in ${msg.ChannelID}` : `From ${msg.SenderUserID}`}
meta={`${formatUnix(msg.Date)}${msg.EditDate ? ` · edited ${formatUnix(msg.EditDate)}` : ""}${msg.ViewsCount ? ` · ${msg.ViewsCount} views` : ""}`}
badges={
<>
{msg.Deleted ? <Badge tone="danger">{"Deleted"}</Badge> : <Badge>{"Live"}</Badge>}
{msg.Pinned && <Badge tone="warn">{"Pinned"}</Badge>}
{msg.Post && <Badge>{"Channel post"}</Badge>}
</>
}
/>
<div className="summary-grid">
<Summary label={"Message ID"} value={String(msg.ID)} mono />
<Summary label={"Channel / Group"} value={String(msg.ChannelID)} mono />
<Summary label="From Peer" value={`${msg.FromPeerType}:${msg.FromPeerID}`} mono />
<Summary label={"Views"} value={String(msg.ViewsCount)} />
<Summary label={"pts"} value={String(msg.PTS)} mono />
</div>
<section className="section-block">
<SectionHead title={"Channel Message Row"} text={"channel_messages read-only snapshot"} />
<JsonBlock value={detail.MessageJSON} />
</section>
<section className="section-block">
<SectionHead title={"Channel Row"} text={"channels read-only snapshot"} />
<JsonBlock value={detail.ChannelJSON} />
</section>
<details className="raw-details">
<summary>{"Stored rows (JSON)"}</summary>
<div className="stacked-sections">
<section className="section-block">
<SectionHead title={"Channel Message Row"} text={"channel_messages read-only snapshot"} />
<JsonBlock value={detail.MessageJSON} />
</section>
<section className="section-block">
<SectionHead title={"Channel Row"} text={"channels read-only snapshot"} />
<JsonBlock value={detail.ChannelJSON} />
</section>
</div>
</details>
<section className="section-block">
<SectionHead title={"Channel Update Events"} text={"durable channel_update_events"} />
<div className="table-wrap">

View file

@ -2,12 +2,12 @@ import { ChevronRight, Search } from "lucide-react";
import { useState } from "react";
import { api, errorMessage } from "../api";
import { ChannelPicker } from "../components/EntityPicker";
import { Alert, Badge, EmptyRow, Metric, PageFrame, QueryPanel } from "../components/ui";
import { Alert, Badge, EmptyRow, Metric, QueryPanel } from "../components/ui";
import { channelKind, formatUnix } from "../lib/format";
import type { Navigate } from "../routing";
import type { ChannelRow, GroupMessageListResponse } from "../types";
export function GroupMessagesPage({ navigate }: { navigate: Navigate }) {
export function GroupMessagesTab({ navigate }: { navigate: Navigate }) {
const [channel, setChannel] = useState<ChannelRow | null>(null);
const [beforeDate, setBeforeDate] = useState("");
const [beforeID, setBeforeID] = useState("");
@ -52,7 +52,7 @@ export function GroupMessagesPage({ navigate }: { navigate: Navigate }) {
const rows = data?.rows ?? [];
return (
<PageFrame title={"Group Messages"} eyebrow={"Supergroup / channel messages"}>
<>
{error && <Alert>{error}</Alert>}
<QueryPanel>
<div className="message-selector-grid single">
@ -114,6 +114,6 @@ export function GroupMessagesPage({ navigate }: { navigate: Navigate }) {
</tbody>
</table>
</div>
</PageFrame>
</>
);
}

View file

@ -3,6 +3,7 @@ import { useEffect, useState } from "react";
import { api, errorMessage } from "../api";
import { ActionButton } from "../components/ActionButton";
import { Alert, Badge, EmptyRow, JsonBlock, LoadingSurface, PageFrame, SectionHead, SplitLayout, Summary } from "../components/ui";
import { MessageView } from "../components/MessageView";
import { formatDate, formatUnix } from "../lib/format";
import type { Navigate } from "../routing";
import type { MessageDetail } from "../types";
@ -41,37 +42,46 @@ export function MessageDetailPage({ ownerUserID, msgID, navigate }: { ownerUserI
<SplitLayout
main={
<div className="stacked-sections">
<section className="entity-head">
<div>
<div className="entity-title">{`Owner ${msg.OwnerUserID} · Peer ${msg.PeerID}`}</div>
<div className="entity-subtitle">{`Sender ${msg.FromUserID} · ${formatUnix(msg.Date)}`}</div>
</div>
<div className="entity-badges">
{msg.Deleted ? <Badge tone="danger">{"Deleted"}</Badge> : <Badge>{"Live"}</Badge>}
<Badge>pts {msg.PTS}</Badge>
<Badge>{msg.Outgoing ? "Outgoing" : "Incoming"}</Badge>
</div>
</section>
<MessageView
body={msg.Body}
media={msg.Media}
sender={`From ${msg.FromUserID}`}
meta={`${msg.Outgoing ? "Sent to" : "Received from"} ${msg.PeerID} · ${formatUnix(msg.Date)}`}
badges={
<>
{msg.Deleted ? <Badge tone="danger">{"Deleted"}</Badge> : <Badge>{"Live"}</Badge>}
<Badge>{msg.Outgoing ? "Outgoing" : "Incoming"}</Badge>
</>
}
/>
<div className="summary-grid">
<Summary label={"Message box ID"} value={String(msg.BoxID)} mono />
<Summary label={"Private message ID"} value={String(msg.PrivateMessageID)} mono />
<Summary label={"Message sender"} value={String(msg.MessageSenderID)} mono />
<Summary label={"Time"} value={formatUnix(msg.Date)} />
</div>
<section className="section-block">
<SectionHead title={"Message Box"} text={"message_boxes read-only snapshot"} />
<JsonBlock value={detail.MessageJSON} />
</section>
<div className="raw-grid">
<section className="section-block">
<SectionHead title={"Dialog Row"} text={"dialogs read-only snapshot"} />
<JsonBlock value={detail.DialogJSON} />
</section>
<section className="section-block">
<SectionHead title={"Private Message Row"} text={"private_messages read-only snapshot"} />
<JsonBlock value={detail.PrivateJSON} />
</section>
<Summary label={"pts"} value={String(msg.PTS)} mono />
</div>
{/* The stored rows stay reachable, but folded: they answer "why is
this message in this state", which is a rarer question than
"what does it say". */}
<details className="raw-details">
<summary>{"Stored rows (JSON)"}</summary>
<div className="stacked-sections">
<section className="section-block">
<SectionHead title={"Message Box"} text={"message_boxes read-only snapshot"} />
<JsonBlock value={detail.MessageJSON} />
</section>
<div className="raw-grid">
<section className="section-block">
<SectionHead title={"Dialog Row"} text={"dialogs read-only snapshot"} />
<JsonBlock value={detail.DialogJSON} />
</section>
<section className="section-block">
<SectionHead title={"Private Message Row"} text={"private_messages read-only snapshot"} />
<JsonBlock value={detail.PrivateJSON} />
</section>
</div>
</div>
</details>
<section className="section-block">
<SectionHead title={"Update Events"} text={"durable user_update_events"} />
<div className="table-wrap">

View file

@ -7,8 +7,9 @@ import { Alert, Badge, EmptyRow, Metric, PageFrame, QueryPanel } from "../compon
import { displayName, formatUnix, parseIDs, toInt } from "../lib/format";
import type { Navigate } from "../routing";
import type { AccountRow, MessageListResponse } from "../types";
import { GroupMessagesTab } from "./GroupMessagesPage";
export function MessagesPage({ navigate }: { navigate: Navigate }) {
export function PrivateMessagesTab({ navigate }: { navigate: Navigate }) {
const [owner, setOwner] = useState<AccountRow | null>(null);
const [peer, setPeer] = useState<AccountRow | null>(null);
const [beforeDate, setBeforeDate] = useState("");
@ -65,7 +66,7 @@ export function MessagesPage({ navigate }: { navigate: Navigate }) {
}
return (
<PageFrame title={"Private Messages"} eyebrow={"Private message boxes"}>
<>
{error && <Alert>{error}</Alert>}
<QueryPanel>
<div className="message-selector-grid">
@ -152,6 +153,42 @@ export function MessagesPage({ navigate }: { navigate: Navigate }) {
</tbody>
</table>
</div>
</>
);
}
// The two message stores are one screen with two tabs rather than two sidebar
// entries: they are the same job ("look at what was said") over different peer
// kinds, and a nested menu made that look like two unrelated sections.
export function MessagesPage({ navigate, tab, onTab }: {
navigate: Navigate;
tab: "private" | "groups";
onTab: (tab: "private" | "groups") => void;
}) {
return (
<PageFrame title={"Messages"} eyebrow={"Message boxes and channel history"}>
<div className="tab-bar" role="tablist" aria-label={"Message sections"}>
<button
className={`tab-btn ${tab === "private" ? "active" : ""}`}
type="button"
role="tab"
aria-selected={tab === "private"}
onClick={() => onTab("private")}
>
{"Private"}
</button>
<button
className={`tab-btn ${tab === "groups" ? "active" : ""}`}
type="button"
role="tab"
aria-selected={tab === "groups"}
onClick={() => onTab("groups")}
>
{"Groups and channels"}
</button>
</div>
{tab === "private" ? <PrivateMessagesTab navigate={navigate} /> : <GroupMessagesTab navigate={navigate} />}
</PageFrame>
);
}

View file

@ -11,7 +11,6 @@ import { BotsPage } from "./BotsPage";
import { BroadcastsPage } from "./BroadcastsPage";
import { Dashboard } from "./Dashboard";
import { GroupMessageDetailPage } from "./GroupMessageDetailPage";
import { GroupMessagesPage } from "./GroupMessagesPage";
import { MessageDetailPage } from "./MessageDetailPage";
import { MessagesPage } from "./MessagesPage";
import { StickerSetsPage } from "./StickerSetsPage";
@ -159,11 +158,16 @@ export function Routes({ route, navigate }: { route: RouteState; navigate: Navig
/>
);
}
if (route.path === "/messages/groups") {
return <GroupMessagesPage navigate={navigate} />;
}
if (route.path === "/messages" || route.path === "/messages/private") {
return <MessagesPage navigate={navigate} />;
// Both tabs keep their own path so a link to one still opens on it -- the
// tab is a view of /messages, not a hidden bit of component state.
if (route.path === "/messages" || route.path === "/messages/private" || route.path === "/messages/groups") {
return (
<MessagesPage
navigate={navigate}
tab={route.path === "/messages/groups" ? "groups" : "private"}
onTab={(tab) => navigate(tab === "groups" ? "/messages/groups" : "/messages/private")}
/>
);
}
return <Dashboard navigate={navigate} />;
}