Update to use matrix-react-sdk

This commit is contained in:
Robert Long 2021-09-29 14:34:29 -07:00
parent c4a626b530
commit 5e4736eba5
13 changed files with 2571 additions and 4231 deletions

View File

@ -6,32 +6,8 @@ Testbed for full mesh video chat.
You must first run a local Synapse server on port 8008
Then install the dependencies:
```
cd matrix-video-chat
npm install
```
Locally checkout the [robertlong/full-mesh-voip](https://github.com/matrix-org/matrix-js-sdk/tree/robertlong/full-mesh-voip) branch of the matrix-js-sdk.
```
cd matrix-js-sdk
git checkout robertlong/full-mesh-voip
yarn
yarn build
npm link
yarn dev
```
Link the matrix-js-sdk into the matrix-video-chat project:
```
cd matrix-video-chat
npm link matrix-js-sdk
```
Finally run the development server
```
npm run dev
```

3064
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,19 +11,18 @@
"color-hash": "^2.0.1",
"events": "^3.3.0",
"lodash-move": "^1.1.1",
"matrix-js-sdk": "file:../matrix-js-sdk",
"matrix-react-sdk": "file:../matrix-react-sdk",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#robertlong/group-call",
"matrix-react-sdk": "github:matrix-org/matrix-react-sdk#robertlong/group-call",
"re-resizable": "^6.9.0",
"react": "^17.0.0",
"react-dom": "^17.0.0",
"react-router-dom": "^5.2.0",
"react-use-gesture": "^9.1.3",
"react-use-measure": "^2.0.4",
"sass": "^1.42.1",
"vite-plugin-svgr": "^0.4.0"
"react-use-measure": "^2.0.4"
},
"devDependencies": {
"@vitejs/plugin-react-refresh": "^1.3.1",
"vite-plugin-svgr": "^0.4.0",
"sass": "^1.42.1",
"vite": "^2.4.2"
}
}

View File

@ -25,7 +25,6 @@ import {
import { useClient } from "./ConferenceCallManagerHooks";
import { Home } from "./Home";
import { Room } from "./Room";
import { GridDemo } from "./GridDemo";
import { RegisterPage } from "./RegisterPage";
import { LoginPage } from "./LoginPage";
import { Center } from "./Layout";
@ -70,9 +69,6 @@ export default function App() {
<GuestAuthPage onRegisterGuest={registerGuest} />
)}
</Route>
<Route exact path="/grid">
<GridDemo />
</Route>
</Switch>
)}
</>

View File

