From ac0f552a8318f1f442103b3e7253856fb3cf5a86 Mon Sep 17 00:00:00 2001 From: BackRunner Date: Sun, 22 Oct 2023 19:42:45 +0800 Subject: [PATCH] fix (frontend): federation settings glitch when the grid has more than one columns fix: remove the overflow flag fix: remove the overflow style fix: modify the initial value of backed revert change feat: add padding to the pagination property fix: update top calculation fix: wrong implementation fix: wrong implementation in federation page feat: new implementation fix: wrong implementation fix: wrong implementation # Conflicts: # packages/frontend/src/components/MkPagination.vue fix: wrong execution timing fix: execution timing fix: wrong implementation fix: wrong execute timing fix: execution timing fix: execution timing --- .../frontend/src/components/MkPagination.vue | 91 +++++++++++++++---- .../frontend/src/pages/admin/federation.vue | 10 +- packages/frontend/src/scripts/scroll.ts | 48 +++++++++- 3 files changed, 124 insertions(+), 25 deletions(-) diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index 5a87273386..9f970fe243 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; -import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@/scripts/scroll.js'; +import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible, onScrollDownOnce, onScrollUpOnce } from '@/scripts/scroll.js'; import { useDocumentVisibility } from '@/scripts/use-document-visibility.js'; import MkButton from '@/components/MkButton.vue'; import { defaultStore } from '@/store.js'; @@ -95,8 +95,11 @@ const props = withDefaults(defineProps<{ pagination: Paging; disableAutoLoad?: boolean; displayLimit?: number; + disableObserver?: boolean; + tolerance?: number; }>(), { displayLimit: 20, + tolerance: TOLERANCE, }); const emit = defineEmits<{ @@ -106,7 +109,7 @@ const emit = defineEmits<{ let rootEl = $shallowRef(); // 遡り中かどうか -let backed = $ref(false); +let backed = $ref(true); let scrollRemove = $ref<(() => void) | null>(null); @@ -151,11 +154,13 @@ const BACKGROUND_PAUSE_WAIT_SEC = 10; // 先頭が表示されているかどうかを検出 // https://qiita.com/mkataigi/items/0154aefd2223ce23398e -let scrollObserver = $ref(); - -watch([() => props.pagination.reversed, $$(scrollableElement)], () => { - if (scrollObserver) scrollObserver.disconnect(); +let scrollObserver = $ref(null); +const createScrollObserver = () => { + if (scrollObserver) { + scrollObserver.disconnect(); + scrollObserver = null; + } scrollObserver = new IntersectionObserver(entries => { backed = entries[0].isIntersecting; }, { @@ -163,6 +168,16 @@ watch([() => props.pagination.reversed, $$(scrollableElement)], () => { rootMargin: props.pagination.reversed ? '-100% 0px 100% 0px' : '100% 0px -100% 0px', threshold: 0.01, }); +}; + +let initialScrollCleaner: () => void | undefined; + +watch([() => props.pagination.reversed, $$(scrollableElement)], () => { + if (props.disableObserver) { + initialScrollCleaner?.(); + } else { + createScrollObserver(); + } }, { immediate: true }); watch($$(rootEl), () => { @@ -175,8 +190,7 @@ watch($$(rootEl), () => { watch([$$(backed), $$(contentEl)], () => { if (!backed) { if (!contentEl) return; - - scrollRemove = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl, executeQueue, TOLERANCE); + scrollRemove = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl, executeQueue, props.tolerance, false, !props.disableObserver); } else { if (scrollRemove) scrollRemove(); scrollRemove = null; @@ -201,9 +215,15 @@ async function init(): Promise { ...params, limit: props.pagination.limit ?? 10, }).then(res => { - for (let i = 0; i < res.length; i++) { + for (let i = 0; i < res.length; i += 3) { const item = res[i]; - if (i === 3) item._shouldInsertAd_ = true; + if (!item) { + continue; + } + if (i === 3) { + item._shouldInsertAd_ = true; + break; + } } if (res.length === 0 || props.pagination.noPaging) { @@ -241,9 +261,9 @@ const fetchMore = async (): Promise => { untilId: Array.from(items.value.keys()).at(-1), }), }).then(res => { - for (let i = 0; i < res.length; i++) { - const item = res[i]; - if (i === 10) item._shouldInsertAd_ = true; + for (let i = 0; i < res.length; i += 10) { + if (!res[i]) continue; + res[i]._shouldInsertAd_ = true; } const reverseConcat = _res => { @@ -388,9 +408,11 @@ const prepend = (item: MisskeyEntity): void => { */ function unshiftItems(newItems: MisskeyEntity[]) { const length = newItems.length + items.value.size; - items.value = new Map([...arrayToEntries(newItems), ...items.value].slice(0, props.displayLimit)); + const mapEntries = [...arrayToEntries(newItems), ...items.value].slice(0, props.displayLimit); + items.value = new Map(mapEntries); if (length >= props.displayLimit) more.value = true; + offset.value = mapEntries.length; } /** @@ -399,9 +421,11 @@ function unshiftItems(newItems: MisskeyEntity[]) { */ function concatItems(oldItems: MisskeyEntity[]) { const length = oldItems.length + items.value.size; - items.value = new Map([...items.value, ...arrayToEntries(oldItems)].slice(0, props.displayLimit)); + const mapEntries = [...items.value, ...arrayToEntries(oldItems)].slice(0, props.displayLimit); + items.value = new Map(mapEntries); if (length >= props.displayLimit) more.value = true; + offset.value = mapEntries.length; } function executeQueue() { @@ -447,11 +471,42 @@ function toBottom() { scrollToBottom(contentEl!); } +const createInitialScrollListener = () => { + if (!props.disableObserver || !contentEl) { + return; + } + setTimeout(() => { + initialScrollCleaner = (props.pagination.reversed ? onScrollUpOnce : onScrollDownOnce)(contentEl, () => { + backed = false; + nextTick(() => { + const cleaner = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl, () => { + setTimeout(() => { + backed = true; + createInitialScrollListener(); + }); + }, props.tolerance, true, false); + if (cleaner) initialScrollCleaner = cleaner; + }); + }); + }); +} + +watch($$(contentEl), () => { + if (props.disableObserver) { + createInitialScrollListener(); + } +}); + onMounted(() => { inited.then(() => { if (props.pagination.reversed) { nextTick(() => { - setTimeout(toBottom, 800); + setTimeout(() => { + toBottom(); + setTimeout(() => { + createInitialScrollListener(); + }); + }, 800); // scrollToBottomでmoreFetchingボタンが画面外まで出るまで // more = trueを遅らせる @@ -459,6 +514,10 @@ onMounted(() => { moreFetching.value = false; }, 2000); }); + } else { + nextTick(() => { + createInitialScrollListener(); + }); } }); }); diff --git a/packages/frontend/src/pages/admin/federation.vue b/packages/frontend/src/pages/admin/federation.vue index 7dc0b46946..6a2c0b3295 100644 --- a/packages/frontend/src/pages/admin/federation.vue +++ b/packages/frontend/src/pages/admin/federation.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
@@ -42,8 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- - +
@@ -70,13 +69,14 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; let host = $ref(''); let state = $ref('federating'); let sort = $ref('+pubSub'); + const pagination = { endpoint: 'federation/instances' as const, - limit: 10, + limit: 30, offsetMode: true, params: computed(() => ({ sort: sort, - host: host !== '' ? host : null, + host: host || null, ...( state === 'federating' ? { federating: true } : state === 'subscribing' ? { subscribing: true } : diff --git a/packages/frontend/src/scripts/scroll.ts b/packages/frontend/src/scripts/scroll.ts index 7338de62b6..9a1518ff04 100644 --- a/packages/frontend/src/scripts/scroll.ts +++ b/packages/frontend/src/scripts/scroll.ts @@ -28,9 +28,9 @@ export function getScrollPosition(el: HTMLElement | null): number { return container == null ? window.scrollY : container.scrollTop; } -export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false) { +export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false, immediate = true) { // とりあえず評価してみる - if (el.isConnected && isTopVisible(el)) { + if (el.isConnected && isTopVisible(el) && immediate) { cb(); if (once) return null; } @@ -50,11 +50,11 @@ export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance = 1, o return removeListener; } -export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false) { +export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false, immediate = true) { const container = getScrollContainer(el); // とりあえず評価してみる - if (el.isConnected && isBottomVisible(el, tolerance, container)) { + if (el.isConnected && isBottomVisible(el, tolerance, container) && immediate) { cb(); if (once) return null; } @@ -132,3 +132,43 @@ export function getBodyScrollHeight() { document.body.clientHeight, document.documentElement.clientHeight, ); } + +export function onScrollDownOnce(el: HTMLElement, cb: () => unknown): () => void { + const containerEl = getScrollContainer(el); + const targetEl = containerEl ?? window; + + let initialScrollTop = containerEl?.scrollTop || 0; + + const listener = () => { + if ((containerEl?.scrollTop || 0) > initialScrollTop) { + cb(); + targetEl.removeEventListener('scroll', listener); + } + }; + + targetEl.addEventListener('scroll', listener, { passive: true }); + + return () => { + targetEl.removeEventListener('scroll', listener); + }; +} + +export function onScrollUpOnce(el: HTMLElement, cb: () => unknown): () => void { + const containerEl = getScrollContainer(el); + const targetEl = containerEl ?? window; + + let initialScrollTop = containerEl?.scrollTop || 0; + + const listener = () => { + if ((containerEl?.scrollTop || 0) < initialScrollTop) { + cb(); + targetEl.removeEventListener('scroll', listener); + } + }; + + targetEl.addEventListener('scroll', listener, { passive: true }); + + return () => { + targetEl.removeEventListener('scroll', listener); + }; +}