Skip to content

Commit

Permalink
3 reftypes message reactions
Browse files Browse the repository at this point in the history
  • Loading branch information
vladvelici committed Feb 11, 2025
1 parent cb7299c commit d37f98c
Show file tree
Hide file tree
Showing 19 changed files with 731 additions and 28 deletions.
9 changes: 4 additions & 5 deletions demo/package-lock.json

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

2 changes: 1 addition & 1 deletion demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
},
"dependencies": {
"@ably/chat": "file:..",
"ably": "^2.6.3",
"ably": "https://github.com/ably/ably-js.git#support-annotations-deletes",
"clsx": "^2.1.1",
"nanoid": "^5.0.9",
"react": "^18.3.1",
Expand Down
31 changes: 29 additions & 2 deletions demo/src/components/ChatBoxComponent/ChatBoxComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { MessageComponent } from '../MessageComponent';
import { useChatClient, useMessages } from '@ably/chat';
import { ReactionRefType, useChatClient, useMessages } from '@ably/chat';
import { FC, useCallback, useEffect, useRef, useState } from 'react';
import { Message, MessageEventPayload, MessageEvents, PaginatedResult } from '@ably/chat';
import { ErrorInfo } from 'ably';
import { useReactionType } from '../MessageReactions';

interface ChatBoxComponentProps {}

Expand All @@ -12,7 +13,7 @@ export const ChatBoxComponent: FC<ChatBoxComponentProps> = () => {
const chatClient = useChatClient();
const clientId = chatClient.clientId;

const { getPreviousMessages, deleteMessage, update } = useMessages({
const { getPreviousMessages, deleteMessage, update, addReaction, removeReaction } = useMessages({
listener: (message: MessageEventPayload) => {
switch (message.type) {
case MessageEvents.Created: {
Expand Down Expand Up @@ -60,6 +61,27 @@ export const ChatBoxComponent: FC<ChatBoxComponentProps> = () => {
}
}
},
reactionsListener: (reaction) => {
const messageSerial = reaction.refSerial;
setMessages((prevMessages) => {
const index = prevMessages.findIndex((m) => m.serial === messageSerial);
if (index === -1) {
return prevMessages;
}

const newMessage = prevMessages[index].with(reaction);

// if no change, do nothing
if (newMessage === prevMessages[index]) {
return prevMessages;
}

// copy array and replace the message
const updatedArray = prevMessages.slice();
updatedArray[index] = newMessage;
return updatedArray;
});
},
onDiscontinuity: (discontinuity) => {
console.log('Discontinuity', discontinuity);
// reset the messages when a discontinuity is detected,
Expand Down Expand Up @@ -150,6 +172,8 @@ export const ChatBoxComponent: FC<ChatBoxComponentProps> = () => {
}
}, [messages, loading]);

const reactionType = useReactionType();

return (
<div className="chat-box">
{loading && <div className="text-center m-auto">loading...</div>}
Expand Down Expand Up @@ -184,6 +208,9 @@ export const ChatBoxComponent: FC<ChatBoxComponentProps> = () => {
key={msg.serial}
self={msg.clientId === clientId}
message={msg}
reactionRefType={reactionType}
onReactionAdd={addReaction}
onReactionRemove={removeReaction}
onMessageDelete={onDeleteMessage}
onMessageUpdate={onUpdateMessage}
></MessageComponent>
Expand Down
53 changes: 52 additions & 1 deletion demo/src/components/MessageComponent/MessageComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { Message } from '@ably/chat';
import { Message, ReactionRefType, useChatClient } from '@ably/chat';
import React, { useCallback } from 'react';
import clsx from 'clsx';
import { FaPencil, FaTrash } from 'react-icons/fa6';
import { MessageReactionsSingle, MessageReactionsUnique, MessageReactionsMany } from '../MessageReactions';

interface MessageProps {
self?: boolean;
message: Message;

reactionRefType?: ReactionRefType;

onMessageUpdate?(message: Message): void;

onMessageDelete?(msg: Message): void;

onReactionAdd?(msg: Message, refType: string, reaction: string, score?: number): void;
onReactionRemove?(msg: Message, refType: string, reaction: string): void;
}

const shortDateTimeFormatter = new Intl.DateTimeFormat('default', {
Expand All @@ -35,9 +41,16 @@ function shortDate(date: Date): string {
export const MessageComponent: React.FC<MessageProps> = ({
self = false,
message,
reactionRefType = ReactionRefType.Single,
onMessageUpdate,
onMessageDelete,
onReactionAdd,
onReactionRemove,
}) => {

const client = useChatClient();
const clientId = client.clientId;

const handleMessageUpdate = useCallback(
(e: React.UIEvent) => {
e.stopPropagation();
Expand All @@ -54,6 +67,43 @@ export const MessageComponent: React.FC<MessageProps> = ({
[message, onMessageDelete],
);

let reactionsUI = <></>;

if (onReactionAdd && onReactionRemove) {
switch (reactionRefType) {
case ReactionRefType.Unique: {
reactionsUI = (
<MessageReactionsUnique
message={message}
clientId={clientId}
onReactionAdd={onReactionAdd}
onReactionRemove={onReactionRemove}
/>
);
break;
}
case ReactionRefType.Single: {
reactionsUI = (
<MessageReactionsSingle
message={message}
clientId={clientId}
onReactionAdd={onReactionAdd}
onReactionRemove={onReactionRemove}
/>
);
break;
}
case ReactionRefType.Many: {
reactionsUI = (<MessageReactionsMany
message={message}
onReactionAdd={onReactionAdd}
onReactionRemove={onReactionRemove}
/>);
break;
}
}
}

return (
<div className="chat-message">
<div className={clsx('flex items-end', { ['justify-end']: self, ['justify-start']: !self })}>
Expand Down Expand Up @@ -106,6 +156,7 @@ export const MessageComponent: React.FC<MessageProps> = ({
onClick={handleMessageDelete}
aria-label="Delete message"
/>
{reactionsUI}
</div>
</div>
</div>
Expand Down
64 changes: 64 additions & 0 deletions demo/src/components/MessageReactions/MessageReactionsMany.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import { Message, ReactionRefType } from '@ably/chat';

interface MessageReactionsManyProps {
message: Message;
onReactionAdd: (message: Message, refType: ReactionRefType, emoji: string, score?: number) => void;
onReactionRemove: (message: Message, refType: ReactionRefType, emoji: string) => void;
}

const emojis = ['👍', '❤️', '🔥', '🚀'];

export const MessageReactionsMany: React.FC<MessageReactionsManyProps> = ({
message,
onReactionAdd,
onReactionRemove,
}) => {
const handleReactionClick = (emoji: string) => {
onReactionAdd(message, ReactionRefType.Many, emoji);
};

const handleReactionRemoveClick = (emoji: string) => {
onReactionRemove(message, ReactionRefType.Many, emoji);
};

const currentEmojis = emojis.slice();
if (message.reactions.many) {
for (const emoji in message.reactions.many) {
if (!currentEmojis.includes(emoji)) {
currentEmojis.push(emoji);
}
}
}

const many = message.reactions.many ?? {};


console.log("current emojis", currentEmojis)
return (
<>
hello
{currentEmojis.map((emoji) => (
<button
key={emoji}
onClick={(e) => {
e.preventDefault();
if (e.type === 'contextmenu') {
console.log("about to remove via onClick")
handleReactionRemoveClick(emoji);
} else {
handleReactionClick(emoji);
}
}}
onContextMenu={(e) => {
e.preventDefault();
console.log("about to remove via right click onmenu")
handleReactionRemoveClick(emoji);
}}
>
{emoji} ({many[emoji]?.total || 0})
</button>
))}
</>
);
};
53 changes: 53 additions & 0 deletions demo/src/components/MessageReactions/MessageReactionsSingle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import { Message, ReactionRefType } from '@ably/chat';

interface MessageReactionsSingleProps {
message: Message;
clientId: string;
onReactionAdd: (message: Message, refType: ReactionRefType, emoji: string, score?: number) => void;
onReactionRemove: (message: Message, refType: ReactionRefType, emoji: string) => void;
}

const emojis = ['👍', '❤️', '🔥', '🚀'];

export const MessageReactionsSingle: React.FC<MessageReactionsSingleProps> = ({
message,
clientId,
onReactionAdd,
onReactionRemove,
}) => {
const single = message.reactions.single ?? {};

const handleReactionClick = (emoji: string) => {
if (single[emoji]?.clientIds.includes(clientId)) {
onReactionRemove(message, ReactionRefType.Single, emoji);
} else {
onReactionAdd(message, ReactionRefType.Single, emoji);
}
};

const currentEmojis = emojis.slice();
if (message.reactions.single) {
for (const emoji in message.reactions.single) {
if (!currentEmojis.includes(emoji)) {
currentEmojis.push(emoji);
}
}
}

return (
<>
{currentEmojis.map((emoji) => (
<button
key={emoji}
onClick={(e) => {
e.preventDefault();
handleReactionClick(emoji);
}}
>
{emoji} ({single[emoji]?.total || 0})
</button>
))}
</>
);
};
53 changes: 53 additions & 0 deletions demo/src/components/MessageReactions/MessageReactionsUnique.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import { Message, ReactionRefType } from '@ably/chat';

interface MessageReactionsUniqueProps {
message: Message;
clientId: string;
onReactionAdd: (message: Message, refType: ReactionRefType, emoji: string, score?: number) => void;
onReactionRemove: (message: Message, refType: ReactionRefType, emoji: string) => void;
}

const emojis = ['👍', '❤️', '🔥', '🚀'];

export const MessageReactionsUnique: React.FC<MessageReactionsUniqueProps> = ({
message,
clientId,
onReactionAdd,
onReactionRemove,
}) => {
const unique = message.reactions.unique ?? {};

const handleReactionClick = (emoji: string) => {
if (unique[emoji]?.clientIds.includes(clientId)) {
onReactionRemove(message, ReactionRefType.Unique, emoji);
} else {
onReactionAdd(message, ReactionRefType.Unique, emoji);
}
};

const currentEmojis = emojis.slice();
if (unique) {
for (const emoji in unique) {
if (!currentEmojis.includes(emoji)) {
currentEmojis.push(emoji);
}
}
}

return (
<>
{currentEmojis.map((emoji) => (
<button
key={emoji}
onClick={(e) => {
e.preventDefault();
handleReactionClick(emoji);
}}
>
{emoji} ({unique[emoji]?.total || 0})
</button>
))}
</>
);
};
Loading

0 comments on commit d37f98c

Please sign in to comment.