question: how do I add custom attachment objects to MessageInput state?
Describe the bug
When customizing the MessageInput component to handle file attachments, the message does not properly accept the attachment. Specifically, when trying to send a message with an attachment (image or file), the message fails to include the attachment.
To Reproduce
Steps to reproduce the behavior:
//home component code start
const removeFilePreview = (index) => {
const newFilePreviews = filePreviews.filter((_, i) => i !== index);
setFilePreviews(newFilePreviews);
};
const handleFileClick = () => {
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = "*/*";
fileInput.multiple = true;
fileInput.onchange = (e) => {
const files = e.target.files;
if (files.length > 0) {
const previews = Array.from(files).map((file) => ({
file,
preview: URL.createObjectURL(file),
}));
setFilePreviews((prev) => [...prev, ...previews]);
}
};
fileInput.click();
};
const isFileImage = (file) => {
const imageExtensions = ["jpg", "jpeg", "png", "gif", "bmp", "webp"];
const fileExtension = file.name.split(".").pop().toLowerCase();
return imageExtensions.includes(fileExtension);
};
const setMainPreview = (index) => {
const selectedPreview = filePreviews.splice(index, 1)[0];
setFilePreviews([selectedPreview, ...filePreviews]);
};
{filePreviews.length > 0 && (
<div className="absolute z-10 top-16 left-0 right-0 bottom-20 w-2/2 bg-[#E5E4E2] flex flex-col items-center justify-center p-0">
<div className="relative w-full h-[90%]">
<button
onClick={() => removeFilePreview(0)}
type="button"
className="absolute top-0 mt-[-30px] bottom-160 left-10 text-grey p-2"
>
<IoCloseCircleOutline size={30} />
</button>
{isFileImage(filePreviews[0].file) ? (
<img
src={filePreviews[0].preview}
alt="Preview"
className="object-contain h-[85%] w-full px-4"
/>
) : (
<div className="flex flex-col items-center justify-center h-[85%]">
<FaFile size={50} className="text-gray-400 mb-2" />
<p className="text-gray-400">No preview available</p>
</div>
)}
{filePreviews.length > 0 && (
<div className="flex items-center justify-center mb-4 px-4 pt-5">
{filePreviews.map((preview, index) => (
<div
key={index}
className={`relative mr-2 ${
index === 0 ? "border-2 border-blue-500" : ""
}`}
onClick={() => setMainPreview(index)}
>
<button
onClick={(e) => {
e.stopPropagation();
removeFilePreview(index);
}}
type="button"
className="absolute top-0 right-0 text-grey p-1 opacity-0 hover:opacity-100 transition-opacity duration-300 shadow-md"
>
<IoCloseCircleOutline color="grey" size={10} />
</button>
{isFileImage(preview.file) ? (
<img
src={preview.preview}
alt="Preview"
className="object-cover w-14 h-14 rounded"
/>
) : (
<div className="flex flex-col items-center justify-center w-14 h-14 bg-gray-200 rounded">
<FaFile size={24} className="text-gray-500 mb-1" />
<p className="text-[10px] text-gray-500">
No preview
</p>
</div>
)}
</div>
))}
<div
className="w-16 h-16 bg-gray-200 rounded flex items-center justify-center cursor-pointer"
onClick={() => handleFileClick()}
>
<IoAddCircleOutline size={24} />
</div>
</div>
)}
</div>
</div>
)}
<Chat client={streamClient} theme="messaging light">
<Channel channel={channel} >
<Window>
<div className="p-2 bg-[#315A9D]">
<CustomChannelHeader
onClick={toggleContactInfo}
isContactInfoOpen={isContactInfoOpen}
user={user}
/>
</div>
<MessageList
/>
<CustomMessageInput
overrideSubmitHandler={overrideSubmitHandler}
channel={channel}
setFilePreviews={setFilePreviews}
EmojiPicker={EmojiPicker}
filePreviews={filePreviews}
/>
</Window>
</Channel>
</Chat>
///home component
import React, { useState } from "react";
import {
MessageInput,
ChatAutoComplete,
useMessageInputContext,
useChannelActionContext,
Attachment,
} from "stream-chat-react";
import Picker from "@emoji-mart/react";
import { FaMapMarkerAlt, FaImage, FaRegFileAlt } from "react-icons/fa";
import { BsEmojiSmile } from "react-icons/bs";
import {
IoAddCircleOutline,
IoMicOutline,
IoCloseCircleOutline,
} from "react-icons/io5";
import "./style.css";
import { PiHeadphones } from "react-icons/pi";
import Modal from "react-modal";
import client from "../../streamClient";
const customStyles = {
content: {
top: "68%",
left: "28%",
bottom: "0%",
width: "170px",
height: "230px",
borderRadius: "20px",
},
overlay: {
backgroundColor: "transparent",
},
};
Modal.setAppElement("#root");
const CustomMessageInput = ({
isContactInfoOpen,
filePreviews,
setFilePreviews,
// overrideSubmitHandler,
...props
}) => {
const [isOpen, setIsOpen] = useState(false);
const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false);
const { handleSubmit, text, setText, uploadNewFiles } =
useMessageInputContext();
const { sendMessage } = useChannelActionContext();
console.log(filePreviews, "previews");
const toggleModal = () => {
setIsOpen(!isOpen);
};
const handleSmileClick = () => {
setIsEmojiPickerOpen(!isEmojiPickerOpen);
};
const handleEmojiSelect = (emoji) => {
setText((prevText) => prevText + emoji.native);
setIsEmojiPickerOpen(false);
};
const handleFileChange = (e) => {
const files = e.target.files;
if (files.length > 0) {
const previews = Array.from(files).map((file) => ({
file,
preview: URL.createObjectURL(file),
}));
setFilePreviews((prev) => [...prev, ...previews]);
}
toggleModal();
};
const handleFileClick = (acceptType) => {
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = acceptType;
fileInput.multiple = true;
fileInput.onchange = handleFileChange;
fileInput.click();
};
const handleLocationClick = () => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
console.log("Latitude:", position.coords.latitude);
console.log("Longitude:", position.coords.longitude);
},
(error) => {
console.error("Error getting location:", error);
}
);
} else {
console.error("Geolocation is not supported by this browser.");
}
};
const handleSend = async (event) => {
event.preventDefault();
if (filePreviews.length > 0) {
await sendMessage(filePreviews.map((preview) => preview.file));
}
await handleSubmit(event);
setFilePreviews([]);
};
const overrideSubmitHandler = (message, filePreviews) => {
let updatedMessage = message;
console.log(message, 'message14')
if (message.attachments) {
message.attachments.forEach((attachment) => {
if (attachment.type === 'image') {
const updatedAttachment = {
...filePreviews,
};
updatedMessage = {
...message,
attachments: [updatedAttachment],
};
}
});
}
sendMessage(updatedMessage);
};
// const meg = channel.sendImage(filePreviews[0])
return (
<div className={`sticky bottom-0 bg-[#DEE3EF] text-gray-800 p-3`}>
<div className="flex items-center">
<div
className="cursor-pointer mr-2 p-2 rounded-full bg-[#315A9D] text-white flex items-center justify-center"
onClick={handleSmileClick}
>
<BsEmojiSmile size={20} />
</div>
<div
className="cursor-pointer mr-2 p-2 rounded-full bg-[#315A9D] text-white flex items-center justify-center"
onClick={toggleModal}
>
{isOpen ? (
<IoCloseCircleOutline size={20} />
) : (
<IoAddCircleOutline size={20} />
)}
</div>
<MessageInput
// doFileUploadRequest={(file, channel) => {
// return client.sendFile(filePreviews, file);
// }}
// doImageUploadRequest={(file, channel) => {
// return client.sendFile(filePreviews, file);
// doFileUploadRequest = {filePreviews}
// attachments={filePreviews}
{...props}
Input={(inputProps) => (
<div className="flex-1 relative">
<ChatAutoComplete
{...inputProps}
value={text}
// filePreviews={filePreviews}
// onChange={(e) => setText(e.target.value)}
className="bg-white text-gray-800 placeholder-gray-400 px-4 py-1 rounded-lg border border-gray-300"
placeholder="Type Message..."
name="message"
overrideSubmitHandler={overrideSubmitHandler}
// onKeyDown={(e) => {
// if (
// (e.key === "Enter" || e.key === "Return") &&
// !e.shiftKey
// ) {
// handleSend(e);
// }
// }}
/>
</div>
)}
/>
<div className="cursor-pointer ml-2 p-2 rounded-full bg-[#315A9D] text-white flex items-center justify-center">
<IoMicOutline size={20} />
</div>
</div>
{isEmojiPickerOpen && (
<div className="absolute bottom-12 left-2 z-10">
<Picker onEmojiSelect={handleEmojiSelect} />
</div>
)}
<Modal
isOpen={isOpen}
onRequestClose={toggleModal}
style={customStyles}
contentLabel="Example Modal"
>
<div className="flex flex-col space-y-4 h-auto">
<label className="flex items-center cursor-pointer">
<FaRegFileAlt color="#315A9D" className="mr-2" />
<span>Document</span>
<input
type="file"
style={{ display: "none" }}
accept=".pdf,.doc,.docx,.txt,.xls,.xlsx,.ppt,.pptx,.odt"
onChange={handleFileChange}
/>
</label>
<button
className="flex items-center"
onClick={() => handleFileClick("image/*")}
>
<FaImage color="#315A9D" className="mr-2" /> Gallery
</button>
<button
className="flex items-center"
onClick={() => handleFileClick("audio/*")}
>
<PiHeadphones color="#315A9D" className="mr-2" /> Audio
</button>
<button
className="flex items-center"
onClick={() => handleFileClick("video/*")}
>
<FaMapMarkerAlt color="#315A9D" className="mr-2" /> Video
</button>
<button className="flex items-center" onClick={handleLocationClick}>
<FaMapMarkerAlt color="#315A9D" className="mr-2" /> Location
</button>
</div>
</Modal>
</div>
);
};
export default CustomMessageInput;
-Implement the above CustomMessageInput component in your project. -Try to send a message with an attachment.
- Observe the issue where the message does not send with the attachment.
- Please let me know if you need any additional information or further assistance.
Expected behavior
A clear and concise description of what you expected to happen.
onClicking the document/image/audi and so on from modal I want to open that and pass that to message input .
Package version
- stream-chat-react:^11.18.1
- stream-chat-css:
- stream-chat-js: "react": "^18.3.1",
Desktop (please complete the following information):
- OS: [e.g. iOS] iOS
- Browser [e.g. chrome, safari] Chrome
- Version [e.g. 22] Version 125.0.6422.142
Additional context
Here's the corrected sentence:
This issue is blocking my development . I tried using overrideSubmitHandler, but this causes a delay in sending the message and not getting attachments.
Hey @ElizabethSobiya, could you please create a repro with Codesandbox forked from template https://codesandbox.io/p/devbox/priceless-forest-g64slj ? Thank you
Hi @MartinCupela
Unfortunately, I cannot share the code due to our company policy. However, I can provide you with detailed information about the issue and the changes I've made. All the required code snippets are attached above. Please let me know how you would like to proceed. Thank you! I would appreciate it if you could solve the issue as soon as possible.
@ElizabethSobiya could you make sure the pasted code will work in forked sandbox of https://codesandbox.io/p/devbox/priceless-forest-g64slj ?
@MartinCupela Hi, The code in your message won't work as it is. Below is the workable code for the Home and CustomMessageInput components:
// Custom Message Input import React, { useState } from "react"; import Picker from "@emoji-mart/react"; import { IoAddCircleOutline, IoMicOutline, IoCloseCircleOutline } from "react-icons/io5"; import { FaImage, FaRegFileAlt, FaMapMarkerAlt, FaFile } from "react-icons/fa"; import { PiHeadphones } from "react-icons/pi"; import Modal from "react-modal"; import { BsEmojiSmile } from "react-icons/bs"; import "./style.css";
const customStyles = { content: { top: "68%", left: "28%", bottom: "0%", width: "170px", height: "230px", borderRadius: "20px", }, overlay: { backgroundColor: "transparent", }, };
Modal.setAppElement("#root");
const CustomMessageInput = ({ setFilePreviews, filePreviews, onSendMessage }) => { const [isOpen, setIsOpen] = useState(false); const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false); const [text, setText] = useState("");
const toggleModal = () => { setIsOpen(!isOpen); };
const handleSmileClick = () => { setIsEmojiPickerOpen(!isEmojiPickerOpen); };
const handleEmojiSelect = (emoji) => { setText((prevText) => prevText + emoji.native); setIsEmojiPickerOpen(false); };
const handleFileChange = (e) => { const files = e.target.files; if (files.length > 0) { const previews = Array.from(files).map((file) => ({ file, preview: URL.createObjectURL(file), })); setFilePreviews((prev) => [...prev, ...previews]); } toggleModal(); };
const handleFileClick = (acceptType) => { const fileInput = document.createElement("input"); fileInput.type = "file"; fileInput.accept = acceptType; fileInput.multiple = true; fileInput.onchange = handleFileChange; fileInput.click(); };
const handleLocationClick = () => { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( (position) => { console.log("Latitude:", position.coords.latitude); console.log("Longitude:", position.coords.longitude); }, (error) => { console.error("Error getting location:", error); } ); } else { console.error("Geolocation is not supported by this browser."); } };
const handleSend = (event) => { event.preventDefault(); onSendMessage({ text }); setText(""); };
return ( <div className="sticky bottom-0 bg-[#DEE3EF] text-gray-800 p-3"> <div className="flex items-center"> <div className="cursor-pointer mr-2 p-2 rounded-full bg-[#315A9D] text-white flex items-center justify-center" onClick={handleSmileClick} > <BsEmojiSmile size={20} /> <div className="cursor-pointer mr-2 p-2 rounded-full bg-[#315A9D] text-white flex items-center justify-center" onClick={toggleModal} > {isOpen ? ( <IoCloseCircleOutline size={20} /> ) : ( <IoAddCircleOutline size={20} /> )} <form onSubmit={handleSend} className="flex-1"> <input type="text" value={text} onChange={(e) => setText(e.target.value)} className="bg-white text-gray-800 placeholder-gray-400 px-4 py-1 rounded-lg border border-gray-300 w-full" placeholder="Type Message..." name="message" /> <div className="cursor-pointer ml-2 p-2 rounded-full bg-[#315A9D] text-white flex items-center justify-center"> <IoMicOutline size={20} />
{isEmojiPickerOpen && (
<div className="absolute bottom-12 left-2 z-10">
<Picker onEmojiSelect={handleEmojiSelect} />
</div>
)}
<Modal
isOpen={isOpen}
onRequestClose={toggleModal}
style={customStyles}
contentLabel="Example Modal"
>
<div className="flex flex-col space-y-4 h-auto">
<label className="flex items-center cursor-pointer">
<FaRegFileAlt color="#315A9D" className="mr-2" />
<span>Document</span>
<input
type="file"
style={{ display: "none" }}
accept=".pdf,.doc,.docx,.txt,.xls,.xlsx,.ppt,.pptx,.odt"
onChange={handleFileChange}
/>
</label>
<button className="flex items-center" onClick={() => handleFileClick("image/*")}>
<FaImage color="#315A9D" className="mr-2" /> Gallery
</button>
<button className="flex items-center" onClick={() => handleFileClick("audio/*")}>
<PiHeadphones color="#315A9D" className="mr-2" /> Audio
</button>
<button className="flex items-center" onClick={() => handleFileClick("video/*")}>
<FaMapMarkerAlt color="#315A9D" className="mr-2" /> Video
</button>
<button className="flex items-center" onClick={handleLocationClick}>
<FaMapMarkerAlt color="#315A9D" className="mr-2" /> Location
</button>
</div>
</Modal>
</div>
); };
export default CustomMessageInput;
//Custom message input end
// Home Component
import React, { useState } from "react"; import { IoCloseCircleOutline, IoAddCircleOutline } from "react-icons/io5"; import { FaFile } from "react-icons/fa"; import CustomMessageInput from "./channel/ChannelInput"; import "stream-chat-react/dist/css/index.css";
const Home = () => { const [messages, setMessages] = useState([]); const [filePreviews, setFilePreviews] = useState([]); const [isContactInfoOpen, setIsContactInfoOpen] = useState(false);
const toggleContactInfo = () => { setIsContactInfoOpen(!isContactInfoOpen); };
const removeFilePreview = (index) => { const newFilePreviews = filePreviews.filter((_, i) => i !== index); setFilePreviews(newFilePreviews); };
const isFileImage = (file) => { const imageExtensions = ["jpg", "jpeg", "png", "gif", "bmp", "webp"]; const fileExtension = file.name.split(".").pop().toLowerCase(); return imageExtensions.includes(fileExtension); };
const setMainPreview = (index) => { const selectedPreview = filePreviews.splice(index, 1)[0]; setFilePreviews([selectedPreview, ...filePreviews]); };
const handleFileClick = () => { const fileInput = document.createElement("input"); fileInput.type = "file"; fileInput.accept = "/"; fileInput.multiple = true; fileInput.onchange = (e) => { const files = e.target.files; if (files.length > 0) { const previews = Array.from(files).map((file) => ({ file, preview: URL.createObjectURL(file), })); setFilePreviews((prev) => [...prev, ...previews]); } }; fileInput.click(); };
const handleSend = (message) => { setMessages((prevMessages) => [...prevMessages, message]); };
return ( <div className="max-h-[99.99vh] flex flex-col dark:bg-gray-900"> {filePreviews.length > 0 && ( <div className="absolute z-10 top-16 left-0 right-0 bottom-20 w-2/2 bg-[#E5E4E2] flex flex-col items-center justify-center p-0"> <div className="relative w-full h-[90%]"> <button onClick={() => removeFilePreview(0)} type="button" className="absolute top-0 mt-[-30px] bottom-160 left-10 text-grey p-2" > <IoCloseCircleOutline size={30} /> {isFileImage(filePreviews[0].file) ? ( <img src={filePreviews[0].preview} alt="Preview" className="object-contain h-[85%] w-full px-4" /> ) : ( <div className="flex flex-col items-center justify-center h-[85%]"> <FaFile size={50} className="text-gray-400 mb-2" /> <p className="text-gray-400">No preview available
)} {filePreviews.length > 0 && ( <div className="flex items-center justify-center mb-4 px-4 pt-5"> {filePreviews.map((preview, index) => ( <div key={index} className={relative mr-2 ${ index === 0 ? "border-2 border-blue-500" : "" }}
onClick={() => setMainPreview(index)}
>
<button
onClick={(e) => {
e.stopPropagation();
removeFilePreview(index);
}}
type="button"
className="absolute top-0 right-0 text-grey p-1 opacity-0 hover:opacity-100 transition-opacity duration-300 shadow-md"
>
<IoCloseCircleOutline color="grey" size={10} />
{isFileImage(preview.file) ? (
<img
src={preview.preview}
alt="Preview"
className="object-cover w-14 h-14 rounded"
/>
) : (
<div className="flex flex-col items-center justify-center w-14 h-14 bg-gray-200 rounded">
<FaFile size={24} className="text-gray-500 mb-1" />
<p className="text-[10px] text-gray-500">No preview
)}
))}
<div
className="w-16 h-16 bg-gray-200 rounded flex items-center justify-center cursor-pointer"
onClick={handleFileClick}
>
<IoAddCircleOutline size={24} />
)}
)}
<div className="flex-grow flex flex-col w-full">
<div className="p-2 bg-[#315A9D] ">
<div
className="flex items-center justify-between"
onClick={toggleContactInfo}
>
<h2 className="text-white">Chat Header
<button className="text-white">Toggle Info
<div className="flex flex-col flex-grow overflow-auto p-4">
{messages.map((msg, index) => (
<div key={index} className="p-2 mb-2 bg-white rounded">
{msg.text}
))}
<CustomMessageInput
setFilePreviews={setFilePreviews}
filePreviews={filePreviews}
onSendMessage={handleSend}
/>
);
};
export default Home;
//Home component end
In the above code, on the left side, there is a "+" icon. Clicking this icon opens a modal. Within the modal, there are separate icons for different attachment types. You can select a file and share it in the message input, with or without an accompanying message, according to your requirement.
This is my requirement for custom message input attachment, but it is not working as expected. Please help us resolve these custom attachment issues as soon as possible.
Thanks in advance
Hey @ElizabethSobiya , if I understand well, you do not want to prepare working example from your code (which you understand) and you expect from us to setup the whole scenario for you? I think this is a misunderstanding from your side. I have to insist on providing the sandbox repro so that we can help you debug your code.
Hey @MartinCupela, I understand, and I apologize for the misunderstanding. I will share the reproducible example with a working setup as soon as possible. Thank you for your patience.
@MartinCupela Sorry for the delay, I tried forking the code but caught with unexpected error and I am struck there.
Can I know how we can resolve this ? And I also tried useMessageInputState hooks for custom attachments like uploadAttachment but didn't got a clear solution from that.
The fork link https://codesandbox.io/p/sandbox/support-to-fork-template-forked-64jc85 .
The error in your sandbox is caused by using LoadingIndicator but not importing it. Please consider this GH issues list to be dedicated to solving documented issues, not debugging integration code.
@MartinCupela Sorry for late reply and the above sandbox code.
I just created a message input with my requirement, in the below sandbox link in that I had used https://getstream.io/chat/docs/sdk/react/hooks/message_input_hooks/#usemessageinputstate this hooks to custom the attachments but it is throwing me TypeError: undefined is not an object (evaluating 'props.additionalTextareaProps and I tried to get attachments for both file and image separately and pass that along with messages.If I removed the hooks that I used and just had messageInput means it is working properly but when I try to use hooks means it is either throwing me an error or just getting an blank page.
https://codesandbox.io/p/sandbox/attachments-gg98tg
@ElizabethSobiya I probably do not have permission to access the sandbox.
The link does not work. Can be the sandbox made public?
https://codesandbox.io/p/sandbox/attachments-forked-7rlv49
Hey, this link is working, cross checked it and it is public
The example build command fails.
The MessageInput component does not have to be wrapped in another form with input. I would say that this is the root of the problem.
Use upsertAttachments function if you need to add custom attachments to the MessageInput state. You can build your custom UI component that will be rendered within MessageInput.
This definitely is not a bug but rather lack of knowledge. Therefore I will re-clasify the issue.
@ElizabethSobiya were you able to resolve your issues?
Closing the issue for inactivity/