Root cause found — it is in useResponsiveFooter, not in the subsession gate. (Correcting my earlier posts in this thread.) Reproduced consistently on UI Toolkit 2.4.5-1, component view.
Symptom: After a host opens a subsession, joins it, and returns to the main session, the Subsession, Whiteboard and Record buttons are gone from the footer — and they are not in the ⋯ overflow menu either. They are absent from the DOM entirely. Only a full leave + rejoin restores them.
Evidence — live React hook state read off the mounted Footer, in the broken state:
enabledButtons = ["participants","chat","whiteboard","record","realTimeMediaStreams","caption","subsession"]
visibleButtons = ["participants","chat","realTimeMediaStreams","caption"]
overflowButtons = []
width state (n) = 0
subsession, whiteboard and record are present in enabledButtons but in neither visibleButtons nor overflowButtons — so nothing renders them anywhere.
All gate terms are satisfied at that same moment, so this is not the button’s enable condition:
session.isHost = true
session.config.featuresOptions.subsession.enable = true
subsession.isSubsessionEnabled = true
getSubsessionClient().isSubsessionEnabled() = true (SDK agrees)
Mechanism. In useResponsiveFooter:
const [n, setN] = useState(0); // measured width
const [state, setState] = useState({
visibleButtons: new Set(enabledButtons),
overflowButtons: []
});
useEffect(() => {
const el = containerRef.current;
if (!el) return; // (1) early return -> no observer
const ro = new ResizeObserver(entries => { ...setN(width)... });
ro.observe(el);
const w = el.offsetWidth;
if (w > 0) setN(w);
return () => ro.disconnect();
}, [containerRef]); // (2) dep is the stable ref -> never re-runs
useEffect(() => {
if (n > 0) setState(compute(n)); // (3) gated on n > 0
}, [n, compute]);
Returning to the main session is a full reconnect (connection-change → { state: "Reconnecting", reason: "back to main session" }), which unmounts and remounts the Footer. On that remount the container is not laid out yet, so containerRef.current is null when the effect runs: (1) early-returns and no ResizeObserver is ever created. Because the dependency is the stable ref object, (2) the effect never runs again. n therefore stays 0 permanently, so (3) never fires.
That leaves the visible/overflow split frozen at its mount-time initial value. At that instant only 4 buttons had satisfied gates, so visibleButtons froze at those 4 and overflowButtons stayed []. whiteboard, record and subsession satisfy their gates a moment later and are correctly appended to enabledButtons — but because the split never recomputes, they are stranded in neither bucket and cannot render. That is also why they never appear in the ⋯ menu.
Confirmation: dispatching ui/setFooterEnable false → true (forcing only the Footer to remount, with the container now laid out) restores all three buttons immediately, with no rejoin. That isolates the defect to this hook.
Suggested fixes (any one resolves it; 1 is the most robust):
- Derive the split instead of storing it. Compute
{ visible, overflow } with useMemo from enabledButtons + width, treating an unmeasured width as “show all” rather than freezing. This structurally removes the “button in neither bucket” state.
- Do not gate the recompute on
n > 0. Recompute whenever enabledButtons changes, falling back to containerRef.current?.offsetWidth.
- Make observer attachment resilient. Use a callback ref (or re-run the effect when
enabledButtons changes) so a null containerRef.current on mount is retried instead of permanently skipped.
Invariant worth enforcing regardless: every entry in enabledButtons should always appear in exactly one of visibleButtons / overflowButtons. Today a button can silently exist in neither and become unreachable to the user.
Separately, in the same reconnect path: after returning to the main session, session.isJoined stays false, isJoinResolved stays false and sessionId stays null, while status is "Connected" and sessionInfo.isInMeeting is true. The Reconnecting branch resets session state, but the Connected branch never re-dispatches the join action.