825160ee46
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>
98 lines
3.1 KiB
TypeScript
98 lines
3.1 KiB
TypeScript
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]);
|
|
});
|
|
});
|