[verified] fix: preserve call footer and compact presence
This commit is contained in:
@@ -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;/);
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user