Compare commits

...

2 Commits

15 changed files with 551 additions and 271 deletions

View File

@@ -89,7 +89,10 @@ export function DialogInput() {
blob: fileContent,
id: generateRandomKey(8),
type: AttachmentType.FILE,
preview: files[0].size + "::" + files[0].name
preview: files[0].size + "::" + files[0].name,
transport_server: "",
transport_tag: "",
encoded_for: dialog
}]);
}
});
@@ -116,7 +119,10 @@ export function DialogInput() {
type: AttachmentType.MESSAGES,
id: generateRandomKey(8),
blob: JSON.stringify([...replyMessages.messages]),
preview: ""
preview: "",
transport_server: "",
transport_tag: "",
encoded_for: dialog
}]);
if(editableDivRef.current){
editableDivRef.current.focus();
@@ -230,7 +236,10 @@ export function DialogInput() {
blob: avatars[0].avatar,
id: generateRandomKey(8),
type: AttachmentType.AVATAR,
preview: ""
preview: "",
transport_server: "",
transport_tag: "",
encoded_for: dialog
}]);
if(editableDivRef.current){
editableDivRef.current.focus();
@@ -270,7 +279,10 @@ export function DialogInput() {
blob: base64Image,
id: attachmentId,
type: AttachmentType.IMAGE,
preview: ""
preview: "",
transport_server: "",
transport_tag: "",
encoded_for: dialog
}]);
}
if(editableDivRef.current){
@@ -304,7 +316,10 @@ export function DialogInput() {
blob: fileContent,
id: attachmentId,
type: AttachmentType.FILE,
preview: files[0].size + "::" + files[0].name
preview: files[0].size + "::" + files[0].name,
transport_server: "",
transport_tag: "",
encoded_for: dialog
}]);
}

View File

