wip?
This commit is contained in:
		
							parent
							
								
									364ac37c0a
								
							
						
					
					
						commit
						a8af328e5b
					
				
					 6 changed files with 128 additions and 80 deletions
				
			
		|  | @ -93,13 +93,19 @@ export default defineComponent({ | |||
| 		return () => h( | ||||
| 			defaultStore.state.animation ? TransitionGroup : 'div', | ||||
| 			defaultStore.state.animation ? { | ||||
| 					class: 'sqadhkmv' + (props.noGap ? ' noGap' : ''), | ||||
| 					class: { | ||||
| 						'sqadhkmv': true, | ||||
| 						'noGap': props.noGap | ||||
| 					}, | ||||
| 					name: 'list', | ||||
| 					tag: 'div', | ||||
| 					'data-direction': props.direction, | ||||
| 					'data-reversed': props.reversed ? 'true' : 'false', | ||||
| 				} : { | ||||
| 					class: 'sqadhkmv' + (props.noGap ? ' noGap' : ''), | ||||
| 					class: { | ||||
| 						'sqadhkmv': true, | ||||
| 						'noGap': props.noGap | ||||
| 					}, | ||||
| 				}, | ||||
| 			{ default: renderChildren }); | ||||
| 	} | ||||
|  | @ -117,24 +123,30 @@ export default defineComponent({ | |||
| 	> *:not(:last-child) { | ||||
| 		margin-bottom: var(--margin); | ||||
| 	} | ||||
| 
 | ||||
| 	> .list-move { | ||||
| 	 | ||||
| 	&:not(.deny-move-transition) > * { | ||||
| 		transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1); | ||||
| 	} | ||||
| 
 | ||||
| 	> .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-active { | ||||
| 		position: absolute; | ||||
| 	} | ||||
| 
 | ||||
| 	&[data-direction="up"] { | ||||
| 		> .list-enter-from { | ||||
| 		> .list-enter-from, | ||||
| 		> .list-leave-to { | ||||
| 			opacity: 0; | ||||
| 			transform: translateY(64px); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	&[data-direction="down"] { | ||||
| 		> .list-enter-from { | ||||
| 		> .list-enter-from, | ||||
| 		> .list-leave-to { | ||||
| 			opacity: 0; | ||||
| 			transform: translateY(-64px); | ||||
| 		} | ||||
|  |  | |||
|  | @ -15,14 +15,14 @@ | |||
| 
 | ||||
| 	<div v-else ref="rootEl"> | ||||
| 		<div v-if="pagination.reversed" v-show="more" key="_more_" class="cxiknjgy _gap"> | ||||
| 			<MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !props.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"> | ||||
| 				{{ $ts.loadMore }} | ||||
| 			</MkButton> | ||||
| 			<MkLoading v-else class="loading"/> | ||||
| 		</div> | ||||
| 		<slot :items="items"></slot> | ||||
| 		<slot :items="items" :fetching="fetching || moreFetching"></slot> | ||||
| 		<div v-if="!pagination.reversed" v-show="more" key="_more_" class="cxiknjgy _gap"> | ||||
| 			<MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !props.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"> | ||||
| 				{{ $ts.loadMore }} | ||||
| 			</MkButton> | ||||
| 			<MkLoading v-else class="loading"/> | ||||
|  | @ -31,12 +31,13 @@ | |||
| </transition> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| <script lang="ts"> | ||||
| import { computed, ComputedRef, isRef, markRaw, nextTick, onActivated, onDeactivated, onMounted, Ref, ref, watch } from 'vue'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import * as os from '@/os'; | ||||
| import { onScrollTop, isTopVisible, getScrollPosition, 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'; | ||||
| 
 | ||||
| const SECOND_FETCH_LIMIT = 30; | ||||
| 
 | ||||
|  | @ -58,9 +59,10 @@ export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints> | |||
| 
 | ||||
| 	offsetMode?: boolean; | ||||
| 
 | ||||
| 	pageEl?: Element; | ||||
| 	pageEl?: HTMLElement; | ||||
| }; | ||||
| 
 | ||||
| </script> | ||||
| <script lang="ts" setup> | ||||
| const props = withDefaults(defineProps<{ | ||||
| 	pagination: Paging; | ||||
| 	disableAutoLoad?: boolean; | ||||
|  | @ -86,8 +88,19 @@ const backed = ref(false); // 遡り中か否か | |||
| const isBackTop = ref(false); | ||||
| const empty = computed(() => items.value.length === 0); | ||||
| const error = ref(false); | ||||
| const { | ||||
| 	enableInfiniteScroll | ||||
| } = defaultStore.reactiveState; | ||||
| 
 | ||||
| const contentEl = $computed(() => props.pagination.pageEl || rootEl); | ||||
| const scrollableElement = $computed(() => { | ||||
| 	if (contentEl) { | ||||
| 		const container = getScrollContainer(contentEl); | ||||
| 		return container || contentEl; | ||||
| 	} | ||||
| 	return null; | ||||
| }); | ||||
| 
 | ||||
| 
 | ||||
| const init = async (): Promise<void> => { | ||||
| 	queue.value = []; | ||||
|  | @ -99,19 +112,15 @@ const init = async (): Promise<void> => { | |||
| 	}).then(res => { | ||||
| 		for (let i = 0; i < res.length; i++) { | ||||
| 			const item = res[i]; | ||||
| 			/*if (props.pagination.reversed) { | ||||
| 				if (i === res.length - 2) item._shouldInsertAd_ = true; | ||||
| 			} else {*/ | ||||
| 				if (i === 3) item._shouldInsertAd_ = true; | ||||
| 			/*}*/ | ||||
| 			if (i === 3) item._shouldInsertAd_ = true; | ||||
| 		} | ||||
| 		if (!props.pagination.noPaging && (res.length > (props.pagination.limit || 10))) { | ||||
| 			res.pop(); | ||||
| 			if (props.pagination.reversed) moreFetching.value = true; | ||||
| 			items.value = /*props.pagination.reversed ? [...res].reverse() : */res; | ||||
| 			items.value = res; | ||||
| 			more.value = true; | ||||
| 		} else { | ||||
| 			items.value = /*props.pagination.reversed ? [...res].reverse() : */res; | ||||
| 			items.value = res; | ||||
| 			more.value = false; | ||||
| 		} | ||||
| 		offset.value = res.length; | ||||
|  | @ -139,28 +148,57 @@ const fetchMore = async (): Promise<void> => { | |||
| 		...(props.pagination.offsetMode ? { | ||||
| 			offset: offset.value, | ||||
| 		} : { | ||||
| 			untilId: /*props.pagination.reversed ? items.value[0].id : */items.value[items.value.length - 1].id, | ||||
| 			untilId: items.value[items.value.length - 1].id, | ||||
| 		}), | ||||
| 	}).then(res => { | ||||
| 		for (let i = 0; i < res.length; i++) { | ||||
| 			const item = res[i]; | ||||
| 			/*if (props.pagination.reversed) { | ||||
| 				if (i === res.length - 9) item._shouldInsertAd_ = true; | ||||
| 			} else {*/ | ||||
| 				if (i === 10) item._shouldInsertAd_ = true; | ||||
| 			//} | ||||
| 			if (i === 10) item._shouldInsertAd_ = true; | ||||
| 		} | ||||
| 		const  | ||||
| 
 | ||||
| 		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.scrollY = oldScroll + (getBodyScrollHeight() - oldHeight); | ||||
| 				} | ||||
| 
 | ||||
| 				return nextTick(); | ||||
| 			}); | ||||
| 		}; | ||||
| 
 | ||||
| 		if (res.length > SECOND_FETCH_LIMIT) { | ||||
| 			res.pop(); | ||||
| 			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 { | ||||
| 			items.value = items.value.concat(res); | ||||
| 			more.value = false; | ||||
| 			if (props.pagination.reversed) { | ||||
| 				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; | ||||
| 		moreFetching.value = false; | ||||
| 	}, e => { | ||||
| 		moreFetching.value = false; | ||||
| 	}); | ||||
|  | @ -195,16 +233,13 @@ const fetchMoreAhead = async (): Promise<void> => { | |||
| }; | ||||
| 
 | ||||
| const prepend = (item: Item, force = false): void => { | ||||
| 	console.log('prepend', item) | ||||
| 	// 初回表示時はunshiftだけでOK | ||||
| 	if (!rootEl) { | ||||
| 		items.value.unshift(item); | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	const el = props.pagination.pageEl || rootEl; | ||||
| 	const isTop = isBackTop.value || (props.pagination.reversed ? isBottom : isTopVisible)(el); | ||||
| 	console.log(isTop || force) | ||||
| 	const isTop = isBackTop.value || (props.pagination.reversed ? isBottom : isTopVisible)(contentEl); | ||||
| 
 | ||||
| 	if (isTop || force) { | ||||
| 		// Prepend the item | ||||
|  | @ -221,7 +256,7 @@ const prepend = (item: Item, force = false): void => { | |||
| 		} | ||||
| 	} else { | ||||
| 		queue.value.push(item); | ||||
| 		(props.pagination.reversed ? onScrollBottom : onScrollTop)(el, () => { | ||||
| 		(props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl, () => { | ||||
| 			for (const item of queue.value) { | ||||
| 				prepend(item, true); | ||||
| 			} | ||||
|  | @ -258,16 +293,7 @@ onDeactivated(() => { | |||
| 	isBackTop.value = props.pagination.reversed ? window.scrollY >= (rootEl ? rootEl?.scrollHeight - window.innerHeight : 0) : window.scrollY === 0; | ||||
| }); | ||||
| 
 | ||||
| function getScrollableElement() { | ||||
| 	if (el) { | ||||
| 		const container = getScrollContainer(contentEl); | ||||
| 		return container || el; | ||||
| 	} | ||||
| 	return null; | ||||
| } | ||||
| 
 | ||||
| function toBottom() { | ||||
| 	const scrollableElement = getScrollableElement(); | ||||
| 	if (scrollableElement) scrollToBottom(scrollableElement); | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -247,8 +247,8 @@ export default defineComponent({ | |||
| 
 | ||||
| 	> .file { | ||||
| 		padding: 8px; | ||||
| 		color: #444; | ||||
| 		background: #eee; | ||||
| 		color: var(--fg); | ||||
| 		background: transparent; | ||||
| 		cursor: pointer; | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -14,8 +14,8 @@ | |||
| 					</div> | ||||
| 				</template> | ||||
| 
 | ||||
| 				<template #default="{ items: messages }"> | ||||
| 					<XList v-if="messages.length > 0" v-slot="{ item: message }" class="messages" :items="messages" direction="up" reversed> | ||||
| 				<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> | ||||
| 						<XMessage :key="message.id" :message="message" :is-group="group != null"/> | ||||
| 					</XList> | ||||
| 				</template> | ||||
|  | @ -30,12 +30,12 @@ | |||
| 				</I18n> | ||||
| 				<MkEllipsis/> | ||||
| 			</div> | ||||
| 			<transition :name="$store.state.animation ? 'fade' : ''"> | ||||
| 				<div v-show="showIndicator" class="new-message"> | ||||
| 					<button class="_buttonPrimary" @click="onIndicatorClick"><i class="fas fa-arrow-circle-down"></i>{{ i18n.locale.newMessageExists }}</button> | ||||
| 			<transition :name="animation ? 'fade' : ''"> | ||||
| 				<div class="new-message" v-if="showIndicator"> | ||||
| 					<button class="_buttonPrimary" @click="onIndicatorClick"><i class="fas fa-fw fa-arrow-circle-down"></i>{{ i18n.locale.newMessageExists }}</button> | ||||
| 				</div> | ||||
| 			</transition> | ||||
| 			<XForm v-if="!fetching" ref="form" :user="user" :group="group" class="form"/> | ||||
| 			<XForm v-if="!fetching" ref="formEl" :user="user" :group="group" class="form"/> | ||||
| 		</footer> | ||||
| 	</div> | ||||
| </div> | ||||
|  | @ -46,8 +46,7 @@ import { computed, watch, onMounted, nextTick, onBeforeUnmount } from 'vue'; | |||
| import * as Misskey from 'misskey-js'; | ||||
| import * as Acct from 'misskey-js/built/acct'; | ||||
| import XList from '@/components/date-separated-list.vue'; | ||||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
| import { Paging } from '@/components/ui/pagination.vue'; | ||||
| import MkPagination, { Paging } from '@/components/ui/pagination.vue'; | ||||
| import XMessage from './messaging-room.message.vue'; | ||||
| import XForm from './messaging-room.form.vue'; | ||||
| import { isBottom, onScrollBottom, scroll } from '@/scripts/scroll'; | ||||
|  | @ -57,6 +56,7 @@ import * as sound from '@/scripts/sound'; | |||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { $i } from '@/account'; | ||||
| import { defaultStore } from '@/store'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	userAcct?: string; | ||||
|  | @ -64,7 +64,7 @@ const props = defineProps<{ | |||
| }>(); | ||||
| 
 | ||||
| let rootEl = $ref<Element>(); | ||||
| let form = $ref<InstanceType<typeof XForm>>(); | ||||
| let formEl = $ref<InstanceType<typeof XForm>>(); | ||||
| let pagingComponent = $ref<InstanceType<typeof MkPagination>>(); | ||||
| 
 | ||||
| let fetching = $ref(true); | ||||
|  | @ -73,7 +73,9 @@ let group: Misskey.entities.UserGroup | null = $ref(null); | |||
| let typers: Misskey.entities.User[] = $ref([]); | ||||
| let connection: Misskey.ChannelConnection<Misskey.Channels['messaging']> | null = $ref(null); | ||||
| let showIndicator = $ref(false); | ||||
| let timer: number | null = $ref(null); | ||||
| const { | ||||
| 	animation | ||||
| } = defaultStore.reactiveState; | ||||
| 
 | ||||
| let pagination: Paging | null = $ref(null); | ||||
| 
 | ||||
|  | @ -155,7 +157,7 @@ function onDrop(e: DragEvent): void { | |||
| 
 | ||||
| 	// ファイルだったら | ||||
| 	if (e.dataTransfer.files.length == 1) { | ||||
| 		form.upload(e.dataTransfer.files[0]); | ||||
| 		formEl.upload(e.dataTransfer.files[0]); | ||||
| 		return; | ||||
| 	} else if (e.dataTransfer.files.length > 1) { | ||||
| 		os.alert({ | ||||
|  | @ -169,7 +171,7 @@ function onDrop(e: DragEvent): void { | |||
| 	const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); | ||||
| 	if (driveFile != null && driveFile != '') { | ||||
| 		const file = JSON.parse(driveFile); | ||||
| 		form.file = file; | ||||
| 		formEl.file = file; | ||||
| 	} | ||||
| 	//#endregion | ||||
| } | ||||
|  | @ -234,6 +236,7 @@ function scrollToBottom() { | |||
| } | ||||
| 
 | ||||
| function onIndicatorClick() { | ||||
| 	showIndicator = false; | ||||
| 	scrollToBottom(); | ||||
| } | ||||
| 
 | ||||
|  | @ -243,11 +246,6 @@ function notifyNewMessage() { | |||
| 	onScrollBottom(rootEl, () => { | ||||
| 		showIndicator = false; | ||||
| 	}); | ||||
| 
 | ||||
| 	if (timer) window.clearTimeout(timer); | ||||
| 	timer = window.setTimeout(() => { | ||||
| 		showIndicator = false; | ||||
| 	}, 4000); | ||||
| } | ||||
| 
 | ||||
| function onVisibilitychange() { | ||||
|  | @ -323,10 +321,15 @@ defineExpose({ | |||
| 
 | ||||
| 	> footer { | ||||
| 		width: 100%; | ||||
| 		position: sticky; | ||||
| 		z-index: 2; | ||||
| 		bottom: 8px; | ||||
| 
 | ||||
| 		@media (max-width: 500px) { | ||||
| 			bottom: 100px; | ||||
| 		} | ||||
| 
 | ||||
| 		> .new-message { | ||||
| 			position: absolute; | ||||
| 			top: -48px; | ||||
| 			width: 100%; | ||||
| 			padding: 8px 0; | ||||
| 			text-align: center; | ||||
|  | @ -334,17 +337,14 @@ defineExpose({ | |||
| 			> button { | ||||
| 				display: inline-block; | ||||
| 				margin: 0; | ||||
| 				padding: 0 12px 0 30px; | ||||
| 				padding: 0 12px; | ||||
| 				line-height: 32px; | ||||
| 				font-size: 12px; | ||||
| 				border-radius: 16px; | ||||
| 
 | ||||
| 				> i { | ||||
| 					position: absolute; | ||||
| 					top: 0; | ||||
| 					left: 10px; | ||||
| 					line-height: 32px; | ||||
| 					font-size: 16px; | ||||
| 					display: inline-block; | ||||
| 					margin-right: 8px; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  |  | |||
|  | @ -71,7 +71,7 @@ export class Storage<T extends StateDef> { | |||
| 					} | ||||
| 					localStorage.setItem(this.keyForLocalStorage + '::cache::' + $i.id, JSON.stringify(cache)); | ||||
| 				}); | ||||
| 			}, 10); | ||||
| 			}, 1); | ||||
| 			// streamingのuser storage updateイベントを監視して更新
 | ||||
| 			connection?.on('registryUpdated', ({ scope, key, value }: { scope: string[], key: keyof T, value: T[typeof key]['default'] }) => { | ||||
| 				if (scope.length !== 2 || scope[0] !== 'client' || scope[1] !== this.key || this.state[key] === value) return; | ||||
|  |  | |||
|  | @ -34,15 +34,16 @@ export function onScrollTop(el: HTMLElement, cb) { | |||
| } | ||||
| 
 | ||||
| export function onScrollBottom(el: HTMLElement, cb) { | ||||
| 	const container = getScrollContainer(el) || window; | ||||
| 	const container = getScrollContainer(el); | ||||
| 	const containerOrWindow = container || window; | ||||
| 	const onScroll = ev => { | ||||
| 		if (!document.body.contains(el)) return; | ||||
| 		if (isBottom(el)) { | ||||
| 		if (isScrollBottom(container)) { | ||||
| 			cb(); | ||||
| 			container.removeEventListener('scroll', onScroll); | ||||
| 			containerOrWindow.removeEventListener('scroll', onScroll); | ||||
| 		} | ||||
| 	}; | ||||
| 	container.addEventListener('scroll', onScroll, { passive: true }); | ||||
| 	containerOrWindow.addEventListener('scroll', onScroll, { passive: true }); | ||||
| } | ||||
| 
 | ||||
| export function scroll(el: HTMLElement, options: { | ||||
|  | @ -79,10 +80,19 @@ export function scrollToBottom(el: HTMLElement, options: { behavior?: ScrollBeha | |||
| // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#determine_if_an_element_has_been_totally_scrolled
 | ||||
| export function isBottom(el: HTMLElement, asobi = 1) { | ||||
| 	const container = getScrollContainer(el); | ||||
| 	if (container) return container.scrollHeight - Math.abs(container.scrollTop) <= container.clientHeight - asobi; | ||||
| 	return isScrollBottom(container, 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 | ||||
| 	) - window.scrollY <= window.innerHeight - asobi; | ||||
| 	); | ||||
| } | ||||
| 
 | ||||
| export function isScrollBottom(container?: HTMLElement | null, asobi = 1) { | ||||
| 	if (container) return container.scrollHeight - Math.abs(container.scrollTop) <= container.clientHeight + asobi; | ||||
| 	return getBodyScrollHeight() - window.scrollY <= window.innerHeight + asobi; | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue