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;
|
||||
|
||||
connection?.on('message', onMessage);
|
||||
connection?.on('read', onRead);
|
||||
connection?.on('deleted', onDeleted);
|
||||
connection?.on('typers', typers => {
|
||||
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 });
|
||||
|
||||
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…
Reference in a new issue