Compare commits

..

3 Commits

10 changed files with 192 additions and 211 deletions

View File

@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="notes" ref="notes"
v-slot="{ item: note }" v-slot="{ item: note }"
:items="notes" :items="notes"
:direction="pagination.reversed ? 'up' : 'down'" :direction="pagination.scrollAtTop ? 'up' : 'down'"
:reversed="pagination.reversed" :reversed="pagination.reversed"
:noGap="noGap" :noGap="noGap"
:ad="true" :ad="true"

View File

@ -25,14 +25,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
<div v-else ref="rootEl"> <div v-else ref="rootEl">
<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) ? appearFetchMoreAhead : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary rounded @click="fetchMoreAhead"> <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 }} {{ i18n.ts.loadMore }}
</MkButton> </MkButton>
<MkLoading v-else class="loading"/> <MkLoading v-else class="loading"/>
</div> </div>
<slot :items="Array.from(items.values())" :fetching="fetching || moreFetching"></slot> <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"> <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 }} {{ i18n.ts.loadMore }}
</MkButton> </MkButton>
@ -73,11 +73,18 @@ export type Paging<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints>
*/ */
reversed?: boolean; reversed?: boolean;
/**
* Fetch more items when scrolling towards the top of the page
* rather than the bottom
*/
scrollAtTop?: boolean;
offsetMode?: boolean; offsetMode?: boolean;
pageEl?: HTMLElement; pageEl?: HTMLElement;
}; };
type MisskeyEntityMap = Map<string, MisskeyEntity>; type MisskeyEntityMap = Map<string, MisskeyEntity>;
function arrayToEntries(entities: MisskeyEntity[]): [string, MisskeyEntity][] { function arrayToEntries(entities: MisskeyEntity[]): [string, MisskeyEntity][] {
@ -156,14 +163,14 @@ const BACKGROUND_PAUSE_WAIT_SEC = 10;
// https://qiita.com/mkataigi/items/0154aefd2223ce23398e // https://qiita.com/mkataigi/items/0154aefd2223ce23398e
const scrollObserver = ref<IntersectionObserver>(); const scrollObserver = ref<IntersectionObserver>();
watch([() => props.pagination.reversed, scrollableElement], () => { watch([() => props.pagination.scrollAtTop, scrollableElement], () => {
if (scrollObserver.value) scrollObserver.value.disconnect(); if (scrollObserver.value) scrollObserver.value.disconnect();
scrollObserver.value = new IntersectionObserver(entries => { scrollObserver.value = new IntersectionObserver(entries => {
backed.value = entries[0].isIntersecting; backed.value = entries[0].isIntersecting;
}, { }, {
root: scrollableElement.value, 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, threshold: 0.01,
}); });
}, { immediate: true }); }, { immediate: true });
@ -179,7 +186,7 @@ watch([backed, contentEl], () => {
if (!backed.value) { if (!backed.value) {
if (!contentEl.value) return; 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 { } else {
if (scrollRemove.value) scrollRemove.value(); if (scrollRemove.value) scrollRemove.value();
scrollRemove.value = null; scrollRemove.value = null;
@ -199,6 +206,23 @@ watch(error, (n, o) => {
emit('status', n); 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> { async function init(): Promise<void> {
items.value = new Map(); items.value = new Map();
queue.value = new Map(); queue.value = new Map();
@ -209,6 +233,23 @@ async function init(): Promise<void> {
limit: props.pagination.limit ?? 10, limit: props.pagination.limit ?? 10,
allowPartial: true, allowPartial: true,
}).then(res => { }).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++) { for (let i = 0; i < res.length; i++) {
const item = res[i]; const item = res[i];
if (i === 3) item._shouldInsertAd_ = true; if (i === 3) item._shouldInsertAd_ = true;
@ -218,7 +259,7 @@ async function init(): Promise<void> {
concatItems(res); concatItems(res);
more.value = false; more.value = false;
} else { } else {
if (props.pagination.reversed) moreFetching.value = true; if (props.pagination.scrollAtTop) moreFetching.value = true;
concatItems(res); concatItems(res);
more.value = true; more.value = true;
} }
@ -232,21 +273,18 @@ async function init(): Promise<void> {
}); });
} }
const reload = (): Promise<void> => { async function fetchMore(): Promise<void> {
return init();
};
const fetchMore = async (): Promise<void> => {
if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return; if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
moreFetching.value = true; moreFetching.value = true;
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; 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, { await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
...params, ...params,
limit: SECOND_FETCH_LIMIT, limit: SECOND_FETCH_LIMIT,
...(props.pagination.offsetMode ? { ...(props.pagination.offsetMode ? {
offset: offset.value, offset: offset.value,
} : { } : {
untilId: Array.from(items.value.keys()).at(-1), [filter]: Array.from(items.value.keys()).at(-1),
}), }),
}).then(res => { }).then(res => {
for (let i = 0; i < res.length; i++) { for (let i = 0; i < res.length; i++) {
@ -254,78 +292,23 @@ const fetchMore = async (): Promise<void> => {
if (i === 10) item._shouldInsertAd_ = true; if (i === 10) item._shouldInsertAd_ = true;
} }
const reverseConcat = _res => { const moreAvailable = res.length !== 0;
const oldHeight = scrollableElement.value ? scrollableElement.value.scrollHeight : getBodyScrollHeight();
const oldScroll = scrollableElement.value ? scrollableElement.value.scrollTop : window.scrollY;
items.value = concatMapWithArray(items.value, _res); if (props.pagination.scrollAtTop) {
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 (res.length === 0) {
if (props.pagination.reversed) {
reverseConcat(res).then(() => { reverseConcat(res).then(() => {
more.value = false; more.value = moreAvailable;
moreFetching.value = false; moreFetching.value = false;
}); });
} else { } else {
items.value = concatMapWithArray(items.value, res); items.value = concatMapWithArray(items.value, res);
more.value = false; more.value = moreAvailable;
moreFetching.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;
}
}
offset.value += res.length; offset.value += res.length;
}, err => { }, err => {
moreFetching.value = false; 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;
});
};
/** /**
* AppearIntersectionObserverによってfetchMoreが呼ばれる場合 * AppearIntersectionObserverによってfetchMoreが呼ばれる場合
@ -346,13 +329,7 @@ const appearFetchMore = async (): Promise<void> => {
fetchMoreAppearTimeout(); fetchMoreAppearTimeout();
}; };
const appearFetchMoreAhead = async (): Promise<void> => { const isTop = (): boolean => isBackTop.value || (props.pagination.scrollAtTop ? isBottomVisible : isTopVisible)(contentEl.value!, TOLERANCE);
if (preventAppearFetchMore.value) return;
await fetchMoreAhead();
fetchMoreAppearTimeout();
};
const isTop = (): boolean => isBackTop.value || (props.pagination.reversed ? isBottomVisible : isTopVisible)(contentEl.value!, TOLERANCE);
watch(visibility, () => { watch(visibility, () => {
if (visibility.value === 'hidden') { if (visibility.value === 'hidden') {
@ -446,7 +423,7 @@ onActivated(() => {
}); });
onDeactivated(() => { 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() { function toBottom() {
@ -455,7 +432,7 @@ function toBottom() {
onBeforeMount(() => { onBeforeMount(() => {
init().then(() => { init().then(() => {
if (props.pagination.reversed) { if (props.pagination.scrollAtTop) {
nextTick(() => { nextTick(() => {
setTimeout(toBottom, 800); setTimeout(toBottom, 800);
@ -486,7 +463,7 @@ defineExpose({
queue, queue,
backed: backed.value, backed: backed.value,
more, more,
reload, reload: init,
prepend, prepend,
append: appendItem, append: appendItem,
removeItem, removeItem,

View File

@ -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;

View File

@ -15,7 +15,6 @@ import anim from './anim.js';
import clickAnime from './click-anime.js'; import clickAnime from './click-anime.js';
import panel from './panel.js'; import panel from './panel.js';
import adaptiveBorder from './adaptive-border.js'; import adaptiveBorder from './adaptive-border.js';
import follow from './follow-append.js';
import adaptiveBg from './adaptive-bg.js'; import adaptiveBg from './adaptive-bg.js';
export default function(app: App) { export default function(app: App) {
@ -36,6 +35,5 @@ export const directives = {
'click-anime': clickAnime, 'click-anime': clickAnime,
'panel': panel, 'panel': panel,
'adaptive-border': adaptiveBorder, 'adaptive-border': adaptiveBorder,
'follow': follow,
'adaptive-bg': adaptiveBg, 'adaptive-bg': adaptiveBg,
}; };

View File

@ -85,6 +85,7 @@ const prevUserPagination: Paging = {
const nextUserPagination: Paging = { const nextUserPagination: Paging = {
reversed: true, reversed: true,
scrollAtTop: true,
endpoint: 'users/notes', endpoint: 'users/notes',
limit: 10, limit: 10,
params: computed(() => note.value ? ({ params: computed(() => note.value ? ({

View File

@ -1,5 +1,6 @@
<template> <template>
<div class="mk-app" @contextmenu.self.prevent="onContextmenu"> <div class="mk-app" @contextmenu.self.prevent="onContextmenu">
<div class="nav"> <div class="nav">
<header class="header"> <header class="header">
<div class="left"> <div class="left">
@ -64,15 +65,20 @@
</footer> </footer>
</div> </div>
<div class="body">
<main class="main" :style="{ background: pageMetadata?.value?.bg }" @contextmenu.stop="onContextmenu"> <main class="main" :style="{ background: pageMetadata?.value?.bg }" @contextmenu.stop="onContextmenu">
<div class="content" style="container-type: inline-size;"> <div class="content" style="container-type: inline-size;">
<RouterView :router="chatRouter"/> <RouterView :router="chatRouter"/>
</div> </div>
</main>
<div class="side widgets"> <div class="side widgets">
<XWidgets/> <XWidgets/>
</div> </div>
</main>
<XStatusBars class="statusbars"/>
<XAnnouncements v-if="$i"/>
</div>
<transition name="menu-back"> <transition name="menu-back">
<div v-if="drawerMenuShowing" <div v-if="drawerMenuShowing"
@ -91,9 +97,8 @@
</template> </template>
<script lang="ts" setup> <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 { instanceName, url } from '@/config.js';
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
import XWidgets from './chat/widgets.vue'; import XWidgets from './chat/widgets.vue';
import XCommon from './_common_/common.vue'; import XCommon from './_common_/common.vue';
import XHeaderClock from './chat/header-clock.vue'; import XHeaderClock from './chat/header-clock.vue';
@ -110,7 +115,10 @@ import { instance } from '@/instance.js';
import { mainRouter } from '@/router/main.js'; import { mainRouter } from '@/router/main.js';
import { defaultRoutes, page } from '@/router/definition.js'; import { defaultRoutes, page } from '@/router/definition.js';
import { useRouterFactory } from '@/router/supplier.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 = [ const routes = [
{ {
@ -479,26 +487,33 @@ onMounted(() => {
} }
} }
> .body {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
> .main { > .main {
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column; flex-direction: row;
height: 100%; overflow: auto;
overscroll-behavior: contain;
> .content { > .content {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
overflow-y: scroll; overflow-y: scroll;
overscroll-behavior: contain;
background: var(--bg); background: var(--bg);
} }
}
> .side { > .side {
width: 350px; width: 350px;
border-left: solid 4px var(--divider); border-left: solid 4px var(--divider);
background: var(--panel); background: var(--panel);
} }
}
}
> .menuDrawer-back { > .menuDrawer-back {
z-index: 1001; z-index: 1001;

View File

@ -21,6 +21,11 @@ export default defineComponent({
required: false, required: false,
default: false default: false
}, },
noMoveTransition: {
type: Boolean,
required: false,
default: false
},
ad: { ad: {
type: Boolean, type: Boolean,
required: false, required: false,
@ -62,10 +67,10 @@ export default defineComponent({
h('i', { h('i', {
class: 'ti ti-chevron-up icon', class: 'ti ti-chevron-up icon',
}), }),
getDateText(item.createdAt) props.reversed ? getDateText(props.items[i + 1].createdAt) : getDateText(item.createdAt),
]), ]),
h('span', [ h('span', [
getDateText(props.items[i + 1].createdAt), props.reversed ? getDateText(item.createdAt) : getDateText(props.items[i + 1].createdAt),
h('i', { h('i', {
class: 'ti ti-chevron-down icon', 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 = { const classes = {
[$style['date-separated-list']]: true, [$style.root]: true,
[$style['reversed']]: props.reversed, [$style['reversed']]: props.reversed,
[$style['direction-down']]: props.direction === 'down', [$style['direction-down']]: props.direction === 'down',
[$style['direction-up']]: props.direction === 'up', [$style['direction-up']]: props.direction === 'up',
}; };
return () => defaultStore.state.animation ? h(TransitionGroup, { return () => defaultStore.state.animation ? h(TransitionGroup, {
class: classes, class: { ...classes, [$style['no-move-transition']]: props.noMoveTransition },
name: 'list', name: 'list',
tag: 'div', tag: 'div',
onBeforeLeave,
onLeaveCancelled,
}, { default: renderChildren }) : h('div', { }, { default: renderChildren }) : h('div', {
class: classes, class: classes,
}, { default: renderChildren }); }, { default: renderChildren });
@ -118,29 +109,35 @@ export default defineComponent({
</script> </script>
<style lang="scss" module> <style lang="scss" module>
.date-separated-list { .root {
container-type: inline-size; container-type: inline-size;
display: flex;
flex-direction: column;
&:global { &:global {
> .list-move { > .list-move {
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1); transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
} }
&.deny-move-transition > .list-move {
transition: none !important;
}
> .list-enter-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); 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 { > *:empty {
display: none; display: none;
} }
}
}
// > *:not(:last-child) { .no-move-transition {
// margin-bottom: var(--margin); &:global {
// } > .list-move {
transition: none !important;
}
} }
} }

View File

@ -8,35 +8,50 @@
</template> </template>
<template #default="{ items: notes }"> <template #default="{ items: notes }">
<div>
<XList <XList
ref="notes"
v-slot="{ item: note }" v-slot="{ item: note }"
:noMoveTransition="reversed && !atBottom"
:items="notes" :items="notes"
:direction="pagination.reversed ? 'up' : 'down'" :direction="reversed ? 'up' : 'down'"
:reversed="pagination.reversed" :reversed="reversed"
:ad="true" :ad="true"
> >
<XNote :key="note._featuredId_ || note._prId_ || note.id" :note="note"/> <XNote :key="note._featuredId_ || note._prId_ || note.id" :note="note"/>
</XList> </XList>
</div>
</template> </template>
</MkPagination> </MkPagination>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { shallowRef } from 'vue'; import { shallowRef, ref, watchEffect } from 'vue';
import XNote from './note.vue'; import XNote from './note.vue';
import XList from './date-separated-list.vue'; import XList from './date-separated-list.vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue'; import MkPagination, { Paging } from '@/components/MkPagination.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { infoImageUrl } from '@/instance.js'; import { infoImageUrl } from '@/instance.js';
import { isBottomVisible, getScrollContainer } from '@/scripts/scroll.js';
const props = defineProps<{ const props = defineProps<{
pagination: Paging; pagination: Paging;
reversed?: boolean;
}>(); }>();
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>(); 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({ defineExpose({
pagingComponent, pagingComponent,

View File

@ -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 }"> <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> <button class="_buttonPrimary" @click="scrollToNew()">{{ i18n.ts.newNoteRecived }}</button>
</div> </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> </div>
</div> </div>
@ -72,7 +72,9 @@ let endpoint = ref(null);
const pagination = computed(() => ({ const pagination = computed(() => ({
endpoint: endpoint.value, endpoint: endpoint.value,
reversed: inChannel.value, scrollAtTop: inChannel.value,
pageEl: scroller.value,
limit: 15,
params: { params: {
untilDate: timetravelTarget.value?.getTime(), untilDate: timetravelTarget.value?.getTime(),
...query, ...query,

View File

@ -19,6 +19,9 @@
<button class="_buttonPrimary" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button> <button class="_buttonPrimary" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button>
</div> </div>
</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"> <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"/> <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"> <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-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></i></span>
<span v-else><i class="ti ti-icons"></i></span> <span v-else><i class="ti ti-icons"></i></span>
</button> </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> <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> </div>
</footer> </footer>
@ -1041,16 +1043,7 @@ onMounted(() => {
} }
} }
> footer {
$height: 44px;
display: flex;
padding: 0 8px 8px 8px;
line-height: $height;
.icon { .icon {
width: $height;
height: $height;
font-size: 16px;
display: inline-block; display: inline-block;
padding: 0; padding: 0;
margin: 0; margin: 0;
@ -1067,6 +1060,32 @@ onMounted(() => {
} }
} }
> .floating {
float: right;
margin-bottom: -32px;
position: relative;
z-index: auto;
opacity: 0.7;
> .icon {
width: 32px;
height: 32px;
font-size: 16px;
}
}
> footer {
$height: 38px;
display: flex;
padding: 0 8px 8px 8px;
line-height: $height;
.icon {
width: $height;
height: $height;
font-size: 16px;
}
> .left { > .left {
display: flex; display: flex;
align-items: center; align-items: center;