use viewport-list

This commit is contained in:
SrGooglo 2023-03-07 02:10:02 +00:00
parent 4e2a1ccc74
commit 38e88b48a7
2 changed files with 168 additions and 101 deletions

View File

@ -2,6 +2,7 @@ import React from "react"
import * as antd from "antd" import * as antd from "antd"
import { Icons } from "components/Icons" import { Icons } from "components/Icons"
import { PostCard, LoadMore } from "components" import { PostCard, LoadMore } from "components"
import { ViewportList } from "react-viewport-list"
import PostModel from "models/post" import PostModel from "models/post"
@ -22,20 +23,29 @@ const NoResultComponent = () => {
/> />
} }
// FIXME: Scroll behavior should scroll to next post or the previous one depending on the direction of the scroll export class PostsListsComponent extends React.Component {
export default class PostsLists extends React.Component {
state = { state = {
currentIndex: 0,
openPost: null, openPost: null,
loading: false,
initialLoading: true,
realtimeUpdates: true,
hasMore: true,
list: this.props.list ?? [], list: this.props.list ?? [],
} }
listRef = React.createRef() viewRef = this.props.innerRef
timelineWsEvents = { timelineWsEvents = {
"post.new": (data) => { "post.new": (data) => {
console.log("New post => ", data) console.log("New post => ", data)
if (!this.state.realtimeUpdates) {
return
}
this.setState({ this.setState({
list: [data, ...this.state.list], list: [data, ...this.state.list],
}) })
@ -51,20 +61,93 @@ export default class PostsLists extends React.Component {
} }
} }
componentDidMount = async () => { handleLoad = async (fn) => {
window.app.cores.shortcuts.register({ this.setState({
id: "postsFeed.scrollUp", loading: true,
key: "ArrowUp",
preventDefault: true,
}, (event) => {
this.scrollUp()
}) })
window.app.cores.shortcuts.register({
id: "postsFeed.scrollDown", const result = await fn({
key: "ArrowDown", trim: this.state.list.length,
preventDefault: true, }).catch((err) => {
}, (event) => { console.error(err)
this.scrollDown()
app.message.error("Failed to load more posts")
return null
})
if (result) {
if (result.length === 0) {
return this.setState({
hasMore: false,
})
}
this.setState({
list: [...this.state.list, ...result],
})
}
this.setState({
loading: false,
})
}
addPost = (post) => {
this.setState({
list: [post, ...this.state.list],
})
}
removePost = (id) => {
this.setState({
list: this.state.list.filter((post) => {
return post._id !== id
}),
})
}
_hacks = {
addPost: this.addPost,
removePost: this.removePost,
addRandomPost: () => {
this.addPost({
_id: Math.random().toString(36).substring(7),
message: `Random post ${Math.random().toString(36).substring(7)}`,
user: {
_id: Math.random().toString(36).substring(7),
username: "random user",
}
})
}
}
onResumeRealtimeUpdates = async () => {
// fetch new posts
await this.handleLoad(this.props.loadFromModel)
this.setState({
realtimeUpdates: true,
})
}
onScrollList = (e) => {
const { scrollTop, scrollHeight, clientHeight } = e.target
if (scrollTop + clientHeight >= scrollHeight - 100) {
this.setState({
realtimeUpdates: false,
})
}
}
componentDidMount = async () => {
if (typeof this.props.loadFromModel === "function") {
await this.handleLoad(this.props.loadFromModel)
}
this.setState({
initialLoading: false,
}) })
if (this.props.watchTimeline) { if (this.props.watchTimeline) {
@ -72,20 +155,31 @@ export default class PostsLists extends React.Component {
app.cores.api.listenEvent(event, callback) app.cores.api.listenEvent(event, callback)
}) })
} }
//console.log("PostsList mounted", this.viewRef)
if (this.viewRef) {
// handle when the user is scrolling a bit down, disable ws events
this.viewRef.current.addEventListener("scroll", this.onScrollList)
}
window._hacks = this._hacks
} }
componentWillUnmount = async () => { componentWillUnmount = async () => {
window.app.shortcuts.remove("postsFeed.scrollUp")
window.app.shortcuts.remove("postsFeed.scrollDown")
if (this.props.watchTimeline) { if (this.props.watchTimeline) {
Object.entries(this.timelineWsEvents).forEach(([event, callback]) => { Object.entries(this.timelineWsEvents).forEach(([event, callback]) => {
app.cores.api.unlistenEvent(event, callback) app.cores.api.unlistenEvent(event, callback)
}) })
} }
if (this.viewRef) {
this.viewRef.current.removeEventListener("scroll", this.onScrollList)
}
window._hacks = null
} }
// watch if props.list has changed and update state.list
componentDidUpdate = async (prevProps) => { componentDidUpdate = async (prevProps) => {
if (prevProps.list !== this.props.list) { if (prevProps.list !== this.props.list) {
this.setState({ this.setState({
@ -94,39 +188,6 @@ export default class PostsLists extends React.Component {
} }
} }
scrollUp = () => {
this.scrollToIndex(this.state.currentIndex - 1)
}
scrollDown = () => {
this.scrollToIndex(this.state.currentIndex + 1)
}
scrollToIndex = (index) => {
const post = this.listRef.current.children[index]
if (post) {
post.scrollIntoView({ behavior: "smooth", block: "center" })
this.setState({ currentIndex: index })
}
}
handleScroll = (event) => {
event.preventDefault()
// check if is scrolling up or down
const isScrollingUp = event.deltaY < 0
// get current index
const currentIndex = this.state.currentIndex
// get next index
const nextIndex = isScrollingUp ? currentIndex - 1 : currentIndex + 1
// scroll to next index
this.scrollToIndex(nextIndex)
}
onLikePost = async (data) => { onLikePost = async (data) => {
let result = await PostModel.toogleLike({ post_id: data._id }).catch(() => { let result = await PostModel.toogleLike({ post_id: data._id }).catch(() => {
antd.message.error("Failed to like post") antd.message.error("Failed to like post")
@ -168,7 +229,17 @@ export default class PostsLists extends React.Component {
} }
} }
onLoadMore = async () => {
if (typeof this.props.onLoadMore === "function") {
return this.handleLoad(this.props.onLoadMore)
}
}
render() { render() {
if (this.state.initialLoading) {
return <antd.Skeleton active />
}
if (this.state.list.length === 0) { if (this.state.list.length === 0) {
if (typeof this.props.emptyListRender === "function") { if (typeof this.props.emptyListRender === "function") {
return React.createElement(this.props.emptyListRender) return React.createElement(this.props.emptyListRender)
@ -180,37 +251,43 @@ export default class PostsLists extends React.Component {
</div> </div>
} }
return <div return <LoadMore
id="postsFeed" className="postList"
className="postsFeed" loadingComponent={LoadingComponent}
onScroll={this.handleScroll} noResultComponent={NoResultComponent}
hasMore={this.state.hasMore}
fetching={this.state.loading}
onBottom={this.onLoadMore}
> >
<LoadMore {
className="posts" !this.state.realtimeUpdates && <div className="realtime_updates_disabled">
ref={this.listRef} <antd.Alert
loadingComponent={LoadingComponent} message="Realtime updates disabled"
noResultComponent={NoResultComponent} description="You are scrolling down, realtime updates are disabled to improve performance"
hasMore={this.props.hasMorePosts} type="warning"
onBottom={this.props.onLoadMore} showIcon
fetching={this.props.loading} />
</div>
}
<ViewportList
viewportRef={this.viewRef}
items={this.state.list}
> >
{ {
this.state.list.map((post, index) => { (item) => <PostCard
console.log("Post => ", post, index) key={item._id}
data={item}
return <PostCard events={{
key={index} onClickLike: this.onLikePost,
data={post} onClickSave: this.onSavePost,
events={{ onClickDelete: this.onDeletePost,
onToogleOpen: this.onToogleOpen, onClickEdit: this.onEditPost,
onClickLike: this.onLikePost, }}
onClickDelete: this.onDeletePost, />
onClickSave: this.onSavePost,
}}
/>
})
} }
</LoadMore> </ViewportList>
</div> </LoadMore>
} }
} }
export default React.forwardRef((props, ref) => <PostsListsComponent innerRef={ref} {...props} />)

View File

@ -1,26 +1,16 @@
.postsFeed { .postList,
.infinite-scroll {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center;
width: 100%; width: fit-content;
.infinite-scroll { // WARN: Only use if is a performance issue
display: flex; //will-change: transform;
flex-direction: column;
align-items: center;
//justify-content: center;
width: 100%; overflow: hidden;
}
.posts { border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
}
} }