diff --git a/README.md b/README.md index 5c54eb80..5237c953 100644 --- a/README.md +++ b/README.md @@ -281,14 +281,12 @@ 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); +const deletedMessage = await room.messages.delete(message, { description: 'This message was deleted for ...' }); ``` ### Subscribing to incoming messages diff --git a/ably-2.5.0.tgz b/ably-2.5.0.tgz deleted file mode 100644 index d63463ad..00000000 Binary files a/ably-2.5.0.tgz and /dev/null differ diff --git a/demo/ably-2.5.0.tgz b/demo/ably-2.5.0.tgz deleted file mode 100644 index d63463ad..00000000 Binary files a/demo/ably-2.5.0.tgz and /dev/null differ 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..026e2e11 100644 --- a/src/core/chat-api.ts +++ b/src/core/chat-api.ts @@ -39,7 +39,6 @@ interface SendMessageParams { } interface DeleteMessageParams { - hard?: boolean; description?: string; metadata?: MessageDetailsMetadata; } @@ -49,6 +48,11 @@ export interface DeleteMessageResponse { * The timestamp of when the deletion occurred. */ deletedAt: number; + + /** + * The clientId of the user who deleted the message. + */ + clientId?: string; } /** @@ -95,12 +99,11 @@ export class ChatApi { params?: DeleteMessageParams, ): Promise { 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..f3667f0f 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. */ @@ -72,23 +82,16 @@ export interface QueryOptions { * The parameters supplied to delete a message. */ export interface DeleteMessageParams { - /** - * The optional method to use for deleting messages. Defaults to 'soft' if not provided. - * `soft` will mark the message as deleted but keep it in the history, while `hard` will - * permanently delete the message from persistent storage. - */ - hard?: boolean; - /** * The optional description for deleting messages. */ 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; } /** @@ -217,6 +220,7 @@ export interface Messages extends EmitsDiscontinuities { * Delete a message in the chat room. * * This method uses the Ably Chat API REST endpoint for deleting messages. + * It performs a `soft` delete, meaning the message is marked as deleted. * * Note that the Promise may resolve before OR after the message is deleted * from the realtime channel. This means you may see the message that was just @@ -477,7 +481,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 +500,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, @@ -508,16 +512,16 @@ export class DefaultMessages message.metadata, message.headers, new Date(response.deletedAt), - this._clientId, + response.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 +565,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..1320d6ad 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. @@ -197,7 +197,7 @@ import { Message } from '@ably/chat'; const MyComponent = () => { const { send, get, deleteMessage } = useMessages(); - + const [message, setMessage] = useState; const handleGetMessages = () => { // fetch the last 3 messages, oldest to newest get({ limit: 3, direction: 'forwards' }).then((result) => console.log('Previous messages: ', result.items)); @@ -208,14 +208,14 @@ const MyComponent = () => { }; const handleDeleteMessage = (message: Message) => { - deleteMessage(message, { description: 'deleted by user', hard: false }); + deleteMessage(message, { description: 'deleted by user' }); }; return (
- +
); }; diff --git a/test/core/message-parser.test.ts b/test/core/message-parser.test.ts index 1706ab72..916976a8 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: ChatMessageActions.MessageUpdate, }, 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: ChatMessageActions.MessageDelete, }, 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..1f9fe569 100644 --- a/test/core/messages.integration.test.ts +++ b/test/core/messages.integration.test.ts @@ -112,7 +112,6 @@ describe('messages integration', () => { const deletedMessage1 = await room.messages.delete(message1, { description: 'Deleted message', metadata: { key: 'value' }, - hard: false, }); // Wait up to 5 seconds for the promises to resolve @@ -175,7 +174,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 +187,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); diff --git a/test/react/hooks/use-messages.test.tsx b/test/react/hooks/use-messages.test.tsx index 27722678..48c3ac16 100644 --- a/test/react/hooks/use-messages.test.tsx +++ b/test/react/hooks/use-messages.test.tsx @@ -109,8 +109,8 @@ describe('useMessages', () => { clientId: '123', roomId: '123', createdAt: new Date(), - isDeleted: vi.fn(), - isUpdated: vi.fn(), + isDeleted: false, + isUpdated: false, before: vi.fn(), after: vi.fn(), equal: vi.fn(), @@ -174,7 +174,6 @@ describe('useMessages', () => { await result.current.deleteMessage(message, { description: 'deleted', metadata: { reason: 'test' }, - hard: false, }); }); @@ -183,7 +182,6 @@ describe('useMessages', () => { expect(deleteSpy).toHaveBeenCalledWith(message, { description: 'deleted', metadata: { reason: 'test' }, - hard: false, }); });