diff --git a/res/css/_components.scss b/res/css/_components.scss index 8288cf34f6..8f5fbf341f 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -51,6 +51,7 @@ @import "./views/avatars/_BaseAvatar.scss"; @import "./views/avatars/_DecoratedRoomAvatar.scss"; @import "./views/avatars/_MemberStatusMessageAvatar.scss"; +@import "./views/avatars/_PulsedAatar.scss"; @import "./views/context_menus/_MessageContextMenu.scss"; @import "./views/context_menus/_RoomTileContextMenu.scss"; @import "./views/context_menus/_StatusMessageContextMenu.scss"; @@ -225,6 +226,8 @@ @import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss"; @import "./views/terms/_InlineTermsAgreement.scss"; @import "./views/verification/_VerificationShowSas.scss"; +@import "./views/voip/_CallContainer.scss"; @import "./views/voip/_CallView.scss"; +@import "./views/voip/_CallView2.scss"; @import "./views/voip/_IncomingCallbox.scss"; @import "./views/voip/_VideoView.scss"; diff --git a/res/css/views/avatars/_PulsedAatar.scss b/res/css/views/avatars/_PulsedAatar.scss new file mode 100644 index 0000000000..ce9e3382ab --- /dev/null +++ b/res/css/views/avatars/_PulsedAatar.scss @@ -0,0 +1,30 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +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. +*/ + +.mx_PulsedAvatar { + @keyframes shadow-pulse { + 0% { + box-shadow: 0 0 0 0px rgba($accent-color, 0.2); + } + 100% { + box-shadow: 0 0 0 6px rgba($accent-color, 0); + } + } + + img { + animation: shadow-pulse 1s infinite; + } +} diff --git a/res/css/views/voip/_CallContainer.scss b/res/css/views/voip/_CallContainer.scss new file mode 100644 index 0000000000..e13c851716 --- /dev/null +++ b/res/css/views/voip/_CallContainer.scss @@ -0,0 +1,89 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +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. +*/ + +.mx_CallContainer { + position: absolute; + right: 20px; + bottom: 72px; + border-radius: 8px; + overflow: hidden; + z-index: 100; + box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08); + + cursor: pointer; + + .mx_CallPreview { + .mx_VideoView { + width: 350px; + } + + .mx_VideoView_localVideoFeed { + border-radius: 8px; + overflow: hidden; + } + } + + .mx_IncomingCallBox2 { + min-width: 250px; + background-color: $primary-bg-color; + padding: 8px; + + .mx_IncomingCallBox2_CallerInfo { + display: flex; + direction: row; + + img { + margin: 8px; + } + + > div { + display: flex; + flex-direction: column; + + justify-content: center; + } + + h1, p { + margin: 0px; + padding: 0px; + font-size: $font-14px; + line-height: $font-16px; + } + + h1 { + font-weight: bold; + } + } + + .mx_IncomingCallBox2_buttons { + padding: 8px; + display: flex; + flex-direction: row; + + > .mx_IncomingCallBox2_spacer { + width: 8px; + } + + > * { + flex-shrink: 0; + flex-grow: 1; + margin-right: 0; + font-size: $font-15px; + line-height: $font-24px; + } + } + } +} diff --git a/res/css/views/voip/_CallView2.scss b/res/css/views/voip/_CallView2.scss new file mode 100644 index 0000000000..98d2052261 --- /dev/null +++ b/res/css/views/voip/_CallView2.scss @@ -0,0 +1,94 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. + +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. +*/ + +.mx_CallView2_voice { + background-color: $accent-color; + color: $accent-fg-color; + cursor: pointer; + padding: 6px; + font-weight: bold; + + border-radius: 8px; + min-width: 200px; + + display: flex; + align-items: center; + + img { + margin: 4px; + margin-right: 10px; + } + + > div { + display: flex; + flex-direction: column; + // Hacky vertical align + padding-top: 3px; + } + + > div > p, + > div > h1 { + padding: 0; + margin: 0; + font-size: $font-13px; + line-height: $font-15px; + } + + > div > p { + font-weight: bold; + } + + > * { + flex-grow: 0; + flex-shrink: 0; + } +} + +.mx_CallView2_hangup { + position: absolute; + + right: 8px; + bottom: 10px; + + height: 35px; + width: 35px; + + border-radius: 35px; + + background-color: $notice-primary-color; + + z-index: 101; + + cursor: pointer; + + &::before { + content: ''; + position: absolute; + + height: 20px; + width: 20px; + + top: 6.5px; + left: 7.5px; + + mask: url('$(res)/img/hangup.svg'); + mask-size: contain; + background-size: contain; + + background-color: $primary-fg-color; + } +} diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 9c01480df2..4294a365ab 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -53,6 +53,7 @@ import { } from "../../toasts/ServerLimitToast"; import { Action } from "../../dispatcher/actions"; import LeftPanel2 from "./LeftPanel2"; +import CallContainer from '../views/voip/CallContainer'; // We need to fetch each pinned message individually (if we don't already have it) // so each pinned message may trigger a request. Limit the number per room for sanity. @@ -703,6 +704,7 @@ class LoggedInView extends React.Component { + ); } diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.tsx similarity index 83% rename from src/components/views/avatars/BaseAvatar.js rename to src/components/views/avatars/BaseAvatar.tsx index 508691e5fd..76a430d1c1 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -18,7 +18,6 @@ limitations under the License. */ import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react'; -import PropTypes from 'prop-types'; import * as AvatarLogic from '../../../Avatar'; import SettingsStore from "../../../settings/SettingsStore"; import AccessibleButton from '../elements/AccessibleButton'; @@ -26,9 +25,24 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; import {toPx} from "../../../utils/units"; -const useImageUrl = ({url, urls}) => { - const [imageUrls, setUrls] = useState([]); - const [urlsIndex, setIndex] = useState(); +interface IProps { + name: string; // The name (first initial used as default) + idName?: string; // ID for generating hash colours + title?: string; // onHover title text + url?: string; // highest priority of them all, shortcut to set in urls[0] + urls?: string[]; // [highest_priority, ... , lowest_priority] + width?: number; + height?: number; + // XXX: resizeMethod not actually used. + resizeMethod?: string; + defaultToInitialLetter?: boolean; // true to add default url + onClick?: React.MouseEventHandler; + inputRef?: React.RefObject; +} + +const useImageUrl = ({url, urls}): [string, () => void] => { + const [imageUrls, setUrls] = useState([]); + const [urlsIndex, setIndex] = useState(); const onError = useCallback(() => { setIndex(i => i + 1); // try the next one @@ -70,7 +84,7 @@ const useImageUrl = ({url, urls}) => { return [imageUrl, onError]; }; -const BaseAvatar = (props) => { +const BaseAvatar = (props: IProps) => { const { name, idName, @@ -173,26 +187,5 @@ const BaseAvatar = (props) => { } }; -BaseAvatar.displayName = "BaseAvatar"; - -BaseAvatar.propTypes = { - name: PropTypes.string.isRequired, // The name (first initial used as default) - idName: PropTypes.string, // ID for generating hash colours - title: PropTypes.string, // onHover title text - url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0] - urls: PropTypes.array, // [highest_priority, ... , lowest_priority] - width: PropTypes.number, - height: PropTypes.number, - // XXX resizeMethod not actually used. - resizeMethod: PropTypes.string, - defaultToInitialLetter: PropTypes.bool, // true to add default url - onClick: PropTypes.func, - inputRef: PropTypes.oneOfType([ - // Either a function - PropTypes.func, - // Or the instance of a DOM native element - PropTypes.shape({ current: PropTypes.instanceOf(Element) }), - ]), -}; - export default BaseAvatar; +export type BaseAvatarType = React.FC; \ No newline at end of file diff --git a/src/components/views/avatars/GroupAvatar.js b/src/components/views/avatars/GroupAvatar.tsx similarity index 64% rename from src/components/views/avatars/GroupAvatar.js rename to src/components/views/avatars/GroupAvatar.tsx index 0da57bcb99..ca3800ebca 100644 --- a/src/components/views/avatars/GroupAvatar.js +++ b/src/components/views/avatars/GroupAvatar.tsx @@ -15,43 +15,36 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; -import * as sdk from '../../../index'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; +import BaseAvatar from './BaseAvatar'; -export default createReactClass({ - displayName: 'GroupAvatar', +export interface IProps { + groupId?: string, + groupName?: string, + groupAvatarUrl?: string, + width?: number, + height?: number, + resizeMethod?: string, + onClick?: React.MouseEventHandler, +} - propTypes: { - groupId: PropTypes.string, - groupName: PropTypes.string, - groupAvatarUrl: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number, - resizeMethod: PropTypes.string, - onClick: PropTypes.func, - }, +export default class GroupAvatar extends React.Component { + public static defaultProps = { + width: 36, + height: 36, + resizeMethod: 'crop', + }; - getDefaultProps: function() { - return { - width: 36, - height: 36, - resizeMethod: 'crop', - }; - }, - - getGroupAvatarUrl: function() { + getGroupAvatarUrl() { return MatrixClientPeg.get().mxcUrlToHttp( this.props.groupAvatarUrl, this.props.width, this.props.height, this.props.resizeMethod, ); - }, + } - render: function() { - const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); + render() { // extract the props we use from props so we can pass any others through // should consider adding this as a global rule in js-sdk? /*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/ @@ -65,5 +58,5 @@ export default createReactClass({ {...otherProps} /> ); - }, -}); + } +} diff --git a/src/components/views/avatars/MemberAvatar.js b/src/components/views/avatars/MemberAvatar.tsx similarity index 64% rename from src/components/views/avatars/MemberAvatar.js rename to src/components/views/avatars/MemberAvatar.tsx index b763129dd8..505fbaa691 100644 --- a/src/components/views/avatars/MemberAvatar.js +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -16,48 +16,50 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; -import * as sdk from "../../../index"; import dis from "../../../dispatcher/dispatcher"; import {Action} from "../../../dispatcher/actions"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; +import BaseAvatar from "./BaseAvatar"; -export default createReactClass({ - displayName: 'MemberAvatar', +interface IProps { + // TODO: replace with correct type + member: any; + fallbackUserId: string; + width: number; + height: number; + resizeMethod: string; + // The onClick to give the avatar + onClick: React.MouseEventHandler; + // Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser` + viewUserOnClick: boolean; + title: string; +} - propTypes: { - member: PropTypes.object, - fallbackUserId: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number, - resizeMethod: PropTypes.string, - // The onClick to give the avatar - onClick: PropTypes.func, - // Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser` - viewUserOnClick: PropTypes.bool, - title: PropTypes.string, - }, +interface IState { + name: string; + title: string; + imageUrl?: string; +} - getDefaultProps: function() { - return { - width: 40, - height: 40, - resizeMethod: 'crop', - viewUserOnClick: false, - }; - }, +export default class MemberAvatar extends React.Component { + public static defaultProps = { + width: 40, + height: 40, + resizeMethod: 'crop', + viewUserOnClick: false, + }; - getInitialState: function() { - return this._getState(this.props); - }, + constructor(props: IProps) { + super(props); - // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - UNSAFE_componentWillReceiveProps: function(nextProps) { - this.setState(this._getState(nextProps)); - }, + this.state = MemberAvatar.getState(props) + } - _getState: function(props) { + public static getDerivedStateFromProps(nextProps: IProps): IState { + return MemberAvatar.getState(nextProps); + } + + private static getState(props: IProps): IState { if (props.member && props.member.name) { return { name: props.member.name, @@ -79,11 +81,9 @@ export default createReactClass({ } else { console.error("MemberAvatar called somehow with null member or fallbackUserId"); } - }, - - render: function() { - const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); + } + render() { let {member, fallbackUserId, onClick, viewUserOnClick, ...otherProps} = this.props; const userId = member ? member.userId : fallbackUserId; @@ -100,5 +100,5 @@ export default createReactClass({ ); - }, -}); + } +} diff --git a/src/components/views/avatars/PulsedAvatar.tsx b/src/components/views/avatars/PulsedAvatar.tsx new file mode 100644 index 0000000000..416937c685 --- /dev/null +++ b/src/components/views/avatars/PulsedAvatar.tsx @@ -0,0 +1,28 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +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 React from 'react'; + +interface IProps { +} + +const PulsedAvatar: React.FC = (props) => { + return
+ {props.children} +
+} + +export default PulsedAvatar; \ No newline at end of file diff --git a/src/components/views/avatars/RoomAvatar.js b/src/components/views/avatars/RoomAvatar.tsx similarity index 60% rename from src/components/views/avatars/RoomAvatar.js rename to src/components/views/avatars/RoomAvatar.tsx index a72d318b8d..c5620a9aa0 100644 --- a/src/components/views/avatars/RoomAvatar.js +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -13,90 +13,96 @@ 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 React from "react"; -import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; +import React from 'react'; +import Room from 'matrix-js-sdk/src/models/room'; +import {getHttpUriForMxc} from 'matrix-js-sdk/src/content-repo'; + +import BaseAvatar from './BaseAvatar'; +import ImageView from '../elements/ImageView'; +import {MatrixClientPeg} from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; -import * as sdk from "../../../index"; import * as Avatar from '../../../Avatar'; -import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; - -export default createReactClass({ - displayName: 'RoomAvatar', +interface IProps { // Room may be left unset here, but if it is, // oobData.avatarUrl should be set (else there // would be nowhere to get the avatar from) - propTypes: { - room: PropTypes.object, - oobData: PropTypes.object, - width: PropTypes.number, - height: PropTypes.number, - resizeMethod: PropTypes.string, - viewAvatarOnClick: PropTypes.bool, - }, + room?: Room; + // TODO: type when js-sdk has types + oobData?: any; + width?: number; + height?: number; + resizeMethod?: string; + viewAvatarOnClick?: boolean; +} - getDefaultProps: function() { - return { - width: 36, - height: 36, - resizeMethod: 'crop', - oobData: {}, - }; - }, +interface IState { + urls: string[]; +} - getInitialState: function() { - return { - urls: this.getImageUrls(this.props), - }; - }, +export default class RoomAvatar extends React.Component { + public static defaultProps = { + width: 36, + height: 36, + resizeMethod: 'crop', + oobData: {}, + }; - componentDidMount: function() { + constructor(props: IProps) { + super(props); + + this.state = { + urls: RoomAvatar.getImageUrls(this.props), + } + } + + public componentDidMount() { MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents); - }, + } - componentWillUnmount: function() { + public componentWillUnmount() { const cli = MatrixClientPeg.get(); if (cli) { cli.removeListener("RoomState.events", this.onRoomStateEvents); } - }, + } - // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - UNSAFE_componentWillReceiveProps: function(newProps) { - this.setState({ - urls: this.getImageUrls(newProps), - }); - }, + public static getDerivedStateFromProps(nextProps: IProps): IState { + return { + urls: RoomAvatar.getImageUrls(nextProps), + }; + } - onRoomStateEvents: function(ev) { + // TODO: type when js-sdk has types + private onRoomStateEvents = (ev: any) => { if (!this.props.room || ev.getRoomId() !== this.props.room.roomId || ev.getType() !== 'm.room.avatar' ) return; this.setState({ - urls: this.getImageUrls(this.props), + urls: RoomAvatar.getImageUrls(this.props), }); - }, + } - getImageUrls: function(props) { + private static getImageUrls(props: IProps): string[] { return [ getHttpUriForMxc( MatrixClientPeg.get().getHomeserverUrl(), + // Default props don't play nicely with getDerivedStateFromProps + //props.oobData !== undefined ? props.oobData.avatarUrl : {}, props.oobData.avatarUrl, Math.floor(props.width * window.devicePixelRatio), Math.floor(props.height * window.devicePixelRatio), props.resizeMethod, ), // highest priority - this.getRoomAvatarUrl(props), + RoomAvatar.getRoomAvatarUrl(props), ].filter(function(url) { return (url != null && url != ""); }); - }, + } - getRoomAvatarUrl: function(props) { + private static getRoomAvatarUrl(props: IProps): string { if (!props.room) return null; return Avatar.avatarUrlForRoom( @@ -105,24 +111,21 @@ export default createReactClass({ Math.floor(props.height * window.devicePixelRatio), props.resizeMethod, ); - }, + } - onRoomAvatarClick: function() { + private onRoomAvatarClick = () => { const avatarUrl = this.props.room.getAvatarUrl( MatrixClientPeg.get().getHomeserverUrl(), null, null, null, false); - const ImageView = sdk.getComponent("elements.ImageView"); const params = { src: avatarUrl, name: this.props.room.name, }; Modal.createDialog(ImageView, params, "mx_Dialog_lightbox"); - }, - - render: function() { - const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); + } + public render() { /*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/ const {room, oobData, viewAvatarOnClick, ...otherProps} = this.props; @@ -132,8 +135,8 @@ export default createReactClass({ + onClick={this.props.viewAvatarOnClick && !this.state.urls[0] ? this.onRoomAvatarClick : null} + /> ); - }, -}); + } +} diff --git a/src/components/views/voip/CallContainer.tsx b/src/components/views/voip/CallContainer.tsx new file mode 100644 index 0000000000..c5274d3844 --- /dev/null +++ b/src/components/views/voip/CallContainer.tsx @@ -0,0 +1,37 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +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 React from 'react'; +import IncomingCallBox2 from './IncomingCallBox2'; +import CallPreview from './CallPreview2'; +import * as VectorConferenceHandler from '../../../VectorConferenceHandler'; + +interface IProps { + +} + +interface IState { + +} + +export default class CallContainer extends React.Component { + public render() { + return
+ + +
+ } +} \ No newline at end of file diff --git a/src/components/views/voip/CallPreview2.tsx b/src/components/views/voip/CallPreview2.tsx new file mode 100644 index 0000000000..bab59ff068 --- /dev/null +++ b/src/components/views/voip/CallPreview2.tsx @@ -0,0 +1,126 @@ +/* +Copyright 2017, 2018 New Vector Ltd +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. + +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 React from 'react'; +import classNames from 'classnames'; + +import CallView from "./CallView2"; +import RoomViewStore from '../../../stores/RoomViewStore'; +import CallHandler from '../../../CallHandler'; +import dis from '../../../dispatcher/dispatcher'; +import { ActionPayload } from '../../../dispatcher/payloads'; +import PersistentApp from "../elements/PersistentApp"; +import SettingsStore from "../../../settings/SettingsStore"; + +interface IProps { + // A Conference Handler implementation + // Must have a function signature: + // getConferenceCallForRoom(roomId: string): MatrixCall + ConferenceHandler: any; +} + +interface IState { + roomId: string; + activeCall: any; + newRoomListActive: boolean; +} + +export default class CallPreview extends React.Component { + private roomStoreToken: any; + private dispatcherRef: string; + + constructor(props: IProps) { + super(props); + + this.state = { + roomId: RoomViewStore.getRoomId(), + activeCall: CallHandler.getAnyActiveCall(), + newRoomListActive: SettingsStore.getValue("feature_new_room_list"), + }; + + SettingsStore.watchSetting("feature_new_room_list", null, (name, roomId, level, valAtLevel, newVal) => this.setState({ + newRoomListActive: newVal, + })); + } + + public componentDidMount() { + this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); + this.dispatcherRef = dis.register(this.onAction); + } + + public componentWillUnmount() { + if (this.roomStoreToken) { + this.roomStoreToken.remove(); + } + dis.unregister(this.dispatcherRef); + } + + private onRoomViewStoreUpdate = (payload) => { + if (RoomViewStore.getRoomId() === this.state.roomId) return; + this.setState({ + roomId: RoomViewStore.getRoomId(), + }); + } + + private onAction = (payload: ActionPayload) => { + switch (payload.action) { + // listen for call state changes to prod the render method, which + // may hide the global CallView if the call it is tracking is dead + case 'call_state': + this.setState({ + activeCall: CallHandler.getAnyActiveCall(), + }); + break; + } + } + + private onCallViewClick = () => { + const call = CallHandler.getAnyActiveCall(); + if (call) { + dis.dispatch({ + action: 'view_room', + room_id: call.groupRoomId || call.roomId, + }); + } + } + + public render() { + if (this.state.newRoomListActive) { + const callForRoom = CallHandler.getCallForRoom(this.state.roomId); + const showCall = ( + this.state.activeCall && + this.state.activeCall.call_state === 'connected' && + !callForRoom + ); + + if (showCall) { + return ( + + ); + } + + return ; + } + + return null; + } +} + diff --git a/src/components/views/voip/CallView2.tsx b/src/components/views/voip/CallView2.tsx new file mode 100644 index 0000000000..cc51a05a6f --- /dev/null +++ b/src/components/views/voip/CallView2.tsx @@ -0,0 +1,197 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. + +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 React, {createRef} from 'react'; +import Room from 'matrix-js-sdk/src/models/room'; +import dis from '../../../dispatcher/dispatcher'; +import CallHandler from '../../../CallHandler'; +import {MatrixClientPeg} from '../../../MatrixClientPeg'; +import { _t } from '../../../languageHandler'; +import AccessibleButton from '../elements/AccessibleButton'; +import VideoView from "./VideoView"; +import RoomAvatar from "../avatars/RoomAvatar"; +import PulsedAvatar from '../avatars/PulsedAvatar'; + +interface IProps { + // js-sdk room object. If set, we will only show calls for the given + // room; if not, we will show any active call. + room?: Room, + + // A Conference Handler implementation + // Must have a function signature: + // getConferenceCallForRoom(roomId: string): MatrixCall + ConferenceHandler?: any, + + // maxHeight style attribute for the video panel + maxVideoHeight?: number, + + // a callback which is called when the user clicks on the video div + onClick?: React.MouseEventHandler, + + // a callback which is called when the content in the callview changes + // in a way that is likely to cause a resize. + onResize?: any, + + // classname applied to view, + className?: string, + + // Whether to show the hang up icon:W + showHangup?: boolean, + +} + +interface IState { + call: any; +} + +export default class CallView extends React.Component { + private videoref: React.RefObject; + private dispatcherRef: string; + + constructor(props: IProps) { + super(props); + + this.state = { + // the call this view is displaying (if any) + call: null, + } + + this.videoref = createRef(); + } + + public componentDidMount() { + this.dispatcherRef = dis.register(this.onAction); + this.showCall(); + } + + public componentWillUnmount() { + dis.unregister(this.dispatcherRef); + } + + private onAction = (payload) => { + // don't filter out payloads for room IDs other than props.room because + // we may be interested in the conf 1:1 room + if (payload.action !== 'call_state') { + return; + } + this.showCall(); + } + + private showCall() { + let call; + + if (this.props.room) { + const roomId = this.props.room.roomId; + call = CallHandler.getCallForRoom(roomId) || + (this.props.ConferenceHandler ? + this.props.ConferenceHandler.getConferenceCallForRoom(roomId) : + null + ); + + if (this.call) { + this.setState({ call: call }); + } + } else { + call = CallHandler.getAnyActiveCall(); + // Ignore calls if we can't get the room associated with them. + // I think the underlying problem is that the js-sdk sends events + // for calls before it has made the rooms available in the store, + // although this isn't confirmed. + if (MatrixClientPeg.get().getRoom(call.roomId) === null) { + call = null; + } + this.setState({ call: call }); + } + + if (call) { + call.setLocalVideoElement(this.getVideoView().getLocalVideoElement()); + call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement()); + // always use a separate element for audio stream playback. + // this is to let us move CallView around the DOM without interrupting remote audio + // during playback, by having the audio rendered by a top-level