export type Action = {
name?: string;
style?: string;
text?: string;
type?: string;
value?: string;
};
Attachment Actions
In this example, we connect several different parts of the library to create a user experience where we add custom attachment actions to uploaded images. Images will render with “Love” and “Loathe” buttons, which on click will post reactions on the message. While this example might not represent a common use case, this demo is meant to highlight the flexibility of the library and show how custom features can be built on top of the existing code.
Custom Actions
The first step is to create an array of custom actions that will populate the AttachmentActions
component on send of an image attachment. The Action
type comes from the stream-chat
JavaScript client and conforms
to the following:
As you can tell, the Action
type has no required values. We are going to simulate a voting feature and trigger the
UI on “Love” and “Loathe” potential actions. Our custom actions array becomes the following:
const attachmentActions: Action[] = [
{ name: "vote", value: "Love" },
{ name: "vote", value: "Loathe" },
];
Custom AttachmentActions
Next, we create a custom AttachmentActions
component to render and display our custom actions. If a chat user uploads an image file, we’ll trigger custom logic. Otherwise,
we’ll render the default library component.
Our custom component will receive the attachment type
and the actions
(if any) via props. We’ll manually add the actions later
in the demo, but for now, know their value will reference our custom array defined above.
If an image attachment is uploaded, we map over the custom actions array and return HTML button
elements with action.value
as the text. Click events on these buttons will post reactions to the message, using the handleReaction
function drawn from the
useMessageContext
custom hook.
const CustomAttachmentActions: React.FC<AttachmentActionsProps> = (props) => {
const { actions, type } = props;
const { handleReaction } = useMessageContext();
const handleClick = async (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
value?: string,
) => {
try {
if (value === "Love") await handleReaction("love", event);
if (value === "Loathe") await handleReaction("angry", event);
} catch (err) {
console.log(err);
}
};
if (type === "image") {
return (
<>
{actions.map((action) => (
<button
className={`action-button ${action.value === "Love" ? "love" : ""}`}
onClick={(event) => handleClick(event, action.value)}
>
{action.value}
</button>
))}
</>
);
}
return <AttachmentActions {...props} />;
};
Custom Attachment
In order to render our CustomAttachmentActions
component, we need to supply it as a prop to the
Attachment
component. The resulting CustomAttachment
component is then added to Channel
, so it can be injected into the
ComponentContext
and consumed within the Message UI component.
const CustomAttachment: React.FC<AttachmentProps> = (props) => (
<Attachment {...props} AttachmentActions={CustomAttachmentActions} />
);
<Channel Attachment={CustomAttachment}>
{/* children of Channel component */}
</Channel>;
Input Submit Handler
To add our attachmentActions
to an uploaded image and trigger the render of the CustomAttachmentActions
component,
we provide custom logic to override the MessageInput
component’s default submit handling behavior. For a detailed,
step-by-step example, see the Input Submit Handler custom code example.
Simply put, if an image
type message attachment exists, we update the attachments
array on the message
object
by adding the attachmentActions
.
const overrideSubmitHandler = (message: MessageToSend, cid: string) => {
let updatedMessage = message;
if (message.attachments) {
message.attachments.forEach((attachment) => {
if (attachment.type === "image") {
const updatedAttachment = {
...attachment,
actions: attachmentActions,
};
updatedMessage = {
...message,
attachments: [updatedAttachment],
};
}
});
}
sendMessage(updatedMessage);
};
Implementation
Now that each individual piece has been constructed, we can assemble all of the snippets into the final code example.
The Code
.action-button {
height: 40px;
width: 100px;
border-radius: 16px;
color: #ffffff;
background: red;
font-weight: 700;
font-size: 1.2rem;
}
.action-button.love {
background-color: green;
}
const attachmentActions: Action[] = [
{ name: "vote", value: "Love" },
{ name: "vote", value: "Loathe" },
];
const CustomAttachmentActions: React.FC<AttachmentActionsProps> = (props) => {
const { actions, type } = props;
const { handleReaction } = useMessageContext();
const handleClick = async (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
value?: string,
) => {
try {
if (value === "Love") await handleReaction("love", event);
if (value === "Loathe") await handleReaction("angry", event);
} catch (err) {
console.log(err);
}
};
if (type === "image") {
return (
<>
{actions.map((action) => (
<button
className={`action-button ${action.value === "Love" ? "love" : ""}`}
onClick={(event) => handleClick(event, action.value)}
>
{action.value}
</button>
))}
</>
);
}
return <AttachmentActions {...props} />;
};
const CustomAttachment: React.FC<AttachmentProps> = (props) => (
<Attachment {...props} AttachmentActions={CustomAttachmentActions} />
);
const ChannelInner: React.FC = () => {
const { sendMessage } = useChannelActionContext();
const overrideSubmitHandler = (message: MessageToSend, cid: string) => {
let updatedMessage = message;
if (message.attachments) {
message.attachments.forEach((attachment) => {
if (attachment.type === "image") {
const updatedAttachment = {
...attachment,
actions: attachmentActions,
};
updatedMessage = {
...message,
attachments: [updatedAttachment],
};
}
});
}
sendMessage(updatedMessage);
};
return (
<>
<ChannelHeader />
<MessageList />
<MessageInput overrideSubmitHandler={overrideSubmitHandler} />
</>
);
};
const App = () => (
<Chat client={client}>
<ChannelList />
<Channel Attachment={CustomAttachment}>
<ChannelInner />
</Channel>
</Chat>
);
The Result
The rendered message before action click:
The rendered message after action click: