283 lines
6.8 KiB
Vue
283 lines
6.8 KiB
Vue
<!--
|
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
-->
|
|
|
|
<template>
|
|
<div class="_gaps">
|
|
<MkButton primary gradate rounded :class="$style.start" @click="start"><i class="ti ti-plus"></i> {{ i18n.ts.startChat }}</MkButton>
|
|
|
|
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
|
|
|
|
<MkInput
|
|
v-model="searchQuery"
|
|
:placeholder="i18n.ts._chat.searchMessages"
|
|
type="search"
|
|
>
|
|
<template #prefix><i class="ti ti-search"></i></template>
|
|
</MkInput>
|
|
|
|
<MkButton v-if="searchQuery.length > 0" primary rounded @click="search">{{ i18n.ts.search }}</MkButton>
|
|
|
|
<MkFoldableSection v-if="searched">
|
|
<template #header>{{ i18n.ts.searchResult }}</template>
|
|
|
|
<div class="_gaps_s">
|
|
<div v-for="message in searchResults" :key="message.id" :class="$style.searchResultItem">
|
|
<XMessage :message="message" :isSearchResult="true"/>
|
|
</div>
|
|
</div>
|
|
</MkFoldableSection>
|
|
|
|
<MkFoldableSection>
|
|
<template #header>{{ i18n.ts._chat.history }}</template>
|
|
|
|
<div v-if="history.length > 0" class="_gaps_s">
|
|
<MkA
|
|
v-for="item in history"
|
|
:key="item.id"
|
|
:class="[$style.message, { [$style.isMe]: item.isMe, [$style.isRead]: item.message.isRead }]"
|
|
class="_panel"
|
|
:to="item.message.toRoomId ? `/chat/room/${item.message.toRoomId}` : `/chat/user/${item.other!.id}`"
|
|
>
|
|
<MkAvatar v-if="item.message.toRoomId" :class="$style.messageAvatar" :user="item.message.fromUser" indicator :preview="false"/>
|
|
<MkAvatar v-else-if="item.other" :class="$style.messageAvatar" :user="item.other" indicator :preview="false"/>
|
|
<div :class="$style.messageBody">
|
|
<header v-if="item.message.toRoom" :class="$style.messageHeader">
|
|
<span :class="$style.messageHeaderName"><i class="ti ti-users"></i> {{ item.message.toRoom.name }}</span>
|
|
<MkTime :time="item.message.createdAt" :class="$style.messageHeaderTime"/>
|
|
</header>
|
|
<header v-else :class="$style.messageHeader">
|
|
<MkUserName :class="$style.messageHeaderName" :user="item.other!"/>
|
|
<MkAcct :class="$style.messageHeaderUsername" :user="item.other!"/>
|
|
<MkTime :time="item.message.createdAt" :class="$style.messageHeaderTime"/>
|
|
</header>
|
|
<div :class="$style.messageBodyText"><span v-if="item.isMe" :class="$style.youSaid">{{ i18n.ts.you }}:</span>{{ item.message.text }}</div>
|
|
</div>
|
|
</MkA>
|
|
</div>
|
|
<div v-if="!initializing && history.length == 0" class="_fullinfo">
|
|
<div>{{ i18n.ts._chat.noHistory }}</div>
|
|
</div>
|
|
<MkLoading v-if="initializing"/>
|
|
</MkFoldableSection>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { computed, onActivated, onDeactivated, onMounted, ref } from 'vue';
|
|
import * as Misskey from 'misskey-js';
|
|
import { useInterval } from '@@/js/use-interval.js';
|
|
import XMessage from './XMessage.vue';
|
|
import MkButton from '@/components/MkButton.vue';
|
|
import { i18n } from '@/i18n.js';
|
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
|
import { ensureSignin } from '@/i.js';
|
|
import { useRouter } from '@/router.js';
|
|
import * as os from '@/os.js';
|
|
import { updateCurrentAccountPartial } from '@/accounts.js';
|
|
import MkInput from '@/components/MkInput.vue';
|
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
|
|
|
const $i = ensureSignin();
|
|
|
|
const router = useRouter();
|
|
|
|
const initializing = ref(true);
|
|
const fetching = ref(false);
|
|
const history = ref<{
|
|
id: string;
|
|
message: Misskey.entities.ChatMessage;
|
|
other: Misskey.entities.ChatMessage['fromUser'] | Misskey.entities.ChatMessage['toUser'] | null;
|
|
isMe: boolean;
|
|
}[]>([]);
|
|
|
|
const searchQuery = ref('');
|
|
const searched = ref(false);
|
|
const searchResults = ref<Misskey.entities.ChatMessage[]>([]);
|
|
|
|
function start(ev: MouseEvent) {
|
|
os.popupMenu([{
|
|
text: i18n.ts._chat.individualChat,
|
|
caption: i18n.ts._chat.individualChat_description,
|
|
icon: 'ti ti-user',
|
|
action: () => { startUser(); },
|
|
}, { type: 'divider' }, {
|
|
type: 'parent',
|
|
text: i18n.ts._chat.roomChat,
|
|
caption: i18n.ts._chat.roomChat_description,
|
|
icon: 'ti ti-users-group',
|
|
children: [{
|
|
text: i18n.ts._chat.createRoom,
|
|
icon: 'ti ti-plus',
|
|
action: () => { createRoom(); },
|
|
}],
|
|
}], ev.currentTarget ?? ev.target);
|
|
}
|
|
|
|
async function startUser() {
|
|
os.selectUser().then(user => {
|
|
router.push(`/chat/user/${user.id}`);
|
|
});
|
|
}
|
|
|
|
async function createRoom() {
|
|
const { canceled, result } = await os.inputText({
|
|
title: i18n.ts.name,
|
|
minLength: 1,
|
|
});
|
|
if (canceled) return;
|
|
|
|
const room = await misskeyApi('chat/rooms/create', {
|
|
name: result,
|
|
});
|
|
|
|
router.push(`/chat/room/${room.id}`);
|
|
}
|
|
|
|
async function search() {
|
|
const res = await misskeyApi('chat/messages/search', {
|
|
query: searchQuery.value,
|
|
});
|
|
|
|
searchResults.value = res;
|
|
searched.value = true;
|
|
}
|
|
|
|
async function fetchHistory() {
|
|
if (fetching.value) return;
|
|
|
|
fetching.value = true;
|
|
|
|
const [userMessages, roomMessages] = await Promise.all([
|
|
misskeyApi('chat/history', { room: false }),
|
|
misskeyApi('chat/history', { room: true }),
|
|
]);
|
|
|
|
history.value = [...userMessages, ...roomMessages]
|
|
.toSorted((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
|
.map(m => ({
|
|
id: m.id,
|
|
message: m,
|
|
other: m.room == null ? (m.fromUserId === $i.id ? m.toUser : m.fromUser) : null,
|
|
isMe: m.fromUserId === $i.id,
|
|
}));
|
|
|
|
fetching.value = false;
|
|
initializing.value = false;
|
|
|
|
updateCurrentAccountPartial({ hasUnreadChatMessages: false });
|
|
}
|
|
|
|
let isActivated = true;
|
|
|
|
onActivated(() => {
|
|
isActivated = true;
|
|
});
|
|
|
|
onDeactivated(() => {
|
|
isActivated = false;
|
|
});
|
|
|
|
useInterval(() => {
|
|
// TODO: DOM的にバックグラウンドになっていないかどうかも考慮する
|
|
if (!window.document.hidden && isActivated) {
|
|
fetchHistory();
|
|
}
|
|
}, 1000 * 10, {
|
|
immediate: false,
|
|
afterMounted: true,
|
|
});
|
|
|
|
onActivated(() => {
|
|
fetchHistory();
|
|
});
|
|
|
|
onMounted(() => {
|
|
fetchHistory();
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss" module>
|
|
.start {
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.message {
|
|
position: relative;
|
|
display: flex;
|
|
padding: 16px 24px;
|
|
|
|
&.isRead,
|
|
&.isMe {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
&:not(.isMe):not(.isRead) {
|
|
&::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 8px;
|
|
right: 8px;
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 100%;
|
|
background-color: var(--MI_THEME-accent);
|
|
}
|
|
}
|
|
}
|
|
|
|
.messageAvatar {
|
|
width: 50px;
|
|
height: 50px;
|
|
margin: 0 16px 0 0;
|
|
}
|
|
|
|
.messageBody {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.messageHeader {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 2px;
|
|
white-space: nowrap;
|
|
overflow: clip;
|
|
}
|
|
|
|
.messageHeaderName {
|
|
margin: 0;
|
|
padding: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
font-size: 1em;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.messageHeaderUsername {
|
|
margin: 0 8px;
|
|
}
|
|
|
|
.messageHeaderTime {
|
|
margin-left: auto;
|
|
}
|
|
|
|
.messageBodyText {
|
|
overflow: hidden;
|
|
overflow-wrap: break-word;
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.youSaid {
|
|
font-weight: bold;
|
|
margin-right: 0.5em;
|
|
}
|
|
|
|
.searchResultItem {
|
|
padding: 12px;
|
|
border: solid 1px var(--MI_THEME-divider);
|
|
border-radius: 12px;
|
|
}
|
|
</style>
|