Keep companywide channel view visible
This commit is contained in:
1 parent
26c933fad7
commit
b84162246a
5 files changed
+155
-52
No files matched your search
@@ -4,6 +4,8 @@ import { EnvelopeSimple, Hash, Buildings, Megaphone, Fire, MagnifyingGlass, Funn
|
|||||||
import type { AgentRecord, MessageChannel } from "@shared/types"
|
import type { AgentRecord, MessageChannel } from "@shared/types"
|
||||||
import { AgentAvatar } from "./MessageItem"
|
import { AgentAvatar } from "./MessageItem"
|
||||||
import type { ChannelDescriptor, TaskDiscussionDescriptor } from "./ChannelHeader"
|
import type { ChannelDescriptor, TaskDiscussionDescriptor } from "./ChannelHeader"
|
||||||
|
import type { MessageFilters } from "./message-filters"
|
||||||
|
import { DEFAULT_MESSAGE_FILTERS, hasActiveMessageFilters } from "./message-filters"
|
||||||
import { useT } from "../../i18n"
|
import { useT } from "../../i18n"
|
||||||
import { Eyebrow, Select } from "../ui"
|
import { Eyebrow, Select } from "../ui"
|
||||||
|
|
||||||
@@ -169,12 +171,7 @@ export function ChannelSidebar({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FilterState {
|
export type FilterState = MessageFilters
|
||||||
search: string
|
|
||||||
channelType: MessageChannel | "all"
|
|
||||||
priority: string
|
|
||||||
unreadOnly: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FilterBar({
|
export function FilterBar({
|
||||||
filters,
|
filters,
|
||||||
@@ -209,7 +206,7 @@ export function FilterBar({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setExpanded(!expanded)}
|
onClick={() => setExpanded(!expanded)}
|
||||||
className={`flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-[11px] font-medium transition ${
|
className={`flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-[11px] font-medium transition ${
|
||||||
expanded || filters.channelType !== "all" || filters.priority !== "all" || filters.unreadOnly
|
expanded || hasActiveMessageFilters(filters)
|
||||||
? "border-transparent bg-primary text-primary-foreground"
|
? "border-transparent bg-primary text-primary-foreground"
|
||||||
: "border-border text-muted-foreground hover:border-border"
|
: "border-border text-muted-foreground hover:border-border"
|
||||||
}`}
|
}`}
|
||||||
@@ -227,19 +224,6 @@ export function FilterBar({
|
|||||||
exit={{ opacity: 0, height: 0 }}
|
exit={{ opacity: 0, height: 0 }}
|
||||||
className="flex flex-wrap items-center gap-2 overflow-hidden"
|
className="flex flex-wrap items-center gap-2 overflow-hidden"
|
||||||
>
|
>
|
||||||
<Select
|
|
||||||
compact
|
|
||||||
value={filters.channelType}
|
|
||||||
onChange={(v) => onChangeFilters({ ...filters, channelType: v as MessageChannel | "all" })}
|
|
||||||
options={[
|
|
||||||
{ value: "all", label: t("comm.allChannels") },
|
|
||||||
{ value: "direct", label: t("comm.direct") },
|
|
||||||
{ value: "department", label: t("comm.department") },
|
|
||||||
{ value: "company", label: t("comm.company") },
|
|
||||||
{ value: "project", label: t("nav.projects") },
|
|
||||||
{ value: "incident", label: t("comm.incident") },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Select
|
<Select
|
||||||
compact
|
compact
|
||||||
value={filters.priority}
|
value={filters.priority}
|
||||||
@@ -251,19 +235,10 @@ export function FilterBar({
|
|||||||
{ value: "low", label: t("comm.low") },
|
{ value: "low", label: t("comm.low") },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<button
|
{filters.priority !== "all" ? (
|
||||||
type="button"
|
|
||||||
onClick={() => onChangeFilters({ ...filters, unreadOnly: !filters.unreadOnly })}
|
|
||||||
className={`rounded-full px-2.5 py-1.5 text-[11px] font-semibold transition ${
|
|
||||||
filters.unreadOnly ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{t("comm.unreadOnly")}
|
|
||||||
</button>
|
|
||||||
{(filters.channelType !== "all" || filters.priority !== "all" || filters.unreadOnly) ? (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onChangeFilters({ search: filters.search, channelType: "all", priority: "all", unreadOnly: false })}
|
onClick={() => onChangeFilters({ ...DEFAULT_MESSAGE_FILTERS, search: filters.search })}
|
||||||
className="text-[11px] font-medium text-muted-foreground underline transition hover:text-foreground"
|
className="text-[11px] font-medium text-muted-foreground underline transition hover:text-foreground"
|
||||||
>
|
>
|
||||||
{t("comm.clearFilters")}
|
{t("comm.clearFilters")}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { MessageComposer, TaskDiscussionComposer, NoChannelComposer } from "./Me
|
|||||||
import { SearchResults } from "./SearchResults"
|
import { SearchResults } from "./SearchResults"
|
||||||
import ThreadPanel from "./ThreadPanel"
|
import ThreadPanel from "./ThreadPanel"
|
||||||
import { deriveChannels, deriveTaskDiscussionChannels, getChannelMessages } from "./channel-utils"
|
import { deriveChannels, deriveTaskDiscussionChannels, getChannelMessages } from "./channel-utils"
|
||||||
|
import { DEFAULT_MESSAGE_FILTERS, filterChannelMessages } from "./message-filters"
|
||||||
import { useT, type TranslationFn } from "../../i18n"
|
import { useT, type TranslationFn } from "../../i18n"
|
||||||
|
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ export function CommunicationHub({ messages, comments, tasks, agents, projects,
|
|||||||
const [replyTo, setReplyTo] = useState<AgentMessageRecord | null>(null)
|
const [replyTo, setReplyTo] = useState<AgentMessageRecord | null>(null)
|
||||||
const [optimisticReadIds, setOptimisticReadIds] = useState<Set<string>>(new Set())
|
const [optimisticReadIds, setOptimisticReadIds] = useState<Set<string>>(new Set())
|
||||||
const pendingReadIdsRef = useRef<Set<string>>(new Set())
|
const pendingReadIdsRef = useRef<Set<string>>(new Set())
|
||||||
const [filters, setFilters] = useState<FilterState>({ search: "", channelType: "all", priority: "all", unreadOnly: false })
|
const [filters, setFilters] = useState<FilterState>(DEFAULT_MESSAGE_FILTERS)
|
||||||
|
|
||||||
// Thread panel state from Zustand store
|
// Thread panel state from Zustand store
|
||||||
const activeThreadId = useAppStore((s) => s.activeThreadId)
|
const activeThreadId = useAppStore((s) => s.activeThreadId)
|
||||||
@@ -96,26 +97,33 @@ export function CommunicationHub({ messages, comments, tasks, agents, projects,
|
|||||||
const taskDiscussionChannels = useMemo(() => deriveTaskDiscussionChannels(comments, tasks, forcedTaskId), [comments, forcedTaskId, tasks])
|
const taskDiscussionChannels = useMemo(() => deriveTaskDiscussionChannels(comments, tasks, forcedTaskId), [comments, forcedTaskId, tasks])
|
||||||
const channels: AnyChannel[] = useMemo(() => [...messageChannels, ...taskDiscussionChannels], [messageChannels, taskDiscussionChannels])
|
const channels: AnyChannel[] = useMemo(() => [...messageChannels, ...taskDiscussionChannels], [messageChannels, taskDiscussionChannels])
|
||||||
|
|
||||||
useEffect(() => { if (channels.length === 0) { if (selectedChannelKey) setSelectedChannelKey(null); return }; if (!selectedChannelKey || !channels.some((c) => c.key === selectedChannelKey)) setSelectedChannelKey(channels[0].key) }, [channels, selectedChannelKey])
|
const handleSelectChannel = useCallback((key: string) => {
|
||||||
|
setSelectedChannelKey(key)
|
||||||
|
setReplyTo(null)
|
||||||
|
setFilters(DEFAULT_MESSAGE_FILTERS)
|
||||||
|
clearSearch()
|
||||||
|
}, [clearSearch])
|
||||||
|
|
||||||
|
useEffect(() => { if (channels.length === 0) { if (selectedChannelKey) setSelectedChannelKey(null); return }; if (!selectedChannelKey || !channels.some((c) => c.key === selectedChannelKey)) handleSelectChannel(channels[0].key) }, [channels, selectedChannelKey, handleSelectChannel])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedEntityId) return
|
if (!selectedEntityId) return
|
||||||
const matchMsg = effectiveMessages.find((m) => m.id === selectedEntityId)
|
const matchMsg = effectiveMessages.find((m) => m.id === selectedEntityId)
|
||||||
if (matchMsg) {
|
if (matchMsg) {
|
||||||
const ch = messageChannels.find((ch) => getChannelMessages(effectiveMessages, ch).some((m) => m.id === selectedEntityId))
|
const ch = messageChannels.find((ch) => getChannelMessages(effectiveMessages, ch).some((m) => m.id === selectedEntityId))
|
||||||
if (ch) setSelectedChannelKey(ch.key)
|
if (ch) handleSelectChannel(ch.key)
|
||||||
onEntityConsumed?.()
|
onEntityConsumed?.()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const matchCom = comments.find((c) => c.id === selectedEntityId)
|
const matchCom = comments.find((c) => c.id === selectedEntityId)
|
||||||
if (matchCom) {
|
if (matchCom) {
|
||||||
setSelectedChannelKey(`task-${matchCom.taskId}`)
|
handleSelectChannel(`task-${matchCom.taskId}`)
|
||||||
onEntityConsumed?.()
|
onEntityConsumed?.()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const matchTask = taskDiscussionChannels.find((d) => d.taskId === selectedEntityId)
|
const matchTask = taskDiscussionChannels.find((d) => d.taskId === selectedEntityId)
|
||||||
if (matchTask) {
|
if (matchTask) {
|
||||||
setSelectedChannelKey(matchTask.key)
|
handleSelectChannel(matchTask.key)
|
||||||
onEntityConsumed?.()
|
onEntityConsumed?.()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -125,21 +133,14 @@ export function CommunicationHub({ messages, comments, tasks, agents, projects,
|
|||||||
// the effect refire on every snapshot and block the user from
|
// the effect refire on every snapshot and block the user from
|
||||||
// navigating manually inside the communication hub.
|
// navigating manually inside the communication hub.
|
||||||
onEntityConsumed?.()
|
onEntityConsumed?.()
|
||||||
}, [selectedEntityId, effectiveMessages, comments, messageChannels, onEntityConsumed, taskDiscussionChannels])
|
}, [selectedEntityId, effectiveMessages, comments, messageChannels, onEntityConsumed, taskDiscussionChannels, handleSelectChannel])
|
||||||
|
|
||||||
const activeChannel = useMemo(() => channels.find((c) => c.key === selectedChannelKey) ?? null, [channels, selectedChannelKey])
|
const activeChannel = useMemo(() => channels.find((c) => c.key === selectedChannelKey) ?? null, [channels, selectedChannelKey])
|
||||||
const channelMessages = useMemo(() => { if (!activeChannel || activeChannel.kind !== "message") return []; return getChannelMessages(effectiveMessages, activeChannel).sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) }, [effectiveMessages, activeChannel])
|
const channelMessages = useMemo(() => { if (!activeChannel || activeChannel.kind !== "message") return []; return getChannelMessages(effectiveMessages, activeChannel).sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) }, [effectiveMessages, activeChannel])
|
||||||
const taskComments = useMemo(() => { if (!activeChannel || activeChannel.kind !== "task") return []; return comments.filter((c) => c.taskId === activeChannel.taskId).sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) }, [comments, activeChannel])
|
const taskComments = useMemo(() => { if (!activeChannel || activeChannel.kind !== "task") return []; return comments.filter((c) => c.taskId === activeChannel.taskId).sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) }, [comments, activeChannel])
|
||||||
const activeTask = useMemo(() => { if (!activeChannel || activeChannel.kind !== "task") return null; return tasks.find((t) => t.id === activeChannel.taskId) ?? null }, [tasks, activeChannel])
|
const activeTask = useMemo(() => { if (!activeChannel || activeChannel.kind !== "task") return null; return tasks.find((t) => t.id === activeChannel.taskId) ?? null }, [tasks, activeChannel])
|
||||||
|
|
||||||
const filteredMessages = useMemo(() => {
|
const filteredMessages = useMemo(() => filterChannelMessages(channelMessages, filters, agentMap, searchQuery), [channelMessages, filters, agentMap, searchQuery])
|
||||||
let r = channelMessages
|
|
||||||
if (filters.channelType !== "all") r = r.filter((m) => m.channel === filters.channelType)
|
|
||||||
if (filters.search && !searchQuery) { const q = filters.search.toLowerCase(); r = r.filter((m) => m.subject.toLowerCase().includes(q) || m.body.toLowerCase().includes(q) || (agentMap.get(m.fromAgentId)?.name ?? "").toLowerCase().includes(q)) }
|
|
||||||
if (filters.priority !== "all") r = r.filter((m) => m.priority === filters.priority)
|
|
||||||
if (filters.unreadOnly) r = r.filter((m) => !m.readAt)
|
|
||||||
return r
|
|
||||||
}, [channelMessages, filters, agentMap, searchQuery])
|
|
||||||
|
|
||||||
// Debounced search via IPC
|
// Debounced search via IPC
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -200,13 +201,13 @@ export function CommunicationHub({ messages, comments, tasks, agents, projects,
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (matchingChannel) {
|
if (matchingChannel) {
|
||||||
setSelectedChannelKey(matchingChannel.key)
|
handleSelectChannel(matchingChannel.key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear search to return to normal view
|
// Clear search to return to normal view
|
||||||
setFilters((prev) => ({ ...prev, search: "" }))
|
setFilters((prev) => ({ ...prev, search: "" }))
|
||||||
clearSearch()
|
clearSearch()
|
||||||
}, [messageChannels, effectiveMessages, clearSearch])
|
}, [messageChannels, effectiveMessages, handleSelectChannel, clearSearch])
|
||||||
|
|
||||||
const handleSearchKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleSearchKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
@@ -271,7 +272,7 @@ export function CommunicationHub({ messages, comments, tasks, agents, projects,
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 overflow-hidden">
|
<div className="flex h-full min-h-0 overflow-hidden">
|
||||||
<AnimatePresence initial={false}>
|
<AnimatePresence initial={false}>
|
||||||
{showSidebar ? <ChannelSidebar channelGroups={channelGroups} taskDiscussionChannels={taskDiscussionChannels} selectedChannelKey={selectedChannelKey} onSelectChannel={setSelectedChannelKey} unreadByChannel={unreadByChannel} agents={agents} totalChannels={channels.length} onHideSidebar={() => setShowSidebar(false)} /> : null}
|
{showSidebar ? <ChannelSidebar channelGroups={channelGroups} taskDiscussionChannels={taskDiscussionChannels} selectedChannelKey={selectedChannelKey} onSelectChannel={handleSelectChannel} unreadByChannel={unreadByChannel} agents={agents} totalChannels={channels.length} onHideSidebar={() => setShowSidebar(false)} /> : null}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
<div className="flex min-w-0 flex-1 flex-col">
|
<div className="flex min-w-0 flex-1 flex-col">
|
||||||
<ChannelHeader activeChannel={activeChannel} showSidebar={showSidebar} onShowSidebar={() => setShowSidebar(true)} channelMessages={channelMessages} taskDiscussionComments={taskComments} activeTask={activeTask} unreadByChannel={unreadByChannel} onNavigateToTask={onNavigateToTask} filterBar={<FilterBar filters={filters} onChangeFilters={setFilters} scopeToggle={scopeToggleElement} onSearchKeyDown={handleSearchKeyDown} />} />
|
<ChannelHeader activeChannel={activeChannel} showSidebar={showSidebar} onShowSidebar={() => setShowSidebar(true)} channelMessages={channelMessages} taskDiscussionComments={taskComments} activeTask={activeTask} unreadByChannel={unreadByChannel} onNavigateToTask={onNavigateToTask} filterBar={<FilterBar filters={filters} onChangeFilters={setFilters} scopeToggle={scopeToggleElement} onSearchKeyDown={handleSearchKeyDown} />} />
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ import { useRovingTabIndex } from "../../lib/keyboard"
|
|||||||
|
|
||||||
interface FilterState {
|
interface FilterState {
|
||||||
search: string
|
search: string
|
||||||
channelType: string
|
|
||||||
priority: string
|
priority: string
|
||||||
unreadOnly: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MessageListProps {
|
interface MessageListProps {
|
||||||
@@ -128,11 +126,11 @@ export function MessageList({
|
|||||||
<div className="flex h-full flex-col items-center justify-center text-center">
|
<div className="flex h-full flex-col items-center justify-center text-center">
|
||||||
<Tray size={36} className="mb-3 text-muted-foreground" />
|
<Tray size={36} className="mb-3 text-muted-foreground" />
|
||||||
<div className="text-[14px] font-medium text-muted-foreground">
|
<div className="text-[14px] font-medium text-muted-foreground">
|
||||||
{filters.search || filters.unreadOnly || filters.priority !== "all"
|
{filters.search || filters.priority !== "all"
|
||||||
? t("comm.noMatchFilters")
|
? t("comm.noMatchFilters")
|
||||||
: t("comm.noMessagesChannel")}
|
: t("comm.noMessagesChannel")}
|
||||||
</div>
|
</div>
|
||||||
{(filters.search || filters.unreadOnly || filters.priority !== "all") ? (
|
{(filters.search || filters.priority !== "all") ? (
|
||||||
<div className="mt-1 text-[13px] text-muted-foreground">{t("comm.adjustFilters")}</div>
|
<div className="mt-1 text-[13px] text-muted-foreground">{t("comm.adjustFilters")}</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { AgentMessageRecord, AgentRecord } from "@shared/types"
|
||||||
|
|
||||||
|
export interface MessageFilters {
|
||||||
|
search: string
|
||||||
|
priority: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_MESSAGE_FILTERS: MessageFilters = {
|
||||||
|
search: "",
|
||||||
|
priority: "all",
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasActiveMessageFilters(filters: MessageFilters): boolean {
|
||||||
|
return filters.search.length > 0 || filters.priority !== "all"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterChannelMessages(
|
||||||
|
messages: AgentMessageRecord[],
|
||||||
|
filters: MessageFilters,
|
||||||
|
agentMap: Map<string, AgentRecord>,
|
||||||
|
searchQuery: string,
|
||||||
|
): AgentMessageRecord[] {
|
||||||
|
let result = messages
|
||||||
|
if (filters.search && !searchQuery) {
|
||||||
|
const query = filters.search.toLowerCase()
|
||||||
|
result = result.filter((message) =>
|
||||||
|
message.subject.toLowerCase().includes(query)
|
||||||
|
|| message.body.toLowerCase().includes(query)
|
||||||
|
|| (agentMap.get(message.fromAgentId)?.name ?? "").toLowerCase().includes(query)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (filters.priority !== "all") {
|
||||||
|
result = result.filter((message) => message.priority === filters.priority)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { AgentMessageRecord, AgentRecord } from "../../src/shared/types";
|
||||||
|
import {
|
||||||
|
DEFAULT_MESSAGE_FILTERS,
|
||||||
|
filterChannelMessages,
|
||||||
|
hasActiveMessageFilters,
|
||||||
|
} from "../../src/renderer/components/communication/message-filters";
|
||||||
|
|
||||||
|
function agent(id: string, name: string): AgentRecord {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
companyId: "company-1",
|
||||||
|
name,
|
||||||
|
role: name,
|
||||||
|
title: name,
|
||||||
|
department: null,
|
||||||
|
status: "active",
|
||||||
|
reportsTo: null,
|
||||||
|
connectorId: "openai-codex",
|
||||||
|
workspaceId: null,
|
||||||
|
model: null,
|
||||||
|
capabilities: "",
|
||||||
|
budgetMonthlyUsd: 0,
|
||||||
|
spentMonthlyUsd: 0,
|
||||||
|
metadataJson: "{}",
|
||||||
|
heartbeatEnabled: false,
|
||||||
|
heartbeatIntervalSec: 300,
|
||||||
|
lastHeartbeatAt: null,
|
||||||
|
createdAt: "2026-04-22T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-04-22T00:00:00.000Z",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function message(input: Partial<AgentMessageRecord>): AgentMessageRecord {
|
||||||
|
return {
|
||||||
|
id: input.id ?? "msg-1",
|
||||||
|
companyId: "company-1",
|
||||||
|
fromAgentId: input.fromAgentId ?? "agent-ceo",
|
||||||
|
toAgentId: input.toAgentId ?? null,
|
||||||
|
senderKind: input.senderKind ?? "agent",
|
||||||
|
channel: input.channel ?? "company",
|
||||||
|
channelTargetId: input.channelTargetId ?? null,
|
||||||
|
subject: input.subject ?? "Subject",
|
||||||
|
body: input.body ?? "Body",
|
||||||
|
priority: input.priority ?? "normal",
|
||||||
|
parentMessageId: input.parentMessageId ?? null,
|
||||||
|
attachmentsJson: input.attachmentsJson ?? "[]",
|
||||||
|
createdAt: input.createdAt ?? "2026-04-22T00:00:00.000Z",
|
||||||
|
readAt: input.readAt ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("message filters", () => {
|
||||||
|
it("keeps read and unread channel messages in the default channel view", () => {
|
||||||
|
const read = message({ id: "read", readAt: "2026-04-22T00:00:01.000Z" });
|
||||||
|
const unread = message({ id: "unread", readAt: null });
|
||||||
|
|
||||||
|
const result = filterChannelMessages(
|
||||||
|
[read, unread],
|
||||||
|
DEFAULT_MESSAGE_FILTERS,
|
||||||
|
new Map(),
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.map((entry) => entry.id)).toEqual(["read", "unread"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters by text and sender inside the selected channel", () => {
|
||||||
|
const ceo = agent("agent-ceo", "Ada CEO");
|
||||||
|
const cto = agent("agent-cto", "Grace CTO");
|
||||||
|
const messages = [
|
||||||
|
message({ id: "ceo", fromAgentId: ceo.id, subject: "Launch", body: "Board update" }),
|
||||||
|
message({ id: "cto", fromAgentId: cto.id, subject: "Infra", body: "Runtime ready" }),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = filterChannelMessages(
|
||||||
|
messages,
|
||||||
|
{ ...DEFAULT_MESSAGE_FILTERS, search: "grace" },
|
||||||
|
new Map([
|
||||||
|
[ceo.id, ceo],
|
||||||
|
[cto.id, cto],
|
||||||
|
]),
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.map((entry) => entry.id)).toEqual(["cto"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects active message filters", () => {
|
||||||
|
expect(hasActiveMessageFilters(DEFAULT_MESSAGE_FILTERS)).toBe(false);
|
||||||
|
expect(hasActiveMessageFilters({ ...DEFAULT_MESSAGE_FILTERS, priority: "urgent" })).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user