feat: add announcement

- fix: bug encryption not working on newer version

Signed-off-by: pakintada@gmail.com <Pakin>
This commit is contained in:
pakintada@gmail.com 2026-06-19 11:26:44 +07:00
parent d4eb3be886
commit 270faf6b34
4 changed files with 183 additions and 2 deletions

View file

@ -0,0 +1,174 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { GlobalEventBus } from '$lib/core/utils/eventBus';
import { XIcon } from '@lucide/svelte/icons';
export interface AnnouncementPayload {
title?: string;
subtitle?: string;
message: string;
buttonText?: string;
type?: 'info' | 'warning' | 'error' | 'success';
}
let visible = $state(false);
let payload = $state<AnnouncementPayload | null>(null);
let animating = $state(false);
function show(p: AnnouncementPayload) {
payload = p;
visible = true;
// Trigger enter animation on next frame
requestAnimationFrame(() => {
animating = true;
});
}
function dismiss() {
animating = false;
// Wait for exit animation
setTimeout(() => {
visible = false;
payload = null;
}, 200);
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape' && visible) {
dismiss();
}
}
let unsubscribe: (() => void) | undefined;
onMount(() => {
unsubscribe = GlobalEventBus.on('announce', (data: AnnouncementPayload) => {
show(data);
});
// if (window) window.addEventListener('keydown', handleKeydown);
});
onDestroy(() => {
unsubscribe?.();
// if (window) window.removeEventListener('keydown', handleKeydown);
});
const typeStyles: Record<string, { border: string; badge: string; icon: string }> = {
info: {
border: 'border-blue-500',
badge: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
icon: ''
},
warning: {
border: 'border-amber-500',
badge: 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200',
icon: ''
},
error: {
border: 'border-red-500',
badge: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
icon: ''
},
success: {
border: 'border-green-500',
badge: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
icon: ''
}
};
function currentStyles() {
return typeStyles[payload?.type ?? 'info'] ?? typeStyles.info;
}
</script>
{#if visible}
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div
class="fixed inset-0 z-[9999] flex items-center justify-center p-4 sm:p-6 md:p-8"
role="dialog"
aria-modal="true"
aria-labelledby="announcement-title"
onclick={(e) => {
// Close on backdrop click
if (e.target === e.currentTarget) dismiss();
}}
>
<!-- Backdrop -->
<div
class="absolute inset-0 bg-black/60 backdrop-blur-lg transition-opacity duration-200"
class:opacity-100={animating}
class:opacity-0={!animating}
></div>
<!-- Card -->
<div
class="relative w-full max-w-lg overflow-hidden rounded-2xl border bg-white shadow-2xl transition-all duration-200 dark:border-neutral-700 dark:bg-neutral-900"
class:opacity-100={animating}
class:opacity-0={!animating}
class:scale-100={animating}
class:scale-95={!animating}
>
<!-- Colored top border accent -->
<div class="h-1.5 w-full {currentStyles().border}" />
<div class="p-6 sm:p-8">
<!-- Close button -->
<button
onclick={dismiss}
class="absolute top-4 right-4 rounded-full p-1.5 text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-600 dark:hover:bg-neutral-800 dark:hover:text-neutral-300"
aria-label="Close announcement"
>
<XIcon size={20} />
</button>
<!-- Type badge -->
{#if payload?.type}
<span
class="mb-4 inline-block rounded-full px-3 py-1 text-xs font-semibold tracking-wider uppercase {currentStyles()
.badge}"
>
{payload.type}
</span>
{/if}
<!-- Title -->
{#if payload?.title}
<h2
id="announcement-title"
class="pr-8 text-2xl font-bold text-neutral-900 dark:text-white"
>
{payload.title}
</h2>
{/if}
<!-- Subtitle -->
{#if payload?.subtitle}
<p class="mt-1 text-sm font-medium text-neutral-500 dark:text-neutral-400">
{payload.subtitle}
</p>
{/if}
<!-- Message body -->
{#if payload?.message}
<div
class="mt-4 text-base leading-relaxed whitespace-pre-wrap text-neutral-700 dark:text-neutral-300"
>
{payload.message}
</div>
{/if}
<!-- Acknowledge / action button -->
{#if payload?.buttonText !== undefined}
<div class="mt-6 flex justify-end gap-3">
<button
onclick={dismiss}
class="inline-flex items-center justify-center rounded-lg bg-neutral-900 px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-neutral-800 focus-visible:ring-2 focus-visible:ring-neutral-400 focus-visible:outline-none dark:bg-white dark:text-neutral-900 dark:hover:bg-neutral-200"
>
{payload.buttonText || 'Acknowledge'}
</button>
</div>
{/if}
</div>
</div>
</div>
{/if}

View file

@ -50,6 +50,7 @@ import { handleSheetResponseFromNoti } from './sheetNotiHandler';
import { env } from '$env/dynamic/public';
import * as semver from 'semver';
import { WebCryptoHelper } from '../utils/crypto';
import { GlobalEventBus } from '../utils/eventBus';
export const messages = writable<string[]>([]);
@ -481,6 +482,10 @@ const handlers: Record<string, (payload: any) => void> = {
raw_stream_end_price: (p) => {
// End for price stream
handleRawStreamEnd('price', p);
},
announce: (p) => {
// Server-pushed announcement (e.g., closing maintenance)
GlobalEventBus.emit('announce', p);
}
};
@ -498,7 +503,7 @@ export async function handleIncomingMessages(raw: string, clientPrivateKey: Cryp
return;
}
if (semver.satisfies(APP_VERSION, '^0.0.2')) {
if (semver.satisfies(APP_VERSION, '>=0.0.2')) {
// secured message decryption
let sharedKeyStore = get(sharedKey);
if (sharedKeyStore) {

View file

@ -75,7 +75,7 @@ export async function sendMessage(
// console.log('send v2', APP_VERSION, semver.satisfies(APP_VERSION, '^0.0.2'));
if (semver.satisfies(APP_VERSION, '^0.0.2')) {
if (semver.satisfies(APP_VERSION, '>=0.0.2')) {
// console.log('sending secured');
let sharedKeyRes = get(sharedKey);

View file

@ -27,6 +27,7 @@
} from '$lib/helpers/cookie';
import { connectToWebsocket } from '$lib/core/stores/websocketStore';
import { GlobalEventBus } from '$lib/core/utils/eventBus';
import AnnouncementDialog from '$lib/components/AnnouncementDialog.svelte';
import * as semver from 'semver';
import { env } from '$env/dynamic/public';
@ -91,4 +92,5 @@
<ModeWatcher />
<Toaster />
<AnnouncementDialog />
{@render children()}