CI / Build & Quality Checks (push) Skipped
CI / Secret scan (gitleaks) (push) Skipped
CI / Playwright smoke (e2e) (push) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 23s
CI / Build & Quality Checks (pull_request) Successful in 5m9s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Playwright smoke (e2e) (pull_request) Successful in 23m21s
Settings → Calls → "In-game overlay (beta)", desktop only, off by default, device-local. While a call is joined it sends the call snapshot to the desktop's new `set_overlay_state` command, which shows a small always-on-top, click-through list while Lotus isn't focused. - utils/callOverlay: pure snapshot builder. Everyone in the call, you first, one row per person across devices (speaking if any device is, muted only if all are), your own mute/deafen/PTT. In PTT mode you're not shown as Muted while the key is up. - useTauriOverlay: live io.lotus.call_state participants plus control state, coalesced to at most one send per 100 ms (latest always lands), unchanged snapshots skipped, cleared on unmount. - Overlay position setting (four corners). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
125 lines
3.8 KiB
TypeScript
125 lines
3.8 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { buildOverlaySnapshot, inactiveOverlay, OverlayInput } from './callOverlay';
|
|
|
|
const names: Record<string, string> = { '@me:x': 'Me', '@a:x': 'Alice', '@b:x': 'Bob' };
|
|
|
|
const base = (over: Partial<OverlayInput> = {}): OverlayInput => ({
|
|
enabled: true,
|
|
joined: true,
|
|
corner: 'top-left',
|
|
myUserId: '@me:x',
|
|
participants: [
|
|
{ userId: '@me:x', speaking: false, audioEnabled: true },
|
|
{ userId: '@a:x', speaking: true, audioEnabled: true },
|
|
{ userId: '@b:x', speaking: false, audioEnabled: false },
|
|
],
|
|
nameOf: (id) => names[id] ?? id,
|
|
muted: false,
|
|
deafened: false,
|
|
pttMode: false,
|
|
...over,
|
|
});
|
|
|
|
test('inactive when the setting is off or there is no joined call', () => {
|
|
assert.deepEqual(buildOverlaySnapshot(base({ enabled: false })), inactiveOverlay('top-left'));
|
|
assert.deepEqual(
|
|
buildOverlaySnapshot(base({ joined: false, corner: 'bottom-right' })),
|
|
inactiveOverlay('bottom-right'),
|
|
);
|
|
});
|
|
|
|
test('everyone is listed, you first, speakers and muted people marked', () => {
|
|
const s = buildOverlaySnapshot(base());
|
|
assert.equal(s.active, true);
|
|
assert.deepEqual(
|
|
s.people.map((p) => [p.name, p.speaking, p.muted, p.isSelf]),
|
|
[
|
|
['Me', false, false, true],
|
|
['Alice', true, false, false],
|
|
['Bob', false, true, false],
|
|
],
|
|
);
|
|
});
|
|
|
|
test('you are listed first even when the fork reports you later', () => {
|
|
const s = buildOverlaySnapshot(
|
|
base({
|
|
participants: [
|
|
{ userId: '@a:x', speaking: false, audioEnabled: true },
|
|
{ userId: '@me:x', speaking: true, audioEnabled: true },
|
|
],
|
|
}),
|
|
);
|
|
assert.deepEqual(
|
|
s.people.map((p) => p.userId),
|
|
['@me:x', '@a:x'],
|
|
);
|
|
assert.equal(s.people[0].speaking, true);
|
|
});
|
|
|
|
test('several devices of one person become one row', () => {
|
|
const s = buildOverlaySnapshot(
|
|
base({
|
|
participants: [
|
|
{ userId: '@a:x', speaking: false, audioEnabled: false },
|
|
{ userId: '@a:x', speaking: true, audioEnabled: true },
|
|
{ userId: '@b:x', speaking: false, audioEnabled: false },
|
|
{ userId: '@b:x', speaking: false, audioEnabled: false },
|
|
],
|
|
}),
|
|
);
|
|
const alice = s.people.find((p) => p.userId === '@a:x');
|
|
const bob = s.people.find((p) => p.userId === '@b:x');
|
|
assert.equal(s.people.filter((p) => p.userId === '@a:x').length, 1);
|
|
assert.deepEqual([alice?.speaking, alice?.muted], [true, false]);
|
|
assert.deepEqual([bob?.speaking, bob?.muted], [false, true]);
|
|
});
|
|
|
|
test('your own mute and deafen state', () => {
|
|
const muted = buildOverlaySnapshot(base({ muted: true }));
|
|
assert.equal(muted.muted, true);
|
|
assert.equal(muted.people[0].muted, true);
|
|
|
|
const deaf = buildOverlaySnapshot(
|
|
base({
|
|
deafened: true,
|
|
participants: [{ userId: '@me:x', speaking: true, audioEnabled: true }],
|
|
}),
|
|
);
|
|
assert.equal(deaf.deafened, true);
|
|
assert.equal(deaf.people[0].muted, true);
|
|
assert.equal(deaf.people[0].speaking, false);
|
|
});
|
|
|
|
test('push-to-talk: not shown as muted while the key is up', () => {
|
|
const s = buildOverlaySnapshot(base({ pttMode: true, muted: true }));
|
|
assert.equal(s.ptt, true);
|
|
assert.equal(s.muted, false);
|
|
assert.equal(s.people[0].muted, false);
|
|
});
|
|
|
|
test('you are still listed before the fork reports you', () => {
|
|
const s = buildOverlaySnapshot(base({ participants: [] }));
|
|
assert.deepEqual(
|
|
s.people.map((p) => [p.name, p.isSelf]),
|
|
[['Me', true]],
|
|
);
|
|
});
|
|
|
|
test('non-user ids from the fork are ignored', () => {
|
|
const s = buildOverlaySnapshot(
|
|
base({
|
|
myUserId: null,
|
|
participants: [
|
|
{ userId: 'screen:1', speaking: true, audioEnabled: true },
|
|
{ userId: '@a:x', speaking: false, audioEnabled: true },
|
|
],
|
|
}),
|
|
);
|
|
assert.deepEqual(
|
|
s.people.map((p) => p.userId),
|
|
['@a:x'],
|
|
);
|
|
});
|