Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion locales/de/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@
"muted_for_me": "Für mich stumm geschaltet",
"screen_share_volume": "Lautstärke der Bildschirmfreigabe",
"volume": "Lautstärke",
"waiting_for_media": "Warten auf Medien..."
"waiting_for_media": "Warten auf Medien...",
"stop_watching": "Nicht mehr zuschauen",
"watch_stream": "Stream zuschauen"
}
}
6 changes: 5 additions & 1 deletion locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,8 @@
"preferences_tab": {
"developer_mode_label": "Developer mode",
"developer_mode_label_description": "Enable developer mode and show developer settings tab.",
"hide_avatars_when_camera_off_description": "Hide avatar tiles of participants whose camera is off.",
"hide_avatars_when_camera_off_label": "Hide avatar tiles when camera is off",
"introduction": "Here you can configure extra options for an improved experience.",
"reactions_play_sound_description": "Play a sound effect when anyone sends a reaction into a call.",
"reactions_play_sound_label": "Play reaction sounds",
Expand Down Expand Up @@ -291,6 +293,8 @@
"muted_for_me": "Muted for me",
"screen_share_volume": "Screen share volume",
"volume": "Volume",
"waiting_for_media": "Waiting for media..."
"waiting_for_media": "Waiting for media...",
"stop_watching": "Stop watching",
"watch_stream": "Watch stream"
}
}
15 changes: 15 additions & 0 deletions src/grid/CallLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,18 @@ export function arrangeTiles(

return { tileWidth, tileHeight, gap, columns };
}

/**
* @param cameraCount - Number of regular participant tiles
* @param streamCount - Number of screen shares
*/
export function arrangeTilesWithStreams(
width: number,
minHeight: number,
cameraCount: number,
streamCount: number,
): GridArrangement {
// Each stream occupies a 2x2 block, so it consumes four regular cells worth of space.
const effectiveTileCount = cameraCount + streamCount * 4;
return arrangeTiles(width, minHeight, effectiveTileCount);
}
26 changes: 22 additions & 4 deletions src/grid/GridLayout.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,34 @@ Please see LICENSE in the repository root for full details.
}

.scrolling {
display: flex;
flex-wrap: wrap;
display: grid;
grid-template-columns: repeat(var(--columns), var(--width));
grid-auto-rows: var(--height);
grid-auto-flow: row;
justify-content: center;
align-content: center;
gap: var(--gap);
}

.scrolling > .slot {
width: var(--width);
height: var(--height);
min-width: 0;
min-height: 0;
}

/* Larger tiles for Screen shares in Grids */
.scrolling > .slot[data-stream="true"] {
grid-column: span 2;
grid-row: span 2;
}

/* Focused tile takes up the entire grid area */
.scrolling.focused {
display: block;
}

.scrolling.focused > .slot {
width: 100%;
height: 100%;
}

