feat(groups): audio and video calls in group chats; release v6.6.6
A group call is N-1 ordinary 1:1 calls, one to each other member, each riding the pairwise session that member already has — a transport a human already authenticated by comparing the safety code. No mixer, no SFU, no point at which two people's media meets anywhere but on a device. Call control is separate from call media, because the two reach different sets of people. Who opened a call, who joined and who left travels as group frames signed with the sender's group identity key, so it reaches members currently reachable only through a relay — and a relaying member can drop one but cannot write one. Media flows only where a direct link exists, so a member without one shows as connecting rather than being omitted. Frames carry a per-sender sequence checked before the action, so a captured leave cannot end a later call, and simultaneous calls converge on the lower random call id. One capture is shared across every leg rather than one getUserMedia per member, and legs answer without prompting: the flag permitting that is set only locally, only while this user is in the call, and cleared when they leave. UI: a gallery that sizes itself from the space it has, a spotlight view, an active-speaker indicator read from the waveform, and the call surface in the same visual language as the 1:1 one. Also in this commit, the v6.5.0 language-suggestion work that had not been pushed yet; its notes are in the changelog. And two fixes: the safety-code input asks for digits rather than text, and starting a new chat from inside a group no longer creates it behind the group where it cannot be seen — which had made it impossible to connect to anyone new, or to add anyone to a group, while a group was open. Claude-Session: https://claude.ai/code/session_01XSxAkET3hQTkYDQfbjCQwZ
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
// the code reaching the store fails here rather than on a user's screen.
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const { GroupSession, GROUP_FRAMES, groupFrameType, decodeEnvelope } =
|
||||
await import('../src/group/GroupSession.js');
|
||||
@@ -199,4 +200,43 @@ async function formPair(groupName) {
|
||||
'a failed group must not be confirmable');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// a group must not be able to trap the user inside it
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// One conversation owns the column at a time, and a group owns it
|
||||
// unconditionally: the 1:1 side of the screen is rendered only when no group is
|
||||
// active. So every action that is supposed to bring a CHAT to the front has to
|
||||
// clear the active group first, or it silently does nothing.
|
||||
//
|
||||
// "New chat" did not, and the consequence was not cosmetic. A group is built out
|
||||
// of chats that have already been verified, so adding a member means first
|
||||
// opening a chat with them — and from inside a group, the button that opens one
|
||||
// created a session that was never rendered: no invitation to copy, nothing to
|
||||
// verify. The admin was told there was nobody left to add and had no way to
|
||||
// change that.
|
||||
//
|
||||
// app.jsx cannot be imported here (it is a .jsx module that expects a browser),
|
||||
// so this reads the wiring. Crude, but it is the invariant that broke.
|
||||
{
|
||||
const app = readFileSync(new URL('../src/app.jsx', import.meta.url), 'utf8');
|
||||
|
||||
const newChat = app.slice(app.indexOf('const handleNewChat'), app.indexOf('const handleRenameSession'));
|
||||
assert.match(newChat, /SET_ACTIVE_GROUP, id: null/,
|
||||
'starting a new chat must clear the active group, or the new chat is never shown');
|
||||
assert.ok(
|
||||
newChat.indexOf('SET_ACTIVE_GROUP') < newChat.indexOf('createSessionRef'),
|
||||
'the group must be cleared before the session is created, so the new chat lands in front',
|
||||
);
|
||||
|
||||
// The sibling path has always done it; keep them together.
|
||||
const sidebar = app.slice(app.indexOf('React.createElement(SessionsSidebar'), app.indexOf('onSelectGroup:'));
|
||||
assert.match(sidebar, /onSelect: \(id\) => \{ groupsDispatch\(\{ type: GA\.SET_ACTIVE_GROUP, id: null \}\)/,
|
||||
'selecting a chat must clear the active group too');
|
||||
|
||||
// And the group view really is exclusive, which is why the above matters.
|
||||
assert.match(app, /activeGroup && React\.createElement\('main', \{\s*key: 'group-main'/);
|
||||
assert.match(app, /!activeGroup && React\.createElement\('main', \{\s*key: 'main'/);
|
||||
}
|
||||
|
||||
console.log('group-app-integration.test.mjs: all assertions passed');
|
||||
|
||||
Reference in New Issue
Block a user