Movex is feature-complete yet still in Development for now. Contributions and feedback are much appreciated!
Docs
Chat

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;
};