actual UI, tons of bug fixes, and rename
This commit is contained in:
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 juls0730
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
# Wormhole
|
# Noctis
|
||||||
(needs a different name I think because I dont want to confuse it with wormhole.app)
|
|
||||||
|
|
||||||
A peer-to-peer encrypted file sharing app.
|
Noctis /ˈnɑktɪs/ *adjective* of the night
|
||||||
|
|
||||||
|
A peer-to-peer end-to-end encrypted chat app.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- E2E communication
|
- E2EE communication
|
||||||
- P2P file sharing
|
- P2P file sharing
|
||||||
- P2P chat
|
- P2P chat
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,20 @@ import { WebSocketServer } from "ws";
|
|||||||
import { Socket, WebSocketMessageType, type WebSocketMessage } from "../src/types/websocket";
|
import { Socket, WebSocketMessageType, type WebSocketMessage } from "../src/types/websocket";
|
||||||
import { LiveMap } from '../src/utils/liveMap.ts';
|
import { LiveMap } from '../src/utils/liveMap.ts';
|
||||||
|
|
||||||
|
const adjectives = ['swift', 'silent', 'hidden', 'clever', 'brave', 'sharp', 'shadow', 'crimson', 'bright', 'quiet', 'loud', 'happy', 'dark', 'evil', 'good', 'intelligent', 'lovely', 'mysterious', 'peaceful', 'powerful', 'pure', 'quiet', 'shiny', 'sleepy', 'strong', 'sweet', 'tall', 'warm', 'gentle', 'kind', 'nice', 'polite', 'rough', 'rude', 'scary', 'shy', 'silly', 'smart', 'strange', 'tough', 'ugly', 'vivid', 'wicked', 'wise', 'young', 'sleepy'];
|
||||||
|
const nouns = ['fox', 'river', 'stone', 'cipher', 'link', 'comet', 'falcon', 'signal', 'anchor', 'spark', 'stone', 'comet', 'rocket', 'snake', 'snail', 'shark', 'elephant', 'cat', 'dog', 'whale', 'orca', 'cactus', 'flower', 'frog', 'toad', 'apple', 'strawberry', 'raspberry', 'lemon', 'bot', 'gopher', 'dinosaur', 'racoon', 'penguin', 'chameleon', 'atom', 'particle', 'witch', 'wizard', 'warlock', 'deer']
|
||||||
|
|
||||||
|
enum ErrorCode {
|
||||||
|
ROOM_NOT_FOUND,
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = {
|
||||||
|
MALFORMED_MESSAGE: "Invalid message",
|
||||||
|
ROOM_NOT_FOUND: "Room does not exist",
|
||||||
|
ROOM_FULL: "Room is full",
|
||||||
|
UNKNOWN_MESSAGE_TYPE: "Unknown message type",
|
||||||
|
}
|
||||||
|
|
||||||
export class ServerRoom {
|
export class ServerRoom {
|
||||||
private clients: Socket[] = [];
|
private clients: Socket[] = [];
|
||||||
|
|
||||||
@@ -38,16 +52,28 @@ export class ServerRoom {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateRoomName(): string {
|
||||||
|
const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
|
||||||
|
const noun = nouns[Math.floor(Math.random() * nouns.length)];
|
||||||
|
return `${adj}-${noun}`;
|
||||||
|
}
|
||||||
|
|
||||||
const rooms = new LiveMap<string, ServerRoom>();
|
const rooms = new LiveMap<string, ServerRoom>();
|
||||||
|
|
||||||
async function createRoom(socket: Socket): Promise<string> {
|
async function createRoom(socket: Socket, roomName?: string): Promise<string> {
|
||||||
let roomId = Math.random().toString(36).substring(2, 10);
|
if (!roomName) {
|
||||||
|
roomName = generateRoomName();
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = Math.floor(Math.random() * 900) + 100;
|
||||||
|
const roomId = `${roomName}-${num}`;
|
||||||
|
|
||||||
let room = rooms.set(roomId, new ServerRoom());
|
let room = rooms.set(roomId, new ServerRoom());
|
||||||
|
|
||||||
socket.send({ type: WebSocketMessageType.ROOM_CREATED, data: room.key });
|
socket.send({ type: WebSocketMessageType.ROOM_CREATED, data: room.key });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await joinRoom(room.key, socket);
|
await joinRoom(room.key, socket, true);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@@ -55,18 +81,18 @@ async function createRoom(socket: Socket): Promise<string> {
|
|||||||
return roomId;
|
return roomId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function joinRoom(roomId: string, socket: Socket): Promise<ServerRoom | undefined> {
|
async function joinRoom(roomId: string, socket: Socket, initial?: boolean): Promise<ServerRoom | undefined> {
|
||||||
let room = rooms.get(roomId);
|
let room = rooms.get(roomId);
|
||||||
console.log(room?.length);
|
console.log(room?.length);
|
||||||
|
|
||||||
// should be unreachable
|
// should be unreachable
|
||||||
if (!room) {
|
if (!room) {
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: `Room ${roomId} does not exist` });
|
socket.send({ type: WebSocketMessageType.ERROR, data: errors.ROOM_NOT_FOUND });
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (room.length == 2) {
|
if (room.length == 2) {
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: "Room is full" });
|
socket.send({ type: WebSocketMessageType.ERROR, data: errors.ROOM_FULL });
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +119,9 @@ async function joinRoom(roomId: string, socket: Socket): Promise<ServerRoom | un
|
|||||||
room.set(room.filter(client => client.ws !== ev.target));
|
room.set(room.filter(client => client.ws !== ev.target));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!initial) {
|
||||||
|
socket.send({ type: WebSocketMessageType.ROOM_JOINED, roomId: roomId, participants: room.length });
|
||||||
|
}
|
||||||
// TODO: consider letting rooms get larger than 2 clients
|
// TODO: consider letting rooms get larger than 2 clients
|
||||||
if (room.length == 2) {
|
if (room.length == 2) {
|
||||||
room.forEachClient(client => client.send({ type: WebSocketMessageType.ROOM_READY, data: { isInitiator: client !== socket } }));
|
room.forEachClient(client => client.send({ type: WebSocketMessageType.ROOM_READY, data: { isInitiator: client !== socket } }));
|
||||||
@@ -109,7 +138,7 @@ function leaveRoom(roomId: string, socket: Socket): ServerRoom | undefined {
|
|||||||
|
|
||||||
// should be unreachable
|
// should be unreachable
|
||||||
if (!room) {
|
if (!room) {
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: `Room ${roomId} does not exist` });
|
socket.send({ type: WebSocketMessageType.ERROR, data: errors.ROOM_NOT_FOUND });
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +185,7 @@ export function confgiureWebsocketServer(wss: WebSocketServer) {
|
|||||||
if (message === undefined) {
|
if (message === undefined) {
|
||||||
console.log("Received non-JSON message:", event);
|
console.log("Received non-JSON message:", event);
|
||||||
// If the message is not JSON, send an error message
|
// If the message is not JSON, send an error message
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: 'Invalid message' });
|
socket.send({ type: WebSocketMessageType.ERROR, data: errors.MALFORMED_MESSAGE });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +195,17 @@ export function confgiureWebsocketServer(wss: WebSocketServer) {
|
|||||||
case WebSocketMessageType.CREATE_ROOM:
|
case WebSocketMessageType.CREATE_ROOM:
|
||||||
// else, create a new room
|
// else, create a new room
|
||||||
try {
|
try {
|
||||||
await createRoom(socket);
|
if (message.roomName) {
|
||||||
|
// sanitize the room name
|
||||||
|
message.roomName = message.roomName.toLowerCase()
|
||||||
|
.replace(/\s+/g, '-') // Replace spaces with -
|
||||||
|
.replace(/[^\w-]+/g, '') // Remove all non-word chars
|
||||||
|
.replace(/--+/g, '-') // Replace multiple - with single -
|
||||||
|
.replace(/^-+/, '') // Trim - from start of text
|
||||||
|
.replace(/-+$/, ''); // Trim - from end of text
|
||||||
|
}
|
||||||
|
|
||||||
|
await createRoom(socket, message.roomName);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: e.message });
|
socket.send({ type: WebSocketMessageType.ERROR, data: e.message });
|
||||||
throw e;
|
throw e;
|
||||||
@@ -174,29 +213,27 @@ export function confgiureWebsocketServer(wss: WebSocketServer) {
|
|||||||
break;
|
break;
|
||||||
case WebSocketMessageType.JOIN_ROOM:
|
case WebSocketMessageType.JOIN_ROOM:
|
||||||
if (!message.roomId) {
|
if (!message.roomId) {
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: 'Invalid message' });
|
socket.send({ type: WebSocketMessageType.ERROR, data: errors.MALFORMED_MESSAGE });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rooms.get(message.roomId) == undefined) {
|
if (rooms.get(message.roomId) == undefined) {
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: 'Invalid roomId' });
|
socket.send({ type: WebSocketMessageType.ERROR, data: errors.ROOM_NOT_FOUND });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
room = await joinRoom(message.roomId, socket);
|
room = await joinRoom(message.roomId, socket);
|
||||||
if (!room) return;
|
if (!room) return;
|
||||||
|
|
||||||
// the client is now in the room and the peer knows about it
|
|
||||||
socket.send({ type: WebSocketMessageType.ROOM_JOINED, roomId: message.roomId, participants: room.length });
|
|
||||||
break;
|
break;
|
||||||
case WebSocketMessageType.LEAVE_ROOM:
|
case WebSocketMessageType.LEAVE_ROOM:
|
||||||
if (!message.roomId) {
|
if (!message.roomId) {
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: 'Invalid message' });
|
socket.send({ type: WebSocketMessageType.ERROR, data: errors.MALFORMED_MESSAGE });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rooms.get(message.roomId) == undefined) {
|
if (rooms.get(message.roomId) == undefined) {
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: 'Invalid roomId' });
|
socket.send({ type: WebSocketMessageType.ERROR, data: errors.ROOM_NOT_FOUND });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,7 +257,7 @@ export function confgiureWebsocketServer(wss: WebSocketServer) {
|
|||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.warn(`Unknown message type: ${message.type}`);
|
console.warn(`Unknown message type: ${message.type}`);
|
||||||
socket.send({ type: WebSocketMessageType.ERROR, data: 'Unknown message type' });
|
socket.send({ type: WebSocketMessageType.ERROR, data: errors.UNKNOWN_MESSAGE_TYPE });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
48
src/app.css
48
src/app.css
@@ -1,9 +1,51 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
|
|
||||||
body, html {
|
@font-face {
|
||||||
@apply bg-neutral-950 text-white font-sans min-h-screen;
|
font-family: "Instrument Sans";
|
||||||
|
src: url("/fonts/InstrumentSans-VariableFont_wdth,wght.woff2") format("woff2");
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--font-sans: "Instrument Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
--test: #00060d;
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-accent: #00e0b8;
|
||||||
|
--color-surface: #0f1626;
|
||||||
|
--color-paragraph: #e0e0e0;
|
||||||
|
--color-paragraph-muted: #8a94a6;
|
||||||
|
--color-primary: #00060d;
|
||||||
|
}
|
||||||
|
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
@apply bg-primary font-sans min-h-screen text-paragraph;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3 {
|
||||||
|
color: #FFF;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@apply text-pink-600 underline hover:no-underline;
|
@apply text-accent underline hover:no-underline;
|
||||||
}
|
}
|
||||||
20
src/components/LoadingSpinner.svelte
Normal file
20
src/components/LoadingSpinner.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<svg
|
||||||
|
class="animate-spin -ml-1 mr-3 h-5 w-5"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
class="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
class="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 481 B |
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { derived, writable, type Writable } from "svelte/store";
|
import { derived, writable, type Writable } from "svelte/store";
|
||||||
// import { room } from "../stores/roomStore";
|
import { WebsocketConnectionState, ws } from "../stores/websocketStore";
|
||||||
import { webSocketConnected, ws } from "../stores/websocketStore";
|
|
||||||
import {
|
import {
|
||||||
isRTCConnected,
|
isRTCConnected,
|
||||||
dataChannelReady,
|
dataChannelReady,
|
||||||
@@ -15,7 +14,7 @@
|
|||||||
receivedOffers,
|
receivedOffers,
|
||||||
} from "../stores/messageStore";
|
} from "../stores/messageStore";
|
||||||
import { WebRTCPacketType } from "../types/webrtc";
|
import { WebRTCPacketType } from "../types/webrtc";
|
||||||
import { ConnectionState, type Room } from "../types/websocket";
|
import { RoomConnectionState, type Room } from "../types/websocket";
|
||||||
import { MessageType } from "../types/message";
|
import { MessageType } from "../types/message";
|
||||||
import { fade } from "svelte/transition";
|
import { fade } from "svelte/transition";
|
||||||
import { WebBuffer } from "../utils/buffer";
|
import { WebBuffer } from "../utils/buffer";
|
||||||
@@ -26,7 +25,7 @@
|
|||||||
let initialConnectionCompleteCount = writable(0);
|
let initialConnectionCompleteCount = writable(0);
|
||||||
let initialConnectionComplete = derived(
|
let initialConnectionComplete = derived(
|
||||||
initialConnectionCompleteCount,
|
initialConnectionCompleteCount,
|
||||||
(value) => value === 3,
|
(value) => value >= 3,
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: is this the most elegant way to do this?
|
// TODO: is this the most elegant way to do this?
|
||||||
@@ -224,21 +223,19 @@
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
{$room?.id}
|
{$room?.id}
|
||||||
({$room?.participants}) - {$room?.connectionState} - {$webSocketConnected}
|
({$room?.participants}) - {$room?.connectionState} - {$ws.status}
|
||||||
- Initial connection {$initialConnectionComplete
|
- Initial connection {$initialConnectionComplete
|
||||||
? "complete"
|
? "complete"
|
||||||
: "incomplete"}
|
: "incomplete"}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- If we are in a room, connected to the websocket server, and have been informed that we are connected to the room -->
|
<!-- If we are in a room, connected to the websocket server, and have been informed that we are connected to the room -->
|
||||||
{#if ($room !== null && $webSocketConnected === true && $room.connectionState === ConnectionState.CONNECTED) || $room.connectionState === ConnectionState.RECONNECTING}
|
{#if ($room !== null && $ws.status === WebsocketConnectionState.CONNECTED && $room.connectionState === RoomConnectionState.CONNECTED) || $room.connectionState === RoomConnectionState.RECONNECTING}
|
||||||
|
<div class="flex flex-col w-full min-h-[calc(5/12_*_100vh)]">
|
||||||
<div
|
<div
|
||||||
class="flex flex-col sm:max-w-4/5 lg:max-w-3/5 min-h-[calc(5/12_*_100vh)]"
|
class="flex-grow flex flex-col overflow-y-auto mb-4 p-2 bg-surface rounded relative whitespace-break-spaces wrap-anywhere"
|
||||||
>
|
>
|
||||||
<div
|
{#if !$initialConnectionComplete || $room.connectionState === RoomConnectionState.RECONNECTING || $room.participants !== 2 || $dataChannelReady === false || !$canCloseLoadingOverlay}
|
||||||
class="flex-grow flex flex-col overflow-y-auto mb-4 p-2 bg-gray-800 rounded relative whitespace-break-spaces wrap-anywhere"
|
|
||||||
>
|
|
||||||
{#if !$initialConnectionComplete || $room.connectionState === ConnectionState.RECONNECTING || $room.participants !== 2 || $dataChannelReady === false || !$canCloseLoadingOverlay}
|
|
||||||
<div
|
<div
|
||||||
transition:fade={{ duration: 300 }}
|
transition:fade={{ duration: 300 }}
|
||||||
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"
|
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"
|
||||||
@@ -249,23 +246,24 @@
|
|||||||
<p>Establishing data channel...</p>
|
<p>Establishing data channel...</p>
|
||||||
{:else if !$keyExchangeDone}
|
{:else if !$keyExchangeDone}
|
||||||
<p>Establishing a secure connection with the peer...</p>
|
<p>Establishing a secure connection with the peer...</p>
|
||||||
{:else if $room.connectionState === ConnectionState.RECONNECTING}
|
{:else if $room.connectionState === RoomConnectionState.RECONNECTING}
|
||||||
<p>
|
<p>
|
||||||
Disconnect from peer, attempting to reconnecting...
|
Disconnect from peer, attempting to reconnecting...
|
||||||
</p>
|
</p>
|
||||||
{:else if $room.participants !== 2 || $dataChannelReady === false}
|
{:else if $room.participants !== 2 || $dataChannelReady === false}
|
||||||
<p>
|
<p>
|
||||||
Peer has disconnected, waiting for other peer to
|
Peer has disconnected, waiting for other peer to
|
||||||
reconnect...
|
<span>reconnect...</span>
|
||||||
</p>
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
<p>
|
<p>
|
||||||
|
<!-- fucking completely stupid shit I have to do because svelte FORCES these to be broken into two lines, and for some reason it just puts all of the whitespace at the beginning of the line in the string, so it looks unbelievably stupid -->
|
||||||
Successfully established a secure connection to
|
Successfully established a secure connection to
|
||||||
peer!
|
<span>peer!</span>
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
{#if !$keyExchangeDone || $room.participants !== 2 || $dataChannelReady === false || $room.connectionState === ConnectionState.RECONNECTING}
|
{#if !$keyExchangeDone || $room.participants !== 2 || $dataChannelReady === false || $room.connectionState === RoomConnectionState.RECONNECTING}
|
||||||
<!-- loading spinner -->
|
<!-- loading spinner -->
|
||||||
<svg
|
<svg
|
||||||
class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
||||||
@@ -326,12 +324,12 @@
|
|||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
<div
|
<div
|
||||||
class="flex flex-col p-2 relative w-8/12 bg-gray-600 rounded"
|
class="flex flex-col p-2 relative w-8/12 bg-primary/50 rounded"
|
||||||
>
|
>
|
||||||
<h2 class="text-lg font-semibold my-1">
|
<h3 class="font-semibold">
|
||||||
{msg.data.fileName}
|
{msg.data.fileName}
|
||||||
</h2>
|
</h3>
|
||||||
<p class="text-sm">
|
<p class="text-sm text-paragraph-muted">
|
||||||
{msg.data.fileSize} bytes
|
{msg.data.fileSize} bytes
|
||||||
</p>
|
</p>
|
||||||
<!-- as the initiator, we cant send ourselves a file -->
|
<!-- as the initiator, we cant send ourselves a file -->
|
||||||
@@ -339,7 +337,7 @@
|
|||||||
<button
|
<button
|
||||||
onclick={() =>
|
onclick={() =>
|
||||||
downloadFile(msg.data.id)}
|
downloadFile(msg.data.id)}
|
||||||
class="absolute right-2 bottom-2 p-1 border border-gray-500 text-gray-100 hover:bg-gray-800/70 transition-colors rounded disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed"
|
class="absolute right-2 bottom-2 p-1 border border-[#2c3444]/80 text-paragraph hover:bg-[#1D1C1F]/60 transition-colors rounded disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -373,12 +371,12 @@
|
|||||||
/>
|
/>
|
||||||
<div class="flex gap-2 w-full flex-row">
|
<div class="flex gap-2 w-full flex-row">
|
||||||
<div
|
<div
|
||||||
class="border rounded border-gray-600 flex-grow flex flex-col bg-gray-700"
|
class="border rounded border-[#2c3444] focus-within:border-[#404c63] transition-colors flex-grow flex flex-col bg-[#232b3e]"
|
||||||
>
|
>
|
||||||
{#if $inputFile}
|
{#if $inputFile}
|
||||||
<div class="flex flex-row gap-2 p-2">
|
<div class="flex flex-row gap-2 p-2">
|
||||||
<div
|
<div
|
||||||
class="p-2 flex flex-col gap-2 w-48 border rounded-md border-gray-600 relative"
|
class="p-2 flex flex-col gap-2 w-48 border rounded-md border-[#2c3444] relative"
|
||||||
>
|
>
|
||||||
<div class="w-full flex justify-center">
|
<div class="w-full flex justify-center">
|
||||||
<svg
|
<svg
|
||||||
@@ -410,7 +408,7 @@
|
|||||||
onclick={() => {
|
onclick={() => {
|
||||||
$inputFile = null;
|
$inputFile = null;
|
||||||
}}
|
}}
|
||||||
class="absolute right-2 top-2 p-1 border border-gray-600 text-gray-100 hover:bg-gray-800/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"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -429,11 +427,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr class="border-gray-600" />
|
<hr class="border-[#2c3444]" />
|
||||||
{/if}
|
{/if}
|
||||||
<div
|
<div class="flex flex-row rounded">
|
||||||
class="flex flex-row focus-within:ring-2 focus-within:ring-blue-500 rounded"
|
|
||||||
>
|
|
||||||
<textarea
|
<textarea
|
||||||
bind:value={$inputMessage}
|
bind:value={$inputMessage}
|
||||||
cols="1"
|
cols="1"
|
||||||
@@ -451,9 +447,9 @@
|
|||||||
!$dataChannelReady ||
|
!$dataChannelReady ||
|
||||||
!$keyExchangeDone ||
|
!$keyExchangeDone ||
|
||||||
$room.connectionState ===
|
$room.connectionState ===
|
||||||
ConnectionState.RECONNECTING}
|
RoomConnectionState.RECONNECTING}
|
||||||
placeholder="Type your message..."
|
placeholder="Type your message..."
|
||||||
class="flex-grow p-2 bg-gray-700 rounded text-gray-100 placeholder-gray-400 min-h-12
|
class="placeholder:text-paragraph-muted flex-grow p-2 bg-[#232b3e] rounded min-h-12
|
||||||
focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed resize-none leading-8"
|
focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed resize-none leading-8"
|
||||||
></textarea>
|
></textarea>
|
||||||
<div class="flex flex-row gap-2 p-2 h-fit mt-auto">
|
<div class="flex flex-row gap-2 p-2 h-fit mt-auto">
|
||||||
@@ -463,9 +459,9 @@
|
|||||||
!$dataChannelReady ||
|
!$dataChannelReady ||
|
||||||
!$keyExchangeDone ||
|
!$keyExchangeDone ||
|
||||||
$room.connectionState ===
|
$room.connectionState ===
|
||||||
ConnectionState.RECONNECTING}
|
RoomConnectionState.RECONNECTING}
|
||||||
aria-label="Pick file"
|
aria-label="Pick file"
|
||||||
class="not-disabled:hover:bg-gray-800/70 h-fit p-1 text-gray-100 transition-colors rounded disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed"
|
class="not-disabled:hover:bg-primary/50 h-fit p-1 text-paragraph transition-colors rounded disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -488,8 +484,8 @@
|
|||||||
!$dataChannelReady ||
|
!$dataChannelReady ||
|
||||||
!$keyExchangeDone ||
|
!$keyExchangeDone ||
|
||||||
$room.connectionState ===
|
$room.connectionState ===
|
||||||
ConnectionState.RECONNECTING}
|
RoomConnectionState.RECONNECTING}
|
||||||
class="not-disabled:hover:bg-gray-800/70 h-fit p-1 text-gray-100 transition-colors rounded disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed"
|
class="not-disabled:hover:bg-primary/50 h-fit p-1 text-paragraph transition-colors rounded disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -512,9 +508,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button
|
|
||||||
onclick={() => {
|
|
||||||
$ws.close();
|
|
||||||
}}>Simulate disconnect</button
|
|
||||||
>
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
|
|
||||||
import { get } from 'svelte/store';
|
|
||||||
import { ws } from '../stores/websocketStore';
|
import { ws } from '../stores/websocketStore';
|
||||||
import { WebSocketMessageType } from '../types/websocket';
|
import { WebSocketMessageType } from '../types/websocket';
|
||||||
import { WebRTCPacketType, type WebRTCPeerCallbacks } from '../types/webrtc';
|
import { WebRTCPacketType, type WebRTCPeerCallbacks } from '../types/webrtc';
|
||||||
import { browser } from '$app/environment';
|
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 GroupContext, type KeyPackage, type PrivateKeyPackage, type Proposal } from 'ts-mls';
|
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';
|
||||||
|
|
||||||
export class WebRTCPeer {
|
export class WebRTCPeer {
|
||||||
private peer: RTCPeerConnection | null = null;
|
private peer: RTCPeerConnection | null = null;
|
||||||
@@ -35,7 +34,7 @@ export class WebRTCPeer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private sendIceCandidate(candidate: RTCIceCandidate) {
|
private sendIceCandidate(candidate: RTCIceCandidate) {
|
||||||
get(ws).send({
|
ws.send({
|
||||||
type: WebSocketMessageType.WEBRTC_ICE_CANDIDATE,
|
type: WebSocketMessageType.WEBRTC_ICE_CANDIDATE,
|
||||||
data: {
|
data: {
|
||||||
roomId: this.roomId,
|
roomId: this.roomId,
|
||||||
@@ -261,7 +260,7 @@ export class WebRTCPeer {
|
|||||||
|
|
||||||
await this.peer.setLocalDescription(offer)
|
await this.peer.setLocalDescription(offer)
|
||||||
|
|
||||||
get(ws).send({
|
ws.send({
|
||||||
type: WebSocketMessageType.WEBRTC_OFFER,
|
type: WebSocketMessageType.WEBRTC_OFFER,
|
||||||
data: {
|
data: {
|
||||||
roomId: this.roomId,
|
roomId: this.roomId,
|
||||||
@@ -295,7 +294,7 @@ export class WebRTCPeer {
|
|||||||
|
|
||||||
console.log("Sending answer", answer);
|
console.log("Sending answer", answer);
|
||||||
|
|
||||||
get(ws).send({
|
ws.send({
|
||||||
type: WebSocketMessageType.WERTC_ANSWER,
|
type: WebSocketMessageType.WERTC_ANSWER,
|
||||||
data: {
|
data: {
|
||||||
roomId: this.roomId,
|
roomId: this.roomId,
|
||||||
|
|||||||
@@ -1,12 +1,40 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "../app.css";
|
import "../app.css";
|
||||||
import favicon from "$lib/assets/favicon.svg";
|
import favicon from "$lib/assets/favicon.svg";
|
||||||
|
import { onDestroy, onMount } from "svelte";
|
||||||
|
import { WebsocketConnectionState, ws } from "../stores/websocketStore";
|
||||||
|
import { room } from "../stores/roomStore";
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
ws.connect();
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
ws.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.subscribe((newWs) => {
|
||||||
|
if (newWs.status === WebsocketConnectionState.CONNECTED) {
|
||||||
|
console.log(
|
||||||
|
"Connected to websocket server, room id:",
|
||||||
|
$room.id,
|
||||||
|
"reconnecting",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<link rel="icon" href={favicon} />
|
<link rel="icon" href={favicon} />
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
href="/fonts/InstrumentSans-VariableFont_wdth,wght.woff2"
|
||||||
|
/>
|
||||||
<script
|
<script
|
||||||
src="https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.0.2/dist/ponyfill.min.js"
|
src="https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.0.2/dist/ponyfill.min.js"
|
||||||
></script>
|
></script>
|
||||||
@@ -15,4 +43,23 @@
|
|||||||
></script>
|
></script>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{@render children?.()}
|
<header class="p-5">
|
||||||
|
<div class="flex justify-between items-center max-w-7xl px-5 mx-auto">
|
||||||
|
<div class="text-2xl font-bold text-white">
|
||||||
|
<a href="/" class="!text-white !no-underline"
|
||||||
|
>Noctis<span class="text-accent">.</span></a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<nav>
|
||||||
|
<a
|
||||||
|
href="https://github.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">GitHub</a
|
||||||
|
>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
{@render children?.()}
|
||||||
|
</main>
|
||||||
|
|||||||
@@ -1,62 +1,265 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ws, webSocketConnected } from "../stores/websocketStore";
|
import { ws } from "../stores/websocketStore";
|
||||||
import { WebSocketMessageType } from "../types/websocket";
|
import { WebSocketMessageType } from "../types/websocket";
|
||||||
import { room } from "../stores/roomStore";
|
import { writable, type Writable } from "svelte/store";
|
||||||
import { browser } from "$app/environment";
|
import LoadingSpinner from "../components/LoadingSpinner.svelte";
|
||||||
import { peer, handleMessage } from "../utils/webrtcUtil";
|
|
||||||
import { onDestroy, onMount } from "svelte";
|
|
||||||
import RtcMessage from "../components/RTCMessage.svelte";
|
|
||||||
import { ConnectionState } from "../types/websocket";
|
|
||||||
|
|
||||||
onMount(async () => {
|
let roomName: Writable<string> = writable("");
|
||||||
$ws.addEventListener("message", handleMessage);
|
let roomLoading: Writable<boolean> = writable(false);
|
||||||
});
|
|
||||||
|
|
||||||
onDestroy(() => {
|
function createRoom() {
|
||||||
if ($ws) {
|
roomLoading.set(true);
|
||||||
room.update((room) => ({
|
let roomId = $roomName.trim() === "" ? undefined : $roomName.trim();
|
||||||
...room,
|
|
||||||
connectionState: ConnectionState.DISCONNECTED,
|
ws.send({
|
||||||
}));
|
type: WebSocketMessageType.CREATE_ROOM,
|
||||||
$ws.removeEventListener("message", handleMessage);
|
roomName: roomId,
|
||||||
}
|
|
||||||
if ($peer) {
|
|
||||||
$peer.close();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
// todo: redirect to the room
|
||||||
|
console.log("Created room:", roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
let showRoomNameInput: Writable<boolean> = writable(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-4">
|
<section class="py-20 text-center">
|
||||||
<h1>Welcome to Wormhole!</h1>
|
<div class="max-w-6xl px-5 mx-auto flex flex-col items-center">
|
||||||
|
<h1 class="font-bold">Your Private, Peer-to-Peer Chat Room</h1>
|
||||||
|
<p class="max-w-xl mx-8">
|
||||||
|
End-to-end encrypted. Peer-to-peer. No servers. No sign-ups. Just
|
||||||
|
chat.
|
||||||
|
</p>
|
||||||
|
|
||||||
{#if $webSocketConnected}
|
<div
|
||||||
|
class="bg-surface p-10 rounded-xl max-w-xl shadow-xl border border-[#21293b] mt-10 mr-auto ml-auto w-full"
|
||||||
|
>
|
||||||
|
<form class="flex flex-col gap-5" id="roomForm">
|
||||||
<button
|
<button
|
||||||
onclick={() => {
|
onclick={createRoom}
|
||||||
// if we are in a room already, leave it
|
class="py-4 px-8 text-xl font-semibold bg-accent text-[#121826] 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 ($room.id) {
|
>
|
||||||
$ws.send({
|
{#if $roomLoading}
|
||||||
type: WebSocketMessageType.LEAVE_ROOM,
|
<span class="flex items-center"
|
||||||
roomId: $room.id,
|
><LoadingSpinner /> Creating Room...</span
|
||||||
});
|
|
||||||
$peer?.close();
|
|
||||||
peer.set(null);
|
|
||||||
room.update((room) => ({
|
|
||||||
...room,
|
|
||||||
connectionState: ConnectionState.DISCONNECTED,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
$ws.send({ type: WebSocketMessageType.CREATE_ROOM }); // send a message when the button is clicked
|
|
||||||
}}>Create Room</button
|
|
||||||
>
|
>
|
||||||
{:else}
|
{:else}
|
||||||
<p>Connecting to server...</p>
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12 2a5 5 0 0 1 5 5v3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-6a3 3 0 0 1 3-3V7a5 5 0 0 1 5-5m0 12a2 2 0 0 0-1.995 1.85L10 16a2 2 0 1 0 2-2m0-10a3 3 0 0 0-3 3v3h6V7a3 3 0 0 0-3-3"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
Create Secure Room
|
||||||
{/if}
|
{/if}
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
class="{$showRoomNameInput
|
||||||
|
? 'max-h-32'
|
||||||
|
: 'max-h-0 opacity-0'} overflow-hidden transition-[max-height,_opacity] duration-700"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
aria-hidden={!$showRoomNameInput}
|
||||||
|
for="roomNameInput"
|
||||||
|
class="text-paragraph block text-sm font-medium mb-2 text-left"
|
||||||
|
>Enter a custom room name</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="roomNameInput"
|
||||||
|
bind:value={$roomName}
|
||||||
|
class="placeholder:text-paragraph-muted w-full py-3 px-4 rounded-lg border border-[#2c3444] bg-[#232b3e] text-paragraph transition-[border-color,_box-shadow] duration-300 ease-in-out focus:outline-none focus:border-accent focus:shadow-sm shadow-accent/20"
|
||||||
|
placeholder="e.g., private-chat"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="text-paragraph {$showRoomNameInput
|
||||||
|
? 'hidden'
|
||||||
|
: '-mt-5'}"
|
||||||
|
>or <button
|
||||||
|
id="showCustomNameLink"
|
||||||
|
class="cursor-pointer underline hover:no-underline text-accent"
|
||||||
|
onclick={() => showRoomNameInput.set(true)}
|
||||||
|
>choose a custom room name</button
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{#if $room.id && browser}
|
<section class="py-20 bg-surface">
|
||||||
<p>Room created!</p>
|
<div class="max-w-6xl px-5 mx-auto">
|
||||||
<p>Share this link with your friend:</p>
|
<h2 class="font-semibold">How It Works</h2>
|
||||||
<a href={`${location.origin}/${$room}`}>{location.origin}/{$room.id}</a>
|
<div class="mt-10 flex justify-around gap-8 flex-wrap">
|
||||||
{/if}
|
<div class="text-center max-w-3xs">
|
||||||
|
<div
|
||||||
|
class="text-2xl font-bold w-12 h-12 leading-12 rounded-full bg-primary text-accent mx-auto mb-5"
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</div>
|
||||||
|
<h3>Create a Room</h3>
|
||||||
|
<p>
|
||||||
|
Click the button above to create a random room instantly, no
|
||||||
|
personal info required.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-center max-w-3xs">
|
||||||
|
<div
|
||||||
|
class="text-2xl font-bold w-12 h-12 leading-12 rounded-full bg-primary text-accent mx-auto mb-5"
|
||||||
|
>
|
||||||
|
2
|
||||||
|
</div>
|
||||||
|
<h3>Share the Link</h3>
|
||||||
|
<p>
|
||||||
|
You'll get a unique link to your private room. Share this
|
||||||
|
link with anyone you want to chat with securely.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-center max-w-3xs">
|
||||||
|
<div
|
||||||
|
class="text-2xl font-bold w-12 h-12 leading-12 rounded-full bg-primary text-accent mx-auto mb-5"
|
||||||
|
>
|
||||||
|
3
|
||||||
|
</div>
|
||||||
|
<h3>Chat Privately</h3>
|
||||||
|
<p>
|
||||||
|
Once they join, your messages are sent directly between your
|
||||||
|
devices, encrypted from end to end. Hidden from everyone
|
||||||
|
else.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<RtcMessage {room} />
|
<section class="py-20">
|
||||||
</div>
|
<div class="max-w-6xl px-10 mx-auto">
|
||||||
|
<h2 class="font-semibold">Security by Design</h2>
|
||||||
|
<div
|
||||||
|
class="mt-10 grid grid-cols-[repeat(auto-fit,_minmax(300px,_1fr))] gap-8"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-surface p-8 rounded-xl border border-[#21293b] text-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mb-5 bg-accent/10 w-16 h-16 rounded-full inline-flex justify-center items-center text-paragraph"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><g
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
><path
|
||||||
|
d="M5 13a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2z"
|
||||||
|
/><path
|
||||||
|
d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0-2 0m-3-5V7a4 4 0 1 1 8 0v4"
|
||||||
|
/></g
|
||||||
|
></svg
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<h3 class="font-bold">End-to-End Encrypted</h3>
|
||||||
|
<p>
|
||||||
|
Only you and the people in your room can read the messages.
|
||||||
|
Your data is encrypted before its sent using the Message
|
||||||
|
Layer Security (MLS) protocol.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="bg-surface p-8 rounded-xl border border-[#21293b] text-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mb-5 bg-accent/10 w-16 h-16 rounded-full inline-flex justify-center items-center text-paragraph"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><path
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M21 11V8a2 2 0 0 0-2-2h-6m0 0l3 3m-3-3l3-3M3 13.013v3a2 2 0 0 0 2 2h6m0 0l-3-3m3 3l-3 3m8-4.511a2 2 0 1 0 4.001-.001a2 2 0 0 0-4.001.001m-12-12a2 2 0 1 0 4.001-.001A2 2 0 0 0 4 4.502m17 16.997a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2m-6-12a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<h3 class="font-bold">Truly Peer-to-Peer</h3>
|
||||||
|
<p>
|
||||||
|
Your messages are sent directly from your device to the
|
||||||
|
recipient's. They never pass through a central server.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="bg-surface p-8 rounded-xl border border-[#21293b] text-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mb-5 bg-accent/10 w-16 h-16 rounded-full inline-flex justify-center items-center text-paragraph"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><path
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="m3 3l18 18M7 3h7l5 5v7m0 4a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V5"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<h3 class="font-bold">No Data Stored</h3>
|
||||||
|
<p>
|
||||||
|
We don't have accounts, and we don't store your messages.
|
||||||
|
Once you close the tab, the conversation is gone forever.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer class="px-20 text-center border-t border-[#21293b]">
|
||||||
|
<div class="max-w-6xl px-10 mx-auto">
|
||||||
|
<p>
|
||||||
|
© {new Date().getFullYear()} Noctis - MIT License
|
||||||
|
<br />
|
||||||
|
Made with
|
||||||
|
<span class="text-accent"
|
||||||
|
><svg
|
||||||
|
class="inline-block"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M6.979 3.074a6 6 0 0 1 4.988 1.425l.037.033l.034-.03a6 6 0 0 1 4.733-1.44l.246.036a6 6 0 0 1 3.364 10.008l-.18.185l-.048.041l-7.45 7.379a1 1 0 0 1-1.313.082l-.094-.082l-7.493-7.422A6 6 0 0 1 6.979 3.074"
|
||||||
|
/></svg
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
by
|
||||||
|
<a href="https://zoeissleeping.com">zoeissleeping</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--color-paragraph-muted);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,58 +1,121 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { room } from "../../stores/roomStore";
|
import { room } from "../../stores/roomStore";
|
||||||
import { error, handleMessage, peer } from "../../utils/webrtcUtil";
|
import { ws } from "../../stores/websocketStore";
|
||||||
import { ws, webSocketConnected } from "../../stores/websocketStore";
|
|
||||||
import { WebSocketMessageType } from "../../types/websocket";
|
import { WebSocketMessageType } from "../../types/websocket";
|
||||||
|
import { dataChannelReady, error } from "../../utils/webrtcUtil";
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
import RtcMessage from "../../components/RTCMessage.svelte";
|
import RtcMessage from "../../components/RTCMessage.svelte";
|
||||||
import { ConnectionState } from "../../types/websocket";
|
|
||||||
|
let isHost = $room.host === true;
|
||||||
|
|
||||||
|
let awaitingJoinConfirmation = !isHost;
|
||||||
|
let roomLink = "";
|
||||||
|
let copyButtonText = "Copy Link";
|
||||||
export let data: { roomId: string };
|
export let data: { roomId: string };
|
||||||
const { roomId } = data;
|
const { roomId } = data;
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(() => {
|
||||||
room.update((room) => ({ ...room, id: roomId }));
|
error.set(null);
|
||||||
|
|
||||||
$ws.addEventListener("message", handleMessage);
|
roomLink = `${window.location.origin}/${roomId}`;
|
||||||
|
});
|
||||||
|
|
||||||
webSocketConnected.subscribe((value) => {
|
function handleCopyLink() {
|
||||||
if (value) {
|
navigator.clipboard.writeText(roomLink).then(() => {
|
||||||
room.update((room) => ({
|
copyButtonText = "Copied!";
|
||||||
...room,
|
setTimeout(() => {
|
||||||
connectionState: ConnectionState.CONNECTING,
|
copyButtonText = "Copy Link";
|
||||||
}));
|
}, 2000);
|
||||||
|
});
|
||||||
if ($room.id === null) {
|
|
||||||
throw new Error("Room ID not set");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$ws.send({
|
function handleConfirmJoin() {
|
||||||
|
awaitingJoinConfirmation = false;
|
||||||
|
|
||||||
|
ws.send({
|
||||||
type: WebSocketMessageType.JOIN_ROOM,
|
type: WebSocketMessageType.JOIN_ROOM,
|
||||||
roomId: $room.id,
|
roomId: roomId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
onDestroy(() => {
|
function handleDeclineJoin() {
|
||||||
if ($ws) {
|
// In a real app, this would close the connection and maybe redirect
|
||||||
room.update((room) => ({
|
alert("You have declined to join the room.");
|
||||||
...room,
|
awaitingJoinConfirmation = false; // Hides the prompt
|
||||||
connectionState: ConnectionState.DISCONNECTED,
|
goto("/");
|
||||||
}));
|
}
|
||||||
$ws.close();
|
|
||||||
|
function handleLeave() {
|
||||||
|
if (
|
||||||
|
confirm(
|
||||||
|
"Are you sure you want to leave? The chat history will be deleted.",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// In a real app, this would disconnect the P2P session and redirect.
|
||||||
|
window.location.href = "/";
|
||||||
}
|
}
|
||||||
if ($peer) {
|
|
||||||
$peer.close();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-4">
|
<div class="max-w-6xl px-5 mx-auto flex flex-col items-center">
|
||||||
{#if $error}
|
{#if $error}
|
||||||
<p>Hm. Something went wrong: {$error.toLocaleLowerCase()}</p>
|
<h2 class="text-3xl font-bold text-white mb-2">
|
||||||
{:else if $room.connectionState !== ConnectionState.CONNECTED && $room.connectionState !== ConnectionState.RECONNECTING}
|
Something went wrong: {$error.toLocaleLowerCase()}
|
||||||
<p>Connecting to server...</p>
|
</h2>
|
||||||
|
<p class="!text-paragraph">
|
||||||
|
click <a href="/">here</a> to go back to the homepage
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !$error}
|
||||||
|
{#if isHost}
|
||||||
|
{#if !$room.RTCConnectionReady}
|
||||||
|
<h2 class="text-3xl font-bold text-white mb-2">
|
||||||
|
Your secure room is ready.
|
||||||
|
</h2>
|
||||||
|
<p class="text-gray-400 mb-6 text-center">
|
||||||
|
Share the link below to invite someone to chat directly with
|
||||||
|
you. Once they join, you will be connected automatically.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="bg-gray-900 rounded-lg p-4 flex items-center justify-between gap-4 border border-gray-600"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-accent font-mono text-sm overflow-x-auto whitespace-nowrap"
|
||||||
|
>{roomLink}</span
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onclick={handleCopyLink}
|
||||||
|
class="bg-accent hover:bg-accent/80 active:bg-accent/60 cursor-pointer text-gray-900 font-bold py-2 px-4 rounded-md transition-colors whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{copyButtonText}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<RtcMessage {room} />
|
<RtcMessage {room} />
|
||||||
{/if}
|
{/if}
|
||||||
|
{:else if awaitingJoinConfirmation}
|
||||||
|
<h2 class="text-3xl font-bold text-white mb-2">
|
||||||
|
You're invited to chat.
|
||||||
|
</h2>
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button
|
||||||
|
onclick={handleConfirmJoin}
|
||||||
|
class="bg-accent hover:bg-accent/80 active:bg-accent/60 cursor-pointer text-gray-900 font-bold py-2 px-4 rounded-md transition-colors whitespace-nowrap"
|
||||||
|
>
|
||||||
|
Accept
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={handleDeclineJoin}
|
||||||
|
class="bg-red-400 hover:bg-red-400/80 active:bg-red-400/60 cursor-pointer text-gray-900 font-bold py-2 px-4 rounded-md transition-colors whitespace-nowrap"
|
||||||
|
>
|
||||||
|
Decline
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<RtcMessage {room} />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import { writable, type Writable } from 'svelte/store';
|
import { writable, type Writable } from 'svelte/store';
|
||||||
import { ConnectionState } from '../types/websocket';
|
import { RoomConnectionState } from '../types/websocket';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
export interface Room {
|
export interface Room {
|
||||||
id: string | null;
|
id: string | null;
|
||||||
|
host: boolean | null;
|
||||||
|
RTCConnectionReady: boolean;
|
||||||
participants: number;
|
participants: number;
|
||||||
connectionState: ConnectionState;
|
connectionState: RoomConnectionState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const room: Writable<Room> = writable({
|
export const room: Writable<Room> = writable({
|
||||||
id: null,
|
id: null,
|
||||||
|
host: null,
|
||||||
|
RTCConnectionReady: false,
|
||||||
participants: 0,
|
participants: 0,
|
||||||
connectionState: ConnectionState.DISCONNECTED,
|
connectionState: RoomConnectionState.DISCONNECTED,
|
||||||
key: null,
|
|
||||||
});
|
});
|
||||||
@@ -1,55 +1,105 @@
|
|||||||
import { get, writable } from 'svelte/store';
|
import { get, writable, type Readable, type Writable } from 'svelte/store';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { room } from './roomStore';
|
import { Socket, type WebSocketMessage } from '../types/websocket';
|
||||||
import { ConnectionState, Socket, WebSocketMessageType } from '../types/websocket';
|
import { handleMessage } from '../utils/webrtcUtil';
|
||||||
|
|
||||||
let socket: Socket | null = null;
|
export enum WebsocketConnectionState {
|
||||||
export const webSocketConnected = writable(false);
|
DISCONNECTED,
|
||||||
|
CONNECTING,
|
||||||
function createSocket(): Socket {
|
CONNECTED,
|
||||||
if (!browser) {
|
RECONNECTING
|
||||||
// this only occurs on the server, which we dont care about because its not a client that can actually connect to the websocket server
|
|
||||||
// @ts-ignore
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (socket) {
|
|
||||||
return socket;
|
|
||||||
}
|
|
||||||
|
|
||||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
||||||
socket = new Socket(new WebSocket(`${protocol}//${location.host}/`));
|
|
||||||
|
|
||||||
socket.addEventListener('open', () => {
|
|
||||||
webSocketConnected.set(true);
|
|
||||||
console.log('Connected to websocket server');
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.addEventListener('close', () => {
|
|
||||||
// TODO: massively rework the reconnection logic, currently it only works if one client disconnects, if the
|
|
||||||
// TODO: other client disconnects after the other client has diconnected at least once, everything explodes
|
|
||||||
if (get(webSocketConnected) && get(room)?.connectionState === ConnectionState.CONNECTED) {
|
|
||||||
room.update((room) => ({ ...room, connectionState: ConnectionState.RECONNECTING }));
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
ws.set(createSocket());
|
|
||||||
|
|
||||||
// attempt to rejoin the room if we were previously connected
|
|
||||||
get(ws).addEventListener('open', () => {
|
|
||||||
let oldRoomId = get(room)?.id;
|
|
||||||
if (oldRoomId) {
|
|
||||||
get(ws).send({ type: WebSocketMessageType.JOIN_ROOM, roomId: oldRoomId });
|
|
||||||
room.update((room) => ({ ...room, connectionState: ConnectionState.CONNECTED }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
webSocketConnected.set(false);
|
|
||||||
socket = null;
|
|
||||||
console.log('Disconnected from websocket server, reconnecting...');
|
|
||||||
});
|
|
||||||
|
|
||||||
return socket;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ws = writable(createSocket());
|
interface WebSocketStoreValue {
|
||||||
|
status: WebsocketConnectionState;
|
||||||
|
socket: Socket | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MessageHandler = (event: MessageEvent) => void;
|
||||||
|
|
||||||
|
interface WebSocketStore extends Readable<WebSocketStoreValue> {
|
||||||
|
connect: () => void;
|
||||||
|
disconnect: () => void;
|
||||||
|
send: (message: WebSocketMessage) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: handle reconnection logic to room elsewhere (not implemented here)
|
||||||
|
function createWebSocketStore(messageHandler: MessageHandler): WebSocketStore {
|
||||||
|
const { subscribe, set, update } = writable<WebSocketStoreValue>({ status: WebsocketConnectionState.DISCONNECTED, socket: null });
|
||||||
|
|
||||||
|
let reconnectTimeout: NodeJS.Timeout | null = null;
|
||||||
|
let reconnectAttempts = 0;
|
||||||
|
|
||||||
|
const send = (message: WebSocketMessage) => {
|
||||||
|
let currentState = get({ subscribe });
|
||||||
|
if (currentState.socket?.readyState === WebSocket.OPEN) {
|
||||||
|
currentState.socket.send(message);
|
||||||
|
} else {
|
||||||
|
console.error("Socket not connected");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const disconnect = () => {
|
||||||
|
let currentState = get({ subscribe });
|
||||||
|
if (currentState.socket) {
|
||||||
|
currentState.socket.close();
|
||||||
|
set({ status: WebsocketConnectionState.DISCONNECTED, socket: null });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const connect = () => {
|
||||||
|
if (!browser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentState = get({ subscribe });
|
||||||
|
if (currentState.socket || currentState.status === WebsocketConnectionState.CONNECTING) {
|
||||||
|
// already connected/connecting
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(s => ({ ...s, status: WebsocketConnectionState.CONNECTING }));
|
||||||
|
|
||||||
|
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
const socket = new Socket(new WebSocket(`${protocol}//${location.host}/`));
|
||||||
|
|
||||||
|
socket.addEventListener('open', () => {
|
||||||
|
console.log('Connected to websocket server');
|
||||||
|
reconnectAttempts = 0;
|
||||||
|
update(s => ({ ...s, status: WebsocketConnectionState.CONNECTED, socket }));
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.addEventListener('message', messageHandler);
|
||||||
|
|
||||||
|
socket.addEventListener('close', () => {
|
||||||
|
console.log('Disconnected from websocket server,');
|
||||||
|
update(s => ({ ...s, socket: null }));
|
||||||
|
|
||||||
|
// exponential backoff
|
||||||
|
const timeout = Math.min(Math.pow(2, reconnectAttempts) * 1000, 30000);
|
||||||
|
reconnectAttempts++;
|
||||||
|
|
||||||
|
console.log(`Reconnecting in ${timeout / 1000}s...`);
|
||||||
|
update(s => ({ ...s, status: WebsocketConnectionState.RECONNECTING }));
|
||||||
|
|
||||||
|
reconnectTimeout = setTimeout(() => {
|
||||||
|
connect();
|
||||||
|
}, timeout);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.addEventListener('error', () => {
|
||||||
|
console.error('Error connecting to websocket server');
|
||||||
|
socket.close();
|
||||||
|
// close will trigger a reconnect
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
send,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ws = createWebSocketStore(handleMessage);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export enum ConnectionState {
|
export enum RoomConnectionState {
|
||||||
CONNECTING,
|
CONNECTING,
|
||||||
RECONNECTING,
|
RECONNECTING,
|
||||||
CONNECTED,
|
CONNECTED,
|
||||||
@@ -8,7 +8,7 @@ export enum ConnectionState {
|
|||||||
export interface Room {
|
export interface Room {
|
||||||
id: string | null;
|
id: string | null;
|
||||||
participants: number;
|
participants: number;
|
||||||
connectionState: ConnectionState;
|
connectionState: RoomConnectionState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum WebSocketMessageType {
|
export enum WebSocketMessageType {
|
||||||
@@ -51,6 +51,7 @@ interface ErrorMessage {
|
|||||||
|
|
||||||
interface CreateRoomMessage {
|
interface CreateRoomMessage {
|
||||||
type: WebSocketMessageType.CREATE_ROOM;
|
type: WebSocketMessageType.CREATE_ROOM;
|
||||||
|
roomName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface JoinRoomMessage {
|
interface JoinRoomMessage {
|
||||||
@@ -129,11 +130,16 @@ export class Socket {
|
|||||||
console.log("WebSocket opened");
|
console.log("WebSocket opened");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
this.addEventListener = this.ws.addEventListener.bind(this.ws);
|
this.addEventListener = this.ws.addEventListener.bind(this.ws);
|
||||||
this.removeEventListener = this.ws.removeEventListener.bind(this.ws);
|
this.removeEventListener = this.ws.removeEventListener.bind(this.ws);
|
||||||
this.close = this.ws.close.bind(this.ws);
|
this.close = this.ws.close.bind(this.ws);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get readyState(): number {
|
||||||
|
return this.ws.readyState;
|
||||||
|
}
|
||||||
|
|
||||||
public send(message: WebSocketMessage) {
|
public send(message: WebSocketMessage) {
|
||||||
console.log("Sending message:", message);
|
console.log("Sending message:", message);
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { writable, get, type Writable } from "svelte/store";
|
|||||||
import { WebRTCPeer } from "$lib/webrtc";
|
import { WebRTCPeer } from "$lib/webrtc";
|
||||||
import { CHUNK_SIZE, WebRTCPacketType } from "../types/webrtc";
|
import { CHUNK_SIZE, WebRTCPacketType } from "../types/webrtc";
|
||||||
import { room } from "../stores/roomStore";
|
import { room } from "../stores/roomStore";
|
||||||
import { ConnectionState, type Room } from "../types/websocket";
|
import { RoomConnectionState, type Room } from "../types/websocket";
|
||||||
import { advertisedOffers, fileRequestIds, messages, receivedOffers } from "../stores/messageStore";
|
import { advertisedOffers, fileRequestIds, messages, receivedOffers } from "../stores/messageStore";
|
||||||
import { MessageType, type Message } from "../types/message";
|
import { MessageType, type Message } from "../types/message";
|
||||||
import { WebSocketMessageType, type WebSocketMessage } from "../types/websocket";
|
import { WebSocketMessageType, type WebSocketMessage } from "../types/websocket";
|
||||||
import { WebBuffer } from "./buffer";
|
import { WebBuffer } from "./buffer";
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
|
|
||||||
export const error: Writable<string | null> = writable(null);
|
export const error: Writable<string | null> = writable(null);
|
||||||
export let peer: Writable<WebRTCPeer | null> = writable(null);
|
export let peer: Writable<WebRTCPeer | null> = writable(null);
|
||||||
@@ -255,7 +256,8 @@ export async function handleMessage(event: MessageEvent) {
|
|||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case WebSocketMessageType.ROOM_CREATED:
|
case WebSocketMessageType.ROOM_CREATED:
|
||||||
console.log("Room created:", message.data);
|
console.log("Room created:", message.data);
|
||||||
room.update((room) => ({ ...room, id: message.data, connectionState: ConnectionState.CONNECTED, participants: 1 }));
|
room.set({ id: message.data, host: true, RTCConnectionReady: false, connectionState: RoomConnectionState.CONNECTED, participants: 1 });
|
||||||
|
goto(`/${message.data}`);
|
||||||
return;
|
return;
|
||||||
case WebSocketMessageType.JOIN_ROOM:
|
case WebSocketMessageType.JOIN_ROOM:
|
||||||
console.log("new client joined room");
|
console.log("new client joined room");
|
||||||
@@ -263,7 +265,8 @@ export async function handleMessage(event: MessageEvent) {
|
|||||||
return;
|
return;
|
||||||
case WebSocketMessageType.ROOM_JOINED:
|
case WebSocketMessageType.ROOM_JOINED:
|
||||||
// TODO: if a client disconnects, we need to resync the room state
|
// TODO: if a client disconnects, we need to resync the room state
|
||||||
room.update((room) => ({ ...room, connectionState: ConnectionState.CONNECTED, participants: message.participants }));
|
|
||||||
|
room.set({ host: false, id: message.roomId, RTCConnectionReady: false, connectionState: RoomConnectionState.CONNECTED, participants: message.participants });
|
||||||
console.log("Joined room");
|
console.log("Joined room");
|
||||||
return;
|
return;
|
||||||
case WebSocketMessageType.ROOM_LEFT:
|
case WebSocketMessageType.ROOM_LEFT:
|
||||||
@@ -282,6 +285,8 @@ export async function handleMessage(event: MessageEvent) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
room.update(r => ({ ...r, RTCConnectionReady: true }));
|
||||||
|
|
||||||
console.log("Creating peer");
|
console.log("Creating peer");
|
||||||
peer.set(new WebRTCPeer(
|
peer.set(new WebRTCPeer(
|
||||||
roomId,
|
roomId,
|
||||||
|
|||||||
BIN
static/fonts/InstrumentSans-VariableFont_wdth,wght.woff2
Normal file
BIN
static/fonts/InstrumentSans-VariableFont_wdth,wght.woff2
Normal file
Binary file not shown.
Reference in New Issue
Block a user