added tab control to chat, When user leaves tab open on chat

This commit is contained in:
NigeParis 2025-11-26 14:41:21 +01:00
parent 40980bedeb
commit 422c0b26c4
9 changed files with 151 additions and 119 deletions

View file

@ -2,7 +2,6 @@ apis/OpenapiOtherApi.ts
apis/index.ts apis/index.ts
index.ts index.ts
models/ChatTest200Response.ts models/ChatTest200Response.ts
models/ChatTest500Response.ts
models/DisableOtp200Response.ts models/DisableOtp200Response.ts
models/DisableOtp401Response.ts models/DisableOtp401Response.ts
models/DisableOtp500Response.ts models/DisableOtp500Response.ts

View file

@ -16,7 +16,6 @@
import * as runtime from '../runtime'; import * as runtime from '../runtime';
import type { import type {
ChatTest200Response, ChatTest200Response,
ChatTest500Response,
DisableOtp200Response, DisableOtp200Response,
DisableOtp401Response, DisableOtp401Response,
DisableOtp500Response, DisableOtp500Response,
@ -50,8 +49,6 @@ import type {
import { import {
ChatTest200ResponseFromJSON, ChatTest200ResponseFromJSON,
ChatTest200ResponseToJSON, ChatTest200ResponseToJSON,
ChatTest500ResponseFromJSON,
ChatTest500ResponseToJSON,
DisableOtp200ResponseFromJSON, DisableOtp200ResponseFromJSON,
DisableOtp200ResponseToJSON, DisableOtp200ResponseToJSON,
DisableOtp401ResponseFromJSON, DisableOtp401ResponseFromJSON,
@ -135,7 +132,7 @@ export class OpenapiOtherApi extends runtime.BaseAPI {
/** /**
*/ */
async chatTestRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<ChatTest200Response | StatusOtp401Response | ChatTest500Response>> { async chatTestRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<ChatTest200Response | StatusOtp401Response>> {
const queryParameters: any = {}; const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {}; const headerParameters: runtime.HTTPHeaders = {};
@ -162,19 +159,15 @@ export class OpenapiOtherApi extends runtime.BaseAPI {
// Object response for status 401 // Object response for status 401
return new runtime.JSONApiResponse(response, (jsonValue) => StatusOtp401ResponseFromJSON(jsonValue)); return new runtime.JSONApiResponse(response, (jsonValue) => StatusOtp401ResponseFromJSON(jsonValue));
} }
if (response.status === 500) {
// Object response for status 500
return new runtime.JSONApiResponse(response, (jsonValue) => ChatTest500ResponseFromJSON(jsonValue));
}
// CHANGED: Throw error if status code is not handled by any of the defined responses // CHANGED: Throw error if status code is not handled by any of the defined responses
// This ensures all code paths return a value and provides clear error messages for unexpected status codes // This ensures all code paths return a value and provides clear error messages for unexpected status codes
// Only throw if responses were defined but none matched the actual status code // Only throw if responses were defined but none matched the actual status code
throw new runtime.ResponseError(response, `Unexpected status code: ${response.status}. Expected one of: 200, 401, 500`); throw new runtime.ResponseError(response, `Unexpected status code: ${response.status}. Expected one of: 200, 401`);
} }
/** /**
*/ */
async chatTest(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<ChatTest200Response | StatusOtp401Response | ChatTest500Response> { async chatTest(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<ChatTest200Response | StatusOtp401Response> {
const response = await this.chatTestRaw(initOverrides); const response = await this.chatTestRaw(initOverrides);
return await response.value(); return await response.value();
} }

View file

@ -1,7 +1,6 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export * from './ChatTest200Response'; export * from './ChatTest200Response';
export * from './ChatTest500Response';
export * from './DisableOtp200Response'; export * from './DisableOtp200Response';
export * from './DisableOtp401Response'; export * from './DisableOtp401Response';
export * from './DisableOtp500Response'; export * from './DisableOtp500Response';

View file

@ -16,29 +16,49 @@ document.addEventListener('ft:pageChange', () => {
document.addEventListener("visibilitychange", async () => { document.addEventListener("visibilitychange", async () => {
// When user leaves tab const socketId = __socket || undefined;
let oldName = localStorage.getItem("oldName") || undefined;
if (socketId == undefined) return;
if (document.visibilityState === "hidden") { if (document.visibilityState === "hidden") {
console.log("User LEFT this tab"); let userName = await updateUser();
oldName = userName?.name || "undefined";
// if (__socket) { localStorage.setItem("oldName", oldName);
// __socket.close(); socketId.emit("client_left");
// __socket = undefined;
// }
return; return;
} }
// When user returns to tab → soft reload using imported HTML file
if (document.visibilityState === "visible") { if (document.visibilityState === "visible") {
// location.reload(); const res = await client.guestLogin();
//console.log(location.replace(location.href)); let user = await updateUser();
socketId.emit('client_entered', {
userName: oldName,
console.log('Chat Visible') user: user?.name,
});
setTitle('Chat Page');
return;
} }
}); });
async function getUserName(): Promise<string | null> {
try {
const res = await client.guestLogin();
if (res.kind !== "success") {
console.error("Login failed:", res.msg);
return null;
}
const user = await updateUser();
if (!user) return null;
return user.name; // <-- return the username
} catch (err) {
console.error("getUserName error:", err);
return null;
}
}
function getSocket(): Socket { function getSocket(): Socket {
if (__socket === undefined) if (__socket === undefined)
__socket = io("wss://localhost:8888", { __socket = io("wss://localhost:8888", {
@ -142,8 +162,8 @@ function handleChat(_url: string, _args: RouteHandlerParams): RouteHandlerReturn
}; };
socket.on("welcome", (data) => { socket.once('welcome', (data) => {
addMessage(`${data.msg}`); addMessage (`${data.msg} ` + getUser()?.name);
}); });
// Send button // Send button
@ -161,7 +181,7 @@ function handleChat(_url: string, _args: RouteHandlerParams): RouteHandlerReturn
timestamp: Date.now(), timestamp: Date.now(),
SenderWindowID: socket.id, SenderWindowID: socket.id,
}; };
socket.send(JSON.stringify(message)); socket.emit('message', JSON.stringify(message));
} }
sendtextbox.value = ""; sendtextbox.value = "";
} }
@ -182,6 +202,9 @@ function handleChat(_url: string, _args: RouteHandlerParams): RouteHandlerReturn
const loggedIn = await isLoggedIn(); const loggedIn = await isLoggedIn();
if (loggedIn?.name === undefined) return ; if (loggedIn?.name === undefined) return ;
const res = await client.guestLogin();
let user = await updateUser();
console.log('USER ', user?.name);
if (chatWindow) { if (chatWindow) {
addMessage('@list - lists all connected users in the chat'); addMessage('@list - lists all connected users in the chat');
socket.emit('list'); socket.emit('list');
@ -228,3 +251,4 @@ function handleChat(_url: string, _args: RouteHandlerParams): RouteHandlerReturn
} }
}; };
addRoute('/chat', handleChat, { bypass_auth: true }); addRoute('/chat', handleChat, { bypass_auth: true });
addRoute('/chat/', handleChat, { bypass_auth: true });

View file

@ -112,32 +112,6 @@
} }
} }
} }
},
"500": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"kind",
"msg"
],
"properties": {
"kind": {
"enum": [
"failed"
]
},
"msg": {
"enum": [
"chat.failed.generic"
]
}
}
}
}
}
} }
} }
} }

