외교 조회 권한자의 문서와 시스템 메시지 도착 알림 복원
This commit is contained in:
@@ -130,6 +130,8 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
const tournamentType = ref<TournamentType | null>(null);
|
||||
const surveyNotice = ref<NonNullable<FrontStatus['latestVote']> | null>(null);
|
||||
const privateMessageNotice = ref<PrivateMessageNotice | null>(null);
|
||||
const diplomacyMessageNotice = ref<PrivateMessageNotice | null>(null);
|
||||
let dismissedDiplomacyMessageId = 0;
|
||||
let dismissedPrivateMessageId = 0;
|
||||
let lastGeneralRecordId = 0;
|
||||
let lastWorldHistoryId = 0;
|
||||
@@ -351,6 +353,39 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
privateMessageNotice.value = null;
|
||||
};
|
||||
|
||||
const reconcileDiplomacyMessageNotice = (nextMessages: MessageBundle | null) => {
|
||||
const viewerGeneralId = general.value?.id;
|
||||
if (!nextMessages || !viewerGeneralId || nextMessages.permission < 3) {
|
||||
diplomacyMessageNotice.value = null;
|
||||
return;
|
||||
}
|
||||
const newestIncomingId = nextMessages.diplomacy
|
||||
.filter(
|
||||
(message) =>
|
||||
message.src.nationId !== nextMessages.nationId &&
|
||||
!message.option?.invalid &&
|
||||
!message.option?.permissionRedacted
|
||||
)
|
||||
.reduce((latest, message) => Math.max(latest, message.id), 0);
|
||||
const latestReadId = nextMessages.latestRead.diplomacy;
|
||||
if (dismissedDiplomacyMessageId <= latestReadId) dismissedDiplomacyMessageId = 0;
|
||||
if (newestIncomingId <= latestReadId || newestIncomingId <= dismissedDiplomacyMessageId) {
|
||||
diplomacyMessageNotice.value = null;
|
||||
return;
|
||||
}
|
||||
if (diplomacyMessageNotice.value?.messageId !== newestIncomingId) {
|
||||
diplomacyMessageNotice.value = { messageId: newestIncomingId };
|
||||
}
|
||||
};
|
||||
|
||||
const dismissDiplomacyMessageNotice = () => {
|
||||
dismissedDiplomacyMessageId = Math.max(
|
||||
dismissedDiplomacyMessageId,
|
||||
diplomacyMessageNotice.value?.messageId ?? 0
|
||||
);
|
||||
diplomacyMessageNotice.value = null;
|
||||
};
|
||||
|
||||
const mergeRecentRecords = (current: RecentRecord[], incoming: RecentRecord[]): RecentRecord[] => {
|
||||
const merged = new Map(current.map((entry) => [entry.id, entry]));
|
||||
for (const entry of incoming) {
|
||||
@@ -370,6 +405,8 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
surveyNotice.value = null;
|
||||
privateMessageNotice.value = null;
|
||||
dismissedPrivateMessageId = 0;
|
||||
diplomacyMessageNotice.value = null;
|
||||
dismissedDiplomacyMessageId = 0;
|
||||
};
|
||||
|
||||
const applyRecentRecords = (records: Awaited<ReturnType<typeof trpc.general.getRecentRecords.query>>) => {
|
||||
@@ -448,10 +485,12 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
if (patch.messages !== undefined) {
|
||||
messages.value = structurallyShare(messages.value, patch.messages);
|
||||
reconcilePrivateMessageNotice(messages.value);
|
||||
reconcileDiplomacyMessageNotice(messages.value);
|
||||
} else if (patch.contextSnapshot !== undefined || patch.general !== undefined) {
|
||||
// Main data is fetched concurrently, so the message bundle can arrive
|
||||
// before the authenticated general context needed to identify senders.
|
||||
reconcilePrivateMessageNotice(messages.value);
|
||||
reconcileDiplomacyMessageNotice(messages.value);
|
||||
}
|
||||
if (patch.messageContacts !== undefined) {
|
||||
messageContacts.value = structurallyShare(messageContacts.value, patch.messageContacts);
|
||||
@@ -688,6 +727,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
worldMap.value = structurallyShare(worldMap.value, map);
|
||||
messages.value = structurallyShare(messages.value, messageData);
|
||||
reconcilePrivateMessageNotice(messages.value);
|
||||
reconcileDiplomacyMessageNotice(messages.value);
|
||||
messageContacts.value = structurallyShare(messageContacts.value, contacts);
|
||||
reservedGeneralTurns.value = structurallyShare<unknown>(
|
||||
reservedGeneralTurns.value,
|
||||
@@ -960,6 +1000,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
},
|
||||
};
|
||||
reconcilePrivateMessageNotice(messages.value);
|
||||
reconcileDiplomacyMessageNotice(messages.value);
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
@@ -974,6 +1015,12 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
return readLatestMessage('private', messageId);
|
||||
};
|
||||
|
||||
const acknowledgeDiplomacyMessageNotice = async (): Promise<boolean> => {
|
||||
const messageId = diplomacyMessageNotice.value?.messageId;
|
||||
if (!messageId) return false;
|
||||
return readLatestMessage('diplomacy', messageId);
|
||||
};
|
||||
|
||||
const deleteMessage = async (messageId: number) => {
|
||||
const id = generalId.value;
|
||||
if (!id) {
|
||||
@@ -1399,6 +1446,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
tournamentType,
|
||||
surveyNotice,
|
||||
privateMessageNotice,
|
||||
diplomacyMessageNotice,
|
||||
messageDraftText,
|
||||
targetMailbox,
|
||||
mailboxGroups,
|
||||
@@ -1409,6 +1457,8 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
stopRealtime,
|
||||
dismissSurveyNotice,
|
||||
dismissPrivateMessageNotice,
|
||||
dismissDiplomacyMessageNotice,
|
||||
acknowledgeDiplomacyMessageNotice,
|
||||
acknowledgePrivateMessageNotice,
|
||||
loadMainData,
|
||||
refreshMessages,
|
||||
|
||||
@@ -89,6 +89,7 @@ const {
|
||||
tournamentType,
|
||||
surveyNotice,
|
||||
privateMessageNotice,
|
||||
diplomacyMessageNotice,
|
||||
messageDraftText,
|
||||
targetMailbox,
|
||||
mailboxGroups,
|
||||
@@ -168,6 +169,7 @@ const formatRecord = (entry: { text: string; createdAt?: string | Date }, append
|
||||
};
|
||||
|
||||
let surveyNoticeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let diplomacyMessageNoticeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let privateMessageNoticeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
watch(surveyNotice, (notice) => {
|
||||
if (surveyNoticeTimer) {
|
||||
@@ -187,6 +189,15 @@ watch(privateMessageNotice, (notice) => {
|
||||
privateMessageNoticeTimer = setTimeout(() => dashboard.dismissPrivateMessageNotice(), 10 * 60_000);
|
||||
}
|
||||
});
|
||||
watch(diplomacyMessageNotice, (notice) => {
|
||||
if (diplomacyMessageNoticeTimer) {
|
||||
clearTimeout(diplomacyMessageNoticeTimer);
|
||||
diplomacyMessageNoticeTimer = null;
|
||||
}
|
||||
if (notice) {
|
||||
diplomacyMessageNoticeTimer = setTimeout(() => dashboard.dismissDiplomacyMessageNotice(), 10 * 60_000);
|
||||
}
|
||||
});
|
||||
onUnmounted(() => {
|
||||
if (surveyNoticeTimer) {
|
||||
clearTimeout(surveyNoticeTimer);
|
||||
@@ -194,6 +205,7 @@ onUnmounted(() => {
|
||||
if (privateMessageNoticeTimer) {
|
||||
clearTimeout(privateMessageNoticeTimer);
|
||||
}
|
||||
if (diplomacyMessageNoticeTimer) clearTimeout(diplomacyMessageNoticeTimer);
|
||||
dashboard.stopRealtime();
|
||||
window.removeEventListener('storage', handleMobilePanelStorage);
|
||||
document.removeEventListener(MOBILE_MAIN_PANEL_ORDER_CHANGED_EVENT, reloadMobilePanelOrder);
|
||||
@@ -253,6 +265,13 @@ const acknowledgePrivateMessageNotice = async (moveToMessage: boolean) => {
|
||||
document.querySelector('.PrivateTalk > .stickyAnchor')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
};
|
||||
|
||||
const acknowledgeDiplomacyMessageNotice = async (moveToMessage: boolean) => {
|
||||
if (!(await dashboard.acknowledgeDiplomacyMessageNotice())) return;
|
||||
if (!moveToMessage) return;
|
||||
await nextTick();
|
||||
document.querySelector('.DiplomacyTalk > .stickyAnchor')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
};
|
||||
|
||||
const handleNavigationAction = (action: NonNullable<MainNavigationLink['action']>) => {
|
||||
if (action === 'show-version') versionDialog.value?.showModal();
|
||||
};
|
||||
@@ -349,6 +368,32 @@ watch(
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<aside
|
||||
v-if="diplomacyMessageNotice"
|
||||
class="private-message-notice diplomacy-message-notice"
|
||||
:class="{ 'diplomacy-message-notice-stacked': privateMessageNotice }"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
data-testid="diplomacy-message-notice"
|
||||
>
|
||||
<div class="private-message-notice-title">
|
||||
<strong>새로운 외교 메시지</strong>
|
||||
<button
|
||||
type="button"
|
||||
class="private-message-notice-close"
|
||||
aria-label="외교 메시지 알림 닫기"
|
||||
@click="dashboard.dismissDiplomacyMessageNotice"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<p>새로운 외교 메시지가 도착했습니다.</p>
|
||||
<div class="private-message-notice-actions">
|
||||
<button type="button" @click="acknowledgeDiplomacyMessageNotice(true)">보러가기</button>
|
||||
<button type="button" @click="acknowledgeDiplomacyMessageNotice(false)">이미읽음</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section v-if="isMobile" class="layout-mobile">
|
||||
<template v-for="(panelId, panelIndex) in mobilePanelOrder" :key="panelId">
|
||||
<div v-if="panelId === 'commands'" class="mobile-panel" data-mobile-panel-id="commands">
|
||||
@@ -871,6 +916,10 @@ button {
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.diplomacy-message-notice-stacked {
|
||||
top: 180px;
|
||||
}
|
||||
|
||||
.private-message-notice-title {
|
||||
display: flex;
|
||||
min-height: 35px;
|
||||
@@ -1137,10 +1186,14 @@ button {
|
||||
}
|
||||
|
||||
.private-message-notice {
|
||||
z-index: 90;
|
||||
z-index: 1080;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
.diplomacy-message-notice-stacked {
|
||||
top: 180px;
|
||||
}
|
||||
|
||||
.layout-mobile [data-main-target='world-history'] {
|
||||
height: 359px;
|
||||
min-height: 0;
|
||||
|
||||
Reference in New Issue
Block a user