+ class="flex-grow flex flex-col overflow-y-auto mb-4 p-2 bg-surface rounded relative whitespace-break-spaces wrap-anywhere">
{#if !$initialConnectionComplete || $room.connectionState === RoomConnectionState.RECONNECTING || $room.participants !== 2 || $dataChannelReady === false || !$canCloseLoadingOverlay}
+ class="absolute top-0 left-0 bottom-0 right-0 flex justify-center items-center flex-col bg-black/55 backdrop-blur-md z-10 text-center">
{#if !$isRTCConnected}
Waiting for peer to connect...
{:else if !$dataChannelReady && !$initialConnectionComplete}
@@ -247,20 +242,11 @@
{:else if !$keyExchangeDone}
Establishing a secure connection with the peer...
{:else if $room.connectionState === RoomConnectionState.RECONNECTING}
-
- Disconnect from peer, attempting to reconnecting...
-
+
Disconnect from peer, attempting to reconnecting...
{:else if $room.participants !== 2 || $dataChannelReady === false}
-
- Peer has disconnected, waiting for other peer to
- reconnect...
-
+
Peer has disconnected, waiting for other peer to reconnect...
{:else}
-
-
- Successfully established a secure connection to
- peer!
-
+
Successfully established a secure connection to peer!
{/if}
{#if !$keyExchangeDone || $room.participants !== 2 || $dataChannelReady === false || $room.connectionState === RoomConnectionState.RECONNECTING}
@@ -269,43 +255,38 @@
class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
- viewBox="0 0 24 24"
- >
+ viewBox="0 0 24 24">
+ stroke-width="4" />
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
{:else}
{/if}
{/if}
- {#each $messages as msg}
+ {#each $messages as msg, index}
{#if msg.initiator}
@@ -317,45 +298,120 @@
{#if msg.type === MessageType.TEXT}
{msg.data}
{:else if msg.type === MessageType.FILE_OFFER}
-
- {#if msg.data.text !== null}
-
- {msg.data.text}
-
- {/if}
-
-
- {msg.data.fileName}
-
-
- {msg.data.fileSize} bytes
-
-
- {#if !msg.initiator}
-
+
+
+ {#if msg.data.fileType.startsWith("image/")}
+ {#if $settingsStore.autoDownloadImages || $downloadedImageFiles.has(msg.data.id) || msg.initiator}
+ {#if msg.initiator}
+
+

+ {:else}
+
+ {#if $downloadedImageFiles.has(msg.data.id)}
+

+ {:else}
+
+ {/if}
+ {/if}
{/if}
-
+ {:else}
+ {#if msg.data.text !== null}
+
+ {msg.data.text}
+
+ {/if}
+
+
+ {msg.data.fileName}
+
+
+ {msg.data.fileSize} bytes
+
+
+ {#if !msg.initiator}
+
+ {#if msg.data.fileType.startsWith("image/")}
+
+ {/if}
+
+
+ {/if}
+
+ {/if}
{:else}
Unknown message type: {msg.type}
@@ -367,17 +423,14 @@
type="file"
bind:files={$inputFile}
bind:this={inputFileElement}
- class="absolute opacity-0 -top-[9999px] -left-[9999px]"
- />
-
+ class="absolute opacity-0 -top-[9999px] -left-[9999px]" />
+
+ class="border rounded border-[#2c3444] focus-within:border-[#404c63] transition-colors flex-grow flex flex-col bg-[#232b3e]">
{#if $inputFile}
+ class="p-2 flex flex-col gap-2 w-48 border rounded-md border-[#2c3444] relative">
-
+
{$inputFile[0].name}
@@ -408,8 +455,7 @@
onclick={() => {
$inputFile = null;
}}
- class="absolute right-2 top-2 p-1 border border-[#2c3444] text-paragraph hover:bg-surface/70 transition-colors rounded disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed"
- >
+ class="absolute right-2 top-2 p-1 border border-[#2c3444] text-paragraph hover:bg-surface/70 transition-colors rounded disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed">
+ d="M18 6L6 18M6 6l12 12" />
@@ -433,12 +477,9 @@
diff --git a/src/components/SettingsOverlay.svelte b/src/components/SettingsOverlay.svelte
new file mode 100644
index 0000000..be1584a
--- /dev/null
+++ b/src/components/SettingsOverlay.svelte
@@ -0,0 +1,119 @@
+
+
+
+{#if open}
+
+
+
+
(open = false)} class="absolute inset-0">
+
+
+{/if}
diff --git a/src/components/ToggleSwitch.svelte b/src/components/ToggleSwitch.svelte
new file mode 100644
index 0000000..e141b5d
--- /dev/null
+++ b/src/components/ToggleSwitch.svelte
@@ -0,0 +1,88 @@
+
+
+
+
+
diff --git a/src/lib/buffer.ts b/src/lib/buffer.ts
index 9a45898..5069df1 100644
--- a/src/lib/buffer.ts
+++ b/src/lib/buffer.ts
@@ -150,20 +150,25 @@ export class WebBuffer {
this.dataView.setBigInt64(offset, value, true);
}
- // if no length is specified, reads until the end of the buffer
- readString(length?: number, offset?: number): string {
- if (length === undefined) {
- length = this.length - this.count;
- }
-
+ readString(offset?: number): string {
if (offset === undefined) {
offset = this.count;
- this.count += length;
}
+ let stringBytes = [];
+ let stringLength = 0;
+ // loop until we find a null byte
+ while (this.data.length >= offset + stringLength && this.data[offset + stringLength] !== 0) {
+ stringBytes.push(this.data[offset + stringLength]);
+ stringLength++;
+ }
+
+ this.count += stringLength + 1;
+
+ console.log("Read string:", stringBytes, stringLength);
+
let textDeccoder = new TextDecoder();
- let readTextBuf = this.data.slice(offset, offset + length);
- let value = textDeccoder.decode(readTextBuf);
+ let value = textDeccoder.decode(new Uint8Array(stringBytes));
return value;
}
@@ -171,12 +176,17 @@ export class WebBuffer {
writeString(value: string, offset?: number) {
if (offset === undefined) {
offset = this.count;
- this.count += value.length;
+ this.count += value.length + 1;
}
+ // use C-style string termination
+ value = value + "\0";
+
let textEncoder = new TextEncoder();
let textBuf = textEncoder.encode(value);
+ console.log("Writing string:", value, textBuf);
+
this.data.set(textBuf, offset);
}
diff --git a/src/lib/webrtc.ts b/src/lib/webrtc.ts
index 8023636..f5cf987 100644
--- a/src/lib/webrtc.ts
+++ b/src/lib/webrtc.ts
@@ -5,12 +5,55 @@ import { browser } from '$app/environment';
import { createApplicationMessage, createCommit, createGroup, decodeMlsMessage, defaultCapabilities, defaultLifetime, emptyPskIndex, encodeMlsMessage, generateKeyPackage, getCiphersuiteFromName, getCiphersuiteImpl, joinGroup, processPrivateMessage, type CiphersuiteImpl, type ClientState, type Credential, type KeyPackage, type PrivateKeyPackage, type Proposal } from 'ts-mls';
import { WebSocketWebRtcMessageType } from '$types/websocket';
+export enum WebRTCCallbackType {
+ CONNECTED,
+ MESSAGE,
+ DATA_CHANNEL_STATE_CHANGE,
+ KEY_EXCHANGE_DONE,
+ NEGOTIATION_NEEDED,
+ ERROR,
+}
+
+type WebRTCCallback = WebRTCConnectedCallback | WebRTCMessageCallback | WebRTCDataChannelStateChangeCallback | WebRTCKeyExchangeDoneCallback | WebRTCNegotiationNeededCallback | WebRTCErrorCallback;
+type GetCallbackFunction
=
+ (WebRTCCallback & { type: T })['cb'];
+
+interface WebRTCConnectedCallback {
+ type: WebRTCCallbackType.CONNECTED;
+ cb: () => void;
+}
+
+interface WebRTCMessageCallback {
+ type: WebRTCCallbackType.MESSAGE;
+ cb: (message: { type: WebRTCPacketType, data: ArrayBuffer }, webRtcPeer: WebRTCPeer) => void;
+}
+
+interface WebRTCDataChannelStateChangeCallback {
+ type: WebRTCCallbackType.DATA_CHANNEL_STATE_CHANGE;
+ cb: (state: boolean) => void;
+}
+
+interface WebRTCKeyExchangeDoneCallback {
+ type: WebRTCCallbackType.KEY_EXCHANGE_DONE;
+ cb: () => void;
+}
+
+interface WebRTCNegotiationNeededCallback {
+ type: WebRTCCallbackType.NEGOTIATION_NEEDED;
+ cb: () => void;
+}
+
+interface WebRTCErrorCallback {
+ type: WebRTCCallbackType.ERROR;
+ cb: (error: any) => void;
+}
+
export class WebRTCPeer {
private peer: RTCPeerConnection | null = null;
private dataChannel: RTCDataChannel | null = null;
private isInitiator: boolean;
private roomId: string;
- private callbacks: WebRTCPeerCallbacks;
+ private callbacks: Map = new Map();
private credential: Credential;
private clientState: ClientState | undefined;
private cipherSuite: CiphersuiteImpl | undefined;
@@ -27,12 +70,46 @@ export class WebRTCPeer {
constructor(roomId: string, isInitiator: boolean, callbacks: WebRTCPeerCallbacks) {
this.roomId = roomId;
this.isInitiator = isInitiator;
- this.callbacks = callbacks;
+ this.callbacks = new Map();
+
+ this.addCallback(WebRTCCallbackType.CONNECTED, callbacks.onConnected);
+ this.addCallback(WebRTCCallbackType.MESSAGE, callbacks.onMessage);
+ this.addCallback(WebRTCCallbackType.DATA_CHANNEL_STATE_CHANGE, callbacks.onDataChannelStateChange);
+ this.addCallback(WebRTCCallbackType.KEY_EXCHANGE_DONE, callbacks.onKeyExchangeDone);
+ this.addCallback(WebRTCCallbackType.NEGOTIATION_NEEDED, callbacks.onNegotiationNeeded);
const id = crypto.getRandomValues(new Uint8Array(32));
this.credential = { credentialType: "basic", identity: id };
}
+ // returns unsubscribe function
+ public addCallback(type: T, func: GetCallbackFunction): () => void {
+ if (this.callbacks.has(type)) {
+ this.callbacks.get(type)!.push(func);
+ return () => {
+ this.callbacks.get(type)!.splice(this.callbacks.get(type)!.indexOf(func), 1);
+ }
+ }
+
+ this.callbacks.set(type, [func]);
+ return () => {
+ this.callbacks.get(type)!.splice(this.callbacks.get(type)!.indexOf(func), 1);
+ }
+ }
+
+ private emitCallback(type: T, ...args: Parameters>) {
+ console.log("Emitting callback:", type, args, WebRTCCallbackType[type]);
+ console.log("Callbacks:", this.callbacks);
+ if (this.callbacks.has(type)) {
+ console.log("Emitting callback:", type, args, WebRTCCallbackType[type]);
+
+ for (let func of this.callbacks.get(type)!) {
+ // @ts-ignore
+ func.apply(this, args);
+ }
+ }
+ }
+
private sendIceCandidate(candidate: RTCIceCandidate) {
ws.send({
type: WebSocketWebRtcMessageType.ICE_CANDIDATE,
@@ -68,14 +145,14 @@ export class WebRTCPeer {
this.peer.oniceconnectionstatechange = () => {
console.log('ICE connection state changed to:', this.peer?.iceConnectionState);
if (this.peer?.iceConnectionState === 'connected' || this.peer?.iceConnectionState === 'completed') {
- this.callbacks.onConnected();
+ this.emitCallback(WebRTCCallbackType.CONNECTED);
} else if (this.peer?.iceConnectionState === 'failed') {
- this.callbacks.onError('ICE connection failed');
+ this.emitCallback(WebRTCCallbackType.ERROR, 'ICE connection failed');
}
};
this.peer.onnegotiationneeded = () => {
- this.callbacks.onNegotiationNeeded();
+ this.emitCallback(WebRTCCallbackType.NEGOTIATION_NEEDED);
};
// 2. Create data channel
@@ -96,7 +173,7 @@ export class WebRTCPeer {
channel.binaryType = "arraybuffer";
channel.onopen = async () => {
- this.callbacks.onDataChannelStateChange(true);
+ this.emitCallback(WebRTCCallbackType.DATA_CHANNEL_STATE_CHANGE, true);
try {
if (this.isInitiator) {
@@ -114,7 +191,7 @@ export class WebRTCPeer {
}
} catch (e) {
console.error("Error starting key exchange:", e);
- this.callbacks.onError(e);
+ this.emitCallback(WebRTCCallbackType.ERROR, e);
}
};
@@ -134,7 +211,7 @@ export class WebRTCPeer {
return;
}
- console.log("parsed data", data, encrypted, type);
+ console.log("parsed data", data, encrypted, WebRTCPacketType[type]);
if (type === WebRTCPacketType.GROUP_OPEN) {
await this.generateKeyPair();
@@ -181,7 +258,7 @@ export class WebRTCPeer {
this.send(encodedWelcomeBuf, WebRTCPacketType.WELCOME);
this.encyptionReady = true;
- this.callbacks.onKeyExchangeDone();
+ this.emitCallback(WebRTCCallbackType.KEY_EXCHANGE_DONE);
return;
}
@@ -205,7 +282,7 @@ export class WebRTCPeer {
console.log("Joined group", this.clientState);
this.encyptionReady = true;
- this.callbacks.onKeyExchangeDone();
+ this.emitCallback(WebRTCCallbackType.KEY_EXCHANGE_DONE);
return;
}
@@ -235,17 +312,17 @@ export class WebRTCPeer {
data: data.buffer,
};
- this.callbacks.onMessage(message, this);
+ this.emitCallback(WebRTCCallbackType.MESSAGE, message, this);
};
channel.onclose = () => {
- this.callbacks.onDataChannelStateChange(false);
+ this.emitCallback(WebRTCCallbackType.DATA_CHANNEL_STATE_CHANGE, false);
};
channel.onerror = (error) => {
console.error('data channel error:', error);
- this.callbacks.onError(error);
+ this.emitCallback(WebRTCCallbackType.ERROR, error);
};
}
@@ -281,7 +358,7 @@ export class WebRTCPeer {
await this.peer.setRemoteDescription(sdp);
} catch (error) {
console.error('Error setting remote description:', error);
- this.callbacks.onError(error);
+ this.emitCallback(WebRTCCallbackType.ERROR, error);
}
}
@@ -304,7 +381,7 @@ export class WebRTCPeer {
} catch (error) {
console.error('Error creating answer:', error);
- this.callbacks.onError(error);
+ this.emitCallback(WebRTCCallbackType.ERROR, error);
}
}
@@ -315,7 +392,7 @@ export class WebRTCPeer {
await this.peer.addIceCandidate(new RTCIceCandidate(candidate));
} catch (error) {
console.error('Error adding ICE candidate:', error);
- this.callbacks.onError(error);
+ this.emitCallback(WebRTCCallbackType.ERROR, error);
}
}
@@ -333,7 +410,7 @@ export class WebRTCPeer {
this.keyPackage = await generateKeyPackage(this.credential, defaultCapabilities(), defaultLifetime, [], this.cipherSuite);
} catch (e) {
console.error("Error generating key package:", e);
- this.callbacks.onError(e);
+ this.emitCallback(WebRTCCallbackType.ERROR, e);
}
}
diff --git a/src/lib/webrtcUtil.ts b/src/lib/webrtcUtil.ts
index 62f4426..1138250 100644
--- a/src/lib/webrtcUtil.ts
+++ b/src/lib/webrtcUtil.ts
@@ -3,11 +3,12 @@ import { WebRTCPeer } from "$lib/webrtc";
import { WebRTCPacketType } from "$types/webrtc";
import { room } from "$stores/roomStore";
import { RoomConnectionState, WebSocketErrorType, WebSocketResponseType, WebSocketRoomMessageType, WebSocketWebRtcMessageType, type Room } from "$types/websocket";
-import { advertisedOffers, fileRequestIds, messages, receivedOffers } from "$stores/messageStore";
+import { advertisedOffers, downloadedImageFiles, fileRequestIds, incompleteAutoDownloadedFiles, messages, receivedOffers } from "$stores/messageStore";
import { MessageType, type Message } from "$types/message";
import { type WebSocketMessage } from "$types/websocket";
import { WebBuffer } from "./buffer";
import { goto } from "$app/navigation";
+import { settingsStore } from "$stores/settingsStore";
export const error: Writable = writable(null);
export let peer: Writable = writable(null);
@@ -46,7 +47,6 @@ const callbacks = {
console.log("Connected to peer");
isRTCConnected.set(true);
},
- //! TODO: come up with a more complex room system. This is largely for testing purposes
onMessage: async (message: { type: WebRTCPacketType, data: ArrayBuffer }, webRtcPeer: WebRTCPeer) => {
console.log("WebRTC Received message:", message);
if (message.type !== WebRTCPacketType.MESSAGE) {
@@ -74,23 +74,41 @@ const callbacks = {
break;
case MessageType.FILE_OFFER:
let fileSize = messageData.readBigInt64LE();
- let fileNameSize = messageData.readInt16LE();
- let fileName = messageData.readString(fileNameSize);
+ let fileName = messageData.readString();
+ let fileType = messageData.readString();
let id = messageData.readBigInt64LE();
- get(receivedOffers).set(id, { name: fileName, size: fileSize });
+ get(receivedOffers).set(id, { name: fileName, size: fileSize, type: fileType });
messages.set([...get(messages), {
initiator: false,
type: messageType,
data: {
fileSize,
- fileNameSize,
fileName,
+ fileType,
id,
text: messageData.peek() ? messageData.readString() : null,
}
}]);
+
+ if (get(settingsStore).autoDownloadImages && get(settingsStore).maxAutoDownloadSize >= fileSize && fileType.startsWith("image/")) {
+ console.log("Auto downloading image");
+
+ let fileRequestBuf = new WebBuffer(new ArrayBuffer(1 + 8 + 8));
+ let requesterId = new WebBuffer(
+ crypto.getRandomValues(new Uint8Array(8)).buffer,
+ ).readBigInt64LE();
+
+ fileRequestBuf.writeInt8(MessageType.FILE_REQUEST);
+ fileRequestBuf.writeBigInt64LE(id);
+ fileRequestBuf.writeBigInt64LE(requesterId);
+
+ get(fileRequestIds).set(requesterId, { saveToDisk: false, offerId: id });
+
+ webRtcPeer.send(fileRequestBuf.buffer, WebRTCPacketType.MESSAGE);
+ }
+
break;
case MessageType.FILE_REQUEST:
// the id that coresponds to our file offer
@@ -171,28 +189,45 @@ const callbacks = {
break;
case MessageType.FILE:
let requestId = messageData.readBigInt64LE();
- let receivedOffserId = get(fileRequestIds).get(requestId);
- if (!receivedOffserId) {
+ let receivedOffer = get(fileRequestIds).get(requestId);
+ if (!receivedOffer) {
console.error("Received file message for unknown file id:", requestId);
return;
}
- let file = get(receivedOffers).get(receivedOffserId);
+ let file = get(receivedOffers).get(receivedOffer.offerId);
if (!file) {
console.error("Unknown file id:", requestId);
return;
}
- if (downloadStream === undefined) {
- window.addEventListener("pagehide", onPageHide);
- window.addEventListener("beforeunload", beforeUnload);
+ let fileData = new Uint8Array(messageData.read());
- // @ts-ignore
- downloadStream = window.streamSaver.createWriteStream(file.name, { size: Number(file.size) });
- downloadWriter = downloadStream!.getWriter();
+ console.log("recieved chunk of auto downloaded file");
+ if (get(receivedOffers).get(receivedOffer.offerId)!.type.startsWith("image/")) {
+ if (!incompleteAutoDownloadedFiles.has(receivedOffer.offerId)) {
+ incompleteAutoDownloadedFiles.set(receivedOffer.offerId, new Blob([fileData.buffer]));
+ console.log("Auto downloaded file:", incompleteAutoDownloadedFiles.get(receivedOffer.offerId)!);
+ } else {
+ // stupidest way ever to extend a buffer
+ let blobParts = incompleteAutoDownloadedFiles.get(receivedOffer.offerId)!;
+ incompleteAutoDownloadedFiles.set(receivedOffer.offerId, new Blob([blobParts, fileData.buffer]));
+ // console.log("Auto downloaded file:", get(autoDownloadedFiles.get(receivedOffser.offerId)!));
+ }
}
- await downloadWriter!.write(new Uint8Array(messageData.read()));
+ if (receivedOffer.saveToDisk) {
+ if (downloadStream === undefined) {
+ window.addEventListener("pagehide", onPageHide);
+ window.addEventListener("beforeunload", beforeUnload);
+
+ // @ts-ignore
+ downloadStream = window.streamSaver.createWriteStream(file.name, { size: Number(file.size) });
+ downloadWriter = downloadStream!.getWriter();
+ }
+
+ await downloadWriter!.write(new Uint8Array(fileData));
+ }
let fileAckBuf = new WebBuffer(new ArrayBuffer(1 + 8));
fileAckBuf.writeInt8(MessageType.FILE_ACK);
@@ -208,6 +243,18 @@ const callbacks = {
return;
}
+ let doneOfferId = get(fileRequestIds).get(fileDoneId)!.offerId;
+ if (!get(receivedOffers).has(doneOfferId)) {
+ console.error("Unknown file done id:", fileDoneId);
+ return;
+ }
+
+ if (incompleteAutoDownloadedFiles.has(doneOfferId)) {
+ console.log("completely auto downloaded file:", incompleteAutoDownloadedFiles.get(doneOfferId)!);
+ downloadedImageFiles.set(get(downloadedImageFiles).set(doneOfferId, incompleteAutoDownloadedFiles.get(doneOfferId)!));
+ incompleteAutoDownloadedFiles.delete(doneOfferId);
+ }
+
window.removeEventListener("pagehide", onPageHide);
window.removeEventListener("beforeunload", beforeUnload);
@@ -264,7 +311,7 @@ export async function handleMessage(event: MessageEvent) {
return;
case WebSocketRoomMessageType.PARTICIPANT_JOINED:
console.log("new client joined room");
- room.update((room) => ({ ...room, participants: room.participants + 1 }));
+ room.update((room) => ({ ...room, participants: message.participants }));
return;
case WebSocketResponseType.ROOM_JOINED:
// TODO: if a client disconnects, we need to resync the room state
@@ -273,7 +320,7 @@ export async function handleMessage(event: MessageEvent) {
console.log("Joined room");
return;
case WebSocketRoomMessageType.PARTICIPANT_LEFT:
- room.update((room) => ({ ...room, participants: room.participants - 1 }));
+ room.update((room) => ({ ...room, participants: message.participants }));
console.log("Participant left room");
return;
case WebSocketErrorType.ERROR:
@@ -288,7 +335,7 @@ export async function handleMessage(event: MessageEvent) {
return;
}
- room.update(r => ({ ...r, RTCConnectionReady: true }));
+ room.update(r => ({ ...r, RTCConnectionReady: true, participants: message.data.participants }));
console.log("Creating peer");
peer.set(new WebRTCPeer(
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 7f23e40..eb31e02 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -4,9 +4,35 @@
import { onDestroy, onMount } from "svelte";
import { WebsocketConnectionState, ws } from "$stores/websocketStore";
import { room } from "$stores/roomStore";
+ import { writable } from "svelte/store";
+ import SettingsOverlay from "$components/SettingsOverlay.svelte";
+ import { settingsStore } from "$stores/settingsStore";
+
+ const settingsOverlayOpen = writable(false);
onMount(() => {
ws.connect();
+
+ let settings = localStorage.getItem("settings");
+ if (settings) {
+ // settingsStore = JSON.parse(settings);
+ let settingsObj = JSON.parse(settings);
+ for (let key in $settingsStore) {
+ // @ts-ignore
+ $settingsStore[key] = settingsObj[key];
+ }
+ } else {
+ localStorage.setItem("settings", JSON.stringify($settingsStore));
+ }
+
+ $effect(() => {
+ localStorage.setItem("settings", JSON.stringify($settingsStore));
+ });
+
+ settingsOverlayOpen.subscribe((value) => {
+ document.getElementById("app")!.style.filter = value ? "blur(10px)" : "";
+ document.documentElement.style.overflow = value ? "hidden" : "auto";
+ });
});
onDestroy(() => {
@@ -42,20 +68,51 @@
-
-
-
-
Noctis.
-
+
+
+
+
+
-
-
-
+
-
- {@render children?.()}
-
+
+ {@render children?.()}
+
+
+
+
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 36394cc..16165c6 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -23,7 +23,7 @@
challenge: {
target: challengeResult.target,
nonce: challengeResult.nonce,
- }
+ },
});
console.log("Created room:", roomId);
@@ -49,11 +49,11 @@
class="py-4 px-8 text-xl font-semibold bg-accent text-[#121826] disabled:opacity-50 disabled:cursor-not-allowed rounded-lg cursor-pointer transition-[background-color,_translate,_box-shadow] ease-out duration-200 w-full inline-flex justify-center items-center gap-2.5 hover:bg-[#00f0c8] hover:-translate-y-1 hover:shadow-md shadow-accent/20">
{#if $ws.status !== WebsocketConnectionState.CONNECTED}
- Connecting to server...
+ Connecting to server...
{:else if $roomLoading}
- Creating Room...
+ Creating Room...
{:else}