Fix linked-comment scrolling

I think this the best solution so far, at the expense of a slight delay in scrolling if the network call stalls.

- Added "fetching by ID" state so that we don't need to use the ugly N-retries method.
- `scrollIntoView` doesn't work if the element is already in the viewport, and the `scrollBy` adjustment doesn't take into account the y-position restoration that we perform on certain type of pages. Use `window.scrollTo` instead and taking into account current scroll position.
This commit is contained in:
infinite-persistence 2021-10-01 15:49:37 +08:00
parent 2cebdc3113
commit c71b90cecf
No known key found for this signature in database
GPG key ID: B9C3252EDC3D0AA0
7 changed files with 27 additions and 14 deletions

View file

@ -43,6 +43,7 @@ declare type CommentsState = {
linkedCommentAncestors: { [string]: Array<string> }, // {"linkedCommentId": ["parentId", "grandParentId", ...]} linkedCommentAncestors: { [string]: Array<string> }, // {"linkedCommentId": ["parentId", "grandParentId", ...]}
pinnedCommentsById: {}, // ClaimId -> array of pinned comment IDs pinnedCommentsById: {}, // ClaimId -> array of pinned comment IDs
isLoading: boolean, isLoading: boolean,
isLoadingById: boolean,
isLoadingByParentId: { [string]: boolean }, isLoadingByParentId: { [string]: boolean },
myComments: ?Set<string>, myComments: ?Set<string>,
isFetchingReacts: boolean, isFetchingReacts: boolean,

View file

@ -9,6 +9,7 @@ import {
makeSelectTopLevelCommentsForUri, makeSelectTopLevelCommentsForUri,
makeSelectTopLevelTotalPagesForUri, makeSelectTopLevelTotalPagesForUri,
selectIsFetchingComments, selectIsFetchingComments,
selectIsFetchingCommentsById,
selectIsFetchingReacts, selectIsFetchingReacts,
makeSelectTotalCommentsCountForUri, makeSelectTotalCommentsCountForUri,
selectOthersReactsById, selectOthersReactsById,
@ -33,6 +34,7 @@ const select = (state, props) => {
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state), claimIsMine: makeSelectClaimIsMine(props.uri)(state),
isFetchingComments: selectIsFetchingComments(state), isFetchingComments: selectIsFetchingComments(state),
isFetchingCommentsById: selectIsFetchingCommentsById(state),
isFetchingReacts: selectIsFetchingReacts(state), isFetchingReacts: selectIsFetchingReacts(state),
fetchingChannels: selectFetchingMyChannels(state), fetchingChannels: selectFetchingMyChannels(state),
settingsByChannelId: selectSettingsByChannelId(state), settingsByChannelId: selectSettingsByChannelId(state),

View file

@ -14,14 +14,12 @@ import usePersistedState from 'effects/use-persisted-state';
import { ENABLE_COMMENT_REACTIONS } from 'config'; import { ENABLE_COMMENT_REACTIONS } from 'config';
import Empty from 'component/common/empty'; import Empty from 'component/common/empty';
import debounce from 'util/debounce'; import debounce from 'util/debounce';
import useFetched from 'effects/use-fetched';
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize'; import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
import { getChannelIdFromClaim } from 'util/claim'; import { getChannelIdFromClaim } from 'util/claim';
const DEBOUNCE_SCROLL_HANDLER_MS = 200; const DEBOUNCE_SCROLL_HANDLER_MS = 200;
// "3" due to 2 separate fetches needed + 1 buffer just in case.
const MAX_LINKED_COMMENT_SCROLL_ATTEMPTS = 3;
function scaleToDevicePixelRatio(value) { function scaleToDevicePixelRatio(value) {
const devicePixelRatio = window.devicePixelRatio || 1.0; const devicePixelRatio = window.devicePixelRatio || 1.0;
if (devicePixelRatio < 1.0) { if (devicePixelRatio < 1.0) {
@ -44,6 +42,7 @@ type Props = {
claimIsMine: boolean, claimIsMine: boolean,
myChannels: ?Array<ChannelClaim>, myChannels: ?Array<ChannelClaim>,
isFetchingComments: boolean, isFetchingComments: boolean,
isFetchingCommentsById: boolean,
isFetchingReacts: boolean, isFetchingReacts: boolean,
linkedCommentId?: string, linkedCommentId?: string,
totalComments: number, totalComments: number,
@ -69,6 +68,7 @@ function CommentList(props: Props) {
claimIsMine, claimIsMine,
myChannels, myChannels,
isFetchingComments, isFetchingComments,
isFetchingCommentsById,
isFetchingReacts, isFetchingReacts,
linkedCommentId, linkedCommentId,
totalComments, totalComments,
@ -83,15 +83,15 @@ function CommentList(props: Props) {
const DEFAULT_SORT = ENABLE_COMMENT_REACTIONS ? SORT_BY.POPULARITY : SORT_BY.NEWEST; const DEFAULT_SORT = ENABLE_COMMENT_REACTIONS ? SORT_BY.POPULARITY : SORT_BY.NEWEST;
const [sort, setSort] = usePersistedState('comment-sort-by', DEFAULT_SORT); const [sort, setSort] = usePersistedState('comment-sort-by', DEFAULT_SORT);
const [page, setPage] = React.useState(0); const [page, setPage] = React.useState(0);
const [lcScrollAttempts, setLcScrollAttempts] = React.useState(
linkedCommentId ? 0 : MAX_LINKED_COMMENT_SCROLL_ATTEMPTS
);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const isMediumScreen = useIsMediumScreen(); const isMediumScreen = useIsMediumScreen();
const [expandedComments, setExpandedComments] = React.useState(!isMobile && !isMediumScreen); const [expandedComments, setExpandedComments] = React.useState(!isMobile && !isMediumScreen);
const totalFetchedComments = allCommentIds ? allCommentIds.length : 0; const totalFetchedComments = allCommentIds ? allCommentIds.length : 0;
const channelId = getChannelIdFromClaim(claim); const channelId = getChannelIdFromClaim(claim);
const channelSettings = channelId ? settingsByChannelId[channelId] : undefined; const channelSettings = channelId ? settingsByChannelId[channelId] : undefined;
const fetchedCommentsOnce = useFetched(isFetchingComments);
const fetchedReactsOnce = useFetched(isFetchingReacts);
const fetchedLinkedComment = useFetched(isFetchingCommentsById);
// Display comments immediately if not fetching reactions // Display comments immediately if not fetching reactions
// If not, wait to show comments until reactions are fetched // If not, wait to show comments until reactions are fetched
@ -203,19 +203,19 @@ function CommentList(props: Props) {
// Scroll to linked-comment // Scroll to linked-comment
useEffect(() => { useEffect(() => {
if (lcScrollAttempts < MAX_LINKED_COMMENT_SCROLL_ATTEMPTS && readyToDisplayComments && !isFetchingComments) { if (fetchedLinkedComment && fetchedCommentsOnce && fetchedReactsOnce) {
const elems = document.getElementsByClassName(COMMENT_HIGHLIGHTED); const elems = document.getElementsByClassName(COMMENT_HIGHLIGHTED);
if (elems.length > 0) { if (elems.length > 0) {
setLcScrollAttempts(MAX_LINKED_COMMENT_SCROLL_ATTEMPTS); const ROUGH_HEADER_HEIGHT = 125; // @see: --header-height
const linkedComment = elems[0]; const linkedComment = elems[0];
linkedComment.scrollIntoView({ block: 'start' }); window.scrollTo({
window.scrollBy(0, -125); top: linkedComment.getBoundingClientRect().top + window.scrollY - ROUGH_HEADER_HEIGHT,
} else { left: 0,
setLcScrollAttempts(lcScrollAttempts + 1); behavior: 'smooth',
});
} }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [fetchedLinkedComment, fetchedCommentsOnce, fetchedReactsOnce]);
}, [readyToDisplayComments, isFetchingComments]); // We just want to respond to these, nothing else.
// Infinite scroll // Infinite scroll
useEffect(() => { useEffect(() => {

View file

@ -253,6 +253,7 @@ export const COMMENT_LIST_STARTED = 'COMMENT_LIST_STARTED';
export const COMMENT_LIST_COMPLETED = 'COMMENT_LIST_COMPLETED'; export const COMMENT_LIST_COMPLETED = 'COMMENT_LIST_COMPLETED';
export const COMMENT_LIST_FAILED = 'COMMENT_LIST_FAILED'; export const COMMENT_LIST_FAILED = 'COMMENT_LIST_FAILED';
export const COMMENT_LIST_RESET = 'COMMENT_LIST_RESET'; export const COMMENT_LIST_RESET = 'COMMENT_LIST_RESET';
export const COMMENT_BY_ID_STARTED = 'COMMENT_BY_ID_STARTED';
export const COMMENT_BY_ID_COMPLETED = 'COMMENT_BY_ID_COMPLETED'; export const COMMENT_BY_ID_COMPLETED = 'COMMENT_BY_ID_COMPLETED';
export const COMMENT_CREATE_STARTED = 'COMMENT_CREATE_STARTED'; export const COMMENT_CREATE_STARTED = 'COMMENT_CREATE_STARTED';
export const COMMENT_CREATE_COMPLETED = 'COMMENT_CREATE_COMPLETED'; export const COMMENT_CREATE_COMPLETED = 'COMMENT_CREATE_COMPLETED';

View file

@ -203,6 +203,10 @@ export function doCommentList(
export function doCommentById(commentId: string, toastIfNotFound: boolean = true) { export function doCommentById(commentId: string, toastIfNotFound: boolean = true) {
return (dispatch: Dispatch, getState: GetState) => { return (dispatch: Dispatch, getState: GetState) => {
dispatch({
type: ACTIONS.COMMENT_BY_ID_STARTED,
});
return Comments.comment_by_id({ comment_id: commentId, with_ancestors: true }) return Comments.comment_by_id({ comment_id: commentId, with_ancestors: true })
.then((result: CommentByIdResponse) => { .then((result: CommentByIdResponse) => {
const { item, items, ancestors } = result; const { item, items, ancestors } = result;

View file

@ -21,6 +21,7 @@ const defaultState: CommentsState = {
superChatsByUri: {}, superChatsByUri: {},
pinnedCommentsById: {}, // ClaimId -> array of pinned comment IDs pinnedCommentsById: {}, // ClaimId -> array of pinned comment IDs
isLoading: false, isLoading: false,
isLoadingById: false,
isLoadingByParentId: {}, isLoadingByParentId: {},
isCommenting: false, isCommenting: false,
myComments: undefined, myComments: undefined,
@ -335,6 +336,8 @@ export default handleActions(
}; };
}, },
[ACTIONS.COMMENT_BY_ID_STARTED]: (state) => ({ ...state, isLoadingById: true }),
[ACTIONS.COMMENT_BY_ID_COMPLETED]: (state: CommentsState, action: any) => { [ACTIONS.COMMENT_BY_ID_COMPLETED]: (state: CommentsState, action: any) => {
const { comment, ancestors } = action.data; const { comment, ancestors } = action.data;
const claimId = comment.claim_id; const claimId = comment.claim_id;
@ -375,6 +378,7 @@ export default handleActions(
return { return {
...state, ...state,
isLoadingById: false,
topLevelCommentsById, topLevelCommentsById,
topLevelTotalCommentsById, topLevelTotalCommentsById,
topLevelTotalPagesById, topLevelTotalPagesById,

View file

@ -9,6 +9,7 @@ const selectState = (state) => state.comments || {};
export const selectCommentsById = createSelector(selectState, (state) => state.commentById || {}); export const selectCommentsById = createSelector(selectState, (state) => state.commentById || {});
export const selectIsFetchingComments = createSelector(selectState, (state) => state.isLoading); export const selectIsFetchingComments = createSelector(selectState, (state) => state.isLoading);
export const selectIsFetchingCommentsById = createSelector(selectState, (state) => state.isLoadingById);
export const selectIsFetchingCommentsByParentId = createSelector(selectState, (state) => state.isLoadingByParentId); export const selectIsFetchingCommentsByParentId = createSelector(selectState, (state) => state.isLoadingByParentId);
export const selectIsPostingComment = createSelector(selectState, (state) => state.isCommenting); export const selectIsPostingComment = createSelector(selectState, (state) => state.isCommenting);
export const selectIsFetchingReacts = createSelector(selectState, (state) => state.isFetchingReacts); export const selectIsFetchingReacts = createSelector(selectState, (state) => state.isFetchingReacts);