[verified] fix: preserve call footer and compact presence

This commit is contained in:
2026-08-03 19:20:33 +02:00
parent 7726614aa0
commit c699ee540b
6 changed files with 111 additions and 7 deletions
+1 -1
View File
@@ -16,7 +16,7 @@
"check:prettier": "prettier --check .",
"fix:prettier": "prettier --write .",
"typecheck": "tsc --noEmit",
"test:botsu": "node --experimental-strip-types --test src/botsu/apps/catalog.test.ts src/botsu/apps/launcher.test.ts src/botsu/profile/summary.test.ts src/botsu/presence/connection-generation.test.ts src/botsu/presence/cursor-publisher.test.ts src/botsu/presence/cursor-state.test.ts src/botsu/presence/socket-message.test.ts src/botsu/presence/state.test.ts",
"test:botsu": "node --experimental-strip-types --test src/botsu/apps/catalog.test.ts src/botsu/apps/launcher.test.ts src/botsu/profile/summary.test.ts src/botsu/presence/compact-presence.test.ts src/botsu/presence/connection-generation.test.ts src/botsu/presence/cursor-publisher.test.ts src/botsu/presence/cursor-state.test.ts src/botsu/presence/socket-message.test.ts src/botsu/presence/state.test.ts src/botsu/shell/layout.test.ts",
"typecheck:botsu": "../../node_modules/.bin/tsc -p tsconfig.botsu.json && ../../node_modules/.bin/tsc -p tsconfig.botsu-ui.json",
"prepare": "husky install",
"commit": "git-cz",
@@ -19,6 +19,7 @@ import {
type RemoteCursorState,
} from './cursor-state';
import { createPresenceWebSocketUrl, parsePresenceSocketMessage } from './socket-message';
import { createCompactPresence } from './compact-presence';
const getInitials = (displayName: string): string =>
displayName
@@ -178,6 +179,7 @@ export function BotsuPresence() {
() => projectVisibleRemoteCursors(remoteCursors, state.participants, location.pathname),
[location.pathname, remoteCursors, state.participants]
);
const compactPresence = useMemo(() => createCompactPresence(participants, 4), [participants]);
const statusLabel = {
connecting: 'Connexion…',
@@ -196,13 +198,19 @@ export function BotsuPresence() {
</div>
{participants.length > 0 && (
<ul aria-label="Membres présents">
{participants.map((participant) => {
{participants.map((participant, index) => {
const avatarUrl = participant.avatarUrl
? mxcUrlToHttp(mx, participant.avatarUrl, useAuthentication, 48, 48, 'crop') ??
undefined
: undefined;
return (
<li key={participant.userId} title={participant.userId}>
<li
className={
index >= compactPresence.visible.length ? 'botsu-visually-hidden' : undefined
}
key={participant.userId}
title={`${participant.displayName} (${participant.userId})`}
>
<span className="botsu-presence-avatar" aria-hidden="true">
{avatarUrl ? (
<img src={avatarUrl} alt="" />
@@ -210,10 +218,15 @@ export function BotsuPresence() {
getInitials(participant.displayName)
)}
</span>
<span>{participant.displayName}</span>
<span className="botsu-visually-hidden">{participant.displayName}</span>
</li>
);
})}
{compactPresence.hiddenCount > 0 && (
<li className="botsu-presence-overflow" aria-hidden="true">
+{compactPresence.hiddenCount}
</li>
)}
</ul>
)}
</aside>
@@ -0,0 +1,23 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createCompactPresence } from './compact-presence.ts';
const participants = ['Ada', 'Bob', 'Chloé', 'Dina', 'Eli'].map((displayName, index) => ({
userId: `@member${index}:botsu.net`,
displayName,
}));
test('compact presence keeps four members visible and counts the remainder', () => {
const compact = createCompactPresence(participants, 4);
assert.deepEqual(
compact.visible.map((participant) => participant.displayName),
['Ada', 'Bob', 'Chloé', 'Dina']
);
assert.equal(compact.hiddenCount, 1);
assert.equal(compact.totalCount, 5);
});
test('compact presence rejects an invalid visible member limit', () => {
assert.throws(() => createCompactPresence(participants, 0), /positive integer/);
});
@@ -0,0 +1,20 @@
export type CompactPresence<T> = {
visible: readonly T[];
hiddenCount: number;
totalCount: number;
};
export const createCompactPresence = <T>(
participants: readonly T[],
visibleLimit: number
): CompactPresence<T> => {
if (!Number.isInteger(visibleLimit) || visibleLimit < 1) {
throw new RangeError('visibleLimit must be a positive integer');
}
return {
visible: participants.slice(0, visibleLimit),
hiddenCount: Math.max(0, participants.length - visibleLimit),
totalCount: participants.length,
};
};
@@ -0,0 +1,20 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
const shellCssUrl = new URL('./shell.css', import.meta.url);
const getRuleBody = (css: string, selector: string): string => {
const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const match = css.match(new RegExp(`${escapedSelector}\\s*\\{([^}]*)\\}`));
assert.ok(match, `Missing CSS rule for ${selector}`);
return match[1];
};
test('BOTSU shell leaves room for the active call footer', async () => {
const css = await readFile(shellCssUrl, 'utf8');
const shellRule = getRuleBody(css, '.botsu-shell');
assert.match(shellRule, /height:\s*100%;/);
assert.doesNotMatch(shellRule, /height:\s*100dvh;/);
});
+31 -3
View File
@@ -1,7 +1,7 @@
.botsu-shell {
width: 100%;
min-width: 0;
height: 100dvh;
height: 100%;
overflow: auto;
color: var(--botsu-color-text);
background-color: var(--botsu-color-canvas);
@@ -183,7 +183,7 @@
.botsu-presence ul {
display: flex;
justify-content: flex-end;
gap: 0.3rem;
gap: 0;
margin: 0;
padding: 0;
list-style: none;
@@ -192,10 +192,13 @@
.botsu-presence li {
display: flex;
align-items: center;
gap: 0.3rem;
font-size: 0.68rem;
}
.botsu-presence li + li {
margin-left: -0.35rem;
}
.botsu-presence-avatar {
width: 1.5rem;
height: 1.5rem;
@@ -215,6 +218,31 @@
object-fit: cover;
}
.botsu-presence-overflow {
min-width: 1.5rem;
height: 1.5rem;
justify-content: center;
color: var(--botsu-color-text);
background: var(--botsu-color-surface);
border: var(--botsu-border-width) solid var(--botsu-color-border);
border-radius: 999px;
font-family: ui-monospace, monospace;
font-size: 0.58rem;
font-weight: 800;
}
.botsu-visually-hidden {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
.botsu-remote-cursors {
position: fixed;
inset: 0;