Inserting HTML into a Snackbar message - reactjs

So I'm forking off a sample Twilio video chat app (https://github.com/twilio/twilio-video-app-react). For the chat feature, Snackbar messages are employed. Which works fine. But I want to allow the user to send a message starting with http, so that the message will then be sent as a hyperlink URL.
The ChatInput component works fine for displaying this message as a hyperlink URL for the local user (i.e. - the sender). But the DataTrack event handler for the remote users doesn't display the message as a hyperlink. Just displays the literal text.
Here is the ChatInput.tsx, where any message starting with http will show up correctly for the local user.
import React, { useState } from 'react';
import { Button, FormControl, TextField } from '#material-ui/core';
import { useSnackbar } from 'notistack';
import useVideoContext from '../../../hooks/useVideoContext/useVideoContext';
export default function ChatInput() {
const [message, setMessage] = useState('');
const { room } = useVideoContext();
const { enqueueSnackbar } = useSnackbar();
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => setMessage(e.target.value);
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (message) {
// Get the LocalDataTrack that we published to the room.
const [localDataTrackPublication] = [...room.localParticipant.dataTracks.values()];
// Construct a message to send
const fullMessage = `${room.localParticipant.identity} says: ${message}`;
if (message.startsWith('http')) {
// Send the message
localDataTrackPublication.track.send(`${message}`);
// Render the message locally so the local participant can see that their message was sent.
enqueueSnackbar(<a href={message}>{message}</a>);
} else {
// Send the full message
localDataTrackPublication.track.send(fullMessage);
// Render the full message locally so the local participant can see that their message was sent.
enqueueSnackbar(fullMessage);
}
//Reset the text field
setMessage('');
}
};
return (
<form autoComplete="off" style={{ display: 'flex', alignItems: 'center' }} onSubmit={handleSubmit}>
<FormControl>
<label htmlFor="chat-snack-input" style={{ color: 'black' }}>
Say something:
</label>
<TextField value={message} autoFocus={true} onChange={handleChange} id="chat-snack-input" size="small" />
</FormControl>
<Button type="submit" color="primary" variant="contained" style={{ marginLeft: '0.8em' }}>
Send
</Button>
</form>
);
}
And here is the DataTrack.ts, which only displays the string literal for any remote user.
import { useEffect } from 'react';
import { DataTrack as IDataTrack } from 'twilio-video';
import { useSnackbar } from 'notistack';
var stringToHTML = function (str) {
var parser = new DOMParser();
var doc = parser.parseFromString(str, 'text/html');
return doc.body;
};
export default function DataTrack({ track }: { track: IDataTrack }) {
const { enqueueSnackbar } = useSnackbar();
useEffect(() => {
const handleMessage = (message: string) => {
if (message.startsWith('http')) {
const newMessage = stringToHTML(message);
enqueueSnackbar(newMessage);
}
else {
enqueueSnackbar(message); }
};
track.on('message', handleMessage);
return () => {
track.off('message', handleMessage);
};
}, [track, enqueueSnackbar]);
return null; // This component does not return any HTML, so we will return 'null' instead.
}
Any suggestions as to how I can get the remote users to receive the same hyperlink URL that the sender sees?