@ -14,25 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { useCallback, useEffect, useRef, useState } from "react";
import matrix from "matrix-js-sdk/src/index";
import { ConferenceCallDebugger } from "./ConferenceCallDebugger";
// https://stackoverflow.com/a/9039885
function isIOS() {
return (
[
"iPad Simulator",
"iPhone Simulator",
"iPod Simulator",
"iPad",
"iPhone",
"iPod",
].includes(navigator.platform) ||
// iPad on iOS 13 detection
(navigator.userAgent.includes("Mac") && "ontouchend" in document)
);
}
import { useCallback, useEffect, useState } from "react";
import matrix from "matrix-js-sdk/src/browser-index";
function waitForSync(client) {
return new Promise((resolve, reject) => {
@ -245,210 +228,6 @@ export function useClient(homeserverUrl) {
};
}
function usePageUnload(callback) {
useEffect(() => {
let pageVisibilityTimeout;
function onBeforeUnload(event) {
if (event.type === "visibilitychange") {
if (document.visibilityState === "visible") {
clearTimeout(pageVisibilityTimeout);
} else {
// Wait 5 seconds before closing the page to avoid accidentally leaving
// TODO: Make this configurable?
pageVisibilityTimeout = setTimeout(() => {
callback();
}, 5000);
}
} else {
callback();
}
}
// iOS doesn't fire beforeunload event, so leave the call when you hide the page.
if (isIOS()) {
window.addEventListener("pagehide", onBeforeUnload);
document.addEventListener("visibilitychange", onBeforeUnload);
}
window.addEventListener("beforeunload", onBeforeUnload);
return () => {
window.removeEventListener("pagehide", onBeforeUnload);
document.removeEventListener("visibilitychange", onBeforeUnload);
window.removeEventListener("beforeunload", onBeforeUnload);
clearTimeout(pageVisibilityTimeout);
};
}, []);
}
function getParticipants(groupCall) {
return [...groupCall.participants];
}
export function useGroupCall(client, roomId, debug = false) {
const groupCallRef = useRef(null);
const [
{
loading,
entered,
entering,
room,
participants,
error,
microphoneMuted,
localVideoMuted,
callDebugger,
},
setState,
] = useState({
loading: true,
entered: false,
entering: false,
room: null,
participants: [],
error: null,
microphoneMuted: false,
localVideoMuted: false,
callDebugger: null,
});
const updateState = (state) =>
setState((prevState) => ({ ...prevState, ...state }));
useEffect(() => {
function onParticipantsChanged(...args) {
updateState({ participants: getParticipants(groupCallRef.current) });
}
function onLocalMuteStateChanged(microphoneMuted, localVideoMuted) {
updateState({
microphoneMuted,
localVideoMuted,
});
}
async function init() {
const room = await fetchRoom(client, roomId, true);
const groupCall = client.createGroupCall(roomId, "video");
groupCallRef.current = groupCall;
groupCall.on("active_speaker_changed", onParticipantsChanged);
groupCall.on("participants_changed", onParticipantsChanged);
groupCall.on("speaking", onParticipantsChanged);
groupCall.on("mute_state_changed", onParticipantsChanged);
groupCall.on("call_replaced", onParticipantsChanged);
groupCall.on("call_feeds_changed", onParticipantsChanged);
groupCall.on("local_mute_state_changed", onLocalMuteStateChanged);
updateState({
room,
loading: false,
callDebugger: debug
? new ConferenceCallDebugger(client, groupCall)
: null,
});
}
init().catch((error) => {
if (groupCallRef.current) {
const groupCall = groupCallRef.current;
groupCall.removeListener(
"active_speaker_changed",
onParticipantsChanged
);
groupCall.removeListener("participants_changed", onParticipantsChanged);
groupCall.removeListener("speaking", onParticipantsChanged);
groupCall.removeListener("mute_state_changed", onParticipantsChanged);
groupCall.removeListener("call_replaced", onParticipantsChanged);
groupCall.removeListener("call_feeds_changed", onParticipantsChanged);
groupCall.removeListener(
"local_mute_state_changed",
onLocalMuteStateChanged
);
groupCall.leave();
}
updateState({ error, loading: false });
});
return () => {
if (groupCallRef.current) {
groupCallRef.current.leave();
}
};
}, [client, roomId]);
usePageUnload(() => {
if (groupCallRef.current) {
groupCallRef.current.leave();
}
});
const initLocalParticipant = useCallback(
() => groupCallRef.current.initLocalParticipant(),
[]
);
const enter = useCallback(() => {
updateState({ entering: true });
groupCallRef.current
.enter()
.then(() => {
updateState({
entered: true,
entering: false,
participants: getParticipants(groupCallRef.current),
});
})
.catch((error) => {
updateState({ error, entering: false });
});
}, []);
const leave = useCallback(() => {
groupCallRef.current.leave();
updateState({
entered: false,
participants: [],
microphoneMuted: false,
localVideoMuted: false,
});
}, []);
const toggleLocalVideoMuted = useCallback(() => {
groupCallRef.current.setLocalVideoMuted(
!groupCallRef.current.isLocalVideoMuted()
);
}, []);
const toggleMicrophoneMuted = useCallback(() => {
groupCallRef.current.setMicrophoneMuted(
!groupCallRef.current.isMicrophoneMuted()
);
}, []);
return {
loading,
entered,
entering,
roomName: room ? room.name : null,
participants,
groupCall: groupCallRef.current,
callDebugger: callDebugger,
microphoneMuted,
localVideoMuted,
error,
initLocalParticipant,
enter,
leave,
toggleLocalVideoMuted,
toggleMicrophoneMuted,
};
}
const tsCache = {};
function getLastTs(client, r) {

View File

@ -1,49 +0,0 @@
import React, { useCallback, useRef, useState } from "react";
import styles from "./GridDemo.module.css";
import { VideoGrid } from "./VideoGrid";
export function GridDemo() {
const participantKey = useRef(0);
const [stream, setStream] = useState();
const [participants, setParticipants] = useState([]);
const startWebcam = useCallback(async () => {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
setStream(stream);
setParticipants([{ stream, userId: participantKey.current++ }]);
}, []);
const addParticipant = useCallback(() => {
setParticipants((participants) => [
...participants,
{ stream: stream.clone(), userId: participantKey.current++ },
]);
}, [stream]);
const removeParticipant = useCallback((key) => {
setParticipants((participants) =>
participants.filter((participant) => participant.userId !== key)
);
}, []);
return (
<div className={styles.gridDemo}>
<div className={styles.buttons}>
{!stream && <button onClick={startWebcam}>Start Webcam</button>}
{stream && participants.length < 12 && (
<button onClick={addParticipant}>Add Tile</button>
)}
{stream && participants.length > 0 && (
<button
onClick={() =>
removeParticipant(participants[participants.length - 1].userId)
}
>
Remove Tile
</button>
)}
</div>
<VideoGrid participants={participants} />
</div>
);
}

View File

@ -1,8 +0,0 @@
.gridDemo {
position: relative;
display: flex;
flex-direction: column;
width: 100vw;
height: 100vh;
overflow: hidden;
}

View File

@ -30,7 +30,9 @@ import {
GroupCallState,
GroupCallType,
} from "matrix-js-sdk/src/webrtc/groupCall";
import VideoGrid from "matrix-react-sdk/src/components/views/voip/GroupCallView/VideoGrid";
import VideoGrid, {
useVideoGridLayout,
} from "matrix-react-sdk/src/components/views/voip/GroupCallView/VideoGrid";
import "matrix-react-sdk/res/css/views/voip/GroupCallView/_VideoGrid.scss";
import { useGroupCall } from "matrix-react-sdk/src/hooks/useGroupCall";
import { useCallFeed } from "matrix-react-sdk/src/hooks/useCallFeed";

View File

@ -1,763 +0,0 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useDrag } from "react-use-gesture";
import { useSprings, animated } from "@react-spring/web";
import classNames from "classnames";
import styles from "./VideoGrid.module.css";
import useMeasure from "react-use-measure";
import moveArrItem from "lodash-move";
import { ReactComponent as MicIcon } from "./icons/Mic.svg";
import { ReactComponent as MuteMicIcon } from "./icons/MuteMic.svg";
import { ReactComponent as DisableVideoIcon } from "./icons/DisableVideo.svg";
function useIsMounted() {
const isMountedRef = useRef(false);
useEffect(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
};
}, []);
return isMountedRef;
}
function isInside([x, y], targetTile) {
const left = targetTile.x;
const top = targetTile.y;
const bottom = targetTile.y + targetTile.height;
const right = targetTile.x + targetTile.width;
if (x < left || x > right || y < top || y > bottom) {
return false;
}
return true;
}
function getTilePositions(
tileCount,
presenterTileCount,
gridWidth,
gridHeight
) {
if (tileCount === 0) {
return [];
}
if (tileCount > 12) {
console.warn("Over 12 tiles is not currently supported");
}
const gap = 8;
const { layoutDirection, participantGridRatio } = getGridLayout(
tileCount,
presenterTileCount,
gridWidth,
gridHeight
);
let participantGridWidth, participantGridHeight;
if (layoutDirection === "vertical") {
participantGridWidth = gridWidth;
participantGridHeight = Math.round(gridHeight * participantGridRatio);
} else {
participantGridWidth = Math.round(gridWidth * participantGridRatio);
participantGridHeight = gridHeight;
}
const participantTileCount = tileCount - presenterTileCount;
const participantGridPositions = getSubGridPositions(
participantTileCount,
participantGridWidth,
participantGridHeight,
gap
);
const participantGridBounds = getSubGridBoundingBox(participantGridPositions);
let presenterGridWidth, presenterGridHeight;
if (presenterTileCount === 0) {
presenterGridWidth = 0;
presenterGridHeight = 0;
} else if (layoutDirection === "vertical") {
presenterGridWidth = gridWidth;
presenterGridHeight =
gridHeight -
(participantGridBounds.height + (participantTileCount ? gap * 2 : 0));
} else {
presenterGridWidth =
gridWidth -
(participantGridBounds.width + (participantTileCount ? gap * 2 : 0));
presenterGridHeight = gridHeight;
}
const presenterGridPositions = getSubGridPositions(
presenterTileCount,
presenterGridWidth,
presenterGridHeight,
gap
);
const tilePositions = [
...presenterGridPositions,
...participantGridPositions,
];
centerTiles(
presenterGridPositions,
presenterGridWidth,
presenterGridHeight,
0,
0
);
if (layoutDirection === "vertical") {
centerTiles(
participantGridPositions,
gridWidth,
gridHeight - presenterGridHeight,
0,
presenterGridHeight
);
} else {
centerTiles(
participantGridPositions,
gridWidth - presenterGridWidth,
gridHeight,
presenterGridWidth,
0
);
}
return tilePositions;
}
function getSubGridBoundingBox(positions) {
let left = 0,
right = 0,
top = 0,
bottom = 0;
for (let i = 0; i < positions.length; i++) {
const { x, y, width, height } = positions[i];
if (i === 0) {
left = x;
right = x + width;
top = y;
bottom = y + height;
} else {
if (x < left) {
left = x;
}
if (y < top) {
top = y;
}
if (x + width > right) {
right = x + width;
}
if (y + height > bottom) {
bottom = y + height;
}
}
}
return {
left,
right,
top,
bottom,
width: right - left,
height: bottom - top,
};
}
function getGridLayout(tileCount, presenterTileCount, gridWidth, gridHeight) {
let layoutDirection = "horizontal";
let participantGridRatio = 1;
if (presenterTileCount === 0) {
return { participantGridRatio, layoutDirection };
}
const gridAspectRatio = gridWidth / gridHeight;
if (gridAspectRatio < 1) {
layoutDirection = "vertical";
participantGridRatio = 1 / 3;
} else {
layoutDirection = "horizontal";
participantGridRatio = 1 / 3;
}
return { participantGridRatio, layoutDirection };
}
function centerTiles(positions, gridWidth, gridHeight, offsetLeft, offsetTop) {
const bounds = getSubGridBoundingBox(positions);
const leftOffset = Math.round((gridWidth - bounds.width) / 2) + offsetLeft;
const topOffset = Math.round((gridHeight - bounds.height) / 2) + offsetTop;
applyTileOffsets(positions, leftOffset, topOffset);
return positions;
}
function applyTileOffsets(positions, leftOffset, topOffset) {
for (const position of positions) {
position.x += leftOffset;
position.y += topOffset;
}
return positions;
}
function getSubGridLayout(tileCount, gridWidth, gridHeight) {
const gridAspectRatio = gridWidth / gridHeight;
let columnCount, rowCount;
let tileAspectRatio = 16 / 9;
if (gridAspectRatio < 3 / 4) {
// Phone
if (tileCount === 1) {
columnCount = 1;
rowCount = 1;
tileAspectRatio = 0;
} else if (tileCount <= 4) {
columnCount = 1;
rowCount = tileCount;
} else if (tileCount <= 12) {
columnCount = 2;
rowCount = Math.ceil(tileCount / columnCount);
tileAspectRatio = 0;
} else {
// Unsupported
columnCount = 3;
rowCount = Math.ceil(tileCount / columnCount);
tileAspectRatio = 1;
}
} else if (gridAspectRatio < 1) {
// Tablet
if (tileCount === 1) {
columnCount = 1;
rowCount = 1;
tileAspectRatio = 0;
} else if (tileCount <= 4) {
columnCount = 1;
rowCount = tileCount;
} else if (tileCount <= 12) {
columnCount = 2;
rowCount = Math.ceil(tileCount / columnCount);
} else {
// Unsupported
columnCount = 3;
rowCount = Math.ceil(tileCount / columnCount);
tileAspectRatio = 1;
}
} else if (gridAspectRatio < 17 / 9) {
// Computer
if (tileCount === 1) {
columnCount = 1;
rowCount = 1;
} else if (tileCount === 2) {
columnCount = 2;
rowCount = 1;
} else if (tileCount <= 4) {
columnCount = 2;
rowCount = 2;
} else if (tileCount <= 6) {
columnCount = 3;
rowCount = 2;
} else if (tileCount <= 8) {
columnCount = 4;
rowCount = 2;
tileAspectRatio = 1;
} else if (tileCount <= 12) {
columnCount = 4;
rowCount = 3;
tileAspectRatio = 1;
} else {
// Unsupported
columnCount = 4;
rowCount = 4;
}
} else if (gridAspectRatio <= 32 / 9) {
// Ultrawide
if (tileCount === 1) {
columnCount = 1;
rowCount = 1;
} else if (tileCount === 2) {
columnCount = 2;
rowCount = 1;
} else if (tileCount <= 4) {
columnCount = 2;
rowCount = 2;
} else if (tileCount <= 6) {
columnCount = 3;
rowCount = 2;
} else if (tileCount <= 8) {
columnCount = 4;
rowCount = 2;
} else if (tileCount <= 12) {
columnCount = 4;
rowCount = 3;
} else {
// Unsupported
columnCount = 4;
rowCount = 4;
}
} else {
// Super Ultrawide
if (tileCount <= 6) {
columnCount = tileCount;
rowCount = 1;
} else {
columnCount = Math.ceil(tileCount / 2);
rowCount = 2;
}
}
return { columnCount, rowCount, tileAspectRatio };
}
function getSubGridPositions(tileCount, gridWidth, gridHeight, gap) {
if (tileCount === 0) {
return [];
}
const { columnCount, rowCount, tileAspectRatio } = getSubGridLayout(
tileCount,
gridWidth,
gridHeight
);
const newTilePositions = [];
const boxWidth = Math.round(
(gridWidth - gap * (columnCount + 1)) / columnCount
);
const boxHeight = Math.round((gridHeight - gap * (rowCount + 1)) / rowCount);
let tileWidth, tileHeight;
if (tileAspectRatio) {
const boxAspectRatio = boxWidth / boxHeight;
if (boxAspectRatio > tileAspectRatio) {
tileWidth = boxHeight * tileAspectRatio;
tileHeight = boxHeight;
} else {
tileWidth = boxWidth;
tileHeight = boxWidth / tileAspectRatio;
}
} else {
tileWidth = boxWidth;
tileHeight = boxHeight;
}
for (let i = 0; i < tileCount; i++) {
const verticalIndex = Math.floor(i / columnCount);
const top = verticalIndex * gap + verticalIndex * tileHeight;
let rowItemCount;
if (verticalIndex + 1 === rowCount && tileCount % columnCount !== 0) {
rowItemCount = tileCount % columnCount;
} else {
rowItemCount = columnCount;
}
const horizontalIndex = i % columnCount;
let centeringPadding = 0;
if (rowItemCount < columnCount) {
const subgridWidth = tileWidth * columnCount + (gap * columnCount - 1);
centeringPadding = Math.round(
(subgridWidth - (tileWidth * rowItemCount + (gap * rowItemCount - 1))) /
2
);
}
const left =
centeringPadding + gap * horizontalIndex + tileWidth * horizontalIndex;
newTilePositions.push({
width: tileWidth,
height: tileHeight,
x: left,
y: top,
});
}
return newTilePositions;
}
export function VideoGrid({ participants, layout }) {
const [{ tiles, tilePositions }, setTileState] = useState({
tiles: [],
tilePositions: [],
});
const draggingTileRef = useRef(null);
const lastTappedRef = useRef({});
const lastLayoutRef = useRef(layout);
const isMounted = useIsMounted();
const [gridRef, gridBounds] = useMeasure();
useEffect(() => {
setTileState(({ tiles }) => {
const newTiles = [];
const removedTileKeys = [];
for (const tile of tiles) {
let participant = participants.find(
(participant) => participant.member.userId === tile.key
);
let remove = false;
if (!participant) {
remove = true;
participant = tile.participant;
removedTileKeys.push(tile.key);
}
let presenter;
if (layout === "spotlight") {
presenter = participant.isActiveSpeaker();
} else {
presenter = layout === lastLayoutRef.current ? tile.presenter : false;
}
newTiles.push({
key: participant.member.userId,
participant,
remove,
presenter,
});
}
for (const participant of participants) {
if (newTiles.some(({ key }) => participant.member.userId === key)) {
continue;
}
// Added tiles
newTiles.push({
key: participant.member.userId,
participant,
remove: false,
presenter: layout === "spotlight" && participant.isActiveSpeaker(),
});
}
newTiles.sort((a, b) => (b.presenter ? 1 : 0) - (a.presenter ? 1 : 0));
if (removedTileKeys.length > 0) {
setTimeout(() => {
if (!isMounted.current) {
return;
}
setTileState(({ tiles }) => {
const newTiles = tiles.filter(
(tile) => !removedTileKeys.includes(tile.key)
);
const presenterTileCount = newTiles.reduce(
(count, tile) => count + (tile.presenter ? 1 : 0),
0
);
return {
tiles: newTiles,
tilePositions: getTilePositions(
newTiles.length,
presenterTileCount,
gridBounds.width,
gridBounds.height
),
};
});
}, 250);
}
const presenterTileCount = newTiles.reduce(
(count, tile) => count + (tile.presenter ? 1 : 0),
0
);
lastLayoutRef.current = layout;
return {
tiles: newTiles,
tilePositions: getTilePositions(
newTiles.length,
presenterTileCount,
gridBounds.width,
gridBounds.height
),
};
});
}, [participants, gridBounds, layout]);
const animate = useCallback(
(tiles) => (tileIndex) => {
const tile = tiles[tileIndex];
const tilePosition = tilePositions[tileIndex];
const draggingTile = draggingTileRef.current;
const dragging = draggingTile && tile.key === draggingTile.key;
const remove = tile.remove;
if (dragging) {
return {
width: tilePosition.width,
height: tilePosition.height,
x: draggingTile.offsetX + draggingTile.x,
y: draggingTile.offsetY + draggingTile.y,
scale: 1.1,
opacity: 1,
zIndex: 1,
shadow: 15,
immediate: (key) => key === "zIndex" || key === "x" || key === "y",
from: {
scale: 0,
opacity: 0,
},
reset: false,
};
} else {
return {
...tilePosition,
scale: remove ? 0 : 1,
opacity: remove ? 0 : 1,
zIndex: 0,
shadow: 1,
from: {
scale: 0,
opacity: 0,
},
reset: false,
immediate: (key) => key === "zIndex",
};
}
},
[tiles, tilePositions]
);
const [springs, api] = useSprings(tiles.length, animate(tiles), [
tilePositions,
tiles,
]);
const onTap = useCallback(
(tileKey) => {
const lastTapped = lastTappedRef.current[tileKey];
if (!lastTapped || Date.now() - lastTapped > 500) {
lastTappedRef.current[tileKey] = Date.now();
return;
}
lastTappedRef.current[tileKey] = 0;
const tile = tiles.find((tile) => tile.key === tileKey);
if (!tile) {
return;
}
const participant = tile.participant;
setTileState((state) => {
let presenterTileCount = 0;
const newTiles = state.tiles.map((tile) => {
let newTile = tile;
if (tile.participant === participant) {
newTile = { ...tile, presenter: !tile.presenter };
}
if (newTile.presenter) {
presenterTileCount++;
}
return newTile;
});
newTiles.sort((a, b) => (b.presenter ? 1 : 0) - (a.presenter ? 1 : 0));
presenterTileCount;
return {
...state,
tiles: newTiles,
tilePositions: getTilePositions(
newTiles.length,
presenterTileCount,
gridBounds.width,
gridBounds.height
),
};
});
},
[tiles, gridBounds]
);
const bind = useDrag(
({ args: [key], active, xy, movement, tap, event }) => {
event.preventDefault();
if (tap) {
onTap(key);
return;
}
const dragTileIndex = tiles.findIndex((tile) => tile.key === key);
const dragTile = tiles[dragTileIndex];
const dragTilePosition = tilePositions[dragTileIndex];
let newTiles = tiles;
const cursorPosition = [xy[0] - gridBounds.left, xy[1] - gridBounds.top];
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, presenter: dragTile.presenter };
} else if (tile === dragTile) {
return { ...tile, presenter: hoverTile.presenter };
} else {
return tile;
}
});
newTiles.sort(
(a, b) => (b.presenter ? 1 : 0) - (a.presenter ? 1 : 0)
);
setTileState((state) => ({ ...state, tiles: newTiles }));
break;
}
}
if (active) {
if (!draggingTileRef.current) {
draggingTileRef.current = {
key: dragTile.key,
offsetX: dragTilePosition.x,
offsetY: dragTilePosition.y,
};
}
draggingTileRef.current.x = movement[0];
draggingTileRef.current.y = movement[1];
} else {
draggingTileRef.current = null;
}
api.start(animate(newTiles));
},
{ filterTaps: true, enabled: layout === "gallery" }
);
return (
<div className={styles.grid} ref={gridRef}>
{springs.map(({ shadow, ...style }, i) => {
const tile = tiles[i];
return (
<ParticipantTile
{...bind(tile.key)}
key={tile.key}
style={{
boxShadow: shadow.to(
(s) => `rgba(0, 0, 0, 0.5) 0px ${s}px ${2 * s}px 0px`
),
...style,
}}
{...tile}
/>
);
})}
</div>
);
}
VideoGrid.defaultProps = {
layout: "gallery",
};
function ParticipantTile({ style, participant, remove, presenter, ...rest }) {
const videoRef = useRef();
useEffect(() => {
if (participant.usermediaStream) {
// Mute the local video
// TODO: Should GroupCallParticipant have a local field?
if (!participant.call) {
videoRef.current.muted = true;
}
videoRef.current.srcObject = participant.usermediaStream;
videoRef.current.play();
} else {
videoRef.current.srcObject = null;
}
}, [participant.usermediaStream]);
// Firefox doesn't respect the disablePictureInPicture attribute
// https://bugzilla.mozilla.org/show_bug.cgi?id=1611831
return (
<animated.div className={styles.participantTile} style={style} {...rest}>
<div
className={classNames(styles.participantName, {
[styles.speaking]: participant.usermediaFeed?.isSpeaking(),
})}
>
{participant.usermediaFeed?.isSpeaking() ? (
<MicIcon />
) : participant.isAudioMuted() ? (
<MuteMicIcon className={styles.muteMicIcon} />
) : null}
<span>{participant.member.rawDisplayName}</span>
</div>
{participant.videoMuted && (
<DisableVideoIcon
className={styles.videoMuted}
width={48}
height={48}
/>
)}
<video ref={videoRef} playsInline disablePictureInPicture />
</animated.div>
);
}

