Feat: メッセージの送受信 / New: ユーザーのiconプロパティ / New: logエンティティ・リポジトリ / Chg: コミュニティリポジトリのスキーマのiconをoptionalに / Del: 不要なimport / Fix: Vue起動前のindex.htmlの背景色をVueと同期 / Enhance: Service Workerを改善 / Fix: 最初に開いたページが動作しない問題 / Feat: 上部の通知モーダル / Feat: 閉じることができないエラーのモーダルに再読み込みボタンを追加 / Fix: はみ出す挙動などのCSSを修正
This commit is contained in:
@@ -9,14 +9,114 @@
|
||||
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 {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
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>
|
||||
|
||||
@@ -24,18 +124,18 @@
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { account, presentCommunity, serverInfo } from "@/lib/account";
|
||||
import client from "@/lib/client";
|
||||
import { createModal } from "@/lib/modal";
|
||||
import { createModal, createTopNotice } from "@/lib/modal";
|
||||
import GoHomeError from "@/components/Modal/GoHomeError.vue";
|
||||
import { 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();
|
||||
const isProcessing = ref<boolean>(false);
|
||||
|
||||
const channel = ref<Extract<ApiMap["channel/list"]["response"], { channels: any }>["channels"][number]>();
|
||||
|
||||
if (!serverInfo.value.success) {
|
||||
throw new Error("サーバー情報の取得に失敗しました。");
|
||||
@@ -55,6 +155,176 @@ if (!account.value.success) {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
isProcessing.value = true;
|
||||
|
||||
@@ -108,6 +378,27 @@ if (!account.value.success) {
|
||||
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>
|
||||
Reference in New Issue
Block a user