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