aaaaaaaaaaa
This commit is contained in:
parent
99d8172ae5
commit
90c6f15a72
20 changed files with 257 additions and 133 deletions
|
@ -1,13 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent, h, PropType, TransitionGroup } from 'vue';
|
||||
import { defineComponent, getCurrentInstance, h, markRaw, onMounted, PropType, TransitionGroup } from 'vue';
|
||||
import MkAd from '@/components/global/ad.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { defaultStore } from '@/store';
|
||||
import { MisskeyEntity } from '@/types/date-separated-list';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
items: {
|
||||
type: Array as PropType<{ id: string; createdAt: string; _shouldInsertAd_: boolean; }[]>,
|
||||
type: Array as PropType<MisskeyEntity[]>,
|
||||
required: true,
|
||||
},
|
||||
direction: {
|
||||
|
@ -30,9 +31,15 @@ export default defineComponent({
|
|||
required: false,
|
||||
default: false
|
||||
},
|
||||
itemsContainer: {
|
||||
type: Object as PropType<HTMLElement | null>,
|
||||
required: false,
|
||||
nullable: true,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
|
||||
setup(props, { slots, expose }) {
|
||||
setup(props, { slots, expose, emit }) {
|
||||
function getDateText(time: string) {
|
||||
const date = new Date(time).getDate();
|
||||
const month = new Date(time).getMonth() + 1;
|
||||
|
@ -90,6 +97,11 @@ export default defineComponent({
|
|||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
const el = getCurrentInstance()?.vnode.el;
|
||||
emit('update:itemsContainer', el ? markRaw(el) : null);
|
||||
});
|
||||
|
||||
function onBeforeLeave(el: HTMLElement) {
|
||||
el.style.top = `${el.offsetTop}px`;
|
||||
el.style.left = `${el.offsetLeft}px`;
|
||||
|
|
|
@ -1,71 +1,63 @@
|
|||
<template>
|
||||
<div ref="rootEl">
|
||||
<slot name="header"></slot>
|
||||
<div ref="bodyEl">
|
||||
<div ref="bodyEl" :data-sticky-container-header-height="headerHeight">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
autoSticky: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
autoSticky?: boolean;
|
||||
}>(), {
|
||||
autoSticky: false,
|
||||
})
|
||||
|
||||
setup(props, context) {
|
||||
const rootEl = ref<HTMLElement>(null);
|
||||
const bodyEl = ref<HTMLElement>(null);
|
||||
const rootEl = $ref<HTMLElement>();
|
||||
const bodyEl = $ref<HTMLElement>();
|
||||
|
||||
const calc = () => {
|
||||
const currentStickyTop = getComputedStyle(rootEl.value).getPropertyValue('--stickyTop') || '0px';
|
||||
let headerHeight: string | undefined = $ref();
|
||||
|
||||
const header = rootEl.value.children[0];
|
||||
if (header === bodyEl.value) {
|
||||
bodyEl.value.style.setProperty('--stickyTop', currentStickyTop);
|
||||
} else {
|
||||
bodyEl.value.style.setProperty('--stickyTop', `calc(${currentStickyTop} + ${header.offsetHeight}px)`);
|
||||
const calc = () => {
|
||||
const currentStickyTop = getComputedStyle(rootEl).getPropertyValue('--stickyTop') || '0px';
|
||||
|
||||
if (props.autoSticky) {
|
||||
header.style.setProperty('--stickyTop', currentStickyTop);
|
||||
header.style.position = 'sticky';
|
||||
header.style.top = 'var(--stickyTop)';
|
||||
header.style.zIndex = '1';
|
||||
}
|
||||
}
|
||||
};
|
||||
const header = rootEl.children[0] as HTMLElement;
|
||||
if (header === bodyEl) {
|
||||
bodyEl.style.setProperty('--stickyTop', currentStickyTop);
|
||||
} else {
|
||||
bodyEl.style.setProperty('--stickyTop', `calc(${currentStickyTop} + ${header.offsetHeight}px)`);
|
||||
headerHeight = header.offsetHeight.toString();
|
||||
|
||||
onMounted(() => {
|
||||
calc();
|
||||
if (props.autoSticky) {
|
||||
header.style.setProperty('--stickyTop', currentStickyTop);
|
||||
header.style.position = 'sticky';
|
||||
header.style.top = 'var(--stickyTop)';
|
||||
header.style.zIndex = '1';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
window.setTimeout(() => {
|
||||
calc();
|
||||
}, 100);
|
||||
});
|
||||
const observer = new MutationObserver(() => {
|
||||
window.setTimeout(() => {
|
||||
calc();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
observer.observe(rootEl.value, {
|
||||
attributes: false,
|
||||
childList: true,
|
||||
subtree: false,
|
||||
});
|
||||
onMounted(() => {
|
||||
calc();
|
||||
|
||||
onUnmounted(() => {
|
||||
observer.disconnect();
|
||||
});
|
||||
});
|
||||
observer.observe(rootEl, {
|
||||
attributes: false,
|
||||
childList: true,
|
||||
subtree: false,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
rootEl,
|
||||
bodyEl,
|
||||
};
|
||||
},
|
||||
onUnmounted(() => {
|
||||
observer.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -7,9 +7,19 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<template #default="{ items: notes }">
|
||||
<template #default="{ items: notes, itemsContainerWrapped }">
|
||||
<div class="giivymft" :class="{ noGap }">
|
||||
<XList ref="notes" v-slot="{ item: note }" :items="notes" :direction="pagination.reversed ? 'up' : 'down'" :reversed="pagination.reversed" :no-gap="noGap" :ad="true" class="notes">
|
||||
<XList
|
||||
ref="notes"
|
||||
v-slot="{ item: note }"
|
||||
:items="notes"
|
||||
:direction="pagination.reversed ? 'up' : 'down'"
|
||||
:reversed="pagination.reversed"
|
||||
:no-gap="noGap"
|
||||
:ad="true"
|
||||
v-model:itemsContainer="itemsContainerWrapped.v.value"
|
||||
class="notes"
|
||||
>
|
||||
<XNote :key="note._featuredId_ || note._prId_ || note.id" class="qtqtichx" :note="note"/>
|
||||
</XList>
|
||||
</div>
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<template #default="{ items: notifications }">
|
||||
<XList v-slot="{ item: notification }" class="elsfgstc" :items="notifications" :no-gap="true">
|
||||
<template #default="{ items: notifications, itemsContainerWrapped }">
|
||||
<XList v-slot="{ item: notification }" class="elsfgstc" :items="notifications" :no-gap="true" v-model:itemsContainer="itemsContainerWrapped.v.value">
|
||||
<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
|
||||
<XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/>
|
||||
</XList>
|
||||
|
@ -29,7 +29,7 @@ import { stream } from '@/stream';
|
|||
import { $i } from '@/account';
|
||||
|
||||
const props = defineProps<{
|
||||
includeTypes?: PropType<typeof notificationTypes[number][]>;
|
||||
includeTypes?: typeof notificationTypes[number][];
|
||||
unreadOnly?: boolean;
|
||||
}>();
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
</MkButton>
|
||||
<MkLoading v-else class="loading"/>
|
||||
</div>
|
||||
<slot :items="items" :fetching="fetching || moreFetching"></slot>
|
||||
<slot :items="items" :fetching="fetching || moreFetching" :itemsContainer="itemsContainer" :itemsContainerWrapped="itemsContainerWrapped"></slot>
|
||||
<div v-if="!pagination.reversed" v-show="more" key="_more_" class="cxiknjgy _gap">
|
||||
<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 }}
|
||||
|
@ -32,12 +32,13 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, ComputedRef, isRef, nextTick, onActivated, onDeactivated, onMounted, ref, watch } from 'vue';
|
||||
import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import * as os from '@/os';
|
||||
import { onScrollTop, isTopVisible, 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';
|
||||
import { MisskeyEntity } from '@/types/date-separated-list';
|
||||
|
||||
const SECOND_FETCH_LIMIT = 30;
|
||||
|
||||
|
@ -75,16 +76,40 @@ const emit = defineEmits<{
|
|||
(e: 'queue', count: number): void;
|
||||
}>();
|
||||
|
||||
type Item = { id: string; [another: string]: unknown; };
|
||||
let rootEl = $ref<HTMLElement>();
|
||||
|
||||
const rootEl = $ref<HTMLElement>();
|
||||
const items = ref<Item[]>([]);
|
||||
const queue = ref<Item[]>([]);
|
||||
/*
|
||||
* itemsContainer: itemsの実体DOMsの親コンテナ(=v-forの直上)のHTMLElement
|
||||
*
|
||||
* IntersectionObserverを使用してスクロールのパフォーマンスを向上させるため必要
|
||||
* この中の最初の要素を評価するので、順番を反転したり変えたりしてはいけない
|
||||
*
|
||||
* これがundefinedのままの場合はrootElにフォールバックする
|
||||
* つまりrootElがitemsの実体DOMsの親であるとする
|
||||
*
|
||||
* 自動ロードやストリーミングでの追加がなければあまり関係ない
|
||||
*/
|
||||
let itemsContainer = $ref<HTMLElement | null>();
|
||||
/*
|
||||
* date-separated-listとやり取りするために入れ子にしたオブジェクトを用意する
|
||||
* slotの中身から変数を直接書き込むことができないため
|
||||
*/
|
||||
const itemsContainerWrapped = { v: $$(itemsContainer) };
|
||||
|
||||
/*
|
||||
* 遡り中かどうか
|
||||
* = (itemsContainer || rootEl).children.item(0) が画面内に入ったかどうか
|
||||
*/
|
||||
let backed = $ref(false);
|
||||
|
||||
let scrollRemove: (() => void) | null = $ref(null);
|
||||
|
||||
const items = ref<MisskeyEntity[]>([]);
|
||||
const queue = ref<MisskeyEntity[]>([]);
|
||||
const offset = ref(0);
|
||||
const fetching = ref(true);
|
||||
const moreFetching = ref(false);
|
||||
const more = ref(false);
|
||||
const backed = ref(false); // 遡り中か否か
|
||||
const isBackTop = ref(false);
|
||||
const empty = computed(() => items.value.length === 0);
|
||||
const error = ref(false);
|
||||
|
@ -92,10 +117,57 @@ const {
|
|||
enableInfiniteScroll
|
||||
} = defaultStore.reactiveState;
|
||||
|
||||
let mounted = $ref(false);
|
||||
|
||||
const contentEl = $computed(() => props.pagination.pageEl || rootEl);
|
||||
const scrollableElement = $computed(() => getScrollContainer(contentEl));
|
||||
|
||||
const init = async (): Promise<void> => {
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
if (entries.some(entry => entry.isIntersecting)) {
|
||||
backed = false;
|
||||
} else {
|
||||
backed = true;
|
||||
}
|
||||
});
|
||||
|
||||
watch([items, $$(itemsContainer)], observeLatestElement);
|
||||
|
||||
function observeLatestElement() {
|
||||
observer.disconnect();
|
||||
nextTick(() => {
|
||||
if (!mounted) return;
|
||||
const latestEl = (itemsContainer || rootEl)?.children.item(0);
|
||||
if (latestEl) observer.observe(latestEl);
|
||||
});
|
||||
}
|
||||
|
||||
watch($$(backed), () => {
|
||||
if (!backed) {
|
||||
if (!contentEl) return;
|
||||
|
||||
scrollRemove = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl, () => {
|
||||
if (queue.value.length === 0) return;
|
||||
for (const item of queue.value) {
|
||||
prepend(item, true);
|
||||
}
|
||||
queue.value = [];
|
||||
});
|
||||
} else {
|
||||
if (scrollRemove) scrollRemove();
|
||||
scrollRemove = null;
|
||||
}
|
||||
});
|
||||
|
||||
if (props.pagination.params && isRef(props.pagination.params)) {
|
||||
watch(props.pagination.params, init, { deep: true });
|
||||
}
|
||||
|
||||
watch(queue, (a, b) => {
|
||||
if (a.length === 0 && b.length === 0) return;
|
||||
emit('queue', queue.value.length);
|
||||
}, { deep: true });
|
||||
|
||||
async function init(): Promise<void> {
|
||||
queue.value = [];
|
||||
fetching.value = true;
|
||||
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
||||
|
@ -133,7 +205,6 @@ const reload = (): Promise<void> => {
|
|||
const fetchMore = async (): Promise<void> => {
|
||||
if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return;
|
||||
moreFetching.value = true;
|
||||
backed.value = true;
|
||||
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
||||
await os.api(props.pagination.endpoint, {
|
||||
...params,
|
||||
|
@ -150,16 +221,16 @@ const fetchMore = async (): Promise<void> => {
|
|||
}
|
||||
|
||||
const reverseConcat = _res => {
|
||||
const oldHeight = contentEl.scrollHeight;
|
||||
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 + (contentEl.scrollHeight - oldHeight), behavior: 'instant' });
|
||||
scroll(scrollableElement, { top: oldScroll + (scrollableElement.scrollHeight - oldHeight), behavior: 'instant' });
|
||||
} else {
|
||||
window.scrollY = oldScroll + (contentEl.scrollHeight - oldHeight);
|
||||
window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' });
|
||||
}
|
||||
|
||||
return nextTick();
|
||||
|
@ -225,7 +296,7 @@ const fetchMoreAhead = async (): Promise<void> => {
|
|||
});
|
||||
};
|
||||
|
||||
const prepend = (item: Item, force = false): void => {
|
||||
const prepend = (item: MisskeyEntity, force = false): void => {
|
||||
// 初回表示時はunshiftだけでOK
|
||||
if (!rootEl) {
|
||||
items.value.unshift(item);
|
||||
|
@ -249,33 +320,18 @@ const prepend = (item: Item, force = false): void => {
|
|||
}
|
||||
} else {
|
||||
queue.value.push(item);
|
||||
(props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl, () => {
|
||||
for (const item of queue.value) {
|
||||
prepend(item, true);
|
||||
}
|
||||
queue.value = [];
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const append = (item: Item): void => {
|
||||
const append = (item: MisskeyEntity): void => {
|
||||
items.value.push(item);
|
||||
};
|
||||
|
||||
const updateItem = (id: Item['id'], replacer: (old: Item) => Item): void => {
|
||||
const updateItem = (id: MisskeyEntity['id'], replacer: (old: MisskeyEntity) => MisskeyEntity): void => {
|
||||
const i = items.value.findIndex(item => item.id === id);
|
||||
items.value[i] = replacer(items.value[i]);
|
||||
};
|
||||
|
||||
if (props.pagination.params && isRef(props.pagination.params)) {
|
||||
watch(props.pagination.params, init, { deep: true });
|
||||
}
|
||||
|
||||
watch(queue, (a, b) => {
|
||||
if (a.length === 0 && b.length === 0) return;
|
||||
emit('queue', queue.value.length);
|
||||
}, { deep: true });
|
||||
|
||||
const inited = init();
|
||||
|
||||
onActivated(() => {
|
||||
|
@ -291,6 +347,8 @@ function toBottom() {
|
|||
}
|
||||
|
||||
onMounted(() => {
|
||||
mounted = true;
|
||||
|
||||
inited.then(() => {
|
||||
if (props.pagination.reversed) {
|
||||
nextTick(() => {
|
||||
|
@ -304,7 +362,11 @@ onMounted(() => {
|
|||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
observer.disconnect();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
items,
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<template #default="{ items: users }">
|
||||
<div class="efvhhmdq">
|
||||
<template #default="{ items: users, itemsContainer }">
|
||||
<div class="efvhhmdq" ref="itemsContainer">
|
||||
<MkUserInfo v-for="user in users" :key="user.id" class="user" :user="user"/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -5,8 +5,10 @@ export default {
|
|||
//const query = binding.value;
|
||||
|
||||
const header = src.children[0];
|
||||
const body = src.children[1];
|
||||
const currentStickyTop = getComputedStyle(src).getPropertyValue('--stickyTop') || '0px';
|
||||
src.style.setProperty('--stickyTop', `calc(${currentStickyTop} + ${header.offsetHeight}px)`);
|
||||
if (body) body.dataset.stickyContainerHeaderHeight = header.offsetHeight.toString();
|
||||
header.style.setProperty('--stickyTop', currentStickyTop);
|
||||
header.style.position = 'sticky';
|
||||
header.style.top = 'var(--stickyTop)';
|
||||
|
|
|
@ -19,8 +19,8 @@
|
|||
</div>
|
||||
<MkPagination ref="emojisPaginationComponent" :pagination="pagination">
|
||||
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
|
||||
<template v-slot="{items}">
|
||||
<div class="ldhfsamy">
|
||||
<template v-slot="{items, itemsContainer}">
|
||||
<div class="ldhfsamy" ref="itemsContainer">
|
||||
<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
|
||||
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
||||
<div class="body">
|
||||
|
@ -45,8 +45,8 @@
|
|||
</FormSplit>
|
||||
<MkPagination :pagination="remotePagination">
|
||||
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
|
||||
<template v-slot="{items}">
|
||||
<div class="ldhfsamy">
|
||||
<template v-slot="{items, itemsContainer}">
|
||||
<div class="ldhfsamy" ref="itemsContainer">
|
||||
<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)">
|
||||
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
||||
<div class="body">
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<template #default="{ items }">
|
||||
<XList v-slot="{ item }" :items="items" :direction="'down'" :no-gap="false" :ad="false">
|
||||
<template #default="{ items, itemsContainerWrapped }">
|
||||
<XList v-slot="{ item }" :items="items" :direction="'down'" :no-gap="false" :ad="false" v-model:itemsContainer="itemsContainerWrapped.v.value">
|
||||
<XNote :key="item.id" :note="item.note" :class="$style.note"/>
|
||||
</XList>
|
||||
</template>
|
||||
|
|
|
@ -41,8 +41,8 @@
|
|||
</FormSplit>
|
||||
</div>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
|
||||
<div class="dqokceoi">
|
||||
<MkPagination v-slot="{items, itemsContainer}" ref="instances" :key="host + state" :pagination="pagination">
|
||||
<div class="dqokceoi" ref="itemsContainer">
|
||||
<MkA v-for="instance in items" :key="instance.id" class="instance" :to="`/instance-info/${instance.host}`">
|
||||
<div class="host"><img :src="instance.faviconUrl">{{ instance.host }}</div>
|
||||
<div class="table">
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
<div>{{ $ts.noFollowRequests }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot="{items}">
|
||||
<div class="mk-follow-requests">
|
||||
<template v-slot="{items, itemsContainer}">
|
||||
<div class="mk-follow-requests" ref="itemsContainer">
|
||||
<div v-for="req in items" :key="req.id" class="user _panel">
|
||||
<MkAvatar class="avatar" :user="req.follower" :show-indicator="true"/>
|
||||
<div class="body">
|
||||
|
|
|
@ -9,32 +9,32 @@
|
|||
<div v-if="tab === 'explore'">
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-clock"></i>{{ $ts.recentPosts }}</template>
|
||||
<MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disable-auto-load="true">
|
||||
<div class="vfpdbgtk">
|
||||
<MkPagination v-slot="{items, itemsContainer}" :pagination="recentPostsPagination" :disable-auto-load="true">
|
||||
<div class="vfpdbgtk" ref="itemsContainer">
|
||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-fire-alt"></i>{{ $ts.popularPosts }}</template>
|
||||
<MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disable-auto-load="true">
|
||||
<div class="vfpdbgtk">
|
||||
<MkPagination v-slot="{items, itemsContainer}" :pagination="popularPostsPagination" :disable-auto-load="true">
|
||||
<div class="vfpdbgtk" ref="itemsContainer">
|
||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</MkFolder>
|
||||
</div>
|
||||
<div v-else-if="tab === 'liked'">
|
||||
<MkPagination v-slot="{items}" :pagination="likedPostsPagination">
|
||||
<div class="vfpdbgtk">
|
||||
<MkPagination v-slot="{items, itemsContainer}" :pagination="likedPostsPagination">
|
||||
<div class="vfpdbgtk" ref="itemsContainer">
|
||||
<MkGalleryPostPreview v-for="like in items" :key="like.id" :post="like.post" class="post"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'my'">
|
||||
<MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="fas fa-plus"></i> {{ $ts.postToGallery }}</MkA>
|
||||
<MkPagination v-slot="{items}" :pagination="myPostsPagination">
|
||||
<div class="vfpdbgtk">
|
||||
<MkPagination v-slot="{items, itemsContainer}" :pagination="myPostsPagination">
|
||||
<div class="vfpdbgtk" ref="itemsContainer">
|
||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
|
|
|
@ -36,8 +36,8 @@
|
|||
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
|
||||
<MkContainer :max-height="300" :foldable="true" class="other">
|
||||
<template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template>
|
||||
<MkPagination v-slot="{items}" :pagination="otherPostsPagination">
|
||||
<div class="sdrarzaf">
|
||||
<MkPagination v-slot="{items, itemsContainer}" :pagination="otherPostsPagination">
|
||||
<div class="sdrarzaf" ref="itemsContainer">
|
||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
|
|
|
@ -14,8 +14,16 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
<template #default="{ items: messages, fetching, itemsContainerWrapped }">
|
||||
<XList
|
||||
v-if="messages.length > 0"
|
||||
v-slot="{ item: message }"
|
||||
:class="{ messages: true, 'deny-move-transition': fetching }"
|
||||
:items="messages"
|
||||
v-model:itemsContainer="itemsContainerWrapped.v.value"
|
||||
direction="up"
|
||||
reversed
|
||||
>
|
||||
<XMessage :key="message.id" :message="message" :is-group="group != null"/>
|
||||
</XList>
|
||||
</template>
|
||||
|
|
|
@ -13,8 +13,8 @@
|
|||
<FormSection>
|
||||
<template #label>{{ $ts.signinHistory }}</template>
|
||||
<MkPagination :pagination="pagination">
|
||||
<template v-slot="{items}">
|
||||
<div>
|
||||
<template v-slot="{items, itemsContainer}">
|
||||
<div ref="itemsContainer">
|
||||
<div v-for="item in items" :key="item.id" v-panel class="timnmucd">
|
||||
<header>
|
||||
<i v-if="item.success" class="fas fa-check icon succ"></i>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div>
|
||||
<MkPagination v-slot="{items}" ref="list" :pagination="type === 'following' ? followingPagination : followersPagination" class="mk-following-or-followers">
|
||||
<div class="users _isolated">
|
||||
<MkPagination v-slot="{items, itemsContainer}" ref="list" :pagination="type === 'following' ? followingPagination : followersPagination" class="mk-following-or-followers">
|
||||
<div class="users _isolated" ref="itemsContainer">
|
||||
<MkUserInfo v-for="user in items.map(x => type === 'following' ? x.followee : x.follower)" :key="user.id" class="user" :user="user"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div>
|
||||
<MkPagination v-slot="{items}" :pagination="pagination">
|
||||
<div class="jrnovfpt">
|
||||
<MkPagination v-slot="{items, itemsContainer}" :pagination="pagination">
|
||||
<div class="jrnovfpt" ref="itemsContainer">
|
||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
|
|
|
@ -12,20 +12,28 @@ export function getScrollContainer(el: HTMLElement | null): HTMLElement | null {
|
|||
}
|
||||
}
|
||||
|
||||
export function getStickyTop(el: HTMLElement, container: HTMLElement | null = null, top: number = 0) {
|
||||
if (!el.parentElement) return top;
|
||||
const data = el.dataset.stickyContainerHeaderHeight;
|
||||
const newTop = data ? Number(data) + top : top;
|
||||
if (el === container) return newTop;
|
||||
return getStickyTop(el.parentElement, container, newTop);
|
||||
}
|
||||
|
||||
export function getScrollPosition(el: HTMLElement | null): number {
|
||||
const container = getScrollContainer(el);
|
||||
return container == null ? window.scrollY : container.scrollTop;
|
||||
}
|
||||
|
||||
export function isTopVisible(el: HTMLElement): boolean {
|
||||
const scrollTop = getScrollPosition(el);
|
||||
const topPosition = el.offsetTop; // TODO: container内でのelの相対位置を取得できればより正確になる
|
||||
export function onScrollTop(el: HTMLElement, cb: Function) {
|
||||
// とりあえず評価してみる
|
||||
if (isTopVisible(el)) {
|
||||
cb();
|
||||
return null;
|
||||
}
|
||||
|
||||
return scrollTop <= topPosition;
|
||||
}
|
||||
|
||||
export function onScrollTop(el: HTMLElement, cb) {
|
||||
const container = getScrollContainer(el) || window;
|
||||
|
||||
const onScroll = ev => {
|
||||
if (!document.body.contains(el)) return;
|
||||
if (isTopVisible(el)) {
|
||||
|
@ -33,13 +41,21 @@ export function onScrollTop(el: HTMLElement, cb) {
|
|||
removeListener();
|
||||
}
|
||||
};
|
||||
|
||||
function removeListener() { container.removeEventListener('scroll', onScroll) }
|
||||
container.addEventListener('scroll', onScroll, { passive: true });
|
||||
return removeListener;
|
||||
}
|
||||
|
||||
export function onScrollBottom(el: HTMLElement, cb) {
|
||||
export function onScrollBottom(el: HTMLElement, cb: Function) {
|
||||
const container = getScrollContainer(el);
|
||||
|
||||
// とりあえず評価してみる
|
||||
if (isBottom(el, 1, container)) {
|
||||
cb();
|
||||
return null;
|
||||
}
|
||||
|
||||
const containerOrWindow = container || window;
|
||||
const onScroll = ev => {
|
||||
if (!document.body.contains(el)) return;
|
||||
|
@ -48,6 +64,7 @@ export function onScrollBottom(el: HTMLElement, cb) {
|
|||
removeListener();
|
||||
}
|
||||
};
|
||||
|
||||
function removeListener() { containerOrWindow.removeEventListener('scroll', onScroll) }
|
||||
containerOrWindow.addEventListener('scroll', onScroll, { passive: true });
|
||||
return removeListener;
|
||||
|
@ -76,16 +93,32 @@ export function scrollToTop(el: HTMLElement, options: { behavior?: ScrollBehavio
|
|||
* @param el Content element
|
||||
* @param options Scroll options
|
||||
* @param container Scroll container element
|
||||
* @param addSticky To add sticky-top or not
|
||||
*/
|
||||
export function scrollToBottom(el: HTMLElement, options: ScrollToOptions = {}, container = getScrollContainer(el)) {
|
||||
export function scrollToBottom(el: HTMLElement, options: ScrollToOptions = {}, container = getScrollContainer(el), addSticky: boolean = true) {
|
||||
const addStickyTop = addSticky ? getStickyTop(el, container) : 0;
|
||||
if (container) {
|
||||
container.scroll({ top: el.scrollHeight - container.clientHeight || 0, ...options });
|
||||
container.scroll({ top: el.scrollHeight - container.clientHeight + addStickyTop || 0, ...options });
|
||||
} else {
|
||||
window.scroll({ top: el.scrollHeight - window.innerHeight || 0, ...options });
|
||||
window.scroll({ top: el.scrollHeight - window.innerHeight + addStickyTop || 0, ...options });
|
||||
}
|
||||
}
|
||||
|
||||
export function isTopVisible(el: HTMLElement, asobi: number = 1): boolean {
|
||||
const scrollTop = getScrollPosition(el);
|
||||
return scrollTop <= asobi;
|
||||
}
|
||||
|
||||
export function isBottom(el: HTMLElement, asobi = 1, container = getScrollContainer(el)) {
|
||||
if (container) return el.scrollHeight <= container.clientHeight + Math.abs(container.scrollTop) + asobi;
|
||||
return el.scrollHeight <= window.innerHeight + window.scrollY + 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
|
||||
);
|
||||
}
|
||||
|
|
7
packages/client/src/types/date-separated-list.ts
Normal file
7
packages/client/src/types/date-separated-list.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export type MisskeyEntity = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
_shouldInsertAd_?:
|
||||
boolean;
|
||||
[x: string]: any;
|
||||
};
|
|
@ -44,7 +44,6 @@ import { defineComponent, defineAsyncComponent } from 'vue';
|
|||
import { host, instanceName } from '@/config';
|
||||
import { search } from '@/scripts/search';
|
||||
import * as os from '@/os';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import XHeader from './header.vue';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
|
@ -55,7 +54,6 @@ const DESKTOP_THRESHOLD = 1100;
|
|||
export default defineComponent({
|
||||
components: {
|
||||
XHeader,
|
||||
MkPagination,
|
||||
MkButton,
|
||||
},
|
||||
|
||||
|
|
Loading…
Reference in a new issue