per member tiles

This commit is contained in:
Timo
2024-11-04 12:23:51 +01:00
parent 34e9e8aec3
commit e1e202d7c8
11 changed files with 103 additions and 21 deletions

View File

@@ -146,6 +146,7 @@
"feedback_tab_thank_you": "Thanks, we received your feedback!", "feedback_tab_thank_you": "Thanks, we received your feedback!",
"feedback_tab_title": "Feedback", "feedback_tab_title": "Feedback",
"more_tab_title": "More", "more_tab_title": "More",
"non_member_tiles": "Show non member tiles",
"opt_in_description": "<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.", "opt_in_description": "<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.",
"preferences_tab_body": "Here you can configure extra options for an improved experience", "preferences_tab_body": "Here you can configure extra options for an improved experience",
"preferences_tab_h4": "Preferences", "preferences_tab_h4": "Preferences",

View File

@@ -28,6 +28,8 @@ import { Initializer } from "./initializer";
import { MediaDevicesProvider } from "./livekit/MediaDevicesContext"; import { MediaDevicesProvider } from "./livekit/MediaDevicesContext";
import { widget } from "./widget"; import { widget } from "./widget";
import { useTheme } from "./useTheme"; import { useTheme } from "./useTheme";
import { nonMemberTiles } from "./settings/settings";
import { Config } from "./config/Config";
const SentryRoute = Sentry.withSentryRouting(Route); const SentryRoute = Sentry.withSentryRouting(Route);
@@ -71,6 +73,13 @@ export const App: FC<AppProps> = ({ history }) => {
.catch(logger.error); .catch(logger.error);
}); });
// Update settings to use the non member tile information from the config if set
useEffect(() => {
if (loaded && Config.get().show_non_member_tiles) {
nonMemberTiles.setValue(true);
}
});
const errorPage = <CrashView />; const errorPage = <CrashView />;
return ( return (

View File

@@ -117,6 +117,7 @@ interface RoomHeaderInfoProps {
avatarUrl: string | null; avatarUrl: string | null;
encrypted: boolean; encrypted: boolean;
participantCount: number | null; participantCount: number | null;
nonMemberItemCount: number | null;
} }
export const RoomHeaderInfo: FC<RoomHeaderInfoProps> = ({ export const RoomHeaderInfo: FC<RoomHeaderInfoProps> = ({
@@ -125,6 +126,7 @@ export const RoomHeaderInfo: FC<RoomHeaderInfoProps> = ({
avatarUrl, avatarUrl,
encrypted, encrypted,
participantCount, participantCount,
nonMemberItemCount,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const size = useMediaQuery("(max-width: 550px)") ? "sm" : "lg"; const size = useMediaQuery("(max-width: 550px)") ? "sm" : "lg";
@@ -157,7 +159,8 @@ export const RoomHeaderInfo: FC<RoomHeaderInfoProps> = ({
aria-label={t("header_participants_label")} aria-label={t("header_participants_label")}
/> />
<Text as="span" size="sm" weight="medium"> <Text as="span" size="sm" weight="medium">
{t("participant_count", { count: participantCount ?? 0 })} {t("participant_count", { count: participantCount ?? 0 })}{" "}
{(nonMemberItemCount ?? 0) > 0 && <>(+ {nonMemberItemCount})</>}
</Text> </Text>
</div> </div>
)} )}

View File

@@ -112,7 +112,7 @@ export interface ResolvedConfigOptions extends ConfigOptions {
enable_video: boolean; enable_video: boolean;
}; };
app_prompt: boolean; app_prompt: boolean;
show_non_member_participants: boolean; show_non_member_tiles: boolean;
} }
export const DEFAULT_CONFIG: ResolvedConfigOptions = { export const DEFAULT_CONFIG: ResolvedConfigOptions = {
@@ -128,5 +128,5 @@ export const DEFAULT_CONFIG: ResolvedConfigOptions = {
enable_video: true, enable_video: true,
}, },
app_prompt: true, app_prompt: true,
show_non_member_participants: false, show_non_member_tiles: false,
}; };

View File

@@ -194,6 +194,7 @@ export const InCallView: FC<InCallViewProps> = ({
} }
}, [connState, onLeave]); }, [connState, onLeave]);
const nonMemberItemCount = useObservableEagerState(vm.nonMemberItemCount);
const containerRef1 = useRef<HTMLDivElement | null>(null); const containerRef1 = useRef<HTMLDivElement | null>(null);
const [containerRef2, bounds] = useMeasure(); const [containerRef2, bounds] = useMeasure();
const boundsValid = bounds.height > 0; const boundsValid = bounds.height > 0;
@@ -633,6 +634,7 @@ export const InCallView: FC<InCallViewProps> = ({
avatarUrl={matrixInfo.roomAvatar} avatarUrl={matrixInfo.roomAvatar}
encrypted={matrixInfo.e2eeSystem.kind !== E2eeType.NONE} encrypted={matrixInfo.e2eeSystem.kind !== E2eeType.NONE}
participantCount={participantCount} participantCount={participantCount}
nonMemberItemCount={nonMemberItemCount}
/> />
</LeftNav> </LeftNav>
<RightNav> <RightNav>

View File

@@ -27,6 +27,7 @@ import {
useSetting, useSetting,
developerSettingsTab as developerSettingsTabSetting, developerSettingsTab as developerSettingsTabSetting,
duplicateTiles as duplicateTilesSetting, duplicateTiles as duplicateTilesSetting,
nonMemberTiles as nonMemberTilesSetting,
useOptInAnalytics, useOptInAnalytics,
} from "./settings"; } from "./settings";
import { isFirefox } from "../Platform"; import { isFirefox } from "../Platform";
@@ -68,6 +69,8 @@ export const SettingsModal: FC<Props> = ({
); );
const [duplicateTiles, setDuplicateTiles] = useSetting(duplicateTilesSetting); const [duplicateTiles, setDuplicateTiles] = useSetting(duplicateTilesSetting);
const [nonMemberTiles, setNonMemberTiles] = useSetting(nonMemberTilesSetting);
// Generate a `SelectInput` with a list of devices for a given device kind. // Generate a `SelectInput` with a list of devices for a given device kind.
const generateDeviceSelection = ( const generateDeviceSelection = (
devices: MediaDevice, devices: MediaDevice,
@@ -236,6 +239,20 @@ export const SettingsModal: FC<Props> = ({
)} )}
/> />
</FieldRow> </FieldRow>
<FieldRow>
<InputField
id="nonMemberTiles"
type="checkbox"
label={t("settings.non_member_tiles")}
checked={nonMemberTiles}
onChange={useCallback(
(event: ChangeEvent<HTMLInputElement>): void => {
setNonMemberTiles(event.target.checked);
},
[setNonMemberTiles],
)}
/>
</FieldRow>
</> </>
), ),
}; };

