diff --git a/package.json b/package.json index c39aa9f3..fbfcfb24 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "classnames": "^2.3.1", "color-hash": "^2.0.1", "events": "^3.3.0", - "lodash-move": "^1.1.1", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#aa0d3bd1f5a006d151f826e6b8c5f286abb6e960", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", diff --git a/src/room/InCallView.jsx b/src/room/InCallView.jsx index 9e1fab0e..a4749633 100644 --- a/src/room/InCallView.jsx +++ b/src/room/InCallView.jsx @@ -104,23 +104,6 @@ export function InCallView({ return participants; }, [userMediaFeeds, activeSpeaker, screenshareFeeds, layout]); - const onFocusTile = useCallback( - (tiles, focusedTile) => { - if (layout === "freedom") { - return tiles.map((tile) => { - if (tile === focusedTile) { - return { ...tile, focused: !tile.focused }; - } - - return tile; - }); - } else { - return tiles; - } - }, - [layout, setLayout] - ); - const renderAvatar = useCallback( (roomMember, width, height) => { const avatarUrl = roomMember.user?.avatarUrl; @@ -160,12 +143,7 @@ export function InCallView({

Waiting for other participants...

) : ( - + {({ item, ...rest }) => ( { - if (is1on1Freedom && a.item.isLocal !== b.item.isLocal) { - return (b.item.isLocal ? 1 : 0) - (a.item.isLocal ? 1 : 0); - } else if (a.focused !== b.focused) { - return (b.focused ? 1 : 0) - (a.focused ? 1 : 0); - } else if (a.presenter !== b.presenter) { - return (b.presenter ? 1 : 0) - (a.presenter ? 1 : 0); - } + const orderedTiles = new Array(tiles.length); + tiles.forEach((tile) => (orderedTiles[tile.order] = tile)); - return 0; - }); + orderedTiles.forEach((tile) => + (tile.focused + ? focusedTiles + : tile.presenter + ? presenterTiles + : otherTiles + ).push(tile) + ); + + [...focusedTiles, ...presenterTiles, ...otherTiles].forEach( + (tile, i) => (tile.order = i) + ); } -export function VideoGrid({ - items, - layout, - onFocusTile, - disableAnimations, - children, -}) { +export function VideoGrid({ items, layout, disableAnimations, children }) { // Place the PiP in the bottom right corner by default const [pipXRatio, setPipXRatio] = useState(1); const [pipYRatio, setPipYRatio] = useState(1); - const [{ tiles, tilePositions, scrollPosition }, setTileState] = useState({ + const [{ tiles, tilePositions }, setTileState] = useState({ tiles: [], tilePositions: [], - scrollPosition: 0, }); + const [scrollPosition, setScrollPosition] = useState(0); const draggingTileRef = useRef(null); const lastTappedRef = useRef({}); const lastLayoutRef = useRef(layout); @@ -646,7 +645,7 @@ export function VideoGrid({ useEffect(() => { setTileState(({ tiles, ...rest }) => { const newTiles = []; - const removedTileKeys = []; + const removedTileKeys = new Set(); for (const tile of tiles) { let item = items.find((item) => item.id === tile.key); @@ -656,7 +655,7 @@ export function VideoGrid({ if (!item) { remove = true; item = tile.item; - removedTileKeys.push(tile.key); + removedTileKeys.add(tile.key); } let focused; @@ -671,6 +670,7 @@ export function VideoGrid({ newTiles.push({ key: item.id, + order: tile.order, item, remove, focused, @@ -691,6 +691,7 @@ export function VideoGrid({ const newTile = { key: item.id, + order: existingTile?.order ?? newTiles.length, item, remove: false, focused: layout === "spotlight" && item.focused, @@ -706,22 +707,19 @@ export function VideoGrid({ } } - sortTiles(layout, newTiles); + reorderTiles(newTiles); - if (removedTileKeys.length > 0) { + if (removedTileKeys.size > 0) { setTimeout(() => { if (!isMounted.current) { return; } setTileState(({ tiles, ...rest }) => { - const newTiles = tiles.filter( - (tile) => !removedTileKeys.includes(tile.key) - ); - - // TODO: When we remove tiles, we reuse the order of the tiles vs calling sort on the - // items array. This can cause the local feed to display large in the room. - // To fix this we need to move to using a reducer and sorting the input items + const newTiles = tiles + .filter((tile) => !removedTileKeys.has(tile.key)) + .map((tile) => ({ ...tile })); // clone before reordering + reorderTiles(newTiles); const presenterTileCount = newTiles.reduce( (count, tile) => count + (tile.focused ? 1 : 0), @@ -771,7 +769,7 @@ export function VideoGrid({ const animate = useCallback( (tiles) => (tileIndex) => { const tile = tiles[tileIndex]; - const tilePosition = tilePositions[tileIndex]; + const tilePosition = tilePositions[tile.order]; const draggingTile = draggingTileRef.current; const dragging = draggingTile && tile.key === draggingTile.key; const remove = tile.remove; @@ -830,6 +828,9 @@ export function VideoGrid({ reset: false, immediate: (key) => disableAnimations || key === "zIndex" || key === "shadow", + // If we just stopped dragging a tile, give it time for its animation + // to settle before pushing its z-index back down + delay: (key) => (key === "zIndex" ? 500 : 0), }; } }, @@ -854,43 +855,25 @@ export function VideoGrid({ lastTappedRef.current[tileKey] = 0; const tile = tiles.find((tile) => tile.key === tileKey); - - if (!tile) { - return; - } - + if (!tile || layout !== "freedom") return; const item = tile.item; - setTileState((state) => { + setTileState(({ tiles, ...state }) => { let presenterTileCount = 0; + const newTiles = tiles.map((tile) => { + let newTile = { ...tile }; // clone before reordering - let newTiles; - - if (onFocusTile) { - newTiles = onFocusTile(state.tiles, tile); - - for (const tile of newTiles) { - if (tile.focused) { - presenterTileCount++; - } + if (tile.item === item) { + newTile.focused = !tile.focused; + } + if (newTile.focused) { + presenterTileCount++; } - } else { - newTiles = state.tiles.map((tile) => { - let newTile = tile; - if (tile.item === item) { - newTile = { ...tile, focused: !tile.focused }; - } + return newTile; + }); - if (newTile.focused) { - presenterTileCount++; - } - - return newTile; - }); - } - - sortTiles(layout, newTiles); + reorderTiles(newTiles); return { ...state, @@ -907,7 +890,7 @@ export function VideoGrid({ }; }); }, - [tiles, gridBounds, onFocusTile, layout] + [tiles, gridBounds, layout] ); const bindTile = useDrag( @@ -923,17 +906,18 @@ export function VideoGrid({ const dragTileIndex = tiles.findIndex((tile) => tile.key === key); const dragTile = tiles[dragTileIndex]; - const dragTilePosition = tilePositions[dragTileIndex]; - - let newTiles = tiles; + const dragTilePosition = tilePositions[dragTile.order]; const cursorPosition = [xy[0] - gridBounds.left, xy[1] - gridBounds.top]; - if (layout === "freedom" && tiles.length === 2) { - // We're in 1:1 mode, so only the local tile should be draggable - if (dragTileIndex !== 0) return; + let newTiles = tiles; - // Only update the position on the last event + if (tiles.length === 2) { + // We're in 1:1 mode, so only the local tile should be draggable + if (!dragTile.item.isLocal) return; + + // Position should only update on the very last event, to avoid + // compounding the offset on every drag event if (last) { const remotePosition = tilePositions[1]; @@ -959,37 +943,40 @@ export function VideoGrid({ setPipXRatio(Math.max(0, Math.min(1, newPipXRatio))); setPipYRatio(Math.max(0, Math.min(1, newPipYRatio))); } - } - - for ( - let hoverTileIndex = 0; - hoverTileIndex < tiles.length; - hoverTileIndex++ - ) { - const hoverTile = tiles[hoverTileIndex]; - const hoverTilePosition = tilePositions[hoverTileIndex]; - - if (hoverTile.key === key) { - continue; - } - - if (isInside(cursorPosition, hoverTilePosition)) { - newTiles = moveArrItem(tiles, dragTileIndex, hoverTileIndex); + } else { + const hoverTile = tiles.find( + (tile) => + tile.key !== key && + isInside(cursorPosition, tilePositions[tile.order]) + ); + if (hoverTile) { + // Shift the tiles into their new order newTiles = newTiles.map((tile) => { - if (tile === hoverTile) { - return { ...tile, focused: dragTile.focused }; - } else if (tile === dragTile) { - return { ...tile, focused: hoverTile.focused }; + let order = tile.order; + if (order < dragTile.order) { + if (order >= hoverTile.order) order++; + } else if (order > dragTile.order) { + if (order <= hoverTile.order) order--; } else { - return tile; + order = hoverTile.order; } + + let focused; + if (tile === hoverTile) { + focused = dragTile.focused; + } else if (tile === dragTile) { + focused = hoverTile.focused; + } else { + focused = tile.focused; + } + + return { ...tile, order, focused }; }); - sortTiles(layout, newTiles); + reorderTiles(newTiles); setTileState((state) => ({ ...state, tiles: newTiles })); - break; } } @@ -1037,13 +1024,9 @@ export function VideoGrid({ : gridBounds.height - lastTile.y - lastTile.height - GAP; } - setTileState((state) => ({ - ...state, - scrollPosition: Math.min( - Math.max(movement + state.scrollPosition, min), - 0 - ), - })); + setScrollPosition((scrollPosition) => + Math.min(Math.max(movement + scrollPosition, min), 0) + ); }, [layout, gridBounds, tilePositions] ); @@ -1060,7 +1043,7 @@ export function VideoGrid({
{springs.map(({ shadow, ...style }, i) => { const tile = tiles[i]; - const tilePosition = tilePositions[i]; + const tilePosition = tilePositions[tile.order]; return children({ ...bindTile(tile.key), diff --git a/yarn.lock b/yarn.lock index 44c9a4e6..74d88f48 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8461,13 +8461,6 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash-move@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/lodash-move/-/lodash-move-1.1.1.tgz#59f76e0f1ac57e6d8683f531bec07c5b6ea4e348" - integrity sha1-WfduDxrFfm2Gg/UxvsB8W26k40g= - dependencies: - lodash "^4.6.1" - lodash.curry@^4.0.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.curry/-/lodash.curry-4.1.1.tgz#248e36072ede906501d75966200a86dab8b23170" @@ -8493,7 +8486,7 @@ lodash.uniq@4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.6.1: +lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==