summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortamaina <tamaina@hotmail.co.jp>2023-01-13 18:25:40 +0900
committerGitHub <noreply@github.com>2023-01-13 18:25:40 +0900
commitd2204fd5c8fb2361e9c29ed31cd0d40eb69d2f4b (patch)
tree3479f9cde6139af5cebf94cf6de005e16c404e21
parentUpdate CHANGELOG.md (diff)
downloadsharkey-d2204fd5c8fb2361e9c29ed31cd0d40eb69d2f4b.tar.gz
sharkey-d2204fd5c8fb2361e9c29ed31cd0d40eb69d2f4b.tar.bz2
sharkey-d2204fd5c8fb2361e9c29ed31cd0d40eb69d2f4b.zip
refactor: pagination/date-separated-list系処理を良い感じに? (#8209)
* pages/messaging/messaging-room.vue * wip * wip * wip??? * wip? * :v: * messaaging-room.form.vue rewrite to compositon api * refactor * 関心事でないのでとりあえず置いておく * :art: * :art: * i18n.ts * fix scroll container find function * fix * FIX * :v: * Fix scroll bottom detect * wip * aaaaaaaaaaa * rename * fix * fix? * :v: * :v: * clean up * clena up * refactor * scroll event once or not * fix * fix once * add safe-area-inset-bottom to spacer * fix * :v: * :art: * fix * fix * wip * :v: * clean up * fix lint * Update packages/client/src/components/global/sticky-container.vue Co-authored-by: Johann150 <johann.galle@protonmail.com> * Update packages/client/src/components/ui/pagination.vue Co-authored-by: Johann150 <johann.galle@protonmail.com> * Update packages/client/src/pages/messaging/messaging-room.form.vue Co-authored-by: Johann150 <johann.galle@protonmail.com> * clean up: single line comment * https://github.com/misskey-dev/misskey/pull/8209#discussion_r867386077 * fix * asobi → tolerance * pick form * pick message * pick room * fix lint * fix scroll? * fix scroll.ts * fix directives/sticky-container * update global/sticky-container.vue * fix, :art: * revert merge * :v: * fix lint errors * :art: * Update packages/client/src/types/date-separated-list.ts Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * https://github.com/misskey-dev/misskey/pull/8209#discussion_r917225080 * use ' * Update packages/client/src/scripts/scroll.ts Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * use Number.EPSILON Co-authored-by: acid-chicken <root@acid-chicken.com> * revert * fix * fix * Use % instead of vh * :art: * :art: * :art: * wip * wip * css modules Co-authored-by: Johann150 <johann.galle@protonmail.com> Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
-rw-r--r--packages/frontend/src/components/MkDateSeparatedList.vue188
-rw-r--r--packages/frontend/src/components/MkNotes.vue11
-rw-r--r--packages/frontend/src/components/MkPagination.vue286
-rw-r--r--packages/frontend/src/pages/messaging/index.vue2
-rw-r--r--packages/frontend/src/pages/messaging/messaging-room.form.vue3
-rw-r--r--packages/frontend/src/pages/messaging/messaging-room.vue91
-rw-r--r--packages/frontend/src/scripts/scroll.ts120
-rw-r--r--packages/frontend/src/types/date-separated-list.ts6
-rw-r--r--packages/frontend/src/ui/visitor/a.vue4
9 files changed, 445 insertions, 266 deletions
diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue
index 5d8e14c3c8..cb88444d34 100644
--- a/packages/frontend/src/components/MkDateSeparatedList.vue
+++ b/packages/frontend/src/components/MkDateSeparatedList.vue
@@ -1,13 +1,14 @@
<script lang="ts">
-import { defineComponent, h, PropType, TransitionGroup } from 'vue';
+import { defineComponent, h, PropType, TransitionGroup, useCssModule } from 'vue';
import MkAd from '@/components/global/MkAd.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: {
@@ -33,6 +34,7 @@ export default defineComponent({
},
setup(props, { slots, expose }) {
+ const $style = useCssModule();
function getDateText(time: string) {
const date = new Date(time).getDate();
const month = new Date(time).getMonth() + 1;
@@ -57,21 +59,25 @@ export default defineComponent({
new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()
) {
const separator = h('div', {
- class: 'separator',
+ class: $style['separator'],
key: item.id + ':separator',
}, h('p', {
- class: 'date',
+ class: $style['date'],
}, [
- h('span', [
+ h('span', {
+ class: $style['date-1'],
+ }, [
h('i', {
- class: 'ti ti-chevron-up icon',
+ class: `ti ti-chevron-up ${$style['date-1-icon']}`,
}),
getDateText(item.createdAt),
]),
- h('span', [
+ h('span', {
+ class: $style['date-2'],
+ }, [
getDateText(props.items[i + 1].createdAt),
h('i', {
- class: 'ti ti-chevron-down icon',
+ class: `ti ti-chevron-down ${$style['date-2-icon']}`,
}),
]),
]));
@@ -89,100 +95,138 @@ export default defineComponent({
}
});
+ function onBeforeLeave(el: HTMLElement) {
+ el.style.top = `${el.offsetTop}px`;
+ el.style.left = `${el.offsetLeft}px`;
+ }
+ function onLeaveCanceled(el: HTMLElement) {
+ el.style.top = '';
+ el.style.left = '';
+ }
+
return () => h(
defaultStore.state.animation ? TransitionGroup : 'div',
- defaultStore.state.animation ? {
- class: 'sqadhkmv' + (props.noGap ? ' noGap' : ''),
- name: 'list',
- tag: 'div',
- 'data-direction': props.direction,
- 'data-reversed': props.reversed ? 'true' : 'false',
- } : {
- class: 'sqadhkmv' + (props.noGap ? ' noGap' : ''),
+ {
+ class: {
+ [$style['date-separated-list']]: true,
+ [$style['date-separated-list-nogap']]: props.noGap,
+ [$style['reversed']]: props.reversed,
+ [$style['direction-down']]: props.direction === 'down',
+ [$style['direction-up']]: props.direction === 'up',
+ },
+ ...(defaultStore.state.animation ? {
+ name: 'list',
+ tag: 'div',
+ onBeforeLeave,
+ onLeaveCanceled,
+ } : {}),
},
{ default: renderChildren });
},
});
</script>
-<style lang="scss">
-.sqadhkmv {
+<style lang="scss" module>
+.date-separated-list {
container-type: inline-size;
- > *:empty {
- display: none;
+ &:global {
+ > .list-move {
+ transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
}
- > *:not(:last-child) {
- margin-bottom: var(--margin);
+ &.deny-move-transition > .list-move {
+ transition: none !important;
}
- > .list-move {
- transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
+ > .list-leave-active,
+ > .list-enter-active {
+ transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
}
- > .list-enter-active {
+ > .list-leave-from,
+ > .list-leave-to,
+ > .list-leave-active {
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
+ position: absolute !important;
}
- &[data-direction="up"] {
- > .list-enter-from {
- opacity: 0;
- transform: translateY(64px);
- }
+ > *:empty {
+ display: none;
}
- &[data-direction="down"] {
- > .list-enter-from {
- opacity: 0;
- transform: translateY(-64px);
+ > *:not(:last-child) {
+ margin-bottom: var(--margin);
+ }
+ }
+}
+
+.date-separated-list-nogap {
+ > * {
+ margin: 0 !important;
+ border: none;
+ border-radius: 0;
+ box-shadow: none;
+
+ &:not(:last-child) {
+ border-bottom: solid 0.5px var(--divider);
}
}
+}
- > .separator {
- text-align: center;
+.direction-up {
+ &:global {
+ > .list-enter-from,
+ > .list-leave-to {
+ opacity: 0;
+ transform: translateY(64px);
+ }
+ }
+}
+.direction-down {
+ &:global {
+ > .list-enter-from,
+ > .list-leave-to {
+ opacity: 0;
+ transform: translateY(-64px);
+ }
+ }
+}
- > .date {
- display: inline-block;
- position: relative;
- margin: 0;
- padding: 0 16px;
- line-height: 32px;
- text-align: center;
- font-size: 12px;
- color: var(--dateLabelFg);
+.reversed {
+ display: flex;
+ flex-direction: column-reverse;
+}
- > span {
- &:first-child {
- margin-right: 8px;
+.separator {
+ text-align: center;
+}
- > .icon {
- margin-right: 8px;
- }
- }
+.date {
+ display: inline-block;
+ position: relative;
+ margin: 0;
+ padding: 0 16px;
+ line-height: 32px;
+ text-align: center;
+ font-size: 12px;
+ color: var(--dateLabelFg);
+}
- &:last-child {
- margin-left: 8px;
+.date-1 {
+ margin-right: 8px;
+}
- > .icon {
- margin-left: 8px;
- }
- }
- }
- }
- }
+.date-1-icon {
+ margin-right: 8px;
+}
- &.noGap {
- > * {
- margin: 0 !important;
- border: none;
- border-radius: 0;
- box-shadow: none;
+.date-2 {
+ margin-left: 8px;
+}
- &:not(:last-child) {
- border-bottom: solid 0.5px var(--divider);
- }
- }
- }
+.date-2-icon {
+ margin-left: 8px;
}
</style>
+
diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue
index 73659fd501..f9952e4245 100644
--- a/packages/frontend/src/components/MkNotes.vue
+++ b/packages/frontend/src/components/MkNotes.vue
@@ -9,7 +9,16 @@
<template #default="{ items: notes }">
<div :class="[$style.root, { [$style.noGap]: noGap }]">
- <MkDateSeparatedList ref="notes" v-slot="{ item: note }" :items="notes" :direction="pagination.reversed ? 'up' : 'down'" :reversed="pagination.reversed" :no-gap="noGap" :ad="true" :class="$style.notes">
+ <MkDateSeparatedList
+ ref="notes"
+ v-slot="{ item: note }"
+ :items="notes"
+ :direction="pagination.reversed ? 'up' : 'down'"
+ :reversed="pagination.reversed"
+ :no-gap="noGap"
+ :ad="true"
+ :class="$style.notes"
+ >
<XNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/>
</MkDateSeparatedList>
</div>
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index b92e6d2360..a78e7e064a 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -15,14 +15,14 @@
<div v-else ref="rootEl">
<div v-show="pagination.reversed && more" key="_more_" class="cxiknjgy _margin">
- <MkButton v-if="!moreFetching" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMoreAhead">
+ <MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
{{ i18n.ts.loadMore }}
</MkButton>
<MkLoading v-else class="loading"/>
</div>
- <slot :items="items"></slot>
+ <slot :items="items" :fetching="fetching || moreFetching"></slot>
<div v-show="!pagination.reversed && more" key="_more_" class="cxiknjgy _margin">
- <MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
+ <MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
{{ i18n.ts.loadMore }}
</MkButton>
<MkLoading v-else class="loading"/>
@@ -31,15 +31,18 @@
</Transition>
</template>
-<script lang="ts" setup>
-import { computed, ComputedRef, isRef, markRaw, onActivated, onDeactivated, Ref, ref, shallowRef, watch } from 'vue';
+<script lang="ts">
+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, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
+import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@/scripts/scroll';
import MkButton from '@/components/MkButton.vue';
+import { defaultStore } from '@/store';
+import { MisskeyEntity } from '@/types/date-separated-list';
import { i18n } from '@/i18n';
const SECOND_FETCH_LIMIT = 30;
+const TOLERANCE = 16;
export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints> = {
endpoint: E;
@@ -58,8 +61,11 @@ export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints>
reversed?: boolean;
offsetMode?: boolean;
-};
+ pageEl?: HTMLElement;
+};
+</script>
+<script lang="ts" setup>
const props = withDefaults(defineProps<{
pagination: Paging;
disableAutoLoad?: boolean;
@@ -72,21 +78,73 @@ const emit = defineEmits<{
(ev: 'queue', count: number): void;
}>();
-type Item = { id: string; [another: string]: unknown; };
+let rootEl = $shallowRef<HTMLElement>();
+
+// 遡り中かどうか
+let backed = $ref(false);
+
+let scrollRemove = $ref<(() => void) | null>(null);
-const rootEl = shallowRef<HTMLElement>();
-const items = ref<Item[]>([]);
-const queue = ref<Item[]>([]);
+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);
+const {
+ enableInfiniteScroll
+} = defaultStore.reactiveState;
-const init = async (): Promise<void> => {
+const contentEl = $computed(() => props.pagination.pageEl || rootEl);
+const scrollableElement = $computed(() => getScrollContainer(contentEl));
+
+// 先頭が表示されているかどうかを検出
+// https://qiita.com/mkataigi/items/0154aefd2223ce23398e
+let scrollObserver = $ref<IntersectionObserver>();
+
+watch([() => props.pagination.reversed, $$(scrollableElement)], () => {
+ if (scrollObserver) scrollObserver.disconnect();
+
+ scrollObserver = new IntersectionObserver(entries => {
+ backed = entries[0].isIntersecting;
+ }, {
+ root: scrollableElement,
+ rootMargin: props.pagination.reversed ? '-100% 0px 100% 0px' : '100% 0px -100% 0px',
+ threshold: 0.01,
+ });
+}, { immediate: true });
+
+watch($$(rootEl), () => {
+ scrollObserver.disconnect();
+ nextTick(() => {
+ if (rootEl) scrollObserver.observe(rootEl);
+ });
+});
+
+watch([$$(backed), $$(contentEl)], () => {
+ if (!backed) {
+ if (!contentEl) return;
+
+ scrollRemove = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl, executeQueue, TOLERANCE);
+ } 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 : {};
@@ -96,18 +154,15 @@ const init = async (): Promise<void> => {
}).then(res => {
for (let i = 0; i < res.length; i++) {
const item = res[i];
- if (props.pagination.reversed) {
- if (i === res.length - 2) item._shouldInsertAd_ = true;
- } else {
- if (i === 3) item._shouldInsertAd_ = true;
- }
+ if (i === 3) item._shouldInsertAd_ = true;
}
if (!props.pagination.noPaging && (res.length > (props.pagination.limit || 10))) {
res.pop();
- items.value = props.pagination.reversed ? [...res].reverse() : res;
+ if (props.pagination.reversed) moreFetching.value = true;
+ items.value = res;
more.value = true;
} else {
- items.value = props.pagination.reversed ? [...res].reverse() : res;
+ items.value = res;
more.value = false;
}
offset.value = res.length;
@@ -117,17 +172,16 @@ const init = async (): Promise<void> => {
error.value = true;
fetching.value = false;
});
-};
+}
-const reload = (): void => {
+const reload = (): Promise<void> => {
items.value = [];
- init();
+ return init();
};
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,
@@ -142,22 +196,52 @@ const fetchMore = async (): Promise<void> => {
}).then(res => {
for (let i = 0; i < res.length; i++) {
const item = res[i];
- if (props.pagination.reversed) {
- if (i === res.length - 9) item._shouldInsertAd_ = true;
- } else {
- if (i === 10) item._shouldInsertAd_ = true;
- }
+ if (i === 10) item._shouldInsertAd_ = true;
}
+
+ const reverseConcat = _res => {
+ const oldHeight = scrollableElement ? scrollableElement.scrollHeight : getBodyScrollHeight();
+ const oldScroll = scrollableElement ? scrollableElement.scrollTop : window.scrollY;
+
+ items.value = items.value.concat(_res);
+
+ return nextTick(() => {
+ if (scrollableElement) {
+ scroll(scrollableElement, { top: oldScroll + (scrollableElement.scrollHeight - oldHeight), behavior: 'instant' });
+ } else {
+ window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' });
+ }
+
+ return nextTick();
+ });
+ };
+
if (res.length > SECOND_FETCH_LIMIT) {
res.pop();
- items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
- more.value = true;
+
+ if (props.pagination.reversed) {
+ reverseConcat(res).then(() => {
+ more.value = true;
+ moreFetching.value = false;
+ });
+ } else {
+ items.value = items.value.concat(res);
+ more.value = true;
+ moreFetching.value = false;
+ }
} else {
- items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
- more.value = false;
+ if (props.pagination.reversed) {
+ reverseConcat(res).then(() => {
+ more.value = false;
+ moreFetching.value = false;
+ });
+ } else {
+ items.value = items.value.concat(res);
+ more.value = false;
+ moreFetching.value = false;
+ }
}
offset.value += res.length;
- moreFetching.value = false;
}, err => {
moreFetching.value = false;
});
@@ -180,10 +264,10 @@ const fetchMoreAhead = async (): Promise<void> => {
}).then(res => {
if (res.length > SECOND_FETCH_LIMIT) {
res.pop();
- items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
+ items.value = items.value.concat(res);
more.value = true;
} else {
- items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
+ items.value = items.value.concat(res);
more.value = false;
}
offset.value += res.length;
@@ -193,106 +277,96 @@ const fetchMoreAhead = async (): Promise<void> => {
});
};
-const prepend = (item: Item): void => {
- if (props.pagination.reversed) {
- if (rootEl.value) {
- const container = getScrollContainer(rootEl.value);
- if (container == null) {
- // TODO?
- } else {
- const pos = getScrollPosition(rootEl.value);
- const viewHeight = container.clientHeight;
- const height = container.scrollHeight;
- const isBottom = (pos + viewHeight > height - 32);
- if (isBottom) {
- // オーバーフローしたら古いアイテムは捨てる
- if (items.value.length >= props.displayLimit) {
- // このやり方だとVue 3.2以降アニメーションが動かなくなる
- //items.value = items.value.slice(-props.displayLimit);
- while (items.value.length >= props.displayLimit) {
- items.value.shift();
- }
- more.value = true;
- }
- }
- }
- }
- items.value.push(item);
- // TODO
- } else {
- // 初回表示時はunshiftだけでOK
- if (!rootEl.value) {
- items.value.unshift(item);
- return;
- }
+const prepend = (item: MisskeyEntity): void => {
+ // 初回表示時はunshiftだけでOK
+ if (!rootEl) {
+ items.value.unshift(item);
+ return;
+ }
- const isTop = isBackTop.value || (document.body.contains(rootEl.value) && isTopVisible(rootEl.value));
+ const isTop = isBackTop.value || (props.pagination.reversed ? isBottomVisible : isTopVisible)(contentEl, TOLERANCE);
- if (isTop) {
- // Prepend the item
- items.value.unshift(item);
+ if (isTop) unshiftItems([item]);
+ else prependQueue(item);
+};
- // オーバーフローしたら古いアイテムは捨てる
- if (items.value.length >= props.displayLimit) {
- // このやり方だとVue 3.2以降アニメーションが動かなくなる
- //this.items = items.value.slice(0, props.displayLimit);
- while (items.value.length >= props.displayLimit) {
- items.value.pop();
- }
- more.value = true;
- }
- } else {
- queue.value.push(item);
- onScrollTop(rootEl.value, () => {
- for (const item of queue.value) {
- prepend(item);
- }
- queue.value = [];
- });
- }
+function unshiftItems(newItems: MisskeyEntity[]) {
+ const length = newItems.length + items.value.length;
+ items.value = [ ...newItems, ...items.value ].slice(0, props.displayLimit);
+
+ if (length >= props.displayLimit) more.value = true;
+}
+
+function executeQueue() {
+ if (queue.value.length === 0) return;
+ unshiftItems(queue.value);
+ queue.value = [];
+}
+
+function prependQueue(newItem: MisskeyEntity) {
+ queue.value.unshift(newItem);
+ if (queue.value.length >= props.displayLimit) {
+ queue.value.pop();
}
-};
+}
-const append = (item: Item): void => {
+const appendItem = (item: MisskeyEntity): void => {
items.value.push(item);
};
-const removeItem = (finder: (item: Item) => boolean) => {
+const removeItem = (finder: (item: MisskeyEntity) => boolean) => {
const i = items.value.findIndex(finder);
items.value.splice(i, 1);
};
-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 });
-
-init();
+const inited = init();
onActivated(() => {
isBackTop.value = false;
});
onDeactivated(() => {
- isBackTop.value = window.scrollY === 0;
+ isBackTop.value = props.pagination.reversed ? window.scrollY >= (rootEl ? rootEl?.scrollHeight - window.innerHeight : 0) : window.scrollY === 0;
+});
+
+function toBottom() {
+ scrollToBottom(contentEl);
+}
+
+onMounted(() => {
+ inited.then(() => {
+ if (props.pagination.reversed) {
+ nextTick(() => {
+ setTimeout(toBottom, 800);
+
+ // scrollToBottomでmoreFetchingボタンが画面外まで出るまで
+ // more = trueを遅らせる
+ setTimeout(() => {
+ moreFetching.value = false;
+ }, 2000);
+ });
+ }
+ });
+});
+
+onBeforeUnmount(() => {
+ scrollObserver.disconnect();
});
defineExpose({
items,
queue,
backed,
+ more,
+ inited,
reload,
prepend,
- append,
+ append: appendItem,
removeItem,
updateItem,
});
diff --git a/packages/frontend/src/pages/messaging/index.vue b/packages/frontend/src/pages/messaging/index.vue
index e751754503..4801348bdb 100644
--- a/packages/frontend/src/pages/messaging/index.vue
+++ b/packages/frontend/src/pages/messaging/index.vue
@@ -10,7 +10,7 @@
v-for="(message, i) in messages"
:key="message.id"
v-anim="i"
- class="message"
+ class="message _panel"
:class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($i.id) : message.isRead }"
:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
:data-index="i"
diff --git a/packages/frontend/src/pages/messaging/messaging-room.form.vue b/packages/frontend/src/pages/messaging/messaging-room.form.vue
index e880129033..6a49d0e89d 100644
--- a/packages/frontend/src/pages/messaging/messaging-room.form.vue
+++ b/packages/frontend/src/pages/messaging/messaging-room.form.vue
@@ -256,9 +256,10 @@ defineExpose({
border: none;
border-radius: 0;
box-shadow: none;
- background: transparent;
box-sizing: border-box;
color: var(--fg);
+ background: rgba(12, 18, 16, 0.85);
+ backdrop-filter: var(--blur, blur(15px));
}
footer {
diff --git a/packages/frontend/src/pages/messaging/messaging-room.vue b/packages/frontend/src/pages/messaging/messaging-room.vue
index 01b227a672..f1749d449d 100644
--- a/packages/frontend/src/pages/messaging/messaging-room.vue
+++ b/packages/frontend/src/pages/messaging/messaging-room.vue
@@ -1,51 +1,48 @@
<template>
<div
ref="rootEl"
- class=""
+ class="root"
@dragover.prevent.stop="onDragover"
@drop.prevent.stop="onDrop"
>
- <div class="mk-messaging-room">
- <div class="body">
- <MkPagination v-if="pagination" ref="pagingComponent" :key="userAcct || groupId" :pagination="pagination">
- <template #empty>
- <div class="_fullinfo">
- <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
- <div>{{ i18n.ts.noMessagesYet }}</div>
- </div>
- </template>
-
- <template #default="{ items: messages, fetching: pFetching }">
- <MkDateSeparatedList
- v-if="messages.length > 0"
- v-slot="{ item: message }"
- :class="{ messages: true, 'deny-move-transition': pFetching }"
- :items="messages"
- direction="up"
- reversed
- >
- <XMessage :key="message.id" :message="message" :is-group="group != null"/>
- </MkDateSeparatedList>
+ <div class="body">
+ <MkPagination v-if="pagination" ref="pagingComponent" :key="userAcct || groupId" :pagination="pagination">
+ <template #empty>
+ <div class="_fullinfo">
+ <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
+ <div>{{ i18n.ts.noMessagesYet }}</div>
+ </div>
+ </template>
+ <template #default="{ items: messages, fetching: pFetching }">
+ <MkDateSeparatedList
+ v-if="messages.length > 0"
+ v-slot="{ item: message }"
+ :class="{ messages: true, 'deny-move-transition': pFetching }"
+ :items="messages"
+ direction="up"
+ reversed
+ >
+ <XMessage :key="message.id" :message="message" :is-group="group != null"/>
+ </MkDateSeparatedList>
+ </template>
+ </MkPagination>
+ </div>
+ <footer>
+ <div v-if="typers.length > 0" class="typers">
+ <I18n :src="i18n.ts.typingUsers" text-tag="span" class="users">
+ <template #users>
+ <b v-for="typer in typers" :key="typer.id" class="user">{{ typer.username }}</b>
</template>
- </MkPagination>
+ </I18n>
+ <MkEllipsis/>
</div>
- <footer>
- <div v-if="typers.length > 0" class="typers">
- <I18n :src="i18n.ts.typingUsers" text-tag="span" class="users">
- <template #users>
- <b v-for="typer in typers" :key="typer.id" class="user">{{ typer.username }}</b>
- </template>
- </I18n>
- <MkEllipsis/>
+ <Transition :name="animation ? 'fade' : ''">
+ <div v-show="showIndicator" class="new-message">
+ <button class="_buttonPrimary" @click="onIndicatorClick"><i class="fas ti-fw fa-arrow-circle-down"></i>{{ i18n.ts.newMessageExists }}</button>
</div>
- <Transition :name="animation ? 'fade' : ''">
- <div v-show="showIndicator" class="new-message">
- <button class="_buttonPrimary" @click="onIndicatorClick"><i class="fas ti-fw fa-arrow-circle-down"></i>{{ i18n.ts.newMessageExists }}</button>
- </div>
- </Transition>
- <XForm v-if="!fetching" ref="formEl" :user="user" :group="group" class="form"/>
- </footer>
- </div>
+ </Transition>
+ <XForm v-if="!fetching" ref="formEl" :user="user" :group="group" class="form"/>
+ </footer>
</div>
</template>
@@ -140,7 +137,9 @@ async function fetch() {
document.addEventListener('visibilitychange', onVisibilitychange);
nextTick(() => {
- thisScrollToBottom();
+ pagingComponent.inited.then(() => {
+ thisScrollToBottom();
+ });
window.setTimeout(() => {
fetching = false;
}, 300);
@@ -305,11 +304,12 @@ definePageMetadata(computed(() => !fetching ? user ? {
</script>
<style lang="scss" scoped>
-.mk-messaging-room {
- position: relative;
- overflow: auto;
+.root {
+ display: content;
> .body {
+ min-height: 80%;
+
.more {
display: block;
margin: 16px auto;
@@ -349,8 +349,9 @@ definePageMetadata(computed(() => !fetching ? user ? {
width: 100%;
position: sticky;
z-index: 2;
- bottom: 0;
padding-top: 8px;
+ bottom: 0;
+ bottom: env(safe-area-inset-bottom, 0px);
> .new-message {
width: 100%;
@@ -395,6 +396,8 @@ definePageMetadata(computed(() => !fetching ? user ? {
max-height: 12em;
overflow-y: scroll;
border-top: solid 0.5px var(--divider);
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
}
}
}
diff --git a/packages/frontend/src/scripts/scroll.ts b/packages/frontend/src/scripts/scroll.ts
index f5bc6bf9ce..e3d9dc00c2 100644
--- a/packages/frontend/src/scripts/scroll.ts
+++ b/packages/frontend/src/scripts/scroll.ts
@@ -10,53 +10,67 @@ export function getScrollContainer(el: HTMLElement | null): HTMLElement | null {
}
}
-export function getScrollPosition(el: Element | null): number {
- const container = getScrollContainer(el);
- return container == null ? window.scrollY : container.scrollTop;
+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 isTopVisible(el: Element | null): boolean {
- const scrollTop = getScrollPosition(el);
- const topPosition = el.offsetTop; // TODO: container内でのelの相対位置を取得できればより正確になる
-
- return scrollTop <= topPosition;
+export function getScrollPosition(el: HTMLElement | null): number {
+ const container = getScrollContainer(el);
+ return container == null ? window.scrollY : container.scrollTop;
}
-export function isBottomVisible(el: HTMLElement, tolerance = 1, container = getScrollContainer(el)) {
- if (container) return el.scrollHeight <= container.clientHeight + Math.abs(container.scrollTop) + tolerance;
- return el.scrollHeight <= window.innerHeight + window.scrollY + tolerance;
-}
+export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance: number = 1, once: boolean = false) {
+ // とりあえず評価してみる
+ if (isTopVisible(el)) {
+ cb();
+ if (once) return null;
+ }
-export function onScrollTop(el: Element, cb) {
const container = getScrollContainer(el) || window;
+
const onScroll = ev => {
if (!document.body.contains(el)) return;
- if (isTopVisible(el)) {
+ if (isTopVisible(el, tolerance)) {
cb();
- container.removeEventListener('scroll', onScroll);
+ if (once) removeListener();
}
};
+
+ function removeListener() { container.removeEventListener('scroll', onScroll); }
container.addEventListener('scroll', onScroll, { passive: true });
+ return removeListener;
}
-export function onScrollBottom(el: Element, cb) {
- const container = getScrollContainer(el) || window;
+export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance: number = 1, once: boolean = false) {
+ const container = getScrollContainer(el);
+
+ // とりあえず評価してみる
+ if (isBottomVisible(el, tolerance, container)) {
+ cb();
+ if (once) return null;
+ }
+
+ const containerOrWindow = container || window;
const onScroll = ev => {
if (!document.body.contains(el)) return;
- const pos = getScrollPosition(el);
- if (pos + el.clientHeight > el.scrollHeight - 1) {
+ if (isBottomVisible(el, 1, container)) {
cb();
- container.removeEventListener('scroll', onScroll);
+ if (once) removeListener();
}
};
- container.addEventListener('scroll', onScroll, { passive: true });
+
+ function removeListener() {
+ containerOrWindow.removeEventListener('scroll', onScroll);
+ }
+ containerOrWindow.addEventListener('scroll', onScroll, { passive: true });
+ return removeListener;
}
-export function scroll(el: Element, options: {
- top?: number;
- left?: number;
- behavior?: ScrollBehavior;
-}) {
+export function scroll(el: HTMLElement, options: ScrollToOptions | undefined) {
const container = getScrollContainer(el);
if (container == null) {
window.scroll(options);
@@ -65,21 +79,51 @@ export function scroll(el: Element, options: {
}
}
-export function scrollToTop(el: Element, options: { behavior?: ScrollBehavior; } = {}) {
+/**
+ * Scroll to Top
+ * @param el Scroll container element
+ * @param options Scroll options
+ */
+export function scrollToTop(el: HTMLElement, options: { behavior?: ScrollBehavior; } = {}) {
scroll(el, { top: 0, ...options });
}
-export function scrollToBottom(el: Element, options: { behavior?: ScrollBehavior; } = {}) {
- scroll(el, { top: 99999, ...options }); // TODO: ちゃんと計算する
+/**
+ * Scroll to Bottom
+ * @param el Content element
+ * @param options Scroll options
+ * @param container Scroll container element
+ */
+export function scrollToBottom(
+ el: HTMLElement,
+ options: ScrollToOptions = {},
+ container = getScrollContainer(el),
+) {
+ if (container) {
+ container.scroll({ top: el.scrollHeight - container.clientHeight + getStickyTop(el, container) || 0, ...options });
+ } else {
+ window.scroll({
+ top: (el.scrollHeight - window.innerHeight + getStickyTop(el, container) + (window.innerWidth <= 500 ? 96 : 0)) || 0,
+ ...options
+ });
+ }
}
-export function isBottom(el: Element, asobi = 0) {
- const container = getScrollContainer(el);
- const current = container
- ? el.scrollTop + el.offsetHeight
- : window.scrollY + window.innerHeight;
- const max = container
- ? el.scrollHeight
- : document.body.offsetHeight;
- return current >= (max - asobi);
+export function isTopVisible(el: HTMLElement, tolerance: number = 1): boolean {
+ const scrollTop = getScrollPosition(el);
+ return scrollTop <= tolerance;
+}
+
+export function isBottomVisible(el: HTMLElement, tolerance = 1, container = getScrollContainer(el)) {
+ if (container) return el.scrollHeight <= container.clientHeight + Math.abs(container.scrollTop) + tolerance;
+ return el.scrollHeight <= window.innerHeight + window.scrollY + tolerance;
+}
+
+// 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
+ );
}
diff --git a/packages/frontend/src/types/date-separated-list.ts b/packages/frontend/src/types/date-separated-list.ts
new file mode 100644
index 0000000000..81bd87fc8e
--- /dev/null
+++ b/packages/frontend/src/types/date-separated-list.ts
@@ -0,0 +1,6 @@
+export type MisskeyEntity = {
+ id: string;
+ createdAt: string;
+ _shouldInsertAd_?: boolean;
+ [x: string]: any;
+};
diff --git a/packages/frontend/src/ui/visitor/a.vue b/packages/frontend/src/ui/visitor/a.vue
index 67c4e122b4..9494b1b705 100644
--- a/packages/frontend/src/ui/visitor/a.vue
+++ b/packages/frontend/src/ui/visitor/a.vue
@@ -37,12 +37,11 @@
</template>
<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
+import { defineComponent } from 'vue';
import XHeader from './header.vue';
import { host, instanceName } from '@/config';
import { search } from '@/scripts/search';
import * as os from '@/os';
-import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue';
import { ColdDeviceStorage } from '@/store';
import { mainRouter } from '@/router';
@@ -52,7 +51,6 @@ const DESKTOP_THRESHOLD = 1100;
export default defineComponent({
components: {
XHeader,
- MkPagination,
MkButton,
},