feat: Add deafen to pc, fix mobiel view, fix freeze on startup

This commit is contained in:
2026-06-05 15:27:06 +02:00
parent 35f52b0356
commit a675f12e61
85 changed files with 2499 additions and 519 deletions

View File

@@ -0,0 +1,11 @@
import { describe, expect, it } from 'vitest';
import { decodeBase64ToUint8Array } from './attachment-blob.rules';
describe('attachment blob rules', () => {
it('decodes base64 payloads into byte arrays', () => {
const bytes = decodeBase64ToUint8Array('QUJD');
expect(Array.from(bytes)).toEqual([65, 66, 67]);
});
});

View File

@@ -0,0 +1,21 @@
/** Chunk size used when rebuilding attachment blobs from disk without blocking the UI thread. */
export const ATTACHMENT_BLOB_READ_CHUNK_SIZE_BYTES = 256 * 1024;
/** Decode a base64 payload into bytes for Blob construction. */
export function decodeBase64ToUint8Array(base64: string): Uint8Array {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index++) {
bytes[index] = binary.charCodeAt(index);
}
return bytes;
}
/** Yield control back to the browser so long attachment hydration cannot freeze Electron. */
export function yieldToAttachmentHydrationLoop(): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, 0);
});
}

View File

@@ -0,0 +1,63 @@
import {
describe,
expect,
it
} from 'vitest';
import {
dedupeImageAttachmentsForDisplay,
hasImageFilename,
isImageAttachment,
isInlineDisplayableImage,
resolvePublishAttachmentIsImage
} from './attachment-image.rules';
describe('attachment-image rules', () => {
it('detects images from mime, flag, or filename extension', () => {
expect(isImageAttachment({
id: '1',
filename: 'logo.PNG',
mime: 'application/octet-stream',
isImage: false,
available: false
})).toBe(true);
expect(hasImageFilename('photo.jpeg')).toBe(true);
expect(resolvePublishAttachmentIsImage({ name: 'photo.png', type: '' })).toBe(true);
});
it('treats file protocol urls as not inline displayable', () => {
expect(isInlineDisplayableImage({
available: true,
objectUrl: 'file:///tmp/photo.png'
})).toBe(false);
expect(isInlineDisplayableImage({
available: true,
objectUrl: 'blob:http://localhost/abc'
})).toBe(true);
});
it('dedupes image attachments by filename and prefers displayable copies', () => {
const deduped = dedupeImageAttachmentsForDisplay([
{
id: 'a',
filename: 'photo.png',
mime: 'image/png',
isImage: true,
available: false
},
{
id: 'b',
filename: 'photo.png',
mime: 'application/octet-stream',
isImage: false,
available: true,
objectUrl: 'blob:http://localhost/photo'
}
]);
expect(deduped).toHaveLength(1);
expect(deduped[0]?.id).toBe('b');
});
});

View File

@@ -0,0 +1,96 @@
import { needsBlobObjectUrlForInlineDisplay } from './attachment-display-url.rules';
const IMAGE_EXTENSIONS = new Set([
'.apng',
'.avif',
'.bmp',
'.gif',
'.heic',
'.heif',
'.jpg',
'.jpeg',
'.png',
'.svg',
'.webp'
]);
export interface ImageAttachmentCandidate {
available: boolean;
filename: string;
filePath?: string;
id: string;
isImage: boolean;
mime: string;
objectUrl?: string;
savedPath?: string;
}
export function hasImageFilename(filename: string): boolean {
const normalized = filename.trim().toLowerCase();
const extensionIndex = normalized.lastIndexOf('.');
if (extensionIndex <= 0) {
return false;
}
return IMAGE_EXTENSIONS.has(normalized.slice(extensionIndex));
}
export function isImageAttachment(attachment: Pick<ImageAttachmentCandidate, 'filename' | 'isImage' | 'mime'>): boolean {
return attachment.isImage ||
attachment.mime.startsWith('image/') ||
hasImageFilename(attachment.filename);
}
export function isInlineDisplayableImage(
attachment: Pick<ImageAttachmentCandidate, 'available' | 'objectUrl'>
): boolean {
return attachment.available &&
!!attachment.objectUrl &&
!needsBlobObjectUrlForInlineDisplay(attachment.objectUrl);
}
export function imageAttachmentDisplayRank(
attachment: Pick<ImageAttachmentCandidate, 'available' | 'filePath' | 'isImage' | 'objectUrl' | 'savedPath'>
): number {
if (isInlineDisplayableImage(attachment)) {
return 4;
}
if (attachment.savedPath || attachment.filePath) {
return 3;
}
if (attachment.available && attachment.objectUrl) {
return 2;
}
if (attachment.isImage) {
return 1;
}
return 0;
}
export function dedupeImageAttachmentsForDisplay<T extends ImageAttachmentCandidate>(attachments: readonly T[]): T[] {
const byFilename = new Map<string, T>();
for (const attachment of attachments) {
if (!isImageAttachment(attachment)) {
continue;
}
const key = attachment.filename.trim().toLowerCase();
const existing = byFilename.get(key);
if (!existing || imageAttachmentDisplayRank(attachment) > imageAttachmentDisplayRank(existing)) {
byFilename.set(key, attachment);
}
}
return Array.from(byFilename.values());
}
export function resolvePublishAttachmentIsImage(file: Pick<File, 'name' | 'type'>): boolean {
return file.type.startsWith('image/') || hasImageFilename(file.name);
}