412 lines
9.7 KiB
Vue
412 lines
9.7 KiB
Vue
<template>
|
||
<Progress
|
||
v-if="isProcessing"
|
||
:size="20"
|
||
class="processing-progress"
|
||
/>
|
||
|
||
<div
|
||
v-if="!isProcessing && channel"
|
||
class="channel"
|
||
>
|
||
<div class="messages" @scroll.passive="messagesScroll">
|
||
<div class="none-message" v-if="messages.length === 0">
|
||
<span>何ということでしょう!!</span>
|
||
<span>メッセージがありませんね!!</span>
|
||
</div>
|
||
|
||
<Progress
|
||
v-if="isLoading"
|
||
:size="10"
|
||
class="loading-progress"
|
||
/>
|
||
|
||
<Message
|
||
v-for="message of messages"
|
||
@deleteMessage="deleteMessage"
|
||
:key="message.id"
|
||
:message="message"
|
||
/>
|
||
</div>
|
||
|
||
<form>
|
||
<textarea
|
||
@input="resize"
|
||
@keydown.ctrl.enter="send"
|
||
class="message-input"
|
||
rows="1"
|
||
v-model="message"
|
||
:disabled="isSending"
|
||
:placeholder="`${channel.name}に送信`"
|
||
/>
|
||
|
||
<Icon
|
||
@click="send"
|
||
:icon='isSending
|
||
? "line-md:loading-twotone-loop"
|
||
: "material-symbols:send-rounded"'
|
||
/>
|
||
</form>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.processing-progress {
|
||
margin: auto;
|
||
}
|
||
|
||
.channel {
|
||
display: flex;
|
||
flex-direction: column;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.messages {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
box-sizing: border-box;
|
||
overflow: scroll;
|
||
}
|
||
|
||
.none-message {
|
||
margin: auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
text-align: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.none-message span:first-child {
|
||
font-size: 1.25rem;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.loading-progress {
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.channel form {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
padding: 1rem;
|
||
margin-bottom: 1rem;
|
||
border-radius: 1rem;
|
||
background-color: var(--bg-sub-color);
|
||
}
|
||
|
||
.message-input {
|
||
width: 100%;
|
||
max-height: 40dvh;
|
||
resize: none;
|
||
border: none;
|
||
outline: none;
|
||
color: var(--text-color);
|
||
background-color: var(--bg-sub-color);
|
||
}
|
||
|
||
.message-input:disabled {
|
||
color: oklch(from var(--text-color) calc(l - 0.2) c h);
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.channel form svg {
|
||
cursor: pointer;
|
||
flex-shrink: 0;
|
||
margin-top: auto;
|
||
width: 2rem;
|
||
height: 2rem;
|
||
}
|
||
</style>
|
||
|
||
<script lang="ts" setup>
|
||
import { useRoute, useRouter } from "vue-router";
|
||
import { account, presentCommunity, serverInfo } from "@/lib/account";
|
||
import client from "@/lib/client";
|
||
import { createModal, createTopNotice } from "@/lib/modal";
|
||
import GoHomeError from "@/components/Modal/GoHomeError.vue";
|
||
import { onMounted, provide, ref } from "vue";
|
||
import Progress from "@/components/Progress.vue";
|
||
import { title } from "@/lib/router";
|
||
import type ApiMap from "lynqchat-js/1.0.0-alpha.0/map";
|
||
import { Icon } from "@iconify/vue";
|
||
import ErrorModal from "@/components/Modal/Error.vue";
|
||
import Message from "@/components/Message.vue";
|
||
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
|
||
if (!serverInfo.value?.success) {
|
||
throw new Error("サーバー情報の取得に失敗しました。");
|
||
}
|
||
|
||
if (!serverInfo.value.isInitialized) {
|
||
router.replace("/setup/initialization");
|
||
}
|
||
|
||
if (!account.value?.success) {
|
||
switch (account.value?.error.bad) {
|
||
case "client":
|
||
router.replace("/signin");
|
||
break;
|
||
default:
|
||
throw new Error("アカウント情報の取得に失敗しました。");
|
||
}
|
||
}
|
||
|
||
const isProcessing = ref<boolean>(false);
|
||
const isSending = ref<boolean>(false);
|
||
|
||
const channel = ref<Extract<ApiMap["channel/list"]["response"], { channels: any }>["channels"][number]>();
|
||
const messages = ref<Extract<ApiMap["message/list"]["response"], { messages: any }>["messages"]>([]);
|
||
|
||
const message = ref<string>("");
|
||
|
||
const isInsideRange = ref(false);
|
||
const isLoading = ref(false);
|
||
const messagesScroll = async (event: any) => {
|
||
const handleSize = 200;
|
||
|
||
const isCurrentInside = event.target.scrollTop < handleSize;
|
||
if (isCurrentInside && !isInsideRange.value && !isLoading.value) {
|
||
isLoading.value = true;
|
||
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
||
document.querySelector(".messages")!.scrollTop = 0;
|
||
|
||
if (channel.value === undefined) {
|
||
isLoading.value = false;
|
||
createModal({
|
||
component: ErrorModal,
|
||
onClose: () => isSending.value = false,
|
||
props: {
|
||
error: "チャンネルの情報がありません。",
|
||
canClose: true,
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
|
||
const messagesRes = await client.value.request("message/list", {
|
||
channel: channel.value.id,
|
||
until: messages.value[0]?.id,
|
||
});
|
||
|
||
if (!messagesRes.success) {
|
||
isLoading.value = false;
|
||
createModal({
|
||
component: ErrorModal,
|
||
onClose: async () => await router.push("/"),
|
||
props: {
|
||
error: "メッセージが取得できませんでした。",
|
||
canClose: true,
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
|
||
messages.value = messagesRes.messages.toReversed().concat(messages.value);
|
||
isLoading.value = false;
|
||
}
|
||
|
||
isInsideRange.value = isCurrentInside;
|
||
}
|
||
|
||
const resize = (event: any) => {
|
||
event.target.style.height = "auto";
|
||
event.target.style.height = event.target.scrollHeight + 'px';
|
||
}
|
||
|
||
const send = async (e: Event) => {
|
||
e.preventDefault();
|
||
isSending.value = true;
|
||
|
||
if (channel.value === undefined) {
|
||
createModal({
|
||
component: ErrorModal,
|
||
onClose: () => isSending.value = false,
|
||
props: {
|
||
error: "チャンネルの情報がありません。",
|
||
canClose: true,
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (message.value.trim().length > 4096) {
|
||
isSending.value = false;
|
||
createTopNotice({
|
||
props: {
|
||
icon: "material-symbols:warning-rounded",
|
||
iconColor: "var(--warn-color)",
|
||
error: "メッセージは4096文字までである必要があります。",
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (message.value.trim().length === 0) {
|
||
isSending.value = false;
|
||
createTopNotice({
|
||
props: {
|
||
icon: "material-symbols:warning-rounded",
|
||
iconColor: "var(--warn-color)",
|
||
message: "メッセージを空にすることは出来ません。",
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
|
||
const channelId = route.params.channelId;
|
||
if (typeof channelId !== "string") {
|
||
isSending.value = false;
|
||
createModal({
|
||
component: ErrorModal,
|
||
props: {
|
||
error: "チャンネルが取得できませんでした。",
|
||
canClose: true,
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (!account.value?.success) {
|
||
isSending.value = false;
|
||
createModal({
|
||
component: ErrorModal,
|
||
props: {
|
||
error: "アカウント情報の取得に失敗しました。",
|
||
canClose: true,
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
|
||
const sendRes = await client.value.request("message/send", {
|
||
message: message.value,
|
||
channel: channel.value.id,
|
||
});
|
||
|
||
if (!sendRes.success) {
|
||
isSending.value = false;
|
||
createModal({
|
||
component: ErrorModal,
|
||
props: {
|
||
error: "メッセージの送信に失敗しました。",
|
||
canClose: true,
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
|
||
messages.value = [...messages.value, {
|
||
id: sendRes.id,
|
||
message: message.value,
|
||
channel: channelId,
|
||
createdAt: new Date().toUTCString(),
|
||
createdBy: {
|
||
...account.value,
|
||
},
|
||
}];
|
||
|
||
message.value = "";
|
||
const inputElem = document.querySelector(".message-input") as HTMLTextAreaElement;
|
||
inputElem.style.height = "auto";
|
||
isSending.value = false;
|
||
|
||
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
||
const messagesElem = document.querySelector(".messages")!;
|
||
messagesElem.scrollTop = messagesElem.scrollHeight;
|
||
}
|
||
|
||
const deleteMessage = (data: { id: string }) => {
|
||
const index = messages.value.findIndex(msg => msg.id === data.id);
|
||
if (index !== -1)
|
||
messages.value = messages.value.toSpliced(index, 1);
|
||
}
|
||
|
||
const now = ref(new Date());
|
||
onMounted(() => {
|
||
setInterval(() =>
|
||
now.value = new Date()
|
||
, 1000);
|
||
});
|
||
provide("now", now);
|
||
|
||
(async () => {
|
||
isProcessing.value = true;
|
||
|
||
if (!serverInfo.value?.success) {
|
||
throw new Error("サーバー情報の取得に失敗しました。");
|
||
}
|
||
|
||
const channelId = route.params.channelId;
|
||
if (typeof channelId !== "string") {
|
||
isProcessing.value = false;
|
||
createModal({
|
||
component: GoHomeError,
|
||
onClose: async () => await router.push("/"),
|
||
props: {
|
||
error: "不正なアクセスです。",
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (presentCommunity.value === undefined) {
|
||
isProcessing.value = false;
|
||
createModal({
|
||
component: GoHomeError,
|
||
onClose: async () => await router.push("/"),
|
||
props: {
|
||
error: "不正なアクセスです。",
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
|
||
const channelRes = await client.value.request("channel/get", {
|
||
id: channelId,
|
||
});
|
||
|
||
if (!channelRes.success) {
|
||
isProcessing.value = false;
|
||
createModal({
|
||
component: GoHomeError,
|
||
onClose: async () => await router.push("/"),
|
||
props: {
|
||
error: "チャンネルが取得できませんでした。",
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
|
||
channel.value = channelRes.channel;
|
||
|
||
document.title = `${channelRes.channel.name} - ${presentCommunity.value.name} | ${serverInfo.value.name}`;
|
||
title.value = `${channelRes.channel.name} - ${presentCommunity.value.name}`;
|
||
|
||
const messagesRes = await client.value.request("message/list", {
|
||
channel: channel.value.id,
|
||
});
|
||
|
||
if (!messagesRes.success) {
|
||
isProcessing.value = false;
|
||
createModal({
|
||
component: GoHomeError,
|
||
onClose: async () => await router.push("/"),
|
||
props: {
|
||
error: "メッセージが取得できませんでした。",
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
|
||
messages.value = messages.value.concat(messagesRes.messages).reverse();
|
||
isProcessing.value = false;
|
||
|
||
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
||
const messagesElem = document.querySelector(".messages")!;
|
||
messagesElem.scrollTop = messagesElem.scrollHeight;
|
||
})();
|
||
</script> |