folks, I am building a chat app. Currently, I am working on a feature in which when the user scrolls a sticky date is shown to the top, based on the date of the messages visible in the viewport area. But I don't know how to get the index of the last item visible in the viewport.
Here is the codesandbox
Here is the code
const MessageComponent = () => {
const { id } = useParams();
const {
data: messageData,
isSuccess,
isError,
isLoading,
isFetching,
hasMoreData,
firstData,
isUninitialized,
} = useGetData(id);
const { loadMore } = useGetScrollData(messageData, id);
const user = User();
const chatId =
messageData[0]?.type == "date"
? messageData[1]?.chat?._id
: messageData[0]?.chat?._id;
const islastItemChatId: boolean = messageData.length > 0 && chatId != id;
const scrollRef = useScrollRef(islastItemChatId, id);
const scrollFunc = (e: any) => {
// let m = e.target.scrollHeight + e.target.scrollTop;
// let i = e.target.scrollHeight - m;
// console.log({ i, e });
if (!scrollRef.current) return;
const containerMiddle =
scrollRef.current.scrollTop +
scrollRef.current.getBoundingClientRect().height / 2;
const infiniteScrollItems = scrollRef.current.children[0].children;
console.log({ containerMiddle, infiniteScrollItems, e: e.target });
};
return (
<>
{messageData.length > 0 && (
<div
id="scrollableDiv"
style={{
height: "80%",
overflow: "auto",
display: "flex",
flexDirection: "column-reverse",
padding: "10px 0px",
}}
ref={scrollRef}
>
<InfiniteScroll
dataLength={messageData.length}
hasMore={hasMoreData}
onScroll={scrollFunc}
loader={
<div className="loading-container">
<div className="lds-ring">
<div></div>
</div>
</div>
}
endMessage={
<div className="message-container date">
<div className={`text-container sender large-margin`}>
<span>You have seen all the messages</span>
</div>
</div>
}
style={{ display: "flex", flexDirection: "column-reverse" }}
next={loadMore}
inverse={true}
scrollableTarget="scrollableDiv"
>
{messageData.map((item, index: number) => {
const isUserChat = item?.sender?._id === user._id;
const className =
item?.type == "date"
? "date"
: isUserChat
? "user-message"
: "sender-message";
const prevItem: IMessageData | null =
index < messageData?.length ? messageData[index - 1] : null;
const nextItem: IMessageData | null =
index < messageData?.length ? messageData[index + 1] : null;
return (
<Message
key={item._id}
item={item}
prevItem={prevItem}
className={className}
isUserChat={isUserChat}
index={index}
nextItem={nextItem}
/>
);
})}
</InfiniteScroll>
</div>
)}
</>
);
};
The recommended way to check the visibility of an element is using the Intersection Observer API.
The lib react-intersection-observer can help you to observe all the elements and map the indexes with some states.
However, for your use case, you can minimize the element to observe by only observing the visibility of where a new date starts and where the last message is for that date and setting the stickiness of the date label accordingly.
I implemented the solution here https://codesandbox.io/s/sticky-date-chat-ykmlx2?file=/src/App.js
I built an UI with this structure
------
marker -> observed element that signals when the block of a date starts going up
------
date label -> a label to show the date which has conditional stickiness (position: sticky)
------
date messages -> the block of messages for the same date
------
last date message -> observe the last message to know if a block of dates is still visible
------
This logic is contained in the MessagesBlock component
const MessagesBlock = ({ block }) => {
const [refDateMarker, inViewDateMarker] = useInView();
const [refLastMessage, inViewLastMessage, entryLastMessage] = useInView();
let styles = {};
// make the date sticky when the marker has already gone up
// and the last message is either visible or hasn't reached the viewport yet
if (
!inViewDateMarker &&
(inViewLastMessage ||
(!inViewLastMessage && entryLastMessage?.boundingClientRect?.y > 0))
) {
styles = { position: "sticky", top: 0 };
}
return (
<>
<div ref={refDateMarker} style={{ height: 1 }} />
<div
style={{
...styles,
// some more styles omitted for brevity
}}
>
{dayjs(block[0].createdAt).format("DD/MM/YYYY")}
</div>
{block.map((item, index) => {
...
return (
<Message
ref={index === block.length - 1 ? refLastMessage : null}
key={item._id}
item={item}
className={className}
isUserChat={isUserChat}
index={index}
/>
);
})}
</>
);
};
Of course, to achieve this, I had to change the data a little. I used the groupby function provided by lodash
import groupBy from "lodash.groupby";
...
const byDate = groupBy(data, (item) =>
dayjs(item.createdAt).format("DD/MM/YYYY")
);
And render the blocks in the scroll container
<InfiniteScroll
...
>
{Object.entries(byDate).map(([date, block]) => (
<MessagesBlock block={block} />
))}
</InfiniteScroll>
I had to make some style changes and omit some details from your version, but I hope you can add them to my provided solution.
Related
I'm trying to display fields based on the value of a props so let's say my props value = 2 then I want to display 2 inputs but I can't manage to get it work.
This is what I tried
const [numberOfFields, setNumberOfFields] = useState(0);
const [loadFields, setloadFields] = useState([]);
const addField = () => {
return loadFields.map((tier) => {
<div>
<p style={{color:'black'}}>Tier {tier + 1}</p>
<InputNumber />
</div>
})
}
const onPropsValueLoaded = (value) => {
let tmp = value
setNumberOfFields(tmp);
if (numberOfFields > 0) {
const generateArrays = Array.from(value).keys()
setloadFields(generateArrays);
} else {
setloadFields([]);
}
}
useEffect(() => {
onPropsValueLoaded(props.numberOfTiers);
}, [])
return (
<>
<Button type="primary" onClick={showModal}>
Buy tickets
</Button>
<Modal
title="Buy ticket"
visible={visible}
onOk={handleOk}
confirmLoading={confirmLoading}
onCancel={handleCancel}
>
<p style={{ color: 'black' }}>{props.numberOfTiers}</p>
{loadFields.length ? (
<div>{addField()}</div>
) : null}
<p style={{ color: 'black' }}>Total price: </p>
</Modal>
</>
);
so here props.NumberOfTiers = 2 so I want 2 input fields to be displayed but right now none are displayed even though loadFields.length is not null
I am displaying this inside a modal (even though I don't think it changes anything).
I am doing this when I load the page that's why I am using the useEffect(), because if I use a field and update this onChange it works nicely.
EDIT:
I changed the onPropsValueLoaded() function
const generateArrays = Array.from({length : tmp}, (v,k) => k)
instead of
const generateArrays = Array.from(value).keys()
There are couple of things you should fix in here,
First, you need to return div in addField function to render the inputs.
Second, you should move your function onPropsValueLoaded inside useEffect or use useCallback to prevent effect change on each render.
Third, your method of creating array using Array.from is not correct syntax which should be Array.from(Array(number).keys()).
So the working code should be , I also made a sample here
import React, { useState, useEffect } from "react";
import "./styles.css";
export default function App() {
const [numberOfFields, setNumberOfFields] = useState(0);
const [loadFields, setloadFields] = useState([]);
const addField = () => {
return loadFields.map((tier) => {
return (
<div key={tier}>
<p style={{ color: "black" }}>Tier {tier + 1}</p>
<input type="text" />
</div>
);
});
};
useEffect(() => {
let tmp = 2; // tier number
setNumberOfFields(tmp);
if (numberOfFields > 0) {
const generateArrays = Array.from(Array(tmp).keys());
setloadFields(generateArrays);
} else {
setloadFields([]);
}
}, [numberOfFields]);
return (
<>
<button type="button">Buy tickets</button>
<p style={{ color: "black" }}>2</p>
{loadFields.length ? <div>{addField()}</div> : null}
<p style={{ color: "black" }}>Total price: </p>
</>
);
}
I have a simple list of options in a menu like so:
Option 1
Option 2
Option 3
When the user clicks on an option, there should be a highlighted bar that shows which one they selected. And when the user clicks on different options, the highlighted bar should slide up and down depending on what they chose. I'm trying to use react-spring, but I can't seem to get the animation and clicking behavior to happen properly.
With my current code, the highlighted bar does not slide up and down; it just shows and hides upon user selection. And clicking on an option once does not put the highlighted bar on it, instead, I have to click twice for it to show up correctly on the selected option.
Help is appreciated! This is my first time using react-spring so I'm a bit lost on this.
Below is the code snippet for the animations and rendering the component:
const [currentIndex, setCurrentIndex] = useState<number>(0);
const [previousIndex, setPreviousIndex] = useState<number>(0);
const onClick = (name: string, index: number) => {
setPreviousIndex(currentIndex);
setCurrentIndex(index);
setSpring(fn());
};
// Spring animation code
const fn = () => (
{
transform: `translateY(${currentIndex * 52}px)`,
from: {
transform: `translateY(${previousIndex * 52}px)`,
},
}
);
const [spring, setSpring] = useState<any>(useSpring(fn()));
// Rendering component
return (
<div>
{options.map((option, index) => (
<>
{currentIndex === index && <animated.div style={{...spring, ...{ background: 'orange', height: 52, width: '100%', position: 'absolute', left: 0, zIndex: 1}}}></animated.div>}
<div onClick={() => onClick(option.name, index)}>
<TextWithIcon icon={currentIndex === index ? option.filledIcon : option.outlineIcon} text={option.name} />
</div>
</>
))}
</div>
);
And here is the custom component, TextWithIcon:
// Interfaces
interface TextWithIconProps {
containerStyle?: Record<any, any>;
icon: ReactElement;
text: string;
textStyle?: Record<any, any>;
}
// TextWithIcon component
const TextWithIcon: React.FC<TextWithIconProps> = ({ containerStyle, icon, text, textStyle}) => {
return (
<div id='menu-items' style={{...styles.container, ...containerStyle}}>
{icon}
<Text style={{...styles.text, ...textStyle}}>{text}</Text>
</div>
)
};
You have to set the duration property of the config property inside the useSpring's parameter to a value. Try the following:
const [currentIndex, setCurrentIndex] = useState<number>(0);
const [previousIndex, setPreviousIndex] = useState<number>(0);
const onClick = (name: string, index: number) => {
setPreviousIndex(currentIndex);
setCurrentIndex(index);
setSpring(fn());
};
// Spring animation code
const fn = () => (
{
transform: `translateY(${currentIndex * 52}px)`,
from: {
transform: `translateY(${previousIndex * 52}px)`,
},
config: {
duration: 1250 //this can be a different number
}
}
);
const [spring, setSpring] = useState<any>(useSpring(fn()));
// Rendering component
return (
<div>
{options.map((option, index) => (
<>
{currentIndex === index && <animated.div style={{...spring, ...{ background: 'orange', height: 52, width: '100%', position: 'absolute', left: 0, zIndex: 1}}}></animated.div>}
<div onClick={() => onClick(option.name, index)}>
<TextWithIcon icon={currentIndex === index ? option.filledIcon : option.outlineIcon} text={option.name} />
</div>
</>
))}
</div>
);
References:
StackOverflow. Animation duration in React Spring.https://stackoverflow.com/a/54076843/8121551. (Accessed August 23, 2021).
I have used tailwind-CSS on react js I want to scroll horizontally using mouse wheel when user hover over the card section so for pc users can scroll horizontally by using mouse wheel instead of both shift and mouse wheel.
Live Anywhere
<div className="flex space-x-3 overflow-y-scroll scrollbar-hide p-3 -ml-3">
{cardsDate?.map(({ img, title }) => (
<MediumCard key={img} img={img} title={title} />
))}
</div>
</section>
You can use a custom scroll function as a ref to your div.
export function useHorizontalScroll() {
const elRef = useRef();
useEffect(() => {
const el = elRef.current;
if (el) {
const onWheel = e => {
if (e.deltaY == 0) return;
e.preventDefault();
el.scrollTo({
left: el.scrollLeft + e.deltaY,
behavior: "smooth"
});
};
el.addEventListener("wheel", onWheel);
return () => el.removeEventListener("wheel", onWheel);
}
}, []);
return elRef;
}
The above function can be imported and used as follows:
<div className="App" ref={scrollRef} style={{ overflow: "auto" }}>
<div style={{ whiteSpace: "nowrap" }}>
<Picture />
</div>
</div>
I created a codesandbox as an example.
try with scrollBy working well
const element = document.querySelector("#container");
element.addEventListener('wheel', (event) => {
event.preventDefault();
element.scrollBy({
left: event.deltaY < 0 ? -30 : 30,
});
});
container with his children elements
You can do this inline without the use of refs or hooks with the following code:
<div
style={{ scrollbarColor="transparent", overflowX="auto" }}
onWheel={(e) => {
// here im handling the horizontal scroll inline, without the use of hooks
const strength = Math.abs(e.deltaY);
if (e.deltaY === 0) return;
const el = e.currentTarget;
if (
!(el.scrollLeft === 0 && e.deltaY < 0) &&
!(
el.scrollWidth -
el.clientWidth -
Math.round(el.scrollLeft) ===
0 && e.deltaY > 0
)
) {
e.preventDefault();
}
el.scrollTo({
left: el.scrollLeft + e.deltaY,
// large scrolls with smooth animation behavior will lag, so switch to auto
behavior: strength > 70 ? "auto" : "smooth",
});
}}
>
// ...
</div>
I am making a web app like Tinder. Currently, I am working with user blocking. I have a "blocked" table in the database. In my application, A user already can block another user. Now, I want to hide the users in the search result if the logged user already blocked them.
Here is the code I have. This code renders for infinity (it doesn't stop). Can anyone tell me how can I make it working?
const ListOfUsers = ({ users }) => {
//console.log('users list rendered', users)
var coords = JSON.parse(window.localStorage.getItem('loggedMatchaUser'));
var from_user_id = coords.user_id;
const [check, setCheck] = useState(0)
const checkBlock = (to_user_id) => {
blockService.blockedUser({from_user_id, to_user_id})
.then(res => {
//If this row exist in the table it return 1 otherwise 0
setCheck(res.value);
})
.catch(e => {
console.log(("Error: couldn't get block info"))
})
}
return (
users && users.length > 0
? <ListGroup className="text-left" variant="flush">
{users.map(u => <Link to={`/users/${u.user_id}`} key={u.user_id}>
{checkBlock(u.user_id)}
{!check &&
<ListGroup.Item>
<div style={{display: "inline-block", width: "60%"}}>{u.username}, {u.age.years}</div>
<div style={{display: "inline-block", width: "40%", textAlign: "right"}}>{parseInt(u.distance)} km<br />
<FontAwesomeIcon icon={faAward} /> {u.fame}</div>
</ListGroup.Item>
}
</Link>)}
</ListGroup>
: <div className="text-info">Could not find any matching users<br />please try different filters</div>
)
}
You are issuing a side-effect (i.e. calling a function that updates state) in the render return.
const ListOfUsers = ({ users }) => {
...
const [check, setCheck] = useState(0);
const checkBlock = (to_user_id) => {
blockService
.blockedUser({ from_user_id, to_user_id })
.then((res) => {
//If this row exist in the table it return 1 otherwise 0
setCheck(res.value); // <-- updates state
})
.catch((e) => {
console.log("Error: couldn't get block info");
});
};
return users && users.length > 0 ? (
<ListGroup className="text-left" variant="flush">
{users.map((u) => (
<Link to={`/users/${u.user_id}`} key={u.user_id}>
{checkBlock(u.user_id)} // <-- function call updates state
...
</Link>
))}
</ListGroup>
) : (
...
);
};
You'll likely want to use an effect hook to check blocked users when the users prop updates. You'll need to "preprocess" your users array and augment it with the blocked status, or simply filter them out, which is probably easier anyway. The reason is because you can't use a single check state value for every user being rendered.
const ListOfUsers = ({ users }) => {
const coords = JSON.parse(window.localStorage.getItem("loggedMatchaUser"));
const from_user_id = coords.user_id;
const [nonBlockedUsers, setNonBlockedUsers] = useState([]);
useEffect(() => {
const checkBlock = (to_user_id) =>
// If this row exist in the table it return 1 otherwise 0
blockService.blockedUser({ from_user_id, to_user_id });
Promise.all(users.map(({ user_id }) => checkBlock(user_id))).then(
(blockedUserStatus) => {
const nonBlockedUsers = [];
users.forEach((user, index) => {
if (!blockedUserStatus[index]) nonBlockedUsers.push(user);
});
setNonBlockedUsers(nonBlockedUsers);
}
);
}, [from_user_id, users]);
return nonBlockedUsers.length > 0 ? (
<ListGroup className="text-left" variant="flush">
{nonBlockedUsers.map((u) => (
<Link to={`/users/${u.user_id}`} key={u.user_id}>
<ListGroup.Item>
<div style={{ display: "inline-block", width: "60%" }}>
{u.username}, {u.age.years}
</div>
<div
style={{
display: "inline-block",
width: "40%",
textAlign: "right"
}}
>
{parseInt(u.distance)} km
<br />
<FontAwesomeIcon icon={faAward} /> {u.fame}
</div>
</ListGroup.Item>
</Link>
))}
</ListGroup>
) : (
<div className="text-info">
Could not find any matching users
<br />
please try different filters
</div>
);
};
Let's say I have 2 react elements: componentSender and componentReceiver. That need to be generated in a loop N times.
The special thing they have is that every time someone click in one componentSender, a prop will change in the respective componentReceiver.
This pair of components could be as simple as:
function ComponentReceiver(props) {
return (
<div>{`Listening to "Sender ${props.indexVar}" and it last received: ${props.showVar}`}</div>
);
}
function ComponentSender(props) {
return (
<input type="button" onClick={() => {props.onChangeValue(props.indexVar);}}
value={`SENDER for ${props.indexVar}> `}
/>
);
}
I am using React.createElement in a loop and creating the pairs, you can see it here:
https://codepen.io/danieljaguiar/pen/bGVJbGw?editors=1111
The big problem in my demo is that, when I change the state in the parent (APP), the child components don't re-render.
You have to fix up few things:
try not to store jsx in state. Iterate and render directly in render.
in handleChangeValue function, the show state reference is not changed at all and hence the component is not re-rendered. Make sure to take a copy of show (use spread operator) and then update state.
remove unnecessary code in useEffect and
Working & simplified copy of your code is here in the codesandbox
Code Snippet with fixes
function ComponentReceiver(props) {
return (
<div>{`Listening to "Sender ${props.indexVar}" and I received: ${
props.showVar
}`}</div>
);
}
function ComponentSender(props) {
return (
<input
type="button"
onClick={() => {
props.onChangeValue(props.indexVar);
}}
value={`SENDER for ${props.indexVar} ----------------------> `}
/>
);
}
export default function App() {
const [show, SetShow] = React.useState([]);
const [pageElements, setPageElements] = React.useState([]);
const handleChangeValue = val => {
const updatedShow = [...show];
updatedShow[val] = !updatedShow[val];
SetShow(updatedShow);
};
React.useEffect(() => {
let index = 0;
let elements = [];
while (index < 5) {
show[index] = true;
SetShow([...show]);
elements.push(
<div key={index} style={{ display: "flex", margin: "20px" }}>
<ComponentSender
key={index + "s"}
indexVar={index}
onChangeValue={handleChangeValue}
/>
<ComponentReceiver
key={index + "R"}
indexVar={index}
showVar={show[index]}
/>
</div>
);
index++;
SetShow([...show]);
}
setPageElements(elements);
}, []);
return (
<div>
{[...Array(5).keys()].map((_, index) => {
return (
<div key={index} style={{ display: "flex", margin: "20px" }}>
<ComponentSender
key={index + "s"}
indexVar={index}
onChangeValue={handleChangeValue}
/>
<ComponentReceiver
key={index + "R"}
indexVar={index}
showVar={show[index]}
/>
</div>
);
})}
</div>
);
}