feat(messages): add text channel filtering for message browser
This commit is contained in:
@@ -11,7 +11,9 @@ 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";
|
||||||
@@ -60,6 +62,8 @@ 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";
|
||||||
@@ -113,7 +117,7 @@ export default function App() {
|
|||||||
},
|
},
|
||||||
onAttachmentUploaded: () =>
|
onAttachmentUploaded: () =>
|
||||||
messages
|
messages
|
||||||
.fetchMessages(monitorGuildId || undefined)
|
.fetchMessages(monitorGuildId || undefined, selectedMessageChannel || undefined)
|
||||||
.catch(() => undefined),
|
.catch(() => undefined),
|
||||||
onMediaState: (state) => media.setMediaState(state as MediaState),
|
onMediaState: (state) => media.setMediaState(state as MediaState),
|
||||||
onVoiceRecordingUploaded: (d) =>
|
onVoiceRecordingUploaded: (d) =>
|
||||||
@@ -141,20 +145,38 @@ export default function App() {
|
|||||||
voice.loadVoiceChannels(selectedVoiceGuild).catch(() => undefined);
|
voice.loadVoiceChannels(selectedVoiceGuild).catch(() => undefined);
|
||||||
}, [selectedVoiceGuild, voice.loadVoiceChannels]);
|
}, [selectedVoiceGuild, voice.loadVoiceChannels]);
|
||||||
|
|
||||||
// Auto-fetch messages for the monitor guild (all channels)
|
// Load text channels when monitor guild changes (Messages tab)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (monitorGuildId)
|
if (monitorGuildId)
|
||||||
messages.fetchMessages(monitorGuildId).catch(() => undefined);
|
getTextChannels(monitorGuildId)
|
||||||
|
.then(setTextChannels)
|
||||||
|
.catch(() => undefined);
|
||||||
|
}, [monitorGuildId]);
|
||||||
|
|
||||||
|
// Auto-fetch messages for the monitor guild
|
||||||
|
useEffect(() => {
|
||||||
|
if (monitorGuildId)
|
||||||
|
messages
|
||||||
|
.fetchMessages(monitorGuildId, selectedMessageChannel || 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).catch(() => undefined);
|
messages.fetchMessages(monitorGuildId, selectedMessageChannel || undefined).catch(() => undefined);
|
||||||
}, 15_000);
|
}, 15_000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [monitorGuildId, messages.fetchMessages]);
|
}, [monitorGuildId, messages.fetchMessages, selectedMessageChannel]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout
|
<DashboardLayout
|
||||||
@@ -212,6 +234,9 @@ 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>
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ 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, RotateCw, Search, X } from "lucide-react";
|
import { Filter, Hash, RotateCw, Search, X } from "lucide-react";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import type { MessageRecord } from "../../shared/api/client";
|
import type { Channel, MessageRecord } from "../../shared/api/client";
|
||||||
import { cardItem, cardStagger } from "../../shared/hooks/useFramerStagger";
|
import { cardItem, cardStagger } from "../../shared/hooks/useFramerStagger";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
@@ -11,6 +11,11 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
Input,
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
Tabs,
|
Tabs,
|
||||||
TabsContent,
|
TabsContent,
|
||||||
TabsList,
|
TabsList,
|
||||||
@@ -27,6 +32,9 @@ 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";
|
||||||
@@ -39,6 +47,9 @@ 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[]>([]);
|
||||||
@@ -118,6 +129,27 @@ 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 || ""}
|
||||||
|
onValueChange={(val) => onChannelChange?.(val)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full max-w-xs h-9 text-sm">
|
||||||
|
<SelectValue placeholder="All channels" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__all">All channels</SelectItem>
|
||||||
|
{textChannels.map((ch) => (
|
||||||
|
<SelectItem key={ch.id} value={ch.id}>
|
||||||
|
# {ch.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
Reference in New Issue
Block a user