Compare commits

..

8 Commits

Author SHA1 Message Date
8ffa66920e 1.0.5-1.5.0
Some checks failed
Windows Kernel Build / build (push) Failing after 4m26s
MacOS Kernel Build / build (push) Failing after 12m6s
Linux Kernel Build / build (push) Has been cancelled
SP Builds / build (push) Has been cancelled
Reviewed-on: #6
2026-02-20 16:26:06 +00:00
RoyceDa
48909a7eeb Поднятие версии 2026-02-20 18:23:48 +02:00
RoyceDa
9379bd656d Ускоренная загрузка диалогов 2026-02-20 18:17:47 +02:00
RoyceDa
142082ba83 Исправлен скролл вниз при подгрузке новых сообщений 2026-02-20 18:01:22 +02:00
RoyceDa
c64a7005d3 Оптимизирован код вложений, исправлена установка аватарки на диалог, а не у отправителя 2026-02-20 17:43:56 +02:00
RoyceDa
db72246e5a Flip-анимация перемещения диалогов 2026-02-20 17:21:09 +02:00
RoyceDa
d53c987e7e Фикс уведомлений при синхронизации 2026-02-20 16:38:22 +02:00
RoyceDa
2e18d489be Оптимизация ядра, исправление гонки потоков при получении версии 2026-02-20 16:34:54 +02:00
28 changed files with 268 additions and 151 deletions

View File

@@ -10,8 +10,6 @@ on:
paths: paths:
- 'lib/**' - 'lib/**'
jobs: jobs:
build: build:
runs-on: windows-latest runs-on: windows-latest

View File

@@ -4,7 +4,7 @@ import animationData from './lottie.json';
import { Box, Flex, Skeleton, Text } from "@mantine/core"; import { Box, Flex, Skeleton, Text } from "@mantine/core";
import { useDialogsList } from "@/app/providers/DialogListProvider/useDialogsList"; import { useDialogsList } from "@/app/providers/DialogListProvider/useDialogsList";
import { GroupDialog } from "../GroupDialog/GroupDialog"; import { GroupDialog } from "../GroupDialog/GroupDialog";
import React from "react"; import { AnimatePresence, motion } from "framer-motion";
interface DialogsListProps { interface DialogsListProps {
mode: 'all' | 'requests'; mode: 'all' | 'requests';
@@ -13,6 +13,7 @@ interface DialogsListProps {
export function DialogsList(props : DialogsListProps) { export function DialogsList(props : DialogsListProps) {
const {dialogs, loadingDialogs} = useDialogsList(); const {dialogs, loadingDialogs} = useDialogsList();
const filteredDialogs = dialogs.filter(v => (v.is_request == (props.mode == 'requests')));
return ( return (
<> <>
@@ -36,21 +37,30 @@ export function DialogsList(props : DialogsListProps) {
))} ))}
</> </>
)} )}
{loadingDialogs === 0 && dialogs.filter(v => (v.is_request == (props.mode == 'requests'))).map((dialog) => ( <motion.div style={{display: 'flex', flexDirection: 'column'}}>
<React.Fragment key={dialog.dialog_id}> <AnimatePresence mode="popLayout">
{dialog.dialog_id.startsWith('#group:') ? ( {loadingDialogs === 0 && filteredDialogs.map((dialog) => (
<GroupDialog <motion.div
onClickDialog={props.onSelectDialog} key={dialog.dialog_id}
{...dialog} layout
/> initial={false}
) : ( transition={{ duration: 0.1, ease: 'easeInOut' }}
<Dialog >
onClickDialog={props.onSelectDialog} {dialog.dialog_id.startsWith('#group:') ? (
{...dialog} <GroupDialog
/> onClickDialog={props.onSelectDialog}
)} {...dialog}
</React.Fragment> />
))} ) : (
<Dialog
onClickDialog={props.onSelectDialog}
{...dialog}
/>
)}
</motion.div>
))}
</AnimatePresence>
</motion.div>
</> </>
); );
} }

View File

