1.1.0-1.5.2 #16

Merged
Royce59 merged 30 commits from dev into main 2026-03-18 17:59:15 +00:00
44 changed files with 1348 additions and 138 deletions

View File

@@ -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>

View File

@@ -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 ?

View 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... */

View 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>
</>
);
}

View 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>
)
}

View File

@@ -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>}

View File

@@ -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={{

View File

@@ -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 && (

View File

@@ -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 {

View File

@@ -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'}>

View File

@@ -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 && (
<> <>

View File

@@ -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>

View File

@@ -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'}>

View File

@@ -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
View 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
}
}

View File

@@ -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
} }
} }

View 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>
)
}

View 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}`;
}

View 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;
}

View File

@@ -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) => {

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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)[] {

View File

@@ -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'
]; ];

View File

@@ -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
- Событийные звуки звонка (сбросить, мутинг, и прочее...)
`; `;

View File

@@ -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 */}

View File

@@ -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>

View File

@@ -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':

View File

@@ -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()) {

View File

@@ -13,5 +13,8 @@ declare global {
downloadsPath: string; downloadsPath: string;
deviceName: string; deviceName: string;
deviceId: string; deviceId: string;
mediaApi: {
getSoundUrl: (fileName: string) => string;
};
} }
} }

View File

@@ -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);
}
}
} }
} }

View File

@@ -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",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.