Skip to content

Commit

Permalink
Add message deletion functionality to chat API and React hooks
Browse files Browse the repository at this point in the history
- This commit introduces the ability to delete messages in the chat API with both soft and hard delete options via `DeleteMessageParams`.It exposes this through the Messages feature as a new `delete` method.
- Updated related testing and documentation.
- Some minor improvements to messages.test.ts for clarity.

- The useMessage hook now also supports this functionality via a `deleteMessage` method returned from the hook.
- Updated related testing and documentation.

- The demo app has been updated to show the new deletion mechanic.
It uses the `react-icons` to display a bin icon, clicking this will delete the highlighted message.
  • Loading branch information
splindsay-92 committed Oct 25, 2024
1 parent 12078ad commit ece5aa8
Show file tree
Hide file tree
Showing 16 changed files with 3,544 additions and 1,412 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,25 @@ const message = await room.messages.send({
});
```

### Deleting messages

To delete a message, call `delete` on the `room.messages` property, with the original message you want to delete.

You can supply optional parameters to the `delete` method to provide additional context for the deletion.

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`.

```ts
const deletedMessage = await room.messages.delete(message);
```

### Subscribing to incoming messages

To subscribe to incoming messages, call `subscribe` with your listener.
Expand Down
11 changes: 10 additions & 1 deletion demo/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"clsx": "^2.1.1",
"nanoid": "^5.0.7",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"react-icons": "^5.3.0"
},
"devDependencies": {
"@types/react": "^18.3.3",
Expand Down
33 changes: 30 additions & 3 deletions demo/src/components/MessageComponent/MessageComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Message } from '@ably/chat';
import React, { useCallback } from 'react';
import React, { useCallback, useState } from 'react';
import clsx from 'clsx';
import { FaTrash } from 'react-icons/fa6';

function twoDigits(input: number): string {
if (input === 0) {
Expand All @@ -18,13 +19,23 @@ interface MessageProps {
message: Message;

onMessageClick?(id: string): void;

onMessageDelete?(msg: Message): void;
}

export const MessageComponent: React.FC<MessageProps> = ({ id, self = false, message, onMessageClick }) => {
export const MessageComponent: React.FC<MessageProps> = ({
id,
self = false,
message,
onMessageClick,
onMessageDelete,
}) => {
const handleMessageClick = useCallback(() => {
onMessageClick?.(id);
}, [id, onMessageClick]);

const [hovered, setHovered] = useState(false);

let displayCreatedAt: string;
if (Date.now() - message.createdAt.getTime() < 1000 * 60 * 60 * 24) {
// last 24h show the time
Expand All @@ -43,14 +54,21 @@ export const MessageComponent: React.FC<MessageProps> = ({ id, self = false, mes
twoDigits(message.createdAt.getMinutes());
}

const handleDelete = useCallback(() => {
// Add your delete handling logic here
onMessageDelete?.(message);
}, [message, onMessageDelete]);

return (
<div
className="chat-message"
onClick={handleMessageClick}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<div className={clsx('flex items-end', { ['justify-end']: self, ['justify-start']: !self })}>
<div
className={clsx('flex flex-col text max-w-xs mx-2', {
className={clsx('flex flex-col text max-w-xs mx-2 relative', {
['items-end order-1']: self,
['items-start order-2']: !self,
})}
Expand All @@ -69,6 +87,15 @@ export const MessageComponent: React.FC<MessageProps> = ({ id, self = false, mes
})}
>
{message.text}
{hovered && (
<FaTrash
className="ml-2 cursor-pointer text-red-500"
onClick={(e) => {
e.stopPropagation();
handleDelete();
}}
/>
)}
</div>
</div>
</div>
Expand Down
50 changes: 46 additions & 4 deletions demo/src/containers/Chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ import { MessageInput } from '../../components/MessageInput';
import { useChatClient, useChatConnection, useMessages, useRoomReactions, useTyping } from '@ably/chat/react';
import { ReactionInput } from '../../components/ReactionInput';
import { ConnectionStatusComponent } from '../../components/ConnectionStatusComponent/ConnectionStatusComponent.tsx';
import { ConnectionLifecycle, Message, MessageEventPayload, PaginatedResult, Reaction } from '@ably/chat';
import {
ConnectionLifecycle,
Message,
MessageEvents,
MessageEventPayload,
PaginatedResult,
Reaction,
} from '@ably/chat';


export const Chat = () => {
const chatClient = useChatClient();
Expand All @@ -15,9 +23,26 @@ export const Chat = () => {

const isConnected: boolean = currentStatus === ConnectionLifecycle.Connected;

const { send: sendMessage, getPreviousMessages } = useMessages({
const {
send: sendMessage,
getPreviousMessages,
deleteMessage,
} = useMessages({
listener: (message: MessageEventPayload) => {
setMessages((prevMessage) => [...prevMessage, message.message]);
switch (message.type) {
case MessageEvents.Created:
setMessages((prevMessage) => [...prevMessage, message.message]);
break;
case MessageEvents.Deleted:
setMessages((prevMessage) => {
return prevMessage.filter((m) => {
return m.timeserial !== message.message.timeserial;
});
});
break;
default:
console.error('Unknown message', message);
}
},
onDiscontinuity: (discontinuity) => {
console.log('Discontinuity', discontinuity);
Expand Down Expand Up @@ -47,7 +72,9 @@ export const Chat = () => {
if (getPreviousMessages && loading) {
getPreviousMessages({ limit: 50 })
.then((result: PaginatedResult<Message>) => {
setMessages(result.items.reverse());
// 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());
setLoading(false);
})
.catch((error: unknown) => {
Expand Down Expand Up @@ -144,6 +171,21 @@ export const Chat = () => {
key={msg.timeserial}
self={msg.clientId === clientId}
message={msg}
onMessageClick={(id) => {
console.log(
'Message clicked',
messages.find((m) => m.timeserial === id),
);
}}
onMessageDelete={(msg) => {
deleteMessage(msg, { description: 'deleted by user' }).then((deletedMessage: Message) => {
setMessages((prevMessages) => {
return prevMessages.filter((m) => {
return m.timeserial !== deletedMessage.timeserial;
});
});
});
}}
></MessageComponent>
))}
<div ref={messagesEndRef} />
Expand Down
Loading

0 comments on commit ece5aa8

Please sign in to comment.