From e2b2f974a942aa7b9726143a5c053ba6cad56352 Mon Sep 17 00:00:00 2001 From: evgeny Date: Fri, 29 Dec 2023 16:51:00 +0000 Subject: [PATCH] feat: update demo --- demo/api/conversations/inMemoryDb.ts | 1 - demo/src/components/Message/Message.tsx | 33 +++-- .../components/MessageInput/MessageInput.tsx | 14 +- demo/src/containers/Chat/Chat.tsx | 125 ++++++++++++++--- demo/src/hooks/useConversation.ts | 13 ++ demo/src/hooks/useMessages.ts | 130 +++++++++++++++--- src/ChatApi.ts | 2 +- src/index.ts | 4 +- 8 files changed, 265 insertions(+), 57 deletions(-) create mode 100644 demo/src/hooks/useConversation.ts diff --git a/demo/api/conversations/inMemoryDb.ts b/demo/api/conversations/inMemoryDb.ts index b1295606..6a8b9032 100644 --- a/demo/api/conversations/inMemoryDb.ts +++ b/demo/api/conversations/inMemoryDb.ts @@ -1,5 +1,4 @@ import { ulid } from 'ulidx'; -import exp = require('constants'); export interface Conversation { id: string; diff --git a/demo/src/components/Message/Message.tsx b/demo/src/components/Message/Message.tsx index 7e766120..7c697068 100644 --- a/demo/src/components/Message/Message.tsx +++ b/demo/src/components/Message/Message.tsx @@ -1,29 +1,36 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, useCallback } from 'react'; import clsx from 'clsx'; interface MessageProps { + id: string; self?: boolean; children?: ReactNode | undefined; + onMessageClick?(id: string): void; } -export const Message: React.FC = ({ self = false, children }) => { +export const Message: React.FC = ({ id, self = false, children, onMessageClick }) => { + const handleMessageClick = useCallback(() => { + onMessageClick?.(id); + }, [id, onMessageClick]); + return ( -
+
-
- - {children} - +
+ {children}
diff --git a/demo/src/components/MessageInput/MessageInput.tsx b/demo/src/components/MessageInput/MessageInput.tsx index c847d2d2..c2d2d41f 100644 --- a/demo/src/components/MessageInput/MessageInput.tsx +++ b/demo/src/components/MessageInput/MessageInput.tsx @@ -1,20 +1,22 @@ -import { useState, ChangeEventHandler, FormEventHandler } from 'react'; +import { FC, ChangeEventHandler, FormEventHandler } from 'react'; interface MessageInputProps { + disabled: boolean; + value: string; + onValueChange(text: string): void; onSend(text: string): void; } -export const MessageInput: React.FC = ({ onSend }) => { - const [value, setValue] = useState(''); +export const MessageInput: FC = ({ value, disabled, onValueChange, onSend }) => { const handleValueChange: ChangeEventHandler = ({ target }) => { - setValue(target.value); + onValueChange(target.value); }; const handleFormSubmit: FormEventHandler = (event) => { event.preventDefault(); event.stopPropagation(); onSend(value); - setValue(''); + onValueChange(''); }; return ( @@ -26,11 +28,13 @@ export const MessageInput: React.FC = ({ onSend }) => { type="text" value={value} onChange={handleValueChange} + disabled={disabled} placeholder="Type.." className="w-full focus:outline-none focus:placeholder-gray-400 text-gray-600 placeholder-gray-600 pl-2 bg-gray-200 rounded-md py-1" />
+ )} + {!selectedMessage.reactions.mine.length && ( + + )} + {!!selectedMessage.reactions.mine.length && ( + + )} +
+ )} + {loading &&
loading...
} + {!loading && ( +
+ {messages.map((msg) => ( + +
+
{msg.content}
+ {!!msg.reactions.counts.like && ( +
{msg.reactions.counts.like} ❤️
+ )} +
+
+ ))} +
+ )}
- +
diff --git a/demo/src/hooks/useConversation.ts b/demo/src/hooks/useConversation.ts new file mode 100644 index 00000000..29c10724 --- /dev/null +++ b/demo/src/hooks/useConversation.ts @@ -0,0 +1,13 @@ +import { useContext } from 'react'; +import { ConversationContext } from '../containers/ConversationContext'; + +export const useConversation = () => { + const context = useContext(ConversationContext); + + if (!context) throw Error('Client is not setup!'); + + return { + conversation: context.conversation, + clientId: context.client.clientId, + }; +}; diff --git a/demo/src/hooks/useMessages.ts b/demo/src/hooks/useMessages.ts index a57c4bc0..45cab8a6 100644 --- a/demo/src/hooks/useMessages.ts +++ b/demo/src/hooks/useMessages.ts @@ -1,46 +1,140 @@ -import { Message, MessageEvents, type MessageListener } from '@ably-labs/chat'; -import { useCallback, useContext, useEffect, useState } from 'react'; -import { ConversationContext } from '../containers/ConversationContext'; +import { Message, MessageEvents, ReactionEvents, type MessageListener, type ReactionListener } from '@ably-labs/chat'; +import { useCallback, useEffect, useState } from 'react'; +import { useConversation } from './useConversation'; export const useMessages = () => { const [messages, setMessages] = useState([]); - const context = useContext(ConversationContext); + const [loading, setLoading] = useState(false); + const { clientId, conversation } = useConversation(); const sendMessage = useCallback( (text: string) => { - if (!context?.conversation) throw Error('Client is not setup!'); - context.conversation.messages.send(text); + conversation.messages.send(text); }, - [context?.conversation], + [conversation], ); - useEffect(() => { - if (!context) throw Error('Client is not setup!'); + const editMessage = useCallback( + (messageId: string, text: string) => { + conversation.messages.edit(messageId, text); + }, + [conversation], + ); + + const deleteMessage = useCallback( + (messageId: string) => { + conversation.messages.delete(messageId); + }, + [conversation], + ); - const handler: MessageListener = ({ message }) => { + const addReaction = useCallback( + (messageId: string, type: string) => { + conversation.messages.addReaction(messageId, type); + }, + [conversation], + ); + + const removeReaction = useCallback( + (reactionId: string) => { + conversation.messages.removeReaction(reactionId); + }, + [conversation], + ); + + useEffect(() => { + setLoading(true); + const handleAdd: MessageListener = ({ message }) => { setMessages((prevMessage) => [...prevMessage, message]); }; - context.conversation.messages.subscribe(MessageEvents.created, handler); + const handleUpdate: MessageListener = ({ message: updated }) => { + setMessages((prevMessage) => + prevMessage.map((message) => + message.id !== updated.id ? message : { ...updated, reactions: message.reactions }, + ), + ); + }; + const handleDelete: MessageListener = ({ message }) => { + setMessages((prevMessage) => prevMessage.filter(({ id }) => id !== message.id)); + }; + const handleReactionAdd: ReactionListener = ({ reaction }) => { + setMessages((prevMessage) => + prevMessage.map((message) => + message.id !== reaction.message_id + ? message + : { + ...message, + reactions: { + mine: + reaction.client_id === clientId ? [...message.reactions.mine, reaction] : message.reactions.mine, + latest: [...message.reactions.latest, reaction], + counts: { + ...message.reactions.counts, + [reaction.type]: (message.reactions.counts[reaction.type] ?? 0) + 1, + }, + }, + }, + ), + ); + }; + const handleReactionDelete: ReactionListener = ({ reaction }) => { + setMessages((prevMessage) => + prevMessage.map((message) => + message.id !== reaction.message_id + ? message + : { + ...message, + reactions: { + mine: + reaction.client_id === clientId + ? message.reactions.mine.filter(({ id }) => id !== reaction.id) + : message.reactions.mine, + latest: message.reactions.latest.filter(({ id }) => id !== reaction.id), + counts: { + ...message.reactions.counts, + [reaction.type]: message.reactions.counts[reaction.type] - 1, + }, + }, + }, + ), + ); + }; + + conversation.messages.subscribe(MessageEvents.created, handleAdd); + conversation.messages.subscribe(MessageEvents.updated, handleUpdate); + conversation.messages.subscribe(MessageEvents.deleted, handleDelete); + conversation.messages.subscribeReactions(ReactionEvents.added, handleReactionAdd); + conversation.messages.subscribeReactions(ReactionEvents.deleted, handleReactionDelete); let mounted = true; const initMessages = async () => { - const lastMessages = await context.conversation.messages.query({ limit: 10 }); - if (mounted) setMessages((prevMessages) => [...lastMessages, ...prevMessages]); + const lastMessages = await conversation.messages.query({ limit: 10 }); + if (mounted) { + setLoading(false); + setMessages((prevMessages) => [...lastMessages, ...prevMessages]); + } }; setMessages([]); initMessages(); return () => { mounted = false; - context.conversation.messages.unsubscribe(MessageEvents.created, handler); + conversation.messages.unsubscribe(MessageEvents.created, handleAdd); + conversation.messages.unsubscribe(MessageEvents.updated, handleUpdate); + conversation.messages.unsubscribe(MessageEvents.deleted, handleDelete); + conversation.messages.unsubscribeReactions(ReactionEvents.added, handleReactionAdd); + conversation.messages.unsubscribeReactions(ReactionEvents.deleted, handleReactionDelete); }; - }, [context]); - - if (!context) throw Error('Client is not setup!'); + }, [clientId, conversation]); return { + loading, messages, - clientId: context.client.clientId, + editMessage, sendMessage, + deleteMessage, + addReaction, + removeReaction, + clientId, }; }; diff --git a/src/ChatApi.ts b/src/ChatApi.ts index 4b516dc4..b2856f5d 100644 --- a/src/ChatApi.ts +++ b/src/ChatApi.ts @@ -82,7 +82,7 @@ export class ChatApi { } async deleteMessageReaction(reactionId: string): Promise { - return this.makeAuthorisedRequest(`v1/conversations/reactions/${reactionId}`, 'DELETE'); + return this.makeAuthorisedRequest(`v1/reactions/${reactionId}`, 'DELETE'); } private async makeAuthorisedRequest( diff --git a/src/index.ts b/src/index.ts index 4a2ff745..19b05d1c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ export { Chat } from './Chat.js'; -export { MessageEvents } from './events.js'; +export { MessageEvents, ReactionEvents } from './events.js'; export type { Message, Reaction, Conversation } from './entities.js'; export type { Conversation as ConversationController } from './Conversation.js'; -export type { MessageListener } from './Messages.js'; +export type { MessageListener, ReactionListener } from './Messages.js';