projects/deliberation_at_scale/packages/frontend/hooks/useRoomMessages.ts (111 lines of code) (raw):
import { alphabetical } from "radash";
import { MessageType, RoomMessageFragment, FullParticipantFragment, useGetRoomMessagesQuery } from "@/generated/graphql";
import useRealtimeQuery from "./useRealtimeQuery";
import { RoomId } from "@/state/slices/room";
import { getIconByMessageType } from "@/components/EntityIcons";
import { Message } from "@/types/flows";
import dayjs from "dayjs";
import { useCallback } from "react";
import { useLingui } from "@lingui/react";
import { msg } from "@lingui/macro";
import { ONE_SECOND_MS } from "@/utilities/constants";
export interface UseMessagesOptions {
roomId: RoomId;
participants: FullParticipantFragment[] | undefined;
userId: string;
participantMessageHistoryAmount?: number;
botMessageHistoryAmount?: number;
}
export default function useRoomMessages(options?: UseMessagesOptions) {
const {
roomId,
participants,
userId,
participantMessageHistoryAmount = 1,
botMessageHistoryAmount = 1,
} = options ?? {};
const hasRoom = !!roomId;
const { _ } = useLingui();
const insertFilter = `room_id=eq.${roomId}`;
const { data: messagesData, loading: messagesLoading, refetch: refetchMessages } = useRealtimeQuery(useGetRoomMessagesQuery({
variables: {
roomId,
botMessageHistoryAmount,
participantMessageHistoryAmount
},
}), {
enableSubscriptions: hasRoom,
autoRefetch: hasRoom,
autoRefetchIntervalMs: ONE_SECOND_MS * 4,
tableEventsLookup: {
messages: {
refetchOperations: [],
appendOnInsertEdgePaths: ['messagesCollection', 'botMessagesCollection', 'participantMessagesCollection'],
listenFilters: {
INSERT: insertFilter,
},
},
},
});
const convertMessage = useCallback((databaseMessage: RoomMessageFragment) => {
const { id, active, content, visibility_type: visibilityType, created_at: createdAt, type, participant_id: participantId, easy_language: easyLanguage, safe_language: safeLanguage } = databaseMessage ?? {};
const isBot = (type === MessageType.Bot);
const participant = participants?.find((participant) => participant.id === participantId);
const nickName = isBot ? _(msg`AI Moderator`) : (participant?.nick_name ?? _(msg`Contributor`));
const isCurrentParticipant = (!!userId && participant?.user_id === userId);
const nameIcon = getIconByMessageType(type);
const highlighted = isBot;
const flagged = easyLanguage === false || safeLanguage === false;
let flaggedReason = '';
if (safeLanguage === false) {
flaggedReason = _(msg`This message has been flagged as inappropriate.`);
} else if (easyLanguage === false) {
flaggedReason = _(msg`This message has been flagged as difficult to understand.`);
}
// guard: skip any inactive messages
if (!active) {
return null;
}
// guard: skip any invalid or private messages are not meant for the current participant
if (!!participant && visibilityType === 'private' && !isCurrentParticipant) {
return null;
}
return {
id,
name: nickName,
nameId: participant?.id,
date: dayjs(createdAt).toISOString(),
nameIcon,
content,
flagged,
flaggedReason,
highlighted,
} satisfies Message;
}, [_, participants, userId]);
const convertMessages = useCallback((messages: RoomMessageFragment[], types?: MessageType[]) => {
return messages.filter((message) => !types || types.includes(message.type)).map(convertMessage).filter((message) => message !== null) as Message[];
}, [convertMessage]);
const databaseMessages = extractNodesFromEdges(messagesData?.messagesCollection?.edges ?? []) as RoomMessageFragment[];
const databaseBotMessages = extractNodesFromEdges(messagesData?.botMessagesCollection?.edges ?? []) as RoomMessageFragment[];
const databaseParticipantMessages = extractNodesFromEdges(messagesData?.participantMessagesCollection?.edges ?? []) as RoomMessageFragment[];
const messages = orderMessages(convertMessages(databaseMessages));
const botMessages = orderMessages(convertMessages(databaseBotMessages, [MessageType.Bot]));
const participantMessages = orderMessages(convertMessages(databaseParticipantMessages, [MessageType.Chat, MessageType.Voice]));
const lastBotMessages = botMessages.slice(-1 * botMessageHistoryAmount);
const lastParticipantMessages = participantMessages.slice(-1 * participantMessageHistoryAmount);
return {
messages,
messagesLoading,
refetchMessages,
lastBotMessages,
lastParticipantMessages,
};
}
function extractNodesFromEdges<T extends { node: K }, K>(edges: T[] | undefined): K[] {
return edges?.map((edge) => edge.node).filter((node) => node !== null) ?? [];
}
function orderMessages(messages: Message[]) {
return alphabetical(
messages,
(message) => String(message.date) ?? '',
"asc"
);
}