refactor: pagination/date-separated-list系処理を良い感じに? (#8209)
* pages/messaging/messaging-room.vue * wip * wip * wip??? * wip? * ✌️ * messaaging-room.form.vue rewrite to compositon api * refactor * 関心事でないのでとりあえず置いておく * 🎨 * 🎨 * i18n.ts * fix scroll container find function * fix * FIX * ✌️ * Fix scroll bottom detect * wip * aaaaaaaaaaa * rename * fix * fix? * ✌️ * ✌️ * clean up * clena up * refactor * scroll event once or not * fix * fix once * add safe-area-inset-bottom to spacer * fix * ✌️ * 🎨 * fix * fix * wip * ✌️ * clean up * fix lint * Update packages/client/src/components/global/sticky-container.vue Co-authored-by: Johann150 <johann.galle@protonmail.com> * Update packages/client/src/components/ui/pagination.vue Co-authored-by: Johann150 <johann.galle@protonmail.com> * Update packages/client/src/pages/messaging/messaging-room.form.vue Co-authored-by: Johann150 <johann.galle@protonmail.com> * clean up: single line comment * https://github.com/misskey-dev/misskey/pull/8209#discussion_r867386077 * fix * asobi → tolerance * pick form * pick message * pick room * fix lint * fix scroll? * fix scroll.ts * fix directives/sticky-container * update global/sticky-container.vue * fix, 🎨 * revert merge * ✌️ * fix lint errors * 🎨 * Update packages/client/src/types/date-separated-list.ts Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * https://github.com/misskey-dev/misskey/pull/8209#discussion_r917225080 * use ' * Update packages/client/src/scripts/scroll.ts Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * use Number.EPSILON Co-authored-by: acid-chicken <root@acid-chicken.com> * revert * fix * fix * Use % instead of vh * 🎨 * 🎨 * 🎨 * wip * wip * css modules Co-authored-by: Johann150 <johann.galle@protonmail.com> Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
This commit is contained in:
		
							parent
							
								
									519a08f8b5
								
							
						
					
					
						commit
						d2204fd5c8
					
				
					 9 changed files with 457 additions and 278 deletions
				
			
		|  | @ -1,13 +1,14 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent, h, PropType, TransitionGroup } from 'vue'; | import { defineComponent, h, PropType, TransitionGroup, useCssModule } from 'vue'; | ||||||
