Compare commits

...

126 commits

Author SHA1 Message Date
tamaina
b312f360c5 Merge branch 'develop' into pag-back 2023-09-16 08:31:47 +00:00
tamaina
e93c58ffa4 Merge branch 'develop' into pag-back 2023-09-13 07:46:01 +00:00
tamaina
4abe7e79a9 Merge branch 'develop' into pag-back 2023-09-11 06:13:04 +00:00
tamaina
f34bcf0c1d fix 2023-08-24 06:34:30 +00:00
tamaina
fb5bb950de
Merge branch 'develop' into pag-back 2023-08-24 14:54:47 +09:00
tamaina
ac96aba0f5 Merge branch 'develop' into pag-back 2023-08-13 12:23:39 +00:00
tamaina
9a352d4949 Merge branch 'develop' into pag-back 2023-08-13 11:17:12 +00:00
tamaina
79934c7931 Merge branch 'develop' into pag-back 2023-07-31 10:33:32 +00:00
tamaina
0994adc748 Merge branch 'develop' into pag-back 2023-07-31 06:07:47 +00:00
tamaina
0449c1a7ea prividingItems 2023-07-31 01:33:25 +00:00
tamaina
f3da1bcbbd fix 2023-07-30 13:56:11 +00:00
tamaina
7fc2309822 fix comment 2023-07-28 15:58:00 +00:00
tamaina
d92fe0803c fix 2023-07-28 15:57:34 +00:00
tamaina
ae949af6c3 fix 2023-07-26 12:59:59 +00:00
tamaina
f7ddff7475 fix 2023-07-26 12:48:34 +00:00
tamaina
848b72ae21 Merge branch 'develop' into pag-back 2023-07-26 12:41:54 +00:00
tamaina
dab76b5e77 Merge branch 'develop' into pag-back 2023-07-26 12:23:13 +00:00
tamaina
4785b9bfdd timelineBackTopBehavior 2023-07-26 12:22:26 +00:00
tamaina
9f79e494f5 yameta 2023-07-26 07:33:17 +00:00
tamaina
51cf5c57f0 Merge branch 'develop' into pag-back 2023-07-25 10:48:01 +00:00
tamaina
9f4df717e8 denyMoveTransitionを復活させる 2023-07-25 10:47:31 +00:00
tamaina
cf764eebe3 Merge branch 'develop' into pag-back 2023-07-25 10:36:55 +00:00
tamaina
d6c3f34eea ✌️ 2023-07-25 04:01:26 +00:00
tamaina
c265569008 adjustScrollをやっぱり復活させる 2023-07-25 03:40:42 +00:00
tamaina
d6e57059e4 backedがfalseになったら通知を既読にする 2023-07-24 07:19:22 +00:00
tamaina
7ccdd503b7 Merge branch 'develop' into pag-back 2023-07-24 07:15:15 +00:00
tamaina
560a1fecf5 Merge branch 'develop' into pag-back 2023-07-24 06:12:24 +00:00
tamaina
ef69eee155 activeでもexecuteQueueする 2023-07-21 11:04:58 +00:00
tamaina
20ae59756f Merge branch 'develop' into pag-back 2023-07-21 11:02:18 +00:00
tamaina
3cc22e5e1c Revert "Merge branch 'use-uri-cache' into pag-back"
This reverts commit d0a119c2ea, reversing
changes made to 5bfb98df00.
2023-07-19 07:08:28 +00:00
tamaina
4e775a670f Revert "beta.7"
This reverts commit e6ee5704e8.
2023-07-19 07:08:15 +00:00
tamaina
e6ee5704e8 beta.7 2023-07-19 07:07:58 +00:00
tamaina
d0a119c2ea Merge branch 'use-uri-cache' into pag-back 2023-07-19 07:07:48 +00:00
tamaina
ee1e2aa200 fix 2023-07-19 07:05:02 +00:00
tamaina
a2f6bf3d5c fix 2023-07-19 07:03:30 +00:00
tamaina
4c83663597 fix 2023-07-19 07:03:07 +00:00
tamaina
4e7a26e6d5 oops 2023-07-19 07:00:50 +00:00
tamaina
660b030233 fix 2023-07-19 06:59:12 +00:00
tamaina
fc50dc7a67 move comment 2023-07-19 06:43:09 +00:00
tamaina
05042a0697 perf(backend): createPersonでキャッシュを積極的に利用する, トランザクション回数を減らす 2023-07-19 06:39:39 +00:00
tamaina
5bfb98df00 Merge branch 'develop' into pag-back 2023-07-19 05:20:36 +00:00
tamaina
b02187d9d0 🎨 2023-07-19 05:20:10 +00:00
tamaina
e2f3091778 🎨 2023-07-19 05:14:42 +00:00
tamaina
18611ab521 wip 2023-07-19 04:45:24 +00:00
tamaina
e8316dc4c4 ✌️ 2023-07-19 03:29:06 +00:00
tamaina
72ae8441e1 Merge branch 'develop' into pag-back 2023-07-19 03:24:01 +00:00
tamaina
4aee99b61a test... 2023-07-19 03:23:31 +00:00
tamaina
e9486d0085 fix 2023-07-18 13:19:04 +00:00
tamaina
7e06305b96 executeQueue after visible 2023-07-18 13:11:42 +00:00
tamaina
94f9ebc80c ✌️ 2023-07-18 13:02:20 +00:00
tamaina
f7d776e4da add comment 2023-07-18 07:36:47 +00:00
tamaina
d5b4fa7e50 test 2023-07-18 07:24:44 +00:00
tamaina
f3a0839552 ✌️ 2023-07-18 07:15:20 +00:00
tamaina
b0c6675ef3 🎨 2023-07-18 06:55:08 +00:00
tamaina
72998adfb6 Revert "isPausingUpdateを省略"
This reverts commit 954d934505.
2023-07-18 06:54:05 +00:00
tamaina
954d934505 isPausingUpdateを省略 2023-07-18 05:22:45 +00:00
tamaina
4cd9623dc3
Merge branch 'develop' into pag-back 2023-07-18 14:16:28 +09:00
tamaina
1ccac0c1e3 32? 2023-07-18 04:48:22 +00:00
tamaina
7895474263 remove console.log 2023-07-18 04:44:52 +00:00
tamaina
fd44a29f2b scroll... 2023-07-18 04:39:56 +00:00
tamaina
054ea30955 no adjusting scroll 2023-07-18 04:27:20 +00:00
tamaina
dd02648f8d Revert "flag test"
This reverts commit 81238fabd2.
2023-07-18 04:03:16 +00:00
tamaina
81238fabd2 flag test 2023-07-18 03:55:44 +00:00
tamaina
3677a91c4a log2 2023-07-18 03:49:06 +00:00
tamaina
b2c1f5873d watch?? 2023-07-18 03:39:10 +00:00
tamaina
76145701af clean up 2023-07-18 03:38:14 +00:00
tamaina
0079f3394b nextTick? 2023-07-18 03:33:38 +00:00
tamaina
cb63a1ed00 test 2023-07-18 03:24:53 +00:00
tamaina
1062371296 fix lint 2023-07-18 01:29:33 +00:00
tamaina
3f6f6a49b6 remove console.log 2023-07-17 16:24:49 +00:00
tamaina
d73ea541bf add a comment 2023-07-17 16:17:30 +00:00
tamaina
f7425f5fe9 korede douda 2 2023-07-17 15:28:29 +00:00
tamaina
b60dba701c ✌️ 2023-07-17 15:16:50 +00:00
tamaina
b5f85aa9a8 ? 2023-07-17 15:04:48 +00:00
tamaina
6152122d43 0.02? 2023-07-17 14:57:00 +00:00
tamaina
d335da5ee4 1000% 2023-07-17 14:34:57 +00:00
tamaina
d82d03890d korede douda 2023-07-17 14:27:13 +00:00
tamaina
4881237955 ? 2023-07-17 14:13:50 +00:00
tamaina
fc91526857 ? 2023-07-17 14:05:06 +00:00
tamaina
da4aba3247 10%? 2023-07-17 11:47:08 +00:00
tamaina
568822944f comment 2023-07-17 11:40:22 +00:00
tamaina
393160eeda ✌️ 2023-07-17 10:42:05 +00:00
tamaina
0f64372abb prepend()でキューが5つ以下の時はexecuteQueueを呼んでしまう 2023-07-17 10:40:55 +00:00
tamaina
02054528f9 isPausingUpdate check 2023-07-17 10:26:42 +00:00
tamaina
31b62db14b backedがtrue→falseになってもexecuteQueue 2023-07-17 10:24:18 +00:00
tamaina
41824ae383 revert... 2023-07-17 10:16:19 +00:00
tamaina
5a5ef7564a ???? 2023-07-17 09:58:31 +00:00
tamaina
78944bf441 ✌️ 2023-07-17 09:50:34 +00:00
tamaina
f565e0f8a5 ✌️ 2023-07-17 09:29:52 +00:00
tamaina
bec510e37d 130% 2023-07-17 09:19:42 +00:00
tamaina
b446bfb0b6 a 2023-07-17 09:11:31 +00:00
tamaina
3bbeac4be2 ✌️ 2023-07-17 08:59:47 +00:00
tamaina
e7251220d5 RouterViewにScrollPositiionManagerを埋め込む 2023-07-17 08:46:32 +00:00
tamaina
4bef4953b8 15% 2023-07-17 08:13:46 +00:00
tamaina
e609b3b7dc test 2023-07-17 08:06:54 +00:00
tamaina
7fe882d0e2 wip 2023-07-17 07:38:42 +00:00
tamaina
b330ede502 Merge branch 'develop' into pag-back 2023-07-17 07:29:32 +00:00
tamaina
f30275a975 fix? 2023-07-14 13:11:59 +00:00
tamaina
04ff07e4e7 🎨 2023-07-14 12:03:59 +00:00
tamaina
7d4f33d2c0 ✌️ 2023-07-14 11:13:00 +00:00
tamaina
2a434c63df Merge branch 'develop' into pag-back 2023-07-14 07:21:05 +00:00
tamaina
a1b90d6dd3 at 2023-07-14 07:20:45 +00:00
tamaina
c7c3c32871 skip executeQueue if no queue 2023-07-14 07:19:43 +00:00
tamaina
4fabe26b07 lob 2023-07-14 07:09:46 +00:00
tamaina
752c01ba91 ? 2023-07-14 05:25:44 +00:00
tamaina
ba3fa8b431 ?? 2023-07-13 07:20:11 +00:00
tamaina
2bbada3cd4 256 2023-07-13 06:24:40 +00:00
tamaina
a26f289dd5 ??? 2023-07-13 06:10:42 +00:00
tamaina
8213380ded ✌️ 2023-07-13 05:58:17 +00:00
tamaina
68d647d6b8 ? 2023-07-13 05:48:18 +00:00
tamaina
130ece74f9 fix 2023-07-13 05:36:32 +00:00
tamaina
fae912a754 ✌️ 2023-07-13 05:27:53 +00:00
tamaina
877a7a81bb ? 2023-07-13 04:38:04 +00:00
tamaina
88315d3e80 ✌️ 2023-07-13 04:15:32 +00:00
tamaina
af00c2c96c Merge branch 'pag-back' of https://github.com/misskey-dev/misskey into pag-back 2023-07-13 03:54:08 +00:00
tamaina
974f7c13d3 ✌️ 2023-07-13 03:54:00 +00:00
Kagami Sascha Rosylight
44dee0f883
Merge branch 'develop' into pag-back 2023-07-13 02:36:00 +02:00
Kagami Sascha Rosylight
794ff58b07
Merge branch 'develop' into pag-back 2023-07-12 22:29:49 +02:00
tamaina
f5a019a6d6 active? 2023-07-11 15:17:52 +00:00
tamaina
ddb41bd0ba ✌️ 2023-07-11 14:39:59 +00:00
tamaina
035c98dc15 ✌️ 2023-07-11 14:14:15 +00:00
tamaina
b4d532efb4 fix 2023-07-11 13:23:56 +00:00
tamaina
28f914f67f wip 2023-07-11 13:11:25 +00:00
tamaina
2481123972 128 2023-07-11 12:47:06 +00:00
tamaina
5f1cd1e532 wip 2023-07-11 12:36:45 +00:00
tamaina
9f246e3dc7 test 2023-07-11 07:53:56 +00:00
26 changed files with 519 additions and 393 deletions

