diff --git a/app/App.tsx b/app/App.tsx index 5087969..5d0d7cf 100644 --- a/app/App.tsx +++ b/app/App.tsx @@ -6,9 +6,8 @@ import { ConfirmSeed } from './views/ConfirmSeed/ConfirmSeed'; import { SetPassword } from './views/SetPassword/SetPassword'; import { Main } from './views/Main/Main'; import { ExistsSeed } from './views/ExistsSeed/ExistsSeed'; -import { Box, Divider } from '@mantine/core'; +import { Box } from '@mantine/core'; import './style.css' -import { useRosettaColors } from './hooks/useRosettaColors'; import { Buffer } from 'buffer'; import { InformationProvider } from './providers/InformationProvider/InformationProvider'; import { BlacklistProvider } from './providers/BlacklistProvider/BlacklistProvider'; @@ -27,8 +26,6 @@ window.Buffer = Buffer; export default function App() { const { allAccounts, accountProviderLoaded } = useAccountProvider(); - const colors = useRosettaColors(); - const getViewByLoginState = () => { if (!accountProviderLoaded) { @@ -59,7 +56,6 @@ export default function App() { - diff --git a/app/components/ActiveCall/ActiveCall.module.css b/app/components/ActiveCall/ActiveCall.module.css new file mode 100644 index 0000000..51b0a41 --- /dev/null +++ b/app/components/ActiveCall/ActiveCall.module.css @@ -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... */ \ No newline at end of file diff --git a/app/components/ActiveCall/ActiveCall.tsx b/app/components/ActiveCall/ActiveCall.tsx new file mode 100644 index 0000000..ed7f027 --- /dev/null +++ b/app/components/ActiveCall/ActiveCall.tsx @@ -0,0 +1,95 @@ +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){ + return classes.connecting; + } + if(callState === CallState.ACTIVE){ + return classes.active; + } + return ""; + } + + return ( + <> + setShowCallView(true)}> + + + + {!muted && ( + { + e.stopPropagation(); + setMuted(true); + }} size={16} color={'#fff'}> + )} + {muted && ( + { + e.stopPropagation(); + setMuted(false); + }} size={16} color={'#fff'}> + )} + + + {userInfo?.title || activeCall} + {callState === CallState.CONNECTING && ( + + )} + {callState == CallState.ACTIVE && ( + {translateDurationToTime(duration)} + )} + + + {sound && ( + { + e.stopPropagation(); + setSound(false) + }} color={'#fff'}> + )} + {!sound && ( + { + e.stopPropagation(); + setSound(true) + }} color={'#fff'}> + )} + { + e.stopPropagation(); + close(); + }} size={16} color={'#fff'}> + + + + + + ); +} \ No newline at end of file diff --git a/app/components/Call/Call.tsx b/app/components/Call/Call.tsx new file mode 100644 index 0000000..aa63880 --- /dev/null +++ b/app/components/Call/Call.tsx @@ -0,0 +1,111 @@ +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, Text } from "@mantine/core"; +import { IconChevronLeft, IconMicrophone, IconMicrophoneOff, IconPhone, IconPhoneX, IconQrcode, IconVolume, IconVolumeOff, IconX } from "@tabler/icons-react"; + +export interface CallProps { + context: CallContextValue; +} + +export function Call(props: CallProps) { + const { + activeCall, + duration, + callState, + close, + sound, + setSound, + setMuted, + setShowCallView, + muted} = props.context; + const [userInfo] = useUserInformation(activeCall); + const avatars = useAvatars(activeCall); + const colors = useRosettaColors(); + + return ( + + + + setShowCallView(false)} justify={'center'} align={'center'}> + + Back + + + + + + + 0 ? '#fff' : undefined} src={avatars.length > 0 ? avatars[0].avatar : undefined} color={'initials'} name={userInfo.title}> + {userInfo.title} + {callState == CallState.ACTIVE && ({translateDurationToTime(duration)})} + {callState == CallState.CONNECTING && (Connecting...)} + {callState == CallState.INCOMING && (Incoming call...)} + + {callState == CallState.ACTIVE || callState == CallState.CONNECTING && ( + <> + setSound(!sound)} style={{ + borderRadius: 25, + cursor: 'pointer' + }} h={50} bg={sound ? colors.chevrons.active : colors.chevrons.disabled}> + + {!sound && } + {sound && } + + + setMuted(!muted)} style={{ + borderRadius: 25, + cursor: 'pointer' + }} h={50} bg={!muted ? colors.chevrons.active : colors.chevrons.disabled}> + + {muted && } + {!muted && } + + + + + + + + + )} + {callState == CallState.INCOMING && ( + <> + {userInfo.title != "Rosetta" && ( + + + + + + )} + + + + + + + )} + + + + + ) +} \ No newline at end of file diff --git a/app/components/ChatHeader/ChatHeader.tsx b/app/components/ChatHeader/ChatHeader.tsx index 4b15637..f14023e 100644 --- a/app/components/ChatHeader/ChatHeader.tsx +++ b/app/components/ChatHeader/ChatHeader.tsx @@ -1,14 +1,13 @@ import { useRosettaColors } from "@/app/hooks/useRosettaColors"; import { OnlineState } from "@/app/providers/ProtocolProvider/protocol/packets/packet.onlinestate"; import { usePublicKey } from "@/app/providers/AccountProvider/usePublicKey"; -import { useBlacklist } from "@/app/providers/BlacklistProvider/useBlacklist"; import { useDialog } from "@/app/providers/DialogProvider/useDialog"; import { useUserInformation } from "@/app/providers/InformationProvider/useUserInformation"; import { ProtocolState } from "@/app/providers/ProtocolProvider/ProtocolProvider"; 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 { IconBookmark, IconLockAccess, IconLockCancel, IconTrashX } from "@tabler/icons-react"; +import { IconBookmark, IconPhone, IconTrashX } from "@tabler/icons-react"; import { useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; import { VerifiedBadge } from "../VerifiedBadge/VerifiedBadge"; @@ -20,6 +19,7 @@ import { ReplyHeader } from "../ReplyHeader/ReplyHeader"; import { useRosettaBreakpoints } from "@/app/hooks/useRosettaBreakpoints"; import { BackToDialogs } from "../BackToDialogs/BackToDialogs"; import { useSystemAccounts } from "@/app/providers/SystemAccountsProvider/useSystemAccounts"; +import { useCalls } from "@/app/providers/CallProvider/useCalls"; export function ChatHeader() { @@ -29,7 +29,6 @@ export function ChatHeader() { const publicKey = usePublicKey(); const {deleteMessages, dialog} = useDialog(); const theme = useMantineTheme(); - const [blocked, blockUser, unblockUser] = useBlacklist(dialog); const [opponent, ___, forceUpdateUserInformation] = useUserInformation(dialog); const [protocolState] = useProtocolState(); const [userTypeing, setUserTypeing] = useState(false); @@ -39,6 +38,7 @@ export function ChatHeader() { const {lg} = useRosettaBreakpoints(); const systemAccounts = useSystemAccounts(); const isSystemAccount = systemAccounts.find((acc) => acc.publicKey == dialog) != undefined; + const {call} = useCalls(); 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 = () => { if(opponent.publicKey != "DELETED" && opponent.publicKey != publicKey){ navigate("/main/profile/" + opponent.publicKey); @@ -149,32 +135,16 @@ export function ChatHeader() { - - - - {publicKey != opponent.publicKey && !blocked && !isSystemAccount && ( - - - - - )} - {blocked && !isSystemAccount && ( - - - - - )} + call(dialog)} + style={{ + cursor: 'pointer' + }} stroke={1.5} color={theme.colors.blue[7]} size={24}> + } {replyMessages.messages.length > 0 && !replyMessages.inDialogInput && } diff --git a/app/components/DialogsPanel/DialogsPanel.tsx b/app/components/DialogsPanel/DialogsPanel.tsx index f4bde5e..8c2b8b1 100644 --- a/app/components/DialogsPanel/DialogsPanel.tsx +++ b/app/components/DialogsPanel/DialogsPanel.tsx @@ -10,6 +10,8 @@ import { DialogsPanelHeader } from '../DialogsPanelHeader/DialogsPanelHeader'; import { useDialogsList } from '@/app/providers/DialogListProvider/useDialogsList'; import { useVerifyRequest } from '@/app/providers/DeviceProvider/useVerifyRequest'; import { DeviceVerify } from '../DeviceVerify/DeviceVerify'; +import { ActiveCall } from '../ActiveCall/ActiveCall'; +import { useViewPanelsState, ViewPanelsState } from '@/app/hooks/useViewPanelsState'; export function DialogsPanel() { const [dialogsMode, setDialogsMode] = useState<'all' | 'requests'>('all'); @@ -18,6 +20,7 @@ export function DialogsPanel() { const colors = useRosettaColors(); const navigate = useNavigate(); const device = useVerifyRequest(); + const [viewState] = useViewPanelsState(); useEffect(() => { ((async () => { @@ -52,6 +55,9 @@ export function DialogsPanel() { direction={'column'} justify={'space-between'} > + {viewState == ViewPanelsState.DIALOGS_PANEL_ONLY && ( + + )} {device && ( diff --git a/app/components/MacFrameButtons/MacFrameButtons.module.css b/app/components/MacFrameButtons/MacFrameButtons.module.css index b00a813..ef4bb89 100644 --- a/app/components/MacFrameButtons/MacFrameButtons.module.css +++ b/app/components/MacFrameButtons/MacFrameButtons.module.css @@ -4,7 +4,7 @@ left: 12px; display: flex; gap: 8px; - z-index: 10; + z-index: 15; app-region: no-drag; } .close_btn, .minimize_btn, .maximize_btn { diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx new file mode 100644 index 0000000..e532c14 --- /dev/null +++ b/app/providers/CallProvider/CallProvider.tsx @@ -0,0 +1,70 @@ +import { Call } from "@/app/components/Call/Call"; +import { createContext, useState } from "react"; + + +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; +} + +export enum CallState { + CONNECTING, + ACTIVE, + ENDED, + INCOMING +} + +export const CallContext = createContext(null); +export interface CallProviderProps { + children: React.ReactNode; +} + +export function CallProvider(props : CallProviderProps) { + const [activeCall, setActiveCall] = useState(""); + const [callState, setCallState] = useState(CallState.ENDED); + const [muted, setMuted] = useState(false); + const [sound, setSound] = useState(true); + const [duration, setDuration] = useState(0); + const [showCallView, setShowCallView] = useState(callState == CallState.INCOMING); + + const call = (dialog: string) => { + setActiveCall(dialog); + setCallState(CallState.CONNECTING); + setShowCallView(true); + } + + const close = () => { + setActiveCall(""); + setCallState(CallState.ENDED); + setShowCallView(false); + setDuration(0); + } + + const context = { + call, + close, + activeCall, + callState, + muted, + sound, + setMuted, + setSound, + duration, + setShowCallView + }; + + return ( + + {props.children} + {showCallView && } + + ) +} \ No newline at end of file diff --git a/app/providers/CallProvider/translateDurationTime.ts b/app/providers/CallProvider/translateDurationTime.ts new file mode 100644 index 0000000..fe27634 --- /dev/null +++ b/app/providers/CallProvider/translateDurationTime.ts @@ -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}`; +} \ No newline at end of file diff --git a/app/providers/CallProvider/useCalls.ts b/app/providers/CallProvider/useCalls.ts new file mode 100644 index 0000000..45e3aea --- /dev/null +++ b/app/providers/CallProvider/useCalls.ts @@ -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; +} \ No newline at end of file diff --git a/app/views/Chat/Chat.tsx b/app/views/Chat/Chat.tsx index 5196d8e..6e393f5 100644 --- a/app/views/Chat/Chat.tsx +++ b/app/views/Chat/Chat.tsx @@ -9,12 +9,13 @@ import { useEffect } from "react"; import { useViewPanelsState, ViewPanelsState } from "@/app/hooks/useViewPanelsState"; import { GroupHeader } from "@/app/components/GroupHeader/GroupHeader"; import { useGroups } from "@/app/providers/DialogProvider/useGroups"; +import { ActiveCall } from "@/app/components/ActiveCall/ActiveCall"; export function Chat() { const params = useParams(); const dialog = params.id || "DELETED"; const {lg} = useRosettaBreakpoints(); - const [__, setViewState] = useViewPanelsState(); + const [viewState, setViewState] = useViewPanelsState(); const {hasGroup} = useGroups(); useEffect(() => { @@ -30,6 +31,9 @@ export function Chat() { return (<> + {viewState != ViewPanelsState.DIALOGS_PANEL_ONLY && ( + + )} {/* Group Header */} {hasGroup(dialog) && } {/* Dialog peer to peer Header */} diff --git a/app/views/Main/Main.tsx b/app/views/Main/Main.tsx index fdb45d1..847bb6d 100644 --- a/app/views/Main/Main.tsx +++ b/app/views/Main/Main.tsx @@ -31,6 +31,7 @@ import { useUpdateMessage } from "@/app/hooks/useUpdateMessage"; import { useDeviceMessage } from "@/app/hooks/useDeviceMessage"; import { UpdateProvider } from "@/app/providers/UpdateProvider/UpdateProvider"; import { useSynchronize } from "@/app/providers/DialogProvider/useSynchronize"; +import { CallProvider } from "@/app/providers/CallProvider/CallProvider"; export function Main() { const { mainColor, borderColor } = useRosettaColors(); @@ -154,52 +155,56 @@ export function Main() { - -
+ - -
- - {viewState != ViewPanelsState.DIALOGS_PANEL_ONLY && - - }> - }> - }> - }> - }> - }> - }> - }> - }> - }> - }> - - } -
- {oldPublicKey && ( - - - - Your account uses an old format public key which is no longer supported. Please create a new account to continue using the application. -

After press "OK" button, the application will close and remove all data. -
- -
-
- )} +
+ +
+ {lg && ( + + )} + {viewState != ViewPanelsState.DIALOGS_PANEL_ONLY && + + }> + }> + }> + }> + }> + }> + }> + }> + }> + }> + }> + + } +
+ {oldPublicKey && ( + + + + Your account uses an old format public key which is no longer supported. Please create a new account to continue using the application. +

After press "OK" button, the application will close and remove all data. +
+ +
+
+ )} +
diff --git a/package.json b/package.json index 153dc92..4c591dc 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,7 @@ "@electron-toolkit/eslint-config": "^2.0.0", "@electron-toolkit/eslint-config-ts": "^3.0.0", "@electron-toolkit/tsconfig": "^1.0.1", + "@electron/rebuild": "^4.0.3", "@rushstack/eslint-patch": "^1.10.5", "@tailwindcss/vite": "^4.0.9", "@types/node": "^22.13.5", @@ -132,7 +133,6 @@ "@vitejs/plugin-react": "^4.3.4", "electron": "^38.3.0", "electron-builder": "^25.1.8", - "@electron/rebuild": "^4.0.3", "electron-vite": "^3.0.0", "eslint": "^9.21.0", "eslint-plugin-react": "^7.37.4",