feat: improve media viewer and push delivery stability
Some checks failed
Android CI / android (push) Failing after 11m0s
Android Release / release (push) Has started running
CI / test (push) Has been cancelled

- add unified Android media viewer with swipe navigation, pinch-to-zoom and swipe-to-dismiss\n- move circle videos out of media gallery and surface them in voice/chat info flows\n- align web chat info handling for circle videos and media viewer exclusions\n- stabilize realtime and tablet chat shell updates already staged in this batch\n- fix Celery push delivery loop handling so FCM jobs can read tokens reliably in worker processes
This commit is contained in:
2026-04-05 14:06:36 +03:00
parent b40dea18f1
commit d2e0969fd5
16 changed files with 1916 additions and 765 deletions

View File

@@ -104,9 +104,21 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
const count = chat.members_count ?? members.length;
return count <= 1;
}, [chat, isGroupLike, myRoleNormalized, members.length]);
const photoAttachments = useMemo(() => attachments.filter((item) => item.file_type.startsWith("image/")).sort((a, b) => b.id - a.id), [attachments]);
const videoAttachments = useMemo(() => attachments.filter((item) => item.file_type.startsWith("video/")).sort((a, b) => b.id - a.id), [attachments]);
const voiceAttachments = useMemo(() => attachments.filter((item) => item.message_type === "voice").sort((a, b) => b.id - a.id), [attachments]);
const photoAttachments = useMemo(
() => attachments.filter((item) => item.file_type.startsWith("image/")).sort((a, b) => b.id - a.id),
[attachments]
);
const videoAttachments = useMemo(
() =>
attachments
.filter((item) => item.file_type.startsWith("video/") && item.message_type !== "circle_video")
.sort((a, b) => b.id - a.id),
[attachments]
);
const voiceAttachments = useMemo(
() => attachments.filter((item) => item.message_type === "voice" || item.message_type === "circle_video").sort((a, b) => b.id - a.id),
[attachments]
);
const audioAttachments = useMemo(
() =>
attachments
@@ -871,10 +883,15 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
.slice(0, 120)
.map((item) => (
<button
className="group relative aspect-square overflow-hidden rounded-md border border-slate-700/70 bg-slate-900"
className={`group relative aspect-square overflow-hidden border border-slate-700/70 bg-slate-900 ${item.message_type === "circle_video" ? "rounded-full" : "rounded-md"}`}
key={`media-item-${item.id}`}
onClick={() => {
if (item.message_type === "circle_video") {
jumpToMessage(item.message_id);
return;
}
const mediaItems = [...photoAttachments, ...videoAttachments]
.filter((it) => it.message_type !== "circle_video")
.sort((a, b) => b.id - a.id)
.map((it) => ({ url: it.file_url, type: it.file_type.startsWith("video/") ? "video" as const : "image" as const, messageId: it.message_id }));
const idx = mediaItems.findIndex((it) => it.url === item.file_url && it.messageId === item.message_id);

View File

@@ -1100,6 +1100,7 @@ function renderMessageContent(
if (messageType === "image" || messageType === "video" || messageType === "circle_video") {
const mediaItems = attachments
.filter((item) => item.message_type !== "circle_video")
.filter((item) => item.file_type.startsWith("image/") || item.file_type.startsWith("video/"))
.map((item) => ({
url: item.file_url,
@@ -1113,13 +1114,14 @@ function renderMessageContent(
}
if (mediaItems.length === 1) {
const item = mediaItems[0];
const isCircleVideo = messageType === "circle_video";
const blockViewerOpen = isStickerOrGifMedia(item.url);
return (
<div className="space-y-1.5">
<button
className="relative block overflow-hidden rounded-xl bg-slate-950/30"
className={`relative block overflow-hidden bg-slate-950/30 ${isCircleVideo ? "aspect-square w-56 rounded-full" : "rounded-xl"}`}
onClick={() => {
if (blockViewerOpen) {
if (blockViewerOpen || isCircleVideo) {
return;
}
opts.onOpenMedia(item.url, item.type);
@@ -1131,10 +1133,15 @@ function renderMessageContent(
type="button"
>
{item.type === "image" ? (
<img alt="attachment" className="max-h-80 rounded-xl object-cover" draggable={false} src={item.url} />
<img
alt="attachment"
className={isCircleVideo ? "h-full w-full object-cover" : "max-h-80 rounded-xl object-cover"}
draggable={false}
src={item.url}
/>
) : (
<>
<video className="max-h-80 rounded-xl" muted src={item.url} />
<video className={isCircleVideo ? "h-full w-full object-cover" : "max-h-80 rounded-xl"} muted src={item.url} />
<span className="absolute inset-0 flex items-center justify-center text-3xl text-white/85"></span>
</>
)}
@@ -1410,6 +1417,7 @@ function collectMediaItems(
const attachments = attachmentsByMessage[message.id] ?? [];
for (const attachment of attachments) {
if (!attachment.file_url) continue;
if (attachment.message_type === "circle_video") continue;
if (!attachment.file_type.startsWith("image/") && !attachment.file_type.startsWith("video/")) continue;
if (isStickerOrGifMedia(attachment.file_url)) continue;
const type = attachment.file_type.startsWith("image/") ? "image" : "video";
@@ -1419,7 +1427,7 @@ function collectMediaItems(
items.push({ url: attachment.file_url, type });
}
if (attachments.length === 0 && message.text && /^https?:\/\//i.test(message.text)) {
if (message.type !== "image" && message.type !== "video" && message.type !== "circle_video") continue;
if (message.type !== "image" && message.type !== "video") continue;
if (isStickerOrGifMedia(message.text)) continue;
const type = message.type === "image" ? "image" : "video";
const key = `${type}:${message.text}`;