diff --git a/packages/app/src/components/LoadMore/index.jsx b/packages/app/src/components/LoadMore/index.jsx
index 9ed6f0a3..2bf1de51 100755
--- a/packages/app/src/components/LoadMore/index.jsx
+++ b/packages/app/src/components/LoadMore/index.jsx
@@ -4,58 +4,55 @@ import classnames from "classnames"
import "./index.less"
export default React.forwardRef((props, ref) => {
- const {
- className,
- children,
- hasMore,
- loadingComponent,
- noResultComponent,
- contentProps = {},
- } = props
+ const {
+ className,
+ children,
+ hasMore = false,
+ loadingComponent,
+ contentProps = {},
+ } = props
- let observer = null
+ let observer = null
- const insideViewportCb = (entries) => {
- const { fetching, onBottom } = props
+ const insideViewportCb = (entries) => {
+ const { fetching, onBottom } = props
- entries.forEach(element => {
- if (element.intersectionRatio > 0 && !fetching) {
- onBottom()
- }
- })
- }
+ entries.forEach((element) => {
+ if (element.intersectionRatio > 0 && !fetching) {
+ onBottom()
+ }
+ })
+ }
- React.useEffect(() => {
- try {
- const node = document.getElementById("bottom")
+ React.useEffect(() => {
+ try {
+ const node = document.getElementById("bottom")
- observer = new IntersectionObserver(insideViewportCb)
- observer.observe(node)
- } catch (err) {
- console.log("err in finding node", err)
- }
+ observer = new IntersectionObserver(insideViewportCb)
+ observer.observe(node)
+ } catch (err) {
+ console.log("err in finding node", err)
+ }
- return () => {
- observer.disconnect()
- observer = null
- }
- }, [])
+ return () => {
+ observer.disconnect()
+ observer = null
+ }
+ }, [])
- return
- {children}
+ return (
+
+ {children}
-
+
-
- {loadingComponent && React.createElement(loadingComponent)}
-
-
-})
\ No newline at end of file
+
+ {loadingComponent && React.createElement(loadingComponent)}
+
+
+ )
+})
diff --git a/packages/app/src/components/PostsList/index.jsx b/packages/app/src/components/PostsList/index.jsx
index afda9260..866dde9c 100755
--- a/packages/app/src/components/PostsList/index.jsx
+++ b/packages/app/src/components/PostsList/index.jsx
@@ -1,5 +1,6 @@
import React from "react"
import * as antd from "antd"
+import lodash from "lodash"
import { AnimatePresence } from "motion/react"
import { Icons } from "@components/Icons"
@@ -23,353 +24,8 @@ const LoadingComponent = () => {
)
}
-const NoResultComponent = () => {
- return (
-
- )
-}
-
-const typeToComponent = {
- post: (args) => ,
- //"playlist": (args) => ,
-}
-
-const Entry = React.memo((props) => {
- const { data } = props
-
- return React.createElement(
- typeToComponent[data.type ?? "post"] ?? PostCard,
- {
- key: data._id,
- data: data,
- disableReplyTag: props.disableReplyTag,
- disableHasReplies: props.disableHasReplies,
- events: {
- onClickLike: props.onLikePost,
- onClickSave: props.onSavePost,
- onClickDelete: props.onDeletePost,
- onClickEdit: props.onEditPost,
- onClickReply: props.onReplyPost,
- onDoubleClick: props.onDoubleClick,
- },
- },
- )
-})
-
-const PostList = React.forwardRef((props, ref) => {
- return (
-
- {!props.realtimeUpdates && !app.isMobile && (
-
- )}
-
-
- {props.list.map((data) => {
- return
- })}
-
-
- )
-})
-
-export class PostsListsComponent extends React.Component {
- state = {
- openPost: null,
-
- loading: false,
- resumingLoading: false,
- initialLoading: true,
- scrollingToTop: false,
-
- topVisible: true,
-
- realtimeUpdates: true,
-
- hasMore: true,
- list: this.props.list ?? [],
- pageCount: 0,
- }
-
- parentRef = this.props.innerRef
- listRef = React.createRef()
-
- timelineWsEvents = {
- "post:new": (data) => {
- console.log("[WS] Recived a post >", data)
-
- this.setState({
- list: [data, ...this.state.list],
- })
- },
- "post:delete": (id) => {
- console.log("[WS] Received a post delete >", id)
-
- this.setState({
- list: this.state.list.filter((post) => {
- return post._id !== id
- }),
- })
- },
- "post:update": (data) => {
- console.log("[WS] Received a post update >", data)
-
- this.setState({
- list: this.state.list.map((post) => {
- if (post._id === data._id) {
- return data
- }
-
- return post
- }),
- })
- },
- }
-
- handleLoad = async (fn, params = {}) => {
- if (this.state.loading === true) {
- console.warn(`Please wait to load the post before load more`)
- return
- }
-
- this.setState({
- loading: true,
- })
-
- let payload = {
- page: this.state.pageCount,
- limit: app.cores.settings.get("feed_max_fetch"),
- }
-
- if (this.props.loadFromModelProps) {
- payload = {
- ...payload,
- ...this.props.loadFromModelProps,
- }
- }
-
- const result = await fn(payload).catch((err) => {
- console.error(err)
-
- app.message.error("Failed to load more posts")
-
- return null
- })
-
- if (result) {
- if (result.length === 0) {
- return this.setState({
- hasMore: false,
- })
- }
-
- if (params.replace) {
- this.setState({
- list: result,
- pageCount: 0,
- })
- } else {
- this.setState({
- list: [...this.state.list, ...result],
- pageCount: this.state.pageCount + 1,
- })
- }
- }
-
- 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: () => {
- const randomId = Math.random().toString(36).substring(7)
-
- this.addPost({
- _id: randomId,
- message: `Random post ${randomId}`,
- user: {
- _id: randomId,
- username: "random user",
- avatar: `https://api.dicebear.com/7.x/thumbs/svg?seed=${randomId}`,
- },
- })
- },
- listRef: this.listRef,
- }
-
- onResumeRealtimeUpdates = async () => {
- console.log("Resuming realtime updates")
-
- this.setState({
- resumingLoading: true,
- scrollingToTop: true,
- })
-
- this.listRef.current.scrollTo({
- top: 0,
- behavior: "smooth",
- })
-
- // reload posts
- await this.handleLoad(this.props.loadFromModel, {
- replace: true,
- })
-
- this.setState({
- realtimeUpdates: true,
- resumingLoading: false,
- })
- }
-
- onScrollList = (e) => {
- const { scrollTop } = e.target
-
- if (this.state.scrollingToTop && scrollTop === 0) {
- this.setState({
- scrollingToTop: false,
- })
- }
-
- if (scrollTop > 200) {
- if (this.state.topVisible) {
- this.setState({
- topVisible: false,
- })
-
- if (typeof this.props.onTopVisibility === "function") {
- this.props.onTopVisibility(false)
- }
- }
-
- if (
- !this.props.realtime ||
- this.state.resumingLoading ||
- this.state.scrollingToTop
- ) {
- return null
- }
-
- this.setState({
- realtimeUpdates: false,
- })
- } else {
- if (!this.state.topVisible) {
- this.setState({
- topVisible: true,
- })
-
- if (typeof this.props.onTopVisibility === "function") {
- this.props.onTopVisibility(true)
- }
-
- // if (this.props.realtime || !this.state.realtimeUpdates && !this.state.resumingLoading && scrollTop < 5) {
- // this.onResumeRealtimeUpdates()
- // }
- }
- }
- }
-
- componentDidMount = async () => {
- if (typeof this.props.loadFromModel === "function") {
- await this.handleLoad(this.props.loadFromModel)
- }
-
- this.setState({
- initialLoading: false,
- })
-
- if (this.props.realtime) {
- for (const [event, handler] of Object.entries(
- this.timelineWsEvents,
- )) {
- app.cores.api.listenEvent(event, handler, "posts")
- }
-
- app.cores.api.joinTopic(
- "posts",
- this.props.customTopic ?? "realtime:feed",
- )
- }
-
- if (this.listRef && this.listRef.current) {
- this.listRef.current.addEventListener("scroll", this.onScrollList)
- }
-
- window._hacks = this._hacks
- }
-
- componentWillUnmount = async () => {
- if (this.props.realtime) {
- for (const [event, handler] of Object.entries(
- this.timelineWsEvents,
- )) {
- app.cores.api.unlistenEvent(event, handler, "posts")
- }
-
- app.cores.api.leaveTopic(
- "posts",
- this.props.customTopic ?? "realtime:feed",
- )
- }
-
- if (this.listRef && this.listRef.current) {
- this.listRef.current.removeEventListener(
- "scroll",
- this.onScrollList,
- )
- }
-
- window._hacks = null
- }
-
- componentDidUpdate = async (prevProps, prevState) => {
- if (prevProps.list !== this.props.list) {
- this.setState({
- list: this.props.list,
- })
- }
- }
-
- onLikePost = async (data) => {
+const PostActions = {
+ onClickLike: async (data) => {
let result = await PostModel.toggleLike({ post_id: data._id }).catch(
() => {
antd.message.error("Failed to like post")
@@ -379,9 +35,8 @@ export class PostsListsComponent extends React.Component {
)
return result
- }
-
- onSavePost = async (data) => {
+ },
+ onClickSave: async (data) => {
let result = await PostModel.toggleSave({ post_id: data._id }).catch(
() => {
antd.message.error("Failed to save post")
@@ -391,25 +46,8 @@ export class PostsListsComponent extends React.Component {
)
return result
- }
-
- onEditPost = (data) => {
- app.controls.openPostCreator({
- edit_post: data._id,
- })
- }
-
- onReplyPost = (data) => {
- app.controls.openPostCreator({
- reply_to: data._id,
- })
- }
-
- onDoubleClickPost = (data) => {
- app.navigation.goToPost(data._id)
- }
-
- onDeletePost = async (data) => {
+ },
+ onClickDelete: async (data) => {
antd.Modal.confirm({
title: "Are you sure you want to delete this post?",
content: "This action is irreversible",
@@ -422,74 +60,218 @@ export class PostsListsComponent extends React.Component {
})
},
})
- }
+ },
+ onClickEdit: async (data) => {
+ app.controls.openPostCreator({
+ edit_post: data._id,
+ })
+ },
+ onClickReply: async (data) => {
+ app.controls.openPostCreator({
+ reply_to: data._id,
+ })
+ },
+ onDoubleClick: async (data) => {
+ app.navigation.goToPost(data._id)
+ },
+}
- ontoggleOpen = (to, data) => {
- if (typeof this.props.onOpenPost === "function") {
- this.props.onOpenPost(to, data)
+const Entry = (props) => {
+ const { data } = props
+
+ return (
+
+ )
+}
+
+const PostList = React.forwardRef((props, ref) => {
+ return (
+
+
+ {props.list.map((data) => {
+ return
+ })}
+
+
+ )
+})
+
+const PostsListsComponent = (props) => {
+ const [list, setList] = React.useState([])
+ const [hasMore, setHasMore] = React.useState(true)
+
+ // Refs
+ const firstLoad = React.useRef(true)
+ const loading = React.useRef(false)
+ const page = React.useRef(0)
+ const listRef = React.useRef(null)
+ const loadModelPropsRef = React.useRef({})
+
+ const timelineWsEvents = React.useRef({
+ "post:new": (data) => {
+ console.debug("post:new", data)
+
+ setList((prev) => {
+ return [data, ...prev]
+ })
+ },
+ "post:delete": (data) => {
+ console.debug("post:delete", data)
+
+ setList((prev) => {
+ return prev.filter((item) => {
+ return item._id !== data._id
+ })
+ })
+ },
+ "post:update": (data) => {
+ console.debug("post:update", data)
+
+ setList((prev) => {
+ return prev.map((item) => {
+ if (item._id === data._id) {
+ return data
+ }
+ return item
+ })
+ })
+ },
+ })
+
+ // Logic
+ async function handleLoad(fn, params = {}) {
+ if (loading.current === true) {
+ console.warn(`Please wait to load the post before load more`)
+ return
+ }
+
+ loading.current = true
+
+ let payload = {
+ page: page.current,
+ limit: app.cores.settings.get("feed_max_fetch"),
+ }
+
+ if (loadModelPropsRef.current) {
+ payload = {
+ ...payload,
+ ...loadModelPropsRef.current,
+ }
+ }
+
+ const result = await fn(payload).catch((err) => {
+ console.error(err)
+ app.message.error("Failed to load more posts")
+ return null
+ })
+
+ loading.current = false
+ firstLoad.current = false
+
+ if (result) {
+ setHasMore(result.has_more)
+
+ if (result.items?.length > 0) {
+ if (params.replace) {
+ setList(result.items)
+ page.current = 0
+ } else {
+ setList((prev) => {
+ return [...prev, ...result.items]
+ })
+ page.current = page.current + 1
+ }
+ }
}
}
- onLoadMore = async () => {
- if (typeof this.props.onLoadMore === "function") {
- return this.handleLoad(this.props.onLoadMore)
- } else if (this.props.loadFromModel) {
- return this.handleLoad(this.props.loadFromModel)
+ const onLoadMore = React.useCallback(() => {
+ if (typeof props.onLoadMore === "function") {
+ return handleLoad(props.onLoadMore)
+ } else if (props.loadFromModel) {
+ return handleLoad(props.loadFromModel)
}
- }
+ }, [props])
- render() {
- if (this.state.initialLoading) {
- return
+ React.useEffect(() => {
+ if (
+ !lodash.isEqual(props.loadFromModelProps, loadModelPropsRef.current)
+ ) {
+ loadModelPropsRef.current = props.loadFromModelProps
+
+ page.current = 0
+ loading.current = false
+
+ setHasMore(true)
+ setList([])
+ handleLoad(props.loadFromModel)
+ }
+ }, [
+ props.loadFromModel,
+ props.loadFromModelProps,
+ firstLoad.current === false,
+ ])
+
+ React.useEffect(() => {
+ if (props.loadFromModelProps) {
+ loadModelPropsRef.current = props.loadFromModelProps
}
- if (this.state.list.length === 0) {
- if (typeof this.props.emptyListRender === "function") {
- return React.createElement(this.props.emptyListRender)
+ if (typeof props.loadFromModel === "function") {
+ handleLoad(props.loadFromModel)
+ }
+
+ if (props.realtime) {
+ for (const [event, handler] of Object.entries(
+ timelineWsEvents.current,
+ )) {
+ app.cores.api.listenEvent(event, handler, "posts")
}
- return (
-
-
-
Whoa, nothing on here...
-
+ app.cores.api.joinTopic(
+ "posts",
+ props.customTopic ?? "realtime:feed",
)
}
- const PostListProps = {
- list: this.state.list,
+ return () => {
+ if (props.realtime) {
+ for (const [event, handler] of Object.entries(
+ timelineWsEvents.current,
+ )) {
+ app.cores.api.unlistenEvent(event, handler, "posts")
+ }
- disableReplyTag: this.props.disableReplyTag,
- disableHasReplies: this.props.disableHasReplies,
-
- onLikePost: this.onLikePost,
- onSavePost: this.onSavePost,
- onDeletePost: this.onDeletePost,
- onEditPost: this.onEditPost,
- onReplyPost: this.onReplyPost,
- onDoubleClick: this.onDoubleClickPost,
-
- onLoadMore: this.onLoadMore,
- hasMore: this.state.hasMore,
- loading: this.state.loading,
-
- realtimeUpdates: this.state.realtimeUpdates,
- resumingLoading: this.state.resumingLoading,
- onResumeRealtimeUpdates: this.onResumeRealtimeUpdates,
+ app.cores.api.leaveTopic(
+ "posts",
+ props.customTopic ?? "realtime:feed",
+ )
+ }
}
+ }, [])
- if (app.isMobile) {
- return
- }
-
- return (
-
- )
- }
+ return (
+
+ )
}
-export default React.forwardRef((props, ref) => (
-
-))
+export default PostsListsComponent
diff --git a/packages/app/src/pages/account/[username]/tabs/posts/index.jsx b/packages/app/src/pages/account/[username]/tabs/posts/index.jsx
index 32f0c89d..ab59e909 100755
--- a/packages/app/src/pages/account/[username]/tabs/posts/index.jsx
+++ b/packages/app/src/pages/account/[username]/tabs/posts/index.jsx
@@ -1,6 +1,8 @@
import React from "react"
import { Result } from "antd"
+import { useNavigation } from "react-router"
+
import PostsList from "@components/PostsList"
import { Icons } from "@components/Icons"
@@ -14,18 +16,17 @@ const emptyListRender = () => {
)
}
-export default class UserPosts extends React.Component {
- render() {
- console.log(this.props.state)
- return (
-
- )
- }
+const UserPosts = (props) => {
+ return (
+
+ )
}
+
+export default UserPosts
diff --git a/packages/server/services/posts/classes/posts/methods/data.js b/packages/server/services/posts/classes/posts/methods/data.js
index 97c6e590..3515d868 100644
--- a/packages/server/services/posts/classes/posts/methods/data.js
+++ b/packages/server/services/posts/classes/posts/methods/data.js
@@ -19,6 +19,7 @@ export default async (payload = {}) => {
}
let posts = []
+ let total_posts = 0
if (post_id) {
try {
@@ -33,6 +34,8 @@ export default async (payload = {}) => {
.sort(sort)
.limit(limit)
.skip(limit * page)
+
+ total_posts = await Post.countDocuments({ ...query })
}
// fullfill data
@@ -50,5 +53,9 @@ export default async (payload = {}) => {
return posts[0]
}
- return posts
+ return {
+ items: posts,
+ total_items: total_posts,
+ has_more: total_posts > limit * page + 1,
+ }
}
diff --git a/packages/server/services/posts/classes/posts/methods/delete.js b/packages/server/services/posts/classes/posts/methods/delete.js
index 246b166c..fdb6cdb6 100644
--- a/packages/server/services/posts/classes/posts/methods/delete.js
+++ b/packages/server/services/posts/classes/posts/methods/delete.js
@@ -42,11 +42,9 @@ export default async (payload = {}) => {
// broadcast post to all users
if (post.visibility === "public") {
- global.websockets.senders.toTopic(
- "realtime:feed",
- "post:delete",
- post_id,
- )
+ global.websockets.senders.toTopic("realtime:feed", "post:delete", {
+ _id: post_id,
+ })
}
if (post.visibility === "private") {
@@ -55,7 +53,9 @@ export default async (payload = {}) => {
)
for (const userSocket of userSockets) {
- userSocket.emit(`post:delete`, post_id)
+ userSocket.emit(`post:delete`, {
+ _id: post_id,
+ })
}
}