diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index e356c40..86557ef 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -32,10 +32,14 @@ jobs: with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@dc7b9719a96d48369863986a06765841d7ea23f6 + - name: Build and push Docker image uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc with: context: . + platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/Dockerfile b/Dockerfile index e31506c..eb7e7a5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:16-buster as builder +FROM --platform=$BUILDPLATFORM node:16-buster as builder WORKDIR /src diff --git a/package.json b/package.json index e50037f..4f0309e 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "build-storybook": "build-storybook", "prettier:check": "prettier -c src", "prettier:format": "prettier -w src", + "lint": "yarn lint:types && yarn lint:js", "lint:js": "eslint --max-warnings 0 src", "lint:types": "tsc" }, @@ -36,7 +37,7 @@ "classnames": "^2.3.1", "color-hash": "^2.0.1", "events": "^3.3.0", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#aa0d3bd1f5a006d151f826e6b8c5f286abb6e960", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#404f8e130e44a78b0159c55902df1b129b3816d1", "mermaid": "^8.13.8", "normalize.css": "^8.0.1", "pako": "^2.0.4", diff --git a/src/Header.jsx b/src/Header.jsx index 85039de..d4f83e5 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..7f94581 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: 1px; + width: 16px; + height: 16px; + mask-image: url("./icons/AlertTriangleFilled.svg"); + mask-repeat: no-repeat; + mask-size: contain; + background-color: var(--alert); + padding-right: 5px; +} + @media (min-width: 800px) { .headerLogo, .roomAvatar, diff --git a/src/IncompatibleVersionModal.tsx b/src/IncompatibleVersionModal.tsx new file mode 100644 index 0000000..6cc5615 --- /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..53e3b2d 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; + color: var(--accent); + cursor: pointer; +} diff --git a/src/home/CallTypeDropdown.tsx b/src/home/CallTypeDropdown.tsx index b84dd77..454813b 100644 --- a/src/home/CallTypeDropdown.tsx +++ b/src/home/CallTypeDropdown.tsx @@ -43,7 +43,7 @@ export const CallTypeDropdown: FC = ({ callType, setCallType }) => { {(props) => ( @@ -55,9 +55,9 @@ export const CallTypeDropdown: FC = ({ callType, setCallType }) => { )} - + - Radio call + Walkie-talkie call {callType === CallType.Radio && ( )} diff --git a/src/home/RegisteredView.jsx b/src/home/RegisteredView.jsx index aaa8a33..1d07d1c 100644 --- a/src/home/RegisteredView.jsx +++ b/src/home/RegisteredView.jsx @@ -80,7 +80,7 @@ export function RegisteredView({ client }) { }, [history, existingRoomId]); const callNameLabel = - callType === CallType.Video ? "Video call name" : "Radio call name"; + callType === CallType.Video ? "Video call name" : "Walkie-talkie call name"; return ( <> diff --git a/src/home/UnauthenticatedView.jsx b/src/home/UnauthenticatedView.jsx index 6af34c5..f324d50 100644 --- a/src/home/UnauthenticatedView.jsx +++ b/src/home/UnauthenticatedView.jsx @@ -104,7 +104,7 @@ export function UnauthenticatedView() { ); const callNameLabel = - callType === CallType.Video ? "Video call name" : "Radio call name"; + callType === CallType.Video ? "Video call name" : "Walkie-talkie call name"; return ( <> diff --git a/src/icons/AlertTriangleFilled.svg b/src/icons/AlertTriangleFilled.svg new file mode 100644 index 0000000..cbdd429 --- /dev/null +++ b/src/icons/AlertTriangleFilled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/input/Input.jsx b/src/input/Input.jsx index 65d6ce3..820956b 100644 --- a/src/input/Input.jsx +++ b/src/input/Input.jsx @@ -39,7 +39,18 @@ export function Field({ children, className, ...rest }) { export const InputField = forwardRef( ( - { id, label, className, type, checked, prefix, suffix, disabled, ...rest }, + { + id, + label, + className, + type, + checked, + prefix, + suffix, + description, + disabled, + ...rest + }, ref ) => { return ( @@ -82,6 +93,7 @@ export const InputField = forwardRef( {label} {suffix && {suffix}} + {description &&

    {description}

    } ); } diff --git a/src/input/Input.module.css b/src/input/Input.module.css index 865a895..c04a85a 100644 --- a/src/input/Input.module.css +++ b/src/input/Input.module.css @@ -118,13 +118,15 @@ .checkboxField { display: flex; align-items: flex-start; + flex-wrap: wrap; } .checkboxField label { display: flex; align-items: center; flex-grow: 1; - font-size: 13px; + font-size: 15px; + line-height: 24px; } .checkboxField input { @@ -176,3 +178,9 @@ color: var(--alert); font-weight: 600; } + +.description { + color: var(--secondary-content); + margin-left: 26px; + width: 100%; /* Ensure that it breaks onto the next row */ +} diff --git a/src/room/AudioPreview.jsx b/src/room/AudioPreview.jsx index 32dfd13..1472695 100644 --- a/src/room/AudioPreview.jsx +++ b/src/room/AudioPreview.jsx @@ -33,7 +33,7 @@ export function AudioPreview({ }) { return ( <> -

    {`${roomName} - Radio Call`}

    +

    {`${roomName} - Walkie-talkie call`}

    {state === GroupCallState.LocalCallFeedUninitialized && ( 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, }; } diff --git a/src/settings/SettingsModal.jsx b/src/settings/SettingsModal.jsx index ad2aeef..919b955 100644 --- a/src/settings/SettingsModal.jsx +++ b/src/settings/SettingsModal.jsx @@ -87,9 +87,10 @@ export const SettingsModal = (props) => { setSpatialAudio(e.target.checked)} /> diff --git a/yarn.lock b/yarn.lock index c95df5b..5d3ab8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8597,9 +8597,9 @@ matrix-events-sdk@^0.0.1-beta.7: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.7.tgz#5ffe45eba1f67cc8d7c2377736c728b322524934" integrity sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#aa0d3bd1f5a006d151f826e6b8c5f286abb6e960": +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#404f8e130e44a78b0159c55902df1b129b3816d1": version "17.2.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/aa0d3bd1f5a006d151f826e6b8c5f286abb6e960" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/404f8e130e44a78b0159c55902df1b129b3816d1" dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0"