refactor: improve image handling and accessibility in MessagesPage
Deploy to VPS / deploy (push) Successful in 2m17s
Deploy to VPS / deploy (push) Successful in 2m17s
This commit is contained in:
@@ -5,14 +5,14 @@ import {
|
|||||||
ExternalLink,
|
ExternalLink,
|
||||||
Flag,
|
Flag,
|
||||||
Hash,
|
Hash,
|
||||||
|
ImageIcon,
|
||||||
Loader2,
|
Loader2,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Search,
|
Search,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Image from "next/image";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
||||||
import { EmptyState, ErrorState, LoadingSkeleton } from "@/components/shared";
|
import { EmptyState, ErrorState, LoadingSkeleton } from "@/components/shared";
|
||||||
import { GuildSelector } from "@/components/shared/guild-selector";
|
import { GuildSelector } from "@/components/shared/guild-selector";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
@@ -247,7 +247,10 @@ export default function MessagesPage() {
|
|||||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 animate-fade-in-up">
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 animate-fade-in-up">
|
||||||
{!images || images.length === 0 ? (
|
{!images || images.length === 0 ? (
|
||||||
<div className="col-span-full flex flex-col items-center justify-center py-20 text-center">
|
<div className="col-span-full flex flex-col items-center justify-center py-20 text-center">
|
||||||
<ImageIcon className="size-10 text-muted-foreground/40 mb-3" />
|
<ImageIcon
|
||||||
|
className="size-10 text-muted-foreground/40 mb-3"
|
||||||
|
aria-label="No images"
|
||||||
|
/>
|
||||||
<p className="text-sm text-muted-foreground">No images yet.</p>
|
<p className="text-sm text-muted-foreground">No images yet.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -261,12 +264,10 @@ export default function MessagesPage() {
|
|||||||
>
|
>
|
||||||
<div className="aspect-square relative bg-muted">
|
<div className="aspect-square relative bg-muted">
|
||||||
{imgUrl ? (
|
{imgUrl ? (
|
||||||
<Image
|
<img
|
||||||
src={imgUrl}
|
src={imgUrl}
|
||||||
alt={msg.content || "Image"}
|
alt={msg.content || "Image"}
|
||||||
fill
|
className="absolute inset-0 size-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||||
className="object-cover transition-transform duration-300 group-hover:scale-105"
|
|
||||||
sizes="(max-width: 768px) 50vw, 25vw"
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center size-full text-muted-foreground text-xs">
|
<div className="flex items-center justify-center size-full text-muted-foreground text-xs">
|
||||||
@@ -422,6 +423,17 @@ function MessageCard({
|
|||||||
>
|
>
|
||||||
{msg.content}
|
{msg.content}
|
||||||
</p>
|
</p>
|
||||||
|
{(() => {
|
||||||
|
const u = extractFirstImage(msg.metadata);
|
||||||
|
if (!u) return null;
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={u}
|
||||||
|
alt=""
|
||||||
|
className="mt-2 max-h-48 rounded-lg border border-border/50 object-cover"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
{msg.ai_moderation_flags && msg.ai_moderation_flags !== "[]" && (
|
{msg.ai_moderation_flags && msg.ai_moderation_flags !== "[]" && (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{safeParseJsonArray(msg.ai_moderation_flags).map((f) => (
|
{safeParseJsonArray(msg.ai_moderation_flags).map((f) => (
|
||||||
@@ -651,7 +663,7 @@ function MiniStat({
|
|||||||
|
|
||||||
// ── Helpers ──────────────────────────────────────────────────────
|
// ── Helpers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
function extractImage(metadata: string | null | undefined): string | null {
|
function extractFirstImage(metadata: string | null | undefined): string | null {
|
||||||
if (!metadata) return null;
|
if (!metadata) return null;
|
||||||
try {
|
try {
|
||||||
const m = JSON.parse(metadata);
|
const m = JSON.parse(metadata);
|
||||||
@@ -663,26 +675,14 @@ function extractImage(metadata: string | null | undefined): string | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ImageIcon({ className }: { className?: string }) {
|
function extractImage(metadata: string | null | undefined): string | null {
|
||||||
return (
|
if (!metadata) return null;
|
||||||
<svg
|
try {
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
const m = JSON.parse(metadata);
|
||||||
width="24"
|
const atts: Array<{ url: string; contentType?: string }> =
|
||||||
height="24"
|
m.attachments ?? [];
|
||||||
viewBox="0 0 24 24"
|
return atts.find((a) => a.contentType?.startsWith("image/"))?.url ?? null;
|
||||||
fill="none"
|
} catch {
|
||||||
stroke="currentColor"
|
return null;
|
||||||
strokeWidth="2"
|
}
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
className={className}
|
|
||||||
role="img"
|
|
||||||
aria-label="Image"
|
|
||||||
>
|
|
||||||
<title>Image</title>
|
|
||||||
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
|
|
||||||
<circle cx="9" cy="9" r="2" />
|
|
||||||
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user