diff --git a/README.md b/README.md index 5c54eb80..1617b27e 100644 --- a/README.md +++ b/README.md @@ -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); diff --git a/demo/src/containers/Chat/Chat.tsx b/demo/src/containers/Chat/Chat.tsx index 094d4666..ff03b097 100644 --- a/demo/src/containers/Chat/Chat.tsx +++ b/demo/src/containers/Chat/Chat.tsx @@ -13,7 +13,6 @@ import { Reaction, } from '@ably/chat'; - export const Chat = () => { const chatClient = useChatClient(); const clientId = chatClient.clientId; @@ -74,7 +73,7 @@ export const Chat = () => { .then((result: PaginatedResult) => { // 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) => { diff --git a/src/core/chat-api.ts b/src/core/chat-api.ts index e9da1a7c..125bbd4e 100644 --- a/src/core/chat-api.ts +++ b/src/core/chat-api.ts @@ -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( - `/chat/v1/rooms/${roomId}/messages/${timeserial}/delete`, + `/chat/v2/rooms/${roomId}/messages/${timeserial}/delete`, 'POST', body, restParams, diff --git a/src/core/details-metadata.ts b/src/core/details-metadata.ts new file mode 100644 index 00000000..0c261f9e --- /dev/null +++ b/src/core/details-metadata.ts @@ -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; diff --git a/src/core/index.ts b/src/core/index.ts index 342d6bd9..1b25528f 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -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'; @@ -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, diff --git a/src/core/message.ts b/src/core/message.ts index fdb5e893..78d6c6bf 100644 --- a/src/core/message.ts +++ b/src/core/message.ts @@ -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'; /** @@ -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 @@ -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; } diff --git a/src/core/messages.ts b/src/core/messages.ts index cf2b9219..c63c5788 100644 --- a/src/core/messages.ts +++ b/src/core/messages.ts @@ -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, @@ -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'; @@ -30,6 +31,15 @@ interface MessageEventsMap { [MessageEvents.Deleted]: MessageEventPayload; } +/** + * Mapping of chat message actions to message events. + */ +const MessageActionsToEventsMap: Map = new Map([ + [ChatMessageActions.MessageCreate, MessageEvents.Created], + [ChatMessageActions.MessageUpdate, MessageEvents.Updated], + [ChatMessageActions.MessageDelete, MessageEvents.Deleted], +]); + /** * Options for querying messages in a chat room. */ @@ -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; } /** @@ -477,7 +487,7 @@ export class DefaultMessages * @throws {@link ErrorInfo} if headers defines any headers prefixed with reserved words. */ async send(params: SendMessageParams): Promise { - this._logger.trace('Messages.send();'); + this._logger.trace('Messages.send();', { params }); const { text, metadata, headers } = params; @@ -496,9 +506,9 @@ export class DefaultMessages /** * @inheritdoc Messages */ - async delete(message: Message, deleteMessageParams?: DeleteMessageParams): Promise { - this._logger.trace('Messages.delete();'); - const response = await this._chatApi.deleteMessage(this._roomId, message.timeserial, deleteMessageParams); + async delete(message: Message, params?: DeleteMessageParams): Promise { + 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, @@ -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; } @@ -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; diff --git a/src/core/metadata.ts b/src/core/metadata.ts index 3225f567..75e11f2a 100644 --- a/src/core/metadata.ts +++ b/src/core/metadata.ts @@ -10,14 +10,3 @@ * */ export type Metadata = Record; - -/** - * 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; diff --git a/src/react/README.md b/src/react/README.md index 88b50490..eb6a38c3 100644 --- a/src/react/README.md +++ b/src/react/README.md @@ -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. diff --git a/test/core/message-parser.test.ts b/test/core/message-parser.test.ts index 1706ab72..bf7d84f5 100644 --- a/test/core/message-parser.test.ts +++ b/test/core/message-parser.test.ts @@ -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', }, @@ -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', }, diff --git a/test/core/message.test.ts b/test/core/message.test.ts index 180fef85..6c42fe46 100644 --- a/test/core/message.test.ts +++ b/test/core/message.test.ts @@ -120,7 +120,7 @@ describe('ChatMessage', () => { new Date(1672531200000), ); - expect(firstMessage.isDeleted()).toBe(true); + expect(firstMessage.isDeleted).toBe(true); }); it('is updated', () => { @@ -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', () => { diff --git a/test/core/messages.integration.test.ts b/test/core/messages.integration.test.ts index c66bbce1..332e84d7 100644 --- a/test/core/messages.integration.test.ts +++ b/test/core/messages.integration.test.ts @@ -175,7 +175,8 @@ describe('messages integration', () => { expect(history.hasNext()).toBe(false); }); - it('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('should be able to retrieve chat deletion in history', async (context) => { const { chat } = context; const room = getRandomRoom(chat); @@ -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({ diff --git a/test/react/hooks/use-messages.integration.test.tsx b/test/react/hooks/use-messages.integration.test.tsx index 7e85023c..f191be44 100644 --- a/test/react/hooks/use-messages.integration.test.tsx +++ b/test/react/hooks/use-messages.integration.test.tsx @@ -100,7 +100,6 @@ describe('useMessages', () => { void deleteMessage(message, { description: 'deleted', metadata: { reason: 'test' }, - hard: false, }); }); } @@ -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);