Compare commits
4 Commits
master
...
fix-tl-jum
Author | SHA1 | Date |
---|---|---|
clovis | ad72508ec5 | |
clovis | 64cd64d09e | |
clovis | 7874224b04 | |
unknown | 091bafe9ec |
|
@ -245,6 +245,7 @@ const expandTimelineSuccess = (timeline: string, statuses: APIEntity[], next: st
|
|||
partial,
|
||||
isLoadingRecent,
|
||||
skipLoading: !isLoadingMore,
|
||||
isLoadingMore,
|
||||
});
|
||||
|
||||
const expandTimelineFail = (timeline: string, error: AxiosError, isLoadingMore: boolean) => ({
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import classNames from 'classnames';
|
||||
import throttle from 'lodash/throttle';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useIntl, MessageDescriptor } from 'react-intl';
|
||||
|
@ -31,38 +30,38 @@ const ScrollTopButton: React.FC<IScrollTopButton> = ({
|
|||
const intl = useIntl();
|
||||
const settings = useSettings();
|
||||
|
||||
const timer = React.useRef(null);
|
||||
const [scrolled, setScrolled] = useState<boolean>(false);
|
||||
const autoload = settings.get('autoloadTimelines') === true;
|
||||
|
||||
const visible = count > 0 && scrolled;
|
||||
|
||||
const classes = classNames('left-1/2 -translate-x-1/2 fixed top-20 z-50', {
|
||||
'hidden': !visible,
|
||||
});
|
||||
|
||||
const getScrollTop = (): number => {
|
||||
const getScrollTop = React.useCallback((): number => {
|
||||
return (document.scrollingElement || document.documentElement).scrollTop;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const maybeUnload = () => {
|
||||
if (autoload && getScrollTop() <= autoloadThreshold) {
|
||||
onClick();
|
||||
}
|
||||
};
|
||||
const maybeUnload = React.useCallback(() => {
|
||||
// we need to add a timer since there is a delay between content render and
|
||||
// scroll top calculation. Without it, new content is always loaded because
|
||||
// scrollTop is 0 at first.
|
||||
if (timer.current) clearTimeout(timer.current);
|
||||
timer.current = setTimeout(() => {
|
||||
if (count > 0 && autoload && getScrollTop() <= autoloadThreshold) {
|
||||
onClick();
|
||||
}
|
||||
timer.current = null;
|
||||
}, 250);
|
||||
}, [autoload, autoloadThreshold, onClick, count]);
|
||||
|
||||
const handleScroll = useCallback(throttle(() => {
|
||||
maybeUnload();
|
||||
|
||||
if (getScrollTop() > threshold) {
|
||||
setScrolled(true);
|
||||
} else {
|
||||
setScrolled(false);
|
||||
}
|
||||
}, 150, { trailing: true }), [autoload, threshold, autoloadThreshold, onClick]);
|
||||
}, 150, { trailing: true }), [threshold]);
|
||||
|
||||
const scrollUp = () => {
|
||||
const scrollUp = React.useCallback(() => {
|
||||
window.scrollTo({ top: 0 });
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleClick: React.MouseEventHandler = () => {
|
||||
setTimeout(scrollUp, 10);
|
||||
|
@ -79,19 +78,23 @@ const ScrollTopButton: React.FC<IScrollTopButton> = ({
|
|||
|
||||
useEffect(() => {
|
||||
maybeUnload();
|
||||
}, [count]);
|
||||
}, [maybeUnload]);
|
||||
|
||||
const visible = React.useMemo(() => count > 0 && scrolled, [count, scrolled]) ;
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<a className='flex items-center bg-primary-600 hover:bg-primary-700 hover:scale-105 active:scale-100 transition-transform text-white rounded-full px-4 py-2 space-x-1.5 cursor-pointer whitespace-nowrap' onClick={handleClick}>
|
||||
<div className='left-1/2 -translate-x-1/2 fixed top-20 z-50'>
|
||||
<button
|
||||
className='flex items-center bg-primary-600 hover:bg-primary-700 hover:scale-105 active:scale-100 transition-transform text-white rounded-full px-4 py-2 space-x-1.5 cursor-pointer whitespace-nowrap'
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Icon src={require('@tabler/icons/arrow-bar-to-up.svg')} />
|
||||
|
||||
{(count > 0) && (
|
||||
<Text theme='inherit' size='sm'>
|
||||
{intl.formatMessage(message, { count })}
|
||||
</Text>
|
||||
)}
|
||||
</a>
|
||||
<Text theme='inherit' size='sm'>
|
||||
{intl.formatMessage(message, { count })}
|
||||
</Text>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
import debounce from 'lodash/debounce';
|
||||
import React, { useCallback } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { defineMessages } from 'react-intl';
|
||||
|
||||
import { dequeueTimeline, scrollTopTimeline } from 'soapbox/actions/timelines';
|
||||
|
@ -53,13 +54,14 @@ const Timeline: React.FC<ITimeline> = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<ScrollTopButton
|
||||
key='timeline-queue-button-header'
|
||||
onClick={handleDequeueTimeline}
|
||||
count={totalQueuedItemsCount}
|
||||
message={messages.queue}
|
||||
/>
|
||||
|
||||
{
|
||||
createPortal(<ScrollTopButton
|
||||
key='timeline-queue-button-header'
|
||||
onClick={handleDequeueTimeline}
|
||||
count={totalQueuedItemsCount}
|
||||
message={messages.queue}
|
||||
/>, document.body)
|
||||
}
|
||||
<StatusList
|
||||
timelineId={timelineId}
|
||||
onScrollToTop={handleScrollToTop}
|
||||
|
|
|
@ -89,32 +89,47 @@ const setFailed = (state: State, timelineId: string, failed: boolean) => {
|
|||
return state.update(timelineId, TimelineRecord(), timeline => timeline.set('loadingFailed', failed));
|
||||
};
|
||||
|
||||
const expandNormalizedTimeline = (state: State, timelineId: string, statuses: ImmutableList<ImmutableMap<string, any>>, next: string | null, isPartial: boolean, isLoadingRecent: boolean) => {
|
||||
const newIds = getStatusIds(statuses);
|
||||
const expandNormalizedTimeline = (state: State, timelineId: string, statuses: ImmutableList<ImmutableMap<string, any>>, next: string | null, isPartial: boolean, isLoadingRecent: boolean, isLoadingMore: boolean) => {
|
||||
let newIds = getStatusIds(statuses);
|
||||
let unseens = ImmutableOrderedSet<any>();
|
||||
|
||||
return state.update(timelineId, TimelineRecord(), timeline => timeline.withMutations(timeline => {
|
||||
timeline.set('isLoading', false);
|
||||
timeline.set('loadingFailed', false);
|
||||
timeline.set('isPartial', isPartial);
|
||||
return state.withMutations((s) => {
|
||||
s.update(timelineId, TimelineRecord(), timeline => timeline.withMutations(timeline => {
|
||||
timeline.set('isLoading', false);
|
||||
timeline.set('loadingFailed', false);
|
||||
timeline.set('isPartial', isPartial);
|
||||
|
||||
if (!next && !isLoadingRecent) timeline.set('hasMore', false);
|
||||
if (!next && !isLoadingRecent) timeline.set('hasMore', false);
|
||||
|
||||
// Pinned timelines can be replaced entirely
|
||||
if (timelineId.endsWith(':pinned')) {
|
||||
timeline.set('items', newIds);
|
||||
return;
|
||||
}
|
||||
// Pinned timelines can be replaced entirely
|
||||
if (timelineId.endsWith(':pinned')) {
|
||||
timeline.set('items', newIds);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newIds.isEmpty()) {
|
||||
timeline.update('items', oldIds => {
|
||||
if (newIds.first() > oldIds.first()!) {
|
||||
return mergeStatusIds(oldIds, newIds);
|
||||
} else {
|
||||
return mergeStatusIds(newIds, oldIds);
|
||||
if (!newIds.isEmpty()) {
|
||||
// we need to sort between queue and actual list to avoid
|
||||
// messing with user position in the timeline by inserting inseen statuses
|
||||
unseens = ImmutableOrderedSet<any>();
|
||||
if (!isLoadingMore
|
||||
&& timeline.items.count() > 0
|
||||
&& newIds.first() > timeline.items.first()
|
||||
) {
|
||||
unseens = newIds.subtract(timeline.items);
|
||||
}
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
newIds = newIds.subtract(unseens);
|
||||
timeline.update('items', oldIds => {
|
||||
if (newIds.first() > oldIds.first()!) {
|
||||
return mergeStatusIds(oldIds, newIds);
|
||||
} else {
|
||||
return mergeStatusIds(newIds, oldIds);
|
||||
}
|
||||
});
|
||||
}
|
||||
}));
|
||||
unseens.forEach((statusId) => s.set(timelineId, updateTimelineQueue(s, timelineId, statusId).get(timelineId)));
|
||||
});
|
||||
};
|
||||
|
||||
const updateTimeline = (state: State, timelineId: string, statusId: string) => {
|
||||
|
@ -326,7 +341,7 @@ export default function timelines(state: State = initialState, action: AnyAction
|
|||
case TIMELINE_EXPAND_FAIL:
|
||||
return handleExpandFail(state, action.timeline);
|
||||
case TIMELINE_EXPAND_SUCCESS:
|
||||
return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses) as ImmutableList<ImmutableMap<string, any>>, action.next, action.partial, action.isLoadingRecent);
|
||||
return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses) as ImmutableList<ImmutableMap<string, any>>, action.next, action.partial, action.isLoadingRecent, action.isLoadingMore);
|
||||
case TIMELINE_UPDATE:
|
||||
return updateTimeline(state, action.timeline, action.statusId);
|
||||
case TIMELINE_UPDATE_QUEUE:
|
||||
|
|
Loading…
Reference in New Issue