TL;DR: the function stringToHTML is returnign a DOM element reference not a React element when passing the message to NotiStack, try wrapping it with a React element:
//const newMessage = stringToHTML(message);
enqueueSnackbar(<div dangerouslySetInnerHTML={{__html:message}} />);
NL;PR: And/Or I'm not sure why your are passing the message value to NotiStack differently in the two components:
if (message.startsWith('http')) {
//local user
enqueueSnackbar(<a href={message}>{message}</a>); //1)
//vs. remote user
const newMessage = stringToHTML(message);
enqueueSnackbar(newMessage); // should it be the same as 1)?

Appreciate the feedback. I was handling the message two different ways depending if a URL was being passed along or not. Actually the project author actually provided a clean solution. Installing the Linkify package allows just those string elements inferred to be HTML to be formatted as such.
Here is the reworked DataTrack.tsx contents. Works like a champ!
import React from 'react';
import { useEffect } from 'react';
import { DataTrack as IDataTrack } from 'twilio-video';
import { useSnackbar } from 'notistack';
import Linkify from 'react-linkify';
export default function DataTrack({ track }: { track: IDataTrack }) {
const { enqueueSnackbar } = useSnackbar();
useEffect(() => {
const handleMessage = (message: string) =>
enqueueSnackbar(
<Linkify
componentDecorator={(decoratedHref, decoratedText, key) => (
<a target="_blank" rel="noopener" href={decoratedHref} key={key}>
{decoratedText}
</a>
)}
>
{message}
</Linkify>
);
track.on('message', handleMessage);
return () => {
track.off('message', handleMessage);
};
}, [track, enqueueSnackbar]);
return null; // This component does not return any HTML, so we will return 'null' instead.
}

Related

Component is changing a controlled input to be uncontrolled

I'm building a site useing graphql and apollo with a react front end. I have created a page where the site admin can update the content for particular sections of pages and it works, but I keep getting the error in the console: Component is changing a controlled input to be uncontrolled...
I'm also using ReactQuill wysiwyg editor. I thought that might be the problem but I remved it and I'm still getting the same error.
Here is the code for the content update page:
import { useState, useEffect } from 'react';
import { useMutation, useQuery } from '#apollo/client';
import { useNavigate, useParams } from 'react-router-dom';
import { GET_CONTENT_BY_ID } from '../../utils/queries';
import { UPDATE_CONTENT } from '../../utils/mutations';
import Form from 'react-bootstrap/Form';
import Button from 'react-bootstrap/Button';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
const Content = () => {
const { id } = useParams();
const { loading, data } = useQuery(GET_CONTENT_BY_ID, {
variables: { id: id },
});
const [contentHead, setContentHead] = useState('');
const [contentText, setContentText] = useState('');
useEffect(() => {
const content = data?.contentById || {};
if (content) {
setContentHead(content.contentHead);
setContentText(content.contentText);
}
}, [data, setContentHead, setContentText]);
const [updateContent, { error }] = useMutation(UPDATE_CONTENT);
const navigate = useNavigate();
const submitHandler = async (e) => {
e.preventDefault();
try {
const { data } = await updateContent({
variables: {
id,
contentHead,
contentText,
},
});
navigate('/_admin');
} catch (error) {
console.log(error);
}
};
return (
<Form onSubmit={submitHandler}>
<Form.Group className="mb-3" controlId="contentHead">
<Form.Label>Section Heading</Form.Label>
<Form.Control
value={contentHead}
onChange={(e) => setContentHead(e.target.value)}
required
/>
</Form.Group>
<Form.Group className="mb-3" controlId="contentText">
<Form.Label>Text</Form.Label>
<ReactQuill
name="contentText"
value={contentText}
theme="snow"
onChange={setContentText}
/>
</Form.Group>
<Button type="submit">Submit</Button>
</Form>
);
};
export default Content;
In the ReqctQuill I tried onChange={(e) => contentText(e.target.value)} but there wasn't any change. The way it is now is what I got from, I think, their git repository.
I found an answer in another question on here. It wasn't the accepted answer but it works to get ride of the error.
A component is changing an uncontrolled input of type text to be controlled error in ReactJS
So for my form I change value={contentHead} and value={contentText} to value={contentHead || ''} and value={contentText || ''} and that works and gets rid of the error!

database get request output to display in return text- react native

We are trying to fetch the name value from the local database and storing it as tlName variable, we want to display the dynamic value of tlName in return function.
Currently, we can see the values in console.log but not in the emulator app screen and getting an error. How to resolve this issue?
import React , {useState} from 'react';
import { Text } from 'react-native';
//import { View } from 'react-native-web';
import logo from '../assets/Logo3.jpg';
import { StatusBar } from 'expo-status-bar';
import {
InnerContainer, PageLogo,
PageTitle,
SubTitle,
StyledFormArea,
StyledButton, ButtonText,
Line,
WelcomeContainer, WelcomeImage, Avatar
} from '../components/Styles';
import axios from 'axios';
const tlName = null;
const name = null;
const Dummy2 = ({navigation}) => {
/**************Test for get request */
setTimeout(() => {
console.log("testing get request")
const url = 'http://10.1.46.41:8090/api/users/';
axios
.get(url)
.then((response) => {
const result = response.data;
console.log(result[3].Name)
name= result[3].Name;
console.log(name)
tlName = name;
}).catch(error => {
console.log(error.JSON);
setSubmitting(false);
handleMesssage("An error occured. Check your network and try again");
});
}, 5000)
return(
<>
<StatusBar style = "light"/>
<InnerContainer>
<WelcomeImage source={require('../assets/Logo3.jpg')}/>
<WelcomeContainer>
<PageTitle welcome = {true}> Trial for getting data </PageTitle>
<SubTitle welcome = {true}>Your request has been sent to the Team lead of {tlName}</SubTitle>
<SubTitle welcome = {true}>Kindly await their response</SubTitle>
<StyledFormArea>
<Avatar source={require('../assets/Logo3.jpg')} />
<StyledButton onPress = {() => {navigation.navigate('Welcome')}}>
<ButtonText> Okay </ButtonText>
</StyledButton>
<Line />
</StyledFormArea>
</WelcomeContainer>
</InnerContainer>
</>
);
};
export default Dummy2;

React Typescript Video Preview

I'm trying to create a video preview for a internal project, with "React & Typescript" using react hooks below is the component code,
import React, { useEffect, useRef, useState } from 'react';
import { INewVideo } from 'src/models';
import { useForm } from 'react-hook-form';
const NewVideo: React.FC = () => {
const { register, handleSubmit } = useForm<INewVideo>();
const [file, setFile] = useState<any>();
const videoChangeHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log(event.currentTarget.files![0]);
setFile(event.currentTarget.files![0])
};
useEffect(() => {
console.log("use effect", file)
}, [file])
return (<div>
<input
accept="video/mp4, video/mov"
onChange={videoChangeHandler}
type="file"
/>
{
file ? (
<div>
{file}
</div>
) : ("No Video")
}
</div>)
};
export default NewVideo;
But I'm not able to set the file, its throwing below error
I need to render upload video & give options for screen capture & trimming features. But these are later stages
You are getting this error because file is not a JSX.Element which you are trying to render in your DOM. Basically you got some Object in your file state. Either you can provide this as a source for HTML.Video Element or you can get file object data from it.
{
file ? <div> {file.name}</div> : "No Video";
}
This code should print the file name in your screen. This is the main place where you are getting some error.
Or if you want to show the preview of your recent upload video you can simply pass that file object as a HTML.Video src. Like it:
{
file ? <div> <video src={URL.createObjectURL(file)} autoPlay /></div> : "No Video";
}
This will show the preview of your video.
I've found below
import React, { useEffect, useState } from 'react';
import { INewVideo } from 'src/models';
import { useForm } from 'react-hook-form';
const NewVideo: React.FC = () => {
const { register } = useForm<INewVideo>();
const [file, setFile] = useState<any>();
const videoChangeHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.currentTarget.files![0];
console.log("File", file);
const reader = new FileReader();
reader.addEventListener("load", () => {
setFile(reader.result);
});
reader.readAsDataURL(event.target.files![0]);
};
useEffect(() => {
console.log("use effect", file)
}, [file])
return (<div>
<input
{...register("Link")}
accept="video/mp4, video/mov"
onChange={videoChangeHandler}
type="file"
/>
<video controls src={file} />
</div>)
};
export default NewVideo;

