Skip to content

Commit

Permalink
Add DetailsMetadata type and refactor message deletion.
Browse files Browse the repository at this point in the history
- Move DetailsMetadata type to its own file.
- Refactored message deletion to use map for message actions to events.
- Fixed typos and improved logging for message operations.
- Updated isDeleted and isUpdated to getters.
- Fix tests
  • Loading branch information
splindsay-92 committed Oct 25, 2024
1 parent ece5aa8 commit a4a8b5c
Show file tree
Hide file tree
Showing 13 changed files with 51 additions and 61 deletions.
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,11 +281,9 @@ You can supply optional parameters to the `delete` method to provide additional
These additional parameters are:
* `description`: a string that can be used to inform others as to why the message was deleted.
* `metadata`: a map of extra information that can be attached to the deletion message.
* `hard`: a boolean that determines whether the message should be hard deleted or not. If `hard` is set to `true`, the message will be permanently deleted and will not be recoverable.

The return of this call will be the deleted message, as it would appear to other subscribers of the room.

By default, the `hard` parameter is set to `false`.
This is a _soft delete_ and the message will still be available in the history, but with the `deletedAt` property set.

```ts
const deletedMessage = await room.messages.delete(message);
Expand Down
3 changes: 1 addition & 2 deletions demo/src/containers/Chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
Reaction,
} from '@ably/chat';


export const Chat = () => {
const chatClient = useChatClient();
const clientId = chatClient.clientId;
Expand Down Expand Up @@ -74,7 +73,7 @@ export const Chat = () => {
.then((result: PaginatedResult<Message>) => {
// reverse the messages so they are displayed in the correct order
// and don't include deleted messages
setMessages(result.items.filter((m) => !m.isDeleted()).reverse());
setMessages(result.items.filter((m) => !m.isDeleted).reverse());
setLoading(false);
})
.catch((error: unknown) => {
Expand Down
2 changes: 1 addition & 1 deletion src/core/chat-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export class ChatApi {
const body: MessageDetails = { description: params?.description, metadata: params?.metadata };
const restParams = params?.hard ? { hard: true } : { hard: false };
return this._makeAuthorizedRequest<DeleteMessageResponse>(
`/chat/v1/rooms/${roomId}/messages/${timeserial}/delete`,
`/chat/v2/rooms/${roomId}/messages/${timeserial}/delete`,
'POST',
body,
restParams,
Expand Down
10 changes: 10 additions & 0 deletions src/core/details-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* The type for metadata contained in the details field of a new chat message.
* This is a key-value pair where the key is a string, and the value is a string, it represents the metadata supplied
* to a message update or deletion request.
*
* Do not use metadata for authoritative information. There is no server-side
* validation. When reading the metadata, treat it like user input.
*
*/
export type DetailsMetadata = Record<string, string>;
3 changes: 2 additions & 1 deletion src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type {
OnConnectionStatusChangeResponse,
} from './connection-status.js';
export { ConnectionLifecycle } from './connection-status.js';
export type { DetailsMetadata } from './details-metadata.js';
export type { DiscontinuityListener, OnDiscontinuitySubscriptionResponse } from './discontinuity.js';
export { ErrorCodes, errorInfoIs } from './errors.js';
export { MessageEvents, PresenceEvents } from './events.js';
Expand All @@ -37,7 +38,7 @@ export type {
QueryOptions,
SendMessageParams,
} from './messages.js';
export type { DetailsMetadata, Metadata } from './metadata.js';
export type { Metadata } from './metadata.js';
export type { Occupancy, OccupancyEvent, OccupancyListener, OccupancySubscriptionResponse } from './occupancy.js';
export type {
Presence,
Expand Down
11 changes: 6 additions & 5 deletions src/core/message.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DetailsMetadata } from './details-metadata.js';
import { Headers } from './headers.js';
import { DetailsMetadata, Metadata } from './metadata.js';
import { Metadata } from './metadata.js';
import { DefaultTimeserial, Timeserial } from './timeserial.js';

/**
Expand Down Expand Up @@ -132,13 +133,13 @@ export interface Message {
* Determines if this message has been deleted.
* @returns true if the message has been deleted.
*/
isDeleted(): boolean;
get isDeleted(): boolean;

/**
* Determines if this message has been updated.
* @returns true if the message has been updated.
*/
isUpdated(): boolean;
get isUpdated(): boolean;

/**
* Determines if this message was created before the given message. This comparison is based on
Expand Down Expand Up @@ -198,11 +199,11 @@ export class DefaultMessage implements Message {
Object.freeze(this);
}

isDeleted(): boolean {
get isDeleted(): boolean {
return this.deletedAt !== undefined;
}

isUpdated(): boolean {
get isUpdated(): boolean {
return this.updatedAt !== undefined;
}

Expand Down
50 changes: 21 additions & 29 deletions src/core/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as Ably from 'ably';

import { getChannel, messagesChannelName } from './channel.js';
import { ChatApi } from './chat-api.js';
import { DetailsMetadata } from './details-metadata.js';
import {
DiscontinuityEmitter,
DiscontinuityListener,
Expand All @@ -13,7 +14,7 @@ import {
import { ErrorCodes } from './errors.js';
import { ChatMessageActions, MessageEvents, RealtimeMessageTypes } from './events.js';
import { Logger } from './logger.js';
import { DefaultMessage, Message, MessageDetailsMetadata, MessageHeaders, MessageMetadata } from './message.js';
import { DefaultMessage, Message, MessageHeaders, MessageMetadata } from './message.js';
import { parseMessage } from './message-parser.js';
import { PaginatedResult } from './query.js';
import { addListenerToChannelWithoutAttach } from './realtime-extensions.js';
Expand All @@ -30,6 +31,15 @@ interface MessageEventsMap {
[MessageEvents.Deleted]: MessageEventPayload;
}

/**
* Mapping of chat message actions to message events.
*/
const MessageActionsToEventsMap: Map<ChatMessageActions, MessageEvents> = new Map<ChatMessageActions, MessageEvents>([
[ChatMessageActions.MessageCreate, MessageEvents.Created],
[ChatMessageActions.MessageUpdate, MessageEvents.Updated],
[ChatMessageActions.MessageDelete, MessageEvents.Deleted],
]);

/**
* Options for querying messages in a chat room.
*/
Expand Down Expand Up @@ -85,10 +95,10 @@ export interface DeleteMessageParams {
description?: string;

/**
* The {@link MessageDetailsMetadata} that will be added to the deletion request. Defaults to empty.
* The {@link DetailsMetadata} that will be added to the deletion request. Defaults to empty.
*
*/
metadata?: MessageDetailsMetadata;
metadata?: DetailsMetadata;
}

/**
Expand Down Expand Up @@ -477,7 +487,7 @@ export class DefaultMessages
* @throws {@link ErrorInfo} if headers defines any headers prefixed with reserved words.
*/
async send(params: SendMessageParams): Promise<Message> {
this._logger.trace('Messages.send();');
this._logger.trace('Messages.send();', { params });

const { text, metadata, headers } = params;

Expand All @@ -496,9 +506,9 @@ export class DefaultMessages
/**
* @inheritdoc Messages
*/
async delete(message: Message, deleteMessageParams?: DeleteMessageParams): Promise<Message> {
this._logger.trace('Messages.delete();');
const response = await this._chatApi.deleteMessage(this._roomId, message.timeserial, deleteMessageParams);
async delete(message: Message, params?: DeleteMessageParams): Promise<Message> {
this._logger.trace('Messages.delete();', { params });
const response = await this._chatApi.deleteMessage(this._roomId, message.timeserial, params);
const deletedMessage: Message = new DefaultMessage(
message.timeserial,
message.clientId,
Expand All @@ -510,14 +520,14 @@ export class DefaultMessages
new Date(response.deletedAt),
this._clientId,
{
description: deleteMessageParams?.description,
metadata: deleteMessageParams?.metadata,
description: params?.description,
metadata: params?.metadata,
},
message.updatedAt,
message.updatedBy,
message.updateDetail,
);
this._logger.debug('Messages.delete(); message deleted successfully', { message });
this._logger.debug('Messages.delete(); message deleted successfully', { deletedMessage });
return deletedMessage;
}

Expand Down Expand Up @@ -561,30 +571,12 @@ export class DefaultMessages
this._listenerSubscriptionPoints.clear();
}

// This function is used to convert the message action from Ably to the chat MessageEvents.
private _messageActionToMessageEvent(action: ChatMessageActions): MessageEvents | undefined {
switch (action) {
case ChatMessageActions.MessageCreate: {
return MessageEvents.Created;
}
case ChatMessageActions.MessageUpdate: {
return MessageEvents.Updated;
}
case ChatMessageActions.MessageDelete: {
return MessageEvents.Deleted;
}
default: {
return undefined;
}
}
}

private _processEvent(channelEventMessage: Ably.InboundMessage) {
this._logger.trace('Messages._processEvent();', {
channelEventMessage,
});
const { action } = channelEventMessage;
const event = this._messageActionToMessageEvent(action as ChatMessageActions);
const event = MessageActionsToEventsMap.get(action as ChatMessageActions);
if (!event) {
this._logger.debug('Messages._processEvent(); received unknown message action', { action });
return;
Expand Down
11 changes: 0 additions & 11 deletions src/core/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,3 @@
*
*/
export type Metadata = Record<string, unknown>;

/**
* The type for metadata contained in the details fields of a chat message.
* This is a key-value pair where the key is a string, and the value is a string, it represents the metadata supplied
* to a message update or deletion request.
*
* Do not use metadata for authoritative information. There is no server-side
* validation. When reading the metadata, treat it like user input.
*
*/
export type DetailsMetadata = Record<string, string>;
2 changes: 1 addition & 1 deletion src/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ This hook allows you to access the `Messages` instance of a `Room` from your Rea
### Sending, Deleting And Getting Messages

The hook will provide the `Messages` instance, should you wish to interact with it directly, a `send` method
that can be used to send a messages to the room,
that can be used to send a message to the room,
a `deleteMessage` method that can be used to delete a message from the room,
and a `get` method that can be used to retrieve messages from the room.

Expand Down
4 changes: 2 additions & 2 deletions test/core/message-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ describe('parseMessage', () => {
timestamp: 1234567890,
extras: {},
serial: 'cbfkKvEYgBhDaZ38195418@1728402074206-0:0',
action: 'MESSAGE_UPDATE',
action: 'message_update',
},
expectedError: 'received incoming update message without updatedAt',
},
Expand All @@ -160,7 +160,7 @@ describe('parseMessage', () => {
timestamp: 1234567890,
extras: {},
serial: 'cbfkKvEYgBhDaZ38195418@1728402074206-0:0',
action: 'MESSAGE_DELETE',
action: 'message_delete',
},
expectedError: 'received incoming deletion message without deletedAt',
},
Expand Down
4 changes: 2 additions & 2 deletions test/core/message.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ describe('ChatMessage', () => {
new Date(1672531200000),
);

expect(firstMessage.isDeleted()).toBe(true);
expect(firstMessage.isDeleted).toBe(true);
});

it('is updated', () => {
Expand All @@ -140,7 +140,7 @@ describe('ChatMessage', () => {
new Date(1672531200000),
);

expect(firstMessage.isUpdated()).toBe(true);
expect(firstMessage.isUpdated).toBe(true);
});

it('throws an error with an invalid timeserial', () => {
Expand Down
5 changes: 3 additions & 2 deletions test/core/messages.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ describe('messages integration', () => {
expect(history.hasNext()).toBe(false);
});

it<TestContext>('should be able to retrieve chat deletion in history', async (context) => {
// At the moment, the history API does not materialize deleted messages in the history.
it.skip<TestContext>('should be able to retrieve chat deletion in history', async (context) => {
const { chat } = context;

const room = getRandomRoom(chat);
Expand All @@ -187,7 +188,7 @@ describe('messages integration', () => {
const deletedMessage1 = await room.messages.delete(message1, { description: 'Deleted message' });

// Do a history request to get the deleted message
const history = await room.messages.get({ limit: 1, direction: 'forwards' });
const history = await room.messages.get({ limit: 3, direction: 'forwards' });

expect(history.items).toEqual([
expect.objectContaining({
Expand Down
3 changes: 1 addition & 2 deletions test/react/hooks/use-messages.integration.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ describe('useMessages', () => {
void deleteMessage(message, {
description: 'deleted',
metadata: { reason: 'test' },
hard: false,
});
});
}
Expand All @@ -124,7 +123,7 @@ describe('useMessages', () => {

// expect a message to be received by the second room
await waitForMessages(deletionsRoomTwo, 1);
expect(deletionsRoomTwo[0]?.isDeleted()).toBe(true);
expect(deletionsRoomTwo[0]?.isDeleted).toBe(true);
expect(deletionsRoomTwo[0]?.deletedBy).toBe(chatClientOne.clientId);
}, 10000);

Expand Down

0 comments on commit a4a8b5c

Please sign in to comment.