I've spun my gears an entire work day trying to figure out how to do this. I have a component that renders a PowerBi embedded report. I have a button on the page that needs to pop open a Bootstrap Modal wrapped component. I can do this normally with state, but due to having a embedded PowerBi report, I cannot change the state (as this will re-render the PowerBI report which takes time, and also resets the report). I have tried using useRef(), to store this variable but obviously it doesn't notify the Modal component of the change.
Thoughts? Any advice?
import React, {useEffect, useRef, useState,useMemo} from 'react';
import ReportManager from '../managers/reportManager';
import SubscribeModel from '../components/subscription/SubscribeModal';
// #ts-ignore
import Report from 'powerbi-report-component';
import {FaChartLine, FaChartBar, FaDesktop, FaPrint, FaMailBulk} from 'react-icons/fa';
import PreloadManager from '../managers/preloadManager';
// #ts-ignore
import LoadingOverlay from 'react-loading-overlay';
import Preloader from '../components/Preloader';
interface ReportConfig {
id : string;
name : string;
webUrl : string;
embedUrl : string; //Embed URL
datasetId : string; //Report ID
embedToken : string; //Embed Token
}
class ReportConfigReal implements ReportConfig {
'id' : string;
'name' : string;
'webUrl' : string;
'embedUrl' : string;
'datasetId' : string;
'embedToken' : string;
'expiration' : string;
}
const Reports = (props : any) => {
const [config,
setConfig] = useState({
id: "",
name: "",
webUrl: "",
embedUrl: "",
embedToken: "",
expiration: ""
});
const [currentUser,
setCurrentUser] = useState(props.currentuser);
const [reportList,
setReportList] = useState(new Array());
const [reportId,
setReportId] = useState(props.match.params.report_id);
const [loading,
setLoading] = useState(false);
const [reportsLoading,
setReportsLoading] = useState(false);
const [report,
setReport] = useState(null);
const [subModal,
setSubModal] = useState(false);
let reportManager : ReportManager = new ReportManager();
let preloadManager : PreloadManager = new PreloadManager();
const currentPage = useRef(null);
const showSubscribe = useRef(null);
const createExpiration = (minutes : number) => {
const date = new Date();
return new Date(date.getTime() + minutes * 60000);
};
const onLoadAndSetTokenListener = (reportFromCallback : any) => {
setTokenExpirationListener(config.expiration, 1);
setReport(reportFromCallback);
};
const setTokenExpirationListener = (tokenExpiration : string, minutesToRefresh = 2) => {
// time until token refresh in milliseconds
const currentTime = Date.now();
const expiration = Date.parse(tokenExpiration);
const safetyInterval = minutesToRefresh * 60 * 1000;
let timeout = expiration - currentTime - safetyInterval;
// if token already expired, generate new token and set the access token
if (timeout <= 0) {
console.log('Updating Report Embed Token');
updateToken() // set timeout so minutesToRefresh minutes before token expires, token will be
// updated;
} else {
console.log('Report Embed Token will be updated in ' + timeout + ' milliseconds.');
setTimeout(() => {
console.log('Set time out');
updateToken();
}, timeout);
}
};
const updateToken = () => {
console.log('Update token called');
// Generate new EmbedToken (Axios call to get config/ token)
reportManager
.generateEmbedToken(currentUser)
.then((res : any) => {
// Set AccessToken (Use the report object)
if (report !== null) {
report
.setAccessToken(res.embedToken)
.then(() => {
console.log('new token set');
setTokenExpirationListener(createExpiration(2).toString(), 1);
})
.catch((err : any) => console.error('Error setting token', err));
}
});
};
const handleFullScreen = () => {
var elem = document.documentElement;
elem.requestFullscreen();
};
const handlePrint = () => {
window.print();
};
const handleSubscribe = () => {
console.log("page", currentPage.current);
console.log("showsub", showSubscribe);
};
const handleReportChange = (e : any) => {
window.location.href = '/reports/' + e;
};
const handleDataSelected = (data : any) => {
console.log("data selected", data);
};
const handlePageChange = (data : any) => {
console.log("page changed", data);
currentPage.current = data.newPage;
};
useEffect(() => {
let promiseList : any = [];
setLoading(true);
setReportsLoading(true);
if (currentUser && currentUser.allowedReports.length !== 0) {
currentUser
.allowedReports
.forEach((report : string) => {
promiseList.push(reportManager.getReportInfo(currentUser, report).then((info : any) => {
return info;
}),);
});
}
Promise
.all(promiseList)
.then(values => {
setReportList(values);
});
if (currentUser && currentUser.allowedReports.length !== 0 && currentUser.authenticated) {
if (reportId === undefined) {
reportManager
.getReport(currentUser)
.then(data => {
setConfig(data);
setLoading(false);
setReportsLoading(false);
})
.catch(error => {
console.log(error);
});
} else {
reportManager
.getReportInfo(currentUser, reportId)
.then((info : any) => {
setConfig(info);
setLoading(false);
setReportsLoading(false);
});
}
}
}, []);
return (
<LoadingOverlay active={loading} spinner={< Preloader />}>
<div>
<div
className={reportsLoading !== true
? 'slide-wrapper'
: 'col-sm-12 loading-delete'}>
<div id="slide-report">
<div className="menu_icon">
<div>
<FaChartLine color="#003267" size="30"/>
</div>
<span>Reports</span>
</div>
<div className="menu_content">
<ul>
{reportList.map((report, index) => {
return (
<li key={index} onClick={() => handleReportChange(report.id)}>
<FaChartBar className="icon-slide" color="#fff" size="30"/>
<span>{report
.name
.slice(0, 25)}</span>
</li>
);
})}
</ul>
</div>
</div>
<div id="slide-full-screen">
<div className="menu_icon" onClick={handleFullScreen}>
<div>
<FaDesktop color="#003267" size="30"/>
</div>
<span>Full Screen</span>
</div>
</div>
<div id="slide-print">
<div className="menu_icon" onClick={handlePrint}>
<div>
<FaPrint color="#003267" size="30"/>
</div>
<span>Print</span>
</div>
</div>
<div id="slide-subscribe">
<div className="menu_icon" onClick={handleSubscribe}>
<div>
<FaMailBulk color="#003267" size="30"/>
</div>
<span>Subscribe</span>
</div>
</div>
</div>
<div
key={reportId}
className={loading !== true
? 'col-sm-12'
: 'col-sm-12 loading-delete'}>
<Report
className={reportId}
embedType="report"
tokenType="Embed"
pageName={reportId}
accessToken={config.embedToken}
embedUrl={config.embedUrl}
embedId={config.id}
permissions="All"
reportMode="view"
onLoad={onLoadAndSetTokenListener}
onPageChange={handlePageChange}
onSelectData={handleDataSelected}
style={{
height: '90vh',
width: '100%',
display: 'block',
top: '0',
left: '0',
position: 'absolute',
border: '0',
padding: '20px',
background: '#fff',
zIndex: '1'
}}/>
</div>
</div>
<SubscribeModel show={false} />
</LoadingOverlay>
);
};
export default Reports;
This looks like the wrong way to go about doing it but this will open a modal without having to update state. Have you considered looking into componentShouldUpdate? The powerBiReport should also not be re-rendered unless any props that are passed into it have changed or state within the report itself has changed so consider looking there.
/* -- snip -- */
onButtonClick={() => createModal(<YourModal onClose={removeModal}/>)}
/* -- snip -- */
const createModal(component) {
const container = document.appendChild("div");
container.id = "modal-container";
ReactDOM.render(container, component);
}
const removeModal() {
const container = document.getElementById("modal-container");
ReactDOM.unmountComponentAtNode(container);
}
Related
I have a problem that children component does not update hidden status when project is selected, it should then display all the the tasks included in selected projects. How ever when getTasks is done and it updates hidden state to false and it passes state to children component props but children component never reintialize select component and remains hidden. What I need to change to make my selectbox class RtSelect to display hidden state changes?
My master component:
import React, { useState, useEffect, useRef } from 'react';
import RtSelect from './RtSelect';
import api, { route } from "#forge/api";
function Projects() {
const projRef = useRef();
const taskRef = useRef();
const [projects, setProjects] = useState(undefined)
const [tasks, setTasks] = useState(undefined)
const [projectid, setProjectid] = useState(undefined)
const [taskid, setTaskid] = useState(undefined)
const [hidden, setHidden] = useState(true)
//haetaan atlasiansita projectit array
useEffect(() => {
let loadedProject = true;
// declare the async data fetching function
const fetchProjects = async () => {
// get the data from the api
const response = await api.asUser().requestJira(route`/rest/api/3/project`, {
headers: {
'Accept': 'application/json'
}
});
const data = await response.json();
//Mapataa hausta tarvittavat tiedot
const result = data.map(function (item) {
console.log('test');
return [
{
label: item.name,
value: item.id,
avatar: item.avatarUrls['16x16']
}
]
})
// set state with the result if `isSubscribed` is true
if (loadedProject) {
setProjects(result);
}
}
//asetetaan state selectbox muutokselle
// call the function
fetchProjects()
// make sure to catch any error
.catch(console.error);;
// cancel any future `setData`
return () => loadedProject = false;
}, [param])
const getTasks = async (p) => {
// get the data from the api
const response = await api.asUser().requestJira(route`/rest/api/3/issuetype/project?projectId={p}`, {
headers: {
'Accept': 'application/json'
}
});
const data = await response.json();
//Mapataa hausta tarvittavat tiedot
const result = data.map(function (item) {
console.log('test');
return [
{
value: item.id,
label: item.description,
avatar: item.iconUrl
}
]
})
setTasks(result)
setHidden(false)
}
useEffect(() => {
projRef.current.addEventListener("onChange", (e) => {
setProjectid(e.target.value)
console.log("Project select boxin arvo on: " + e.target.value);
getTasks(projectid)
});
});
useEffect(() => {
taskRef.current.addEventListener("onChange", (e) => {
setTaskid(e.target.value)
console.log("Select task boxin arvo on: " + e.target.value);
});
});
return (
<div>
<div className='projects'>
<RtSelect info="Choose project:" options={projects} hidden={false} ref={projRef} />
</div>
<div className='tasks'>
<RtSelect info="Choose Task:" options={tasks} hidden={hidden} ref={taskRef} />
</div>
</div>
);
}
export default Projects
Here is my RtSelect class code:
import React from "react";
import Select from "react-select";
class RtSelect extends React.Component {
state = {
info: this.props.info,
options: this.props.options,
hidden: this.props.hidden,
menuIsOpen: '',
menuWidth: "",
IsCalculatingWidth: ''
};
constructor(props) {
super(props);
this.selectRef = props.ref
this.onMenuOpen = this.onMenuOpen.bind(this);
this.setData = this.setData.bind(this);
}
componentDidMount() {
if (!this.state.menuWidth && !this.state.isCalculatingWidth) {
setTimeout(() => {
this.setState({IsCalculatingWidth: true});
// setIsOpen doesn't trigger onOpenMenu, so calling internal method
this.selectRef.current.select.openMenu();
this.setState({menuIsOpen: true});
}, 1);
}
}
onMenuOpen() {
if (!this.state.menuWidth && this.state.IsCalculatingWidth) {
setTimeout(() => {
const width = this.selectRef.current.select.menuListRef.getBoundingClientRect()
.width;
this.setState({menuWidth: width});
this.setState({IsCalculatingWidth: false});
// setting isMenuOpen to undefined and closing menu
this.selectRef.current.select.onMenuClose();
this.setState({menuIsOpen: undefined});
}, 1);
}
}
styles = {
menu: (css) => ({
...css,
width: "auto",
...(this.state.IsCalculatingWidth && { height: 0, visibility: "hidden" })
}),
control: (css) => ({ ...css, display: "inline-flex " }),
valueContainer: (css) => ({
...css,
...(this.state.menuWidth && { width: this.state.menuWidth })
})
};
setData (props) {
if (props.info) {
this.setState({
info: props.info
})
}
if (props.options) {
this.setState({
options: props.options
})
}
if (props.hidden) {
this.setState({
hidden: props.hidden
})
}
}
render () {
return (
<div style={{ display: "flex" }}>
<div style={{ margin: "8px" }}>{this.state.info}</div>
<div style={{minWidth: "200px"}}>
<Select
ref={this.selectRef}
onMenuOpen={this.onMenuOpen}
options={this.state.options}
menuIsOpen={this.state.menuIsOpen}
styles={this.styles}
isDisabled={this.state.hidden}
formatOptionLabel={(options) => (
<div className="select-option" style={{ display: "flex", menuWidth: "200px"}}>
<div style={{ display: "inline", verticalAlign: "center" }}>
<img src={options.avatar} width="30px" alt="Avatar" />
</div>
<div style={{ display: "inline", marginLeft: "10px" }}>
<span>{options.label}</span>
</div>
</div>
)}
/>
</div>
</div>
);
}
}
export default RtSelect;
Ok I found from other examples that I can use the ref to acces child method so here is they way to update component:
useEffect(() => {
projRef.current.addEventListener("onChange", (e) => {
setProjectid(e.target.value)
console.log("Project select boxin arvo on: " + e.target.value);
getTasks(projectid)
//Using RtSelect taskRef to locate children component method to update component
taskRef.current.setData({hidden: false})
});
});
Good morning. I am trying to integrate PayPal check out in my React app. Everything works fine except that the Paypal Checkout amount is not showing the same as the total cart amount. Checkout amount is fixed at $0.01 and not changing as cart amount changes. Please what could i be doing wrong ??? This is the code
#cart.js code
import React, { useContext, useState, useEffect } from "react";
import { GlobalState } from "../../../GlobalState";
import axios from "axios";
import PayPalButton from "./PaypalButton";
function Cart() {
const state = useContext(GlobalState);
const [cart, setCart] = state.userAPI.cart;
const [token] = state.token;
const [total, setTotal] = useState(0);
useEffect(() => {
const getTotal = () => {
const total = cart.reduce((prev, item) => {
return prev + item.price * item.quantity;
}, 0);
setTotal(total);
};
getTotal();
}, [cart]);
const addToCart = async () => {
await axios.patch(
"/user/addcart",
{ cart },
{
headers: { Authorization: token },
}
);
};
const increment = (id) => {
cart.forEach((item) => {
if (item._id === id) {
item.quantity += 1;
}
});
setCart([...cart]);
addToCart();
};
const decrement = (id) => {
cart.forEach((item) => {
if (item._id === id) {
item.quantity === 1 ? (item.quantity = 1) : (item.quantity -= 1);
}
});
setCart([...cart]);
addToCart();
};
const removeProduct = (id) => {
if (window.confirm("Do you want to delete this product?")) {
cart.forEach((item, index) => {
if (item._id === id) {
cart.splice(index, 1);
}
});
setCart([...cart]);
addToCart();
}
};
const tranSuccess = async (payment) => {
console.log(payment);
};
if (cart.length === 0)
return (
<h2 style={{ textAlign: "center", fontSize: "5rem" }}>Cart Empty</h2>
);
return (
<div>
{cart.map((product) => (
<div className="detail cart" key={product._id}>
<img src={product.images.url} alt="" />
<div className="box-detail">
<h2>{product.title}</h2>
<h3>${product.price * product.quantity}</h3>
<p>{product.description}</p>
<p>{product.content}</p>
<div className="amount">
<button onClick={() => decrement(product._id)}> - </button>
<span>{product.quantity}</span>
<button onClick={() => increment(product._id)}> + </button>
</div>
<div className="delete" onClick={() => removeProduct(product._id)}>
X
</div>
</div>
</div>
))}
<div className="total">
<h3>Total: $ {total}</h3>
<PayPalButton total={total} tranSuccess={tranSuccess} />
</div>
</div>
);
}
export default Cart;
PayPal button code
import React from "react";
import { PayPalScriptProvider, PayPalButtons } from "#paypal/react-paypal-js";
export default class PayPalButton extends React.Component {
render() {
const onSuccess = (payment) => {
console.log("The payment was succeeded!", payment);
this.props.tranSuccess(payment);
};
const onCancel = (data) => {
console.log("The payment was cancelled!", data);
};
const onError = (err) => {
console.log("Error!", err);
};
let env = "sandbox"; // you can set here to 'production' for production
let total = this.props.total;
let currency = "USD";
const client = {
sandbox:
"ARhnfWu_QrcGQa-PdvaY1RVriEmqGiSkfEWf-plauZQpQN_gyxaLjH9RXOhdQw7fxxxxxxxxxxxx",
production: "YOUR-PRODUCTION-APP-ID",
};
let style = {
size: "small",
color: "blue",
shape: "rect",
label: "checkout",
tagline: false,
};
return (
<PayPalScriptProvider
options={{
"client-id": "ARhnfWu_QrcGQa-PdvaY1RVriEmqGiSkfEWf-plauZQpQN_gyxaLjH9RXOhdQw7fxxxxxxxxxxxx",
}}
>
<PayPalButtons
env={env}
client={client}
commit={true}
total={total}
currency={currency}
onError={onError}
onSuccess={onSuccess}
onCancel={onCancel}
style={style}
/>
</PayPalScriptProvider>
);
}
}
I don't see a createOrder function anywhere. You need one, and it needs to invoke actions.order.create() with a JSON object that uses your total to set the amount.
See examples in the react-paypal-js storybook.
My code keeps bringing torusPlugin is assigned a value but never used, LoggedInView is assigned a value but never used and I don't know where I went wrong. I have checked the code and the torusPlugin function was used, same with the loggedInView.
import { useEffect, useState } from "react";
import { Card, Form } from "react-bootstrap";
import { FaComment, FaRecycle, FaRetweet, FaThumbsUp } from "react-icons/fa";
import { Web3AuthCore } from "#web3auth/core";
import {
WALLET_ADAPTERS,
CHAIN_NAMESPACES,
SafeEventEmitterProvider,
} from "#web3auth/base";
import { OpenloginAdapter } from "#web3auth/openlogin-adapter";
import { TorusWalletConnectorPlugin } from "#web3auth/torus-wallet-connector-plugin";
import Twitter from "./twitter";
import RPC from "./evm";
import { APP_CONSTANTS } from "./constants";
import "./App.css";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
const clientId = APP_CONSTANTS.CLIENT_ID; // get from https://dashboard.web3auth.io
function App() {
const [web3auth, setWeb3auth] = useState<Web3AuthCore | null>(null);
const [provider, setProvider] = useState<SafeEventEmitterProvider | null>(
null
);
const [tweets, setTweets] = useState<Array<any> | null>(null);
const [comment, setComment] = useState<string | "">("");
const [userName, setUserName] = useState<string | "">("");
const [profileImage, setProfileImage] = useState<string | "">("");
const [newTweetName, setNewTweetName] = useState<string | "">("");
const [newTweetDescription, setNewTweetDescription] = useState<string | "">(
""
);
const refreshTime = APP_CONSTANTS.REACT_APP_REFRESH_TIMER * 1000
const [torusPlugin, setTorusPlugin] =
useState<TorusWalletConnectorPlugin | null>(null);
useEffect(() => {
const init = async () => {
try {
const web3auth = new Web3AuthCore({
clientId,
chainConfig: {
chainNamespace: CHAIN_NAMESPACES.EIP155,
chainId: "0x13881",
rpcTarget: APP_CONSTANTS.RPC_TARGET, // This is the mainnet RPC we have added, please pass on your own endpoint while creating an app
},
});
const openloginAdapter = new OpenloginAdapter({
adapterSettings: {
clientId,
network: "testnet",
uxMode: "popup",
whiteLabel: {
name: "Twitter DApp",
logoLight: APP_CONSTANTS.APP_LOGO,
logoDark: APP_CONSTANTS.APP_LOGO,
defaultLanguage: "en",
dark: true, // whether to enable dark mode. defaultValue: false
},
loginConfig: {
// Add login configs corresponding to the providers on modal
// Twitter login
jwt: {
name: "Custom Auth Login",
verifier: APP_CONSTANTS.ADAPTER_TWITTER_CLIENT_VERIFIER, // Please create a verifier on the developer dashboard and pass the name here
typeOfLogin: "twitter", // Pass on the login provider of the verifier you've created
clientId: APP_CONSTANTS.ADAPTER_TWITTER_CLIENT_ID, // Pass on the clientId of the login provider here - Please note this differs from the Web3Auth ClientID. This is the JWT Client ID
},
// Add other login providers here
},
},
});
const torusPlugin = new TorusWalletConnectorPlugin({
torusWalletOpts: {},
walletInitOptions: {
whiteLabel: {
theme: { isDark: true, colors: { primary: "#ffffff" } },
logoDark:
"https://i.ibb.co/kDNCfC9/reshot-icon-wallet-9-H3-QMSDLFR.png",
logoLight:
"https://i.ibb.co/kDNCfC9/reshot-icon-wallet-9-H3-QMSDLFR.png",
},
useWalletConnect: true,
enableLogging: true,
},
});
await web3auth.addPlugin(torusPlugin);
setTorusPlugin(torusPlugin);
await web3auth.configureAdapter(openloginAdapter);
setWeb3auth(web3auth);
await web3auth.init();
if (web3auth.provider) {
await setProvider(web3auth.provider);
let user = await web3auth.getUserInfo();
console.log('user ', user)
if(user.name && user.name !== null && user.name !== " " && user.name !== "")
setUserName(user.name)
if(user.profileImage && user.profileImage !== null && user.profileImage !== " " && user.profileImage !== "")
setProfileImage(user.profileImage)
}
await fetchAllTweets();
//eslint-disable-next-line react-hooks/exhaustive-deps
} catch (error) {
console.error(error);
}
};
init();
}, []);
const logout = async () => {
if (!web3auth) {
console.log("web3auth not initialized yet");
return;
}
await web3auth.logout();
setProvider(null);
};
const login = async () => {
if (!web3auth) {
console.log("web3auth not initialized yet");
return;
}
const web3authProvider = await web3auth.connectTo(
WALLET_ADAPTERS.OPENLOGIN,
{
loginProvider: "jwt",
extraLoginOptions: {
domain: APP_CONSTANTS.AUTH0_DOMAIN, // Please append "https://" before your domain
verifierIdField: "sub",
},
}
);
setProvider(web3authProvider);
if(web3authProvider){
let user = await web3auth.getUserInfo();
if(user.name && user.name !== null && user.name !== " " && user.name !== "")
setUserName(user.name)
if(user.profileImage && user.profileImage !== null && user.profileImage !== " " && user.profileImage !== "")
setProfileImage(user.profileImage)
}
};
/*
const getAccounts = async () => {
if (!provider) {
console.log("provider not initialized yet");
return;
}
const rpc = new RPC(provider);
const userAccount = await rpc.getAccounts();
return userAccount;
};
*/
const refresh = (e: any) => {
e.preventDefault();
fetchAllTweets();
};
const fetchAllTweets = async () => {
console.log("fetchalltweetsrunning");
if (!provider) {
console.log("provider not initialized yet");
return;
}
const rpc = new RPC(provider);
try {
let fetchedTweets = await rpc.getAllTweets();
let tweets = [...fetchedTweets];
setTweets(tweets.reverse());
} catch (error) {
console.log("error in fetching tweets", error);
}
};
const upVote = async (tweetIndex: any) => {
if (!provider) {
console.log("provider not initialized yet");
return;
}
try {
const rpc = new RPC(provider);
await rpc.sendUpVoteTransaction(tweetIndex);
fetchAllTweets();
} catch (error) {
console.log("failed to execute upvote transaction", error);
}
};
const addNewTweet = (e: any) => {
e.preventDefault();
if (!provider) {
console.log("provider not initialized yet");
return;
}
try {
const rpc = new RPC(provider);
toast.success("Tweet added successfully", {
position: toast.POSITION.TOP_CENTER,
});
rpc.sendWriteTweetTransaction(newTweetName, newTweetDescription);
setTimeout(function () {
fetchAllTweets();
}, refreshTime);
fetchAllTweets();
} catch (error) {
toast.error("Something went wrong", {
position: toast.POSITION.TOP_LEFT,
});
console.log("failed to execute new tweet transaction", error);
}
};
const addComment = async (event: any, tweetIndex: any) => {
event.preventDefault();
if (!provider) {
console.log("provider not initialized yet");
return;
}
try {
const rpc = new RPC(provider);
toast.success("Comment added successfully - refresh after 30 sec", {
position: toast.POSITION.TOP_CENTER,
});
await rpc.sendAddCommentTransaction(tweetIndex, comment);
fetchAllTweets();
} catch (error) {
toast.error("Something went wrong", {
position: toast.POSITION.TOP_LEFT,
});
console.log("failed to execute add comment transaction", error);
}
};
// Event handlers
const handleCommentChange = async (event: any) => {
setComment(event.target.value);
};
const handleNewTweetNameChange = async (event: any) => {
setNewTweetName(event.target.value);
};
const handleNewTweetDescriptionChange = async (event: any) => {
setNewTweetDescription(event.target.value);
};
const loggedInView = (
<>
<button className="button" onClick={logout}>
Logout
</button>
<div>
<h1>New Tweet</h1>
<Card>
<Card.Body>
<Card.Title>What are you thinking? Tweet it out!</Card.Title>
<Card.Text></Card.Text>
<Form.Control
as="input"
onChange={handleNewTweetNameChange}
placeholder="Tweet Name"
/>
<br></br>
<br></br>
<Form.Control
as="textarea"
onChange={handleNewTweetDescriptionChange}
placeholder="Description"
/>
<br></br>
<FaRetweet onClick={addNewTweet} />
</Card.Body>
</Card>
</div>
<div>
<h1>
All Tweets <FaRecycle onClick={fetchAllTweets} />
</h1>
{(tweets || []).map((tweet: any, i) => (
<div key={i}>
<div>
<Card>
<Card.Body>
<Card.Title>
<FaThumbsUp onClick={(event) => upVote(i)} /> {tweet.name}
</Card.Title>
<p>Total Upvotes: {tweet.upvotes}</p>
<p>Tweeted by: {tweet.fromAddress}</p>
<Card.Text>{tweet.description}</Card.Text>
<div>
<h3>All Comments</h3>
{tweet.comments.map((comment: any, j: any) => (
<div key={j}>
Comment {j + 1}: {comment}
</div>
))}
<h3>New Comment</h3>
<span>
<Form.Control
as="input"
onChange={handleCommentChange}
placeholder="Your comment..."
/>
</span>
<span>
<FaComment onClick={(event) => addComment(event, i)} />
</span>
</div>
</Card.Body>
<a
href={
APP_CONSTANTS.OPENSEA_ASSETS_URL +
"/" +
APP_CONSTANTS.CONTRACT_ADDRESS +
"/" +
i
}
rel="opener"
>
Buy Now
</a>
</Card>
</div>
</div>
))}
</div>
<div></div>
<div id="console" style={{ whiteSpace: "pre-line" }}>
<p style={{ whiteSpace: "pre-line" }}></p>
</div>
</>
);
const unloggedInView = (
<>
<div className="login-account">
<button className="twitter-bg btn" onClick={login}>
<img src="images/twitter-white.png" alt=""></img>
Login to your Twitter account
</button>
</div>
</>
);
return (
<div className="grid">
{provider ? (
<Twitter
logoutButton={logout}
handleNewTweetDescriptionChange={handleNewTweetDescriptionChange}
handleNewTweetNameChange={handleNewTweetNameChange}
addNewTweet={addNewTweet}
fetchAllTweets={fetchAllTweets}
tweets={tweets}
upVote={upVote}
handleCommentChange={handleCommentChange}
addComment={addComment}
refresh={refresh}
username={userName}
profileimage={profileImage}
/>
) : (
unloggedInView
)}{" "}
<ToastContainer />
</div>
// <div className="grid">{provider
// ? loggedInView
// : unloggedInView}</div>
// {/* <div className="grid">{loggedInView}</div> */}
);
}
export default App;
enter image description here
this is a screenshot of my code terminal
You are actually not using torusPlugin (line 38 screenshot) anywhere. Inside your useEffect you created a new const torusPlugin and then you are calling that const instead of the [torusPlugin, ...] state you declared on line 38 of the screenshot. Try to differentiate your variables / constants names otherwise would be difficult for you to identify mistakes.
*Trying to show a chat history with infinite reload similar to Skype or any popular chat app
In a chat page. If my chat messages limit is 10 messages.
And the chat has 30.
It will show latest 10 when I load the chat.
When I scroll to the top I want to see the previous 10.
I tried this myself and the scroll position stays the same but the messages load in the view.
It should load to the top and hold the scroll position.
How can this be done?
Here's my page:
https://pastebin.com/36xZPG1W
import React, { useRef, useState, useEffect } from 'react';
import produce from 'immer';
import dayjs from 'dayjs';
import { WithT } from 'i18next';
import * as ErrorHandler from 'components/ErrorHandler';
import useOnScreen from 'utils/useOnScreen';
import getLang from 'utils/getLang';
import Message from './Message';
const limit = 10;
const lang = getLang();
interface IMessagesProps extends WithT {
messages: any;
currentUserID: string;
chatID: string;
fetchMore: any;
typingText: any;
setSelectedMsg: any;
removeMessage: any;
}
const Messages: React.FC<IMessagesProps> = ({
messages,
currentUserID,
chatID,
fetchMore,
setSelectedMsg,
removeMessage,
t,
}) => {
const elementRef = useRef(null);
const isOnScreen = useOnScreen(elementRef);
const topElementRef = useRef(null);
const topIsOnScreen = useOnScreen(topElementRef);
const isUserInside = useRef(true);
const scroller = useRef<HTMLDivElement>(null);
const messagesEnd = useRef<HTMLDivElement>(null);
const [hasMore, setHasMore] = useState(true);
useEffect(() => {
scrollToBottom();
}, []);
useEffect(() => {
autoscroll();
}, [messages]);
//NOT WORKING
const autoscroll = () => {
// Visible height
const visibleHeight = scroller.current.offsetHeight;
// Height of messages container
const containerHeight = scroller.current.scrollHeight;
// How far have I scrolled?
const scrollOffset = scroller.current.scrollTop + visibleHeight;
// New message element
const firstChild = scroller.current.firstElementChild;
console.log(`visibleHeight`, visibleHeight);
console.log(`containerHeight`, containerHeight);
console.log(`scrollOffset`, scrollOffset);
console.log(`firstChild`, firstChild.offsetHeight);
console.log(`firstChild`, firstChild.scrollHeight);
console.log(`firstChild`, firstChild);
scroller.current.scrollTop = scrollOffset;
// // Height of the new message
// const newMessageStyles = getComputedStyle($newMessage)
// const newMessageMargin = parseInt(newMessageStyles.marginBottom)
// const newMessageHeight = $newMessage.offsetHeight + newMessageMargin
// // Visible height
// const visibleHeight = $messages.offsetHeight
// // Height of messages container
// const containerHeight = $messages.scrollHeight
// // How far have I scrolled?
// const scrollOffset = $messages.scrollTop + visibleHeight
// if (containerHeight - newMessageHeight <= scrollOffset) {
// $messages.scrollTop = $messages.scrollHeight
// }
};
const fetchDataForScrollUp = cursor => {
ErrorHandler.setBreadcrumb('fetch more messages');
if (!hasMore) {
return;
}
fetchMore({
variables: {
chatID,
limit,
cursor,
},
updateQuery: (previousResult, { fetchMoreResult }) => {
if (!fetchMoreResult?.getMessages || fetchMoreResult.getMessages.messages.length < limit) {
setHasMore(false);
return previousResult;
}
const newData = produce(previousResult, draftState => {
draftState.getMessages.messages = [...previousResult.getMessages.messages, ...fetchMoreResult.getMessages.messages];
});
return newData;
},
});
};
if (messages?.length >= limit) {
if (topIsOnScreen) {
fetchDataForScrollUp(messages[messages.length - 1].id);
}
}
if (isOnScreen) {
isUserInside.current = true;
} else {
isUserInside.current = false;
}
const scrollToBottom = () => {
if (messagesEnd.current) {
messagesEnd.current.scrollIntoView({ behavior: 'smooth' });
}
};
const groupBy = function (arr, criteria) {
return arr.reduce(function (obj, item) {
// Check if the criteria is a function to run on the item or a property of it
const key = typeof criteria === 'function' ? criteria(item) : item[criteria];
// If the key doesn't exist yet, create it
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
obj[key] = [];
}
// Push the value to the object
obj[key].push(item);
// Return the object to the next item in the loop
return obj;
}, {});
};
const objectMap = object => {
return Object.keys(object).reduce(function (result, key) {
result.push({ date: key, messages: object[key] });
return result;
}, []);
};
const group = groupBy(messages, datum => dayjs(datum.createdAt).locale(lang).format('dddd, MMMM D, YYYY').toLocaleUpperCase());
const messageElements = objectMap(group)
.reverse()
.map((item, index) => {
const messageElements = item.messages
.map(message => {
return (
<Message
key={uniqueKey}
message={message}
currentUserID={currentUserID}
lang={lang}
removeMessage={removeMessage}
t={t}
chatID={chatID}
setSelectedMsg={setSelectedMsg}
/>
);
})
.reverse();
return messageElements;
})
.reduce((a, b) => a.concat(b), []);
return (
<div style={{ marginBottom: '25px' }}>
<div ref={topElementRef} />
<div
style={{
position: 'relative',
display: 'flex',
flexDirection: 'column',
flexWrap: 'wrap',
height: '100%',
overflow: 'hidden',
}}
ref={scroller}
>
{messageElements}
<div ref={elementRef} style={{ position: 'absolute', bottom: '5%' }} />
</div>
</div>
);
};
export default Messages;
Been stuck on this for 2 weeks lol. Any advice is helpful :)
Have you tried scrollIntoView ? you can try after changing your autoscroll function like following
const autoscroll = () => {
elementRef.current.scrollIntoView({ behavior: 'smooth' })
};
I am trying to figure out how to create a clean up function as I keep getting an error, if I remove "comments" from the useEffect dependencies, the error goes away, but then the app doesn't update in realtime, which is a problem. If anyone has worked with React and the realtime database or even Firestore and have any ideas on what I should do please let me know.
import React, { useContext, useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'react-toastify';
import User from '../assets/images/user.svg';
import { AuthContext } from '../helpers/firebaseAuth';
import firebase from '../helpers/Firebase';
import Loading from '../helpers/Loading';
export const Comments = ({ match, history }) => {
const { register, handleSubmit, reset } = useForm();
const slug = match.params.slug;
const {...currentUser} = useContext(AuthContext);
const [comments, setComments] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = () => {
const data = firebase.database().ref(`/posts/${slug}/comments`)
data.once('value')
.then((snapshot) => {
if (snapshot) {
let comments = [];
const snapshotVal = snapshot.val();
for (let comment in snapshotVal) {
comments.push(snapshotVal[comment]);
}
setComments(comments);
setLoading(false);
}
});
}
fetchData();
}, [slug, comments])
if (loading) {
return <Loading />;
};
const postComment = (values) => {
console.log(!!currentUser.currentUser)
if (!!currentUser.currentUser) {
const comment = {
commentText: values.commentText,
commentCreator: currentUser.currentUser.displayName,
currentUserId: currentUser.currentUser.uid,
}
const postRef = firebase.database().ref(`posts/${slug}/comments`);
postRef.push(comment);
reset();
} else {
toast.error('You are not authenticated 😕');
}
};
const deleteComment = () => {
console.log(comments[0].commentUserId);
console.log(currentUser.currentUser.uid);
if (currentUser.currentUser.uid === comments[0].commentUserId) {
console.log('correct');
}
const key = firebase.database().ref(`posts/${slug}/comments`).once('value');
key.then(snapshot => {
console.log(snapshot.val());
}).catch((error) => {
console.log(error);
});
};
const back = () => {
history.push('./');
};
return (
<div className='main' style={{ maxWidth: '600px' }}>
<div className='see-user-comments' onClick={back} style={{ cursor: 'pointer', height: '50px' }}>
Commenting on the post: {slug}
</div>
<div className='see-user-comments' style={{ padding: '10px 0' }}>
<div>
<img src={User} alt='Profile' style={{ width: '30px' }} />
<span className='usertag-span'>{currentUser.displayName}</span>
</div>
<div>
<form onSubmit={handleSubmit(postComment)}>
<textarea
name='commentText'
rows='3'
style={{ margin: '10px 0' }}
placeholder='Add to the conversation!'
ref={register}
/>
<span style={{ width: '90%' }}>
<button>Comment</button>
</span>
</form>
</div>
</div>
{comments.map((comment, index) =>
<div key={index} className='see-user-comments' style={{ padding: '15px 0' }}>
<div style={{ height: '30px' }}>
<img src={User} alt='Profile' style={{ width: '30px' }} />
<div style={{ flexDirection: 'column', alignItems: 'flex-start', justifyItems: 'center' }}>
<span className='usertag-span'>{comment.commentCreator}</span>
</div>
</div>
<span className='commentText-span'>{comment.commentText}
{ !!currentUser?.currentUser?.uid === comments[0].commentUserId ?
(<button onClick={deleteComment}>Delete</button>) : null
}
</span>
</div>
)}
</div>
)
}
export default Comments;
Without seeing the error in question, I can only assume it's because using the following pattern causes an infinite loop because the effect is re-triggered every time count changes:
const [count, setCount] = useState(0);
useEffect(() => setCount(count + 1), [count]);
When you add comments to your effect, you are doing the same thing.
To solve this, you must change your effect to rely on Firebase's realtime events to update your comments array instead. This can be as simple as changing once('value').then((snap) => {...}) to on('value', (snap) => {...});. Because this is now a realtime listener, you must also return a function that unsubscribes the listener from inside your useEffect call. The least amount of code to do this correctly is:
const [postId, setPostId] = useState('post001');
useEffect(() => {
const postRef = firebase.database().ref('posts').child(postId);
const listener = postRef.on(
'value',
postSnapshot => {
const postData = postSnapshot.val();
// ... update UI ...
},
err => {
console.log('Failed to get post', err);
// ... update UI ...
}
)
return () => postRef.off('value', listener);
}, [postId]);
Applying these changes to your code (as well as some QoL improvements) yields:
const { register, handleSubmit, reset } = useForm();
const slug = match.params.slug;
const { ...authContext } = useContext(AuthContext); // renamed: currentUser -> authContext (misleading & ambiguous)
const [comments, setComments] = useState([]);
const [loading, setLoading] = useState(true);
let _postCommentHandler, _deleteCommentHandler;
useEffect(() => {
// don't call this data - it's not the data but a reference to it - always call it `somethingRef` instead
const postCommentsRef = firebase.database().ref(`/posts/${slug}/comments`);
// create realtime listener
const listener = postCommentsRef.on(
'value',
querySnapshot => {
let _comments = [];
querySnapshot.forEach(commentSnapshot => {
const thisComment = commentSnapshot.val();
thisComment.key = commentSnapshot.key; // store the key for delete/edit operations
_comments.push(thisComment);
});
setComments(_comments);
setLoading(false);
},
err => {
console.log(`Error whilst getting comments for post #${slug}`, err);
// TODO: handle error
});
// update new comment handler
_postCommentHandler = (formData) => {
console.log({
isLoggedIn: !!authContext.currentUser
});
if (!authContext.currentUser) {
toast.error('You are not authenticated 😕');
return;
}
const newComment = {
commentText: formData.commentText, // suggested: commentText -> content
commentCreator: authContext.currentUser.displayName, // suggested: commentCreator -> author
currentUserId: authContext.currentUser.uid, // suggested: commentUserId -> authorId
}
postCommentsRef.push(newComment)
.then(() => {
// commented successfully
reset(); // reset form completely
})
.catch(err => {
console.log(`Error whilst posting new comment`, err);
// TODO: handle error
reset({ commentText: formData.commentText }) // reset form, but leave comment as-is
})
}
// update delete handler
_deleteCommentHandler = () => {
if (!comments || !comments[0]) {
console.log('Nothing to delete');
return;
}
const commentToDelete = comments[0];
console.log({
commentUserId: commentToDelete.commentUserId,
currentUser: authContext.currentUser.uid
});
if (authContext.currentUser.uid !== commentToDelete.commentUserId) {
toast.error('That\'s not your comment to delete!');
return;
}
postCommentsRef.child(commentToDelete.key)
.remove()
.then(() => {
// deleted successfully
})
.catch(err => {
console.log(`Error whilst deleting comment #${commentToDelete.key}`, err);
// TODO: handle error
});
};
// return listener cleanup function
return () => postCommentsRef.off('value', listener);
}, [slug]);
const postComment = (values) => _postCommentHandler(values);
const deleteComment = () => _deleteCommentHandler();
Because I renamed currentUser to authContext, this will also need updating:
<div>
<img src={User} alt='Profile' style={{ width: '30px' }} />
<span className='usertag-span'>{authContext?.currentUser?.displayName}</span>
</div>