From 4f5d3f6f7d724eafa6cf82a3ce24f8fbb7fe7f84 Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Thu, 31 Jul 2025 21:45:34 +0900
Subject: fix(frontend): MkNotesTimelineの日付dividerのスタイル修正 (#16306)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/frontend/src/components/MkStreamingNotesTimeline.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

(limited to 'packages/frontend/src/components/MkStreamingNotesTimeline.vue')

diff --git a/packages/frontend/src/components/MkStreamingNotesTimeline.vue b/packages/frontend/src/components/MkStreamingNotesTimeline.vue
index 3e50bdefd2..c2b4d6cd04 100644
--- a/packages/frontend/src/components/MkStreamingNotesTimeline.vue
+++ b/packages/frontend/src/components/MkStreamingNotesTimeline.vue
@@ -32,9 +32,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<template v-for="(note, i) in paginator.items.value" :key="note.id">
 				<div v-if="i > 0 && isSeparatorNeeded(paginator.items.value[i -1].createdAt, note.createdAt)" :data-scroll-anchor="note.id">
 					<div :class="$style.date">
-						<span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(paginator.items.value[i -1].createdAt, note.createdAt).prevText }}</span>
+						<span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(paginator.items.value[i -1].createdAt, note.createdAt)?.prevText }}</span>
 						<span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span>
-						<span>{{ getSeparatorInfo(paginator.items.value[i -1].createdAt, note.createdAt).nextText }} <i class="ti ti-chevron-down"></i></span>
+						<span>{{ getSeparatorInfo(paginator.items.value[i -1].createdAt, note.createdAt)?.nextText }} <i class="ti ti-chevron-down"></i></span>
 					</div>
 					<MkNote :class="$style.note" :note="note" :withHardMute="true"/>
 				</div>
-- 
cgit v1.2.3-freya


From bae92a944dc81b5033e918fd78052a206cb6f10f Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Fri, 15 Aug 2025 12:40:37 +0900
Subject: enhance(frontend): improve enableInfiniteScroll stability

Close #16318
---
 CHANGELOG.md                                                         | 1 +
 packages/frontend/src/components/MkStreamingNotesTimeline.vue        | 2 +-
 .../frontend/src/components/MkStreamingNotificationsTimeline.vue     | 2 +-
 packages/frontend/src/directives/appear.ts                           | 5 ++++-
 4 files changed, 7 insertions(+), 3 deletions(-)

(limited to 'packages/frontend/src/components/MkStreamingNotesTimeline.vue')

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b998dfee6f..fab0f01629 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -33,6 +33,7 @@
 		- URLに`?safemode=true`を付ける
 		- PWAのショートカットで Safemode を選択して起動する
 - Feat: ページのタブバーを下部に表示できるように
+- Enhance: 「自動でもっと見る」オプションが有効になり、安定性が向上しました
 - Enhance: コントロールパネルを検索できるように
 - Enhance: トルコ語 (tr-TR) に対応
 - Enhance: 不必要な翻訳データを読み込まなくなり、パフォーマンスが向上しました
diff --git a/packages/frontend/src/components/MkStreamingNotesTimeline.vue b/packages/frontend/src/components/MkStreamingNotesTimeline.vue
index c2b4d6cd04..9ace0b32d5 100644
--- a/packages/frontend/src/components/MkStreamingNotesTimeline.vue
+++ b/packages/frontend/src/components/MkStreamingNotesTimeline.vue
@@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkNote v-else :class="$style.note" :note="note" :withHardMute="true" :data-scroll-anchor="note.id"/>
 			</template>
 		</component>
-		<button v-show="paginator.canFetchOlder.value" key="_more_" :disabled="paginator.fetchingOlder.value" class="_button" :class="$style.more" @click="paginator.fetchOlder">
+		<button v-show="paginator.canFetchOlder.value" key="_more_" v-appear="prefer.s.enableInfiniteScroll ? paginator.fetchOlder : null" :disabled="paginator.fetchingOlder.value" class="_button" :class="$style.more" @click="paginator.fetchOlder">
 			<div v-if="!paginator.fetchingOlder.value">{{ i18n.ts.loadMore }}</div>
 			<MkLoading v-else :inline="true"/>
 		</button>
