feat(desktop): port v0.11.4-v0.15.2 from Tauri to Electron + Discord-parity audio (v0.16.0)
Chronological port of every Tauri release commit (v0.11.4 -> v0.15.2)
into the Electron rebuild, plus Discord-style audio handling that goes
beyond the Tauri original.
Highlights:
- All 17 Tauri release commits ported (audio fixes, custom notification
sound, Discord-style chat UX, profile banner, changelog page,
Discord-parity call UX, screen-share echo + re-watch UX, audio-loop
fix).
- Native napi-rs audio-loopback addon with WASAPI process-loopback:
* EXCLUDE_TARGET_PROCESS_TREE for full-screen shares -> peers
never hear themselves echoed back through the capture.
* INCLUDE_TARGET_PROCESS_TREE for window shares -> only the
picked window's audio is captured, not the whole OS mixer
(Discord parity).
* HWND -> PID resolution via Win32 GetWindowThreadProcessId.
- Discord-style screen-source picker (thumbnail grid, screens vs
apps tabs, live-refreshing thumbnails).
- Hash routing fix for packaged builds (file:// can't resolve
BrowserRouter paths).
- Tauri sources removed (apps/desktop/src-tauri).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
parseMessagePayload,
|
||||
type AttachmentHandle,
|
||||
type DecryptedMessage,
|
||||
} from '@chat-app/shared/chat';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
collectConversationAttachments,
|
||||
createPollPayload,
|
||||
summarizePollVotes,
|
||||
} from './conversationFeatures';
|
||||
|
||||
function handle(id: string, mimeType: string): AttachmentHandle {
|
||||
return {
|
||||
id,
|
||||
storagePath: 'conversation/' + id + '.bin',
|
||||
mimeType,
|
||||
sizeBytes: 2048,
|
||||
keyB64: 'key',
|
||||
nonceB64: 'nonce',
|
||||
};
|
||||
}
|
||||
|
||||
function message(id: string, createdAt: string, attachments: AttachmentHandle[]): DecryptedMessage {
|
||||
return {
|
||||
id,
|
||||
conversationId: 'conv-1',
|
||||
senderId: 'sender-1',
|
||||
senderDeviceId: 'device-1',
|
||||
replyToId: null,
|
||||
editedAt: null,
|
||||
deletedAt: null,
|
||||
createdAt,
|
||||
plaintext: JSON.stringify({ v: 1, type: 'text', text: '', attachments }),
|
||||
};
|
||||
}
|
||||
|
||||
describe('collectConversationAttachments', () => {
|
||||
it('indexes media, audio, and files newest first', () => {
|
||||
const first = message('m1', '2026-04-24T09:00:00.000Z', [
|
||||
handle('img-1', 'image/png'),
|
||||
handle('file-1', 'application/pdf'),
|
||||
]);
|
||||
const second = message('m2', '2026-04-24T10:00:00.000Z', [
|
||||
handle('audio-1', 'audio/webm'),
|
||||
handle('video-1', 'video/mp4'),
|
||||
]);
|
||||
|
||||
const index = collectConversationAttachments([first, second]);
|
||||
|
||||
expect(index.media.map((item) => item.handle.id)).toEqual(['video-1', 'img-1']);
|
||||
expect(index.audio.map((item) => item.handle.id)).toEqual(['audio-1']);
|
||||
expect(index.files.map((item) => item.handle.id)).toEqual(['file-1']);
|
||||
expect(index.all.map((item) => item.messageId)).toEqual(['m2', 'm2', 'm1', 'm1']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPollPayload', () => {
|
||||
it('creates a parseable encrypted-message poll payload', () => {
|
||||
const payload = createPollPayload(' Lieblings Feature? ', [' Media Drawer ', '', 'Polls']);
|
||||
const parsed = parseMessagePayload(payload);
|
||||
|
||||
expect(parsed.kind).toBe('poll');
|
||||
if (parsed.kind !== 'poll') throw new Error('expected poll payload');
|
||||
expect(parsed.question).toBe('Lieblings Feature?');
|
||||
expect(parsed.options).toEqual([
|
||||
{ id: 'option-1', emoji: '1️⃣', text: 'Media Drawer' },
|
||||
{ id: 'option-2', emoji: '2️⃣', text: 'Polls' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('requires a question and at least two non-empty options', () => {
|
||||
expect(() => createPollPayload('', ['A', 'B'])).toThrow('question');
|
||||
expect(() => createPollPayload('Feature?', ['A', ''])).toThrow('options');
|
||||
});
|
||||
});
|
||||
|
||||
describe('summarizePollVotes', () => {
|
||||
it('counts only configured poll options and marks the current user vote', () => {
|
||||
const summary = summarizePollVotes(
|
||||
[
|
||||
{ id: 'option-1', emoji: '1️⃣', text: 'Media Drawer' },
|
||||
{ id: 'option-2', emoji: '2️⃣', text: 'Polls' },
|
||||
],
|
||||
[
|
||||
{ emoji: '1️⃣', count: 3, mine: false },
|
||||
{ emoji: '2️⃣', count: 1, mine: true },
|
||||
{ emoji: '🔥', count: 99, mine: true },
|
||||
],
|
||||
);
|
||||
|
||||
expect(summary.totalVotes).toBe(4);
|
||||
expect(summary.options.map((option) => option.percent)).toEqual([75, 25]);
|
||||
expect(summary.options.map((option) => option.mine)).toEqual([false, true]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user