Compare commits
No commits in common. "8d4614a48df392baa45599e59ba5c6eb4b1d6e2d" and "02511e28fb9c27dab2b3ad74a0f4d20961391b2c" have entirely different histories.
8d4614a48d
...
02511e28fb
|
@ -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.scrollAtTop ? 'up' : 'down'"
|
:direction="pagination.reversed ? 'up' : 'down'"
|
||||||
:reversed="pagination.reversed"
|
:reversed="pagination.reversed"
|
||||||
:noGap="noGap"
|
:noGap="noGap"
|
||||||
:ad="true"
|
:ad="true"
|
||||||
|
|
|
@ -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.scrollAtTop && more" key="_more_" class="_margin">
|
<div v-show="pagination.reversed && 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) ? appearFetchMoreAhead : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary rounded @click="fetchMoreAhead">
|
||||||
{{ 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.scrollAtTop && more" key="_more_" class="_margin">
|
<div v-show="!pagination.reversed && 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,18 +73,11 @@ 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][] {
|
||||||
|
@ -163,14 +156,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.scrollAtTop, scrollableElement], () => {
|
watch([() => props.pagination.reversed, 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.scrollAtTop ? '-100% 0px 100% 0px' : '100% 0px -100% 0px',
|
rootMargin: props.pagination.reversed ? '-100% 0px 100% 0px' : '100% 0px -100% 0px',
|
||||||
threshold: 0.01,
|
threshold: 0.01,
|
||||||
});
|
});
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
@ -186,7 +179,7 @@ watch([backed, contentEl], () => {
|
||||||
if (!backed.value) {
|
if (!backed.value) {
|
||||||
if (!contentEl.value) return;
|
if (!contentEl.value) return;
|
||||||
|
|
||||||
scrollRemove.value = (props.pagination.scrollAtTop ? onScrollBottom : onScrollTop)(contentEl.value, executeQueue, TOLERANCE);
|
scrollRemove.value = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl.value, executeQueue, TOLERANCE);
|
||||||
} else {
|
} else {
|
||||||
if (scrollRemove.value) scrollRemove.value();
|
if (scrollRemove.value) scrollRemove.value();
|
||||||
scrollRemove.value = null;
|
scrollRemove.value = null;
|
||||||
|
@ -206,23 +199,6 @@ 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();
|
||||||
|
@ -233,23 +209,6 @@ 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;
|
||||||
|
@ -259,7 +218,7 @@ async function init(): Promise<void> {
|
||||||
concatItems(res);
|
concatItems(res);
|
||||||
more.value = false;
|
more.value = false;
|
||||||
} else {
|
} else {
|
||||||
if (props.pagination.scrollAtTop) moreFetching.value = true;
|
if (props.pagination.reversed) moreFetching.value = true;
|
||||||
concatItems(res);
|
concatItems(res);
|
||||||
more.value = true;
|
more.value = true;
|
||||||
}
|
}
|
||||||
|
@ -273,18 +232,21 @@ async function init(): Promise<void> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchMore(): Promise<void> {
|
const reload = (): 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,
|
||||||
} : {
|
} : {
|
||||||
[filter]: Array.from(items.value.keys()).at(-1),
|
untilId: 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++) {
|
||||||
|
@ -292,23 +254,78 @@ async function fetchMore(): Promise<void> {
|
||||||
if (i === 10) item._shouldInsertAd_ = true;
|
if (i === 10) item._shouldInsertAd_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const moreAvailable = res.length !== 0;
|
const reverseConcat = _res => {
|
||||||
|
const oldHeight = scrollableElement.value ? scrollableElement.value.scrollHeight : getBodyScrollHeight();
|
||||||
|
const oldScroll = scrollableElement.value ? scrollableElement.value.scrollTop : window.scrollY;
|
||||||
|
|
||||||
if (props.pagination.scrollAtTop) {
|
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 (res.length === 0) {
|
||||||
|
if (props.pagination.reversed) {
|
||||||
reverseConcat(res).then(() => {
|
reverseConcat(res).then(() => {
|
||||||
more.value = moreAvailable;
|
more.value = false;
|
||||||
moreFetching.value = false;
|
moreFetching.value = false;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
items.value = concatMapWithArray(items.value, res);
|
items.value = concatMapWithArray(items.value, res);
|
||||||
more.value = moreAvailable;
|
more.value = false;
|
||||||
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;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appear(IntersectionObserver)によってfetchMoreが呼ばれる場合、
|
* Appear(IntersectionObserver)によってfetchMoreが呼ばれる場合、
|
||||||
|
@ -329,7 +346,13 @@ const appearFetchMore = async (): Promise<void> => {
|
||||||
fetchMoreAppearTimeout();
|
fetchMoreAppearTimeout();
|
||||||
};
|
};
|
||||||
|
|
||||||
const isTop = (): boolean => isBackTop.value || (props.pagination.scrollAtTop ? isBottomVisible : isTopVisible)(contentEl.value!, TOLERANCE);
|
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);
|
||||||
|
|
||||||
watch(visibility, () => {
|
watch(visibility, () => {
|
||||||
if (visibility.value === 'hidden') {
|
if (visibility.value === 'hidden') {
|
||||||
|
@ -423,7 +446,7 @@ onActivated(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
onDeactivated(() => {
|
onDeactivated(() => {
|
||||||
isBackTop.value = props.pagination.scrollAtTop ? window.scrollY >= (rootEl.value ? rootEl.value.scrollHeight - window.innerHeight : 0) : window.scrollY === 0;
|
isBackTop.value = props.pagination.reversed ? window.scrollY >= (rootEl.value ? rootEl.value.scrollHeight - window.innerHeight : 0) : window.scrollY === 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
function toBottom() {
|
function toBottom() {
|
||||||
|
@ -432,7 +455,7 @@ function toBottom() {
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
init().then(() => {
|
init().then(() => {
|
||||||
if (props.pagination.scrollAtTop) {
|
if (props.pagination.reversed) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
setTimeout(toBottom, 800);
|
setTimeout(toBottom, 800);
|
||||||
|
|
||||||
|
@ -463,7 +486,7 @@ defineExpose({
|
||||||
queue,
|
queue,
|
||||||
backed: backed.value,
|
backed: backed.value,
|
||||||
more,
|
more,
|
||||||
reload: init,
|
reload,
|
||||||
prepend,
|
prepend,
|
||||||
append: appendItem,
|
append: appendItem,
|
||||||
removeItem,
|
removeItem,
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* 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,6 +15,7 @@ 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) {
|
||||||
|
@ -35,5 +36,6 @@ 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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -85,7 +85,6 @@ 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 ? ({
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<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">
|
||||||
|
@ -65,20 +64,15 @@
|
||||||
</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"
|
||||||
|
@ -97,8 +91,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { provide, ref, shallowRef, toRefs, computed, onMounted, defineAsyncComponent } from 'vue';
|
import { provide, ref, shallowRef, toRefs, computed, onMounted } 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';
|
||||||
|
@ -115,10 +110,7 @@ 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 = [
|
||||||
{
|
{
|
||||||
|
@ -487,33 +479,26 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
> .main {
|
> .main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
overflow: auto;
|
height: 100%;
|
||||||
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;
|
||||||
|
|
|
@ -21,11 +21,6 @@ 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,
|
||||||
|
@ -67,10 +62,10 @@ export default defineComponent({
|
||||||
h('i', {
|
h('i', {
|
||||||
class: 'ti ti-chevron-up icon',
|
class: 'ti ti-chevron-up icon',
|
||||||
}),
|
}),
|
||||||
props.reversed ? getDateText(props.items[i + 1].createdAt) : getDateText(item.createdAt),
|
getDateText(item.createdAt)
|
||||||
]),
|
]),
|
||||||
h('span', [
|
h('span', [
|
||||||
props.reversed ? getDateText(item.createdAt) : getDateText(props.items[i + 1].createdAt),
|
getDateText(props.items[i + 1].createdAt),
|
||||||
h('i', {
|
h('i', {
|
||||||
class: 'ti ti-chevron-down icon',
|
class: 'ti ti-chevron-down icon',
|
||||||
})
|
})
|
||||||
|
@ -90,17 +85,31 @@ 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.root]: true,
|
[$style['date-separated-list']]: 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, [$style['no-move-transition']]: props.noMoveTransition },
|
class: classes,
|
||||||
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 });
|
||||||
|
@ -109,35 +118,29 @@ export default defineComponent({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.root {
|
.date-separated-list {
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
> .list-enter-active {
|
&.deny-move-transition > .list-move {
|
||||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
transition: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .list-leave-active {
|
> .list-enter-active {
|
||||||
position: absolute;
|
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
> *:empty {
|
> *:empty {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-move-transition {
|
// > *:not(:last-child) {
|
||||||
&:global {
|
// margin-bottom: var(--margin);
|
||||||
> .list-move {
|
// }
|
||||||
transition: none !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,50 +8,35 @@
|
||||||
</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="reversed ? 'up' : 'down'"
|
:direction="pagination.reversed ? 'up' : 'down'"
|
||||||
:reversed="reversed"
|
:reversed="pagination.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, ref, watchEffect } from 'vue';
|
import { shallowRef } 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,
|
||||||
|
|
|
@ -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" :reversed="inChannel" @queue="queueUpdated"/>
|
<XNotes ref="tlElement" class="tl" :pagination="pagination" @queue="queueUpdated" v-follow="!!props.channelId"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -72,9 +72,7 @@ let endpoint = ref(null);
|
||||||
|
|
||||||
const pagination = computed(() => ({
|
const pagination = computed(() => ({
|
||||||
endpoint: endpoint.value,
|
endpoint: endpoint.value,
|
||||||
scrollAtTop: inChannel.value,
|
reversed: inChannel.value,
|
||||||
pageEl: scroller.value,
|
|
||||||
limit: 15,
|
|
||||||
params: {
|
params: {
|
||||||
untilDate: timetravelTarget.value?.getTime(),
|
untilDate: timetravelTarget.value?.getTime(),
|
||||||
...query,
|
...query,
|
||||||
|
|
|
@ -19,9 +19,6 @@
|
||||||
<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">
|
||||||
|
@ -56,6 +53,7 @@
|
||||||
<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>
|
||||||
|
@ -1043,7 +1041,16 @@ 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;
|
||||||
|
@ -1060,32 +1067,6 @@ 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;
|
||||||
|
|
Loading…
Reference in New Issue