Skip to content

Commit

Permalink
Merge pull request #2307 from daostack/CW-2196-pagination
Browse files Browse the repository at this point in the history
Implement pagination in the chat #2196
  • Loading branch information
pvm-code authored Nov 27, 2023
2 parents ff9ed50 + a06c129 commit 27a892b
Show file tree
Hide file tree
Showing 18 changed files with 1,074 additions and 265 deletions.
2 changes: 2 additions & 0 deletions src/pages/OldCommon/store/saga.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ export function* loadDiscussionDetail(
...(checkIsUserDiscussionMessage(parentMessage) && {
ownerId: parentMessage.ownerId,
}),
createdAt: parentMessage.createdAt,
}
: null;
return newDiscussionMessage;
Expand Down Expand Up @@ -472,6 +473,7 @@ export function* loadProposalDetail(
...(checkIsUserDiscussionMessage(parentMessage) && {
ownerId: parentMessage.ownerId,
}),
createdAt: parentMessage.createdAt,
}
: null;
return newDiscussionMessage;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,16 @@ $phone-breakpoint: 415px;
font-size: $large;
line-height: 2.125rem;
}

.mentionTextCurrentUser {
color: $c-pink-mention-2;
font-weight: 600;
}

.singleEmojiText {
font-size: $xlarge;
}

.multipleEmojiText {
font-size: $large;
}
74 changes: 56 additions & 18 deletions src/pages/common/components/ChatComponent/ChatComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import React, {
ReactNode,
} from "react";
import { useDispatch, useSelector } from "react-redux";
import { useDebounce, useMeasure } from "react-use";
import { useDebounce, useMeasure, useScroll } from "react-use";
import classNames from "classnames";
import isHotkey from "is-hotkey";
import { debounce, delay, omit } from "lodash";
Expand Down Expand Up @@ -36,7 +36,7 @@ import {
CommonMember,
DirectParent,
Discussion,
DiscussionMessage,
DiscussionMessageWithParsedText,
Timestamp,
UserDiscussionMessage,
} from "@/shared/models";
Expand Down Expand Up @@ -94,15 +94,18 @@ interface ChatComponentInterface {
}

interface Messages {
[key: number]: DiscussionMessage[];
[key: number]: DiscussionMessageWithParsedText[];
}

type CreateDiscussionMessageDtoWithFilesPreview = CreateDiscussionMessageDto & {
filesPreview?: FileInfo[] | null;
imagesPreview?: FileInfo[] | null;
};

