404 lines
9.0 KiB
Vue
404 lines
9.0 KiB
Vue
<template>
|
|
<div
|
|
ref="rootEl"
|
|
class="_section"
|
|
@dragover.prevent.stop="onDragover"
|
|
@drop.prevent.stop="onDrop"
|
|
>
|
|
<div class="_content mk-messaging-room">
|
|
<div class="body">
|
|
<MkPagination v-if="pagination" ref="pagingComponent" :key="userAcct || groupId" :pagination="pagination">
|
|
<template #empty>
|
|
<div class="_fullinfo">
|
|
<img src="/static-assets/badges/info.png" class="_ghost" alt="Info"/>
|
|
<div>{{ i18n.ts.noMessagesYet }}</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template #default="{ items: messages, fetching: pFetching }">
|
|
<XList
|
|
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"/>
|
|
</XList>
|
|
</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>
|
|
</I18n>
|
|
<MkEllipsis/>
|
|
</div>
|
|
<transition :name="animation ? 'fade' : ''">
|
|
<div v-show="showIndicator" class="new-message">
|
|
<button class="_buttonPrimary" @click="onIndicatorClick"><i class="fas fa-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>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { computed, watch, onMounted, nextTick, onBeforeUnmount } from 'vue';
|
|
import * as Misskey from 'misskey-js';
|
|
import * as Acct from 'misskey-js/built/acct';
|
|
import XMessage from './messaging-room.message.vue';
|
|
import XForm from './messaging-room.form.vue';
|
|
import XList from '@/components/MkDateSeparatedList.vue';
|
|
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
|
import { isBottomVisible, onScrollBottom, scrollToBottom } from '@/scripts/scroll';
|
|
import * as os from '@/os';
|
|
import { stream } from '@/stream';
|
|
import * as sound from '@/scripts/sound';
|
|
import { i18n } from '@/i18n';
|
|
import { $i } from '@/account';
|
|
import { defaultStore } from '@/store';
|
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
|
|
|
const props = defineProps<{
|
|
userAcct?: string;
|
|
groupId?: string;
|
|
}>();
|
|
|
|
let rootEl = $ref<HTMLDivElement>();
|
|
let formEl = $ref<InstanceType<typeof XForm>>();
|
|
let pagingComponent = $ref<InstanceType<typeof MkPagination>>();
|
|
|
|
let fetching = $ref(true);
|
|
let user: Misskey.entities.UserDetailed | null = $ref(null);
|
|
let group: Misskey.entities.UserGroup | null = $ref(null);
|
|
let typers: Misskey.entities.User[] = $ref([]);
|
|
let connection: Misskey.ChannelConnection<Misskey.Channels['messaging']> | null = $ref(null);
|
|
let showIndicator = $ref(false);
|
|
const {
|
|
animation,
|
|
} = defaultStore.reactiveState;
|
|
|
|
let pagination: Paging | null = $ref(null);
|
|
|
|
watch([() => props.userAcct, () => props.groupId], () => {
|
|
if (connection) connection.dispose();
|
|
fetch();
|
|
});
|
|
|
|
async function fetch() {
|
|
fetching = true;
|
|
|
|
if (props.userAcct) {
|
|
const acct = Acct.parse(props.userAcct);
|
|
user = await os.api('users/show', { username: acct.username, host: acct.host || undefined });
|
|
group = null;
|
|
|
|
pagination = {
|
|
endpoint: 'messaging/messages',
|
|
limit: 20,
|
|
params: {
|
|
userId: user.id,
|
|
},
|
|
reversed: true,
|
|
pageEl: $$(rootEl).value,
|
|
};
|
|
connection = stream.useChannel('messaging', {
|
|
otherparty: user.id,
|
|
});
|
|
} else {
|
|
user = null;
|
|
group = await os.api('users/groups/show', { groupId: props.groupId });
|
|
|
|
pagination = {
|
|
endpoint: 'messaging/messages',
|
|
limit: 20,
|
|
params: {
|
|
groupId: group?.id,
|
|
},
|
|
reversed: true,
|
|
pageEl: $$(rootEl).value,
|
|
};
|
|
connection = stream.useChannel('messaging', {
|
|
group: group?.id,
|
|
});
|
|
}
|
|
|
|
connection.on('message', onMessage);
|
|
connection.on('read', onRead);
|
|
connection.on('deleted', onDeleted);
|
|
connection.on('typers', _typers => {
|
|
typers = _typers.filter(u => u.id !== $i?.id);
|
|
});
|
|
|
|
document.addEventListener('visibilitychange', onVisibilitychange);
|
|
|
|
nextTick(() => {
|
|
// thisScrollToBottom();
|
|
window.setTimeout(() => {
|
|
fetching = false;
|
|
}, 300);
|
|
});
|
|
}
|
|
|
|
function onDragover(ev: DragEvent) {
|
|
if (!ev.dataTransfer) return;
|
|
|
|
const isFile = ev.dataTransfer.items[0].kind === 'file';
|
|
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
|
|
|
|
if (isFile || isDriveFile) {
|
|
ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed === 'all' ? 'copy' : 'move';
|
|
} else {
|
|
ev.dataTransfer.dropEffect = 'none';
|
|
}
|
|
}
|
|
|
|
function onDrop(ev: DragEvent): void {
|
|
if (!ev.dataTransfer) return;
|
|
|
|
// ファイルだったら
|
|
if (ev.dataTransfer.files.length === 1) {
|
|
formEl.upload(ev.dataTransfer.files[0]);
|
|
return;
|
|
} else if (ev.dataTransfer.files.length > 1) {
|
|
os.alert({
|
|
type: 'error',
|
|
text: i18n.ts.onlyOneFileCanBeAttached,
|
|
});
|
|
return;
|
|
}
|
|
|
|
//#region ドライブのファイル
|
|
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
|
if (driveFile != null && driveFile !== '') {
|
|
const file = JSON.parse(driveFile);
|
|
formEl.file = file;
|
|
}
|
|
//#endregion
|
|
}
|
|
|
|
function onMessage(message) {
|
|
sound.play('chat');
|
|
|
|
const _isBottom = isBottomVisible(rootEl, 64);
|
|
|
|
pagingComponent.prepend(message);
|
|
if (message.userId !== $i?.id && !document.hidden) {
|
|
connection?.send('read', {
|
|
id: message.id,
|
|
});
|
|
}
|
|
|
|
if (_isBottom) {
|
|
// Scroll to bottom
|
|
nextTick(() => {
|
|
thisScrollToBottom();
|
|
});
|
|
} else if (message.userId !== $i?.id) {
|
|
// Notify
|
|
notifyNewMessage();
|
|
}
|
|
}
|
|
|
|
function onRead(x) {
|
|
if (user) {
|
|
if (!Array.isArray(x)) x = [x];
|
|
for (const id of x) {
|
|
if (pagingComponent.items.some(y => y.id === id)) {
|
|
const exist = pagingComponent.items.map(y => y.id).indexOf(id);
|
|
pagingComponent.items[exist] = {
|
|
...pagingComponent.items[exist],
|
|
isRead: true,
|
|
};
|
|
}
|
|
}
|
|
} else if (group) {
|
|
for (const id of x.ids) {
|
|
if (pagingComponent.items.some(y => y.id === id)) {
|
|
const exist = pagingComponent.items.map(y => y.id).indexOf(id);
|
|
pagingComponent.items[exist] = {
|
|
...pagingComponent.items[exist],
|
|
reads: [...pagingComponent.items[exist].reads, x.userId],
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function onDeleted(id) {
|
|
const msg = pagingComponent.items.find(m => m.id === id);
|
|
if (msg) {
|
|
pagingComponent.items = pagingComponent.items.filter(m => m.id !== msg.id);
|
|
}
|
|
}
|
|
|
|
function thisScrollToBottom() {
|
|
if (window.location.href.includes('my/messaging/')) {
|
|
scrollToBottom($$(rootEl).value, { behavior: 'smooth' });
|
|
}
|
|
}
|
|
|
|
function onIndicatorClick() {
|
|
showIndicator = false;
|
|
thisScrollToBottom();
|
|
}
|
|
|
|
let scrollRemove: (() => void) | null = $ref(null);
|
|
|
|
function notifyNewMessage() {
|
|
showIndicator = true;
|
|
|
|
scrollRemove = onScrollBottom(rootEl, () => {
|
|
showIndicator = false;
|
|
scrollRemove = null;
|
|
});
|
|
}
|
|
|
|
function onVisibilitychange() {
|
|
if (document.hidden) return;
|
|
for (const message of pagingComponent.items) {
|
|
if (message.userId !== $i?.id && !message.isRead) {
|
|
connection?.send('read', {
|
|
id: message.id,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
fetch();
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
connection?.dispose();
|
|
document.removeEventListener('visibilitychange', onVisibilitychange);
|
|
if (scrollRemove) scrollRemove();
|
|
});
|
|
|
|
definePageMetadata(computed(() => !fetching ? user ? {
|
|
userName: user,
|
|
avatar: user,
|
|
} : {
|
|
title: group?.name,
|
|
icon: 'fas fa-users',
|
|
} : null));
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
XMessage:last-of-type {
|
|
margin-bottom: 4rem;
|
|
}
|
|
|
|
.mk-messaging-room {
|
|
position: relative;
|
|
overflow: auto;
|
|
|
|
> .body {
|
|
.more {
|
|
display: block;
|
|
margin: 16px auto;
|
|
padding: 0 12px;
|
|
line-height: 24px;
|
|
color: #fff;
|
|
background: rgba(#000, 0.3);
|
|
border-radius: 12px;
|
|
|
|
&:hover {
|
|
background: rgba(#000, 0.4);
|
|
}
|
|
|
|
&:active {
|
|
background: rgba(#000, 0.5);
|
|
}
|
|
|
|
&.fetching {
|
|
cursor: wait;
|
|
}
|
|
|
|
> i {
|
|
margin-right: 4px;
|
|
}
|
|
}
|
|
|
|
.messages {
|
|
padding: 8px 0;
|
|
|
|
> ::v-deep(*) {
|
|
margin-bottom: 16px;
|
|
}
|
|
}
|
|
}
|
|
|
|
> footer {
|
|
width: 100%;
|
|
position: sticky;
|
|
z-index: 2;
|
|
bottom: 0;
|
|
padding-top: 8px;
|
|
bottom: calc(env(safe-area-inset-bottom, 0px) + 8px);
|
|
|
|
> .new-message {
|
|
width: 100%;
|
|
padding-bottom: 8px;
|
|
text-align: center;
|
|
|
|
> button {
|
|
display: inline-block;
|
|
margin: 0;
|
|
padding: 0 12px;
|
|
line-height: 32px;
|
|
font-size: 12px;
|
|
border-radius: 16px;
|
|
|
|
> i {
|
|
display: inline-block;
|
|
margin-right: 8px;
|
|
}
|
|
}
|
|
}
|
|
|
|
> .typers {
|
|
position: absolute;
|
|
bottom: 100%;
|
|
padding: 0 8px 0 8px;
|
|
font-size: 0.9em;
|
|
color: var(--fgTransparentWeak);
|
|
|
|
> .users {
|
|
> .user + .user:before {
|
|
content: ", ";
|
|
font-weight: normal;
|
|
}
|
|
|
|
> .user:last-of-type:after {
|
|
content: " ";
|
|
}
|
|
}
|
|
}
|
|
|
|
> .form {
|
|
max-height: 12em;
|
|
overflow-y: scroll;
|
|
border-top: solid 0.5px var(--divider);
|
|
}
|
|
}
|
|
}
|
|
|
|
.fade-enter-active, .fade-leave-active {
|
|
transition: opacity 0.1s;
|
|
}
|
|
|
|
.fade-enter-from, .fade-leave-to {
|
|
transition: opacity 0.5s;
|
|
opacity: 0;
|
|
}
|
|
</style>
|