mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Restructure code
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @fileoverview Application configuration constants
|
||||
* @description Contains all configuration values used throughout the application
|
||||
* @author Cursor
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base domain name for all requests in production
|
||||
* @constant
|
||||
*/
|
||||
export const BASE_DOMAIN = import.meta.env.VITE_API_BASE_URL ?? "fromchat.ru";
|
||||
|
||||
/**
|
||||
* Base API endpoint for all backend requests
|
||||
* @constant
|
||||
*/
|
||||
export const API_BASE_URL = `${location.host ? "" : `https://${BASE_DOMAIN}`}/api`;
|
||||
|
||||
/**
|
||||
* Full API URL including hostname and port for WebSocket connections
|
||||
* @constant
|
||||
*/
|
||||
export const API_WS_BASE_URL = `${location.host || BASE_DOMAIN}/api`;
|
||||
|
||||
/**
|
||||
* Application name displayed in UI and document title
|
||||
* @constant
|
||||
*/
|
||||
export const PRODUCT_NAME = "FromChat";
|
||||
|
||||
export const MINIMUM_WIDTH = 800;
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @fileoverview Application initialization logic
|
||||
* @description Handles initial application setup and state
|
||||
* @author FromChat Team
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import { PRODUCT_NAME } from "./config";
|
||||
import { enableMapSet } from "immer";
|
||||
|
||||
document.title = PRODUCT_NAME;
|
||||
enableMapSet();
|
||||
+378
@@ -0,0 +1,378 @@
|
||||
/**
|
||||
* @fileoverview Global TypeScript type definitions
|
||||
* @description Contains all type definitions used throughout the application
|
||||
* @author Cursor
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* HTTP headers object type
|
||||
* @typedef {Object.<string, string>} Headers
|
||||
*/
|
||||
export type Headers = {[x: string]: string}
|
||||
|
||||
/**
|
||||
* API error response structure
|
||||
* @interface ErrorResponse
|
||||
* @property {string} message - Error message from the server
|
||||
*/
|
||||
export interface ErrorResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 2D coordinate structure
|
||||
* @interface Size2D
|
||||
* @property {number} x - X coordinate
|
||||
* @property {number} y - Y coordinate
|
||||
*/
|
||||
export interface Size2D {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Rect extends Size2D {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
// App types
|
||||
|
||||
/**
|
||||
* Chat message structure
|
||||
* @interface Message
|
||||
* @property {number} id - Unique message identifier
|
||||
* @property {string} username - Username of the message sender
|
||||
* @property {string} content - Message content
|
||||
* @property {boolean} is_read - Whether the message has been read
|
||||
* @property {boolean} is_edited - Whether the message has been edited
|
||||
* @property {string} timestamp - ISO timestamp of the message
|
||||
* @property {string} [profile_picture] - URL to sender's profile picture
|
||||
* @property {Message} [reply_to] - The message this is replying to
|
||||
*/
|
||||
export interface Message {
|
||||
id: number;
|
||||
username: string;
|
||||
content: string;
|
||||
is_read: boolean;
|
||||
is_edited: boolean;
|
||||
timestamp: string;
|
||||
profile_picture?: string;
|
||||
reply_to?: Message;
|
||||
files?: Attachment[];
|
||||
|
||||
runtimeData?: {
|
||||
dmEnvelope?: DmEnvelope;
|
||||
sendingState?: {
|
||||
status: 'sending' | 'sent' | 'failed';
|
||||
tempId?: string; // Temporary ID for tracking until server confirms
|
||||
retryData?: {
|
||||
content: string;
|
||||
replyToId?: number;
|
||||
files?: File[];
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collection of messages
|
||||
* @interface Messages
|
||||
* @property {Message[]} messages - Array of message objects
|
||||
*/
|
||||
export interface Messages {
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
/**
|
||||
* User information structure
|
||||
* @interface User
|
||||
* @property {number} id - Unique user identifier
|
||||
* @property {string} created_at - ISO timestamp of account creation
|
||||
* @property {string} last_seen - ISO timestamp of last activity
|
||||
* @property {boolean} online - Whether the user is currently online
|
||||
* @property {string} username - Username
|
||||
* @property {string} [bio] - User biography
|
||||
*/
|
||||
export interface User {
|
||||
id: number;
|
||||
created_at: string;
|
||||
last_seen: string;
|
||||
online: boolean;
|
||||
username: string;
|
||||
admin?: boolean;
|
||||
bio?: string;
|
||||
profile_picture: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* User profile response structure
|
||||
* @interface UserProfile
|
||||
* @property {number} id - Unique user identifier
|
||||
* @property {string} username - Username
|
||||
* @property {string} [profile_picture] - URL to user's profile picture
|
||||
* @property {string} [bio] - User biography
|
||||
* @property {boolean} online - Whether the user is currently online
|
||||
* @property {string} last_seen - ISO timestamp of last activity
|
||||
* @property {string} created_at - ISO timestamp of account creation
|
||||
*/
|
||||
export interface UserProfile {
|
||||
id: number;
|
||||
username: string;
|
||||
profile_picture?: string;
|
||||
bio?: string;
|
||||
online: boolean;
|
||||
last_seen: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// ----------
|
||||
// API models
|
||||
// ----------
|
||||
|
||||
// Requests
|
||||
|
||||
/**
|
||||
* Login request structure
|
||||
* @interface LoginRequest
|
||||
* @property {string} username - Username for authentication
|
||||
* @property {string} password - Password for authentication
|
||||
*/
|
||||
export interface LoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registration request structure
|
||||
* @interface RegisterRequest
|
||||
* @property {string} username - Desired username
|
||||
* @property {string} password - Desired password
|
||||
* @property {string} confirm_password - Password confirmation
|
||||
*/
|
||||
export interface RegisterRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
confirm_password: string;
|
||||
}
|
||||
|
||||
export interface UploadPublicKeyRequest {
|
||||
publicKey: string;
|
||||
}
|
||||
|
||||
export interface SendDMRequest {
|
||||
recipientId: number;
|
||||
iv: string;
|
||||
ciphertext: string;
|
||||
salt: string;
|
||||
iv2: string;
|
||||
wrappedMk: string;
|
||||
replyToId?: number;
|
||||
}
|
||||
|
||||
// Responses
|
||||
|
||||
/**
|
||||
* Login response structure
|
||||
* @interface LoginResponse
|
||||
* @property {User} user - User information
|
||||
* @property {string} token - JWT authentication token
|
||||
*/
|
||||
export interface LoginResponse {
|
||||
user: User;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface BackupBlob {
|
||||
blob: string;
|
||||
}
|
||||
|
||||
export interface BaseDmEnvelope {
|
||||
iv: string;
|
||||
ciphertext: string;
|
||||
salt: string;
|
||||
iv2: string;
|
||||
wrappedMk: string;
|
||||
recipientId: number;
|
||||
}
|
||||
|
||||
export interface DmEnvelope extends BaseDmEnvelope {
|
||||
id: number;
|
||||
senderId: number;
|
||||
files?: DmFile[];
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface DmFile {
|
||||
name: string;
|
||||
id: number;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface DmEditedPayload {
|
||||
id: number;
|
||||
iv: string;
|
||||
ciphertext: string;
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface DmDeletedPayload {
|
||||
id: number;
|
||||
senderId: number;
|
||||
recipientId: number
|
||||
}
|
||||
|
||||
export interface FetchDMResponse {
|
||||
messages: DmEnvelope[]
|
||||
}
|
||||
|
||||
export interface DmEncryptedJSON {
|
||||
type: "text",
|
||||
data: {
|
||||
content: string;
|
||||
reply_to_id?: number;
|
||||
files?: Attachment[];
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------
|
||||
// WebSocket types
|
||||
// ---------------
|
||||
|
||||
/**
|
||||
* WebSocket message structure
|
||||
* @interface WebSocketMessage
|
||||
* @property {string} type - Message type identifier
|
||||
* @property {WebSocketCredentials} [credentials] - Authentication credentials
|
||||
* @property {any} [data] - Message payload data
|
||||
* @property {WebSocketError} [error] - Error information if applicable
|
||||
*/
|
||||
export interface WebSocketMessage<T> {
|
||||
type: string;
|
||||
credentials?: WebSocketCredentials;
|
||||
data?: T;
|
||||
error?: WebSocketError;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket error structure
|
||||
* @interface WebSocketError
|
||||
* @property {number} code - Error code
|
||||
* @property {string} detail - Error detail message
|
||||
*/
|
||||
export interface WebSocketError {
|
||||
code: number;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket authentication credentials
|
||||
* @interface WebSocketCredentials
|
||||
* @property {string} scheme - Authentication scheme (e.g., "Bearer")
|
||||
* @property {string} credentials - Authentication token or credentials
|
||||
*/
|
||||
export interface WebSocketCredentials {
|
||||
scheme: string;
|
||||
credentials: string;
|
||||
}
|
||||
|
||||
export interface Attachment {
|
||||
path: string;
|
||||
encrypted: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// WebSocket message types
|
||||
// -----------------------
|
||||
|
||||
// Utils
|
||||
export interface DMEditPayload {
|
||||
id: number;
|
||||
iv: string;
|
||||
ciphertext: string;
|
||||
iv2: string;
|
||||
wrappedMk: string;
|
||||
salt: string;
|
||||
}
|
||||
|
||||
// Requests
|
||||
export interface DMEditRequest extends WebSocketMessage {
|
||||
type: "dmEdit",
|
||||
credentials: WebSocketCredentials;
|
||||
data: DMEditPayload
|
||||
}
|
||||
|
||||
export interface SendMessageRequest extends WebSocketMessage {
|
||||
type: "sendMessage",
|
||||
credentials: WebSocketCredentials;
|
||||
data: {
|
||||
content: string;
|
||||
reply_to_id: number | null;
|
||||
}
|
||||
}
|
||||
|
||||
// Messages
|
||||
export interface DMNewWebSocketMessage extends WebSocketMessage {
|
||||
type: "dmNew",
|
||||
data: DmEnvelope
|
||||
}
|
||||
|
||||
export interface DMEditedWebSocketMessage extends WebSocketMessage {
|
||||
type: "dmEdited",
|
||||
data: DMEditPayload
|
||||
}
|
||||
|
||||
export interface DMDeletedWebSocketMessage extends WebSocketMessage {
|
||||
type: "dmDeleted",
|
||||
data: {
|
||||
id: number;
|
||||
}
|
||||
}
|
||||
|
||||
export interface MessageEditedWebSocketMessage extends WebSocketMessage {
|
||||
type: "messageEdited",
|
||||
data: Partial<Message> & { id: number }
|
||||
}
|
||||
|
||||
export interface MessageDeletedWebSocketMessage extends WebSocketMessage {
|
||||
type: "messageDeleted",
|
||||
data: {
|
||||
message_id: number;
|
||||
}
|
||||
}
|
||||
|
||||
export interface NewMessageWebSocketMessage extends WebSocketMessage {
|
||||
type: "newMessage",
|
||||
data: Message
|
||||
}
|
||||
|
||||
// Shared types
|
||||
export type DMWebSocketMessage = DMNewWebSocketMessage | DMEditedWebSocketMessage | DMDeletedWebSocketMessage
|
||||
export type ChatWebSocketMessage = MessageEditedWebSocketMessage | MessageDeletedWebSocketMessage | NewMessageWebSocketMessage
|
||||
|
||||
// -----------
|
||||
// Encrypted message JSON (plaintext structure before encryption)
|
||||
// -----------
|
||||
|
||||
export type ChatMessageKind = "text"; // Extendable for future kinds
|
||||
|
||||
export interface EncryptedTextMessageData {
|
||||
content: string;
|
||||
files?: Attachment[];
|
||||
reply_to_id?: number | null;
|
||||
}
|
||||
|
||||
export interface EncryptedMessageJson {
|
||||
type: ChatMessageKind;
|
||||
data: EncryptedTextMessageData;
|
||||
}
|
||||
|
||||
// -----------
|
||||
// React types
|
||||
// -----------
|
||||
export interface DialogProps {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (value: boolean) => void;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* @fileoverview WebSocket connection management for real-time chat
|
||||
* @description Handles WebSocket connections, message processing, and auto-reconnection
|
||||
* @author Cursor
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import { API_WS_BASE_URL } from "./config";
|
||||
import type { WebSocketMessage } from "./types";
|
||||
import { delay } from "../utils/utils";
|
||||
|
||||
/**
|
||||
* Creates a new WebSocket connection to the chat server
|
||||
* @returns {WebSocket} New WebSocket instance
|
||||
* @private
|
||||
*/
|
||||
function create(): WebSocket {
|
||||
let prefix = "ws://";
|
||||
if (location.protocol.includes("https")) {
|
||||
prefix = "wss://";
|
||||
}
|
||||
|
||||
return new WebSocket(`${prefix}${API_WS_BASE_URL}/chat/ws`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Global WebSocket instance
|
||||
* @type {WebSocket}
|
||||
*/
|
||||
export let websocket: WebSocket = create();
|
||||
|
||||
/**
|
||||
* Global WebSocket message handler reference
|
||||
* This will be set by the active panel to handle incoming messages
|
||||
*/
|
||||
let globalMessageHandler: ((response: WebSocketMessage<any>) => void) | null = null;
|
||||
|
||||
/**
|
||||
* Set the global WebSocket message handler
|
||||
* @param handler - Function to handle WebSocket messages
|
||||
*/
|
||||
export function setGlobalMessageHandler(handler: ((response: WebSocketMessage<any>) => void) | null): void {
|
||||
globalMessageHandler = handler;
|
||||
}
|
||||
|
||||
export function request<Request, Response = any>(payload: WebSocketMessage<Request>): Promise<WebSocketMessage<Response>> {
|
||||
console.log("WebSocket request:", payload);
|
||||
return new Promise((resolve, reject) => {
|
||||
function requestInner() {
|
||||
let listener: ((e: MessageEvent) => void) | null = null;
|
||||
listener = (e) => {
|
||||
resolve(JSON.parse(e.data));
|
||||
websocket.removeEventListener("message", listener!);
|
||||
}
|
||||
websocket.addEventListener("message", listener);
|
||||
websocket.send(JSON.stringify(payload))
|
||||
|
||||
setTimeout(() => reject("Request timed out"), 10000);
|
||||
}
|
||||
|
||||
if (websocket.readyState == 0) {
|
||||
websocket.addEventListener("open", requestInner);
|
||||
setTimeout(() => reject("Request timed out"), 10000);
|
||||
} else {
|
||||
requestInner();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* This function will wait 3 seconds and them attempts to reconnect the WebSocket.
|
||||
* If it fails, tries again in an endless loop until the connection is established
|
||||
* again.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
async function onError() {
|
||||
console.warn("WebSocket disconnected, retrying in 3 seconds...");
|
||||
await delay(3000);
|
||||
websocket = create();
|
||||
|
||||
let listener: () => void | null;
|
||||
listener = () => {
|
||||
console.log("WebSocket successfully reconnected!");
|
||||
websocket.removeEventListener("open", listener);
|
||||
}
|
||||
|
||||
websocket.addEventListener("open", listener);
|
||||
websocket.addEventListener("error", onError);
|
||||
}
|
||||
|
||||
// --------------
|
||||
// Initialization
|
||||
// --------------
|
||||
|
||||
websocket.addEventListener("message", (e) => {
|
||||
try {
|
||||
const response: WebSocketMessage<any> = JSON.parse(e.data);
|
||||
|
||||
// Route message to global handler if set
|
||||
if (globalMessageHandler) {
|
||||
globalMessageHandler(response);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing WebSocket message:", error);
|
||||
}
|
||||
});
|
||||
websocket.addEventListener("error", onError);
|
||||
Reference in New Issue
Block a user