misskey/packages/frontend/src/pages/chat/room.vue

292 lines
7.0 KiB
Vue

<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<PageWithHeader reversed>
<MkSpacer :contentMax="700">
<div v-if="initializing">
<MkLoading/>
</div>
<div v-else-if="messages.length === 0">
<div class="_gaps" style="text-align: center;">
<div>{{ i18n.ts.noMessagesYet }}</div>
<template v-if="user">
<div v-if="user.chatScope === 'followers'">{{ i18n.ts._chat.thisUserAllowsChatOnlyFromFollowers }}</div>
<div v-else-if="user.chatScope === 'following'">{{ i18n.ts._chat.thisUserAllowsChatOnlyFromFollowing }}</div>
<div v-else-if="user.chatScope === 'mutual'">{{ i18n.ts._chat.thisUserAllowsChatOnlyFromMutualFollowing }}</div>
<div v-else>{{ i18n.ts._chat.thisUserNotAllowedChatAnyone }}</div>
</template>
</div>
</div>
<div v-else class="_gaps">
<div v-if="canFetchMore">
<MkButton :class="$style.more" :wait="moreFetching" primary rounded @click="fetchMore">{{ i18n.ts.loadMore }}</MkButton>
</div>
<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 v-for="message in messages.toReversed()" :key="message.id" :message="message" :user="message.fromUserId === $i.id ? $i : user" :isRoom="room != null"/>
</TransitionGroup>
</div>
</MkSpacer>
<template #footer>
<div :class="$style.footer">
<div class="_gaps">
<Transition name="fade">
<div v-show="showIndicator" :class="$style.new">
<button class="_buttonPrimary" :class="$style.newButton" @click="onIndicatorClick">
<i class="fas ti-fw fa-arrow-circle-down" :class="$style.newIcon"></i>{{ i18n.ts.newMessageExists }}
</button>
</div>
</Transition>
<XForm v-if="!initializing" :user="user" :room="room" :class="$style.form"/>
</div>
</div>
</template>
</PageWithHeader>
</template>
<script lang="ts" setup>
import { ref, useTemplateRef, computed, watch, onMounted, nextTick, onBeforeUnmount, onDeactivated, onActivated } from 'vue';
import * as Misskey from 'misskey-js';
import { isTailVisible } from '@@/js/scroll.js';
import XMessage from './room.message.vue';
import XForm from './room.form.vue';
import * as os from '@/os.js';
import { useStream } from '@/stream.js';
import * as sound from '@/utility/sound.js';
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';
import MkButton from '@/components/MkButton.vue';
const $i = ensureSignin();
const props = defineProps<{
userId?: string;
roomId?: string;
}>();
const initializing = ref(true);
const moreFetching = ref(false);
const messages = ref<Misskey.entities.ChatMessage[]>([]);
const canFetchMore = ref(false);
const user = ref<Misskey.entities.UserDetailed | null>(null);
const room = ref<Misskey.entities.ChatRoom | null>(null);
const connection = ref<Misskey.ChannelConnection<Misskey.Channels['chat']> | null>(null);
const showIndicator = ref(false);
watch([() => props.userId, () => props.roomId], () => {
if (connection.value) connection.value.dispose();
initialize();
});
async function initialize() {
initializing.value = true;
if (props.userId) {
const LIMIT = 20;
const [u, m] = await Promise.all([
misskeyApi('users/show', { userId: props.userId }),
misskeyApi('chat/messages/timeline', { userId: props.userId, limit: LIMIT }),
]);
user.value = u;
messages.value = m;
if (messages.value.length === LIMIT) {
canFetchMore.value = true;
}
connection.value = useStream().useChannel('chat', {
otherId: user.value.id,
});
}/* else {
user = null;
room = await misskeyApi('users/rooms/show', { roomId: props.roomId });
pagination = {
endpoint: 'chat/messages',
limit: 20,
params: {
roomId: room?.id,
},
reversed: true,
};
connection = useStream().useChannel('chat', {
room: room?.id,
});
}*/
connection.value.on('message', onMessage);
connection.value.on('deleted', onDeleted);
window.document.addEventListener('visibilitychange', onVisibilitychange);
initializing.value = false;
}
let isActivated = true;
onActivated(() => {
isActivated = true;
});
onDeactivated(() => {
isActivated = false;
});
function fetchMore() {
const LIMIT = 30;
moreFetching.value = true;
misskeyApi('chat/messages/timeline', {
userId: user.value.id,
limit: LIMIT,
untilId: messages.value[messages.value.length - 1].id,
}).then(newMessages => {
messages.value.push(...newMessages);
canFetchMore.value = newMessages.length === LIMIT;
moreFetching.value = false;
});
}
function onMessage(message: Misskey.entities.ChatMessage) {
//sound.play('chat');
messages.value.unshift(message);
// TODO: DOM的にバックグラウンドになっていないかどうかも考慮する
if (message.fromUserId !== $i.id && !window.document.hidden && isActivated) {
connection.value?.send('read', {
id: message.id,
});
}
if (message.fromUserId !== $i.id) {
//notifyNewMessage();
}
}
function onDeleted(id) {
const index = messages.value.findIndex(m => m.id === id);
if (index !== -1) {
messages.value.splice(index, 1);
}
}
function onIndicatorClick() {
showIndicator.value = false;
}
function notifyNewMessage() {
showIndicator.value = true;
}
function onVisibilitychange() {
if (window.document.hidden) return;
// TODO
}
onMounted(() => {
initialize();
});
onBeforeUnmount(() => {
connection.value?.dispose();
window.document.removeEventListener('visibilitychange', onVisibilitychange);
});
definePage(computed(() => !initializing.value ? user.value ? {
userName: user,
avatar: user,
} : {
title: room.value?.name,
icon: 'ti ti-users',
} : null));
</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 {
}
.more {
margin: 0 auto;
}
.footer {
width: 100%;
position: sticky;
z-index: 2;
padding-top: 8px;
bottom: var(--minBottomSpacing);
}
.new {
width: 100%;
padding-bottom: 8px;
text-align: center;
}
.newButton {
display: inline-block;
margin: 0;
padding: 0 12px;
line-height: 32px;
font-size: 12px;
border-radius: 16px;
}
.newIcon {
display: inline-block;
margin-right: 8px;
}
.footer {
}
.form {
margin: 0 auto;
width: 100%;
max-width: 700px;
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.1s;
}
.fade-enter-from, .fade-leave-to {
transition: opacity 0.5s;
opacity: 0;
}
</style>