From ac3c6f3df8cec47d9f823f0eebacc26f569b7fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?= =?UTF-8?q?=E3=81=AB=E3=82=85?= <17376330+u1-liquid@users.noreply.github.com> Date: Fri, 3 Nov 2023 14:10:47 +0900 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E3=82=B9=E3=83=AF=E3=82=A4?= =?UTF-8?q?=E3=83=97=E3=82=84=E3=83=9C=E3=82=BF=E3=83=B3=E3=81=A7=E3=82=BF?= =?UTF-8?q?=E3=82=A4=E3=83=A0=E3=83=A9=E3=82=A4=E3=83=B3=E3=82=92=E5=86=8D?= =?UTF-8?q?=E8=AA=AD=E8=BE=BC=E3=81=99=E3=82=8B=E6=A9=9F=E8=83=BD=20(missk?= =?UTF-8?q?ey-dev#12113)=20(MisskeyIO#206)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(frontend): スワイプやボタンでタイムラインを再読込する機能 (misskey-dev#12113) * tweak MkPullToRefresh cheery-picked from 52dbab56a47f2242df08d95de8e0a58c5eb8ab9c Co-authored-by: syuilo * enhance(frontend): improve pull to refresh cheery-picked from d0d32e88466cef53a0d4429cce1ce4b9cb97d5b1 Co-authored-by: syuilo --- locales/en-US.yml | 4 + locales/index.d.ts | 4 + locales/ja-JP.yml | 4 + packages/frontend/src/boot/main-boot.ts | 3 +- .../src/components/MkNotifications.vue | 53 ++-- .../frontend/src/components/MkPageWindow.vue | 2 + .../frontend/src/components/MkPagination.vue | 6 + .../src/components/MkPullToRefresh.vue | 248 ++++++++++++++++++ .../frontend/src/components/MkTimeline.vue | 146 ++++++----- .../frontend/src/pages/settings/general.vue | 3 + packages/frontend/src/pages/timeline.vue | 9 +- packages/frontend/src/store.ts | 4 + packages/frontend/src/stream.ts | 20 +- .../src/ui/_common_/stream-indicator.vue | 4 +- packages/frontend/src/ui/universal.vue | 2 +- 15 files changed, 429 insertions(+), 83 deletions(-) create mode 100644 packages/frontend/src/components/MkPullToRefresh.vue diff --git a/locales/en-US.yml b/locales/en-US.yml index d448686187..b1725a2fb8 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1098,6 +1098,10 @@ expired: "Expired" doYouAgree: "Agree?" beSureToReadThisAsItIsImportant: "Please read this important information." iHaveReadXCarefullyAndAgree: "I have read the text \"{x}\" and agree." +releaseToRefresh: "Release to reload" +refreshing: "Reloading" +pullDownToRefresh: "Pull down to reload" +disableStreamingTimeline: "Disable realtime update on timeline" _initialAccountSetting: accountCreated: "Your account was successfully created!" letsStartAccountSetup: "For starters, let's set up your profile." diff --git a/locales/index.d.ts b/locales/index.d.ts index b0b8a256f2..114247b437 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1109,6 +1109,10 @@ export interface Locale { "pastAnnouncements": string; "youHaveUnreadAnnouncements": string; "externalServices": string; + "releaseToRefresh": string; + "refreshing": string; + "pullDownToRefresh": string; + "disableStreamingTimeline": string; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 979022e33a..907cbf0f5f 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1106,6 +1106,10 @@ currentAnnouncements: "現在のお知らせ" pastAnnouncements: "過去のお知らせ" youHaveUnreadAnnouncements: "未読のお知らせがあります。" externalServices: "外部サービス" +releaseToRefresh: "離してリロード" +refreshing: "リロード中" +pullDownToRefresh: "引っ張ってリロード" +disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする" _announcement: forExistingUsers: "既存ユーザーのみ" diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 2038ef3455..6f5ddcac14 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'; import { version, ui, lang, updateLocale } from '@/config'; import { i18n, updateI18n } from '@/i18n'; import { confirm, alert, post, popup, toast } from '@/os'; -import { useStream } from '@/stream'; +import { useStream, isReloading } from '@/stream'; import * as sound from '@/scripts/sound'; import { $i, refreshAccount, login, updateAccount, signout } from '@/account'; import { defaultStore, ColdDeviceStorage } from '@/store'; @@ -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/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index cc7657ba97..b7910d475a 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -4,26 +4,29 @@ SPDX-License-Identifier: AGPL-3.0-only --> + + diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index 714bc17e7b..0320d8df96 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 --> diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index 3b39a5c00a..1a62bfb343 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -135,6 +135,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.openImageInNewTab }} {{ i18n.ts.enableInfiniteScroll }} + {{ i18n.ts.disableStreamingTimeline }}
@@ -231,6 +232,7 @@ const mediaListWithOneImageAppearance = computed(defaultStore.makeGetterSetter(' const notificationPosition = computed(defaultStore.makeGetterSetter('notificationPosition')); const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis')); const showTimelineReplies = computed(defaultStore.makeGetterSetter('showTimelineReplies')); +const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline')); watch(lang, () => { miLocalStorage.setItem('lang', lang.value as string); @@ -264,6 +266,7 @@ watch([ instanceTicker, overridedDeviceKind, mediaListWithOneImageAppearance, + disableStreamingTimeline, ], async () => { await reloadAsk(); }); diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index d7bccc8000..f16dcd66ed 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -38,6 +38,7 @@ import { i18n } from '@/i18n'; import { instance } from '@/instance'; import { $i } from '@/account'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { deviceKind } from '@/scripts/device-kind.js'; provide('shouldOmitHeaderTitle', true); @@ -121,7 +122,13 @@ function focus(): void { tlComponent.focus(); } -const headerActions = $computed(() => []); +const headerActions = $computed(() => [ + ...[deviceKind === 'desktop' ? { + icon: 'ti ti-refresh', + text: i18n.ts.reload, + handler: () => { tlComponent.reloadTimeline(); }, + } : {}], +]); const headerTabs = $computed(() => [{ key: 'home', diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 9aba8017ff..7a708917e0 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -351,6 +351,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: {} as Record>, }, + disableStreamingTimeline: { + where: 'device', + default: false, + }, })); // TODO: 他のタブと永続化されたstateを同期 diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts index b241316648..ce07d39556 100644 --- a/packages/frontend/src/stream.ts +++ b/packages/frontend/src/stream.ts @@ -9,6 +9,9 @@ import { $i } from '@/account'; import { url } from '@/config'; 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 d252496829..a5af019582 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