diff --git a/package.json b/package.json index 58e1785..bc2835e 100644 --- a/package.json +++ b/package.json @@ -48,11 +48,11 @@ "react": "^18.2.0", "react-advanced-cropper": "^0.19.6", "react-colorful": "^5.6.1", - "react-content-loader": "^7.0.2", "react-device-detect": "^2.2.3", "react-dom": "^18.2.0", "react-fps-stats": "^0.3.1", "react-hook-form": "^7.51.3", + "react-infinite-scroll-component": "^6.1.0", "react-loading-skeleton": "^3.4.0", "react-markdown": "^9.0.1", "react-measure": "^2.5.2", @@ -71,7 +71,6 @@ "reoverlay": "^1.0.3", "styled-components": "5.3.11", "use-resize-observer": "^9.1.0", - "virtua": "^0.33.1", "yup": "^1.4.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c63368..4f01c47 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,9 +137,6 @@ dependencies: react-colorful: specifier: ^5.6.1 version: 5.6.1(react-dom@18.2.0)(react@18.2.0) - react-content-loader: - specifier: ^7.0.2 - version: 7.0.2(react@18.2.0) react-device-detect: specifier: ^2.2.3 version: 2.2.3(react-dom@18.2.0)(react@18.2.0) @@ -152,6 +149,9 @@ dependencies: react-hook-form: specifier: ^7.51.3 version: 7.51.3(react@18.2.0) + react-infinite-scroll-component: + specifier: ^6.1.0 + version: 6.1.0(react@18.2.0) react-loading-skeleton: specifier: ^3.4.0 version: 3.4.0(react@18.2.0) @@ -206,9 +206,6 @@ dependencies: use-resize-observer: specifier: ^9.1.0 version: 9.1.0(react-dom@18.2.0)(react@18.2.0) - virtua: - specifier: ^0.33.1 - version: 0.33.1(react-dom@18.2.0)(react@18.2.0) yup: specifier: ^1.4.0 version: 1.4.0 @@ -12959,15 +12956,6 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /react-content-loader@7.0.2(react@18.2.0): - resolution: {integrity: sha512-773S98JTyC8VB2nu7LXUhpHx8tZMieGxMcx3qTe7IkohT6Br7d9AXnIXs/wQ6IhlUdKQcw6JLKk1QKigYCWDRA==} - engines: {node: '>=10'} - peerDependencies: - react: '>=16.0.0' - dependencies: - react: 18.2.0 - dev: false - /react-dev-utils@12.0.1(eslint@8.57.0)(typescript@5.4.5)(webpack@5.90.3): resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==} engines: {node: '>=14'} @@ -13064,6 +13052,15 @@ packages: react: 18.2.0 dev: false + /react-infinite-scroll-component@6.1.0(react@18.2.0): + resolution: {integrity: sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==} + peerDependencies: + react: '>=16.0.0' + dependencies: + react: 18.2.0 + throttle-debounce: 2.3.0 + dev: false + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -14803,6 +14800,11 @@ packages: /throat@6.0.2: resolution: {integrity: sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==} + /throttle-debounce@2.3.0: + resolution: {integrity: sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==} + engines: {node: '>=8'} + dev: false + /through2@2.0.5: resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} dependencies: @@ -15289,30 +15291,6 @@ packages: vfile-message: 4.0.2 dev: false - /virtua@0.33.1(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-xOzF5J+4KN12w1k7rNbocxjLYrWmGP2gXEDXqcHz7zWkf2vrCrsP/N3M/m3RhME/f/bNc5X22cvpIPqQeOP2MA==} - peerDependencies: - react: '>=16.14.0' - react-dom: '>=16.14.0' - solid-js: '>=1.0' - svelte: '>=4.0' - vue: '>=3.2' - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true - solid-js: - optional: true - svelte: - optional: true - vue: - optional: true - dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false - /vite-plugin-chunk-split@0.5.0(vite@5.2.7): resolution: {integrity: sha512-pasNKLhH+ICjoCF6HoKKvgmZ1LEPSCIKAa8Lz0ZpMyQC9bLmCLT7UxgKMULewsc9SUw89OX0udsGiIQCtr8wLA==} peerDependencies: diff --git a/src/components/SkeletonLoader.tsx b/src/components/SkeletonLoader.tsx deleted file mode 100644 index d2fa6c6..0000000 --- a/src/components/SkeletonLoader.tsx +++ /dev/null @@ -1,27 +0,0 @@ -function SkeletonLoader() { - return ( - - Loading... - - - - - - - - - - - - ); -} - -export default SkeletonLoader; diff --git a/src/components/messaging/MessageGroup.tsx b/src/components/messaging/MessageGroup.tsx index a3efe73..5f738b3 100644 --- a/src/components/messaging/MessageGroup.tsx +++ b/src/components/messaging/MessageGroup.tsx @@ -18,7 +18,7 @@ function MessageGroup({ group }: Props) { <> {messages.map((message, index) => { if (message.type === MessageType.Default || message.type === MessageType.Reply) { - return ; + return ; } else return ; })} diff --git a/src/components/messaging/MessageList.tsx b/src/components/messaging/MessageList.tsx index a1a944c..94d39b0 100644 --- a/src/components/messaging/MessageList.tsx +++ b/src/components/messaging/MessageList.tsx @@ -1,8 +1,9 @@ import { observer } from "mobx-react-lite"; -import React, { useEffect, useRef, useState } from "react"; +import React from "react"; +import InfiniteScroll from "react-infinite-scroll-component"; +import PulseLoader from "react-spinners/PulseLoader"; import styled from "styled-components"; import useResizeObserver from "use-resize-observer"; -import { VList, VListHandle } from "virtua"; import useLogger from "../../hooks/useLogger"; import { useAppStore } from "../../stores/AppStore"; import { MessageGroup as MessageGroupType } from "../../stores/MessageStore"; @@ -10,95 +11,40 @@ import Channel from "../../stores/objects/Channel"; import Guild from "../../stores/objects/Guild"; import { Permissions } from "../../utils/Permissions"; import { HorizontalDivider } from "../Divider"; -import SkeletonLoader from "../SkeletonLoader.tsx"; import MessageGroup from "./MessageGroup"; export const MessageAreaWidthContext = React.createContext(0); export const MESSAGE_AREA_PADDING = 82; const Container = styled.div` + flex: 1 1 auto; + overflow-y: auto; display: flex; - flex: 1; + flex-direction: column-reverse; `; const EndMessageContainer = styled.div` margin: 16px 16px 0 16px; `; -const Spacer = styled.div` - height: 20px; -`; - interface Props { guild: Guild; channel: Channel; - before?: string; } /** * Main component for rendering the messages list of a channel */ -function MessageList({ guild, channel, before }: Props) { +function MessageList({ guild, channel }: Props) { const app = useAppStore(); const logger = useLogger("MessageList.tsx"); - const [hasMore, setHasMore] = useState(true); - const [canView, setCanView] = useState(false); - const [loading, setLoading] = useState(true); + const [hasMore, setHasMore] = React.useState(true); + const [canView, setCanView] = React.useState(false); const messageGroups = channel.messages.groups; - const wrapperRef = useRef(null); - const { width } = useResizeObserver({ ref: wrapperRef }); - const ref = useRef(null); + const ref = React.useRef(null); + const { width } = useResizeObserver({ ref }); - useEffect(() => { - // if (before) { - // // find message group containing the message - // const group = messageGroups.find((x) => x.messages.some((y) => y.id === before)); - // const index = group - // ? messageGroups.indexOf(group) + - // (hasMore - // ? 10 - // : 0) /** +10 to account for the "skeleton" divs used to add some padding for scrolling */ - // : -1; - // if (index !== -1) { - // ref.current?.scrollToIndex(index); - - // return; - // } - // } - - // this is for switching to a channel with cached messages, it ensures that we correctly set hasMore to false if there are messages but its less than 50 - if (channel.messages.count > 0 && channel.messages.count < 50) setHasMore(false); - - const hasSkeleton = hasMore || loading; - ref.current?.scrollToIndex(channel.messages.count + (hasSkeleton ? 30 : 0)); - }, [messageGroups, hasMore, loading]); - - const fetchMore = React.useCallback(() => { - setLoading(true); - if (!channel.messages.count) { - logger.warn("channel has no messages, aborting!"); - setLoading(false); - return; - } - // get last group - const lastGroup = messageGroups[messageGroups.length - 1]; - if (!lastGroup) { - logger.warn("No last group found, aborting fetchMore"); - setLoading(false); - return; - } - // ignore queued messages - if ("status" in lastGroup.messages[0]) return; - // get first message in the group to use as before - const before = lastGroup.messages[0].id; - logger.debug(`Fetching 50 messages before ${before} for channel ${channel.id}`); - channel.getMessages(app, false, 50, before).then((r) => { - if (r < 50) setHasMore(false); - setLoading(false); - }); - }, [channel, messageGroups]); - - useEffect(() => { + React.useEffect(() => { const permission = Permissions.getPermission(app.account!.id, guild, channel); const hasPermission = permission.has("READ_MESSAGE_HISTORY"); setCanView(hasPermission); @@ -109,24 +55,43 @@ function MessageList({ guild, channel, before }: Props) { } if (guild && channel && channel.messages.count === 0) { - logger.debug(`Fetching 50 messages for channel ${channel.id}`); channel.getMessages(app, true).then((r) => { - if (r < 50) setHasMore(false); - - setLoading(false); + if (r < 50) { + setHasMore(false); + } }); - } else if (channel.messages.count !== 0) { - setLoading(false); } return () => { logger.debug("MessageList unmounted"); - setLoading(true); setHasMore(true); setCanView(false); }; }, [guild, channel]); + const fetchMore = React.useCallback(() => { + if (!channel.messages.count) { + logger.warn("channel has no messages, aborting!"); + return; + } + // get last group + const lastGroup = messageGroups[messageGroups.length - 1]; + if (!lastGroup) { + logger.warn("No last group found, aborting fetchMore"); + return; + } + // ignore queued messages + if ("status" in lastGroup.messages[0]) return; + // get first message in the group to use as before + const before = lastGroup.messages[0].id; + logger.debug(`Fetching 50 messages before ${before} for channel ${channel.id}`); + channel.getMessages(app, false, 50, before).then((r) => { + if (r < 50) { + setHasMore(false); + } + }); + }, [channel, messageGroups]); + const renderGroup = React.useCallback( (group: MessageGroupType) => ( @@ -136,21 +101,34 @@ function MessageList({ guild, channel, before }: Props) { return ( - + {canView ? ( - - {Array.from({ - length: hasMore || loading ? 30 : 0, - }).map((_, i) => ( - - ))} - {!loading && !hasMore && ( + display: "flex", + flexDirection: "column-reverse", + marginBottom: 30, + overflow: "hidden", + }} // to put endMessage and loader to the top. + hasMore={hasMore} + inverse={true} + loader={ + + } + // FIXME: seems to be broken in react-infinite-scroll-component when using inverse + scrollThreshold={0.5} + scrollableTarget="scrollable-div" + endMessage={

Welcome to #{channel.name}!

@@ -158,10 +136,10 @@ function MessageList({ guild, channel, before }: Props) {

- )} - {messageGroups.map((group, index) => renderGroup(group))} - -
+ } + > + {messageGroups.map((group) => renderGroup(group))} + ) : (
b.timestamp.getTime() - a.timestamp.getTime()) + .sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()) .reduce((groups, message) => { const lastGroup = groups[groups.length - 1]; const lastMessage = lastGroup?.messages[lastGroup.messages.length - 1]; @@ -95,7 +95,7 @@ export default class MessageStore { lastMessage.author.id === message.author.id && lastMessage.type === message.type && message.type === MessageType.Default && - lastMessage.timestamp.getTime() - message.timestamp.getTime() <= 10 * 60 * 1000 + message.timestamp.getTime() - lastMessage.timestamp.getTime() <= 10 * 60 * 1000 ) { // add to last group lastGroup.messages.unshift(message);