@@ -29,6 +29,7 @@ export function MessageImage(props: AttachmentProps) {
const [blurhashPreview, setBlurhashPreview] = useState("");
useEffect(() => {
console.info(props.attachment);
console.info("Consturcting image, download status: " + downloadStatus);
constructBlob();
constructFromBlurhash();

View File

@@ -27,11 +27,10 @@ export enum DownloadStatus {
}
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 uploadedPercentage = useUploadStatus(attachment.id);
const downloadPercentage = useDownloadStatus(attachment.id);
const [downloadStatus, setDownloadStatus] = useMemory("attachment-downloaded-status-" + attachment.id, DownloadStatus.PENDING, true);
const [downloadTag, setDownloadTag] = useState("");
const [downloadTag, setDownloadTag] = useState(attachment.transport_tag || "");
const {readFile, writeFile, fileExists, size} = useFileStorage();
const { downloadFile } = useTransport();
const publicKey = usePublicKey();
@@ -50,30 +49,18 @@ export function useAttachment(attachment: Attachment, parentMessage: MessageProp
}, []);
const getPreview = () => {
if(attachment.preview.split("::")[0].match(uuidRegex)){
/**
* Это тег загрузки
*/
return attachment.preview.split("::").splice(1).join("::");
}
return attachment.preview;
}
const calcDownloadStatus = async () => {
if(attachment.preview.split("::")[0].match(uuidRegex)){
/**
* Это тег загрузки
*/
setDownloadTag(attachment.preview.split("::")[0]);
}
if(!attachment.preview.split("::")[0].match(uuidRegex)){
/**
* Там не тег загрузки, значит это наш файл
*/
setDownloadStatus(DownloadStatus.DOWNLOADED);
if (downloadStatus == DownloadStatus.DOWNLOADED) {
return;
}
if (downloadStatus == DownloadStatus.DOWNLOADED) {
if(attachment.transport_tag == ""){
/**
* Транспортного тега нет только у сообщений отправленных нами, значит он точно наш
*/
setDownloadStatus(DownloadStatus.DOWNLOADED);
return;
}
if(attachment.type == AttachmentType.FILE){
@@ -143,7 +130,7 @@ export function useAttachment(attachment: Attachment, parentMessage: MessageProp
let downloadedBlob = '';
try {
downloadedBlob = await downloadFile(attachment.id,
downloadTag);
downloadTag, attachment.transport_server);
} catch (e) {
console.info(e);
info("Error downloading attachment: " + attachment.id);

View File

@@ -10,6 +10,7 @@ import { useDatabase } from "../DatabaseProvider/useDatabase";
import { useConsoleLogger } from "@/app/hooks/useConsoleLogger";
import { useDialogsCache } from "../DialogProvider/useDialogsCache";
import { DialogContext } from "../DialogProvider/DialogProvider";
import { useTransportServer } from "../TransportProvider/useTransportServer";
export function usePrepareAttachment() {
const intervalsRef = useRef<NodeJS.Timeout>(null);
@@ -19,6 +20,7 @@ export function usePrepareAttachment() {
const {info} = useConsoleLogger('usePrepareAttachment');
const {getDialogCache} = useDialogsCache();
const context = useContext(DialogContext);
const transportServer = useTransportServer();
const updateTimestampInDialogCache = (dialog : string, message_id: string) => {
const dialogCache = getDialogCache(dialog);
@@ -74,18 +76,6 @@ export function usePrepareAttachment() {
}, (MESSAGE_MAX_TIME_TO_DELEVERED_S / 2) * 1000);
}
/**
* Удаляет старый тег если вложения были подготовлены заново
* например при пересылке сообщений
*/
const removeOldTagIfAttachemtnsRePreapred = (preview : string) => {
if(preview.indexOf("::") == -1){
return preview;
}
let parts = preview.split("::");
return parts.slice(1).join("::");
}
/**
* Подготавливает вложения для отправки. Подготовка
* состоит в загрузке файлов на транспортный сервер, мы не делаем
@@ -127,6 +117,16 @@ export function usePrepareAttachment() {
const blurhash = await base64ImageToBlurhash(attachment.blob);
attachment.preview = blurhash;
}
if(rePrepared && attachment.encoded_for == dialog){
/**
* Это пересланное сообщение и оно уже закодировано для этого диалога, значит не нужно его кодировать и загружать заново
*/
prepared.push({
...attachment,
blob: ""
});
continue;
}
doTimestampUpdateImMessageWhileAttachmentsSend(message_id, dialog);
const content = await encodeWithPassword(password, attachment.blob);
const upid = attachment.id;
@@ -139,7 +139,10 @@ export function usePrepareAttachment() {
}
prepared.push({
...attachment,
preview: tag + "::" + (rePrepared ? removeOldTagIfAttachemtnsRePreapred(attachment.preview) : attachment.preview),
transport_server: transportServer || "",
transport_tag: tag,
encoded_for: dialog,
preview: attachment.preview,
blob: ""
});
}

View File

@@ -472,6 +472,9 @@ export function CallProvider(props : CallProviderProps) {
id: generateRandomKey(16),
preview: duration.toString(),
type: AttachmentType.CALL,
transport_server: "",
transport_tag: "",
encoded_for: "",
blob: ""
}], true);
}

View File

@@ -46,6 +46,9 @@ export interface AttachmentMeta {
id: string;
type: AttachmentType;
preview: string;
transport_tag: string;
encoded_for: string;
transport_server: string;
}
export interface Message {
@@ -469,9 +472,7 @@ export function DialogProvider(props: DialogProviderProps) {
for(let i = 0; i < packet.getAttachments().length; i++) {
const attachment = packet.getAttachments()[i];
attachments.push({
id: attachment.id,
preview: attachment.preview,
type: attachment.type,
...attachment,
blob: attachment.type == AttachmentType.MESSAGES ? await decodeWithPassword(chachaDecryptedKey.toString('utf-8'), attachment.blob) : ""
});
}
@@ -549,9 +550,7 @@ export function DialogProvider(props: DialogProviderProps) {
for(let i = 0; i < packet.getAttachments().length; i++) {
const attachment = packet.getAttachments()[i];
attachments.push({
id: attachment.id,
preview: attachment.preview,
type: attachment.type,
...attachment,
blob: attachment.type == AttachmentType.MESSAGES ? await decodeWithPassword(groupKey, attachment.blob) : ""
});
}
@@ -627,9 +626,7 @@ export function DialogProvider(props: DialogProviderProps) {
for(let i = 0; i < packet.getAttachments().length; i++) {
const attachment = packet.getAttachments()[i];
attachments.push({
id: attachment.id,
preview: attachment.preview,
type: attachment.type,
...attachment,
blob: attachment.type == AttachmentType.MESSAGES ? await decodeWithPassword(chachaDecryptedKey.toString('utf-8'), attachment.blob) : ""
});
}
@@ -707,9 +704,7 @@ export function DialogProvider(props: DialogProviderProps) {
for(let i = 0; i < packet.getAttachments().length; i++) {
const attachment = packet.getAttachments()[i];
attachments.push({
id: attachment.id,
preview: attachment.preview,
type: attachment.type,
...attachment,
blob: attachment.type == AttachmentType.MESSAGES ? await decodeWithPassword(groupKey, attachment.blob) : ""
});
}
@@ -964,7 +959,10 @@ export function DialogProvider(props: DialogProviderProps) {
id: meta.id,
blob: blob,
type: meta.type,
preview: meta.preview
preview: meta.preview,
transport_server: meta.transport_server,
transport_tag: meta.transport_tag,
encoded_for: meta.encoded_for
});
}
return attachments;

View File

@@ -118,7 +118,10 @@ export function useDialog() : {
attachmentsMeta.push({
id: attachment.id,
type: attachment.type,
preview: attachment.preview
preview: attachment.preview,
transport_server: attachment.transport_server,
transport_tag: attachment.transport_tag,
encoded_for: dialog
});
if(attachment.type == AttachmentType.FILE){
/**

View File

@@ -20,7 +20,7 @@ import { useGroupInviteStatus } from "./useGroupInviteStatus";
import { Attachment, AttachmentType, PacketMessage } from "../ProtocolProvider/protocol/packets/packet.message";
import { useUpdateSyncTime } from "./useUpdateSyncTime";
import { useFileStorage } from "@/app/hooks/useFileStorage";
import { DeliveredMessageState, Message } from "./DialogProvider";
import { AttachmentMeta, DeliveredMessageState, Message } from "./DialogProvider";
import { MESSAGE_MAX_LOADED, TIME_TO_INACTIVE_FOR_MESSAGES_UNREAD } from "@/app/constants";
import { useMemory } from "../MemoryProvider/useMemory";
import { useDialogsCache } from "./useDialogsCache";
@@ -165,7 +165,7 @@ export function useSynchronize() {
const nonce = chachaDecryptedKey.slice(32);
const decryptedContent = await chacha20Decrypt(content, nonce.toString('hex'), key.toString('hex'));
await updateSyncTime(timestamp);
let attachmentsMeta: any[] = [];
let attachmentsMeta: AttachmentMeta[] = [];
let messageAttachments: Attachment[] = [];
for (let i = 0; i < packet.getAttachments().length; i++) {
const attachment = packet.getAttachments()[i];
@@ -190,7 +190,10 @@ export function useSynchronize() {
attachmentsMeta.push({
id: attachment.id,
type: attachment.type,
preview: attachment.preview
preview: attachment.preview,
encoded_for: attachment.encoded_for,
transport_server: attachment.transport_server,
transport_tag: attachment.transport_tag
});
}
@@ -347,7 +350,7 @@ export function useSynchronize() {
decryptedContent = '';
}
let attachmentsMeta: any[] = [];
let attachmentsMeta: AttachmentMeta[] = [];
let messageAttachments: Attachment[] = [];
for (let i = 0; i < packet.getAttachments().length; i++) {
const attachment = packet.getAttachments()[i];
@@ -372,7 +375,10 @@ export function useSynchronize() {
attachmentsMeta.push({
id: attachment.id,
type: attachment.type,
preview: attachment.preview
preview: attachment.preview,
encoded_for: attachment.encoded_for,
transport_server: attachment.transport_server,
transport_tag: attachment.transport_tag
});
}

View File

@@ -14,6 +14,12 @@ export interface Attachment {
blob: string;
type: AttachmentType;
preview: string;
transport_tag: string;
transport_server: string;
/**
* Обозначает, для кого закодировано это вложение, нужно для того, чтобы не кодировать и не загружать заново пересланные сообщения, если они уже были закодированы для этого диалога
*/
encoded_for: string;
}
export class PacketMessage extends Packet {
@@ -42,7 +48,7 @@ export class PacketMessage extends Packet {
this.toPublicKey = stream.readString();
this.content = stream.readString();
this.chachaKey = stream.readString();
this.timestamp = stream.readInt64();
this.timestamp = Number(stream.readInt64());
this.privateKey = stream.readString();
this.messageId = stream.readString();
let attachmentsCount = stream.readInt8();
@@ -51,7 +57,10 @@ export class PacketMessage extends Packet {
let preview = stream.readString();
let blob = stream.readString();
let type = stream.readInt8() as AttachmentType;
this.attachments.push({id, preview, type, blob});
let transport_tag = stream.readString();
let transport_server = stream.readString();
let encoded_for = stream.readString();
this.attachments.push({id, preview, type, blob, transport_tag, transport_server, encoded_for});
}
this.aesChachaKey = stream.readString();
}
@@ -63,7 +72,7 @@ export class PacketMessage extends Packet {
stream.writeString(this.toPublicKey);
stream.writeString(this.content);
stream.writeString(this.chachaKey);
stream.writeInt64(this.timestamp);
stream.writeInt64(BigInt(this.timestamp));
stream.writeString(this.privateKey);
stream.writeString(this.messageId);
stream.writeInt8(this.attachments.length);
@@ -72,6 +81,9 @@ export class PacketMessage extends Packet {
stream.writeString(this.attachments[i].preview);
stream.writeString(this.attachments[i].blob);
stream.writeInt8(this.attachments[i].type);
stream.writeString(this.attachments[i].transport_tag);
stream.writeString(this.attachments[i].transport_server);
stream.writeString(this.attachments[i].encoded_for);
}
stream.writeString(this.aesChachaKey);
return stream;

View File

@@ -18,14 +18,14 @@ export class PacketSync extends Packet {
public _receive(stream: Stream): void {
this.status = stream.readInt8() as SyncStatus;
this.timestamp = stream.readInt64();
this.timestamp = Number(stream.readInt64());
}
public _send(): Promise<Stream> | Stream {
let stream = new Stream();
stream.writeInt16(this.getPacketId());
stream.writeInt8(this.status);
stream.writeInt64(this.timestamp);
stream.writeInt64(BigInt(this.timestamp));
return stream;
}

View File

@@ -1,151 +1,372 @@
export default class Stream {
private _stream: number[];
private _readPoiner: number = 0;
private _writePointer: number = 0;
private stream: Uint8Array;
private readPointer = 0; // bits
private writePointer = 0; // bits
constructor(stream : number[] = []) {
this._stream = stream;
constructor(stream?: Uint8Array | number[]) {
if (!stream) {
this.stream = new Uint8Array(0);
} else {
const src = stream instanceof Uint8Array ? stream : Uint8Array.from(stream);
this.stream = src;
this.writePointer = this.stream.length << 3;
}
}
getStream(): Uint8Array {
return this.stream.slice(0, this.length());
}
setStream(stream?: Uint8Array | number[]) {
if (!stream) {
this.stream = new Uint8Array(0);
this.readPointer = 0;
this.writePointer = 0;
return;
}
const src = stream instanceof Uint8Array ? stream : Uint8Array.from(stream);
this.stream = src;
this.readPointer = 0;
this.writePointer = this.stream.length << 3;
}
getBuffer(): Uint8Array {
return this.getStream();
}
isEmpty(): boolean {
return this.writePointer === 0;
}
length(): number {
return (this.writePointer + 7) >> 3;
}
// ---------- bit / boolean ----------
writeBit(value: number) {
this.writeBits(BigInt(value & 1), 1);
}
readBit(): number {
return Number(this.readBits(1));
}
writeBoolean(value: boolean) {
this.writeBit(value ? 1 : 0);
}
readBoolean(): boolean {
return this.readBit() === 1;
}
// ---------- byte ----------
writeByte(b: number) {
this.writeUInt8(b & 0xff);
}
readByte(): number {
const v = this.readUInt8();
return (v << 24) >> 24; // signed byte
}
// ---------- UInt / Int 8 ----------
writeUInt8(value: number) {
const v = value & 0xff;
if ((this.writePointer & 7) === 0) {
this.reserveBits(8);
this.stream[this.writePointer >> 3] = v;
this.writePointer += 8;
return;
}
public getStream(): number[] {
return this._stream;
this.writeBits(BigInt(v), 8);
}
readUInt8(): number {
if (this.remainingBits() < 8n) {
throw new Error("Not enough bits to read UInt8");
}
public setStream(stream: number[]) {
this._stream = stream;
if ((this.readPointer & 7) === 0) {
const v = this.stream[this.readPointer >> 3] & 0xff;
this.readPointer += 8;
return v;
}
public writeInt8(value: number) {
const negationBit = value < 0 ? 1 : 0;
const int8Value = Math.abs(value) & 0xFF;
this._stream[this._writePointer >> 3] |= negationBit << (7 - (this._writePointer & 7));
this._writePointer++;
for (let i = 0; i < 8; i++) {
const bit = (int8Value >> (7 - i)) & 1;
this._stream[this._writePointer >> 3] |= bit << (7 - (this._writePointer & 7));
this._writePointer++;
}
return Number(this.readBits(8));
}
writeInt8(value: number) {
this.writeUInt8(value);
}
readInt8(): number {
const u = this.readUInt8();
return (u << 24) >> 24;
}
// ---------- UInt / Int 16 ----------
writeUInt16(value: number) {
const v = value & 0xffff;
this.writeUInt8((v >>> 8) & 0xff);
this.writeUInt8(v & 0xff);
}
readUInt16(): number {
const hi = this.readUInt8();
const lo = this.readUInt8();
return (hi << 8) | lo;
}
writeInt16(value: number) {
this.writeUInt16(value);
}
readInt16(): number {
const u = this.readUInt16();
return (u << 16) >> 16;
}
// ---------- UInt / Int 32 ----------
writeUInt32(value: number) {
if (!Number.isFinite(value) || value < 0 || value > 0xffffffff) {
throw new Error(`UInt32 out of range: ${value}`);
}
public readInt8(): number {
let value = 0;
const negationBit = (this._stream[this._readPoiner >> 3] >> (7 - (this._readPoiner & 7))) & 1;
this._readPoiner++;
for (let i = 0; i < 8; i++) {
const bit = (this._stream[this._readPoiner >> 3] >> (7 - (this._readPoiner & 7))) & 1;
value |= bit << (7 - i);
this._readPoiner++;
}
return negationBit ? -value : value;
const v = Math.floor(value);
this.writeUInt8((v >>> 24) & 0xff);
this.writeUInt8((v >>> 16) & 0xff);
this.writeUInt8((v >>> 8) & 0xff);
this.writeUInt8(v & 0xff);
}
readUInt32(): number {
const b1 = this.readUInt8();
const b2 = this.readUInt8();
const b3 = this.readUInt8();
const b4 = this.readUInt8();
return (((b1 * 0x1000000) + (b2 << 16) + (b3 << 8) + b4) >>> 0);
}
writeInt32(value: number) {
this.writeUInt32(value >>> 0);
}
readInt32(): number {
return this.readUInt32() | 0;
}
// ---------- UInt / Int 64 ----------
writeUInt64(value: bigint) {
if (value < 0n || value > 0xffff_ffff_ffff_ffffn) {
throw new Error(`UInt64 out of range: ${value.toString()}`);
}
public writeBit(value: number) {
const bit = value & 1;
this._stream[this._writePointer >> 3] |= bit << (7 - (this._writePointer & 7));
this._writePointer++;
this.writeUInt8(Number((value >> 56n) & 0xffn));
this.writeUInt8(Number((value >> 48n) & 0xffn));
this.writeUInt8(Number((value >> 40n) & 0xffn));
this.writeUInt8(Number((value >> 32n) & 0xffn));
this.writeUInt8(Number((value >> 24n) & 0xffn));
this.writeUInt8(Number((value >> 16n) & 0xffn));
this.writeUInt8(Number((value >> 8n) & 0xffn));
this.writeUInt8(Number(value & 0xffn));
}
readUInt64(): bigint {
const high = BigInt(this.readUInt32() >>> 0);
const low = BigInt(this.readUInt32() >>> 0);
return (high << 32n) | low;
}
writeInt64(value: bigint) {
const u = BigInt.asUintN(64, value);
this.writeUInt64(u);
}
readInt64(): bigint {
return BigInt.asIntN(64, this.readUInt64());
}
// ---------- float ----------
writeFloat32(value: number) {
const ab = new ArrayBuffer(4);
const dv = new DataView(ab);
dv.setFloat32(0, value, false); // big-endian
this.writeUInt8(dv.getUint8(0));
this.writeUInt8(dv.getUint8(1));
this.writeUInt8(dv.getUint8(2));
this.writeUInt8(dv.getUint8(3));
}
readFloat32(): number {
const ab = new ArrayBuffer(4);
const dv = new DataView(ab);
dv.setUint8(0, this.readUInt8());
dv.setUint8(1, this.readUInt8());
dv.setUint8(2, this.readUInt8());
dv.setUint8(3, this.readUInt8());
return dv.getFloat32(0, false); // big-endian
}
// ---------- string / bytes ----------
// String: length(UInt32) + chars(UInt16), как в Java charAt()
writeString(value: string | null | undefined) {
const s = value ?? "";
this.writeUInt32(s.length);
if (s.length === 0) return;
this.reserveBits(BigInt(s.length) * 16n);
for (let i = 0; i < s.length; i++) {
this.writeUInt16(s.charCodeAt(i) & 0xffff);
}
}
readString(): string {
const len = this.readUInt32();
if (len > 0x7fffffff) {
throw new Error(`String length too large: ${len}`);
}
public readBit(): number {
const bit = (this._stream[this._readPoiner >> 3] >> (7 - (this._readPoiner & 7))) & 1;
this._readPoiner++;
return bit;
const requiredBits = BigInt(len) * 16n;
if (requiredBits > this.remainingBits()) {
throw new Error("Not enough bits to read string");
}
public writeBoolean(value: boolean) {
this.writeBit(value ? 1 : 0);
const chars = new Array<number>(len);
for (let i = 0; i < len; i++) {
chars[i] = this.readUInt16();
}
return String.fromCharCode(...chars);
}
// byte[]: length(UInt32) + payload
writeBytes(value: Uint8Array | number[] | null | undefined) {
const arr = value == null
? new Uint8Array(0)
: (value instanceof Uint8Array ? value : Uint8Array.from(value));
this.writeUInt32(arr.length);
if (arr.length === 0) return;
this.reserveBits(BigInt(arr.length) * 8n);
if ((this.writePointer & 7) === 0) {
const byteIndex = this.writePointer >> 3;
this.ensureCapacity(byteIndex + arr.length - 1);
this.stream.set(arr, byteIndex);
this.writePointer += arr.length << 3;
return;
}
public readBoolean(): boolean {
return this.readBit() === 1;
for (let i = 0; i < arr.length; i++) {
this.writeUInt8(arr[i]);
}
public writeInt16(value: number) {
this.writeInt8(value >> 8);
this.writeInt8(value & 0xFF);
}
readBytes(): Uint8Array {
const len = this.readUInt32();
if (len === 0) return new Uint8Array(0);
const requiredBits = BigInt(len) * 8n;
if (requiredBits > this.remainingBits()) {
return new Uint8Array(0);
}
public readInt16(): number {
const value = this.readInt8() << 8;
return value | this.readInt8();
const out = new Uint8Array(len);
if ((this.readPointer & 7) === 0) {
const byteIndex = this.readPointer >> 3;
out.set(this.stream.slice(byteIndex, byteIndex + len));
this.readPointer += len << 3;
return out;
}
public writeInt32(value: number) {
this.writeInt16(value >> 16);
this.writeInt16(value & 0xFFFF);
for (let i = 0; i < len; i++) {
out[i] = this.readUInt8();
}
return out;
}
// ---------- internals ----------
private remainingBits(): bigint {
return BigInt(this.writePointer - this.readPointer);
}
private writeBits(value: bigint, bits: number) {
if (bits <= 0) return;
this.reserveBits(bits);
for (let i = bits - 1; i >= 0; i--) {
const bit = Number((value >> BigInt(i)) & 1n);
const byteIndex = this.writePointer >> 3;
const shift = 7 - (this.writePointer & 7);
if (bit === 1) {
this.stream[byteIndex] = this.stream[byteIndex] | (1 << shift);
} else {
this.stream[byteIndex] = this.stream[byteIndex] & ~(1 << shift);
}
this.writePointer++;
}
}
private readBits(bits: number): bigint {
if (bits <= 0) return 0n;
if (this.remainingBits() < BigInt(bits)) {
throw new Error("Not enough bits to read");
}
public readInt32(): number {
const value = this.readInt16() << 16;
return value | this.readInt16();
let value = 0n;
for (let i = 0; i < bits; i++) {
const bit = (this.stream[this.readPointer >> 3] >> (7 - (this.readPointer & 7))) & 1;
value = (value << 1n) | BigInt(bit);
this.readPointer++;
}
return value;
}
private reserveBits(bitsToWrite: number | bigint) {
const bits = typeof bitsToWrite === "number" ? BigInt(bitsToWrite) : bitsToWrite;
if (bits <= 0n) return;
const lastBitIndex = BigInt(this.writePointer) + bits - 1n;
if (lastBitIndex < 0n) throw new Error("Bit index overflow");
const byteIndex = lastBitIndex >> 3n;
if (byteIndex > BigInt(Number.MAX_SAFE_INTEGER)) {
throw new Error("Stream too large");
}
public writeInt64(value: number) {
const high = Math.floor(value / 0x100000000);
const low = value >>> 0;
this.writeInt32(high);
this.writeInt32(low);
}
public readInt64(): number {
const high = this.readInt32();
const low = this.readInt32() >>> 0;
return high * 0x100000000 + low;
}
public writeFloat32(value: number) {
const buffer = new ArrayBuffer(4);
new DataView(buffer).setFloat32(0, value, true);
const float32Value = new Uint32Array(buffer)[0];
this.writeInt32(float32Value);
}
public readFloat32(): number {
const float32Value = this.readInt32();
const buffer = new ArrayBuffer(4);
new Uint32Array(buffer)[0] = float32Value;
return new DataView(buffer).getFloat32(0, true);
}
public writeString(value: string) {
let length = value.length;
this.writeInt32(length);
for (let i = 0; i < value.length; i++) {
this.writeInt16(value.charCodeAt(i));
}
}
public readString(): string {
let length = this.readInt32();
/**
* Фикс уязвимости с длинной строки, превышающей
* возможность для чтения _stream
*/
if (length < 0 || length > (this._stream.length - (this._readPoiner >> 3))) {
console.info("Stream readString length invalid", length, this._stream.length, this._readPoiner);
return "";
}
let value = "";
for (let i = 0; i < length; i++) {
value += String.fromCharCode(this.readInt16());
}
return value;
}
public writeBytes(value: number[]) {
this.writeInt32(value.length);
for (let i = 0; i < value.length; i++) {
this.writeInt8(value[i]);
}
}
public readBytes(): number[] {
let length = this.readInt32();
let value : any = [];
for (let i = 0; i < length; i++) {
value.push(this.readInt8());
}
return value;
this.ensureCapacity(Number(byteIndex));
}
private ensureCapacity(byteIndex: number) {
const requiredSize = byteIndex + 1;
if (requiredSize <= this.stream.length) return;
let newSize = this.stream.length === 0 ? 32 : this.stream.length;
while (newSize < requiredSize) {
if (newSize > (0x7fffffff >> 1)) {
newSize = requiredSize;
break;
}
newSize <<= 1;
}
const next = new Uint8Array(newSize);
next.set(this.stream);
this.stream = next;
}
}

View File

@@ -7,7 +7,7 @@ import { useConsoleLogger } from "@/app/hooks/useConsoleLogger";
interface TransportContextValue {
transportServer: string | null;
uploadFile: (id: string, content: string) => Promise<any>;
downloadFile: (id: string, tag: string) => Promise<string>;
downloadFile: (id: string, tag: string, transportServer: string) => Promise<string>;
uploading: TransportState[];
downloading: TransportState[];
}
@@ -86,14 +86,14 @@ export function TransportProvider(props: TransportProviderProps) {
* @param tag тег файла
* @param chachaDecryptedKey ключ для расшифровки файла
*/
const downloadFile = (id: string, tag : string) : Promise<string> => {
const downloadFile = (id: string, tag : string, transportServer: string) : Promise<string> => {
return new Promise((resolve, reject) => {
if (!transportServerRef.current) {
if (!transportServer) {
throw new Error("Transport server is not set");
}
setDownloading(prev => [...prev, { id: id, progress: 0 }]);
const xhr = new XMLHttpRequest();
xhr.open('GET', `${transportServerRef.current}/d/${tag}`);
xhr.open('GET', `${transportServer}/d/${tag}`);
xhr.responseType = 'text';
xhr.onprogress = (event) => {

View File

@@ -0,0 +1,12 @@
import { useContext } from "react";
import { TransportContext } from "./TransportProvider";
export function useTransportServer() {
const context = useContext(TransportContext);
if(!context){
throw new Error("useTransportServer must be used within a TransportProvider");
}
const { transportServer } = context;
return transportServer;
}

View File

@@ -1,8 +1,8 @@
export const SERVERS = [
//'wss://cdn.rosetta-im.com',
//'ws://10.211.55.2:3000',
'ws://10.211.55.2:3000',
//'ws://192.168.6.82:3000',
'wss://wss.rosetta.im'
//'wss://wss.rosetta.im'
];
export function selectServer(): string {

View File

@@ -18,109 +18,128 @@ const size = process.platform === 'darwin' ? 18 : 22
const logger = Logger('main')
const icon = nativeImage
.createFromPath(join(__dirname, '../../resources/R.png'))
.resize({ width: size, height: size })
.createFromPath(join(__dirname, '../../resources/R.png'))
.resize({ width: size, height: size })
if (!lockInstance) {
app.quit()
process.exit(0)
app.quit()
process.exit(0)
}
process.on('unhandledRejection', (reason) => {
logger.log(`main thread error, reason: ${reason}`)
logger.log(`main thread error, reason: ${reason}`)
})
app.disableHardwareAcceleration()
app.on('second-instance', () => {
const allWindows = BrowserWindow.getAllWindows()
if (allWindows.length) {
const mainWindow = allWindows[0]
if (mainWindow.isMinimized()) mainWindow.restore()
if (!mainWindow.isVisible()) mainWindow.show()
mainWindow.focus()
}
const allWindows = BrowserWindow.getAllWindows()
if (allWindows.length) {
const mainWindow = allWindows[0]
if (mainWindow.isMinimized()) mainWindow.restore()
if (!mainWindow.isVisible()) mainWindow.show()
mainWindow.focus()
}
})
export const restoreApplicationAfterClickOnTrayOrDock = () => {
const allWindows = BrowserWindow.getAllWindows()
if (allWindows.length > 0) {
const mainWindow = allWindows[0]
if (mainWindow.isMinimized()) {
mainWindow.restore()
return
}
if (!mainWindow.isVisible()) {
mainWindow.show()
}
mainWindow.focus()
} else {
createAppWindow()
}
const allWindows = BrowserWindow.getAllWindows()
if (allWindows.length > 0) {
const mainWindow = allWindows[0]
if (mainWindow.isMinimized()) {
mainWindow.restore()
return
}
if (!mainWindow.isVisible()) {
mainWindow.show()
}
mainWindow.focus()
} else {
createAppWindow()
}
}
app.whenReady().then(async () => {
electronApp.setAppUserModelId('Rosetta')
electronApp.setAppUserModelId('Rosetta')
// Убираем File/View и оставляем только app + минимальный Edit (roles)
if (process.platform === 'darwin') {
const minimalMenu = Menu.buildFromTemplate([
{
label: app.name,
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
]
},
{
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ role: 'pasteAndMatchStyle' },
{ role: 'delete' },
{ role: 'selectAll' }
]
}
])
Menu.setApplicationMenu(minimalMenu)
} else {
Menu.setApplicationMenu(null)
}
// Убираем File/View и оставляем только app + минимальный Edit (roles)
if (process.platform === 'darwin') {
const minimalMenu = Menu.buildFromTemplate([
{
label: app.name,
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
]
},
{
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ role: 'pasteAndMatchStyle' },
{ role: 'delete' },
{ role: 'selectAll' }
]
}
])
Menu.setApplicationMenu(minimalMenu)
} else {
Menu.setApplicationMenu(null)
}
tray = new Tray(icon)
const contextMenu = Menu.buildFromTemplate([
{ label: 'Open App', click: () => restoreApplicationAfterClickOnTrayOrDock() },
{ label: 'Quit', click: () => app.quit() }
])
tray.setContextMenu(contextMenu)
tray.setToolTip('Rosetta')
tray.on('click', () => {
restoreApplicationAfterClickOnTrayOrDock()
})
tray = new Tray(icon)
const contextMenu = Menu.buildFromTemplate([
{ label: 'Open App', click: () => restoreApplicationAfterClickOnTrayOrDock() },
{ label: 'Quit', click: () => app.quit() }
])
tray.setContextMenu(contextMenu)
tray.setToolTip('Rosetta')
tray.on('click', () => {
restoreApplicationAfterClickOnTrayOrDock()
})
startApplication()
startApplication()
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
const isDevBuild =
!app.isPackaged ||
process.env.NODE_ENV === 'development' ||
Boolean(process.env.ELECTRON_RENDERER_URL)
app.on('activate', () => {
restoreApplicationAfterClickOnTrayOrDock()
})
app.on('browser-window-created', (_, window) => {
// В production оставляем стандартную защиту шорткатов
if (!isDevBuild) {
optimizer.watchWindowShortcuts(window)
return
}
// В dev явно разрешаем Ctrl+R и Cmd+R для перезагрузки, так как в режиме разработки это часто нужно
window.webContents.on('before-input-event', (event, input) => {
const key = input.key?.toLowerCase?.() ?? ''
const isReload = input.type === 'keyDown' && (input.meta || input.control) && key === 'r'
if (isReload) {
event.preventDefault()
window.webContents.reloadIgnoringCache()
}
})
})
app.on('activate', () => {
restoreApplicationAfterClickOnTrayOrDock()
})
})
app.on('window-all-closed', () => {
if (process.platform === 'darwin') {
app.hide()
}
if (process.platform === 'darwin') {
app.hide()
}
})