Add a background without putting it on, before joining too

- Added to the backgrounds on offer and in force once chosen, in the lobby
  exactly as in a call. Choosing a file and choosing to wear it are two
  decisions, and one rule for both places is one less thing to know.
- The footer's before-joining flag existed only for the other rule, so it
  goes, from both view models and the story.
- The edge-fade story waits longer for a scroll to settle: under the full
  parallel run a wait could outlast the default second.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
fkwp
2026-09-23 13:07:17 +02:00
co-authored by Claude Opus 5.5
parent fd972a717e
commit f2782c565e
4 changed files with 32 additions and 39 deletions
-1
View File
@@ -136,7 +136,6 @@ export const Default: Story = {
videoBlurEnabled: true, videoBlurEnabled: true,
backgroundEffect: "none", backgroundEffect: "none",
selectBackgroundEffect: fn(), selectBackgroundEffect: fn(),
beforeJoining: false,
hangup: fn(), hangup: fn(),
buttonSize: "lg", buttonSize: "lg",
showFooter: true, showFooter: true,
+14 -26
View File
@@ -99,12 +99,6 @@ export interface FooterState {
videoBlurEnabled: boolean; videoBlurEnabled: boolean;
/** The chosen background effect, in its stored form. */ /** The chosen background effect, in its stored form. */
backgroundEffect: string; backgroundEffect: string;
/**
* Whether this footer is shown before joining. An image added there becomes
* the background at once; one added in a call waits to be chosen, because
* putting it on would change what everyone sees with no further word.
*/
beforeJoining: boolean;
showFooter: boolean; showFooter: boolean;
/* This is needed for WindowMode = "flat" */ /* This is needed for WindowMode = "flat" */
@@ -189,7 +183,6 @@ export const CallFooter: FC<FooterProps> = ({
const selectVideoButtonOption = useBehavior(vm.selectVideoButtonOption$); const selectVideoButtonOption = useBehavior(vm.selectVideoButtonOption$);
const backgroundEffect = useBehavior(vm.backgroundEffect$); const backgroundEffect = useBehavior(vm.backgroundEffect$);
const selectBackgroundEffect = useBehavior(vm.selectBackgroundEffect$); const selectBackgroundEffect = useBehavior(vm.selectBackgroundEffect$);
const beforeJoining = useBehavior(vm.beforeJoining$);
const { added, addBackground, removeBackground } = useAddedBackgrounds(); const { added, addBackground, removeBackground } = useAddedBackgrounds();
const { settling, cameraTrack } = useBackgroundProcessing(); const { settling, cameraTrack } = useBackgroundProcessing();
@@ -241,26 +234,21 @@ export const CallFooter: FC<FooterProps> = ({
const onAddBackgroundImage = useCallback( const onAddBackgroundImage = useCallback(
(file: File): void => { (file: File): void => {
setBackgroundEffectError(undefined); setBackgroundEffectError(undefined);
addBackground(file) // Added, not put on — before joining and during a call alike. It joins
.then((id) => { // the backgrounds on offer and is in force once the user chooses it:
// Before joining, nobody sees the change, so the picture goes on at // choosing a file and choosing to wear it are two decisions, and one
// once. In a call it waits to be chosen: otherwise choosing a file // rule for both places is one less thing to know.
// would change what everyone sees, with no further word from the addBackground(file).catch((e) => {
// user. Teams draws the line in the same place. setBackgroundEffectError(whyRefused(e));
if (beforeJoining) logger.warn(
selectBackgroundEffect?.(serializeEffect({ kind: "added", id })); e instanceof UnusableImage
}) ? `Cannot use that file as a background: ${e.reason}`
.catch((e) => { : "Could not keep that background",
setBackgroundEffectError(whyRefused(e)); e,
logger.warn( );
e instanceof UnusableImage });
? `Cannot use that file as a background: ${e.reason}`
: "Could not keep that background",
e,
);
});
}, },
[addBackground, beforeJoining, selectBackgroundEffect, whyRefused], [addBackground, whyRefused],
); );
// The catalogue is named here rather than in the view model: the names are // The catalogue is named here rather than in the view model: the names are
-2
View File
@@ -181,7 +181,6 @@ export function createCallFooterViewModel(
return { return {
...buildMuteBehaviors(scope, muteStates), ...buildMuteBehaviors(scope, muteStates),
...buildDeviceBehaviors(scope, mediaDevices, disableDeviceSwitcher$), ...buildDeviceBehaviors(scope, mediaDevices, disableDeviceSwitcher$),
beforeJoining$: constant(false),
// candidat to move into the FooterViewModel // candidat to move into the FooterViewModel
showFooter$: callModel.showFooter$, showFooter$: callModel.showFooter$,
hideControls$: constant(!showControls), hideControls$: constant(!showControls),
@@ -295,6 +294,5 @@ export function createLobbyFooterViewModel(
}), }),
...buildMuteBehaviors(scope, muteStates), ...buildMuteBehaviors(scope, muteStates),
...buildDeviceBehaviors(scope, mediaDevices, constant(false)), ...buildDeviceBehaviors(scope, mediaDevices, constant(false)),
beforeJoining$: constant(true),
}; };
} }
@@ -1097,7 +1097,9 @@ export const BackgroundEffectsShowThereIsMore: Story = {
edge.classList.contains(styles.scrollEdgeShown); edge.classList.contains(styles.scrollEdgeShown);
// Opened at the top: more below, nothing above. // Opened at the top: more below, nothing above.
await waitFor(async () => expect(shown(bottom)).toBe(true)); await waitFor(async () => expect(shown(bottom)).toBe(true), {
timeout: 4000,
});
await expect(shown(top)).toBe(false); await expect(shown(top)).toBe(false);
// And the fade is at the foot of what is in view, not of the content. // And the fade is at the foot of what is in view, not of the content.
await expect( await expect(
@@ -1109,8 +1111,12 @@ export const BackgroundEffectsShowThereIsMore: Story = {
// At the end: nothing below, more above. // At the end: nothing below, more above.
list.scrollTop = list.scrollHeight; list.scrollTop = list.scrollHeight;
await waitFor(async () => expect(shown(bottom)).toBe(false)); await waitFor(async () => expect(shown(bottom)).toBe(false), {
await waitFor(async () => expect(shown(top)).toBe(true)); timeout: 4000,
});
await waitFor(async () => expect(shown(top)).toBe(true), {
timeout: 4000,
});
// Standing just below the heading holding the top, where the content comes // Standing just below the heading holding the top, where the content comes
// out from under it — not behind the heading, where it could not be seen. // out from under it — not behind the heading, where it could not be seen.
@@ -1124,13 +1130,15 @@ export const BackgroundEffectsShowThereIsMore: Story = {
) < 2, ) < 2,
)!; )!;
await expect(stuck).toBeDefined(); await expect(stuck).toBeDefined();
await waitFor(async () => await waitFor(
expect( async () =>
Math.round( expect(
top.getBoundingClientRect().top - Math.round(
stuck.getBoundingClientRect().bottom, top.getBoundingClientRect().top -
), stuck.getBoundingClientRect().bottom,
).toBe(0), ),
).toBe(0),
{ timeout: 4000 },
); );
}, },
}; };