function groupday(acc: any, currentValue: DiscussionMessage): Messages {
function groupday(
acc: any,
currentValue: DiscussionMessageWithParsedText,
): Messages {
const d = new Date(currentValue.createdAt.seconds * 1000);
const i = Math.floor(d.getTime() / (1000 * 60 * 60 * 24));
const timestamp = i * (1000 * 60 * 60 * 24);
Expand All @@ -113,6 +116,8 @@ function groupday(acc: any, currentValue: DiscussionMessage): Messages {

const CHAT_HOT_KEYS = [HotKeys.Enter, HotKeys.ModEnter, HotKeys.ShiftEnter];

const SCROLL_THRESHOLD = 400;

export default function ChatComponent({
commonId,
type,
Expand Down Expand Up @@ -162,7 +167,13 @@ export default function ChatComponent({
discussionUsers,
fetchDiscussionUsers,
} = useDiscussionChatAdapter({
discussionId,
hasPermissionToHide,
textStyles: {
mentionTextCurrentUser: styles.mentionTextCurrentUser,
singleEmojiText: styles.singleEmojiText,
multipleEmojiText: styles.multipleEmojiText,
},
});
const {
chatMessagesData,
Expand Down Expand Up @@ -232,18 +243,15 @@ export default function ChatComponent({
);

const messages = useMemo(
() => (discussionMessages ?? []).reduce(groupday, {}),
() =>
((discussionMessages ?? []) as DiscussionMessageWithParsedText[]).reduce(
groupday,
{},
),
[discussionMessages],
);
const dateList = useMemo(() => Object.keys(messages), [messages]);

useEffect(() => {
if (discussionId) {
discussionMessagesData.fetchDiscussionMessages(discussionId);
dispatch(chatActions.clearCurrentDiscussionMessageReply());
}
}, [discussionId]);

const [newMessages, setMessages] = useState<
CreateDiscussionMessageDtoWithFilesPreview[]
>([]);
Expand Down Expand Up @@ -451,6 +459,7 @@ export default function ChatComponent({
text: discussionMessageReply.text,
files: discussionMessageReply.files,
images: discussionMessageReply.images,
createdAt: discussionMessageReply.createdAt,
}
: null,
images: imagesPreview?.map((file) =>
Expand Down Expand Up @@ -478,10 +487,7 @@ export default function ChatComponent({
});
} else {
pendingMessages.forEach((pendingMessage) => {
discussionMessagesData.addDiscussionMessage(
discussionId,
pendingMessage,
);
discussionMessagesData.addDiscussionMessage(pendingMessage);
});
}

Expand Down Expand Up @@ -686,6 +692,35 @@ export default function ChatComponent({
);
};

const { y } = useScroll(chatContainerRef);

const isTopReached = useMemo(() => {
const currentScrollPosition = Math.abs(y); // Since y can be negative
const container = chatContainerRef.current;

if (!container) return false;

const scrollHeight = container.scrollHeight;
const clientHeight = container.clientHeight;

return (
scrollHeight - clientHeight - currentScrollPosition <= SCROLL_THRESHOLD
);
}, [y]);

useEffect(() => {
if (discussionId) {
discussionMessagesData.fetchDiscussionMessages();
dispatch(chatActions.clearCurrentDiscussionMessageReply());
}
}, [discussionId, dispatch]);

useEffect(() => {
if (isTopReached && discussionId) {
discussionMessagesData.fetchDiscussionMessages();
}
}, [isTopReached, discussionId]);

return (
<div className={styles.chatWrapper}>
<div
Expand All @@ -698,6 +733,10 @@ export default function ChatComponent({
<ChatContentContext.Provider value={chatContentContextValue}>
<ChatContent
ref={chatContentRef}
discussionMessages={discussionMessagesData.data}
fetchReplied={discussionMessagesData.fetchRepliedMessages}
isChatChannel={isChatChannel}
discussionId={discussionId}
type={type}
commonMember={commonMember}
governanceCircles={governanceCircles}
Expand All @@ -707,7 +746,6 @@ export default function ChatComponent({
lastSeenItem={lastSeenItem}
hasPermissionToHide={hasPermissionToHide}
users={users}
discussionId={discussionId}
feedItemId={feedItemId}
isLoading={!discussion || isLoadingDiscussionMessages}
onMessageDelete={handleMessageDelete}
Expand All @@ -717,7 +755,7 @@ export default function ChatComponent({
onInternalLinkClick={onInternalLinkClick}
isEmpty={
discussionMessagesData.fetched &&
!discussionMessagesData.data?.length && // for non direct messages chats. not using messageCount because it includes the deleted messages as well.
!discussionMessagesData.rawData?.length && // for non direct messages chats. not using messageCount because it includes the deleted messages as well.
Object.keys(discussionMessages).length === 0 // for direct messages chats
}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,26 @@ import { useSelector } from "react-redux";
import { scroller, animateScroll } from "react-scroll";
import { v4 as uuidv4 } from "uuid";
import { selectUser } from "@/pages/Auth/store/selectors";
import { ChatMessage, InternalLinkData } from "@/shared/components";
import { DiscussionMessageService } from "@/services";
import {
ChatMessage,
InternalLinkData,
DMChatMessage,
} from "@/shared/components";
import {
ChatType,
LOADER_APPEARANCE_DELAY,
QueryParamKey,
LOADER_APPEARANCE_DELAY,
} from "@/shared/constants";
import { useForceUpdate, useQueryParams } from "@/shared/hooks";
import { useQueryParams } from "@/shared/hooks";
import {
checkIsUserDiscussionMessage,
CommonFeedObjectUserUnique,
CommonMember,
DirectParent,
DiscussionMessage,
User,
Circles,
DiscussionMessageWithParsedText,
} from "@/shared/models";
import { Loader } from "@/shared/ui-kit";
import { formatDate } from "@/shared/utils";
Expand All @@ -42,7 +47,8 @@ interface ChatContentInterface {
commonMember: CommonMember | null;
governanceCircles?: Circles;
chatWrapperId: string;
messages: Record<number, DiscussionMessage[]>;
messages: Record<number, DiscussionMessageWithParsedText[]>;
discussionMessages: DiscussionMessageWithParsedText[] | null;
dateList: string[];
lastSeenItem?: CommonFeedObjectUserUnique["lastSeen"];
hasPermissionToHide: boolean;
Expand All @@ -56,6 +62,8 @@ interface ChatContentInterface {
onFeedItemClick?: (feedItemId: string) => void;
onInternalLinkClick?: (data: InternalLinkData) => void;
isEmpty?: boolean;
isChatChannel: boolean;
fetchReplied: (messageId: string, endDate: Date) => Promise<void>;
}

const isToday = (someDate: Date) => {
Expand All @@ -76,7 +84,6 @@ const ChatContent: ForwardRefRenderFunction<
commonMember,
governanceCircles,
chatWrapperId,
messages,
dateList,
lastSeenItem,
hasPermissionToHide,
Expand All @@ -90,20 +97,17 @@ const ChatContent: ForwardRefRenderFunction<
onFeedItemClick,
onInternalLinkClick,
isEmpty,
messages,
isChatChannel,
fetchReplied,
discussionMessages,
},
chatContentRef,
) => {
const user = useSelector(selectUser());
const userId = user?.uid;
const queryParams = useQueryParams();
const messageIdParam = queryParams[QueryParamKey.Message];
const forceUpdate = useForceUpdate();

useEffect(() => {
if (messages) {
forceUpdate();
}
}, [messages]);

const [highlightedMessageId, setHighlightedMessageId] = useState(
() => (typeof messageIdParam === "string" && messageIdParam) || null,
Expand Down Expand Up @@ -154,7 +158,25 @@ const ChatContent: ForwardRefRenderFunction<
setScrolledToMessage(true);
}, [chatWrapperId, highlightedMessageId, dateList.length, scrolledToMessage]);

function scrollToRepliedMessage(messageId: string) {
const [shouldScrollToElementId, setShouldScrollToElementId] =
useState<string>();

useEffect(() => {
if (
shouldScrollToElementId &&
discussionMessages?.find((item) => item.id === shouldScrollToElementId)
) {
setHighlightedMessageId(shouldScrollToElementId);
setShouldScrollToElementId("");
}
}, [shouldScrollToElementId, discussionMessages]);

async function scrollToRepliedMessage(messageId: string, endDate: Date) {
await fetchReplied(messageId, endDate);
setShouldScrollToElementId(messageId);
}

function scrollToRepliedMessageDMChat(messageId: string) {
scroller.scrollTo(messageId, {
containerId: chatWrapperId,
delay: 0,
Expand All @@ -167,7 +189,20 @@ const ChatContent: ForwardRefRenderFunction<

useEffect(() => {
if (typeof messageIdParam === "string") {
setHighlightedMessageId(messageIdParam);
(async () => {
try {
const messageData =
await DiscussionMessageService.getDiscussionMessageById(
messageIdParam,
);
scrollToRepliedMessage(
messageData.id,
messageData.createdAt.toDate(),
);
} catch (err) {
setShouldScrollToElementId("");
}
})();
}
}, [messageIdParam]);

Expand Down Expand Up @@ -218,7 +253,26 @@ const ChatContent: ForwardRefRenderFunction<
const isMyMessageNext =
checkIsUserDiscussionMessage(nextMessage) &&
nextMessage.ownerId === userId;
const messageEl = (
const messageEl = isChatChannel ? (
<DMChatMessage
key={message.id}
user={user}
discussionMessage={message}
chatType={type}
scrollToRepliedMessage={scrollToRepliedMessageDMChat}
highlighted={message.id === highlightedMessageId}
hasPermissionToHide={hasPermissionToHide}
users={users}
feedItemId={feedItemId}
commonMember={commonMember}
governanceCircles={governanceCircles}
onMessageDelete={onMessageDelete}
directParent={directParent}
onUserClick={onUserClick}
onFeedItemClick={onFeedItemClick}
onInternalLinkClick={onInternalLinkClick}
/>
) : (
<ChatMessage
key={message.id}
user={user}
Expand Down
Loading

0 comments on commit 27a892b

Please sign in to comment.