View File

@ -1,86 +0,0 @@
/*
Copyright 2021 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.grid {
position: relative;
overflow: hidden;
flex: 1;
}
.participantTile {
position: absolute;
will-change: transform, width, height, opacity, box-shadow;
border-radius: 24px;
overflow: hidden;
background-color: #444;
}
.participantTile * {
touch-action: none;
-moz-user-select: none;
-webkit-user-drag: none;
user-select: none;
}
.participantTile video {
width: 100%;
height: 100%;
object-fit: cover;
}
.participantName {
position: absolute;
bottom: 16px;
left: 16px;
height: 32px;
padding: 0 8px;
background-color: rgba(23, 25, 28, 0.96);
display: none;
align-items: center;
justify-content: center;
border-radius: 8px;
user-select: none;
cursor: pointer;
}
.participantName > * {
margin-right: 8px;
}
.participantName > :last-child {
margin-right: 0px;
}
.participantName span {
font-size: 15px;
font-weight: 600;
line-height: 32px;
}
.muteMicIcon * {
fill: #FF5B55;
}
.videoMuted {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.participantTile:hover .participantName, .participantName.speaking {
display: flex;
}

17
tsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"module": "commonjs",
"moduleResolution": "node",
"target": "es2016",
"noImplicitAny": false,
"sourceMap": false,
"declaration": true,
"noEmit": true,
"jsx": "react",
"lib": ["es2019", "dom", "dom.iterable"]
}
}

View File

@ -15,12 +15,11 @@ limitations under the License.
*/
import { defineConfig } from "vite";
import reactRefresh from "@vitejs/plugin-react-refresh";
import svgrPlugin from "vite-plugin-svgr";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [reactRefresh(), svgrPlugin()],
plugins: [svgrPlugin()],
server: {
proxy: {
"/_matrix": "http://localhost:8008",

2542
yarn.lock Normal file

File diff suppressed because it is too large Load Diff