외교 조회 권한자의 문서와 시스템 메시지 도착 알림 복원
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { resolveNationPermission } from '../src/router/nation/shared.js';
|
||||||
|
|
||||||
import { createEmptyRealtimeReadModelChanges, type RealtimeEvent } from '@sammo-ts/common';
|
import { createEmptyRealtimeReadModelChanges, type RealtimeEvent } from '@sammo-ts/common';
|
||||||
import { MESSAGE_MAILBOX_NATIONAL_BASE } from '@sammo-ts/logic';
|
import { MESSAGE_MAILBOX_NATIONAL_BASE } from '@sammo-ts/logic';
|
||||||
@@ -288,3 +289,28 @@ describe('public realtime event privacy boundary', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('diplomatic notification recipient roles', () => {
|
||||||
|
it.each([
|
||||||
|
['군주', 12, 'normal', {}, true],
|
||||||
|
['외교권자', 1, 'ambassador', {}, true],
|
||||||
|
['조언자', 1, 'auditor', {}, true],
|
||||||
|
['일반 장수', 1, 'normal', {}, false],
|
||||||
|
['수뇌', 11, 'normal', {}, false],
|
||||||
|
['외교 금지', 1, 'ambassador', { noAmbassador: 1 }, false],
|
||||||
|
] as const)(
|
||||||
|
'%s receives only eligible nation diplomacy invalidations',
|
||||||
|
(_, officerLevel, permission, penalty, allowed) => {
|
||||||
|
const canReadDiplomacy =
|
||||||
|
resolveNationPermission({ nationId: 2, officerLevel, meta: { permission }, penalty }, {}, false) >= 3;
|
||||||
|
expect(canReadDiplomacy).toBe(allowed);
|
||||||
|
const event: RealtimeEvent = { type: 'messagesChanged', mailboxes: [], diplomacyMailboxes: [9002] };
|
||||||
|
expect(toPublicRealtimeEvent(event, [{ ...viewer, canReadDiplomacy }])).toEqual(
|
||||||
|
allowed ? { type: 'messagesInvalidated', refreshGrant } : null
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
toPublicRealtimeEvent({ ...event, diplomacyMailboxes: [9003] }, [{ ...viewer, canReadDiplomacy }])
|
||||||
|
).toBeNull();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -6418,3 +6418,128 @@ for (const viewport of [
|
|||||||
await page.screenshot({ path: testInfo.outputPath('npc-message-icons.png'), fullPage: true });
|
await page.screenshot({ path: testInfo.outputPath('npc-message-icons.png'), fullPage: true });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const role of [
|
||||||
|
{ name: '군주', officerLevel: 12, permission: 4 },
|
||||||
|
{ name: '외교권자', officerLevel: 1, permission: 4 },
|
||||||
|
{ name: '조언자', officerLevel: 1, permission: 3 },
|
||||||
|
{ name: '일반 장수', officerLevel: 1, permission: 0 },
|
||||||
|
]) {
|
||||||
|
for (const width of [1200, 390]) {
|
||||||
|
test(`diplomacy arrival notice ${role.name} ${width}`, async ({ page }, testInfo) => {
|
||||||
|
const state: NavigationFixture = {
|
||||||
|
...role,
|
||||||
|
nationLevel: 3,
|
||||||
|
stage: 0,
|
||||||
|
npcMode: 1,
|
||||||
|
latestVote: null,
|
||||||
|
generalMeCalls: 0,
|
||||||
|
operations: [],
|
||||||
|
messages: { ...emptyMessages(role.permission), nationId: 1 },
|
||||||
|
};
|
||||||
|
await installRealtimeHarness(page);
|
||||||
|
await installFixture(page, state);
|
||||||
|
await page.setViewportSize({ width, height: 900 });
|
||||||
|
await waitForMain(page);
|
||||||
|
await waitForMainRealtime(page);
|
||||||
|
const notice = page.getByTestId('diplomacy-message-notice');
|
||||||
|
const message = (id: number, sourceNation = 2, option: Record<string, unknown> | null = null) => ({
|
||||||
|
...privateMessage(id, 9),
|
||||||
|
msgType: 'diplomacy',
|
||||||
|
src: { ...privateMessage(id, 9).src, nationId: sourceNation },
|
||||||
|
text: id === 50 ? '외교 문서가 도착했습니다.' : '불가침 제의 서신',
|
||||||
|
option,
|
||||||
|
});
|
||||||
|
state.messages = { ...emptyMessages(role.permission), nationId: 1, diplomacy: [message(49, 1)] };
|
||||||
|
await emitMessagesInvalidation(page);
|
||||||
|
await expect(notice).toHaveCount(0);
|
||||||
|
for (const id of [50, 51]) {
|
||||||
|
state.messages = {
|
||||||
|
...emptyMessages(role.permission),
|
||||||
|
nationId: 1,
|
||||||
|
diplomacy: [message(id, 2, id === 51 ? { action: 'noAggression' } : null)],
|
||||||
|
};
|
||||||
|
const before = state.operations.filter((op) => op === 'messages.getRecent').length;
|
||||||
|
await emitMessagesInvalidation(page);
|
||||||
|
await expect
|
||||||
|
.poll(() => state.operations.filter((op) => op === 'messages.getRecent').length)
|
||||||
|
.toBeGreaterThan(before);
|
||||||
|
if (role.permission < 3) {
|
||||||
|
await expect(notice).toHaveCount(0);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await expect(notice).toContainText('새로운 외교 메시지가 도착했습니다.');
|
||||||
|
await emitMessagesInvalidation(page);
|
||||||
|
await expect(notice).toHaveCount(1);
|
||||||
|
const titleVisible = await notice.locator('strong').evaluate((el) => {
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
const hit = document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2);
|
||||||
|
return hit === el || el.contains(hit);
|
||||||
|
});
|
||||||
|
expect(titleVisible).toBe(true);
|
||||||
|
|
||||||
|
await testInfo.attach(`diplomacy-${id}.png`, {
|
||||||
|
body: await notice.screenshot(),
|
||||||
|
contentType: 'image/png',
|
||||||
|
});
|
||||||
|
const geometry = await notice.evaluate((el) => {
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: r.x,
|
||||||
|
y: r.y,
|
||||||
|
width: r.width,
|
||||||
|
height: r.height,
|
||||||
|
font: getComputedStyle(el).font,
|
||||||
|
html: el.outerHTML,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
expect(geometry.x).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(geometry.x + geometry.width).toBeLessThanOrEqual(width);
|
||||||
|
await testInfo.attach(`diplomacy-${id}.json`, {
|
||||||
|
body: JSON.stringify(geometry),
|
||||||
|
contentType: 'application/json',
|
||||||
|
});
|
||||||
|
await notice.getByRole('button', { name: id === 50 ? '이미읽음' : '보러가기' }).click();
|
||||||
|
await expect(notice).toHaveCount(0);
|
||||||
|
await expect
|
||||||
|
.poll(() =>
|
||||||
|
state.trpcRequests?.some(
|
||||||
|
({ operations, body }) =>
|
||||||
|
operations.includes('messages.readLatest') &&
|
||||||
|
JSON.stringify(body).includes('"type":"diplomacy"') &&
|
||||||
|
JSON.stringify(body).includes(`"messageId":${id}`)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.toBe(true);
|
||||||
|
}
|
||||||
|
if (role.permission >= 3) {
|
||||||
|
state.messages = {
|
||||||
|
...emptyMessages(role.permission),
|
||||||
|
nationId: 1,
|
||||||
|
private: [privateMessage(60, 9)],
|
||||||
|
diplomacy: [message(60)],
|
||||||
|
};
|
||||||
|
await emitMessagesInvalidation(page);
|
||||||
|
await expect(notice).toBeVisible();
|
||||||
|
const privateNotice = page.getByTestId('private-message-notice');
|
||||||
|
await expect(privateNotice).toBeVisible();
|
||||||
|
const first = await privateNotice.boundingBox();
|
||||||
|
const second = await notice.boundingBox();
|
||||||
|
expect(second!.y).toBeGreaterThan(first!.y + first!.height);
|
||||||
|
await notice.getByRole('button', { name: '외교 메시지 알림 닫기' }).click();
|
||||||
|
await emitMessagesInvalidation(page);
|
||||||
|
await expect(notice).toHaveCount(0);
|
||||||
|
state.messages = { ...emptyMessages(role.permission), nationId: 1, diplomacy: [message(61)] };
|
||||||
|
await emitMessagesInvalidation(page);
|
||||||
|
await expect(notice).toBeVisible();
|
||||||
|
state.messages = {
|
||||||
|
...emptyMessages(2),
|
||||||
|
nationId: 1,
|
||||||
|
diplomacy: [message(61, 2, { permissionRedacted: true })],
|
||||||
|
};
|
||||||
|
await emitMessagesInvalidation(page);
|
||||||
|
await expect(notice).toHaveCount(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -130,6 +130,8 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
|||||||
const tournamentType = ref<TournamentType | null>(null);
|
const tournamentType = ref<TournamentType | null>(null);
|
||||||
const surveyNotice = ref<NonNullable<FrontStatus['latestVote']> | null>(null);
|
const surveyNotice = ref<NonNullable<FrontStatus['latestVote']> | null>(null);
|
||||||
const privateMessageNotice = ref<PrivateMessageNotice | null>(null);
|
const privateMessageNotice = ref<PrivateMessageNotice | null>(null);
|
||||||
|
const diplomacyMessageNotice = ref<PrivateMessageNotice | null>(null);
|
||||||
|
let dismissedDiplomacyMessageId = 0;
|
||||||
let dismissedPrivateMessageId = 0;
|
let dismissedPrivateMessageId = 0;
|
||||||
let lastGeneralRecordId = 0;
|
let lastGeneralRecordId = 0;
|
||||||
let lastWorldHistoryId = 0;
|
let lastWorldHistoryId = 0;
|
||||||
@@ -351,6 +353,39 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
|||||||
privateMessageNotice.value = null;
|
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 mergeRecentRecords = (current: RecentRecord[], incoming: RecentRecord[]): RecentRecord[] => {
|
||||||
const merged = new Map(current.map((entry) => [entry.id, entry]));
|
const merged = new Map(current.map((entry) => [entry.id, entry]));
|
||||||
for (const entry of incoming) {
|
for (const entry of incoming) {
|
||||||
@@ -370,6 +405,8 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
|||||||
surveyNotice.value = null;
|
surveyNotice.value = null;
|
||||||
privateMessageNotice.value = null;
|
privateMessageNotice.value = null;
|
||||||
dismissedPrivateMessageId = 0;
|
dismissedPrivateMessageId = 0;
|
||||||
|
diplomacyMessageNotice.value = null;
|
||||||
|
dismissedDiplomacyMessageId = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyRecentRecords = (records: Awaited<ReturnType<typeof trpc.general.getRecentRecords.query>>) => {
|
const applyRecentRecords = (records: Awaited<ReturnType<typeof trpc.general.getRecentRecords.query>>) => {
|
||||||
@@ -448,10 +485,12 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
|||||||
if (patch.messages !== undefined) {
|
if (patch.messages !== undefined) {
|
||||||
messages.value = structurallyShare(messages.value, patch.messages);
|
messages.value = structurallyShare(messages.value, patch.messages);
|
||||||
reconcilePrivateMessageNotice(messages.value);
|
reconcilePrivateMessageNotice(messages.value);
|
||||||
|
reconcileDiplomacyMessageNotice(messages.value);
|
||||||
} else if (patch.contextSnapshot !== undefined || patch.general !== undefined) {
|
} else if (patch.contextSnapshot !== undefined || patch.general !== undefined) {
|
||||||
// Main data is fetched concurrently, so the message bundle can arrive
|
// Main data is fetched concurrently, so the message bundle can arrive
|
||||||
// before the authenticated general context needed to identify senders.
|
// before the authenticated general context needed to identify senders.
|
||||||
reconcilePrivateMessageNotice(messages.value);
|
reconcilePrivateMessageNotice(messages.value);
|
||||||
|
reconcileDiplomacyMessageNotice(messages.value);
|
||||||
}
|
}
|
||||||
if (patch.messageContacts !== undefined) {
|
if (patch.messageContacts !== undefined) {
|
||||||
messageContacts.value = structurallyShare(messageContacts.value, patch.messageContacts);
|
messageContacts.value = structurallyShare(messageContacts.value, patch.messageContacts);
|
||||||
@@ -688,6 +727,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
|||||||
worldMap.value = structurallyShare(worldMap.value, map);
|
worldMap.value = structurallyShare(worldMap.value, map);
|
||||||
messages.value = structurallyShare(messages.value, messageData);
|
messages.value = structurallyShare(messages.value, messageData);
|
||||||
reconcilePrivateMessageNotice(messages.value);
|
reconcilePrivateMessageNotice(messages.value);
|
||||||
|
reconcileDiplomacyMessageNotice(messages.value);
|
||||||
messageContacts.value = structurallyShare(messageContacts.value, contacts);
|
messageContacts.value = structurallyShare(messageContacts.value, contacts);
|
||||||
reservedGeneralTurns.value = structurallyShare<unknown>(
|
reservedGeneralTurns.value = structurallyShare<unknown>(
|
||||||
reservedGeneralTurns.value,
|
reservedGeneralTurns.value,
|
||||||
@@ -960,6 +1000,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
reconcilePrivateMessageNotice(messages.value);
|
reconcilePrivateMessageNotice(messages.value);
|
||||||
|
reconcileDiplomacyMessageNotice(messages.value);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -974,6 +1015,12 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
|||||||
return readLatestMessage('private', messageId);
|
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 deleteMessage = async (messageId: number) => {
|
||||||
const id = generalId.value;
|
const id = generalId.value;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
@@ -1399,6 +1446,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
|||||||
tournamentType,
|
tournamentType,
|
||||||
surveyNotice,
|
surveyNotice,
|
||||||
privateMessageNotice,
|
privateMessageNotice,
|
||||||
|
diplomacyMessageNotice,
|
||||||
messageDraftText,
|
messageDraftText,
|
||||||
targetMailbox,
|
targetMailbox,
|
||||||
mailboxGroups,
|
mailboxGroups,
|
||||||
@@ -1409,6 +1457,8 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
|||||||
stopRealtime,
|
stopRealtime,
|
||||||
dismissSurveyNotice,
|
dismissSurveyNotice,
|
||||||
dismissPrivateMessageNotice,
|
dismissPrivateMessageNotice,
|
||||||
|
dismissDiplomacyMessageNotice,
|
||||||
|
acknowledgeDiplomacyMessageNotice,
|
||||||
acknowledgePrivateMessageNotice,
|
acknowledgePrivateMessageNotice,
|
||||||
loadMainData,
|
loadMainData,
|
||||||
refreshMessages,
|
refreshMessages,
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ const {
|
|||||||
tournamentType,
|
tournamentType,
|
||||||
surveyNotice,
|
surveyNotice,
|
||||||
privateMessageNotice,
|
privateMessageNotice,
|
||||||
|
diplomacyMessageNotice,
|
||||||
messageDraftText,
|
messageDraftText,
|
||||||
targetMailbox,
|
targetMailbox,
|
||||||
mailboxGroups,
|
mailboxGroups,
|
||||||
@@ -168,6 +169,7 @@ const formatRecord = (entry: { text: string; createdAt?: string | Date }, append
|
|||||||
};
|
};
|
||||||
|
|
||||||
let surveyNoticeTimer: ReturnType<typeof setTimeout> | null = null;
|
let surveyNoticeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let diplomacyMessageNoticeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
let privateMessageNoticeTimer: ReturnType<typeof setTimeout> | null = null;
|
let privateMessageNoticeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
watch(surveyNotice, (notice) => {
|
watch(surveyNotice, (notice) => {
|
||||||
if (surveyNoticeTimer) {
|
if (surveyNoticeTimer) {
|
||||||
@@ -187,6 +189,15 @@ watch(privateMessageNotice, (notice) => {
|
|||||||
privateMessageNoticeTimer = setTimeout(() => dashboard.dismissPrivateMessageNotice(), 10 * 60_000);
|
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(() => {
|
onUnmounted(() => {
|
||||||
if (surveyNoticeTimer) {
|
if (surveyNoticeTimer) {
|
||||||
clearTimeout(surveyNoticeTimer);
|
clearTimeout(surveyNoticeTimer);
|
||||||
@@ -194,6 +205,7 @@ onUnmounted(() => {
|
|||||||
if (privateMessageNoticeTimer) {
|
if (privateMessageNoticeTimer) {
|
||||||
clearTimeout(privateMessageNoticeTimer);
|
clearTimeout(privateMessageNoticeTimer);
|
||||||
}
|
}
|
||||||
|
if (diplomacyMessageNoticeTimer) clearTimeout(diplomacyMessageNoticeTimer);
|
||||||
dashboard.stopRealtime();
|
dashboard.stopRealtime();
|
||||||
window.removeEventListener('storage', handleMobilePanelStorage);
|
window.removeEventListener('storage', handleMobilePanelStorage);
|
||||||
document.removeEventListener(MOBILE_MAIN_PANEL_ORDER_CHANGED_EVENT, reloadMobilePanelOrder);
|
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' });
|
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']>) => {
|
const handleNavigationAction = (action: NonNullable<MainNavigationLink['action']>) => {
|
||||||
if (action === 'show-version') versionDialog.value?.showModal();
|
if (action === 'show-version') versionDialog.value?.showModal();
|
||||||
};
|
};
|
||||||
@@ -349,6 +368,32 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
</aside>
|
</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">
|
<section v-if="isMobile" class="layout-mobile">
|
||||||
<template v-for="(panelId, panelIndex) in mobilePanelOrder" :key="panelId">
|
<template v-for="(panelId, panelIndex) in mobilePanelOrder" :key="panelId">
|
||||||
<div v-if="panelId === 'commands'" class="mobile-panel" data-mobile-panel-id="commands">
|
<div v-if="panelId === 'commands'" class="mobile-panel" data-mobile-panel-id="commands">
|
||||||
@@ -871,6 +916,10 @@ button {
|
|||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.diplomacy-message-notice-stacked {
|
||||||
|
top: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
.private-message-notice-title {
|
.private-message-notice-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 35px;
|
min-height: 35px;
|
||||||
@@ -1137,10 +1186,14 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.private-message-notice {
|
.private-message-notice {
|
||||||
z-index: 90;
|
z-index: 1080;
|
||||||
top: 16px;
|
top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.diplomacy-message-notice-stacked {
|
||||||
|
top: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
.layout-mobile [data-main-target='world-history'] {
|
.layout-mobile [data-main-target='world-history'] {
|
||||||
height: 359px;
|
height: 359px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user