feat: Add deafen to pc, fix mobiel view, fix freeze on startup
This commit is contained in:
@@ -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]);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user