Merge pull request #2381 from robintown/observable-hooks
Replace react-rxjs with observable-hooks
This commit is contained in:
@@ -38,15 +38,6 @@ module.exports = {
|
||||
"jsx-a11y/media-has-caption": "off",
|
||||
// We should use the js-sdk logger, never console directly.
|
||||
"no-console": ["error"],
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
name: "@react-rxjs/core",
|
||||
importNames: ["Subscribe", "RemoveSubscribe"],
|
||||
message:
|
||||
"These components are easy to misuse, please use the 'subscribe' component wrapper instead",
|
||||
},
|
||||
],
|
||||
"react/display-name": "error",
|
||||
},
|
||||
settings: {
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
"@react-aria/tabs": "^3.1.0",
|
||||
"@react-aria/tooltip": "^3.1.3",
|
||||
"@react-aria/utils": "^3.10.0",
|
||||
"@react-rxjs/core": "^0.10.7",
|
||||
"@react-spring/web": "^9.4.4",
|
||||
"@react-stately/collections": "^3.3.4",
|
||||
"@react-stately/select": "^3.1.3",
|
||||
|
||||
@@ -14,16 +14,15 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { CSSProperties, useMemo } from "react";
|
||||
import { StateObservable, state, useStateObservable } from "@react-rxjs/core";
|
||||
import { BehaviorSubject, distinctUntilChanged } from "rxjs";
|
||||
import { CSSProperties, forwardRef, useMemo } from "react";
|
||||
import { BehaviorSubject, Observable, distinctUntilChanged } from "rxjs";
|
||||
import { useObservableEagerState } from "observable-hooks";
|
||||
|
||||
import { GridLayout as GridLayoutModel } from "../state/CallViewModel";
|
||||
import { MediaViewModel } from "../state/MediaViewModel";
|
||||
import { LayoutSystem, Slot } from "./Grid";
|
||||
import styles from "./GridLayout.module.css";
|
||||
import { useReactiveState } from "../useReactiveState";
|
||||
import { subscribe } from "../state/subscribe";
|
||||
import { Alignment } from "../room/InCallView";
|
||||
import { useInitial } from "../useInitial";
|
||||
|
||||
@@ -48,7 +47,7 @@ const slotMaxAspectRatio = 17 / 9;
|
||||
const slotMinAspectRatio = 4 / 3;
|
||||
|
||||
export const gridLayoutSystems = (
|
||||
minBounds: StateObservable<Bounds>,
|
||||
minBounds: Observable<Bounds>,
|
||||
floatingAlignment: BehaviorSubject<Alignment>,
|
||||
): GridLayoutSystems => ({
|
||||
// The "fixed" (non-scrolling) part of the layout is where the spotlight tile
|
||||
@@ -58,18 +57,16 @@ export const gridLayoutSystems = (
|
||||
new Map(
|
||||
model.spotlight === undefined ? [] : [["spotlight", model.spotlight]],
|
||||
),
|
||||
Layout: subscribe(function GridLayoutFixed({ model }, ref) {
|
||||
const { width, height } = useStateObservable(minBounds);
|
||||
const alignment = useStateObservable(
|
||||
useInitial<StateObservable<Alignment>>(() =>
|
||||
state(
|
||||
Layout: forwardRef(function GridLayoutFixed({ model }, ref) {
|
||||
const { width, height } = useObservableEagerState(minBounds);
|
||||
const alignment = useObservableEagerState(
|
||||
useInitial(() =>
|
||||
floatingAlignment.pipe(
|
||||
distinctUntilChanged(
|
||||
(a1, a2) => a1.block === a2.block && a1.inline === a2.inline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
const [generation] = useReactiveState<number>(
|
||||
(prev) => (prev === undefined ? 0 : prev + 1),
|
||||
@@ -106,8 +103,8 @@ export const gridLayoutSystems = (
|
||||
// The scrolling part of the layout is where all the grid tiles live
|
||||
scrolling: {
|
||||
tiles: (model) => new Map(model.grid.map((tile) => [tile.id, tile])),
|
||||
Layout: subscribe(function GridLayout({ model }, ref) {
|
||||
const { width, height: minHeight } = useStateObservable(minBounds);
|
||||
Layout: forwardRef(function GridLayout({ model }, ref) {
|
||||
const { width, height: minHeight } = useObservableEagerState(minBounds);
|
||||
|
||||
// The goal here is to determine the grid size and padding that maximizes
|
||||
// use of screen space for n tiles without making those tiles too small or
|
||||
|
||||
@@ -35,8 +35,8 @@ import {
|
||||
import useMeasure from "react-use-measure";
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
import classNames from "classnames";
|
||||
import { state, useStateObservable } from "@react-rxjs/core";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { useObservableEagerState } from "observable-hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import LogoMark from "../icons/LogoMark.svg?react";
|
||||
@@ -76,7 +76,6 @@ import {
|
||||
TileDescriptor,
|
||||
useCallViewModel,
|
||||
} from "../state/CallViewModel";
|
||||
import { subscribe } from "../state/subscribe";
|
||||
import { Grid, TileProps } from "../grid/Grid";
|
||||
import { MediaViewModel } from "../state/MediaViewModel";
|
||||
import { gridLayoutSystems } from "../grid/GridLayout";
|
||||
@@ -143,8 +142,7 @@ export interface InCallViewProps {
|
||||
onShareClick: (() => void) | null;
|
||||
}
|
||||
|
||||
export const InCallView: FC<InCallViewProps> = subscribe(
|
||||
({
|
||||
export const InCallView: FC<InCallViewProps> = ({
|
||||
client,
|
||||
matrixInfo,
|
||||
rtcSession,
|
||||
@@ -223,9 +221,7 @@ export const InCallView: FC<InCallViewProps> = subscribe(
|
||||
setLegacyLayout("grid");
|
||||
widget!.api.transport.reply(ev.detail, {});
|
||||
};
|
||||
const onSpotlightLayout = (
|
||||
ev: CustomEvent<IWidgetApiRequest>,
|
||||
): void => {
|
||||
const onSpotlightLayout = (ev: CustomEvent<IWidgetApiRequest>): void => {
|
||||
setLegacyLayout("spotlight");
|
||||
widget!.api.transport.reply(ev.detail, {});
|
||||
};
|
||||
@@ -237,10 +233,7 @@ export const InCallView: FC<InCallViewProps> = subscribe(
|
||||
);
|
||||
|
||||
return (): void => {
|
||||
widget!.lazyActions.off(
|
||||
ElementWidgetActions.TileLayout,
|
||||
onTileLayout,
|
||||
);
|
||||
widget!.lazyActions.off(ElementWidgetActions.TileLayout, onTileLayout);
|
||||
widget!.lazyActions.off(
|
||||
ElementWidgetActions.SpotlightLayout,
|
||||
onSpotlightLayout,
|
||||
@@ -259,8 +252,8 @@ export const InCallView: FC<InCallViewProps> = subscribe(
|
||||
matrixInfo.e2eeSystem.kind !== E2eeType.NONE,
|
||||
connState,
|
||||
);
|
||||
const items = useStateObservable(vm.tiles);
|
||||
const layout = useStateObservable(vm.layout);
|
||||
const items = useObservableEagerState(vm.tiles);
|
||||
const layout = useObservableEagerState(vm.layout);
|
||||
const hasSpotlight = layout.spotlight !== undefined;
|
||||
// Hack: We insert a dummy "spotlight" tile into the tiles we pass to
|
||||
// useFullscreen so that we can control the fullscreen state of the
|
||||
@@ -326,7 +319,7 @@ export const InCallView: FC<InCallViewProps> = subscribe(
|
||||
() => new BehaviorSubject(defaultAlignment),
|
||||
);
|
||||
const { fixed, scrolling } = useInitial(() =>
|
||||
gridLayoutSystems(state(gridBoundsObservable), floatingAlignment),
|
||||
gridLayoutSystems(gridBoundsObservable, floatingAlignment),
|
||||
);
|
||||
|
||||
const setGridMode = useCallback(
|
||||
@@ -583,9 +576,7 @@ export const InCallView: FC<InCallViewProps> = subscribe(
|
||||
<RoomAudioRenderer />
|
||||
{renderContent()}
|
||||
{footer}
|
||||
{!noControls && (
|
||||
<RageshakeRequestModal {...rageshakeRequestModalProps} />
|
||||
)}
|
||||
{!noControls && <RageshakeRequestModal {...rageshakeRequestModalProps} />}
|
||||
<SettingsModal
|
||||
client={client}
|
||||
roomId={rtcSession.room.roomId}
|
||||
@@ -596,5 +587,4 @@ export const InCallView: FC<InCallViewProps> = subscribe(
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -50,7 +50,6 @@ import {
|
||||
timer,
|
||||
zip,
|
||||
} from "rxjs";
|
||||
import { StateObservable, state } from "@react-rxjs/core";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { ViewModel } from "./ViewModel";
|
||||
@@ -183,7 +182,7 @@ class UserMedia {
|
||||
? new LocalUserMediaViewModel(id, member, participant, callEncrypted)
|
||||
: new RemoteUserMediaViewModel(id, member, participant, callEncrypted);
|
||||
|
||||
this.speaker = this.vm.speaking.pipeState(
|
||||
this.speaker = this.vm.speaking.pipe(
|
||||
// Require 1 s of continuous speaking to become a speaker, and 60 s of
|
||||
// continuous silence to stop being considered a speaker
|
||||
audit((s) =>
|
||||
@@ -259,9 +258,9 @@ function findMatrixMember(
|
||||
|
||||
// TODO: Move wayyyy more business logic from the call and lobby views into here
|
||||
export class CallViewModel extends ViewModel {
|
||||
private readonly rawRemoteParticipants = state(
|
||||
connectedParticipantsObserver(this.livekitRoom),
|
||||
);
|
||||
private readonly rawRemoteParticipants = connectedParticipantsObserver(
|
||||
this.livekitRoom,
|
||||
).pipe(shareReplay(1));
|
||||
|
||||
// Lists of participants to "hold" on display, even if LiveKit claims that
|
||||
// they've left
|
||||
@@ -334,8 +333,7 @@ export class CallViewModel extends ViewModel {
|
||||
},
|
||||
);
|
||||
|
||||
private readonly mediaItems: StateObservable<MediaItem[]> = state(
|
||||
combineLatest([
|
||||
private readonly mediaItems: Observable<MediaItem[]> = combineLatest([
|
||||
this.remoteParticipants,
|
||||
observeParticipantMedia(this.livekitRoom.localParticipant),
|
||||
duplicateTiles.value,
|
||||
@@ -343,11 +341,7 @@ export class CallViewModel extends ViewModel {
|
||||
scan(
|
||||
(
|
||||
prevItems,
|
||||
[
|
||||
remoteParticipants,
|
||||
{ participant: localParticipant },
|
||||
duplicateTiles,
|
||||
],
|
||||
[remoteParticipants, { participant: localParticipant }, duplicateTiles],
|
||||
) => {
|
||||
let allGhosts = true;
|
||||
|
||||
@@ -380,12 +374,7 @@ export class CallViewModel extends ViewModel {
|
||||
yield [
|
||||
screenShareId,
|
||||
prevItems.get(screenShareId) ??
|
||||
new ScreenShare(
|
||||
screenShareId,
|
||||
member,
|
||||
p,
|
||||
this.encrypted,
|
||||
),
|
||||
new ScreenShare(screenShareId, member, p, this.encrypted),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -393,8 +382,6 @@ export class CallViewModel extends ViewModel {
|
||||
}.bind(this)(),
|
||||
);
|
||||
|
||||
for (const [id, t] of prevItems) if (!newItems.has(id)) t.destroy();
|
||||
|
||||
// If every item is a ghost, that probably means we're still connecting
|
||||
// and shouldn't bother showing anything yet
|
||||
return allGhosts ? new Map() : newItems;
|
||||
@@ -405,7 +392,7 @@ export class CallViewModel extends ViewModel {
|
||||
finalizeValue((ts) => {
|
||||
for (const t of ts) t.destroy();
|
||||
}),
|
||||
),
|
||||
shareReplay(1),
|
||||
);
|
||||
|
||||
private readonly userMedia: Observable<UserMedia[]> = this.mediaItems.pipe(
|
||||
@@ -507,14 +494,15 @@ export class CallViewModel extends ViewModel {
|
||||
/**
|
||||
* The layout mode of the media tile grid.
|
||||
*/
|
||||
public readonly gridMode = state(this._gridMode);
|
||||
public readonly gridMode: Observable<GridMode> = this._gridMode;
|
||||
|
||||
public setGridMode(value: GridMode): void {
|
||||
this._gridMode.next(value);
|
||||
}
|
||||
|
||||
public readonly layout: StateObservable<Layout> = state(
|
||||
combineLatest([this._gridMode, this.windowMode], (gridMode, windowMode) => {
|
||||
public readonly layout: Observable<Layout> = combineLatest(
|
||||
[this._gridMode, this.windowMode],
|
||||
(gridMode, windowMode) => {
|
||||
switch (windowMode) {
|
||||
case "full screen":
|
||||
throw new Error("unimplemented");
|
||||
@@ -543,16 +531,15 @@ export class CallViewModel extends ViewModel {
|
||||
}
|
||||
}
|
||||
}
|
||||
}).pipe(switchAll()),
|
||||
);
|
||||
},
|
||||
).pipe(switchAll(), shareReplay(1));
|
||||
|
||||
/**
|
||||
* The media tiles to be displayed in the call view.
|
||||
*/
|
||||
// TODO: Get rid of this field, replacing it with the 'layout' field above
|
||||
// which keeps more details of the layout order internal to the view model
|
||||
public readonly tiles: StateObservable<TileDescriptor<MediaViewModel>[]> =
|
||||
state(
|
||||
public readonly tiles: Observable<TileDescriptor<MediaViewModel>[]> =
|
||||
combineLatest([
|
||||
this.remoteParticipants,
|
||||
observeParticipantMedia(this.livekitRoom.localParticipant),
|
||||
@@ -646,7 +633,7 @@ export class CallViewModel extends ViewModel {
|
||||
finalizeValue((ts) => {
|
||||
for (const t of ts) t.data.destroy();
|
||||
}),
|
||||
),
|
||||
shareReplay(1),
|
||||
);
|
||||
|
||||
public constructor(
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
observeParticipantEvents,
|
||||
observeParticipantMedia,
|
||||
} from "@livekit/components-core";
|
||||
import { StateObservable, state } from "@react-rxjs/core";
|
||||
import {
|
||||
LocalParticipant,
|
||||
LocalTrack,
|
||||
@@ -35,12 +34,14 @@ import {
|
||||
import { RoomMember, RoomMemberEvent } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
Observable,
|
||||
combineLatest,
|
||||
distinctUntilChanged,
|
||||
distinctUntilKeyChanged,
|
||||
fromEvent,
|
||||
map,
|
||||
of,
|
||||
shareReplay,
|
||||
startWith,
|
||||
switchMap,
|
||||
} from "rxjs";
|
||||
@@ -92,16 +93,15 @@ export function useNameData(vm: MediaViewModel): NameData {
|
||||
function observeTrackReference(
|
||||
participant: Participant,
|
||||
source: Track.Source,
|
||||
): StateObservable<TrackReferenceOrPlaceholder> {
|
||||
return state(
|
||||
observeParticipantMedia(participant).pipe(
|
||||
): Observable<TrackReferenceOrPlaceholder> {
|
||||
return observeParticipantMedia(participant).pipe(
|
||||
map(() => ({
|
||||
participant,
|
||||
publication: participant.getTrackPublication(source),
|
||||
source,
|
||||
})),
|
||||
distinctUntilKeyChanged("publication"),
|
||||
),
|
||||
shareReplay(1),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -113,11 +113,11 @@ abstract class BaseMediaViewModel extends ViewModel {
|
||||
/**
|
||||
* The LiveKit video track for this media.
|
||||
*/
|
||||
public readonly video: StateObservable<TrackReferenceOrPlaceholder>;
|
||||
public readonly video: Observable<TrackReferenceOrPlaceholder>;
|
||||
/**
|
||||
* Whether there should be a warning that this media is unencrypted.
|
||||
*/
|
||||
public readonly unencryptedWarning: StateObservable<boolean>;
|
||||
public readonly unencryptedWarning: Observable<boolean>;
|
||||
|
||||
public constructor(
|
||||
/**
|
||||
@@ -138,15 +138,13 @@ abstract class BaseMediaViewModel extends ViewModel {
|
||||
super();
|
||||
const audio = observeTrackReference(participant, audioSource);
|
||||
this.video = observeTrackReference(participant, videoSource);
|
||||
this.unencryptedWarning = state(
|
||||
combineLatest(
|
||||
this.unencryptedWarning = combineLatest(
|
||||
[audio, this.video],
|
||||
(a, v) =>
|
||||
callEncrypted &&
|
||||
(a.publication?.isEncrypted === false ||
|
||||
v.publication?.isEncrypted === false),
|
||||
).pipe(distinctUntilChanged()),
|
||||
);
|
||||
).pipe(distinctUntilChanged(), shareReplay(1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,27 +163,28 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel {
|
||||
/**
|
||||
* Whether the participant is speaking.
|
||||
*/
|
||||
public readonly speaking = state(
|
||||
observeParticipantEvents(
|
||||
public readonly speaking = observeParticipantEvents(
|
||||
this.participant,
|
||||
ParticipantEvent.IsSpeakingChanged,
|
||||
).pipe(map((p) => p.isSpeaking)),
|
||||
).pipe(
|
||||
map((p) => p.isSpeaking),
|
||||
shareReplay(1),
|
||||
);
|
||||
|
||||
/**
|
||||
* Whether this participant is sending audio (i.e. is unmuted on their side).
|
||||
*/
|
||||
public readonly audioEnabled: StateObservable<boolean>;
|
||||
public readonly audioEnabled: Observable<boolean>;
|
||||
/**
|
||||
* Whether this participant is sending video.
|
||||
*/
|
||||
public readonly videoEnabled: StateObservable<boolean>;
|
||||
public readonly videoEnabled: Observable<boolean>;
|
||||
|
||||
private readonly _cropVideo = new BehaviorSubject(true);
|
||||
/**
|
||||
* Whether the tile video should be contained inside the tile or be cropped to fit.
|
||||
*/
|
||||
public readonly cropVideo = state(this._cropVideo);
|
||||
public readonly cropVideo: Observable<boolean> = this._cropVideo;
|
||||
|
||||
public constructor(
|
||||
id: string,
|
||||
@@ -202,12 +201,12 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel {
|
||||
Track.Source.Camera,
|
||||
);
|
||||
|
||||
const media = observeParticipantMedia(participant);
|
||||
this.audioEnabled = state(
|
||||
media.pipe(map((m) => m.microphoneTrack?.isMuted === false)),
|
||||
const media = observeParticipantMedia(participant).pipe(shareReplay(1));
|
||||
this.audioEnabled = media.pipe(
|
||||
map((m) => m.microphoneTrack?.isMuted === false),
|
||||
);
|
||||
this.videoEnabled = state(
|
||||
media.pipe(map((m) => m.cameraTrack?.isMuted === false)),
|
||||
this.videoEnabled = media.pipe(
|
||||
map((m) => m.cameraTrack?.isMuted === false),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -223,8 +222,7 @@ export class LocalUserMediaViewModel extends BaseUserMediaViewModel {
|
||||
/**
|
||||
* Whether the video should be mirrored.
|
||||
*/
|
||||
public readonly mirror = state(
|
||||
this.video.pipe(
|
||||
public readonly mirror = this.video.pipe(
|
||||
switchMap((v) => {
|
||||
const track = v.publication?.track;
|
||||
if (!(track instanceof LocalTrack)) return of(false);
|
||||
@@ -235,7 +233,7 @@ export class LocalUserMediaViewModel extends BaseUserMediaViewModel {
|
||||
map(() => facingModeFromLocalTrack(track).facingMode === "user"),
|
||||
);
|
||||
}),
|
||||
),
|
||||
shareReplay(1),
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -263,14 +261,14 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
|
||||
/**
|
||||
* Whether we've disabled this participant's audio.
|
||||
*/
|
||||
public readonly locallyMuted = state(this._locallyMuted);
|
||||
public readonly locallyMuted: Observable<boolean> = this._locallyMuted;
|
||||
|
||||
private readonly _localVolume = new BehaviorSubject(1);
|
||||
/**
|
||||
* The volume to which we've set this participant's audio, as a scalar
|
||||
* multiplier.
|
||||
*/
|
||||
public readonly localVolume = state(this._localVolume);
|
||||
public readonly localVolume: Observable<number> = this._localVolume;
|
||||
|
||||
public constructor(
|
||||
id: string,
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
Copyright 2023 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ForwardRefExoticComponent,
|
||||
ForwardRefRenderFunction,
|
||||
PropsWithoutRef,
|
||||
RefAttributes,
|
||||
forwardRef,
|
||||
} from "react";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Subscribe, RemoveSubscribe } from "@react-rxjs/core";
|
||||
|
||||
/**
|
||||
* Wraps a React component that consumes Observables, resulting in a component
|
||||
* that safely subscribes to its Observables before rendering. The component
|
||||
* will return null until the subscriptions are created.
|
||||
*/
|
||||
export function subscribe<P, R>(
|
||||
render: ForwardRefRenderFunction<R, P>,
|
||||
): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<R>> {
|
||||
const Subscriber = forwardRef<R, { p: P }>(({ p }, ref) => (
|
||||
<RemoveSubscribe>{render(p, ref)}</RemoveSubscribe>
|
||||
));
|
||||
Subscriber.displayName = "Subscriber";
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const OuterComponent = forwardRef<R, P>((p, ref) => (
|
||||
<Subscribe>
|
||||
<Subscriber ref={ref} p={p} />
|
||||
</Subscribe>
|
||||
));
|
||||
// Copy over the component's display name, default props, etc.
|
||||
Object.assign(OuterComponent, render);
|
||||
return OuterComponent;
|
||||
}
|
||||
@@ -28,14 +28,13 @@ import CollapseIcon from "@vector-im/compound-design-tokens/icons/collapse.svg?r
|
||||
import ChevronLeftIcon from "@vector-im/compound-design-tokens/icons/chevron-left.svg?react";
|
||||
import ChevronRightIcon from "@vector-im/compound-design-tokens/icons/chevron-right.svg?react";
|
||||
import { animated } from "@react-spring/web";
|
||||
import { state, useStateObservable } from "@react-rxjs/core";
|
||||
import { Observable, map, of } from "rxjs";
|
||||
import { useObservableEagerState } from "observable-hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { MediaView } from "./MediaView";
|
||||
import styles from "./SpotlightTile.module.css";
|
||||
import { subscribe } from "../state/subscribe";
|
||||
import {
|
||||
LocalUserMediaViewModel,
|
||||
MediaViewModel,
|
||||
@@ -49,11 +48,11 @@ import { useReactiveState } from "../useReactiveState";
|
||||
import { useLatest } from "../useLatest";
|
||||
|
||||
// Screen share video is always enabled
|
||||
const videoEnabledDefault = state(of(true));
|
||||
const videoEnabledDefault = of(true);
|
||||
// Never mirror screen share video
|
||||
const mirrorDefault = state(of(false));
|
||||
const mirrorDefault = of(false);
|
||||
// Never crop screen share video
|
||||
const cropVideoDefault = state(of(false));
|
||||
const cropVideoDefault = of(false);
|
||||
|
||||
interface SpotlightItemProps {
|
||||
vm: MediaViewModel;
|
||||
@@ -66,28 +65,28 @@ interface SpotlightItemProps {
|
||||
snap: boolean;
|
||||
}
|
||||
|
||||
const SpotlightItem = subscribe<SpotlightItemProps, HTMLDivElement>(
|
||||
const SpotlightItem = forwardRef<HTMLDivElement, SpotlightItemProps>(
|
||||
({ vm, targetWidth, targetHeight, intersectionObserver, snap }, theirRef) => {
|
||||
const ourRef = useRef<HTMLDivElement | null>(null);
|
||||
const ref = useMergedRefs(ourRef, theirRef);
|
||||
const { displayName, nameTag } = useNameData(vm);
|
||||
const video = useStateObservable(vm.video);
|
||||
const videoEnabled = useStateObservable(
|
||||
const video = useObservableEagerState(vm.video);
|
||||
const videoEnabled = useObservableEagerState(
|
||||
vm instanceof LocalUserMediaViewModel ||
|
||||
vm instanceof RemoteUserMediaViewModel
|
||||
? vm.videoEnabled
|
||||
: videoEnabledDefault,
|
||||
);
|
||||
const mirror = useStateObservable(
|
||||
const mirror = useObservableEagerState(
|
||||
vm instanceof LocalUserMediaViewModel ? vm.mirror : mirrorDefault,
|
||||
);
|
||||
const cropVideo = useStateObservable(
|
||||
const cropVideo = useObservableEagerState(
|
||||
vm instanceof LocalUserMediaViewModel ||
|
||||
vm instanceof RemoteUserMediaViewModel
|
||||
? vm.cropVideo
|
||||
: cropVideoDefault,
|
||||
);
|
||||
const unencryptedWarning = useStateObservable(vm.unencryptedWarning);
|
||||
const unencryptedWarning = useObservableEagerState(vm.unencryptedWarning);
|
||||
|
||||
// Hook this item up to the intersection observer
|
||||
useEffect(() => {
|
||||
@@ -124,6 +123,8 @@ const SpotlightItem = subscribe<SpotlightItemProps, HTMLDivElement>(
|
||||
},
|
||||
);
|
||||
|
||||
SpotlightItem.displayName = "SpotlightItem";
|
||||
|
||||
interface Props {
|
||||
vms: MediaViewModel[];
|
||||
maximised: boolean;
|
||||
|
||||
18
yarn.lock
18
yarn.lock
@@ -2847,14 +2847,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@react-hook/latest/-/latest-1.0.3.tgz#c2d1d0b0af8b69ec6e2b3a2412ba0768ac82db80"
|
||||
integrity sha512-dy6duzl+JnAZcDbNTfmaP3xHiKtbXYOaz3G51MGVljh548Y8MWzTr+PHLOfvpypEVW9zwvl+VyKjbWKEVbV1Rg==
|
||||
|
||||
"@react-rxjs/core@^0.10.7":
|
||||
version "0.10.7"
|
||||
resolved "https://registry.yarnpkg.com/@react-rxjs/core/-/core-0.10.7.tgz#09951f43a6c80892526ac13d51859098b0e74993"
|
||||
integrity sha512-dornp8pUs9OcdqFKKRh9+I2FVe21gWufNun6RYU1ddts7kUy9i4Thvl0iqcPFbGY61cJQMAJF7dxixWMSD/A/A==
|
||||
dependencies:
|
||||
"@rx-state/core" "0.1.4"
|
||||
use-sync-external-store "^1.0.0"
|
||||
|
||||
"@react-spring/animated@~9.7.3":
|
||||
version "9.7.3"
|
||||
resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.7.3.tgz#4211b1a6d48da0ff474a125e93c0f460ff816e0f"
|
||||
@@ -3194,11 +3186,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz#5d694d345ce36b6ecf657349e03eb87297e68da4"
|
||||
integrity sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==
|
||||
|
||||
"@rx-state/core@0.1.4":
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@rx-state/core/-/core-0.1.4.tgz#586dde80be9dbdac31844006a0dcaa2bc7f35a5c"
|
||||
integrity sha512-Z+3hjU2xh1HisLxt+W5hlYX/eGSDaXXP+ns82gq/PLZpkXLu0uwcNUh9RLY3Clq4zT+hSsA3vcpIGt6+UAb8rQ==
|
||||
|
||||
"@sentry-internal/browser-utils@8.13.0":
|
||||
version "8.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.13.0.tgz#b7c3bdd49d2382f60dde31745716d29dd419b6ba"
|
||||
@@ -8988,11 +8975,6 @@ use-sidecar@^1.1.2:
|
||||
detect-node-es "^1.1.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
use-sync-external-store@^1.0.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||
|
||||
usehooks-ts@2.16.0:
|
||||
version "2.16.0"
|
||||
resolved "https://registry.yarnpkg.com/usehooks-ts/-/usehooks-ts-2.16.0.tgz#31deaa2f1147f65666aae925bd890b54e63b0d3f"
|
||||
|
||||
Reference in New Issue
Block a user