Put an added background on before joining, not during a call

- Before joining nobody sees the change, so the picture goes on at once
- In a call it waits to be chosen: otherwise choosing a file would change
  what everyone sees with no further word from the user
- Teams draws the line in the same place, which is what settled it: a
  background added before joining applies by itself there, one added in a
  meeting has to be applied
- The footer learns which side of joining it is on from its view model,
  since the lobby and the call build their own

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
fkwp
2026-09-17 23:01:54 +02:00
co-authored by Claude Opus 5
parent ea25cde725
commit c7c44eea4e
3 changed files with 29 additions and 16 deletions
+1
View File
@@ -136,6 +136,7 @@ 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,
+26 -16
View File
@@ -94,6 +94,12 @@ 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" */
@@ -178,6 +184,7 @@ 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 [backgroundEffectError, setBackgroundEffectError] = useState< const [backgroundEffectError, setBackgroundEffectError] = useState<
string | undefined string | undefined
@@ -214,24 +221,27 @@ export const CallFooter: FC<FooterProps> = ({
const onAddBackgroundImage = useCallback( const onAddBackgroundImage = useCallback(
(file: File): void => { (file: File): void => {
// Added, not chosen. Whether picking a file should also put it on is
// still open, so this does the smaller thing: the image appears among
// the others and the user says when to wear it.
setBackgroundEffectError(undefined); setBackgroundEffectError(undefined);
addBackground(file).catch((e) => { addBackground(file)
// TODO: FR-021 wants the user told what went wrong. There is no .then((id) => {
// surface for that in the menu yet, and inventing one is design's // Before joining, nobody sees the change, so the picture goes on at
// call, so for now this is only logged. // once. In a call it waits to be chosen: otherwise choosing a file
setBackgroundEffectError(whyRefused(e)); // would change what everyone sees, with no further word from the
logger.warn( // user. Teams draws the line in the same place.
e instanceof UnusableImage if (beforeJoining)
? `Cannot use that file as a background: ${e.reason}` selectBackgroundEffect?.(serializeEffect({ kind: "added", id }));
: "Could not keep that background", })
e, .catch((e) => {
); setBackgroundEffectError(whyRefused(e));
}); logger.warn(
e instanceof UnusableImage
? `Cannot use that file as a background: ${e.reason}`
: "Could not keep that background",
e,
);
});
}, },
[addBackground, whyRefused], [addBackground, beforeJoining, selectBackgroundEffect, 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
@@ -180,6 +180,7 @@ 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),
@@ -293,5 +294,6 @@ export function createLobbyFooterViewModel(
}), }),
...buildMuteBehaviors(scope, muteStates), ...buildMuteBehaviors(scope, muteStates),
...buildDeviceBehaviors(scope, mediaDevices, constant(false)), ...buildDeviceBehaviors(scope, mediaDevices, constant(false)),
beforeJoining$: constant(true),
}; };
} }