feat(chat): add presence metadata and improve web chat core
Some checks failed
CI / test (push) Failing after 22s

- add user last_seen_at with alembic migration and persist on realtime disconnect
- extend chat serialization with private online/last_seen, group members/online, channel subscribers
- add Redis batch presence lookup helper
- update web chat list/header to display status counters and last-seen labels
- improve delivery receipt handling using last_delivered/last_read boundaries
- include chat info panel and related API/type updates
This commit is contained in:
2026-03-08 02:02:09 +03:00
parent 51275692ac
commit e6a271f8be
17 changed files with 564 additions and 6 deletions

View File

@@ -1,10 +1,12 @@
import { useEffect } from "react";
import { ChatList } from "../components/ChatList";
import { ChatInfoPanel } from "../components/ChatInfoPanel";
import { MessageComposer } from "../components/MessageComposer";
import { MessageList } from "../components/MessageList";
import { useRealtime } from "../hooks/useRealtime";
import { useAuthStore } from "../store/authStore";
import { useChatStore } from "../store/chatStore";
import { useState } from "react";
export function ChatsPage() {
const me = useAuthStore((s) => s.me);
@@ -15,6 +17,7 @@ export function ChatsPage() {
const setActiveChatId = useChatStore((s) => s.setActiveChatId);
const loadMessages = useChatStore((s) => s.loadMessages);
const activeChat = chats.find((chat) => chat.id === activeChatId);
const [infoOpen, setInfoOpen] = useState(false);
useRealtime();
@@ -41,9 +44,14 @@ export function ChatsPage() {
<button className="rounded-full bg-slate-700/70 px-2 py-1 text-xs md:hidden" onClick={() => setActiveChatId(null)}>
Back
</button>
{activeChatId ? (
<button className="rounded-full bg-slate-700/70 px-2 py-1 text-xs" onClick={() => setInfoOpen(true)}>
Info
</button>
) : null}
<div className="min-w-0">
<p className="truncate text-sm font-semibold">{activeChat?.display_title || activeChat?.title || me?.name || `@${me?.username}`}</p>
<p className="truncate text-xs text-slate-300/80">{activeChat ? activeChat.type : "Select a chat"}</p>
<p className="truncate text-xs text-slate-300/80">{activeChat ? headerMetaLabel(activeChat) : "Select a chat"}</p>
</div>
</div>
<button className="rounded-full bg-slate-700/70 px-3 py-1.5 text-xs font-semibold hover:bg-slate-600/80" onClick={logout}>
@@ -62,6 +70,50 @@ export function ChatsPage() {
)}
</section>
</div>
<ChatInfoPanel chatId={activeChatId} open={infoOpen} onClose={() => setInfoOpen(false)} />
</main>
);
}
function headerMetaLabel(chat: {
type: "private" | "group" | "channel";
is_saved?: boolean;
counterpart_is_online?: boolean | null;
counterpart_last_seen_at?: string | null;
members_count?: number | null;
online_count?: number | null;
subscribers_count?: number | null;
}): string {
if (chat.is_saved) {
return "Saved Messages";
}
if (chat.type === "private") {
if (chat.counterpart_is_online) {
return "online";
}
if (chat.counterpart_last_seen_at) {
return `last seen ${formatLastSeen(chat.counterpart_last_seen_at)}`;
}
return "offline";
}
if (chat.type === "group") {
const members = chat.members_count ?? 0;
const online = chat.online_count ?? 0;
return `${members} members, ${online} online`;
}
const subscribers = chat.subscribers_count ?? chat.members_count ?? 0;
return `${subscribers} subscribers`;
}
function formatLastSeen(value: string): string {
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return "recently";
}
return date.toLocaleString(undefined, {
day: "2-digit",
month: "short",
hour: "2-digit",
minute: "2-digit"
});
}