wip?
This commit is contained in:
parent
364ac37c0a
commit
a8af328e5b
@ -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…
Reference in New Issue
Block a user