View file

@ -163,12 +163,10 @@ async function onReady(fastify: FastifyInstance) {
} }
}); });
} }
fastify.io.on('connection', (socket: Socket) => { fastify.io.on('connection', (socket: Socket) => {
socket.on('message', (message: string) => { socket.on('message', (message: string) => {
console.info( console.info(
color.blue, color.blue,
@ -191,6 +189,10 @@ async function onReady(fastify: FastifyInstance) {
color.reset, color.reset,
`Sender: login name: "${obj.user}" - windowID "${obj.SenderWindowID}" - text message: "${obj.text}"`, `Sender: login name: "${obj.user}" - windowID "${obj.SenderWindowID}" - text message: "${obj.text}"`,
); );
socket.emit('welcome', {
msg: `Welcome to the chat! `,
});
// Send object directly — DO NOT wrap it in a string // Send object directly — DO NOT wrap it in a string
broadcast(obj, obj.SenderWindowID); broadcast(obj, obj.SenderWindowID);
console.log( console.log(
@ -201,33 +203,35 @@ async function onReady(fastify: FastifyInstance) {
); );
}); });
socket.emit("welcome", {
msg: `Welcome to the chat!`,
id: socket.id
});
socket.on('testend', (sock_id_cl: string) => { socket.on('testend', (sock_id_cl: string) => {
console.log('testend received from client socket id:', sock_id_cl); console.log('testend received from client socket id:', sock_id_cl);
}); });
socket.on('wakeup', (message: string) => {
const obj: ClientMessage = JSON.parse(message) as ClientMessage;
clientChat.set(socket.id, { user: obj.user, lastSeen: Date.now() });
connectedUser(fastify.io),
console.log('Wakeup: ', message);
});
socket.on('list', () => { socket.on('list', () => {
console.log(color.red, 'list activated', color.reset, socket.id); console.log(color.red, 'list activated', color.reset, socket.id);
connectedUser(fastify.io, socket.id); connectedUser(fastify.io, socket.id);
}); });
socket.on('disconnecting', (reason) => { socket.on('disconnecting', (reason) => {
const clientName = clientChat.get(socket.id) || null; const clientName = clientChat.get(socket.id)?.user|| null;
console.log( console.log(
color.green, color.green,
`Client disconnecting: ${clientName?.user} (${socket.id}) reason:`, `Client disconnecting: ${clientName} (${socket.id}) reason:`,
reason, reason,
); );
if (reason === 'transport error') return; if (reason === 'transport error') return;
if (clientName?.user !== null) { if (clientName !== null) {
const obj = { const obj = {
type: 'chat', type: 'chat',
user: clientName!.user, user: clientName,
token: '', token: '',
text: 'LEFT the chat', text: 'LEFT the chat',
timestamp: Date.now(), timestamp: Date.now(),
@ -238,5 +242,70 @@ async function onReady(fastify: FastifyInstance) {
// clientChat.delete(obj.user); // clientChat.delete(obj.user);
} }
}); });
socket.on('client_left', (reason) => {
const clientName = clientChat.get(socket.id)?.user|| null;
console.log(
color.green,
`Client left the Chat: ${clientName} (${socket.id}) reason:`,
reason,
);
if (reason === 'transport error') return;
if (clientName !== null) {
const obj = {
type: 'chat',
user: clientName,
token: '',
text: 'LEFT the chat but the window is still open',
timestamp: Date.now(),
SenderWindowID: socket.id,
};
console.log(obj.SenderWindowID);
broadcast(obj, obj.SenderWindowID);
// clientChat.delete(obj.user);
}
});
socket.on('client_entered', (data) => {
// data may be undefined (when frontend calls emit with no payload)
const userNameFromFrontend = data?.userName || null;
const userFromFrontend = data?.user || null;
let clientName = clientChat.get(socket.id)?.user || null;
const client = clientChat.get(socket.id) || null;
let text = 'is back in the chat';
// connectedUser(fastify.io, socket.id);
if(clientName === null) {console.log('ERROR: clientName is NULL'); return;};
if(client === null) {console.log('ERROR: client is NULL'); return;};
if (userNameFromFrontend !== userFromFrontend) {
text = `'is back in the chat, I used to be called '${userNameFromFrontend}`;
clientName = userFromFrontend;
if(clientName === null) {console.log('ERROR: clientName is NULL'); return;};
if (client) {
client.user = clientName;
}
}
console.log(
color.green,
`Client entered the Chat: ${clientName} (${socket.id})`
);
if (clientName !== null) {
const obj = {
type: 'chat',
user: clientName, // server-side stored name
frontendUserName: userNameFromFrontend, // from frontend
frontendUser: userFromFrontend, // from frontend
token: '',
text: text,
timestamp: Date.now(),
SenderWindowID: socket.id,
};
broadcast(obj, obj.SenderWindowID);
}
});
}); });
} }

View file

@ -1294,32 +1294,6 @@
} }
} }
} }
},
"500": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"kind",
"msg"
],
"properties": {
"kind": {
"enum": [
"failed"
]
},
"msg": {
"enum": [
"chat.failed.generic"
]
}
}
}
}
}
} }
}, },
"tags": [ "tags": [

View file

@ -36,7 +36,7 @@
"vite": "^7.2.4" "vite": "^7.2.4"
}, },
"dependencies": { "dependencies": {
"@redocly/cli": "^2.11.1", "@redocly/cli": "^2.12.0",
"bindings": "^1.5.0" "bindings": "^1.5.0"
} }
} }

46
src/pnpm-lock.yaml generated
View file

@ -9,8 +9,8 @@ importers:
.: .:
dependencies: dependencies:
'@redocly/cli': '@redocly/cli':
specifier: ^2.11.1 specifier: ^2.12.0
version: 2.11.1(@opentelemetry/api@1.9.0)(ajv@8.17.1)(core-js@3.47.0) version: 2.12.0(@opentelemetry/api@1.9.0)(ajv@8.17.1)(core-js@3.47.0)
bindings: bindings:
specifier: ^1.5.0 specifier: ^1.5.0
version: 1.5.0 version: 1.5.0
@ -946,27 +946,27 @@ packages:
'@redocly/ajv@8.17.1': '@redocly/ajv@8.17.1':
resolution: {integrity: sha512-EDtsGZS964mf9zAUXAl9Ew16eYbeyAFWhsPr0fX6oaJxgd8rApYlPBf0joyhnUHz88WxrigyFtTaqqzXNzPgqw==} resolution: {integrity: sha512-EDtsGZS964mf9zAUXAl9Ew16eYbeyAFWhsPr0fX6oaJxgd8rApYlPBf0joyhnUHz88WxrigyFtTaqqzXNzPgqw==}
'@redocly/cli@2.11.1': '@redocly/cli@2.12.0':
resolution: {integrity: sha512-doNs+sdrFzzXmyf1yIeJbPh8OChacHWkvTE9N0QbuCmnYQ4k0v1IMP20qsitkwR+fK8O1hXSnFnSTVvIunMVVw==} resolution: {integrity: sha512-/q8RnBe+Duo+XYFCG8LnaD0kroGZ8MoS6575Xq59tCgjaCL16F+pZZ75xNBU2oXfEypJClNz/6ilc2G0q1+tlw==}
engines: {node: '>=22.12.0 || >=20.19.0 <21.0.0', npm: '>=10'} engines: {node: '>=22.12.0 || >=20.19.0 <21.0.0', npm: '>=10'}
hasBin: true hasBin: true
'@redocly/config@0.22.2': '@redocly/config@0.22.2':
resolution: {integrity: sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==} resolution: {integrity: sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==}
'@redocly/config@0.38.0': '@redocly/config@0.40.0':
resolution: {integrity: sha512-kSgMG3rRzgXIP/6gWMRuWbu9/ms0Cyuphcx19dPR9qlgc1tt9IKYPsFQ+KhJuEtqd3bcY/+Uflysf33dQkZWVQ==} resolution: {integrity: sha512-MZQZs7QEGnue3rVN9Q9QvDbcGjesxbpKXUvDeckS69R1xjtgsnT9B39VA25zmwSJtgUeA9ST+sMf9GxIqixNbw==}
'@redocly/openapi-core@1.34.5': '@redocly/openapi-core@1.34.5':
resolution: {integrity: sha512-0EbE8LRbkogtcCXU7liAyC00n9uNG9hJ+eMyHFdUsy9lB/WGqnEBgwjA9q2cyzAVcdTkQqTBBU1XePNnN3OijA==} resolution: {integrity: sha512-0EbE8LRbkogtcCXU7liAyC00n9uNG9hJ+eMyHFdUsy9lB/WGqnEBgwjA9q2cyzAVcdTkQqTBBU1XePNnN3OijA==}
engines: {node: '>=18.17.0', npm: '>=9.5.0'} engines: {node: '>=18.17.0', npm: '>=9.5.0'}
'@redocly/openapi-core@2.11.1': '@redocly/openapi-core@2.12.0':
resolution: {integrity: sha512-FVCDnZxaoUJwLQxfW4inCojxUO56J3ntu7dDAE2qyWd6tJBK45CnXMQQUxpqeRTeXROr3jYQoApAw+GCEnyBeg==} resolution: {integrity: sha512-RsVwmRD0KhyJbR8acIeU98ce6N+/YCuLJf6IGN+2SOsbwnDhnI5MG0TFV9D7URK/ukEewaNA701dVYsoP1VtRQ==}
engines: {node: '>=22.12.0 || >=20.19.0 <21.0.0', npm: '>=10'} engines: {node: '>=22.12.0 || >=20.19.0 <21.0.0', npm: '>=10'}
'@redocly/respect-core@2.11.1': '@redocly/respect-core@2.12.0':
resolution: {integrity: sha512-jSMJvCJeo5gmhQfg82AhuwCG0h8gbW5vqHyRITBu8KHVsBiQTgvfhXepu8SKHeJu0OexYtEc0nUnGLJlefevYw==} resolution: {integrity: sha512-mrYrfE81shSRS96ygXaRiSithV4Fe4Y7XlSYLSTfM8Lo3YAz7Geirg7HZ5fNFsI+hdW05ZuQewqpKL8XLwaAeA==}
engines: {node: '>=22.12.0 || >=20.19.0 <21.0.0', npm: '>=10'} engines: {node: '>=22.12.0 || >=20.19.0 <21.0.0', npm: '>=10'}
'@rollup/rollup-android-arm-eabi@4.53.3': '@rollup/rollup-android-arm-eabi@4.53.3':
@ -1483,8 +1483,8 @@ packages:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
cookie@1.0.2: cookie@1.1.0:
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} resolution: {integrity: sha512-vXiThu1/rlos7EGu8TuNZQEg2e9TvhH9dmS4T4ZVzB7Ao1agEZ6EG3sn5n+hZRYUgduISd1HpngFzAZiDGm5vQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
core-js@3.47.0: core-js@3.47.0:
@ -3436,7 +3436,7 @@ snapshots:
'@fastify/cookie@11.0.2': '@fastify/cookie@11.0.2':
dependencies: dependencies:
cookie: 1.0.2 cookie: 1.1.0
fastify-plugin: 5.1.0 fastify-plugin: 5.1.0
'@fastify/deepmerge@3.1.0': {} '@fastify/deepmerge@3.1.0': {}
@ -3873,14 +3873,14 @@ snapshots:
json-schema-traverse: 1.0.0 json-schema-traverse: 1.0.0
require-from-string: 2.0.2 require-from-string: 2.0.2
'@redocly/cli@2.11.1(@opentelemetry/api@1.9.0)(ajv@8.17.1)(core-js@3.47.0)': '@redocly/cli@2.12.0(@opentelemetry/api@1.9.0)(ajv@8.17.1)(core-js@3.47.0)':
dependencies: dependencies:
'@opentelemetry/exporter-trace-otlp-http': 0.202.0(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-trace-otlp-http': 0.202.0(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-node': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-node': 2.0.1(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.34.0 '@opentelemetry/semantic-conventions': 1.34.0
'@redocly/openapi-core': 2.11.1(ajv@8.17.1) '@redocly/openapi-core': 2.12.0(ajv@8.17.1)
'@redocly/respect-core': 2.11.1(ajv@8.17.1) '@redocly/respect-core': 2.12.0(ajv@8.17.1)
abort-controller: 3.0.0 abort-controller: 3.0.0
chokidar: 3.6.0 chokidar: 3.6.0
colorette: 1.4.0 colorette: 1.4.0
@ -3913,7 +3913,7 @@ snapshots:
'@redocly/config@0.22.2': {} '@redocly/config@0.22.2': {}
'@redocly/config@0.38.0': '@redocly/config@0.40.0':
dependencies: dependencies:
json-schema-to-ts: 2.7.2 json-schema-to-ts: 2.7.2
@ -3931,10 +3931,10 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@redocly/openapi-core@2.11.1(ajv@8.17.1)': '@redocly/openapi-core@2.12.0(ajv@8.17.1)':
dependencies: dependencies:
'@redocly/ajv': 8.17.1 '@redocly/ajv': 8.17.1
'@redocly/config': 0.38.0 '@redocly/config': 0.40.0
ajv-formats: 2.1.1(ajv@8.17.1) ajv-formats: 2.1.1(ajv@8.17.1)
colorette: 1.4.0 colorette: 1.4.0
js-levenshtein: 1.1.6 js-levenshtein: 1.1.6
@ -3945,12 +3945,12 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- ajv - ajv
'@redocly/respect-core@2.11.1(ajv@8.17.1)': '@redocly/respect-core@2.12.0(ajv@8.17.1)':
dependencies: dependencies:
'@faker-js/faker': 7.6.0 '@faker-js/faker': 7.6.0
'@noble/hashes': 1.8.0 '@noble/hashes': 1.8.0
'@redocly/ajv': 8.11.4 '@redocly/ajv': 8.11.4
'@redocly/openapi-core': 2.11.1(ajv@8.17.1) '@redocly/openapi-core': 2.12.0(ajv@8.17.1)
better-ajv-errors: 1.2.0(ajv@8.17.1) better-ajv-errors: 1.2.0(ajv@8.17.1)
colorette: 2.0.20 colorette: 2.0.20
json-pointer: 0.6.2 json-pointer: 0.6.2
@ -4448,7 +4448,7 @@ snapshots:
cookie@0.7.2: {} cookie@0.7.2: {}
cookie@1.0.2: {} cookie@1.1.0: {}
core-js@3.47.0: {} core-js@3.47.0: {}
@ -5167,7 +5167,7 @@ snapshots:
light-my-request@6.6.0: light-my-request@6.6.0:
dependencies: dependencies:
cookie: 1.0.2 cookie: 1.1.0
process-warning: 4.0.1 process-warning: 4.0.1
set-cookie-parser: 2.7.2 set-cookie-parser: 2.7.2