// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors
// SPDX-License-Identifier: Apache-2.0
import { useState, useEffect } from 'react';
import type { Communication } from '@medplum/fhirtypes';
import { useMedplum } from '@medplum/react';
import { getReferenceString } from '@medplum/core';
export interface UseThreadInboxOptions {
query: string;
threadId: string | undefined;
offset?: number;
count?: number;
}
export interface UseThreadInboxReturn {
loading: boolean;
error: Error | null;
// Tuple: [Parent Thread, Last Message in Thread (optional)]
threadMessages: [Communication, Communication | undefined][];
selectedThread: Communication | undefined;
total: number | undefined;
addThreadMessage: (message: Communication) => void;
handleThreadStatusChange: (newStatus: Communication['status']) => Promise<void>;
}
/*
useThreadInbox is a hook that fetches all communications and returns the thread messages and selected thread.
All comunications returned do not have a partOf field.
It also provides a function to update the status of the selected thread.
@param query - The query to fetch all communications.
@param threadId - The id of the thread to select.
@returns The thread messages and selected thread.
@returns A function to update the status of the selected thread.
*/
export function useThreadInbox({ query, threadId, offset, count }: UseThreadInboxOptions): UseThreadInboxReturn {
const medplum = useMedplum();
const [loading, setLoading] = useState(false);
const [threadMessages, setThreadMessages] = useState<[Communication, Communication | undefined][]>([]);
const [selectedThread, setSelectedThread] = useState<Communication | undefined>(undefined);
const [error, setError] = useState<Error | null>(null);
const [total, setTotal] = useState<number | undefined>(undefined);
useEffect(() => {
const fetchAllCommunications = async (): Promise<void> => {
const searchParams = new URLSearchParams(query);
searchParams.append('identifier:not', 'ai-message-topic');
searchParams.append('part-of:missing', 'true');
if (offset !== undefined) {
searchParams.append('_offset', offset.toString());
}
if (count !== undefined) {
searchParams.append('_count', count.toString());
}
searchParams.append('_total', 'accurate');
const bundle = await medplum.search('Communication', searchParams.toString(), { cache: 'no-cache' });
const parents =
bundle.entry
?.map((entry) => entry.resource as Communication)
.filter((r): r is Communication => r !== undefined) || [];
if (bundle.total !== undefined) {
setTotal(bundle.total);
}
if (parents.length === 0) {
setThreadMessages([]);
return;
}
const queryParts = parents.map((parent) => {
const safeId = parent.id?.replace(/-/g, '') || '';
const alias = `thread_${safeId}`;
const ref = getReferenceString(parent);
return `
${alias}: CommunicationList(
part_of: "${ref}"
_sort: "-sent"
_count: 1
) {
id
meta {
lastUpdated
}
partOf {
reference
}
sender {
display
reference
}
payload {
contentString
}
sent
status
}
`;
});
const fullQuery = `
query {
${queryParts.join('\n')}
}
`;
const response = await medplum.graphql(fullQuery);
const threadsWithReplies = parents
.map((parent) => {
const safeId = parent.id?.replace(/-/g, '') || '';
const alias = `thread_${safeId}`;
const childList = response.data[alias] as Communication[] | undefined;
const lastMessage = childList && childList.length > 0 ? childList[0] : undefined;
return [parent, lastMessage];
})
.filter((thread): thread is [Communication, Communication] => thread[1] !== undefined);
setThreadMessages(threadsWithReplies);
};
setLoading(true);
fetchAllCommunications()
.catch((err) => {
console.error('Error fetching inbox threads:', err);
setError(err as Error);
})
.finally(() => {
setLoading(false);
});
}, [medplum, query, offset, count]);
useEffect(() => {
const fetchThread = async (): Promise<void> => {
if (threadId) {
const thread = threadMessages.find((t) => t[0].id === threadId);
if (thread) {
setSelectedThread(thread[0]);
} else {
try {
const communication: Communication = await medplum.readResource('Communication', threadId);
if (communication.partOf === undefined) {
setSelectedThread(communication);
} else {
const parentRef = communication.partOf[0].reference;
if (parentRef) {
const parent = await medplum.readReference({ reference: parentRef } as any);
setSelectedThread(parent as Communication);
}
}
} catch (err) {
setError(err as Error);
}
}
} else {
setSelectedThread(undefined);
}
};
fetchThread().catch((err) => {
setError(err as Error);
});
}, [threadId, threadMessages, medplum]);
const handleThreadStatusChange = async (newStatus: Communication['status']): Promise<void> => {
if (!selectedThread) {
return;
}
try {
const updatedThread = await medplum.updateResource({
...selectedThread,
status: newStatus,
});
setSelectedThread(updatedThread);
setThreadMessages((prev) =>
prev.map(([parent, lastMsg]) => (parent.id === updatedThread.id ? [updatedThread, lastMsg] : [parent, lastMsg]))
);
} catch (err) {
setError(err as Error);
}
};
const addThreadMessage = (message: Communication): void => {
// Assuming this adds a new TOP level thread
setThreadMessages((prev) => [[message, undefined], ...prev]);
};
return {
loading,
error,
threadMessages,
selectedThread,
total,
addThreadMessage,
handleThreadStatusChange,
};
}