Browse Source

Introduce message acking.

main
Titouan Rigoudy 4 years ago
parent
commit
8e2293d738
3 changed files with 86 additions and 7 deletions
  1. +10
    -3
      src/modules/room/RoomChatMessageList.tsx
  2. +1
    -0
      src/modules/room/message.ts
  3. +75
    -4
      src/modules/room/slice.ts

+ 10
- 3
src/modules/room/RoomChatMessageList.tsx View File

@ -9,16 +9,21 @@ interface Props {
const messageShape = "px-3 py-1 rounded-xl"; const messageShape = "px-3 py-1 rounded-xl";
function boolToString(b: boolean): string {
return b ? "true" : "false";
}
const RoomChatMessageList: FC<Props> = ({ loginUserName, messages }) => { const RoomChatMessageList: FC<Props> = ({ loginUserName, messages }) => {
// Append all messages in the chat room. // Append all messages in the chat room.
const children = []; const children = [];
let i = 0; let i = 0;
for (const { userName, message } of messages) {
for (const { userName, message, acked } of messages) {
const ackedString = boolToString(acked);
if (userName === loginUserName) { if (userName === loginUserName) {
children.push( children.push(
<li key={i} className="self-end py-1 max-w-3/4"> <li key={i} className="self-end py-1 max-w-3/4">
<div className={`bg-yellow-600 text-white ${messageShape}`}> <div className={`bg-yellow-600 text-white ${messageShape}`}>
{message}
{message}, acked: {ackedString}
</div> </div>
</li> </li>
); );
@ -26,7 +31,9 @@ const RoomChatMessageList: FC<Props> = ({ loginUserName, messages }) => {
children.push( children.push(
<li key={i} className="self-start py-1 max-w-3/4"> <li key={i} className="self-start py-1 max-w-3/4">
<div className="text-sm text-yellow-600 px-1 py-1">{userName}</div> <div className="text-sm text-yellow-600 px-1 py-1">{userName}</div>
<div className={`bg-yellow-100 ${messageShape}`}>{message}</div>
<div className={`bg-yellow-100 ${messageShape}`}>
{message}, acked: {ackedString}
</div>
</li> </li>
); );
} }


+ 1
- 0
src/modules/room/message.ts View File

@ -38,6 +38,7 @@ function convertMessages(messages: any[]): RoomMessage[] {
result.push({ result.push({
userName: message.user_name, userName: message.user_name,
message: message.message, message: message.message,
acked: true,
}); });
} }
return result; return result;


+ 75
- 4
src/modules/room/slice.ts View File

@ -1,7 +1,10 @@
// Defines the slice of state pertaining to chat rooms.
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { RootState } from "app/store"; import { RootState } from "app/store";
// The status of our membership in a chat room.
export enum RoomMembership { export enum RoomMembership {
Joining, Joining,
Joined, Joined,
@ -9,28 +12,81 @@ export enum RoomMembership {
Left, Left,
} }
// A message sent to a chat room.
export interface RoomMessage { export interface RoomMessage {
// The sender's name.
userName: string; userName: string;
// The message contents.
message: string; 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 { export interface RoomState {
// The name of the chat room.
name: string; name: string;
// Our membership status in this room.
membership: RoomMembership; membership: RoomMembership;
// Un visibility of this room.
visibility: string; visibility: string;
// Whether this room is operated by anyone.
operated: boolean; operated: boolean;
// How many users are members of this chat room.
userCount: number; userCount: number;
// The user name of the chat room's owner, if any.
owner: string; owner: string;
// The user names of the chat room's operators.
operators: string[]; operators: string[];
// The user names of the members of the chat room.
members: string[]; members: string[];
// The list of messages sent to the chat room.
messages: RoomMessage[]; messages: RoomMessage[];
// Tickers for the room: [sender user name, ticker contents].
tickers: [string, string][]; tickers: [string, string][];
} }
// Maps room names to room state.
export interface RoomMap { export interface RoomMap {
[name: string]: RoomState; [name: string]: RoomState;
} }
// The slice of global state pertaining to chat rooms.
export interface RoomSliceState { export interface RoomSliceState {
rooms: RoomMap; rooms: RoomMap;
} }
@ -39,6 +95,7 @@ const initialState: RoomSliceState = {
rooms: {}, rooms: {},
}; };
// The payload of an action to send a message to a chat room.
export interface RoomMessagePayload { export interface RoomMessagePayload {
roomName: string; roomName: string;
userName: string; userName: string;
@ -70,17 +127,31 @@ export const roomSlice = createSlice({
state: RoomSliceState, state: RoomSliceState,
action: PayloadAction<RoomMessagePayload> action: PayloadAction<RoomMessagePayload>
) => { ) => {
const { roomName, userName, message } = action.payload;
const { roomName, userName } = action.payload;
const messageContents = action.payload.message;
const room = state.rooms[roomName]; const room = state.rooms[roomName];
if (room === undefined) { if (room === undefined) {
console.log( console.log(
`Unknown room ${roomName} received message from ` + `Unknown room ${roomName} received message from ` +
`${userName}: ${message}`
`${userName}: ${messageContents}`
); );
return; 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: ( roomSendMessage: (
state: RoomSliceState, state: RoomSliceState,
@ -95,7 +166,7 @@ export const roomSlice = createSlice({
return; return;
} }
room.messages.push({ userName, message });
room.messages.push({ userName, message, acked: false });
}, },
roomSetAll: (state: RoomSliceState, action: PayloadAction<RoomState[]>) => { roomSetAll: (state: RoomSliceState, action: PayloadAction<RoomState[]>) => {
state.rooms = {}; state.rooms = {};


Loading…
Cancel
Save