This commit is contained in:
syuilo
2025-03-21 18:46:25 +09:00
parent 3c6f3992b0
commit 2eef19d95d
9 changed files with 96 additions and 63 deletions

View File

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

View File

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

View File

@@ -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) {

View File

@@ -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を適用する

View File

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

View File

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

View 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>

View File

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

View File

@@ -136,6 +136,7 @@ export const PREF_DEF = {
'clips',
'drive',
'followRequests',
'chat',
'-',
'explore',
'announcements',