Compare commits
3 Commits
02511e28fb
...
8d4614a48d
Author | SHA1 | Date |
---|---|---|
Derek | 8d4614a48d | |
Derek | 1c91784062 | |
Derek | c4469d657c |
|
@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
ref="notes"
|
||||
v-slot="{ item: note }"
|
||||
:items="notes"
|
||||
:direction="pagination.reversed ? 'up' : 'down'"
|
||||
:direction="pagination.scrollAtTop ? 'up' : 'down'"
|
||||
:reversed="pagination.reversed"
|
||||
:noGap="noGap"
|
||||
:ad="true"
|
||||
|
|
|
@ -25,14 +25,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
|
||||
<div v-else ref="rootEl">
|
||||
<div v-show="pagination.reversed && more" key="_more_" class="_margin">
|
||||
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMoreAhead : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary rounded @click="fetchMoreAhead">
|
||||
<div v-show="pagination.scrollAtTop && more" key="_more_" class="_margin">
|
||||
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMore : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary rounded @click="fetchMore">
|
||||
{{ i18n.ts.loadMore }}
|
||||
</MkButton>
|
||||
<MkLoading v-else class="loading"/>
|
||||
</div>
|
||||
<slot :items="Array.from(items.values())" :fetching="fetching || moreFetching"></slot>
|
||||
<div v-show="!pagination.reversed && more" key="_more_" class="_margin">
|
||||
<div v-show="!pagination.scrollAtTop && more" key="_more_" class="_margin">
|
||||
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearFetchMore : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary rounded @click="fetchMore">
|
||||
{{ i18n.ts.loadMore }}
|
||||
</MkButton>
|
||||
|
@ -73,11 +73,18 @@ export type Paging<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints>
|
|||
*/
|
||||
reversed?: boolean;
|
||||
|
||||
/**
|
||||
* Fetch more items when scrolling towards the top of the page
|
||||
* rather than the bottom
|
||||
*/
|
||||
scrollAtTop?: boolean;
|
||||
|
||||
offsetMode?: boolean;
|
||||
|
||||
pageEl?: HTMLElement;
|
||||
};
|
||||
|
||||
|
||||
type MisskeyEntityMap = Map<string, MisskeyEntity>;
|
||||
|
||||
function arrayToEntries(entities: MisskeyEntity[]): [string, MisskeyEntity][] {
|
||||
|
@ -156,14 +163,14 @@ const BACKGROUND_PAUSE_WAIT_SEC = 10;
|
|||
// https://qiita.com/mkataigi/items/0154aefd2223ce23398e
|
||||
const scrollObserver = ref<IntersectionObserver>();
|
||||
|
||||
watch([() => props.pagination.reversed, scrollableElement], () => {
|
||||
watch([() => props.pagination.scrollAtTop, scrollableElement], () => {
|
||||
if (scrollObserver.value) scrollObserver.value.disconnect();
|
||||
|
||||
scrollObserver.value = new IntersectionObserver(entries => {
|
||||
backed.value = entries[0].isIntersecting;
|
||||
}, {
|
||||
root: scrollableElement.value,
|
||||
rootMargin: props.pagination.reversed ? '-100% 0px 100% 0px' : '100% 0px -100% 0px',
|
||||
rootMargin: props.pagination.scrollAtTop ? '-100% 0px 100% 0px' : '100% 0px -100% 0px',
|
||||
threshold: 0.01,
|
||||
});
|
||||
}, { immediate: true });
|
||||
|
@ -179,7 +186,7 @@ watch([backed, contentEl], () => {
|
|||
if (!backed.value) {
|
||||
if (!contentEl.value) return;
|
||||
|
||||
scrollRemove.value = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl.value, executeQueue, TOLERANCE);
|
||||
scrollRemove.value = (props.pagination.scrollAtTop ? onScrollBottom : onScrollTop)(contentEl.value, executeQueue, TOLERANCE);
|
||||
} else {
|
||||
if (scrollRemove.value) scrollRemove.value();
|
||||
scrollRemove.value = null;
|
||||
|
@ -199,6 +206,23 @@ watch(error, (n, o) => {
|
|||
emit('status', n);
|
||||
});
|
||||
|
||||
function reverseConcat(res): Promise<void> {
|
||||
const oldHeight = scrollableElement.value ? scrollableElement.value.scrollHeight : getBodyScrollHeight();
|
||||
const oldScroll = scrollableElement.value ? scrollableElement.value.scrollTop : window.scrollY;
|
||||
|
||||
items.value = concatMapWithArray(items.value, res);
|
||||
|
||||
return nextTick(() => {
|
||||
if (scrollableElement.value) {
|
||||
scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' });
|
||||
} else {
|
||||
window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' });
|
||||
}
|
||||
|
||||
return nextTick();
|
||||
});
|
||||
};
|
||||
|
||||
async function init(): Promise<void> {
|
||||
items.value = new Map();
|
||||
queue.value = new Map();
|
||||
|
@ -209,6 +233,23 @@ async function init(): Promise<void> {
|
|||
limit: props.pagination.limit ?? 10,
|
||||
allowPartial: true,
|
||||
}).then(res => {
|
||||
/* HACK: Sort to avoid order inconsitencies on next fetch
|
||||
|
||||
APIs default to sorting by id descending if no filters are provided
|
||||
However, if in "future" mode this must not be the case, as then our
|
||||
first chunk will be out of order.
|
||||
|
||||
Since filters can be provided in props.pagination.params, just
|
||||
assume that this chunk is in the wrong order to be safe.
|
||||
*/
|
||||
res.sort((a, b) => {
|
||||
if (props.pagination.reversed) {
|
||||
return a.id > b.id ? 1 : -1; // oldest -> newest
|
||||
} else {
|
||||
return a.id < b.id ? 1 : -1; // newest -> oldest
|
||||
}
|
||||
});
|
||||
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
const item = res[i];
|
||||
if (i === 3) item._shouldInsertAd_ = true;
|
||||
|
@ -218,7 +259,7 @@ async function init(): Promise<void> {
|
|||
concatItems(res);
|
||||
more.value = false;
|
||||
} else {
|
||||
if (props.pagination.reversed) moreFetching.value = true;
|
||||
if (props.pagination.scrollAtTop) moreFetching.value = true;
|
||||
concatItems(res);
|
||||
more.value = true;
|
||||
}
|
||||
|
@ -232,21 +273,18 @@ async function init(): Promise<void> {
|
|||
});
|
||||
}
|
||||
|
||||
const reload = (): Promise<void> => {
|
||||
return init();
|
||||
};
|
||||
|
||||
const fetchMore = async (): Promise<void> => {
|
||||
async function fetchMore(): Promise<void> {
|
||||
if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
|
||||
moreFetching.value = true;
|
||||
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
||||
const filter = props.pagination.reversed ? 'sinceId' : 'untilId';
|
||||
await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
|
||||
...params,
|
||||
limit: SECOND_FETCH_LIMIT,
|
||||
...(props.pagination.offsetMode ? {
|
||||
offset: offset.value,
|
||||
} : {
|
||||
untilId: Array.from(items.value.keys()).at(-1),
|
||||
[filter]: Array.from(items.value.keys()).at(-1),
|
||||
}),
|
||||
}).then(res => {
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
|
@ -254,78 +292,23 @@ const fetchMore = async (): Promise<void> => {
|
|||
if (i === 10) item._shouldInsertAd_ = true;
|
||||
}
|
||||
|
||||
const reverseConcat = _res => {
|
||||
const oldHeight = scrollableElement.value ? scrollableElement.value.scrollHeight : getBodyScrollHeight();
|
||||
const oldScroll = scrollableElement.value ? scrollableElement.value.scrollTop : window.scrollY;
|
||||
const moreAvailable = res.length !== 0;
|
||||
|
||||
items.value = concatMapWithArray(items.value, _res);
|
||||
|
||||
return nextTick(() => {
|
||||
if (scrollableElement.value) {
|
||||
scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' });
|
||||
} else {
|
||||
window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' });
|
||||
}
|
||||
|
||||
return nextTick();
|
||||
if (props.pagination.scrollAtTop) {
|
||||
reverseConcat(res).then(() => {
|
||||
more.value = moreAvailable;
|
||||
moreFetching.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
if (res.length === 0) {
|
||||
if (props.pagination.reversed) {
|
||||
reverseConcat(res).then(() => {
|
||||
more.value = false;
|
||||
moreFetching.value = false;
|
||||
});
|
||||
} else {
|
||||
items.value = concatMapWithArray(items.value, res);
|
||||
more.value = false;
|
||||
moreFetching.value = false;
|
||||
}
|
||||
} else {
|
||||
if (props.pagination.reversed) {
|
||||
reverseConcat(res).then(() => {
|
||||
more.value = true;
|
||||
moreFetching.value = false;
|
||||
});
|
||||
} else {
|
||||
items.value = concatMapWithArray(items.value, res);
|
||||
more.value = true;
|
||||
moreFetching.value = false;
|
||||
}
|
||||
items.value = concatMapWithArray(items.value, res);
|
||||
more.value = moreAvailable;
|
||||
moreFetching.value = false;
|
||||
}
|
||||
offset.value += res.length;
|
||||
}, err => {
|
||||
moreFetching.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const fetchMoreAhead = async (): Promise<void> => {
|
||||
if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
|
||||
moreFetching.value = true;
|
||||
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
||||
await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
|
||||
...params,
|
||||
limit: SECOND_FETCH_LIMIT,
|
||||
...(props.pagination.offsetMode ? {
|
||||
offset: offset.value,
|
||||
} : {
|
||||
sinceId: Array.from(items.value.keys()).at(-1),
|
||||
}),
|
||||
}).then(res => {
|
||||
if (res.length === 0) {
|
||||
items.value = concatMapWithArray(items.value, res);
|
||||
more.value = false;
|
||||
} else {
|
||||
items.value = concatMapWithArray(items.value, res);
|
||||
more.value = true;
|
||||
}
|
||||
offset.value += res.length;
|
||||
moreFetching.value = false;
|
||||
}, err => {
|
||||
moreFetching.value = false;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Appear(IntersectionObserver)によってfetchMoreが呼ばれる場合、
|
||||
|
@ -346,13 +329,7 @@ const appearFetchMore = async (): Promise<void> => {
|
|||
fetchMoreAppearTimeout();
|
||||
};
|
||||
|
||||
const appearFetchMoreAhead = async (): Promise<void> => {
|
||||
if (preventAppearFetchMore.value) return;
|
||||
await fetchMoreAhead();
|
||||
fetchMoreAppearTimeout();
|
||||
};
|
||||
|
||||
const isTop = (): boolean => isBackTop.value || (props.pagination.reversed ? isBottomVisible : isTopVisible)(contentEl.value!, TOLERANCE);
|
||||
const isTop = (): boolean => isBackTop.value || (props.pagination.scrollAtTop ? isBottomVisible : isTopVisible)(contentEl.value!, TOLERANCE);
|
||||
|
||||
watch(visibility, () => {
|
||||
if (visibility.value === 'hidden') {
|
||||
|
@ -446,7 +423,7 @@ onActivated(() => {
|
|||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
isBackTop.value = props.pagination.reversed ? window.scrollY >= (rootEl.value ? rootEl.value.scrollHeight - window.innerHeight : 0) : window.scrollY === 0;
|
||||
isBackTop.value = props.pagination.scrollAtTop ? window.scrollY >= (rootEl.value ? rootEl.value.scrollHeight - window.innerHeight : 0) : window.scrollY === 0;
|
||||
});
|
||||
|
||||
function toBottom() {
|
||||
|
@ -455,7 +432,7 @@ function toBottom() {
|
|||
|
||||
onBeforeMount(() => {
|
||||
init().then(() => {
|
||||
if (props.pagination.reversed) {
|
||||
if (props.pagination.scrollAtTop) {
|
||||
nextTick(() => {
|
||||
setTimeout(toBottom, 800);
|
||||
|
||||
|
@ -486,7 +463,7 @@ defineExpose({
|
|||
queue,
|
||||
backed: backed.value,
|
||||
more,
|
||||
reload,
|
||||
reload: init,
|
||||
prepend,
|
||||
append: appendItem,
|
||||
removeItem,
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Directive } from 'vue';
|
||||
import { getScrollContainer, getScrollPosition } from '@/scripts/scroll.js';
|
||||
|
||||
export default {
|
||||
mounted(src, binding, vn) {
|
||||
if (binding.value === false) return;
|
||||
|
||||
let isBottom = true;
|
||||
|
||||
const container = getScrollContainer(src)!;
|
||||
container.addEventListener('scroll', () => {
|
||||
const pos = getScrollPosition(container);
|
||||
const viewHeight = container.clientHeight;
|
||||
const height = container.scrollHeight;
|
||||
isBottom = (pos + viewHeight > height - 32);
|
||||
}, { passive: true });
|
||||
console.log('mount', container.scrollHeight);
|
||||
|
||||
container.scroll({ top: container.scrollHeight, behavior: 'smooth' });
|
||||
|
||||
const ro = new ResizeObserver((entries, observer) => {
|
||||
console.log('resize', container.scrollHeight);
|
||||
if (isBottom) {
|
||||
const height = container.scrollHeight;
|
||||
container.scroll({ top: height, behavior: 'instant' });
|
||||
}
|
||||
});
|
||||
|
||||
ro.observe(src);
|
||||
|
||||
// TODO: 新たにプロパティを作るのをやめMapを使う
|
||||
src._ro_ = ro;
|
||||
},
|
||||
|
||||
unmounted(src, binding, vn) {
|
||||
if (src._ro_) src._ro_.unobserve(src);
|
||||
},
|
||||
} as Directive;
|
|
@ -15,7 +15,6 @@ import anim from './anim.js';
|
|||
import clickAnime from './click-anime.js';
|
||||
import panel from './panel.js';
|
||||
import adaptiveBorder from './adaptive-border.js';
|
||||
import follow from './follow-append.js';
|
||||
import adaptiveBg from './adaptive-bg.js';
|
||||
|
||||
export default function(app: App) {
|
||||
|
@ -36,6 +35,5 @@ export const directives = {
|
|||
'click-anime': clickAnime,
|
||||
'panel': panel,
|
||||
'adaptive-border': adaptiveBorder,
|
||||
'follow': follow,
|
||||
'adaptive-bg': adaptiveBg,
|
||||
};
|
||||
|
|
|
@ -85,6 +85,7 @@ const prevUserPagination: Paging = {
|
|||
|
||||
const nextUserPagination: Paging = {
|
||||
reversed: true,
|
||||
scrollAtTop: true,
|
||||
endpoint: 'users/notes',
|
||||
limit: 10,
|
||||
params: computed(() => note.value ? ({
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<div class="mk-app" @contextmenu.self.prevent="onContextmenu">
|
||||
|
||||
<div class="nav">
|
||||
<header class="header">
|
||||
<div class="left">
|
||||
|
@ -64,14 +65,19 @@
|
|||
</footer>
|
||||
</div>
|
||||
|
||||
<main class="main" :style="{ background: pageMetadata?.value?.bg }" @contextmenu.stop="onContextmenu">
|
||||
<div class="content" style="container-type: inline-size;">
|
||||
<RouterView :router="chatRouter"/>
|
||||
</div>
|
||||
</main>
|
||||
<div class="body">
|
||||
<main class="main" :style="{ background: pageMetadata?.value?.bg }" @contextmenu.stop="onContextmenu">
|
||||
<div class="content" style="container-type: inline-size;">
|
||||
<RouterView :router="chatRouter"/>
|
||||
</div>
|
||||
|
||||
<div class="side widgets">
|
||||
<XWidgets/>
|
||||
<div class="side widgets">
|
||||
<XWidgets/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<XStatusBars class="statusbars"/>
|
||||
<XAnnouncements v-if="$i"/>
|
||||
</div>
|
||||
|
||||
<transition name="menu-back">
|
||||
|
@ -91,9 +97,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { provide, ref, shallowRef, toRefs, computed, onMounted } from 'vue';
|
||||
import { provide, ref, shallowRef, toRefs, computed, onMounted, defineAsyncComponent } from 'vue';
|
||||
import { instanceName, url } from '@/config.js';
|
||||
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
|
||||
import XWidgets from './chat/widgets.vue';
|
||||
import XCommon from './_common_/common.vue';
|
||||
import XHeaderClock from './chat/header-clock.vue';
|
||||
|
@ -110,7 +115,10 @@ import { instance } from '@/instance.js';
|
|||
import { mainRouter } from '@/router/main.js';
|
||||
import { defaultRoutes, page } from '@/router/definition.js';
|
||||
import { useRouterFactory } from '@/router/supplier.js';
|
||||
import { mainRouter } from '@/router/main.js';
|
||||
|
||||
const XDrawerMenu = defineAsyncComponent(() => import('@/ui/_common_/navbar-for-mobile.vue'));
|
||||
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
|
||||
const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
|
||||
|
||||
const routes = [
|
||||
{
|
||||
|
@ -479,25 +487,32 @@ onMounted(() => {
|
|||
}
|
||||
}
|
||||
|
||||
> .main {
|
||||
> .body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
|
||||
> .content {
|
||||
> .main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
overflow: auto;
|
||||
overflow-y: scroll;
|
||||
overscroll-behavior: contain;
|
||||
background: var(--bg);
|
||||
}
|
||||
}
|
||||
|
||||
> .side {
|
||||
width: 350px;
|
||||
border-left: solid 4px var(--divider);
|
||||
background: var(--panel);
|
||||
> .content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
overflow-y: scroll;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
> .side {
|
||||
width: 350px;
|
||||
border-left: solid 4px var(--divider);
|
||||
background: var(--panel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .menuDrawer-back {
|
||||
|
|
|
@ -21,6 +21,11 @@ export default defineComponent({
|
|||
required: false,
|
||||
default: false
|
||||
},
|
||||
noMoveTransition: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
ad: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
@ -62,10 +67,10 @@ export default defineComponent({
|
|||
h('i', {
|
||||
class: 'ti ti-chevron-up icon',
|
||||
}),
|
||||
getDateText(item.createdAt)
|
||||
props.reversed ? getDateText(props.items[i + 1].createdAt) : getDateText(item.createdAt),
|
||||
]),
|
||||
h('span', [
|
||||
getDateText(props.items[i + 1].createdAt),
|
||||
props.reversed ? getDateText(item.createdAt) : getDateText(props.items[i + 1].createdAt),
|
||||
h('i', {
|
||||
class: 'ti ti-chevron-down icon',
|
||||
})
|
||||
|
@ -85,31 +90,17 @@ export default defineComponent({
|
|||
}
|
||||
});
|
||||
|
||||
function onBeforeLeave(element: Element) {
|
||||
const el = element as HTMLElement;
|
||||
el.style.top = `${el.offsetTop}px`;
|
||||
el.style.left = `${el.offsetLeft}px`;
|
||||
}
|
||||
|
||||
function onLeaveCancelled(element: Element) {
|
||||
const el = element as HTMLElement;
|
||||
el.style.top = '';
|
||||
el.style.left = '';
|
||||
}
|
||||
|
||||
const classes = {
|
||||
[$style['date-separated-list']]: true,
|
||||
[$style.root]: true,
|
||||
[$style['reversed']]: props.reversed,
|
||||
[$style['direction-down']]: props.direction === 'down',
|
||||
[$style['direction-up']]: props.direction === 'up',
|
||||
};
|
||||
|
||||
return () => defaultStore.state.animation ? h(TransitionGroup, {
|
||||
class: classes,
|
||||
class: { ...classes, [$style['no-move-transition']]: props.noMoveTransition },
|
||||
name: 'list',
|
||||
tag: 'div',
|
||||
onBeforeLeave,
|
||||
onLeaveCancelled,
|
||||
}, { default: renderChildren }) : h('div', {
|
||||
class: classes,
|
||||
}, { default: renderChildren });
|
||||
|
@ -118,29 +109,35 @@ export default defineComponent({
|
|||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.date-separated-list {
|
||||
.root {
|
||||
container-type: inline-size;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&:global {
|
||||
> .list-move {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
&.deny-move-transition > .list-move {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
> .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;
|
||||
}
|
||||
|
||||
> *:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// > *:not(:last-child) {
|
||||
// margin-bottom: var(--margin);
|
||||
// }
|
||||
.no-move-transition {
|
||||
&:global {
|
||||
> .list-move {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,35 +8,50 @@
|
|||
</template>
|
||||
|
||||
<template #default="{ items: notes }">
|
||||
<div>
|
||||
<XList
|
||||
ref="notes"
|
||||
v-slot="{ item: note }"
|
||||
:items="notes"
|
||||
:direction="pagination.reversed ? 'up' : 'down'"
|
||||
:reversed="pagination.reversed"
|
||||
:ad="true"
|
||||
>
|
||||
<XNote :key="note._featuredId_ || note._prId_ || note.id" :note="note"/>
|
||||
</XList>
|
||||
</div>
|
||||
<XList
|
||||
v-slot="{ item: note }"
|
||||
:noMoveTransition="reversed && !atBottom"
|
||||
:items="notes"
|
||||
:direction="reversed ? 'up' : 'down'"
|
||||
:reversed="reversed"
|
||||
:ad="true"
|
||||
>
|
||||
<XNote :key="note._featuredId_ || note._prId_ || note.id" :note="note"/>
|
||||
</XList>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef } from 'vue';
|
||||
import { shallowRef, ref, watchEffect } from 'vue';
|
||||
import XNote from './note.vue';
|
||||
import XList from './date-separated-list.vue';
|
||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { isBottomVisible, getScrollContainer } from '@/scripts/scroll.js';
|
||||
|
||||
const props = defineProps<{
|
||||
pagination: Paging;
|
||||
reversed?: boolean;
|
||||
}>();
|
||||
|
||||
|
||||
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
const notes = shallowRef();
|
||||
const atBottom = ref(null);
|
||||
|
||||
watchEffect((cleanup) => {
|
||||
if (props.pagination.pageEl) {
|
||||
let scroller = getScrollContainer(props.pagination.pageEl);
|
||||
const scrollCallback = () => {
|
||||
atBottom.value = isBottomVisible(props.pagination.pageEl, 16, scroller);
|
||||
};
|
||||
|
||||
scroller.addEventListener('scroll', scrollCallback, { passive: true });
|
||||
cleanup(() => scroller.removeEventListener('scroll', scrollCallback));
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
pagingComponent,
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
<div v-if="queue > 0" class="new" :style="{ width: width + 'px', top: inChannel ? null : top + 'px', bottom: inChannel ? bottom + 'px' : null }">
|
||||
<button class="_buttonPrimary" @click="scrollToNew()">{{ i18n.ts.newNoteRecived }}</button>
|
||||
</div>
|
||||
<XNotes ref="tlElement" class="tl" :pagination="pagination" @queue="queueUpdated" v-follow="!!props.channelId"/>
|
||||
<XNotes ref="tlElement" class="tl" :pagination="pagination" :reversed="inChannel" @queue="queueUpdated"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -72,7 +72,9 @@ let endpoint = ref(null);
|
|||
|
||||
const pagination = computed(() => ({
|
||||
endpoint: endpoint.value,
|
||||
reversed: inChannel.value,
|
||||
scrollAtTop: inChannel.value,
|
||||
pageEl: scroller.value,
|
||||
limit: 15,
|
||||
params: {
|
||||
untilDate: timetravelTarget.value?.getTime(),
|
||||
...query,
|
||||
|
|
|
@ -19,6 +19,9 @@
|
|||
<button class="_buttonPrimary" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="floating">
|
||||
<button v-tooltip="i18n.ts.previewNoteText" class="_button icon" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button>
|
||||
</div>
|
||||
<input v-show="useCw" ref="cwInputEl" v-model="cw" class="cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
|
||||
<textarea ref="textareaEl" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting" :placeholder="placeholder" @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
|
||||
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
||||
|
@ -53,7 +56,6 @@
|
|||
<span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></i></span>
|
||||
<span v-else><i class="ti ti-icons"></i></span>
|
||||
</button>
|
||||
<button v-tooltip="i18n.ts.previewNoteText" class="_button icon" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button>
|
||||
<button class="submit _buttonPrimary" :disabled="!canPost" @click="post">{{ submitText }}<i :class="reply ? 'ti ti-arrow-back-up' : renote ? 'ti ti-quote' : 'ti ti-send'"></i></button>
|
||||
</div>
|
||||
</footer>
|
||||
|
@ -1041,8 +1043,39 @@ onMounted(() => {
|
|||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover {
|
||||
background: var(--X5);
|
||||
}
|
||||
&.active {
|
||||
color: var(--accent);
|
||||
}
|
||||
&.danger {
|
||||
color: var(--error);
|
||||
}
|
||||
}
|
||||
|
||||
> .floating {
|
||||
float: right;
|
||||
margin-bottom: -32px;
|
||||
position: relative;
|
||||
z-index: auto;
|
||||
opacity: 0.7;
|
||||
|
||||
> .icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
> footer {
|
||||
$height: 44px;
|
||||
$height: 38px;
|
||||
display: flex;
|
||||
padding: 0 8px 8px 8px;
|
||||
line-height: $height;
|
||||
|
@ -1051,20 +1084,6 @@ onMounted(() => {
|
|||
width: $height;
|
||||
height: $height;
|
||||
font-size: 16px;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover {
|
||||
background: var(--X5);
|
||||
}
|
||||
&.active {
|
||||
color: var(--accent);
|
||||
}
|
||||
&.danger {
|
||||
color: var(--error);
|
||||
}
|
||||
}
|
||||
|
||||
> .left {
|
||||
|
|
Loading…
Reference in New Issue