April 17, 2023
Social Networking Application using React.js and Nest.js. Part-5 Chat Form Functionality.
This article is about the functionality of chat form.

By Fahad Ali
13 min read
Chat Form:
Connecting Emojis with the Input. Open /page-components/chats-page/chat/ChatForm.tsx.
import { EmojiPicker } from "@/components/EmojiPicker"
import { Group, Stack, TextInput, UnstyledButton } from "@mantine/core"
import { FormEvent, useRef, useState } from "react"
import { IconMoodSmile, IconPaperclip, IconMicrophone } from '@tabler/icons-react';
import { useClickOutside } from "@mantine/hooks";
import { EmojiClickData } from "emoji-picker-react";
type Props = {
}
export function ChatForm({ }: Props) {
const [showEmoji, setShowEmoji] = useState(false);
const emojiRef = useClickOutside(() => setShowEmoji(false));
const messageRef = useRef<HTMLInputElement>(null);
const [message, setMessage] = useState("");
function handleSubmit(e: FormEvent) {
e.preventDefault();
console.log("MESSAGE", message)
}
function onEmojiPicker(emoji: EmojiClickData, event: MouseEvent) {
if (messageRef.current) {
console.log(emoji.unified);
const { selectionStart, selectionEnd } = messageRef.current;
const newValue = message.slice(0, selectionStart!) + emoji.emoji + message.slice(selectionEnd!);
setMessage(newValue);
}
}
return <Stack justify="center" className="absolute px-8 border w-full bottom-0 h-[85px]">
<Group className="relative">
{showEmoji && <EmojiPicker onEmojiPicker={onEmojiPicker} emojiRef={emojiRef} />}
<UnstyledButton onClick={() => setShowEmoji(true)}>
<IconMoodSmile className="text-[#646d75] cursor-pointer" />
</UnstyledButton>
<UnstyledButton>
<IconPaperclip className="text-[#646d75] cursor-pointer" />
</UnstyledButton>
<form className="grow" onSubmit={handleSubmit}>
<TextInput
placeholder="Type your message here..."
size="md"
ref={messageRef}
value={message}
onChange={e => setMessage(e.target.value)}
/>
</form>
<UnstyledButton>
<IconMicrophone className="text-[#646d75] cursor-pointer" />
</UnstyledButton>
</Group>
</Stack>
}import { EmojiPicker } from "@/components/EmojiPicker"
import { Group, Stack, TextInput, UnstyledButton } from "@mantine/core"
import { FormEvent, useRef, useState } from "react"
import { IconMoodSmile, IconPaperclip, IconMicrophone } from '@tabler/icons-react';
import { useClickOutside } from "@mantine/hooks";
import { EmojiClickData } from "emoji-picker-react";
type Props = {
}
export function ChatForm({ }: Props) {
const [showEmoji, setShowEmoji] = useState(false);
const emojiRef = useClickOutside(() => setShowEmoji(false));
const messageRef = useRef<HTMLInputElement>(null);
const [message, setMessage] = useState("");
function handleSubmit(e: FormEvent) {
e.preventDefault();
console.log("MESSAGE", message)
}
function onEmojiPicker(emoji: EmojiClickData, event: MouseEvent) {
if (messageRef.current) {
console.log(emoji.unified);
const { selectionStart, selectionEnd } = messageRef.current;
const newValue = message.slice(0, selectionStart!) + emoji.emoji + message.slice(selectionEnd!);
setMessage(newValue);
}
}
return <Stack justify="center" className="absolute px-8 border w-full bottom-0 h-[85px]">
<Group className="relative">
{showEmoji && <EmojiPicker onEmojiPicker={onEmojiPicker} emojiRef={emojiRef} />}
<UnstyledButton onClick={() => setShowEmoji(true)}>
<IconMoodSmile className="text-[#646d75] cursor-pointer" />
</UnstyledButton>
<UnstyledButton>
<IconPaperclip className="text-[#646d75] cursor-pointer" />
</UnstyledButton>
<form className="grow" onSubmit={handleSubmit}>
<TextInput
placeholder="Type your message here..."
size="md"
ref={messageRef}
value={message}
onChange={e => setMessage(e.target.value)}
/>
</form>
<UnstyledButton>
<IconMicrophone className="text-[#646d75] cursor-pointer" />
</UnstyledButton>
</Group>
</Stack>
}- This code is a React functional component called
ChatFormthat renders a chat message input form. Here is a brief overview of what the code does: useStatehooks are used to manage the state ofshowEmoji,message, andsetMessage.useRefhook is used to create a reference to the input field element.- The
handleSubmitfunction logs the message to the console when the form is submitted. - The
onEmojiPickerfunction is called when the user selects an emoji. It usesmessageRef.currentto determine the cursor position in the input field and inserts the selected emoji at that position. - The component renders a
Stackcomponent with ajustifyprop set to "center" and aGroupcomponent containing severalUnstyledButtoncomponents and a form with aTextInputcomponent.
The EmojiPicker component is imported from a custom component called EmojiPicker. The useClickOutside hook is used to handle the closing of the emoji picker when the user clicks outside of the emoji picker container.
The next step is to update EmojiPicker Component. Open the /components/EmojiPicker.tsx file.
import { EmojiClickData } from 'emoji-picker-react';
import dynamic from 'next/dynamic';
import { MutableRefObject } from 'react';
const Picker = dynamic(
() => {
return import('emoji-picker-react');
},
{ ssr: false }
);
type Props = {
emojiRef: MutableRefObject<any>,
onEmojiPicker: (emoji: EmojiClickData, event: MouseEvent) => void
}
export function EmojiPicker({ emojiRef, onEmojiPicker }: Props) {
return <div ref={emojiRef} className='absolute w-full bottom-6'>
<Picker onEmojiClick={onEmojiPicker} width={"100%"} />
</div>
}import { EmojiClickData } from 'emoji-picker-react';
import dynamic from 'next/dynamic';
import { MutableRefObject } from 'react';
const Picker = dynamic(
() => {
return import('emoji-picker-react');
},
{ ssr: false }
);
type Props = {
emojiRef: MutableRefObject<any>,
onEmojiPicker: (emoji: EmojiClickData, event: MouseEvent) => void
}
export function EmojiPicker({ emojiRef, onEmojiPicker }: Props) {
return <div ref={emojiRef} className='absolute w-full bottom-6'>
<Picker onEmojiClick={onEmojiPicker} width={"100%"} />
</div>
}- The above code is defining a functional React component
EmojiPicker. It takes in two props:
emojiRef: a mutable reference object that will be used to reference thedivelement containing thePickercomponentonEmojiPicker: a function that will be called when an emoji is clicked on thePicker. It takes in two arguments:emoji, an object containing data about the clicked emoji, andevent, the click event that triggered the selection.
Choose Media File:
In this section, the user will be able to select multiple images from their system. Then the user will be able to preview and remove the image from the list. When the user clicks the upload button, the images will be sent to the server.
Create a new component named ChatFileUpload.tsx inside /page-components/chats-page/chat.
import { Button, Grid, Group, Image, Modal, ScrollArea, UnstyledButton } from "@mantine/core"
import { ChangeEvent, useRef, useState } from "react";
import { IconPaperclip, IconX} from '@tabler/icons-react';
import { useDisclosure } from "@mantine/hooks";
type Props = {
uploadFilesHandler: (files: File[]) => void;
}
export function ChatFileUploadComponent({ uploadFilesHandler }: Props) {
const fileInputRef = useRef<HTMLInputElement>(null);
const [media, setMedia] = useState<File[]>([]);
const [opened, { open, close }] = useDisclosure(false);
function onFileInputClick() {
if (fileInputRef.current) {
fileInputRef.current.click();
}
}
function onFileChange(e: ChangeEvent<HTMLInputElement>) {
const files = e.target.files;
const elements = [];
if (files && files.length > 0) {
for (let index = 0; index < files.length; index++) {
const file = files[index];
elements.push(file);
}
setMedia(elements);
open();
}
}
function handleRemove(index: number) {
const newMedia = media.filter((_image, i) => i !== index);
setMedia(newMedia);
}
console.log("FILES", media.length);
function onModalClose() {
close();
setMedia([]);
}
function uploadFiles() {
uploadFilesHandler(media);
}
return <>
{/* Preview Images */}
<Modal centered size="calc(100vw - 3rem)" opened={opened} onClose={onModalClose} withCloseButton
scrollAreaComponent={ScrollArea.Autosize}
>
<div className="px-8">
<Grid gutter={"md"} grow>
{media.map((image, i) => {
const url = URL.createObjectURL(image);
return <Grid.Col key={i} span={4} className="mx-2 relative">
<Image radius={"lg"} height={300} fit="fill" src={url} alt="" className="relative">
</Image>
<IconX
size={30}
onClick={() => handleRemove(i)} className="absolute cursor-pointer top-2 right-4 z-40" color="#646d75" />
</Grid.Col>
})}
</Grid>
<Group position="right" className="pr-3 py-6">
<Button onClick={uploadFiles} variant="outline" color="indigo" size="md">
Upload
</Button>
</Group>
</div>
</Modal>
<UnstyledButton onClick={onFileInputClick}>
<IconPaperclip className="text-[#646d75] cursor-pointer" />
<input ref={fileInputRef} onChange={onFileChange} multiple accept="image/*" type="file" className="hidden" />
</UnstyledButton>
</>
}import { Button, Grid, Group, Image, Modal, ScrollArea, UnstyledButton } from "@mantine/core"
import { ChangeEvent, useRef, useState } from "react";
import { IconPaperclip, IconX} from '@tabler/icons-react';
import { useDisclosure } from "@mantine/hooks";
type Props = {
uploadFilesHandler: (files: File[]) => void;
}
export function ChatFileUploadComponent({ uploadFilesHandler }: Props) {
const fileInputRef = useRef<HTMLInputElement>(null);
const [media, setMedia] = useState<File[]>([]);
const [opened, { open, close }] = useDisclosure(false);
function onFileInputClick() {
if (fileInputRef.current) {
fileInputRef.current.click();
}
}
function onFileChange(e: ChangeEvent<HTMLInputElement>) {
const files = e.target.files;
const elements = [];
if (files && files.length > 0) {
for (let index = 0; index < files.length; index++) {
const file = files[index];
elements.push(file);
}
setMedia(elements);
open();
}
}
function handleRemove(index: number) {
const newMedia = media.filter((_image, i) => i !== index);
setMedia(newMedia);
}
console.log("FILES", media.length);
function onModalClose() {
close();
setMedia([]);
}
function uploadFiles() {
uploadFilesHandler(media);
}
return <>
{/* Preview Images */}
<Modal centered size="calc(100vw - 3rem)" opened={opened} onClose={onModalClose} withCloseButton
scrollAreaComponent={ScrollArea.Autosize}
>
<div className="px-8">
<Grid gutter={"md"} grow>
{media.map((image, i) => {
const url = URL.createObjectURL(image);
return <Grid.Col key={i} span={4} className="mx-2 relative">
<Image radius={"lg"} height={300} fit="fill" src={url} alt="" className="relative">
</Image>
<IconX
size={30}
onClick={() => handleRemove(i)} className="absolute cursor-pointer top-2 right-4 z-40" color="#646d75" />
</Grid.Col>
})}
</Grid>
<Group position="right" className="pr-3 py-6">
<Button onClick={uploadFiles} variant="outline" color="indigo" size="md">
Upload
</Button>
</Group>
</div>
</Modal>
<UnstyledButton onClick={onFileInputClick}>
<IconPaperclip className="text-[#646d75] cursor-pointer" />
<input ref={fileInputRef} onChange={onFileChange} multiple accept="image/*" type="file" className="hidden" />
</UnstyledButton>
</>
}- This is a React component called
ChatFileUploadComponentwhich renders a button with a paperclip icon for attaching files to a chat message. When the button is clicked, a file input dialog is opened where the user can select one or more image files. Once the user selects files, the component shows a preview of the selected images in a modal dialog. The preview images can be removed and when the user clicks on the "Upload" button, the selected files are uploaded by calling theuploadFilesHandlerfunction. - Here's a brief overview of what the code does:
- Import necessary components and hooks from the
@mantine/coreand@mantine/hookspackages, and icons from@tabler/icons-react. - Define the type of props that the component expects: an
uploadFilesHandlerfunction that takes an array ofFileobjects. - Create a reference to an HTML input element using the
useRefhook. - Define a state variable
mediato store the selected image files, initialized as an empty array. - Use the
useDisclosurehook to create a state variableopenedand functionsopenandcloseto manage the state of the modal dialog. - Define some utility functions to handle user interactions:
onFileInputClickopens the file input dialog,onFileChangeis called when the user selects files and sets the selected files to themediastate, andhandleRemoveremoves a selected file from themediastate. - Render the component using the Mantine UI components and styles. The
UnstyledButtoncomponent wraps the paperclip icon and aninputelement. Clicking on the paperclip icon triggers the file input dialog to open. When a user selects one or more files, themediastate is updated, and the preview modal is opened using theModalcomponent. TheGridandImagecomponents are used to display the preview images, and theIconXcomponent is used to provide a delete button for each image. Finally, the "Upload" button is rendered using theButtoncomponent, which triggers theuploadFilesHandlerfunction when clicked.
Back to ChatForm.tsx that is located inside /page-components/chats-page/chat.
import { EmojiPicker } from "@/components/EmojiPicker"
import { Group, Stack, TextInput, UnstyledButton } from "@mantine/core"
import { FormEvent, useRef, useState } from "react"
import { IconMoodSmile, IconPaperclip, IconMicrophone } from '@tabler/icons-react';
import { useClickOutside } from "@mantine/hooks";
import { EmojiClickData } from "emoji-picker-react";
import { ChatFileUploadComponent } from "./ChatFileUpload";
type Props = {
}
export function ChatForm({ }: Props) {
const [showEmoji, setShowEmoji] = useState(false);
const emojiRef = useClickOutside(() => setShowEmoji(false));
const [message, setMessage] = useState("");
const messageRef = useRef<HTMLInputElement>(null);
function handleSubmit(e: FormEvent) {
e.preventDefault();
console.log("MESSAGE", message)
}
function onEmojiPicker(emoji: EmojiClickData, _event: MouseEvent) {
if (messageRef.current) {
console.log(emoji.unified);
const { selectionStart, selectionEnd } = messageRef.current;
const newValue = message.slice(0, selectionStart!) + emoji.emoji + message.slice(selectionEnd!);
setMessage(newValue);
}
}
function uploadFilesHandler(files: File[]) {
console.log("GOT IMAGES", files)
}
return <Stack justify="center" className="absolute px-8 border w-full bottom-0 h-[85px]">
<Group className="relative">
{/* Emoji */}
{showEmoji && <EmojiPicker onEmojiPicker={onEmojiPicker} emojiRef={emojiRef} />}
<UnstyledButton onClick={() => setShowEmoji(true)}>
<IconMoodSmile className="text-[#646d75] cursor-pointer" />
</UnstyledButton>
{/* File Input */}
<ChatFileUploadComponent uploadFilesHandler={uploadFilesHandler} />
{/* Text Input */}
<form className="grow" onSubmit={handleSubmit}>
<TextInput
placeholder="Type your message here..."
size="md"
ref={messageRef}
value={message}
onChange={e => setMessage(e.target.value)}
/>
</form>
{/* Recording */}
<UnstyledButton>
<IconMicrophone className="text-[#646d75] cursor-pointer" />
</UnstyledButton>
</Group>
</Stack>
}import { EmojiPicker } from "@/components/EmojiPicker"
import { Group, Stack, TextInput, UnstyledButton } from "@mantine/core"
import { FormEvent, useRef, useState } from "react"
import { IconMoodSmile, IconPaperclip, IconMicrophone } from '@tabler/icons-react';
import { useClickOutside } from "@mantine/hooks";
import { EmojiClickData } from "emoji-picker-react";
import { ChatFileUploadComponent } from "./ChatFileUpload";
type Props = {
}
export function ChatForm({ }: Props) {
const [showEmoji, setShowEmoji] = useState(false);
const emojiRef = useClickOutside(() => setShowEmoji(false));
const [message, setMessage] = useState("");
const messageRef = useRef<HTMLInputElement>(null);
function handleSubmit(e: FormEvent) {
e.preventDefault();
console.log("MESSAGE", message)
}
function onEmojiPicker(emoji: EmojiClickData, _event: MouseEvent) {
if (messageRef.current) {
console.log(emoji.unified);
const { selectionStart, selectionEnd } = messageRef.current;
const newValue = message.slice(0, selectionStart!) + emoji.emoji + message.slice(selectionEnd!);
setMessage(newValue);
}
}
function uploadFilesHandler(files: File[]) {
console.log("GOT IMAGES", files)
}
return <Stack justify="center" className="absolute px-8 border w-full bottom-0 h-[85px]">
<Group className="relative">
{/* Emoji */}
{showEmoji && <EmojiPicker onEmojiPicker={onEmojiPicker} emojiRef={emojiRef} />}
<UnstyledButton onClick={() => setShowEmoji(true)}>
<IconMoodSmile className="text-[#646d75] cursor-pointer" />
</UnstyledButton>
{/* File Input */}
<ChatFileUploadComponent uploadFilesHandler={uploadFilesHandler} />
{/* Text Input */}
<form className="grow" onSubmit={handleSubmit}>
<TextInput
placeholder="Type your message here..."
size="md"
ref={messageRef}
value={message}
onChange={e => setMessage(e.target.value)}
/>
</form>
{/* Recording */}
<UnstyledButton>
<IconMicrophone className="text-[#646d75] cursor-pointer" />
</UnstyledButton>
</Group>
</Stack>
}- The
ChatFormcomponent is a form for sending messages in a chat. It includes an input field for typing messages, an emoji picker, a file upload component, and a microphone button for recording voice messages. - The
ChatFileUploadComponentis a reusable component used for uploading files in the chat. It includes a file input button and a preview of the uploaded files. When theUploadbutton is clicked, it invokes auploadFilesHandlerfunction that is passed as a prop from the parent component (ChatForm). - The
uploadFilesHandlerfunction receives an array of files and logs them to the console. In the context of theChatFormthis function can be used to send the uploaded files to the server.
Recording Voice Messages:
Create a ChatRecording.tsx file inside /page-components/chats-page/chat.
import { UnstyledButton } from "@mantine/core";
import { IconMicrophone } from '@tabler/icons-react';
export function ChatRecordingComponent() {
return <>
<UnstyledButton>
<IconMicrophone className="text-[#646d75] cursor-pointer" />
</UnstyledButton>
</>
}import { UnstyledButton } from "@mantine/core";
import { IconMicrophone } from '@tabler/icons-react';
export function ChatRecordingComponent() {
return <>
<UnstyledButton>
<IconMicrophone className="text-[#646d75] cursor-pointer" />
</UnstyledButton>
</>
}- This code defines a React functional component called
ChatRecordingComponent, which renders a button with a microphone icon using theUnstyledButtoncomponent from the@mantine/corelibrary and theIconMicrophonecomponent from the@tabler/icons-reactlibrary. - When the button is clicked, nothing happens as there is no event handler defined. This component is likely intended to be used in conjunction with other components to implement some sort of voice recording functionality within a chat application.
Update the ChatForm.tsx file located inside page-components/chats-page/chat
import { EmojiPicker } from "@/components/EmojiPicker"
import { Group, Stack, TextInput, UnstyledButton } from "@mantine/core"
import { FormEvent, useRef, useState } from "react"
import { IconMoodSmile } from '@tabler/icons-react';
import { useClickOutside } from "@mantine/hooks";
import { EmojiClickData } from "emoji-picker-react";
import { ChatFileUploadComponent } from "./ChatFileUpload";
import { ChatRecordingComponent } from "./ChatRecording";
type Props = {
}
export function ChatForm({ }: Props) {
const [showEmoji, setShowEmoji] = useState(false);
const emojiRef = useClickOutside(() => setShowEmoji(false));
const [message, setMessage] = useState("");
const messageRef = useRef<HTMLInputElement>(null);
function handleSubmit(e: FormEvent) {
e.preventDefault();
console.log("MESSAGE", message)
}
function onEmojiPicker(emoji: EmojiClickData, _event: MouseEvent) {
if (messageRef.current) {
console.log(emoji.unified);
const { selectionStart, selectionEnd } = messageRef.current;
const newValue = message.slice(0, selectionStart!) + emoji.emoji + message.slice(selectionEnd!);
setMessage(newValue);
}
}
function uploadFilesHandler(files: File[]) {
console.log("GOT IMAGES", files)
}
return <Stack justify="center" className="absolute px-8 border w-full bottom-0 h-[85px]">
<Group className="relative">
{/* Emoji */}
{showEmoji && <EmojiPicker onEmojiPicker={onEmojiPicker} emojiRef={emojiRef} />}
<UnstyledButton onClick={() => setShowEmoji(true)}>
<IconMoodSmile className="text-[#646d75] cursor-pointer" />
</UnstyledButton>
{/* File Input */}
<ChatFileUploadComponent uploadFilesHandler={uploadFilesHandler} />
{/* Text Input */}
<form className="grow" onSubmit={handleSubmit}>
<TextInput
placeholder="Type your message here..."
size="md"
ref={messageRef}
value={message}
onChange={e => setMessage(e.target.value)}
/>
</form>
{/* Recording */}
<ChatRecordingComponent />
</Group>
</Stack>
}import { EmojiPicker } from "@/components/EmojiPicker"
import { Group, Stack, TextInput, UnstyledButton } from "@mantine/core"
import { FormEvent, useRef, useState } from "react"
import { IconMoodSmile } from '@tabler/icons-react';
import { useClickOutside } from "@mantine/hooks";
import { EmojiClickData } from "emoji-picker-react";
import { ChatFileUploadComponent } from "./ChatFileUpload";
import { ChatRecordingComponent } from "./ChatRecording";
type Props = {
}
export function ChatForm({ }: Props) {
const [showEmoji, setShowEmoji] = useState(false);
const emojiRef = useClickOutside(() => setShowEmoji(false));
const [message, setMessage] = useState("");
const messageRef = useRef<HTMLInputElement>(null);
function handleSubmit(e: FormEvent) {
e.preventDefault();
console.log("MESSAGE", message)
}
function onEmojiPicker(emoji: EmojiClickData, _event: MouseEvent) {
if (messageRef.current) {
console.log(emoji.unified);
const { selectionStart, selectionEnd } = messageRef.current;
const newValue = message.slice(0, selectionStart!) + emoji.emoji + message.slice(selectionEnd!);
setMessage(newValue);
}
}
function uploadFilesHandler(files: File[]) {
console.log("GOT IMAGES", files)
}
return <Stack justify="center" className="absolute px-8 border w-full bottom-0 h-[85px]">
<Group className="relative">
{/* Emoji */}
{showEmoji && <EmojiPicker onEmojiPicker={onEmojiPicker} emojiRef={emojiRef} />}
<UnstyledButton onClick={() => setShowEmoji(true)}>
<IconMoodSmile className="text-[#646d75] cursor-pointer" />
</UnstyledButton>
{/* File Input */}
<ChatFileUploadComponent uploadFilesHandler={uploadFilesHandler} />
{/* Text Input */}
<form className="grow" onSubmit={handleSubmit}>
<TextInput
placeholder="Type your message here..."
size="md"
ref={messageRef}
value={message}
onChange={e => setMessage(e.target.value)}
/>
</form>
{/* Recording */}
<ChatRecordingComponent />
</Group>
</Stack>
}
Create a hooks folder at the root of the front end and then a file named useAudioRecorder.ts.
import { useEffect, useRef, useState } from 'react';
type Action = () => void;
export type AudioData = {
blob: Blob;
url: string;
chunks: Blob[];
};
export type RecorderProps = {
stop: Action;
start: Action;
pause: Action;
reset: Action;
resume: Action;
data: AudioData;
paused: boolean;
recording: boolean;
hasRecorder: boolean;
};
export type State = {
seconds: number;
audioBlob: Blob;
paused: boolean;
recording: boolean;
audioData: AudioData;
medianotFound: boolean;
};
const emptyBlob = new Blob() || '';
const initState: State = {
seconds: 0,
recording: false,
paused: false,
medianotFound: false,
audioBlob: emptyBlob,
audioData: {
url: '',
chunks: [],
blob: emptyBlob,
},
};
let timer!: any;
let chunks: Blob[] = [];
let mediaRecorder!: MediaRecorder;
type Props = {
mimeTypeToUseWhenRecording?: string;
};
export function useRecorder(props?: Props) {
const [, sF] = useState({});
const dataRef = useRef({ ...initState });
const { paused, recording, medianotFound, audioData, } = dataRef.current;
const updatState = () => sF({});
const [stream, setStream] = useState<MediaStream | null>();
useEffect(() => {
if (typeof window !== "undefined") {
setStream(new MediaStream());
}
}, [])
const initRecorder = async () => {
// @ts-ignore
navigator.getUserMedia =
// @ts-ignore
navigator.getUserMedia ||
// @ts-ignore
navigator.msGetUserMedia ||
// @ts-ignore
navigator.mozGetUserMedia ||
// @ts-ignore
navigator.webkitGetUserMedia;
if (navigator.mediaDevices) {
const _stream = await navigator.mediaDevices.getUserMedia({ audio: true });
setStream(_stream)
if (props) {
const { mimeTypeToUseWhenRecording = '' } = props;
mediaRecorder = new MediaRecorder(_stream, {
mimeType: mimeTypeToUseWhenRecording,
});
} else {
mediaRecorder = new MediaRecorder(_stream);
}
chunks = [];
mediaRecorder.ondataavailable = (e) => {
if (e.data && e.data.size > 0) {
chunks.push(e.data);
}
};
return true;
} else {
dataRef.current = {
...dataRef.current,
medianotFound: true,
};
updatState();
return false;
}
};
const handleAudioPause = () => {
if (!paused) {
clearInterval(timer);
mediaRecorder.pause();
dataRef.current = {
...dataRef.current,
paused: true,
};
updatState();
}
};
const handleAudioStart = () => {
if (paused) {
startTimer();
mediaRecorder.resume();
dataRef.current = {
...dataRef.current,
paused: false,
};
updatState();
}
};
const countDown = () => {
let seconds = dataRef.current.seconds + 1;
dataRef.current = {
...dataRef.current,
seconds,
};
updatState();
};
const startTimer = () => {
timer = setInterval(countDown, 1000);
};
const startRecording = async () => {
if (!recording) {
const isReady = await initRecorder();
if (isReady) {
chunks = [];
mediaRecorder.start(10);
startTimer();
dataRef.current = {
...dataRef.current,
recording: true,
};
updatState();
}
}
};
const stopRecording = () => {
if (recording) {
clearInterval(timer);
mediaRecorder.stop();
dataRef.current = {
...dataRef.current,
paused: false,
recording: false,
seconds: 0,
};
saveAudio();
stream?.getTracks().forEach(function (track) {
if (track.readyState === 'live') {
track.stop();
}
});
updatState();
}
};
const handleReset = () => {
if (dataRef.current.recording) {
stopRecording();
}
dataRef.current = {
...dataRef.current,
seconds: 0,
recording: false,
medianotFound: false,
audioBlob: emptyBlob,
audioData: initState.audioData,
};
updatState();
};
const saveAudio = () => {
// convert saved chunks to blob
const blob = new Blob(chunks, { type: 'audio/*' });
// generate video url from blob
const audioURL = window.URL.createObjectURL(blob);
// append videoURL to list of saved videos for rendering
dataRef.current = {
...dataRef.current,
audioBlob: blob,
audioData: {
blob: blob,
url: audioURL,
chunks: chunks,
},
};
updatState();
};
return {
paused,
recording,
data: audioData,
reset: handleReset,
stop: stopRecording,
start: startRecording,
pause: handleAudioPause,
resume: handleAudioStart,
hasRecorder: !medianotFound,
};
}import { useEffect, useRef, useState } from 'react';
type Action = () => void;
export type AudioData = {
blob: Blob;
url: string;
chunks: Blob[];
};
export type RecorderProps = {
stop: Action;
start: Action;
pause: Action;
reset: Action;
resume: Action;
data: AudioData;
paused: boolean;
recording: boolean;
hasRecorder: boolean;
};
export type State = {
seconds: number;
audioBlob: Blob;
paused: boolean;
recording: boolean;
audioData: AudioData;
medianotFound: boolean;
};
const emptyBlob = new Blob() || '';
const initState: State = {
seconds: 0,
recording: false,
paused: false,
medianotFound: false,
audioBlob: emptyBlob,
audioData: {
url: '',
chunks: [],
blob: emptyBlob,
},
};
let timer!: any;
let chunks: Blob[] = [];
let mediaRecorder!: MediaRecorder;
type Props = {
mimeTypeToUseWhenRecording?: string;
};
export function useRecorder(props?: Props) {
const [, sF] = useState({});
const dataRef = useRef({ ...initState });
const { paused, recording, medianotFound, audioData, } = dataRef.current;
const updatState = () => sF({});
const [stream, setStream] = useState<MediaStream | null>();
useEffect(() => {
if (typeof window !== "undefined") {
setStream(new MediaStream());
}
}, [])
const initRecorder = async () => {
// @ts-ignore
navigator.getUserMedia =
// @ts-ignore
navigator.getUserMedia ||
// @ts-ignore
navigator.msGetUserMedia ||
// @ts-ignore
navigator.mozGetUserMedia ||
// @ts-ignore
navigator.webkitGetUserMedia;
if (navigator.mediaDevices) {
const _stream = await navigator.mediaDevices.getUserMedia({ audio: true });
setStream(_stream)
if (props) {
const { mimeTypeToUseWhenRecording = '' } = props;
mediaRecorder = new MediaRecorder(_stream, {
mimeType: mimeTypeToUseWhenRecording,
});
} else {
mediaRecorder = new MediaRecorder(_stream);
}
chunks = [];
mediaRecorder.ondataavailable = (e) => {
if (e.data && e.data.size > 0) {
chunks.push(e.data);
}
};
return true;
} else {
dataRef.current = {
...dataRef.current,
medianotFound: true,
};
updatState();
return false;
}
};
const handleAudioPause = () => {
if (!paused) {
clearInterval(timer);
mediaRecorder.pause();
dataRef.current = {
...dataRef.current,
paused: true,
};
updatState();
}
};
const handleAudioStart = () => {
if (paused) {
startTimer();
mediaRecorder.resume();
dataRef.current = {
...dataRef.current,
paused: false,
};
updatState();
}
};
const countDown = () => {
let seconds = dataRef.current.seconds + 1;
dataRef.current = {
...dataRef.current,
seconds,
};
updatState();
};
const startTimer = () => {
timer = setInterval(countDown, 1000);
};
const startRecording = async () => {
if (!recording) {
const isReady = await initRecorder();
if (isReady) {
chunks = [];
mediaRecorder.start(10);
startTimer();
dataRef.current = {
...dataRef.current,
recording: true,
};
updatState();
}
}
};
const stopRecording = () => {
if (recording) {
clearInterval(timer);
mediaRecorder.stop();
dataRef.current = {
...dataRef.current,
paused: false,
recording: false,
seconds: 0,
};
saveAudio();
stream?.getTracks().forEach(function (track) {
if (track.readyState === 'live') {
track.stop();
}
});
updatState();
}
};
const handleReset = () => {
if (dataRef.current.recording) {
stopRecording();
}
dataRef.current = {
...dataRef.current,
seconds: 0,
recording: false,
medianotFound: false,
audioBlob: emptyBlob,
audioData: initState.audioData,
};
updatState();
};
const saveAudio = () => {
// convert saved chunks to blob
const blob = new Blob(chunks, { type: 'audio/*' });
// generate video url from blob
const audioURL = window.URL.createObjectURL(blob);
// append videoURL to list of saved videos for rendering
dataRef.current = {
...dataRef.current,
audioBlob: blob,
audioData: {
blob: blob,
url: audioURL,
chunks: chunks,
},
};
updatState();
};
return {
paused,
recording,
data: audioData,
reset: handleReset,
stop: stopRecording,
start: startRecording,
pause: handleAudioPause,
resume: handleAudioStart,
hasRecorder: !medianotFound,
};
}- This code sets up the initial state and types for a React audio recorder component. It also defines a Props type that accepts an optional
mimeTypeToUseWhenRecordingparameter. - The
useRef,useState, anduseEffecthooks from the React library are imported at the top of the file. Additionally, two types are defined:Action, which is a function that takes no arguments and returns nothing, andAudioData, which represents the data returned by the audio recorder. - The
RecorderPropstype defines the interface for the component's props. It includes functions forstart,stop,pause,reset, andresume, as well as properties fordata,paused,recording, andhasRecorder. - The
Statetype defines the shape of the component's state, including the number ofsecondselapsed, whether a recording ispausedorrecording, whether media access ismedianotFound, and theaudioBlobandaudioDataobjects. - The
emptyBlobconstant creates an emptyBlobobject. TheinitStateconstant initializes the component's state to its default values. - The
timer,chunks, andmediaRecordervariables are defined but not yet initialized. Finally, thePropstype is defined to include an optionalmimeTypeToUseWhenRecordingproperty. - The hook takes an optional
Propsobject as input and returns an object with the following properties: paused: a boolean indicating whether the recording is currently pausedrecording: a boolean indicating whether the recording is currently in progressdata: an object containing information about the recorded audio, including the blob, URL, and chunksreset: a function that stops the recording and resets the recording statestop: a function that stops the recordingstart: a function that starts the recordingpause: a function that pauses the recordingresume: a function that resumes the recordinghasRecorder: a boolean indicating whether the required APIs for recording audio are available
The hook uses useState to manage a state object called dataRef.current that tracks the current state of the recording. It also uses useRef to store an initial state object called initState.
When the hook is called, it initializes the stream state using useState and sets it to null. It then defines an initRecorder function that uses the navigator.mediaDevices.getUserMedia() method to get access to the user's microphone and create a MediaRecorder object. It also sets up an event listener to handle ondataavailable events and collect the recorded audio data.
The hook also defines several helper functions, including handleAudioPause, handleAudioStart, countDown, startTimer, startRecording, stopRecording, handleReset, and saveAudio. These functions are used to handle various aspects of the recording process, such as starting and stopping the timer, starting and stopping the recording, and saving the recorded audio data.
Finally, the hook returns an object containing the state variables and helper functions, along with a boolean indicating whether the required APIs for recording audio are available.
Switch Back to the CharRecording component. Update it with the following code.
import { Center, Group, Loader, Modal, UnstyledButton } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { IconMicrophone, IconTrashFilled, IconUpload, IconCheck } from '@tabler/icons-react';
import { useEffect, useState } from "react";
import { useRecorder } from "@/hooks/useAudioRecorder";
import { notifications } from "@mantine/notifications";
type Props = {
onUploadRecording?: () => void
}
export function ChatRecordingComponent({ }: Props) {
const [opened, { open, close }] = useDisclosure(false);
const [audioUrl, setAudioUrl] = useState("");
const { data, hasRecorder, start: startRecording, stop: stopRecording, reset, recording } = useRecorder()
useEffect(() => {
if (data.url.length > 0)
setAudioUrl(data.url)
}, [data.url])
async function start() {
startRecording().then(() => {
open();
}).catch(() => {
notifications.show({
title: "Microphone",
message: "Error occured while recording. Please try again.",
color: "red"
})
});
}
async function stop() {
stopRecording();
}
async function deleteRecording() {
reset();
close();
}
function uploadRecording() {
console.log("FINAL DATA", data)
close();
}
// console.log(data);
return <>
<Modal opened={opened} onClose={deleteRecording}>
<Center className="mb-3">
{recording && <Loader variant="bars" />}
</Center>
<Center>
<Group>
<audio src={audioUrl} controls />
<UnstyledButton onClick={stop}>
<IconCheck size={30} className="text-gray-400" />
</UnstyledButton>
</Group>
</Center>
<Group position="right" spacing={"md"} className="mt-5">
<UnstyledButton onClick={deleteRecording}>
<IconTrashFilled size={30} className="text-red-400" />
</UnstyledButton>
<UnstyledButton onClick={uploadRecording}>
<IconUpload size={30} className="text-gray-400" />
</UnstyledButton>
</Group>
</Modal>
<UnstyledButton disabled={!hasRecorder} onClick={start} className="relative">
<IconMicrophone className={`text-[#646d75] ${hasRecorder ? "cursor-pointer" : ""}`} />
</UnstyledButton>
</>
}import { Center, Group, Loader, Modal, UnstyledButton } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { IconMicrophone, IconTrashFilled, IconUpload, IconCheck } from '@tabler/icons-react';
import { useEffect, useState } from "react";
import { useRecorder } from "@/hooks/useAudioRecorder";
import { notifications } from "@mantine/notifications";
type Props = {
onUploadRecording?: () => void
}
export function ChatRecordingComponent({ }: Props) {
const [opened, { open, close }] = useDisclosure(false);
const [audioUrl, setAudioUrl] = useState("");
const { data, hasRecorder, start: startRecording, stop: stopRecording, reset, recording } = useRecorder()
useEffect(() => {
if (data.url.length > 0)
setAudioUrl(data.url)
}, [data.url])
async function start() {
startRecording().then(() => {
open();
}).catch(() => {
notifications.show({
title: "Microphone",
message: "Error occured while recording. Please try again.",
color: "red"
})
});
}
async function stop() {
stopRecording();
}
async function deleteRecording() {
reset();
close();
}
function uploadRecording() {
console.log("FINAL DATA", data)
close();
}
// console.log(data);
return <>
<Modal opened={opened} onClose={deleteRecording}>
<Center className="mb-3">
{recording && <Loader variant="bars" />}
</Center>
<Center>
<Group>
<audio src={audioUrl} controls />
<UnstyledButton onClick={stop}>
<IconCheck size={30} className="text-gray-400" />
</UnstyledButton>
</Group>
</Center>
<Group position="right" spacing={"md"} className="mt-5">
<UnstyledButton onClick={deleteRecording}>
<IconTrashFilled size={30} className="text-red-400" />
</UnstyledButton>
<UnstyledButton onClick={uploadRecording}>
<IconUpload size={30} className="text-gray-400" />
</UnstyledButton>
</Group>
</Modal>
<UnstyledButton disabled={!hasRecorder} onClick={start} className="relative">
<IconMicrophone className={`text-[#646d75] ${hasRecorder ? "cursor-pointer" : ""}`} />
</UnstyledButton>
</>
}- This is a React component that implements a chat recording feature. The component uses the Mantine UI library for some of its UI components and Tabler Icons for icons. The component has a prop named
onUploadRecordingthat can be used to handle the uploaded recording. - The component starts with defining the props type as an interface with an optional
onUploadRecordingfunction. useDisclosurehook is used to handle the state of the recording modal. Theopenedstate is used to determine whether the modal is open or closed, and theopenandclosefunctions are used to open and close the modal respectively.useStatehook is used to handle the state of the audio URL. Thedata,hasRecorder,start,stop,reset, andrecordingvalues are returned by the customuseRecorderhook that is imported from "@/hooks/useAudioRecorder" and is used to handle the audio recording logic.- An
useEffecthook is used to set the audio URL when thedata.urlvalue changes. Thestartfunction is used to start the recording and open the modal when the recording is started. If an error occurs while starting the recording, a notification is displayed. Thestopfunction is used to stop the recording, and thedeleteRecordingfunction is used to reset the recording and closes the modal. TheuploadRecordingfunction is used to handle the uploaded recording, which currently just logs the final data object to the console. - The
Modalcomponent from Mantine is used to display the recording UI. A loader is displayed when the recording is in progress. The recorded audio can be played back and the recording can be stopped using the check icon. The trash icon is used to delete the recording, and the upload icon is used to upload the recording. - The
UnstyledButtoncomponent from Mantine is used to display the microphone icon, which is clickable to start recording. If there is no audio recorder available, the microphone icon is disabled.
The next article will be about connecting the backend with the front end to save messages.