diff --git a/src/routes/_actions/timeline.js b/src/routes/_actions/timeline.js
index f0274f5..fcaf2a0 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 a443016..1d56971 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 c33a4ff..e623680 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 33ff8a7..6660eaa 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 eaba1c4..f604c1c 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 0000000..f6cb47d
--- /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 0000000..7ee91d7
--- /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 2e84d43..d11b64d 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) {