diff --git a/packages/frontend/src/components/MkStreamingNotificationsTimeline.vue b/packages/frontend/src/components/MkStreamingNotificationsTimeline.vue
index ac1f06619a..93ffee8d98 100644
--- a/packages/frontend/src/components/MkStreamingNotificationsTimeline.vue
+++ b/packages/frontend/src/components/MkStreamingNotificationsTimeline.vue
@@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<XNotification v-else :class="$style.content" :notification="notification" :withTime="true" :full="true"/>
 			</div>
 		</component>
-		<button v-show="paginator.canFetchOlder.value" key="_more_" :disabled="paginator.fetchingOlder.value" class="_button" :class="$style.more" @click="paginator.fetchOlder">
+		<button v-show="paginator.canFetchOlder.value" key="_more_" v-appear="prefer.s.enableInfiniteScroll ? paginator.fetchOlder : null" :disabled="paginator.fetchingOlder.value" class="_button" :class="$style.more" @click="paginator.fetchOlder">
 			<div v-if="!paginator.fetchingOlder.value">{{ i18n.ts.loadMore }}</div>
 			<MkLoading v-else/>
 		</button>
diff --git a/packages/frontend/src/directives/appear.ts b/packages/frontend/src/directives/appear.ts
index 802477e00b..f5fec108dc 100644
--- a/packages/frontend/src/directives/appear.ts
+++ b/packages/frontend/src/directives/appear.ts
@@ -3,6 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
+import { throttle } from 'throttle-debounce';
 import type { Directive } from 'vue';
 
 export default {
@@ -10,12 +11,14 @@ export default {
 		const fn = binding.value;
 		if (fn == null) return;
 
-		const observer = new IntersectionObserver(entries => {
+		const check = throttle(1000, (entries) => {
 			if (entries.some(entry => entry.isIntersecting)) {
 				fn();
 			}
 		});
 
+		const observer = new IntersectionObserver(check);
+
 		observer.observe(src);
 
 		src._observer_ = observer;
-- 
cgit v1.2.3-freya


From 05cc8047fae2d827ed3d98e735d19cd704f6126d Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Tue, 26 Aug 2025 13:58:57 +0900
Subject: refactor

---
 .../src/components/MkStreamingNotesTimeline.vue    | 59 +++++++++++++++-------
 packages/frontend/src/pages/user/home.vue          |  4 +-
 packages/frontend/src/ui/_common_/navbar.vue       |  2 +-
 .../src/ui/_common_/statusbar-federation.vue       |  6 +--
 .../src/ui/_common_/statusbar-user-list.vue        |  6 +--
 packages/frontend/src/ui/_common_/titlebar.vue     |  2 +-
 6 files changed, 50 insertions(+), 29 deletions(-)

(limited to 'packages/frontend/src/components/MkStreamingNotesTimeline.vue')

diff --git a/packages/frontend/src/components/MkStreamingNotesTimeline.vue b/packages/frontend/src/components/MkStreamingNotesTimeline.vue
index 9ace0b32d5..bc6ebf0918 100644
--- a/packages/frontend/src/components/MkStreamingNotesTimeline.vue
+++ b/packages/frontend/src/components/MkStreamingNotesTimeline.vue
@@ -297,76 +297,97 @@ function prepend(note: Misskey.entities.Note & MisskeyEntity) {
 	}
 }
 
-let connection: Misskey.IChannelConnection | null = null;
-let connection2: Misskey.IChannelConnection | null = null;
-
 const stream = store.s.realtimeMode ? useStream() : null;
 
+const connections = {
+	antenna: null as Misskey.IChannelConnection<Misskey.Channels['antenna']> | null,
+	homeTimeline: null as Misskey.IChannelConnection<Misskey.Channels['homeTimeline']> | null,
+	localTimeline: null as Misskey.IChannelConnection<Misskey.Channels['localTimeline']> | null,
+	hybridTimeline: null as Misskey.IChannelConnection<Misskey.Channels['hybridTimeline']> | null,
+	globalTimeline: null as Misskey.IChannelConnection<Misskey.Channels['globalTimeline']> | null,
+	main: null as Misskey.IChannelConnection<Misskey.Channels['main']> | null,
+	userList: null as Misskey.IChannelConnection<Misskey.Channels['userList']> | null,
+	channel: null as Misskey.IChannelConnection<Misskey.Channels['channel']> | null,
+	roleTimeline: null as Misskey.IChannelConnection<Misskey.Channels['roleTimeline']> | null,
+};
+
 function connectChannel() {
 	if (stream == null) return;
 	if (props.src === 'antenna') {
 		if (props.antenna == null) return;
-		connection = stream.useChannel('antenna', {
+		connections.antenna = stream.useChannel('antenna', {
 			antennaId: props.antenna,
 		});
+		connections.antenna.on('note', prepend);
 	} else if (props.src === 'home') {
-		connection = stream.useChannel('homeTimeline', {
+		connections.homeTimeline = stream.useChannel('homeTimeline', {
 			withRenotes: props.withRenotes,
 			withFiles: props.onlyFiles ? true : undefined,
 		});
-		connection2 = stream.useChannel('main');
+		connections.main = stream.useChannel('main');
+		connections.homeTimeline.on('note', prepend);
 	} else if (props.src === 'local') {
-		connection = stream.useChannel('localTimeline', {
+		connections.localTimeline = stream.useChannel('localTimeline', {
 			withRenotes: props.withRenotes,
 			withReplies: props.withReplies,
 			withFiles: props.onlyFiles ? true : undefined,
 		});
+		connections.localTimeline.on('note', prepend);
 	} else if (props.src === 'social') {
-		connection = stream.useChannel('hybridTimeline', {
+		connections.hybridTimeline = stream.useChannel('hybridTimeline', {
 			withRenotes: props.withRenotes,
 			withReplies: props.withReplies,
 			withFiles: props.onlyFiles ? true : undefined,
 		});
+		connections.hybridTimeline.on('note', prepend);
 	} else if (props.src === 'global') {
-		connection = stream.useChannel('globalTimeline', {
+		connections.globalTimeline = stream.useChannel('globalTimeline', {
 			withRenotes: props.withRenotes,
 			withFiles: props.onlyFiles ? true : undefined,
 		});
+		connections.globalTimeline.on('note', prepend);
 	} else if (props.src === 'mentions') {
-		connection = stream.useChannel('main');
-		connection.on('mention', prepend);
+		connections.main = stream.useChannel('main');
+		connections.main.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);
+		connections.main = stream.useChannel('main');
+		connections.main.on('mention', onNote);
 	} else if (props.src === 'list') {
 		if (props.list == null) return;
-		connection = stream.useChannel('userList', {
+		connections.userList = stream.useChannel('userList', {
 			withRenotes: props.withRenotes,
 			withFiles: props.onlyFiles ? true : undefined,
 			listId: props.list,
 		});
+		connections.userList.on('note', prepend);
 	} else if (props.src === 'channel') {
 		if (props.channel == null) return;
-		connection = stream.useChannel('channel', {
+		connections.channel = stream.useChannel('channel', {
 			channelId: props.channel,
 		});
+		connections.channel.on('note', prepend);
 	} else if (props.src === 'role') {
 		if (props.role == null) return;
-		connection = stream.useChannel('roleTimeline', {
+		connections.roleTimeline = stream.useChannel('roleTimeline', {
 			roleId: props.role,
 		});
+		connections.roleTimeline.on('note', prepend);
 	}
-	if (props.src !== 'directs' && props.src !== 'mentions') connection?.on('note', prepend);
 }
 
 function disconnectChannel() {
-	if (connection) connection.dispose();
-	if (connection2) connection2.dispose();
+	for (const key in connections) {
+		const conn = connections[key as keyof typeof connections];
+		if (conn != null) {
+			conn.dispose();
+			connections[key as keyof typeof connections] = null;
+		}
+	}
 }
 
 if (store.s.realtimeMode) {
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index e10c44960a..6933d64214 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 							</MkFukidashi>
 						</div>
 						<div v-if="user.roles.length > 0" class="roles">
-							<span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }">
+							<span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color ?? '' }">
 								<MkA v-adaptive-bg :to="`/roles/${role.id}`">
 									<img v-if="role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="role.iconUrl"/>
 									{{ role.name }}
@@ -249,7 +249,7 @@ const style = computed(() => {
 });
 
 const age = computed(() => {
-	return calcAge(props.user.birthday);
+	return props.user.birthday ? calcAge(props.user.birthday) : NaN;
 });
 
 function menu(ev: MouseEvent) {
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index 28913a54ab..4c43bf2b3b 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div :class="$style.body">
 		<div :class="$style.top">
 			<button v-tooltip.noDelay.right="instance.name ?? i18n.ts.instance" class="_button" :class="$style.instance" @click="openInstanceMenu">
-				<img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon" style="viewTransitionName: navbar-serverIcon;"/>
+				<img :src="instance.iconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon" style="viewTransitionName: navbar-serverIcon;"/>
 			</button>
 			<button v-if="!iconOnly" v-tooltip.noDelay.right="i18n.ts.realtimeMode" class="_button" :class="[$style.realtimeMode, store.r.realtimeMode.value ? $style.on : null]" @click="toggleRealtimeMode">
 				<i class="ti ti-bolt ti-fw"></i>
diff --git a/packages/frontend/src/ui/_common_/statusbar-federation.vue b/packages/frontend/src/ui/_common_/statusbar-federation.vue
index 7248e8826b..c79f690cff 100644
--- a/packages/frontend/src/ui/_common_/statusbar-federation.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-federation.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			mode="default"
 		>
 			<MkMarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse">
-				<span v-for="instance in instances" :key="instance.id" :class="[$style.item, { [$style.colored]: colored }]" :style="{ background: colored ? instance.themeColor : null }">
+				<span v-for="instance in instances" :key="instance.id" :class="[$style.item, { [$style.colored]: colored }]" :style="{ background: colored ? instance.themeColor : '' }">
 					<img :class="$style.icon" :src="getInstanceIcon(instance)" alt=""/>
 					<MkA :to="`/instance-info/${instance.host}`" :class="$style.host" class="_monospace">
 						{{ instance.host }}
@@ -33,9 +33,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import { useInterval } from '@@/js/use-interval.js';
 import MkMarqueeText from '@/components/MkMarqueeText.vue';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { useInterval } from '@@/js/use-interval.js';
 import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js';
 
 const props = defineProps<{
@@ -44,7 +44,7 @@ const props = defineProps<{
 	marqueeDuration?: number;
 	marqueeReverse?: boolean;
 	oneByOneInterval?: number;
-	refreshIntervalSec?: number;
+	refreshIntervalSec: number;
 }>();
 
 const instances = ref<Misskey.entities.FederationInstance[]>([]);
diff --git a/packages/frontend/src/ui/_common_/statusbar-user-list.vue b/packages/frontend/src/ui/_common_/statusbar-user-list.vue
index 13139a1064..8a754e6e64 100644
--- a/packages/frontend/src/ui/_common_/statusbar-user-list.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-user-list.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		>
 			<MkMarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse">
 				<span v-for="note in notes" :key="note.id" :class="$style.item">
-					<img :class="$style.avatar" :src="note.user.avatarUrl" decoding="async"/>
+					<img v-if="note.user.avatarUrl" :class="$style.avatar" :src="note.user.avatarUrl" decoding="async"/>
 					<MkA :class="$style.text" :to="notePage(note)">
 						<Mfm :text="getNoteSummary(note)" :plain="true" :nowrap="true"/>
 					</MkA>
@@ -33,9 +33,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref, watch } from 'vue';
 import * as Misskey from 'misskey-js';
+import { useInterval } from '@@/js/use-interval.js';
 import MkMarqueeText from '@/components/MkMarqueeText.vue';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { useInterval } from '@@/js/use-interval.js';
 import { getNoteSummary } from '@/utility/get-note-summary.js';
 import { notePage } from '@/filters/note.js';
 
@@ -45,7 +45,7 @@ const props = defineProps<{
 	marqueeDuration?: number;
 	marqueeReverse?: boolean;
 	oneByOneInterval?: number;
-	refreshIntervalSec?: number;
+	refreshIntervalSec: number;
 }>();
 
 const notes = ref<Misskey.entities.Note[]>([]);
diff --git a/packages/frontend/src/ui/_common_/titlebar.vue b/packages/frontend/src/ui/_common_/titlebar.vue
index c62b13b73a..1b9d47ec40 100644
--- a/packages/frontend/src/ui/_common_/titlebar.vue
+++ b/packages/frontend/src/ui/_common_/titlebar.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div :class="$style.root">
 	<div :class="$style.title">
-		<img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/>
+		<img :src="instance.iconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/>
 		<span :class="$style.instanceTitle">{{ instance.name ?? host }}</span>
 	</div>
 	<div :class="$style.controls">
-- 
cgit v1.2.3-freya

