feat: add announcement
- fix: bug encryption not working on newer version Signed-off-by: pakintada@gmail.com <Pakin>
This commit is contained in:
parent
d4eb3be886
commit
270faf6b34
4 changed files with 183 additions and 2 deletions
174
src/lib/components/AnnouncementDialog.svelte
Normal file
174
src/lib/components/AnnouncementDialog.svelte
Normal 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}
|
||||||
|
|
@ -50,6 +50,7 @@ import { handleSheetResponseFromNoti } from './sheetNotiHandler';
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
import * as semver from 'semver';
|
import * as semver from 'semver';
|
||||||
import { WebCryptoHelper } from '../utils/crypto';
|
import { WebCryptoHelper } from '../utils/crypto';
|
||||||
|
import { GlobalEventBus } from '../utils/eventBus';
|
||||||
|
|
||||||
export const messages = writable<string[]>([]);
|
export const messages = writable<string[]>([]);
|
||||||
|
|
||||||
|
|
@ -481,6 +482,10 @@ const handlers: Record<string, (payload: any) => void> = {
|
||||||
raw_stream_end_price: (p) => {
|
raw_stream_end_price: (p) => {
|
||||||
// End for price stream
|
// End for price stream
|
||||||
handleRawStreamEnd('price', p);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (semver.satisfies(APP_VERSION, '^0.0.2')) {
|
if (semver.satisfies(APP_VERSION, '>=0.0.2')) {
|
||||||
// secured message decryption
|
// secured message decryption
|
||||||
let sharedKeyStore = get(sharedKey);
|
let sharedKeyStore = get(sharedKey);
|
||||||
if (sharedKeyStore) {
|
if (sharedKeyStore) {
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ export async function sendMessage(
|
||||||
|
|
||||||
// console.log('send v2', APP_VERSION, semver.satisfies(APP_VERSION, '^0.0.2'));
|
// 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');
|
// console.log('sending secured');
|
||||||
let sharedKeyRes = get(sharedKey);
|
let sharedKeyRes = get(sharedKey);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
} from '$lib/helpers/cookie';
|
} from '$lib/helpers/cookie';
|
||||||
import { connectToWebsocket } from '$lib/core/stores/websocketStore';
|
import { connectToWebsocket } from '$lib/core/stores/websocketStore';
|
||||||
import { GlobalEventBus } from '$lib/core/utils/eventBus';
|
import { GlobalEventBus } from '$lib/core/utils/eventBus';
|
||||||
|
import AnnouncementDialog from '$lib/components/AnnouncementDialog.svelte';
|
||||||
import * as semver from 'semver';
|
import * as semver from 'semver';
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
|
|
||||||
|
|
@ -91,4 +92,5 @@
|
||||||
|
|
||||||
<ModeWatcher />
|
<ModeWatcher />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
<AnnouncementDialog />
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue