refactor(messages): remove text channel filter and channel-section grouping
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user