mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Compare commits
6 Commits
@@ -39,9 +39,8 @@ When working with this project, follow these rules:
|
|||||||
- NEVER create database migrations, they are auto-generated.
|
- NEVER create database migrations, they are auto-generated.
|
||||||
|
|
||||||
## Project Structure Awareness
|
## 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
|
- Uses MDUI components for UI
|
||||||
- Has Electron support for desktop app
|
|
||||||
- Uses Zustand for state management
|
- Uses Zustand for state management
|
||||||
- Uses use-immer for immutable state updates
|
- Uses use-immer for immutable state updates
|
||||||
- Uses React Router for navigation
|
- Uses React Router for navigation
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ node_modules
|
|||||||
npm-debug.log
|
npm-debug.log
|
||||||
.vite
|
.vite
|
||||||
dist
|
dist
|
||||||
dist-electron
|
|
||||||
build
|
build
|
||||||
out
|
out
|
||||||
coverage
|
coverage
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -599,3 +599,5 @@ backend/files
|
|||||||
|
|
||||||
!scripts/offline_python_linux_amd64/venv_linux_amd64/venv/
|
!scripts/offline_python_linux_amd64/venv_linux_amd64/venv/
|
||||||
!scripts/offline_python_linux_amd64/venv_linux_amd64/venv/lib
|
!scripts/offline_python_linux_amd64/venv_linux_amd64/venv/lib
|
||||||
|
|
||||||
|
*.module.d.scss.ts
|
||||||
Vendored
-33
@@ -19,25 +19,6 @@
|
|||||||
},
|
},
|
||||||
"isBackground": true
|
"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",
|
"label": "Web",
|
||||||
"dependsOn": ["Frontend (Web)"],
|
"dependsOn": ["Frontend (Web)"],
|
||||||
@@ -51,20 +32,6 @@
|
|||||||
"focus": false,
|
"focus": false,
|
||||||
"panel": "shared"
|
"panel": "shared"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Electron",
|
|
||||||
"dependsOn": ["Frontend (Electron)"],
|
|
||||||
"dependsOrder": "parallel",
|
|
||||||
"group": {
|
|
||||||
"kind": "build"
|
|
||||||
},
|
|
||||||
"presentation": {
|
|
||||||
"echo": true,
|
|
||||||
"reveal": "always",
|
|
||||||
"focus": false,
|
|
||||||
"panel": "shared"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+3
-8
@@ -7,14 +7,14 @@
|
|||||||
|
|
||||||
**Web client for FromChat messenger**
|
**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>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📝 Description
|
## 📝 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).
|
**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
|
- Device management
|
||||||
- WebSocket real-time updates
|
- WebSocket real-time updates
|
||||||
- Dark mode
|
- 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 |
|
| Zustand + use-immer | state |
|
||||||
| Motion | animations |
|
| Motion | animations |
|
||||||
| TweetNaCl.js | cryptography |
|
| 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:typecheck # TypeScript
|
||||||
npm run frontend:build # typecheck + production build → build/normal
|
npm run frontend:build # typecheck + production build → build/normal
|
||||||
npm run frontend:preview # preview built frontend
|
npm run frontend:preview # preview built frontend
|
||||||
npm run frontend:electron:dev # Electron + Vite
|
|
||||||
npm run frontend:electron:build # Electron package
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Project structure
|
### Project structure
|
||||||
@@ -114,7 +110,6 @@ web/
|
|||||||
│ │ ├── state/ # Zustand stores
|
│ │ ├── state/ # Zustand stores
|
||||||
│ │ ├── utils/
|
│ │ ├── utils/
|
||||||
│ │ └── css/ # SCSS (Material Design)
|
│ │ └── css/ # SCSS (Material Design)
|
||||||
│ ├── electron/ # Electron main/preload
|
|
||||||
│ └── protocol/ # shared protocol (@fromchat/protocol)
|
│ └── protocol/ # shared protocol (@fromchat/protocol)
|
||||||
├── plugins/ # Vite plugins
|
├── plugins/ # Vite plugins
|
||||||
├── vite.config.ts
|
├── vite.config.ts
|
||||||
@@ -157,7 +152,7 @@ GNU Affero General Public License v3.0 — see [LICENSE](./LICENSE).
|
|||||||
## 🔗 Related Repositories
|
## 🔗 Related Repositories
|
||||||
|
|
||||||
- [Backend API](https://github.com/fromchat-messenger/backend)
|
- [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)
|
- [Website](https://github.com/fromchat-messenger/site)
|
||||||
- [Deployment](https://github.com/fromchat-messenger/deployment)
|
- [Deployment](https://github.com/fromchat-messenger/deployment)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ _Написано ИИ. Могут быть ошибки._
|
|||||||
|
|
||||||
## 📝 Описание
|
## 📝 Описание
|
||||||
|
|
||||||
Веб-клиент FromChat — React/TypeScript приложение (браузер и Electron) для работы с сервером FromChat.
|
Веб-клиент FromChat — React/TypeScript приложение для браузера. Десктоп-клиент — Compose Multiplatform в [Android/KMP репозитории](https://github.com/fromchat-messenger/android).
|
||||||
|
|
||||||
## Развернуть в 1 клик
|
## Развернуть в 1 клик
|
||||||
|
|
||||||
@@ -23,7 +23,6 @@ docker run -d --restart always -p 8301:80 fromchat/web:latest
|
|||||||
- Управление устройствами
|
- Управление устройствами
|
||||||
- WebSocket для реал-тайма
|
- WebSocket для реал-тайма
|
||||||
- Тёмный режим
|
- Тёмный режим
|
||||||
- Сборка Electron (опционально)
|
|
||||||
|
|
||||||
## 🏗️ Технологический стек
|
## 🏗️ Технологический стек
|
||||||
|
|
||||||
@@ -36,7 +35,6 @@ docker run -d --restart always -p 8301:80 fromchat/web:latest
|
|||||||
| Zustand + use-immer | состояние |
|
| Zustand + use-immer | состояние |
|
||||||
| Motion | анимации |
|
| Motion | анимации |
|
||||||
| TweetNaCl.js | криптография |
|
| TweetNaCl.js | криптография |
|
||||||
| Electron | десктоп (опционально) |
|
|
||||||
|
|
||||||
## 🔧 Разработка
|
## 🔧 Разработка
|
||||||
|
|
||||||
@@ -65,8 +63,6 @@ npm run frontend:dev # Vite на :8301
|
|||||||
npm run frontend:typecheck # TypeScript
|
npm run frontend:typecheck # TypeScript
|
||||||
npm run frontend:build # typecheck + production build → build/normal
|
npm run frontend:build # typecheck + production build → build/normal
|
||||||
npm run frontend:preview # preview собранного фронта
|
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
|
│ │ ├── state/ # Zustand stores
|
||||||
│ │ ├── utils/
|
│ │ ├── utils/
|
||||||
│ │ └── css/ # SCSS (Material Design)
|
│ │ └── css/ # SCSS (Material Design)
|
||||||
│ ├── electron/ # main/preload Electron
|
|
||||||
│ └── protocol/ # общий протокол (@fromchat/protocol)
|
│ └── protocol/ # общий протокол (@fromchat/protocol)
|
||||||
├── plugins/ # Vite-плагины
|
├── plugins/ # Vite-плагины
|
||||||
├── vite.config.ts
|
├── vite.config.ts
|
||||||
@@ -112,6 +107,6 @@ GNU Affero General Public License v3.0 — см. [LICENSE](./LICENSE).
|
|||||||
## 🔗 Связанные репозитории
|
## 🔗 Связанные репозитории
|
||||||
|
|
||||||
- [Backend API](https://github.com/fromchat-messenger/backend)
|
- [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)
|
- [Website](https://github.com/fromchat-messenger/site)
|
||||||
- [Deployment](https://github.com/fromchat-messenger/deployment)
|
- [Deployment](https://github.com/fromchat-messenger/deployment)
|
||||||
+3
-25
@@ -3,7 +3,6 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "build/electron/core/main.js",
|
|
||||||
"description": "A 100% Open Source Messenger",
|
"description": "A 100% Open Source Messenger",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"authors": "denis0001-dev",
|
"authors": "denis0001-dev",
|
||||||
@@ -11,38 +10,23 @@
|
|||||||
"frontend:dev": "vite",
|
"frontend:dev": "vite",
|
||||||
"frontend:typecheck": "tsc --project tsconfig.json",
|
"frontend:typecheck": "tsc --project tsconfig.json",
|
||||||
"frontend:build": "npm run frontend:typecheck && vite build",
|
"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:preview": "vite preview",
|
||||||
"frontend:dependencies": "npm install --ignore-scripts",
|
"frontend:dependencies": "npm install --ignore-scripts",
|
||||||
"frontend:clean": "rm -rf build",
|
"frontend:clean": "rm -rf build",
|
||||||
"build:electron": "npm run frontend:electron:build",
|
"build": "npm run frontend:build",
|
||||||
"build": "npm run frontend:build && npm run build:electron",
|
|
||||||
"preview": "docker compose -f docker-compose.yml up --build --watch",
|
"preview": "docker compose -f docker-compose.yml up --build --watch",
|
||||||
"preview:clean": "docker compose -f docker-compose.yml down -v --remove-orphans",
|
"preview:clean": "docker compose -f docker-compose.yml down -v --remove-orphans",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"install": "if [ ! -e .env ]; then cp .env.example .env; fi"
|
"install": "if [ ! -e .env ]; then cp .env.example .env; fi"
|
||||||
},
|
},
|
||||||
"files": [
|
|
||||||
"build/electron"
|
|
||||||
],
|
|
||||||
"devDependencies": {
|
"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/he": "^1.2.3",
|
||||||
|
"@types/node": "^26.1.1",
|
||||||
"@types/react": "^19.1.13",
|
"@types/react": "^19.1.13",
|
||||||
"@types/react-dom": "^19.1.9",
|
"@types/react-dom": "^19.1.9",
|
||||||
"@vitejs/plugin-react": "^5.0.3",
|
"@vitejs/plugin-react": "^5.0.3",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
"electron": "^39.2.7",
|
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"rollup-plugin-visualizer": "^6.0.4",
|
"rollup-plugin-visualizer": "^6.0.4",
|
||||||
@@ -51,27 +35,21 @@
|
|||||||
"terser": "^5.44.0",
|
"terser": "^5.44.0",
|
||||||
"typescript": "~5.9.2",
|
"typescript": "~5.9.2",
|
||||||
"vite": "^7.1.6",
|
"vite": "^7.1.6",
|
||||||
"vite-plugin-electron": "^0.29.0",
|
|
||||||
"vite-plugin-electron-renderer": "^0.14.6",
|
|
||||||
"vite-plugin-html": "^3.2.2",
|
"vite-plugin-html": "^3.2.2",
|
||||||
"vite-plugin-sass-dts": "^1.3.34"
|
"vite-plugin-sass-dts": "^1.3.34"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
|
||||||
"escape-string-regexp": "^5.0.0",
|
"escape-string-regexp": "^5.0.0",
|
||||||
"he": "^1.2.0",
|
"he": "^1.2.0",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"marked": "^17.0.1",
|
"marked": "^17.0.1",
|
||||||
"mdui": "^2.1.4",
|
"mdui": "^2.1.4",
|
||||||
"motion": "^12.23.24",
|
"motion": "^13.0.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-router-dom": "^7.9.3",
|
"react-router-dom": "^7.9.3",
|
||||||
"tweetnacl": "^1.0.3",
|
"tweetnacl": "^1.0.3",
|
||||||
"use-immer": "^0.11.0",
|
"use-immer": "^0.11.0",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.8"
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"forge": "src/electron/forge.config.ts"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
-25
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { BrowserRouter, Routes, Route, useNavigate, useLocation, matchRoutes, Navigate, type RouteObject } from "react-router-dom";
|
import { BrowserRouter, Routes, Route, useNavigate, useLocation, matchRoutes, Navigate, type RouteObject } from "react-router-dom";
|
||||||
import { AnimatePresence, motion } from "motion/react";
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
import { ElectronTitleBar } from "./Electron";
|
|
||||||
import { useUserStore } from "./state/user";
|
import { useUserStore } from "./state/user";
|
||||||
import { lazy, useEffect, useRef, useState } from "react";
|
import { lazy, useEffect, useRef, useState } from "react";
|
||||||
import { parseProfileLink } from "./core/profileLinks";
|
import { parseProfileLink } from "./core/profileLinks";
|
||||||
@@ -133,7 +132,6 @@ export default function App() {
|
|||||||
|
|
||||||
return authReady && (
|
return authReady && (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<ElectronTitleBar />
|
|
||||||
<AlertDialogProvider />
|
<AlertDialogProvider />
|
||||||
<div id="main-wrapper">
|
<div id="main-wrapper">
|
||||||
<AnimatedRoutes />
|
<AnimatedRoutes />
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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> {
|
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",
|
method: "POST",
|
||||||
headers: getAuthHeaders(null, true),
|
headers: getAuthHeaders(null, true),
|
||||||
body: JSON.stringify(request)
|
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" }));
|
const error = await res.json().catch(() => ({ detail: "Login failed" }));
|
||||||
throw new Error(error.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> {
|
export async function register(request: RegisterRequest & { registration_proof?: string }): Promise<LoginResponse> {
|
||||||
const res = await fetch(`${API_BASE_URL}/register`, {
|
const res = await fetch(`${API_BASE_URL}/auth/steps/register/confirm`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: getAuthHeaders(null, true),
|
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) {
|
if (!res.ok) {
|
||||||
const error = await res.json().catch(() => ({ detail: "Registration failed" }));
|
const error = await res.json().catch(() => ({ detail: "Registration failed" }));
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { request } from "@/core/websocket";
|
|||||||
import type { DmEnvelope, User } from "@/core/types";
|
import type { DmEnvelope, User } from "@/core/types";
|
||||||
import { ub64 } from "@/utils/utils";
|
import { ub64 } from "@/utils/utils";
|
||||||
import { fetchUserPublicKey } from "../crypto/identity";
|
import { fetchUserPublicKey } from "../crypto/identity";
|
||||||
import { fetchUsers, searchUsers } from "../user/search";
|
import { searchUsers } from "../user/search";
|
||||||
import { deriveWrappingKey, importAesGcmKey, aesGcmDecrypt } from "@fromchat/protocol";
|
import { deriveWrappingKey, importAesGcmKey, aesGcmDecrypt } from "@fromchat/protocol";
|
||||||
import tweetnacl from "tweetnacl";
|
import tweetnacl from "tweetnacl";
|
||||||
|
|
||||||
@@ -320,4 +320,4 @@ export async function editMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Re-export user functions for convenience
|
// Re-export user functions for convenience
|
||||||
export { fetchUsers, searchUsers, fetchUserPublicKey };
|
export { searchUsers, fetchUserPublicKey };
|
||||||
@@ -3,7 +3,7 @@ import { getAuthHeaders } from "./account";
|
|||||||
import { request } from "@/core/websocket";
|
import { request } from "@/core/websocket";
|
||||||
import type { DmEnvelope, User } from "@/core/types";
|
import type { DmEnvelope, User } from "@/core/types";
|
||||||
import { fetchUserPublicKey } from "./crypto";
|
import { fetchUserPublicKey } from "./crypto";
|
||||||
import { fetchUsers, searchUsers } from "./users";
|
import { searchUsers } from "./users";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrypt a DM envelope using client-side MEK unwrapping.
|
* Decrypt a DM envelope using client-side MEK unwrapping.
|
||||||
@@ -25,7 +25,7 @@ export async function fetchDMHistory(userId: number, token: string, limit: numbe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Re-export user functions for convenience
|
// Re-export user functions for convenience
|
||||||
export { fetchUsers, searchUsers, fetchUserPublicKey };
|
export { searchUsers, fetchUserPublicKey };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send DM via WebSocket using transport encryption.
|
* Send DM via WebSocket using transport encryption.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getAuthHeaders } from "./account";
|
|||||||
import { request } from "@/core/websocket";
|
import { request } from "@/core/websocket";
|
||||||
import type { DmEnvelope, User } from "@/core/types";
|
import type { DmEnvelope, User } from "@/core/types";
|
||||||
import { fetchUserPublicKey } from "./crypto";
|
import { fetchUserPublicKey } from "./crypto";
|
||||||
import { fetchUsers, searchUsers } from "./users";
|
import { searchUsers } from "./users";
|
||||||
|
|
||||||
export async function decryptDm(envelope: DmEnvelope): Promise<string> {
|
export async function decryptDm(envelope: DmEnvelope): Promise<string> {
|
||||||
const { decrypt } = await import("./chats/dm");
|
const { decrypt } = await import("./chats/dm");
|
||||||
@@ -20,7 +20,7 @@ export async function fetchDMHistory(userId: number, token: string, limit: numbe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Re-export user functions for convenience
|
// Re-export user functions for convenience
|
||||||
export { fetchUsers, searchUsers, fetchUserPublicKey };
|
export { searchUsers, fetchUserPublicKey };
|
||||||
|
|
||||||
export async function sendDMViaWebSocket(recipientId: number, recipientPublicKeyB64: string, plaintext: string, authToken: string, replyToId?: number): Promise<void> {
|
export async function sendDMViaWebSocket(recipientId: number, recipientPublicKeyB64: string, plaintext: string, authToken: string, replyToId?: number): Promise<void> {
|
||||||
const { send } = await import("./chats/dm");
|
const { send } = await import("./chats/dm");
|
||||||
|
|||||||
@@ -70,11 +70,25 @@ export async function checkAuth(token: string): Promise<CheckAuthResponse> {
|
|||||||
return await res.json();
|
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> {
|
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",
|
method: "POST",
|
||||||
headers: getAuthHeaders(null, true),
|
headers: getAuthHeaders(null, true),
|
||||||
body: JSON.stringify(request)
|
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" }));
|
const error = await res.json().catch(() => ({ detail: "Login failed" }));
|
||||||
throw new Error(error.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> {
|
export async function register(request: RegisterRequest & { registration_proof?: string }): Promise<LoginResponse> {
|
||||||
const res = await fetch(`${API_BASE_URL}/register`, {
|
const res = await fetch(`${API_BASE_URL}/auth/steps/register/confirm`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: getAuthHeaders(null, true),
|
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) {
|
if (!res.ok) {
|
||||||
const error = await res.json().catch(() => ({ detail: "Registration failed" }));
|
const error = await res.json().catch(() => ({ detail: "Registration failed" }));
|
||||||
|
|||||||
@@ -2,16 +2,6 @@ import { API_BASE_URL } from "@/core/config";
|
|||||||
import { getAuthHeaders } from "./auth";
|
import { getAuthHeaders } from "./auth";
|
||||||
import type { User } from "@/core/types";
|
import type { User } from "@/core/types";
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches a list of all users (excluding current user)
|
|
||||||
*/
|
|
||||||
export async function fetchUsers(token: string): Promise<User[]> {
|
|
||||||
const res = await fetch(`${API_BASE_URL}/users`, { headers: getAuthHeaders(token, true) });
|
|
||||||
if (!res.ok) return [];
|
|
||||||
const data = await res.json();
|
|
||||||
return data.users || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Searches for users by username query
|
* Searches for users by username query
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,16 +2,6 @@ import { API_BASE_URL } from "@/core/config";
|
|||||||
import { getAuthHeaders } from "./account";
|
import { getAuthHeaders } from "./account";
|
||||||
import type { User } from "@/core/types";
|
import type { User } from "@/core/types";
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches a list of all users (excluding current user)
|
|
||||||
*/
|
|
||||||
export async function fetchUsers(token: string): Promise<User[]> {
|
|
||||||
const res = await fetch(`${API_BASE_URL}/users`, { headers: getAuthHeaders(token, true) });
|
|
||||||
if (!res.ok) return [];
|
|
||||||
const data = await res.json();
|
|
||||||
return data.users || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Searches for users by username query
|
* Searches for users by username query
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,8 @@
|
|||||||
|
|
||||||
import { PRODUCT_NAME } from "./config";
|
import { PRODUCT_NAME } from "./config";
|
||||||
import { enableMapSet } from "immer";
|
import { enableMapSet } from "immer";
|
||||||
import type { Platform } from "../../electron/electron.d";
|
|
||||||
|
type Platform = "win32" | "darwin" | "linux";
|
||||||
|
|
||||||
function detectPlatform(): Platform {
|
function detectPlatform(): Platform {
|
||||||
const userAgent = navigator.userAgent.toLowerCase();
|
const userAgent = navigator.userAgent.toLowerCase();
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import api from "@/core/api";
|
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 serviceWorker from "./service-worker?worker&url";
|
||||||
import logo from "@/images/logo.svg";
|
|
||||||
|
|
||||||
export interface PushSubscriptionData {
|
export interface PushSubscriptionData {
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
@@ -26,8 +22,6 @@ export interface NotificationPayload {
|
|||||||
let isInitialized = false;
|
let isInitialized = false;
|
||||||
let registration: ServiceWorkerRegistration | null = null;
|
let registration: ServiceWorkerRegistration | null = null;
|
||||||
let subscription: PushSubscription | null = null;
|
let subscription: PushSubscription | null = null;
|
||||||
let isElectronReceiverRunning = false;
|
|
||||||
let messageListener: ((event: MessageEvent) => void) | null = null;
|
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
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
|
// Public API functions
|
||||||
export async function initialize(): Promise<boolean> {
|
export async function initialize(): Promise<boolean> {
|
||||||
if (isInitialized) {
|
if (isInitialized) {
|
||||||
@@ -133,13 +98,6 @@ export async function initialize(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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)) {
|
if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
|
||||||
console.log("Push messaging is not supported");
|
console.log("Push messaging is not supported");
|
||||||
return false;
|
return false;
|
||||||
@@ -159,7 +117,6 @@ export async function initialize(): Promise<boolean> {
|
|||||||
console.error("Service Worker registration failed:", error);
|
console.error("Service Worker registration failed:", error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to initialize notification service:", error);
|
console.error("Failed to initialize notification service:", error);
|
||||||
return false;
|
return false;
|
||||||
@@ -171,11 +128,6 @@ export async function subscribe(token: string): Promise<boolean> {
|
|||||||
return false;
|
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 doesn't exist, try to get it from the push manager or create a new one
|
||||||
if (!subscription && registration) {
|
if (!subscription && registration) {
|
||||||
try {
|
try {
|
||||||
@@ -194,32 +146,12 @@ export async function subscribe(token: string): Promise<boolean> {
|
|||||||
return await sendSubscriptionToServer(token);
|
return await sendSubscriptionToServer(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function showNotification(payload: NotificationPayload): Promise<boolean> {
|
export async function showNotification(_payload: NotificationPayload): Promise<boolean> {
|
||||||
if (isElectron) {
|
// Notifications are handled by the service worker when push messages are received from the server
|
||||||
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
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function unsubscribe(): Promise<boolean> {
|
export async function unsubscribe(): Promise<boolean> {
|
||||||
if (isElectron) {
|
|
||||||
// In Electron, we don't need to unsubscribe from server
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -235,43 +167,5 @@ export async function unsubscribe(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isSupported(): boolean {
|
export function isSupported(): boolean {
|
||||||
if (isElectron) {
|
|
||||||
return true; // Electron always supports notifications
|
|
||||||
}
|
|
||||||
return "serviceWorker" in navigator && "PushManager" in window;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Vendored
+1
@@ -182,6 +182,7 @@ export interface RegisterRequest {
|
|||||||
display_name: string;
|
display_name: string;
|
||||||
password: string;
|
password: string;
|
||||||
confirm_password: string;
|
confirm_password: string;
|
||||||
|
bio?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UploadPublicKeyRequest {
|
export interface UploadPublicKeyRequest {
|
||||||
|
|||||||
@@ -22,6 +22,16 @@ export function isDeletedPeer(user: {
|
|||||||
return isDeletedUser(user) || isDeletedAccountUsername(user.username);
|
return isDeletedUser(user) || isDeletedAccountUsername(user.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Peers should treat suspended accounts like deleted (no PII). */
|
||||||
|
export function isRedactedPeer(user: {
|
||||||
|
id?: number;
|
||||||
|
deleted?: boolean;
|
||||||
|
suspended?: boolean;
|
||||||
|
username?: string | null;
|
||||||
|
}): boolean {
|
||||||
|
return isDeletedPeer(user) || isSuspendedUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
export const DELETED_ACCOUNT_LABEL = "Deleted account";
|
export const DELETED_ACCOUNT_LABEL = "Deleted account";
|
||||||
|
|
||||||
export function deletedUserLabel(): string {
|
export function deletedUserLabel(): string {
|
||||||
@@ -33,8 +43,9 @@ export function displayNameForUser(user: {
|
|||||||
display_name?: string | null;
|
display_name?: string | null;
|
||||||
username?: string | null;
|
username?: string | null;
|
||||||
deleted?: boolean;
|
deleted?: boolean;
|
||||||
|
suspended?: boolean;
|
||||||
}): string {
|
}): string {
|
||||||
if (isDeletedPeer(user)) {
|
if (isRedactedPeer(user)) {
|
||||||
return deletedUserLabel();
|
return deletedUserLabel();
|
||||||
}
|
}
|
||||||
return user.display_name?.trim() || user.username?.trim() || "";
|
return user.display_name?.trim() || user.username?.trim() || "";
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import './css/style.scss';
|
|||||||
|
|
||||||
import "./utils/material";
|
import "./utils/material";
|
||||||
import "./core/init";
|
import "./core/init";
|
||||||
import "./core/electron/electron";
|
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import { useUserStore } from "@/state/user";
|
|||||||
import { MaterialButton } from "@/utils/material";
|
import { MaterialButton } from "@/utils/material";
|
||||||
import api from "@/core/api";
|
import api from "@/core/api";
|
||||||
import { AuthTextField, type AuthTextFieldHandle } from "./AuthTextField";
|
import { AuthTextField, type AuthTextFieldHandle } from "./AuthTextField";
|
||||||
import { initialize, isSupported, startElectronReceiver, subscribe } from "@/core/push-notifications/push-notifications";
|
import { initialize, isSupported, subscribe } from "@/core/push-notifications/push-notifications";
|
||||||
import { isElectron } from "@/core/electron/electron";
|
|
||||||
import type { Alert, AlertType } from "./Auth";
|
import type { Alert, AlertType } from "./Auth";
|
||||||
import { AuthHeader, AlertsContainer } from "./Auth";
|
import { AuthHeader, AlertsContainer } from "./Auth";
|
||||||
import styles from "./auth.module.scss";
|
import styles from "./auth.module.scss";
|
||||||
@@ -115,11 +114,6 @@ export function LoginForm({ onSwitchMode }: LoginFormProps) {
|
|||||||
const initialized = await initialize();
|
const initialized = await initialize();
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
await subscribe(data.token);
|
await subscribe(data.token);
|
||||||
|
|
||||||
if (isElectron) {
|
|
||||||
await startElectronReceiver();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Notifications enabled");
|
console.log("Notifications enabled");
|
||||||
} else {
|
} else {
|
||||||
console.log("Notification permission denied");
|
console.log("Notification permission denied");
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { onlineStatusManager } from "@/core/onlineStatusManager";
|
|||||||
import { OnlineIndicator } from "@/pages/chat/ui/right/OnlineIndicator";
|
import { OnlineIndicator } from "@/pages/chat/ui/right/OnlineIndicator";
|
||||||
import defaultAvatar from "@/images/default-avatar.png";
|
import defaultAvatar from "@/images/default-avatar.png";
|
||||||
import { MaterialBadge, MaterialCircularProgress, MaterialIcon, MaterialList, MaterialListItem } from "@/utils/material";
|
import { MaterialBadge, MaterialCircularProgress, MaterialIcon, MaterialList, MaterialListItem } from "@/utils/material";
|
||||||
import { displayNameForUser, isDeletedPeer } from "@/core/userDisplay";
|
import { displayNameForUser, isRedactedPeer } from "@/core/userDisplay";
|
||||||
import { DeletedUserAvatar } from "@/core/DeletedUserAvatar";
|
import { DeletedUserAvatar } from "@/core/DeletedUserAvatar";
|
||||||
import styles from "@/pages/chat/css/left-panel.module.scss";
|
import styles from "@/pages/chat/css/left-panel.module.scss";
|
||||||
|
|
||||||
@@ -229,7 +229,7 @@ export function UnifiedChatsList() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDeletedDm = isDeletedPeer(chat);
|
const isDeletedDm = isRedactedPeer(chat);
|
||||||
const displayName = displayNameForUser({ ...chat, id: chat.id });
|
const displayName = displayNameForUser({ ...chat, id: chat.id });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useState, useRef } from "react";
|
|||||||
import { MaterialList, MaterialListItem, MaterialSwitch, type MDUISwitch } from "@/utils/material";
|
import { MaterialList, MaterialListItem, MaterialSwitch, type MDUISwitch } from "@/utils/material";
|
||||||
import { useUserStore } from "@/state/user";
|
import { useUserStore } from "@/state/user";
|
||||||
import { initialize, subscribe, unsubscribe, isSupported } from "@/core/push-notifications/push-notifications";
|
import { initialize, subscribe, unsubscribe, isSupported } from "@/core/push-notifications/push-notifications";
|
||||||
import { isElectron } from "@/core/electron/electron";
|
|
||||||
import api from "@/core/api";
|
import api from "@/core/api";
|
||||||
import styles from "@/pages/chat/css/settings-dialog.module.scss";
|
import styles from "@/pages/chat/css/settings-dialog.module.scss";
|
||||||
|
|
||||||
@@ -23,12 +22,7 @@ export function NotificationsPanel() {
|
|||||||
|
|
||||||
setChecking(true);
|
setChecking(true);
|
||||||
try {
|
try {
|
||||||
let permission: string;
|
const permission = Notification.permission;
|
||||||
if (isElectron) {
|
|
||||||
permission = await window.electronInterface.notifications.requestPermission();
|
|
||||||
} else {
|
|
||||||
permission = Notification.permission;
|
|
||||||
}
|
|
||||||
console.log("checkPushStatus", permission);
|
console.log("checkPushStatus", permission);
|
||||||
setPushEnabled(permission === "granted");
|
setPushEnabled(permission === "granted");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { useImmer } from "use-immer";
|
|||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { parseProfileLink } from "@/core/profileLinks";
|
import { parseProfileLink } from "@/core/profileLinks";
|
||||||
import { MaterialCircularProgress, MaterialIconButton, MaterialList, MaterialListItem } from "@/utils/material";
|
import { MaterialCircularProgress, MaterialIconButton, MaterialList, MaterialListItem } from "@/utils/material";
|
||||||
import { displayNameForUser, isDeletedPeer } from "@/core/userDisplay";
|
import { displayNameForUser, isRedactedPeer } from "@/core/userDisplay";
|
||||||
import { DeletedUserAvatar } from "@/core/DeletedUserAvatar";
|
import { DeletedUserAvatar } from "@/core/DeletedUserAvatar";
|
||||||
import styles from "@/pages/chat/css/Message.module.scss";
|
import styles from "@/pages/chat/css/Message.module.scss";
|
||||||
import replyPreviewStyles from "@/pages/chat/css/reply-preview.module.scss";
|
import replyPreviewStyles from "@/pages/chat/css/reply-preview.module.scss";
|
||||||
@@ -519,7 +519,7 @@ export function Message({ message, isAuthor, onContextMenu, onReactionClick, isD
|
|||||||
return messageText.length > 0 && emojiRegex.test(messageText) && messageText.length <= 4; // Most emojis are 1-4 characters
|
return messageText.length > 0 && emojiRegex.test(messageText) && messageText.length <= 4; // Most emojis are 1-4 characters
|
||||||
}, [messageText]);
|
}, [messageText]);
|
||||||
|
|
||||||
const isDeletedSender = isDeletedPeer({ id: message.user_id, username: message.username });
|
const isDeletedSender = isRedactedPeer({ id: message.user_id, username: message.username });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import { create } from "zustand";
|
|||||||
import type { User } from "@/core/types";
|
import type { User } from "@/core/types";
|
||||||
import api from "@/core/api";
|
import api from "@/core/api";
|
||||||
import { API_BASE_URL } from "@/core/config";
|
import { API_BASE_URL } from "@/core/config";
|
||||||
import { initialize, subscribe, startElectronReceiver, isSupported } from "@/core/push-notifications/push-notifications";
|
import { initialize, subscribe, isSupported } from "@/core/push-notifications/push-notifications";
|
||||||
import { isElectron } from "@/core/electron/electron";
|
|
||||||
import { onlineStatusManager } from "@/core/onlineStatusManager";
|
import { onlineStatusManager } from "@/core/onlineStatusManager";
|
||||||
import { typingManager } from "@/core/typingManager";
|
import { typingManager } from "@/core/typingManager";
|
||||||
import type { UserState } from "./types";
|
import type { UserState } from "./types";
|
||||||
@@ -126,10 +125,6 @@ export const useUserStore = create<UserStore>((set) => ({
|
|||||||
const initialized = await initialize();
|
const initialized = await initialize();
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
await subscribe(token);
|
await subscribe(token);
|
||||||
|
|
||||||
if (isElectron) {
|
|
||||||
await startElectronReceiver();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
+1
-1
@@ -31,6 +31,6 @@
|
|||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"jsxImportSource": "react"
|
"jsxImportSource": "react"
|
||||||
},
|
},
|
||||||
"include": ["src", "src/electron/electron.d.ts", "src/protocol"],
|
"include": ["src", "src/protocol"],
|
||||||
"exclude": ["**/__*/**", "__*"]
|
"exclude": ["**/__*/**", "__*"]
|
||||||
}
|
}
|
||||||
+1
-40
@@ -1,7 +1,6 @@
|
|||||||
import { defineConfig, PluginOption } from "vite";
|
import { defineConfig, PluginOption } from "vite";
|
||||||
import { createHtmlPlugin } from "vite-plugin-html";
|
import { createHtmlPlugin } from "vite-plugin-html";
|
||||||
import autoprefixer from "autoprefixer";
|
import autoprefixer from "autoprefixer";
|
||||||
import electron from "vite-plugin-electron/simple";
|
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { visualizer } from 'rollup-plugin-visualizer';
|
import { visualizer } from 'rollup-plugin-visualizer';
|
||||||
@@ -10,7 +9,7 @@ import { optimizeCssModules } from './plugins/optimizeCssModules';
|
|||||||
import { optimizeSvg } from './plugins/optimizeSvg';
|
import { optimizeSvg } from './plugins/optimizeSvg';
|
||||||
|
|
||||||
const currentDir = path.resolve(__dirname);
|
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[] = [
|
const plugins: PluginOption[] = [
|
||||||
react({
|
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({
|
export default defineConfig({
|
||||||
root: path.resolve(__dirname, "src"),
|
root: path.resolve(__dirname, "src"),
|
||||||
envDir: currentDir,
|
envDir: currentDir,
|
||||||
@@ -80,20 +51,10 @@ export default defineConfig({
|
|||||||
"@fromchat/protocol": path.resolve(__dirname, "./src/protocol/index.ts")
|
"@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: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
port: 8301,
|
port: 8301,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
proxy: {
|
|
||||||
"/api": {
|
|
||||||
target: apiProxyTarget,
|
|
||||||
changeOrigin: true,
|
|
||||||
rewrite: (path) => path.replace(/^\/api/, ""),
|
|
||||||
ws: true,
|
|
||||||
proxyTimeout: 0,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
allowedHosts: ["beta.fromchat.ru"]
|
allowedHosts: ["beta.fromchat.ru"]
|
||||||
},
|
},
|
||||||
appType: "spa",
|
appType: "spa",
|
||||||
|
|||||||
Reference in New Issue
Block a user