4 Commits

27 changed files with 91 additions and 669 deletions
+1 -2
View File
@@ -39,9 +39,8 @@ When working with this project, follow these rules:
- NEVER create database migrations, they are auto-generated.
## Project Structure Awareness
- This is a React/TypeScript frontend with Python FastAPI backend
- This is a React/TypeScript browser frontend (desktop is Compose Multiplatform in the Android/KMP repo)
- Uses MDUI components for UI
- Has Electron support for desktop app
- Uses Zustand for state management
- Uses use-immer for immutable state updates
- Uses React Router for navigation
-1
View File
@@ -17,7 +17,6 @@ node_modules
npm-debug.log
.vite
dist
dist-electron
build
out
coverage
-171
View File
@@ -1,171 +0,0 @@
name: Build Electron app
on:
workflow_dispatch:
inputs:
apiBaseUrl:
description: "API base URL"
required: false
default: "fromchat.ru"
type: string
push:
tags: ["v**"]
paths:
- 'frontend/**'
- 'package.json'
- '.github/workflows/build.yml'
concurrency:
group: build-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
build-linux:
name: Build Linux
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Cache dependencies
uses: actions/cache@v4
with:
path: node_modules
key: npm-ubuntu-latest-node-24-${{ hashFiles('package.json') }}
restore-keys: |
npm-ubuntu-latest-node-24-
- name: Install dependencies
run: npm install --ignore-scripts --no-audit --no-fund
- name: Cache Electron downloads
uses: actions/cache@v4
with:
path: |
~/.cache/electron
~/.cache/electron-builder
key: electron-ubuntu-latest-${{ hashFiles('package.json', 'package.json') }}
restore-keys: |
electron-ubuntu-latest-
- name: Build Electron app
shell: bash
env:
VITE_API_BASE_URL: ${{ inputs.apiBaseUrl }}
run: npm run build:electron
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: FromChat-linux
path: |
frontend/build/electron/forge/make/*/**
if-no-files-found: error
build-macos:
name: Build macOS
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Cache dependencies
uses: actions/cache@v4
with:
path: node_modules
key: npm-macos-latest-node-24-${{ hashFiles('package.json') }}
restore-keys: |
npm-macos-latest-node-24-
- name: Install dependencies
run: npm install --ignore-scripts --no-audit --no-fund
# - name: Cache Electron downloads
# uses: actions/cache@v4
# with:
# path: |
# ~/.cache/electron
# ~/.cache/electron-builder
# key: electron-macos-latest-${{ hashFiles('package.json') }}
# restore-keys: |
# electron-macos-latest-
- name: Build Electron app
shell: bash
env:
VITE_API_BASE_URL: ${{ inputs.apiBaseUrl }}
run: npm run build:electron
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: FromChat-macOS
path: |
frontend/build/electron/forge/make/zip/darwin/*/**.zip
if-no-files-found: error
build-windows:
name: Build Windows
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Force npm to use Bash
run: npm config set script-shell "C:\Program Files\Git\bin\bash.exe"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Cache dependencies
uses: actions/cache@v4
with:
path: node_modules
key: npm-windows-latest-node-24-${{ hashFiles('package.json') }}
restore-keys: |
npm-windows-latest-node-24-
- name: Install dependencies
run: npm install --ignore-scripts --no-audit --no-fund
- name: Cache Electron downloads
uses: actions/cache@v4
with:
path: |
~\AppData\Local\electron\Cache
~\AppData\Local\electron-builder\Cache
key: electron-windows-latest-${{ hashFiles('package.json') }}
restore-keys: |
electron-windows-latest-
- name: Build Electron app
shell: bash
env:
VITE_API_BASE_URL: ${{ inputs.apiBaseUrl }}
run: npm run build:electron
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: FromChat-windows
path: |
frontend/build/electron/forge/make/zip/win32/x64
if-no-files-found: error
+2
View File
@@ -599,3 +599,5 @@ backend/files
!scripts/offline_python_linux_amd64/venv_linux_amd64/venv/
!scripts/offline_python_linux_amd64/venv_linux_amd64/venv/lib
*.module.d.scss.ts
-33
View File
@@ -19,25 +19,6 @@
},
"isBackground": true
},
{
"label": "Frontend (Electron)",
"type": "shell",
"command": "npm run frontend:electron:dev",
"options": {
"cwd": "${workspaceFolder}"
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared"
},
"group": {
"kind": "build"
},
"isBackground": true
},
{
"label": "Web",
"dependsOn": ["Frontend (Web)"],
@@ -51,20 +32,6 @@
"focus": false,
"panel": "shared"
}
},
{
"label": "Electron",
"dependsOn": ["Frontend (Electron)"],
"dependsOrder": "parallel",
"group": {
"kind": "build"
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared"
}
}
]
}
+3 -8
View File
@@ -7,14 +7,14 @@
**Web client for FromChat messenger**
[🌐 Web Client](https://github.com/fromchat-messenger/web) • [🖥️ Backend](https://github.com/fromchat-messenger/backend) • [📱 Android](https://github.com/fromchat-messenger/android) • [🌍 Website](https://github.com/fromchat-messenger/site)
[🌐 Web Client](https://github.com/fromchat-messenger/web) • [🖥️ Backend](https://github.com/fromchat-messenger/backend) • [📱 Android / Desktop](https://github.com/fromchat-messenger/android) • [🌍 Website](https://github.com/fromchat-messenger/site)
</div>
---
## 📝 Description
FromChat Web is a React/TypeScript client (browser and Electron) for the FromChat server.
FromChat Web is a React/TypeScript browser client for the FromChat server. The desktop client is Compose Multiplatform in the [Android/KMP repository](https://github.com/fromchat-messenger/android).
**Note:** Landing pages and legal documents live in [fromchat-messenger/site](https://github.com/fromchat-messenger/site).
@@ -43,7 +43,6 @@ FromChat Web is a React/TypeScript client (browser and Electron) for the FromCha
- Device management
- WebSocket real-time updates
- Dark mode
- Optional Electron desktop build
---
@@ -58,7 +57,6 @@ FromChat Web is a React/TypeScript client (browser and Electron) for the FromCha
| Zustand + use-immer | state |
| Motion | animations |
| TweetNaCl.js | cryptography |
| Electron | desktop (optional) |
---
@@ -98,8 +96,6 @@ npm run frontend:dev # Vite on :8301
npm run frontend:typecheck # TypeScript
npm run frontend:build # typecheck + production build → build/normal
npm run frontend:preview # preview built frontend
npm run frontend:electron:dev # Electron + Vite
npm run frontend:electron:build # Electron package
```
### Project structure
@@ -114,7 +110,6 @@ web/
│ │ ├── state/ # Zustand stores
│ │ ├── utils/
│ │ └── css/ # SCSS (Material Design)
│ ├── electron/ # Electron main/preload
│ └── protocol/ # shared protocol (@fromchat/protocol)
├── plugins/ # Vite plugins
├── vite.config.ts
@@ -157,7 +152,7 @@ GNU Affero General Public License v3.0 — see [LICENSE](./LICENSE).
## 🔗 Related Repositories
- [Backend API](https://github.com/fromchat-messenger/backend)
- [Android Client](https://github.com/fromchat-messenger/android)
- [Android / Desktop (KMP)](https://github.com/fromchat-messenger/android)
- [Website](https://github.com/fromchat-messenger/site)
- [Deployment](https://github.com/fromchat-messenger/deployment)
+2 -7
View File
@@ -6,7 +6,7 @@ _Написано ИИ. Могут быть ошибки._
## 📝 Описание
Веб-клиент FromChat — React/TypeScript приложение (браузер и Electron) для работы с сервером FromChat.
Веб-клиент FromChat — React/TypeScript приложение для браузера. Десктоп-клиент — Compose Multiplatform в [Android/KMP репозитории](https://github.com/fromchat-messenger/android).
## Развернуть в 1 клик
@@ -23,7 +23,6 @@ docker run -d --restart always -p 8301:80 fromchat/web:latest
- Управление устройствами
- WebSocket для реал-тайма
- Тёмный режим
- Сборка Electron (опционально)
## 🏗️ Технологический стек
@@ -36,7 +35,6 @@ docker run -d --restart always -p 8301:80 fromchat/web:latest
| Zustand + use-immer | состояние |
| Motion | анимации |
| TweetNaCl.js | криптография |
| Electron | десктоп (опционально) |
## 🔧 Разработка
@@ -65,8 +63,6 @@ npm run frontend:dev # Vite на :8301
npm run frontend:typecheck # TypeScript
npm run frontend:build # typecheck + production build → build/normal
npm run frontend:preview # preview собранного фронта
npm run frontend:electron:dev # Electron + Vite
npm run frontend:electron:build # сборка Electron
```
### Структура проекта
@@ -81,7 +77,6 @@ web/
│ │ ├── state/ # Zustand stores
│ │ ├── utils/
│ │ └── css/ # SCSS (Material Design)
│ ├── electron/ # main/preload Electron
│ └── protocol/ # общий протокол (@fromchat/protocol)
├── plugins/ # Vite-плагины
├── vite.config.ts
@@ -112,6 +107,6 @@ GNU Affero General Public License v3.0 — см. [LICENSE](./LICENSE).
## 🔗 Связанные репозитории
- [Backend API](https://github.com/fromchat-messenger/backend)
- [Android Client](https://github.com/fromchat-messenger/android)
- [Android / Desktop (KMP)](https://github.com/fromchat-messenger/android)
- [Website](https://github.com/fromchat-messenger/site)
- [Deployment](https://github.com/fromchat-messenger/deployment)
+3 -25
View File
@@ -3,7 +3,6 @@
"private": true,
"version": "0.0.0",
"type": "module",
"main": "build/electron/core/main.js",
"description": "A 100% Open Source Messenger",
"license": "GPL-3.0",
"authors": "denis0001-dev",
@@ -11,38 +10,23 @@
"frontend:dev": "vite",
"frontend:typecheck": "tsc --project tsconfig.json",
"frontend:build": "npm run frontend:typecheck && vite build",
"frontend:electron:dev": "VITE_ELECTRON=true npm run frontend:dev",
"frontend:electron:build": "VITE_ELECTRON=true npm run frontend:build && rm -rf out && electron-forge make --force --arch arm64,x64",
"frontend:preview": "vite preview",
"frontend:dependencies": "npm install --ignore-scripts",
"frontend:clean": "rm -rf build",
"build:electron": "npm run frontend:electron:build",
"build": "npm run frontend:build && npm run build:electron",
"build": "npm run frontend:build",
"preview": "docker compose -f docker-compose.yml up --build --watch",
"preview:clean": "docker compose -f docker-compose.yml down -v --remove-orphans",
"prepare": "husky",
"install": "if [ ! -e .env ]; then cp .env.example .env; fi"
},
"files": [
"build/electron"
],
"devDependencies": {
"@electron-forge/cli": "^7.9.0",
"@electron-forge/maker-deb": "^7.9.0",
"@electron-forge/maker-dmg": "^7.9.0",
"@electron-forge/maker-rpm": "^7.9.0",
"@electron-forge/maker-squirrel": "^7.9.0",
"@electron-forge/maker-zip": "^7.9.0",
"@electron-forge/plugin-auto-unpack-natives": "^7.9.0",
"@electron-forge/plugin-fuses": "^7.9.0",
"@electron/fuses": "^1.0.0",
"@types/he": "^1.2.3",
"@types/node": "^26.1.1",
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.3",
"autoprefixer": "^10.4.21",
"dotenv-cli": "^11.0.0",
"electron": "^39.2.7",
"husky": "^9.1.7",
"postcss": "^8.5.6",
"rollup-plugin-visualizer": "^6.0.4",
@@ -50,14 +34,11 @@
"svgo": "^4.0.0",
"terser": "^5.44.0",
"typescript": "~5.9.2",
"vite": "^7.1.6",
"vite-plugin-electron": "^0.29.0",
"vite-plugin-electron-renderer": "^0.14.6",
"vite": "^8.2.1",
"vite-plugin-html": "^3.2.2",
"vite-plugin-sass-dts": "^1.3.34"
},
"dependencies": {
"electron-squirrel-startup": "^1.0.1",
"escape-string-regexp": "^5.0.0",
"he": "^1.2.0",
"idb": "^8.0.3",
@@ -70,8 +51,5 @@
"tweetnacl": "^1.0.3",
"use-immer": "^0.11.0",
"zustand": "^5.0.8"
},
"config": {
"forge": "src/electron/forge.config.ts"
}
}
-25
View File
@@ -1,25 +0,0 @@
export type Platform = "win32" | "darwin" | "linux"
export interface NotificationShowOptions {
title: string;
body: string;
icon?: string;
tag?: string;
}
export interface ElectronNotifications {
requestPermission: () => Promise<NotificationPermission>;
show: (options: NotificationShowOptions) => Promise<boolean>;
}
export interface ElectronInterface {
desktop: true,
platform: Platform,
notifications: ElectronNotifications
}
declare global {
interface Window {
electronInterface: ElectronInterface
}
}
-45
View File
@@ -1,45 +0,0 @@
import { FusesPlugin } from '@electron-forge/plugin-fuses';
import { FuseV1Options, FuseVersion } from '@electron/fuses';
import type { ForgeConfig } from '@electron-forge/shared-types';
export default {
packagerConfig: {
asar: true,
},
outDir: "build/electron/forge",
rebuildConfig: {},
makers: [
{
name: '@electron-forge/maker-zip',
config: {},
platforms: ['win32', 'darwin'],
},
{
name: '@electron-forge/maker-deb',
config: {},
platforms: ['linux'],
},
{
name: '@electron-forge/maker-rpm',
config: {},
platforms: ['linux'],
},
],
plugins: [
{
name: '@electron-forge/plugin-auto-unpack-natives',
config: {},
},
// Fuses are used to enable/disable various Electron functionality
// at package time, before code signing the application
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
}),
],
} satisfies ForgeConfig;
-65
View File
@@ -1,65 +0,0 @@
import { app, BrowserWindow, Notification, ipcMain } from 'electron';
import path from "path";
import type { NotificationShowOptions } from "./electron.d.ts";
let mainWindow: BrowserWindow | null = null;
app.whenReady().then(() => {
mainWindow = new BrowserWindow({
title: 'Main window',
minWidth: 800,
minHeight: 420,
webPreferences: {
preload: path.join(import.meta.dirname, "preload.mjs")
},
titleBarStyle: "hidden",
trafficLightPosition: {
x: 16 - 4,
y: 16 - 4
},
titleBarOverlay: process.platform !== "darwin"
});
if (process.env.VITE_DEV_SERVER_URL) {
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
} else {
mainWindow.loadFile('build/electron/dist/index.html');
}
// Handle notification permission requests
ipcMain.handle('request-notification-permission', async () => {
if (Notification.isSupported()) {
return 'granted';
}
return 'denied';
});
// Handle showing notifications
ipcMain.handle("show-notification", async (_event, options: NotificationShowOptions) => {
if (Notification.isSupported()) {
try {
const notification = new Notification({
title: options.title,
body: options.body,
icon: options.icon,
silent: false,
urgency: 'normal'
});
notification.on('click', () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
});
notification.show();
return true;
} catch (error) {
console.error('Error creating notification:', error);
return false;
}
}
return false;
});
});
-11
View File
@@ -1,11 +0,0 @@
import { contextBridge, ipcRenderer } from "electron";
import type { ElectronInterface, NotificationShowOptions, Platform } from "./electron.d.ts";
contextBridge.exposeInMainWorld("electronInterface", {
desktop: true,
platform: process.platform as Platform,
notifications: {
requestPermission: () => ipcRenderer.invoke("request-notification-permission"),
show: (options: NotificationShowOptions) => ipcRenderer.invoke("show-notification", options)
}
} satisfies ElectronInterface);
-2
View File
@@ -1,6 +1,5 @@
import { BrowserRouter, Routes, Route, useNavigate, useLocation, matchRoutes, Navigate, type RouteObject } from "react-router-dom";
import { AnimatePresence, motion } from "motion/react";
import { ElectronTitleBar } from "./Electron";
import { useUserStore } from "./state/user";
import { lazy, useEffect, useRef, useState } from "react";
import { parseProfileLink } from "./core/profileLinks";
@@ -133,7 +132,6 @@ export default function App() {
return authReady && (
<BrowserRouter>
<ElectronTitleBar />
<AlertDialogProvider />
<div id="main-wrapper">
<AnimatedRoutes />
-11
View File
@@ -1,11 +0,0 @@
import { PRODUCT_NAME } from "./core/config";
import { isElectron } from "./core/electron/electron";
export function ElectronTitleBar() {
return isElectron && (
<div id="electron-title-bar">
{window.electronInterface.platform == "darwin" && <div className="macos-padding"></div>}
<div id="window-title">{PRODUCT_NAME}</div>
</div>
)
}
+18 -7
View File
@@ -71,10 +71,10 @@ export async function checkAuth(token: string): Promise<CheckAuthResponse> {
}
/**
* Logs in a user with username and password
* Logs in a user with username and password (step-based auth).
*/
export async function login(request: LoginRequest): Promise<LoginResponse> {
const res = await fetch(`${API_BASE_URL}/login`, {
const res = await fetch(`${API_BASE_URL}/auth/steps/password`, {
method: "POST",
headers: getAuthHeaders(null, true),
body: JSON.stringify(request)
@@ -83,17 +83,28 @@ export async function login(request: LoginRequest): Promise<LoginResponse> {
const error = await res.json().catch(() => ({ detail: "Login failed" }));
throw new Error(error.detail || "Login failed");
}
return await res.json();
const data = await res.json();
if (data.status === "needs_register") {
throw new Error("Account not found");
}
return data;
}
/**
* Registers a new user
* Registers a new user (step-based auth; Yandex proof required when enabled on server).
*/
export async function register(request: RegisterRequest): Promise<LoginResponse> {
const res = await fetch(`${API_BASE_URL}/register`, {
export async function register(request: RegisterRequest & { registration_proof?: string }): Promise<LoginResponse> {
const res = await fetch(`${API_BASE_URL}/auth/steps/register/confirm`, {
method: "POST",
headers: getAuthHeaders(null, true),
body: JSON.stringify(request)
body: JSON.stringify({
display_name: request.display_name,
username: request.username,
password: request.password,
confirm_password: request.confirm_password,
bio: request.bio,
registration_proof: request.registration_proof,
})
});
if (!res.ok) {
const error = await res.json().catch(() => ({ detail: "Registration failed" }));
+32 -7
View File
@@ -70,11 +70,25 @@ export async function checkAuth(token: string): Promise<CheckAuthResponse> {
return await res.json();
}
export interface RegisterConfirmRequest {
display_name: string;
username: string;
password: string;
confirm_password: string;
bio?: string;
registration_proof?: string;
}
export interface AuthPasswordNeedsRegister {
status: "needs_register";
yandex_required: boolean;
}
/**
* Logs in a user with username and password
* Logs in a user with username and password (step-based auth).
*/
export async function login(request: LoginRequest): Promise<LoginResponse> {
const res = await fetch(`${API_BASE_URL}/login`, {
const res = await fetch(`${API_BASE_URL}/auth/steps/password`, {
method: "POST",
headers: getAuthHeaders(null, true),
body: JSON.stringify(request)
@@ -83,17 +97,28 @@ export async function login(request: LoginRequest): Promise<LoginResponse> {
const error = await res.json().catch(() => ({ detail: "Login failed" }));
throw new Error(error.detail || "Login failed");
}
return await res.json();
const data = await res.json();
if (data.status === "needs_register") {
throw new Error("Account not found");
}
return data;
}
/**
* Registers a new user
* Registers a new user (step-based auth; Yandex proof required when enabled on server).
*/
export async function register(request: RegisterRequest): Promise<LoginResponse> {
const res = await fetch(`${API_BASE_URL}/register`, {
export async function register(request: RegisterRequest & { registration_proof?: string }): Promise<LoginResponse> {
const res = await fetch(`${API_BASE_URL}/auth/steps/register/confirm`, {
method: "POST",
headers: getAuthHeaders(null, true),
body: JSON.stringify(request)
body: JSON.stringify({
display_name: request.display_name,
username: request.username,
password: request.password,
confirm_password: request.confirm_password,
bio: request.bio,
registration_proof: request.registration_proof,
})
});
if (!res.ok) {
const error = await res.json().catch(() => ({ detail: "Registration failed" }));
-41
View File
@@ -1,41 +0,0 @@
@use "../../css/material" as *;
#electron-title-bar {
display: none;
}
html.electron {
#electron-title-bar {
display: flex;
flex-direction: row;
gap: 8px;
min-height: 40px;
background-color: $color-dark-surface-container;
width: 100%;
-webkit-app-region: drag;
user-select: none;
z-index: 10;
transition: background-color 0.5s ease;
flex-shrink: 0;
&.color-surface {
background-color: $color-dark-surface;
}
}
#window-title {
flex: 1;
display: flex;
align-items: center;
font-weight: 500;
}
#main-wrapper {
flex: 1;
min-height: 0;
}
&.platform-darwin .macos-padding {
width: 80px;
}
}
-17
View File
@@ -1,17 +0,0 @@
/**
* @fileoverview Electron-specific code
* @description This module initializes Electron-specific functionality.
* @author denis0001-dev
* @version 1.0.0
*/
import "./electron.scss";
export const isElectron = import.meta.env.VITE_ELECTRON && window.electronInterface != undefined;
if (isElectron) {
console.log("Running in Electron");
document.documentElement.classList.add("electron", `platform-${window.electronInterface.platform}`);
} else {
console.log("Running in normal browser");
}
+2 -1
View File
@@ -7,7 +7,8 @@
import { PRODUCT_NAME } from "./config";
import { enableMapSet } from "immer";
import type { Platform } from "../../electron/electron.d";
type Platform = "win32" | "darwin" | "linux";
function detectPlatform(): Platform {
const userAgent = navigator.userAgent.toLowerCase();
@@ -1,9 +1,5 @@
import api from "@/core/api";
import { isElectron } from "@/core/electron/electron";
import { websocket } from "@/core/websocket";
import type { NewMessageWebSocketMessage, WebSocketMessage } from "@/core/types";
import serviceWorker from "./service-worker?worker&url";
import logo from "@/images/logo.svg";
export interface PushSubscriptionData {
endpoint: string;
@@ -26,8 +22,6 @@ export interface NotificationPayload {
let isInitialized = false;
let registration: ServiceWorkerRegistration | null = null;
let subscription: PushSubscription | null = null;
let isElectronReceiverRunning = false;
let messageListener: ((event: MessageEvent) => void) | null = null;
// Helper functions
function urlBase64ToUint8Array(base64String: string): Uint8Array {
@@ -97,35 +91,6 @@ async function sendSubscriptionToServer(token: string): Promise<boolean> {
}
}
async function showMessageNotification(message: any): Promise<void> {
try {
await showNotification({
title: `New message from ${message.username}`,
body: message.content.length > 100
? message.content.substring(0, 100) + "..."
: message.content,
icon: message.profile_picture || logo,
tag: `message_${message.id}`,
data: {
type: "public_message",
message_id: message.id,
sender_id: message.user_id,
sender_username: message.username
}
});
} catch (error) {
console.error("Failed to show message notification:", error);
}
}
async function handleWebSocketMessage(response: WebSocketMessage<any>): Promise<void> {
// Handle notifications for new messages
if (response.type === "newMessage" && response.data) {
const newResponse = response as NewMessageWebSocketMessage;
await showMessageNotification(newResponse.data);
}
}
// Public API functions
export async function initialize(): Promise<boolean> {
if (isInitialized) {
@@ -133,13 +98,6 @@ export async function initialize(): Promise<boolean> {
}
try {
if (isElectron) {
// For Electron, we just need to request permission
const permission = await window.electronInterface.notifications.requestPermission();
isInitialized = permission === "granted";
return isInitialized;
} else {
// For web browsers, initialize service worker and push manager
if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
console.log("Push messaging is not supported");
return false;
@@ -159,7 +117,6 @@ export async function initialize(): Promise<boolean> {
console.error("Service Worker registration failed:", error);
return false;
}
}
} catch (error) {
console.error("Failed to initialize notification service:", error);
return false;
@@ -171,11 +128,6 @@ export async function subscribe(token: string): Promise<boolean> {
return false;
}
if (isElectron) {
// In Electron, we don't need server-side subscription
return true;
}
// If subscription doesn't exist, try to get it from the push manager or create a new one
if (!subscription && registration) {
try {
@@ -194,32 +146,12 @@ export async function subscribe(token: string): Promise<boolean> {
return await sendSubscriptionToServer(token);
}
export async function showNotification(payload: NotificationPayload): Promise<boolean> {
if (isElectron) {
try {
return await window.electronInterface.notifications.show({
title: payload.title,
body: payload.body,
icon: payload.icon,
tag: payload.tag
});
} catch (error) {
console.error("Failed to show Electron notification:", error);
return false;
}
}
// For web browsers, notifications are handled by the service worker
// when push messages are received from the server
export async function showNotification(_payload: NotificationPayload): Promise<boolean> {
// Notifications are handled by the service worker when push messages are received from the server
return false;
}
export async function unsubscribe(): Promise<boolean> {
if (isElectron) {
// In Electron, we don't need to unsubscribe from server
return true;
}
if (!subscription) {
return true;
}
@@ -235,43 +167,5 @@ export async function unsubscribe(): Promise<boolean> {
}
export function isSupported(): boolean {
if (isElectron) {
return true; // Electron always supports notifications
}
return "serviceWorker" in navigator && "PushManager" in window;
}
// Electron-specific functions
export async function startElectronReceiver(): Promise<void> {
if (!isElectron || isElectronReceiverRunning) {
return;
}
isElectronReceiverRunning = true;
// Add our own message listener to the existing WebSocket
messageListener = (event: MessageEvent) => {
try {
const response: WebSocketMessage<any> = JSON.parse(event.data);
handleWebSocketMessage(response);
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};
websocket.addEventListener('message', messageListener);
}
export function stopElectronReceiver(): void {
if (!isElectron) {
return;
}
isElectronReceiverRunning = false;
// Remove our message listener
if (messageListener) {
websocket.removeEventListener('message', messageListener);
messageListener = null;
}
}
+1
View File
@@ -182,6 +182,7 @@ export interface RegisterRequest {
display_name: string;
password: string;
confirm_password: string;
bio?: string;
}
export interface UploadPublicKeyRequest {
-1
View File
@@ -9,7 +9,6 @@ import './css/style.scss';
import "./utils/material";
import "./core/init";
import "./core/electron/electron";
import { createRoot } from 'react-dom/client';
import App from './App';
import { StrictMode } from 'react';
+1 -7
View File
@@ -7,8 +7,7 @@ import { useUserStore } from "@/state/user";
import { MaterialButton } from "@/utils/material";
import api from "@/core/api";
import { AuthTextField, type AuthTextFieldHandle } from "./AuthTextField";
import { initialize, isSupported, startElectronReceiver, subscribe } from "@/core/push-notifications/push-notifications";
import { isElectron } from "@/core/electron/electron";
import { initialize, isSupported, subscribe } from "@/core/push-notifications/push-notifications";
import type { Alert, AlertType } from "./Auth";
import { AuthHeader, AlertsContainer } from "./Auth";
import styles from "./auth.module.scss";
@@ -115,11 +114,6 @@ export function LoginForm({ onSwitchMode }: LoginFormProps) {
const initialized = await initialize();
if (initialized) {
await subscribe(data.token);
if (isElectron) {
await startElectronReceiver();
}
console.log("Notifications enabled");
} else {
console.log("Notification permission denied");
@@ -2,7 +2,6 @@ import { useState, useRef } from "react";
import { MaterialList, MaterialListItem, MaterialSwitch, type MDUISwitch } from "@/utils/material";
import { useUserStore } from "@/state/user";
import { initialize, subscribe, unsubscribe, isSupported } from "@/core/push-notifications/push-notifications";
import { isElectron } from "@/core/electron/electron";
import api from "@/core/api";
import styles from "@/pages/chat/css/settings-dialog.module.scss";
@@ -23,12 +22,7 @@ export function NotificationsPanel() {
setChecking(true);
try {
let permission: string;
if (isElectron) {
permission = await window.electronInterface.notifications.requestPermission();
} else {
permission = Notification.permission;
}
const permission = Notification.permission;
console.log("checkPushStatus", permission);
setPushEnabled(permission === "granted");
} catch (error) {
+1 -6
View File
@@ -2,8 +2,7 @@ import { create } from "zustand";
import type { User } from "@/core/types";
import api from "@/core/api";
import { API_BASE_URL } from "@/core/config";
import { initialize, subscribe, startElectronReceiver, isSupported } from "@/core/push-notifications/push-notifications";
import { isElectron } from "@/core/electron/electron";
import { initialize, subscribe, isSupported } from "@/core/push-notifications/push-notifications";
import { onlineStatusManager } from "@/core/onlineStatusManager";
import { typingManager } from "@/core/typingManager";
import type { UserState } from "./types";
@@ -126,10 +125,6 @@ export const useUserStore = create<UserStore>((set) => ({
const initialized = await initialize();
if (initialized) {
await subscribe(token);
if (isElectron) {
await startElectronReceiver();
}
}
}
} catch (e) {
+1 -1
View File
@@ -31,6 +31,6 @@
"jsx": "react-jsx",
"jsxImportSource": "react"
},
"include": ["src", "src/electron/electron.d.ts", "src/protocol"],
"include": ["src", "src/protocol"],
"exclude": ["**/__*/**", "__*"]
}
+1 -40
View File
@@ -1,7 +1,6 @@
import { defineConfig, PluginOption } from "vite";
import { createHtmlPlugin } from "vite-plugin-html";
import autoprefixer from "autoprefixer";
import electron from "vite-plugin-electron/simple";
import react from "@vitejs/plugin-react";
import path from "path";
import { visualizer } from 'rollup-plugin-visualizer';
@@ -10,7 +9,7 @@ import { optimizeCssModules } from './plugins/optimizeCssModules';
import { optimizeSvg } from './plugins/optimizeSvg';
const currentDir = path.resolve(__dirname);
const outDir = process.env.VITE_ELECTRON ? `${currentDir}/build/electron` : `${currentDir}/build/normal`;
const outDir = `${currentDir}/build/normal`;
const plugins: PluginOption[] = [
react({
@@ -42,34 +41,6 @@ const plugins: PluginOption[] = [
})
]
if (process.env.VITE_ELECTRON) {
plugins.push(
electron({
main: {
entry: "src/electron/main.ts",
vite: {
build: {
outDir: `${outDir}/core`
}
}
},
preload: {
input: "src/electron/preload.ts",
vite: {
build: {
outDir: `${outDir}/core`
}
}
},
renderer: {},
})
);
}
const apiProxyTarget = process.env.VITE_API_BASE_URL?.startsWith("http")
? process.env.VITE_API_BASE_URL
: "http://127.0.0.1:8300";
export default defineConfig({
root: path.resolve(__dirname, "src"),
envDir: currentDir,
@@ -80,20 +51,10 @@ export default defineConfig({
"@fromchat/protocol": path.resolve(__dirname, "./src/protocol/index.ts")
}
},
// Dev entry on 8301: browser uses same-origin `/api` (HTTP + WebSocket, e.g. `/api/chat/ws`).
server: {
host: '0.0.0.0',
port: 8301,
strictPort: true,
proxy: {
"/api": {
target: apiProxyTarget,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
ws: true,
proxyTimeout: 0,
}
},
allowedHosts: ["beta.fromchat.ru"]
},
appType: "spa",