From 45630c185ff990cca1bc4dc79a9e728f7d9c8856 Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Tue, 28 May 2019 22:46:01 -0700 Subject: [PATCH] feat: add option to disable infinite scroll (#1253) * feat: add option to disable infinite scroll fixes #391 and fixes #270. Also makes me less nervous about #1251 because now keyboard users can disable infinite load and easily access the "reload" button in the snackbar footer. * fix test --- src/routes/_actions/timeline.js | 2 +- .../_components/timeline/LoadingFooter.html | 82 +++++++++++++++++-- src/routes/_components/timeline/Timeline.html | 18 ++-- src/routes/_pages/settings/general.html | 17 +++- src/routes/_store/store.js | 1 + tests/spec/036-disable-infinite-load.js | 39 +++++++++ tests/spec/128-disable-infinite-load.js | 32 ++++++++ tests/utils.js | 19 ++++- 8 files changed, 189 insertions(+), 21 deletions(-) create mode 100644 tests/spec/036-disable-infinite-load.js create mode 100644 tests/spec/128-disable-infinite-load.js diff --git a/src/routes/_actions/timeline.js b/src/routes/_actions/timeline.js index f0274f52..fcaf2a0a 100644 --- a/src/routes/_actions/timeline.js +++ b/src/routes/_actions/timeline.js @@ -114,7 +114,7 @@ export async function setupTimeline () { stop('setupTimeline') } -export async function fetchTimelineItemsOnScrollToBottom (instanceName, timelineName) { +export async function fetchMoreItemsAtBottomOfTimeline (instanceName, timelineName) { console.log('setting runningUpdate: true') store.setForTimeline(instanceName, timelineName, { runningUpdate: true }) await fetchTimelineItemsAndPossiblyFallBack() diff --git a/src/routes/_components/timeline/LoadingFooter.html b/src/routes/_components/timeline/LoadingFooter.html index a4430167..1d569711 100644 --- a/src/routes/_components/timeline/LoadingFooter.html +++ b/src/routes/_components/timeline/LoadingFooter.html @@ -1,32 +1,100 @@ \ No newline at end of file + diff --git a/src/routes/_components/timeline/Timeline.html b/src/routes/_components/timeline/Timeline.html index c33a4ffa..e6236808 100644 --- a/src/routes/_components/timeline/Timeline.html +++ b/src/routes/_components/timeline/Timeline.html @@ -45,7 +45,7 @@ } from '../../_utils/asyncModules' import { timelines } from '../../_static/timelines' import { - fetchTimelineItemsOnScrollToBottom, + fetchMoreItemsAtBottomOfTimeline, setupTimeline, showMoreItemsForTimeline, showMoreItemsForThread, @@ -165,18 +165,16 @@ }, onScrollToBottom () { let { timelineType } = this.get() - let { timelineInitialized, runningUpdate } = this.store.get() + let { timelineInitialized, runningUpdate, disableInfiniteScroll } = this.store.get() if (!timelineInitialized || runningUpdate || + disableInfiniteScroll || timelineType === 'status') { // for status contexts, we've already fetched the whole thread return } let { currentInstance } = this.store.get() let { timeline } = this.get() - fetchTimelineItemsOnScrollToBottom( - currentInstance, - timeline - ) + /* no await */ fetchMoreItemsAtBottomOfTimeline(currentInstance, timeline) }, onScrollToTop () { let { shouldShowHeader } = this.store.get() @@ -188,7 +186,7 @@ } }, setupStreaming () { - let { currentInstance } = this.store.get() + let { currentInstance, disableInfiniteScroll } = this.store.get() let { timeline, timelineType } = this.get() let handleItemIdsToAdd = () => { let { itemIdsToAdd } = this.get() @@ -204,13 +202,17 @@ if (timelineType === 'status') { // this is a thread, just insert the statuses already showMoreItemsForThread(currentInstance, timeline) - } else if (scrollTop === 0 && !shouldShowHeader && !showHeader) { + } else if (!disableInfiniteScroll && scrollTop === 0 && !shouldShowHeader && !showHeader) { // if the user is scrolled to the top and we're not showing the header, then // just insert the statuses. this is "chat room mode" showMoreItemsForTimeline(currentInstance, timeline) } else { // user hasn't scrolled to the top, show a header instead this.store.setForTimeline(currentInstance, timeline, { shouldShowHeader: true }) + // unless the user has disabled infinite scroll entirely + if (disableInfiniteScroll) { + this.store.setForTimeline(currentInstance, timeline, { showHeader: true }) + } } stop('handleItemIdsToAdd') } diff --git a/src/routes/_pages/settings/general.html b/src/routes/_pages/settings/general.html index 33ff8a79..6660eaa3 100644 --- a/src/routes/_pages/settings/general.html +++ b/src/routes/_pages/settings/general.html @@ -32,6 +32,19 @@ bind:checked="$disableCustomScrollbars" on:change="onChange(event)"> +
+ + +
@@ -89,11 +102,13 @@ import SettingsLayout from '../../_components/settings/SettingsLayout.html' import ThemeSettings from '../../_components/settings/instance/ThemeSettings.html' import { store } from '../../_store/store' + import Tooltip from '../../_components/Tooltip.html' export default { components: { SettingsLayout, - ThemeSettings + ThemeSettings, + Tooltip }, methods: { onChange (event) { diff --git a/src/routes/_store/store.js b/src/routes/_store/store.js index eaba1c4b..f604c1c1 100644 --- a/src/routes/_store/store.js +++ b/src/routes/_store/store.js @@ -13,6 +13,7 @@ const persistedState = { // we disable scrollbars by default on iOS disableCustomScrollbars: process.browser && /iP(?:hone|ad|od)/.test(navigator.userAgent), disableHotkeys: false, + disableInfiniteScroll: false, disableLongAriaLabels: false, disableTapOnStatus: false, hideCards: false, diff --git a/tests/spec/036-disable-infinite-load.js b/tests/spec/036-disable-infinite-load.js new file mode 100644 index 00000000..f6cb47d0 --- /dev/null +++ b/tests/spec/036-disable-infinite-load.js @@ -0,0 +1,39 @@ +import { + settingsNavButton, + homeNavButton, + disableInfiniteScroll, + scrollToStatus, + loadMoreButton, getFirstVisibleStatus, scrollFromStatusToStatus, sleep, getActiveElementAriaPosInSet +} from '../utils' +import { loginAsFoobar } from '../roles' +import { Selector as $ } from 'testcafe' + +fixture`036-disable-infinite-load.js` + .page`http://localhost:4002` + +test('Can disable loading items at bottom of timeline', async t => { + await loginAsFoobar(t) + await t.click(settingsNavButton) + .click($('a').withText('General')) + .click(disableInfiniteScroll) + .expect(disableInfiniteScroll.checked).ok() + .click(homeNavButton) + .expect(getFirstVisibleStatus().getAttribute('aria-setsize')).eql('20') + await scrollToStatus(t, 20) + await t + .click(loadMoreButton) + .expect(getActiveElementAriaPosInSet()).eql('20') + .expect(getFirstVisibleStatus().getAttribute('aria-setsize')).eql('40') + await scrollFromStatusToStatus(t, 20, 40) + await t + .click(loadMoreButton) + .expect(getActiveElementAriaPosInSet()).eql('40') + .expect(getFirstVisibleStatus().getAttribute('aria-setsize')).eql('47') + await scrollFromStatusToStatus(t, 40, 47) + await t + .click(loadMoreButton) + await sleep(1000) + await t + .expect(loadMoreButton.exists).ok() + .expect(getFirstVisibleStatus().getAttribute('aria-setsize')).eql('47') +}) diff --git a/tests/spec/128-disable-infinite-load.js b/tests/spec/128-disable-infinite-load.js new file mode 100644 index 00000000..7ee91d72 --- /dev/null +++ b/tests/spec/128-disable-infinite-load.js @@ -0,0 +1,32 @@ +import { + settingsNavButton, + homeNavButton, + disableInfiniteScroll, + getFirstVisibleStatus, + getUrl, + showMoreButton, getNthStatusContent +} from '../utils' +import { loginAsFoobar } from '../roles' +import { Selector as $ } from 'testcafe' +import { postAs } from '../serverActions' + +fixture`128-disable-infinite-load.js` + .page`http://localhost:4002` + +test('Can disable loading items at top of timeline', async t => { + await loginAsFoobar(t) + await t.click(settingsNavButton) + .click($('a').withText('General')) + .click(disableInfiniteScroll) + .expect(disableInfiniteScroll.checked).ok() + .click(homeNavButton) + .expect(getUrl()).eql('http://localhost:4002/') + .expect(getFirstVisibleStatus().exists).ok() + await postAs('admin', 'hey hey hey this is new') + await t + .expect(showMoreButton.innerText).contains('Show 1 more', { + timeout: 20000 + }) + .click(showMoreButton) + .expect(getNthStatusContent(1).innerText).contains('hey hey hey this is new') +}) diff --git a/tests/utils.js b/tests/utils.js index 2e84d435..d11b64d3 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -48,11 +48,14 @@ export const generalSettingsButton = $('a[href="/settings/general"]') export const markMediaSensitiveInput = $('#choice-mark-media-sensitive') export const neverMarkMediaSensitiveInput = $('#choice-never-mark-media-sensitive') export const removeEmojiFromDisplayNamesInput = $('#choice-omit-emoji-in-display-names') +export const disableInfiniteScroll = $('#choice-disable-infinite-scroll') export const dialogOptionsOption = $(`.modal-dialog button`) export const emojiSearchInput = $('.emoji-mart-search input') export const confirmationDialogOKButton = $('.confirmation-dialog-form-flex button:nth-child(1)') export const confirmationDialogCancelButton = $('.confirmation-dialog-form-flex button:nth-child(2)') +export const loadMoreButton = $('.loading-footer button') + export const composeModalInput = $('.modal-dialog .compose-box-input') export const composeModalComposeButton = $('.modal-dialog .compose-box-button') export const composeModalContentWarningInput = $('.modal-dialog .content-warning-input') @@ -119,6 +122,10 @@ export const getActiveElementRectTop = exec(() => ( (document.activeElement && document.activeElement.getBoundingClientRect().top) || -1 )) +export const getActiveElementAriaPosInSet = exec(() => ( + (document.activeElement && document.activeElement.getAttribute('aria-posinset')) || '' +)) + export const getActiveElementInsideNthStatus = exec(() => { let element = document.activeElement while (element) { @@ -428,16 +435,20 @@ export async function validateTimeline (t, timeline) { } export async function scrollToStatus (t, n) { + return scrollFromStatusToStatus(t, 1, n) +} + +export async function scrollFromStatusToStatus (t, start, end) { let timeout = 20000 - for (let i = 1; i < n; i++) { + for (let i = start; i < end; i++) { await t.expect(getNthStatus(i).exists).ok({ timeout }) .hover(getNthStatus(i)) - .expect($('.loading-footer').exist).notOk({ timeout }) .expect($(`${getNthStatusSelector(i)} .status-toolbar`).exists).ok({ timeout }) .hover($(`${getNthStatusSelector(i)} .status-toolbar`)) - .expect($('.loading-footer').exist).notOk({ timeout }) } - await t.hover(getNthStatus(n)) + await t + .expect(getNthStatus(end).exists).ok({ timeout }) + .hover(getNthStatus(end)) } export async function clickToNotificationsAndBackHome (t) {