Drop observable-hooks in favor of useBehavior

With this change, views now read every reactive value through `useBehavior`, as
`architecture.md` already prescribes. Where a view reads a plain stream, the view
model now exposes a `Behavior`.
This commit is contained in:
Johannes Marbach
2026-09-28 17:54:50 +02:00
parent addc1b10a0
commit 3f63a6fc3c
22 changed files with 459 additions and 113 deletions
+8 -12
View File
@@ -20,8 +20,6 @@ import {
import useMeasure from "react-use-measure";
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
import classNames from "classnames";
import { map } from "rxjs";
import { useObservable } from "observable-hooks";
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
import { useTranslation } from "react-i18next";
@@ -77,10 +75,11 @@ import {
useAppBarSubtitle,
} from "../AppBar.tsx";
import { useBehavior } from "../useBehavior.ts";
import { useValueBehavior } from "../useValueBehavior.ts";
import { constant } from "../state/Behavior.ts";
import { Toast } from "../Toast.tsx";
import overlayStyles from "../Overlay.module.css";
import { useTrackProcessorObservable$ } from "../livekit/TrackProcessorContext.tsx";
import { useTrackProcessorState$ } from "../livekit/TrackProcessorContext.tsx";
import { type Layout } from "../state/layout-types.ts";
import { ObservableScope } from "../state/ObservableScope.ts";
import { CallFooter, type FooterSnapshot } from "../components/CallFooter.tsx";
@@ -121,7 +120,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
const urlParams = useUrlParams();
const hostBridge = useHostBridge();
const mediaDevices = useMediaDevices();
const trackProcessorState$ = useTrackProcessorObservable$();
const trackProcessorState$ = useTrackProcessorState$();
// The element we have to draw the call in: the page, or the container a host
// gave us. Its size, not the window's, decides how the call is laid out.
const rootElement = useRootElement();
@@ -151,7 +150,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
},
reactionsReader.raisedHands$,
reactionsReader.reactions$,
scope.behavior(trackProcessorState$),
trackProcessorState$,
);
// TODO move this somewhere else once we use the callViewModel in the lobby as well!
vm.join();
@@ -252,7 +251,7 @@ export const InCallView: FC<InCallViewProps> = ({
useWakeLock();
// TODO-MULTI-SFU This is unused now??
// const connectionState = useObservableEagerState(vm.livekitConnectionState$);
// const connectionState = useBehavior(vm.livekitConnectionState$);
// annoyingly we don't get the disconnection reason this way,
// only by listening for the emitted event
@@ -374,10 +373,7 @@ export const InCallView: FC<InCallViewProps> = ({
edgeToEdge,
],
);
const gridBoundsObservable$ = useObservable(
(inputs$) => inputs$.pipe(map(([gridBounds]) => gridBounds)),
[gridBounds],
);
const gridBounds$ = useValueBehavior(gridBounds);
useAppBarHidden(!showHeader);
useAppBarSubtitle(
@@ -538,7 +534,7 @@ export const InCallView: FC<InCallViewProps> = ({
);
const layouts = useMemo(() => {
const inputs = { minBounds$: gridBoundsObservable$ };
const inputs = { minBounds$: gridBounds$ };
return {
grid: makeGridLayout(inputs),
"spotlight-landscape": makeSpotlightLandscapeLayout(inputs),
@@ -547,7 +543,7 @@ export const InCallView: FC<InCallViewProps> = ({
"one-on-one-desktop": makeOneOnOneDesktopLayout(inputs),
"one-on-one-mobile": makeOneOnOneMobileLayout(inputs),
};
}, [gridBoundsObservable$]);
}, [gridBounds$]);
const showFooter = useBehavior(footerVm.showFooter$);
const renderContent = (): JSX.Element => {
+1 -4
View File
@@ -24,7 +24,6 @@ import {
type LocalVideoTrack,
Track,
} from "livekit-client";
import { useObservableEagerState } from "observable-hooks";
import inCallStyles from "./InCallView.module.css";
import styles from "./LobbyView.module.css";
@@ -125,9 +124,7 @@ export const LobbyView: FC<Props> = ({
);
const devices = useMediaDevices();
const videoInputId = useObservableEagerState(
devices.videoInput.selected$,
)?.id;
const videoInputId = useBehavior(devices.videoInput.selected$)?.id;
// Capture the audio options as they were when we first mounted, because
// we're not doing anything with the audio anyway so we don't need to