diff --git a/src/Header.jsx b/src/Header.jsx index 85039de..882dac3 100644 --- a/src/Header.jsx +++ b/src/Header.jsx @@ -1,5 +1,5 @@ import classNames from "classnames"; -import React, { useRef } from "react"; +import React, { useCallback, useRef } from "react"; import { Link } from "react-router-dom"; import styles from "./Header.module.css"; import { ReactComponent as Logo } from "./icons/Logo.svg"; @@ -8,6 +8,9 @@ import { ReactComponent as ArrowLeftIcon } from "./icons/ArrowLeft.svg"; import { useButton } from "@react-aria/button"; import { Subtitle } from "./typography/Typography"; import { Avatar } from "./Avatar"; +import { IncompatibleversionModal } from "./IncompatibleversionModal"; +import { useModalTriggerState } from "./Modal"; +import { Button } from "./button"; export function Header({ children, className, ...rest }) { return ( @@ -84,3 +87,25 @@ export function RoomSetupHeaderInfo({ roomName, avatarUrl, ...rest }) { ); } + +export function VersionMismatchWarning({ users, room }) { + const { modalState, modalProps } = useModalTriggerState(); + + const onDetailsClick = useCallback(() => { + modalState.open(); + }, [modalState]); + + if (users.size === 0) return null; + + return ( + + Incomaptible versions! + + {modalState.isOpen && ( + + )} + + ); +} diff --git a/src/Header.module.css b/src/Header.module.css index 04313d9..cab276a 100644 --- a/src/Header.module.css +++ b/src/Header.module.css @@ -104,6 +104,24 @@ flex-shrink: 0; } +.versionMismatchWarning { + padding-left: 15px; +} + +.versionMismatchWarning::before { + content: ""; + display: inline-block; + position: relative; + top: 2px; + width: 16px; + height: 16px; + mask-image: url("./icons/AlertTriangleFilled.svg"); + mask-repeat: no-repeat; + mask-size: contain; + background-color: var(--warning); + padding-right: 3px; +} + @media (min-width: 800px) { .headerLogo, .roomAvatar, diff --git a/src/IncompatibleVersionModal.tsx b/src/IncompatibleVersionModal.tsx new file mode 100644 index 0000000..5f8c9a8 --- /dev/null +++ b/src/IncompatibleVersionModal.tsx @@ -0,0 +1,48 @@ +/* +Copyright 2022 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. +*/ + +import { Room } from "matrix-js-sdk"; +import React from "react"; + +import { Modal, ModalContent } from "./Modal"; +import { Body } from "./typography/Typography"; + +interface Props { + userIds: Set; + room: Room; +} + +export const IncompatibleversionModal: React.FC = ({ + userIds, + room, + ...rest +}) => { + const userLis = Array.from(userIds).map((u) => ( +
  • {room.getMember(u).name}
  • + )); + + return ( + + + + Other users are trying to join this call from incompatible versions. + These users should ensure that they have refreshed their browsers: +
      {userLis}
    + +
    +
    + ); +}; diff --git a/src/button/Button.jsx b/src/button/Button.jsx index 1244caf..aabc309 100644 --- a/src/button/Button.jsx +++ b/src/button/Button.jsx @@ -41,6 +41,7 @@ export const variantToClassName = { iconCopy: [styles.iconCopyButton], secondaryHangup: [styles.secondaryHangup], dropdown: [styles.dropdownButton], + link: [styles.linkButton], }; export const sizeToClassName = { diff --git a/src/button/Button.module.css b/src/button/Button.module.css index ad5c7e3..5be346f 100644 --- a/src/button/Button.module.css +++ b/src/button/Button.module.css @@ -207,3 +207,10 @@ limitations under the License. .lg { height: 40px; } + +.linkButton { + background-color: transparent; + border: none; + text-decoration: underline; + cursor: pointer; +} diff --git a/src/icons/AlertTriangleFilled.svg b/src/icons/AlertTriangleFilled.svg new file mode 100644 index 0000000..59a5d51 --- /dev/null +++ b/src/icons/AlertTriangleFilled.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/index.css b/src/index.css index da77b1c..d5d5fcb 100644 --- a/src/index.css +++ b/src/index.css @@ -29,6 +29,7 @@ limitations under the License. --accent-20: #0dbd8b33; --alert: #ff5b55; --alert-20: #ff5b5533; + --warning: #e8bf37; --links: #0086e6; --primary-content: #ffffff; --secondary-content: #a9b2bc; diff --git a/src/room/GroupCallView.jsx b/src/room/GroupCallView.jsx index 2809b82..4b84926 100644 --- a/src/room/GroupCallView.jsx +++ b/src/room/GroupCallView.jsx @@ -53,6 +53,7 @@ export function GroupCallView({ screenshareFeeds, hasLocalParticipant, participants, + unencryptedEventsFromUsers, } = useGroupCall(groupCall); const avatarUrl = useRoomAvatar(groupCall.room); @@ -112,6 +113,7 @@ export function GroupCallView({ localScreenshareFeed={localScreenshareFeed} screenshareFeeds={screenshareFeeds} roomId={roomId} + unencryptedEventsFromUsers={unencryptedEventsFromUsers} /> ); } diff --git a/src/room/InCallView.jsx b/src/room/InCallView.jsx index 2c6240e..de9f5bc 100644 --- a/src/room/InCallView.jsx +++ b/src/room/InCallView.jsx @@ -22,7 +22,13 @@ import { VideoButton, ScreenshareButton, } from "../button"; -import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header"; +import { + Header, + LeftNav, + RightNav, + RoomHeaderInfo, + VersionMismatchWarning, +} from "../Header"; import { VideoGrid, useVideoGridLayout } from "../video-grid/VideoGrid"; import { VideoTileContainer } from "../video-grid/VideoTileContainer"; import { GroupCallInspector } from "./GroupCallInspector"; @@ -59,6 +65,7 @@ export function InCallView({ isScreensharing, screenshareFeeds, roomId, + unencryptedEventsFromUsers, }) { usePreventScroll(); const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0); @@ -135,6 +142,10 @@ export function InCallView({
    + diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts index 6816bcf..b3697cc 100644 --- a/src/room/useGroupCall.ts +++ b/src/room/useGroupCall.ts @@ -14,11 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useReducer, useState } from "react"; import { GroupCallEvent, GroupCallState, GroupCall, + GroupCallErrorCode, + GroupCallUnknownDeviceError, + GroupCallError, } from "matrix-js-sdk/src/webrtc/groupCall"; import { MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed"; @@ -48,6 +51,7 @@ export interface UseGroupCallType { localDesktopCapturerSourceId: string; participants: RoomMember[]; hasLocalParticipant: boolean; + unencryptedEventsFromUsers: Set; } interface State { @@ -106,6 +110,13 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallType { hasLocalParticipant: false, }); + const [unencryptedEventsFromUsers, addUnencryptedEventUser] = useReducer( + (state: Set, newVal: string) => { + return new Set(state).add(newVal); + }, + new Set() + ); + const updateState = (state: Partial) => setState((prevState) => ({ ...prevState, ...state })); @@ -180,6 +191,13 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallType { }); } + function onError(e: GroupCallError): void { + if (e.code === GroupCallErrorCode.UnknownDevice) { + const unknownDeviceError = e as GroupCallUnknownDeviceError; + addUnencryptedEventUser(unknownDeviceError.userId); + } + } + groupCall.on(GroupCallEvent.GroupCallStateChanged, onGroupCallStateChanged); groupCall.on(GroupCallEvent.UserMediaFeedsChanged, onUserMediaFeedsChanged); groupCall.on( @@ -194,6 +212,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallType { ); groupCall.on(GroupCallEvent.CallsChanged, onCallsChanged); groupCall.on(GroupCallEvent.ParticipantsChanged, onParticipantsChanged); + groupCall.on(GroupCallEvent.Error, onError); updateState({ error: null, @@ -242,6 +261,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallType { GroupCallEvent.ParticipantsChanged, onParticipantsChanged ); + groupCall.removeListener(GroupCallEvent.Error, onError); groupCall.leave(); }; }, [groupCall]); @@ -319,5 +339,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallType { localDesktopCapturerSourceId, participants, hasLocalParticipant, + unencryptedEventsFromUsers, }; }