|
|
|
@ -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<RoomMessagePayload> |
|
|
|
) => { |
|
|
|
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<RoomState[]>) => { |
|
|
|
state.rooms = {}; |
|
|
|
|