| import MkAd from '@/components/global/MkAd.vue'; | import MkAd from '@/components/global/MkAd.vue'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
| import { defaultStore } from '@/store'; | import { defaultStore } from '@/store'; | ||||||
|  | import { MisskeyEntity } from '@/types/date-separated-list'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		items: { | 		items: { | ||||||
| 			type: Array as PropType<{ id: string; createdAt: string; _shouldInsertAd_: boolean; }[]>, | 			type: Array as PropType<MisskeyEntity[]>, | ||||||
| 			required: true, | 			required: true, | ||||||
| 		}, | 		}, | ||||||
| 		direction: { | 		direction: { | ||||||
|  | @ -33,6 +34,7 @@ export default defineComponent({ | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	setup(props, { slots, expose }) { | 	setup(props, { slots, expose }) { | ||||||
|  | 		const $style = useCssModule(); | ||||||
| 		function getDateText(time: string) { | 		function getDateText(time: string) { | ||||||
| 			const date = new Date(time).getDate(); | 			const date = new Date(time).getDate(); | ||||||
| 			const month = new Date(time).getMonth() + 1; | 			const month = new Date(time).getMonth() + 1; | ||||||
|  | @ -57,21 +59,25 @@ export default defineComponent({ | ||||||
| 				new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate() | 				new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate() | ||||||
| 			) { | 			) { | ||||||
| 				const separator = h('div', { | 				const separator = h('div', { | ||||||
| 					class: 'separator', | 					class: $style['separator'], | ||||||
| 					key: item.id + ':separator', | 					key: item.id + ':separator', | ||||||
| 				}, h('p', { | 				}, h('p', { | ||||||
| 					class: 'date', | 					class: $style['date'], | ||||||
| 				}, [ | 				}, [ | ||||||
| 					h('span', [ | 					h('span', { | ||||||
|  | 						class: $style['date-1'], | ||||||
|  | 					}, [ | ||||||
| 						h('i', { | 						h('i', { | ||||||
| 							class: 'ti ti-chevron-up icon', | 							class: `ti ti-chevron-up ${$style['date-1-icon']}`, | ||||||
| 						}), | 						}), | ||||||
| 						getDateText(item.createdAt), | 						getDateText(item.createdAt), | ||||||
| 					]), | 					]), | ||||||
| 					h('span', [ | 					h('span', { | ||||||
|  | 						class: $style['date-2'], | ||||||
|  | 					}, [ | ||||||
| 						getDateText(props.items[i + 1].createdAt), | 						getDateText(props.items[i + 1].createdAt), | ||||||
| 						h('i', { | 						h('i', { | ||||||
| 							class: 'ti ti-chevron-down icon', | 							class: `ti ti-chevron-down ${$style['date-2-icon']}`, | ||||||
| 						}), | 						}), | ||||||
| 					]), | 					]), | ||||||
| 				])); | 				])); | ||||||
|  | @ -89,26 +95,62 @@ export default defineComponent({ | ||||||
| 			} | 			} | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
|  | 		function onBeforeLeave(el: HTMLElement) { | ||||||
|  | 			el.style.top = `${el.offsetTop}px`; | ||||||
|  | 			el.style.left = `${el.offsetLeft}px`; | ||||||
|  | 		} | ||||||
|  | 		function onLeaveCanceled(el: HTMLElement) { | ||||||
|  | 			el.style.top = ''; | ||||||
|  | 			el.style.left = ''; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		return () => h( | 		return () => h( | ||||||
| 			defaultStore.state.animation ? TransitionGroup : 'div', | 			defaultStore.state.animation ? TransitionGroup : 'div', | ||||||
| 			defaultStore.state.animation ? { | 			{ | ||||||
| 				class: 'sqadhkmv' + (props.noGap ? ' noGap' : ''), | 					class: { | ||||||
| 				name: 'list', | 						[$style['date-separated-list']]: true, | ||||||
| 				tag: 'div', | 						[$style['date-separated-list-nogap']]: props.noGap, | ||||||
| 				'data-direction': props.direction, | 						[$style['reversed']]: props.reversed, | ||||||
| 				'data-reversed': props.reversed ? 'true' : 'false', | 						[$style['direction-down']]: props.direction === 'down', | ||||||
| 			} : { | 						[$style['direction-up']]: props.direction === 'up', | ||||||
| 				class: 'sqadhkmv' + (props.noGap ? ' noGap' : ''), | 					}, | ||||||
|  | 					...(defaultStore.state.animation ? { | ||||||
|  | 						name: 'list', | ||||||
|  | 						tag: 'div', | ||||||
|  | 						onBeforeLeave, | ||||||
|  | 						onLeaveCanceled, | ||||||
|  | 					} : {}), | ||||||
| 			}, | 			}, | ||||||
| 			{ default: renderChildren }); | 			{ default: renderChildren }); | ||||||
| 	}, | 	}, | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss"> | <style lang="scss" module> | ||||||
| .sqadhkmv { | .date-separated-list { | ||||||
| 	container-type: inline-size; | 	container-type: inline-size; | ||||||
| 
 | 
 | ||||||
|  | 	&:global { | ||||||
|  | 	> .list-move { | ||||||
|  | 		transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	&.deny-move-transition > .list-move { | ||||||
|  | 		transition: none !important; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	> .list-leave-active, | ||||||
|  | 	> .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); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	> .list-leave-from, | ||||||
|  | 	> .list-leave-to, | ||||||
|  | 	> .list-leave-active { | ||||||
|  | 		transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1); | ||||||
|  | 		position: absolute !important; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	> *:empty { | 	> *:empty { | ||||||
| 		display: none; | 		display: none; | ||||||
| 	} | 	} | ||||||
|  | @ -116,73 +158,75 @@ export default defineComponent({ | ||||||
| 	> *:not(:last-child) { | 	> *:not(:last-child) { | ||||||
| 		margin-bottom: var(--margin); | 		margin-bottom: var(--margin); | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	> .list-move { |  | ||||||
| 		transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1); |  | ||||||
| 	} | 	} | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| 	> .list-enter-active { | .date-separated-list-nogap { | ||||||
| 		transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1); | 	> * { | ||||||
| 	} | 		margin: 0 !important; | ||||||
|  | 		border: none; | ||||||
|  | 		border-radius: 0; | ||||||
|  | 		box-shadow: none; | ||||||
| 
 | 
 | ||||||
| 	&[data-direction="up"] { | 		&:not(:last-child) { | ||||||
| 		> .list-enter-from { | 			border-bottom: solid 0.5px var(--divider); | ||||||
| 			opacity: 0; |  | ||||||
| 			transform: translateY(64px); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	&[data-direction="down"] { |  | ||||||
| 		> .list-enter-from { |  | ||||||
| 			opacity: 0; |  | ||||||
| 			transform: translateY(-64px); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	> .separator { |  | ||||||
| 		text-align: center; |  | ||||||
| 
 |  | ||||||
| 		> .date { |  | ||||||
| 			display: inline-block; |  | ||||||
| 			position: relative; |  | ||||||
| 			margin: 0; |  | ||||||
| 			padding: 0 16px; |  | ||||||
| 			line-height: 32px; |  | ||||||
| 			text-align: center; |  | ||||||
| 			font-size: 12px; |  | ||||||
| 			color: var(--dateLabelFg); |  | ||||||
| 
 |  | ||||||
| 			> span { |  | ||||||
| 				&:first-child { |  | ||||||
| 					margin-right: 8px; |  | ||||||
| 
 |  | ||||||
| 					> .icon { |  | ||||||
| 						margin-right: 8px; |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				&:last-child { |  | ||||||
| 					margin-left: 8px; |  | ||||||
| 
 |  | ||||||
| 					> .icon { |  | ||||||
| 						margin-left: 8px; |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	&.noGap { |  | ||||||
| 		> * { |  | ||||||
| 			margin: 0 !important; |  | ||||||
| 			border: none; |  | ||||||
| 			border-radius: 0; |  | ||||||
| 			box-shadow: none; |  | ||||||
| 
 |  | ||||||
| 			&:not(:last-child) { |  | ||||||
| 				border-bottom: solid 0.5px var(--divider); |  | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | .direction-up { | ||||||
|  | 	&:global { | ||||||
|  | 	> .list-enter-from, | ||||||
|  | 	> .list-leave-to { | ||||||
|  | 		opacity: 0; | ||||||
|  | 		transform: translateY(64px); | ||||||
|  | 	} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | .direction-down { | ||||||
|  | 	&:global { | ||||||
|  | 	> .list-enter-from, | ||||||
|  | 	> .list-leave-to { | ||||||
|  | 		opacity: 0; | ||||||
|  | 		transform: translateY(-64px); | ||||||
|  | 	} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .reversed { | ||||||
|  | 	display: flex; | ||||||
|  | 	flex-direction: column-reverse; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .separator { | ||||||
|  | 	text-align: center; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .date { | ||||||
|  | 	display: inline-block; | ||||||
|  | 	position: relative; | ||||||
|  | 	margin: 0; | ||||||
|  | 	padding: 0 16px; | ||||||
|  | 	line-height: 32px; | ||||||
|  | 	text-align: center; | ||||||
|  | 	font-size: 12px; | ||||||
|  | 	color: var(--dateLabelFg); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .date-1 { | ||||||
|  | 	margin-right: 8px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .date-1-icon { | ||||||
|  | 	margin-right: 8px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .date-2 { | ||||||
|  | 	margin-left: 8px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .date-2-icon { | ||||||
|  | 	margin-left: 8px; | ||||||
|  | } | ||||||
| </style> | </style> | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | @ -9,7 +9,16 @@ | ||||||
| 
 | 
 | ||||||
| 	<template #default="{ items: notes }"> | 	<template #default="{ items: notes }"> | ||||||
| 		<div :class="[$style.root, { [$style.noGap]: noGap }]"> | 		<div :class="[$style.root, { [$style.noGap]: noGap }]"> | ||||||
| 			<MkDateSeparatedList ref="notes" v-slot="{ item: note }" :items="notes" :direction="pagination.reversed ? 'up' : 'down'" :reversed="pagination.reversed" :no-gap="noGap" :ad="true" :class="$style.notes"> | 			<MkDateSeparatedList | ||||||
|  | 				ref="notes" | ||||||
|  | 				v-slot="{ item: note }" | ||||||
|  | 				:items="notes" | ||||||
|  | 				:direction="pagination.reversed ? 'up' : 'down'" | ||||||
|  | 				:reversed="pagination.reversed" | ||||||
|  | 				:no-gap="noGap" | ||||||
|  | 				:ad="true" | ||||||
|  | 				:class="$style.notes" | ||||||
|  | 			> | ||||||
| 				<XNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/> | 				<XNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/> | ||||||
| 			</MkDateSeparatedList> | 			</MkDateSeparatedList> | ||||||
| 		</div> | 		</div> | ||||||
|  |  | ||||||
|  | @ -15,14 +15,14 @@ | ||||||
| 
 | 
 | ||||||
| 	<div v-else ref="rootEl"> | 	<div v-else ref="rootEl"> | ||||||
| 		<div v-show="pagination.reversed && more" key="_more_" class="cxiknjgy _margin"> | 		<div v-show="pagination.reversed && more" key="_more_" class="cxiknjgy _margin"> | ||||||
| 			<MkButton v-if="!moreFetching" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMoreAhead"> | 			<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore"> | ||||||
| 				{{ i18n.ts.loadMore }} | 				{{ i18n.ts.loadMore }} | ||||||
| 			</MkButton> | 			</MkButton> | ||||||
| 			<MkLoading v-else class="loading"/> | 			<MkLoading v-else class="loading"/> | ||||||
| 		</div> | 		</div> | ||||||
| 		<slot :items="items"></slot> | 		<slot :items="items" :fetching="fetching || moreFetching"></slot> | ||||||
| 		<div v-show="!pagination.reversed && more" key="_more_" class="cxiknjgy _margin"> | 		<div v-show="!pagination.reversed && more" key="_more_" class="cxiknjgy _margin"> | ||||||
| 			<MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore"> | 			<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore"> | ||||||
| 				{{ i18n.ts.loadMore }} | 				{{ i18n.ts.loadMore }} | ||||||
| 			</MkButton> | 			</MkButton> | ||||||
| 			<MkLoading v-else class="loading"/> | 			<MkLoading v-else class="loading"/> | ||||||
|  | @ -31,15 +31,18 @@ | ||||||
| </Transition> | </Transition> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts"> | ||||||
| import { computed, ComputedRef, isRef, markRaw, onActivated, onDeactivated, Ref, ref, shallowRef, watch } from 'vue'; | import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue'; | ||||||
| import * as misskey from 'misskey-js'; | import * as misskey from 'misskey-js'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from '@/scripts/scroll'; | import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@/scripts/scroll'; | ||||||
| import MkButton from '@/components/MkButton.vue'; | import MkButton from '@/components/MkButton.vue'; | ||||||
|  | import { defaultStore } from '@/store'; | ||||||
|  | import { MisskeyEntity } from '@/types/date-separated-list'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
| 
 | 
 | ||||||
| const SECOND_FETCH_LIMIT = 30; | const SECOND_FETCH_LIMIT = 30; | ||||||
|  | const TOLERANCE = 16; | ||||||
| 
 | 
 | ||||||
| export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints> = { | export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints> = { | ||||||
| 	endpoint: E; | 	endpoint: E; | ||||||
|  | @ -58,8 +61,11 @@ export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints> | ||||||
| 	reversed?: boolean; | 	reversed?: boolean; | ||||||
| 
 | 
 | ||||||
| 	offsetMode?: boolean; | 	offsetMode?: boolean; | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
|  | 	pageEl?: HTMLElement; | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  | <script lang="ts" setup> | ||||||
| const props = withDefaults(defineProps<{ | const props = withDefaults(defineProps<{ | ||||||
| 	pagination: Paging; | 	pagination: Paging; | ||||||
| 	disableAutoLoad?: boolean; | 	disableAutoLoad?: boolean; | ||||||
|  | @ -72,21 +78,73 @@ const emit = defineEmits<{ | ||||||
| 	(ev: 'queue', count: number): void; | 	(ev: 'queue', count: number): void; | ||||||
| }>(); | }>(); | ||||||
| 
 | 
 | ||||||
| type Item = { id: string; [another: string]: unknown; }; | let rootEl = $shallowRef<HTMLElement>(); | ||||||
| 
 | 
 | ||||||
| const rootEl = shallowRef<HTMLElement>(); | // 遡り中かどうか | ||||||
| const items = ref<Item[]>([]); | let backed = $ref(false); | ||||||
| const queue = ref<Item[]>([]); | 
 | ||||||
|  | let scrollRemove = $ref<(() => void) | null>(null); | ||||||
|  | 
 | ||||||
|  | const items = ref<MisskeyEntity[]>([]); | ||||||
|  | const queue = ref<MisskeyEntity[]>([]); | ||||||
| const offset = ref(0); | const offset = ref(0); | ||||||
| const fetching = ref(true); | const fetching = ref(true); | ||||||
| const moreFetching = ref(false); | const moreFetching = ref(false); | ||||||
| const more = ref(false); | const more = ref(false); | ||||||
| const backed = ref(false); // 遡り中か否か |  | ||||||
| const isBackTop = ref(false); | const isBackTop = ref(false); | ||||||
| const empty = computed(() => items.value.length === 0); | const empty = computed(() => items.value.length === 0); | ||||||
| const error = ref(false); | const error = ref(false); | ||||||
|  | const { | ||||||
|  | 	enableInfiniteScroll | ||||||
|  | } = defaultStore.reactiveState; | ||||||
| 
 | 
 | ||||||
| const init = async (): Promise<void> => { | const contentEl = $computed(() => props.pagination.pageEl || rootEl); | ||||||
|  | const scrollableElement = $computed(() => getScrollContainer(contentEl)); | ||||||
|  | 
 | ||||||
|  | // 先頭が表示されているかどうかを検出 | ||||||
|  | // 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; | ||||||
|  | 	}, { | ||||||
|  | 		root: scrollableElement, | ||||||
|  | 		rootMargin: props.pagination.reversed ? '-100% 0px 100% 0px' : '100% 0px -100% 0px', | ||||||
|  | 		threshold: 0.01, | ||||||
|  | 	}); | ||||||
|  | }, { immediate: true }); | ||||||
|  | 
 | ||||||
|  | watch($$(rootEl), () => { | ||||||
|  | 	scrollObserver.disconnect(); | ||||||
|  | 	nextTick(() => { | ||||||
|  | 		if (rootEl) scrollObserver.observe(rootEl); | ||||||
|  | 	}); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | watch([$$(backed), $$(contentEl)], () => { | ||||||
|  | 	if (!backed) { | ||||||
|  | 		if (!contentEl) return; | ||||||
|  | 
 | ||||||
|  | 		scrollRemove = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl, executeQueue, TOLERANCE); | ||||||
|  | 	} else { | ||||||
|  | 		if (scrollRemove) scrollRemove(); | ||||||
|  | 		scrollRemove = null; | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | 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 = []; | 	queue.value = []; | ||||||
| 	fetching.value = true; | 	fetching.value = true; | ||||||
| 	const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; | 	const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; | ||||||
|  | @ -96,18 +154,15 @@ const init = async (): Promise<void> => { | ||||||
| 	}).then(res => { | 	}).then(res => { | ||||||
| 		for (let i = 0; i < res.length; i++) { | 		for (let i = 0; i < res.length; i++) { | ||||||
| 			const item = res[i]; | 			const item = res[i]; | ||||||
| 			if (props.pagination.reversed) { | 			if (i === 3) item._shouldInsertAd_ = true; | ||||||
| 				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))) { | 		if (!props.pagination.noPaging && (res.length > (props.pagination.limit || 10))) { | ||||||
| 			res.pop(); | 			res.pop(); | ||||||
| 			items.value = props.pagination.reversed ? [...res].reverse() : res; | 			if (props.pagination.reversed) moreFetching.value = true; | ||||||
|  | 			items.value = res; | ||||||
| 			more.value = true; | 			more.value = true; | ||||||
| 		} else { | 		} else { | ||||||
| 			items.value = props.pagination.reversed ? [...res].reverse() : res; | 			items.value = res; | ||||||
| 			more.value = false; | 			more.value = false; | ||||||
| 		} | 		} | ||||||
| 		offset.value = res.length; | 		offset.value = res.length; | ||||||
|  | @ -117,17 +172,16 @@ const init = async (): Promise<void> => { | ||||||
| 		error.value = true; | 		error.value = true; | ||||||
| 		fetching.value = false; | 		fetching.value = false; | ||||||
| 	}); | 	}); | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| const reload = (): void => { | const reload = (): Promise<void> => { | ||||||
| 	items.value = []; | 	items.value = []; | ||||||
| 	init(); | 	return init(); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const fetchMore = async (): Promise<void> => { | const fetchMore = async (): Promise<void> => { | ||||||
| 	if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return; | 	if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return; | ||||||
| 	moreFetching.value = true; | 	moreFetching.value = true; | ||||||
| 	backed.value = true; |  | ||||||
| 	const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; | 	const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; | ||||||
| 	await os.api(props.pagination.endpoint, { | 	await os.api(props.pagination.endpoint, { | ||||||
| 		...params, | 		...params, | ||||||
|  | @ -142,22 +196,52 @@ const fetchMore = async (): Promise<void> => { | ||||||
| 	}).then(res => { | 	}).then(res => { | ||||||
| 		for (let i = 0; i < res.length; i++) { | 		for (let i = 0; i < res.length; i++) { | ||||||
| 			const item = res[i]; | 			const item = res[i]; | ||||||
| 			if (props.pagination.reversed) { | 			if (i === 10) item._shouldInsertAd_ = true; | ||||||
| 				if (i === res.length - 9) item._shouldInsertAd_ = true; |  | ||||||
| 			} else { |  | ||||||
| 				if (i === 10) item._shouldInsertAd_ = true; |  | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
|  | 
 | ||||||
|  | 		const reverseConcat = _res => { | ||||||
|  | 			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 + (scrollableElement.scrollHeight - oldHeight), behavior: 'instant' }); | ||||||
|  | 				} else { | ||||||
|  | 					window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' }); | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				return nextTick(); | ||||||
|  | 			}); | ||||||
|  | 		}; | ||||||
|  | 
 | ||||||
| 		if (res.length > SECOND_FETCH_LIMIT) { | 		if (res.length > SECOND_FETCH_LIMIT) { | ||||||
| 			res.pop(); | 			res.pop(); | ||||||
| 			items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res); | 
 | ||||||
| 			more.value = true; | 			if (props.pagination.reversed) { | ||||||
|  | 				reverseConcat(res).then(() => { | ||||||
|  | 					more.value = true; | ||||||
|  | 					moreFetching.value = false; | ||||||
|  | 				}); | ||||||
|  | 			} else { | ||||||
|  | 				items.value = items.value.concat(res); | ||||||
|  | 				more.value = true; | ||||||
|  | 				moreFetching.value = false; | ||||||
|  | 			} | ||||||
| 		} else { | 		} else { | ||||||
| 			items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res); | 			if (props.pagination.reversed) { | ||||||
| 			more.value = false; | 				reverseConcat(res).then(() => { | ||||||
|  | 					more.value = false; | ||||||
|  | 					moreFetching.value = false; | ||||||
|  | 				}); | ||||||
|  | 			} else { | ||||||
|  | 				items.value = items.value.concat(res); | ||||||
|  | 				more.value = false; | ||||||
|  | 				moreFetching.value = false; | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 		offset.value += res.length; | 		offset.value += res.length; | ||||||
| 		moreFetching.value = false; |  | ||||||
| 	}, err => { | 	}, err => { | ||||||
| 		moreFetching.value = false; | 		moreFetching.value = false; | ||||||
| 	}); | 	}); | ||||||
|  | @ -180,10 +264,10 @@ const fetchMoreAhead = async (): Promise<void> => { | ||||||
| 	}).then(res => { | 	}).then(res => { | ||||||
| 		if (res.length > SECOND_FETCH_LIMIT) { | 		if (res.length > SECOND_FETCH_LIMIT) { | ||||||
| 			res.pop(); | 			res.pop(); | ||||||
| 			items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res); | 			items.value = items.value.concat(res); | ||||||
| 			more.value = true; | 			more.value = true; | ||||||
| 		} else { | 		} else { | ||||||
| 			items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res); | 			items.value = items.value.concat(res); | ||||||
| 			more.value = false; | 			more.value = false; | ||||||
| 		} | 		} | ||||||
| 		offset.value += res.length; | 		offset.value += res.length; | ||||||
|  | @ -193,106 +277,96 @@ const fetchMoreAhead = async (): Promise<void> => { | ||||||
| 	}); | 	}); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const prepend = (item: Item): void => { | const prepend = (item: MisskeyEntity): void => { | ||||||
| 	if (props.pagination.reversed) { | 	// 初回表示時はunshiftだけでOK | ||||||
| 		if (rootEl.value) { | 	if (!rootEl) { | ||||||
| 			const container = getScrollContainer(rootEl.value); | 		items.value.unshift(item); | ||||||
| 			if (container == null) { | 		return; | ||||||
| 				// TODO? |  | ||||||
| 			} else { |  | ||||||
| 				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 { |  | ||||||
| 		// 初回表示時はunshiftだけでOK |  | ||||||
| 		if (!rootEl.value) { |  | ||||||
| 			items.value.unshift(item); |  | ||||||
| 			return; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		const isTop = isBackTop.value || (document.body.contains(rootEl.value) && isTopVisible(rootEl.value)); |  | ||||||
| 
 |  | ||||||
| 		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 isTop = isBackTop.value || (props.pagination.reversed ? isBottomVisible : isTopVisible)(contentEl, TOLERANCE); | ||||||
|  | 
 | ||||||
|  | 	if (isTop) unshiftItems([item]); | ||||||
|  | 	else prependQueue(item); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const append = (item: Item): void => { | function unshiftItems(newItems: MisskeyEntity[]) { | ||||||
|  | 	const length = newItems.length + items.value.length; | ||||||
|  | 	items.value = [ ...newItems, ...items.value ].slice(0, props.displayLimit); | ||||||
|  | 
 | ||||||
|  | 	if (length >= props.displayLimit) more.value = true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function executeQueue() { | ||||||
|  | 	if (queue.value.length === 0) return; | ||||||
|  | 	unshiftItems(queue.value); | ||||||
|  | 	queue.value = []; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function prependQueue(newItem: MisskeyEntity) { | ||||||
|  | 	queue.value.unshift(newItem); | ||||||
|  | 	if (queue.value.length >= props.displayLimit) { | ||||||
|  | 		queue.value.pop(); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const appendItem = (item: MisskeyEntity): void => { | ||||||
| 	items.value.push(item); | 	items.value.push(item); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const removeItem = (finder: (item: Item) => boolean) => { | const removeItem = (finder: (item: MisskeyEntity) => boolean) => { | ||||||
| 	const i = items.value.findIndex(finder); | 	const i = items.value.findIndex(finder); | ||||||
| 	items.value.splice(i, 1); | 	items.value.splice(i, 1); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| 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); | 	const i = items.value.findIndex(item => item.id === id); | ||||||
| 	items.value[i] = replacer(items.value[i]); | 	items.value[i] = replacer(items.value[i]); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| if (props.pagination.params && isRef(props.pagination.params)) { | const inited = init(); | ||||||
| 	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 }); |  | ||||||
| 
 |  | ||||||
| init(); |  | ||||||
| 
 | 
 | ||||||
| onActivated(() => { | onActivated(() => { | ||||||
| 	isBackTop.value = false; | 	isBackTop.value = false; | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| onDeactivated(() => { | onDeactivated(() => { | ||||||
| 	isBackTop.value = window.scrollY === 0; | 	isBackTop.value = props.pagination.reversed ? window.scrollY >= (rootEl ? rootEl?.scrollHeight - window.innerHeight : 0) : window.scrollY === 0; | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | function toBottom() { | ||||||
|  | 	scrollToBottom(contentEl); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | onMounted(() => { | ||||||
|  | 	inited.then(() => { | ||||||
|  | 		if (props.pagination.reversed) { | ||||||
|  | 			nextTick(() => { | ||||||
|  | 				setTimeout(toBottom, 800); | ||||||
|  | 
 | ||||||
|  | 				// scrollToBottomでmoreFetchingボタンが画面外まで出るまで | ||||||
|  | 				// more = trueを遅らせる | ||||||
|  | 				setTimeout(() => { | ||||||
|  | 					moreFetching.value = false; | ||||||
|  | 				}, 2000); | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | onBeforeUnmount(() => { | ||||||
|  | 	scrollObserver.disconnect(); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| defineExpose({ | defineExpose({ | ||||||
| 	items, | 	items, | ||||||
| 	queue, | 	queue, | ||||||
| 	backed, | 	backed, | ||||||
|  | 	more, | ||||||
|  | 	inited, | ||||||
| 	reload, | 	reload, | ||||||
| 	prepend, | 	prepend, | ||||||
| 	append, | 	append: appendItem, | ||||||
| 	removeItem, | 	removeItem, | ||||||
| 	updateItem, | 	updateItem, | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -10,7 +10,7 @@ | ||||||
| 					v-for="(message, i) in messages" | 					v-for="(message, i) in messages" | ||||||
| 					:key="message.id" | 					:key="message.id" | ||||||
| 					v-anim="i" | 					v-anim="i" | ||||||
| 					class="message" | 					class="message _panel" | ||||||
| 					:class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($i.id) : message.isRead }" | 					:class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($i.id) : message.isRead }" | ||||||
| 					:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`" | 					:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`" | ||||||
| 					:data-index="i" | 					:data-index="i" | ||||||
|  |  | ||||||
|  | @ -256,9 +256,10 @@ defineExpose({ | ||||||
| 		border: none; | 		border: none; | ||||||
| 		border-radius: 0; | 		border-radius: 0; | ||||||
| 		box-shadow: none; | 		box-shadow: none; | ||||||
| 		background: transparent; |  | ||||||
| 		box-sizing: border-box; | 		box-sizing: border-box; | ||||||
| 		color: var(--fg); | 		color: var(--fg); | ||||||
|  | 		background: rgba(12, 18, 16, 0.85); | ||||||
|  | 		backdrop-filter: var(--blur, blur(15px)); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	footer { | 	footer { | ||||||
|  |  | ||||||
|  | @ -1,51 +1,48 @@ | ||||||
| <template> | <template> | ||||||
| <div | <div | ||||||
| 	ref="rootEl" | 	ref="rootEl" | ||||||
| 	class="" | 	class="root" | ||||||
| 	@dragover.prevent.stop="onDragover" | 	@dragover.prevent.stop="onDragover" | ||||||
| 	@drop.prevent.stop="onDrop" | 	@drop.prevent.stop="onDrop" | ||||||
| > | > | ||||||
| 	<div class="mk-messaging-room"> | 	<div class="body"> | ||||||
| 		<div class="body"> | 		<MkPagination v-if="pagination" ref="pagingComponent" :key="userAcct || groupId" :pagination="pagination"> | ||||||
| 			<MkPagination v-if="pagination" ref="pagingComponent" :key="userAcct || groupId" :pagination="pagination"> | 			<template #empty> | ||||||
| 				<template #empty> | 				<div class="_fullinfo"> | ||||||
| 					<div class="_fullinfo"> | 					<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> | ||||||
| 						<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> | 					<div>{{ i18n.ts.noMessagesYet }}</div> | ||||||
| 						<div>{{ i18n.ts.noMessagesYet }}</div> |  | ||||||
| 					</div> |  | ||||||
| 				</template> |  | ||||||
| 
 |  | ||||||
| 				<template #default="{ items: messages, fetching: pFetching }"> |  | ||||||
| 					<MkDateSeparatedList |  | ||||||
| 						v-if="messages.length > 0" |  | ||||||
| 						v-slot="{ item: message }" |  | ||||||
| 						:class="{ messages: true, 'deny-move-transition': pFetching }" |  | ||||||
| 						:items="messages" |  | ||||||
| 						direction="up" |  | ||||||
| 						reversed |  | ||||||
| 					> |  | ||||||
| 						<XMessage :key="message.id" :message="message" :is-group="group != null"/> |  | ||||||
| 					</MkDateSeparatedList> |  | ||||||
| 				</template> |  | ||||||
| 			</MkPagination> |  | ||||||
| 		</div> |  | ||||||
| 		<footer> |  | ||||||
| 			<div v-if="typers.length > 0" class="typers"> |  | ||||||
| 				<I18n :src="i18n.ts.typingUsers" text-tag="span" class="users"> |  | ||||||
| 					<template #users> |  | ||||||
| 						<b v-for="typer in typers" :key="typer.id" class="user">{{ typer.username }}</b> |  | ||||||
| 					</template> |  | ||||||
| 				</I18n> |  | ||||||
| 				<MkEllipsis/> |  | ||||||
| 			</div> |  | ||||||
| 			<Transition :name="animation ? 'fade' : ''"> |  | ||||||
| 				<div v-show="showIndicator" class="new-message"> |  | ||||||
| 					<button class="_buttonPrimary" @click="onIndicatorClick"><i class="fas ti-fw fa-arrow-circle-down"></i>{{ i18n.ts.newMessageExists }}</button> |  | ||||||
| 				</div> | 				</div> | ||||||
| 			</Transition> | 			</template> | ||||||
| 			<XForm v-if="!fetching" ref="formEl" :user="user" :group="group" class="form"/> | 			<template #default="{ items: messages, fetching: pFetching }"> | ||||||
| 		</footer> | 				<MkDateSeparatedList | ||||||
|  | 					v-if="messages.length > 0" | ||||||
|  | 					v-slot="{ item: message }" | ||||||
|  | 					:class="{ messages: true, 'deny-move-transition': pFetching }" | ||||||
|  | 					:items="messages" | ||||||
|  | 					direction="up" | ||||||
|  | 					reversed | ||||||
|  | 				> | ||||||
|  | 					<XMessage :key="message.id" :message="message" :is-group="group != null"/> | ||||||
|  | 				</MkDateSeparatedList> | ||||||
|  | 			</template> | ||||||
|  | 		</MkPagination> | ||||||
| 	</div> | 	</div> | ||||||
|  | 	<footer> | ||||||
|  | 		<div v-if="typers.length > 0" class="typers"> | ||||||
|  | 			<I18n :src="i18n.ts.typingUsers" text-tag="span" class="users"> | ||||||
|  | 				<template #users> | ||||||
|  | 					<b v-for="typer in typers" :key="typer.id" class="user">{{ typer.username }}</b> | ||||||
|  | 				</template> | ||||||
|  | 			</I18n> | ||||||
|  | 			<MkEllipsis/> | ||||||
|  | 		</div> | ||||||
|  | 		<Transition :name="animation ? 'fade' : ''"> | ||||||
|  | 			<div v-show="showIndicator" class="new-message"> | ||||||
|  | 				<button class="_buttonPrimary" @click="onIndicatorClick"><i class="fas ti-fw fa-arrow-circle-down"></i>{{ i18n.ts.newMessageExists }}</button> | ||||||
|  | 			</div> | ||||||
|  | 		</Transition> | ||||||
|  | 		<XForm v-if="!fetching" ref="formEl" :user="user" :group="group" class="form"/> | ||||||
|  | 	</footer> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | @ -140,7 +137,9 @@ async function fetch() { | ||||||
| 	document.addEventListener('visibilitychange', onVisibilitychange); | 	document.addEventListener('visibilitychange', onVisibilitychange); | ||||||
| 
 | 
 | ||||||
| 	nextTick(() => { | 	nextTick(() => { | ||||||
| 		thisScrollToBottom(); | 		pagingComponent.inited.then(() => { | ||||||
|  | 			thisScrollToBottom(); | ||||||
|  | 		}); | ||||||
| 		window.setTimeout(() => { | 		window.setTimeout(() => { | ||||||
| 			fetching = false; | 			fetching = false; | ||||||
| 		}, 300); | 		}, 300); | ||||||
|  | @ -305,11 +304,12 @@ definePageMetadata(computed(() => !fetching ? user ? { | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .mk-messaging-room { | .root { | ||||||
| 	position: relative; | 	display: content; | ||||||
| 	overflow: auto; |  | ||||||
| 
 | 
 | ||||||
| 	> .body { | 	> .body { | ||||||
|  | 		min-height: 80%; | ||||||
|  | 
 | ||||||
| 		.more { | 		.more { | ||||||
| 			display: block; | 			display: block; | ||||||
| 			margin: 16px auto; | 			margin: 16px auto; | ||||||
|  | @ -349,8 +349,9 @@ definePageMetadata(computed(() => !fetching ? user ? { | ||||||
| 		width: 100%; | 		width: 100%; | ||||||
| 		position: sticky; | 		position: sticky; | ||||||
| 		z-index: 2; | 		z-index: 2; | ||||||
| 		bottom: 0; |  | ||||||
| 		padding-top: 8px; | 		padding-top: 8px; | ||||||
|  | 		bottom: 0; | ||||||
|  | 		bottom: env(safe-area-inset-bottom, 0px); | ||||||
| 
 | 
 | ||||||
| 		> .new-message { | 		> .new-message { | ||||||
| 			width: 100%; | 			width: 100%; | ||||||
|  | @ -395,6 +396,8 @@ definePageMetadata(computed(() => !fetching ? user ? { | ||||||
| 			max-height: 12em; | 			max-height: 12em; | ||||||
| 			overflow-y: scroll; | 			overflow-y: scroll; | ||||||
| 			border-top: solid 0.5px var(--divider); | 			border-top: solid 0.5px var(--divider); | ||||||
|  | 			border-bottom-left-radius: 0; | ||||||
|  | 			border-bottom-right-radius: 0; | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -10,53 +10,67 @@ export function getScrollContainer(el: HTMLElement | null): HTMLElement | null { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function getScrollPosition(el: Element | null): number { | 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); | 	const container = getScrollContainer(el); | ||||||
| 	return container == null ? window.scrollY : container.scrollTop; | 	return container == null ? window.scrollY : container.scrollTop; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function isTopVisible(el: Element | null): boolean { | export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance: number = 1, once: boolean = false) { | ||||||
| 	const scrollTop = getScrollPosition(el); | 	// とりあえず評価してみる
 | ||||||
| 	const topPosition = el.offsetTop; // TODO: container内でのelの相対位置を取得できればより正確になる
 | 	if (isTopVisible(el)) { | ||||||
|  | 		cb(); | ||||||
|  | 		if (once) return null; | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	return scrollTop <= topPosition; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export function isBottomVisible(el: HTMLElement, tolerance = 1, container = getScrollContainer(el)) { |  | ||||||
| 	if (container) return el.scrollHeight <= container.clientHeight + Math.abs(container.scrollTop) + tolerance; |  | ||||||
| 	return el.scrollHeight <= window.innerHeight + window.scrollY + tolerance; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export function onScrollTop(el: Element, cb) { |  | ||||||
| 	const container = getScrollContainer(el) || window; | 	const container = getScrollContainer(el) || window; | ||||||
|  | 
 | ||||||
| 	const onScroll = ev => { | 	const onScroll = ev => { | ||||||
| 		if (!document.body.contains(el)) return; | 		if (!document.body.contains(el)) return; | ||||||
| 		if (isTopVisible(el)) { | 		if (isTopVisible(el, tolerance)) { | ||||||
| 			cb(); | 			cb(); | ||||||
| 			container.removeEventListener('scroll', onScroll); | 			if (once) removeListener(); | ||||||
| 		} | 		} | ||||||
| 	}; | 	}; | ||||||
|  | 
 | ||||||
|  | 	function removeListener() { container.removeEventListener('scroll', onScroll); } | ||||||
| 	container.addEventListener('scroll', onScroll, { passive: true }); | 	container.addEventListener('scroll', onScroll, { passive: true }); | ||||||
|  | 	return removeListener; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function onScrollBottom(el: Element, cb) { | export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance: number = 1, once: boolean = false) { | ||||||
| 	const container = getScrollContainer(el) || window; | 	const container = getScrollContainer(el); | ||||||
|  | 
 | ||||||
|  | 	// とりあえず評価してみる
 | ||||||
|  | 	if (isBottomVisible(el, tolerance, container)) { | ||||||
|  | 		cb(); | ||||||
|  | 		if (once) return null; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	const containerOrWindow = container || window; | ||||||
| 	const onScroll = ev => { | 	const onScroll = ev => { | ||||||
| 		if (!document.body.contains(el)) return; | 		if (!document.body.contains(el)) return; | ||||||
| 		const pos = getScrollPosition(el); | 		if (isBottomVisible(el, 1, container)) { | ||||||
| 		if (pos + el.clientHeight > el.scrollHeight - 1) { |  | ||||||
| 			cb(); | 			cb(); | ||||||
| 			container.removeEventListener('scroll', onScroll); | 			if (once) removeListener(); | ||||||
| 		} | 		} | ||||||
| 	}; | 	}; | ||||||
| 	container.addEventListener('scroll', onScroll, { passive: true }); | 
 | ||||||
|  | 	function removeListener() { | ||||||
|  | 		containerOrWindow.removeEventListener('scroll', onScroll); | ||||||
|  | 	} | ||||||
|  | 	containerOrWindow.addEventListener('scroll', onScroll, { passive: true }); | ||||||
|  | 	return removeListener; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function scroll(el: Element, options: { | export function scroll(el: HTMLElement, options: ScrollToOptions | undefined) { | ||||||
| 	top?: number; |  | ||||||
| 	left?: number; |  | ||||||
| 	behavior?: ScrollBehavior; |  | ||||||
| }) { |  | ||||||
| 	const container = getScrollContainer(el); | 	const container = getScrollContainer(el); | ||||||
| 	if (container == null) { | 	if (container == null) { | ||||||
| 		window.scroll(options); | 		window.scroll(options); | ||||||
|  | @ -65,21 +79,51 @@ export function scroll(el: Element, options: { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function scrollToTop(el: Element, options: { behavior?: ScrollBehavior; } = {}) { | /** | ||||||
|  |  * Scroll to Top | ||||||
|  |  * @param el Scroll container element | ||||||
|  |  * @param options Scroll options | ||||||
|  |  */ | ||||||
|  | export function scrollToTop(el: HTMLElement, options: { behavior?: ScrollBehavior; } = {}) { | ||||||
| 	scroll(el, { top: 0, ...options }); | 	scroll(el, { top: 0, ...options }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function scrollToBottom(el: Element, options: { behavior?: ScrollBehavior; } = {}) { | /** | ||||||
| 	scroll(el, { top: 99999, ...options }); // TODO: ちゃんと計算する
 |  * Scroll to Bottom | ||||||
|  |  * @param el Content element | ||||||
|  |  * @param options Scroll options | ||||||
|  |  * @param container Scroll container element | ||||||
|  |  */ | ||||||
|  | export function scrollToBottom( | ||||||
|  | 	el: HTMLElement, | ||||||
|  | 	options: ScrollToOptions = {}, | ||||||
|  | 	container = getScrollContainer(el), | ||||||
|  | ) { | ||||||
|  | 	if (container) { | ||||||
|  | 		container.scroll({ top: el.scrollHeight - container.clientHeight + getStickyTop(el, container) || 0, ...options }); | ||||||
|  | 	} else { | ||||||
|  | 		window.scroll({ | ||||||
|  | 			top: (el.scrollHeight - window.innerHeight + getStickyTop(el, container) + (window.innerWidth <= 500 ? 96 : 0)) || 0, | ||||||
|  | 			...options | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function isBottom(el: Element, asobi = 0) { | export function isTopVisible(el: HTMLElement, tolerance: number = 1): boolean { | ||||||
| 	const container = getScrollContainer(el); | 	const scrollTop = getScrollPosition(el); | ||||||
| 	const current = container | 	return scrollTop <= tolerance; | ||||||
| 		? el.scrollTop + el.offsetHeight | } | ||||||
| 		: window.scrollY + window.innerHeight; | 
 | ||||||
| 	const max = container | export function isBottomVisible(el: HTMLElement, tolerance = 1, container = getScrollContainer(el)) { | ||||||
| 		? el.scrollHeight | 	if (container) return el.scrollHeight <= container.clientHeight + Math.abs(container.scrollTop) + tolerance; | ||||||
| 		: document.body.offsetHeight; | 	return el.scrollHeight <= window.innerHeight + window.scrollY + tolerance; | ||||||
| 	return current >= (max - 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 | ||||||
|  | 	); | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										6
									
								
								packages/frontend/src/types/date-separated-list.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								packages/frontend/src/types/date-separated-list.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | ||||||
|  | export type MisskeyEntity = { | ||||||
|  | 	id: string; | ||||||
|  | 	createdAt: string; | ||||||
|  | 	_shouldInsertAd_?: boolean; | ||||||
|  | 	[x: string]: any; | ||||||
|  | }; | ||||||
|  | @ -37,12 +37,11 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent, defineAsyncComponent } from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import XHeader from './header.vue'; | import XHeader from './header.vue'; | ||||||
| import { host, instanceName } from '@/config'; | import { host, instanceName } from '@/config'; | ||||||
| import { search } from '@/scripts/search'; | import { search } from '@/scripts/search'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import MkPagination from '@/components/MkPagination.vue'; |  | ||||||
| import MkButton from '@/components/MkButton.vue'; | import MkButton from '@/components/MkButton.vue'; | ||||||
| import { ColdDeviceStorage } from '@/store'; | import { ColdDeviceStorage } from '@/store'; | ||||||
| import { mainRouter } from '@/router'; | import { mainRouter } from '@/router'; | ||||||
|  | @ -52,7 +51,6 @@ const DESKTOP_THRESHOLD = 1100; | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XHeader, | 		XHeader, | ||||||
| 		MkPagination, |  | ||||||
| 		MkButton, | 		MkButton, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue