wip???
This commit is contained in:
		
							parent
							
								
									6f9ccf6b02
								
							
						
					
					
						commit
						364ac37c0a
					
				
					 6 changed files with 197 additions and 182 deletions
				
			
		|  | @ -108,6 +108,8 @@ export default defineComponent({ | |||
| 
 | ||||
| <style lang="scss"> | ||||
| .sqadhkmv { | ||||
| 	display: flex; | ||||
| 
 | ||||
| 	> *:empty { | ||||
| 		display: none; | ||||
| 	} | ||||
|  | @ -138,6 +140,14 @@ export default defineComponent({ | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	&[data-reversed="true"] { | ||||
| 		flex-direction: column-reverse; | ||||
| 	} | ||||
| 
 | ||||
| 	&[data-reversed="false"] { | ||||
| 		flex-direction: column; | ||||
| 	} | ||||
| 
 | ||||
| 	> .separator { | ||||
| 		text-align: center; | ||||
| 
 | ||||
|  |  | |||
|  | @ -14,9 +14,15 @@ | |||
| 	</div> | ||||
| 
 | ||||
| 	<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"> | ||||
| 				{{ $ts.loadMore }} | ||||
| 			</MkButton> | ||||
| 			<MkLoading v-else class="loading"/> | ||||
| 		</div> | ||||
| 		<slot :items="items"></slot> | ||||
| 		<div v-show="more" key="_more_" class="cxiknjgy _gap"> | ||||
| 			<MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore"> | ||||
| 		<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"> | ||||
| 				{{ $ts.loadMore }} | ||||
| 			</MkButton> | ||||
| 			<MkLoading v-else class="loading"/> | ||||
|  | @ -26,10 +32,10 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { computed, ComputedRef, isRef, markRaw, onActivated, onDeactivated, Ref, ref, watch } from 'vue'; | ||||
| 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 } from '@/scripts/scroll'; | ||||
| import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottom } from '@/scripts/scroll'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| 
 | ||||
| const SECOND_FETCH_LIMIT = 30; | ||||
|  | @ -51,6 +57,8 @@ export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints> | |||
| 	reversed?: boolean; | ||||
| 
 | ||||
| 	offsetMode?: boolean; | ||||
| 
 | ||||
| 	pageEl?: Element; | ||||
| }; | ||||
| 
 | ||||
| const props = withDefaults(defineProps<{ | ||||
|  | @ -67,7 +75,7 @@ const emit = defineEmits<{ | |||
| 
 | ||||
| type Item = { id: string; [another: string]: unknown; }; | ||||
| 
 | ||||
| const rootEl = ref<HTMLElement>(); | ||||
| const rootEl = $ref<HTMLElement>(); | ||||
| const items = ref<Item[]>([]); | ||||
| const queue = ref<Item[]>([]); | ||||
| const offset = ref(0); | ||||
|  | @ -79,6 +87,8 @@ const isBackTop = ref(false); | |||
| const empty = computed(() => items.value.length === 0); | ||||
| const error = ref(false); | ||||
| 
 | ||||
| const contentEl = $computed(() => props.pagination.pageEl || rootEl); | ||||
| 
 | ||||
| const init = async (): Promise<void> => { | ||||
| 	queue.value = []; | ||||
| 	fetching.value = true; | ||||
|  | @ -89,18 +99,19 @@ const init = async (): Promise<void> => { | |||
| 	}).then(res => { | ||||
| 		for (let i = 0; i < res.length; i++) { | ||||
| 			const item = res[i]; | ||||
| 			if (props.pagination.reversed) { | ||||
| 			/*if (props.pagination.reversed) { | ||||
| 				if (i === res.length - 2) item._shouldInsertAd_ = true; | ||||
| 			} else { | ||||
| 			} else {*/ | ||||
| 				if (i === 3) item._shouldInsertAd_ = true; | ||||
| 			} | ||||
| 			/*}*/ | ||||
| 		} | ||||
| 		if (!props.pagination.noPaging && (res.length > (props.pagination.limit || 10))) { | ||||
| 			res.pop(); | ||||
| 			items.value = props.pagination.reversed ? [...res].reverse() : res; | ||||
| 			if (props.pagination.reversed) moreFetching.value = true; | ||||
| 			items.value = /*props.pagination.reversed ? [...res].reverse() : */res; | ||||
| 			more.value = true; | ||||
| 		} else { | ||||
| 			items.value = props.pagination.reversed ? [...res].reverse() : res; | ||||
| 			items.value = /*props.pagination.reversed ? [...res].reverse() : */res; | ||||
| 			more.value = false; | ||||
| 		} | ||||
| 		offset.value = res.length; | ||||
|  | @ -112,9 +123,9 @@ const init = async (): Promise<void> => { | |||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| const reload = (): void => { | ||||
| const reload = (): Promise<void> => { | ||||
| 	items.value = []; | ||||
| 	init(); | ||||
| 	return init(); | ||||
| }; | ||||
| 
 | ||||
| const fetchMore = async (): Promise<void> => { | ||||
|  | @ -128,23 +139,24 @@ 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: /*props.pagination.reversed ? items.value[0].id : */items.value[items.value.length - 1].id, | ||||
| 		}), | ||||
| 	}).then(res => { | ||||
| 		for (let i = 0; i < res.length; i++) { | ||||
| 			const item = res[i]; | ||||
| 			if (props.pagination.reversed) { | ||||
| 			/*if (props.pagination.reversed) { | ||||
| 				if (i === res.length - 9) item._shouldInsertAd_ = true; | ||||
| 			} else { | ||||
| 			} else {*/ | ||||
| 				if (i === 10) item._shouldInsertAd_ = true; | ||||
| 			} | ||||
| 			//} | ||||
| 		} | ||||
| 		const  | ||||
| 		if (res.length > SECOND_FETCH_LIMIT) { | ||||
| 			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; | ||||
| 		} else { | ||||
| 			items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res); | ||||
| 			items.value = items.value.concat(res); | ||||
| 			more.value = false; | ||||
| 		} | ||||
| 		offset.value += res.length; | ||||
|  | @ -164,15 +176,15 @@ const fetchMoreAhead = async (): Promise<void> => { | |||
| 		...(props.pagination.offsetMode ? { | ||||
| 			offset: offset.value, | ||||
| 		} : { | ||||
| 			sinceId: props.pagination.reversed ? items.value[0].id : items.value[items.value.length - 1].id, | ||||
| 			sinceId: /*props.pagination.reversed ? items.value[0].id : */items.value[items.value.length - 1].id, | ||||
| 		}), | ||||
| 	}).then(res => { | ||||
| 		if (res.length > SECOND_FETCH_LIMIT) { | ||||
| 			res.pop(); | ||||
| 			items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res); | ||||
| 			items.value = /*props.pagination.reversed ? [...res].reverse().concat(items.value) : */items.value.concat(res); | ||||
| 			more.value = true; | ||||
| 		} else { | ||||
| 			items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res); | ||||
| 			items.value = /*props.pagination.reversed ? [...res].reverse().concat(items.value) : */items.value.concat(res) ; | ||||
| 			more.value = false; | ||||
| 		} | ||||
| 		offset.value += res.length; | ||||
|  | @ -182,61 +194,39 @@ const fetchMoreAhead = async (): Promise<void> => { | |||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| const prepend = (item: Item): void => { | ||||
| 	if (props.pagination.reversed) { | ||||
| 		if (rootEl.value) { | ||||
| 			const container = getScrollContainer(rootEl.value); | ||||
| 			if (container == null) return; // TODO? | ||||
| const prepend = (item: Item, force = false): void => { | ||||
| 	console.log('prepend', item) | ||||
| 	// 初回表示時はunshiftだけでOK | ||||
| 	if (!rootEl) { | ||||
| 		items.value.unshift(item); | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 			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; | ||||
| 				} | ||||
| 	const el = props.pagination.pageEl || rootEl; | ||||
| 	const isTop = isBackTop.value || (props.pagination.reversed ? isBottom : isTopVisible)(el); | ||||
| 	console.log(isTop || force) | ||||
| 
 | ||||
| 	if (isTop || force) { | ||||
| 		// 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; | ||||
| 		} | ||||
| 		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; | ||||
| 		queue.value.push(item); | ||||
| 		(props.pagination.reversed ? onScrollBottom : onScrollTop)(el, () => { | ||||
| 			for (const item of queue.value) { | ||||
| 				prepend(item, true); | ||||
| 			} | ||||
| 		} else { | ||||
| 			queue.value.push(item); | ||||
| 			onScrollTop(rootEl.value, () => { | ||||
| 				for (const item of queue.value) { | ||||
| 					prepend(item); | ||||
| 				} | ||||
| 				queue.value = []; | ||||
| 			}); | ||||
| 		} | ||||
| 			queue.value = []; | ||||
| 		}); | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
|  | @ -258,20 +248,50 @@ watch(queue, (a, b) => { | |||
| 	emit('queue', queue.value.length); | ||||
| }, { deep: true }); | ||||
| 
 | ||||
| init(); | ||||
| const inited = init(); | ||||
| 
 | ||||
| onActivated(() => { | ||||
| 	isBackTop.value = false; | ||||
| }); | ||||
| 
 | ||||
| onDeactivated(() => { | ||||
| 	isBackTop.value = window.scrollY === 0; | ||||
| 	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); | ||||
| } | ||||
| 
 | ||||
| onMounted(() => { | ||||
| 	inited.then(() => { | ||||
| 		if (props.pagination.reversed) { | ||||
| 			nextTick(() => { | ||||
| 				setTimeout(toBottom, 800); | ||||
| 
 | ||||
| 				// scrollToBottomでmoreFetchingボタンが画面外まで出るまで | ||||
| 				// more = trueを遅らせる | ||||
| 				setTimeout(() => { | ||||
| 					moreFetching.value = false; | ||||
| 				}, 3000); | ||||
| 			}); | ||||
| 		} | ||||
| 	}); | ||||
| }) | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	items, | ||||
| 	backed, | ||||
| 	more, | ||||
| 	inited, | ||||
| 	reload, | ||||
| 	fetchMoreAhead, | ||||
| 	prepend, | ||||
|  |  | |||
|  | @ -52,7 +52,6 @@ const props = defineProps<{ | |||
| const isMe = $computed(() => props.message.userId === $i?.id); | ||||
| const urls = $computed(() => props.message.text ? extractUrlFromMfm(mfm.parse(props.message.text)) : []); | ||||
| 
 | ||||
| 
 | ||||
| function del() { | ||||
| 	os.api('messaging/messages/delete', { | ||||
| 		messageId: props.message.id | ||||
|  |  | |||
|  | @ -6,12 +6,15 @@ | |||
| > | ||||
| 	<div class="_content mk-messaging-room"> | ||||
| 		<div class="body"> | ||||
| 			<MkPagination v-if="pagination" ref="pagingComponent" :pagination="pagination"> | ||||
| 			<MkPagination ref="pagingComponent" :key="userAcct || groupId" v-if="pagination" :pagination="pagination"> | ||||
| 				<template #empty> | ||||
| 					<i class="fas fa-info-circle"></i>{{ $ts.noMessagesYet }}</p> | ||||
| 					<div class="_fullinfo"> | ||||
| 						<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> | ||||
| 						<div>{{ i18n.locale.noMessagesYet }}</div> | ||||
| 					</div> | ||||
| 				</template> | ||||
| 
 | ||||
| 				<template #defalut="{ items: messages }"> | ||||
| 				<template #default="{ items: messages }"> | ||||
| 					<XList v-if="messages.length > 0" v-slot="{ item: message }" class="messages" :items="messages" direction="up" reversed> | ||||
| 						<XMessage :key="message.id" :message="message" :is-group="group != null"/> | ||||
| 					</XList> | ||||
|  | @ -20,7 +23,7 @@ | |||
| 		</div> | ||||
| 		<footer> | ||||
| 			<div v-if="typers.length > 0" class="typers"> | ||||
| 				<I18n :src="$ts.typingUsers" text-tag="span" class="users"> | ||||
| 				<I18n :src="i18n.locale.typingUsers" text-tag="span" class="users"> | ||||
| 					<template #users> | ||||
| 						<b v-for="user in typers" :key="user.id" class="user">{{ user.username }}</b> | ||||
| 					</template> | ||||
|  | @ -29,7 +32,7 @@ | |||
| 			</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>{{ $ts.newMessageExists }}</button> | ||||
| 					<button class="_buttonPrimary" @click="onIndicatorClick"><i class="fas fa-arrow-circle-down"></i>{{ i18n.locale.newMessageExists }}</button> | ||||
| 				</div> | ||||
| 			</transition> | ||||
| 			<XForm v-if="!fetching" ref="form" :user="user" :group="group" class="form"/> | ||||
|  | @ -41,6 +44,7 @@ | |||
| <script lang="ts" setup> | ||||
| 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'; | ||||
|  | @ -49,11 +53,9 @@ import XForm from './messaging-room.form.vue'; | |||
| import { isBottom, onScrollBottom, scroll } from '@/scripts/scroll'; | ||||
| import * as os from '@/os'; | ||||
| import { stream } from '@/stream'; | ||||
| import { popout } from '@/scripts/popout'; | ||||
| import * as sound from '@/scripts/sound'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { defaultStore } from '@/store'; | ||||
| import { $i } from '@/account'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|  | @ -63,34 +65,17 @@ const props = defineProps<{ | |||
| 
 | ||||
| let rootEl = $ref<Element>(); | ||||
| let form = $ref<InstanceType<typeof XForm>>(); | ||||
| let loadMore = $ref<HTMLDivElement>(); | ||||
| let pagingComponent = $ref<InstanceType<typeof MkPagination>>(); | ||||
| 
 | ||||
| let fetching = $ref(true); | ||||
| let user: Misskey.entities.UserDetailed | null = $ref(null); | ||||
| 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); | ||||
| 
 | ||||
| let pagination: Paging = $computed(() => { | ||||
| 	return { | ||||
| 		endpoint: 'messaging/messages', | ||||
| 		limit: pagingComponent?.more ? 20 : 10, | ||||
| 		params: { | ||||
| 			userId: user ? user.id : undefined, | ||||
| 			groupId: group ? group.id : undefined, | ||||
| 		}, | ||||
| 		reversed: true, | ||||
| 	}; | ||||
| }); | ||||
| 
 | ||||
| const ilObserver = new IntersectionObserver( | ||||
| 	(entries) => entries.some((entry) => entry.isIntersecting) | ||||
| 		&& !fetching | ||||
| 		&& pagingComponent?.more | ||||
| 		&& fetchMoreMessages() | ||||
| ); | ||||
| let pagination: Paging | null = $ref(null); | ||||
| 
 | ||||
| watch([() => props.userAcct, () => props.groupId], () => { | ||||
| 	if (connection) connection.dispose(); | ||||
|  | @ -100,27 +85,55 @@ watch([() => props.userAcct, () => props.groupId], () => { | |||
| async function fetch() { | ||||
| 	fetching = true; | ||||
| 
 | ||||
| 	connection = stream.useChannel('messaging', { | ||||
| 		otherparty: user ? user.id : undefined, | ||||
| 		group: group ? group.id : undefined, | ||||
| 	}); | ||||
| 	if (props.userAcct) { | ||||
| 		user = await os.api('users/show', Acct.parse(props.userAcct)); | ||||
| 		group = null; | ||||
| 		 | ||||
| 		pagination = { | ||||
| 			endpoint: 'messaging/messages', | ||||
| 			params: { | ||||
| 				userId: user.id, | ||||
| 			}, | ||||
| 			reversed: true, | ||||
| 			pageEl: $$(rootEl), | ||||
| 		}; | ||||
| 		connection = stream.useChannel('messaging', { | ||||
| 			otherparty: user.id, | ||||
| 		}); | ||||
| 	} else { | ||||
| 		user = null; | ||||
| 		group = await os.api('users/groups/show', { groupId: props.groupId }); | ||||
| 
 | ||||
| 	connection?.on('message', onMessage); | ||||
| 	connection?.on('read', onRead); | ||||
| 	connection?.on('deleted', onDeleted); | ||||
| 	connection?.on('typers', typers => { | ||||
| 		pagination = { | ||||
| 			endpoint: 'messaging/messages', | ||||
| 			params: { | ||||
| 				groupId: group.id, | ||||
| 			}, | ||||
| 			reversed: true, | ||||
| 			pageEl: $$(rootEl), | ||||
| 		}; | ||||
| 		connection = stream.useChannel('messaging', { | ||||
| 			group: group.id, | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
| 	connection.on('message', onMessage); | ||||
| 	connection.on('read', onRead); | ||||
| 	connection.on('deleted', onDeleted); | ||||
| 	connection.on('typers', typers => { | ||||
| 		typers = typers.filter(u => u.id !== $i.id); | ||||
| 	}); | ||||
| 
 | ||||
| 	document.addEventListener('visibilitychange', onVisibilitychange); | ||||
| 
 | ||||
| 	pagingComponent.fetchMoreAhead().then(() => { | ||||
| 		scrollToBottom(); | ||||
| 
 | ||||
| 		// もっと見るの交差検知を発火させないためにfetchは | ||||
| 		// スクロールが終わるまでfalseにしておく | ||||
| 		// scrollendのようなイベントはないのでsetTimeoutで | ||||
| 		window.setTimeout(() => fetching = false, 300); | ||||
| 	nextTick(() => { | ||||
| 		pagingComponent.inited.then(() => { | ||||
| 			scrollToBottom(); | ||||
| 		}); | ||||
| 		window.setTimeout(() => { | ||||
| 			fetching = false | ||||
| 		}, 300); | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
|  | @ -161,19 +174,12 @@ function onDrop(e: DragEvent): void { | |||
| 	//#endregion | ||||
| } | ||||
| 
 | ||||
| function fetchMoreMessages() { | ||||
| 	fetching = true; | ||||
| 	pagingComponent.fetchMoreAhead().then(() => { | ||||
| 		fetching = false; | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| function onMessage(message) { | ||||
| 	sound.play('chat'); | ||||
| 
 | ||||
| 	const _isBottom = isBottom(rootEl, 64); | ||||
| 
 | ||||
| 	pagingComponent.append(message); | ||||
| 	pagingComponent.prepend(message); | ||||
| 	if (message.userId != $i.id && !document.hidden) { | ||||
| 		connection?.send('read', { | ||||
| 			id: message.id | ||||
|  | @ -219,12 +225,12 @@ function onRead(x) { | |||
| function onDeleted(id) { | ||||
| 	const msg = pagingComponent.items.find(m => m.id === id); | ||||
| 	if (msg) { | ||||
| 		pagingComponent.prepend(msg); | ||||
| 		pagingComponent.items = pagingComponent.items.filter(m => m.id !== msg.id); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| function scrollToBottom() { | ||||
| 	scroll(rootEl, { top: rootEl.offsetHeight }); | ||||
| 	scroll(rootEl, { top: rootEl.scrollHeight || 999999, behavior: "smooth" }); | ||||
| } | ||||
| 
 | ||||
| function onIndicatorClick() { | ||||
|  | @ -257,15 +263,11 @@ function onVisibilitychange() { | |||
| 
 | ||||
| onMounted(() => { | ||||
| 	fetch(); | ||||
| 	if (defaultStore.state.enableInfiniteScroll) { | ||||
| 		nextTick(() => ilObserver.observe(loadMore)); | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
| onBeforeUnmount(() => { | ||||
| 	connection?.dispose(); | ||||
| 	document.removeEventListener('visibilitychange', onVisibilitychange); | ||||
| 	ilObserver.disconnect(); | ||||
| }); | ||||
| 
 | ||||
| defineExpose({ | ||||
|  | @ -281,35 +283,10 @@ defineExpose({ | |||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .mk-messaging-room { | ||||
| 	position: relative; | ||||
| 
 | ||||
| 	> .body { | ||||
| 		> .empty { | ||||
| 			width: 100%; | ||||
| 			margin: 0; | ||||
| 			padding: 16px 8px 8px 8px; | ||||
| 			text-align: center; | ||||
| 			font-size: 0.8em; | ||||
| 			opacity: 0.5; | ||||
| 
 | ||||
| 			i { | ||||
| 				margin-right: 4px; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		> .no-history { | ||||
| 			display: block; | ||||
| 			margin: 0; | ||||
| 			padding: 16px; | ||||
| 			text-align: center; | ||||
| 			font-size: 0.8em; | ||||
| 			color: var(--messagingRoomInfo); | ||||
| 			opacity: 0.5; | ||||
| 
 | ||||
| 			i { | ||||
| 				margin-right: 4px; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		> .more { | ||||
| 		.more { | ||||
| 			display: block; | ||||
| 			margin: 16px auto; | ||||
| 			padding: 0 12px; | ||||
|  | @ -335,7 +312,9 @@ defineExpose({ | |||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		> .messages { | ||||
| 		.messages { | ||||
| 			padding-top: 8px; | ||||
| 
 | ||||
| 			> ::v-deep(*) { | ||||
| 				margin-bottom: 16px; | ||||
| 			} | ||||
|  | @ -344,7 +323,6 @@ defineExpose({ | |||
| 
 | ||||
| 	> footer { | ||||
| 		width: 100%; | ||||
| 		position: relative; | ||||
| 
 | ||||
| 		> .new-message { | ||||
| 			position: absolute; | ||||
|  |  | |||
|  | @ -71,7 +71,7 @@ export class Storage<T extends StateDef> { | |||
| 					} | ||||
| 					localStorage.setItem(this.keyForLocalStorage + '::cache::' + $i.id, JSON.stringify(cache)); | ||||
| 				}); | ||||
| 			}, 1); | ||||
| 			}, 10); | ||||
| 			// 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; | ||||
|  |  | |||
|  | @ -1,28 +1,27 @@ | |||
| type ScrollBehavior = 'auto' | 'smooth' | 'instant'; | ||||
| 
 | ||||
| export function getScrollContainer(el: Element | null): Element | null { | ||||
| 	if (el == null || el.tagName === 'BODY') return null; | ||||
| 	const overflow = window.getComputedStyle(el).getPropertyValue('overflow'); | ||||
| 	if (overflow.endsWith('auto')) { // xとyを個別に指定している場合、hidden auto みたいな値になる
 | ||||
| export function getScrollContainer(el: HTMLElement | null): HTMLElement | null { | ||||
| 	if (el == null) return null; | ||||
| 	if (el.scrollHeight > el.clientHeight) { | ||||
| 		return el; | ||||
| 	} else { | ||||
| 		return getScrollContainer(el.parentElement); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export function getScrollPosition(el: Element | null): number { | ||||
| export function getScrollPosition(el: HTMLElement | null): number { | ||||
| 	const container = getScrollContainer(el); | ||||
| 	return container == null ? window.scrollY : container.scrollTop; | ||||
| } | ||||
| 
 | ||||
| export function isTopVisible(el: Element | null): boolean { | ||||
| export function isTopVisible(el: HTMLElement | null): boolean { | ||||
| 	const scrollTop = getScrollPosition(el); | ||||
| 	const topPosition = el.offsetTop; // TODO: container内でのelの相対位置を取得できればより正確になる
 | ||||
| 
 | ||||
| 	return scrollTop <= topPosition; | ||||
| } | ||||
| 
 | ||||
| export function onScrollTop(el: Element, cb) { | ||||
| export function onScrollTop(el: HTMLElement, cb) { | ||||
| 	const container = getScrollContainer(el) || window; | ||||
| 	const onScroll = ev => { | ||||
| 		if (!document.body.contains(el)) return; | ||||
|  | @ -34,12 +33,11 @@ export function onScrollTop(el: Element, cb) { | |||
| 	container.addEventListener('scroll', onScroll, { passive: true }); | ||||
| } | ||||
| 
 | ||||
| export function onScrollBottom(el: Element, cb) { | ||||
| export function onScrollBottom(el: HTMLElement, cb) { | ||||
| 	const container = getScrollContainer(el) || window; | ||||
| 	const onScroll = ev => { | ||||
| 		if (!document.body.contains(el)) return; | ||||
| 		const pos = getScrollPosition(el); | ||||
| 		if (pos + el.clientHeight > el.scrollHeight - 1) { | ||||
| 		if (isBottom(el)) { | ||||
| 			cb(); | ||||
| 			container.removeEventListener('scroll', onScroll); | ||||
| 		} | ||||
|  | @ -47,7 +45,7 @@ export function onScrollBottom(el: Element, cb) { | |||
| 	container.addEventListener('scroll', onScroll, { passive: true }); | ||||
| } | ||||
| 
 | ||||
| export function scroll(el: Element, options: { | ||||
| export function scroll(el: HTMLElement, options: { | ||||
| 	top?: number; | ||||
| 	left?: number; | ||||
| 	behavior?: ScrollBehavior; | ||||
|  | @ -60,21 +58,31 @@ 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 }); | ||||
| } | ||||
| 
 | ||||
| export function scrollToBottom(el: Element, options: { behavior?: ScrollBehavior; } = {}) { | ||||
| 	scroll(el, { top: 99999, ...options }); // TODO: ちゃんと計算する
 | ||||
| /** | ||||
|  * Scroll to Bottom | ||||
|  * @param el Scroll container element | ||||
|  * @param options Scroll options | ||||
|  */ | ||||
| export function scrollToBottom(el: HTMLElement, options: { behavior?: ScrollBehavior; } = {}) { | ||||
| 	scroll(el, { top: el.scrollHeight, ...options }); // TODO: ちゃんと計算する
 | ||||
| } | ||||
| 
 | ||||
| export function isBottom(el: Element, asobi = 0) { | ||||
| // 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); | ||||
| 	const current = container | ||||
| 		? el.scrollTop + el.offsetHeight | ||||
| 		: window.scrollY + window.innerHeight; | ||||
| 	const max = container | ||||
| 		? el.scrollHeight | ||||
| 		: document.body.offsetHeight; | ||||
| 	return current >= (max - asobi); | ||||
| 	if (container) return container.scrollHeight - Math.abs(container.scrollTop) <= container.clientHeight - asobi; | ||||
| 	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; | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue