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
-->
-
-
-
-
-
{{ i18n.ts.noNotifications }}
-
-
+
+
+
+
+
+
{{ i18n.ts.noNotifications }}
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
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 }}
{{ i18n.ts.whenServerDisconnected }}
@@ -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