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 when sending 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.
import React from "react";
import { AttachmentActions } from "stream-chat-react";
import type { AttachmentActionsProps } from "stream-chat-react";
const CustomAttachmentActions = (props: AttachmentActionsProps) => {
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>;
Enrich attachments before message submission
To add our attachmentActions
to an uploaded image and trigger the render of the CustomAttachmentActions
component,
we provide custom logic to message composition before the message is sent to the server:
import { isImageAttachment } from "stream-chat";
import type {
Action,
Attachment,
MessageComposerMiddlewareState,
MessageDraftComposerMiddlewareValueState,
MiddlewareHandlerParams,
} from "stream-chat";
const attachmentActions: Action[] = [
{ name: "vote", value: "Love" },
{ name: "vote", value: "Loathe" },
];
const enrichAttachmentsWithActions = (attachments: Attachment[]) =>
attachments.map((att) =>
isImageAttachment(att) ? { ...att, actions: attachmentActions } : att,
);
const attachmentEnrichmentMiddleware = {
id: "custom/message-composer-middleware/attachments-enrichment",
handlers: {
compose: ({
state,
next,
forward,
}: MiddlewareHandlerParams<MessageComposerMiddlewareState>) => {
const { attachmentManager } = composer;
if (!attachmentManager) return forward();
const attachments = enrichAttachmentsWithActions(state.attachments ?? []);
return next({
...state,
localMessage: {
...state.localMessage,
attachments,
},
message: {
...state.message,
attachments,
},
});
},
},
};
const draftAttachmentEnrichmentMiddleware = {
id: "custom/message-composer-middleware/draft-attachments-enrichment",
handlers: {
compose: ({
state,
next,
forward,
}: MiddlewareHandlerParams<MessageDraftComposerMiddlewareValueState>) => {
const { attachmentManager } = composer;
if (!attachmentManager) return forward();
return next({
...state,
draft: {
...state.draft,
attachments: enrichAttachmentsWithActions(state.attachments ?? []),
},
});
},
},
};
const App = () => {
const client = useCreateChatClient({
apiKey,
tokenOrProvider: userToken,
userData: { id: userId },
});
useEffect(() => {
if (!client) return;
client.setMessageComposerSetupFunction(({ composer }) => {
composer.compositionMiddlewareExecutor.insert({
middleware: [attachmentEnrichmentMiddleware],
position: {
after: "stream-io/message-composer-middleware/attachments",
},
});
composer.draftCompositionMiddlewareExecutor.insert({
middleware: [draftAttachmentEnrichmentMiddleware],
position: {
after: "stream-io/message-composer-middleware/draft-attachments",
},
});
});
}, [client]);
if (!client) return <>Loading...</>;
return (
<Chat client={client}>
<ChannelList />
<Channel Attachment={CustomAttachment}>
{/* children of Channel component */}
</Channel>
</Chat>
);
};
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;
}
import React from "react";
import { isImageAttachment } from "stream-chat";
import {
AttachmentActions,
Attachment as AttachmentComponent,
} from "stream-chat-react";
import type {
Action,
Attachment,
MessageComposerMiddlewareState,
MessageDraftComposerMiddlewareValueState,
MiddlewareHandlerParams,
} from "stream-chat";
import type { AttachmentActionsProps, AttachmentProps } from "stream-chat";
const CustomAttachmentActions = (props: AttachmentActionsProps) => {
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 = (props: AttachmentProps) => (
<AttachmentComponent {...props} AttachmentActions={CustomAttachmentActions} />
);
const attachmentActions: Action[] = [
{ name: "vote", value: "Love" },
{ name: "vote", value: "Loathe" },
];
const enrichAttachmentsWithActions = (attachments: Attachment[]) =>
attachments.map((att) =>
isImageAttachment(att) ? { ...att, actions: attachmentActions } : att,
);
const attachmentEnrichmentMiddleware = {
id: "custom/message-composer-middleware/attachments-enrichment",
handlers: {
compose: ({
state,
next,
forward,
}: MiddlewareHandlerParams<MessageComposerMiddlewareState>) => {
const { attachmentManager } = composer;
if (!attachmentManager) return forward();
const attachments = enrichAttachmentsWithActions(state.attachments ?? []);
return next({
...state,
localMessage: {
...state.localMessage,
attachments,
},
message: {
...state.message,
attachments,
},
});
},
},
};
const draftAttachmentEnrichmentMiddleware = {
id: "custom/message-composer-middleware/draft-attachments-enrichment",
handlers: {
compose: ({
state,
next,
forward,
}: MiddlewareHandlerParams<MessageDraftComposerMiddlewareValueState>) => {
const { attachmentManager } = composer;
if (!attachmentManager) return forward();
return next({
...state,
draft: {
...state.draft,
attachments: enrichAttachmentsWithActions(state.attachments ?? []),
},
});
},
},
};
const App = () => {
useEffect(() => {
if (!client) return;
client.setMessageComposerSetupFunction(({ composer }) => {
composer.compositionMiddlewareExecutor.insert({
middleware: [attachmentEnrichmentMiddleware],
position: {
after: "stream-io/message-composer-middleware/attachments",
},
});
composer.draftCompositionMiddlewareExecutor.insert({
middleware: [draftAttachmentEnrichmentMiddleware],
position: {
after: "stream-io/message-composer-middleware/draft-attachments",
},
});
});
}, [client]);
return (
<Chat client={client}>
<ChannelList />
<Channel Attachment={CustomAttachment}>
{/* children of Channel component */}
</Channel>
</Chat>
);
};
The Result
The rendered message before action click:
The rendered message after action click: