setDisplayingActions(!isDisplayingActions)}>
+
setDisplayingActions(!isDisplayingActions)}
+ >
{contents}
)
diff --git a/todoblue/src/app/board/[board]/useBoard.tsx b/todoblue/src/app/board/[board]/useBoard.tsx
index 2516c24..9d2bfa5 100644
--- a/todoblue/src/app/board/[board]/useBoard.tsx
+++ b/todoblue/src/app/board/[board]/useBoard.tsx
@@ -4,7 +4,7 @@ import {TASK_GROUPERS} from "@/app/board/[board]/doTaskGrouping"
import {TASK_SORTERS} from "@/app/board/[board]/doTaskSorting"
import {BoardAction, Task} from "@/app/board/[board]/Types"
import {useBoardTaskEditor} from "@/app/board/[board]/useBoardTaskEditor"
-import {useBoardWebSocket} from "@/app/board/[board]/useBoardWebSocket"
+import {useBoardWs} from "@/app/board/[board]/useBoardWs"
import {TaskGroup, useBoardTaskArranger} from "@/app/board/[board]/useBoardTaskArranger"
import {useBoardTitleEditor} from "@/app/board/[board]/useBoardTitleEditor"
import {useCycleState} from "@/app/useCycleState"
@@ -14,7 +14,7 @@ export interface UseBoardReturns {
title: string,
tasksById: {[id: string]: Task},
taskGroups: TaskGroup[],
- websocketState: number,
+ webSocketState: number | undefined,
isEditingTitle: boolean,
stopEditingTitle: () => void,
startEditingTitle: () => void,
@@ -35,13 +35,13 @@ export interface UseBoardReturns {
}
export function useBoard(name: string): UseBoardReturns {
- const {state: {title, tasksById}, send, websocketState} = useBoardWebSocket(name);
+ const {state: {title, tasksById}, sendAction, webSocketState} = useBoardWs(name);
const {value: [taskGrouper, groupSorter, groupNamer], move: moveGrouper, next: nextGrouper, previous: previousGrouper} = useCycleState(TASK_GROUPERS);
const {value: taskSorter, move: moveSorter, next: nextSorter, previous: previousSorter} = useCycleState(TASK_SORTERS);
const {taskGroups} = useBoardTaskArranger(tasksById, taskGrouper, groupSorter, groupNamer, taskSorter);
- const {isEditingTitle, stopEditingTitle, startEditingTitle, toggleEditingTitle, editTitle, setEditTitle} = useBoardTitleEditor(title, send);
+ const {isEditingTitle, stopEditingTitle, startEditingTitle, toggleEditingTitle, editTitle, setEditTitle} = useBoardTitleEditor(title, sendAction);
const {editedTaskText, setEditedTaskText, editedTask, setEditedTask} = useBoardTaskEditor()
@@ -49,7 +49,7 @@ export function useBoard(name: string): UseBoardReturns {
title,
tasksById,
taskGroups,
- websocketState,
+ webSocketState,
isEditingTitle,
stopEditingTitle,
startEditingTitle,
@@ -62,7 +62,7 @@ export function useBoard(name: string): UseBoardReturns {
previousSorter,
editTitle,
setEditTitle,
- send,
+ send: sendAction,
editedTaskText,
setEditedTaskText,
editedTask,
diff --git a/todoblue/src/app/board/[board]/useBoardWebSocket.ts b/todoblue/src/app/board/[board]/useBoardWebSocket.ts
deleted file mode 100644
index 39d3bee..0000000
--- a/todoblue/src/app/board/[board]/useBoardWebSocket.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-'use client';
-
-import {BoardAction} from "@/app/board/[board]/Types"
-import {useBoardState} from "@/app/board/[board]/useBoardState"
-import {useBoardWebSocketURL} from "@/app/board/[board]/useBoardWebSocketURL"
-import {useCallback} from "react"
-import {useWebSocket} from "@/app/useWebSocket"
-
-
-export function useBoardWebSocket(name: string) {
- const {webSocketURL} = useBoardWebSocketURL(name)
- const {state, act} = useBoardState();
-
- const {websocket, websocketState} = useWebSocket(webSocketURL, {
- onopen: useCallback((_sock: WebSocket, _event: Event) => {
- console.debug("[useBoard] Connected to board:", name);
- act(null);
- }, []),
- onmessage: useCallback((_sock: WebSocket, event: MessageEvent) => {
- const action: BoardAction = JSON.parse(event.data);
- console.debug("[useBoard] Received:", action);
- act(action)
- }, []),
- });
-
- const send = useCallback((data: BoardAction) => {
- if(!websocket || websocketState !== WebSocket.OPEN) {
- console.warn("[useBoardWebSocket] Webbsocket is not yet ready, cannot send:", data);
- return;
- }
- console.debug("[useBoardWebSocket] Sending:", data);
- websocket.send(JSON.stringify(data));
- }, [websocket, websocketState])
-
- return {state, send, websocketState}
-}
diff --git a/todoblue/src/app/board/[board]/useBoardWebSocketURL.ts b/todoblue/src/app/board/[board]/useBoardWebSocketURL.ts
deleted file mode 100644
index 785cb0a..0000000
--- a/todoblue/src/app/board/[board]/useBoardWebSocketURL.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-"use client";
-import {useMemo} from "react"
-
-
-const HTTP_TO_WS = {
- "http:": "ws:",
- "https:": "wss:",
-}
-
-export function useBoardWebSocketURL(name: string) {
- // @ts-ignore
- const protocol = HTTP_TO_WS[window.location.protocol]
- const host = window.location.host
-
- const webSocketURL = useMemo(() => `${protocol}//${host}/api/board/${name}/ws`, [name]);
- return {webSocketURL}
-}
diff --git a/todoblue/src/app/board/[board]/useBoardWs.ts b/todoblue/src/app/board/[board]/useBoardWs.ts
new file mode 100644
index 0000000..fec591c
--- /dev/null
+++ b/todoblue/src/app/board/[board]/useBoardWs.ts
@@ -0,0 +1,46 @@
+'use client';
+
+import {BoardAction} from "@/app/board/[board]/Types"
+import {useBoardState} from "@/app/board/[board]/useBoardState"
+import {useWsBaseURL} from "@/app/useWsBaseURL"
+import {useCallback, useMemo} from "react"
+import {useWs, WebSocketHandlerParams} from "@/app/useWs"
+
+
+export function useBoardWs(name: string) {
+ const wsBaseURL = useWsBaseURL()
+ const wsFullURL = useMemo(() => wsBaseURL ? `${wsBaseURL}/board/${name}/ws` : undefined, [wsBaseURL, name])
+
+ const {state, act} = useBoardState();
+
+ const {webSocket, webSocketState} = useWs(wsFullURL, {
+ onopen: useCallback(({}) => {
+ console.debug("[useBoardWs] Connected to board:", name);
+ act(null);
+ }, [act]),
+ onmessage: useCallback(({event}: WebSocketHandlerParams
) => {
+ const action: BoardAction = JSON.parse(event.data);
+ console.debug("[useBoardWs] Parsed payload as:", action);
+ act(action)
+ }, [act]),
+ onerror: useCallback(({event, closeWebSocket}: WebSocketHandlerParams) => {
+ console.error("[useBoardWs] Encountered a WebSocket error, closing current connection:", event);
+ closeWebSocket()
+ }, []),
+ onclose: useCallback(({event, openWebSocket}: WebSocketHandlerParams) => {
+ console.debug("[useBoardWs] WebSocket was closed, trying to reconnect:", event);
+ openWebSocket()
+ }, [])
+ });
+
+ const sendAction = useCallback((data: BoardAction) => {
+ if(!webSocket || webSocketState !== WebSocket.OPEN) {
+ console.warn("[useBoardWs] WebSocket is not yet ready, cannot send:", data);
+ return;
+ }
+ console.debug("[useBoardWs] Sending:", data);
+ webSocket.send(JSON.stringify(data));
+ }, [webSocket, webSocketState])
+
+ return {state, sendAction, webSocketState}
+}
diff --git a/todoblue/src/app/useHttpBaseURL.ts b/todoblue/src/app/useHttpBaseURL.ts
new file mode 100644
index 0000000..54ff204
--- /dev/null
+++ b/todoblue/src/app/useHttpBaseURL.ts
@@ -0,0 +1,20 @@
+import {useEffect, useState} from "react"
+
+
+/**
+ * **Hook** returning the base URL to use in API calls.
+ *
+ * Obtains the base URL from `window.location`, unless the `NEXT_PUBLIC_TODOBLUE_OVERRIDE_BASE_URL` environment variable is set, which takes precedence in that case.
+ */
+export function useHttpBaseURL(): string | undefined {
+ const [baseURL, setBaseURL] = useState(undefined);
+
+ useEffect(() => {
+ let url = process.env.NEXT_PUBLIC_TODOBLUE_OVERRIDE_BASE_URL;
+ if(!url) url = `${window.location.protocol}//${window.location.host}`;
+ console.debug("[useBaseURL] Using base URL:", url);
+ setBaseURL(url);
+ }, []);
+
+ return baseURL;
+}
diff --git a/todoblue/src/app/useWebSocket.ts b/todoblue/src/app/useWebSocket.ts
deleted file mode 100644
index c08800f..0000000
--- a/todoblue/src/app/useWebSocket.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-'use client';
-
-import {useState, useEffect, useCallback} from "react"
-
-export interface WebSocketHandlers {
- onclose?: (sock: WebSocket, event: CloseEvent) => void,
- onerror?: (sock: WebSocket, event: Event) => void,
- onmessage?: (sock: WebSocket, event: MessageEvent) => void,
- onopen?: (sock: WebSocket, event: Event) => void,
-}
-
-export function useWebSocket(url: string, {onclose, onerror, onmessage, onopen}: WebSocketHandlers) {
- const [websocket, setWebsocket] = useState(null)
- const [websocketState, setWebsocketState] = useState(0);
-
- useEffect(() => {
- console.debug("[useWebSocket] Creating websocket...");
- const sock = new WebSocket(url);
- setWebsocket(sock);
- sock.onopen = (ev) => {
- setWebsocketState(sock.readyState);
- onopen?.(sock, ev);
- }
- sock.onclose = (ev) => {
- setWebsocketState(sock.readyState);
- onclose?.(sock, ev);
- }
- sock.onerror = (ev) => {
- setWebsocketState(sock.readyState);
- onerror?.(sock, ev);
- }
- sock.onmessage = (ev) => {
- onmessage?.(sock, ev);
- }
- return () => {
- console.debug("[useWebSocket] Closing websocket...");
- sock.close();
- setWebsocket(null);
- }
- }, [url, onclose, onerror, onmessage, onopen])
-
- return {websocket, websocketState}
-}
diff --git a/todoblue/src/app/useWs.ts b/todoblue/src/app/useWs.ts
new file mode 100644
index 0000000..8942f7b
--- /dev/null
+++ b/todoblue/src/app/useWs.ts
@@ -0,0 +1,91 @@
+'use client';
+
+import {useState, useCallback, useEffect} from "react"
+
+export interface WebSocketHandlerParams {
+ event: E,
+ openWebSocket: () => void,
+ closeWebSocket: () => void,
+}
+
+export interface WebSocketHandlers {
+ onclose?: (params: WebSocketHandlerParams) => void,
+ onerror?: (params: WebSocketHandlerParams) => void,
+ onmessage?: (params: WebSocketHandlerParams) => void,
+ onopen?: (params: WebSocketHandlerParams) => void,
+}
+
+export function useWs(url: string | undefined, {onclose, onerror, onmessage, onopen}: WebSocketHandlers) {
+ const [webSocket, setWebSocket] = useState(undefined)
+ const [webSocketState, setWebSocketState] = useState(undefined);
+ const [webSocketBackoffMs, setWebSocketBackoffMs] = useState(1);
+
+ const closeWebSocket = useCallback(() => {
+ console.debug("[useWebSocket] Closing WebSocket:", webSocket);
+ if(webSocket === undefined) {
+ console.warn("[useWebSocket] Trying to close WebSocket, but no WebSocket is open; ignoring request...")
+ return;
+ }
+ try {
+ webSocket.close();
+ }
+ catch(closeErr) {
+ console.debug("[useWebSocket] Failed to close the websocket (it might be already closed):", closeErr)
+ }
+ setWebSocket(undefined);
+ setWebSocketState(WebSocket.CLOSED);
+ }, [webSocket])
+
+ const openWebSocket = useCallback(() => {
+ console.debug("[useWebSocket] Opening WebSocket:", url);
+ if(url === undefined) {
+ console.warn("[useWebSocket] Trying to open WebSocket, but no URL has been given; ignoring request...")
+ return;
+ }
+ setWebSocketState(WebSocket.CONNECTING); // Workaround for constructor blocking and giving no feedback to the user
+ const sock = new WebSocket(url);
+ sock.onopen = (event) => {
+ console.debug("[useWebSocket] Opened connection:", event)
+ setWebSocket(sock)
+ setWebSocketState(sock.readyState)
+ setWebSocketBackoffMs(1)
+ onopen?.({event, openWebSocket, closeWebSocket});
+ }
+ sock.onclose = (event) => {
+ console.debug("[useWebSocket] Closed connection:", event)
+ setWebSocket(undefined)
+ setWebSocketState(sock.readyState);
+ onclose?.({event, openWebSocket, closeWebSocket});
+ }
+ sock.onerror = (event) => {
+ console.error("[useWebSocket] Error in connection:", event)
+ setWebSocketState(sock.readyState)
+ setWebSocketBackoffMs(prev => prev * 2)
+ onerror?.({event, openWebSocket, closeWebSocket});
+ }
+ sock.onmessage = (event) => {
+ console.debug("[useWebSocket] Received message:", event)
+ onmessage?.({event, openWebSocket, closeWebSocket});
+ }
+ }, [url, onopen, onclose, onerror, onmessage])
+
+ // @ts-ignore
+ const openWebSocketAfterBackoff = useCallback(() => {
+ console.debug("[useWebSocket] Backing off for:", webSocketBackoffMs, "ms")
+ return setTimeout(openWebSocket, webSocketBackoffMs)
+ }, [openWebSocket, webSocketBackoffMs])
+
+ useEffect(() => {
+ if(!url) {
+ return;
+ }
+ console.debug("[useWebSocket] Hook mounted, opening connection as soon as possible...")
+ // @ts-ignore
+ const handle = openWebSocketAfterBackoff()
+ return () => {
+ clearTimeout(handle)
+ }
+ }, [url, openWebSocketAfterBackoff])
+
+ return {webSocket, webSocketState, openWebSocket, closeWebSocket}
+}
diff --git a/todoblue/src/app/useWsBaseURL.ts b/todoblue/src/app/useWsBaseURL.ts
new file mode 100644
index 0000000..5b5a271
--- /dev/null
+++ b/todoblue/src/app/useWsBaseURL.ts
@@ -0,0 +1,9 @@
+import {useHttpBaseURL} from "@/app/useHttpBaseURL"
+
+
+/**
+ * **Hook** similar to {@link useHttpBaseURL}, but returning the websocket URL instead.
+ */
+export function useWsBaseURL() {
+ return useHttpBaseURL()?.replace(/^http/, "ws")
+}