From e33fbd77d14daeec4b506da4a2d68f97409e6128 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 16 May 2024 12:32:18 -0400 Subject: [PATCH 1/6] Split local and remote user media into different classes --- src/room/InCallView.tsx | 4 +- src/state/CallViewModel.ts | 23 ++- src/state/MediaViewModel.ts | 122 ++++++++----- src/tile/GridTile.tsx | 332 ++++++++++++++++++++---------------- src/tile/SpotlightTile.tsx | 21 ++- 5 files changed, 292 insertions(+), 210 deletions(-) diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 1ccdb2ec..f2e08e5c 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -384,7 +384,7 @@ export const InCallView: FC = subscribe( targetHeight={targetHeight} className={className} style={style} - showSpeakingIndicator={showSpeakingIndicators} + showSpeakingIndicators={showSpeakingIndicators} /> ); }, @@ -424,7 +424,7 @@ export const InCallView: FC = subscribe( targetHeight={gridBounds.height} targetWidth={gridBounds.width} key={maximisedParticipant.id} - showSpeakingIndicator={false} + showSpeakingIndicators={false} onOpenProfile={openProfile} /> ); diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index e6d55b72..df3ca42a 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -61,9 +61,11 @@ import { } from "../livekit/useECConnectionState"; import { usePrevious } from "../usePrevious"; import { + LocalUserMediaViewModel, MediaViewModel, - UserMediaViewModel, + RemoteUserMediaViewModel, ScreenShareViewModel, + UserMediaViewModel, } from "./MediaViewModel"; import { finalizeValue } from "../observable-utils"; import { ObservableScope } from "./ObservableScope"; @@ -151,7 +153,10 @@ class UserMedia { participant: LocalParticipant | RemoteParticipant, callEncrypted: boolean, ) { - this.vm = new UserMediaViewModel(id, member, participant, callEncrypted); + this.vm = + participant instanceof LocalParticipant + ? new LocalUserMediaViewModel(id, member, participant, callEncrypted) + : new RemoteUserMediaViewModel(id, member, participant, callEncrypted); this.speaker = this.vm.speaking.pipeState( // Require 1 s of continuous speaking to become a speaker, and 60 s of @@ -520,7 +525,19 @@ export class CallViewModel extends ViewModel { const userMediaVm = tilesById.get(userMediaId)?.data ?? - new UserMediaViewModel(userMediaId, member, p, this.encrypted); + (p instanceof LocalParticipant + ? new LocalUserMediaViewModel( + userMediaId, + member, + p, + this.encrypted, + ) + : new RemoteUserMediaViewModel( + userMediaId, + member, + p, + this.encrypted, + )); tilesById.delete(userMediaId); const userMediaTile: TileDescriptor = { diff --git a/src/state/MediaViewModel.ts b/src/state/MediaViewModel.ts index f1e772da..7f7307f4 100644 --- a/src/state/MediaViewModel.ts +++ b/src/state/MediaViewModel.ts @@ -153,29 +153,14 @@ abstract class BaseMediaViewModel extends ViewModel { * Some participant's media. */ export type MediaViewModel = UserMediaViewModel | ScreenShareViewModel; +export type UserMediaViewModel = + | LocalUserMediaViewModel + | RemoteUserMediaViewModel; /** * Some participant's user media. */ -export class UserMediaViewModel extends BaseMediaViewModel { - /** - * Whether the video should be mirrored. - */ - public readonly mirror = state( - this.video.pipe( - switchMap((v) => { - const track = v.publication?.track; - if (!(track instanceof LocalTrack)) return of(false); - // Watch for track restarts, because they indicate a camera switch - return fromEvent(track, TrackEvent.Restarted).pipe( - startWith(null), - // Mirror only front-facing cameras (those that face the user) - map(() => facingModeFromLocalTrack(track).facingMode === "user"), - ); - }), - ), - ); - +abstract class BaseUserMediaViewModel extends BaseMediaViewModel { /** * Whether the participant is speaking. */ @@ -186,19 +171,6 @@ export class UserMediaViewModel extends BaseMediaViewModel { ).pipe(map((p) => p.isSpeaking)), ); - private readonly _locallyMuted = new BehaviorSubject(false); - /** - * Whether we've disabled this participant's audio. - */ - public readonly locallyMuted = state(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); - /** * Whether this participant is sending audio (i.e. is unmuted on their side). */ @@ -236,25 +208,83 @@ export class UserMediaViewModel extends BaseMediaViewModel { this.videoEnabled = state( media.pipe(map((m) => m.cameraTrack?.isMuted === false)), ); - - // Sync the local mute state and volume with LiveKit - if (!this.local) - combineLatest([this._locallyMuted, this._localVolume], (muted, volume) => - muted ? 0 : volume, - ) - .pipe(this.scope.bind()) - .subscribe((volume) => { - (this.participant as RemoteParticipant).setVolume(volume); - }); - } - - public toggleLocallyMuted(): void { - this._locallyMuted.next(!this._locallyMuted.value); } public toggleFitContain(): void { this._cropVideo.next(!this._cropVideo.value); } +} + +/** + * The local participant's user media. + */ +export class LocalUserMediaViewModel extends BaseUserMediaViewModel { + /** + * Whether the video should be mirrored. + */ + public readonly mirror = state( + this.video.pipe( + switchMap((v) => { + const track = v.publication?.track; + if (!(track instanceof LocalTrack)) return of(false); + // Watch for track restarts, because they indicate a camera switch + return fromEvent(track, TrackEvent.Restarted).pipe( + startWith(null), + // Mirror only front-facing cameras (those that face the user) + map(() => facingModeFromLocalTrack(track).facingMode === "user"), + ); + }), + ), + ); + + public constructor( + id: string, + member: RoomMember | undefined, + participant: LocalParticipant, + callEncrypted: boolean, + ) { + super(id, member, participant, callEncrypted); + } +} + +/** + * A remote participant's user media. + */ +export class RemoteUserMediaViewModel extends BaseUserMediaViewModel { + private readonly _locallyMuted = new BehaviorSubject(false); + /** + * Whether we've disabled this participant's audio. + */ + public readonly locallyMuted = state(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 constructor( + id: string, + member: RoomMember | undefined, + participant: RemoteParticipant, + callEncrypted: boolean, + ) { + super(id, member, participant, callEncrypted); + + // Sync the local mute state and volume with LiveKit + combineLatest([this._locallyMuted, this._localVolume], (muted, volume) => + muted ? 0 : volume, + ) + .pipe(this.scope.bind()) + .subscribe((volume) => { + (this.participant as RemoteParticipant).setVolume(volume); + }); + } + + public toggleLocallyMuted(): void { + this._locallyMuted.next(!this._locallyMuted.value); + } public setLocalVolume(value: number): void { this._localVolume.next(value); diff --git a/src/tile/GridTile.tsx b/src/tile/GridTile.tsx index d88b189f..b0753aca 100644 --- a/src/tile/GridTile.tsx +++ b/src/tile/GridTile.tsx @@ -14,7 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ComponentProps, forwardRef, useCallback, useState } from "react"; +import { + ComponentProps, + ReactNode, + forwardRef, + useCallback, + useState, +} from "react"; import { animated } from "@react-spring/web"; import classNames from "classnames"; import { useTranslation } from "react-i18next"; @@ -33,7 +39,7 @@ import { ToggleMenuItem, Menu, } from "@vector-im/compound-web"; -import { useStateObservable } from "@react-rxjs/core"; +import { useObservableEagerState } from "observable-hooks"; import styles from "./GridTile.module.css"; import { @@ -41,71 +47,92 @@ import { MediaViewModel, UserMediaViewModel, useNameData, + LocalUserMediaViewModel, + RemoteUserMediaViewModel, } from "../state/MediaViewModel"; -import { subscribe } from "../state/subscribe"; import { Slider } from "../Slider"; import { MediaView } from "./MediaView"; -interface UserMediaTileProps { - vm: UserMediaViewModel; +interface TileProps { className?: string; style?: ComponentProps["style"]; targetWidth: number; targetHeight: number; maximised: boolean; - onOpenProfile: () => void; - showSpeakingIndicator: boolean; + displayName: string; + nameTag: string; } -const UserMediaTile = subscribe( +interface MediaTileProps + extends TileProps, + Omit, "className"> { + vm: MediaViewModel; + videoEnabled: boolean; + videoFit: "contain" | "cover"; + nameTagLeadingIcon?: ReactNode; + primaryButton: ReactNode; + secondaryButton?: ReactNode; +} + +const MediaTile = forwardRef( + ({ vm, className, maximised, ...props }, ref) => { + const video = useObservableEagerState(vm.video); + const unencryptedWarning = useObservableEagerState(vm.unencryptedWarning); + + return ( + + ); + }, +); + +MediaTile.displayName = "MediaTile"; + +interface UserMediaTileProps extends TileProps { + vm: UserMediaViewModel; + showSpeakingIndicators: boolean; + menuStart?: ReactNode; + menuEnd?: ReactNode; +} + +const UserMediaTile = forwardRef( ( { vm, + showSpeakingIndicators, + menuStart, + menuEnd, className, - style, - targetWidth, - targetHeight, - maximised, - onOpenProfile, - showSpeakingIndicator, + nameTag, + ...props }, ref, ) => { const { t } = useTranslation(); - const { displayName, nameTag } = useNameData(vm); - const video = useStateObservable(vm.video); - const audioEnabled = useStateObservable(vm.audioEnabled); - const videoEnabled = useStateObservable(vm.videoEnabled); - const unencryptedWarning = useStateObservable(vm.unencryptedWarning); - const mirror = useStateObservable(vm.mirror); - const speaking = useStateObservable(vm.speaking); - const locallyMuted = useStateObservable(vm.locallyMuted); - const cropVideo = useStateObservable(vm.cropVideo); - const localVolume = useStateObservable(vm.localVolume); - const onChangeMute = useCallback(() => vm.toggleLocallyMuted(), [vm]); + const audioEnabled = useObservableEagerState(vm.audioEnabled); + const videoEnabled = useObservableEagerState(vm.videoEnabled); + const speaking = useObservableEagerState(vm.speaking); + const cropVideo = useObservableEagerState(vm.cropVideo); const onChangeFitContain = useCallback(() => vm.toggleFitContain(), [vm]); - const onSelectMute = useCallback((e: Event) => e.preventDefault(), []); const onSelectFitContain = useCallback( (e: Event) => e.preventDefault(), [], ); - const onChangeLocalVolume = useCallback( - (v: number) => vm.setLocalVolume(v), - [vm], - ); - const MicIcon = audioEnabled ? MicOnSolidIcon : MicOffSolidIcon; - const VolumeIcon = locallyMuted ? VolumeOffIcon : VolumeOnIcon; const [menuOpen, setMenuOpen] = useState(false); - const menu = vm.local ? ( + const menu = ( <> - + {menuStart} ( onChange={onChangeFitContain} onSelect={onSelectFitContain} /> - - ) : ( - <> - - - {/* TODO: Figure out how to make this slider keyboard accessible */} - - - + {menuEnd} ); const tile = ( - ( /> } nameTag={nameTag} - displayName={displayName} primaryButton={ ( {menu} } + {...props} /> ); @@ -202,35 +193,102 @@ const UserMediaTile = subscribe( UserMediaTile.displayName = "UserMediaTile"; -interface ScreenShareTileProps { +interface LocalUserMediaTileProps extends TileProps { + vm: LocalUserMediaViewModel; + onOpenProfile: () => void; + showSpeakingIndicators: boolean; +} + +const LocalUserMediaTile = forwardRef( + ({ vm, onOpenProfile, className, ...props }, ref) => { + const { t } = useTranslation(); + const mirror = useObservableEagerState(vm.mirror); + + return ( + + } + className={classNames(className, { [styles.mirror]: mirror })} + {...props} + /> + ); + }, +); + +LocalUserMediaTile.displayName = "LocalUserMediaTile"; + +interface RemoteUserMediaTileProps extends TileProps { + vm: RemoteUserMediaViewModel; + showSpeakingIndicators: boolean; +} + +const RemoteUserMediaTile = forwardRef< + HTMLDivElement, + RemoteUserMediaTileProps +>(({ vm, ...props }, ref) => { + const { t } = useTranslation(); + const locallyMuted = useObservableEagerState(vm.locallyMuted); + const localVolume = useObservableEagerState(vm.localVolume); + const onChangeMute = useCallback(() => vm.toggleLocallyMuted(), [vm]); + const onSelectMute = useCallback((e: Event) => e.preventDefault(), []); + const onChangeLocalVolume = useCallback( + (v: number) => vm.setLocalVolume(v), + [vm], + ); + + const VolumeIcon = locallyMuted ? VolumeOffIcon : VolumeOnIcon; + + return ( + + + {/* TODO: Figure out how to make this slider keyboard accessible */} + + + + + } + {...props} + /> + ); +}); + +RemoteUserMediaTile.displayName = "RemoteUserMediaTile"; + +interface ScreenShareTileProps extends TileProps { vm: ScreenShareViewModel; - className?: string; - style?: ComponentProps["style"]; - targetWidth: number; - targetHeight: number; - maximised: boolean; fullscreen: boolean; onToggleFullscreen: (itemId: string) => void; } -const ScreenShareTile = subscribe( - ( - { - vm, - className, - style, - targetWidth, - targetHeight, - maximised, - fullscreen, - onToggleFullscreen, - }, - ref, - ) => { +const ScreenShareTile = forwardRef( + ({ vm, fullscreen, onToggleFullscreen, ...props }, ref) => { const { t } = useTranslation(); - const { displayName, nameTag } = useNameData(vm); - const video = useStateObservable(vm.video); - const unencryptedWarning = useStateObservable(vm.unencryptedWarning); const onClickFullScreen = useCallback( () => onToggleFullscreen(vm.id), [onToggleFullscreen, vm], @@ -239,23 +297,10 @@ const ScreenShareTile = subscribe( const FullScreenIcon = fullscreen ? CollapseIcon : ExpandIcon; return ( - ( ) } + videoEnabled + {...props} /> ); }, @@ -277,7 +324,7 @@ const ScreenShareTile = subscribe( ScreenShareTile.displayName = "ScreenShareTile"; -interface Props { +interface GridTileProps { vm: MediaViewModel; maximised: boolean; fullscreen: boolean; @@ -287,51 +334,34 @@ interface Props { targetHeight: number; className?: string; style?: ComponentProps["style"]; - showSpeakingIndicator: boolean; + showSpeakingIndicators: boolean; } -export const GridTile = forwardRef( - ( - { - vm, - maximised, - fullscreen, - onToggleFullscreen, - onOpenProfile, - className, - style, - targetWidth, - targetHeight, - showSpeakingIndicator, - }, - ref, - ) => { - if (vm instanceof UserMediaViewModel) { +export const GridTile = forwardRef( + ({ vm, fullscreen, onToggleFullscreen, onOpenProfile, ...props }, ref) => { + const nameData = useNameData(vm); + + if (vm instanceof LocalUserMediaViewModel) { return ( - ); + } else if (vm instanceof RemoteUserMediaViewModel) { + return ; } else { return ( ); } diff --git a/src/tile/SpotlightTile.tsx b/src/tile/SpotlightTile.tsx index 6abf0cdd..e4bb085d 100644 --- a/src/tile/SpotlightTile.tsx +++ b/src/tile/SpotlightTile.tsx @@ -37,8 +37,9 @@ import { MediaView } from "./MediaView"; import styles from "./SpotlightTile.module.css"; import { subscribe } from "../state/subscribe"; import { + LocalUserMediaViewModel, MediaViewModel, - UserMediaViewModel, + RemoteUserMediaViewModel, useNameData, } from "../state/MediaViewModel"; import { useInitial } from "../useInitial"; @@ -48,11 +49,11 @@ import { useReactiveState } from "../useReactiveState"; import { useLatest } from "../useLatest"; // Screen share video is always enabled -const screenShareVideoEnabled = state(of(true)); +const videoEnabledDefault = state(of(true)); // Never mirror screen share video -const screenShareMirror = state(of(false)); +const mirrorDefault = state(of(false)); // Never crop screen share video -const screenShareCropVideo = state(of(false)); +const cropVideoDefault = state(of(false)); interface SpotlightItemProps { vm: MediaViewModel; @@ -72,15 +73,19 @@ const SpotlightItem = subscribe( const { displayName, nameTag } = useNameData(vm); const video = useStateObservable(vm.video); const videoEnabled = useStateObservable( - vm instanceof UserMediaViewModel + vm instanceof LocalUserMediaViewModel || + vm instanceof RemoteUserMediaViewModel ? vm.videoEnabled - : screenShareVideoEnabled, + : videoEnabledDefault, ); const mirror = useStateObservable( - vm instanceof UserMediaViewModel ? vm.mirror : screenShareMirror, + vm instanceof LocalUserMediaViewModel ? vm.mirror : mirrorDefault, ); const cropVideo = useStateObservable( - vm instanceof UserMediaViewModel ? vm.cropVideo : screenShareCropVideo, + vm instanceof LocalUserMediaViewModel || + vm instanceof RemoteUserMediaViewModel + ? vm.cropVideo + : cropVideoDefault, ); const unencryptedWarning = useStateObservable(vm.unencryptedWarning); From 8a414012a03571cb886588b008dfc6da065df282 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 16 May 2024 13:33:02 -0400 Subject: [PATCH 2/6] Add always show flag to view model --- src/settings/settings.ts | 2 ++ src/state/MediaViewModel.ts | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 307a557e..4ccc78b8 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -88,3 +88,5 @@ export const videoInput = new Setting( "video-input", undefined, ); + +export const alwaysShowSelf = new Setting("always-show-self", true); diff --git a/src/state/MediaViewModel.ts b/src/state/MediaViewModel.ts index 7f7307f4..c61b5255 100644 --- a/src/state/MediaViewModel.ts +++ b/src/state/MediaViewModel.ts @@ -49,6 +49,7 @@ import { useEffect } from "react"; import { ViewModel } from "./ViewModel"; import { useReactiveState } from "../useReactiveState"; +import { alwaysShowSelf } from "../settings/settings"; export interface NameData { /** @@ -237,6 +238,13 @@ export class LocalUserMediaViewModel extends BaseUserMediaViewModel { ), ); + /** + * Whether to show this tile in a highly visible location near the start of + * the grid. + */ + public readonly alwaysShow = alwaysShowSelf.value; + public readonly setAlwaysShow = alwaysShowSelf.setValue; + public constructor( id: string, member: RoomMember | undefined, From 5647619b366902d8dbc27a7ba56b6281d73f2c26 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 20 Jun 2024 10:37:42 -0400 Subject: [PATCH 3/6] Add always show toggle to the UI --- public/locales/en-GB/app.json | 1 + src/tile/GridTile.tsx | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index 4279bbb5..922a4c79 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -155,6 +155,7 @@ "unmute_microphone_button_label": "Unmute microphone", "version": "Version: {{version}}", "video_tile": { + "always_show": "Always show", "change_fit_contain": "Fit to frame", "exit_full_screen": "Exit full screen", "full_screen": "Full screen", diff --git a/src/tile/GridTile.tsx b/src/tile/GridTile.tsx index b0753aca..ba615953 100644 --- a/src/tile/GridTile.tsx +++ b/src/tile/GridTile.tsx @@ -30,6 +30,7 @@ import MicOffIcon from "@vector-im/compound-design-tokens/icons/mic-off.svg?reac import OverflowHorizontalIcon from "@vector-im/compound-design-tokens/icons/overflow-horizontal.svg?react"; import VolumeOnIcon from "@vector-im/compound-design-tokens/icons/volume-on.svg?react"; import VolumeOffIcon from "@vector-im/compound-design-tokens/icons/volume-off.svg?react"; +import VisibilityOnIcon from "@vector-im/compound-design-tokens/icons/visibility-on.svg?react"; import UserProfileIcon from "@vector-im/compound-design-tokens/icons/user-profile.svg?react"; import ExpandIcon from "@vector-im/compound-design-tokens/icons/expand.svg?react"; import CollapseIcon from "@vector-im/compound-design-tokens/icons/collapse.svg?react"; @@ -52,6 +53,7 @@ import { } from "../state/MediaViewModel"; import { Slider } from "../Slider"; import { MediaView } from "./MediaView"; +import { useLatest } from "../useLatest"; interface TileProps { className?: string; @@ -203,12 +205,31 @@ const LocalUserMediaTile = forwardRef( ({ vm, onOpenProfile, className, ...props }, ref) => { const { t } = useTranslation(); const mirror = useObservableEagerState(vm.mirror); + const alwaysShow = useObservableEagerState(vm.alwaysShow); + const latestAlwaysShow = useLatest(alwaysShow); + const onSelectAlwaysShow = useCallback( + (e: Event) => e.preventDefault(), + [], + ); + const onChangeAlwaysShow = useCallback( + () => vm.setAlwaysShow(!latestAlwaysShow.current), + [vm, latestAlwaysShow], + ); return ( + } + menuEnd={ Date: Thu, 16 May 2024 13:55:31 -0400 Subject: [PATCH 4/6] Use always show flag in importance ordering --- src/state/CallViewModel.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index df3ca42a..f2656166 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -131,14 +131,14 @@ export type WindowMode = "normal" | "full screen" | "pip"; * Sorting bins defining the order in which media tiles appear in the layout. */ enum SortingBin { - SelfStart, + SelfAlwaysShown, Presenters, Speakers, VideoAndAudio, Video, Audio, NoMedia, - SelfEnd, + SelfNotAlwaysShown, } class UserMedia { @@ -410,10 +410,21 @@ export class CallViewModel extends ViewModel { switchMap((ms) => { const bins = ms.map((m) => combineLatest( - [m.speaker, m.presenter, m.vm.audioEnabled, m.vm.videoEnabled], - (speaker, presenter, audio, video) => { + [ + m.speaker, + m.presenter, + m.vm.audioEnabled, + m.vm.videoEnabled, + m.vm instanceof LocalUserMediaViewModel + ? m.vm.alwaysShow + : of(false), + ], + (speaker, presenter, audio, video, alwaysShow) => { let bin: SortingBin; - if (m.vm.local) bin = SortingBin.SelfStart; + if (m.vm.local) + bin = alwaysShow + ? SortingBin.SelfAlwaysShown + : SortingBin.SelfNotAlwaysShown; else if (presenter) bin = SortingBin.Presenters; else if (speaker) bin = SortingBin.Speakers; else if (video) From a59875dab5936ed606b0533c486712de8b86d989 Mon Sep 17 00:00:00 2001 From: Robin Date: Wed, 17 Jul 2024 15:37:41 -0400 Subject: [PATCH 5/6] Explain what each sorting bin means --- src/state/CallViewModel.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index f2656166..74061abe 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -131,13 +131,37 @@ export type WindowMode = "normal" | "full screen" | "pip"; * Sorting bins defining the order in which media tiles appear in the layout. */ enum SortingBin { + /** + * Yourself, when the "always show self" option is on. + */ SelfAlwaysShown, + /** + * Participants that are sharing their screen. + */ Presenters, + /** + * Participants that have been speaking recently. + */ Speakers, + /** + * Participants with both video and audio. + */ VideoAndAudio, + /** + * Participants with video but no audio. + */ Video, + /** + * Participants with audio but no video. + */ Audio, + /** + * Participants not sharing any media. + */ NoMedia, + /** + * Yourself, when the "always show self" option is off. + */ SelfNotAlwaysShown, } From 2bc56dbff2efad277edbde1c80614842d0def8d5 Mon Sep 17 00:00:00 2001 From: Robin Date: Wed, 17 Jul 2024 15:37:55 -0400 Subject: [PATCH 6/6] Use fewer ML-style variable names --- src/state/CallViewModel.ts | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index 74061abe..083cf556 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -386,7 +386,7 @@ export class CallViewModel extends ViewModel { }, new Map(), ), - map((ms) => [...ms.values()]), + map((mediaItems) => [...mediaItems.values()]), finalizeValue((ts) => { for (const t of ts) t.destroy(); }), @@ -394,35 +394,41 @@ export class CallViewModel extends ViewModel { ); private readonly userMedia: Observable = this.mediaItems.pipe( - map((ms) => ms.filter((m): m is UserMedia => m instanceof UserMedia)), + map((mediaItems) => + mediaItems.filter((m): m is UserMedia => m instanceof UserMedia), + ), ); private readonly screenShares: Observable = this.mediaItems.pipe( - map((ms) => ms.filter((m): m is ScreenShare => m instanceof ScreenShare)), + map((mediaItems) => + mediaItems.filter((m): m is ScreenShare => m instanceof ScreenShare), + ), ); private readonly spotlightSpeaker: Observable = this.userMedia.pipe( - switchMap((ms) => - ms.length === 0 + switchMap((mediaItems) => + mediaItems.length === 0 ? of([]) : combineLatest( - ms.map((m) => m.vm.speaking.pipe(map((s) => [m, s] as const))), + mediaItems.map((m) => + m.vm.speaking.pipe(map((s) => [m, s] as const)), + ), ), ), scan<(readonly [UserMedia, boolean])[], UserMedia | null, null>( - (prev, ms) => + (prev, mediaItems) => // Decide who to spotlight: // If the previous speaker is still speaking, stick with them rather // than switching eagerly to someone else - ms.find(([m, s]) => m === prev && s)?.[0] ?? + mediaItems.find(([m, s]) => m === prev && s)?.[0] ?? // Otherwise, select anyone who is speaking - ms.find(([, s]) => s)?.[0] ?? + mediaItems.find(([, s]) => s)?.[0] ?? // Otherwise, stick with the person who was last speaking prev ?? // Otherwise, spotlight the local user - ms.find(([m]) => m.vm.local)?.[0] ?? + mediaItems.find(([m]) => m.vm.local)?.[0] ?? null, null, ), @@ -431,8 +437,8 @@ export class CallViewModel extends ViewModel { ); private readonly grid: Observable = this.userMedia.pipe( - switchMap((ms) => { - const bins = ms.map((m) => + switchMap((mediaItems) => { + const bins = mediaItems.map((m) => combineLatest( [ m.speaker,