Merge pull request #354 from robintown/smooth-dnd
Smoother drag-and-drop
This commit is contained in:
commit
edf58f1d7d
4 changed files with 93 additions and 135 deletions
|
@ -35,7 +35,6 @@
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"color-hash": "^2.0.1",
|
"color-hash": "^2.0.1",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"lodash-move": "^1.1.1",
|
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#aa0d3bd1f5a006d151f826e6b8c5f286abb6e960",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#aa0d3bd1f5a006d151f826e6b8c5f286abb6e960",
|
||||||
"mermaid": "^8.13.8",
|
"mermaid": "^8.13.8",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
|
|
|
@ -104,23 +104,6 @@ export function InCallView({
|
||||||
return participants;
|
return participants;
|
||||||
}, [userMediaFeeds, activeSpeaker, screenshareFeeds, layout]);
|
}, [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(
|
const renderAvatar = useCallback(
|
||||||
(roomMember, width, height) => {
|
(roomMember, width, height) => {
|
||||||
const avatarUrl = roomMember.user?.avatarUrl;
|
const avatarUrl = roomMember.user?.avatarUrl;
|
||||||
|
@ -160,12 +143,7 @@ export function InCallView({
|
||||||
<p>Waiting for other participants...</p>
|
<p>Waiting for other participants...</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<VideoGrid
|
<VideoGrid items={items} layout={layout} disableAnimations={isSafari}>
|
||||||
items={items}
|
|
||||||
layout={layout}
|
|
||||||
onFocusTile={onFocusTile}
|
|
||||||
disableAnimations={isSafari}
|
|
||||||
>
|
|
||||||
{({ item, ...rest }) => (
|
{({ item, ...rest }) => (
|
||||||
<VideoTileContainer
|
<VideoTileContainer
|
||||||
key={item.id}
|
key={item.id}
|
||||||
|
|
|
@ -19,7 +19,6 @@ import { useDrag, useGesture } from "@use-gesture/react";
|
||||||
import { useSprings } from "@react-spring/web";
|
import { useSprings } from "@react-spring/web";
|
||||||
import useMeasure from "react-use-measure";
|
import useMeasure from "react-use-measure";
|
||||||
import { ResizeObserver } from "@juggle/resize-observer";
|
import { ResizeObserver } from "@juggle/resize-observer";
|
||||||
import moveArrItem from "lodash-move";
|
|
||||||
import styles from "./VideoGrid.module.css";
|
import styles from "./VideoGrid.module.css";
|
||||||
|
|
||||||
export function useVideoGridLayout(hasScreenshareFeeds) {
|
export function useVideoGridLayout(hasScreenshareFeeds) {
|
||||||
|
@ -604,38 +603,43 @@ function getSubGridPositions(
|
||||||
return newTilePositions;
|
return newTilePositions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortTiles(layout, tiles) {
|
function reorderTiles(tiles, layout) {
|
||||||
const is1on1Freedom = layout === "freedom" && tiles.length === 2;
|
if (layout === "freedom" && tiles.length === 2) {
|
||||||
|
// 1:1 layout
|
||||||
|
tiles.forEach((tile) => (tile.order = tile.item.isLocal ? 0 : 1));
|
||||||
|
} else {
|
||||||
|
const focusedTiles = [];
|
||||||
|
const presenterTiles = [];
|
||||||
|
const otherTiles = [];
|
||||||
|
|
||||||
tiles.sort((a, b) => {
|
const orderedTiles = new Array(tiles.length);
|
||||||
if (is1on1Freedom && a.item.isLocal !== b.item.isLocal) {
|
tiles.forEach((tile) => (orderedTiles[tile.order] = tile));
|
||||||
return (b.item.isLocal ? 1 : 0) - (a.item.isLocal ? 1 : 0);
|
|
||||||
} else if (a.focused !== b.focused) {
|
orderedTiles.forEach((tile) =>
|
||||||
return (b.focused ? 1 : 0) - (a.focused ? 1 : 0);
|
(tile.focused
|
||||||
} else if (a.presenter !== b.presenter) {
|
? focusedTiles
|
||||||
return (b.presenter ? 1 : 0) - (a.presenter ? 1 : 0);
|
: tile.presenter
|
||||||
|
? presenterTiles
|
||||||
|
: otherTiles
|
||||||
|
).push(tile)
|
||||||
|
);
|
||||||
|
|
||||||
|
[...focusedTiles, ...presenterTiles, ...otherTiles].forEach(
|
||||||
|
(tile, i) => (tile.order = i)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
export function VideoGrid({ items, layout, disableAnimations, children }) {
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function VideoGrid({
|
|
||||||
items,
|
|
||||||
layout,
|
|
||||||
onFocusTile,
|
|
||||||
disableAnimations,
|
|
||||||
children,
|
|
||||||
}) {
|
|
||||||
// Place the PiP in the bottom right corner by default
|
// Place the PiP in the bottom right corner by default
|
||||||
const [pipXRatio, setPipXRatio] = useState(1);
|
const [pipXRatio, setPipXRatio] = useState(1);
|
||||||
const [pipYRatio, setPipYRatio] = useState(1);
|
const [pipYRatio, setPipYRatio] = useState(1);
|
||||||
|
|
||||||
const [{ tiles, tilePositions, scrollPosition }, setTileState] = useState({
|
const [{ tiles, tilePositions }, setTileState] = useState({
|
||||||
tiles: [],
|
tiles: [],
|
||||||
tilePositions: [],
|
tilePositions: [],
|
||||||
scrollPosition: 0,
|
|
||||||
});
|
});
|
||||||
|
const [scrollPosition, setScrollPosition] = useState(0);
|
||||||
const draggingTileRef = useRef(null);
|
const draggingTileRef = useRef(null);
|
||||||
const lastTappedRef = useRef({});
|
const lastTappedRef = useRef({});
|
||||||
const lastLayoutRef = useRef(layout);
|
const lastLayoutRef = useRef(layout);
|
||||||
|
@ -646,7 +650,7 @@ export function VideoGrid({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTileState(({ tiles, ...rest }) => {
|
setTileState(({ tiles, ...rest }) => {
|
||||||
const newTiles = [];
|
const newTiles = [];
|
||||||
const removedTileKeys = [];
|
const removedTileKeys = new Set();
|
||||||
|
|
||||||
for (const tile of tiles) {
|
for (const tile of tiles) {
|
||||||
let item = items.find((item) => item.id === tile.key);
|
let item = items.find((item) => item.id === tile.key);
|
||||||
|
@ -656,7 +660,7 @@ export function VideoGrid({
|
||||||
if (!item) {
|
if (!item) {
|
||||||
remove = true;
|
remove = true;
|
||||||
item = tile.item;
|
item = tile.item;
|
||||||
removedTileKeys.push(tile.key);
|
removedTileKeys.add(tile.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
let focused;
|
let focused;
|
||||||
|
@ -671,6 +675,7 @@ export function VideoGrid({
|
||||||
|
|
||||||
newTiles.push({
|
newTiles.push({
|
||||||
key: item.id,
|
key: item.id,
|
||||||
|
order: tile.order,
|
||||||
item,
|
item,
|
||||||
remove,
|
remove,
|
||||||
focused,
|
focused,
|
||||||
|
@ -691,6 +696,7 @@ export function VideoGrid({
|
||||||
|
|
||||||
const newTile = {
|
const newTile = {
|
||||||
key: item.id,
|
key: item.id,
|
||||||
|
order: existingTile?.order ?? newTiles.length,
|
||||||
item,
|
item,
|
||||||
remove: false,
|
remove: false,
|
||||||
focused: layout === "spotlight" && item.focused,
|
focused: layout === "spotlight" && item.focused,
|
||||||
|
@ -706,22 +712,19 @@ export function VideoGrid({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sortTiles(layout, newTiles);
|
reorderTiles(newTiles, layout);
|
||||||
|
|
||||||
if (removedTileKeys.length > 0) {
|
if (removedTileKeys.size > 0) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!isMounted.current) {
|
if (!isMounted.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTileState(({ tiles, ...rest }) => {
|
setTileState(({ tiles, ...rest }) => {
|
||||||
const newTiles = tiles.filter(
|
const newTiles = tiles
|
||||||
(tile) => !removedTileKeys.includes(tile.key)
|
.filter((tile) => !removedTileKeys.has(tile.key))
|
||||||
);
|
.map((tile) => ({ ...tile })); // clone before reordering
|
||||||
|
reorderTiles(newTiles, layout);
|
||||||
// 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 presenterTileCount = newTiles.reduce(
|
const presenterTileCount = newTiles.reduce(
|
||||||
(count, tile) => count + (tile.focused ? 1 : 0),
|
(count, tile) => count + (tile.focused ? 1 : 0),
|
||||||
|
@ -771,7 +774,7 @@ export function VideoGrid({
|
||||||
const animate = useCallback(
|
const animate = useCallback(
|
||||||
(tiles) => (tileIndex) => {
|
(tiles) => (tileIndex) => {
|
||||||
const tile = tiles[tileIndex];
|
const tile = tiles[tileIndex];
|
||||||
const tilePosition = tilePositions[tileIndex];
|
const tilePosition = tilePositions[tile.order];
|
||||||
const draggingTile = draggingTileRef.current;
|
const draggingTile = draggingTileRef.current;
|
||||||
const dragging = draggingTile && tile.key === draggingTile.key;
|
const dragging = draggingTile && tile.key === draggingTile.key;
|
||||||
const remove = tile.remove;
|
const remove = tile.remove;
|
||||||
|
@ -830,6 +833,9 @@ export function VideoGrid({
|
||||||
reset: false,
|
reset: false,
|
||||||
immediate: (key) =>
|
immediate: (key) =>
|
||||||
disableAnimations || key === "zIndex" || key === "shadow",
|
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 +860,25 @@ export function VideoGrid({
|
||||||
lastTappedRef.current[tileKey] = 0;
|
lastTappedRef.current[tileKey] = 0;
|
||||||
|
|
||||||
const tile = tiles.find((tile) => tile.key === tileKey);
|
const tile = tiles.find((tile) => tile.key === tileKey);
|
||||||
|
if (!tile || layout !== "freedom") return;
|
||||||
if (!tile) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const item = tile.item;
|
const item = tile.item;
|
||||||
|
|
||||||
setTileState((state) => {
|
setTileState(({ tiles, ...state }) => {
|
||||||
let presenterTileCount = 0;
|
let presenterTileCount = 0;
|
||||||
|
const newTiles = tiles.map((tile) => {
|
||||||
let newTiles;
|
let newTile = { ...tile }; // clone before reordering
|
||||||
|
|
||||||
if (onFocusTile) {
|
|
||||||
newTiles = onFocusTile(state.tiles, tile);
|
|
||||||
|
|
||||||
for (const tile of newTiles) {
|
|
||||||
if (tile.focused) {
|
|
||||||
presenterTileCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
newTiles = state.tiles.map((tile) => {
|
|
||||||
let newTile = tile;
|
|
||||||
|
|
||||||
if (tile.item === item) {
|
if (tile.item === item) {
|
||||||
newTile = { ...tile, focused: !tile.focused };
|
newTile.focused = !tile.focused;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newTile.focused) {
|
if (newTile.focused) {
|
||||||
presenterTileCount++;
|
presenterTileCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return newTile;
|
return newTile;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
sortTiles(layout, newTiles);
|
reorderTiles(newTiles, layout);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
@ -907,7 +895,7 @@ export function VideoGrid({
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[tiles, gridBounds, onFocusTile, layout]
|
[tiles, gridBounds, layout]
|
||||||
);
|
);
|
||||||
|
|
||||||
const bindTile = useDrag(
|
const bindTile = useDrag(
|
||||||
|
@ -923,17 +911,18 @@ export function VideoGrid({
|
||||||
|
|
||||||
const dragTileIndex = tiles.findIndex((tile) => tile.key === key);
|
const dragTileIndex = tiles.findIndex((tile) => tile.key === key);
|
||||||
const dragTile = tiles[dragTileIndex];
|
const dragTile = tiles[dragTileIndex];
|
||||||
const dragTilePosition = tilePositions[dragTileIndex];
|
const dragTilePosition = tilePositions[dragTile.order];
|
||||||
|
|
||||||
let newTiles = tiles;
|
|
||||||
|
|
||||||
const cursorPosition = [xy[0] - gridBounds.left, xy[1] - gridBounds.top];
|
const cursorPosition = [xy[0] - gridBounds.left, xy[1] - gridBounds.top];
|
||||||
|
|
||||||
if (layout === "freedom" && tiles.length === 2) {
|
let newTiles = tiles;
|
||||||
// We're in 1:1 mode, so only the local tile should be draggable
|
|
||||||
if (dragTileIndex !== 0) return;
|
|
||||||
|
|
||||||
// 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) {
|
if (last) {
|
||||||
const remotePosition = tilePositions[1];
|
const remotePosition = tilePositions[1];
|
||||||
|
|
||||||
|
@ -959,37 +948,40 @@ export function VideoGrid({
|
||||||
setPipXRatio(Math.max(0, Math.min(1, newPipXRatio)));
|
setPipXRatio(Math.max(0, Math.min(1, newPipXRatio)));
|
||||||
setPipYRatio(Math.max(0, Math.min(1, newPipYRatio)));
|
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);
|
|
||||||
|
|
||||||
newTiles = newTiles.map((tile) => {
|
|
||||||
if (tile === hoverTile) {
|
|
||||||
return { ...tile, focused: dragTile.focused };
|
|
||||||
} else if (tile === dragTile) {
|
|
||||||
return { ...tile, focused: hoverTile.focused };
|
|
||||||
} else {
|
} else {
|
||||||
return tile;
|
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) => {
|
||||||
|
let order = tile.order;
|
||||||
|
if (order < dragTile.order) {
|
||||||
|
if (order >= hoverTile.order) order++;
|
||||||
|
} else if (order > dragTile.order) {
|
||||||
|
if (order <= hoverTile.order) order--;
|
||||||
|
} else {
|
||||||
|
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, layout);
|
||||||
|
|
||||||
setTileState((state) => ({ ...state, tiles: newTiles }));
|
setTileState((state) => ({ ...state, tiles: newTiles }));
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1037,13 +1029,9 @@ export function VideoGrid({
|
||||||
: gridBounds.height - lastTile.y - lastTile.height - GAP;
|
: gridBounds.height - lastTile.y - lastTile.height - GAP;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTileState((state) => ({
|
setScrollPosition((scrollPosition) =>
|
||||||
...state,
|
Math.min(Math.max(movement + scrollPosition, min), 0)
|
||||||
scrollPosition: Math.min(
|
);
|
||||||
Math.max(movement + state.scrollPosition, min),
|
|
||||||
0
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
[layout, gridBounds, tilePositions]
|
[layout, gridBounds, tilePositions]
|
||||||
);
|
);
|
||||||
|
@ -1060,7 +1048,7 @@ export function VideoGrid({
|
||||||
<div className={styles.videoGrid} ref={gridRef} {...bindGrid()}>
|
<div className={styles.videoGrid} ref={gridRef} {...bindGrid()}>
|
||||||
{springs.map(({ shadow, ...style }, i) => {
|
{springs.map(({ shadow, ...style }, i) => {
|
||||||
const tile = tiles[i];
|
const tile = tiles[i];
|
||||||
const tilePosition = tilePositions[i];
|
const tilePosition = tilePositions[tile.order];
|
||||||
|
|
||||||
return children({
|
return children({
|
||||||
...bindTile(tile.key),
|
...bindTile(tile.key),
|
||||||
|
|
|
@ -8461,13 +8461,6 @@ locate-path@^6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-locate "^5.0.0"
|
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:
|
lodash.curry@^4.0.1:
|
||||||
version "4.1.1"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.curry/-/lodash.curry-4.1.1.tgz#248e36072ede906501d75966200a86dab8b23170"
|
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"
|
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||||
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
|
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"
|
version "4.17.21"
|
||||||
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
|
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
|
|
Loading…
Reference in a new issue