Skip to content

Commit

Permalink
Various scrolling and swiping comment logic fixes (aeharding#48)
Browse files Browse the repository at this point in the history
* Various scrolling and swiping comment logic fixes

- Add highlighted comment in a comment context chain scroll into view
- Fix swipe to collapse not collapsing entire root comment chain
- Add scroll to top of root comment chain upon swipe to collapse

* Move root comment collapsing to sliding comment component
  • Loading branch information
aeharding authored Jun 28, 2023
1 parent 23d7a28 commit 0da72f7
Show file tree
Hide file tree
Showing 10 changed files with 96 additions and 44 deletions.
28 changes: 25 additions & 3 deletions src/features/auth/AppContext.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import React, { RefObject, createContext, useState } from "react";
import { useIonViewDidEnter } from "@ionic/react";
import React, {
RefObject,
createContext,
useContext,
useEffect,
useState,
} from "react";
import { VirtuosoHandle } from "react-virtuoso";

type Page = HTMLElement | RefObject<VirtuosoHandle>;

interface IAppContext {
// used for determining whether page needs to be scrolled up first
activePage: HTMLElement | RefObject<VirtuosoHandle> | undefined;
setActivePage: (activePage: HTMLElement | RefObject<VirtuosoHandle>) => void;
activePage: Page | undefined;
setActivePage: (activePage: Page) => void;
}

export const AppContext = createContext<IAppContext>({
Expand All @@ -27,3 +36,16 @@ export function AppContextProvider({
</AppContext.Provider>
);
}

export function useSetActivePage(page?: Page) {
const { activePage, setActivePage } = useContext(AppContext);

useIonViewDidEnter(() => {
if (page) setActivePage(page);
});

useEffect(() => {
if (!activePage && page) setActivePage(page);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [page]);
}
24 changes: 22 additions & 2 deletions src/features/comment/Comment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { IonIcon, IonItem } from "@ionic/react";
import { chevronDownOutline } from "ionicons/icons";
import { CommentView } from "lemmy-js-client";
import { css } from "@emotion/react";
import React from "react";
import React, { useEffect, useRef } from "react";
import Ago from "../labels/Ago";
import { maxWidthCss } from "../shared/AppContent";
import PersonLink from "../labels/links/PersonLink";
Expand All @@ -28,6 +28,8 @@ const rainbowColors = [
];

const CustomIonItem = styled(IonItem)`
scroll-margin-bottom: 35vh;
--padding-start: 0;
--inner-padding-end: 0;
--border-style: none;
Expand Down Expand Up @@ -166,6 +168,8 @@ interface CommentProps {
context?: React.ReactNode;

className?: string;

rootIndex?: number;
}

export default function Comment({
Expand All @@ -179,15 +183,30 @@ export default function Comment({
context,
routerLink,
className,
rootIndex,
}: CommentProps) {
const keyPressed = useKeyPressed();
// eslint-disable-next-line no-undef
const commentRef = useRef<HTMLIonItemElement>(null);

useEffect(() => {
if (highlightedCommentId !== comment.comment.id) return;

setTimeout(() => {
commentRef.current?.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "nearest",
});
}, 100);
}, [highlightedCommentId, comment]);

return (
<AnimateHeight duration={200} height={fullyCollapsed ? 0 : "auto"}>
<SlidingNestedCommentVote
item={comment}
className={className}
collapse={() => onClick?.()}
rootIndex={rootIndex}
collapsed={!!collapsed}
>
<CustomIonItem
Expand All @@ -196,6 +215,7 @@ export default function Comment({
onClick={() => {
if (!keyPressed) onClick?.();
}}
ref={commentRef}
>
<PositionedContainer
depth={depth || 0}
Expand Down
4 changes: 4 additions & 0 deletions src/features/comment/CommentTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface CommentTreeProps {
first?: boolean;
op: Person;
fullyCollapsed?: boolean;
rootIndex: number;
}

export default function CommentTree({
Expand All @@ -20,6 +21,7 @@ export default function CommentTree({
first,
op,
fullyCollapsed,
rootIndex,
}: CommentTreeProps) {
const dispatch = useAppDispatch();
const commentCollapsedById = useAppSelector(
Expand Down Expand Up @@ -53,6 +55,7 @@ export default function CommentTree({
collapsed={collapsed}
childCount={childCount}
fullyCollapsed={!!fullyCollapsed}
rootIndex={rootIndex}
/>
</React.Fragment>,
...comment.children.map((comment) => (
Expand All @@ -62,6 +65,7 @@ export default function CommentTree({
comment={comment}
op={op}
fullyCollapsed={collapsed || fullyCollapsed}
rootIndex={rootIndex}
/>
)),
,
Expand Down
11 changes: 4 additions & 7 deletions src/features/comment/Comments.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { buildCommentsTree } from "../../helpers/lemmy";
import CommentTree from "./CommentTree";
import {
IonRefresher,
IonRefresherContent,
IonSpinner,
useIonToast,
useIonViewWillEnter,
} from "@ionic/react";
import styled from "@emotion/styled";
import { css } from "@emotion/react";
Expand All @@ -18,7 +17,7 @@ import { receivedComments } from "./commentSlice";
import { RefresherCustomEvent } from "@ionic/core";
import { getPost } from "../post/postSlice";
import useClient from "../../helpers/useClient";
import { AppContext } from "../auth/AppContext";
import { useSetActivePage } from "../auth/AppContext";
import { PostContext } from "../post/detail/PostContext";
import { jwtSelector } from "../auth/authSlice";

Expand Down Expand Up @@ -85,12 +84,9 @@ export default function Comments({
: undefined;
const commentId = commentPath ? +commentPath.split(".")[1] : undefined;

const { setActivePage } = useContext(AppContext);
const virtuosoRef = useRef<VirtuosoHandle>(null);

useIonViewWillEnter(() => {
setActivePage(virtuosoRef);
});
useSetActivePage(virtuosoRef);

useEffect(() => {
fetchComments(true);
Expand Down Expand Up @@ -199,6 +195,7 @@ export default function Comments({
key={comment.comment_view.comment.id}
first={index === 0}
op={op}
rootIndex={index + 1} /* Plus header index = 0 */
/>
));
}, [commentTree, comments.length, highlightedCommentId, loading, op]);
Expand Down
5 changes: 5 additions & 0 deletions src/features/comment/commentSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ export const commentSlice = createSlice({
state.commentCollapsedById[action.payload.commentId] =
action.payload.collapsed;
},
toggleCommentCollapseState: (state, action: PayloadAction<number>) => {
state.commentCollapsedById[action.payload] =
!state.commentCollapsedById[action.payload];
},
updateCommentVote: (
state,
action: PayloadAction<{ commentId: number; vote: -1 | 1 | 0 | undefined }>
Expand All @@ -47,6 +51,7 @@ export const commentSlice = createSlice({
export const {
receivedComments,
updateCommentCollapseState,
toggleCommentCollapseState,
updateCommentVote,
resetComments,
} = commentSlice.actions;
Expand Down
10 changes: 3 additions & 7 deletions src/features/feed/Feed.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, {
ComponentType,
useCallback,
useContext,
useEffect,
useRef,
useState,
Expand All @@ -12,12 +11,11 @@ import {
IonRefresherContent,
RefresherCustomEvent,
useIonToast,
useIonViewDidEnter,
} from "@ionic/react";
import { LIMIT } from "../../services/lemmy";
import { CenteredSpinner } from "../post/detail/PostDetail";
import { pullAllBy } from "lodash";
import { AppContext } from "../auth/AppContext";
import { useSetActivePage } from "../auth/AppContext";
import EndPost from "./EndPost";

export type FetchFn<I> = (page: number) => Promise<I[]>;
Expand All @@ -43,12 +41,9 @@ export default function Feed<I>({
const [atEnd, setAtEnd] = useState(false);
const [present] = useIonToast();

const { setActivePage } = useContext(AppContext);
const virtuosoRef = useRef<VirtuosoHandle>(null);

useIonViewDidEnter(() => {
setActivePage(virtuosoRef);
});
useSetActivePage(virtuosoRef);

useEffect(() => {
fetchMore(true);
Expand Down Expand Up @@ -121,6 +116,7 @@ export default function Feed<I>({
>
<IonRefresherContent />
</IonRefresher>

<Virtuoso
ref={virtuosoRef}
style={{ height: "100%" }}
Expand Down
30 changes: 25 additions & 5 deletions src/features/shared/sliding/SlidingNestedCommentVote.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useIonModal } from "@ionic/react";
import { arrowUndo, chevronCollapse, chevronExpand } from "ionicons/icons";
import React, { useContext, useMemo } from "react";
import React, { useCallback, useContext, useMemo } from "react";
import { SlidingItemAction } from "./SlidingItem";
import { CommentView } from "lemmy-js-client";
import CommentReply from "../../comment/reply/CommentReply";
Expand All @@ -10,25 +10,30 @@ import BaseSlidingVote from "./BaseSlidingVote";
import { useAppSelector } from "../../../store";
import { jwtSelector } from "../../auth/authSlice";
import Login from "../../auth/Login";
import { AppContext } from "../../auth/AppContext";
import { useDispatch } from "react-redux";
import { toggleCommentCollapseState } from "../../comment/commentSlice";

interface SlidingVoteProps {
children: React.ReactNode;
className?: string;
item: CommentView;
collapse: () => void;
rootIndex: number | undefined;
collapsed: boolean;
}

export default function SlidingNestedCommentVote({
children,
className,
item,
collapse,
rootIndex,
collapsed,
}: SlidingVoteProps) {
const dispatch = useDispatch();
const { refreshPost } = useContext(PostContext);
const pageContext = useContext(PageContext);
const jwt = useAppSelector(jwtSelector);
const { activePage } = useContext(AppContext);

const [login, onDismissLogin] = useIonModal(Login, {
onDismiss: (data: string, role: string) => onDismissLogin(data, role),
Expand All @@ -42,12 +47,27 @@ export default function SlidingNestedCommentVote({
item,
});

const collapseRootComment = useCallback(() => {
if (!rootIndex) return;

const rootCommentId = +item.comment.path.split(".")[1];

dispatch(toggleCommentCollapseState(rootCommentId));

if (!activePage || !("current" in activePage)) return;

activePage.current?.scrollToIndex({
index: rootIndex,
behavior: "smooth",
});
}, [activePage, dispatch, item.comment.path, rootIndex]);

const endActions: [SlidingItemAction, SlidingItemAction] = useMemo(() => {
return [
{
render: collapsed ? chevronExpand : chevronCollapse,
trigger: () => {
collapse();
collapseRootComment();
},
bgColor: "tertiary",
},
Expand All @@ -61,7 +81,7 @@ export default function SlidingNestedCommentVote({
bgColor: "primary",
},
];
}, [pageContext.page, reply, jwt, login, collapse, collapsed]);
}, [collapsed, collapseRootComment, jwt, login, pageContext.page, reply]);

return (
<BaseSlidingVote endActions={endActions} className={className} item={item}>
Expand Down
10 changes: 3 additions & 7 deletions src/pages/posts/CommunitiesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
IonPage,
IonTitle,
IonToolbar,
useIonViewWillEnter,
} from "@ionic/react";
import AppContent from "../../features/shared/AppContent";
import { useParams } from "react-router";
Expand All @@ -19,8 +18,8 @@ import { home, library, people } from "ionicons/icons";
import styled from "@emotion/styled";
import { pullAllBy, sortBy, uniqBy } from "lodash";
import { notEmpty } from "../../helpers/array";
import { useContext, useMemo, useRef } from "react";
import { AppContext } from "../../features/auth/AppContext";
import { useMemo, useRef } from "react";
import { useSetActivePage } from "../../features/auth/AppContext";
import { useBuildGeneralBrowseLink } from "../../helpers/routes";
import ItemIcon from "../../features/labels/img/ItemIcon";
import { jwtSelector } from "../../features/auth/authSlice";
Expand Down Expand Up @@ -51,7 +50,6 @@ const Content = styled.div`

export default function CommunitiesPage() {
const buildGeneralBrowseLink = useBuildGeneralBrowseLink();
const { setActivePage } = useContext(AppContext);
const { actor } = useParams<{ actor: string }>();
const jwt = useAppSelector(jwtSelector);
const pageRef = useRef();
Expand All @@ -62,9 +60,7 @@ export default function CommunitiesPage() {
(state) => state.community.communityByHandle
);

useIonViewWillEnter(() => {
if (pageRef.current) setActivePage(pageRef.current);
});
useSetActivePage(pageRef.current);

const communities = useMemo(() => {
const communities = uniqBy(
Expand Down
8 changes: 2 additions & 6 deletions src/pages/profile/ProfilePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
IonTitle,
IonToolbar,
useIonModal,
useIonViewWillEnter,
} from "@ionic/react";
import AppContent from "../../features/shared/AppContent";
import {
Expand All @@ -25,7 +24,7 @@ import { InsetIonItem, SettingLabel } from "../../features/user/Profile";
import { ReactComponent as IncognitoSvg } from "../../features/user/incognito.svg";
import styled from "@emotion/styled";
import UserPage from "../shared/UserPage";
import { AppContext } from "../../features/auth/AppContext";
import { useSetActivePage } from "../../features/auth/AppContext";
import { swapHorizontalOutline } from "ionicons/icons";
import { css } from "@emotion/react";
import AccountSwitcher from "../../features/auth/AccountSwitcher";
Expand All @@ -43,7 +42,6 @@ const Incognito = styled(IncognitoSvg)`
export default function ProfilePage() {
const dispatch = useAppDispatch();
const pageRef = useRef();
const { setActivePage } = useContext(AppContext);
const connectedInstance = useAppSelector(
(state) => state.auth.connectedInstance
);
Expand All @@ -66,9 +64,7 @@ export default function ProfilePage() {
}
);

useIonViewWillEnter(() => {
if (pageRef.current) setActivePage(pageRef.current);
});
useSetActivePage(pageRef.current);

if (jwt)
return (
Expand Down
Loading

0 comments on commit 0da72f7

Please sign in to comment.