parent
							
								
									a10be38d0e
								
							
						
					
					
						commit
						586c11251a
					
				
					 8 changed files with 374 additions and 677 deletions
				
			
		|  | @ -1,44 +0,0 @@ | |||
| <template> | ||||
| <FormSlot> | ||||
| 	<template #label><slot name="label"></slot></template> | ||||
| 	<div class="abcaccfa"> | ||||
| 		<slot :items="items"></slot> | ||||
| 		<div v-if="empty" key="_empty_" class="empty"> | ||||
| 			<slot name="empty"></slot> | ||||
| 		</div> | ||||
| 		<MkButton v-show="more" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore"> | ||||
| 			<template v-if="!moreFetching">{{ $ts.loadMore }}</template> | ||||
| 			<template v-if="moreFetching"><MkLoading inline/></template> | ||||
| 		</MkButton> | ||||
| 	</div> | ||||
| </FormSlot> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import FormSlot from './slot.vue'; | ||||
| import paging from '@/scripts/paging'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkButton, | ||||
| 		FormSlot, | ||||
| 	}, | ||||
| 
 | ||||
| 	mixins: [ | ||||
| 		paging({}), | ||||
| 	], | ||||
| 
 | ||||
| 	props: { | ||||
| 		pagination: { | ||||
| 			required: true | ||||
| 		}, | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .abcaccfa { | ||||
| } | ||||
| </style> | ||||
|  | @ -1,114 +1,49 @@ | |||
| <template> | ||||
| <transition name="fade" mode="out-in"> | ||||
| 	<MkLoading v-if="fetching"/> | ||||
| 
 | ||||
| 	<MkError v-else-if="error" @retry="init()"/> | ||||
| 
 | ||||
| 	<div v-else-if="empty" class="_fullinfo"> | ||||
| 		<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> | ||||
| 		<div>{{ $ts.noNotes }}</div> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div v-else class="giivymft" :class="{ noGap }"> | ||||
| 		<div v-show="more && reversed" style="margin-bottom: var(--margin);"> | ||||
| 			<MkButton style="margin: 0 auto;" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMoreFeature"> | ||||
| 				<template v-if="!moreFetching">{{ $ts.loadMore }}</template> | ||||
| 				<template v-if="moreFetching"><MkLoading inline/></template> | ||||
| 			</MkButton> | ||||
| <MkPagination ref="pagingComponent" :pagination="pagination"> | ||||
| 	<template #empty> | ||||
| 		<div class="_fullinfo"> | ||||
| 			<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> | ||||
| 			<div>{{ $ts.noNotes }}</div> | ||||
| 		</div> | ||||
| 	</template> | ||||
| 
 | ||||
| 		<XList ref="notes" v-slot="{ item: note }" :items="notes" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :no-gap="noGap" :ad="true" class="notes"> | ||||
| 			<XNote :key="note._featuredId_ || note._prId_ || note.id" class="qtqtichx" :note="note" @update:note="updated(note, $event)"/> | ||||
| 		</XList> | ||||
| 
 | ||||
| 		<div v-show="more && !reversed" style="margin-top: var(--margin);"> | ||||
| 			<MkButton v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" style="margin: 0 auto;" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore"> | ||||
| 				<template v-if="!moreFetching">{{ $ts.loadMore }}</template> | ||||
| 				<template v-if="moreFetching"><MkLoading inline/></template> | ||||
| 			</MkButton> | ||||
| 	<template #default="{ items: notes }"> | ||||
| 		<div class="giivymft" :class="{ noGap }"> | ||||
| 			<XList ref="notes" v-slot="{ item: note }" :items="notes" :direction="pagination.reversed ? 'up' : 'down'" :reversed="pagination.reversed" :no-gap="noGap" :ad="true" class="notes"> | ||||
| 				<XNote :key="note._featuredId_ || note._prId_ || note.id" class="qtqtichx" :note="note" @update:note="updated(note, $event)"/> | ||||
| 			</XList> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </transition> | ||||
| 	</template> | ||||
| </MkPagination> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import paging from '@/scripts/paging'; | ||||
| import XNote from './note.vue'; | ||||
| import XList from './date-separated-list.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| <script lang="ts" setup> | ||||
| import { ref } from 'vue'; | ||||
| import XNote from '@/components/note.vue'; | ||||
| import XList from '@/components/date-separated-list.vue'; | ||||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
| import { Paging } from '@/components/ui/pagination.vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		XNote, XList, MkButton, | ||||
| const props = defineProps<{ | ||||
| 	pagination: Paging; | ||||
| 	noGap?: boolean; | ||||
| }>(); | ||||
| 
 | ||||
| const pagingComponent = ref<InstanceType<typeof MkPagination>>(); | ||||
| 
 | ||||
| const updated = (oldValue, newValue) => { | ||||
| 	const i = pagingComponent.value.items.findIndex(n => n === oldValue); | ||||
| 	pagingComponent.value.items[i] = newValue; | ||||
| }; | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	prepend: (note) => { | ||||
| 		pagingComponent.value?.prepend(note); | ||||
| 	}, | ||||
| 
 | ||||
| 	mixins: [ | ||||
| 		paging({ | ||||
| 			before: (self) => { | ||||
| 				self.$emit('before'); | ||||
| 			}, | ||||
| 
 | ||||
| 			after: (self, e) => { | ||||
| 				self.$emit('after', e); | ||||
| 			} | ||||
| 		}), | ||||
| 	], | ||||
| 
 | ||||
| 	props: { | ||||
| 		pagination: { | ||||
| 			required: true | ||||
| 		}, | ||||
| 		prop: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		noGap: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['before', 'after'], | ||||
| 
 | ||||
| 	computed: { | ||||
| 		notes(): any[] { | ||||
| 			return this.prop ? this.items.map(item => item[this.prop]) : this.items; | ||||
| 		}, | ||||
| 
 | ||||
| 		reversed(): boolean { | ||||
| 			return this.pagination.reversed; | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		updated(oldValue, newValue) { | ||||
| 			const i = this.notes.findIndex(n => n === oldValue); | ||||
| 			if (this.prop) { | ||||
| 				this.items[i][this.prop] = newValue; | ||||
| 			} else { | ||||
| 				this.items[i] = newValue; | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		focus() { | ||||
| 			this.$refs.notes.focus(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .fade-enter-active, | ||||
| .fade-leave-active { | ||||
| 	transition: opacity 0.125s ease; | ||||
| } | ||||
| .fade-enter-from, | ||||
| .fade-leave-to { | ||||
| 	opacity: 0; | ||||
| } | ||||
| 
 | ||||
| .giivymft { | ||||
| 	&.noGap { | ||||
| 		> .notes { | ||||
|  |  | |||
|  | @ -1,159 +1,85 @@ | |||
| <template> | ||||
| <transition name="fade" mode="out-in"> | ||||
| 	<MkLoading v-if="fetching"/> | ||||
| <MkPagination ref="pagingComponent" :pagination="pagination"> | ||||
| 	<template #empty> | ||||
| 		<div class="_fullinfo"> | ||||
| 			<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> | ||||
| 			<div>{{ $ts.noNotifications }}</div> | ||||
| 		</div> | ||||
| 	</template> | ||||
| 
 | ||||
| 	<MkError v-else-if="error" @retry="init()"/> | ||||
| 
 | ||||
| 	<p v-else-if="empty" class="mfcuwfyp">{{ $ts.noNotifications }}</p> | ||||
| 
 | ||||
| 	<div v-else> | ||||
| 		<XList v-slot="{ item: notification }" class="elsfgstc" :items="items" :no-gap="true"> | ||||
| 	<template #default="{ items: notifications }"> | ||||
| 		<XList v-slot="{ item: notification }" class="elsfgstc" :items="notifications" :no-gap="true"> | ||||
| 			<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" @update:note="noteUpdated(notification.note, $event)"/> | ||||
| 			<XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/> | ||||
| 		</XList> | ||||
| 
 | ||||
| 		<MkButton v-show="more" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" primary style="margin: var(--margin) auto;" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore"> | ||||
| 			<template v-if="!moreFetching">{{ $ts.loadMore }}</template> | ||||
| 			<template v-if="moreFetching"><MkLoading inline/></template> | ||||
| 		</MkButton> | ||||
| 	</div> | ||||
| </transition> | ||||
| 	</template> | ||||
| </MkPagination> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, PropType, markRaw } from 'vue'; | ||||
| import paging from '@/scripts/paging'; | ||||
| import XNotification from './notification.vue'; | ||||
| import XList from './date-separated-list.vue'; | ||||
| import XNote from './note.vue'; | ||||
| <script lang="ts" setup> | ||||
| import { defineComponent, PropType, markRaw, onUnmounted, onMounted, computed, ref } from 'vue'; | ||||
| import { notificationTypes } from 'misskey-js'; | ||||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
| import { Paging } from '@/components/ui/pagination.vue'; | ||||
| import XNotification from '@/components/notification.vue'; | ||||
| import XList from '@/components/date-separated-list.vue'; | ||||
| import XNote from '@/components/note.vue'; | ||||
| import * as os from '@/os'; | ||||
| import { stream } from '@/stream'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import { $i } from '@/account'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		XNotification, | ||||
| 		XList, | ||||
| 		XNote, | ||||
| 		MkButton, | ||||
| 	}, | ||||
| const props = defineProps<{ | ||||
| 	includeTypes?: PropType<typeof notificationTypes[number][]>; | ||||
| 	unreadOnly?: boolean; | ||||
| }>(); | ||||
| 
 | ||||
| 	mixins: [ | ||||
| 		paging({}), | ||||
| 	], | ||||
| const pagingComponent = ref<InstanceType<typeof MkPagination>>(); | ||||
| 
 | ||||
| 	props: { | ||||
| 		includeTypes: { | ||||
| 			type: Array as PropType<typeof notificationTypes[number][]>, | ||||
| 			required: false, | ||||
| 			default: null, | ||||
| 		}, | ||||
| 		unreadOnly: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false, | ||||
| 		}, | ||||
| 	}, | ||||
| const allIncludeTypes = computed(() => props.includeTypes ?? notificationTypes.filter(x => !$i.mutingNotificationTypes.includes(x))); | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			connection: null, | ||||
| 			pagination: { | ||||
| 				endpoint: 'i/notifications', | ||||
| 				limit: 10, | ||||
| 				params: () => ({ | ||||
| 					includeTypes: this.allIncludeTypes || undefined, | ||||
| 					unreadOnly: this.unreadOnly, | ||||
| 				}) | ||||
| 			}, | ||||
| 		}; | ||||
| 	}, | ||||
| const pagination: Paging = { | ||||
| 	endpoint: 'i/notifications' as const, | ||||
| 	limit: 10, | ||||
| 	params: computed(() => ({ | ||||
| 		includeTypes: allIncludeTypes.value || undefined, | ||||
| 		unreadOnly: props.unreadOnly, | ||||
| 	})), | ||||
| }; | ||||
| 
 | ||||
| 	computed: { | ||||
| 		allIncludeTypes() { | ||||
| 			return this.includeTypes ?? notificationTypes.filter(x => !this.$i.mutingNotificationTypes.includes(x)); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		includeTypes: { | ||||
| 			handler() { | ||||
| 				this.reload(); | ||||
| 			}, | ||||
| 			deep: true | ||||
| 		}, | ||||
| 		unreadOnly: { | ||||
| 			handler() { | ||||
| 				this.reload(); | ||||
| 			}, | ||||
| 		}, | ||||
| 		// TODO: vue/vuexのバグか仕様かは不明なものの、プロフィール更新するなどして $i が更新されると、 | ||||
| 		// mutingNotificationTypes に変化が無くてもこのハンドラーが呼び出され無駄なリロードが発生するのを直す | ||||
| 		'$i.mutingNotificationTypes': { | ||||
| 			handler() { | ||||
| 				if (this.includeTypes === null) { | ||||
| 					this.reload(); | ||||
| 				} | ||||
| 			}, | ||||
| 			deep: true | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.connection = markRaw(stream.useChannel('main')); | ||||
| 		this.connection.on('notification', this.onNotification); | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeUnmount() { | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		onNotification(notification) { | ||||
| 			const isMuted = !this.allIncludeTypes.includes(notification.type); | ||||
| 			if (isMuted || document.visibilityState === 'visible') { | ||||
| 				stream.send('readNotification', { | ||||
| 					id: notification.id | ||||
| 				}); | ||||
| 			} | ||||
| 
 | ||||
| 			if (!isMuted) { | ||||
| 				this.prepend({ | ||||
| 					...notification, | ||||
| 					isRead: document.visibilityState === 'visible' | ||||
| 				}); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		noteUpdated(oldValue, newValue) { | ||||
| 			const i = this.items.findIndex(n => n.note === oldValue); | ||||
| 			this.items[i] = { | ||||
| 				...this.items[i], | ||||
| 				note: newValue | ||||
| 			}; | ||||
| 		}, | ||||
| const onNotification = (notification) => { | ||||
| 	const isMuted = !allIncludeTypes.value.includes(notification.type); | ||||
| 	if (isMuted || document.visibilityState === 'visible') { | ||||
| 		stream.send('readNotification', { | ||||
| 			id: notification.id | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	if (!isMuted) { | ||||
| 		pagingComponent.value.prepend({ | ||||
| 			...notification, | ||||
| 			isRead: document.visibilityState === 'visible' | ||||
| 		}); | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| const noteUpdated = (oldValue, newValue) => { | ||||
| 	const i = pagingComponent.value.items.findIndex(n => n.note === oldValue); | ||||
| 	pagingComponent.value.items[i] = { | ||||
| 		...pagingComponent.value.items[i], | ||||
| 		note: newValue, | ||||
| 	}; | ||||
| }; | ||||
| 
 | ||||
| onMounted(() => { | ||||
| 	const connection = stream.useChannel('main'); | ||||
| 	connection.on('notification', onNotification); | ||||
| 	onUnmounted(() => { | ||||
| 		connection.dispose(); | ||||
| 	}); | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .fade-enter-active, | ||||
| .fade-leave-active { | ||||
| 	transition: opacity 0.125s ease; | ||||
| } | ||||
| .fade-enter-from, | ||||
| .fade-leave-to { | ||||
| 	opacity: 0; | ||||
| } | ||||
| 
 | ||||
| .mfcuwfyp { | ||||
| 	margin: 0; | ||||
| 	padding: 16px; | ||||
| 	text-align: center; | ||||
| 	color: var(--fg); | ||||
| } | ||||
| 
 | ||||
| .elsfgstc { | ||||
| 	background: var(--panel); | ||||
| } | ||||
|  |  | |||
|  | @ -13,43 +13,247 @@ | |||
| 		</slot> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div v-else class="cxiknjgy"> | ||||
| 	<div v-else ref="rootEl"> | ||||
| 		<slot :items="items"></slot> | ||||
| 		<div v-show="more" key="_more_" class="more _gap"> | ||||
| 			<MkButton v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore"> | ||||
| 				<template v-if="!moreFetching">{{ $ts.loadMore }}</template> | ||||
| 				<template v-if="moreFetching"><MkLoading inline/></template> | ||||
| 		<div v-show="more" key="_more_" class="cxiknjgy _gap"> | ||||
| 			<MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore"> | ||||
| 				{{ $ts.loadMore }} | ||||
| 			</MkButton> | ||||
| 			<MkLoading v-else class="loading"/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </transition> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import MkButton from './button.vue'; | ||||
| import paging from '@/scripts/paging'; | ||||
| <script lang="ts" setup> | ||||
| import { computed, ComputedRef, isRef, markRaw, onActivated, onDeactivated, Ref, ref, watch } from 'vue'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import * as os from '@/os'; | ||||
| import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from '@/scripts/scroll'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkButton | ||||
| 	}, | ||||
| const SECOND_FETCH_LIMIT = 30; | ||||
| 
 | ||||
| 	mixins: [ | ||||
| 		paging({}), | ||||
| 	], | ||||
| export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints> = { | ||||
| 	endpoint: E; | ||||
| 	limit: number; | ||||
| 	params?: misskey.Endpoints[E]['req'] | ComputedRef<misskey.Endpoints[E]['req']>; | ||||
| 
 | ||||
| 	props: { | ||||
| 		pagination: { | ||||
| 			required: true | ||||
| 		}, | ||||
| 	/** | ||||
| 	 * 検索APIのような、ページング不可なエンドポイントを利用する場合 | ||||
| 	 * (そのようなAPIをこの関数で使うのは若干矛盾してるけど) | ||||
| 	 */ | ||||
| 	noPaging?: boolean; | ||||
| 
 | ||||
| 		disableAutoLoad: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false, | ||||
| 	/** | ||||
| 	 * items 配列の中身を逆順にする(新しい方が最後) | ||||
| 	 */ | ||||
| 	reversed?: boolean; | ||||
| }; | ||||
| 
 | ||||
| const props = withDefaults(defineProps<{ | ||||
| 	pagination: Paging; | ||||
| 	disableAutoLoad?: boolean; | ||||
| 	displayLimit?: number; | ||||
| }>(), { | ||||
| 	displayLimit: 30, | ||||
| }); | ||||
| 
 | ||||
| const rootEl = ref<HTMLElement>(); | ||||
| const items = ref([]); | ||||
| const queue = ref([]); | ||||
| const offset = ref(0); | ||||
| const fetching = ref(true); | ||||
| const moreFetching = ref(false); | ||||
| const inited = ref(false); | ||||
| const more = ref(false); | ||||
| const backed = ref(false); // 遡り中か否か | ||||
| const isBackTop = ref(false); | ||||
| const empty = computed(() => items.value.length === 0 && !fetching.value && inited.value); | ||||
| const error = computed(() => !fetching.value && !inited.value); | ||||
| 
 | ||||
| const init = async () => { | ||||
| 	queue.value = []; | ||||
| 	fetching.value = true; | ||||
| 	const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; | ||||
| 	await os.api(props.pagination.endpoint, { | ||||
| 		...params, | ||||
| 		limit: props.pagination.noPaging ? (props.pagination.limit || 10) : (props.pagination.limit || 10) + 1, | ||||
| 	}).then(res => { | ||||
| 		for (let i = 0; i < res.length; i++) { | ||||
| 			const item = res[i]; | ||||
| 			markRaw(item); | ||||
| 			if (props.pagination.reversed) { | ||||
| 				if (i === res.length - 2) item._shouldInsertAd_ = true; | ||||
| 			} else { | ||||
| 				if (i === 3) item._shouldInsertAd_ = true; | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
| 		if (!props.pagination.noPaging && (res.length > (props.pagination.limit || 10))) { | ||||
| 			res.pop(); | ||||
| 			items.value = props.pagination.reversed ? [...res].reverse() : res; | ||||
| 			more.value = true; | ||||
| 		} else { | ||||
| 			items.value = props.pagination.reversed ? [...res].reverse() : res; | ||||
| 			more.value = false; | ||||
| 		} | ||||
| 		offset.value = res.length; | ||||
| 		inited.value = true; | ||||
| 		fetching.value = false; | ||||
| 	}, e => { | ||||
| 		fetching.value = false; | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| const reload = () => { | ||||
| 	items.value = []; | ||||
| 	init(); | ||||
| }; | ||||
| 
 | ||||
| const fetchMore = async () => { | ||||
| 	if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return; | ||||
| 	moreFetching.value = true; | ||||
| 	backed.value = true; | ||||
| 	const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; | ||||
| 	await os.api(props.pagination.endpoint, { | ||||
| 		...params, | ||||
| 		limit: SECOND_FETCH_LIMIT + 1, | ||||
| 		...(props.pagination.offsetMode ? { | ||||
| 			offset: offset.value, | ||||
| 		} : { | ||||
| 			untilId: props.pagination.reversed ? items.value[0].id : items.value[items.value.length - 1].id, | ||||
| 		}), | ||||
| 	}).then(res => { | ||||
| 		for (let i = 0; i < res.length; i++) { | ||||
| 			const item = res[i]; | ||||
| 			markRaw(item); | ||||
| 			if (props.pagination.reversed) { | ||||
| 				if (i === res.length - 9) item._shouldInsertAd_ = true; | ||||
| 			} else { | ||||
| 				if (i === 10) item._shouldInsertAd_ = true; | ||||
| 			} | ||||
| 		} | ||||
| 		if (res.length > SECOND_FETCH_LIMIT) { | ||||
| 			res.pop(); | ||||
| 			items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res); | ||||
| 			more.value = true; | ||||
| 		} else { | ||||
| 			items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res); | ||||
| 			more.value = false; | ||||
| 		} | ||||
| 		offset.value += res.length; | ||||
| 		moreFetching.value = false; | ||||
| 	}, e => { | ||||
| 		moreFetching.value = false; | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| const fetchMoreAhead = async () => { | ||||
| 	if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return; | ||||
| 	moreFetching.value = true; | ||||
| 	const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; | ||||
| 	await os.api(props.pagination.endpoint, { | ||||
| 		...params, | ||||
| 		limit: SECOND_FETCH_LIMIT + 1, | ||||
| 		...(props.pagination.offsetMode ? { | ||||
| 			offset: offset.value, | ||||
| 		} : { | ||||
| 			sinceId: props.pagination.reversed ? items.value[0].id : items.value[items.value.length - 1].id, | ||||
| 		}), | ||||
| 	}).then(res => { | ||||
| 		for (const item of res) { | ||||
| 			markRaw(item); | ||||
| 		} | ||||
| 		if (res.length > SECOND_FETCH_LIMIT) { | ||||
| 			res.pop(); | ||||
| 			items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res); | ||||
| 			more.value = true; | ||||
| 		} else { | ||||
| 			items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res); | ||||
| 			more.value = false; | ||||
| 		} | ||||
| 		offset.value += res.length; | ||||
| 		moreFetching.value = false; | ||||
| 	}, e => { | ||||
| 		moreFetching.value = false; | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| const prepend = (item) => { | ||||
| 	if (props.pagination.reversed) { | ||||
| 		const container = getScrollContainer(rootEl.value); | ||||
| 		const pos = getScrollPosition(rootEl.value); | ||||
| 		const viewHeight = container.clientHeight; | ||||
| 		const height = container.scrollHeight; | ||||
| 		const isBottom = (pos + viewHeight > height - 32); | ||||
| 		if (isBottom) { | ||||
| 			// オーバーフローしたら古いアイテムは捨てる | ||||
| 			if (items.value.length >= props.displayLimit) { | ||||
| 				// このやり方だとVue 3.2以降アニメーションが動かなくなる | ||||
| 				//items.value = items.value.slice(-props.displayLimit); | ||||
| 				while (items.value.length >= props.displayLimit) { | ||||
| 					items.value.shift(); | ||||
| 				} | ||||
| 				more.value = true; | ||||
| 			} | ||||
| 		} | ||||
| 		items.value.push(item); | ||||
| 		// TODO | ||||
| 	} else { | ||||
| 		const isTop = isBackTop.value || (document.body.contains(rootEl.value) && isTopVisible(rootEl.value)); | ||||
| 		console.log(item, top); | ||||
| 
 | ||||
| 		if (isTop) { | ||||
| 			// Prepend the item | ||||
| 			items.value.unshift(item); | ||||
| 
 | ||||
| 			// オーバーフローしたら古いアイテムは捨てる | ||||
| 			if (items.value.length >= props.displayLimit) { | ||||
| 				// このやり方だとVue 3.2以降アニメーションが動かなくなる | ||||
| 				//this.items = items.value.slice(0, props.displayLimit); | ||||
| 				while (items.value.length >= props.displayLimit) { | ||||
| 					items.value.pop(); | ||||
| 				} | ||||
| 				more.value = true; | ||||
| 			} | ||||
| 		} else { | ||||
| 			queue.value.push(item); | ||||
| 			onScrollTop(rootEl.value, () => { | ||||
| 				for (const item of queue.value) { | ||||
| 					prepend(item); | ||||
| 				} | ||||
| 				queue.value = []; | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| const append = (item) => { | ||||
| 	items.value.push(item); | ||||
| }; | ||||
| 
 | ||||
| watch(props.pagination.params, init, { deep: true }); | ||||
| watch(queue, (a, b) => { | ||||
| 	if (a.length === 0 && b.length === 0) return; | ||||
| 	this.$emit('queue', queue.value.length); | ||||
| }, { deep: true }); | ||||
| 
 | ||||
| init(); | ||||
| 
 | ||||
| onActivated(() => { | ||||
| 	isBackTop.value = false; | ||||
| }); | ||||
| 
 | ||||
| onDeactivated(() => { | ||||
| 	isBackTop.value = window.scrollY === 0; | ||||
| }); | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	items, | ||||
| 	reload, | ||||
| 	fetchMoreAhead, | ||||
| 	prepend, | ||||
| 	append, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  | @ -64,11 +268,9 @@ export default defineComponent({ | |||
| } | ||||
| 
 | ||||
| .cxiknjgy { | ||||
| 	> .more > .button { | ||||
| 	> .button { | ||||
| 		margin-left: auto; | ||||
| 		margin-right: auto; | ||||
| 		height: 48px; | ||||
| 		min-width: 150px; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -1,91 +1,39 @@ | |||
| <template> | ||||
| <MkError v-if="error" @retry="init()"/> | ||||
| <MkPagination ref="pagingComponent" :pagination="pagination"> | ||||
| 	<template #empty> | ||||
| 		<div class="_fullinfo"> | ||||
| 			<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> | ||||
| 			<div>{{ $ts.noUsers }}</div> | ||||
| 		</div> | ||||
| 	</template> | ||||
| 
 | ||||
| <div v-else class="efvhhmdq _isolated"> | ||||
| 	<div v-if="empty" class="no-users"> | ||||
| 		<p>{{ $ts.noUsers }}</p> | ||||
| 	</div> | ||||
| 	<div class="users"> | ||||
| 		<MkUserInfo v-for="user in users" :key="user.id" class="user" :user="user"/> | ||||
| 	</div> | ||||
| 	<button v-show="more" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" class="more" :class="{ fetching: moreFetching }" :disabled="moreFetching" @click="fetchMore"> | ||||
| 		<template v-if="moreFetching"><i class="fas fa-spinner fa-pulse fa-fw"></i></template>{{ moreFetching ? $ts.loading : $ts.loadMore }} | ||||
| 	</button> | ||||
| </div> | ||||
| 	<template #default="{ items: users }"> | ||||
| 		<div class="efvhhmdq"> | ||||
| 			<MkUserInfo v-for="user in users" :key="user.id" class="user" :user="user"/> | ||||
| 		</div> | ||||
| 	</template> | ||||
| </MkPagination> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import paging from '@/scripts/paging'; | ||||
| import MkUserInfo from './user-info.vue'; | ||||
| <script lang="ts" setup> | ||||
| import { ref } from 'vue'; | ||||
| import MkUserInfo from '@/components/user-info.vue'; | ||||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
| import { Paging } from '@/components/ui/pagination.vue'; | ||||
| import { userPage } from '@/filters/user'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkUserInfo, | ||||
| 	}, | ||||
| const props = defineProps<{ | ||||
| 	pagination: Paging; | ||||
| 	noGap?: boolean; | ||||
| }>(); | ||||
| 
 | ||||
| 	mixins: [ | ||||
| 		paging({}), | ||||
| 	], | ||||
| 
 | ||||
| 	props: { | ||||
| 		pagination: { | ||||
| 			required: true | ||||
| 		}, | ||||
| 		extract: { | ||||
| 			required: false | ||||
| 		}, | ||||
| 		expanded: { | ||||
| 			type: Boolean, | ||||
| 			default: true | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		users() { | ||||
| 			return this.extract ? this.extract(this.items) : this.items; | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		userPage | ||||
| 	} | ||||
| }); | ||||
| const pagingComponent = ref<InstanceType<typeof MkPagination>>(); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .efvhhmdq { | ||||
| 	> .no-users { | ||||
| 		text-align: center; | ||||
| 	} | ||||
| 
 | ||||
| 	> .users { | ||||
| 		display: grid; | ||||
| 		grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); | ||||
| 		grid-gap: var(--margin); | ||||
| 	} | ||||
| 
 | ||||
| 	> .more { | ||||
| 		display: block; | ||||
| 		width: 100%; | ||||
| 		padding: 16px; | ||||
| 
 | ||||
| 		&:hover { | ||||
| 			background: rgba(#000, 0.025); | ||||
| 		} | ||||
| 
 | ||||
| 		&:active { | ||||
| 			background: rgba(#000, 0.05); | ||||
| 		} | ||||
| 
 | ||||
| 		&.fetching { | ||||
| 			cursor: wait; | ||||
| 		} | ||||
| 
 | ||||
| 		> i { | ||||
| 			margin-right: 4px; | ||||
| 		} | ||||
| 	} | ||||
| 	display: grid; | ||||
| 	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); | ||||
| 	grid-gap: var(--margin); | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -12,7 +12,7 @@ | |||
| 	 | ||||
| 	<FormSection> | ||||
| 		<template #label>{{ $ts.signinHistory }}</template> | ||||
| 		<FormPagination :pagination="pagination"> | ||||
| 		<MkPagination :pagination="pagination"> | ||||
| 			<template v-slot="{items}"> | ||||
| 				<div> | ||||
| 					<div v-for="item in items" :key="item.id" v-panel class="timnmucd"> | ||||
|  | @ -25,7 +25,7 @@ | |||
| 					</div> | ||||
| 				</div> | ||||
| 			</template> | ||||
| 		</FormPagination> | ||||
| 		</MkPagination> | ||||
| 	</FormSection> | ||||
| 
 | ||||
| 	<FormSection> | ||||
|  | @ -42,7 +42,7 @@ import { defineComponent } from 'vue'; | |||
| import FormSection from '@/components/form/section.vue'; | ||||
| import FormSlot from '@/components/form/slot.vue'; | ||||
| import FormButton from '@/components/ui/button.vue'; | ||||
| import FormPagination from '@/components/form/pagination.vue'; | ||||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
| import X2fa from './2fa.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
|  | @ -51,7 +51,7 @@ export default defineComponent({ | |||
| 	components: { | ||||
| 		FormSection, | ||||
| 		FormButton, | ||||
| 		FormPagination, | ||||
| 		MkPagination, | ||||
| 		FormSlot, | ||||
| 		X2fa, | ||||
| 	}, | ||||
|  |  | |||
|  | @ -1,60 +1,36 @@ | |||
| <template> | ||||
| <div v-sticky-container class="yrzkoczt"> | ||||
| 	<MkTab v-model="with_" class="tab"> | ||||
| 	<MkTab v-model="include" class="tab"> | ||||
| 		<option :value="null">{{ $ts.notes }}</option> | ||||
| 		<option value="replies">{{ $ts.notesAndReplies }}</option> | ||||
| 		<option value="files">{{ $ts.withFiles }}</option> | ||||
| 	</MkTab> | ||||
| 	<XNotes ref="timeline" :no-gap="true" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)"/> | ||||
| 	<XNotes ref="timeline" :no-gap="true" :pagination="pagination"/> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { ref, computed } from 'vue'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import XNotes from '@/components/notes.vue'; | ||||
| import MkTab from '@/components/tab.vue'; | ||||
| import * as os from '@/os'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		XNotes, | ||||
| 		MkTab, | ||||
| 	}, | ||||
| const props = defineProps<{ | ||||
| 	user: misskey.entities.UserDetailed; | ||||
| }>(); | ||||
| 
 | ||||
| 	props: { | ||||
| 		user: { | ||||
| 			type: Object, | ||||
| 			required: true, | ||||
| 		}, | ||||
| 	}, | ||||
| const include = ref<string | null>(null); | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			date: null, | ||||
| 			with_: null, | ||||
| 			pagination: { | ||||
| 				endpoint: 'users/notes', | ||||
| 				limit: 10, | ||||
| 				params: init => ({ | ||||
| 					userId: this.user.id, | ||||
| 					includeReplies: this.with_ === 'replies', | ||||
| 					withFiles: this.with_ === 'files', | ||||
| 					untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined), | ||||
| 				}) | ||||
| 			} | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		user() { | ||||
| 			this.$refs.timeline.reload(); | ||||
| 		}, | ||||
| 
 | ||||
| 		with_() { | ||||
| 			this.$refs.timeline.reload(); | ||||
| 		}, | ||||
| 	}, | ||||
| }); | ||||
| const pagination = { | ||||
| 	endpoint: 'users/notes' as const, | ||||
| 	limit: 10, | ||||
| 	params: computed(() => ({ | ||||
| 		userId: props.user.id, | ||||
| 		includeReplies: include.value === 'replies', | ||||
| 		withFiles: include.value === 'files', | ||||
| 	})), | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -1,246 +0,0 @@ | |||
| import { markRaw } from 'vue'; | ||||
| import * as os from '@/os'; | ||||
| import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from './scroll'; | ||||
| 
 | ||||
| const SECOND_FETCH_LIMIT = 30; | ||||
| 
 | ||||
| // reversed: items 配列の中身を逆順にする(新しい方が最後)
 | ||||
| 
 | ||||
| export default (opts) => ({ | ||||
| 	emits: ['queue'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			items: [], | ||||
| 			queue: [], | ||||
| 			offset: 0, | ||||
| 			fetching: true, | ||||
| 			moreFetching: false, | ||||
| 			inited: false, | ||||
| 			more: false, | ||||
| 			backed: false, // 遡り中か否か
 | ||||
| 			isBackTop: false, | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		empty(): boolean { | ||||
| 			return this.items.length === 0 && !this.fetching && this.inited; | ||||
| 		}, | ||||
| 
 | ||||
| 		error(): boolean { | ||||
| 			return !this.fetching && !this.inited; | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		pagination: { | ||||
| 			handler() { | ||||
| 				this.init(); | ||||
| 			}, | ||||
| 			deep: true | ||||
| 		}, | ||||
| 
 | ||||
| 		queue: { | ||||
| 			handler(a, b) { | ||||
| 				if (a.length === 0 && b.length === 0) return; | ||||
| 				this.$emit('queue', this.queue.length); | ||||
| 			}, | ||||
| 			deep: true | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		opts.displayLimit = opts.displayLimit || 30; | ||||
| 		this.init(); | ||||
| 	}, | ||||
| 
 | ||||
| 	activated() { | ||||
| 		this.isBackTop = false; | ||||
| 	}, | ||||
| 
 | ||||
| 	deactivated() { | ||||
| 		this.isBackTop = window.scrollY === 0; | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		reload() { | ||||
| 			this.items = []; | ||||
| 			this.init(); | ||||
| 		}, | ||||
| 
 | ||||
| 		replaceItem(finder, data) { | ||||
| 			const i = this.items.findIndex(finder); | ||||
| 			this.items[i] = data; | ||||
| 		}, | ||||
| 
 | ||||
| 		removeItem(finder) { | ||||
| 			const i = this.items.findIndex(finder); | ||||
| 			this.items.splice(i, 1); | ||||
| 		}, | ||||
| 
 | ||||
| 		async init() { | ||||
| 			this.queue = []; | ||||
| 			this.fetching = true; | ||||
| 			if (opts.before) opts.before(this); | ||||
| 			let params = typeof this.pagination.params === 'function' ? this.pagination.params(true) : this.pagination.params; | ||||
| 			if (params && params.then) params = await params; | ||||
| 			if (params === null) return; | ||||
| 			const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint; | ||||
| 			await os.api(endpoint, { | ||||
| 				...params, | ||||
| 				limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1, | ||||
| 			}).then(items => { | ||||
| 				for (let i = 0; i < items.length; i++) { | ||||
| 					const item = items[i]; | ||||
| 					markRaw(item); | ||||
| 					if (this.pagination.reversed) { | ||||
| 						if (i === items.length - 2) item._shouldInsertAd_ = true; | ||||
| 					} else { | ||||
| 						if (i === 3) item._shouldInsertAd_ = true; | ||||
| 					} | ||||
| 				} | ||||
| 				if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) { | ||||
| 					items.pop(); | ||||
| 					this.items = this.pagination.reversed ? [...items].reverse() : items; | ||||
| 					this.more = true; | ||||
| 				} else { | ||||
| 					this.items = this.pagination.reversed ? [...items].reverse() : items; | ||||
| 					this.more = false; | ||||
| 				} | ||||
| 				this.offset = items.length; | ||||
| 				this.inited = true; | ||||
| 				this.fetching = false; | ||||
| 				if (opts.after) opts.after(this, null); | ||||
| 			}, e => { | ||||
| 				this.fetching = false; | ||||
| 				if (opts.after) opts.after(this, e); | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		async fetchMore() { | ||||
| 			if (!this.more || this.fetching || this.moreFetching || this.items.length === 0) return; | ||||
| 			this.moreFetching = true; | ||||
| 			this.backed = true; | ||||
| 			let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params; | ||||
| 			if (params && params.then) params = await params; | ||||
| 			const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint; | ||||
| 			await os.api(endpoint, { | ||||
| 				...params, | ||||
| 				limit: SECOND_FETCH_LIMIT + 1, | ||||
| 				...(this.pagination.offsetMode ? { | ||||
| 					offset: this.offset, | ||||
| 				} : { | ||||
| 					untilId: this.pagination.reversed ? this.items[0].id : this.items[this.items.length - 1].id, | ||||
| 				}), | ||||
| 			}).then(items => { | ||||
| 				for (let i = 0; i < items.length; i++) { | ||||
| 					const item = items[i]; | ||||
| 					markRaw(item); | ||||
| 					if (this.pagination.reversed) { | ||||
| 						if (i === items.length - 9) item._shouldInsertAd_ = true; | ||||
| 					} else { | ||||
| 						if (i === 10) item._shouldInsertAd_ = true; | ||||
| 					} | ||||
| 				} | ||||
| 				if (items.length > SECOND_FETCH_LIMIT) { | ||||
| 					items.pop(); | ||||
| 					this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items); | ||||
| 					this.more = true; | ||||
| 				} else { | ||||
| 					this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items); | ||||
| 					this.more = false; | ||||
| 				} | ||||
| 				this.offset += items.length; | ||||
| 				this.moreFetching = false; | ||||
| 			}, e => { | ||||
| 				this.moreFetching = false; | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		async fetchMoreFeature() { | ||||
| 			if (!this.more || this.fetching || this.moreFetching || this.items.length === 0) return; | ||||
| 			this.moreFetching = true; | ||||
| 			let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params; | ||||
| 			if (params && params.then) params = await params; | ||||
| 			const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint; | ||||
| 			await os.api(endpoint, { | ||||
| 				...params, | ||||
| 				limit: SECOND_FETCH_LIMIT + 1, | ||||
| 				...(this.pagination.offsetMode ? { | ||||
| 					offset: this.offset, | ||||
| 				} : { | ||||
| 					sinceId: this.pagination.reversed ? this.items[0].id : this.items[this.items.length - 1].id, | ||||
| 				}), | ||||
| 			}).then(items => { | ||||
| 				for (const item of items) { | ||||
| 					markRaw(item); | ||||
| 				} | ||||
| 				if (items.length > SECOND_FETCH_LIMIT) { | ||||
| 					items.pop(); | ||||
| 					this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items); | ||||
| 					this.more = true; | ||||
| 				} else { | ||||
| 					this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items); | ||||
| 					this.more = false; | ||||
| 				} | ||||
| 				this.offset += items.length; | ||||
| 				this.moreFetching = false; | ||||
| 			}, e => { | ||||
| 				this.moreFetching = false; | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		prepend(item) { | ||||
| 			if (this.pagination.reversed) { | ||||
| 				const container = getScrollContainer(this.$el); | ||||
| 				const pos = getScrollPosition(this.$el); | ||||
| 				const viewHeight = container.clientHeight; | ||||
| 				const height = container.scrollHeight; | ||||
| 				const isBottom = (pos + viewHeight > height - 32); | ||||
| 				if (isBottom) { | ||||
| 					// オーバーフローしたら古いアイテムは捨てる
 | ||||
| 					if (this.items.length >= opts.displayLimit) { | ||||
| 						// このやり方だとVue 3.2以降アニメーションが動かなくなる
 | ||||
| 						//this.items = this.items.slice(-opts.displayLimit);
 | ||||
| 						while (this.items.length >= opts.displayLimit) { | ||||
| 							this.items.shift(); | ||||
| 						} | ||||
| 						this.more = true; | ||||
| 					} | ||||
| 				} | ||||
| 				this.items.push(item); | ||||
| 				// TODO
 | ||||
| 			} else { | ||||
| 				const isTop = this.isBackTop || (document.body.contains(this.$el) && isTopVisible(this.$el)); | ||||
| 
 | ||||
| 				if (isTop) { | ||||
| 					// Prepend the item
 | ||||
| 					this.items.unshift(item); | ||||
| 
 | ||||
| 					// オーバーフローしたら古いアイテムは捨てる
 | ||||
| 					if (this.items.length >= opts.displayLimit) { | ||||
| 						// このやり方だとVue 3.2以降アニメーションが動かなくなる
 | ||||
| 						//this.items = this.items.slice(0, opts.displayLimit);
 | ||||
| 						while (this.items.length >= opts.displayLimit) { | ||||
| 							this.items.pop(); | ||||
| 						} | ||||
| 						this.more = true; | ||||
| 					} | ||||
| 				} else { | ||||
| 					this.queue.push(item); | ||||
| 					onScrollTop(this.$el, () => { | ||||
| 						for (const item of this.queue) { | ||||
| 							this.prepend(item); | ||||
| 						} | ||||
| 						this.queue = []; | ||||
| 					}); | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		append(item) { | ||||
| 			this.items.push(item); | ||||
| 		}, | ||||
| 	} | ||||
| }); | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue