Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

question: how do I add custom attachment objects to MessageInput state? #2417

Open
ElizabethSobiya opened this issue Jun 9, 2024 · 13 comments
Labels
question Support request

Comments

@ElizabethSobiya
Copy link

ElizabethSobiya commented Jun 9, 2024

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.

@ElizabethSobiya ElizabethSobiya added bug Something isn't working status: unconfirmed labels Jun 9, 2024
@ElizabethSobiya ElizabethSobiya changed the title bug:Unable to 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. Jun 9, 2024
@ElizabethSobiya ElizabethSobiya changed the title 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. bug: When customizing the MessageInput component to handle file attachments, the message does not properly accept the attachment. Jun 9, 2024
@MartinCupela
Copy link
Contributor

Hey @ElizabethSobiya, could you please create a repro with Codesandbox forked from template https://codesandbox.io/p/devbox/priceless-forest-g64slj ? Thank you

@ElizabethSobiya
Copy link
Author

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.

@MartinCupela
Copy link
Contributor

@ElizabethSobiya could you make sure the pasted code will work in forked sandbox of https://codesandbox.io/p/devbox/priceless-forest-g64slj ?

@ElizabethSobiya
Copy link
Author

@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 (







{isOpen ? (

) : (

)}


<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"
/>




  {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 (


{filePreviews.length > 0 && (


<button
onClick={() => removeFilePreview(0)}
type="button"
className="absolute top-0 mt-[-30px] bottom-160 left-10 text-grey p-2"
>


{isFileImage(filePreviews[0].file) ? (
Preview
) : (


No preview available



)}
{filePreviews.length > 0 && (

{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"
>


{isFileImage(preview.file) ? (
Preview
) : (


No preview



)}

))}




)}


)}



Chat Header


Toggle Info



{messages.map((msg, index) => (

{msg.text}

))}




);
};

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

@MartinCupela
Copy link
Contributor

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.

@ElizabethSobiya
Copy link
Author

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.

@ElizabethSobiya
Copy link
Author

@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 .

@MartinCupela
Copy link
Contributor

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.

@ElizabethSobiya
Copy link
Author

@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

@MartinCupela
Copy link
Contributor

@ElizabethSobiya I probably do not have permission to access the sandbox.

image

@MartinCupela
Copy link
Contributor

The link does not work. Can be the sandbox made public?

image

@ElizabethSobiya
Copy link
Author

https://codesandbox.io/p/sandbox/attachments-forked-7rlv49

Hey, this link is working, cross checked it and it is public

@MartinCupela
Copy link
Contributor

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.

@MartinCupela MartinCupela added question Support request and removed bug Something isn't working status: unconfirmed labels Jun 26, 2024
@MartinCupela MartinCupela changed the title bug: When customizing the MessageInput component to handle file attachments, the message does not properly accept the attachment. question: how do I add custom attachment objects to MessageInput state? Jun 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Support request
Projects
None yet
Development

No branches or pull requests

2 participants