Skip to content
Merged
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
16 changes: 8 additions & 8 deletions src/components/CallView/CallView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
:model="selectedCallParticipantModel"
:sharedData="sharedDatas[selectedVideoPeerId]"
:showTalkingHighlight="false"
:isOneToOne="isOneToOne"
:isFullPage="showFullPage"
isGrid
isBig
fitVideo />
Expand Down Expand Up @@ -77,7 +77,6 @@
:models="promotedSpeakerModels"
:sharedDatas="sharedDatas"
:showVideoOverlay="showVideoOverlay"
:isOneToOne="isOneToOne"
@selectVideo="handleSelectVideo" />
<!-- Promoted "autopilot" mode -->
<VideoVue
Expand All @@ -91,7 +90,7 @@
isGrid
fitVideo
isBig
:isOneToOne="isOneToOne"
:isFullPage="showFullPage"
:isSidebar="isSidebar"
@forcePromoteVideo="forcePromotedModel = $event" />
<!-- presenter overlay -->
Expand Down Expand Up @@ -451,12 +450,13 @@ export default {
return this.selectedVideoPeerId !== null && !this.screens.includes(this.selectedVideoPeerId)
},

isOneToOne() {
return this.callParticipantModels.length === 1
},

// Whether the promoted video covers the whole call view, with the stripe
// floating over it rather than taking room of its own. A promoted area
// held by a single participant is laid out that way, and so is one with
// nobody in it yet.
showFullPage() {
return this.isOneToOne && !(this.showLocalScreen || this.showRemoteScreen || this.showSelectedScreen)
return this.callParticipantModels.length <= 1
&& !(this.showLocalScreen || this.showRemoteScreen || this.showSelectedScreen)
},