onClickHandler sometimes work, sometimes not - React

The onClickHandler in the following code, in this component, 'SearchResult', sometimes work and sometimes not.
I can't figure out any logic that can explain why it works when it works, and why it's not working, when it's not working.
I've put a debugger inside the onClickHandler, at the beginning of it, and when it's not working, it doesn't get to the debugger at all - what indicates that the function sometimes isn't even called, and I can't figure out why.
Furthermore, I've tried to move all the code in function to the onClick, inline, but then, it's not working at all.
In addition, I've tried to use a function declaration instead of an arrow function, and it still behaves the same - sometimes it works, and sometimes it's not...
This is the site, you can see the behavior for yourself, in the search box.
This is the GitHub repository
Here you can see a video demonstrating how it's not working, except for one time it did work
Please help.
The problematic component:
import { useDispatch } from 'react-redux'
import { Col } from 'react-bootstrap'
import { getWeatherRequest } from '../redux/weather/weatherActions'
import { GENERAL_RESET } from '../redux/general/generalConstants'
const SearchResult = ({ Key, LocalizedName, setText }) => {
const dispatch = useDispatch()
const onClickHandler = () => {
dispatch({ type: GENERAL_RESET })
dispatch(
getWeatherRequest({
location: Key,
cityName: LocalizedName,
})
)
setText('')
}
return (
<Col className='suggestion' onClick={onClickHandler}>
{LocalizedName}
</Col>
)
}
export default SearchResult
This is the parent component:
import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Form } from 'react-bootstrap'
import { getAutoCompleteResultsRequest } from '../redux/autoComplete/autoCompleteActions'
import { AUTO_COMPLETE_RESET } from '../redux/autoComplete/autoCompleteConstants'
import SearchResult from './SearchResult'
const SearchBox = () => {
const [text, setText] = useState('')
const dispatch = useDispatch()
const autoComplete = useSelector((state) => state.autoComplete)
const { results } = autoComplete
const onChangeHandler = (e) => {
if (e.target.value === '') {
dispatch({ type: AUTO_COMPLETE_RESET })
setText('')
}
setText(e.target.value)
dispatch(getAutoCompleteResultsRequest(e.target.value))
}
const onBlurHandler = () => {
setTimeout(() => {
dispatch({ type: AUTO_COMPLETE_RESET })
setText('')
}, 100)
}
return (
<div className='search-box'>
<Form inline>
<div className='input-group search-md search-sm'>
<input
type='search'
name='q'
value={text}
onChange={onChangeHandler}
onBlur={onBlurHandler}
placeholder='Search Location...'
className='mr-sm-2 ml-sm-3 form-control'
/>
</div>
</Form>
<div className='search-results'>
{results &&
results.map((result) => {
return (
<SearchResult key={result.Key} {...result} setText={setText} />
)
})}
</div>
</div>
)
}
export default SearchBox
I played a bit with your code and it looks like a possible solution may be the following addition in the SearchResult.js:
const onClickHandler = (e) => {
e.preventDefault();
...
After some tests
Please remove the onBlurHandler. It seams to fire ahaed of the onClickHandler of the result.
Can you put console.log(e.target.value) inside the onChangeHandler,
press again search results and make sure that one of it doesn't working and show us the console.
In searchResult component print to the console LocalizedName as well

How to send radio button's value into state and change the value when another radio button is clicked

I'm trying to push the value of selected radio button in component into the state on the page, such that when another radio button is selected, it updates the value in the state with the newly selected value.
The problem is, whenever I change the selected radio button, it is adding a new value instead of updating it.
This is my page
import React, { useContext, useState, useEffect } from 'react';
import { Form, Button, Popconfirm, Icon, Input, Radio, Collapse, Checkbox } from 'antd';
import Axios from 'axios';
import CompanyContext from '../util/UserContext';
import renderEmpty from 'antd/lib/config-provider/renderEmpty';
import InputOnly from '../components/InputOnly';
import CustomInput from '../components/Input';
import Lampiran from '../components/Lampiran';
import CustomCheckBox from '../components/CheckBox';
export default function Hitung() {
let [loading, setLoading] = useState(false);
let [form, setForm] = useState([]);
let [soal, setSoal] = useState([]);
let [pilihan, setPilihan] = useState([]);
let [test, setTest] = useState([1, 2, 3, 4, 5]);
let [answer, setAnswer] = useState("");
let [coba, setCoba] = useState([]);
let [coba2, setCoba2] = useState([]);
const company = useContext(CompanyContext);
let answertemp = []
useEffect(() => {
async function getData() {
try {
let data = await Axios.get('http://localhost:5000/lelang/getquestion');
setSoal(data.data.pertanyaan);
}
catch (e) {
console.log(e);
}
}
getData();
}, []);
function onAnswer(data) {
answertemp.push(data);
console.log(answertemp);
}
function RenderQuestion() {
if (soal.length != 0) {
return soal.map(data => {
switch (data.type_question) {
case "input":
return (<CustomInput data={data} onAnswer={onAnswer} />)
case "input_only":
return (<InputOnly data={data} onAnswer={answer => {
if(coba.length!=0){
for(let i =0;i<coba.length;i++){
if (coba[i].id_question===answer.id_question){
coba[i].answer=answer.answer;
break;
}
else if (i<coba.length-1){
continue;
}
else{
setCoba([...coba,answer])
}
}
}
else{
console.log('add');
setCoba([...coba,answer])
}
}} />)
case "lampiran":
return (<Lampiran data={data} onAnswer={onAnswer} />)
case "checkbox":
return (<CustomCheckBox data={data} onAnswer={onAnswer} />)
}
})
}
else {
return (<h2 style={{ textAlign: "center" }}>Loading....</h2>)
}
}
//insert_answer
async function submit(e) {
setAnswer(answertemp)
setLoading(true);
e.preventDefault();
try {
const token = await Axios.post('http://localhost:5000/lelang/insert_answer', company.data);
}
catch (e) {
alert("error " + e)
}
setLoading(false);
}
return (
<div>
<h1 style={{ textAlign: 'center' }}>Input Penilaian {company.data}</h1>
<div style={{ padding: '30px' }}>
<Form>
{RenderQuestion()}
<button onClick={() => console.log(coba)}>Simpan</button>
</Form>
</div>
</div>
);
}
This is the component
import React, { useContext, useState, useEffect } from 'react';
import { Radio, Input, Form } from 'antd';
export default function CustomInput (props){
let[radioInput,setRadioInput]=useState({});
let[chooseInput, setChooseInput] = useState();
useEffect(()=>{
setRadioInput(props.data);
},[]);
return (
<div>
<p style={{fontWeight:"bold"}}>{radioInput.question}</p>
<Form.Item required>
<Radio.Group
options = {radioInput.variable}
onChange={(data) => props.onAnswer(data.target.value)}
onClick={data => props.onAnswer(data.radioInput.value)}
>
</Radio.Group>
</Form.Item>
</div>
)
}
You need to pass dependencies in effect to listen tho changes. When sending empty array in dependencies, hook will happen only on first render. In your case change the use effect to
useEffect(()=>{
setRadioInput(props.data);
},[props.data]);

Resources