.fixed {
Expand Down
44 changes: 37 additions & 7 deletions src/grid/GridLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,19 @@ import {
} from "react";
import { distinctUntilChanged } from "rxjs";
import { useObservableEagerState } from "observable-hooks";
import classNames from "classnames";

import { type GridLayout as GridLayoutModel } from "../state/layout-types.ts";
import styles from "./GridLayout.module.css";
import { useInitial } from "../useInitial";
import { type CallLayout, arrangeTiles } from "./CallLayout";
import { type CallLayout, arrangeTilesWithStreams } from "./CallLayout";
import { type DragCallback, useUpdateLayout, useVisibleTiles } from "./Grid";

interface GridCSSProperties extends CSSProperties {
"--gap": string;
"--width": string;
"--height": string;
"--columns": string;
}

/**
Expand Down Expand Up @@ -79,26 +81,54 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
useUpdateLayout();
useVisibleTiles(model.setVisibleTiles);
const { width, height: minHeight } = useObservableEagerState(minBounds$);
const { gap, tileWidth, tileHeight } = useMemo(
() => arrangeTiles(width, minHeight, model.grid.length),
[width, minHeight, model.grid.length],
// Screen shares are shown as larger 2x2 tiles
const { gap, tileWidth, tileHeight, columns } = useMemo(() => {
const streamCount = model.grid.filter(
(m) => m.media$.value.type === "screen share",
).length;
return arrangeTilesWithStreams(
width,
minHeight,
model.grid.length - streamCount,
streamCount,
);
}, [width, minHeight, model.grid]);

// Render camera tiles before screen shares
const orderedTiles = useMemo(
() => [
...model.grid.filter((m) => m.media$.value.type !== "screen share"),
...model.grid.filter((m) => m.media$.value.type === "screen share"),
],
[model.grid],
);

return (
<div
ref={ref}
className={styles.scrolling}
className={classNames(styles.scrolling, {
[styles.focused]: model.focused,
})}
style={
{
width,
"--gap": `${gap}px`,
"--width": `${Math.floor(tileWidth)}px`,
"--height": `${Math.floor(tileHeight)}px`,
"--columns": `${columns}`,
} as GridCSSProperties
}
>
{model.grid.map((m) => (
<Slot key={m.id} className={styles.slot} id={m.id} model={m} />
{orderedTiles.map((m) => (
<Slot
key={m.id}
className={classNames(styles.slot, {
[styles.focused]: model.focused,
})}
id={m.id}
model={m}
data-stream={m.media$.value.type === "screen share" || undefined}
/>
))}
</div>
);
Expand Down
2 changes: 2 additions & 0 deletions src/room/InCallView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,8 @@ export const InCallView: FC<InCallViewProps> = ({
showRingingStatus={showRingingStatus}
showOutline={showOutline}
focusable={!contentObscured}
focusedStream$={vm.focusedStream$}
onToggleFocusedStream={vm.setFocusedStream}
/>
) : (
<SpotlightTile
Expand Down
19 changes: 19 additions & 0 deletions src/settings/PreferencesSettingsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
showReactions as showReactionsSetting,
playReactionsSound as playReactionsSoundSetting,
developerMode as developerModeSetting,
hideAvatarTilesWhenCameraOff as hideAvatarTilesWhenCameraOffSetting,
useSetting,
} from "./settings";

Expand All @@ -30,6 +31,10 @@ export const PreferencesSettingsTab: FC = () => {
playReactionsSoundSetting,
);

const [hideAvatarTilesWhenCameraOff, setHideAvatarTilesWhenCameraOff] = useSetting(
hideAvatarTilesWhenCameraOffSetting,
);

const onChangeSetting = (
e: ChangeEvent<HTMLInputElement>,
fn: (value: boolean) => void,
Expand Down Expand Up @@ -76,6 +81,20 @@ export const PreferencesSettingsTab: FC = () => {
onChange={(e) => onChangeSetting(e, setPlayReactionSound)}
/>
</FieldRow>
<FieldRow>
<InputField
id="hideAvatarsWhenCameraOff"
label={t(
"settings.preferences_tab.hide_avatars_when_camera_off_label",
)}
description={t(
"settings.preferences_tab.hide_avatars_when_camera_off_description",
)}
type="checkbox"
checked={hideAvatarTilesWhenCameraOff}
onChange={(e) => onChangeSetting(e, setHideAvatarTilesWhenCameraOff)}
/>
</FieldRow>
<FieldRow>
<InputField
id="developerSettingsTab"
Expand Down
5 changes: 5 additions & 0 deletions src/settings/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ export const muteAllAudio = new Setting<boolean>("mute-all-audio", false);

export const alwaysShowSelf = new Setting<boolean>("always-show-self", true);

export const hideAvatarTilesWhenCameraOff = new Setting<boolean>(
"hide-avatars-when-camera-off",
false,
);

export const alwaysShowIphoneEarpiece = new Setting<boolean>(
"always-show-iphone-earpiece",
false,
Expand Down
Loading