hasLocalVideo() {
Expand Down
3 changes: 0 additions & 3 deletions src/components/CallView/Grid/SpeakersGrid.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ const props = defineProps<{
sharedDatas: Record<string, object>
/** Whether the video overlay is currently shown */
showVideoOverlay?: boolean
/** Whether the call is a one to one conversation */
isOneToOne?: boolean
}>()

const emit = defineEmits<{
Expand Down Expand Up @@ -101,7 +99,6 @@ function tileStyle(index: number) {
:model="model"
:sharedData="sharedDatas[model.attributes.peerId]"
:showVideoOverlay="showVideoOverlay"
:isOneToOne="isOneToOne"
isGrid
fitVideo
@clickVideo="emit('selectVideo', model.attributes.peerId)" />
Expand Down
2 changes: 1 addition & 1 deletion src/components/CallView/Grid/StripeControls.vue
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ const toggleLabel = computed(() => props.isOpen
align-items: center;
gap: var(--default-grid-baseline);
width: fit-content;
padding-block-start: var(--default-grid-baseline);
padding-block-start: var(--grid-gap);
// Barely there while the call is not being looked at, as they sit over the
// tiles, and in their own color as soon as it is
opacity: .4;
Expand Down
34 changes: 29 additions & 5 deletions src/components/CallView/Grid/VideosGrid.vue
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ import {
getHalfColumnCount,
getHalfColumnMaxWidth,
getHalfColumnMinWidth,
getJustifyContent,
GRID_GAP,
TARGET_ASPECT_RATIO,
TILE_COLUMN_SPAN,
Expand Down Expand Up @@ -465,8 +466,14 @@ export default {
gridTemplateColumns: `repeat(${getHalfColumnCount(columns)}, ${halfColumnWidth})`,
gridTemplateRows: `repeat(${rows}, minmax(${this.dpiAwareMinHeight}px, 1fr))`,
// The columns no longer take the whole width once they are
// capped, so the grid itself has to center them
justifyContent: 'center',
// capped, so the grid itself has to place them
justifyContent: getJustifyContent({
isStripe: this.isStripe,
totalTiles: this.totalTiles,
// The local video is laid out after the remote tiles, so it
// is the only tile of the grid when no remote one is shown
isLocalVideoAlone: !this.noLocalVideoReserve && this.displayedVideos.length === 0,
}),
}
},

Expand Down Expand Up @@ -614,6 +621,7 @@ export default {

&--stripe {
height: var(--stripe-height);
margin-block-start: var(--grid-gap);
}
}

Expand Down Expand Up @@ -669,6 +677,22 @@ export default {
// Kept out of the grid itself, whose measured height is the height of its
// tiles
padding-block: var(--grid-gap);
// Keeps the tiles off the rounded corners of the card
padding-inline: var(--grid-gap);

// The card the tiles sit on
.grid-main-wrapper:not(.overlap) & {
background-color: #262626;
border-radius: var(--border-radius-container);
}

// A stripe overlapping the promoted video takes no card of its own, which
// would band that video across: its tiles - the self camera, the only one
// left in a one to one call - are lifted off the call by a shadow instead
.overlap & :deep(.localVideoContainer),
.overlap & :deep(.video-container-grid) {
box-shadow: 0 0px 10px rgba(0, 0, 0, 0.5);
}
}

.dev-mode-video {
Expand Down Expand Up @@ -744,15 +768,15 @@ export default {

.stripe-controls-position {
position: absolute;
top: var(--grid-gap);
inset-inline-end: var(--grid-gap);
top: calc(var(--default-grid-baseline) * 3);
inset-inline-end: calc(var(--default-grid-baseline) * 3);
z-index: 2;
transition: top var(--animation-slow) ease-in-out;

// A collapsed stripe holds no tile to sit over, and no room of its own to
// sit in, so the controls take the room above it
&--collapsed {
top: calc(-1 * (var(--clickable-area-small) + var(--default-grid-baseline) + var(--grid-gap)));
top: calc(-1 * (var(--clickable-area-small) + var(--grid-gap) + var(--grid-gap)));
}
}

Expand Down
22 changes: 22 additions & 0 deletions src/components/CallView/Grid/gridLayout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getHalfColumnCount,
getHalfColumnMaxWidth,
getHalfColumnMinWidth,
getJustifyContent,
getMinTileHeight,
getMinTileWidth,
GRID_GAP,
Expand Down Expand Up @@ -342,4 +343,25 @@ describe('gridLayout', () => {
}
})
})

describe('getJustifyContent', () => {
test('centers the tiles of the grid', () => {
expect(getJustifyContent({ isStripe: false, totalTiles: 1, isLocalVideoAlone: true })).toBe('center')
expect(getJustifyContent({ isStripe: false, totalTiles: 5, isLocalVideoAlone: false })).toBe('center')
})

test('centers the tiles of a stripe holding several of them', () => {
expect(getJustifyContent({ isStripe: true, totalTiles: 2, isLocalVideoAlone: false })).toBe('center')
})

test('keeps a lone local video at the end of the stripe', () => {
expect(getJustifyContent({ isStripe: true, totalTiles: 1, isLocalVideoAlone: true })).toBe('end')
})

test('keeps a lone remote tile at the start of the stripe', () => {
// The local video is not shown as a tile, so the only tile left is a
// remote one and it belongs to the start of the row
expect(getJustifyContent({ isStripe: true, totalTiles: 1, isLocalVideoAlone: false })).toBe('start')
})
})
})
40 changes: 40 additions & 0 deletions src/components/CallView/Grid/gridLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,3 +375,43 @@ export function computeTilePlacements({

return placements
}

/** Alignment of the tile columns within the grid */
export type GridJustifyContent = 'center' | 'start' | 'end'

type JustifyContentOptions = {
/** Whether the grid is shown as a stripe */
isStripe: boolean
/** Number of tiles laid out on the page, including the local video tile */
totalTiles: number
/** Whether the local video is the only tile of the grid */
isLocalVideoAlone: boolean
}

/**
* Alignment of the tile columns within the grid.
*
* The columns no longer take the whole width once they are capped, so the grid
* itself has to place them. They are centered, except in a stripe holding a
* single tile: that tile keeps the side it would sit on in a fuller stripe
* rather than moving to the middle. The local video is laid out after the
* remote ones, so it stays at the inline end, while a lone remote tile keeps
* the inline start the tiles are laid out from - which is where it belongs once
* the local video is not shown as a tile.
*
* @param options - the layout inputs
* @param options.isStripe - whether the grid is shown as a stripe
* @param options.totalTiles - number of tiles on the page, including the local video tile
* @param options.isLocalVideoAlone - whether the local video is the only tile
*/
export function getJustifyContent({
isStripe,
totalTiles,
isLocalVideoAlone,
}: JustifyContentOptions): GridJustifyContent {
if (!isStripe || totalTiles !== 1) {
return 'center'
}

return isLocalVideoAlone ? 'end' : 'start'
}
35 changes: 32 additions & 3 deletions src/components/CallView/Grid/useGridDimensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,21 @@ export function useGridDimensions({
return
}

gridWidth.value = element.clientWidth
gridHeight.value = element.clientHeight
const width = element.clientWidth
const height = element.clientHeight

// The grid is hidden or mid-transition (the stripe collapses and expands
// by its height): measuring it now would shrink the layout down to a
// single row and it would be kept until the next resize, as the element
// is given back its size by a transition rather than by a layout change
// of its own. Keep the current layout instead and wait for the
// `ResizeObserver` to report the final size.
if (width <= 0 || height <= 0) {
return
}

gridWidth.value = width
gridHeight.value = height

const dimensions = computeGridDimensions({
gridWidth: gridWidth.value,
Expand All @@ -115,10 +128,26 @@ export function useGridDimensions({
let resizeObserver: ResizeObserver | null = null

onMounted(() => {
resizeObserver = new ResizeObserver(debouncedRecompute)

if (wrapper.value) {
resizeObserver = new ResizeObserver(debouncedRecompute)
resizeObserver.observe(wrapper.value)
}

// The grid element is observed as well: it is the one being measured and
// its size does not always follow the size of the wrapper, which keeps
// the whole height of the call view while the grid is collapsed and
// expanded by a transition. The element is unmounted along with the
// collapsed stripe, so it is observed again whenever it comes back.
watch(grid, (element, previousElement) => {
if (previousElement) {
resizeObserver?.unobserve(previousElement)
}
if (element) {
resizeObserver?.observe(element)
}
}, { immediate: true })

recompute()
})

Expand Down
10 changes: 6 additions & 4 deletions src/components/CallView/shared/VideoBackground.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ const isDarkTheme = useIsDarkTheme()
top: 0;
height: 100%;
width: 100%;
background-color: rgba(var(--overlay-color), 0.3);
background-image: none;
// A solid surface: nothing of the call view behind the tile shows through
// it. The two colours are what the translucent tint they replace used to
// composite to over the background of the call, so a tile keeps the shade
// it had in either theme.
background-color: #363636;

--overlay-color: 0, 0, 0;
&.dark-theme {
--overlay-color: 255, 255, 255;
background-color: #515151;
}
}
</style>
9 changes: 5 additions & 4 deletions src/components/CallView/shared/VideoVue.vue
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,9 @@ export default {
default: false,
},

// True when this video component is used in one to one conversations
isOneToOne: {
// True when this video fills the whole call view, with the stripe
// floating over it
isFullPage: {
type: Boolean,
default: false,
},
Expand Down Expand Up @@ -323,7 +324,7 @@ export default {
presenter: this.isPresenterOverlay && this.mouseover,
'video-container-grid': this.isGrid,
'video-container-big': this.isBig,
'one-to-one': this.isOneToOne,
'full-page': this.isFullPage,
'presenter-overlay': this.isPresenterOverlay,
}
},
Expand Down Expand Up @@ -654,7 +655,7 @@ export default {
.video-container-big {
position: absolute;

&.one-to-one {
&.full-page {
width: calc(100% - var(--wrapper-padding) * 2);
}

Expand Down
Loading