From 8e2293d7380f4a248713f95e7e62d73e255b7cc4 Mon Sep 17 00:00:00 2001 From: Titouan Rigoudy Date: Thu, 23 Sep 2021 23:28:25 +0200 Subject: [PATCH] Introduce message acking. --- src/modules/room/RoomChatMessageList.tsx | 13 +++- src/modules/room/message.ts | 1 + src/modules/room/slice.ts | 79 ++++++++++++++++++++++-- 3 files changed, 86 insertions(+), 7 deletions(-) diff --git a/src/modules/room/RoomChatMessageList.tsx b/src/modules/room/RoomChatMessageList.tsx index c5a5ecc..e7111f3 100644 --- a/src/modules/room/RoomChatMessageList.tsx +++ b/src/modules/room/RoomChatMessageList.tsx @@ -9,16 +9,21 @@ interface Props { const messageShape = "px-3 py-1 rounded-xl"; +function boolToString(b: boolean): string { + return b ? "true" : "false"; +} + const RoomChatMessageList: FC = ({ loginUserName, messages }) => { // Append all messages in the chat room. const children = []; let i = 0; - for (const { userName, message } of messages) { + for (const { userName, message, acked } of messages) { + const ackedString = boolToString(acked); if (userName === loginUserName) { children.push(
  • - {message} + {message}, acked: {ackedString}
  • ); @@ -26,7 +31,9 @@ const RoomChatMessageList: FC = ({ loginUserName, messages }) => { children.push(
  • {userName}
    -
    {message}
    +
    + {message}, acked: {ackedString} +
  • ); } diff --git a/src/modules/room/message.ts b/src/modules/room/message.ts index 6aabf28..08ac6ad 100644 --- a/src/modules/room/message.ts +++ b/src/modules/room/message.ts @@ -38,6 +38,7 @@ function convertMessages(messages: any[]): RoomMessage[] { result.push({ userName: message.user_name, message: message.message, + acked: true, }); } return result; diff --git a/src/modules/room/slice.ts b/src/modules/room/slice.ts index ea235b2..bcacea2 100644 --- a/src/modules/room/slice.ts +++ b/src/modules/room/slice.ts @@ -1,7 +1,10 @@ +// Defines the slice of state pertaining to chat rooms. + import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { RootState } from "app/store"; +// The status of our membership in a chat room. export enum RoomMembership { Joining, Joined, @@ -9,28 +12,81 @@ export enum RoomMembership { Left, } +// A message sent to a chat room. export interface RoomMessage { + // The sender's name. userName: string; + + // The message contents. message: string; + + // Whether the server has acknowledged the message in a response. + // Only ever false for messages we sent. + acked: boolean; + + // TODO: timestamp. +} + +// Attempts to ack `message` upon receival of `messageContents` from `userName`. +// +// Returns true if `message` was unacked, was sent by `userName` and contains +// `messageContents`, in which case `message.acked` is set to `true`. +// Returns false otherwise. +function maybeAckMessage( + message: RoomMessage, + userName: string, + messageContents: string +): boolean { + if (message.acked) { + return false; // Already acked. + } + if (message.userName !== userName || message.message !== messageContents) { + return false; // Wrong message. + } + + message.acked = true; + return true; } +// The state of a chat room. export interface RoomState { + // The name of the chat room. name: string; + + // Our membership status in this room. membership: RoomMembership; + + // Un visibility of this room. visibility: string; + + // Whether this room is operated by anyone. operated: boolean; + + // How many users are members of this chat room. userCount: number; + + // The user name of the chat room's owner, if any. owner: string; + + // The user names of the chat room's operators. operators: string[]; + + // The user names of the members of the chat room. members: string[]; + + // The list of messages sent to the chat room. messages: RoomMessage[]; + + // Tickers for the room: [sender user name, ticker contents]. tickers: [string, string][]; } +// Maps room names to room state. export interface RoomMap { [name: string]: RoomState; } +// The slice of global state pertaining to chat rooms. export interface RoomSliceState { rooms: RoomMap; } @@ -39,6 +95,7 @@ const initialState: RoomSliceState = { rooms: {}, }; +// The payload of an action to send a message to a chat room. export interface RoomMessagePayload { roomName: string; userName: string; @@ -70,17 +127,31 @@ export const roomSlice = createSlice({ state: RoomSliceState, action: PayloadAction ) => { - const { roomName, userName, message } = action.payload; + const { roomName, userName } = action.payload; + const messageContents = action.payload.message; const room = state.rooms[roomName]; if (room === undefined) { console.log( `Unknown room ${roomName} received message from ` + - `${userName}: ${message}` + `${userName}: ${messageContents}` ); return; } - room.messages.push({ userName, message }); + // Find the last unacked message that corresponds, if any. + // + // TODO: This lookup is O(#messages). Speed it up: + // - using a list of unacked messages -> O(#unacked) + // - using a map of unacked contents to messages + // + for (let i = room.messages.length - 1; i >= 0; i--) { + const message = room.messages[i]; + if (maybeAckMessage(message, userName, messageContents)) { + return; + } + } + + room.messages.push({ userName, message: messageContents, acked: true }); }, roomSendMessage: ( state: RoomSliceState, @@ -95,7 +166,7 @@ export const roomSlice = createSlice({ return; } - room.messages.push({ userName, message }); + room.messages.push({ userName, message, acked: false }); }, roomSetAll: (state: RoomSliceState, action: PayloadAction) => { state.rooms = {};