conversationSlice

This commit is contained in:
bedlam343 2024-05-25 19:57:31 -07:00
parent f0755936fc
commit 6888a0a7bc
16 changed files with 144 additions and 58 deletions

View File

@ -7,17 +7,21 @@ type ListElementProps = {
element: Element;
outerDivStyleClass: string;
children?: ReactNode;
unreadCount?: number;
};
const ListElement = ({
element,
outerDivStyleClass,
children,
unreadCount,
}: ListElementProps) => {
const renderElement = () => {
switch (element.type) {
case 'request-approval':
return <RequestApprovalElement element={element} />;
return (
<RequestApprovalElement element={element} unreadCount={unreadCount} />
);
case 'missile-incoming':
return <MissileIncomingElement element={element} />;
default:

View File

@ -1,7 +1,7 @@
import type { MissileIncomingElement as MissileIncomingElementType } from 'src/types/element';
import IconElement from 'src/components/Element/Simple/IconElement';
import { useAppSelector } from 'src/redux/hooks';
import { getMessage } from 'src/redux/slices/minimapSlice';
import { getMessage } from 'src/redux/slices/conversationSlice';
type MissileIncomingProps = {
element: MissileIncomingElementType;

View File

@ -1,20 +1,21 @@
import { useEffect, type ReactNode } from 'react';
import { type RequestApprovalElement as RequestApprovalElementType } from 'src/types/element';
import ButtonElement from '../Simple/ButtonElement';
import IconElement from '../Simple/IconElement';
import { useAppSelector } from 'src/redux/hooks';
import { getMessage } from 'src/redux/slices/minimapSlice';
import { getMessage } from 'src/redux/slices/conversationSlice';
type RequestApprovalProps = {
element: RequestApprovalElementType;
children?: ReactNode;
unreadCount?: number;
};
const RequestApprovalElement = ({
element,
children,
unreadCount,
}: RequestApprovalProps) => {
const { id, collapsed, icon, rightButton, leftButton, messageId } = element;
const { id, icon, messageId } = element;
const message = useAppSelector((state) => getMessage(state, messageId));
return (
@ -22,6 +23,8 @@ const RequestApprovalElement = ({
<IconElement element={icon} />
{/* @ts-ignore */}
<span>{message?.data?.target?.type}</span>
{unreadCount && unreadCount > 0 && <span>{unreadCount}</span>}
</div>
);
};

View File

@ -1,7 +1,7 @@
import { useAppSelector } from 'src/redux/hooks';
import { type HistoryWidget as HistoryWidgetType } from 'src/types/widget';
import HistoryElement from 'src/components/Element/Complex/HistoryElement';
import { getConversationMessages } from 'src/redux/slices/minimapSlice';
import { getConversation } from 'src/redux/slices/conversationSlice';
import { getCommunication } from 'src/redux/slices/communicationSlice';
import MessageNumber from 'src/ui/history/MessageNumber';
@ -13,10 +13,11 @@ const HistoryWidget = ({ widget }: HistoryWidgetProps) => {
const { id, x, y, w, h, elements } = widget;
const { activeConversationId } = useAppSelector(getCommunication);
const convoMessages = useAppSelector((state) =>
getConversationMessages(state, activeConversationId),
const conversation = useAppSelector((state) =>
getConversation(state, activeConversationId),
);
const numMessages = convoMessages.length;
const messages = conversation?.messages ? [...conversation.messages] : [];
const numMessages = messages.length || 0;
// const activeElementID = useAppSelector(getSelectedElementID);
// const highlightIndex = convoMessages.findIndex(
@ -27,7 +28,7 @@ const HistoryWidget = ({ widget }: HistoryWidgetProps) => {
if (numMessages) {
return (
<div className="grid grid-cols-12 items-start justify-start p-4 gap-4">
{convoMessages.reverse().map((message, index) => (
{messages.reverse().map((message, index) => (
<>
<div key={message.id} className="col-span-1 flex flex-col h-full">
<MessageNumber number={numMessages - index} glow />

View File

@ -7,7 +7,10 @@ import {
import { getElementsInGaze, getGazesAndKeys } from 'src/redux/slices/gazeSlice';
import type { Widget } from 'src/types/widget';
import ListElement from 'src/components/Element/Complex/ListElement';
import { getMessages } from 'src/redux/slices/minimapSlice';
import {
getConversations,
getMessages,
} from 'src/redux/slices/conversationSlice';
type ListWidgetProps = {
widget: Widget;
@ -17,7 +20,8 @@ const LIST_ELEMENT_HEIGHT = 80;
const GAP_BETWEEN_ELEMENTS = 6;
const ListWidget = ({ widget }: ListWidgetProps) => {
const messages = useAppSelector(getMessages);
const conversations = useAppSelector(getConversations);
const elementsInGaze = useAppSelector(getElementsInGaze);
const gazesAndKeys = useAppSelector(getGazesAndKeys);
const { activeElementId } = useAppSelector(getCommunication);
@ -87,13 +91,13 @@ const ListWidget = ({ widget }: ListWidgetProps) => {
dispatch(
updateCommunication({
// @ts-ignore
activeConversationId: messages[element.messageId].conversationId,
activeConversationId: element.conversationId,
activeElementId: element.id,
}),
);
}
});
}, [gazesAndKeys, dispatch, elementInGazeId, messages, widget.elements]);
}, [gazesAndKeys, dispatch, elementInGazeId, widget.elements]);
// check if the list is overflowed
// useEffect(() => {
@ -148,10 +152,15 @@ const ListWidget = ({ widget }: ListWidgetProps) => {
// don't render element if it's message is not the latest in the conversation
// @ts-ignore
const elemMessageId = element.messageId;
if (elemMessageId) {
if (!messages[elemMessageId].latestInConvo) {
return null;
}
// @ts-ignore
const elemConvoId = element.conversationId;
if (
elemMessageId &&
elemConvoId &&
conversations[elemConvoId] &&
conversations[elemConvoId].latestMessageId !== elemMessageId
) {
return null;
}
// style for the element which is current being hoverd over

View File

@ -19,7 +19,7 @@ const useWorldSim = () => {
if (message)
setMessages((prevMessages) => [
...prevMessages,
{ ...message, fulfilled: false, read: false, latestInConvo: true }, // newest message is latest in convo
{ ...message, fulfilled: false, read: false }, // newest message is latest in convo
]);
if (stressLevel) setStressLevel(stressLevel);
});

View File

@ -5,10 +5,10 @@ import {
type InitialMinimapState,
getWidgets,
initializeState,
addMessage,
getStressLevel,
setStressLevel,
} from 'src/redux/slices/minimapSlice';
import { addMessage } from 'src/redux/slices/conversationSlice';
// ~~~~~~~ Cusdom Hooks ~~~~~~~
import useWorldSim from 'src/hooks/useWorldSim';
// ~~~~~~~ Prototype ~~~~~~~

View File

@ -35,6 +35,7 @@ const requestApprovalToAttackMessageHigh = (
'list',
),
message.id,
message.conversationId,
listWidgetId,
lpdHelper.generateIconElement(
lpdHelper.generateBaseElement(uuid(), 'visual', 56, 56),
@ -182,6 +183,7 @@ const missileToOwnshipDetectedMessageHigh = (
'list',
),
message.id,
message.conversationId,
listWidgetId,
lpdHelper.generateIconElement(
lpdHelper.generateBaseElement(uuid(), 'visual', 56, 56),

View File

@ -28,6 +28,7 @@ const requestApprovalToAttackMessageLow = (
lpdHelper.generateRequestApprovalElement(
lpdHelper.generateBaseElement(uuid(), 'visual', 30, 30, message.priority),
message.id,
message.conversationId,
listWidgetId,
lpdHelper.generateIconElement(
lpdHelper.generateBaseElement(uuid(), 'visual', 56, 56),
@ -224,6 +225,7 @@ const missileToOwnshipDetectedMessageLow = (
message.priority,
),
message.id,
message.conversationId,
listWidgetId,
lpdHelper.generateIconElement(
lpdHelper.generateBaseElement(uuid(), 'visual', 56, 56),

View File

@ -28,6 +28,7 @@ const requestApprovalToAttackMessageMedium = (
lpdHelper.generateRequestApprovalElement(
lpdHelper.generateBaseElement(uuid(), 'visual', 30, 30, message.priority),
message.id,
message.conversationId,
listWidgetId,
lpdHelper.generateIconElement(
lpdHelper.generateBaseElement(uuid(), 'visual', 56, 56),
@ -174,6 +175,7 @@ const missileToOwnshipDetectedMessageMedium = (
message.priority,
),
message.id,
message.conversationId,
listWidgetId,
lpdHelper.generateIconElement(
lpdHelper.generateBaseElement(uuid(), 'visual', 56, 56),

View File

@ -0,0 +1,88 @@
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { Message } from 'src/types/schema-types';
type InitialState = {
conversations: {
[key: string]: {
id: string;
messages: Message[];
numUnreadMessages: number;
latestMessageId: string;
};
};
};
const initialState: InitialState = {
conversations: {},
};
export const conversationSlice = createSlice({
name: 'conversation',
initialState,
reducers: {
addMessage: (state, action: PayloadAction<Message>) => {
const message = action.payload;
const { conversationId } = message;
if (!state.conversations[conversationId]) {
state.conversations[conversationId] = {
id: conversationId,
messages: [message],
numUnreadMessages: 1,
latestMessageId: message.id,
};
} else {
state.conversations[conversationId].messages.push(message);
state.conversations[conversationId].numUnreadMessages++;
state.conversations[conversationId].latestMessageId = message.id;
}
},
},
selectors: {
getMessages: (state) => {
const messages: Message[] = [];
for (const conversationId in state.conversations) {
const conversation = state.conversations[conversationId];
messages.push(...conversation.messages);
}
return messages;
},
getMessage: (state, messageId: string) => {
for (const conversationId in state.conversations) {
const conversation = state.conversations[conversationId];
const message = conversation.messages.find(
(message) => message.id === messageId,
);
if (message) return message;
}
return null;
},
getConversationOfMessage: (state, messageId: string) => {
for (const conversationId in state.conversations) {
const conversation = state.conversations[conversationId];
const message = conversation.messages.find(
(message) => message.id === messageId,
);
if (message) return conversation;
return null;
}
},
getConversations: (state) => state.conversations,
getConversation: (state, conversationId: string) =>
state.conversations[conversationId],
},
});
export const { addMessage } = conversationSlice.actions;
export const {
getConversations,
getConversation,
getConversationOfMessage,
getMessage,
getMessages,
} = conversationSlice.selectors;

View File

@ -15,7 +15,6 @@ export type InitialMinimapState = {
drones: VehicleWidget[];
widgets: WidgetMap;
messages: MessageMap;
sections: Section[];
stressLevel: number;
@ -26,7 +25,6 @@ const initialState: InitialMinimapState = {
audioComplexity: 0,
ownship: null,
drones: [],
messages: {},
widgets: {},
sections: [],
stressLevel: 0,
@ -48,7 +46,6 @@ export const minimapSlice = createSlice({
state.ownship = action.payload.ownship;
state.drones = action.payload.drones;
state.widgets = action.payload.widgets;
state.messages = action.payload.messages;
state.sections = action.payload.sections;
},
@ -396,22 +393,6 @@ export const minimapSlice = createSlice({
state.audioComplexity = action.payload;
},
addMessage: (state, action: PayloadAction<Message>) => {
const updatedMessages = { ...state.messages };
// set latestInConvo to false for all messages in the same conversation
Object.keys(updatedMessages).forEach((id) => {
if (
updatedMessages[id].conversationId === action.payload.conversationId
) {
updatedMessages[id].latestInConvo = false;
}
});
updatedMessages[action.payload.id] = action.payload;
state.messages = updatedMessages;
},
setStressLevel: (state, action: PayloadAction<number>) => {
state.stressLevel = action.payload;
},
@ -460,18 +441,6 @@ export const minimapSlice = createSlice({
getVisualComplexity: (state) => state.visualComplexity,
getAudioComplexity: (state) => state.audioComplexity,
getMessages: (state) => state.messages,
getMessage: (state, messageId: string) => state.messages[messageId],
getConversationMessages: (state, conversationId: string) => {
const messages: Message[] = [];
Object.keys(state.messages).forEach((messageId) => {
if (state.messages[messageId].conversationId === conversationId) {
messages.push(state.messages[messageId]);
}
});
return messages;
},
getStressLevel: (state) => state.stressLevel,
// ~~~~~ selectors for ships ~~~~~
@ -495,7 +464,6 @@ export const {
initializeState,
addMapSection,
addMessage,
addWidget,
addHandledMessageToWidget,
addElementsToWidget,
@ -528,10 +496,6 @@ export const {
getAllElements,
getElementsOnScreen,
getMessages,
getMessage,
getConversationMessages,
getOwnship,
getDrones,

View File

@ -8,11 +8,17 @@ import {
import { minimapSlice } from './slices/minimapSlice';
import { gazeSlice } from './slices/gazeSlice';
import { communicationSlice } from './slices/communicationSlice';
import { conversationSlice } from './slices/conversationSlice';
// pass in slices to combine into combineSlices()
// `combineSlices` automatically combines the reducers using
// their `reducerPath`s, therefore we no longer need to call `combineReducers`.
const rootReducer = combineSlices(minimapSlice, gazeSlice, communicationSlice);
const rootReducer = combineSlices(
minimapSlice,
gazeSlice,
communicationSlice,
conversationSlice,
);
// Infer the `RootState` type from the root reducer
type RootState = ReturnType<typeof rootReducer>;

View File

@ -86,6 +86,7 @@ export type CustomElement = BaseElement & {
export type RequestApprovalElement = BaseElement & {
type: 'request-approval';
messageId: string;
conversationId: string;
icon: IconElement;
leftButton: ButtonElement;
rightButton: ButtonElement;
@ -96,6 +97,7 @@ export type RequestApprovalElement = BaseElement & {
export type MissileIncomingElement = BaseElement & {
type: 'missile-incoming';
messageId: string;
conversationId: string;
icon: IconElement;
widgetId: string;
};

View File

@ -24,7 +24,6 @@ export type BaseMessage<TKind extends string, TData extends object> = {
priority: Priority;
kind: TKind;
data: TData;
latestInConvo?: boolean;
read?: boolean;
fulfilled?: boolean;
tags?: string[];

View File

@ -211,12 +211,14 @@ const generateAudioElement = (
const generateMissileIncomingElement = (
baseElement: Element.BaseElement,
messageId: string,
conversationId: string,
widgetId: string,
icon: Element.IconElement,
): Element.MissileIncomingElement => ({
...baseElement,
type: 'missile-incoming',
messageId,
conversationId,
widgetId,
icon,
});
@ -224,6 +226,7 @@ const generateMissileIncomingElement = (
const generateRequestApprovalElement = (
baseElement: Element.BaseElement,
messageId: string,
conversationId: string,
widgetId: string,
icon: Element.IconElement,
leftButton: Element.ButtonElement,
@ -232,6 +235,7 @@ const generateRequestApprovalElement = (
...baseElement,
type: 'request-approval',
messageId,
conversationId,
widgetId,
icon,
leftButton,