Compare commits
	
		
			126 Commits
		
	
	
		
			2024.9.0-a
			...
			pag-back
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					b312f360c5 | ||
| 
						 | 
					e93c58ffa4 | ||
| 
						 | 
					4abe7e79a9 | ||
| 
						 | 
					f34bcf0c1d | ||
| 
						 | 
					fb5bb950de | ||
| 
						 | 
					ac96aba0f5 | ||
| 
						 | 
					9a352d4949 | ||
| 
						 | 
					79934c7931 | ||
| 
						 | 
					0994adc748 | ||
| 
						 | 
					0449c1a7ea | ||
| 
						 | 
					f3da1bcbbd | ||
| 
						 | 
					7fc2309822 | ||
| 
						 | 
					d92fe0803c | ||
| 
						 | 
					ae949af6c3 | ||
| 
						 | 
					f7ddff7475 | ||
| 
						 | 
					848b72ae21 | ||
| 
						 | 
					dab76b5e77 | ||
| 
						 | 
					4785b9bfdd | ||
| 
						 | 
					9f79e494f5 | ||
| 
						 | 
					51cf5c57f0 | ||
| 
						 | 
					9f4df717e8 | ||
| 
						 | 
					cf764eebe3 | ||
| 
						 | 
					d6c3f34eea | ||
| 
						 | 
					c265569008 | ||
| 
						 | 
					d6e57059e4 | ||
| 
						 | 
					7ccdd503b7 | ||
| 
						 | 
					560a1fecf5 | ||
| 
						 | 
					ef69eee155 | ||
| 
						 | 
					20ae59756f | ||
| 
						 | 
					3cc22e5e1c | ||
| 
						 | 
					4e775a670f | ||
| 
						 | 
					e6ee5704e8 | ||
| 
						 | 
					d0a119c2ea | ||
| 
						 | 
					ee1e2aa200 | ||
| 
						 | 
					a2f6bf3d5c | ||
| 
						 | 
					4c83663597 | ||
| 
						 | 
					4e7a26e6d5 | ||
| 
						 | 
					660b030233 | ||
| 
						 | 
					fc50dc7a67 | ||
| 
						 | 
					05042a0697 | ||
| 
						 | 
					5bfb98df00 | ||
| 
						 | 
					b02187d9d0 | ||
| 
						 | 
					e2f3091778 | ||
| 
						 | 
					18611ab521 | ||
| 
						 | 
					e8316dc4c4 | ||
| 
						 | 
					72ae8441e1 | ||
| 
						 | 
					4aee99b61a | ||
| 
						 | 
					e9486d0085 | ||
| 
						 | 
					7e06305b96 | ||
| 
						 | 
					94f9ebc80c | ||
| 
						 | 
					f7d776e4da | ||
| 
						 | 
					d5b4fa7e50 | ||
| 
						 | 
					f3a0839552 | ||
| 
						 | 
					b0c6675ef3 | ||
| 
						 | 
					72998adfb6 | ||
| 
						 | 
					954d934505 | ||
| 
						 | 
					4cd9623dc3 | ||
| 
						 | 
					1ccac0c1e3 | ||
| 
						 | 
					7895474263 | ||
| 
						 | 
					fd44a29f2b | ||
| 
						 | 
					054ea30955 | ||
| 
						 | 
					dd02648f8d | ||
| 
						 | 
					81238fabd2 | ||
| 
						 | 
					3677a91c4a | ||
| 
						 | 
					b2c1f5873d | ||
| 
						 | 
					76145701af | ||
| 
						 | 
					0079f3394b | ||
| 
						 | 
					cb63a1ed00 | ||
| 
						 | 
					1062371296 | ||
| 
						 | 
					3f6f6a49b6 | ||
| 
						 | 
					d73ea541bf | ||
| 
						 | 
					f7425f5fe9 | ||
| 
						 | 
					b60dba701c | ||
| 
						 | 
					b5f85aa9a8 | ||
| 
						 | 
					6152122d43 | ||
| 
						 | 
					d335da5ee4 | ||
| 
						 | 
					d82d03890d | ||
| 
						 | 
					4881237955 | ||
| 
						 | 
					fc91526857 | ||
| 
						 | 
					da4aba3247 | ||
| 
						 | 
					568822944f | ||
| 
						 | 
					393160eeda | ||
| 
						 | 
					0f64372abb | ||
| 
						 | 
					02054528f9 | ||
| 
						 | 
					31b62db14b | ||
| 
						 | 
					41824ae383 | ||
| 
						 | 
					5a5ef7564a | ||
| 
						 | 
					78944bf441 | ||
| 
						 | 
					f565e0f8a5 | ||
| 
						 | 
					bec510e37d | ||
| 
						 | 
					b446bfb0b6 | ||
| 
						 | 
					3bbeac4be2 | ||
| 
						 | 
					e7251220d5 | ||
| 
						 | 
					4bef4953b8 | ||
| 
						 | 
					e609b3b7dc | ||
| 
						 | 
					7fe882d0e2 | ||
| 
						 | 
					b330ede502 | ||
| 
						 | 
					f30275a975 | ||
| 
						 | 
					04ff07e4e7 | ||
| 
						 | 
					7d4f33d2c0 | ||
| 
						 | 
					2a434c63df | ||
| 
						 | 
					a1b90d6dd3 | ||
| 
						 | 
					c7c3c32871 | ||
| 
						 | 
					4fabe26b07 | ||
| 
						 | 
					752c01ba91 | ||
| 
						 | 
					ba3fa8b431 | ||
| 
						 | 
					2bbada3cd4 | ||
| 
						 | 
					a26f289dd5 | ||
| 
						 | 
					8213380ded | ||
| 
						 | 
					68d647d6b8 | ||
| 
						 | 
					130ece74f9 | ||
| 
						 | 
					fae912a754 | ||
| 
						 | 
					877a7a81bb | ||
| 
						 | 
					88315d3e80 | ||
| 
						 | 
					af00c2c96c | ||
| 
						 | 
					974f7c13d3 | ||
| 
						 | 
					44dee0f883 | ||
| 
						 | 
					794ff58b07 | ||
| 
						 | 
					f5a019a6d6 | ||
| 
						 | 
					ddb41bd0ba | ||
| 
						 | 
					035c98dc15 | ||
| 
						 | 
					b4d532efb4 | ||
| 
						 | 
					28f914f67f | ||
| 
						 | 
					2481123972 | ||
| 
						 | 
					5f1cd1e532 | ||
| 
						 | 
					9f246e3dc7 | 
							
								
								
									
										7
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -533,7 +533,7 @@ export interface Locale {
 | 
			
		||||
    "deleteAll": string;
 | 
			
		||||
    "showFixedPostForm": string;
 | 
			
		||||
    "showFixedPostFormInChannel": string;
 | 
			
		||||
    "newNoteRecived": string;
 | 
			
		||||
    "goToTheHeadOfTimeline": string;
 | 
			
		||||
    "sounds": string;
 | 
			
		||||
    "sound": string;
 | 
			
		||||
    "listen": string;
 | 
			
		||||
@@ -1103,6 +1103,7 @@ export interface Locale {
 | 
			
		||||
    "doYouAgree": string;
 | 
			
		||||
    "beSureToReadThisAsItIsImportant": string;
 | 
			
		||||
    "iHaveReadXCarefullyAndAgree": string;
 | 
			
		||||
    "timelineBackTopBehavior": string;
 | 
			
		||||
    "dialog": string;
 | 
			
		||||
    "icon": string;
 | 
			
		||||
    "forYou": string;
 | 
			
		||||
@@ -1672,6 +1673,10 @@ export interface Locale {
 | 
			
		||||
        "dialog": string;
 | 
			
		||||
        "quiet": string;
 | 
			
		||||
    };
 | 
			
		||||
    "_timelineBackTopBehavior": {
 | 
			
		||||
        "newest": string;
 | 
			
		||||
        "next": string;
 | 
			
		||||
    };
 | 
			
		||||
    "_channel": {
 | 
			
		||||
        "create": string;
 | 
			
		||||
        "edit": string;
 | 
			
		||||
 
 | 
			
		||||
@@ -530,7 +530,7 @@ serverLogs: "サーバーログ"
 | 
			
		||||
deleteAll: "全て削除"
 | 
			
		||||
showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
 | 
			
		||||
showFixedPostFormInChannel: "タイムライン上部に投稿フォームを表示する(チャンネル)"
 | 
			
		||||
newNoteRecived: "新しいノートがあります"
 | 
			
		||||
goToTheHeadOfTimeline: "最新のノートに移動"
 | 
			
		||||
sounds: "サウンド"
 | 
			
		||||
sound: "サウンド"
 | 
			
		||||
listen: "聴く"
 | 
			
		||||
@@ -1100,6 +1100,7 @@ expired: "期限切れ"
 | 
			
		||||
doYouAgree: "同意しますか?"
 | 
			
		||||
beSureToReadThisAsItIsImportant: "重要ですので必ずお読みください。"
 | 
			
		||||
iHaveReadXCarefullyAndAgree: "「{x}」の内容をよく読み、同意します。"
 | 
			
		||||
timelineBackTopBehavior: "タイムラインのスクロールが先頭に戻った時の挙動"
 | 
			
		||||
dialog: "ダイアログ"
 | 
			
		||||
icon: "アイコン"
 | 
			
		||||
forYou: "あなたへ"
 | 
			
		||||
@@ -1589,6 +1590,10 @@ _serverDisconnectedBehavior:
 | 
			
		||||
  dialog: "ダイアログで警告"
 | 
			
		||||
  quiet: "控えめに警告"
 | 
			
		||||
 | 
			
		||||
_timelineBackTopBehavior:
 | 
			
		||||
  newest: "最新の投稿を表示"
 | 
			
		||||
  next: "次の投稿を遡る"
 | 
			
		||||
 | 
			
		||||
_channel:
 | 
			
		||||
  create: "チャンネルを作成"
 | 
			
		||||
  edit: "チャンネルを編集"
 | 
			
		||||
 
 | 
			
		||||
@@ -68,6 +68,7 @@
 | 
			
		||||
		"tsconfig-paths": "4.2.0",
 | 
			
		||||
		"twemoji-parser": "14.0.0",
 | 
			
		||||
		"typescript": "5.2.2",
 | 
			
		||||
		"ua-parser-js": "2.0.0-alpha.2",
 | 
			
		||||
		"uuid": "9.0.1",
 | 
			
		||||
		"vanilla-tilt": "1.8.1",
 | 
			
		||||
		"vite": "4.4.9",
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, h, PropType, TransitionGroup, useCssModule } from 'vue';
 | 
			
		||||
import { defineComponent, h, PropType, TransitionGroup, useCssModule, watch } from 'vue';
 | 
			
		||||
import MkAd from '@/components/global/MkAd.vue';
 | 
			
		||||
import { isDebuggerEnabled, stackTraceInstances } from '@/debug';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
@@ -38,6 +38,11 @@ export default defineComponent({
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: false,
 | 
			
		||||
		},
 | 
			
		||||
		denyMoveTransition: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: false,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	setup(props, { slots, expose }) {
 | 
			
		||||
@@ -135,6 +140,7 @@ export default defineComponent({
 | 
			
		||||
					[$style['reversed']]: props.reversed,
 | 
			
		||||
					[$style['direction-down']]: props.direction === 'down',
 | 
			
		||||
					[$style['direction-up']]: props.direction === 'up',
 | 
			
		||||
					'deny-move-transition': props.denyMoveTransition,
 | 
			
		||||
				},
 | 
			
		||||
				...(defaultStore.state.animation ? {
 | 
			
		||||
					name: 'list',
 | 
			
		||||
@@ -153,15 +159,11 @@ export default defineComponent({
 | 
			
		||||
	container-type: inline-size;
 | 
			
		||||
 | 
			
		||||
	&:global {
 | 
			
		||||
	> .list-move {
 | 
			
		||||
	&:not(.deny-move-transition) > .list-move {
 | 
			
		||||
		transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.deny-move-transition > .list-move {
 | 
			
		||||
		transition: none !important;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .list-enter-active {
 | 
			
		||||
	&:not(.deny-move-transition) > .list-enter-active {
 | 
			
		||||
		transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
		</div>
 | 
			
		||||
	</template>
 | 
			
		||||
 | 
			
		||||
	<template #default="{ items: notes }">
 | 
			
		||||
	<template #default="{ items: notes, denyMoveTransition }">
 | 
			
		||||
		<div :class="[$style.root, { [$style.noGap]: noGap }]">
 | 
			
		||||
			<MkDateSeparatedList
 | 
			
		||||
				ref="notes"
 | 
			
		||||
@@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
				:noGap="noGap"
 | 
			
		||||
				:ad="true"
 | 
			
		||||
				:class="$style.notes"
 | 
			
		||||
				:denyMoveTransition="denyMoveTransition"
 | 
			
		||||
			>
 | 
			
		||||
				<MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/>
 | 
			
		||||
			</MkDateSeparatedList>
 | 
			
		||||
 
 | 
			
		||||
@@ -12,9 +12,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
		</div>
 | 
			
		||||
	</template>
 | 
			
		||||
 | 
			
		||||
	<template #default="{ items: notifications }">
 | 
			
		||||
		<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
 | 
			
		||||
			<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
 | 
			
		||||
	<template #default="{ items: notifications, denyMoveTransition }">
 | 
			
		||||
		<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true" :denyMoveTransition="denyMoveTransition">
 | 
			
		||||
			<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="`showNotificationAsNote:${notification.id}`" :note="notification.note"/>
 | 
			
		||||
			<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel notification"/>
 | 
			
		||||
		</MkDateSeparatedList>
 | 
			
		||||
	</template>
 | 
			
		||||
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onUnmounted, onMounted, computed, shallowRef } from 'vue';
 | 
			
		||||
import { onUnmounted, onMounted, computed, shallowRef, watch } from 'vue';
 | 
			
		||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
 | 
			
		||||
import XNotification from '@/components/MkNotification.vue';
 | 
			
		||||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 | 
			
		||||
@@ -55,10 +55,16 @@ const onNotification = (notification) => {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (!isMuted) {
 | 
			
		||||
		pagingComponent.value.prepend(notification);
 | 
			
		||||
		pagingComponent.value?.prepend(notification);
 | 
			
		||||
	}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
watch(() => pagingComponent.value?.backed, (backed) => {
 | 
			
		||||
	if (backed === false) {
 | 
			
		||||
		useStream().send('readNotification');
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
let connection;
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
	</template>
 | 
			
		||||
 | 
			
		||||
	<div ref="contents" :class="$style.root" style="container-type: inline-size;">
 | 
			
		||||
		<RouterView :key="reloadCount" :router="router"/>
 | 
			
		||||
		<RouterView :key="reloadCount" :router="router" :scrollContainer="contents"/>
 | 
			
		||||
	</div>
 | 
			
		||||
</MkWindow>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -37,12 +37,11 @@ import copyToClipboard from '@/scripts/copy-to-clipboard';
 | 
			
		||||
import { url } from '@/config';
 | 
			
		||||
import { mainRouter, routes, page } from '@/router';
 | 
			
		||||
import { $i } from '@/account';
 | 
			
		||||
import { Router, useScrollPositionManager } from '@/nirax';
 | 
			
		||||
import { Router } from '@/nirax';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata';
 | 
			
		||||
import { openingWindowsCount } from '@/os';
 | 
			
		||||
import { claimAchievement } from '@/scripts/achievements';
 | 
			
		||||
import { getScrollContainer } from '@/scripts/scroll';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	initialPath: string;
 | 
			
		||||
@@ -146,8 +145,6 @@ function popout() {
 | 
			
		||||
	windowEl.close();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
useScrollPositionManager(() => getScrollContainer(contents.value), router);
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	openingWindowsCount.value++;
 | 
			
		||||
	if (openingWindowsCount.value >= 3) {
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
>
 | 
			
		||||
	<MkLoading v-if="fetching"/>
 | 
			
		||||
 | 
			
		||||
	<MkError v-else-if="error" @retry="init()"/>
 | 
			
		||||
	<MkError v-else-if="empty && error" @retry="reload()"/>
 | 
			
		||||
 | 
			
		||||
	<div v-else-if="empty" key="_empty_" class="empty">
 | 
			
		||||
		<slot name="empty">
 | 
			
		||||
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
			</MkButton>
 | 
			
		||||
			<MkLoading v-else class="loading"/>
 | 
			
		||||
		</div>
 | 
			
		||||
		<slot :items="Array.from(items.values())" :fetching="fetching || moreFetching"></slot>
 | 
			
		||||
		<slot :items="providingItems" :fetching="fetching || moreFetching" :denyMoveTransition="denyMoveTransition"></slot>
 | 
			
		||||
		<div v-show="!pagination.reversed && more" key="_more_" class="_margin">
 | 
			
		||||
			<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMore : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary rounded @click="fetchMore">
 | 
			
		||||
				{{ i18n.ts.loadMore }}
 | 
			
		||||
@@ -46,20 +46,31 @@ 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';
 | 
			
		||||
import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@/scripts/scroll';
 | 
			
		||||
import { isBottomVisible, isTopVisible, getScrollContainer, scrollToBottom, scrollToTop, scrollBy, scroll, getBodyScrollHeight } from '@/scripts/scroll';
 | 
			
		||||
import { useDocumentVisibility } from '@/scripts/use-document-visibility';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
import { MisskeyEntity } from '@/types/date-separated-list';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
import { isWebKit } from '@/scripts/useragent';
 | 
			
		||||
 | 
			
		||||
const SECOND_FETCH_LIMIT = 30;
 | 
			
		||||
const TOLERANCE = 16;
 | 
			
		||||
const TOLERANCE = 6;
 | 
			
		||||
const APPEAR_MINIMUM_INTERVAL = 600;
 | 
			
		||||
 | 
			
		||||
export type Paging<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints> = {
 | 
			
		||||
	endpoint: E;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 一度にAPIへ取得する件数
 | 
			
		||||
	 */
 | 
			
		||||
	limit: number;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * タイムラインに表示する最大件数
 | 
			
		||||
	 */
 | 
			
		||||
	displayLimit?: number;
 | 
			
		||||
 | 
			
		||||
	params?: Misskey.Endpoints[E]['req'] | ComputedRef<Misskey.Endpoints[E]['req']>;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
@@ -87,6 +98,8 @@ function arrayToEntries(entities: MisskeyEntity[]): [string, MisskeyEntity][] {
 | 
			
		||||
function concatMapWithArray(map: MisskeyEntityMap, entities: MisskeyEntity[]): MisskeyEntityMap {
 | 
			
		||||
	return new Map([...map, ...arrayToEntries(entities)]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const timelineBackTopBehavior = computed(() => isWebKit() ? 'newest' : defaultStore.reactiveState.timelineBackTopBehavior.value);
 | 
			
		||||
</script>
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { infoImageUrl } from '@/instance';
 | 
			
		||||
@@ -94,19 +107,19 @@ import { infoImageUrl } from '@/instance';
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
	pagination: Paging;
 | 
			
		||||
	disableAutoLoad?: boolean;
 | 
			
		||||
	displayLimit?: number;
 | 
			
		||||
}>(), {
 | 
			
		||||
	displayLimit: 20,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(ev: 'queue', count: number): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
let rootEl = $shallowRef<HTMLElement>();
 | 
			
		||||
 | 
			
		||||
// 遡り中かどうか
 | 
			
		||||
/**
 | 
			
		||||
 * スクロールが先頭にある場合はfalse
 | 
			
		||||
 * スクロールが先頭にない場合にtrue
 | 
			
		||||
 */
 | 
			
		||||
// 先頭にいるか(prependでキューに追加するかどうかの判定に使う)
 | 
			
		||||
let backed = $ref(false);
 | 
			
		||||
// true→falseの変更でexecuteQueueする
 | 
			
		||||
let weakBacked = $ref(false);
 | 
			
		||||
 | 
			
		||||
let scrollRemove = $ref<(() => void) | null>(null);
 | 
			
		||||
 | 
			
		||||
@@ -115,12 +128,14 @@ let scrollRemove = $ref<(() => void) | null>(null);
 | 
			
		||||
 * 最新が0番目
 | 
			
		||||
 */
 | 
			
		||||
const items = ref<MisskeyEntityMap>(new Map());
 | 
			
		||||
const providingItems = computed(() => Array.from(items.value.values()));
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * タブが非アクティブなどの場合に更新を貯めておく
 | 
			
		||||
 * 最新が0番目
 | 
			
		||||
 * 最新が最後(パフォーマンス上の理由でitemsと逆にした)
 | 
			
		||||
 */
 | 
			
		||||
const queue = ref<MisskeyEntityMap>(new Map());
 | 
			
		||||
const queueSize = computed(() => queue.value.size);
 | 
			
		||||
 | 
			
		||||
const offset = ref(0);
 | 
			
		||||
 | 
			
		||||
@@ -129,69 +144,153 @@ const offset = ref(0);
 | 
			
		||||
 */
 | 
			
		||||
const fetching = ref(true);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * onActivatedでtrue, onDeactivatedでfalseになる
 | 
			
		||||
 */
 | 
			
		||||
const active = ref(true);
 | 
			
		||||
 | 
			
		||||
const moreFetching = ref(false);
 | 
			
		||||
const more = ref(false);
 | 
			
		||||
const preventAppearFetchMore = ref(false);
 | 
			
		||||
const preventAppearFetchMoreTimer = ref<number | null>(null);
 | 
			
		||||
const isBackTop = ref(false);
 | 
			
		||||
const empty = computed(() => items.value.size === 0);
 | 
			
		||||
const error = ref(false);
 | 
			
		||||
const {
 | 
			
		||||
	enableInfiniteScroll,
 | 
			
		||||
} = defaultStore.reactiveState;
 | 
			
		||||
 | 
			
		||||
const displayLimit = computed(() => props.pagination.displayLimit ?? props.pagination.limit * 2);
 | 
			
		||||
 | 
			
		||||
const contentEl = $computed(() => props.pagination.pageEl ?? rootEl);
 | 
			
		||||
const scrollableElement = $computed(() => contentEl ? getScrollContainer(contentEl) : document.body);
 | 
			
		||||
const scrollableElement = $computed(() => contentEl ? getScrollContainer(contentEl) ?? null : null);
 | 
			
		||||
const scrollableElementOrHtml = $computed(() => scrollableElement ?? document.getElementsByName('html')[0]);
 | 
			
		||||
 | 
			
		||||
const visibility = useDocumentVisibility();
 | 
			
		||||
 | 
			
		||||
let isPausingUpdate = false;
 | 
			
		||||
let timerForSetPause: number | null = null;
 | 
			
		||||
const BACKGROUND_PAUSE_WAIT_SEC = 10;
 | 
			
		||||
const isPausingUpdateByExecutingQueue = ref(false);
 | 
			
		||||
const denyMoveTransition = ref(false);
 | 
			
		||||
 | 
			
		||||
// 先頭が表示されているかどうかを検出
 | 
			
		||||
// https://qiita.com/mkataigi/items/0154aefd2223ce23398e
 | 
			
		||||
//#region scrolling
 | 
			
		||||
const checkFn = props.pagination.reversed ? isBottomVisible : isTopVisible;
 | 
			
		||||
const checkTop = (tolerance?: number) => {
 | 
			
		||||
	if (!contentEl) return true;
 | 
			
		||||
	if (!document.body.contains(contentEl)) return true;
 | 
			
		||||
	return checkFn(contentEl, tolerance, scrollableElement);
 | 
			
		||||
};
 | 
			
		||||
/**
 | 
			
		||||
 * IntersectionObserverで大まかに検出
 | 
			
		||||
 * https://qiita.com/mkataigi/items/0154aefd2223ce23398e
 | 
			
		||||
 */
 | 
			
		||||
let scrollObserver = $ref<IntersectionObserver>();
 | 
			
		||||
 | 
			
		||||
watch([() => props.pagination.reversed, $$(scrollableElement)], () => {
 | 
			
		||||
	if (scrollObserver) scrollObserver.disconnect();
 | 
			
		||||
 | 
			
		||||
	scrollObserver = new IntersectionObserver(entries => {
 | 
			
		||||
		backed = entries[0].isIntersecting;
 | 
			
		||||
		if (!active.value) return; // activeでない時は触らない
 | 
			
		||||
		weakBacked = entries[0].intersectionRatio >= 0.1;
 | 
			
		||||
	}, {
 | 
			
		||||
		root: scrollableElement,
 | 
			
		||||
		rootMargin: props.pagination.reversed ? '-100% 0px 100% 0px' : '100% 0px -100% 0px',
 | 
			
		||||
		threshold: 0.01,
 | 
			
		||||
		rootMargin: props.pagination.reversed ? '-100% 0px 1000% 0px' : '1000% 0px -100% 0px',
 | 
			
		||||
		threshold: [0.01, 0.05, 0.1, 0.12, 0.15],
 | 
			
		||||
	});
 | 
			
		||||
}, { immediate: true });
 | 
			
		||||
 | 
			
		||||
watch($$(rootEl), () => {
 | 
			
		||||
watch([$$(rootEl), $$(scrollObserver)], () => {
 | 
			
		||||
	scrollObserver?.disconnect();
 | 
			
		||||
	nextTick(() => {
 | 
			
		||||
	if (rootEl) scrollObserver?.observe(rootEl);
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch([$$(backed), $$(contentEl)], () => {
 | 
			
		||||
	if (!backed) {
 | 
			
		||||
		if (!contentEl) return;
 | 
			
		||||
 | 
			
		||||
		scrollRemove = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl, executeQueue, TOLERANCE);
 | 
			
		||||
	} else {
 | 
			
		||||
		if (scrollRemove) scrollRemove();
 | 
			
		||||
		scrollRemove = null;
 | 
			
		||||
/**
 | 
			
		||||
 * weakBackedがtrue→falseになったらexecuteQueue
 | 
			
		||||
 */
 | 
			
		||||
watch($$(weakBacked), () => {
 | 
			
		||||
	if (timelineBackTopBehavior.value === 'next' && !weakBacked) {
 | 
			
		||||
		executeQueue();
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
if (props.pagination.params && isRef(props.pagination.params)) {
 | 
			
		||||
	watch(props.pagination.params, init, { deep: true });
 | 
			
		||||
/**
 | 
			
		||||
 * backedがtrue→falseになってもexecuteQueue
 | 
			
		||||
 */
 | 
			
		||||
watch($$(backed), () => {
 | 
			
		||||
	if (!backed) {
 | 
			
		||||
		executeQueue();
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * onScrollTop/onScrollBottomでbackedを厳密に検出する
 | 
			
		||||
 */
 | 
			
		||||
watch([$$(weakBacked), $$(contentEl)], () => {
 | 
			
		||||
	if (scrollRemove) scrollRemove();
 | 
			
		||||
	scrollRemove = null;
 | 
			
		||||
 | 
			
		||||
	if (weakBacked || !contentEl) {
 | 
			
		||||
		if (weakBacked) backed = true;
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	scrollRemove = (() => {
 | 
			
		||||
		const checkBacked = () => {
 | 
			
		||||
			if (!active.value) return; // activeでない時は触らない
 | 
			
		||||
			backed = !checkTop(TOLERANCE);
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		// とりあえず評価してみる
 | 
			
		||||
		checkBacked();
 | 
			
		||||
 | 
			
		||||
		const container = scrollableElementOrHtml;
 | 
			
		||||
 | 
			
		||||
		function removeListener() { container.removeEventListener('scroll', checkBacked); }
 | 
			
		||||
		container.addEventListener('scroll', checkBacked, { passive: true });
 | 
			
		||||
		return removeListener;
 | 
			
		||||
	})();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function preventDefault(ev: Event) {
 | 
			
		||||
	ev.preventDefault();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
watch(queue, (a, b) => {
 | 
			
		||||
	if (a.size === 0 && b.size === 0) return;
 | 
			
		||||
	emit('queue', queue.value.size);
 | 
			
		||||
}, { deep: true });
 | 
			
		||||
/**
 | 
			
		||||
 * アイテムを上に追加した場合に追加分だけスクロールを下にずらす
 | 
			
		||||
 * Safariでは使わない方がいいかも?
 | 
			
		||||
 * @param fn DOM操作(unshiftItemsなど)
 | 
			
		||||
 */
 | 
			
		||||
async function adjustScroll(fn: () => void): Promise<void> {
 | 
			
		||||
	await nextTick();
 | 
			
		||||
	const oldHeight = scrollableElement ? scrollableElement.scrollHeight : getBodyScrollHeight();
 | 
			
		||||
	const oldScroll = scrollableElement ? scrollableElement.scrollTop : window.scrollY;
 | 
			
		||||
	// スクロールをやめさせる
 | 
			
		||||
	try {
 | 
			
		||||
		// なぜかscrollableElementOrHtmlがundefinedであるというエラーが出る
 | 
			
		||||
		scrollableElementOrHtml.addEventListener('wheel', preventDefault, { passive: false });
 | 
			
		||||
		scrollableElementOrHtml.addEventListener('touchmove', preventDefault, { passive: false });
 | 
			
		||||
		// スクロールを強制的に停止
 | 
			
		||||
		scroll(scrollableElement, { top: oldScroll, behavior: 'instant' });
 | 
			
		||||
	} catch (err) {
 | 
			
		||||
		console.error(err, { scrollableElementOrHtml });
 | 
			
		||||
	}
 | 
			
		||||
	denyMoveTransition.value = true;
 | 
			
		||||
	fn();
 | 
			
		||||
	return await nextTick().then(() => {
 | 
			
		||||
		const top = oldScroll + ((scrollableElement ? scrollableElement.scrollHeight : getBodyScrollHeight()) - oldHeight);
 | 
			
		||||
		scroll(scrollableElement, { top, behavior: 'instant' });
 | 
			
		||||
		// なぜかscrollableElementOrHtmlがundefinedであるというエラーが出る
 | 
			
		||||
		scrollableElementOrHtml.removeEventListener('wheel', preventDefault);
 | 
			
		||||
		scrollableElementOrHtml.removeEventListener('touchmove', preventDefault);
 | 
			
		||||
	}).then(() => nextTick()).finally(() => {
 | 
			
		||||
		denyMoveTransition.value = false;
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
//#endregion
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 初期化
 | 
			
		||||
 * scrollAfterInitなどの後処理もあるので、reload関数を使うべき
 | 
			
		||||
 * 
 | 
			
		||||
 * 注意: moreFetchingをtrueにするのでfalseにする必要がある
 | 
			
		||||
 */
 | 
			
		||||
async function init(): Promise<void> {
 | 
			
		||||
	items.value = new Map();
 | 
			
		||||
	queue.value = new Map();
 | 
			
		||||
@@ -210,7 +309,7 @@ async function init(): Promise<void> {
 | 
			
		||||
			concatItems(res);
 | 
			
		||||
			more.value = false;
 | 
			
		||||
		} else {
 | 
			
		||||
			if (props.pagination.reversed) moreFetching.value = true;
 | 
			
		||||
			moreFetching.value = true;
 | 
			
		||||
			concatItems(res);
 | 
			
		||||
			more.value = true;
 | 
			
		||||
		}
 | 
			
		||||
@@ -224,10 +323,50 @@ async function init(): Promise<void> {
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const reload = (): Promise<void> => {
 | 
			
		||||
	return init();
 | 
			
		||||
/**
 | 
			
		||||
 * initの後に呼ぶ
 | 
			
		||||
 * コンポーネント作成直後でinitが呼ばれた時はonMountedで呼ばれる
 | 
			
		||||
 * reloadでinitが呼ばれた時はreload内でinitの後に呼ばれる
 | 
			
		||||
 */
 | 
			
		||||
function scrollAfterInit() {
 | 
			
		||||
	if (props.pagination.reversed) {
 | 
			
		||||
		nextTick(() => {
 | 
			
		||||
			setTimeout(async () => {
 | 
			
		||||
				if (contentEl) {
 | 
			
		||||
					scrollToBottom(contentEl);
 | 
			
		||||
					// scrollToしてもbacked周りがうまく動かないので手動で戻す必要がある
 | 
			
		||||
					weakBacked = false;
 | 
			
		||||
				}
 | 
			
		||||
			}, 200);
 | 
			
		||||
 | 
			
		||||
			// scrollToBottomでmoreFetchingボタンが画面外まで出るまで
 | 
			
		||||
			// more = trueを遅らせる
 | 
			
		||||
			setTimeout(() => {
 | 
			
		||||
				moreFetching.value = false;
 | 
			
		||||
			}, 2000);
 | 
			
		||||
		});
 | 
			
		||||
	} else {
 | 
			
		||||
		nextTick(() => {
 | 
			
		||||
			setTimeout(() => {
 | 
			
		||||
				scrollToTop(scrollableElement);
 | 
			
		||||
				// scrollToしてもbacked周りがうまく動かないので手動で戻す必要がある
 | 
			
		||||
				weakBacked = false;
 | 
			
		||||
 | 
			
		||||
				moreFetching.value = false;
 | 
			
		||||
			}, 200);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const reload = async (): Promise<void> => {
 | 
			
		||||
	await init();
 | 
			
		||||
	scrollAfterInit();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
if (props.pagination.params && isRef(props.pagination.params)) {
 | 
			
		||||
	watch(props.pagination.params, reload, { deep: true });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const fetchMore = async (): Promise<void> => {
 | 
			
		||||
	if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
 | 
			
		||||
	moreFetching.value = true;
 | 
			
		||||
@@ -246,29 +385,13 @@ const fetchMore = async (): Promise<void> => {
 | 
			
		||||
			if (i === 10) item._shouldInsertAd_ = true;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const reverseConcat = _res => {
 | 
			
		||||
			const oldHeight = scrollableElement ? scrollableElement.scrollHeight : getBodyScrollHeight();
 | 
			
		||||
			const oldScroll = scrollableElement ? scrollableElement.scrollTop : window.scrollY;
 | 
			
		||||
 | 
			
		||||
			items.value = concatMapWithArray(items.value, _res);
 | 
			
		||||
 | 
			
		||||
			return nextTick(() => {
 | 
			
		||||
				if (scrollableElement) {
 | 
			
		||||
					scroll(scrollableElement, { top: oldScroll + (scrollableElement.scrollHeight - oldHeight), behavior: 'instant' });
 | 
			
		||||
				} else {
 | 
			
		||||
					window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' });
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				return nextTick();
 | 
			
		||||
			});
 | 
			
		||||
		};
 | 
			
		||||
		const reverseConcat = (_res) => adjustScroll(() => concatMapWithArray(items.value, _res));
 | 
			
		||||
 | 
			
		||||
		if (res.length === 0) {
 | 
			
		||||
			if (props.pagination.reversed) {
 | 
			
		||||
				reverseConcat(res).then(() => {
 | 
			
		||||
				reverseConcat(res);
 | 
			
		||||
				more.value = false;
 | 
			
		||||
				moreFetching.value = false;
 | 
			
		||||
				});
 | 
			
		||||
			} else {
 | 
			
		||||
				items.value = concatMapWithArray(items.value, res);
 | 
			
		||||
				more.value = false;
 | 
			
		||||
@@ -276,10 +399,9 @@ const fetchMore = async (): Promise<void> => {
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			if (props.pagination.reversed) {
 | 
			
		||||
				reverseConcat(res).then(() => {
 | 
			
		||||
				reverseConcat(res);
 | 
			
		||||
				more.value = true;
 | 
			
		||||
				moreFetching.value = false;
 | 
			
		||||
				});
 | 
			
		||||
			} else {
 | 
			
		||||
				items.value = concatMapWithArray(items.value, res);
 | 
			
		||||
				more.value = true;
 | 
			
		||||
@@ -344,26 +466,20 @@ const appearFetchMoreAhead = async (): Promise<void> => {
 | 
			
		||||
	fetchMoreAppearTimeout();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const isTop = (): boolean => isBackTop.value || (props.pagination.reversed ? isBottomVisible : isTopVisible)(contentEl!, TOLERANCE);
 | 
			
		||||
onActivated(() => {
 | 
			
		||||
	nextTick(() => {
 | 
			
		||||
		active.value = true;
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(visibility, () => {
 | 
			
		||||
	if (visibility.value === 'hidden') {
 | 
			
		||||
		timerForSetPause = window.setTimeout(() => {
 | 
			
		||||
			isPausingUpdate = true;
 | 
			
		||||
			timerForSetPause = null;
 | 
			
		||||
		},
 | 
			
		||||
		BACKGROUND_PAUSE_WAIT_SEC * 1000);
 | 
			
		||||
	} else { // 'visible'
 | 
			
		||||
		if (timerForSetPause) {
 | 
			
		||||
			clearTimeout(timerForSetPause);
 | 
			
		||||
			timerForSetPause = null;
 | 
			
		||||
		} else {
 | 
			
		||||
			isPausingUpdate = false;
 | 
			
		||||
			if (isTop()) {
 | 
			
		||||
onDeactivated(() => {
 | 
			
		||||
	active.value = false;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch([active, visibility], () => {
 | 
			
		||||
	if (!backed && active.value && visibility.value === 'visible') {
 | 
			
		||||
		executeQueue();
 | 
			
		||||
	}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -378,19 +494,39 @@ const prepend = (item: MisskeyEntity): void => {
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (isTop() && !isPausingUpdate) unshiftItems([item]);
 | 
			
		||||
	else prependQueue(item);
 | 
			
		||||
	if (
 | 
			
		||||
		!isPausingUpdateByExecutingQueue.value && // スクロール調整中はキューに追加する
 | 
			
		||||
		visibility.value !== 'hidden' && // バックグラウンドの場合はキューに追加する
 | 
			
		||||
		queueSize.value === 0 && // キューに残っている場合はキューに追加する
 | 
			
		||||
		active.value // keepAliveで隠されている間はキューに追加する
 | 
			
		||||
	) {
 | 
			
		||||
		if (!backed) {
 | 
			
		||||
			// かなりスクロールの先頭にいる場合
 | 
			
		||||
			if (items.value.has(item.id)) return; // 既にタイムラインにある場合は何もしない
 | 
			
		||||
			unshiftItems([item]);
 | 
			
		||||
		} else if (timelineBackTopBehavior.value === 'next' && !weakBacked) {
 | 
			
		||||
			// ちょっと先頭にいる場合はスクロールを調整する
 | 
			
		||||
			prependQueue(item);
 | 
			
		||||
			executeQueue();
 | 
			
		||||
		} else {
 | 
			
		||||
			// 先頭にいない場合はキューに追加する
 | 
			
		||||
			prependQueue(item);
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		prependQueue(item);
 | 
			
		||||
	}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 新着アイテムをitemsの先頭に追加し、displayLimitを適用する
 | 
			
		||||
 * 新着アイテムをitemsの先頭に追加し、limitを適用する
 | 
			
		||||
 * @param newItems 新しいアイテムの配列
 | 
			
		||||
 * @param limit デフォルトはdisplayLimit
 | 
			
		||||
 */
 | 
			
		||||
function unshiftItems(newItems: MisskeyEntity[]) {
 | 
			
		||||
function unshiftItems(newItems: MisskeyEntity[], limit = displayLimit.value) {
 | 
			
		||||
	const length = newItems.length + items.value.size;
 | 
			
		||||
	items.value = new Map([...arrayToEntries(newItems), ...items.value].slice(0, props.displayLimit));
 | 
			
		||||
	items.value = new Map([...arrayToEntries(newItems), ...(newItems.length >= limit ? [] : items.value)].slice(0, limit));
 | 
			
		||||
 | 
			
		||||
	if (length >= props.displayLimit) more.value = true;
 | 
			
		||||
	if (length >= limit) more.value = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -399,18 +535,43 @@ 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));
 | 
			
		||||
	items.value = new Map([...items.value, ...arrayToEntries(oldItems)].slice(0, displayLimit.value));
 | 
			
		||||
 | 
			
		||||
	if (length >= props.displayLimit) more.value = true;
 | 
			
		||||
	if (length >= displayLimit.value) more.value = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function executeQueue() {
 | 
			
		||||
	unshiftItems(Array.from(queue.value.values()));
 | 
			
		||||
async function executeQueue() {
 | 
			
		||||
	// キューが空の場合でもタイムライン表示数を制限する役割がある
 | 
			
		||||
	// ため続行する!
 | 
			
		||||
	// if (queue.value.size === 0) return;
 | 
			
		||||
 | 
			
		||||
	if (isPausingUpdateByExecutingQueue.value) return;
 | 
			
		||||
	if (timelineBackTopBehavior.value === 'newest') {
 | 
			
		||||
		// Safariは最新のアイテムにするだけ
 | 
			
		||||
		const newItems = Array.from(queue.value.values()).slice(-1 * props.pagination.limit);
 | 
			
		||||
		unshiftItems(newItems);
 | 
			
		||||
		queue.value = new Map();
 | 
			
		||||
	} else {
 | 
			
		||||
		if (queue.value.size > 0) {
 | 
			
		||||
			const queueArr = Array.from(queue.value.entries());
 | 
			
		||||
			queue.value = new Map(queueArr.slice(props.pagination.limit));
 | 
			
		||||
			const newItems = Array.from({ length: Math.min(queueArr.length, props.pagination.limit) }, (_, i) => queueArr[i][1]).reverse();
 | 
			
		||||
			isPausingUpdateByExecutingQueue.value = true;
 | 
			
		||||
 | 
			
		||||
			await adjustScroll(() => unshiftItems(newItems, Infinity));
 | 
			
		||||
			backed = true;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		denyMoveTransition.value = true;
 | 
			
		||||
		items.value = new Map([...items.value].slice(0, displayLimit.value));
 | 
			
		||||
		await nextTick();
 | 
			
		||||
		isPausingUpdateByExecutingQueue.value = false;
 | 
			
		||||
		denyMoveTransition.value = false;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function prependQueue(newItem: MisskeyEntity) {
 | 
			
		||||
	queue.value = new Map([[newItem.id, newItem], ...queue.value].slice(0, props.displayLimit) as [string, MisskeyEntity][]);
 | 
			
		||||
	queue.value.set(newItem.id, newItem);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
@@ -435,52 +596,27 @@ const updateItem = (id: MisskeyEntity['id'], replacer: (old: MisskeyEntity) => M
 | 
			
		||||
 | 
			
		||||
const inited = init();
 | 
			
		||||
 | 
			
		||||
onActivated(() => {
 | 
			
		||||
	isBackTop.value = false;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onDeactivated(() => {
 | 
			
		||||
	isBackTop.value = props.pagination.reversed ? window.scrollY >= (rootEl ? rootEl.scrollHeight - window.innerHeight : 0) : window.scrollY === 0;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function toBottom() {
 | 
			
		||||
	scrollToBottom(contentEl!);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	inited.then(() => {
 | 
			
		||||
		if (props.pagination.reversed) {
 | 
			
		||||
			nextTick(() => {
 | 
			
		||||
				setTimeout(toBottom, 800);
 | 
			
		||||
 | 
			
		||||
				// scrollToBottomでmoreFetchingボタンが画面外まで出るまで
 | 
			
		||||
				// more = trueを遅らせる
 | 
			
		||||
				setTimeout(() => {
 | 
			
		||||
					moreFetching.value = false;
 | 
			
		||||
				}, 2000);
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
	active.value = true;
 | 
			
		||||
	inited.then(scrollAfterInit);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onBeforeUnmount(() => {
 | 
			
		||||
	if (timerForSetPause) {
 | 
			
		||||
		clearTimeout(timerForSetPause);
 | 
			
		||||
		timerForSetPause = null;
 | 
			
		||||
	}
 | 
			
		||||
	if (preventAppearFetchMoreTimer.value) {
 | 
			
		||||
		clearTimeout(preventAppearFetchMoreTimer.value);
 | 
			
		||||
		preventAppearFetchMoreTimer.value = null;
 | 
			
		||||
	}
 | 
			
		||||
	scrollObserver?.disconnect();
 | 
			
		||||
	if (scrollRemove) scrollRemove();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
	items,
 | 
			
		||||
	queue,
 | 
			
		||||
	backed,
 | 
			
		||||
	more,
 | 
			
		||||
	inited,
 | 
			
		||||
	queueSize,
 | 
			
		||||
	backed: $$(backed),
 | 
			
		||||
	reload,
 | 
			
		||||
	prepend,
 | 
			
		||||
	append: appendItem,
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<MkNotes ref="tlComponent" :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)"/>
 | 
			
		||||
<div>
 | 
			
		||||
	<div v-if="queueSize > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="reload()">{{ i18n.ts.goToTheHeadOfTimeline }}</button></div>
 | 
			
		||||
	<div v-if="(((src === 'local' || src === 'social') && !isLocalTimelineAvailable) || (src === 'global' && !isGlobalTimelineAvailable))" :class="$style.disabled">
 | 
			
		||||
		<p :class="$style.disabledTitle">
 | 
			
		||||
			<i class="ti ti-circle-minus"></i>
 | 
			
		||||
			{{ i18n.ts._disabledTimeline.title }}
 | 
			
		||||
		</p>
 | 
			
		||||
		<p :class="$style.disabledDescription">{{ i18n.ts._disabledTimeline.description }}</p>
 | 
			
		||||
	</div>
 | 
			
		||||
	<MkNotes v-else ref="tlComponent" :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" :pagination="pagination"/>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
@@ -14,6 +24,8 @@ import { useStream } from '@/stream';
 | 
			
		||||
import * as sound from '@/scripts/sound';
 | 
			
		||||
import { $i } from '@/account';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
import { instance } from '@/instance';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	src: string;
 | 
			
		||||
@@ -26,15 +38,22 @@ const props = defineProps<{
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(ev: 'note'): void;
 | 
			
		||||
	(ev: 'queue', count: number): void;
 | 
			
		||||
	(ev: 'reload'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable));
 | 
			
		||||
const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable));
 | 
			
		||||
 | 
			
		||||
provide('inChannel', computed(() => props.src === 'channel'));
 | 
			
		||||
 | 
			
		||||
const tlComponent: InstanceType<typeof MkNotes> = $ref();
 | 
			
		||||
let tlComponent: InstanceType<typeof MkNotes> | undefined = $ref();
 | 
			
		||||
 | 
			
		||||
const queueSize = computed(() => {
 | 
			
		||||
	return tlComponent?.pagingComponent?.queueSize ?? 0;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const prepend = note => {
 | 
			
		||||
	tlComponent.pagingComponent?.prepend(note);
 | 
			
		||||
	tlComponent?.pagingComponent?.prepend(note);
 | 
			
		||||
 | 
			
		||||
	emit('note');
 | 
			
		||||
 | 
			
		||||
@@ -159,4 +178,48 @@ const timetravel = (date?: Date) => {
 | 
			
		||||
	this.$refs.tl.reload();
 | 
			
		||||
};
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const reload = () => {
 | 
			
		||||
	tlComponent?.pagingComponent?.reload();
 | 
			
		||||
	emit('reload');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
	reload,
 | 
			
		||||
	queueSize,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.new {
 | 
			
		||||
	position: sticky;
 | 
			
		||||
	top: calc(var(--stickyTop, 0px) + 12px);
 | 
			
		||||
	z-index: 1000;
 | 
			
		||||
	width: 100%;
 | 
			
		||||
	margin: calc(-0.675em - 8px) 0;
 | 
			
		||||
 | 
			
		||||
	&:first-child {
 | 
			
		||||
		margin-top: calc(-0.675em - 8px - var(--margin));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.newButton {
 | 
			
		||||
	display: block;
 | 
			
		||||
	margin: var(--margin) auto 0 auto;
 | 
			
		||||
	padding: 8px 16px;
 | 
			
		||||
	border-radius: 32px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.disabled {
 | 
			
		||||
	text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.disabledTitle {
 | 
			
		||||
	margin: 16px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.disabledDescription {
 | 
			
		||||
	font-size: 90%;
 | 
			
		||||
	margin: 16px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -40,9 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
	</div>
 | 
			
		||||
	<div v-if="instance.policies.ltlAvailable" :class="[$style.tl, $style.panel]">
 | 
			
		||||
		<div :class="$style.tlHeader">{{ i18n.ts.letsLookAtTimeline }}</div>
 | 
			
		||||
		<div :class="$style.tlBody">
 | 
			
		||||
			<MkTimeline src="local"/>
 | 
			
		||||
		</div>
 | 
			
		||||
		<MkTimeline src="local" :class="$style.tlBody"/>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div :class="$style.panel">
 | 
			
		||||
		<XActiveUsersChart/>
 | 
			
		||||
 
 | 
			
		||||
@@ -16,12 +16,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { inject, onBeforeUnmount, provide } from 'vue';
 | 
			
		||||
import { Resolved, Router } from '@/nirax';
 | 
			
		||||
import { computed, inject, onBeforeUnmount, provide, nextTick } from 'vue';
 | 
			
		||||
import { NiraxChangeEvent, Resolved, Router } from '@/nirax';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
import { getScrollContainer } from '@/scripts/scroll';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	router?: Router;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Set any element if scroll position management needed
 | 
			
		||||
	 */
 | 
			
		||||
	scrollContainer?: HTMLElement | null;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const router = props.router ?? inject('router');
 | 
			
		||||
@@ -50,17 +56,49 @@ let currentPageComponent = $shallowRef(current.route.component);
 | 
			
		||||
let currentPageProps = $ref(current.props);
 | 
			
		||||
let key = $ref(current.route.path + JSON.stringify(Object.fromEntries(current.props)));
 | 
			
		||||
 | 
			
		||||
function onChange({ resolved, key: newKey }) {
 | 
			
		||||
	const current = resolveNested(resolved);
 | 
			
		||||
const scrollContainer = computed(() => props.scrollContainer ? (getScrollContainer(props.scrollContainer) ?? document.getElementsByTagName('html')[0]) : undefined);
 | 
			
		||||
 | 
			
		||||
const scrollPosStore = new Map<string, number>();
 | 
			
		||||
 | 
			
		||||
function onChange(ctx: NiraxChangeEvent) {
 | 
			
		||||
	// save scroll position
 | 
			
		||||
	if (scrollContainer.value) scrollPosStore.set(key, scrollContainer.value.scrollTop);
 | 
			
		||||
 | 
			
		||||
	//#region change page
 | 
			
		||||
	const current = resolveNested(ctx.resolved);
 | 
			
		||||
	if (current == null) return;
 | 
			
		||||
	currentPageComponent = current.route.component;
 | 
			
		||||
	currentPageProps = current.props;
 | 
			
		||||
	key = current.route.path + JSON.stringify(Object.fromEntries(current.props));
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	//#region scroll
 | 
			
		||||
	nextTick(() => {
 | 
			
		||||
		if (!scrollContainer.value) return;
 | 
			
		||||
 | 
			
		||||
		const scrollPos = scrollPosStore.get(key) ?? 0;
 | 
			
		||||
		scrollContainer.value.scroll({ top: scrollPos, behavior: 'instant' });
 | 
			
		||||
		if (scrollPos !== 0) {
 | 
			
		||||
			window.setTimeout(() => { // 遷移直後はタイミングによってはコンポーネントが復元し切ってない可能性も考えられるため少し時間を空けて再度スクロール
 | 
			
		||||
				if (!scrollContainer.value) return;
 | 
			
		||||
				scrollContainer.value.scroll({ top: scrollPos, behavior: 'instant' });
 | 
			
		||||
			}, 100);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
	//#endregion
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
router.addListener('change', onChange);
 | 
			
		||||
 | 
			
		||||
function onSame() {
 | 
			
		||||
	if (!scrollContainer.value) return;
 | 
			
		||||
	scrollContainer.value.scroll({ top: 0, behavior: 'smooth' });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
router.addListener('same', onSame);
 | 
			
		||||
 | 
			
		||||
onBeforeUnmount(() => {
 | 
			
		||||
	router.removeListener('change', onChange);
 | 
			
		||||
	router.removeListener('same', onSame);
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -54,24 +54,30 @@ function parsePath(path: string): ParsedPath {
 | 
			
		||||
	return res;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class Router extends EventEmitter<{
 | 
			
		||||
	change: (ctx: {
 | 
			
		||||
export type NiraxChangeEvent = {
 | 
			
		||||
	beforePath: string;
 | 
			
		||||
	path: string;
 | 
			
		||||
	resolved: Resolved;
 | 
			
		||||
	key: string;
 | 
			
		||||
	}) => void;
 | 
			
		||||
	replace: (ctx: {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type NiraxExportEvent = {
 | 
			
		||||
	path: string;
 | 
			
		||||
	key: string;
 | 
			
		||||
	}) => void;
 | 
			
		||||
	push: (ctx: {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type NiraxPushEvent = {
 | 
			
		||||
	beforePath: string;
 | 
			
		||||
	path: string;
 | 
			
		||||
	route: RouteDef | null;
 | 
			
		||||
	props: Map<string, string> | null;
 | 
			
		||||
	key: string;
 | 
			
		||||
	}) => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export class Router extends EventEmitter<{
 | 
			
		||||
	change: (ctx: NiraxChangeEvent) => void;
 | 
			
		||||
	replace: (ctx: NiraxExportEvent) => void;
 | 
			
		||||
	push: (ctx: NiraxExportEvent) => void;
 | 
			
		||||
	same: () => void;
 | 
			
		||||
}> {
 | 
			
		||||
	private routes: RouteDef[];
 | 
			
		||||
@@ -276,29 +282,3 @@ export class Router extends EventEmitter<{
 | 
			
		||||
		this.navigate(path, key);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useScrollPositionManager(getScrollContainer: () => HTMLElement, router: Router) {
 | 
			
		||||
	const scrollPosStore = new Map<string, number>();
 | 
			
		||||
 | 
			
		||||
	onMounted(() => {
 | 
			
		||||
		const scrollContainer = getScrollContainer();
 | 
			
		||||
 | 
			
		||||
		scrollContainer.addEventListener('scroll', () => {
 | 
			
		||||
			scrollPosStore.set(router.getCurrentKey(), scrollContainer.scrollTop);
 | 
			
		||||
		}, { passive: true });
 | 
			
		||||
 | 
			
		||||
		router.addListener('change', ctx => {
 | 
			
		||||
			const scrollPos = scrollPosStore.get(ctx.key) ?? 0;
 | 
			
		||||
			scrollContainer.scroll({ top: scrollPos, behavior: 'instant' });
 | 
			
		||||
			if (scrollPos !== 0) {
 | 
			
		||||
				window.setTimeout(() => { // 遷移直後はタイミングによってはコンポーネントが復元し切ってない可能性も考えられるため少し時間を空けて再度スクロール
 | 
			
		||||
					scrollContainer.scroll({ top: scrollPos, behavior: 'instant' });
 | 
			
		||||
				}, 100);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		router.addListener('same', () => {
 | 
			
		||||
			scrollContainer.scroll({ top: 0, behavior: 'smooth' });
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,17 +8,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 | 
			
		||||
	<MkSpacer :contentMax="800">
 | 
			
		||||
		<div ref="rootEl" v-hotkey.global="keymap">
 | 
			
		||||
			<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
 | 
			
		||||
			<div :class="$style.tl">
 | 
			
		||||
			<MkTimeline
 | 
			
		||||
				ref="tlEl" :key="antennaId"
 | 
			
		||||
				src="antenna"
 | 
			
		||||
				:antenna="antennaId"
 | 
			
		||||
				:sound="true"
 | 
			
		||||
					@queue="queueUpdated"
 | 
			
		||||
				:class="$style.tl"
 | 
			
		||||
			/>
 | 
			
		||||
		</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</MkSpacer>
 | 
			
		||||
</MkStickyContainer>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -26,7 +23,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, watch } from 'vue';
 | 
			
		||||
import MkTimeline from '@/components/MkTimeline.vue';
 | 
			
		||||
import { scroll } from '@/scripts/scroll';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { useRouter } from '@/router';
 | 
			
		||||
import { definePageMetadata } from '@/scripts/page-metadata';
 | 
			
		||||
@@ -39,19 +35,14 @@ const props = defineProps<{
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
let antenna = $ref(null);
 | 
			
		||||
let queue = $ref(0);
 | 
			
		||||
let rootEl = $shallowRef<HTMLElement>();
 | 
			
		||||
let tlEl = $shallowRef<InstanceType<typeof MkTimeline>>();
 | 
			
		||||
const keymap = $computed(() => ({
 | 
			
		||||
	't': focus,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
function queueUpdated(q) {
 | 
			
		||||
	queue = q;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function top() {
 | 
			
		||||
	scroll(rootEl, { top: 0 });
 | 
			
		||||
	tlEl?.reload();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function timetravel() {
 | 
			
		||||
@@ -96,25 +87,6 @@ definePageMetadata(computed(() => antenna ? {
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.new {
 | 
			
		||||
	position: sticky;
 | 
			
		||||
	top: calc(var(--stickyTop, 0px) + 16px);
 | 
			
		||||
	z-index: 1000;
 | 
			
		||||
	width: 100%;
 | 
			
		||||
	margin: calc(-0.675em - 8px) 0;
 | 
			
		||||
 | 
			
		||||
	&:first-child {
 | 
			
		||||
		margin-top: calc(-0.675em - 8px - var(--margin));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.newButton {
 | 
			
		||||
	display: block;
 | 
			
		||||
	margin: var(--margin) auto 0 auto;
 | 
			
		||||
	padding: 8px 16px;
 | 
			
		||||
	border-radius: 32px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tl {
 | 
			
		||||
	background: var(--bg);
 | 
			
		||||
	border-radius: var(--radius);
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
			<!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる -->
 | 
			
		||||
			<MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
 | 
			
		||||
 | 
			
		||||
			<MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after"/>
 | 
			
		||||
			<MkTimeline :key="channelId" src="channel" :channel="channelId" />
 | 
			
		||||
		</div>
 | 
			
		||||
		<div v-else-if="tab === 'featured'">
 | 
			
		||||
			<MkNotes :pagination="featuredPagination"/>
 | 
			
		||||
 
 | 
			
		||||
@@ -26,11 +26,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
	</MkRadios>
 | 
			
		||||
 | 
			
		||||
	<FormSection>
 | 
			
		||||
		<div class="_gaps_m">
 | 
			
		||||
			<div class="_gaps_s">
 | 
			
		||||
				<MkSwitch v-model="showFixedPostForm">{{ i18n.ts.showFixedPostForm }}</MkSwitch>
 | 
			
		||||
				<MkSwitch v-model="showFixedPostFormInChannel">{{ i18n.ts.showFixedPostFormInChannel }}</MkSwitch>
 | 
			
		||||
				<MkSwitch v-model="showTimelineReplies">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></MkSwitch>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<MkSelect v-model="timelineBackTopBehavior" :disabled="isWebKit()" :readonly="isWebKit()">
 | 
			
		||||
				<template #label>{{ i18n.ts.timelineBackTopBehavior }}</template>
 | 
			
		||||
				<option value="newest">{{ i18n.ts._timelineBackTopBehavior.newest }}</option>
 | 
			
		||||
				<option value="next">{{ i18n.ts._timelineBackTopBehavior.next }}</option>
 | 
			
		||||
			</MkSelect>
 | 
			
		||||
		</div>
 | 
			
		||||
	</FormSection>
 | 
			
		||||
 | 
			
		||||
	<FormSection>
 | 
			
		||||
@@ -193,6 +201,8 @@ import { unisonReload } from '@/scripts/unison-reload';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
import { definePageMetadata } from '@/scripts/page-metadata';
 | 
			
		||||
import { miLocalStorage } from '@/local-storage';
 | 
			
		||||
import { isWebKit } from '@/scripts/useragent';
 | 
			
		||||
import { testNotification } from '@/scripts/test-notification';
 | 
			
		||||
import { globalEvents } from '@/events';
 | 
			
		||||
import { claimAchievement } from '@/scripts/achievements';
 | 
			
		||||
 | 
			
		||||
@@ -241,6 +251,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 timelineBackTopBehavior = computed(defaultStore.makeGetterSetter('timelineBackTopBehavior'));
 | 
			
		||||
 | 
			
		||||
watch(lang, () => {
 | 
			
		||||
	miLocalStorage.setItem('lang', lang.value as string);
 | 
			
		||||
 
 | 
			
		||||
@@ -92,6 +92,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
 | 
			
		||||
	'numberOfPageCache',
 | 
			
		||||
	'aiChanMode',
 | 
			
		||||
	'mediaListWithOneImageAppearance',
 | 
			
		||||
	'timelineBackTopBehavior',
 | 
			
		||||
];
 | 
			
		||||
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
 | 
			
		||||
	'lightTheme',
 | 
			
		||||
 
 | 
			
		||||
@@ -11,17 +11,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
			<XTutorial v-if="$i && defaultStore.reactiveState.timelineTutorial.value != -1" class="_panel" style="margin-bottom: var(--margin);"/>
 | 
			
		||||
			<MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/>
 | 
			
		||||
 | 
			
		||||
			<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
 | 
			
		||||
			<div :class="$style.tl">
 | 
			
		||||
			<MkTimeline
 | 
			
		||||
				ref="tlComponent"
 | 
			
		||||
				:key="src"
 | 
			
		||||
				:src="src"
 | 
			
		||||
				:sound="true"
 | 
			
		||||
					@queue="queueUpdated"
 | 
			
		||||
				:class="$style.tl"
 | 
			
		||||
			/>
 | 
			
		||||
		</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</MkSpacer>
 | 
			
		||||
</MkStickyContainer>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -31,7 +28,6 @@ import { defineAsyncComponent, computed, watch, provide } from 'vue';
 | 
			
		||||
import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
 | 
			
		||||
import MkTimeline from '@/components/MkTimeline.vue';
 | 
			
		||||
import MkPostForm from '@/components/MkPostForm.vue';
 | 
			
		||||
import { scroll } from '@/scripts/scroll';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
@@ -54,18 +50,11 @@ const keymap = {
 | 
			
		||||
const tlComponent = $shallowRef<InstanceType<typeof MkTimeline>>();
 | 
			
		||||
const rootEl = $shallowRef<HTMLElement>();
 | 
			
		||||
 | 
			
		||||
let queue = $ref(0);
 | 
			
		||||
let srcWhenNotSignin = $ref(isLocalTimelineAvailable ? 'local' : 'global');
 | 
			
		||||
const src = $computed({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin), set: (x) => saveSrc(x) });
 | 
			
		||||
 | 
			
		||||
watch ($$(src), () => queue = 0);
 | 
			
		||||
 | 
			
		||||
function queueUpdated(q: number): void {
 | 
			
		||||
	queue = q;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function top(): void {
 | 
			
		||||
	if (rootEl) scroll(rootEl, { top: 0 });
 | 
			
		||||
	tlComponent?.reload();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function chooseList(ev: MouseEvent): Promise<void> {
 | 
			
		||||
@@ -184,25 +173,6 @@ definePageMetadata(computed(() => ({
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.new {
 | 
			
		||||
	position: sticky;
 | 
			
		||||
	top: calc(var(--stickyTop, 0px) + 16px);
 | 
			
		||||
	z-index: 1000;
 | 
			
		||||
	width: 100%;
 | 
			
		||||
	margin: calc(-0.675em - 8px) 0;
 | 
			
		||||
 | 
			
		||||
	&:first-child {
 | 
			
		||||
		margin-top: calc(-0.675em - 8px - var(--margin));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.newButton {
 | 
			
		||||
	display: block;
 | 
			
		||||
	margin: var(--margin) auto 0 auto;
 | 
			
		||||
	padding: 8px 16px;
 | 
			
		||||
	border-radius: 32px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.postForm {
 | 
			
		||||
	border-radius: var(--radius);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,17 +8,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 | 
			
		||||
	<MkSpacer :contentMax="800">
 | 
			
		||||
		<div ref="rootEl">
 | 
			
		||||
			<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
 | 
			
		||||
			<div :class="$style.tl">
 | 
			
		||||
			<MkTimeline
 | 
			
		||||
				ref="tlEl" :key="listId"
 | 
			
		||||
				src="list"
 | 
			
		||||
				:list="listId"
 | 
			
		||||
				:sound="true"
 | 
			
		||||
					@queue="queueUpdated"
 | 
			
		||||
				:class="$style.tl"
 | 
			
		||||
			/>
 | 
			
		||||
		</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</MkSpacer>
 | 
			
		||||
</MkStickyContainer>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -26,7 +23,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, watch } from 'vue';
 | 
			
		||||
import MkTimeline from '@/components/MkTimeline.vue';
 | 
			
		||||
import { scroll } from '@/scripts/scroll';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { useRouter } from '@/router';
 | 
			
		||||
import { definePageMetadata } from '@/scripts/page-metadata';
 | 
			
		||||
@@ -39,7 +35,6 @@ const props = defineProps<{
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
let list = $ref(null);
 | 
			
		||||
let queue = $ref(0);
 | 
			
		||||
let tlEl = $shallowRef<InstanceType<typeof MkTimeline>>();
 | 
			
		||||
let rootEl = $shallowRef<HTMLElement>();
 | 
			
		||||
 | 
			
		||||
@@ -49,12 +44,8 @@ watch(() => props.listId, async () => {
 | 
			
		||||
	});
 | 
			
		||||
}, { immediate: true });
 | 
			
		||||
 | 
			
		||||
function queueUpdated(q) {
 | 
			
		||||
	queue = q;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function top() {
 | 
			
		||||
	scroll(rootEl, { top: 0 });
 | 
			
		||||
	tlEl?.reload();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function settings() {
 | 
			
		||||
@@ -89,24 +80,6 @@ definePageMetadata(computed(() => list ? {
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.new {
 | 
			
		||||
	position: sticky;
 | 
			
		||||
	top: calc(var(--stickyTop, 0px) + 16px);
 | 
			
		||||
	z-index: 1000;
 | 
			
		||||
	width: 100%;
 | 
			
		||||
	margin: calc(-0.675em - 8px) 0;
 | 
			
		||||
 | 
			
		||||
	&:first-child {
 | 
			
		||||
		margin-top: calc(-0.675em - 8px - var(--margin));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.newButton {
 | 
			
		||||
	display: block;
 | 
			
		||||
	margin: var(--margin) auto 0 auto;
 | 
			
		||||
	padding: 8px 16px;
 | 
			
		||||
	border-radius: 32px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tl {
 | 
			
		||||
	background: var(--bg);
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ export function getScrollPosition(el: HTMLElement | null): number {
 | 
			
		||||
 | 
			
		||||
export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false) {
 | 
			
		||||
	// とりあえず評価してみる
 | 
			
		||||
	if (el.isConnected && isTopVisible(el)) {
 | 
			
		||||
	if (el.isConnected && isTopVisible(el, tolerance)) {
 | 
			
		||||
		cb();
 | 
			
		||||
		if (once) return null;
 | 
			
		||||
	}
 | 
			
		||||
@@ -75,12 +75,29 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1
 | 
			
		||||
	return removeListener;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function scroll(el: HTMLElement, options: ScrollToOptions | undefined) {
 | 
			
		||||
	const container = getScrollContainer(el);
 | 
			
		||||
	if (container == null) {
 | 
			
		||||
/**
 | 
			
		||||
 * コンテナを指定してスクロールする
 | 
			
		||||
 * @param el Container element
 | 
			
		||||
 * @param options ScrollToOptions
 | 
			
		||||
 */
 | 
			
		||||
export function scroll(el: HTMLElement | null, options: ScrollToOptions | undefined) {
 | 
			
		||||
	if (el == null) {
 | 
			
		||||
		window.scroll(options);
 | 
			
		||||
	} else {
 | 
			
		||||
		container.scroll(options);
 | 
			
		||||
		el.scroll(options);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * コンテナを指定してscrollByする
 | 
			
		||||
 * @param el Container element
 | 
			
		||||
 * @param options ScrollToOptions
 | 
			
		||||
 */
 | 
			
		||||
export function scrollBy(el: HTMLElement | null, options: ScrollToOptions | undefined) {
 | 
			
		||||
	if (el == null) {
 | 
			
		||||
		window.scrollBy(options);
 | 
			
		||||
	} else {
 | 
			
		||||
		el.scrollBy(options);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -89,8 +106,8 @@ export function scroll(el: HTMLElement, options: ScrollToOptions | undefined) {
 | 
			
		||||
 * @param el Scroll container element
 | 
			
		||||
 * @param options Scroll options
 | 
			
		||||
 */
 | 
			
		||||
export function scrollToTop(el: HTMLElement, options: { behavior?: ScrollBehavior; } = {}) {
 | 
			
		||||
	scroll(el, { top: 0, ...options });
 | 
			
		||||
export function scrollToTop(el: HTMLElement | null, options: { behavior?: ScrollBehavior; } = {}) {
 | 
			
		||||
	scroll(getScrollContainer(el), { top: 0, ...options });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								packages/frontend/src/scripts/useragent.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								packages/frontend/src/scripts/useragent.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
import { UAParser } from 'ua-parser-js';
 | 
			
		||||
const ua = new UAParser(navigator.userAgent);
 | 
			
		||||
export const isWebKit = () => ua.getEngine().name === 'WebKit';
 | 
			
		||||
@@ -6,6 +6,7 @@
 | 
			
		||||
import { markRaw, ref } from 'vue';
 | 
			
		||||
import misskey from 'misskey-js';
 | 
			
		||||
import { Storage } from './pizzax';
 | 
			
		||||
import { isWebKit } from './scripts/useragent';
 | 
			
		||||
 | 
			
		||||
interface PostFormAction {
 | 
			
		||||
	title: string,
 | 
			
		||||
@@ -352,6 +353,10 @@ export const defaultStore = markRaw(new Storage('base', {
 | 
			
		||||
		where: 'device',
 | 
			
		||||
		default: {} as Record<string, Record<string, string[]>>,
 | 
			
		||||
	},
 | 
			
		||||
	timelineBackTopBehavior: {
 | 
			
		||||
		where: 'device',
 | 
			
		||||
		default: (isWebKit() ? 'newest' : 'next') as 'newest' | 'next',
 | 
			
		||||
	},
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
// TODO: 他のタブと永続化されたstateを同期
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
	</template>
 | 
			
		||||
 | 
			
		||||
	<div ref="contents">
 | 
			
		||||
		<RouterView @contextmenu.stop="onContextmenu"/>
 | 
			
		||||
		<RouterView :scrollContainer="contents" @contextmenu.stop="onContextmenu"/>
 | 
			
		||||
	</div>
 | 
			
		||||
</XColumn>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -26,8 +26,6 @@ import * as os from '@/os';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
import { mainRouter } from '@/router';
 | 
			
		||||
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata';
 | 
			
		||||
import { useScrollPositionManager } from '@/nirax';
 | 
			
		||||
import { getScrollContainer } from '@/scripts/scroll';
 | 
			
		||||
 | 
			
		||||
defineProps<{
 | 
			
		||||
	column: Column;
 | 
			
		||||
@@ -71,6 +69,4 @@ function onContextmenu(ev: MouseEvent) {
 | 
			
		||||
		},
 | 
			
		||||
	}], ev);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
useScrollPositionManager(() => getScrollContainer(contents.value), mainRouter);
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -13,14 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
		<span style="margin-left: 8px;">{{ column.name }}</span>
 | 
			
		||||
	</template>
 | 
			
		||||
 | 
			
		||||
	<div v-if="(((column.tl === 'local' || column.tl === 'social') && !isLocalTimelineAvailable) || (column.tl === 'global' && !isGlobalTimelineAvailable))" :class="$style.disabled">
 | 
			
		||||
		<p :class="$style.disabledTitle">
 | 
			
		||||
			<i class="ti ti-circle-minus"></i>
 | 
			
		||||
			{{ i18n.ts._disabledTimeline.title }}
 | 
			
		||||
		</p>
 | 
			
		||||
		<p :class="$style.disabledDescription">{{ i18n.ts._disabledTimeline.description }}</p>
 | 
			
		||||
	</div>
 | 
			
		||||
	<MkTimeline v-else-if="column.tl" ref="timeline" :key="column.tl" :src="column.tl"/>
 | 
			
		||||
	<MkTimeline v-if="column.tl" ref="timeline" :key="column.tl" :src="column.tl"/>
 | 
			
		||||
</XColumn>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
@@ -30,27 +23,16 @@ import XColumn from './column.vue';
 | 
			
		||||
import { removeColumn, updateColumn, Column } from './deck-store';
 | 
			
		||||
import MkTimeline from '@/components/MkTimeline.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { $i } from '@/account';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
import { instance } from '@/instance';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	column: Column;
 | 
			
		||||
	isStacked: boolean;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
let disabled = $ref(false);
 | 
			
		||||
 | 
			
		||||
const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable));
 | 
			
		||||
const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable));
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	if (props.column.tl == null) {
 | 
			
		||||
		setType();
 | 
			
		||||
	} else if ($i) {
 | 
			
		||||
		disabled = (
 | 
			
		||||
			(!((instance.policies.ltlAvailable) || ($i.policies.ltlAvailable)) && ['local', 'social'].includes(props.column.tl)) ||
 | 
			
		||||
			(!((instance.policies.gtlAvailable) || ($i.policies.gtlAvailable)) && ['global'].includes(props.column.tl)));
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@@ -84,17 +66,3 @@ const menu = [{
 | 
			
		||||
	action: setType,
 | 
			
		||||
}];
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.disabled {
 | 
			
		||||
	text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.disabledTitle {
 | 
			
		||||
	margin: 16px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.disabledDescription {
 | 
			
		||||
	font-size: 90%;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
				<XStatusBars :class="$style.statusbars"/>
 | 
			
		||||
			</div>
 | 
			
		||||
		</template>
 | 
			
		||||
		<RouterView/>
 | 
			
		||||
		<RouterView :scrollContainer="contents?.rootEl"/>
 | 
			
		||||
		<div :class="$style.spacer"></div>
 | 
			
		||||
	</MkStickyContainer>
 | 
			
		||||
 | 
			
		||||
@@ -105,7 +105,6 @@ import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata';
 | 
			
		||||
import { deviceKind } from '@/scripts/device-kind';
 | 
			
		||||
import { miLocalStorage } from '@/local-storage';
 | 
			
		||||
import { CURRENT_STICKY_BOTTOM } from '@/const';
 | 
			
		||||
import { useScrollPositionManager } from '@/nirax';
 | 
			
		||||
 | 
			
		||||
const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
 | 
			
		||||
const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue'));
 | 
			
		||||
@@ -227,8 +226,6 @@ watch($$(navFooter), () => {
 | 
			
		||||
}, {
 | 
			
		||||
	immediate: true,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
useScrollPositionManager(() => contents.value.rootEl, mainRouter);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
 
 | 
			
		||||
@@ -20,33 +20,20 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
		</button>
 | 
			
		||||
	</template>
 | 
			
		||||
 | 
			
		||||
	<div v-if="(((widgetProps.src === 'local' || widgetProps.src === 'social') && !isLocalTimelineAvailable) || (widgetProps.src === 'global' && !isGlobalTimelineAvailable))" :class="$style.disabled">
 | 
			
		||||
		<p :class="$style.disabledTitle">
 | 
			
		||||
			<i class="ti ti-minus"></i>
 | 
			
		||||
			{{ i18n.ts._disabledTimeline.title }}
 | 
			
		||||
		</p>
 | 
			
		||||
		<p :class="$style.disabledDescription">{{ i18n.ts._disabledTimeline.description }}</p>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div v-else>
 | 
			
		||||
	<MkTimeline :key="widgetProps.src === 'list' ? `list:${widgetProps.list.id}` : widgetProps.src === 'antenna' ? `antenna:${widgetProps.antenna.id}` : widgetProps.src" :src="widgetProps.src" :list="widgetProps.list ? widgetProps.list.id : null" :antenna="widgetProps.antenna ? widgetProps.antenna.id : null"/>
 | 
			
		||||
	</div>
 | 
			
		||||
</MkContainer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
 | 
			
		||||
import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
 | 
			
		||||
import { GetFormResultType } from '@/scripts/form';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import MkContainer from '@/components/MkContainer.vue';
 | 
			
		||||
import MkTimeline from '@/components/MkTimeline.vue';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
import { $i } from '@/account';
 | 
			
		||||
import { instance } from '@/instance';
 | 
			
		||||
 | 
			
		||||
const name = 'timeline';
 | 
			
		||||
const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable));
 | 
			
		||||
const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable));
 | 
			
		||||
 | 
			
		||||
const widgetPropsDef = {
 | 
			
		||||
	showHeader: {
 | 
			
		||||
@@ -141,17 +128,3 @@ defineExpose<WidgetComponentExpose>({
 | 
			
		||||
	id: props.widget ? props.widget.id : null,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.disabled {
 | 
			
		||||
	text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.disabledTitle {
 | 
			
		||||
	margin: 16px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.disabledDescription {
 | 
			
		||||
	font-size: 90%;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@@ -799,6 +799,9 @@ importers:
 | 
			
		||||
      typescript:
 | 
			
		||||
        specifier: 5.2.2
 | 
			
		||||
        version: 5.2.2
 | 
			
		||||
      ua-parser-js:
 | 
			
		||||
        specifier: 2.0.0-alpha.2
 | 
			
		||||
        version: 2.0.0-alpha.2
 | 
			
		||||
      uuid:
 | 
			
		||||
        specifier: 9.0.1
 | 
			
		||||
        version: 9.0.1
 | 
			
		||||
@@ -11846,6 +11849,7 @@ packages:
 | 
			
		||||
  /form-data@3.0.1:
 | 
			
		||||
    resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==}
 | 
			
		||||
    engines: {node: '>= 6'}
 | 
			
		||||
    requiresBuild: true
 | 
			
		||||
    dependencies:
 | 
			
		||||
      asynckit: 0.4.0
 | 
			
		||||
      combined-stream: 1.0.8
 | 
			
		||||
@@ -18818,6 +18822,10 @@ packages:
 | 
			
		||||
    engines: {node: '>=14.17'}
 | 
			
		||||
    hasBin: true
 | 
			
		||||
 | 
			
		||||
  /ua-parser-js@2.0.0-alpha.2:
 | 
			
		||||
    resolution: {integrity: sha512-Vz+BJN/EFC1OaUv0eu5kPyX7HEZIO7Dv29jIK7rMuKjUB1qqq+Is/XIpu5iV5XDvoNl62dM7ay8DtzYjBDI0WA==}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /ufo@1.1.2:
 | 
			
		||||
    resolution: {integrity: sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==}
 | 
			
		||||
    dev: true
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user