Compare commits
3 Commits
7b9936dcc4
...
824b1fec65
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
824b1fec65 | ||
|
|
41d7a89830 | ||
|
|
88288317ab |
@@ -16,7 +16,10 @@ export function ActiveCall() {
|
||||
}
|
||||
|
||||
const getConnectingClass = () => {
|
||||
if(callState === CallState.CONNECTING){
|
||||
if(callState === CallState.CONNECTING
|
||||
|| callState === CallState.INCOMING
|
||||
|| callState === CallState.KEY_EXCHANGE
|
||||
|| callState === CallState.WEB_RTC_EXCHANGE){
|
||||
return classes.connecting;
|
||||
}
|
||||
if(callState === CallState.ACTIVE){
|
||||
|
||||
@@ -40,13 +40,13 @@ export function Call(props: CallProps) {
|
||||
<Flex style={{
|
||||
cursor: 'pointer'
|
||||
}} onClick={() => setShowCallView(false)} justify={'center'} align={'center'}>
|
||||
<IconChevronLeft size={20}></IconChevronLeft>
|
||||
<Text fw={500}>Back</Text>
|
||||
<IconChevronLeft color="white" size={20}></IconChevronLeft>
|
||||
<Text fw={500} c={'white'}>Back</Text>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<Popover width={300} withArrow>
|
||||
<Popover width={300} disabled={getKeyCast() == ''} withArrow>
|
||||
<Popover.Target>
|
||||
<IconQrcode size={24}></IconQrcode>
|
||||
<IconQrcode color={getKeyCast() == '' ? 'gray' : 'white'} size={24}></IconQrcode>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown p={'xs'}>
|
||||
<Flex direction={'row'} align={'center'} gap={'xs'}>
|
||||
|
||||
78
app/hooks/useSound.ts
Normal file
78
app/hooks/useSound.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { useRef } from "react";
|
||||
|
||||
export function useSound() {
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
const loopingAudioRef = useRef<HTMLAudioElement | null>(null);
|
||||
|
||||
const stopSound = () => {
|
||||
if (!audioRef.current) {
|
||||
return;
|
||||
}
|
||||
audioRef.current.pause();
|
||||
audioRef.current.currentTime = 0;
|
||||
audioRef.current.removeAttribute("src");
|
||||
audioRef.current.load();
|
||||
};
|
||||
|
||||
const playSound = (sound : string, loop: boolean = false) => {
|
||||
try {
|
||||
if(loop){
|
||||
if (!loopingAudioRef.current) {
|
||||
loopingAudioRef.current = new Audio();
|
||||
loopingAudioRef.current.volume = 0.1;
|
||||
loopingAudioRef.current.preload = "auto";
|
||||
loopingAudioRef.current.loop = true;
|
||||
}
|
||||
|
||||
const url = window.mediaApi.getSoundUrl(sound);
|
||||
const player = loopingAudioRef.current;
|
||||
|
||||
player.src = url;
|
||||
const playPromise = player.play();
|
||||
if (playPromise) {
|
||||
void playPromise.catch((e) => {
|
||||
console.error("Failed to play looping UI sound:", e);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!audioRef.current) {
|
||||
audioRef.current = new Audio();
|
||||
audioRef.current.volume = 0.1;
|
||||
audioRef.current.preload = "auto";
|
||||
audioRef.current.loop = loop;
|
||||
}
|
||||
|
||||
const url = window.mediaApi.getSoundUrl(sound);
|
||||
const player = audioRef.current;
|
||||
|
||||
stopSound();
|
||||
|
||||
player.src = url;
|
||||
const playPromise = player.play();
|
||||
if (playPromise) {
|
||||
void playPromise.catch((e) => {
|
||||
console.error("Failed to play UI sound:", e);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to prepare UI sound:", e);
|
||||
}
|
||||
}
|
||||
|
||||
const stopLoopSound = () => {
|
||||
if (!loopingAudioRef.current) {
|
||||
return;
|
||||
}
|
||||
loopingAudioRef.current.pause();
|
||||
loopingAudioRef.current.currentTime = 0;
|
||||
loopingAudioRef.current.removeAttribute("src");
|
||||
loopingAudioRef.current.load();
|
||||
}
|
||||
|
||||
return {
|
||||
playSound,
|
||||
stopSound,
|
||||
stopLoopSound
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,19 @@ const useWindow = () => {
|
||||
window.api.send('window-theme', theme);
|
||||
}
|
||||
|
||||
const setWindowPriority = (isTop: boolean) => {
|
||||
if(isTop){
|
||||
window.api.invoke('window-top');
|
||||
} else {
|
||||
window.api.invoke('window-priority-normal');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
setSize,
|
||||
setResizeble,
|
||||
setTheme
|
||||
setTheme,
|
||||
setWindowPriority
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ import { PacketWebRTC, WebRTCSignalType } from "../ProtocolProvider/protocol/pac
|
||||
import { PacketIceServers } from "../ProtocolProvider/protocol/packets/packet.ice.servers";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { Button, Flex, Text } from "@mantine/core";
|
||||
|
||||
import { useSound } from "@/app/hooks/useSound";
|
||||
import useWindow from "@/app/hooks/useWindow";
|
||||
|
||||
export interface CallContextValue {
|
||||
call: (callable: string) => void;
|
||||
@@ -59,8 +60,9 @@ export interface CallProviderProps {
|
||||
export function CallProvider(props : CallProviderProps) {
|
||||
const [activeCall, setActiveCall] = useState<string>("");
|
||||
const [callState, setCallState] = useState<CallState>(CallState.ENDED);
|
||||
const [muted, setMuted] = useState<boolean>(false);
|
||||
const [sound, setSound] = useState<boolean>(true);
|
||||
const [muted, setMutedState] = useState<boolean>(false);
|
||||
const [sound, setSoundState] = useState<boolean>(true);
|
||||
const durationIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const [duration, setDuration] = useState<number>(0);
|
||||
const [showCallView, setShowCallView] = useState<boolean>(callState == CallState.INCOMING);
|
||||
const {info} = useConsoleLogger("CallProvider");
|
||||
@@ -73,8 +75,26 @@ export function CallProvider(props : CallProviderProps) {
|
||||
const roleRef = useRef<CallRole | null>(null);
|
||||
const [sharedSecret, setSharedSecret] = useState<string>("");
|
||||
const iceServersRef = useRef<RTCIceServer[]>([]);
|
||||
const remoteAudioRef = useRef<HTMLAudioElement>(null);
|
||||
const remoteAudioRef = useRef<HTMLAudioElement | null>(null);
|
||||
const iceCandidatesBufferRef = useRef<RTCIceCandidate[]>([]);
|
||||
const mutedRef = useRef<boolean>(false);
|
||||
const soundRef = useRef<boolean>(true);
|
||||
|
||||
const {playSound, stopSound, stopLoopSound} = useSound();
|
||||
const {setWindowPriority} = useWindow();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if(callState == CallState.ACTIVE){
|
||||
stopLoopSound();
|
||||
stopSound();
|
||||
playSound("connected.mp3");
|
||||
setWindowPriority(false);
|
||||
durationIntervalRef.current = setInterval(() => {
|
||||
setDuration(prev => prev + 1);
|
||||
}, 1000);
|
||||
}
|
||||
}, [callState]);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
@@ -83,6 +103,16 @@ export function CallProvider(props : CallProviderProps) {
|
||||
*/
|
||||
let packet = new PacketIceServers();
|
||||
send(packet);
|
||||
|
||||
return () => {
|
||||
stopSound();
|
||||
if (remoteAudioRef.current) {
|
||||
remoteAudioRef.current.pause();
|
||||
remoteAudioRef.current.srcObject = null;
|
||||
}
|
||||
peerConnectionRef.current?.close();
|
||||
peerConnectionRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
usePacket(28, async (packet: PacketIceServers) => {
|
||||
@@ -168,43 +198,11 @@ export function CallProvider(props : CallProviderProps) {
|
||||
usePacket(26, async (packet: PacketSignalPeer) => {
|
||||
const signalType = packet.getSignalType();
|
||||
if(signalType == SignalType.END_CALL_BECAUSE_BUSY) {
|
||||
modals.open({
|
||||
title: 'Busy',
|
||||
centered: true,
|
||||
children: (
|
||||
<>
|
||||
<Text size="sm">
|
||||
Line is busy, the user is currently on another call. Please try again later.
|
||||
</Text>
|
||||
<Flex align={'center'} justify={'flex-end'}>
|
||||
<Button color={'red'} variant={'subtle'} onClick={() => modals.closeAll()} mt="md">
|
||||
Close
|
||||
</Button>
|
||||
</Flex>
|
||||
</>
|
||||
),
|
||||
withCloseButton: false
|
||||
});
|
||||
openCallsModal("Line is busy, the user is currently on another call. Please try again later.");
|
||||
end();
|
||||
}
|
||||
if(signalType == SignalType.END_CALL_BECAUSE_PEER_DISCONNECTED) {
|
||||
modals.open({
|
||||
title: 'Connection lost',
|
||||
centered: true,
|
||||
children: (
|
||||
<>
|
||||
<Text size="sm">
|
||||
The connection with the user was lost. The call has ended.
|
||||
</Text>
|
||||
<Flex align={'center'} justify={'flex-end'}>
|
||||
<Button color={'red'} variant={'subtle'} onClick={() => modals.closeAll()} mt="md">
|
||||
Close
|
||||
</Button>
|
||||
</Flex>
|
||||
</>
|
||||
),
|
||||
withCloseButton: false
|
||||
});
|
||||
openCallsModal("The connection with the user was lost. The call has ended.")
|
||||
end();
|
||||
}
|
||||
if(activeCall){
|
||||
@@ -228,6 +226,8 @@ export function CallProvider(props : CallProviderProps) {
|
||||
/**
|
||||
* Нам поступает звонок
|
||||
*/
|
||||
setWindowPriority(true);
|
||||
playSound("ringtone.mp3", true);
|
||||
setActiveCall(packet.getSrc());
|
||||
setCallState(CallState.INCOMING);
|
||||
setShowCallView(true);
|
||||
@@ -325,10 +325,13 @@ export function CallProvider(props : CallProviderProps) {
|
||||
/**
|
||||
* При получении медиа-трека с другой стороны
|
||||
*/
|
||||
console.info("TRACK RECV!!!!!");
|
||||
if(remoteAudioRef.current){
|
||||
if(remoteAudioRef.current && event.streams[0]){
|
||||
console.info(event.streams);
|
||||
remoteAudioRef.current.srcObject = event.streams[0];
|
||||
remoteAudioRef.current.muted = !soundRef.current;
|
||||
void remoteAudioRef.current.play().catch((e) => {
|
||||
console.error("Failed to play remote audio:", e);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,6 +358,25 @@ export function CallProvider(props : CallProviderProps) {
|
||||
}
|
||||
}, [activeCall, sessionKeys]);
|
||||
|
||||
const openCallsModal = (text : string) => {
|
||||
modals.open({
|
||||
centered: true,
|
||||
children: (
|
||||
<>
|
||||
<Text size="sm">
|
||||
{text}
|
||||
</Text>
|
||||
<Flex align={'center'} justify={'flex-end'}>
|
||||
<Button color={'red'} variant={'subtle'} onClick={() => modals.closeAll()} mt="md">
|
||||
Close
|
||||
</Button>
|
||||
</Flex>
|
||||
</>
|
||||
),
|
||||
withCloseButton: false
|
||||
});
|
||||
}
|
||||
|
||||
const generateSessionKeys = () => {
|
||||
const sessionKeys = nacl.box.keyPair();
|
||||
info("Generated keys for call session, len: " + sessionKeys.publicKey.length);
|
||||
@@ -363,6 +385,14 @@ export function CallProvider(props : CallProviderProps) {
|
||||
}
|
||||
|
||||
const call = (dialog: string) => {
|
||||
if(callState == CallState.ACTIVE
|
||||
|| callState == CallState.CONNECTING
|
||||
|| callState == CallState.KEY_EXCHANGE
|
||||
|| callState == CallState.WEB_RTC_EXCHANGE){
|
||||
openCallsModal("You are already on a call, please end the current call before starting a new one.");
|
||||
return;
|
||||
}
|
||||
setWindowPriority(false);
|
||||
setActiveCall(dialog);
|
||||
setCallState(CallState.CONNECTING);
|
||||
setShowCallView(true);
|
||||
@@ -372,6 +402,7 @@ export function CallProvider(props : CallProviderProps) {
|
||||
signalPacket.setSignalType(SignalType.CALL);
|
||||
send(signalPacket);
|
||||
roleRef.current = CallRole.CALLER;
|
||||
playSound("calling.mp3", true);
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
@@ -384,14 +415,28 @@ export function CallProvider(props : CallProviderProps) {
|
||||
}
|
||||
|
||||
const end = () => {
|
||||
stopLoopSound();
|
||||
stopSound();
|
||||
if (remoteAudioRef.current) {
|
||||
remoteAudioRef.current.pause();
|
||||
remoteAudioRef.current.srcObject = null;
|
||||
}
|
||||
setDuration(0);
|
||||
durationIntervalRef.current && clearInterval(durationIntervalRef.current);
|
||||
setWindowPriority(false);
|
||||
playSound("end_call.mp3");
|
||||
peerConnectionRef.current?.close();
|
||||
peerConnectionRef.current = null;
|
||||
roomIdRef.current = "";
|
||||
mutedRef.current = false;
|
||||
soundRef.current = true;
|
||||
setActiveCall("");
|
||||
setCallState(CallState.ENDED);
|
||||
setShowCallView(false);
|
||||
setSessionKeys(null);
|
||||
setDuration(0);
|
||||
setMutedState(false);
|
||||
setSoundState(true);
|
||||
roleRef.current = null;
|
||||
}
|
||||
|
||||
@@ -402,6 +447,9 @@ export function CallProvider(props : CallProviderProps) {
|
||||
*/
|
||||
return;
|
||||
}
|
||||
setWindowPriority(false);
|
||||
stopLoopSound();
|
||||
stopSound();
|
||||
/**
|
||||
* Звонок принят, генерируем ключи для сессии и отправляем их другой стороне для установления защищенного канала связи
|
||||
*/
|
||||
@@ -428,6 +476,46 @@ export function CallProvider(props : CallProviderProps) {
|
||||
return sharedSecret;
|
||||
}
|
||||
|
||||
|
||||
const setMuted = (nextMuted: boolean) => {
|
||||
if (mutedRef.current === nextMuted) {
|
||||
return;
|
||||
}
|
||||
|
||||
mutedRef.current = nextMuted;
|
||||
playSound(nextMuted ? "micro_enable.mp3" : "micro_disable.mp3");
|
||||
|
||||
if(peerConnectionRef.current){
|
||||
peerConnectionRef.current.getSenders().forEach(sender => {
|
||||
if(sender.track?.kind == "audio"){
|
||||
sender.track.enabled = !nextMuted;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setMutedState(nextMuted);
|
||||
}
|
||||
|
||||
const setSound = (nextSound: boolean) => {
|
||||
if (soundRef.current === nextSound) {
|
||||
return;
|
||||
}
|
||||
|
||||
soundRef.current = nextSound;
|
||||
playSound(nextSound ? "sound_enable.mp3" : "sound_disable.mp3");
|
||||
|
||||
if(remoteAudioRef.current){
|
||||
remoteAudioRef.current.muted = !nextSound;
|
||||
if (nextSound) {
|
||||
void remoteAudioRef.current.play().catch((e) => {
|
||||
console.error("Failed to resume remote audio:", e);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setSoundState(nextSound);
|
||||
}
|
||||
|
||||
const context = {
|
||||
call,
|
||||
close,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BrowserWindow, shell, ipcMain, nativeTheme, screen, powerMonitor } from 'electron'
|
||||
import { BrowserWindow, shell, ipcMain, nativeTheme, screen, powerMonitor, app } from 'electron'
|
||||
import { join } from 'path'
|
||||
import fs from 'fs'
|
||||
import { WORKING_DIR } from './constants';
|
||||
@@ -45,7 +45,8 @@ export function createAppWindow(preloaderWindow?: BrowserWindow): void {
|
||||
nodeIntegrationInSubFrames: true,
|
||||
nodeIntegrationInWorker: true,
|
||||
webSecurity: false,
|
||||
allowRunningInsecureContent: true
|
||||
allowRunningInsecureContent: true,
|
||||
autoplayPolicy: 'no-user-gesture-required'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -73,6 +74,7 @@ export function createAppWindow(preloaderWindow?: BrowserWindow): void {
|
||||
}
|
||||
|
||||
export function foundationIpcRegistration(mainWindow: BrowserWindow) {
|
||||
let bounceId: number | null = null;
|
||||
ipcMain.removeAllListeners('window-resize');
|
||||
ipcMain.removeAllListeners('window-resizeble');
|
||||
ipcMain.removeAllListeners('window-theme');
|
||||
@@ -86,6 +88,38 @@ export function foundationIpcRegistration(mainWindow: BrowserWindow) {
|
||||
ipcMain.removeHandler('window-minimize');
|
||||
ipcMain.removeHandler('showItemInFolder');
|
||||
ipcMain.removeHandler('openExternal');
|
||||
ipcMain.removeHandler('window-top');
|
||||
ipcMain.removeHandler('window-priority-normal');
|
||||
|
||||
ipcMain.handle('window-top', () => {
|
||||
if (mainWindow.isMinimized()){
|
||||
mainWindow.restore();
|
||||
}
|
||||
mainWindow.setAlwaysOnTop(true, "screen-saver"); // самый высокий уровень
|
||||
mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
||||
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
/**
|
||||
* Только в macos! Подпрыгивание иконки в Dock
|
||||
*/
|
||||
bounceId = app.dock!.bounce("critical");
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('window-priority-normal', () => {
|
||||
mainWindow.setAlwaysOnTop(false);
|
||||
mainWindow.setVisibleOnAllWorkspaces(false);
|
||||
if(process.platform === "darwin" && bounceId !== null){
|
||||
/**
|
||||
* Только в macos! Отмена подпрыгивания иконки в Dock
|
||||
*/
|
||||
app.dock!.cancelBounce(bounceId);
|
||||
bounceId = null;
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('open-dev-tools', () => {
|
||||
if (mainWindow.webContents.isDevToolsOpened()) {
|
||||
|
||||
3
lib/preload/index.d.ts
vendored
3
lib/preload/index.d.ts
vendored
@@ -13,5 +13,8 @@ declare global {
|
||||
downloadsPath: string;
|
||||
deviceName: string;
|
||||
deviceId: string;
|
||||
mediaApi: {
|
||||
getSoundUrl: (fileName: string) => string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
import { contextBridge, ipcRenderer, shell } from 'electron'
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
import api from './api'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import path from 'node:path'
|
||||
import fs from "node:fs";
|
||||
|
||||
|
||||
function resolveSound(fileName: string) {
|
||||
const isDev = !process.env.APP_PACKAGED; // или свой флаг dev
|
||||
const fullPath = isDev
|
||||
? path.join(process.cwd(), "resources", "sounds", fileName)
|
||||
: path.join(process.resourcesPath, "resources", "sounds", fileName);
|
||||
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
throw new Error(`Sound not found: ${fullPath}`);
|
||||
}
|
||||
return pathToFileURL(fullPath).toString();
|
||||
}
|
||||
|
||||
const exposeContext = async () => {
|
||||
if (process.contextIsolated) {
|
||||
try {
|
||||
@@ -16,6 +31,11 @@ const exposeContext = async () => {
|
||||
ipcRenderer.invoke('ipcCore:showItemInFolder', fullPath);
|
||||
}
|
||||
});
|
||||
contextBridge.exposeInMainWorld("mediaApi", {
|
||||
getSoundUrl: (fileName: string) => {
|
||||
return resolveSound(fileName);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
@@ -23,6 +43,11 @@ const exposeContext = async () => {
|
||||
window.electron = electronAPI
|
||||
window.api = api;
|
||||
window.shell = shell;
|
||||
window.mediaApi = {
|
||||
getSoundUrl: (fileName: string) => {
|
||||
return resolveSound(fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
{
|
||||
"name": "Rosetta",
|
||||
"version": "1.5.0",
|
||||
"version": "1.5.2",
|
||||
"description": "Rosetta Messenger",
|
||||
"main": "./out/main/main.js",
|
||||
"license": "MIT",
|
||||
"build": {
|
||||
"electronUpdaterCompatibility": false,
|
||||
"extraResources": [
|
||||
{ "from": "resources/", "to": "resources/" }
|
||||
],
|
||||
"files": [
|
||||
"node_modules/sqlite3/**/*",
|
||||
"out/main/**/*",
|
||||
|
||||
BIN
resources/sounds/calling.mp3
Normal file
BIN
resources/sounds/calling.mp3
Normal file
Binary file not shown.
BIN
resources/sounds/connected.mp3
Normal file
BIN
resources/sounds/connected.mp3
Normal file
Binary file not shown.
BIN
resources/sounds/end_call.mp3
Normal file
BIN
resources/sounds/end_call.mp3
Normal file
Binary file not shown.
BIN
resources/sounds/micro_disable.mp3
Normal file
BIN
resources/sounds/micro_disable.mp3
Normal file
Binary file not shown.
BIN
resources/sounds/micro_enable.mp3
Normal file
BIN
resources/sounds/micro_enable.mp3
Normal file
Binary file not shown.
BIN
resources/sounds/ringtone.mp3
Normal file
BIN
resources/sounds/ringtone.mp3
Normal file
Binary file not shown.
BIN
resources/sounds/sound_disable.mp3
Normal file
BIN
resources/sounds/sound_disable.mp3
Normal file
Binary file not shown.
BIN
resources/sounds/sound_enable.mp3
Normal file
BIN
resources/sounds/sound_enable.mp3
Normal file
Binary file not shown.
Reference in New Issue
Block a user