Skip to content

Commit

Permalink
Add message.with(event) method for applying message events to a Message
Browse files Browse the repository at this point in the history
  • Loading branch information
vladvelici committed Feb 3, 2025
1 parent 418aff0 commit 9623b5a
Show file tree
Hide file tree
Showing 9 changed files with 291 additions and 56 deletions.
90 changes: 55 additions & 35 deletions demo/src/components/ChatBoxComponent/ChatBoxComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const ChatBoxComponent: FC<ChatBoxComponentProps> = () => {
return prevMessages;
}

// if the message is not in the list, add it
// if the message is not in the list, make a new list that contains it
const newArray = [...prevMessages, message.message];

// and put it at the right place
Expand All @@ -33,25 +33,28 @@ export const ChatBoxComponent: FC<ChatBoxComponentProps> = () => {
});
break;
}
case MessageEvents.Updated:
case MessageEvents.Deleted: {
setMessages((prevMessage) => {
const updatedArray = prevMessage.filter((m) => {
return m.serial !== message.message.serial;
});

// don't change state if deleted message is not in the current list
if (prevMessage.length === updatedArray.length) {
return prevMessage;
setMessages((prevMessages) => {
const index = prevMessages.findIndex((m) => m.serial === message.message.serial);
if (index === -1) {
return prevMessages;
}

const newMessage = prevMessages[index].with(message);

// if no change, do nothing
if (newMessage === prevMessages[index]) {
return prevMessages;
}

// copy array and replace the message
const updatedArray = prevMessages.slice();
updatedArray[index] = newMessage;
return updatedArray;
});
break;
}
case MessageEvents.Updated: {
handleUpdatedMessage(message.message);
break;
}
default: {
console.error('Unknown message', message);
}
Expand All @@ -75,7 +78,7 @@ export const ChatBoxComponent: FC<ChatBoxComponentProps> = () => {
if (getPreviousMessages) {
getPreviousMessages({ limit: 50 })
.then((result: PaginatedResult<Message>) => {
setMessages(result.items.filter((m) => !m.isDeleted).reverse());
setMessages(result.items.reverse());
setLoading(false);
})
.catch((error: ErrorInfo) => {
Expand All @@ -84,20 +87,17 @@ export const ChatBoxComponent: FC<ChatBoxComponentProps> = () => {
}
};

const handleUpdatedMessage = (message: Message) => {
const handleRESTMessageUpdate = (updatedMessage: Message) => {
setMessages((prevMessages) => {
const index = prevMessages.findIndex((m) => m.serial === message.serial);
const index = prevMessages.findIndex((m) => m.serial === updatedMessage.serial);
if (index === -1) {
return prevMessages;
}

// skip update if the received version is not newer
if (!prevMessages[index].versionBefore(message)) {
if (updatedMessage.version <= prevMessages[index].version) {
return prevMessages;
}

const updatedArray = [...prevMessages];
updatedArray[index] = message;
const updatedArray = prevMessages.slice();
updatedArray[index] = updatedMessage;
return updatedArray;
});
};
Expand All @@ -114,7 +114,7 @@ export const ChatBoxComponent: FC<ChatBoxComponentProps> = () => {
headers: message.headers,
})
.then((updatedMessage: Message) => {
handleUpdatedMessage(updatedMessage);
handleRESTMessageUpdate(updatedMessage);
})
.catch((error: unknown) => {
console.warn('Failed to update message', error);
Expand All @@ -126,9 +126,7 @@ export const ChatBoxComponent: FC<ChatBoxComponentProps> = () => {
const onDeleteMessage = useCallback(
(message: Message) => {
deleteMessage(message, { description: 'deleted by user' }).then((deletedMessage: Message) => {
setMessages((prevMessages) => {
return prevMessages.filter((m) => m.serial !== deletedMessage.serial);
});
handleRESTMessageUpdate(deletedMessage);
});
},
[deleteMessage],
Expand Down Expand Up @@ -160,15 +158,37 @@ export const ChatBoxComponent: FC<ChatBoxComponentProps> = () => {
id="messages"
className="chat-window"
>
{messages.map((msg) => (
<MessageComponent
key={msg.serial}
self={msg.clientId === clientId}
message={msg}
onMessageDelete={onDeleteMessage}
onMessageUpdate={onUpdateMessage}
></MessageComponent>
))}
{messages.map((msg) => {
if (msg.isDeleted) {
return (
<div
key={msg.serial}
className="deleted-message"
>
This message was deleted.
<a
href="#"
onClick={(e) => {
e.preventDefault();
onUpdateMessage(msg);
}}
>
Edit
</a>
.
</div>
);
}
return (
<MessageComponent
key={msg.serial}
self={msg.clientId === clientId}
message={msg}
onMessageDelete={onDeleteMessage}
onMessageUpdate={onUpdateMessage}
></MessageComponent>
);
})}
<div ref={messagesEndRef} />
</div>
)}
Expand Down
19 changes: 19 additions & 0 deletions demo/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
@tailwind components;
@tailwind utilities;

a {
color: rgb(37 99 235);
}

a:hover {
color: rgb(66, 126, 255);
text-decoration: underline;
}

:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
Expand Down Expand Up @@ -52,10 +61,12 @@ body {
padding: 5px;
margin-right: 2px;
transition: all 100ms;
text-decoration: none;
}

.reactions-picker > a:hover {
scale: 1.5;
text-decoration: none;
}

.reactions-picker > a:active {
Expand Down Expand Up @@ -86,3 +97,11 @@ body {
.chat-message:hover .buttons {
display: block;
}

.deleted-message {
color: gray;
margin: 10px;
padding: 5px;
border-radius: 5px;
background-color: rgba(255, 255, 255, 0.1);
}
17 changes: 17 additions & 0 deletions src/core/events.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Message } from './message.js';

/**
* All chat message events.
*/
Expand Down Expand Up @@ -85,3 +87,18 @@ export enum RoomReactionEvents {
*/
Reaction = 'roomReaction',
}

/**
* Payload for a message event.
*/
export interface MessageEventPayload {
/**
* The type of the message event.
*/
type: MessageEvents;

/**
* The message that was received.
*/
message: Message;
}
2 changes: 1 addition & 1 deletion src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type {
export { ConnectionStatus } from './connection.js';
export type { DiscontinuityListener, OnDiscontinuitySubscriptionResponse } from './discontinuity.js';
export { ErrorCodes, errorInfoIs } from './errors.js';
export type { MessageEventPayload } from './events.js';
export { ChatMessageActions, MessageEvents, PresenceEvents } from './events.js';
export type { Headers } from './headers.js';
export {
Expand All @@ -29,7 +30,6 @@ export { LogLevel } from './logger.js';
export type { Message, MessageHeaders, MessageMetadata, MessageOperationMetadata, Operation } from './message.js';
export type {
DeleteMessageParams,
MessageEventPayload,
MessageListener,
Messages,
MessageSubscriptionResponse,
Expand Down
25 changes: 24 additions & 1 deletion src/core/message.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ErrorInfo } from 'ably';

import { ChatMessageActions } from './events.js';
import { ChatMessageActions, MessageEventPayload, MessageEvents } from './events.js';
import { Headers } from './headers.js';
import { Metadata } from './metadata.js';
import { OperationMetadata } from './operation-metadata.js';
Expand Down Expand Up @@ -201,6 +201,17 @@ export interface Message {
* @throws {@link ErrorInfo} if serials of either message is invalid.
*/
equal(message: Message): boolean;

/**
* Creates a new message instance with the event applied.
*
* @param event The event to be applied to the returned message.
* @throws {@link ErrorInfo} if the event is for a different message.
* @throws {@link ErrorInfo} if the event is a {@link MessageEvents.Created}.
* @returns A new message instance with the event applied. If the event is a no-op, such
* as an event for an old version, the same message is returned (not a copy).
*/
with(event: MessageEventPayload): Message;
}

/**
Expand Down Expand Up @@ -288,4 +299,16 @@ export class DefaultMessage implements Message {
equal(message: Message): boolean {
return this.serial === message.serial;
}

with(event: MessageEventPayload): Message {
if (event.type === MessageEvents.Created) {
throw new ErrorInfo('cannot apply a created event to a message', 40000, 400);
}

if (event.message.serial !== this.serial) {
throw new ErrorInfo('cannot apply event for a different message', 40000, 400);
}

return this.version >= event.message.version ? this : event.message;
}
}
17 changes: 1 addition & 16 deletions src/core/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
OnDiscontinuitySubscriptionResponse,
} from './discontinuity.js';
import { ErrorCodes } from './errors.js';
import { ChatMessageActions, MessageEvents, RealtimeMessageNames } from './events.js';
import { ChatMessageActions, MessageEventPayload, MessageEvents, RealtimeMessageNames } from './events.js';
import { Logger } from './logger.js';
import { DefaultMessage, Message, MessageHeaders, MessageMetadata, MessageOperationMetadata } from './message.js';
import { parseMessage } from './message-parser.js';
Expand Down Expand Up @@ -166,21 +166,6 @@ export interface SendMessageParams {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface UpdateMessageParams extends SendMessageParams {}

/**
* Payload for a message event.
*/
export interface MessageEventPayload {
/**
* The type of the message event.
*/
type: MessageEvents;

/**
* The message that was received.
*/
message: Message;
}

/**
* A listener for message events in a chat room.
* @param event The message event that was received.
Expand Down
Loading

0 comments on commit 9623b5a

Please sign in to comment.