Spaces:
Runtime error
Runtime error
File size: 3,619 Bytes
9e93b10 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | <script lang="ts">
import { REXPRO_BASE_URL } from "$lib/constants";
import {
settings,
playingNotificationSound,
isLastActiveTab,
} from "$lib/stores";
import DOMPurify from "dompurify";
import { marked } from "marked";
import { createEventDispatcher, onMount } from "svelte";
import XMark from "$lib/components/icons/XMark.svelte";
const dispatch = createEventDispatcher();
export let onClick: Function = () => {};
export let title: string = "HI";
export let content: string;
let startX = 0,
startY = 0;
let moved = false;
let closeButtonElement: HTMLButtonElement;
const DRAG_THRESHOLD_PX = 6;
const clickHandler = () => {
onClick();
dispatch("closeToast");
};
const closeHandler = () => {
dispatch("closeToast");
};
function onPointerDown(e: PointerEvent) {
startX = e.clientX;
startY = e.clientY;
moved = false;
// Ensure we continue to get events even if the toast moves under the pointer.
(e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);
}
function onPointerMove(e: PointerEvent) {
if (moved) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
if (dx * dx + dy * dy > DRAG_THRESHOLD_PX * DRAG_THRESHOLD_PX) {
moved = true;
}
}
function onPointerUp(e: PointerEvent) {
// Release capture if taken
(e.currentTarget as HTMLElement).releasePointerCapture?.(e.pointerId);
// Skip if clicking the close button
if (
closeButtonElement &&
(e.target === closeButtonElement ||
closeButtonElement.contains(e.target as Node))
) {
return;
}
// Only treat as a click if there wasn't a drag
if (!moved) {
clickHandler();
}
}
onMount(() => {
if (!navigator.userActivation.hasBeenActive) {
return;
}
if ($settings?.notificationSound ?? true) {
if (!$playingNotificationSound && $isLastActiveTab) {
playingNotificationSound.set(true);
const audio = new Audio(`/audio/notification.mp3`);
audio.play().finally(() => {
// Ensure the global state is reset after the sound finishes
playingNotificationSound.set(false);
});
}
}
});
</script>
<div
role="status"
aria-live="polite"
class="group relative flex gap-2.5 text-left min-w-[var(--width)] w-full dark:bg-gray-850 dark:text-white bg-white text-black border border-gray-100 dark:border-gray-800 rounded-3xl px-4 py-3.5 cursor-pointer select-none"
on:dragstart|preventDefault
on:pointerdown={onPointerDown}
on:pointermove={onPointerMove}
on:pointerup={onPointerUp}
on:pointercancel={() => (moved = true)}
on:keydown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
clickHandler();
}
}}
>
<!-- Close button (visible on hover) -->
<button
bind:this={closeButtonElement}
class="absolute -top-0.5 -left-0.5 p-0.5 rounded-full opacity-0 group-hover:opacity-100 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-opacity z-10"
on:click|stopPropagation={closeHandler}
aria-label="Dismiss notification"
>
<XMark className="size-3" />
</button>
<div class="shrink-0 self-top -translate-y-0.5">
<img
src="{REXPRO_BASE_URL}/static/favicon.png"
alt="favicon"
class="size-6 rounded-full"
/>
</div>
<div>
{#if title}
<div class=" text-[13px] font-medium mb-0.5 line-clamp-1">
{title}
</div>
{/if}
<div
class=" line-clamp-2 text-xs self-center dark:text-gray-300 font-normal"
>
{@html DOMPurify.sanitize(
marked(DOMPurify.sanitize(content, { ALLOWED_TAGS: [] })),
)}
</div>
</div>
</div>
|