commit 87c722b5ae30897aab72b0f49196d67a27eca46a Author: Myx Date: Sun Dec 28 05:37:19 2025 +0100 init diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f166060 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single +ij_typescript_use_double_quotes = false + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8005f1d --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log +dist-electron +node_modules/* +*server/node_modules/* +*package-lock.json +.angular +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings +__screenshots__/ + +# System files +.DS_Store +Thumbs.db diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..77b3745 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": ["angular.ng-template"] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..925af83 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ng serve", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: start", + "url": "http://localhost:4200/" + }, + { + "name": "ng test", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: test", + "url": "http://localhost:9876/debug.html" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..244306f --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "Changes detected" + }, + "endsPattern": { + "regexp": "bundle generation (complete|failed)" + } + } + } + }, + { + "type": "npm", + "script": "test", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "Changes detected" + }, + "endsPattern": { + "regexp": "bundle generation (complete|failed)" + } + } + } + } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..e5d8355 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# Client + +This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 21.0.4. + +## Development server + +To start a local development server, run: + +```bash +ng serve +``` + +Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. + +## Code scaffolding + +Angular CLI includes powerful code scaffolding tools. To generate a new component, run: + +```bash +ng generate component component-name +``` + +For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run: + +```bash +ng generate --help +``` + +## Building + +To build the project run: + +```bash +ng build +``` + +This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed. + +## Running unit tests + +To execute unit tests with the [Vitest](https://vitest.dev/) test runner, use the following command: + +```bash +ng test +``` + +## Running end-to-end tests + +For end-to-end (e2e) testing, run: + +```bash +ng e2e +``` + +Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. + +## Additional Resources + +For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. diff --git a/angular.json b/angular.json new file mode 100644 index 0000000..ad43434 --- /dev/null +++ b/angular.json @@ -0,0 +1,102 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "npm", + "analytics": false + }, + "newProjectRoot": "projects", + "projects": { + "client": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss", + "skipTests": true + }, + "@schematics/angular:class": { + "skipTests": true + }, + "@schematics/angular:directive": { + "skipTests": true + }, + "@schematics/angular:guard": { + "skipTests": true + }, + "@schematics/angular:interceptor": { + "skipTests": true + }, + "@schematics/angular:pipe": { + "skipTests": true + }, + "@schematics/angular:resolver": { + "skipTests": true + }, + "@schematics/angular:service": { + "skipTests": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.scss" + ], + "allowedCommonJsDependencies": [ + "simple-peer", + "uuid" + ] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "client:build:production" + }, + "development": { + "buildTarget": "client:build:development" + } + }, + "defaultConfiguration": "development" + } + } + } + } +} diff --git a/electron/main.js b/electron/main.js new file mode 100644 index 0000000..4713144 --- /dev/null +++ b/electron/main.js @@ -0,0 +1,85 @@ +const { app, BrowserWindow, ipcMain, desktopCapturer } = require('electron'); +const path = require('path'); + +let mainWindow; + +function createWindow() { + mainWindow = new BrowserWindow({ + width: 1400, + height: 900, + minWidth: 800, + minHeight: 600, + frame: false, + titleBarStyle: 'hidden', + backgroundColor: '#0a0a0f', + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + preload: path.join(__dirname, 'preload.js'), + webSecurity: true, + }, + }); + + // In development, load from Angular dev server + if (process.env.NODE_ENV === 'development') { + mainWindow.loadURL('http://localhost:4200'); + mainWindow.webContents.openDevTools(); + } else { + // In production, load the built Angular app + // The dist folder is at the project root, not in electron folder + mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'client', 'browser', 'index.html')); + } + + mainWindow.on('closed', () => { + mainWindow = null; + }); +} + +app.whenReady().then(createWindow); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } +}); + +// IPC handlers for window controls +ipcMain.on('window-minimize', () => { + mainWindow?.minimize(); +}); + +ipcMain.on('window-maximize', () => { + if (mainWindow?.isMaximized()) { + mainWindow.unmaximize(); + } else { + mainWindow?.maximize(); + } +}); + +ipcMain.on('window-close', () => { + mainWindow?.close(); +}); + +// IPC handler for desktop capturer (screen sharing) +ipcMain.handle('get-sources', async () => { + const sources = await desktopCapturer.getSources({ + types: ['window', 'screen'], + thumbnailSize: { width: 150, height: 150 }, + }); + return sources.map((source) => ({ + id: source.id, + name: source.name, + thumbnail: source.thumbnail.toDataURL(), + })); +}); + +// IPC handler for app data path +ipcMain.handle('get-app-data-path', () => { + return app.getPath('userData'); +}); diff --git a/electron/preload.js b/electron/preload.js new file mode 100644 index 0000000..e9240d0 --- /dev/null +++ b/electron/preload.js @@ -0,0 +1,19 @@ +const { contextBridge, ipcRenderer } = require('electron'); + +contextBridge.exposeInMainWorld('electronAPI', { + // Window controls + minimizeWindow: () => ipcRenderer.send('window-minimize'), + maximizeWindow: () => ipcRenderer.send('window-maximize'), + closeWindow: () => ipcRenderer.send('window-close'), + + // Desktop capturer for screen sharing + getSources: () => ipcRenderer.invoke('get-sources'), + + // App data path for SQLite storage + getAppDataPath: () => ipcRenderer.invoke('get-app-data-path'), + + // File system operations for database persistence + readFile: (filePath) => ipcRenderer.invoke('read-file', filePath), + writeFile: (filePath, data) => ipcRenderer.invoke('write-file', filePath, data), + fileExists: (filePath) => ipcRenderer.invoke('file-exists', filePath), +}); diff --git a/instructions.md b/instructions.md new file mode 100644 index 0000000..5252476 --- /dev/null +++ b/instructions.md @@ -0,0 +1,335 @@ +# User Stories for P2P Chat Application + +> **Reference:** [webrtc.org](https://webrtc.org) + +The following user stories describe a peer-to-peer chat and voice application (Discord-like) built with **Electron** and **Angular** (using RxJS and NgRx) with a local SQLite store (via SQL.js). A central server only provides a directory of active rooms and users; all message and audio/video data is exchanged directly between peers (no port forwarding needed). Stories are grouped by role and include acceptance criteria and technical notes. + +--- + +## Regular User + +### Search and Join Chat Server + +**User Story:** As a Regular User, I want to search for available chat servers (rooms) by name or topic so that I can find and join active communities. + +**Acceptance Criteria:** +- A search input allows queries against the central directory of servers. +- Matching servers are returned and listed with details (name, current user count, etc.). +- I can select a server from the list and send a join request. +- If the server exists and is active, I am connected to that server's peer network. +- Error is shown if the server is not found or unavailable. + +**Technical Notes:** +- Use Angular's `HttpClient` to call the central REST API (e.g. `GET /servers?search={query}`), which returns an Observable. +- Use RxJS operators (e.g. `debounceTime`) to handle user input. +- Use Angular components/forms for the search UI and display results. Use `async` pipe in templates for Observables. +- Store search results and selected server info in an NgRx store or Angular Signals to manage state and reactively update UI. + +--- + +### Create Chatroom (Become Host) + +**User Story:** As a Regular User, I want to create a new chatroom so that I can start my own voice/text server; I should automatically become the room's host. + +**Acceptance Criteria:** +- I can create a named room; the central server registers the new room. +- I join the newly created room and see myself listed as the host/owner. +- The host can set an optional password or topic for the room. +- Other users searching for rooms see this new room available. +- If I disconnect without others online, the room is removed from the server directory. + +**Technical Notes:** +- Use Angular forms to input room details and NgRx actions to handle creation. Dispatch an action that posts to the central server API (e.g. `POST /servers`) and upon success updates the local store. +- In the NgRx state, mark the creator as `host: true`. Follow NgRx best practices (immutable state, action-driven updates). +- No code style notes here beyond following lint rules (e.g. `prefer const`, `no var`, 2-space indent). +- After creation, initiate the P2P mesh: the host opens listening sockets (WebRTC or TCP sockets) and waits for peers (see "P2P Connectivity" story). + +--- + +### Join Existing Chatroom + +**User Story:** As a Regular User, I want to join an existing chatroom so that I can participate in it. + +**Acceptance Criteria:** +- I can select an available room and request to join. +- The host is notified of my request and (if approved) I connect to the room. +- If no approval step, I immediately join and synchronize chat history and participants. +- Upon joining, I receive the current chat history (text and possibly voice stream) from one or more peers. + +**Technical Notes:** +- Use Angular Router or services to handle navigation/joining logic. +- When joining, use a WebRTC signaling channel (via the central server or direct peer signaling) to exchange ICE candidates (STUN/TURN). +- Once connected, use WebRTC DataChannels for chat data and Streams for audio/video. +- On the host side, use NgRx Effects to handle incoming join events and broadcast initial state (messages and user list) to new peer. + +--- + +### Send, Edit, Delete, and React to Messages + +**User Story:** As a Regular User, I want to send text messages, and edit or delete my own messages (and react to any message), so that I can communicate effectively and correct mistakes. + +**Acceptance Criteria:** +- I can type and send a chat message, which is immediately delivered to all peers. +- I can edit or delete any of my own previously sent messages; these edits/deletions update on all peers in real time. +- I can react (e.g. add emoji) to any message; reactions are displayed next to the message for all users. +- All message operations (send/edit/delete/react) are performed instantly (optimistic UI) and confirmed or synchronized with peers. +- Deleted messages are removed from the UI for all users. + +**Technical Notes:** +- Use WebRTC `RTCDataChannel` (via a library like `simple-peer` or native API) for sending JSON-encoded message events between peers. Data channels provide reliable, ordered delivery for text. +- Maintain a local message store (NgRx store or Signals) that holds all chat messages and reactions. Update the store immutably and broadcast updates to other peers. +- Persist messages locally using SQL.js: run SQLite in-memory and after significant changes call `db.export()` to save state (or write to a file via Electron). On join, import/export to synchronize history (SQL.js can load an existing `Uint8Array` DB). +- Implement send/edit/delete/react actions and reducers/effects in NgRx. Ensure all reducers treat state immutably and use NgRx Effects for any asynchronous broadcasts. +- Follow lint rules for event handlers and functions (`no-unused-vars`, consistent naming). + +--- + +### Voice Chat (Push-to-Talk) + +**User Story:** As a Regular User, I want to join a voice channel within the chatroom so that I can talk to other participants (e.g. using push-to-talk or a switch). + +**Acceptance Criteria:** +- I can enable my microphone and connect to the voice channel. +- Other users hear my voice with low latency, and I hear theirs. +- Only one person needs to be host; no central server relaying audio. +- If I mute or leave, others no longer hear me. +- Voice quality adapts to network (e.g. uses Opus codec). + +**Technical Notes:** +- Use WebRTC MediaStreams for audio: call `navigator.mediaDevices.getUserMedia({ audio: true })` to capture microphone audio. Add this audio track to the peer connection. +- On receiving side, use a `