View File

@@ -72,6 +72,8 @@ export const developerSettingsTab = new Setting(
export const duplicateTiles = new Setting("duplicate-tiles", 0); export const duplicateTiles = new Setting("duplicate-tiles", 0);
export const nonMemberTiles = new Setting("non-member-tiles", true);
export const audioInput = new Setting<string | undefined>( export const audioInput = new Setting<string | undefined>(
"audio-input", "audio-input",
undefined, undefined,

View File

@@ -68,7 +68,7 @@ import {
} from "./MediaViewModel"; } from "./MediaViewModel";
import { accumulate, finalizeValue } from "../utils/observable"; import { accumulate, finalizeValue } from "../utils/observable";
import { ObservableScope } from "./ObservableScope"; import { ObservableScope } from "./ObservableScope";
import { duplicateTiles } from "../settings/settings"; import { duplicateTiles, nonMemberTiles } from "../settings/settings";
import { isFirefox } from "../Platform"; import { isFirefox } from "../Platform";
import { setPipEnabled } from "../controls"; import { setPipEnabled } from "../controls";
import { EncryptionSystem } from "../e2ee/sharedKeyManagement"; import { EncryptionSystem } from "../e2ee/sharedKeyManagement";
@@ -177,6 +177,7 @@ class UserMedia {
member: RoomMember | undefined, member: RoomMember | undefined,
participant: LocalParticipant | RemoteParticipant | undefined, participant: LocalParticipant | RemoteParticipant | undefined,
encryptionSystem: EncryptionSystem, encryptionSystem: EncryptionSystem,
rtcSession: MatrixRTCSession,
) { ) {
this.participant = new BehaviorSubject(participant); this.participant = new BehaviorSubject(participant);
@@ -186,6 +187,7 @@ class UserMedia {
member, member,
this.participant.asObservable() as Observable<LocalParticipant>, this.participant.asObservable() as Observable<LocalParticipant>,
encryptionSystem, encryptionSystem,
rtcSession,
); );
} else { } else {
this.vm = new RemoteUserMediaViewModel( this.vm = new RemoteUserMediaViewModel(
@@ -195,6 +197,7 @@ class UserMedia {
RemoteParticipant | undefined RemoteParticipant | undefined
>, >,
encryptionSystem, encryptionSystem,
rtcSession,
); );
} }
@@ -362,6 +365,7 @@ export class CallViewModel extends ViewModel {
}, },
); );
public readonly nonMemberItemCount = new BehaviorSubject<number>(0);
private readonly mediaItems: Observable<MediaItem[]> = combineLatest([ private readonly mediaItems: Observable<MediaItem[]> = combineLatest([
this.remoteParticipants, this.remoteParticipants,
observeParticipantMedia(this.livekitRoom.localParticipant), observeParticipantMedia(this.livekitRoom.localParticipant),
@@ -371,15 +375,18 @@ export class CallViewModel extends ViewModel {
this.matrixRTCSession, this.matrixRTCSession,
MatrixRTCSessionEvent.MembershipsChanged, MatrixRTCSessionEvent.MembershipsChanged,
).pipe(startWith(null)), ).pipe(startWith(null)),
// fromEvent( nonMemberTiles.value,
// this.matrixRTCSession,
// MatrixRTCSessionEvent.EncryptionKeyChanged,
// ).pipe(startWith(null)),
]).pipe( ]).pipe(
scan( scan(
( (
prevItems, prevItems,
[remoteParticipants, { participant: localParticipant }, duplicateTiles], [
remoteParticipants,
{ participant: localParticipant },
duplicateTiles,
_participantChange,
nonMemberTiles,
],
) => { ) => {
const newItems = new Map( const newItems = new Map(
function* (this: CallViewModel): Iterable<[string, MediaItem]> { function* (this: CallViewModel): Iterable<[string, MediaItem]> {
@@ -423,6 +430,7 @@ export class CallViewModel extends ViewModel {
member, member,
participant, participant,
this.encryptionSystem, this.encryptionSystem,
this.matrixRTCSession,
), ),
]; ];
} }
@@ -455,27 +463,28 @@ export class CallViewModel extends ViewModel {
// - If one wants to test scalability using the livekit cli. // - If one wants to test scalability using the livekit cli.
// - If an experimental project does not yet do the matrixRTC bits. // - If an experimental project does not yet do the matrixRTC bits.
// - If someone wants to debug if the LK connection works but matrixRTC room state failed to arrive. // - If someone wants to debug if the LK connection works but matrixRTC room state failed to arrive.
const debugShowNonMember = Config.get().show_non_member_participants; const debugShowNonMember = nonMemberTiles; //Config.get().show_non_member_tiles;
const newNonMemberItems = debugShowNonMember const newNonMemberItems = debugShowNonMember
? new Map( ? new Map(
function* (this: CallViewModel): Iterable<[string, MediaItem]> { function* (this: CallViewModel): Iterable<[string, MediaItem]> {
for (let p = 0; p < remoteParticipants.length; p++) { for (const participant of remoteParticipants) {
for (let i = 0; i < 1 + duplicateTiles; i++) { for (let i = 0; i < 1 + duplicateTiles; i++) {
const participant = remoteParticipants[p]; const maybeNonMemberParticipantId =
const maybeNoMemberParticipantId =
participant.identity + ":" + i; participant.identity + ":" + i;
if (!newItems.has(maybeNoMemberParticipantId)) { if (!newItems.has(maybeNonMemberParticipantId)) {
const nonMemberId = maybeNonMemberParticipantId;
yield [ yield [
maybeNoMemberParticipantId, nonMemberId,
// We create UserMedia with or without a participant. // We create UserMedia with or without a participant.
// This will be the initial value of a BehaviourSubject. // This will be the initial value of a BehaviourSubject.
// Once a participant appears we will update the BehaviourSubject. (see above) // Once a participant appears we will update the BehaviourSubject. (see above)
prevItems.get(maybeNoMemberParticipantId) ?? prevItems.get(nonMemberId) ??
new UserMedia( new UserMedia(
maybeNoMemberParticipantId, nonMemberId,
undefined, undefined,
participant, participant,
this.encrypted, this.encrypted,
this.matrixRTCSession,
), ),
]; ];
} }
@@ -487,13 +496,18 @@ export class CallViewModel extends ViewModel {
if (newNonMemberItems.size > 0) { if (newNonMemberItems.size > 0) {
logger.debug("Added NonMember items: ", newNonMemberItems); logger.debug("Added NonMember items: ", newNonMemberItems);
} }
const newNonMemberItemCount =
newNonMemberItems.size / (1 + duplicateTiles);
if (this.nonMemberItemCount.value !== newNonMemberItemCount)
this.nonMemberItemCount.next(newNonMemberItemCount);
const combinedNew = new Map([ const combinedNew = new Map([
...newNonMemberItems.entries(), ...newNonMemberItems.entries(),
...newItems.entries(), ...newItems.entries(),
]); ]);
for (const [id, t] of prevItems) if (!combinedNew.has(id)) t.destroy(); for (const [id, t] of prevItems) if (!combinedNew.has(id)) t.destroy();
return newItems; return combinedNew;
}, },
new Map<string, MediaItem>(), new Map<string, MediaItem>(),
), ),

View File

@@ -37,6 +37,11 @@ import {
switchMap, switchMap,
} from "rxjs"; } from "rxjs";
import { useEffect } from "react"; import { useEffect } from "react";
import {
MatrixRTCSession,
MatrixRTCSessionEvent,
} from "matrix-js-sdk/src/matrixrtc";
import { logger } from "matrix-js-sdk/src/logger";
import { ViewModel } from "./ViewModel"; import { ViewModel } from "./ViewModel";
import { useReactiveState } from "../useReactiveState"; import { useReactiveState } from "../useReactiveState";
@@ -196,11 +201,16 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel {
*/ */
public readonly cropVideo: Observable<boolean> = this._cropVideo; public readonly cropVideo: Observable<boolean> = this._cropVideo;
public readonly keys = new BehaviorSubject(
[] as { index: number; key: Uint8Array }[],
);
public constructor( public constructor(
id: string, id: string,
member: RoomMember | undefined, member: RoomMember | undefined,
participant: Observable<LocalParticipant | RemoteParticipant | undefined>, participant: Observable<LocalParticipant | RemoteParticipant | undefined>,
encryptionSystem: EncryptionSystem, encryptionSystem: EncryptionSystem,
rtcSession: MatrixRTCSession,
) { ) {
super( super(
id, id,
@@ -211,7 +221,18 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel {
Track.Source.Camera, Track.Source.Camera,
); );
// const media = observeParticipantMedia(participant).pipe(this.scope.state()); // rtcSession.on(
// MatrixRTCSessionEvent.EncryptionKeyChanged,
// (key, index, participantId) => {
// if (id.startsWith(participantId))
// logger.info("got new keys: ", participant, { index, key });
// logger.info("All keys for participant ", participant, " - ", [
// ...this.keys.value,
// { index, key },
// ]);
// this.keys.next([...this.keys.value, { index, key }]);
// },
// );
const media = participant.pipe( const media = participant.pipe(
switchMap((p) => (p && observeParticipantMedia(p)) ?? of(undefined)), switchMap((p) => (p && observeParticipantMedia(p)) ?? of(undefined)),
@@ -263,8 +284,9 @@ export class LocalUserMediaViewModel extends BaseUserMediaViewModel {
member: RoomMember | undefined, member: RoomMember | undefined,
participant: Observable<LocalParticipant | undefined>, participant: Observable<LocalParticipant | undefined>,
encryptionSystem: EncryptionSystem, encryptionSystem: EncryptionSystem,
rtcSession: MatrixRTCSession,
) { ) {
super(id, member, participant, encryptionSystem); super(id, member, participant, encryptionSystem, rtcSession);
} }
} }
@@ -323,8 +345,9 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
member: RoomMember | undefined, member: RoomMember | undefined,
participant: Observable<RemoteParticipant | undefined>, participant: Observable<RemoteParticipant | undefined>,
encryptionSystem: EncryptionSystem, encryptionSystem: EncryptionSystem,
rtcSession: MatrixRTCSession,
) { ) {
super(id, member, participant, encryptionSystem); super(id, member, participant, encryptionSystem, rtcSession);
// Sync the local volume with LiveKit // Sync the local volume with LiveKit
combineLatest([ combineLatest([

View File

@@ -84,6 +84,7 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
const videoEnabled = useObservableEagerState(vm.videoEnabled); const videoEnabled = useObservableEagerState(vm.videoEnabled);
const speaking = useObservableEagerState(vm.speaking); const speaking = useObservableEagerState(vm.speaking);
const cropVideo = useObservableEagerState(vm.cropVideo); const cropVideo = useObservableEagerState(vm.cropVideo);
const keys = useObservableEagerState(vm.keys);
const isRTCParticipantAvailable = useObservableEagerState( const isRTCParticipantAvailable = useObservableEagerState(
vm.isRTCParticipantAvailable, vm.isRTCParticipantAvailable,
); );
@@ -121,6 +122,7 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
ref={ref} ref={ref}
video={video} video={video}
member={vm.member} member={vm.member}
keys={keys}
unencryptedWarning={unencryptedWarning} unencryptedWarning={unencryptedWarning}
videoEnabled={videoEnabled && showVideo} videoEnabled={videoEnabled && showVideo}
videoFit={cropVideo ? "cover" : "contain"} videoFit={cropVideo ? "cover" : "contain"}

View File

@@ -29,6 +29,7 @@ interface Props extends ComponentProps<typeof animated.div> {
videoFit: "cover" | "contain"; videoFit: "cover" | "contain";
mirror: boolean; mirror: boolean;
member: RoomMember | undefined; member: RoomMember | undefined;
keys: { index: number; key: Uint8Array }[];
videoEnabled: boolean; videoEnabled: boolean;
unencryptedWarning: boolean; unencryptedWarning: boolean;
nameTagLeadingIcon?: ReactNode; nameTagLeadingIcon?: ReactNode;
@@ -48,6 +49,7 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
videoFit, videoFit,
mirror, mirror,
member, member,
keys,
videoEnabled, videoEnabled,
unencryptedWarning, unencryptedWarning,
nameTagLeadingIcon, nameTagLeadingIcon,
@@ -98,11 +100,18 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
minature={avatarSize < 96} minature={avatarSize < 96}
showTimer={handRaiseTimerVisible} showTimer={handRaiseTimerVisible}
/> />
{/* {keys &&
keys.map(({ index, key }) => (
<Text as="span" size="sm">
index:{index}, key:{key}
</Text>
))} */}
<div className={styles.nameTag}> <div className={styles.nameTag}>
{nameTagLeadingIcon} {nameTagLeadingIcon}
<Text as="span" size="sm" weight="medium" className={styles.name}> <Text as="span" size="sm" weight="medium" className={styles.name}>
{displayName} {displayName}
</Text> </Text>
{unencryptedWarning && ( {unencryptedWarning && (
<Tooltip <Tooltip
label={t("common.unencrypted")} label={t("common.unencrypted")}