The Message Reactions API provides RPC methods for managing emoji reactions on messages in Hazel Chat. Users can add reactions to express sentiment without sending a full message.
Overview
Message reactions allow users to react to messages with emoji. Each reaction:
Is associated with a specific message and user
Contains a single emoji
Tracks when it was created
Can be toggled on/off
Reactions are commonly used for:
Acknowledging messages
Expressing sentiment
Quick feedback
Voting or polling
Authentication
All reaction operations require authentication via AuthMiddleware. The userId is automatically set from the authenticated user context.
Methods
messageReaction.toggle
Toggles a reaction on a message. If the user has already reacted with this emoji, the reaction will be removed. If not, a new reaction will be created.
This is the recommended method for handling reactions in UI, as it handles both adding and removing reactions in a single call.
The ID of the message to react to
The ID of the channel containing the message
The emoji to react with (e.g., ”👍”, “❤️”, “:smile:“)
Response
true if a new reaction was created, false if an existing reaction was removed
data
MessageReaction | undefined
The reaction object if one was created, undefined if removed Show MessageReaction fields
Unique identifier for the reaction
ID of the message being reacted to
ID of the channel containing the message
ID of the user who created the reaction
The emoji used for the reaction
Timestamp when the reaction was created
Transaction ID for optimistic UI updates
Errors
MessageNotFoundError - The specified message does not exist
UnauthorizedError - User lacks permission to react to this message
InternalServerError - An unexpected error occurred
Example
import { RpcClient } from "@hazel/domain/rpc"
import { Effect } from "effect"
const toggleReaction = Effect . gen ( function* () {
const client = yield * RpcClient
const result = yield * client . MessageReactionToggle ({
messageId: "550e8400-e29b-41d4-a716-446655440000" ,
channelId: "660e8400-e29b-41d4-a716-446655440001" ,
emoji: "👍" ,
})
if ( result . wasCreated ) {
console . log ( "Reaction added:" , result . data ?. emoji )
} else {
console . log ( "Reaction removed" )
}
return result
})
messageReaction.create
Creates a new reaction on a message. Use this when you want explicit control over reaction creation.
For most use cases, messageReaction.toggle is preferred as it handles both creation and removal.
The ID of the message to react to
The ID of the channel containing the message
Response
The created reaction object (see messageReaction.toggle for fields)
Transaction ID for optimistic UI updates
Errors
MessageNotFoundError - The specified message does not exist
UnauthorizedError - User lacks permission to react to this message
InternalServerError - An unexpected error occurred
Example
import { RpcClient } from "@hazel/domain/rpc"
import { Effect } from "effect"
const createReaction = Effect . gen ( function* () {
const client = yield * RpcClient
const result = yield * client . MessageReactionCreate ({
messageId: "550e8400-e29b-41d4-a716-446655440000" ,
channelId: "660e8400-e29b-41d4-a716-446655440001" ,
emoji: "❤️" ,
})
console . log ( "Reaction created:" , result . data . id )
return result
})
messageReaction.update
Updates an existing message reaction. Only the reaction creator or users with appropriate permissions can update reactions.
The ID of the reaction to update
New emoji for the reaction
Response
The updated reaction object
Transaction ID for optimistic UI updates
Errors
MessageReactionNotFoundError - The specified reaction does not exist
UnauthorizedError - User lacks permission to update this reaction
InternalServerError - An unexpected error occurred
Example
import { RpcClient } from "@hazel/domain/rpc"
import { Effect } from "effect"
const updateReaction = Effect . gen ( function* () {
const client = yield * RpcClient
const result = yield * client . MessageReactionUpdate ({
id: "770e8400-e29b-41d4-a716-446655440002" ,
emoji: "🎉" ,
})
return result
})
messageReaction.delete
Deletes a message reaction. Only the reaction creator or users with appropriate permissions can delete reactions.
The ID of the reaction to delete
Response
Transaction ID for optimistic UI updates
Errors
MessageReactionNotFoundError - The specified reaction does not exist
UnauthorizedError - User lacks permission to delete this reaction
InternalServerError - An unexpected error occurred
Example
import { RpcClient } from "@hazel/domain/rpc"
import { Effect } from "effect"
const deleteReaction = Effect . gen ( function* () {
const client = yield * RpcClient
const result = yield * client . MessageReactionDelete ({
id: "770e8400-e29b-41d4-a716-446655440002" ,
})
console . log ( "Reaction deleted, transaction:" , result . transactionId )
return result
})
Best Practices
Use Toggle for UI Interactions
The toggle method is ideal for reaction buttons:
const ReactionButton = ({ emoji , messageId , channelId }) => {
const handleClick = () =>
Effect . gen ( function* () {
const client = yield * RpcClient
yield * client . MessageReactionToggle ({
messageId ,
channelId ,
emoji ,
})
})
return < button onClick ={ handleClick }>{ emoji } </ button >
}
Optimistic Updates
Implement optimistic updates for instant feedback:
const optimisticToggle = Effect . gen ( function* () {
const client = yield * RpcClient
// Immediately update UI
const hasReaction = toggleReactionInUI ( emoji )
try {
// Send to server
const result = yield * client . MessageReactionToggle ({
messageId ,
channelId ,
emoji ,
})
// Verify UI matches server state
if ( result . wasCreated !== hasReaction ) {
// Revert if mismatch
toggleReactionInUI ( emoji )
}
} catch ( error ) {
// Revert on error
toggleReactionInUI ( emoji )
}
})
Emoji Validation
Validate emoji before sending:
const isValidEmoji = ( str : string ) : boolean => {
const emojiRegex = / ^ ( \p {Emoji} | : \w + : ) $ / u
return emojiRegex . test ( str )
}
const safeToggle = ( emoji : string ) =>
Effect . gen ( function* () {
if ( ! isValidEmoji ( emoji )) {
return yield * Effect . fail ( new Error ( "Invalid emoji" ))
}
const client = yield * RpcClient
return yield * client . MessageReactionToggle ({
messageId ,
channelId ,
emoji ,
})
})
Reaction Aggregation
Aggregate reactions for display:
interface AggregatedReaction {
emoji : string
count : number
users : string []
hasReacted : boolean
}
const aggregateReactions = (
reactions : MessageReaction [],
currentUserId : string
) : AggregatedReaction [] => {
const grouped = reactions . reduce (( acc , reaction ) => {
if ( ! acc [ reaction . emoji ]) {
acc [ reaction . emoji ] = {
emoji: reaction . emoji ,
count: 0 ,
users: [],
hasReacted: false ,
}
}
acc [ reaction . emoji ]. count ++
acc [ reaction . emoji ]. users . push ( reaction . userId )
if ( reaction . userId === currentUserId ) {
acc [ reaction . emoji ]. hasReacted = true
}
return acc
}, {} as Record < string , AggregatedReaction >)
return Object . values ( grouped )
}
Source Code Reference
RPC Contracts: packages/domain/src/rpc/message-reactions.ts
MessageReaction Model: packages/domain/src/models/message-reaction-model.ts
Backend Handlers: apps/backend/src/rpc/handlers/message-reactions.ts