wip
This commit is contained in:
@@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import { ChatService } from '@/core/ChatService.js';
|
||||
import Channel, { type MiChannelService } from '../channel.js';
|
||||
|
||||
class ChatChannel extends Channel {
|
||||
@@ -17,6 +18,8 @@ class ChatChannel extends Channel {
|
||||
private otherId: string;
|
||||
|
||||
constructor(
|
||||
private chatService: ChatService,
|
||||
|
||||
id: string,
|
||||
connection: Channel['connection'],
|
||||
) {
|
||||
@@ -36,6 +39,17 @@ class ChatChannel extends Channel {
|
||||
this.send(data.type, data.body);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onMessage(type: string, body: any) {
|
||||
switch (type) {
|
||||
case 'read':
|
||||
if (this.otherId) {
|
||||
this.chatService.readUserChatMessage(this.user!.id, this.otherId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose() {
|
||||
// Unsubscribe events
|
||||
@@ -50,12 +64,14 @@ export class ChatChannelService implements MiChannelService<true> {
|
||||
public readonly kind = ChatChannel.kind;
|
||||
|
||||
constructor(
|
||||
private chatService: ChatService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public create(id: string, connection: Channel['connection']): ChatChannel {
|
||||
return new ChatChannel(
|
||||
this.chatService,
|
||||
id,
|
||||
connection,
|
||||
);
|
||||
|
@@ -38,7 +38,7 @@ export function getScrollPosition(el: HTMLElement | null): number {
|
||||
|
||||
export function onScrollTop(el: HTMLElement, cb: (topVisible: boolean) => unknown, tolerance = 1, once = false) {
|
||||
// とりあえず評価してみる
|
||||
const firstTopVisible = isTopVisible(el);
|
||||
const firstTopVisible = isHeadVisible(el);
|
||||
if (el.isConnected && firstTopVisible) {
|
||||
cb(firstTopVisible);
|
||||
if (once) return null;
|
||||
@@ -53,7 +53,7 @@ export function onScrollTop(el: HTMLElement, cb: (topVisible: boolean) => unknow
|
||||
const onScroll = () => {
|
||||
if (!document.body.contains(el)) return;
|
||||
|
||||
const topVisible = isTopVisible(el, tolerance);
|
||||
const topVisible = isHeadVisible(el, tolerance);
|
||||
if (topVisible !== prevTopVisible) {
|
||||
prevTopVisible = topVisible;
|
||||
cb(topVisible);
|
||||
@@ -71,7 +71,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1
|
||||
const container = getScrollContainer(el);
|
||||
|
||||
// とりあえず評価してみる
|
||||
if (el.isConnected && isBottomVisible(el, tolerance, container)) {
|
||||
if (el.isConnected && isTailVisible(el, tolerance, container)) {
|
||||
cb();
|
||||
if (once) return null;
|
||||
}
|
||||
@@ -79,7 +79,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1
|
||||
const containerOrWindow = container ?? window;
|
||||
const onScroll = () => {
|
||||
if (!document.body.contains(el)) return;
|
||||
if (isBottomVisible(el, 1, container)) {
|
||||
if (isTailVisible(el, 1, container)) {
|
||||
cb();
|
||||
if (once) removeListener();
|
||||
}
|
||||
@@ -132,12 +132,12 @@ export function scrollToBottom(
|
||||
}
|
||||
}
|
||||
|
||||
export function isTopVisible(el: HTMLElement, tolerance = 1): boolean {
|
||||
export function isHeadVisible(el: HTMLElement, tolerance = 1): boolean {
|
||||
const scrollTop = getScrollPosition(el);
|
||||
return scrollTop <= tolerance;
|
||||
}
|
||||
|
||||
export function isBottomVisible(el: HTMLElement, tolerance = 1, container = getScrollContainer(el)) {
|
||||
export function isTailVisible(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;
|
||||
}
|
||||
|
@@ -168,21 +168,17 @@ export default defineComponent({
|
||||
container-type: inline-size;
|
||||
|
||||
&:global {
|
||||
> .list-move {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
> .list-move {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
&.deny-move-transition > .list-move {
|
||||
transition: none !important;
|
||||
}
|
||||
> .list-enter-active {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
> .list-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);
|
||||
}
|
||||
|
||||
> *:empty {
|
||||
display: none;
|
||||
}
|
||||
> *:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.date-separated-list-nogap) > *:not(:last-child) {
|
||||
|
@@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { computed, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, useTemplateRef, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
|
||||
import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@@/js/scroll.js';
|
||||
import { onScrollTop, isHeadVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isTailVisible } from '@@/js/scroll.js';
|
||||
import type { ComputedRef } from 'vue';
|
||||
import type { MisskeyEntity } from '@/types/date-separated-list.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
@@ -97,6 +97,7 @@ const props = withDefaults(defineProps<{
|
||||
pagination: Paging;
|
||||
disableAutoLoad?: boolean;
|
||||
displayLimit?: number;
|
||||
scrollReversed?: boolean;
|
||||
}>(), {
|
||||
displayLimit: 20,
|
||||
});
|
||||
@@ -349,7 +350,7 @@ const appearFetchMoreAhead = async (): Promise<void> => {
|
||||
fetchMoreAppearTimeout();
|
||||
};
|
||||
|
||||
const isTop = (): boolean => isBackTop.value || (props.pagination.reversed ? isBottomVisible : isTopVisible)(contentEl.value!, TOLERANCE);
|
||||
const isHead = (): boolean => isBackTop.value || (props.pagination.reversed && !props.scrollReversed ? isTailVisible : isHeadVisible)(contentEl.value!, TOLERANCE);
|
||||
|
||||
watch(visibility, () => {
|
||||
if (visibility.value === 'hidden') {
|
||||
@@ -364,7 +365,7 @@ watch(visibility, () => {
|
||||
timerForSetPause = null;
|
||||
} else {
|
||||
isPausingUpdate = false;
|
||||
if (isTop()) {
|
||||
if (isHead()) {
|
||||
executeQueue();
|
||||
}
|
||||
}
|
||||
@@ -376,16 +377,18 @@ watch(visibility, () => {
|
||||
* ストリーミングから降ってきたアイテムはこれで追加する
|
||||
* @param item アイテム
|
||||
*/
|
||||
const prepend = (item: MisskeyEntity): void => {
|
||||
function prepend(item: MisskeyEntity): void {
|
||||
if (items.value.size === 0) {
|
||||
items.value.set(item.id, item);
|
||||
fetching.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTop() && !isPausingUpdate) unshiftItems([item]);
|
||||
console.log(isHead(), isPausingUpdate);
|
||||
|
||||
if (isHead() && !isPausingUpdate) unshiftItems([item]);
|
||||
else prependQueue(item);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 新着アイテムをitemsの先頭に追加し、displayLimitを適用する
|
||||
|
@@ -4,8 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
|
||||
<MkSpacer :contentMax="800">
|
||||
<div class="_gaps">
|
||||
<MkButton primary :class="$style.start" @click="start"><i class="ti ti-plus"></i> {{ i18n.ts.startChat }}</MkButton>
|
||||
@@ -39,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkLoading v-if="fetching"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</PageWithHeader>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@@ -233,6 +233,10 @@ onMounted(() => {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
border: solid 1px var(--MI_THEME-divider);
|
||||
border-bottom: none;
|
||||
border-radius: 14px 14px 0 0;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
@@ -252,13 +256,13 @@ onMounted(() => {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
box-sizing: border-box;
|
||||
color: var(--fg);
|
||||
color: var(--MI_THEME-fg);
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
background: var(--panel);
|
||||
background: var(--MI_THEME-panel);
|
||||
}
|
||||
|
||||
.file {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div :class="[$style.root, { [$style.isMe]: isMe }]">
|
||||
<MkAvatar :class="$style.avatar" :user="user" indicator link preview/>
|
||||
<MkAvatar :class="$style.avatar" :user="user" link/>
|
||||
<div :class="$style.body">
|
||||
<MkFukidashi :class="$style.fukidashi" :tail="isMe ? 'right' : 'left'" :accented="isMe">
|
||||
<div v-if="!message.isDeleted" :class="$style.content">
|
||||
@@ -113,6 +113,6 @@ function del(): void {
|
||||
|
||||
.time {
|
||||
font-size: 75%;
|
||||
opacity: 0.7;
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
|
@@ -12,30 +12,30 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<div ref="rootEl" :class="$style.root">
|
||||
<MkSpacer :contentMax="700">
|
||||
<MkPagination v-if="pagination" ref="pagingComponent" :key="userId || roomId" :pagination="pagination" :disableAutoLoad="true">
|
||||
<MkPagination v-if="pagination" ref="pagingComponent" :key="userId || roomId" :pagination="pagination" :disableAutoLoad="true" :scrollReversed="true">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<div>{{ i18n.ts.noMessagesYet }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ items: messages, fetching: pFetching }">
|
||||
<MkDateSeparatedList
|
||||
v-if="messages.length > 0"
|
||||
v-slot="{ item: message }"
|
||||
:class="{ [$style['messages']]: true, 'deny-move-transition': pFetching }"
|
||||
:items="messages"
|
||||
direction="up"
|
||||
reversed
|
||||
<template #default="{ items: messages }">
|
||||
<TransitionGroup
|
||||
:enterActiveClass="prefer.s.animation ? $style.transition_x_enterActive : ''"
|
||||
:leaveActiveClass="prefer.s.animation ? $style.transition_x_leaveActive : ''"
|
||||
:enterFromClass="prefer.s.animation ? $style.transition_x_enterFrom : ''"
|
||||
:leaveToClass="prefer.s.animation ? $style.transition_x_leaveTo : ''"
|
||||
:moveClass="prefer.s.animation ? $style.transition_x_move : ''"
|
||||
tag="div" class="_gaps"
|
||||
>
|
||||
<XMessage :key="message.id" :message="message" :user="message.fromUserId === $i.id ? $i : user" :isRoom="room != null"/>
|
||||
</MkDateSeparatedList>
|
||||
<XMessage v-for="message in messages.toReversed()" :key="message.id" :message="message" :user="message.fromUserId === $i.id ? $i : user" :isRoom="room != null"/>
|
||||
</TransitionGroup>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<MkSpacer :contentMax="700">
|
||||
<div :class="$style.footer">
|
||||
<div class="_gaps">
|
||||
<Transition name="fade">
|
||||
<div v-show="showIndicator" :class="$style.new">
|
||||
@@ -46,16 +46,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</Transition>
|
||||
<XForm v-if="!fetching" :user="user" :room="room" :class="$style.form"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
</MkStickyContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef, computed, watch, onMounted, nextTick, onBeforeUnmount } from 'vue';
|
||||
import { ref, useTemplateRef, computed, watch, onMounted, nextTick, onBeforeUnmount } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { isBottomVisible } from '@@/js/scroll.js';
|
||||
import { isTailVisible } from '@@/js/scroll.js';
|
||||
import XMessage from './room.message.vue';
|
||||
import XForm from './room.form.vue';
|
||||
import type { Paging } from '@/components/MkPagination.vue';
|
||||
@@ -68,6 +68,7 @@ import { i18n } from '@/i18n.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
@@ -76,8 +77,8 @@ const props = defineProps<{
|
||||
roomId?: string;
|
||||
}>();
|
||||
|
||||
const rootEl = shallowRef<HTMLDivElement>();
|
||||
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
const pagingComponent = useTemplateRef('pagingComponent');
|
||||
|
||||
const fetching = ref(true);
|
||||
const user = ref<Misskey.entities.UserDetailed | null>(null);
|
||||
@@ -109,7 +110,7 @@ async function fetch() {
|
||||
pageEl: rootEl.value,
|
||||
};
|
||||
connection.value = useStream().useChannel('chat', {
|
||||
other: user.value.id,
|
||||
otherId: user.value.id,
|
||||
});
|
||||
}/* else {
|
||||
user = null;
|
||||
@@ -140,15 +141,16 @@ async function fetch() {
|
||||
function onMessage(message) {
|
||||
//sound.play('chat');
|
||||
|
||||
const _isBottom = isBottomVisible(rootEl, 64);
|
||||
//const _isBottom = isBottomVisible(rootEl, 64);
|
||||
|
||||
pagingComponent.value.prepend(message);
|
||||
if (message.userId !== $i.id && !document.hidden) {
|
||||
if (message.userId !== $i.id && !window.document.hidden) {
|
||||
connection.value?.send('read', {
|
||||
id: message.id,
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
if (_isBottom) {
|
||||
// Scroll to bottom
|
||||
nextTick(() => {
|
||||
@@ -157,7 +159,7 @@ function onMessage(message) {
|
||||
} else if (message.userId !== $i.id) {
|
||||
// Notify
|
||||
notifyNewMessage();
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
function onDeleted(id) {
|
||||
@@ -215,8 +217,22 @@ definePage(computed(() => !fetching.value ? user.value ? {
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.transition_x_move,
|
||||
.transition_x_enterActive,
|
||||
.transition_x_leaveActive {
|
||||
transition: opacity 0.2s cubic-bezier(0,.5,.5,1), transform 0.2s cubic-bezier(0,.5,.5,1) !important;
|
||||
}
|
||||
.transition_x_enterFrom,
|
||||
.transition_x_leaveTo {
|
||||
opacity: 0;
|
||||
transform: translateY(80px);
|
||||
}
|
||||
.transition_x_leaveActive {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.root {
|
||||
display: content;
|
||||
min-height: 100cqh;
|
||||
}
|
||||
|
||||
.more {
|
||||
@@ -241,10 +257,6 @@ definePage(computed(() => !fetching.value ? user.value ? {
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.messages {
|
||||
padding: 16px 0 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100%;
|
||||
position: sticky;
|
||||
@@ -273,12 +285,14 @@ definePage(computed(() => !fetching.value ? user.value ? {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
|
||||
}
|
||||
|
||||
.form {
|
||||
max-height: 12em;
|
||||
overflow-y: scroll;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
|
@@ -136,6 +136,7 @@ export const PREF_DEF = {
|
||||
'clips',
|
||||
'drive',
|
||||
'followRequests',
|
||||
'chat',
|
||||
'-',
|
||||
'explore',
|
||||
'announcements',
|
||||
|
Reference in New Issue
Block a user