refactor(messages): remove text channel filter and channel-section grouping

This commit is contained in:
MythEclipse
2026-06-03 20:53:47 +07:00
parent 2a2003e638
commit a24a5e7dfd
4 changed files with 18 additions and 137 deletions
+4 -27
View File
@@ -11,9 +11,7 @@ import {
} from "./features/messages/hooks/useMessages"; } from "./features/messages/hooks/useMessages";
import { import {
type ActiveSpeaker, type ActiveSpeaker,
type Channel,
getAppConfig, getAppConfig,
getTextChannels,
type MediaState, type MediaState,
type MessageRecord, type MessageRecord,
} from "./shared/api/client"; } from "./shared/api/client";
@@ -62,8 +60,6 @@ export default function App() {
!!localStorage.getItem("admin-password"), !!localStorage.getItem("admin-password"),
); );
const [monitorGuildId, setMonitorGuildId] = useState(""); const [monitorGuildId, setMonitorGuildId] = useState("");
const [textChannels, setTextChannels] = useState<Channel[]>([]);
const [selectedMessageChannel, setSelectedMessageChannel] = useState<string>("");
const audio = useAudioPlayback(); const audio = useAudioPlayback();
const activeTab = uiState.activeTab || "live"; const activeTab = uiState.activeTab || "live";
@@ -117,7 +113,7 @@ export default function App() {
}, },
onAttachmentUploaded: () => onAttachmentUploaded: () =>
messages messages
.fetchMessages(monitorGuildId || undefined, selectedMessageChannel || undefined) .fetchMessages(monitorGuildId || undefined)
.catch(() => undefined), .catch(() => undefined),
onMediaState: (state) => media.setMediaState(state as MediaState), onMediaState: (state) => media.setMediaState(state as MediaState),
onVoiceRecordingUploaded: (d) => onVoiceRecordingUploaded: (d) =>
@@ -145,38 +141,22 @@ export default function App() {
voice.loadVoiceChannels(selectedVoiceGuild).catch(() => undefined); voice.loadVoiceChannels(selectedVoiceGuild).catch(() => undefined);
}, [selectedVoiceGuild, voice.loadVoiceChannels]); }, [selectedVoiceGuild, voice.loadVoiceChannels]);
// Load text channels when monitor guild changes (Messages tab)
useEffect(() => {
if (monitorGuildId)
getTextChannels(monitorGuildId)
.then(setTextChannels)
.catch(() => undefined);
}, [monitorGuildId]);
// Auto-fetch messages for the monitor guild // Auto-fetch messages for the monitor guild
useEffect(() => { useEffect(() => {
if (monitorGuildId) if (monitorGuildId)
messages messages
.fetchMessages(monitorGuildId, selectedMessageChannel || undefined) .fetchMessages(monitorGuildId)
.catch(() => undefined); .catch(() => undefined);
}, [monitorGuildId, messages.fetchMessages]); }, [monitorGuildId, messages.fetchMessages]);
// Re-fetch when channel filter changes
useEffect(() => {
if (monitorGuildId)
messages
.fetchMessages(monitorGuildId, selectedMessageChannel || undefined)
.catch(() => undefined);
}, [selectedMessageChannel, monitorGuildId, messages.fetchMessages]);
// Periodic refetch — keeps dashboard in sync even if WS events missed // Periodic refetch — keeps dashboard in sync even if WS events missed
useEffect(() => { useEffect(() => {
if (!monitorGuildId) return; if (!monitorGuildId) return;
const interval = setInterval(() => { const interval = setInterval(() => {
messages.fetchMessages(monitorGuildId, selectedMessageChannel || undefined).catch(() => undefined); messages.fetchMessages(monitorGuildId).catch(() => undefined);
}, 15_000); }, 15_000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, [monitorGuildId, messages.fetchMessages, selectedMessageChannel]); }, [monitorGuildId, messages.fetchMessages]);
return ( return (
<DashboardLayout <DashboardLayout
@@ -234,9 +214,6 @@ export default function App() {
onLoadMore={messages.loadMore} onLoadMore={messages.loadMore}
hasMore={messages.hasMore} hasMore={messages.hasMore}
loadingMore={messages.loadingMore} loadingMore={messages.loadingMore}
textChannels={textChannels}
selectedChannel={selectedMessageChannel}
onChannelChange={setSelectedMessageChannel}
/> />
) : ( ) : (
<AnalyticsErrorBoundary> <AnalyticsErrorBoundary>
@@ -1,7 +1,5 @@
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Hash } from "lucide-react";
import { useEffect, useMemo, useRef } from "react"; import { useEffect, useMemo, useRef } from "react";
import { parseMetadata } from "../../../entities/message/types";
import type { MessageRecord } from "../../../shared/api/client"; import type { MessageRecord } from "../../../shared/api/client";
import { cardItem, cardStagger } from "../../../shared/hooks/useFramerStagger"; import { cardItem, cardStagger } from "../../../shared/hooks/useFramerStagger";
import { ScrollArea, EmptyStateMascot } from "../../../shared/ui"; import { ScrollArea, EmptyStateMascot } from "../../../shared/ui";
@@ -20,12 +18,12 @@ export interface MessageFeedProps {
/** Messages from the same user within 5 minutes are visually grouped. */ /** Messages from the same user within 5 minutes are visually grouped. */
const GROUP_WINDOW_MS = 5 * 60 * 1000; const GROUP_WINDOW_MS = 5 * 60 * 1000;
interface UserMessageGroup { interface MessageGroup {
messages: MessageRecord[]; messages: MessageRecord[];
} }
function groupByUser(messages: MessageRecord[]): UserMessageGroup[] { function groupMessages(messages: MessageRecord[]): MessageGroup[] {
const groups: UserMessageGroup[] = []; const groups: MessageGroup[] = [];
for (const msg of messages) { for (const msg of messages) {
const lastGroup = groups[groups.length - 1]; const lastGroup = groups[groups.length - 1];
if ( if (
@@ -43,57 +41,6 @@ function groupByUser(messages: MessageRecord[]): UserMessageGroup[] {
return groups; return groups;
} }
interface ChannelSection {
key: string;
channelId: string;
threadId: string | null;
label: string;
groups: UserMessageGroup[];
}
function computeChannelSections(messages: MessageRecord[]): ChannelSection[] {
if (messages.length === 0) return [];
// Group by (channel_id, thread_id)
const map = new Map<string, MessageRecord[]>();
for (const msg of messages) {
const key = msg.thread_id
? `${msg.channel_id}:t:${msg.thread_id}`
: msg.channel_id;
const list = map.get(key);
if (list) list.push(msg);
else map.set(key, [msg]);
}
const sections: ChannelSection[] = [];
for (const [key, channelMessages] of map) {
const first = channelMessages[0];
const meta = parseMetadata(first.metadata);
const ch = meta?.channel;
let label: string;
if (ch?.threadName && ch?.channelName) {
label = `# ${ch.channelName} ${ch.threadName}`;
} else if (ch?.channelName) {
label = `# ${ch.channelName}`;
} else if (first.thread_id) {
label = `thread:${first.thread_id.slice(0, 8)}`;
} else {
label = `# ${first.channel_id.slice(0, 8)}`;
}
sections.push({
key,
channelId: first.channel_id,
threadId: first.thread_id,
label,
groups: groupByUser(channelMessages),
});
}
return sections;
}
export function MessageFeed({ export function MessageFeed({
messages, messages,
onReanalyze, onReanalyze,
@@ -121,7 +68,7 @@ export function MessageFeed({
return () => observer.disconnect(); return () => observer.disconnect();
}, [onLoadMore, hasMore]); }, [onLoadMore, hasMore]);
const sections = useMemo(() => computeChannelSections(messages), [messages]); const groupedMessages = useMemo(() => groupMessages(messages), [messages]);
if (loading) { if (loading) {
return ( return (
@@ -147,27 +94,13 @@ export function MessageFeed({
initial="initial" initial="initial"
animate="animate" animate="animate"
> >
{sections.map((section) => ( {groupedMessages.map((group) => (
<div key={section.key} className="space-y-2"> <motion.div key={group.messages[0].id} variants={cardItem}>
{/* Channel/Thread section header */} <MessageCard
<div className="sticky top-0 z-10 -mx-1 rounded-lg bg-muted/80 backdrop-blur-sm px-3 py-1.5 flex items-center gap-1.5"> messages={group.messages}
<Hash className="h-3.5 w-3.5 text-primary/60" /> onReanalyze={onReanalyze}
<span className="text-xs font-medium text-muted-foreground"> />
{section.label} </motion.div>
</span>
</div>
<div className="space-y-3">
{section.groups.map((group) => (
<motion.div key={group.messages[0].id} variants={cardItem}>
<MessageCard
messages={group.messages}
onReanalyze={onReanalyze}
/>
</motion.div>
))}
</div>
</div>
))} ))}
{/* Infinite-scroll sentinel */} {/* Infinite-scroll sentinel */}
@@ -29,9 +29,8 @@ export function useMessages() {
const [cursor, setCursor] = useState<string | null>(null); const [cursor, setCursor] = useState<string | null>(null);
const [hasMore, setHasMore] = useState(false); const [hasMore, setHasMore] = useState(false);
const currentGuild = useRef<string | null>(null); const currentGuild = useRef<string | null>(null);
const currentChannel = useRef<string | undefined>(undefined);
const fetchMessages = useCallback(async (guildId?: string, channelId?: string) => { const fetchMessages = useCallback(async (guildId?: string) => {
if (!guildId) { if (!guildId) {
setMessages([]); setMessages([]);
setCursor(null); setCursor(null);
@@ -39,13 +38,11 @@ export function useMessages() {
return []; return [];
} }
currentGuild.current = guildId; currentGuild.current = guildId;
currentChannel.current = channelId;
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const result = await listMessages({ const result = await listMessages({
guildId, guildId,
channelId,
limit: PAGE_SIZE, limit: PAGE_SIZE,
}); });
if (currentGuild.current === guildId) { if (currentGuild.current === guildId) {
@@ -69,7 +66,6 @@ export function useMessages() {
try { try {
const result = await listMessages({ const result = await listMessages({
guildId: currentGuild.current, guildId: currentGuild.current,
channelId: currentChannel.current,
cursor, cursor,
limit: PAGE_SIZE, limit: PAGE_SIZE,
}); });
@@ -113,7 +109,6 @@ export function useMessages() {
); );
const { count } = await reanalyzeErrorBatch({ const { count } = await reanalyzeErrorBatch({
guildId: currentGuild.current ?? undefined, guildId: currentGuild.current ?? undefined,
channelId: currentChannel.current,
}); });
return count; return count;
}, []); }, []);
@@ -1,7 +1,7 @@
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Filter, Hash, RotateCw, Search, X } from "lucide-react"; import { Filter, RotateCw, Search, X } from "lucide-react";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import type { Channel, MessageRecord } from "../../shared/api/client"; import type { MessageRecord } from "../../shared/api/client";
import { cardItem, cardStagger } from "../../shared/hooks/useFramerStagger"; import { cardItem, cardStagger } from "../../shared/hooks/useFramerStagger";
import { import {
Badge, Badge,
@@ -11,7 +11,6 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
Input, Input,
Select,
Tabs, Tabs,
TabsContent, TabsContent,
TabsList, TabsList,
@@ -28,9 +27,6 @@ interface MessagesPanelProps {
onLoadMore?: () => void; onLoadMore?: () => void;
hasMore?: boolean; hasMore?: boolean;
loadingMore?: boolean; loadingMore?: boolean;
textChannels?: Channel[];
selectedChannel?: string;
onChannelChange?: (channelId: string) => void;
} }
type AiFilter = "all" | "clean" | "flagged" | "error" | "pending"; type AiFilter = "all" | "clean" | "flagged" | "error" | "pending";
@@ -43,9 +39,6 @@ export function MessagesPanel({
onLoadMore, onLoadMore,
hasMore, hasMore,
loadingMore, loadingMore,
textChannels,
selectedChannel,
onChannelChange,
}: MessagesPanelProps) { }: MessagesPanelProps) {
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState<MessageRecord[]>([]); const [searchResults, setSearchResults] = useState<MessageRecord[]>([]);
@@ -125,23 +118,6 @@ export function MessagesPanel({
Messages are automatically captured from all text channels in the Messages are automatically captured from all text channels in the
monitored guild. Real-time updates arrive via WebSocket. monitored guild. Real-time updates arrive via WebSocket.
</p> </p>
{textChannels && textChannels.length > 0 && (
<div className="mt-3 flex items-center gap-2">
<Hash className="h-4 w-4 text-muted-foreground shrink-0" />
<Select
value={selectedChannel || ""}
onChange={(e) => onChannelChange?.(e.target.value === "__all" ? "" : e.target.value)}
placeholder="All channels"
options={[
{ value: "__all", label: "All channels" },
...textChannels.map((ch) => ({
value: ch.id,
label: `# ${ch.name}`,
})),
]}
/>
</div>
)}
</CardContent> </CardContent>
</Card> </Card>
</motion.div> </motion.div>