1.1.0-1.5.2 #16
@@ -6,9 +6,8 @@ import { ConfirmSeed } from './views/ConfirmSeed/ConfirmSeed';
|
|||||||
import { SetPassword } from './views/SetPassword/SetPassword';
|
import { SetPassword } from './views/SetPassword/SetPassword';
|
||||||
import { Main } from './views/Main/Main';
|
import { Main } from './views/Main/Main';
|
||||||
import { ExistsSeed } from './views/ExistsSeed/ExistsSeed';
|
import { ExistsSeed } from './views/ExistsSeed/ExistsSeed';
|
||||||
import { Box, Divider } from '@mantine/core';
|
import { Box } from '@mantine/core';
|
||||||
import './style.css'
|
import './style.css'
|
||||||
import { useRosettaColors } from './hooks/useRosettaColors';
|
|
||||||
import { Buffer } from 'buffer';
|
import { Buffer } from 'buffer';
|
||||||
import { InformationProvider } from './providers/InformationProvider/InformationProvider';
|
import { InformationProvider } from './providers/InformationProvider/InformationProvider';
|
||||||
import { BlacklistProvider } from './providers/BlacklistProvider/BlacklistProvider';
|
import { BlacklistProvider } from './providers/BlacklistProvider/BlacklistProvider';
|
||||||
@@ -27,8 +26,6 @@ window.Buffer = Buffer;
|
|||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { allAccounts, accountProviderLoaded } = useAccountProvider();
|
const { allAccounts, accountProviderLoaded } = useAccountProvider();
|
||||||
const colors = useRosettaColors();
|
|
||||||
|
|
||||||
|
|
||||||
const getViewByLoginState = () => {
|
const getViewByLoginState = () => {
|
||||||
if (!accountProviderLoaded) {
|
if (!accountProviderLoaded) {
|
||||||
@@ -59,7 +56,6 @@ export default function App() {
|
|||||||
<SystemAccountProvider>
|
<SystemAccountProvider>
|
||||||
<Box h={'100%'}>
|
<Box h={'100%'}>
|
||||||
<Topbar></Topbar>
|
<Topbar></Topbar>
|
||||||
<Divider color={colors.borderColor}></Divider>
|
|
||||||
<ContextMenuProvider>
|
<ContextMenuProvider>
|
||||||
<ImageViwerProvider>
|
<ImageViwerProvider>
|
||||||
<AvatarProvider>
|
<AvatarProvider>
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export function ActionAvatar(props : ActionAvatarProps) {
|
|||||||
size={120}
|
size={120}
|
||||||
radius={120}
|
radius={120}
|
||||||
mx="auto"
|
mx="auto"
|
||||||
|
bg={'#fff'}
|
||||||
name={props.title.trim() || props.publicKey}
|
name={props.title.trim() || props.publicKey}
|
||||||
color={'initials'}
|
color={'initials'}
|
||||||
src={avatars.length > 0 ?
|
src={avatars.length > 0 ?
|
||||||
|
|||||||
42
app/components/ActiveCall/ActiveCall.module.css
Normal file
42
app/components/ActiveCall/ActiveCall.module.css
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
.active {
|
||||||
|
background: linear-gradient(90deg,rgba(0, 186, 59, 1) 0%, rgba(0, 194, 81, 1) 50%);
|
||||||
|
background-size: 200% 200%;
|
||||||
|
animation: activeFlow 5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes activeFlow {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
filter: saturate(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
filter: saturate(1.15);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
filter: saturate(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.connecting {
|
||||||
|
background: linear-gradient(120deg, #ff2d2d, #ff7a00, #ff2d2d);
|
||||||
|
background-size: 220% 220%;
|
||||||
|
animation: connectingFlow 5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes connectingFlow {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
filter: saturate(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
filter: saturate(1.15);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
filter: saturate(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* ...existing code... */
|
||||||
98
app/components/ActiveCall/ActiveCall.tsx
Normal file
98
app/components/ActiveCall/ActiveCall.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { useCalls } from "@/app/providers/CallProvider/useCalls";
|
||||||
|
import { useUserInformation } from "@/app/providers/InformationProvider/useUserInformation";
|
||||||
|
import { Box, Flex, Loader, Text } from "@mantine/core";
|
||||||
|
import classes from "./ActiveCall.module.css";
|
||||||
|
import { CallState } from "@/app/providers/CallProvider/CallProvider";
|
||||||
|
import { IconMicrophone, IconMicrophoneOff, IconPhoneX, IconVolume, IconVolumeOff } from "@tabler/icons-react";
|
||||||
|
import { translateDurationToTime } from "@/app/providers/CallProvider/translateDurationTime";
|
||||||
|
|
||||||
|
export function ActiveCall() {
|
||||||
|
const {activeCall, callState, duration, muted, sound, close, setMuted, setSound, setShowCallView} = useCalls();
|
||||||
|
const [userInfo] = useUserInformation(activeCall);
|
||||||
|
//const colors = useRosettaColors();
|
||||||
|
|
||||||
|
if(activeCall == ""){
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
|
const getConnectingClass = () => {
|
||||||
|
if(callState === CallState.CONNECTING
|
||||||
|
|| callState === CallState.INCOMING
|
||||||
|
|| callState === CallState.KEY_EXCHANGE
|
||||||
|
|| callState === CallState.WEB_RTC_EXCHANGE){
|
||||||
|
return classes.connecting;
|
||||||
|
}
|
||||||
|
if(callState === CallState.ACTIVE){
|
||||||
|
return classes.active;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box py={4} style={{
|
||||||
|
cursor: 'pointer'
|
||||||
|
}} px={10} className={getConnectingClass()} onClick={() => setShowCallView(true)}>
|
||||||
|
<Flex align={'center'} justify={'row'} gap={10}>
|
||||||
|
<Flex w={'100%'} justify={'space-between'} align={'center'}>
|
||||||
|
<Flex>
|
||||||
|
{!muted && (
|
||||||
|
<IconMicrophoneOff style={{
|
||||||
|
cursor: 'pointer'
|
||||||
|
}} onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setMuted(true);
|
||||||
|
}} size={16} color={'#fff'}></IconMicrophoneOff>
|
||||||
|
)}
|
||||||
|
{muted && (
|
||||||
|
<IconMicrophone style={{
|
||||||
|
cursor: 'pointer'
|
||||||
|
}} onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setMuted(false);
|
||||||
|
}} size={16} color={'#fff'}></IconMicrophone>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
<Flex justify={'center'} align={'center'} gap={'xs'}>
|
||||||
|
<Text fw={500} c={'#fff'} style={{
|
||||||
|
userSelect: 'none'
|
||||||
|
}} fz={13}>{userInfo?.title || activeCall}</Text>
|
||||||
|
{callState === CallState.CONNECTING && (
|
||||||
|
<Loader type={'dots'} size={12} color="white"></Loader>
|
||||||
|
)}
|
||||||
|
{callState == CallState.ACTIVE && (
|
||||||
|
<Text fw={500} c={'#ffffff'} style={{
|
||||||
|
userSelect: 'none'
|
||||||
|
}} fz={12}>{translateDurationToTime(duration)}</Text>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
<Flex gap={'xs'} align={'center'} justify={'center'}>
|
||||||
|
{sound && (
|
||||||
|
<IconVolumeOff style={{
|
||||||
|
cursor: 'pointer'
|
||||||
|
}} size={16} onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setSound(false)
|
||||||
|
}} color={'#fff'}></IconVolumeOff>
|
||||||
|
)}
|
||||||
|
{!sound && (
|
||||||
|
<IconVolume style={{
|
||||||
|
cursor: 'pointer'
|
||||||
|
}} size={16} onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setSound(true)
|
||||||
|
}} color={'#fff'}></IconVolume>
|
||||||
|
)}
|
||||||
|
<IconPhoneX style={{
|
||||||
|
cursor: 'pointer'
|
||||||
|
}} onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
close();
|
||||||
|
}} size={16} color={'#fff'}></IconPhoneX>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
139
app/components/Call/Call.tsx
Normal file
139
app/components/Call/Call.tsx
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { useRosettaColors } from "@/app/hooks/useRosettaColors";
|
||||||
|
import { useAvatars } from "@/app/providers/AvatarProvider/useAvatars";
|
||||||
|
import { CallContextValue, CallState } from "@/app/providers/CallProvider/CallProvider";
|
||||||
|
import { translateDurationToTime } from "@/app/providers/CallProvider/translateDurationTime";
|
||||||
|
import { useUserInformation } from "@/app/providers/InformationProvider/useUserInformation";
|
||||||
|
import { Avatar, Box, Flex, Popover, Text, useMantineTheme } from "@mantine/core";
|
||||||
|
import { IconChevronLeft, IconMicrophone, IconMicrophoneOff, IconPhone, IconPhoneX, IconQrcode, IconVolume, IconVolumeOff, IconX } from "@tabler/icons-react";
|
||||||
|
import { KeyImage } from "../KeyImage/KeyImage";
|
||||||
|
|
||||||
|
export interface CallProps {
|
||||||
|
context: CallContextValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Call(props: CallProps) {
|
||||||
|
const {
|
||||||
|
activeCall,
|
||||||
|
duration,
|
||||||
|
callState,
|
||||||
|
close,
|
||||||
|
sound,
|
||||||
|
setSound,
|
||||||
|
setMuted,
|
||||||
|
setShowCallView,
|
||||||
|
muted,
|
||||||
|
getKeyCast,
|
||||||
|
accept
|
||||||
|
} = props.context;
|
||||||
|
const [userInfo] = useUserInformation(activeCall);
|
||||||
|
const avatars = useAvatars(activeCall);
|
||||||
|
const colors = useRosettaColors();
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box pos={'absolute'} top={0} left={0} w={'100%'} h={'100vh'} style={{
|
||||||
|
zIndex: 11,
|
||||||
|
background: 'linear-gradient(120deg,#141414 0%, #000000 100%)',
|
||||||
|
}}>
|
||||||
|
<Flex h={'100%'} w={'100vw'} direction={'column'} gap={'lg'} pt={'xl'}>
|
||||||
|
<Flex direction={'row'} w={'100%'} gap={'sm'} align={'center'} justify={'space-between'} p={'sm'}>
|
||||||
|
<Flex style={{
|
||||||
|
cursor: 'pointer'
|
||||||
|
}} onClick={() => setShowCallView(false)} justify={'center'} align={'center'}>
|
||||||
|
<IconChevronLeft color="white" size={20}></IconChevronLeft>
|
||||||
|
<Text fw={500} c={'white'}>Back</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Popover width={300} disabled={getKeyCast() == ''} withArrow>
|
||||||
|
<Popover.Target>
|
||||||
|
<IconQrcode color={getKeyCast() == '' ? 'gray' : 'white'} size={24}></IconQrcode>
|
||||||
|
</Popover.Target>
|
||||||
|
<Popover.Dropdown p={'xs'}>
|
||||||
|
<Flex direction={'row'} align={'center'} gap={'xs'}>
|
||||||
|
<Text maw={300} c={'dimmed'} fz={'xs'}>
|
||||||
|
This call is secured by 256 bit end-to-end encryption. Only you and the recipient can read or listen to the content of this call.
|
||||||
|
</Text>
|
||||||
|
<KeyImage radius={0} colors={[
|
||||||
|
theme.colors.blue[1],
|
||||||
|
theme.colors.blue[2],
|
||||||
|
theme.colors.blue[3],
|
||||||
|
theme.colors.blue[4],
|
||||||
|
theme.colors.blue[5]
|
||||||
|
]} size={80} keyRender={getKeyCast()}></KeyImage>
|
||||||
|
</Flex>
|
||||||
|
</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Flex direction={'column'} mt={'xl'} style={{
|
||||||
|
userSelect: 'none'
|
||||||
|
}} w={'100vw'} gap={'sm'} align={'center'} justify={'center'}>
|
||||||
|
<Avatar size={128} bg={avatars.length > 0 ? '#fff' : undefined} src={avatars.length > 0 ? avatars[0].avatar : undefined} color={'initials'} name={userInfo.title}></Avatar>
|
||||||
|
<Text fz={20} fw={'bold'} c={'#FFF'}>{userInfo.title}</Text>
|
||||||
|
{callState == CallState.ACTIVE && (<Text fz={14} c={'#FFF'}>{translateDurationToTime(duration)}</Text>)}
|
||||||
|
{callState == CallState.CONNECTING && (<Text fz={14} c={'#FFF'}>Connecting...</Text>)}
|
||||||
|
{callState == CallState.INCOMING && (<Text fz={14} c={'#FFF'}>Incoming call...</Text>)}
|
||||||
|
{callState == CallState.KEY_EXCHANGE && (<Text fz={14} c={'#FFF'}>Exchanging encryption keys...</Text>)}
|
||||||
|
{callState == CallState.WEB_RTC_EXCHANGE && (<Text fz={14} c={'#FFF'}>Exchanging encryption keys...</Text>)}
|
||||||
|
<Flex gap={'xl'} align={'center'} justify={'center'} mt={'xl'}>
|
||||||
|
{(callState == CallState.ACTIVE
|
||||||
|
|| callState == CallState.WEB_RTC_EXCHANGE
|
||||||
|
|| callState == CallState.CONNECTING
|
||||||
|
|| callState == CallState.KEY_EXCHANGE) && (
|
||||||
|
<>
|
||||||
|
<Box w={50} onClick={() => setSound(!sound)} style={{
|
||||||
|
borderRadius: 25,
|
||||||
|
cursor: 'pointer'
|
||||||
|
}} h={50} bg={sound ? colors.chevrons.active : colors.chevrons.disabled}>
|
||||||
|
<Flex w={'100%'} h={'100%'} justify={'center'} align={'center'}>
|
||||||
|
{!sound && <IconVolume size={24} color={'#fff'}></IconVolume>}
|
||||||
|
{sound && <IconVolumeOff size={24} color={'#fff'}></IconVolumeOff>}
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
<Box w={50} onClick={() => setMuted(!muted)} style={{
|
||||||
|
borderRadius: 25,
|
||||||
|
cursor: 'pointer'
|
||||||
|
}} h={50} bg={!muted ? colors.chevrons.active : colors.chevrons.disabled}>
|
||||||
|
<Flex w={'100%'} h={'100%'} justify={'center'} align={'center'}>
|
||||||
|
{muted && <IconMicrophone size={24} color={'#fff'}></IconMicrophone>}
|
||||||
|
{!muted && <IconMicrophoneOff size={24} color={'#fff'}></IconMicrophoneOff>}
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
<Box w={50} onClick={close} style={{
|
||||||
|
borderRadius: 25,
|
||||||
|
cursor: 'pointer'
|
||||||
|
}} h={50} bg={colors.error}>
|
||||||
|
<Flex w={'100%'} h={'100%'} justify={'center'} align={'center'}>
|
||||||
|
<IconPhoneX size={24} color={'#fff'}></IconPhoneX>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{callState == CallState.INCOMING && (
|
||||||
|
<>
|
||||||
|
{userInfo.title != "Rosetta" && (
|
||||||
|
<Box w={50} onClick={close} style={{
|
||||||
|
borderRadius: 25,
|
||||||
|
cursor: 'pointer'
|
||||||
|
}} h={50} bg={colors.error}>
|
||||||
|
<Flex w={'100%'} h={'100%'} justify={'center'} align={'center'}>
|
||||||
|
<IconX size={24} color={'#fff'}></IconX>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Box w={userInfo.title != "Rosetta" ? 50 : 100} onClick={accept} style={{
|
||||||
|
borderRadius: '50%',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}} h={userInfo.title != "Rosetta" ? 50 : 100} bg={colors.success}>
|
||||||
|
<Flex w={'100%'} h={'100%'} justify={'center'} align={'center'}>
|
||||||
|
<IconPhone size={24} color={'#fff'}></IconPhone>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
import { useRosettaColors } from "@/app/hooks/useRosettaColors";
|
import { useRosettaColors } from "@/app/hooks/useRosettaColors";
|
||||||
import { OnlineState } from "@/app/providers/ProtocolProvider/protocol/packets/packet.onlinestate";
|
import { OnlineState } from "@/app/providers/ProtocolProvider/protocol/packets/packet.onlinestate";
|
||||||
import { usePublicKey } from "@/app/providers/AccountProvider/usePublicKey";
|
import { usePublicKey } from "@/app/providers/AccountProvider/usePublicKey";
|
||||||
import { useBlacklist } from "@/app/providers/BlacklistProvider/useBlacklist";
|
|
||||||
import { useDialog } from "@/app/providers/DialogProvider/useDialog";
|
import { useDialog } from "@/app/providers/DialogProvider/useDialog";
|
||||||
import { useUserInformation } from "@/app/providers/InformationProvider/useUserInformation";
|
import { useUserInformation } from "@/app/providers/InformationProvider/useUserInformation";
|
||||||
import { ProtocolState } from "@/app/providers/ProtocolProvider/ProtocolProvider";
|
import { ProtocolState } from "@/app/providers/ProtocolProvider/ProtocolProvider";
|
||||||
import { useProtocolState } from "@/app/providers/ProtocolProvider/useProtocolState";
|
import { useProtocolState } from "@/app/providers/ProtocolProvider/useProtocolState";
|
||||||
import { Avatar, Box, Divider, Flex, Loader, Text, Tooltip, useComputedColorScheme, useMantineTheme } from "@mantine/core";
|
import { Avatar, Box, Divider, Flex, Loader, Text, useComputedColorScheme, useMantineTheme } from "@mantine/core";
|
||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import { IconBookmark, IconLockAccess, IconLockCancel, IconTrashX } from "@tabler/icons-react";
|
import { IconBookmark, IconPhone, IconTrashX } from "@tabler/icons-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { VerifiedBadge } from "../VerifiedBadge/VerifiedBadge";
|
import { VerifiedBadge } from "../VerifiedBadge/VerifiedBadge";
|
||||||
@@ -20,6 +19,7 @@ import { ReplyHeader } from "../ReplyHeader/ReplyHeader";
|
|||||||
import { useRosettaBreakpoints } from "@/app/hooks/useRosettaBreakpoints";
|
import { useRosettaBreakpoints } from "@/app/hooks/useRosettaBreakpoints";
|
||||||
import { BackToDialogs } from "../BackToDialogs/BackToDialogs";
|
import { BackToDialogs } from "../BackToDialogs/BackToDialogs";
|
||||||
import { useSystemAccounts } from "@/app/providers/SystemAccountsProvider/useSystemAccounts";
|
import { useSystemAccounts } from "@/app/providers/SystemAccountsProvider/useSystemAccounts";
|
||||||
|
import { useCalls } from "@/app/providers/CallProvider/useCalls";
|
||||||
|
|
||||||
|
|
||||||
export function ChatHeader() {
|
export function ChatHeader() {
|
||||||
@@ -29,7 +29,6 @@ export function ChatHeader() {
|
|||||||
const publicKey = usePublicKey();
|
const publicKey = usePublicKey();
|
||||||
const {deleteMessages, dialog} = useDialog();
|
const {deleteMessages, dialog} = useDialog();
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
const [blocked, blockUser, unblockUser] = useBlacklist(dialog);
|
|
||||||
const [opponent, ___, forceUpdateUserInformation] = useUserInformation(dialog);
|
const [opponent, ___, forceUpdateUserInformation] = useUserInformation(dialog);
|
||||||
const [protocolState] = useProtocolState();
|
const [protocolState] = useProtocolState();
|
||||||
const [userTypeing, setUserTypeing] = useState(false);
|
const [userTypeing, setUserTypeing] = useState(false);
|
||||||
@@ -39,6 +38,7 @@ export function ChatHeader() {
|
|||||||
const {lg} = useRosettaBreakpoints();
|
const {lg} = useRosettaBreakpoints();
|
||||||
const systemAccounts = useSystemAccounts();
|
const systemAccounts = useSystemAccounts();
|
||||||
const isSystemAccount = systemAccounts.find((acc) => acc.publicKey == dialog) != undefined;
|
const isSystemAccount = systemAccounts.find((acc) => acc.publicKey == dialog) != undefined;
|
||||||
|
const {call} = useCalls();
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -78,20 +78,6 @@ export function ChatHeader() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClickBlockUser = () => {
|
|
||||||
if(opponent.publicKey != "DELETED"
|
|
||||||
&& opponent.publicKey != publicKey){
|
|
||||||
blockUser();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onClickUnblockUser = () => {
|
|
||||||
if(opponent.publicKey != "DELETED"
|
|
||||||
&& opponent.publicKey != publicKey){
|
|
||||||
unblockUser();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onClickProfile = () => {
|
const onClickProfile = () => {
|
||||||
if(opponent.publicKey != "DELETED" && opponent.publicKey != publicKey){
|
if(opponent.publicKey != "DELETED" && opponent.publicKey != publicKey){
|
||||||
navigate("/main/profile/" + opponent.publicKey);
|
navigate("/main/profile/" + opponent.publicKey);
|
||||||
@@ -116,7 +102,7 @@ export function ChatHeader() {
|
|||||||
onClick={onClickProfile}
|
onClick={onClickProfile}
|
||||||
>
|
>
|
||||||
<IconBookmark stroke={2} size={20}></IconBookmark>
|
<IconBookmark stroke={2} size={20}></IconBookmark>
|
||||||
</Avatar> : <Avatar onClick={onClickProfile} color={'initials'} src={avatars.length > 0 ? avatars[0].avatar : undefined} name={opponent.title}></Avatar>
|
</Avatar> : <Avatar onClick={onClickProfile} bg={avatars.length > 0 ? '#fff' : undefined} color={'initials'} src={avatars.length > 0 ? avatars[0].avatar : undefined} name={opponent.title}></Avatar>
|
||||||
}
|
}
|
||||||
<Flex direction={'column'} onClick={onClickProfile}>
|
<Flex direction={'column'} onClick={onClickProfile}>
|
||||||
<Flex align={'center'} gap={3}>
|
<Flex align={'center'} gap={3}>
|
||||||
@@ -149,32 +135,16 @@ export function ChatHeader() {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex h={'100%'} align={'center'} gap={'sm'}>
|
<Flex h={'100%'} align={'center'} gap={'sm'}>
|
||||||
<Tooltip onClick={onClickClearMessages} withArrow position={'bottom'} label={"Clear all messages"}>
|
<IconPhone
|
||||||
<IconTrashX
|
onClick={() => call(dialog)}
|
||||||
style={{
|
style={{
|
||||||
cursor: 'pointer'
|
cursor: 'pointer'
|
||||||
}} stroke={1.5} color={theme.colors.blue[7]} size={24}></IconTrashX>
|
}} stroke={1.5} color={theme.colors.blue[7]} size={24}></IconPhone>
|
||||||
</Tooltip>
|
<IconTrashX
|
||||||
{publicKey != opponent.publicKey && !blocked && !isSystemAccount && (
|
onClick={onClickClearMessages}
|
||||||
<Tooltip onClick={onClickBlockUser} withArrow position={'bottom'} label={"Block user"}>
|
style={{
|
||||||
<IconLockCancel
|
cursor: 'pointer'
|
||||||
style={{
|
}} stroke={1.5} color={theme.colors.blue[7]} size={24}></IconTrashX>
|
||||||
cursor: 'pointer'
|
|
||||||
}} stroke={1.5} color={theme.colors.red[7]} size={24}
|
|
||||||
>
|
|
||||||
</IconLockCancel>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{blocked && !isSystemAccount && (
|
|
||||||
<Tooltip onClick={onClickUnblockUser} withArrow position={'bottom'} label={"Unblock user"}>
|
|
||||||
<IconLockAccess
|
|
||||||
style={{
|
|
||||||
cursor: 'pointer'
|
|
||||||
}} stroke={1.5} color={theme.colors.green[7]} size={24}
|
|
||||||
>
|
|
||||||
</IconLockAccess>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>}
|
</Flex>}
|
||||||
{replyMessages.messages.length > 0 && !replyMessages.inDialogInput && <ReplyHeader></ReplyHeader>}
|
{replyMessages.messages.length > 0 && !replyMessages.inDialogInput && <ReplyHeader></ReplyHeader>}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export function Dialog(props : DialogProps) {
|
|||||||
<IconBookmark stroke={2} size={20}></IconBookmark>
|
<IconBookmark stroke={2} size={20}></IconBookmark>
|
||||||
</Avatar> :
|
</Avatar> :
|
||||||
<Box style={{ position: 'relative', display: 'inline-block' }}>
|
<Box style={{ position: 'relative', display: 'inline-block' }}>
|
||||||
<Avatar src={avatars.length > 0 ? avatars[0].avatar : undefined} variant={isInCurrentDialog ? 'filled' : 'light'} name={userInfo.title} size={50} color={'initials'} />
|
<Avatar bg={avatars.length > 0 ? '#fff' : undefined} src={avatars.length > 0 ? avatars[0].avatar : undefined} variant={isInCurrentDialog ? 'filled' : 'light'} name={userInfo.title} size={50} color={'initials'} />
|
||||||
{userInfo.online == OnlineState.ONLINE && (
|
{userInfo.online == OnlineState.ONLINE && (
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { DialogsPanelHeader } from '../DialogsPanelHeader/DialogsPanelHeader';
|
|||||||
import { useDialogsList } from '@/app/providers/DialogListProvider/useDialogsList';
|
import { useDialogsList } from '@/app/providers/DialogListProvider/useDialogsList';
|
||||||
import { useVerifyRequest } from '@/app/providers/DeviceProvider/useVerifyRequest';
|
import { useVerifyRequest } from '@/app/providers/DeviceProvider/useVerifyRequest';
|
||||||
import { DeviceVerify } from '../DeviceVerify/DeviceVerify';
|
import { DeviceVerify } from '../DeviceVerify/DeviceVerify';
|
||||||
|
import { ActiveCall } from '../ActiveCall/ActiveCall';
|
||||||
|
import { useViewPanelsState, ViewPanelsState } from '@/app/hooks/useViewPanelsState';
|
||||||
|
|
||||||
export function DialogsPanel() {
|
export function DialogsPanel() {
|
||||||
const [dialogsMode, setDialogsMode] = useState<'all' | 'requests'>('all');
|
const [dialogsMode, setDialogsMode] = useState<'all' | 'requests'>('all');
|
||||||
@@ -18,6 +20,7 @@ export function DialogsPanel() {
|
|||||||
const colors = useRosettaColors();
|
const colors = useRosettaColors();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const device = useVerifyRequest();
|
const device = useVerifyRequest();
|
||||||
|
const [viewState] = useViewPanelsState();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
((async () => {
|
((async () => {
|
||||||
@@ -52,6 +55,9 @@ export function DialogsPanel() {
|
|||||||
direction={'column'}
|
direction={'column'}
|
||||||
justify={'space-between'}
|
justify={'space-between'}
|
||||||
>
|
>
|
||||||
|
{viewState == ViewPanelsState.DIALOGS_PANEL_ONLY && (
|
||||||
|
<ActiveCall></ActiveCall>
|
||||||
|
)}
|
||||||
<Box>
|
<Box>
|
||||||
<DialogsPanelHeader></DialogsPanelHeader>
|
<DialogsPanelHeader></DialogsPanelHeader>
|
||||||
{device && (
|
{device && (
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
left: 12px;
|
left: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
z-index: 10;
|
z-index: 15;
|
||||||
app-region: no-drag;
|
app-region: no-drag;
|
||||||
}
|
}
|
||||||
.close_btn, .minimize_btn, .maximize_btn {
|
.close_btn, .minimize_btn, .maximize_btn {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export function MentionRow(props : MentionRowProps) {
|
|||||||
name={props.title}
|
name={props.title}
|
||||||
variant="light"
|
variant="light"
|
||||||
color="initials"
|
color="initials"
|
||||||
|
bg={avatars.length > 0 ? '#fff' : undefined}
|
||||||
src={avatars.length > 0 ? avatars[0].avatar : null}
|
src={avatars.length > 0 ? avatars[0].avatar : null}
|
||||||
></Avatar>}
|
></Avatar>}
|
||||||
<Flex direction={'column'}>
|
<Flex direction={'column'}>
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ export function MessageAvatar(props: AttachmentProps) {
|
|||||||
height: 60,
|
height: 60,
|
||||||
width: 60,
|
width: 60,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
objectFit: 'cover'
|
objectFit: 'cover',
|
||||||
|
background: '#fff'
|
||||||
}} src={blob}></img>)}
|
}} src={blob}></img>)}
|
||||||
{downloadStatus != DownloadStatus.DOWNLOADED && downloadStatus != DownloadStatus.PENDING && preview.length >= 20 && (
|
{downloadStatus != DownloadStatus.DOWNLOADED && downloadStatus != DownloadStatus.PENDING && preview.length >= 20 && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ export function Message(props: MessageProps) {
|
|||||||
{computedMessageStyle == MessageStyle.ROWS && (
|
{computedMessageStyle == MessageStyle.ROWS && (
|
||||||
<Flex direction={'row'} justify={'space-between'} gap={'sm'}>
|
<Flex direction={'row'} justify={'space-between'} gap={'sm'}>
|
||||||
<Flex direction={'row'} gap={'sm'}>
|
<Flex direction={'row'} gap={'sm'}>
|
||||||
{(!props.avatar_no_render && (md || !props.replyed)) && <Avatar onClick={navigateToUserProfile} src={avatars.length > 0 ? avatars[0].avatar : undefined} name={user.title} variant={props.parent ? 'filled' : 'light'} color="initials"></Avatar>}
|
{(!props.avatar_no_render && (md || !props.replyed)) && <Avatar bg={avatars.length > 0 ? '#fff' : undefined} onClick={navigateToUserProfile} src={avatars.length > 0 ? avatars[0].avatar : undefined} name={user.title} variant={props.parent ? 'filled' : 'light'} color="initials"></Avatar>}
|
||||||
<Flex direction={'column'}>
|
<Flex direction={'column'}>
|
||||||
<Flex direction={'row'} gap={3} align={'center'}>
|
<Flex direction={'row'} gap={3} align={'center'}>
|
||||||
{!props.avatar_no_render && (
|
{!props.avatar_no_render && (
|
||||||
@@ -262,7 +262,7 @@ export function Message(props: MessageProps) {
|
|||||||
return (
|
return (
|
||||||
<Flex direction={props.from_me ? 'row-reverse' : 'row'} gap={'sm'} align={'flex-end'}>
|
<Flex direction={props.from_me ? 'row-reverse' : 'row'} gap={'sm'} align={'flex-end'}>
|
||||||
{(md && props.is_last_message_in_stack) && (
|
{(md && props.is_last_message_in_stack) && (
|
||||||
<Avatar onClick={navigateToUserProfile} src={avatars.length > 0 ? avatars[0].avatar : undefined} name={user.title} color="initials" variant={wallpaper != '' ? 'filled' : 'light'} style={{ flexShrink: 0 }}></Avatar>
|
<Avatar bg={avatars.length > 0 ? '#fff' : undefined} onClick={navigateToUserProfile} src={avatars.length > 0 ? avatars[0].avatar : undefined} name={user.title} color="initials" variant={wallpaper != '' ? 'filled' : 'light'} style={{ flexShrink: 0 }}></Avatar>
|
||||||
)}
|
)}
|
||||||
{(md && !props.is_last_message_in_stack) && (
|
{(md && !props.is_last_message_in_stack) && (
|
||||||
<Box style={{ width: 40, height: 40, flexShrink: 0 }}></Box>
|
<Box style={{ width: 40, height: 40, flexShrink: 0 }}></Box>
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export function UserRow(props: UserRowProps) {
|
|||||||
radius="xl"
|
radius="xl"
|
||||||
name={userInfo.title}
|
name={userInfo.title}
|
||||||
color={'initials'}
|
color={'initials'}
|
||||||
|
bg={avatars.length > 0 ? '#fff' : undefined}
|
||||||
src={avatars.length > 0 ? avatars[0].avatar : undefined}
|
src={avatars.length > 0 ? avatars[0].avatar : undefined}
|
||||||
/>
|
/>
|
||||||
<Flex direction={'column'}>
|
<Flex direction={'column'}>
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
<script>
|
||||||
|
window.global = window;
|
||||||
|
</script>
|
||||||
<script type="module" src="/renderer.tsx"></script>
|
<script type="module" src="/renderer.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
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);
|
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 {
|
return {
|
||||||
setSize,
|
setSize,
|
||||||
setResizeble,
|
setResizeble,
|
||||||
setTheme
|
setTheme,
|
||||||
|
setWindowPriority
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
541
app/providers/CallProvider/CallProvider.tsx
Normal file
541
app/providers/CallProvider/CallProvider.tsx
Normal file
@@ -0,0 +1,541 @@
|
|||||||
|
import { Call } from "@/app/components/Call/Call";
|
||||||
|
import { useConsoleLogger } from "@/app/hooks/useConsoleLogger";
|
||||||
|
import { createContext, useEffect, useRef, useState } from "react";
|
||||||
|
import nacl from 'tweetnacl';
|
||||||
|
import { useSender } from "../ProtocolProvider/useSender";
|
||||||
|
import { PacketSignalPeer, SignalType } from "../ProtocolProvider/protocol/packets/packet.signal.peer";
|
||||||
|
import { usePacket } from "../ProtocolProvider/usePacket";
|
||||||
|
import { usePublicKey } from "../AccountProvider/usePublicKey";
|
||||||
|
import { PacketWebRTC, WebRTCSignalType } from "../ProtocolProvider/protocol/packets/packet.webrtc";
|
||||||
|
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;
|
||||||
|
close: () => void;
|
||||||
|
activeCall: string;
|
||||||
|
callState: CallState;
|
||||||
|
muted: boolean;
|
||||||
|
sound: boolean;
|
||||||
|
setMuted: (muted: boolean) => void;
|
||||||
|
setSound: (sound: boolean) => void;
|
||||||
|
duration: number;
|
||||||
|
setShowCallView: (show: boolean) => void;
|
||||||
|
getKeyCast: () => string;
|
||||||
|
accept: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CallState {
|
||||||
|
CONNECTING,
|
||||||
|
KEY_EXCHANGE,
|
||||||
|
/**
|
||||||
|
* Финальная стадия сигналинга, на которой обе стороны обменялись ключами и теперь устанавливают защищенный канал связи для звонка,
|
||||||
|
* через WebRTC, и готовятся к активному звонку.
|
||||||
|
*/
|
||||||
|
WEB_RTC_EXCHANGE,
|
||||||
|
ACTIVE,
|
||||||
|
ENDED,
|
||||||
|
INCOMING
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CallRole {
|
||||||
|
/**
|
||||||
|
* Вызывающая сторона, которая инициирует звонок
|
||||||
|
*/
|
||||||
|
CALLER,
|
||||||
|
/**
|
||||||
|
* Принимающая сторона, которая отвечает на звонок и принимает его
|
||||||
|
*/
|
||||||
|
CALLEE
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CallContext = createContext<CallContextValue | null>(null);
|
||||||
|
export interface CallProviderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CallProvider(props : CallProviderProps) {
|
||||||
|
const [activeCall, setActiveCall] = useState<string>("");
|
||||||
|
const [callState, setCallState] = useState<CallState>(CallState.ENDED);
|
||||||
|
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");
|
||||||
|
const [sessionKeys, setSessionKeys] = useState<nacl.BoxKeyPair | null>(null);
|
||||||
|
const send = useSender();
|
||||||
|
const publicKey = usePublicKey();
|
||||||
|
const peerConnectionRef = useRef<RTCPeerConnection | null>(null);
|
||||||
|
const roomIdRef = useRef<string>("");
|
||||||
|
|
||||||
|
const roleRef = useRef<CallRole | null>(null);
|
||||||
|
const [sharedSecret, setSharedSecret] = useState<string>("");
|
||||||
|
const iceServersRef = useRef<RTCIceServer[]>([]);
|
||||||
|
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(() => {
|
||||||
|
/**
|
||||||
|
* Нам нужно получить ICE серверы для установки соединения из разных сетей
|
||||||
|
* Получаем их от сервера
|
||||||
|
*/
|
||||||
|
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) => {
|
||||||
|
let iceServers = packet.getIceServers();
|
||||||
|
/**
|
||||||
|
* ICE серверы получены, теперь нужно привести их к форматку клиента и добавить udp и tcp варианты
|
||||||
|
*/
|
||||||
|
let formattedIceServers: RTCIceServer[] = [];
|
||||||
|
for(let i = 0; i < iceServers.length; i++){
|
||||||
|
let server = iceServers[i];
|
||||||
|
formattedIceServers.push({
|
||||||
|
urls: "turn:" + server.url + "?transport=" + server.transport,
|
||||||
|
username: server.username,
|
||||||
|
credential: server.credential
|
||||||
|
});
|
||||||
|
}
|
||||||
|
iceServersRef.current = formattedIceServers;
|
||||||
|
info("Received ICE servers from server, count: " + formattedIceServers.length);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
usePacket(27, async (packet: PacketWebRTC) => {
|
||||||
|
if(callState != CallState.WEB_RTC_EXCHANGE && callState != CallState.ACTIVE){
|
||||||
|
/**
|
||||||
|
* Нет активного звонка или мы не на стадии обмена WebRTC сигналами, игнорируем
|
||||||
|
*/
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const signalType = packet.getSignalType();
|
||||||
|
if(signalType == WebRTCSignalType.ANSWER){
|
||||||
|
/**
|
||||||
|
* Другая сторона (сервер SFU) отправил нам SDP ответ на наш оффер
|
||||||
|
*/
|
||||||
|
const sdp = JSON.parse(packet.getSdpOrCandidate());
|
||||||
|
await peerConnectionRef.current?.setRemoteDescription(new RTCSessionDescription(sdp));
|
||||||
|
if(iceCandidatesBufferRef.current.length > 0){
|
||||||
|
/**
|
||||||
|
* У нас есть буферизированные ICE кандидаты, которые мы получили до установки удаленного описания, теперь мы можем их добавить в PeerConnection
|
||||||
|
*/
|
||||||
|
for(let i = 0; i < iceCandidatesBufferRef.current.length; i++){
|
||||||
|
await peerConnectionRef.current?.addIceCandidate(iceCandidatesBufferRef.current[i]);
|
||||||
|
}
|
||||||
|
iceCandidatesBufferRef.current = [];
|
||||||
|
}
|
||||||
|
info("Received WebRTC answer and set remote description");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(signalType == WebRTCSignalType.ICE_CANDIDATE){
|
||||||
|
/**
|
||||||
|
* Другая сторона отправила нам ICE кандидата для установления WebRTC соединения
|
||||||
|
*/
|
||||||
|
const candidate = JSON.parse(packet.getSdpOrCandidate());
|
||||||
|
console.info(candidate);
|
||||||
|
if(peerConnectionRef.current?.remoteDescription == null){
|
||||||
|
/**
|
||||||
|
* Удаленное описание еще не установлено, буферизуем кандидата, чтобы добавить его после установки удаленного описания
|
||||||
|
*/
|
||||||
|
iceCandidatesBufferRef.current.push(new RTCIceCandidate(candidate));
|
||||||
|
info("Received WebRTC ICE candidate but remote description is not set yet, buffering candidate");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await peerConnectionRef.current?.addIceCandidate(new RTCIceCandidate(candidate));
|
||||||
|
info("Received WebRTC ICE candidate and added to peer connection");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(signalType == WebRTCSignalType.OFFER && peerConnectionRef.current){
|
||||||
|
/**
|
||||||
|
* SFU сервер отправил нам оффер, например при renegotiation, нам нужно его принять и
|
||||||
|
* отправить ответ (ANSWER)
|
||||||
|
*/
|
||||||
|
const sdp = JSON.parse(packet.getSdpOrCandidate());
|
||||||
|
await peerConnectionRef.current?.setRemoteDescription(new RTCSessionDescription(sdp));
|
||||||
|
let answer = await peerConnectionRef.current?.createAnswer();
|
||||||
|
await peerConnectionRef.current?.setLocalDescription(answer);
|
||||||
|
let answerSignal = new PacketWebRTC();
|
||||||
|
answerSignal.setSignalType(WebRTCSignalType.ANSWER);
|
||||||
|
answerSignal.setSdpOrCandidate(JSON.stringify(answer));
|
||||||
|
send(answerSignal);
|
||||||
|
info("Received WebRTC offer, set remote description and sent answer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [activeCall, sessionKeys, callState, roomIdRef]);
|
||||||
|
|
||||||
|
usePacket(26, async (packet: PacketSignalPeer) => {
|
||||||
|
const signalType = packet.getSignalType();
|
||||||
|
if(signalType == SignalType.END_CALL_BECAUSE_BUSY) {
|
||||||
|
openCallsModal("Line is busy, the user is currently on another call. Please try again later.");
|
||||||
|
end();
|
||||||
|
}
|
||||||
|
if(signalType == SignalType.END_CALL_BECAUSE_PEER_DISCONNECTED) {
|
||||||
|
openCallsModal("The connection with the user was lost. The call has ended.")
|
||||||
|
end();
|
||||||
|
}
|
||||||
|
if(activeCall){
|
||||||
|
/**
|
||||||
|
* У нас уже есть активный звонок, игнорируем все сигналы, кроме сигналов от текущего звонка
|
||||||
|
*/
|
||||||
|
if(packet.getSrc() != activeCall && packet.getSrc() != publicKey){
|
||||||
|
console.info("Received signal from " + packet.getSrc() + " but active call is with " + activeCall + ", ignoring");
|
||||||
|
info("Received signal for another call, ignoring");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(signalType == SignalType.END_CALL){
|
||||||
|
/**
|
||||||
|
* Сбросили звонок
|
||||||
|
*/
|
||||||
|
end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(signalType == SignalType.CALL){
|
||||||
|
/**
|
||||||
|
* Нам поступает звонок
|
||||||
|
*/
|
||||||
|
setWindowPriority(true);
|
||||||
|
playSound("ringtone.mp3", true);
|
||||||
|
setActiveCall(packet.getSrc());
|
||||||
|
setCallState(CallState.INCOMING);
|
||||||
|
setShowCallView(true);
|
||||||
|
}
|
||||||
|
if(signalType == SignalType.KEY_EXCHANGE && roleRef.current == CallRole.CALLER){
|
||||||
|
console.info("EXCHANGE SIGNAL RECEIVED, CALLER ROLE");
|
||||||
|
/**
|
||||||
|
* Другая сторона сгенерировала ключи для сессии и отправила нам публичную часть,
|
||||||
|
* теперь мы можем создать общую секретную сессию для шифрования звонка
|
||||||
|
*/
|
||||||
|
const sharedPublic = packet.getSharedPublic();
|
||||||
|
if(!sharedPublic){
|
||||||
|
info("Received key exchange signal without shared public key");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sessionKeys = generateSessionKeys();
|
||||||
|
const computedSharedSecret = nacl.box.before(Buffer.from(sharedPublic, 'hex'), sessionKeys.secretKey);
|
||||||
|
setSharedSecret(Buffer.from(computedSharedSecret).toString('hex'));
|
||||||
|
info("Generated shared secret for call session: " + Buffer.from(computedSharedSecret).toString('hex'));
|
||||||
|
/**
|
||||||
|
* Нам нужно отправить свой публичный ключ другой стороне, чтобы она тоже могла создать общую секретную сессию
|
||||||
|
*/
|
||||||
|
const signalPacket = new PacketSignalPeer();
|
||||||
|
signalPacket.setSrc(publicKey);
|
||||||
|
signalPacket.setDst(packet.getSrc());
|
||||||
|
signalPacket.setSignalType(SignalType.KEY_EXCHANGE);
|
||||||
|
signalPacket.setSharedPublic(Buffer.from(sessionKeys.publicKey).toString('hex'));
|
||||||
|
send(signalPacket);
|
||||||
|
setCallState(CallState.WEB_RTC_EXCHANGE);
|
||||||
|
/**
|
||||||
|
* Создаем комнату на сервере SFU, комнату создает звонящий
|
||||||
|
*/
|
||||||
|
let webRtcSignal = new PacketSignalPeer();
|
||||||
|
webRtcSignal.setSignalType(SignalType.CREATE_ROOM);
|
||||||
|
webRtcSignal.setSrc(publicKey);
|
||||||
|
webRtcSignal.setDst(packet.getSrc());
|
||||||
|
send(webRtcSignal);
|
||||||
|
}
|
||||||
|
if(signalType == SignalType.KEY_EXCHANGE && roleRef.current == CallRole.CALLEE){
|
||||||
|
console.info("EXCHANGE SIGNAL RECEIVED, CALLEE ROLE");
|
||||||
|
/**
|
||||||
|
* Мы отправили свою публичную часть ключа другой стороне,
|
||||||
|
* теперь мы получили ее публичную часть и можем создать общую
|
||||||
|
* секретную сессию для шифрования звонка
|
||||||
|
*/
|
||||||
|
const sharedPublic = packet.getSharedPublic();
|
||||||
|
if(!sharedPublic){
|
||||||
|
info("Received key exchange signal without shared public key");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!sessionKeys){
|
||||||
|
info("Received key exchange signal but session keys are not generated");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const computedSharedSecret = nacl.box.before(Buffer.from(sharedPublic, 'hex'), sessionKeys.secretKey);
|
||||||
|
info("Generated shared secret for call session: " + Buffer.from(computedSharedSecret).toString('hex'));
|
||||||
|
setSharedSecret(Buffer.from(computedSharedSecret).toString('hex'));
|
||||||
|
setCallState(CallState.WEB_RTC_EXCHANGE);
|
||||||
|
}
|
||||||
|
if(signalType == SignalType.CREATE_ROOM) {
|
||||||
|
/**
|
||||||
|
* Создана комната для обмена WebRTC потоками
|
||||||
|
*/
|
||||||
|
roomIdRef.current = packet.getRoomId();
|
||||||
|
info("WebRTC room created with id: " + packet.getRoomId());
|
||||||
|
/**
|
||||||
|
* Нужно отправить свой SDP оффер другой стороне, чтобы установить WebRTC соединение
|
||||||
|
*/
|
||||||
|
peerConnectionRef.current = new RTCPeerConnection({
|
||||||
|
iceServers: iceServersRef.current
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* Подписываемся на ICE кандидат
|
||||||
|
*/
|
||||||
|
peerConnectionRef.current.onicecandidate = (event) => {
|
||||||
|
if(event.candidate){
|
||||||
|
let candidateSignal = new PacketWebRTC();
|
||||||
|
candidateSignal.setSignalType(WebRTCSignalType.ICE_CANDIDATE);
|
||||||
|
candidateSignal.setSdpOrCandidate(JSON.stringify(event.candidate));
|
||||||
|
send(candidateSignal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Соединение установлено, можно начинать звонок, переходим в активное состояние звонка
|
||||||
|
*/
|
||||||
|
peerConnectionRef.current.onconnectionstatechange = () => {
|
||||||
|
console.info("Peer connection state changed: " + peerConnectionRef.current?.connectionState);
|
||||||
|
if(peerConnectionRef.current?.connectionState == "connected"){
|
||||||
|
setCallState(CallState.ACTIVE);
|
||||||
|
info("WebRTC connection established, call is active");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
peerConnectionRef.current.ontrack = (event) => {
|
||||||
|
/**
|
||||||
|
* При получении медиа-трека с другой стороны
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запрашиваем Аудио поток с микрофона и добавляем его в PeerConnection, чтобы другая сторона могла его получить и воспроизвести,
|
||||||
|
* когда мы установим WebRTC соединение
|
||||||
|
*/
|
||||||
|
const localStream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
audio: true
|
||||||
|
});
|
||||||
|
localStream.getTracks().forEach(track => {
|
||||||
|
peerConnectionRef.current?.addTrack(track, localStream);
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* Отправляем свой оффер другой стороне
|
||||||
|
*/
|
||||||
|
let offer = await peerConnectionRef.current.createOffer();
|
||||||
|
await peerConnectionRef.current.setLocalDescription(offer);
|
||||||
|
let offerSignal = new PacketWebRTC();
|
||||||
|
offerSignal.setSignalType(WebRTCSignalType.OFFER);
|
||||||
|
offerSignal.setSdpOrCandidate(JSON.stringify(offer));
|
||||||
|
send(offerSignal);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [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);
|
||||||
|
setSessionKeys(sessionKeys);
|
||||||
|
return sessionKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
const signalPacket = new PacketSignalPeer();
|
||||||
|
signalPacket.setSrc(publicKey);
|
||||||
|
signalPacket.setDst(dialog);
|
||||||
|
signalPacket.setSignalType(SignalType.CALL);
|
||||||
|
send(signalPacket);
|
||||||
|
roleRef.current = CallRole.CALLER;
|
||||||
|
playSound("calling.mp3", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
const packetSignal = new PacketSignalPeer();
|
||||||
|
packetSignal.setSrc(publicKey);
|
||||||
|
packetSignal.setDst(activeCall);
|
||||||
|
packetSignal.setSignalType(SignalType.END_CALL);
|
||||||
|
send(packetSignal);
|
||||||
|
end();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accept = () => {
|
||||||
|
if(callState != CallState.INCOMING){
|
||||||
|
/**
|
||||||
|
* Нечего принимать
|
||||||
|
*/
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setWindowPriority(false);
|
||||||
|
stopLoopSound();
|
||||||
|
stopSound();
|
||||||
|
/**
|
||||||
|
* Звонок принят, генерируем ключи для сессии и отправляем их другой стороне для установления защищенного канала связи
|
||||||
|
*/
|
||||||
|
const keys = generateSessionKeys();
|
||||||
|
const signalPacket = new PacketSignalPeer();
|
||||||
|
signalPacket.setSrc(publicKey);
|
||||||
|
signalPacket.setDst(activeCall);
|
||||||
|
signalPacket.setSignalType(SignalType.KEY_EXCHANGE);
|
||||||
|
signalPacket.setSharedPublic(Buffer.from(keys.publicKey).toString('hex'));
|
||||||
|
send(signalPacket);
|
||||||
|
setCallState(CallState.KEY_EXCHANGE);
|
||||||
|
roleRef.current = CallRole.CALLEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает слепок ключа для отображения в UI
|
||||||
|
* чтобы не показывать настоящий ключ
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const getKeyCast = () => {
|
||||||
|
if(!sharedSecret){
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
activeCall,
|
||||||
|
callState,
|
||||||
|
muted,
|
||||||
|
sound,
|
||||||
|
setMuted,
|
||||||
|
setSound,
|
||||||
|
duration,
|
||||||
|
setShowCallView,
|
||||||
|
getKeyCast,
|
||||||
|
accept
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CallContext.Provider value={context}>
|
||||||
|
{props.children}
|
||||||
|
<audio ref={remoteAudioRef} autoPlay playsInline style={{ display: 'none' }} />
|
||||||
|
{showCallView && <Call context={context}></Call>}
|
||||||
|
</CallContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
5
app/providers/CallProvider/translateDurationTime.ts
Normal file
5
app/providers/CallProvider/translateDurationTime.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const translateDurationToTime = (duration: number) => {
|
||||||
|
const minutes = Math.floor(duration / 60);
|
||||||
|
const seconds = duration % 60;
|
||||||
|
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
|
||||||
|
}
|
||||||
15
app/providers/CallProvider/useCalls.ts
Normal file
15
app/providers/CallProvider/useCalls.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
import { CallContext, CallContextValue } from "./CallProvider";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Хук предоставляет функции для работы с звонками, такие как инициирование звонка, принятие звонка, завершение звонка и т.д.
|
||||||
|
* Он может использоваться в компонентах, связанных с звонками, для управления состоянием звонков и взаимодействия с сервером.
|
||||||
|
*/
|
||||||
|
export function useCalls() : CallContextValue {
|
||||||
|
const context = useContext(CallContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useCalls must be used within a CallProvider");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -295,7 +295,6 @@ export function DialogProvider(props: DialogProviderProps) {
|
|||||||
* Обработчик чтения для личных сообщений
|
* Обработчик чтения для личных сообщений
|
||||||
*/
|
*/
|
||||||
usePacket(0x07, async (packet : PacketRead) => {
|
usePacket(0x07, async (packet : PacketRead) => {
|
||||||
info("Read packet received in dialog provider");
|
|
||||||
const fromPublicKey = packet.getFromPublicKey();
|
const fromPublicKey = packet.getFromPublicKey();
|
||||||
if(fromPublicKey == publicKey){
|
if(fromPublicKey == publicKey){
|
||||||
/**
|
/**
|
||||||
@@ -309,7 +308,10 @@ export function DialogProvider(props: DialogProviderProps) {
|
|||||||
*/
|
*/
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(fromPublicKey != props.dialog && !idle){
|
if(idle){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(fromPublicKey != props.dialog){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setMessages((prev) => prev.map((msg) => {
|
setMessages((prev) => prev.map((msg) => {
|
||||||
@@ -342,7 +344,10 @@ export function DialogProvider(props: DialogProviderProps) {
|
|||||||
*/
|
*/
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(toPublicKey != props.dialog && !idle){
|
if(idle){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(toPublicKey != props.dialog){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setMessages((prev) => prev.map((msg) => {
|
setMessages((prev) => prev.map((msg) => {
|
||||||
|
|||||||
@@ -367,13 +367,11 @@ export function useDialogFiber() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await updateSyncTime(Date.now());
|
await updateSyncTime(Date.now());
|
||||||
console.info("PACKED_READ_IM");
|
|
||||||
await runQuery(`UPDATE messages SET read = 1 WHERE from_public_key = ? AND to_public_key = ? AND account = ?`, [toPublicKey, fromPublicKey, publicKey]);
|
await runQuery(`UPDATE messages SET read = 1 WHERE from_public_key = ? AND to_public_key = ? AND account = ?`, [toPublicKey, fromPublicKey, publicKey]);
|
||||||
console.info("read im with params ", [fromPublicKey, toPublicKey, publicKey]);
|
|
||||||
updateDialog(fromPublicKey);
|
updateDialog(fromPublicKey);
|
||||||
log("Read packet received from " + fromPublicKey + " for " + toPublicKey);
|
|
||||||
addOrUpdateDialogCache(fromPublicKey, getDialogCache(fromPublicKey).map((message) => {
|
addOrUpdateDialogCache(fromPublicKey, getDialogCache(fromPublicKey).map((message) => {
|
||||||
if (message.from_public_key == toPublicKey && !message.readed) {
|
if (message.from_public_key == publicKey && !message.readed) {
|
||||||
console.info("Marking message as read in cache for dialog with " + fromPublicKey);
|
console.info("Marking message as read in cache for dialog with " + fromPublicKey);
|
||||||
console.info({ fromPublicKey, toPublicKey });
|
console.info({ fromPublicKey, toPublicKey });
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useContext, useEffect } from "react";
|
import { useContext } from "react";
|
||||||
import { useMemory } from "../MemoryProvider/useMemory";
|
import { useMemory } from "../MemoryProvider/useMemory";
|
||||||
import { Attachment } from "../ProtocolProvider/protocol/packets/packet.message";
|
import { Attachment } from "../ProtocolProvider/protocol/packets/packet.message";
|
||||||
import { DialogContext } from "./DialogProvider";
|
import { DialogContext } from "./DialogProvider";
|
||||||
@@ -35,8 +35,6 @@ export function useReplyMessages() {
|
|||||||
const {dialog} = context;
|
const {dialog} = context;
|
||||||
|
|
||||||
const selectMessage = (message : MessageReply) => {
|
const selectMessage = (message : MessageReply) => {
|
||||||
console.info("-> ", replyMessages);
|
|
||||||
console.info(message);
|
|
||||||
if(replyMessages.publicKey != dialog){
|
if(replyMessages.publicKey != dialog){
|
||||||
/**
|
/**
|
||||||
* Сброс выбора сообщений из другого диалога
|
* Сброс выбора сообщений из другого диалога
|
||||||
@@ -71,7 +69,6 @@ export function useReplyMessages() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deselectAllMessages = () => {
|
const deselectAllMessages = () => {
|
||||||
console.info("Deselecting all messages");
|
|
||||||
setReplyMessages({
|
setReplyMessages({
|
||||||
publicKey: "",
|
publicKey: "",
|
||||||
messages: []
|
messages: []
|
||||||
@@ -108,16 +105,6 @@ export function useReplyMessages() {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if(replyMessages.publicKey != dialog
|
|
||||||
&& replyMessages.inDialogInput != dialog){
|
|
||||||
/**
|
|
||||||
* Сброс выбора сообщений при смене диалога
|
|
||||||
*/
|
|
||||||
deselectAllMessages();
|
|
||||||
}
|
|
||||||
}, [dialog]);
|
|
||||||
|
|
||||||
return {replyMessages,
|
return {replyMessages,
|
||||||
translateMessagesToDialogInput,
|
translateMessagesToDialogInput,
|
||||||
isSelectionInCurrentDialog,
|
isSelectionInCurrentDialog,
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ export function ImageViewer(props : ImageViewerProps) {
|
|||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
cursor: isDragging ? 'grabbing' : 'grab',
|
cursor: isDragging ? 'grabbing' : 'grab',
|
||||||
transformOrigin: '0 0',
|
transformOrigin: '0 0',
|
||||||
|
background: '#FFF',
|
||||||
transform: `translate(${pos.x}px, ${pos.y}px) scale(${pos.scale})`,
|
transform: `translate(${pos.x}px, ${pos.y}px) scale(${pos.scale})`,
|
||||||
}}
|
}}
|
||||||
onWheel={onWheel}
|
onWheel={onWheel}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import Packet from "../packet";
|
||||||
|
import Stream from "../stream";
|
||||||
|
|
||||||
|
export interface G365IceServer {
|
||||||
|
url: string;
|
||||||
|
username: string;
|
||||||
|
credential: string;
|
||||||
|
transport: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PacketIceServers extends Packet {
|
||||||
|
private iceServers: G365IceServer[] = [];
|
||||||
|
|
||||||
|
public getPacketId(): number {
|
||||||
|
return 28;
|
||||||
|
}
|
||||||
|
|
||||||
|
public _receive(stream: Stream): void {
|
||||||
|
const serversCount = stream.readInt16();
|
||||||
|
this.iceServers = [];
|
||||||
|
for(let i = 0; i < serversCount; i++){
|
||||||
|
const url = stream.readString();
|
||||||
|
const username = stream.readString();
|
||||||
|
const credential = stream.readString();
|
||||||
|
const transport = stream.readString();
|
||||||
|
this.iceServers.push({
|
||||||
|
url,
|
||||||
|
username,
|
||||||
|
credential,
|
||||||
|
transport
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public _send(): Promise<Stream> | Stream {
|
||||||
|
const stream = new Stream();
|
||||||
|
stream.writeInt16(this.getPacketId());
|
||||||
|
stream.writeInt16(this.iceServers.length);
|
||||||
|
for(let i = 0; i < this.iceServers.length; i++){
|
||||||
|
const server = this.iceServers[i];
|
||||||
|
stream.writeString(server.url);
|
||||||
|
stream.writeString(server.username || "");
|
||||||
|
stream.writeString(server.credential || "");
|
||||||
|
stream.writeString(server.transport || "");
|
||||||
|
}
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getIceServers(): G365IceServer[] {
|
||||||
|
return this.iceServers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setIceServers(servers: G365IceServer[]) {
|
||||||
|
this.iceServers = servers;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import Packet from "../packet";
|
||||||
|
import Stream from "../stream";
|
||||||
|
|
||||||
|
export enum SignalType {
|
||||||
|
CALL = 0,
|
||||||
|
KEY_EXCHANGE = 1,
|
||||||
|
ACTIVE_CALL = 2,
|
||||||
|
END_CALL = 3,
|
||||||
|
CREATE_ROOM = 4,
|
||||||
|
END_CALL_BECAUSE_PEER_DISCONNECTED = 5,
|
||||||
|
END_CALL_BECAUSE_BUSY = 6
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Пакет сигналинга, для сигналов WebRTC используется отдельный пакет 27 PacketWebRTCExchange
|
||||||
|
*/
|
||||||
|
export class PacketSignalPeer extends Packet {
|
||||||
|
|
||||||
|
private src: string = "";
|
||||||
|
/**
|
||||||
|
* Назначение
|
||||||
|
*/
|
||||||
|
private dst: string = "";
|
||||||
|
/**
|
||||||
|
* Используется если SignalType == KEY_EXCHANGE, для идентификации сессии обмена ключами
|
||||||
|
*/
|
||||||
|
private sharedPublic: string = "";
|
||||||
|
|
||||||
|
private signalType: SignalType = SignalType.CALL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Используется если SignalType == CREATE_ROOM,
|
||||||
|
* для идентификации комнаты на SFU сервере, в которой будет происходить обмен сигналами
|
||||||
|
* WebRTC для установления P2P соединения между участниками звонка
|
||||||
|
*/
|
||||||
|
private roomId: string = "";
|
||||||
|
|
||||||
|
|
||||||
|
public getPacketId(): number {
|
||||||
|
return 26;
|
||||||
|
}
|
||||||
|
|
||||||
|
public _receive(stream: Stream): void {
|
||||||
|
this.signalType = stream.readInt8();
|
||||||
|
if(this.signalType == SignalType.END_CALL_BECAUSE_BUSY || this.signalType == SignalType.END_CALL_BECAUSE_PEER_DISCONNECTED){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.src = stream.readString();
|
||||||
|
this.dst = stream.readString();
|
||||||
|
if(this.signalType == SignalType.KEY_EXCHANGE){
|
||||||
|
this.sharedPublic = stream.readString();
|
||||||
|
}
|
||||||
|
if(this.signalType == SignalType.CREATE_ROOM){
|
||||||
|
this.roomId = stream.readString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public _send(): Promise<Stream> | Stream {
|
||||||
|
const stream = new Stream();
|
||||||
|
stream.writeInt16(this.getPacketId());
|
||||||
|
stream.writeInt8(this.signalType);
|
||||||
|
if(this.signalType == SignalType.END_CALL_BECAUSE_BUSY || this.signalType == SignalType.END_CALL_BECAUSE_PEER_DISCONNECTED){
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
stream.writeString(this.src);
|
||||||
|
stream.writeString(this.dst);
|
||||||
|
if(this.signalType == SignalType.KEY_EXCHANGE){
|
||||||
|
stream.writeString(this.sharedPublic);
|
||||||
|
}
|
||||||
|
if(this.signalType == SignalType.CREATE_ROOM){
|
||||||
|
stream.writeString(this.roomId);
|
||||||
|
}
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setDst(dst: string) {
|
||||||
|
this.dst = dst;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSharedPublic(sharedPublic: string) {
|
||||||
|
this.sharedPublic = sharedPublic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSignalType(signalType: SignalType) {
|
||||||
|
this.signalType = signalType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getDst(): string {
|
||||||
|
return this.dst;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSharedPublic(): string {
|
||||||
|
return this.sharedPublic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSignalType(): SignalType {
|
||||||
|
return this.signalType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSrc(): string {
|
||||||
|
return this.src;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSrc(src: string) {
|
||||||
|
this.src = src;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRoomId(): string {
|
||||||
|
return this.roomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setRoomId(roomId: string) {
|
||||||
|
this.roomId = roomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import Packet from "../packet";
|
||||||
|
import Stream from "../stream";
|
||||||
|
|
||||||
|
export enum WebRTCSignalType {
|
||||||
|
OFFER = 0,
|
||||||
|
ANSWER = 1,
|
||||||
|
ICE_CANDIDATE = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Пакет для обмена сигналами WebRTC, такими как оффер, ответ и ICE кандидаты.
|
||||||
|
* Используется на стадии WEB_RTC_EXCHANGE в сигналинге звонков.
|
||||||
|
*/
|
||||||
|
export class PacketWebRTC extends Packet {
|
||||||
|
|
||||||
|
private signalType: WebRTCSignalType = WebRTCSignalType.OFFER;
|
||||||
|
private sdpOrCandidate: string = "";
|
||||||
|
|
||||||
|
public getPacketId(): number {
|
||||||
|
return 27;
|
||||||
|
}
|
||||||
|
|
||||||
|
public _receive(stream: Stream): void {
|
||||||
|
this.signalType = stream.readInt8();
|
||||||
|
this.sdpOrCandidate = stream.readString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public _send(): Promise<Stream> | Stream {
|
||||||
|
let stream = new Stream();
|
||||||
|
stream.writeInt16(this.getPacketId());
|
||||||
|
stream.writeInt8(this.signalType);
|
||||||
|
stream.writeString(this.sdpOrCandidate);
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSignalType(type: WebRTCSignalType) {
|
||||||
|
this.signalType = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSignalType(): WebRTCSignalType {
|
||||||
|
return this.signalType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSdpOrCandidate(data: string) {
|
||||||
|
this.sdpOrCandidate = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSdpOrCandidate(): string {
|
||||||
|
return this.sdpOrCandidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -25,6 +25,9 @@ import { PacketDeviceNew } from "./packets/packet.device.new";
|
|||||||
import { PacketDeviceList } from "./packets/packet.device.list";
|
import { PacketDeviceList } from "./packets/packet.device.list";
|
||||||
import { PacketDeviceResolve } from "./packets/packet.device.resolve";
|
import { PacketDeviceResolve } from "./packets/packet.device.resolve";
|
||||||
import { PacketSync } from "./packets/packet.sync";
|
import { PacketSync } from "./packets/packet.sync";
|
||||||
|
import { PacketSignalPeer } from "./packets/packet.signal.peer";
|
||||||
|
import { PacketWebRTC } from "./packets/packet.webrtc";
|
||||||
|
import { PacketIceServers } from "./packets/packet.ice.servers";
|
||||||
|
|
||||||
export default class Protocol extends EventEmitter {
|
export default class Protocol extends EventEmitter {
|
||||||
private serverAddress: string;
|
private serverAddress: string;
|
||||||
@@ -125,6 +128,9 @@ export default class Protocol extends EventEmitter {
|
|||||||
this._supportedPackets.set(0x17, new PacketDeviceList());
|
this._supportedPackets.set(0x17, new PacketDeviceList());
|
||||||
this._supportedPackets.set(0x18, new PacketDeviceResolve());
|
this._supportedPackets.set(0x18, new PacketDeviceResolve());
|
||||||
this._supportedPackets.set(25, new PacketSync());
|
this._supportedPackets.set(25, new PacketSync());
|
||||||
|
this._supportedPackets.set(26, new PacketSignalPeer());
|
||||||
|
this._supportedPackets.set(27, new PacketWebRTC());
|
||||||
|
this._supportedPackets.set(28, new PacketIceServers());
|
||||||
}
|
}
|
||||||
|
|
||||||
private _findWaiters(packetId: number): ((packet: Packet) => void)[] {
|
private _findWaiters(packetId: number): ((packet: Packet) => void)[] {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export const SERVERS = [
|
export const SERVERS = [
|
||||||
//'wss://cdn.rosetta-im.com',
|
//'wss://cdn.rosetta-im.com',
|
||||||
//'ws://10.211.55.2:3000',
|
//'ws://10.211.55.2:3000',
|
||||||
//'ws://127.0.0.1:3000',
|
//'ws://192.168.6.82:3000',
|
||||||
'wss://wss.rosetta.im'
|
'wss://wss.rosetta.im'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
export const APP_VERSION = "1.0.8";
|
export const APP_VERSION = "1.1.0";
|
||||||
export const CORE_MIN_REQUIRED_VERSION = "1.5.0";
|
export const CORE_MIN_REQUIRED_VERSION = "1.5.2";
|
||||||
|
|
||||||
export const RELEASE_NOTICE = `
|
export const RELEASE_NOTICE = `
|
||||||
**Обновление v1.0.8** :emoji_1f631:
|
**Обновление v1.1.0** :emoji_1f631:
|
||||||
- Фикс проблемы с загрузкой аватарок в некоторых случаях
|
- Добавлена поддержка звонков
|
||||||
- Фикс фонового скролла при увеличении картинки
|
- Прозрачным аватаркам добавлена подложка
|
||||||
- Фикс артефактов у картинки
|
- Фикс ошибки чтения
|
||||||
|
- Подложка к вложению аватарки
|
||||||
|
- Обмен ключами шифрования DH
|
||||||
|
- Поддерджка WebRTC
|
||||||
|
- Событийные звуки звонка (сбросить, мутинг, и прочее...)
|
||||||
`;
|
`;
|
||||||
@@ -9,12 +9,13 @@ import { useEffect } from "react";
|
|||||||
import { useViewPanelsState, ViewPanelsState } from "@/app/hooks/useViewPanelsState";
|
import { useViewPanelsState, ViewPanelsState } from "@/app/hooks/useViewPanelsState";
|
||||||
import { GroupHeader } from "@/app/components/GroupHeader/GroupHeader";
|
import { GroupHeader } from "@/app/components/GroupHeader/GroupHeader";
|
||||||
import { useGroups } from "@/app/providers/DialogProvider/useGroups";
|
import { useGroups } from "@/app/providers/DialogProvider/useGroups";
|
||||||
|
import { ActiveCall } from "@/app/components/ActiveCall/ActiveCall";
|
||||||
|
|
||||||
export function Chat() {
|
export function Chat() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const dialog = params.id || "DELETED";
|
const dialog = params.id || "DELETED";
|
||||||
const {lg} = useRosettaBreakpoints();
|
const {lg} = useRosettaBreakpoints();
|
||||||
const [__, setViewState] = useViewPanelsState();
|
const [viewState, setViewState] = useViewPanelsState();
|
||||||
const {hasGroup} = useGroups();
|
const {hasGroup} = useGroups();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -30,6 +31,9 @@ export function Chat() {
|
|||||||
return (<>
|
return (<>
|
||||||
<DialogProvider dialog={dialog} key={dialog}>
|
<DialogProvider dialog={dialog} key={dialog}>
|
||||||
<Flex direction={'column'} justify={'space-between'} h={'100%'}>
|
<Flex direction={'column'} justify={'space-between'} h={'100%'}>
|
||||||
|
{viewState != ViewPanelsState.DIALOGS_PANEL_ONLY && (
|
||||||
|
<ActiveCall></ActiveCall>
|
||||||
|
)}
|
||||||
{/* Group Header */}
|
{/* Group Header */}
|
||||||
{hasGroup(dialog) && <GroupHeader></GroupHeader>}
|
{hasGroup(dialog) && <GroupHeader></GroupHeader>}
|
||||||
{/* Dialog peer to peer Header */}
|
{/* Dialog peer to peer Header */}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { useUpdateMessage } from "@/app/hooks/useUpdateMessage";
|
|||||||
import { useDeviceMessage } from "@/app/hooks/useDeviceMessage";
|
import { useDeviceMessage } from "@/app/hooks/useDeviceMessage";
|
||||||
import { UpdateProvider } from "@/app/providers/UpdateProvider/UpdateProvider";
|
import { UpdateProvider } from "@/app/providers/UpdateProvider/UpdateProvider";
|
||||||
import { useSynchronize } from "@/app/providers/DialogProvider/useSynchronize";
|
import { useSynchronize } from "@/app/providers/DialogProvider/useSynchronize";
|
||||||
|
import { CallProvider } from "@/app/providers/CallProvider/CallProvider";
|
||||||
|
|
||||||
export function Main() {
|
export function Main() {
|
||||||
const { mainColor, borderColor } = useRosettaColors();
|
const { mainColor, borderColor } = useRosettaColors();
|
||||||
@@ -154,52 +155,56 @@ export function Main() {
|
|||||||
<SystemAccountProvider>
|
<SystemAccountProvider>
|
||||||
<TransportProvider>
|
<TransportProvider>
|
||||||
<UpdateProvider>
|
<UpdateProvider>
|
||||||
<Flex direction={'row'} style={{
|
<CallProvider>
|
||||||
height: '100%',
|
<Flex direction={'row'} style={{
|
||||||
width: '100vw',
|
height: '100%',
|
||||||
}}>
|
width: '100vw',
|
||||||
<div style={{
|
|
||||||
display: viewState != ViewPanelsState.DIALOGS_PANEL_HIDE ? 'block' : 'none',
|
|
||||||
width: viewState == ViewPanelsState.DIALOGS_PANEL_ONLY ? '100%' : '300px',
|
|
||||||
}}>
|
}}>
|
||||||
<DialogsPanel></DialogsPanel>
|
<div style={{
|
||||||
</div>
|
display: viewState != ViewPanelsState.DIALOGS_PANEL_HIDE ? 'block' : 'none',
|
||||||
<Divider color={borderColor} orientation={'vertical'}></Divider>
|
width: viewState == ViewPanelsState.DIALOGS_PANEL_ONLY ? '100%' : '300px',
|
||||||
{viewState != ViewPanelsState.DIALOGS_PANEL_ONLY && <Box
|
}}>
|
||||||
bg={mainColor}
|
<DialogsPanel></DialogsPanel>
|
||||||
style={{
|
</div>
|
||||||
flexGrow: 1,
|
{lg && (
|
||||||
height: 'calc(100vh - 27px)',
|
<Divider color={borderColor} orientation={'vertical'}></Divider>
|
||||||
width: `calc(100% - 300px)`,
|
)}
|
||||||
minWidth: 0
|
{viewState != ViewPanelsState.DIALOGS_PANEL_ONLY && <Box
|
||||||
}}
|
bg={mainColor}
|
||||||
>
|
style={{
|
||||||
<Routes>
|
flexGrow: 1,
|
||||||
<Route path={'/chat/:id'} element={<Chat />}></Route>
|
height: 'calc(100vh - 27px)',
|
||||||
<Route path={'/profile/:id'} element={<Profile />}></Route>
|
width: `calc(100% - 300px)`,
|
||||||
<Route path={'/'} element={<DialogPreview />}></Route>
|
minWidth: 0
|
||||||
<Route path={'/theme'} element={<Theme />}></Route>
|
}}
|
||||||
<Route path={'/safety'} element={<Safety />}></Route>
|
>
|
||||||
<Route path={'/update'} element={<Update />}></Route>
|
<Routes>
|
||||||
<Route path={'/backup'} element={<Backup />}></Route>
|
<Route path={'/chat/:id'} element={<Chat />}></Route>
|
||||||
<Route path={'/dialogs'} element={<Dialogs />}></Route>
|
<Route path={'/profile/:id'} element={<Profile />}></Route>
|
||||||
<Route path={'/newgroup'} element={<CreateGroup />}></Route>
|
<Route path={'/'} element={<DialogPreview />}></Route>
|
||||||
<Route path={'/group/:id'} element={<GroupInfo />}></Route>
|
<Route path={'/theme'} element={<Theme />}></Route>
|
||||||
<Route path={'/groupencrypt/:key'} element={<GroupEncryption />}></Route>
|
<Route path={'/safety'} element={<Safety />}></Route>
|
||||||
</Routes>
|
<Route path={'/update'} element={<Update />}></Route>
|
||||||
</Box>}
|
<Route path={'/backup'} element={<Backup />}></Route>
|
||||||
</Flex>
|
<Route path={'/dialogs'} element={<Dialogs />}></Route>
|
||||||
{oldPublicKey && (
|
<Route path={'/newgroup'} element={<CreateGroup />}></Route>
|
||||||
<Overlay blur={8} color="#333">
|
<Route path={'/group/:id'} element={<GroupInfo />}></Route>
|
||||||
<Flex direction={'column'} align={'center'} justify={'center'} h={'100%'}>
|
<Route path={'/groupencrypt/:key'} element={<GroupEncryption />}></Route>
|
||||||
<Alert w={400} variant="filled" color="red" title="Old account">
|
</Routes>
|
||||||
Your account uses an old format public key which is no longer supported. Please create a new account to continue using the application.
|
</Box>}
|
||||||
<br></br>After press "OK" button, the application will close and remove all data.
|
</Flex>
|
||||||
</Alert>
|
{oldPublicKey && (
|
||||||
<Button w={400} mt={'md'} color="red" onClick={dropAccountsAndMessages}>OK</Button>
|
<Overlay blur={8} color="#333">
|
||||||
</Flex>
|
<Flex direction={'column'} align={'center'} justify={'center'} h={'100%'}>
|
||||||
</Overlay>
|
<Alert w={400} variant="filled" color="red" title="Old account">
|
||||||
)}
|
Your account uses an old format public key which is no longer supported. Please create a new account to continue using the application.
|
||||||
|
<br></br>After press "OK" button, the application will close and remove all data.
|
||||||
|
</Alert>
|
||||||
|
<Button w={400} mt={'md'} color="red" onClick={dropAccountsAndMessages}>OK</Button>
|
||||||
|
</Flex>
|
||||||
|
</Overlay>
|
||||||
|
)}
|
||||||
|
</CallProvider>
|
||||||
</UpdateProvider>
|
</UpdateProvider>
|
||||||
</TransportProvider>
|
</TransportProvider>
|
||||||
</SystemAccountProvider>
|
</SystemAccountProvider>
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ self.onmessage = async (event: MessageEvent) => {
|
|||||||
result = await encrypt(payload.data, payload.publicKey);
|
result = await encrypt(payload.data, payload.publicKey);
|
||||||
break;
|
break;
|
||||||
case 'decrypt':
|
case 'decrypt':
|
||||||
console.info("decrypt", payload.privateKey, payload.data);
|
|
||||||
result = await decrypt(payload.data, payload.privateKey);
|
result = await decrypt(payload.data, payload.privateKey);
|
||||||
break;
|
break;
|
||||||
case 'chacha20Encrypt':
|
case 'chacha20Encrypt':
|
||||||
|
|||||||
@@ -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 { join } from 'path'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { WORKING_DIR } from './constants';
|
import { WORKING_DIR } from './constants';
|
||||||
@@ -45,7 +45,8 @@ export function createAppWindow(preloaderWindow?: BrowserWindow): void {
|
|||||||
nodeIntegrationInSubFrames: true,
|
nodeIntegrationInSubFrames: true,
|
||||||
nodeIntegrationInWorker: true,
|
nodeIntegrationInWorker: true,
|
||||||
webSecurity: false,
|
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) {
|
export function foundationIpcRegistration(mainWindow: BrowserWindow) {
|
||||||
|
let bounceId: number | null = null;
|
||||||
ipcMain.removeAllListeners('window-resize');
|
ipcMain.removeAllListeners('window-resize');
|
||||||
ipcMain.removeAllListeners('window-resizeble');
|
ipcMain.removeAllListeners('window-resizeble');
|
||||||
ipcMain.removeAllListeners('window-theme');
|
ipcMain.removeAllListeners('window-theme');
|
||||||
@@ -86,6 +88,38 @@ export function foundationIpcRegistration(mainWindow: BrowserWindow) {
|
|||||||
ipcMain.removeHandler('window-minimize');
|
ipcMain.removeHandler('window-minimize');
|
||||||
ipcMain.removeHandler('showItemInFolder');
|
ipcMain.removeHandler('showItemInFolder');
|
||||||
ipcMain.removeHandler('openExternal');
|
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', () => {
|
ipcMain.handle('open-dev-tools', () => {
|
||||||
if (mainWindow.webContents.isDevToolsOpened()) {
|
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;
|
downloadsPath: string;
|
||||||
deviceName: string;
|
deviceName: string;
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
|
mediaApi: {
|
||||||
|
getSoundUrl: (fileName: string) => string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,23 @@
|
|||||||
import { contextBridge, ipcRenderer, shell } from 'electron'
|
import { contextBridge, ipcRenderer, shell } from 'electron'
|
||||||
import { electronAPI } from '@electron-toolkit/preload'
|
import { electronAPI } from '@electron-toolkit/preload'
|
||||||
import api from './api'
|
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 () => {
|
const exposeContext = async () => {
|
||||||
if (process.contextIsolated) {
|
if (process.contextIsolated) {
|
||||||
try {
|
try {
|
||||||
@@ -16,6 +31,11 @@ const exposeContext = async () => {
|
|||||||
ipcRenderer.invoke('ipcCore:showItemInFolder', fullPath);
|
ipcRenderer.invoke('ipcCore:showItemInFolder', fullPath);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
contextBridge.exposeInMainWorld("mediaApi", {
|
||||||
|
getSoundUrl: (fileName: string) => {
|
||||||
|
return resolveSound(fileName);
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
@@ -23,6 +43,11 @@ const exposeContext = async () => {
|
|||||||
window.electron = electronAPI
|
window.electron = electronAPI
|
||||||
window.api = api;
|
window.api = api;
|
||||||
window.shell = shell;
|
window.shell = shell;
|
||||||
|
window.mediaApi = {
|
||||||
|
getSoundUrl: (fileName: string) => {
|
||||||
|
return resolveSound(fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
14
package.json
14
package.json
@@ -1,11 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "Rosetta",
|
"name": "Rosetta",
|
||||||
"version": "1.5.0",
|
"version": "1.5.2",
|
||||||
"description": "Rosetta Messenger",
|
"description": "Rosetta Messenger",
|
||||||
"main": "./out/main/main.js",
|
"main": "./out/main/main.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"build": {
|
"build": {
|
||||||
"electronUpdaterCompatibility": false,
|
"electronUpdaterCompatibility": false,
|
||||||
|
"extraResources": [
|
||||||
|
{ "from": "resources/", "to": "resources/" }
|
||||||
|
],
|
||||||
"files": [
|
"files": [
|
||||||
"node_modules/sqlite3/**/*",
|
"node_modules/sqlite3/**/*",
|
||||||
"out/main/**/*",
|
"out/main/**/*",
|
||||||
@@ -81,6 +84,8 @@
|
|||||||
"@noble/ciphers": "^1.2.1",
|
"@noble/ciphers": "^1.2.1",
|
||||||
"@noble/secp256k1": "^3.0.0",
|
"@noble/secp256k1": "^3.0.0",
|
||||||
"@tabler/icons-react": "^3.31.0",
|
"@tabler/icons-react": "^3.31.0",
|
||||||
|
"@types/crypto-js": "^4.2.2",
|
||||||
|
"@types/diffie-hellman": "^5.0.3",
|
||||||
"@types/elliptic": "^6.4.18",
|
"@types/elliptic": "^6.4.18",
|
||||||
"@types/node-forge": "^1.3.11",
|
"@types/node-forge": "^1.3.11",
|
||||||
"@types/npm": "^7.19.3",
|
"@types/npm": "^7.19.3",
|
||||||
@@ -90,7 +95,6 @@
|
|||||||
"bip39": "^3.1.0",
|
"bip39": "^3.1.0",
|
||||||
"blurhash": "^2.0.5",
|
"blurhash": "^2.0.5",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"crypto-browserify": "^3.12.1",
|
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"elliptic": "^6.6.1",
|
"elliptic": "^6.6.1",
|
||||||
@@ -103,9 +107,11 @@
|
|||||||
"i": "^0.3.7",
|
"i": "^0.3.7",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
|
"libsodium": "^0.8.2",
|
||||||
"lottie-react": "^2.4.1",
|
"lottie-react": "^2.4.1",
|
||||||
"node-forge": "^1.3.1",
|
"node-forge": "^1.3.1",
|
||||||
"node-machine-id": "^1.1.12",
|
"node-machine-id": "^1.1.12",
|
||||||
|
"npm": "^11.11.0",
|
||||||
"pako": "^2.1.0",
|
"pako": "^2.1.0",
|
||||||
"react-router-dom": "^7.4.0",
|
"react-router-dom": "^7.4.0",
|
||||||
"react-syntax-highlighter": "^16.1.0",
|
"react-syntax-highlighter": "^16.1.0",
|
||||||
@@ -115,6 +121,8 @@
|
|||||||
"recharts": "^2.15.1",
|
"recharts": "^2.15.1",
|
||||||
"sql.js": "^1.13.0",
|
"sql.js": "^1.13.0",
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "^5.1.7",
|
||||||
|
"tweetnacl": "^1.0.3",
|
||||||
|
"tweetnacl-util": "^0.15.1",
|
||||||
"wa-sqlite": "^1.0.0",
|
"wa-sqlite": "^1.0.0",
|
||||||
"web-bip39": "^0.0.3"
|
"web-bip39": "^0.0.3"
|
||||||
},
|
},
|
||||||
@@ -122,6 +130,7 @@
|
|||||||
"@electron-toolkit/eslint-config": "^2.0.0",
|
"@electron-toolkit/eslint-config": "^2.0.0",
|
||||||
"@electron-toolkit/eslint-config-ts": "^3.0.0",
|
"@electron-toolkit/eslint-config-ts": "^3.0.0",
|
||||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||||
|
"@electron/rebuild": "^4.0.3",
|
||||||
"@rushstack/eslint-patch": "^1.10.5",
|
"@rushstack/eslint-patch": "^1.10.5",
|
||||||
"@tailwindcss/vite": "^4.0.9",
|
"@tailwindcss/vite": "^4.0.9",
|
||||||
"@types/node": "^22.13.5",
|
"@types/node": "^22.13.5",
|
||||||
@@ -132,7 +141,6 @@
|
|||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"electron": "^38.3.0",
|
"electron": "^38.3.0",
|
||||||
"electron-builder": "^25.1.8",
|
"electron-builder": "^25.1.8",
|
||||||
"@electron/rebuild": "^4.0.3",
|
|
||||||
"electron-vite": "^3.0.0",
|
"electron-vite": "^3.0.0",
|
||||||
"eslint": "^9.21.0",
|
"eslint": "^9.21.0",
|
||||||
"eslint-plugin-react": "^7.37.4",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
|
|||||||
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