aaaaaaaaaaa
This commit is contained in:
		
							parent
							
								
									99d8172ae5
								
							
						
					
					
						commit
						90c6f15a72
					
				
					 20 changed files with 257 additions and 133 deletions
				
			
		|  | @ -1,13 +1,14 @@ | |||
| <script lang="ts"> | ||||
| import { defineComponent, h, PropType, TransitionGroup } from 'vue'; | ||||
| import { defineComponent, getCurrentInstance, h, markRaw, onMounted, PropType, TransitionGroup } from 'vue'; | ||||
| import MkAd from '@/components/global/ad.vue'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { defaultStore } from '@/store'; | ||||
| import { MisskeyEntity } from '@/types/date-separated-list'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	props: { | ||||
| 		items: { | ||||
| 			type: Array as PropType<{ id: string; createdAt: string; _shouldInsertAd_: boolean; }[]>, | ||||
| 			type: Array as PropType<MisskeyEntity[]>, | ||||
| 			required: true, | ||||
| 		}, | ||||
| 		direction: { | ||||
|  | @ -30,9 +31,15 @@ export default defineComponent({ | |||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		itemsContainer: { | ||||
| 			type: Object as PropType<HTMLElement | null>, | ||||
| 			required: false, | ||||
| 			nullable: true, | ||||
| 			default: null, | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	setup(props, { slots, expose }) { | ||||
| 	setup(props, { slots, expose, emit }) { | ||||
| 		function getDateText(time: string) { | ||||
| 			const date = new Date(time).getDate(); | ||||
| 			const month = new Date(time).getMonth() + 1; | ||||
|  | @ -90,6 +97,11 @@ export default defineComponent({ | |||
| 			} | ||||
| 		}); | ||||
| 
 | ||||
| 		onMounted(() => { | ||||
| 			const el = getCurrentInstance()?.vnode.el; | ||||
| 			emit('update:itemsContainer', el ? markRaw(el) : null); | ||||
| 		}); | ||||
| 
 | ||||
| 		function onBeforeLeave(el: HTMLElement) { | ||||
| 			el.style.top = `${el.offsetTop}px`; | ||||
| 			el.style.left = `${el.offsetLeft}px`; | ||||
|  |  | |||
|  | @ -1,71 +1,63 @@ | |||
| <template> | ||||
| <div ref="rootEl"> | ||||
| 	<slot name="header"></slot> | ||||
| 	<div ref="bodyEl"> | ||||
| 	<div ref="bodyEl" :data-sticky-container-header-height="headerHeight"> | ||||
| 		<slot></slot> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, onMounted, onUnmounted, ref } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { onMounted, onUnmounted } from 'vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	props: { | ||||
| 		autoSticky: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false, | ||||
| 		}, | ||||
| 	}, | ||||
| const props = withDefaults(defineProps<{ | ||||
| 	autoSticky?: boolean; | ||||
| }>(), { | ||||
| 	autoSticky: false, | ||||
| }) | ||||
| 
 | ||||
| 	setup(props, context) { | ||||
| 		const rootEl = ref<HTMLElement>(null); | ||||
| 		const bodyEl = ref<HTMLElement>(null); | ||||
| const rootEl = $ref<HTMLElement>(); | ||||
| const bodyEl = $ref<HTMLElement>(); | ||||
| 
 | ||||
| 		const calc = () => { | ||||
| 			const currentStickyTop = getComputedStyle(rootEl.value).getPropertyValue('--stickyTop') || '0px'; | ||||
| let headerHeight: string | undefined = $ref(); | ||||
| 
 | ||||
| 			const header = rootEl.value.children[0]; | ||||
| 			if (header === bodyEl.value) { | ||||
| 				bodyEl.value.style.setProperty('--stickyTop', currentStickyTop); | ||||
| 			} else { | ||||
| 				bodyEl.value.style.setProperty('--stickyTop', `calc(${currentStickyTop} + ${header.offsetHeight}px)`); | ||||
| const calc = () => { | ||||
| 	const currentStickyTop = getComputedStyle(rootEl).getPropertyValue('--stickyTop') || '0px'; | ||||
| 
 | ||||
| 				if (props.autoSticky) { | ||||
| 					header.style.setProperty('--stickyTop', currentStickyTop); | ||||
| 					header.style.position = 'sticky'; | ||||
| 					header.style.top = 'var(--stickyTop)'; | ||||
| 					header.style.zIndex = '1'; | ||||
| 				} | ||||
| 			} | ||||
| 		}; | ||||
| 	const header = rootEl.children[0] as HTMLElement; | ||||
| 	if (header === bodyEl) { | ||||
| 		bodyEl.style.setProperty('--stickyTop', currentStickyTop); | ||||
| 	} else { | ||||
| 		bodyEl.style.setProperty('--stickyTop', `calc(${currentStickyTop} + ${header.offsetHeight}px)`); | ||||
| 		headerHeight = header.offsetHeight.toString(); | ||||
| 
 | ||||
| 		onMounted(() => { | ||||
| 			calc(); | ||||
| 		if (props.autoSticky) { | ||||
| 			header.style.setProperty('--stickyTop', currentStickyTop); | ||||
| 			header.style.position = 'sticky'; | ||||
| 			header.style.top = 'var(--stickyTop)'; | ||||
| 			header.style.zIndex = '1'; | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| 			const observer = new MutationObserver(() => { | ||||
| 				window.setTimeout(() => { | ||||
| 					calc(); | ||||
| 				}, 100); | ||||
| 			}); | ||||
| const observer = new MutationObserver(() => { | ||||
| 	window.setTimeout(() => { | ||||
| 		calc(); | ||||
| 	}, 100); | ||||
| }); | ||||
| 
 | ||||
| 			observer.observe(rootEl.value, { | ||||
| 				attributes: false, | ||||
| 				childList: true, | ||||
| 				subtree: false, | ||||
| 			}); | ||||
| onMounted(() => { | ||||
| 	calc(); | ||||
| 
 | ||||
| 			onUnmounted(() => { | ||||
| 				observer.disconnect(); | ||||
| 			}); | ||||
| 		}); | ||||
| 	observer.observe(rootEl, { | ||||
| 		attributes: false, | ||||
| 		childList: true, | ||||
| 		subtree: false, | ||||
| 	}); | ||||
| }); | ||||
| 
 | ||||
| 		return { | ||||
| 			rootEl, | ||||
| 			bodyEl, | ||||
| 		}; | ||||
| 	}, | ||||
| onUnmounted(() => { | ||||
| 	observer.disconnect(); | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,9 +7,19 @@ | |||
| 		</div> | ||||
| 	</template> | ||||
| 
 | ||||
| 	<template #default="{ items: notes }"> | ||||
| 	<template #default="{ items: notes, itemsContainerWrapped }"> | ||||
| 		<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"> | ||||
| 			<XList | ||||
| 				ref="notes" | ||||
| 				v-slot="{ item: note }" | ||||
| 				:items="notes" | ||||
| 				:direction="pagination.reversed ? 'up' : 'down'" | ||||
| 				:reversed="pagination.reversed" | ||||
| 				:no-gap="noGap" | ||||
| 				:ad="true" | ||||
| 				v-model:itemsContainer="itemsContainerWrapped.v.value" | ||||
| 				class="notes" | ||||
| 			> | ||||
| 				<XNote :key="note._featuredId_ || note._prId_ || note.id" class="qtqtichx" :note="note"/> | ||||
| 			</XList> | ||||
| 		</div> | ||||
|  |  | |||
|  | @ -7,8 +7,8 @@ | |||
| 		</div> | ||||
| 	</template> | ||||
| 
 | ||||
| 	<template #default="{ items: notifications }"> | ||||
| 		<XList v-slot="{ item: notification }" class="elsfgstc" :items="notifications" :no-gap="true"> | ||||
| 	<template #default="{ items: notifications, itemsContainerWrapped }"> | ||||
| 		<XList v-slot="{ item: notification }" class="elsfgstc" :items="notifications" :no-gap="true" v-model:itemsContainer="itemsContainerWrapped.v.value"> | ||||
| 			<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/> | ||||
| 			<XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/> | ||||
| 		</XList> | ||||
|  | @ -29,7 +29,7 @@ import { stream } from '@/stream'; | |||
| import { $i } from '@/account'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	includeTypes?: PropType<typeof notificationTypes[number][]>; | ||||
| 	includeTypes?: typeof notificationTypes[number][]; | ||||
| 	unreadOnly?: boolean; | ||||
| }>(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -20,7 +20,7 @@ | |||
| 			</MkButton> | ||||
| 			<MkLoading v-else class="loading"/> | ||||
| 		</div> | ||||
| 		<slot :items="items" :fetching="fetching || moreFetching"></slot> | ||||
| 		<slot :items="items" :fetching="fetching || moreFetching" :itemsContainer="itemsContainer" :itemsContainerWrapped="itemsContainerWrapped"></slot> | ||||
| 		<div v-if="!pagination.reversed" v-show="more" key="_more_" class="cxiknjgy _gap"> | ||||
| 			<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore"> | ||||
| 				{{ $ts.loadMore }} | ||||
|  | @ -32,12 +32,13 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { computed, ComputedRef, isRef, nextTick, onActivated, onDeactivated, onMounted, ref, watch } from 'vue'; | ||||
| 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, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottom } from '@/scripts/scroll'; | ||||
| import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottom } from '@/scripts/scroll'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import { defaultStore } from '@/store'; | ||||
| import { MisskeyEntity } from '@/types/date-separated-list'; | ||||
| 
 | ||||
| const SECOND_FETCH_LIMIT = 30; | ||||
| 
 | ||||
|  | @ -75,16 +76,40 @@ const emit = defineEmits<{ | |||
| 	(e: 'queue', count: number): void; | ||||
| }>(); | ||||
| 
 | ||||
| type Item = { id: string; [another: string]: unknown; }; | ||||
| let rootEl = $ref<HTMLElement>(); | ||||
| 
 | ||||
| const rootEl = $ref<HTMLElement>(); | ||||
| const items = ref<Item[]>([]); | ||||
| const queue = ref<Item[]>([]); | ||||
| /* | ||||
|  * itemsContainer: itemsの実体DOMsの親コンテナ(=v-forの直上)のHTMLElement | ||||
|  * | ||||
|  * IntersectionObserverを使用してスクロールのパフォーマンスを向上させるため必要 | ||||
|  * この中の最初の要素を評価するので、順番を反転したり変えたりしてはいけない | ||||
|  *  | ||||
|  * これがundefinedのままの場合はrootElにフォールバックする | ||||
|  * つまりrootElがitemsの実体DOMsの親であるとする | ||||
|  *  | ||||
|  * 自動ロードやストリーミングでの追加がなければあまり関係ない | ||||
|  */ | ||||
| let itemsContainer = $ref<HTMLElement | null>(); | ||||
| /* | ||||
|  * date-separated-listとやり取りするために入れ子にしたオブジェクトを用意する | ||||
|  * slotの中身から変数を直接書き込むことができないため | ||||
|  */ | ||||
| const itemsContainerWrapped = { v: $$(itemsContainer) }; | ||||
| 
 | ||||
| /* | ||||
|  * 遡り中かどうか | ||||
|  * = (itemsContainer || rootEl).children.item(0) が画面内に入ったかどうか | ||||
|  */ | ||||
| let backed = $ref(false); | ||||
| 
 | ||||
| let scrollRemove: (() => void) | null = $ref(null); | ||||
| 
 | ||||
| const items = ref<MisskeyEntity[]>([]); | ||||
| const queue = ref<MisskeyEntity[]>([]); | ||||
| const offset = ref(0); | ||||
| const fetching = ref(true); | ||||
| const moreFetching = ref(false); | ||||
| const more = ref(false); | ||||
| const backed = ref(false); // 遡り中か否か | ||||
| const isBackTop = ref(false); | ||||
| const empty = computed(() => items.value.length === 0); | ||||
| const error = ref(false); | ||||
|  | @ -92,10 +117,57 @@ const { | |||
| 	enableInfiniteScroll | ||||
| } = defaultStore.reactiveState; | ||||
| 
 | ||||
| let mounted = $ref(false); | ||||
| 
 | ||||
| const contentEl = $computed(() => props.pagination.pageEl || rootEl); | ||||
| const scrollableElement = $computed(() => getScrollContainer(contentEl)); | ||||
| 
 | ||||
| const init = async (): Promise<void> => { | ||||
| const observer = new IntersectionObserver(entries => { | ||||
| 	if (entries.some(entry => entry.isIntersecting)) { | ||||
| 		backed = false; | ||||
| 	} else { | ||||
| 		backed = true; | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
| watch([items, $$(itemsContainer)], observeLatestElement); | ||||
| 
 | ||||
| function observeLatestElement() { | ||||
| 	observer.disconnect(); | ||||
| 	nextTick(() => { | ||||
| 		if (!mounted) return; | ||||
| 		const latestEl = (itemsContainer || rootEl)?.children.item(0); | ||||
| 		if (latestEl) observer.observe(latestEl); | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| watch($$(backed), () => { | ||||
| 	if (!backed) { | ||||
| 		if (!contentEl) return; | ||||
| 
 | ||||
| 		scrollRemove = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl, () => { | ||||
| 			if (queue.value.length === 0) return; | ||||
| 			for (const item of queue.value) { | ||||
| 				prepend(item, true); | ||||
| 			} | ||||
| 			queue.value = []; | ||||
| 		}); | ||||
| 	} else { | ||||
| 		if (scrollRemove) scrollRemove(); | ||||
| 		scrollRemove = null; | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
| if (props.pagination.params && isRef(props.pagination.params)) { | ||||
| 	watch(props.pagination.params, init, { deep: true }); | ||||
| } | ||||
| 
 | ||||
| watch(queue, (a, b) => { | ||||
| 	if (a.length === 0 && b.length === 0) return; | ||||
| 	emit('queue', queue.value.length); | ||||
| }, { deep: true }); | ||||
| 
 | ||||
| async function init(): Promise<void> { | ||||
| 	queue.value = []; | ||||
| 	fetching.value = true; | ||||
| 	const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; | ||||
|  | @ -133,7 +205,6 @@ const reload = (): Promise<void> => { | |||
| const fetchMore = async (): Promise<void> => { | ||||
| 	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, | ||||
|  | @ -150,16 +221,16 @@ const fetchMore = async (): Promise<void> => { | |||
| 		} | ||||
| 
 | ||||
| 		const reverseConcat = _res => { | ||||
| 			const oldHeight = contentEl.scrollHeight; | ||||
| 			const oldHeight = scrollableElement ? scrollableElement.scrollHeight : getBodyScrollHeight(); | ||||
| 			const oldScroll = scrollableElement ? scrollableElement.scrollTop : window.scrollY; | ||||
| 
 | ||||
| 			items.value = items.value.concat(_res); | ||||
| 
 | ||||
| 			return nextTick(() => { | ||||
| 				if (scrollableElement) { | ||||
| 					scroll(scrollableElement, { top: oldScroll + (contentEl.scrollHeight - oldHeight), behavior: 'instant' }); | ||||
| 					scroll(scrollableElement, { top: oldScroll + (scrollableElement.scrollHeight - oldHeight), behavior: 'instant' }); | ||||
| 				} else { | ||||
| 					window.scrollY = oldScroll + (contentEl.scrollHeight - oldHeight); | ||||
| 					window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' }); | ||||
| 				} | ||||
| 
 | ||||
| 				return nextTick(); | ||||
|  | @ -225,7 +296,7 @@ const fetchMoreAhead = async (): Promise<void> => { | |||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| const prepend = (item: Item, force = false): void => { | ||||
| const prepend = (item: MisskeyEntity, force = false): void => { | ||||
| 	// 初回表示時はunshiftだけでOK | ||||
| 	if (!rootEl) { | ||||
| 		items.value.unshift(item); | ||||
|  | @ -249,33 +320,18 @@ const prepend = (item: Item, force = false): void => { | |||
| 		} | ||||
| 	} else { | ||||
| 		queue.value.push(item); | ||||
| 		(props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl, () => { | ||||
| 			for (const item of queue.value) { | ||||
| 				prepend(item, true); | ||||
| 			} | ||||
| 			queue.value = []; | ||||
| 		}); | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| const append = (item: Item): void => { | ||||
| const append = (item: MisskeyEntity): void => { | ||||
| 	items.value.push(item); | ||||
| }; | ||||
| 
 | ||||
| const updateItem = (id: Item['id'], replacer: (old: Item) => Item): void => { | ||||
| const updateItem = (id: MisskeyEntity['id'], replacer: (old: MisskeyEntity) => MisskeyEntity): void => { | ||||
| 	const i = items.value.findIndex(item => item.id === id); | ||||
| 	items.value[i] = replacer(items.value[i]); | ||||
| }; | ||||
| 
 | ||||
| if (props.pagination.params && isRef(props.pagination.params)) { | ||||
| 	watch(props.pagination.params, init, { deep: true }); | ||||
| } | ||||
| 
 | ||||
| watch(queue, (a, b) => { | ||||
| 	if (a.length === 0 && b.length === 0) return; | ||||
| 	emit('queue', queue.value.length); | ||||
| }, { deep: true }); | ||||
| 
 | ||||
| const inited = init(); | ||||
| 
 | ||||
| onActivated(() => { | ||||
|  | @ -291,6 +347,8 @@ function toBottom() { | |||
| } | ||||
| 
 | ||||
| onMounted(() => { | ||||
| 	mounted = true; | ||||
| 
 | ||||
| 	inited.then(() => { | ||||
| 		if (props.pagination.reversed) { | ||||
| 			nextTick(() => { | ||||
|  | @ -304,7 +362,11 @@ onMounted(() => { | |||
| 			}); | ||||
| 		} | ||||
| 	}); | ||||
| }) | ||||
| }); | ||||
| 
 | ||||
| onBeforeUnmount(() => { | ||||
| 	observer.disconnect(); | ||||
| }); | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	items, | ||||
|  |  | |||
|  | @ -7,8 +7,8 @@ | |||
| 		</div> | ||||
| 	</template> | ||||
| 
 | ||||
| 	<template #default="{ items: users }"> | ||||
| 		<div class="efvhhmdq"> | ||||
| 	<template #default="{ items: users, itemsContainer }"> | ||||
| 		<div class="efvhhmdq" ref="itemsContainer"> | ||||
| 			<MkUserInfo v-for="user in users" :key="user.id" class="user" :user="user"/> | ||||
| 		</div> | ||||
| 	</template> | ||||
|  |  | |||
|  | @ -5,8 +5,10 @@ export default { | |||
| 		//const query = binding.value;
 | ||||
| 
 | ||||
| 		const header = src.children[0]; | ||||
| 		const body = src.children[1]; | ||||
| 		const currentStickyTop = getComputedStyle(src).getPropertyValue('--stickyTop') || '0px'; | ||||
| 		src.style.setProperty('--stickyTop', `calc(${currentStickyTop} + ${header.offsetHeight}px)`); | ||||
| 		if (body) body.dataset.stickyContainerHeaderHeight = header.offsetHeight.toString(); | ||||
| 		header.style.setProperty('--stickyTop', currentStickyTop); | ||||
| 		header.style.position = 'sticky'; | ||||
| 		header.style.top = 'var(--stickyTop)'; | ||||
|  |  | |||
|  | @ -19,8 +19,8 @@ | |||
| 			</div> | ||||
| 			<MkPagination ref="emojisPaginationComponent" :pagination="pagination"> | ||||
| 				<template #empty><span>{{ $ts.noCustomEmojis }}</span></template> | ||||
| 				<template v-slot="{items}"> | ||||
| 					<div class="ldhfsamy"> | ||||
| 				<template v-slot="{items, itemsContainer}"> | ||||
| 					<div class="ldhfsamy" ref="itemsContainer"> | ||||
| 						<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)"> | ||||
| 							<img :src="emoji.url" class="img" :alt="emoji.name"/> | ||||
| 							<div class="body"> | ||||
|  | @ -45,8 +45,8 @@ | |||
| 			</FormSplit> | ||||
| 			<MkPagination :pagination="remotePagination"> | ||||
| 				<template #empty><span>{{ $ts.noCustomEmojis }}</span></template> | ||||
| 				<template v-slot="{items}"> | ||||
| 					<div class="ldhfsamy"> | ||||
| 				<template v-slot="{items, itemsContainer}"> | ||||
| 					<div class="ldhfsamy" ref="itemsContainer"> | ||||
| 						<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)"> | ||||
| 							<img :src="emoji.url" class="img" :alt="emoji.name"/> | ||||
| 							<div class="body"> | ||||
|  |  | |||
|  | @ -8,8 +8,8 @@ | |||
| 			</div> | ||||
| 		</template> | ||||
| 
 | ||||
| 		<template #default="{ items }"> | ||||
| 			<XList v-slot="{ item }" :items="items" :direction="'down'" :no-gap="false" :ad="false"> | ||||
| 		<template #default="{ items, itemsContainerWrapped }"> | ||||
| 			<XList v-slot="{ item }" :items="items" :direction="'down'" :no-gap="false" :ad="false" v-model:itemsContainer="itemsContainerWrapped.v.value"> | ||||
| 				<XNote :key="item.id" :note="item.note" :class="$style.note"/> | ||||
| 			</XList> | ||||
| 		</template> | ||||
|  |  | |||
|  | @ -41,8 +41,8 @@ | |||
| 			</FormSplit> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination"> | ||||
| 			<div class="dqokceoi"> | ||||
| 		<MkPagination v-slot="{items, itemsContainer}" ref="instances" :key="host + state" :pagination="pagination"> | ||||
| 			<div class="dqokceoi" ref="itemsContainer"> | ||||
| 				<MkA v-for="instance in items" :key="instance.id" class="instance" :to="`/instance-info/${instance.host}`"> | ||||
| 					<div class="host"><img :src="instance.faviconUrl">{{ instance.host }}</div> | ||||
| 					<div class="table"> | ||||
|  |  | |||
|  | @ -7,8 +7,8 @@ | |||
| 				<div>{{ $ts.noFollowRequests }}</div> | ||||
| 			</div> | ||||
| 		</template> | ||||
| 		<template v-slot="{items}"> | ||||
| 			<div class="mk-follow-requests"> | ||||
| 		<template v-slot="{items, itemsContainer}"> | ||||
| 			<div class="mk-follow-requests" ref="itemsContainer"> | ||||
| 				<div v-for="req in items" :key="req.id" class="user _panel"> | ||||
| 					<MkAvatar class="avatar" :user="req.follower" :show-indicator="true"/> | ||||
| 					<div class="body"> | ||||
|  |  | |||
|  | @ -9,32 +9,32 @@ | |||
| 	<div v-if="tab === 'explore'"> | ||||
| 		<MkFolder class="_gap"> | ||||
| 			<template #header><i class="fas fa-clock"></i>{{ $ts.recentPosts }}</template> | ||||
| 			<MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disable-auto-load="true"> | ||||
| 				<div class="vfpdbgtk"> | ||||
| 			<MkPagination v-slot="{items, itemsContainer}" :pagination="recentPostsPagination" :disable-auto-load="true"> | ||||
| 				<div class="vfpdbgtk" ref="itemsContainer"> | ||||
| 					<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> | ||||
| 				</div> | ||||
| 			</MkPagination> | ||||
| 		</MkFolder> | ||||
| 		<MkFolder class="_gap"> | ||||
| 			<template #header><i class="fas fa-fire-alt"></i>{{ $ts.popularPosts }}</template> | ||||
| 			<MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disable-auto-load="true"> | ||||
| 				<div class="vfpdbgtk"> | ||||
| 			<MkPagination v-slot="{items, itemsContainer}" :pagination="popularPostsPagination" :disable-auto-load="true"> | ||||
| 				<div class="vfpdbgtk" ref="itemsContainer"> | ||||
| 					<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> | ||||
| 				</div> | ||||
| 			</MkPagination> | ||||
| 		</MkFolder> | ||||
| 	</div> | ||||
| 	<div v-else-if="tab === 'liked'"> | ||||
| 		<MkPagination v-slot="{items}" :pagination="likedPostsPagination"> | ||||
| 			<div class="vfpdbgtk"> | ||||
| 		<MkPagination v-slot="{items, itemsContainer}" :pagination="likedPostsPagination"> | ||||
| 			<div class="vfpdbgtk" ref="itemsContainer"> | ||||
| 				<MkGalleryPostPreview v-for="like in items" :key="like.id" :post="like.post" class="post"/> | ||||
| 			</div> | ||||
| 		</MkPagination> | ||||
| 	</div> | ||||
| 	<div v-else-if="tab === 'my'"> | ||||
| 		<MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="fas fa-plus"></i> {{ $ts.postToGallery }}</MkA> | ||||
| 		<MkPagination v-slot="{items}" :pagination="myPostsPagination"> | ||||
| 			<div class="vfpdbgtk"> | ||||
| 		<MkPagination v-slot="{items, itemsContainer}" :pagination="myPostsPagination"> | ||||
| 			<div class="vfpdbgtk" ref="itemsContainer"> | ||||
| 				<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> | ||||
| 			</div> | ||||
| 		</MkPagination> | ||||
|  |  | |||
|  | @ -36,8 +36,8 @@ | |||
| 			<MkAd :prefer="['horizontal', 'horizontal-big']"/> | ||||
| 			<MkContainer :max-height="300" :foldable="true" class="other"> | ||||
| 				<template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template> | ||||
| 				<MkPagination v-slot="{items}" :pagination="otherPostsPagination"> | ||||
| 					<div class="sdrarzaf"> | ||||
| 				<MkPagination v-slot="{items, itemsContainer}" :pagination="otherPostsPagination"> | ||||
| 					<div class="sdrarzaf" ref="itemsContainer"> | ||||
| 						<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> | ||||
| 					</div> | ||||
| 				</MkPagination> | ||||
|  |  | |||
|  | @ -14,8 +14,16 @@ | |||
| 					</div> | ||||
| 				</template> | ||||
| 
 | ||||
| 				<template #default="{ items: messages, fetching }"> | ||||
| 					<XList v-if="messages.length > 0" v-slot="{ item: message }" :class="{ messages: true, 'deny-move-transition': fetching }" :items="messages" direction="up" reversed> | ||||
| 				<template #default="{ items: messages, fetching, itemsContainerWrapped }"> | ||||
| 					<XList | ||||
| 						v-if="messages.length > 0" | ||||
| 						v-slot="{ item: message }" | ||||
| 						:class="{ messages: true, 'deny-move-transition': fetching }" | ||||
| 						:items="messages" | ||||
| 						v-model:itemsContainer="itemsContainerWrapped.v.value" | ||||
| 						direction="up" | ||||
| 						reversed | ||||
| 					> | ||||
| 						<XMessage :key="message.id" :message="message" :is-group="group != null"/> | ||||
| 					</XList> | ||||
| 				</template> | ||||
|  |  | |||
|  | @ -13,8 +13,8 @@ | |||
| 	<FormSection> | ||||
| 		<template #label>{{ $ts.signinHistory }}</template> | ||||
| 		<MkPagination :pagination="pagination"> | ||||
| 			<template v-slot="{items}"> | ||||
| 				<div> | ||||
| 			<template v-slot="{items, itemsContainer}"> | ||||
| 				<div ref="itemsContainer"> | ||||
| 					<div v-for="item in items" :key="item.id" v-panel class="timnmucd"> | ||||
| 						<header> | ||||
| 							<i v-if="item.success" class="fas fa-check icon succ"></i> | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<MkPagination v-slot="{items}" ref="list" :pagination="type === 'following' ? followingPagination : followersPagination" class="mk-following-or-followers"> | ||||
| 		<div class="users _isolated"> | ||||
| 	<MkPagination v-slot="{items, itemsContainer}" ref="list" :pagination="type === 'following' ? followingPagination : followersPagination" class="mk-following-or-followers"> | ||||
| 		<div class="users _isolated" ref="itemsContainer"> | ||||
| 			<MkUserInfo v-for="user in items.map(x => type === 'following' ? x.followee : x.follower)" :key="user.id" class="user" :user="user"/> | ||||
| 		</div> | ||||
| 	</MkPagination> | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<MkPagination v-slot="{items}" :pagination="pagination"> | ||||
| 		<div class="jrnovfpt"> | ||||
| 	<MkPagination v-slot="{items, itemsContainer}" :pagination="pagination"> | ||||
| 		<div class="jrnovfpt" ref="itemsContainer"> | ||||
| 			<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> | ||||
| 		</div> | ||||
| 	</MkPagination> | ||||
|  |  | |||
|  | @ -12,20 +12,28 @@ export function getScrollContainer(el: HTMLElement | null): HTMLElement | null { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| export function getStickyTop(el: HTMLElement, container: HTMLElement | null = null, top: number = 0) { | ||||
| 	if (!el.parentElement) return top; | ||||
| 	const data = el.dataset.stickyContainerHeaderHeight; | ||||
| 	const newTop = data ? Number(data) + top : top; | ||||
| 	if (el === container) return newTop; | ||||
| 	return getStickyTop(el.parentElement, container, newTop); | ||||
| } | ||||
| 
 | ||||
| export function getScrollPosition(el: HTMLElement | null): number { | ||||
| 	const container = getScrollContainer(el); | ||||
| 	return container == null ? window.scrollY : container.scrollTop; | ||||
| } | ||||
| 
 | ||||
| export function isTopVisible(el: HTMLElement): boolean { | ||||
| 	const scrollTop = getScrollPosition(el); | ||||
| 	const topPosition = el.offsetTop; // TODO: container内でのelの相対位置を取得できればより正確になる
 | ||||
| export function onScrollTop(el: HTMLElement, cb: Function) { | ||||
| 	// とりあえず評価してみる
 | ||||
| 	if (isTopVisible(el)) { | ||||
| 		cb(); | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
| 	return scrollTop <= topPosition; | ||||
| } | ||||
| 
 | ||||
| export function onScrollTop(el: HTMLElement, cb) { | ||||
| 	const container = getScrollContainer(el) || window; | ||||
| 
 | ||||
| 	const onScroll = ev => { | ||||
| 		if (!document.body.contains(el)) return; | ||||
| 		if (isTopVisible(el)) { | ||||
|  | @ -33,13 +41,21 @@ export function onScrollTop(el: HTMLElement, cb) { | |||
| 			removeListener(); | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	function removeListener() { container.removeEventListener('scroll', onScroll) } | ||||
| 	container.addEventListener('scroll', onScroll, { passive: true }); | ||||
| 	return removeListener; | ||||
| } | ||||
| 
 | ||||
| export function onScrollBottom(el: HTMLElement, cb) { | ||||
| export function onScrollBottom(el: HTMLElement, cb: Function) { | ||||
| 	const container = getScrollContainer(el); | ||||
| 
 | ||||
| 	// とりあえず評価してみる
 | ||||
| 	if (isBottom(el, 1, container)) { | ||||
| 		cb(); | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
| 	const containerOrWindow = container || window; | ||||
| 	const onScroll = ev => { | ||||
| 		if (!document.body.contains(el)) return; | ||||
|  | @ -48,6 +64,7 @@ export function onScrollBottom(el: HTMLElement, cb) { | |||
| 			removeListener(); | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	function removeListener() { containerOrWindow.removeEventListener('scroll', onScroll) } | ||||
| 	containerOrWindow.addEventListener('scroll', onScroll, { passive: true }); | ||||
| 	return removeListener; | ||||
|  | @ -76,16 +93,32 @@ export function scrollToTop(el: HTMLElement, options: { behavior?: ScrollBehavio | |||
|  * @param el Content element | ||||
|  * @param options Scroll options | ||||
|  * @param container Scroll container element | ||||
|  * @param addSticky To add sticky-top or not | ||||
|  */ | ||||
| export function scrollToBottom(el: HTMLElement, options: ScrollToOptions = {}, container = getScrollContainer(el)) { | ||||
| export function scrollToBottom(el: HTMLElement, options: ScrollToOptions = {}, container = getScrollContainer(el), addSticky: boolean = true) { | ||||
| 	const addStickyTop = addSticky ? getStickyTop(el, container) : 0; | ||||
| 	if (container) { | ||||
| 		container.scroll({ top: el.scrollHeight - container.clientHeight || 0, ...options }); | ||||
| 		container.scroll({ top: el.scrollHeight - container.clientHeight + addStickyTop || 0, ...options }); | ||||
| 	} else { | ||||
| 		window.scroll({ top: el.scrollHeight - window.innerHeight || 0, ...options }); | ||||
| 		window.scroll({ top: el.scrollHeight - window.innerHeight + addStickyTop || 0, ...options }); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export function isTopVisible(el: HTMLElement, asobi: number = 1): boolean { | ||||
| 	const scrollTop = getScrollPosition(el); | ||||
| 	return scrollTop <= asobi; | ||||
| } | ||||
| 
 | ||||
| export function isBottom(el: HTMLElement, asobi = 1, container = getScrollContainer(el)) { | ||||
| 	if (container) return el.scrollHeight <= container.clientHeight + Math.abs(container.scrollTop) + asobi; | ||||
| 	return el.scrollHeight <= window.innerHeight + window.scrollY + asobi; | ||||
| } | ||||
| 
 | ||||
| // https://ja.javascript.info/size-and-scroll-window#ref-932
 | ||||
| export function getBodyScrollHeight() { | ||||
| 	return Math.max( | ||||
| 		document.body.scrollHeight, document.documentElement.scrollHeight, | ||||
| 		document.body.offsetHeight, document.documentElement.offsetHeight, | ||||
| 		document.body.clientHeight, document.documentElement.clientHeight | ||||
| 	); | ||||
| } | ||||
|  |  | |||
							
								
								
									
										7
									
								
								packages/client/src/types/date-separated-list.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								packages/client/src/types/date-separated-list.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | |||
| export type MisskeyEntity = { | ||||
| 	id: string; | ||||
| 	createdAt: string; | ||||
| 	_shouldInsertAd_?: | ||||
| 	boolean; | ||||
| 	[x: string]: any; | ||||
| }; | ||||
|  | @ -44,7 +44,6 @@ import { defineComponent, defineAsyncComponent } from 'vue'; | |||
| import { host, instanceName } from '@/config'; | ||||
| import { search } from '@/scripts/search'; | ||||
| import * as os from '@/os'; | ||||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import XHeader from './header.vue'; | ||||
| import { ColdDeviceStorage } from '@/store'; | ||||
|  | @ -55,7 +54,6 @@ const DESKTOP_THRESHOLD = 1100; | |||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		XHeader, | ||||
| 		MkPagination, | ||||
| 		MkButton, | ||||
| 	}, | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue