Skip to content

Commit

Permalink
message reactions (3 ways) prototype
Browse files Browse the repository at this point in the history
  • Loading branch information
vladvelici committed Jan 22, 2025
1 parent 84c7cc3 commit c8b500b
Show file tree
Hide file tree
Showing 8 changed files with 281 additions and 100 deletions.
97 changes: 75 additions & 22 deletions demo/src/components/MessageComponent/MessageComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ interface MessageProps {

onMessageDelete?(msg: Message): void;

onMessageReact?(message: Message, reaction: string): void;
onAddReaction?(message: Message, reaction: string, score?: number, unique?: boolean): void;

onRemoveReaction?(message: Message, reaction: string): void;
}

const shortDateTimeFormatter = new Intl.DateTimeFormat('default', {
Expand Down Expand Up @@ -39,7 +41,8 @@ export const MessageComponent: React.FC<MessageProps> = ({
message,
onMessageUpdate,
onMessageDelete,
onMessageReact,
onAddReaction,
onRemoveReaction,
}) => {
const handleMessageUpdate = useCallback(
(e: React.UIEvent) => {
Expand All @@ -57,35 +60,85 @@ export const MessageComponent: React.FC<MessageProps> = ({
[message, onMessageDelete],
);

const handleReaction = useCallback(
(reaction: string) => {
onMessageReact?.(message, reaction);
},
[message, onMessageReact],
);

const currentReactions = message.reactions;
const reactionsWithCounts = ['👍', '🚀', '🔥', '❤️'].map((emoji) => {
const count = currentReactions.get(emoji)?.count || 0;
return { emoji, count};
const data = currentReactions.get(emoji);

const clientIdArr : {clientId : string, total: number, score : number}[] = [];
if (data?.clientIds) {
for (let clientId in data.clientIds) {
clientIdArr.push({ clientId, score: data.clientIds[clientId].score, total: data.clientIds[clientId].total });
}
}
if (data) {
console.log("for emoji", emoji, "data", data, "clientIdArr", clientIdArr);
}

return { emoji, data, clientIdArr };
})

const messageReactionsUI = (
<span className="message-reactions">
<div className="message-reactions">
{reactionsWithCounts.map((rwc) => (
<a
<div
className="message-reaction"
key={rwc.emoji}
onClick={(e) => {
e.preventDefault();
handleReaction(rwc.emoji);
}}
href="#"
>
{rwc.emoji}
{rwc.count > 0 ? "(" + rwc.count + ")" : ""}
</a>
<a href="#" onClick={(e) => {
e.preventDefault();
onAddReaction?.(message, rwc.emoji, 1, false);
}}>{rwc.emoji}</a>
{rwc.data?.score && rwc.data?.score > 0 ? "(" + rwc.data.score + ")" : ""}
<div className="message-reaction-menu">
<ul>
<li><a href="#" onClick={(e) => {
e.preventDefault();
onAddReaction?.(message, rwc.emoji, 1, false);
}}>Add reaction (default)</a></li>
<li><a href="#" onClick={(e) => {
e.preventDefault();
onAddReaction?.(message, rwc.emoji, 1, true);
}}>Add unique reaction</a></li>
<li><a href="#" onClick={(e) => {
e.preventDefault();
let scoreStr = prompt("Enter score");
if (!scoreStr) return;
let score = parseInt(scoreStr);
if (!score || score <= 0) return;
onAddReaction?.(message, rwc.emoji, score, false);
}}>Add reaction with score</a></li>
<li><a href="#" onClick={(e) => {
e.preventDefault();
let scoreStr = prompt("Enter score");
if (!scoreStr) return;
let score = parseInt(scoreStr);
if (!score || score <= 0) return;
onAddReaction?.(message, rwc.emoji, score, true);
}}>Add unique reaction with score</a></li>
<li><a href="#" onClick={(e) => {
e.preventDefault();
onRemoveReaction?.(message, rwc.emoji);
}}>Remove reaction</a></li>
</ul>
<div>
<p>
<strong>Score:</strong> {rwc.data?.score && rwc.data?.score > 0 ? "(" + rwc.data.score + ")" : "-"}.
<strong>Total:</strong> {rwc.data?.total && rwc.data?.total > 0 ? "(" + rwc.data.total + ")" : "-"}.
</p>
{rwc.clientIdArr.length > 0 ? (
<ul>
{rwc.clientIdArr.map((clientIdData) => (
<li key={clientIdData.clientId}>
<strong>{clientIdData.clientId}</strong> - Score: {clientIdData.score} - Total: {clientIdData.total}
</li>
))}
</ul>
) : ""}
</div>
</div>
</div>
))}
</span>
</div>
);
const messageActionsUI = (
<div
Expand Down
7 changes: 5 additions & 2 deletions demo/src/containers/Chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ export const Chat = (props: { roomId: string; setRoomId: (roomId: string) => voi
send: sendMessage,
deleteMessage,
update,
react: reactToMessage,
addReaction,
removeReaction,
removeAllReactions,
messages: untypedMessagesObject,
} = useMsgResponse;

Expand Down Expand Up @@ -243,7 +245,8 @@ export const Chat = (props: { roomId: string; setRoomId: (roomId: string) => voi
message={msg}
onMessageDelete={onDeleteMessage}
onMessageUpdate={onUpdateMessage}
onMessageReact={reactToMessage}
onAddReaction={addReaction}
onRemoveReaction={removeReaction}
></MessageComponent>
))}
<div ref={messagesEndRef} />
Expand Down
35 changes: 35 additions & 0 deletions demo/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,41 @@ body {
opacity: 1;
}


.message-reactions {
display: inline-block;
}

.message-reaction-menu {
display: none;
}

.message-reaction {
display: inline-block;
position: relative;
}

.message-reaction:hover .message-reaction-menu {
display: block;
position: absolute;
right: 10%;
width: 200px;
z-index: 10000;
background: black;
}

.message-reaction-menu a {
display: block;
padding: 5px;
}

.message-reaction-menu a:hover {
display: block;
padding: 5px;
background: red;
}


.chat-message .message-reactions > a {
opacity: 0.8;
transition: all 0.1s ease-in-out;
Expand Down
31 changes: 28 additions & 3 deletions src/core/chat-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,18 +202,43 @@ export class ChatApi {
);
}

async reactToMessage(roomId: string, serial: string, reaction: string): Promise<void> {
async addMessageReaction(roomId: string, serial: string, reaction: string, score : number = 1, unique : boolean = false): Promise<void> {
roomId = encodeURIComponent(roomId);
return this._makeAuthorizedRequest(`/channels/${roomId}::$chat::$chatMessages/messages`, 'POST', {
action: 4,
data: reaction,
action: 5,
data: JSON.stringify({emoji: reaction, score: score, unique: unique}),
refType: 'reaction:emoji.v1',
refSerial: serial,
}).then((response) => {
console.log('response from sending reaction:', response);
});
}

async removeAllMessageReactions(roomId: string, serial: string): Promise<void> {
roomId = encodeURIComponent(roomId);
return this._makeAuthorizedRequest(`/channels/${roomId}::$chat::$chatMessages/messages`, 'POST', {
action: 6,
data: "",
refType: 'reaction:emoji.v1',
refSerial: serial,
}).then((response) => {
console.log('response from removing all reactions:', response);
});
}


async removeMessageReaction(roomId: string, serial: string, reaction: string): Promise<void> {
roomId = encodeURIComponent(roomId);
return this._makeAuthorizedRequest(`/channels/${roomId}::$chat::$chatMessages/messages`, 'POST', {
action: 6,
data: JSON.stringify({emoji: reaction}),
refType: 'reaction:emoji.v1',
refSerial: serial,
}).then((response) => {
console.log(`response from removing '${reaction}' reaction:`, response);
});
}

async getOccupancy(roomId: string): Promise<OccupancyEvent> {
roomId = encodeURIComponent(roomId);
return this._makeAuthorizedRequest<OccupancyEvent>(`/chat/v1/rooms/${roomId}/occupancy`, 'GET');
Expand Down
2 changes: 1 addition & 1 deletion src/core/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export enum ChatMessageActions {
MessageMetaOccupancy = 'meta.occupancy',

/** Action applied to an annotation summary message. */
MessageAnnotationSummary = 7,
MessageAnnotationSummary = 'message.summary',
}

/**
Expand Down
101 changes: 48 additions & 53 deletions src/core/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { Headers } from './headers.js';
import {
AnyMessageEvent,
MessageEventPayload,
MessageReactionPayload,
MessageReactionSummaryPayload,
} from './message-events.js';
import { Metadata } from './metadata.js';
import { OperationMetadata } from './operation-metadata.js';


/**
* {@link Headers} type for chat messages.
*/
Expand Down Expand Up @@ -238,10 +238,16 @@ export interface MessageReactionSummary {
reaction: string;

/** The count of reactions. */
count: number;
total: number;

/** Total score of reactions */
score: number;

/** Mode B simulation for oneOfEach - num clients. Same as length of the keys of clientIds */
numClients: number;

/** Deduplicated list of users that have reacted. */
clientIds: string[];
clientIds: { [clientId: string]: { total: number; score: number } };
}

/**
Expand Down Expand Up @@ -354,39 +360,40 @@ export class DefaultMessage implements Message {
return DefaultMessage.clone(event.message, { reactions: this.reactions });
}
case MessageEvents.ReactionCreated: {
const reactions = cloneReactions(this.reactions);
event = event as MessageReactionPayload;
const r = reactions.get(event.reaction.reaction);
if (r) {
r.count++;
if (!r.clientIds.includes(event.reaction.clientId)) {
r.clientIds.push(event.reaction.clientId);
}
} else {
reactions.set(event.reaction.reaction, {
reaction: event.reaction.reaction,
count: 1,
clientIds: [event.reaction.clientId],
});
}
return DefaultMessage.clone(this, { reactions });
// const reactions = cloneReactions(this.reactions);
// event = event as MessageReactionPayload;
// const r = reactions.get(event.reaction.reaction);
// if (r) {
// r.count++;
// if (!r.clientIds.includes(event.reaction.clientId)) {
// r.clientIds.push(event.reaction.clientId);
// }
// } else {
// reactions.set(event.reaction.reaction, {
// reaction: event.reaction.reaction,
// count: 1,
// clientIds: [event.reaction.clientId],
// });
// }
// return DefaultMessage.clone(this, { reactions });
return this;
}
case MessageEvents.ReactionDeleted: {
event = event as MessageReactionPayload;
// if the reaction doesn't exist return same Message object early
if (!this.reactions.get(event.reaction.reaction)) {
return this;
}
const reactions = cloneReactions(this.reactions);
const r = reactions.get(event.reaction.reaction);
if (r) {
r.count--;
const idx = r.clientIds.indexOf(event.reaction.clientId);
if (idx !== -1) {
r.clientIds.splice(idx, 1);
}
return DefaultMessage.clone(this, { reactions });
}
// event = event as MessageReactionPayload;
// // if the reaction doesn't exist return same Message object early
// if (!this.reactions.get(event.reaction.reaction)) {
// return this;
// }
// const reactions = cloneReactions(this.reactions);
// const r = reactions.get(event.reaction.reaction);
// if (r) {
// r.count--;
// const idx = r.clientIds.indexOf(event.reaction.clientId);
// if (idx !== -1) {
// r.clientIds.splice(idx, 1);
// }
// return DefaultMessage.clone(this, { reactions });
// }
// if no change return same object
// this should be unreachable but the if(r) above makes typescript happy
return this;
Expand All @@ -399,10 +406,10 @@ export class DefaultMessage implements Message {
}
return DefaultMessage.clone(this, { reactions });
}
default: {
return this;
}
}

console.log("unreachable code reached in apply() method of DefaultMessage for event", event);
return this;
}

// Clone a message, optionally replace the given fields
Expand All @@ -412,26 +419,14 @@ export class DefaultMessage implements Message {
replace?.clientId ?? source.clientId,
replace?.roomId ?? source.roomId,
replace?.text ?? source.text,
replace?.metadata ?? source.metadata, // deep clone?
replace?.headers ?? source.headers, // deep clone?
replace?.metadata ?? structuredClone(source.metadata), // deep clone?
replace?.headers ?? structuredClone(source.headers), // deep clone?
replace?.action ?? source.action,
replace?.version ?? source.version,
replace?.createdAt ?? source.createdAt,
replace?.timestamp ?? source.timestamp,
replace?.operation ?? source.operation, // deep clone?
replace?.reactions ?? source.reactions, // deep clone?
replace?.operation ?? structuredClone(source.operation), // deep clone?
replace?.reactions ?? structuredClone(source.reactions), // deep clone?
);
}
}

function cloneReactions(obj: Map<string, MessageReactionSummary>): Map<string, MessageReactionSummary> {
const clone = new Map<string, MessageReactionSummary>();
for (const [key, value] of obj.entries()) {
clone.set(key, {
clientIds: value.clientIds.slice(),
count: value.count,
reaction: value.reaction,
});
}
return clone;
}
Loading

0 comments on commit c8b500b

Please sign in to comment.