mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Compare commits
7 Commits
@@ -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
|
||||
|
||||
@@ -17,7 +17,6 @@ node_modules
|
||||
npm-debug.log
|
||||
.vite
|
||||
dist
|
||||
dist-electron
|
||||
build
|
||||
out
|
||||
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/lib
|
||||
|
||||
*.module.d.scss.ts
|
||||
Vendored
-33
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+8
-4
@@ -14,12 +14,16 @@ COPY . .
|
||||
|
||||
WORKDIR /app
|
||||
ARG NODE_ENV=production
|
||||
# Overridable at build time. Must be a real http(s) URL — never a compose ${...} stub.
|
||||
ARG VITE_API_BASE_URL=https://api.fromchat.ru
|
||||
ENV NODE_ENV=production
|
||||
ENV VITE_API_BASE_URL=${VITE_API_BASE_URL}
|
||||
|
||||
# 1.3. Build production assets
|
||||
RUN npm run frontend:build
|
||||
# Fail the build if the arg is not a real URL (e.g. unexpanded Compose ${...}).
|
||||
RUN if ! printf "%s" "${VITE_API_BASE_URL}" | grep -Eq '^https?://'; then \
|
||||
echo "ERROR: VITE_API_BASE_URL must be an http(s) URL, got: ${VITE_API_BASE_URL}" >&2; \
|
||||
exit 1; \
|
||||
fi && \
|
||||
export NODE_ENV=production VITE_API_BASE_URL && \
|
||||
npm run frontend:build
|
||||
|
||||
|
||||
# 2. Production web static server
|
||||
|
||||
+3
-8
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
+2
-24
@@ -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",
|
||||
@@ -51,13 +35,10 @@
|
||||
"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-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"
|
||||
}
|
||||
}
|
||||
|
||||
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 { 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 />
|
||||
|
||||
@@ -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> {
|
||||
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" }));
|
||||
|
||||
@@ -5,7 +5,7 @@ import { request } from "@/core/websocket";
|
||||
import type { DmEnvelope, User } from "@/core/types";
|
||||
import { ub64 } from "@/utils/utils";
|
||||
import { fetchUserPublicKey } from "../crypto/identity";
|
||||
import { fetchUsers, searchUsers } from "../user/search";
|
||||
import { searchUsers } from "../user/search";
|
||||
import { deriveWrappingKey, importAesGcmKey, aesGcmDecrypt } from "@fromchat/protocol";
|
||||
import tweetnacl from "tweetnacl";
|
||||
|
||||
@@ -277,7 +277,7 @@ export async function markRead(id: number, authToken: string): Promise<void> {
|
||||
await request({
|
||||
type: "dmMarkRead",
|
||||
credentials: { scheme: "Bearer", credentials: authToken },
|
||||
data: { id }
|
||||
data: { messageIds: [id] }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -320,4 +320,4 @@ export async function editMessage(
|
||||
}
|
||||
|
||||
// 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 type { DmEnvelope, User } from "@/core/types";
|
||||
import { fetchUserPublicKey } from "./crypto";
|
||||
import { fetchUsers, searchUsers } from "./users";
|
||||
import { searchUsers } from "./users";
|
||||
|
||||
/**
|
||||
* 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
|
||||
export { fetchUsers, searchUsers, fetchUserPublicKey };
|
||||
export { searchUsers, fetchUserPublicKey };
|
||||
|
||||
/**
|
||||
* Send DM via WebSocket using transport encryption.
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getAuthHeaders } from "./account";
|
||||
import { request } from "@/core/websocket";
|
||||
import type { DmEnvelope, User } from "@/core/types";
|
||||
import { fetchUserPublicKey } from "./crypto";
|
||||
import { fetchUsers, searchUsers } from "./users";
|
||||
import { searchUsers } from "./users";
|
||||
|
||||
export async function decryptDm(envelope: DmEnvelope): Promise<string> {
|
||||
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
|
||||
export { fetchUsers, searchUsers, fetchUserPublicKey };
|
||||
export { searchUsers, fetchUserPublicKey };
|
||||
|
||||
export async function sendDMViaWebSocket(recipientId: number, recipientPublicKeyB64: string, plaintext: string, authToken: string, replyToId?: number): Promise<void> {
|
||||
const { send } = await import("./chats/dm");
|
||||
|
||||
@@ -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" }));
|
||||
|
||||
@@ -2,16 +2,6 @@ import { API_BASE_URL } from "@/core/config";
|
||||
import { getAuthHeaders } from "./auth";
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -2,16 +2,6 @@ import { API_BASE_URL } from "@/core/config";
|
||||
import { getAuthHeaders } from "./account";
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -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 { 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;
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -182,6 +182,7 @@ export interface RegisterRequest {
|
||||
display_name: string;
|
||||
password: string;
|
||||
confirm_password: string;
|
||||
bio?: string;
|
||||
}
|
||||
|
||||
export interface UploadPublicKeyRequest {
|
||||
|
||||
@@ -22,6 +22,16 @@ export function isDeletedPeer(user: {
|
||||
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 function deletedUserLabel(): string {
|
||||
@@ -33,8 +43,9 @@ export function displayNameForUser(user: {
|
||||
display_name?: string | null;
|
||||
username?: string | null;
|
||||
deleted?: boolean;
|
||||
suspended?: boolean;
|
||||
}): string {
|
||||
if (isDeletedPeer(user)) {
|
||||
if (isRedactedPeer(user)) {
|
||||
return deletedUserLabel();
|
||||
}
|
||||
return user.display_name?.trim() || user.username?.trim() || "";
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -10,7 +10,7 @@ import { onlineStatusManager } from "@/core/onlineStatusManager";
|
||||
import { OnlineIndicator } from "@/pages/chat/ui/right/OnlineIndicator";
|
||||
import defaultAvatar from "@/images/default-avatar.png";
|
||||
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 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 });
|
||||
|
||||
return (
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useImmer } from "use-immer";
|
||||
import { createPortal } from "react-dom";
|
||||
import { parseProfileLink } from "@/core/profileLinks";
|
||||
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 styles from "@/pages/chat/css/Message.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
|
||||
}, [messageText]);
|
||||
|
||||
const isDeletedSender = isDeletedPeer({ id: message.user_id, username: message.username });
|
||||
const isDeletedSender = isRedactedPeer({ id: message.user_id, username: message.username });
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user