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