Files
lynq-chat/packages/frontend/src/routes/community/channel.vue
T

412 lines
9.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>