@@ -7,14 +7,16 @@ import { useHotkeys } from "@mantine/hooks";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { usePublicKey } from "@/app/providers/AccountProvider/usePublicKey"; import { usePublicKey } from "@/app/providers/AccountProvider/usePublicKey";
import { DialogHeaderText } from "../DialogHeaderText/DialogHeaderText"; import { DialogHeaderText } from "../DialogHeaderText/DialogHeaderText";
import { useCoreDevice } from "@/app/providers/DeviceProvider/useCoreDevice";
export function DialogsPanelHeader() { export function DialogsPanelHeader() {
const colors = useRosettaColors(); const colors = useRosettaColors();
const logout = useLogout(); const logout = useLogout();
const navigate = useNavigate(); const navigate = useNavigate();
const publicKey = usePublicKey(); const publicKey = usePublicKey();
const viewKeys = window.platform == 'darwin' ? '⌘' : 'Ctrl+'; const {platform} = useCoreDevice();
const triggerKeys = window.platform == 'darwin' ? 'mod' : 'Ctrl'; const viewKeys = platform == 'darwin' ? '' : 'Ctrl+';
const triggerKeys = platform == 'darwin' ? 'mod' : 'Ctrl';
useHotkeys([ useHotkeys([
[`${triggerKeys}+L`, () => logout()], [`${triggerKeys}+L`, () => logout()],

View File

@@ -42,6 +42,7 @@ export function MessageAttachments(props: MessageAttachmentsProps) {
text: props.text, text: props.text,
parent: props.parent, parent: props.parent,
} }
console.info("Rendering attachment", attachProps);
switch (att.type) { switch (att.type) {
case AttachmentType.MESSAGES: case AttachmentType.MESSAGES:
return <MessageReplyMessages {...attachProps} key={index}></MessageReplyMessages> return <MessageReplyMessages {...attachProps} key={index}></MessageReplyMessages>

View File

@@ -19,7 +19,7 @@ export function MessageAvatar(props: AttachmentProps) {
download, download,
downloadStatus, downloadStatus,
getBlob, getBlob,
getPreview} = useAttachment(props.attachment, props.chacha_key_plain); getPreview} = useAttachment(props.attachment, props.parent);
const mainRef = useRef<HTMLDivElement>(null); const mainRef = useRef<HTMLDivElement>(null);
const { open } = useImageViewer(); const { open } = useImageViewer();
const preview = getPreview(); const preview = getPreview();

View File

@@ -6,6 +6,7 @@ import { IconArrowDown, IconFile, IconX } from "@tabler/icons-react";
import { dotCenterIfNeeded, humanFilesize } from "@/app/utils/utils"; import { dotCenterIfNeeded, humanFilesize } from "@/app/utils/utils";
import { AnimatedRoundedProgress } from "../AnimatedRoundedProgress/AnimatedRoundedProgress"; import { AnimatedRoundedProgress } from "../AnimatedRoundedProgress/AnimatedRoundedProgress";
import { DeliveredMessageState } from "@/app/providers/DialogProvider/DialogProvider"; import { DeliveredMessageState } from "@/app/providers/DialogProvider/DialogProvider";
import { useCore } from "@/app/hooks/useCore";
export function MessageFile(props : AttachmentProps) { export function MessageFile(props : AttachmentProps) {
const colors = useRosettaColors(); const colors = useRosettaColors();
@@ -18,7 +19,7 @@ export function MessageFile(props : AttachmentProps) {
} = } =
useAttachment( useAttachment(
props.attachment, props.attachment,
props.chacha_key_plain, props.parent,
); );
const preview = getPreview(); const preview = getPreview();
const error = downloadStatus == DownloadStatus.ERROR; const error = downloadStatus == DownloadStatus.ERROR;
@@ -27,15 +28,15 @@ export function MessageFile(props : AttachmentProps) {
const filetype = filename.split(".")[filename.split(".").length - 1]; const filetype = filename.split(".")[filename.split(".").length - 1];
const isEncrypting = props.delivered == DeliveredMessageState.WAITING && uploadedPercentage <= 0; const isEncrypting = props.delivered == DeliveredMessageState.WAITING && uploadedPercentage <= 0;
const isUploading = props.delivered == DeliveredMessageState.WAITING && uploadedPercentage > 0 && uploadedPercentage < 100; const isUploading = props.delivered == DeliveredMessageState.WAITING && uploadedPercentage > 0 && uploadedPercentage < 100;
const {getDownloadsPath} = useCore();
const onClick = async () => { const onClick = async () => {
if(downloadStatus == DownloadStatus.ERROR){ if(downloadStatus == DownloadStatus.ERROR){
return; return;
} }
if(downloadStatus == DownloadStatus.DOWNLOADED){ if(downloadStatus == DownloadStatus.DOWNLOADED){
//let content = await getBlob(); const downloadsPath = await getDownloadsPath();
//let buffer = Buffer.from(content.split(",")[1], 'base64'); let pathInDownloads = downloadsPath + "/Rosetta Downloads/" + filename;
let pathInDownloads = window.downloadsPath + "/Rosetta Downloads/" + filename;
//await writeFile(pathInDownloads, buffer, false); //await writeFile(pathInDownloads, buffer, false);
window.shell.showItemInFolder(pathInDownloads); window.shell.showItemInFolder(pathInDownloads);
return; return;

View File

@@ -19,7 +19,7 @@ export function MessageImage(props: AttachmentProps) {
download, download,
downloadStatus, downloadStatus,
getBlob, getBlob,
getPreview } = useAttachment(props.attachment, props.chacha_key_plain); getPreview } = useAttachment(props.attachment, props.parent);
const mainRef = useRef<HTMLDivElement>(null); const mainRef = useRef<HTMLDivElement>(null);
const error = downloadStatus == DownloadStatus.ERROR; const error = downloadStatus == DownloadStatus.ERROR;
const { open } = useImageViewer(); const { open } = useImageViewer();
@@ -29,6 +29,7 @@ export function MessageImage(props: AttachmentProps) {
const [blurhashPreview, setBlurhashPreview] = useState(""); const [blurhashPreview, setBlurhashPreview] = useState("");
useEffect(() => { useEffect(() => {
console.info("Consturcting image, download status: " + downloadStatus);
constructBlob(); constructBlob();
constructFromBlurhash(); constructFromBlurhash();
}, [downloadStatus]); }, [downloadStatus]);

View File

@@ -21,6 +21,7 @@ export function Messages() {
const isFirstRenderRef = useRef(true); const isFirstRenderRef = useRef(true);
const previousScrollHeightRef = useRef(0); const previousScrollHeightRef = useRef(0);
const distanceFromButtomRef = useRef(0); const distanceFromButtomRef = useRef(0);
const distanceFromTopRef = useRef(0);
const [affix, setAffix] = useState(false); const [affix, setAffix] = useState(false);
const [wallpaper] = useSetting<string> const [wallpaper] = useSetting<string>
@@ -120,7 +121,8 @@ export function Messages() {
const lastMessage = messages[messages.length - 1]; const lastMessage = messages[messages.length - 1];
// Скроллим если пользователь внизу или это его собственное сообщение // Скроллим если пользователь внизу или это его собственное сообщение
if ((shouldAutoScrollRef.current || lastMessage.from_me)) { if ((shouldAutoScrollRef.current || lastMessage.from_me) && distanceFromTopRef.current > 10) {
console.info(distanceFromTopRef.current);
scrollToBottom(true); scrollToBottom(true);
} }
}, [messages.length, loading, scrollToBottom]); }, [messages.length, loading, scrollToBottom]);
@@ -175,6 +177,8 @@ export function Messages() {
onScrollPositionChange={(scroll) => { onScrollPositionChange={(scroll) => {
if (!viewportRef.current) return; if (!viewportRef.current) return;
distanceFromTopRef.current = scroll.y;
// Загружаем старые сообщения при достижении верха // Загружаем старые сообщения при достижении верха
if (scroll.y === 0 && !loading && messages.length >= 20) { if (scroll.y === 0 && !loading && messages.length >= 20) {
loadMessagesToScrollAreaTop(); loadMessagesToScrollAreaTop();

View File

@@ -5,17 +5,19 @@ import { useProtocolState } from "@/app/providers/ProtocolProvider/useProtocolSt
import { ProtocolState } from "@/app/providers/ProtocolProvider/ProtocolProvider"; import { ProtocolState } from "@/app/providers/ProtocolProvider/ProtocolProvider";
import { WindowsFrameButtons } from "../WindowsFrameButtons/WindowsFrameButtons"; import { WindowsFrameButtons } from "../WindowsFrameButtons/WindowsFrameButtons";
import { MacFrameButtons } from "../MacFrameButtons/MacFrameButtons"; import { MacFrameButtons } from "../MacFrameButtons/MacFrameButtons";
import { useCoreDevice } from "@/app/providers/DeviceProvider/useCoreDevice";
export function Topbar() { export function Topbar() {
const colors = useRosettaColors(); const colors = useRosettaColors();
const [protocolState] = useProtocolState(); const [protocolState] = useProtocolState();
const {platform} = useCoreDevice();
return ( return (
<Box className={classes.drag} ta={'center'} p={3} bg={colors.mainColor}> <Box className={classes.drag} ta={'center'} p={3} bg={colors.mainColor}>
{window.platform == 'win32' && <WindowsFrameButtons></WindowsFrameButtons>} {platform == 'win32' && <WindowsFrameButtons></WindowsFrameButtons>}
{window.platform == 'darwin' && <MacFrameButtons></MacFrameButtons>} {platform == 'darwin' && <MacFrameButtons></MacFrameButtons>}
{window.platform == 'linux' && <WindowsFrameButtons></WindowsFrameButtons>} {platform == 'linux' && <WindowsFrameButtons></WindowsFrameButtons>}
{(protocolState == ProtocolState.CONNECTED || protocolState == ProtocolState.SYNCHRONIZATION || !window.location.hash.includes("main")) && {(protocolState == ProtocolState.CONNECTED || protocolState == ProtocolState.SYNCHRONIZATION || !window.location.hash.includes("main")) &&
<Flex align={'center'} justify={'center'}> <Flex align={'center'} justify={'center'}>
<Text fw={'bolder'} fz={13} c={'gray'}> <Text fw={'bolder'} fz={13} c={'gray'}>

View File

@@ -1,13 +1,7 @@
import { AttachmentType } from "./providers/ProtocolProvider/protocol/packets/packet.message"; import { AttachmentType } from "./providers/ProtocolProvider/protocol/packets/packet.message";
export const CORE_VERSION = window.version || "1.0.0";
/** /**
* Application directives * Application directives
*/ */
export const APPLICATION_PLATFROM = window.platform || "unknown";
export const APPLICATION_ARCH = window.arch || "unknown";
export const APP_PATH = window.appPath || ".";
export const SIZE_LOGIN_WIDTH_PX = 300; export const SIZE_LOGIN_WIDTH_PX = 300;
export const DEVTOOLS_CHEATCODE = "rosettadev1"; export const DEVTOOLS_CHEATCODE = "rosettadev1";
export const AVATAR_PASSWORD_TO_ENCODE = "rosetta-a"; export const AVATAR_PASSWORD_TO_ENCODE = "rosetta-a";
@@ -62,5 +56,6 @@ export const ALLOWED_DOMAINS_ZONES = [
'gg', 'gg',
'fm', 'fm',
'tv', 'tv',
'im' 'im',
'sc'
]; ];

62
app/hooks/useCore.ts Normal file
View File

@@ -0,0 +1,62 @@
export function useCore() {
const openExternal = (url: string) => {
window.shell.openExternal(url);
};
const showItemInFolder = (fullPath: string) => {
window.shell.showItemInFolder(fullPath);
};
const getCoreVersion = async () => {
const version = await window.electron.ipcRenderer.invoke('ipcCore:getCoreVersion');
return version;
}
const getArch = async () => {
const arch = await window.electron.ipcRenderer.invoke('ipcCore:getArch');
return arch;
}
const getUserDir = async () => {
const userDir = await window.electron.ipcRenderer.invoke('ipcCore:getUserDir');
return userDir;
}
const getAppPath = async () => {
const appPath = await window.electron.ipcRenderer.invoke('ipcCore:getAppPath');
return appPath;
}
const getDownloadsPath = async () => {
const downloadsPath = await window.electron.ipcRenderer.invoke('ipcCore:getDownloadsPath');
return downloadsPath;
}
const getPlatform = async () => {
const platform = await window.electron.ipcRenderer.invoke('ipcCore:getPlatform');
return platform;
}
const getDeviceName = async () => {
const deviceName = await window.electron.ipcRenderer.invoke('device:name');
return deviceName;
}
const getDeviceId = async () => {
const deviceId = await window.electron.ipcRenderer.invoke('device:id');
return deviceId;
}
return {
openExternal,
showItemInFolder,
getCoreVersion,
getArch,
getUserDir,
getAppPath,
getDownloadsPath,
getPlatform,
getDeviceName,
getDeviceId
}
}

View File

View File

@@ -1,4 +1,4 @@
import { useContext, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useDownloadStatus } from "../TransportProvider/useDownloadStatus"; import { useDownloadStatus } from "../TransportProvider/useDownloadStatus";
import { useUploadStatus } from "../TransportProvider/useUploadStatus"; import { useUploadStatus } from "../TransportProvider/useUploadStatus";
import { useFileStorage } from "../../hooks/useFileStorage"; import { useFileStorage } from "../../hooks/useFileStorage";
@@ -10,10 +10,11 @@ import { useDialogsCache } from "../DialogProvider/useDialogsCache";
import { useConsoleLogger } from "../../hooks/useConsoleLogger"; import { useConsoleLogger } from "../../hooks/useConsoleLogger";
import { Attachment, AttachmentType } from "../ProtocolProvider/protocol/packets/packet.message"; import { Attachment, AttachmentType } from "../ProtocolProvider/protocol/packets/packet.message";
import { useMemory } from "../MemoryProvider/useMemory"; import { useMemory } from "../MemoryProvider/useMemory";
import { DialogContext } from "../DialogProvider/DialogProvider";
import { useSaveAvatar } from "../AvatarProvider/useSaveAvatar"; import { useSaveAvatar } from "../AvatarProvider/useSaveAvatar";
import { AVATAR_PASSWORD_TO_ENCODE } from "@/app/constants"; import { AVATAR_PASSWORD_TO_ENCODE } from "@/app/constants";
import { useDialog } from "../DialogProvider/useDialog"; import { useDialog } from "../DialogProvider/useDialog";
import { useCore } from "@/app/hooks/useCore";
import { MessageProps } from "@/app/components/Messages/Message";
export enum DownloadStatus { export enum DownloadStatus {
DOWNLOADED, DOWNLOADED,
@@ -24,7 +25,7 @@ export enum DownloadStatus {
ERROR ERROR
} }
export function useAttachment(attachment: Attachment, keyPlain: string) { export function useAttachment(attachment: Attachment, parentMessage: MessageProps) {
const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
const uploadedPercentage = useUploadStatus(attachment.id); const uploadedPercentage = useUploadStatus(attachment.id);
const downloadPercentage = useDownloadStatus(attachment.id); const downloadPercentage = useDownloadStatus(attachment.id);
@@ -37,13 +38,8 @@ export function useAttachment(attachment: Attachment, keyPlain: string) {
const {updateAttachmentInDialogCache} = useDialogsCache(); const {updateAttachmentInDialogCache} = useDialogsCache();
const {info} = useConsoleLogger('useAttachment'); const {info} = useConsoleLogger('useAttachment');
const {updateAttachmentsInMessagesByAttachmentId} = useDialog(); const {updateAttachmentsInMessagesByAttachmentId} = useDialog();
const {getDownloadsPath} = useCore();
const context = useContext(DialogContext);
if(!context) {
throw new Error("useAttachment must be used within a DialogProvider");
}
const {dialog} = context;
const saveAvatar = useSaveAvatar(); const saveAvatar = useSaveAvatar();
useEffect(() => { useEffect(() => {
@@ -85,7 +81,8 @@ export function useAttachment(attachment: Attachment, keyPlain: string) {
const preview = getPreview(); const preview = getPreview();
const filesize = parseInt(preview.split("::")[0]); const filesize = parseInt(preview.split("::")[0]);
const filename = preview.split("::")[1]; const filename = preview.split("::")[1];
let pathInDownloads = window.downloadsPath + "/Rosetta Downloads/" + filename; const downloadsPath = await getDownloadsPath();
let pathInDownloads = downloadsPath + "/Rosetta Downloads/" + filename;
const exists = await fileExists(pathInDownloads, false); const exists = await fileExists(pathInDownloads, false);
const existsLength = await size(pathInDownloads, false); const existsLength = await size(pathInDownloads, false);
if(exists && existsLength == filesize){ if(exists && existsLength == filesize){
@@ -152,7 +149,7 @@ export function useAttachment(attachment: Attachment, keyPlain: string) {
} }
setDownloadStatus(DownloadStatus.DECRYPTING); setDownloadStatus(DownloadStatus.DECRYPTING);
//console.info("Decrypted attachment ", Buffer.from(keyPlain, 'binary').toString('hex')); //console.info("Decrypted attachment ", Buffer.from(keyPlain, 'binary').toString('hex'));
const decrypted = await decodeWithPassword(keyPlain, downloadedBlob); const decrypted = await decodeWithPassword(parentMessage.chacha_key_plain, downloadedBlob);
setDownloadTag(""); setDownloadTag("");
if(attachment.type == AttachmentType.FILE) { if(attachment.type == AttachmentType.FILE) {
/** /**
@@ -161,8 +158,9 @@ export function useAttachment(attachment: Attachment, keyPlain: string) {
*/ */
const preview = getPreview(); const preview = getPreview();
const filename = preview.split("::")[1]; const filename = preview.split("::")[1];
const downloadsPath = await getDownloadsPath();
let buffer = Buffer.from(decrypted.split(",")[1], 'base64'); let buffer = Buffer.from(decrypted.split(",")[1], 'base64');
let pathInDownloads = window.downloadsPath + "/Rosetta Downloads/" + filename; let pathInDownloads = downloadsPath + "/Rosetta Downloads/" + filename;
/** /**
* Пишем файл в загрузки, но перед этим выбираем ему название, если файл в загрузках * Пишем файл в загрузки, но перед этим выбираем ему название, если файл в загрузках
* уже есть с таким названием то добавляем к названию (1), (2) и так далее, чтобы не перезаписать существующий файл * уже есть с таким названием то добавляем к названию (1), (2) и так далее, чтобы не перезаписать существующий файл
@@ -170,7 +168,7 @@ export function useAttachment(attachment: Attachment, keyPlain: string) {
let finalPath = pathInDownloads; let finalPath = pathInDownloads;
let fileIndex = 1; let fileIndex = 1;
while (await fileExists(finalPath, false)) { while (await fileExists(finalPath, false)) {
finalPath = window.downloadsPath + "/Rosetta Downloads/" + filename.split(".").slice(0, -1).join(".") + ` (${fileIndex})` + "." + filename.split(".").slice(-1); finalPath = downloadsPath + "/Rosetta Downloads/" + filename.split(".").slice(0, -1).join(".") + ` (${fileIndex})` + "." + filename.split(".").slice(-1);
fileIndex++; fileIndex++;
} }
await writeFile(finalPath, buffer, false); await writeFile(finalPath, buffer, false);
@@ -185,7 +183,10 @@ export function useAttachment(attachment: Attachment, keyPlain: string) {
await writeFile(avatarPath, await writeFile(avatarPath,
Buffer.from(await encodeWithPassword(AVATAR_PASSWORD_TO_ENCODE, decrypted))); Buffer.from(await encodeWithPassword(AVATAR_PASSWORD_TO_ENCODE, decrypted)));
setDownloadStatus(DownloadStatus.DOWNLOADED); setDownloadStatus(DownloadStatus.DOWNLOADED);
saveAvatar(dialog, avatarPath, decrypted); /**
* Устанавливаем аватарку тому, кто ее прислал.
*/
saveAvatar(parentMessage.from, avatarPath, decrypted);
return; return;
} }
/** /**

View File

@@ -7,7 +7,15 @@ import { useConsoleLogger } from "@/app/hooks/useConsoleLogger";
import { useSystemAccounts } from "../SystemAccountsProvider/useSystemAccounts"; import { useSystemAccounts } from "../SystemAccountsProvider/useSystemAccounts";
import { AVATAR_PASSWORD_TO_ENCODE } from "@/app/constants"; import { AVATAR_PASSWORD_TO_ENCODE } from "@/app/constants";
export const AvatarContext = createContext({}); export interface AvatarProviderContextValue {
deliveredAvatars: string[];
saveAvatar: (fromPublicKey: string, path : string, decryptedContent : string) => Promise<void>;
loadAvatarsFromCacheByPublicKey: (publicKey : string, allDecode? : boolean) => Promise<void>;
changeAvatar: (base64Image : string, entity : string) => Promise<void>;
decodedAvatarsCache: AvatarCacheEntry[];
}
export const AvatarContext = createContext<AvatarProviderContextValue | null>(null);
interface AvatarProviderProps { interface AvatarProviderProps {
children: React.ReactNode; children: React.ReactNode;

View File

@@ -1,9 +1,9 @@
import { useContext } from "react"; import { useContext } from "react";
import { AvatarContext } from "./AvatarProvider"; import { AvatarContext } from "./AvatarProvider";
export function useSaveAvatar() { export function useSaveAvatar() : (fromPublicKey: string, path : string, decryptedContent : string) => Promise<void> {
const context : any = useContext(AvatarContext); const context : any = useContext(AvatarContext);
if(!context){ if(!context){
throw new Error("useSaveAvatar must be used within an AvatarProvider"); throw new Error("useSaveAvatar must be used within an AvatarProvider");
} }
return context.saveAvatar; return context.saveAvatar;

View File

@@ -0,0 +1,33 @@
import { useCore } from "@/app/hooks/useCore";
import { useEffect, useState } from "react";
export function useCoreDevice() : {
deviceId: string;
deviceName: string;
platform: string;
} {
const { getDeviceId, getDeviceName, getPlatform } = useCore();
const [deviceId, setDeviceId] = useState<string>("");
const [deviceName, setDeviceName] = useState<string>("");
const [platform, setPlatform] = useState<string>("");
useEffect(() => {
fetchDeviceInfo();
}, []);
const fetchDeviceInfo = async () => {
const deviceId = await getDeviceId();
const deviceName = await getDeviceName();
const platform = await getPlatform();
setDeviceId(deviceId);
setDeviceName(deviceName);
setPlatform(platform);
console.info("Device info - ID:", deviceId, "Name:", deviceName);
}
return {
deviceId,
deviceName,
platform
}
}

View File

@@ -840,10 +840,13 @@ export function DialogProvider(props: DialogProviderProps) {
}); });
continue; continue;
} }
const decrypted = await decodeWithPassword(privatePlain, Buffer.from(fileData, 'binary').toString()); let blob = "";
if(meta.type != AttachmentType.IMAGE){
blob = await decodeWithPassword(privatePlain, Buffer.from(fileData, 'binary').toString());
}
attachments.push({ attachments.push({
id: meta.id, id: meta.id,
blob: decrypted, blob: blob,
type: meta.type, type: meta.type,
preview: meta.preview preview: meta.preview
}); });

View File

@@ -445,7 +445,7 @@ export function useDialogFiber() {
* чтобы когда приходит пачка сообщений с сервера в момент того как * чтобы когда приходит пачка сообщений с сервера в момент того как
* пользователь был неактивен, не слать уведомления по всем этим сообщениям * пользователь был неактивен, не слать уведомления по всем этим сообщениям
*/ */
if (!muted.includes(fromPublicKey) || protocolState == ProtocolState.SYNCHRONIZATION) { if (!muted.includes(fromPublicKey) || protocolState != ProtocolState.SYNCHRONIZATION) {
/** /**
* Если пользователь в муте или сейчас идет синхронизация - не отправляем уведомление * Если пользователь в муте или сейчас идет синхронизация - не отправляем уведомление
*/ */

View File

@@ -2,6 +2,7 @@ import { decodeWithPassword, encodeWithPassword } from "@/app/workers/crypto/cry
import { useFileStorage } from "@/app/hooks/useFileStorage"; import { useFileStorage } from "@/app/hooks/useFileStorage";
import { generateRandomKey } from "@/app/utils/utils"; import { generateRandomKey } from "@/app/utils/utils";
import { createContext, useEffect, useState } from "react"; import { createContext, useEffect, useState } from "react";
import { useCore } from "@/app/hooks/useCore";
interface SystemProviderContextValue { interface SystemProviderContextValue {
id: string; id: string;
@@ -21,7 +22,10 @@ export interface SystemProviderProps {
*/ */
export function SystemProvider(props: SystemProviderProps) { export function SystemProvider(props: SystemProviderProps) {
const [deviceId, setDeviceId] = useState<string>(""); const [deviceId, setDeviceId] = useState<string>("");
const [deviceName, setDeviceName] = useState<string>("");
const [deviceOs, setDeviceOs] = useState<string>("");
const {writeFile, readFile} = useFileStorage(); const {writeFile, readFile} = useFileStorage();
const { getDeviceId, getDeviceName, getPlatform } = useCore();
useEffect(() => { useEffect(() => {
fetchDeviceId(); fetchDeviceId();
@@ -29,6 +33,10 @@ export function SystemProvider(props: SystemProviderProps) {
const fetchDeviceId = async () => { const fetchDeviceId = async () => {
const device = await readFile("device"); const device = await readFile("device");
const name = await getDeviceName();
const platform = await getPlatform();
setDeviceName(name);
setDeviceOs(platform);
if(device){ if(device){
const decoded = await decodeDevice(Buffer.from(device).toString('utf-8')); const decoded = await decodeDevice(Buffer.from(device).toString('utf-8'));
if(decoded){ if(decoded){
@@ -47,12 +55,11 @@ export function SystemProvider(props: SystemProviderProps) {
} }
const decodeDevice = async (data: string) => { const decodeDevice = async (data: string) => {
const hwid = window.deviceId; const hwid = await getDeviceId();
const platform = window.deviceName; const deviceName = await getDeviceName();
const salt = "rosetta-device-salt"; const salt = "rosetta-device-salt";
try { try {
const decoded = await decodeWithPassword(hwid + platform + salt, data); const decoded = await decodeWithPassword(hwid + deviceName + salt, data);
return decoded; return decoded;
} catch (e) { } catch (e) {
console.error("Failed to decode device data:", e); console.error("Failed to decode device data:", e);
@@ -61,29 +68,24 @@ export function SystemProvider(props: SystemProviderProps) {
} }
const encodeDevice = async (data: string) => { const encodeDevice = async (data: string) => {
const hwid = window.deviceId; const hwid = await getDeviceId();
const platform = window.deviceName; const deviceName = await getDeviceName();
const salt = "rosetta-device-salt"; const salt = "rosetta-device-salt";
try { try {
const encoded = await encodeWithPassword(hwid + platform + salt, data); const encoded = await encodeWithPassword(hwid + deviceName + salt, data);
return encoded; return encoded;
} catch (e) { } catch (e) {
console.error("Failed to encode device data:", e); console.error("Failed to encode device data:", e);
return null; return null;
} }
} }
const systemName = window.deviceName || "Unknown Device";
const systemOs = window.platform || "Unknown OS";
return ( return (
<SystemProviderContext.Provider value={{ <SystemProviderContext.Provider value={{
id: deviceId, id: deviceId,
name: systemName, name: deviceName || "Unknown Device",
os: systemOs os: deviceOs || "Unknown OS"
}}> }}>
{props.children} {props.children}
</SystemProviderContext.Provider> </SystemProviderContext.Provider>

View File

@@ -4,8 +4,8 @@ import { useSender } from "../ProtocolProvider/useSender";
import { usePacket } from "../ProtocolProvider/usePacket"; import { usePacket } from "../ProtocolProvider/usePacket";
import { useConsoleLogger } from "@/app/hooks/useConsoleLogger"; import { useConsoleLogger } from "@/app/hooks/useConsoleLogger";
import { useFileStorage } from "@/app/hooks/useFileStorage"; import { useFileStorage } from "@/app/hooks/useFileStorage";
import { APPLICATION_ARCH, APPLICATION_PLATFROM, CORE_VERSION } from "@/app/constants";
import { APP_VERSION } from "@/app/version"; import { APP_VERSION } from "@/app/version";
import { useCore } from "@/app/hooks/useCore";
export interface UpdateProviderProps { export interface UpdateProviderProps {
children: React.ReactNode; children: React.ReactNode;
@@ -58,6 +58,7 @@ export function UpdateProvider(props: UpdateProviderProps) {
const [appUpdateUrl, setAppUpdateUrl] = useState<string>(""); const [appUpdateUrl, setAppUpdateUrl] = useState<string>("");
const [appActualVersion, setAppActualVersion] = useState<string>(""); const [appActualVersion, setAppActualVersion] = useState<string>("");
const {writeFile} = useFileStorage(); const {writeFile} = useFileStorage();
const {getCoreVersion, getArch, getPlatform} = useCore();
useEffect(() => { useEffect(() => {
let packet = new PacketRequestUpdate(); let packet = new PacketRequestUpdate();
@@ -75,6 +76,9 @@ export function UpdateProvider(props: UpdateProviderProps) {
}, []); }, []);
const checkForUpdates = async () => { const checkForUpdates = async () => {
const coreVersion = await getCoreVersion();
const arch = await getArch();
const platform = await getPlatform();
if(updateServerRef.current == null){ if(updateServerRef.current == null){
/** /**
* SDU еще не определен * SDU еще не определен
@@ -85,7 +89,7 @@ export function UpdateProvider(props: UpdateProviderProps) {
* Запрашиваем обновления с SDU сервера * Запрашиваем обновления с SDU сервера
*/ */
let response = await fetch let response = await fetch
(`${updateServerRef.current}/updates/get?app=${APP_VERSION}&kernel=${CORE_VERSION}&arch=${APPLICATION_ARCH}&platform=${APPLICATION_PLATFROM}`).catch((e) => { (`${updateServerRef.current}/updates/get?app=${APP_VERSION}&kernel=${coreVersion}&arch=${arch}&platform=${platform}`).catch((e) => {
error("Failed to check for updates: " + e.message); error("Failed to check for updates: " + e.message);
}); });
if(!response || response.status != 200){ if(!response || response.status != 200){

View File

@@ -1,15 +1,13 @@
export const APP_VERSION = "1.0.4"; export const APP_VERSION = "1.0.5";
export const CORE_MIN_REQUIRED_VERSION = "1.4.9"; export const CORE_MIN_REQUIRED_VERSION = "1.5.0";
export const RELEASE_NOTICE = ` export const RELEASE_NOTICE = `
**Обновление v1.0.4** :emoji_1f631: **Обновление v1.0.5** :emoji_1f631:
- Улучшеный UI для взаимодействия с отправкой изображений - Оптимизирован код ядра
- Исправлена блокировка потока при отправке изображений большого размера - Исправление ошибки с системой обновления в результате гонки потоков в ядре
- Исправлены проблемы с утечками памяти - Исправление уведомлений при синхронизации
- Исправлен вылет из приложения при попытке переслать сообщение - Анимация перемещения диалогов
- Исправлены проблемы со скроллам в групповых чатах - Оптимизирован код вложений
- Исправлены проблемы с дерганием скролла в личных сообщениях - Исправлен скролл при подгрузке сообщений сверху
- Улучшен наблюдатель за изменениями размера в контенте - Ускорена загрузка диалогов при большом количестве тяжелых изображений
- Исправлена проблема с отображением аватара в упоминаниях
- Множественные исправления мелких багов и улучшения производительности
`; `;

View File

@@ -10,11 +10,14 @@ import { AnimatedButton } from "@/app/components/AnimatedButton/AnimatedButton";
import { useLogout } from "@/app/providers/AccountProvider/useLogout"; import { useLogout } from "@/app/providers/AccountProvider/useLogout";
import { usePacket } from "@/app/providers/ProtocolProvider/usePacket"; import { usePacket } from "@/app/providers/ProtocolProvider/usePacket";
import { PacketDeviceResolve, Solution } from "@/app/providers/ProtocolProvider/protocol/packets/packet.device.resolve"; import { PacketDeviceResolve, Solution } from "@/app/providers/ProtocolProvider/protocol/packets/packet.device.resolve";
import { useCoreDevice } from "@/app/providers/DeviceProvider/useCoreDevice";
export function DeviceConfirm() { export function DeviceConfirm() {
const [protocolState] = useProtocolState(); const [protocolState] = useProtocolState();
const navigate = useNavigate(); const navigate = useNavigate();
const logout = useLogout(); const logout = useLogout();
const {deviceName} = useCoreDevice();
useEffect(() => { useEffect(() => {
if(protocolState == ProtocolState.CONNECTED) { if(protocolState == ProtocolState.CONNECTED) {
@@ -60,7 +63,7 @@ export function DeviceConfirm() {
<Flex justify={'center'} mt={'xl'} px={'lg'} align={'center'}> <Flex justify={'center'} mt={'xl'} px={'lg'} align={'center'}>
<Flex justify={'center'} gap={'sm'} align={'center'}> <Flex justify={'center'} gap={'sm'} align={'center'}>
<Text ta={'center'} c={'dimmed'} fz={12}> <Text ta={'center'} c={'dimmed'} fz={12}>
Confirm device <strong>{window.deviceName}</strong> on your first device to loading your chats. Confirm device <strong>{deviceName}</strong> on your first device to loading your chats.
</Text> </Text>
</Flex> </Flex>
</Flex> </Flex>

View File

@@ -4,15 +4,26 @@ import { RosettaPower } from "@/app/components/RosettaPower/RosettaPower";
import { SettingsAlert } from "@/app/components/SettingsAlert/SettingsAlert"; import { SettingsAlert } from "@/app/components/SettingsAlert/SettingsAlert";
import { SettingsInput } from "@/app/components/SettingsInput/SettingsInput"; import { SettingsInput } from "@/app/components/SettingsInput/SettingsInput";
import { UpdateAlert } from "@/app/components/UpdateAlert/UpdateAlert"; import { UpdateAlert } from "@/app/components/UpdateAlert/UpdateAlert";
import { CORE_VERSION } from "@/app/constants"; import { useCore } from "@/app/hooks/useCore";
import { UpdateStatus } from "@/app/providers/UpdateProvider/UpdateProvider"; import { UpdateStatus } from "@/app/providers/UpdateProvider/UpdateProvider";
import { useUpdater } from "@/app/providers/UpdateProvider/useUpdater"; import { useUpdater } from "@/app/providers/UpdateProvider/useUpdater";
import { APP_VERSION } from "@/app/version"; import { APP_VERSION } from "@/app/version";
import { Box, Text } from "@mantine/core"; import { Box, Text } from "@mantine/core";
import { useEffect, useState } from "react";
export function Update() { export function Update() {
const {updateStatus} = useUpdater(); const {updateStatus} = useUpdater();
const {getCoreVersion} = useCore();
const [coreVersion, setCoreVersion] = useState<string>("");
useEffect(() => {
const fetchCoreVersion = async () => {
const version = await getCoreVersion();
setCoreVersion(version);
}
fetchCoreVersion();
}, [getCoreVersion]);
return ( return (
<> <>
<Breadcrumbs text="Updates"></Breadcrumbs> <Breadcrumbs text="Updates"></Breadcrumbs>
@@ -23,7 +34,7 @@ export function Update() {
<Box mt={'sm'}> <Box mt={'sm'}>
<UpdateAlert radius={'sm'}></UpdateAlert> <UpdateAlert radius={'sm'}></UpdateAlert>
</Box> </Box>
<SettingsInput.Copy mt={'sm'} hit="Kernel" value={CORE_VERSION}></SettingsInput.Copy> <SettingsInput.Copy mt={'sm'} hit="Kernel" value={coreVersion}></SettingsInput.Copy>
<Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}> <Text fz={10} mt={3} c={'gray'} pl={'xs'} pr={'xs'}>
If the kernel version is outdated, you need to reinstall the application so that this kernel continues to receive current updates. If the kernel version is outdated, you need to reinstall the application so that this kernel continues to receive current updates.
</Text> </Text>

View File

@@ -1,4 +1,4 @@
import { BrowserWindow, shell, app, ipcMain, nativeTheme, screen, powerMonitor } from 'electron' import { BrowserWindow, shell, ipcMain, nativeTheme, screen, powerMonitor } 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';
@@ -79,11 +79,6 @@ export function foundationIpcRegistration(mainWindow: BrowserWindow) {
ipcMain.removeAllListeners("write-file"); ipcMain.removeAllListeners("write-file");
ipcMain.removeAllListeners("read-file"); ipcMain.removeAllListeners("read-file");
ipcMain.removeAllListeners("mkdir"); ipcMain.removeAllListeners("mkdir");
ipcMain.removeHandler("get-core-version");
ipcMain.removeHandler("get-arch");
ipcMain.removeAllListeners("get-user-dir");
ipcMain.removeHandler("get-downloads-path")
ipcMain.removeHandler("get-app-path");
ipcMain.removeHandler('open-dev-tools'); ipcMain.removeHandler('open-dev-tools');
ipcMain.removeHandler('window-state'); ipcMain.removeHandler('window-state');
ipcMain.removeHandler('window-toggle'); ipcMain.removeHandler('window-toggle');
@@ -92,14 +87,6 @@ export function foundationIpcRegistration(mainWindow: BrowserWindow) {
ipcMain.removeHandler('showItemInFolder'); ipcMain.removeHandler('showItemInFolder');
ipcMain.removeHandler('openExternal'); ipcMain.removeHandler('openExternal');
ipcMain.handle('showItemInFolder', (_, fullPath: string) => {
shell.showItemInFolder(fullPath);
});
ipcMain.handle('openExternal', (_, url: string) => {
shell.openExternal(url);
});
ipcMain.handle('open-dev-tools', () => { ipcMain.handle('open-dev-tools', () => {
if (mainWindow.webContents.isDevToolsOpened()) { if (mainWindow.webContents.isDevToolsOpened()) {
return; return;
@@ -208,27 +195,4 @@ export function foundationIpcRegistration(mainWindow: BrowserWindow) {
mainWindow.webContents.send("mkdir-reply"); mainWindow.webContents.send("mkdir-reply");
}); });
}); });
/**
* Change to get-core-version
*/
ipcMain.handle("get-core-version", () => {
return app.getVersion();
});
ipcMain.handle("get-arch", () => {
return process.arch;
})
ipcMain.on("get-user-dir", () => {
const userDir = app.getPath("userData");
mainWindow.webContents.send("get-user-dir-reply", userDir);
});
ipcMain.handle("get-app-path", () => {
return app.getAppPath();
});
ipcMain.handle("get-downloads-path", () => {
return app.getPath("downloads");
});
} }

34
lib/main/ipcs/ipcCore.ts Normal file
View File

@@ -0,0 +1,34 @@
import { app, ipcMain, shell } from "electron";
ipcMain.handle("ipcCore:getCoreVersion", () => {
return app.getVersion();
});
ipcMain.handle("ipcCore:getArch", () => {
return process.arch;
})
ipcMain.handle("ipcCore:getUserDir", () => {
const userDir = app.getPath("userData");
return userDir;
});
ipcMain.handle("ipcCore:getAppPath", () => {
return app.getAppPath();
});
ipcMain.handle("ipcCore:getDownloadsPath", () => {
return app.getPath("downloads");
});
ipcMain.handle('ipcCore:showItemInFolder', (_, fullPath: string) => {
shell.showItemInFolder(fullPath);
});
ipcMain.handle('ipcCore:openExternal', (_, url: string) => {
shell.openExternal(url);
});
ipcMain.handle('ipcCore:getPlatform', () => {
return process.platform;
});

View File

@@ -7,6 +7,7 @@ import './ipcs/ipcFilestorage'
import './ipcs/ipcUpdate' import './ipcs/ipcUpdate'
import './ipcs/ipcNotification' import './ipcs/ipcNotification'
import './ipcs/ipcDevice' import './ipcs/ipcDevice'
import './ipcs/ipcCore'
import { Tray } from 'electron/main' import { Tray } from 'electron/main'
import { join } from 'path' import { join } from 'path'
import { Logger } from './logger' import { Logger } from './logger'

View File

@@ -4,46 +4,25 @@ import api from './api'
const exposeContext = async () => { const exposeContext = async () => {
let version = await ipcRenderer.invoke("get-core-version");
let appPath = await ipcRenderer.invoke("get-app-path");
let arch = await ipcRenderer.invoke("get-arch");
let deviceName = await ipcRenderer.invoke("device:name");
let deviceId = await ipcRenderer.invoke("device:id");
let downloadsPath = await ipcRenderer.invoke("get-downloads-path");
if (process.contextIsolated) { if (process.contextIsolated) {
try { try {
contextBridge.exposeInMainWorld('electron', electronAPI) contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api) contextBridge.exposeInMainWorld('api', api)
contextBridge.exposeInMainWorld('version', version);
contextBridge.exposeInMainWorld('platform', process.platform);
contextBridge.exposeInMainWorld('appPath', appPath);
contextBridge.exposeInMainWorld('arch', arch);
contextBridge.exposeInMainWorld('deviceName', deviceName);
contextBridge.exposeInMainWorld('deviceId', deviceId);
contextBridge.exposeInMainWorld('shell', { contextBridge.exposeInMainWorld('shell', {
openExternal: (url: string) => { openExternal: (url: string) => {
ipcRenderer.invoke('openExternal', url); ipcRenderer.invoke('ipcCore:openExternal', url);
}, },
showItemInFolder: (fullPath: string) => { showItemInFolder: (fullPath: string) => {
ipcRenderer.invoke('showItemInFolder', fullPath); ipcRenderer.invoke('ipcCore:showItemInFolder', fullPath);
} }
}); });
contextBridge.exposeInMainWorld('downloadsPath', downloadsPath)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }
} else { } else {
window.electron = electronAPI window.electron = electronAPI
window.api = api window.api = api;
window.version = version;
window.platform = process.platform;
window.appPath = appPath;
window.arch = arch;
window.shell = shell; window.shell = shell;
window.downloadsPath = downloadsPath;
window.deviceName = deviceName;
window.deviceId = deviceId;
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "Rosetta", "name": "Rosetta",
"version": "1.4.9", "version": "1.5.0",
"description": "Rosetta Messenger", "description": "Rosetta Messenger",
"main": "./out/main/main.js", "main": "./out/main/main.js",
"license": "MIT", "license": "MIT",