7
locales/index.d.ts vendored
View file

@ -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;

View file

@ -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: "チャンネルを編集"

View file

@ -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",

View file

@ -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);
}

View file

@ -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>

View file

@ -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(() => {

View file

@ -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) {

View file

@ -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);
// truefalseexecuteQueue
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);
});
/**
* weakBackedがtruefalseになったらexecuteQueue
*/
watch($$(weakBacked), () => {
if (timelineBackTopBehavior.value === 'next' && !weakBacked) {
executeQueue();
}
});
watch([$$(backed), $$(contentEl)], () => {
/**
* backedがtruefalseになってもexecuteQueue
*/
watch($$(backed), () => {
if (!backed) {
if (!contentEl) return;
executeQueue();
}
});
scrollRemove = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl, executeQueue, TOLERANCE);
} else {
/**
* 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;
})();
});
if (props.pagination.params && isRef(props.pagination.params)) {
watch(props.pagination.params, init, { deep: true });
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 {
// scrollableElementOrHtmlundefined
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' });
// scrollableElementOrHtmlundefined
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);
// scrollTobacked
weakBacked = false;
}
}, 200);
// scrollToBottommoreFetching
// more = true
setTimeout(() => {
moreFetching.value = false;
}, 2000);
});
} else {
nextTick(() => {
setTimeout(() => {
scrollToTop(scrollableElement);
// scrollTobacked
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);
// scrollToBottommoreFetching
// 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,

View file

@ -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>

View file

@ -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/>

View file

@ -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>

View file

@ -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' });
});
});
}

View file

@ -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);

View file

@ -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"/>

View file

@ -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);

View file

@ -92,6 +92,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'numberOfPageCache',
'aiChanMode',
'mediaListWithOneImageAppearance',
'timelineBackTopBehavior',
];
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
'lightTheme',

View file

@ -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);
}

View file

@ -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);

View file

@ -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 });
}
/**

View file

@ -0,0 +1,3 @@
import { UAParser } from 'ua-parser-js';
const ua = new UAParser(navigator.userAgent);
export const isWebKit = () => ua.getEngine().name === 'WebKit';

View file

@ -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を同期

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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