Merge pull request #4904 from matrix-org/joriks/room-list-voip
Move voip previews to bottom right corner
This commit is contained in:
commit
0368bff5b1
16 changed files with 919 additions and 163 deletions
|
@ -51,6 +51,7 @@
|
||||||
@import "./views/avatars/_BaseAvatar.scss";
|
@import "./views/avatars/_BaseAvatar.scss";
|
||||||
@import "./views/avatars/_DecoratedRoomAvatar.scss";
|
@import "./views/avatars/_DecoratedRoomAvatar.scss";
|
||||||
@import "./views/avatars/_MemberStatusMessageAvatar.scss";
|
@import "./views/avatars/_MemberStatusMessageAvatar.scss";
|
||||||
|
@import "./views/avatars/_PulsedAvatar.scss";
|
||||||
@import "./views/context_menus/_MessageContextMenu.scss";
|
@import "./views/context_menus/_MessageContextMenu.scss";
|
||||||
@import "./views/context_menus/_RoomTileContextMenu.scss";
|
@import "./views/context_menus/_RoomTileContextMenu.scss";
|
||||||
@import "./views/context_menus/_StatusMessageContextMenu.scss";
|
@import "./views/context_menus/_StatusMessageContextMenu.scss";
|
||||||
|
@ -225,6 +226,8 @@
|
||||||
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
|
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
|
||||||
@import "./views/terms/_InlineTermsAgreement.scss";
|
@import "./views/terms/_InlineTermsAgreement.scss";
|
||||||
@import "./views/verification/_VerificationShowSas.scss";
|
@import "./views/verification/_VerificationShowSas.scss";
|
||||||
|
@import "./views/voip/_CallContainer.scss";
|
||||||
@import "./views/voip/_CallView.scss";
|
@import "./views/voip/_CallView.scss";
|
||||||
|
@import "./views/voip/_CallView2.scss";
|
||||||
@import "./views/voip/_IncomingCallbox.scss";
|
@import "./views/voip/_IncomingCallbox.scss";
|
||||||
@import "./views/voip/_VideoView.scss";
|
@import "./views/voip/_VideoView.scss";
|
||||||
|
|
30
res/css/views/avatars/_PulsedAvatar.scss
Normal file
30
res/css/views/avatars/_PulsedAvatar.scss
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
89
res/css/views/voip/_CallContainer.scss
Normal file
89
res/css/views/voip/_CallContainer.scss
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
96
res/css/views/voip/_CallView2.scss
Normal file
96
res/css/views/voip/_CallView2.scss
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -52,6 +52,7 @@ import {
|
||||||
} from "../../toasts/ServerLimitToast";
|
} from "../../toasts/ServerLimitToast";
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import LeftPanel2 from "./LeftPanel2";
|
import LeftPanel2 from "./LeftPanel2";
|
||||||
|
import CallContainer from '../views/voip/CallContainer';
|
||||||
import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
|
import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
|
||||||
|
|
||||||
// We need to fetch each pinned message individually (if we don't already have it)
|
// We need to fetch each pinned message individually (if we don't already have it)
|
||||||
|
@ -696,6 +697,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
</div>
|
</div>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
</div>
|
</div>
|
||||||
|
<CallContainer />
|
||||||
</MatrixClientContext.Provider>
|
</MatrixClientContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
|
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import classNames from 'classnames';
|
||||||
import * as AvatarLogic from '../../../Avatar';
|
import * as AvatarLogic from '../../../Avatar';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
@ -26,9 +26,25 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||||
import {toPx} from "../../../utils/units";
|
import {toPx} from "../../../utils/units";
|
||||||
|
|
||||||
const useImageUrl = ({url, urls}) => {
|
interface IProps {
|
||||||
const [imageUrls, setUrls] = useState([]);
|
name: string; // The name (first initial used as default)
|
||||||
const [urlsIndex, setIndex] = useState();
|
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<HTMLImageElement & HTMLSpanElement>;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useImageUrl = ({url, urls}): [string, () => void] => {
|
||||||
|
const [imageUrls, setUrls] = useState<string[]>([]);
|
||||||
|
const [urlsIndex, setIndex] = useState<number>();
|
||||||
|
|
||||||
const onError = useCallback(() => {
|
const onError = useCallback(() => {
|
||||||
setIndex(i => i + 1); // try the next one
|
setIndex(i => i + 1); // try the next one
|
||||||
|
@ -70,17 +86,17 @@ const useImageUrl = ({url, urls}) => {
|
||||||
return [imageUrl, onError];
|
return [imageUrl, onError];
|
||||||
};
|
};
|
||||||
|
|
||||||
const BaseAvatar = (props) => {
|
const BaseAvatar = (props: IProps) => {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
idName,
|
idName,
|
||||||
title,
|
title,
|
||||||
url,
|
url,
|
||||||
urls,
|
urls,
|
||||||
width=40,
|
width = 40,
|
||||||
height=40,
|
height = 40,
|
||||||
resizeMethod="crop", // eslint-disable-line no-unused-vars
|
resizeMethod = "crop", // eslint-disable-line no-unused-vars
|
||||||
defaultToInitialLetter=true,
|
defaultToInitialLetter = true,
|
||||||
onClick,
|
onClick,
|
||||||
inputRef,
|
inputRef,
|
||||||
...otherProps
|
...otherProps
|
||||||
|
@ -117,7 +133,7 @@ const BaseAvatar = (props) => {
|
||||||
aria-hidden="true" />
|
aria-hidden="true" />
|
||||||
);
|
);
|
||||||
|
|
||||||
if (onClick != null) {
|
if (onClick !== null) {
|
||||||
return (
|
return (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
|
@ -132,7 +148,12 @@ const BaseAvatar = (props) => {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps} role="presentation">
|
<span
|
||||||
|
className="mx_BaseAvatar"
|
||||||
|
ref={inputRef}
|
||||||
|
{...otherProps}
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
{ textNode }
|
{ textNode }
|
||||||
{ imgNode }
|
{ imgNode }
|
||||||
</span>
|
</span>
|
||||||
|
@ -140,7 +161,7 @@ const BaseAvatar = (props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onClick != null) {
|
if (onClick !== null) {
|
||||||
return (
|
return (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className="mx_BaseAvatar mx_BaseAvatar_image"
|
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||||
|
@ -173,26 +194,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 default BaseAvatar;
|
||||||
|
export type BaseAvatarType = React.FC<IProps>;
|
|
@ -15,43 +15,36 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
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 {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
|
import BaseAvatar from './BaseAvatar';
|
||||||
|
|
||||||
export default createReactClass({
|
export interface IProps {
|
||||||
displayName: 'GroupAvatar',
|
groupId?: string;
|
||||||
|
groupName?: string;
|
||||||
|
groupAvatarUrl?: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
resizeMethod?: string;
|
||||||
|
onClick?: React.MouseEventHandler;
|
||||||
|
}
|
||||||
|
|
||||||
propTypes: {
|
export default class GroupAvatar extends React.Component<IProps> {
|
||||||
groupId: PropTypes.string,
|
public static defaultProps = {
|
||||||
groupName: PropTypes.string,
|
width: 36,
|
||||||
groupAvatarUrl: PropTypes.string,
|
height: 36,
|
||||||
width: PropTypes.number,
|
resizeMethod: 'crop',
|
||||||
height: PropTypes.number,
|
};
|
||||||
resizeMethod: PropTypes.string,
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getGroupAvatarUrl() {
|
||||||
return {
|
|
||||||
width: 36,
|
|
||||||
height: 36,
|
|
||||||
resizeMethod: 'crop',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getGroupAvatarUrl: function() {
|
|
||||||
return MatrixClientPeg.get().mxcUrlToHttp(
|
return MatrixClientPeg.get().mxcUrlToHttp(
|
||||||
this.props.groupAvatarUrl,
|
this.props.groupAvatarUrl,
|
||||||
this.props.width,
|
this.props.width,
|
||||||
this.props.height,
|
this.props.height,
|
||||||
this.props.resizeMethod,
|
this.props.resizeMethod,
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
|
||||||
render: function() {
|
render() {
|
||||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
|
||||||
// extract the props we use from props so we can pass any others through
|
// 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?
|
// should consider adding this as a global rule in js-sdk?
|
||||||
/*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
|
/*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
|
||||||
|
@ -65,5 +58,5 @@ export default createReactClass({
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
});
|
}
|
|
@ -16,48 +16,50 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
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 dis from "../../../dispatcher/dispatcher";
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import {Action} from "../../../dispatcher/actions";
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
|
import BaseAvatar from "./BaseAvatar";
|
||||||
|
|
||||||
export default createReactClass({
|
interface IProps {
|
||||||
displayName: 'MemberAvatar',
|
// 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: {
|
interface IState {
|
||||||
member: PropTypes.object,
|
name: string;
|
||||||
fallbackUserId: PropTypes.string,
|
title: string;
|
||||||
width: PropTypes.number,
|
imageUrl?: string;
|
||||||
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,
|
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultProps: function() {
|
export default class MemberAvatar extends React.Component<IProps, IState> {
|
||||||
return {
|
public static defaultProps = {
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
resizeMethod: 'crop',
|
resizeMethod: 'crop',
|
||||||
viewUserOnClick: false,
|
viewUserOnClick: false,
|
||||||
};
|
};
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
constructor(props: IProps) {
|
||||||
return this._getState(this.props);
|
super(props);
|
||||||
},
|
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
this.state = MemberAvatar.getState(props);
|
||||||
UNSAFE_componentWillReceiveProps: function(nextProps) {
|
}
|
||||||
this.setState(this._getState(nextProps));
|
|
||||||
},
|
|
||||||
|
|
||||||
_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) {
|
if (props.member && props.member.name) {
|
||||||
return {
|
return {
|
||||||
name: props.member.name,
|
name: props.member.name,
|
||||||
|
@ -79,11 +81,9 @@ export default createReactClass({
|
||||||
} else {
|
} else {
|
||||||
console.error("MemberAvatar called somehow with null member or fallbackUserId");
|
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;
|
let {member, fallbackUserId, onClick, viewUserOnClick, ...otherProps} = this.props;
|
||||||
const userId = member ? member.userId : fallbackUserId;
|
const userId = member ? member.userId : fallbackUserId;
|
||||||
|
|
||||||
|
@ -100,5 +100,5 @@ export default createReactClass({
|
||||||
<BaseAvatar {...otherProps} name={this.state.name} title={this.state.title}
|
<BaseAvatar {...otherProps} name={this.state.name} title={this.state.title}
|
||||||
idName={userId} url={this.state.imageUrl} onClick={onClick} />
|
idName={userId} url={this.state.imageUrl} onClick={onClick} />
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
});
|
}
|
28
src/components/views/avatars/PulsedAvatar.tsx
Normal file
28
src/components/views/avatars/PulsedAvatar.tsx
Normal file
|
@ -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<IProps> = (props) => {
|
||||||
|
return <div className="mx_PulsedAvatar">
|
||||||
|
{props.children}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PulsedAvatar;
|
|
@ -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
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import Room from 'matrix-js-sdk/src/models/room';
|
||||||
import createReactClass from 'create-react-class';
|
import {getHttpUriForMxc} from 'matrix-js-sdk/src/content-repo';
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
|
||||||
|
import BaseAvatar from './BaseAvatar';
|
||||||
|
import ImageView from '../elements/ImageView';
|
||||||
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import * as sdk from "../../../index";
|
|
||||||
import * as Avatar from '../../../Avatar';
|
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,
|
// Room may be left unset here, but if it is,
|
||||||
// oobData.avatarUrl should be set (else there
|
// oobData.avatarUrl should be set (else there
|
||||||
// would be nowhere to get the avatar from)
|
// would be nowhere to get the avatar from)
|
||||||
propTypes: {
|
room?: Room;
|
||||||
room: PropTypes.object,
|
// TODO: type when js-sdk has types
|
||||||
oobData: PropTypes.object,
|
oobData?: any;
|
||||||
width: PropTypes.number,
|
width?: number;
|
||||||
height: PropTypes.number,
|
height?: number;
|
||||||
resizeMethod: PropTypes.string,
|
resizeMethod?: string;
|
||||||
viewAvatarOnClick: PropTypes.bool,
|
viewAvatarOnClick?: boolean;
|
||||||
},
|
}
|
||||||
|
|
||||||
getDefaultProps: function() {
|
interface IState {
|
||||||
return {
|
urls: string[];
|
||||||
width: 36,
|
}
|
||||||
height: 36,
|
|
||||||
resizeMethod: 'crop',
|
export default class RoomAvatar extends React.Component<IProps, IState> {
|
||||||
oobData: {},
|
public static defaultProps = {
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
resizeMethod: 'crop',
|
||||||
|
oobData: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
urls: RoomAvatar.getImageUrls(this.props),
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
|
|
||||||
getInitialState: function() {
|
public componentDidMount() {
|
||||||
return {
|
|
||||||
urls: this.getImageUrls(this.props),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount: function() {
|
|
||||||
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
||||||
},
|
}
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
public componentWillUnmount() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (cli) {
|
if (cli) {
|
||||||
cli.removeListener("RoomState.events", this.onRoomStateEvents);
|
cli.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
public static getDerivedStateFromProps(nextProps: IProps): IState {
|
||||||
UNSAFE_componentWillReceiveProps: function(newProps) {
|
return {
|
||||||
this.setState({
|
urls: RoomAvatar.getImageUrls(nextProps),
|
||||||
urls: this.getImageUrls(newProps),
|
};
|
||||||
});
|
}
|
||||||
},
|
|
||||||
|
|
||||||
onRoomStateEvents: function(ev) {
|
// TODO: type when js-sdk has types
|
||||||
|
private onRoomStateEvents = (ev: any) => {
|
||||||
if (!this.props.room ||
|
if (!this.props.room ||
|
||||||
ev.getRoomId() !== this.props.room.roomId ||
|
ev.getRoomId() !== this.props.room.roomId ||
|
||||||
ev.getType() !== 'm.room.avatar'
|
ev.getType() !== 'm.room.avatar'
|
||||||
) return;
|
) return;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
urls: this.getImageUrls(this.props),
|
urls: RoomAvatar.getImageUrls(this.props),
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
|
|
||||||
getImageUrls: function(props) {
|
private static getImageUrls(props: IProps): string[] {
|
||||||
return [
|
return [
|
||||||
getHttpUriForMxc(
|
getHttpUriForMxc(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
MatrixClientPeg.get().getHomeserverUrl(),
|
||||||
|
// Default props don't play nicely with getDerivedStateFromProps
|
||||||
|
//props.oobData !== undefined ? props.oobData.avatarUrl : {},
|
||||||
props.oobData.avatarUrl,
|
props.oobData.avatarUrl,
|
||||||
Math.floor(props.width * window.devicePixelRatio),
|
Math.floor(props.width * window.devicePixelRatio),
|
||||||
Math.floor(props.height * window.devicePixelRatio),
|
Math.floor(props.height * window.devicePixelRatio),
|
||||||
props.resizeMethod,
|
props.resizeMethod,
|
||||||
), // highest priority
|
), // highest priority
|
||||||
this.getRoomAvatarUrl(props),
|
RoomAvatar.getRoomAvatarUrl(props),
|
||||||
].filter(function(url) {
|
].filter(function(url) {
|
||||||
return (url != null && url != "");
|
return (url !== null && url !== "");
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
getRoomAvatarUrl: function(props) {
|
private static getRoomAvatarUrl(props: IProps): string {
|
||||||
if (!props.room) return null;
|
if (!props.room) return null;
|
||||||
|
|
||||||
return Avatar.avatarUrlForRoom(
|
return Avatar.avatarUrlForRoom(
|
||||||
|
@ -105,24 +111,21 @@ export default createReactClass({
|
||||||
Math.floor(props.height * window.devicePixelRatio),
|
Math.floor(props.height * window.devicePixelRatio),
|
||||||
props.resizeMethod,
|
props.resizeMethod,
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
|
||||||
onRoomAvatarClick: function() {
|
private onRoomAvatarClick = () => {
|
||||||
const avatarUrl = this.props.room.getAvatarUrl(
|
const avatarUrl = this.props.room.getAvatarUrl(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
MatrixClientPeg.get().getHomeserverUrl(),
|
||||||
null, null, null, false);
|
null, null, null, false);
|
||||||
const ImageView = sdk.getComponent("elements.ImageView");
|
|
||||||
const params = {
|
const params = {
|
||||||
src: avatarUrl,
|
src: avatarUrl,
|
||||||
name: this.props.room.name,
|
name: this.props.room.name,
|
||||||
};
|
};
|
||||||
|
|
||||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||||
},
|
};
|
||||||
|
|
||||||
render: function() {
|
|
||||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
|
||||||
|
|
||||||
|
public render() {
|
||||||
/*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
|
/*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
|
||||||
const {room, oobData, viewAvatarOnClick, ...otherProps} = this.props;
|
const {room, oobData, viewAvatarOnClick, ...otherProps} = this.props;
|
||||||
|
|
||||||
|
@ -132,8 +135,8 @@ export default createReactClass({
|
||||||
<BaseAvatar {...otherProps} name={roomName}
|
<BaseAvatar {...otherProps} name={roomName}
|
||||||
idName={room ? room.roomId : null}
|
idName={room ? room.roomId : null}
|
||||||
urls={this.state.urls}
|
urls={this.state.urls}
|
||||||
onClick={this.props.viewAvatarOnClick ? this.onRoomAvatarClick : null}
|
onClick={this.props.viewAvatarOnClick && !this.state.urls[0] ? this.onRoomAvatarClick : null}
|
||||||
disabled={!this.state.urls[0]} />
|
/>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
});
|
}
|
37
src/components/views/voip/CallContainer.tsx
Normal file
37
src/components/views/voip/CallContainer.tsx
Normal file
|
@ -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.PureComponent<IProps, IState> {
|
||||||
|
public render() {
|
||||||
|
return <div className="mx_CallContainer">
|
||||||
|
<IncomingCallBox2 />
|
||||||
|
<CallPreview ConferenceHandler={VectorConferenceHandler} />
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
129
src/components/views/voip/CallPreview2.tsx
Normal file
129
src/components/views/voip/CallPreview2.tsx
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
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<IProps, IState> {
|
||||||
|
private roomStoreToken: any;
|
||||||
|
private dispatcherRef: string;
|
||||||
|
private settingsWatcherRef: string;
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
roomId: RoomViewStore.getRoomId(),
|
||||||
|
activeCall: CallHandler.getAnyActiveCall(),
|
||||||
|
newRoomListActive: SettingsStore.getValue("feature_new_room_list"),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.settingsWatcherRef = 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);
|
||||||
|
SettingsStore.unwatchSetting(this.settingsWatcherRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<CallView
|
||||||
|
className="mx_CallPreview" onClick={this.onCallViewClick}
|
||||||
|
ConferenceHandler={this.props.ConferenceHandler}
|
||||||
|
showHangup={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <PersistentApp />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
200
src/components/views/voip/CallView2.tsx
Normal file
200
src/components/views/voip/CallView2.tsx
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
|
|
||||||
|
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<IProps, IState> {
|
||||||
|
private videoref: React.RefObject<any>;
|
||||||
|
private dispatcherRef: string;
|
||||||
|
public call: any;
|
||||||
|
|
||||||
|
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 <audio/> element.
|
||||||
|
// rather than being rendered by the main remoteVideo <video/> element.
|
||||||
|
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
|
||||||
|
}
|
||||||
|
if (call && call.type === "video" && call.call_state !== "ended" && call.call_state !== "ringing") {
|
||||||
|
// if this call is a conf call, don't display local video as the
|
||||||
|
// conference will have us in it
|
||||||
|
this.getVideoView().getLocalVideoElement().style.display = (
|
||||||
|
call.confUserId ? "none" : "block"
|
||||||
|
);
|
||||||
|
this.getVideoView().getRemoteVideoElement().style.display = "block";
|
||||||
|
} else {
|
||||||
|
this.getVideoView().getLocalVideoElement().style.display = "none";
|
||||||
|
this.getVideoView().getRemoteVideoElement().style.display = "none";
|
||||||
|
dis.dispatch({action: 'video_fullscreen', fullscreen: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.onResize) {
|
||||||
|
this.props.onResize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getVideoView() {
|
||||||
|
return this.videoref.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
let view: React.ReactNode;
|
||||||
|
if (this.state.call && this.state.call.type === "voice") {
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
const callRoom = client.getRoom(this.state.call.roomId);
|
||||||
|
|
||||||
|
view = <AccessibleButton className="mx_CallView2_voice" onClick={this.props.onClick}>
|
||||||
|
<PulsedAvatar>
|
||||||
|
<RoomAvatar
|
||||||
|
room={callRoom}
|
||||||
|
height={35}
|
||||||
|
width={35}
|
||||||
|
/>
|
||||||
|
</PulsedAvatar>
|
||||||
|
<div>
|
||||||
|
<h1>{callRoom.name}</h1>
|
||||||
|
<p>{ _t("Active call") }</p>
|
||||||
|
</div>
|
||||||
|
</AccessibleButton>;
|
||||||
|
} else {
|
||||||
|
view = <VideoView
|
||||||
|
ref={this.videoref}
|
||||||
|
onClick={this.props.onClick}
|
||||||
|
onResize={this.props.onResize}
|
||||||
|
maxHeight={this.props.maxVideoHeight}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hangup: React.ReactNode;
|
||||||
|
if (this.props.showHangup) {
|
||||||
|
hangup = <div
|
||||||
|
className="mx_CallView2_hangup"
|
||||||
|
onClick={() => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'hangup',
|
||||||
|
room_id: this.state.call.roomId,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={this.props.className}>
|
||||||
|
{view}
|
||||||
|
{hangup}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
141
src/components/views/voip/IncomingCallBox2.tsx
Normal file
141
src/components/views/voip/IncomingCallBox2.tsx
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||||
|
import CallHandler from '../../../CallHandler';
|
||||||
|
import PulsedAvatar from '../avatars/PulsedAvatar';
|
||||||
|
import RoomAvatar from '../avatars/RoomAvatar';
|
||||||
|
import FormButton from '../elements/FormButton';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
incomingCall: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class IncomingCallBox2 extends React.Component<IProps, IState> {
|
||||||
|
private dispatcherRef: string;
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
this.state = {
|
||||||
|
incomingCall: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
dis.unregister(this.dispatcherRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onAction = (payload: ActionPayload) => {
|
||||||
|
switch (payload.action) {
|
||||||
|
case 'call_state':
|
||||||
|
const call = CallHandler.getCall(payload.room_id);
|
||||||
|
if (call && call.call_state === 'ringing') {
|
||||||
|
this.setState({
|
||||||
|
incomingCall: call,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
incomingCall: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onAnswerClick: React.MouseEventHandler = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'answer',
|
||||||
|
room_id: this.state.incomingCall.roomId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onRejectClick: React.MouseEventHandler = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'hangup',
|
||||||
|
room_id: this.state.incomingCall.roomId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
if (!this.state.incomingCall) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let room = null;
|
||||||
|
if (this.state.incomingCall) {
|
||||||
|
room = MatrixClientPeg.get().getRoom(this.state.incomingCall.roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const caller = room ? room.name : _t("Unknown caller");
|
||||||
|
|
||||||
|
let incomingCallText = null;
|
||||||
|
if (this.state.incomingCall) {
|
||||||
|
if (this.state.incomingCall.type === "voice") {
|
||||||
|
incomingCallText = _t("Incoming voice call");
|
||||||
|
} else if (this.state.incomingCall.type === "video") {
|
||||||
|
incomingCallText = _t("Incoming video call");
|
||||||
|
} else {
|
||||||
|
incomingCallText = _t("Incoming call");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="mx_IncomingCallBox2">
|
||||||
|
<div className="mx_IncomingCallBox2_CallerInfo">
|
||||||
|
<PulsedAvatar>
|
||||||
|
<RoomAvatar
|
||||||
|
room={room}
|
||||||
|
height={32}
|
||||||
|
width={32}
|
||||||
|
/>
|
||||||
|
</PulsedAvatar>
|
||||||
|
<div>
|
||||||
|
<h1>{caller}</h1>
|
||||||
|
<p>{incomingCallText}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mx_IncomingCallBox2_buttons">
|
||||||
|
<FormButton
|
||||||
|
className={"mx_IncomingCallBox2_decline"}
|
||||||
|
onClick={this.onRejectClick}
|
||||||
|
kind="danger"
|
||||||
|
label={_t("Decline")}
|
||||||
|
/>
|
||||||
|
<div className="mx_IncomingCallBox2_spacer" />
|
||||||
|
<FormButton
|
||||||
|
className={"mx_IncomingCallBox2_accept"}
|
||||||
|
onClick={this.onAnswerClick}
|
||||||
|
kind="primary"
|
||||||
|
label={_t("Accept")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -558,12 +558,17 @@
|
||||||
"My Ban List": "My Ban List",
|
"My Ban List": "My Ban List",
|
||||||
"This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!",
|
"This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!",
|
||||||
"Active call (%(roomName)s)": "Active call (%(roomName)s)",
|
"Active call (%(roomName)s)": "Active call (%(roomName)s)",
|
||||||
|
"Active call": "Active call",
|
||||||
"unknown caller": "unknown caller",
|
"unknown caller": "unknown caller",
|
||||||
"Incoming voice call from %(name)s": "Incoming voice call from %(name)s",
|
"Incoming voice call from %(name)s": "Incoming voice call from %(name)s",
|
||||||
"Incoming video call from %(name)s": "Incoming video call from %(name)s",
|
"Incoming video call from %(name)s": "Incoming video call from %(name)s",
|
||||||
"Incoming call from %(name)s": "Incoming call from %(name)s",
|
"Incoming call from %(name)s": "Incoming call from %(name)s",
|
||||||
"Decline": "Decline",
|
"Decline": "Decline",
|
||||||
"Accept": "Accept",
|
"Accept": "Accept",
|
||||||
|
"Unknown caller": "Unknown caller",
|
||||||
|
"Incoming voice call": "Incoming voice call",
|
||||||
|
"Incoming video call": "Incoming video call",
|
||||||
|
"Incoming call": "Incoming call",
|
||||||
"The other party cancelled the verification.": "The other party cancelled the verification.",
|
"The other party cancelled the verification.": "The other party cancelled the verification.",
|
||||||
"Verified!": "Verified!",
|
"Verified!": "Verified!",
|
||||||
"You've successfully verified this user.": "You've successfully verified this user.",
|
"You've successfully verified this user.": "You've successfully verified this user.",
|
||||||
|
@ -2102,7 +2107,6 @@
|
||||||
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|one": "<resendText>Resend message</resendText> or <cancelText>cancel message</cancelText> now.",
|
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|one": "<resendText>Resend message</resendText> or <cancelText>cancel message</cancelText> now.",
|
||||||
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
|
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
|
||||||
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
|
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
|
||||||
"Active call": "Active call",
|
|
||||||
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
|
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
|
||||||
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
|
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
|
||||||
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
|
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
|
||||||
|
|
|
@ -205,8 +205,9 @@ describe("<TextualBody />", () => {
|
||||||
expect(content.html()).toBe('<span class="mx_EventTile_body markdown-body" dir="auto">' +
|
expect(content.html()).toBe('<span class="mx_EventTile_body markdown-body" dir="auto">' +
|
||||||
'Hey <span>' +
|
'Hey <span>' +
|
||||||
'<a class="mx_Pill mx_UserPill" title="@user:server">' +
|
'<a class="mx_Pill mx_UserPill" title="@user:server">' +
|
||||||
'<img class="mx_BaseAvatar mx_BaseAvatar_image" src="mxc://avatar.url/image.png" ' +
|
'<img src="mxc://avatar.url/image.png" style="width: 16px; height: 16px;" ' +
|
||||||
'style="width: 16px; height: 16px;" title="@member:domain.bla" alt="" aria-hidden="true">Member</a>' +
|
'title="@member:domain.bla" alt="" aria-hidden="true" role="button" tabindex="0" ' +
|
||||||
|
'class="mx_AccessibleButton mx_BaseAvatar mx_BaseAvatar_image">Member</a>' +
|
||||||
'</span></span>');
|
'</span></span>');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue