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:

export type Action = {
  name?: string;
  style?: string;
  text?: string;
  type?: string;
  value?: string;
};

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:

Attachment Actions 1

The rendered message after action click:

Attachment Actions 2

© Getstream.io, Inc. All Rights Reserved.