Files
core/hwe/ts/components/MessagePanel.vue
T
Hide_D f056ef9da3 feat: PageFront 메시지 입력(wip)
- '즐겨찾기' 기능 없음
2023-03-09 02:20:22 +09:00

516 lines
13 KiB
Vue

<template>
<div class="MessagePanel">
<div class="MessageInputForm row gx-0">
<div id="mailbox_list-col" class="col-6 col-md-2 d-grid">
<BFormSelect v-model="targetMailbox" class="bg-dark text-white">
<optgroup
v-for="group of mailboxList"
:key="group.label"
:label="group.label"
:style="{
backgroundColor: group.color,
color: !group.color ? undefined : isBrightColor(group.color) ? '#000000' : '#ffffff',
}"
>
<BFormSelectOption
v-for="target of group.options"
:key="target.value"
:disabled="target.disabled"
:value="target.value"
:style="{
backgroundColor: target.color ?? '#000000',
color: isBrightColor(target.color ?? '#000000') ? '#000000' : '#ffffff',
}"
>{{ target.text }}
</BFormSelectOption>
</optgroup>
</BFormSelect>
</div>
<div id="msg_input-col" class="col-12 col-md-8 d-grid">
<input v-model="newMessageText" type="text" maxlength="99" @keydown.enter="sendMessage" class="form-control" />
</div>
<div id="msg_submit-col" class="col-6 col-md-2 d-grid">
<BButton variant="primary" @click="sendMessage">서신전달&amp;갱신</BButton>
</div>
</div>
<div class="PublicTalk">
<div class="BoardHeader bg0">전체 메시지</div>
<template v-if="messagePublic.length == 0">
<div>메시지가 없습니다.</div>
</template>
<template v-else>
<MessagePlate
v-for="msg of messagePublic"
:key="msg.id"
:modelValue="msg"
:generalID="generalID"
:generalName="generalName"
:nationID="nationID"
:permissionLevel="permissionLevel"
></MessagePlate>
</template>
</div>
<div class="NationalTalk">
<div class="BoardHeader bg0">국가 메시지</div>
<template v-if="messageNational.length == 0">
<div>메시지가 없습니다.</div>
</template>
<template v-else>
<MessagePlate
v-for="msg of messageNational"
:key="msg.id"
:modelValue="msg"
:generalID="generalID"
:generalName="generalName"
:nationID="nationID"
:permissionLevel="permissionLevel"
></MessagePlate>
</template>
</div>
<div class="PrivateTalk">
<div class="BoardHeader bg0">개인 메시지</div>
<template v-if="messagePrivate.length == 0">
<div>메시지가 없습니다.</div>
</template>
<template v-else>
<MessagePlate
v-for="msg of messagePrivate"
:key="msg.id"
:modelValue="msg"
:generalID="generalID"
:generalName="generalName"
:nationID="nationID"
:permissionLevel="permissionLevel"
></MessagePlate>
</template>
</div>
<div class="DiplomacyTalk">
<div class="BoardHeader bg0">외교 메시지</div>
<template v-if="messageDiplomacy.length == 0">
<div>메시지가 없습니다.</div>
</template>
<template v-else>
<MessagePlate
v-for="msg of messageDiplomacy"
:key="msg.id"
:modelValue="msg"
:generalID="generalID"
:generalName="generalName"
:nationID="nationID"
:permissionLevel="permissionLevel"
></MessagePlate>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, toRef, watch, type Ref } from "vue";
import { delay } from "@/util/delay";
import { SammoAPI } from "@/SammoAPI";
import { isString } from "lodash";
import type { MabilboxListResponse, MailboxItem, MsgItem, MsgResponse, MsgType } from "@/defs/API/Message";
import MessagePlate from "@/components/MessagePlate.vue";
import { useToast, BFormSelect, BFormSelectOptionGroup } from "bootstrap-vue-3";
import { unwrap } from "@/util/unwrap";
import { isBrightColor } from "@/util/isBrightColor";
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");
let nationMailbox = nationID.value + 9000;
watch(nationID, (newVal) => {
nationMailbox = newVal + 9000;
});
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;
}
const targetMailbox = ref(nationID.value + 9000);
type MailboxGroup = {
label: string;
color?: string;
options: MailboxTarget[];
};
type MailboxTarget = {
value: number;
text: string;
nationID: number;
disabled?: true;
color?: string;
};
const mailboxList = ref<MailboxGroup[]>([]);
const newMessageText = ref<string>("");
function refreshMailboxList(obj: MabilboxListResponse) {
let myNationColor = "#000000";
const diplomacyMailboxList: MailboxGroup = {
label: "외교메시지",
color: "#000000",
options: [],
};
const nationMailboxList: MailboxGroup[] = [];
obj.nation.sort(function (lhs, rhs) {
if (lhs.mailbox == nationMailbox) {
return -1;
}
if (rhs.mailbox == nationMailbox) {
return 1;
}
return lhs.mailbox - rhs.mailbox;
});
for (const nation of obj.nation) {
if (nationMailbox == nation.mailbox) {
myNationColor = nation.color;
//nationColor저장하는 코드가 있었음
}
const nationBox: MailboxGroup = {
label: nation.name,
color: nation.color,
options: [],
};
nation.general.sort(function (lhs, rhs) {
if (lhs[1] < rhs[1]) {
return -1;
}
if (lhs[1] > rhs[1]) {
return 1;
}
return 0;
});
for (const [destGeneralID, destGeneralName, destGeneralFlag] of nation.general) {
const isRuler = !!(destGeneralFlag & 0x1);
const isAmbassador = !!(destGeneralFlag & 0x4);
if (destGeneralID == generalID.value) {
continue;
}
let textName = destGeneralName;
if (isRuler) {
textName = `*${textName}*`;
} else if (isAmbassador) {
textName = `#${textName}#`;
}
const target: MailboxTarget = {
value: destGeneralID,
text: textName,
nationID: nation.nationID,
};
if (permissionLevel.value == 4 && isAmbassador && nationMailbox != nation.mailbox) {
target.disabled = true;
}
nationBox.options.push(target);
}
nationMailboxList.push(nationBox);
if (permissionLevel.value < 4 || nationMailbox == nation.mailbox) {
continue;
}
diplomacyMailboxList.options.push({
value: nation.mailbox,
text: nation.name,
nationID: nation.nationID,
color: nation.color,
});
}
const favoriteBox: MailboxGroup = {
label: "즐겨찾기",
color: "#000000",
options: [
{
value: nationMailbox,
text: "【 아국 메세지 】",
nationID: nationID.value,
color: myNationColor,
},
{
value: 9999,
text: "【 전체 메세지 】",
nationID: 0,
},
],
};
mailboxList.value = [favoriteBox, diplomacyMailboxList, ...nationMailboxList];
}
async function sendMessage() {
const text = newMessageText.value;
if (!text) {
return tryRefresh();
}
const mailbox = targetMailbox.value;
try {
const waiter = SammoAPI.Message.SendMessage({
mailbox,
text,
});
newMessageText.value = "";
await waiter;
await tryRefresh();
} catch (e) {
if (isString(e)) {
toasts.warning({
title: "메시지 전송 실패",
body: e,
});
}
console.error(e);
}
}
async function tryFullRefresh() {
try {
const refreshP = tryRefresh();
const response = await SammoAPI.Message.GetContactList();
refreshMailboxList(response);
await refreshP;
} catch (e) {
if (isString(e)) {
toasts.warning({
title: " 실패했습니다.",
body: e,
});
}
console.error(e);
return;
}
beginRefreshTimer();
}
defineExpose({
tryRefresh,
tryFullRefresh,
});
onMounted(async () => {
await tryFullRefresh();
});
</script>
<style lang="scss" scoped>
@import "@scss/common/break_500px.scss";
.BoardHeader {
color: white;
outline-style: solid;
outline-width: 1px;
outline-color: gray;
}
.PublicTalk {
}
@include media-1000px {
.MessagePanel {
display: grid;
grid-template-columns: 1fr 1fr;
}
.MessageInputForm {
grid-column: 1 / 3;
}
.PublicTalk {
border-right: 1px solid gray;
}
.PrivateTalk {
border-right: 1px solid gray;
}
}
</style>