Chat
// chat.movex.ts
import { Action } from 'movex';
import { MovexClient } from 'movex-core-util';
export type ParticipantId = MovexClient['id'];
export type Color = string;
type ChatMsg = {
at: number;
content: string;
participantId: ParticipantId;
};
export type ChatState = {
participants: {
[id in ParticipantId]: {
id: ParticipantId;
color: Color;
joinedAt: number;
} & (
| {
active: false;
isTyping?: null;
leftAt: number;
}
| {
active: true;
isTyping: boolean;
leftAt?: null; // Limitation with undefined. Some jsons remove it altogether which could create mismatches. Use null
}
);
};
messages: ChatMsg[];
};
export const initialChatState: ChatState = {
participants: {},
messages: [],
};
export type ChatActions =
| Action<
'addParticipant',
{
id: ParticipantId;
color: Color;
atTimestamp: number;
}
>
| Action<
'removeParticipant',
{
id: ParticipantId;
atTimestamp: number;
}
>
| Action<
'writeMessage',
{
participantId: ParticipantId;
msg: string;
atTimestamp: number;
// id: string;
}
>
| Action<
'setTyping',
{
participantId: ParticipantId;
isTyping: boolean;
}
>;
export const chatReducer = (
state = initialChatState as ChatState,
action: ChatActions
): ChatState => {
if (action.type === 'addParticipant') {
return {
...state,
participants: {
...state.participants,
[action.payload.id]: {
id: action.payload.id,
joinedAt: action.payload.atTimestamp,
color: action.payload.color,
active: true,
leftAt: null,
isTyping: false,
},
},
};
} else if (action.type === 'removeParticipant') {
if (!state.participants[action.payload.id]) {
return state;
}
return {
...state,
participants: {
...state.participants,
[action.payload.id]: {
...state.participants[action.payload.id],
active: false,
isTyping: undefined,
leftAt: action.payload.atTimestamp,
},
},
};
} else if (action.type === 'writeMessage') {
return {
...state,
messages: [
...state.messages,
{
// id: action.payload.id, // This could be improved if needed. Can come from outside etc...
at: action.payload.atTimestamp,
content: action.payload.msg,
participantId: action.payload.participantId,
},
],
};
} else if (action.type === 'setTyping') {
const prevParticipant = state.participants[action.payload.participantId];
if (!prevParticipant) {
return state;
}
if (!prevParticipant.active) {
return state;
}
return {
...state,
participants: {
...state.participants,
[action.payload.participantId]: {
...prevParticipant,
isTyping: action.payload.isTyping,
},
},
};
}
return state;
};