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:

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.

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:

Attachment Actions 1

The rendered message after action click:

Attachment Actions 2

© Getstream.io, Inc. All Rights Reserved.