diff options
| author | _ <phy.public@gmail.com> | 2023-10-30 09:12:20 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-10-30 09:12:20 +0900 |
| commit | c239058624dcd880ec1c5f3c436f3a2a06fc22c3 (patch) | |
| tree | 1ffe780569e2535337d3bebdff1d562b90922dcf /packages/frontend | |
| parent | fix(backend): プロフィールの自己紹介欄のMFMを連合するよう... (diff) | |
| download | misskey-c239058624dcd880ec1c5f3c436f3a2a06fc22c3.tar.gz misskey-c239058624dcd880ec1c5f3c436f3a2a06fc22c3.tar.bz2 misskey-c239058624dcd880ec1c5f3c436f3a2a06fc22c3.zip | |
feat(frontend): スワイプやボタンでタイムラインを再読込する機能 (#12113)
* pc reloading
* add: disable TL websocket option
* fix: stream disconnect when reload
* add: pull to refresh
* fix: pull to refresh
* add changelog
* fact: change to disableStreamingTimeline
* lint
* remove: en-US text
* refactor
* refactor
* add license identifier
* tweak
* Update MkPullToRefresh.vue
* Update MkPullToRefresh.vue
* change name timeoutHeartBeat
* tweak
* :art:
---------
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
Diffstat (limited to 'packages/frontend')
| -rw-r--r-- | packages/frontend/src/boot/main-boot.ts | 3 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkPageWindow.vue | 2 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkPagination.vue | 6 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkPullToRefresh.vue | 238 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkTimeline.vue | 136 | ||||
| -rw-r--r-- | packages/frontend/src/pages/settings/general.vue | 3 | ||||
| -rw-r--r-- | packages/frontend/src/pages/timeline.vue | 52 | ||||
| -rw-r--r-- | packages/frontend/src/store.ts | 4 | ||||
| -rw-r--r-- | packages/frontend/src/stream.ts | 20 | ||||
| -rw-r--r-- | packages/frontend/src/ui/_common_/stream-indicator.vue | 3 | ||||
| -rw-r--r-- | packages/frontend/src/ui/universal.vue | 2 |
11 files changed, 389 insertions, 80 deletions
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index f2af951d63..800a3b079f 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -8,7 +8,7 @@ import { common } from './common.js'; import { version, ui, lang, updateLocale } from '@/config.js'; import { i18n, updateI18n } from '@/i18n.js'; import { confirm, alert, post, popup, toast } from '@/os.js'; -import { useStream } from '@/stream.js'; +import { useStream, isReloading } from '@/stream.js'; import * as sound from '@/scripts/sound.js'; import { $i, refreshAccount, login, updateAccount, signout } from '@/account.js'; import { defaultStore, ColdDeviceStorage } from '@/store.js'; @@ -39,6 +39,7 @@ export async function mainBoot() { let reloadDialogShowing = false; stream.on('_disconnected_', async () => { + if (isReloading) return; if (defaultStore.state.serverDisconnectedBehavior === 'reload') { location.reload(); } else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') { diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue index 3b273ac545..5edae1bc3c 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -166,6 +166,8 @@ defineExpose({ <style lang="scss" module> .root { + overscroll-behavior: none; + min-height: 100%; background: var(--bg); diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index 80b469f632..5643de7683 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -102,6 +102,7 @@ const props = withDefaults(defineProps<{ const emit = defineEmits<{ (ev: 'queue', count: number): void; + (ev: 'status', error: boolean): void; }>(); let rootEl = $shallowRef<HTMLElement>(); @@ -193,6 +194,11 @@ watch(queue, (a, b) => { emit('queue', queue.value.size); }, { deep: true }); +watch(error, (n, o) => { + if (n === o) return; + emit('status', n); +}); + async function init(): Promise<void> { items.value = new Map(); queue.value = new Map(); diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue new file mode 100644 index 0000000000..0b5ae8e826 --- /dev/null +++ b/packages/frontend/src/components/MkPullToRefresh.vue @@ -0,0 +1,238 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div ref="rootEl"> + <div v-if="isPullStart" :class="$style.frame" :style="`--frame-min-height: ${currentHeight / 3}px;`"> + <div :class="$style.frameContent"> + <MkLoading v-if="isRefreshing" :class="$style.loader" :em="true"/> + <i v-else class="ti ti-arrow-bar-to-down" :class="[$style.icon, { [$style.refresh]: isPullEnd }]"></i> + <div :class="$style.text"> + <template v-if="isPullEnd">{{ i18n.ts.releaseToRefresh }}</template> + <template v-else-if="isRefreshing">{{ i18n.ts.refreshing }}</template> + <template v-else>{{ i18n.ts.pullDownToRefresh }}</template> + </div> + </div> + </div> + <div :class="{ [$style.slotClip]: isPullStart }"> + <slot/> + </div> +</div> +</template> + +<script lang="ts" setup> +import { onMounted, onUnmounted } from 'vue'; +import { deviceKind } from '@/scripts/device-kind.js'; +import { i18n } from '@/i18n.js'; + +const SCROLL_STOP = 10; +const MAX_PULL_DISTANCE = Infinity; +const FIRE_THRESHOLD = 200; +const RELEASE_TRANSITION_DURATION = 200; + +let isPullStart = $ref(false); +let isPullEnd = $ref(false); +let isRefreshing = $ref(false); +let currentHeight = $ref(0); + +let supportPointerDesktop = false; +let startScreenY: number | null = null; + +const rootEl = $shallowRef<HTMLDivElement>(); +let scrollEl: HTMLElement | null = null; + +let disabled = false; + +const emits = defineEmits<{ + (ev: 'refresh'): void; +}>(); + +function getScrollableParentElement(node) { + if (node == null) { + return null; + } + + if (node.scrollHeight > node.clientHeight) { + return node; + } else { + return getScrollableParentElement(node.parentNode); + } +} + +function getScreenY(event) { + if (supportPointerDesktop) { + return event.screenY; + } + return event.touches[0].screenY; +} + +function moveStart(event) { + if (!isPullStart && !isRefreshing && !disabled) { + isPullStart = true; + startScreenY = getScreenY(event); + currentHeight = 0; + } +} + +function moveBySystem(to: number): Promise<void> { + return new Promise(r => { + const startHeight = currentHeight; + const overHeight = currentHeight - to; + if (overHeight < 1) { + r(); + return; + } + const startTime = Date.now(); + let intervalId = setInterval(() => { + const time = Date.now() - startTime; + if (time > RELEASE_TRANSITION_DURATION) { + currentHeight = to; + clearInterval(intervalId); + r(); + return; + } + const nextHeight = startHeight - (overHeight / RELEASE_TRANSITION_DURATION) * time; + if (currentHeight < nextHeight) return; + currentHeight = nextHeight; + }, 1); + }); +} + +async function fixOverContent() { + if (currentHeight > FIRE_THRESHOLD) { + await moveBySystem(FIRE_THRESHOLD); + } +} + +async function closeContent() { + if (currentHeight > 0) { + await moveBySystem(0); + } +} + +function moveEnd() { + if (isPullStart && !isRefreshing) { + startScreenY = null; + if (isPullEnd) { + isPullEnd = false; + isRefreshing = true; + fixOverContent().then(() => emits('refresh')); + } else { + closeContent().then(() => isPullStart = false); + } + } +} + +function moving(event) { + if (!isPullStart || isRefreshing || disabled) return; + + if (!scrollEl) { + scrollEl = getScrollableParentElement(rootEl); + } + if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + currentHeight)) { + currentHeight = 0; + isPullEnd = false; + moveEnd(); + return; + } + + if (startScreenY === null) { + startScreenY = getScreenY(event); + } + const moveScreenY = getScreenY(event); + + const moveHeight = moveScreenY - startScreenY!; + currentHeight = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE); + + isPullEnd = currentHeight >= FIRE_THRESHOLD; +} + +/** + * emit(refresh)が完了したことを知らせる関数 + * + * タイムアウトがないのでこれを最終的に実行しないと出たままになる + */ +function refreshFinished() { + closeContent().then(() => { + isPullStart = false; + isRefreshing = false; + }); +} + +function setDisabled(value) { + disabled = value; +} + +onMounted(() => { + // マウス操作でpull to refreshするのは不便そう + //supportPointerDesktop = !!window.PointerEvent && deviceKind === 'desktop'; + + if (supportPointerDesktop) { + rootEl.addEventListener('pointerdown', moveStart); + // ポインターの場合、ポップアップ系の動作をするとdownだけ発火されてupが発火されないため + window.addEventListener('pointerup', moveEnd); + rootEl.addEventListener('pointermove', moving, { passive: true }); + } else { + rootEl.addEventListener('touchstart', moveStart); + rootEl.addEventListener('touchend', moveEnd); + rootEl.addEventListener('touchmove', moving, { passive: true }); + } +}); + +onUnmounted(() => { + if (supportPointerDesktop) window.removeEventListener('pointerup', moveEnd); +}); + +defineExpose({ + refreshFinished, + setDisabled, +}); +</script> + +<style lang="scss" module> +.frame { + position: relative; + overflow: clip; + + width: 100%; + min-height: var(--frame-min-height, 0px); + + mask-image: linear-gradient(90deg, #000 0%, #000 80%, transparent); + -webkit-mask-image: -webkit-linear-gradient(90deg, #000 0%, #000 80%, transparent); + + pointer-events: none; +} + +.frameContent { + position: absolute; + bottom: 0; + width: 100%; + margin: 5px 0; + display: flex; + flex-direction: column; + align-items: center; + font-size: 14px; + + > .icon, > .loader { + margin: 6px 0; + } + + > .icon { + transition: transform .25s; + + &.refresh { + transform: rotate(180deg); + } + } + + > .text { + margin: 5px 0; + } +} + +.slotClip { + overflow-y: clip; +} +</style> diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index cdd72febd1..a2ada35f91 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -4,13 +4,16 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkNotes ref="tlComponent" :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)"/> +<MkPullToRefresh ref="prComponent" @refresh="() => reloadTimeline(true)"> + <MkNotes ref="tlComponent" :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)" @status="prComponent.setDisabled($event)"/> +</MkPullToRefresh> </template> <script lang="ts" setup> import { computed, provide, onUnmounted } from 'vue'; import MkNotes from '@/components/MkNotes.vue'; -import { useStream } from '@/stream.js'; +import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; +import { useStream, reloadStream } from '@/stream.js'; import * as sound from '@/scripts/sound.js'; import { $i } from '@/account.js'; import { instance } from '@/instance.js'; @@ -39,6 +42,7 @@ const emit = defineEmits<{ provide('inChannel', computed(() => props.src === 'channel')); +const prComponent: InstanceType<typeof MkPullToRefresh> = $ref(); const tlComponent: InstanceType<typeof MkNotes> = $ref(); let tlNotesCount = 0; @@ -65,29 +69,73 @@ let connection; let connection2; const stream = useStream(); +const connectChannel = () => { + if (props.src === 'antenna') { + connection = stream.useChannel('antenna', { + antennaId: props.antenna, + }); + } else if (props.src === 'home') { + connection = stream.useChannel('homeTimeline', { + withRenotes: props.withRenotes, + withFiles: props.onlyFiles ? true : undefined, + }); + connection2 = stream.useChannel('main'); + } else if (props.src === 'local') { + connection = stream.useChannel('localTimeline', { + withRenotes: props.withRenotes, + withReplies: props.withReplies, + withFiles: props.onlyFiles ? true : undefined, + }); + } else if (props.src === 'social') { + connection = stream.useChannel('hybridTimeline', { + withRenotes: props.withRenotes, + withReplies: props.withReplies, + withFiles: props.onlyFiles ? true : undefined, + }); + } else if (props.src === 'global') { + connection = stream.useChannel('globalTimeline', { + withRenotes: props.withRenotes, + withFiles: props.onlyFiles ? true : undefined, + }); + } else if (props.src === 'mentions') { + connection = stream.useChannel('main'); + connection.on('mention', prepend); + } else if (props.src === 'directs') { + const onNote = note => { + if (note.visibility === 'specified') { + prepend(note); + } + }; + connection = stream.useChannel('main'); + connection.on('mention', onNote); + } else if (props.src === 'list') { + connection = stream.useChannel('userList', { + withFiles: props.onlyFiles ? true : undefined, + listId: props.list, + }); + } else if (props.src === 'channel') { + connection = stream.useChannel('channel', { + channelId: props.channel, + }); + } else if (props.src === 'role') { + connection = stream.useChannel('roleTimeline', { + roleId: props.role, + }); + } + if (props.src !== 'directs' || props.src !== 'mentions') connection.on('note', prepend); +}; if (props.src === 'antenna') { endpoint = 'antennas/notes'; query = { antennaId: props.antenna, }; - connection = stream.useChannel('antenna', { - antennaId: props.antenna, - }); - connection.on('note', prepend); } else if (props.src === 'home') { endpoint = 'notes/timeline'; query = { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, }; - connection = stream.useChannel('homeTimeline', { - withRenotes: props.withRenotes, - withFiles: props.onlyFiles ? true : undefined, - }); - connection.on('note', prepend); - - connection2 = stream.useChannel('main'); } else if (props.src === 'local') { endpoint = 'notes/local-timeline'; query = { @@ -95,12 +143,6 @@ if (props.src === 'antenna') { withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined, }; - connection = stream.useChannel('localTimeline', { - withRenotes: props.withRenotes, - withReplies: props.withReplies, - withFiles: props.onlyFiles ? true : undefined, - }); - connection.on('note', prepend); } else if (props.src === 'social') { endpoint = 'notes/hybrid-timeline'; query = { @@ -108,68 +150,44 @@ if (props.src === 'antenna') { withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined, }; - connection = stream.useChannel('hybridTimeline', { - withRenotes: props.withRenotes, - withReplies: props.withReplies, - withFiles: props.onlyFiles ? true : undefined, - }); - connection.on('note', prepend); } else if (props.src === 'global') { endpoint = 'notes/global-timeline'; query = { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, }; - connection = stream.useChannel('globalTimeline', { - withRenotes: props.withRenotes, - withFiles: props.onlyFiles ? true : undefined, - }); - connection.on('note', prepend); } else if (props.src === 'mentions') { endpoint = 'notes/mentions'; - connection = stream.useChannel('main'); - connection.on('mention', prepend); } else if (props.src === 'directs') { endpoint = 'notes/mentions'; query = { visibility: 'specified', }; - const onNote = note => { - if (note.visibility === 'specified') { - prepend(note); - } - }; - connection = stream.useChannel('main'); - connection.on('mention', onNote); } else if (props.src === 'list') { endpoint = 'notes/user-list-timeline'; query = { withFiles: props.onlyFiles ? true : undefined, listId: props.list, }; - connection = stream.useChannel('userList', { - withFiles: props.onlyFiles ? true : undefined, - listId: props.list, - }); - connection.on('note', prepend); } else if (props.src === 'channel') { endpoint = 'channels/timeline'; query = { channelId: props.channel, }; - connection = stream.useChannel('channel', { - channelId: props.channel, - }); - connection.on('note', prepend); } else if (props.src === 'role') { endpoint = 'roles/notes'; query = { roleId: props.role, }; - connection = stream.useChannel('roleTimeline', { - roleId: props.role, +} + +if (!defaultStore.state.disableStreamingTimeline) { + connectChannel(); + + onUnmounted(() => { + connection.dispose(); + if (connection2) connection2.dispose(); }); - connection.on('note', prepend); } const pagination = { @@ -178,9 +196,19 @@ const pagination = { params: query, }; -onUnmounted(() => { - connection.dispose(); - if (connection2) connection2.dispose(); +const reloadTimeline = (fromPR = false) => { + tlNotesCount = 0; + + tlComponent.pagingComponent?.reload().then(() => { + reloadStream(); + if (fromPR) prComponent.refreshFinished(); + }); +}; + +//const pullRefresh = () => reloadTimeline(true); + +defineExpose({ + reloadTimeline, }); /* TODO diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index 323dfc0722..85d038e3d1 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -151,6 +151,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="imageNewTab">{{ i18n.ts.openImageInNewTab }}</MkSwitch> <MkSwitch v-model="enableInfiniteScroll">{{ i18n.ts.enableInfiniteScroll }}</MkSwitch> <MkSwitch v-model="keepScreenOn">{{ i18n.ts.keepScreenOn }}</MkSwitch> + <MkSwitch v-model="disableStreamingTimeline">{{ i18n.ts.disableStreamingTimeline }}</MkSwitch> </div> <MkSelect v-model="serverDisconnectedBehavior"> <template #label>{{ i18n.ts.whenServerDisconnected }}</template> @@ -253,6 +254,7 @@ const notificationPosition = computed(defaultStore.makeGetterSetter('notificatio const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis')); const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn')); const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies')); +const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline')); watch(lang, () => { miLocalStorage.setItem('lang', lang.value as string); @@ -289,6 +291,7 @@ watch([ reactionsDisplaySize, highlightSensitiveMedia, keepScreenOn, + disableStreamingTimeline, ], async () => { await reloadAsk(); }); diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 8cc540779b..f601bc8a85 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -44,6 +44,7 @@ import { $i } from '@/account.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { miLocalStorage } from '@/local-storage.js'; import { antennasCache, userListsCache } from '@/cache.js'; +import { deviceKind } from '@/scripts/device-kind.js'; provide('shouldOmitHeaderTitle', true); @@ -139,27 +140,36 @@ function focus(): void { tlComponent.focus(); } -const headerActions = $computed(() => [{ - icon: 'ti ti-dots', - text: i18n.ts.options, - handler: (ev) => { - os.popupMenu([{ - type: 'switch', - text: i18n.ts.showRenotes, - icon: 'ti ti-repeat', - ref: $$(withRenotes), - }, src === 'local' || src === 'social' ? { - type: 'switch', - text: i18n.ts.showRepliesToOthersInTimeline, - ref: $$(withReplies), - } : undefined, { - type: 'switch', - text: i18n.ts.fileAttachedOnly, - icon: 'ti ti-photo', - ref: $$(onlyFiles), - }], ev.currentTarget ?? ev.target); - }, -}]); +const headerActions = $computed(() => [ + ...[deviceKind === 'desktop' ? { + icon: 'ti ti-refresh', + text: i18n.ts.reload, + handler: (ev) => { + console.log('called'); + tlComponent.reloadTimeline(); + }, + } : {}], { + icon: 'ti ti-dots', + text: i18n.ts.options, + handler: (ev) => { + os.popupMenu([{ + type: 'switch', + text: i18n.ts.showRenotes, + icon: 'ti ti-repeat', + ref: $$(withRenotes), + }, src === 'local' || src === 'social' ? { + type: 'switch', + text: i18n.ts.showRepliesToOthersInTimeline, + ref: $$(withReplies), + } : undefined, { + type: 'switch', + text: i18n.ts.fileAttachedOnly, + icon: 'ti ti-photo', + ref: $$(onlyFiles), + }], ev.currentTarget ?? ev.target); + }, + } +]); const headerTabs = $computed(() => [...(defaultStore.reactiveState.pinnedUserLists.value.map(l => ({ key: 'list:' + l.id, diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 6196e684e1..803f2f648d 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -369,6 +369,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'account', default: false, }, + disableStreamingTimeline: { + where: 'device', + default: false, + }, })); // TODO: 他のタブと永続化されたstateを同期 diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts index 27fce4d4b8..1e2d31480c 100644 --- a/packages/frontend/src/stream.ts +++ b/packages/frontend/src/stream.ts @@ -9,6 +9,9 @@ import { $i } from '@/account.js'; import { url } from '@/config.js'; let stream: Misskey.Stream | null = null; +let timeoutHeartBeat: number | null = null; + +export let isReloading: boolean = false; export function useStream(): Misskey.Stream { if (stream) return stream; @@ -17,7 +20,20 @@ export function useStream(): Misskey.Stream { token: $i.token, } : null)); - window.setTimeout(heartbeat, 1000 * 60); + timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60); + + return stream; +} + +export function reloadStream() { + if (!stream) return useStream(); + if (timeoutHeartBeat) window.clearTimeout(timeoutHeartBeat); + isReloading = true; + + stream.close(); + stream.once('_connected_', () => isReloading = false); + stream.stream.reconnect(); + timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60); return stream; } @@ -26,5 +42,5 @@ function heartbeat(): void { if (stream != null && document.visibilityState === 'visible') { stream.heartbeat(); } - window.setTimeout(heartbeat, 1000 * 60); + timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60); } diff --git a/packages/frontend/src/ui/_common_/stream-indicator.vue b/packages/frontend/src/ui/_common_/stream-indicator.vue index b09221f5d2..c3107b4e40 100644 --- a/packages/frontend/src/ui/_common_/stream-indicator.vue +++ b/packages/frontend/src/ui/_common_/stream-indicator.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onUnmounted } from 'vue'; -import { useStream } from '@/stream.js'; +import { useStream, isReloading } from '@/stream.js'; import { i18n } from '@/i18n.js'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; @@ -26,6 +26,7 @@ const zIndex = os.claimZIndex('high'); let hasDisconnected = $ref(false); function onDisconnected() { + if (isReloading) return; hasDisconnected = true; } diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index cc66bb47a4..c9fb8a931d 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -319,7 +319,7 @@ $widgets-hide-threshold: 1090px; min-width: 0; overflow: auto; overflow-y: scroll; - overscroll-behavior: contain; + overscroll-behavior: none; background: var(--bg); } |