277 lines
6.8 KiB
Vue
277 lines
6.8 KiB
Vue
<template>
|
|
<div class="MessageInputForm">메시지 입력</div>
|
|
<div class="PublicTalk">
|
|
<div>전체 메시지</div>
|
|
<template v-if="messagePublic.length == 0">
|
|
<div>메시지가 없습니다.</div>
|
|
</template>
|
|
<MessagePlate
|
|
v-for="msg of messagePublic"
|
|
:key="msg.id"
|
|
:modelValue="msg"
|
|
:generalID="generalID"
|
|
:generalName="generalName"
|
|
:nationID="nationID"
|
|
:permissionLevel="permissionLevel"
|
|
></MessagePlate>
|
|
</div>
|
|
<div class="NationalTalk">
|
|
<div>국가 메시지</div>
|
|
<template v-if="messageNational.length == 0">
|
|
<div>메시지가 없습니다.</div>
|
|
</template>
|
|
<MessagePlate
|
|
v-for="msg of messageNational"
|
|
:key="msg.id"
|
|
:modelValue="msg"
|
|
:generalID="generalID"
|
|
:generalName="generalName"
|
|
:nationID="nationID"
|
|
:permissionLevel="permissionLevel"
|
|
></MessagePlate>
|
|
</div>
|
|
<div class="PrivateTalk">
|
|
<div>개인 메시지</div>
|
|
<template v-if="messagePrivate.length == 0">
|
|
<div>메시지가 없습니다.</div>
|
|
</template>
|
|
<MessagePlate
|
|
v-for="msg of messagePrivate"
|
|
:key="msg.id"
|
|
:modelValue="msg"
|
|
:generalID="generalID"
|
|
:generalName="generalName"
|
|
:nationID="nationID"
|
|
:permissionLevel="permissionLevel"
|
|
></MessagePlate>
|
|
</div>
|
|
<div class="DiplomacyTalk">
|
|
<div>외교 메시지</div>
|
|
<template v-if="messageDiplomacy.length == 0">
|
|
<div>메시지가 없습니다.</div>
|
|
</template>
|
|
<MessagePlate
|
|
v-for="msg of messageDiplomacy"
|
|
:key="msg.id"
|
|
:modelValue="msg"
|
|
:generalID="generalID"
|
|
:generalName="generalName"
|
|
:nationID="nationID"
|
|
:permissionLevel="permissionLevel"
|
|
></MessagePlate>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { onMounted, ref, toRef, type Ref } from "vue";
|
|
import { delay } from "@/util/delay";
|
|
import { SammoAPI } from "@/SammoAPI";
|
|
import { isString } from "lodash";
|
|
import type { MsgItem, MsgResponse, MsgType } from "@/defs/API/Message";
|
|
import MessagePlate from "@/components/MessagePlate.vue";
|
|
import { useToast } from "bootstrap-vue-3";
|
|
import { unwrap } from "@/util/unwrap";
|
|
|
|
const toasts = unwrap(useToast());
|
|
|
|
const emit = defineEmits<{
|
|
(event: "request-refresh"): void;
|
|
}>();
|
|
|
|
const props = defineProps<{
|
|
generalID: number;
|
|
generalName: string;
|
|
nationID: number;
|
|
permissionLevel: number;
|
|
}>();
|
|
|
|
const generalID = toRef(props, "generalID");
|
|
const generalName = toRef(props, "generalName");
|
|
const nationID = toRef(props, "nationID");
|
|
const permissionLevel = toRef(props, "permissionLevel");
|
|
|
|
onMounted(() => {
|
|
console.log("MessagePanel mounted");
|
|
console.log(props);
|
|
});
|
|
|
|
const lastSequence = ref(-1);
|
|
|
|
const initRefreshLimit = 20;
|
|
let refreshLimit = initRefreshLimit;
|
|
const refreshP: Set<Promise<boolean>> = new Set();
|
|
let lastRefreshDone = 0;
|
|
let refreshTimer: null | number = null;
|
|
|
|
const messageStorage = new Map<number, MsgItem>();
|
|
const messagePublic = ref<MsgItem[]>([]);
|
|
const messageNational = ref<MsgItem[]>([]);
|
|
const messagePrivate = ref<MsgItem[]>([]);
|
|
const messageDiplomacy = ref<MsgItem[]>([]);
|
|
|
|
const messageIndexedList: Record<MsgType, Ref<MsgItem[]>> = {
|
|
public: messagePublic,
|
|
national: messageNational,
|
|
private: messagePrivate,
|
|
diplomacy: messageDiplomacy,
|
|
};
|
|
|
|
function updateMsgResponse(response: MsgResponse) {
|
|
if (!response.keepRecent) {
|
|
messageStorage.clear();
|
|
for (const msgList of Object.values(messageIndexedList)) {
|
|
msgList.value.length = 0;
|
|
}
|
|
}
|
|
|
|
if (response.generalName != generalName.value) {
|
|
emit("request-refresh");
|
|
return;
|
|
}
|
|
if (response.nationID != nationID.value) {
|
|
emit("request-refresh");
|
|
return;
|
|
}
|
|
|
|
lastSequence.value = Math.max(lastSequence.value, response.sequence);
|
|
|
|
for (const msgType of Object.keys(messageIndexedList) as (keyof typeof messageIndexedList)[]) {
|
|
const msgList = messageIndexedList[msgType];
|
|
|
|
const newMsgList = response[msgType];
|
|
if (newMsgList.length == 0) {
|
|
continue;
|
|
}
|
|
|
|
//순서가 어떤 순서인지 모르니, 여기서 내림차순으로 맞춘다.
|
|
newMsgList.sort((a, b) => b.id - a.id);
|
|
|
|
if (msgList.value.length == 0) {
|
|
msgList.value = newMsgList;
|
|
continue;
|
|
}
|
|
|
|
//head test
|
|
const filteredMsgList: MsgItem[] = [];
|
|
|
|
for (const msg of newMsgList) {
|
|
const oldMsg = messageStorage.get(msg.id);
|
|
if (oldMsg !== undefined) {
|
|
continue;
|
|
}
|
|
messageStorage.set(msg.id, msg);
|
|
filteredMsgList.push(msg);
|
|
}
|
|
|
|
if (filteredMsgList.length == 0) {
|
|
continue;
|
|
}
|
|
|
|
const msgHeadID = msgList.value[0].id;
|
|
const newMsgTailID = filteredMsgList[filteredMsgList.length - 1].id;
|
|
if (newMsgTailID > msgHeadID) {
|
|
msgList.value = [...filteredMsgList, ...msgList.value];
|
|
continue;
|
|
}
|
|
|
|
const msgTailID = msgList.value[msgList.value.length - 1].id;
|
|
const newMsgHeadID = filteredMsgList[0].id;
|
|
|
|
if (msgTailID > newMsgHeadID) {
|
|
msgList.value.push(...filteredMsgList);
|
|
continue;
|
|
}
|
|
|
|
//중간에 삽입되는 경우는 에러이다.
|
|
console.error("중간 삽입 있음", msgType, newMsgList);
|
|
}
|
|
}
|
|
|
|
function beginRefreshTimer() {
|
|
if (refreshTimer) {
|
|
clearInterval(refreshTimer);
|
|
refreshTimer = null;
|
|
}
|
|
refreshTimer = window.setInterval(function () {
|
|
const now = Date.now();
|
|
if (lastRefreshDone + 5000 < now) {
|
|
//만약 서버 응답이 없다면?
|
|
|
|
if (refreshP.size > refreshLimit && refreshTimer) {
|
|
clearInterval(refreshTimer);
|
|
refreshTimer = null;
|
|
toasts.danger(
|
|
{
|
|
title: "메시지 자동 갱신 실패",
|
|
body: "서버 응답이 없습니다. 새로고침을 해주세요.",
|
|
},
|
|
{
|
|
delay: 1000 * 3600,
|
|
}
|
|
);
|
|
return;
|
|
}
|
|
|
|
void tryRefresh();
|
|
}
|
|
}, 2500);
|
|
}
|
|
|
|
async function tryRefresh() {
|
|
if (refreshP.size > 0) {
|
|
await Promise.race([...refreshP, delay(500)]);
|
|
}
|
|
|
|
const waiterP = (async () => {
|
|
let response: MsgResponse | undefined = undefined;
|
|
try {
|
|
response = await SammoAPI.Message.GetRecentMessage({
|
|
sequence: lastSequence.value,
|
|
});
|
|
|
|
const now = Date.now();
|
|
if (lastRefreshDone < now) {
|
|
lastRefreshDone = now;
|
|
}
|
|
} catch (e) {
|
|
if (isString(e)) {
|
|
toasts.warning({
|
|
title: '갱신 실패',
|
|
body: e,
|
|
});
|
|
}
|
|
console.error(e);
|
|
}
|
|
|
|
if (!response) {
|
|
return false;
|
|
}
|
|
|
|
updateMsgResponse(response);
|
|
|
|
return true;
|
|
})();
|
|
void waiterP.then((result) => {
|
|
if (!result) {
|
|
console.error("?! result");
|
|
refreshLimit--;
|
|
} else {
|
|
refreshLimit = initRefreshLimit;
|
|
}
|
|
refreshP.delete(waiterP);
|
|
});
|
|
refreshP.add(waiterP);
|
|
|
|
return waiterP;
|
|
}
|
|
|
|
defineExpose({
|
|
tryRefresh,
|
|
});
|
|
|
|
onMounted(async () => {
|
|
beginRefreshTimer();
|
|
await tryRefresh();
|
|
});
|
|
</script>
|