Merge remote-tracking branch 'upstream/develop' into feature/call-event-tile

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
Šimon Brandner 2021-07-16 21:43:33 +02:00
commit 16ed5c6645
No known key found for this signature in database
GPG key ID: CC823428E9B582FB
215 changed files with 4697 additions and 1812 deletions

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, { createRef } from 'react';
import CallView from "./CallView";
import RoomViewStore from '../../../stores/RoomViewStore';
@ -27,6 +27,22 @@ import SettingsStore from "../../../settings/SettingsStore";
import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import UIStore from '../../../stores/UIStore';
import { lerp } from '../../../utils/AnimationUtils';
import { MarkedExecution } from '../../../utils/MarkedExecution';
const PIP_VIEW_WIDTH = 336;
const PIP_VIEW_HEIGHT = 232;
const MOVING_AMT = 0.2;
const SNAPPING_AMT = 0.05;
const PADDING = {
top: 58,
bottom: 58,
left: 76,
right: 8,
};
const SHOW_CALL_IN_STATES = [
CallState.Connected,
@ -49,6 +65,10 @@ interface IState {
// Any other call we're displaying: only if the user is on two calls and not viewing either of the rooms
// they belong to
secondaryCall: MatrixCall;
// Position of the CallPreview
translationX: number;
translationY: number;
}
// Splits a list of calls into one 'primary' one and a list
@ -91,6 +111,16 @@ export default class CallPreview extends React.Component<IProps, IState> {
private roomStoreToken: any;
private dispatcherRef: string;
private settingsWatcherRef: string;
private callViewWrapper = createRef<HTMLDivElement>();
private initX = 0;
private initY = 0;
private desiredTranslationX = UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH;
private desiredTranslationY = UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_WIDTH;
private moving = false;
private scheduledUpdate = new MarkedExecution(
() => this.animationCallback(),
() => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
);
constructor(props: IProps) {
super(props);
@ -105,12 +135,17 @@ export default class CallPreview extends React.Component<IProps, IState> {
roomId,
primaryCall: primaryCall,
secondaryCall: secondaryCalls[0],
translationX: UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH,
translationY: UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_WIDTH,
};
}
public componentDidMount() {
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
document.addEventListener("mousemove", this.onMoving);
document.addEventListener("mouseup", this.onEndMoving);
window.addEventListener("resize", this.snap);
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
}
@ -118,6 +153,9 @@ export default class CallPreview extends React.Component<IProps, IState> {
public componentWillUnmount() {
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
document.removeEventListener("mousemove", this.onMoving);
document.removeEventListener("mouseup", this.onEndMoving);
window.removeEventListener("resize", this.snap);
if (this.roomStoreToken) {
this.roomStoreToken.remove();
}
@ -125,6 +163,83 @@ export default class CallPreview extends React.Component<IProps, IState> {
SettingsStore.unwatchSetting(this.settingsWatcherRef);
}
private animationCallback = () => {
// If the PiP isn't being dragged and there is only a tiny difference in
// the desiredTranslation and translation, quit the animationCallback
// loop. If that is the case, it means the PiP has snapped into its
// position and there is nothing to do. Not doing this would cause an
// infinite loop
if (
!this.moving &&
Math.abs(this.state.translationX - this.desiredTranslationX) <= 1 &&
Math.abs(this.state.translationY - this.desiredTranslationY) <= 1
) return;
const amt = this.moving ? MOVING_AMT : SNAPPING_AMT;
this.setState({
translationX: lerp(this.state.translationX, this.desiredTranslationX, amt),
translationY: lerp(this.state.translationY, this.desiredTranslationY, amt),
});
this.scheduledUpdate.mark();
};
private setTranslation(inTranslationX: number, inTranslationY: number) {
const width = this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH;
const height = this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT;
// Avoid overflow on the x axis
if (inTranslationX + width >= UIStore.instance.windowWidth) {
this.desiredTranslationX = UIStore.instance.windowWidth - width;
} else if (inTranslationX <= 0) {
this.desiredTranslationX = 0;
} else {
this.desiredTranslationX = inTranslationX;
}
// Avoid overflow on the y axis
if (inTranslationY + height >= UIStore.instance.windowHeight) {
this.desiredTranslationY = UIStore.instance.windowHeight - height;
} else if (inTranslationY <= 0) {
this.desiredTranslationY = 0;
} else {
this.desiredTranslationY = inTranslationY;
}
}
private snap = () => {
const translationX = this.desiredTranslationX;
const translationY = this.desiredTranslationY;
// We subtract the PiP size from the window size in order to calculate
// the position to snap to from the PiP center and not its top-left
// corner
const windowWidth = (
UIStore.instance.windowWidth -
(this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH)
);
const windowHeight = (
UIStore.instance.windowHeight -
(this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT)
);
if (translationX >= windowWidth / 2 && translationY >= windowHeight / 2) {
this.desiredTranslationX = windowWidth - PADDING.right;
this.desiredTranslationY = windowHeight - PADDING.bottom;
} else if (translationX >= windowWidth / 2 && translationY <= windowHeight / 2) {
this.desiredTranslationX = windowWidth - PADDING.right;
this.desiredTranslationY = PADDING.top;
} else if (translationX <= windowWidth / 2 && translationY >= windowHeight / 2) {
this.desiredTranslationX = PADDING.left;
this.desiredTranslationY = windowHeight - PADDING.bottom;
} else {
this.desiredTranslationX = PADDING.left;
this.desiredTranslationY = PADDING.top;
}
// We start animating here because we want the PiP to move when we're
// resizing the window
this.scheduledUpdate.mark();
};
private onRoomViewStoreUpdate = (payload) => {
if (RoomViewStore.getRoomId() === this.state.roomId) return;
@ -173,10 +288,52 @@ export default class CallPreview extends React.Component<IProps, IState> {
});
};
private onStartMoving = (event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
this.moving = true;
this.initX = event.pageX - this.desiredTranslationX;
this.initY = event.pageY - this.desiredTranslationY;
this.scheduledUpdate.mark();
};
private onMoving = (event: React.MouseEvent | MouseEvent) => {
if (!this.moving) return;
event.preventDefault();
event.stopPropagation();
this.setTranslation(event.pageX - this.initX, event.pageY - this.initY);
};
private onEndMoving = () => {
this.moving = false;
this.snap();
};
public render() {
if (this.state.primaryCall) {
const translatePixelsX = this.state.translationX + "px";
const translatePixelsY = this.state.translationY + "px";
const style = {
transform: `translateX(${translatePixelsX})
translateY(${translatePixelsY})`,
};
return (
<CallView call={this.state.primaryCall} secondaryCall={this.state.secondaryCall} pipMode={true} />
<div
className="mx_CallPreview"
style={style}
ref={this.callViewWrapper}
>
<CallView
call={this.state.primaryCall}
secondaryCall={this.state.secondaryCall}
pipMode={true}
onMouseDownOnHeader={this.onStartMoving}
/>
</div>
);
}

View file

@ -49,6 +49,9 @@ interface IProps {
// This is sort of a proxy for a number of things but we currently have no
// need to control those things separately, so this is simpler.
pipMode?: boolean;
// Used for dragging the PiP CallView
onMouseDownOnHeader?: (event: React.MouseEvent) => void;
}
interface IState {
@ -698,19 +701,24 @@ export default class CallView extends React.Component<IProps, IState> {
</span>;
}
header = <div className="mx_CallView_header">
<AccessibleButton onClick={this.onRoomAvatarClick}>
<RoomAvatar room={callRoom} height={32} width={32} />
</AccessibleButton>
<div className="mx_CallView_header_callInfo">
<div className="mx_CallView_header_roomName">{callRoom.name}</div>
<div className="mx_CallView_header_callTypeSmall">
{callTypeText}
{secondaryCallInfo}
header = (
<div
className="mx_CallView_header"
onMouseDown={this.props.onMouseDownOnHeader}
>
<AccessibleButton onClick={this.onRoomAvatarClick}>
<RoomAvatar room={callRoom} height={32} width={32} />
</AccessibleButton>
<div className="mx_CallView_header_callInfo">
<div className="mx_CallView_header_roomName">{callRoom.name}</div>
<div className="mx_CallView_header_callTypeSmall">
{callTypeText}
{secondaryCallInfo}
</div>
</div>
{headerControls}
</div>
{headerControls}
</div>;
);
myClassName = 'mx_CallView_pip';
}

View file

@ -19,16 +19,17 @@ import AccessibleButton from "../elements/AccessibleButton";
import { replaceableComponent } from "../../../utils/replaceableComponent";
const BUTTONS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'];
const BUTTON_LETTERS = ['', 'ABC', 'DEF', 'GHI', 'JKL', 'MNO', 'PQRS', 'TUV', 'WXYZ', '', '+', ''];
enum DialPadButtonKind {
Digit,
Delete,
Dial,
}
interface IButtonProps {
kind: DialPadButtonKind;
digit?: string;
digitSubtext?: string;
onButtonPress: (string) => void;
}
@ -42,11 +43,10 @@ class DialPadButton extends React.PureComponent<IButtonProps> {
case DialPadButtonKind.Digit:
return <AccessibleButton className="mx_DialPad_button" onClick={this.onClick}>
{this.props.digit}
<div className="mx_DialPad_buttonSubText">
{this.props.digitSubtext}
</div>
</AccessibleButton>;
case DialPadButtonKind.Delete:
return <AccessibleButton className="mx_DialPad_button mx_DialPad_deleteButton"
onClick={this.onClick}
/>;
case DialPadButtonKind.Dial:
return <AccessibleButton className="mx_DialPad_button mx_DialPad_dialButton" onClick={this.onClick} />;
}
@ -55,7 +55,7 @@ class DialPadButton extends React.PureComponent<IButtonProps> {
interface IProps {
onDigitPress: (string) => void;
hasDialAndDelete: boolean;
hasDial: boolean;
onDeletePress?: (string) => void;
onDialPress?: (string) => void;
}
@ -65,16 +65,15 @@ export default class Dialpad extends React.PureComponent<IProps> {
render() {
const buttonNodes = [];
for (const button of BUTTONS) {
for (let i = 0; i < BUTTONS.length; i++) {
const button = BUTTONS[i];
const digitSubtext = BUTTON_LETTERS[i];
buttonNodes.push(<DialPadButton key={button} kind={DialPadButtonKind.Digit}
digit={button} onButtonPress={this.props.onDigitPress}
digit={button} digitSubtext={digitSubtext} onButtonPress={this.props.onDigitPress}
/>);
}
if (this.props.hasDialAndDelete) {
buttonNodes.push(<DialPadButton key="del" kind={DialPadButtonKind.Delete}
onButtonPress={this.props.onDeletePress}
/>);
if (this.props.hasDial) {
buttonNodes.push(<DialPadButton key="dial" kind={DialPadButtonKind.Dial}
onButtonPress={this.props.onDialPress}
/>);

View file

@ -15,7 +15,6 @@ limitations under the License.
*/
import * as React from "react";
import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import Field from "../elements/Field";
import DialPad from './DialPad';
@ -23,6 +22,7 @@ import dis from '../../../dispatcher/dispatcher';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { DialNumberPayload } from "../../../dispatcher/payloads/DialNumberPayload";
import { Action } from "../../../dispatcher/actions";
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
interface IProps {
onFinished: (boolean) => void;
@ -74,22 +74,38 @@ export default class DialpadModal extends React.PureComponent<IProps, IState> {
};
render() {
const backspaceButton = (
<DialPadBackspaceButton onBackspacePress={this.onDeletePress} />
);
// Only show the backspace button if the field has content
let dialPadField;
if (this.state.value.length !== 0) {
dialPadField = <Field className="mx_DialPadModal_field" id="dialpad_number"
value={this.state.value}
autoFocus={true}
onChange={this.onChange}
postfixComponent={backspaceButton}
/>;
} else {
dialPadField = <Field className="mx_DialPadModal_field" id="dialpad_number"
value={this.state.value}
autoFocus={true}
onChange={this.onChange}
/>;
}
return <div className="mx_DialPadModal">
<div>
<AccessibleButton className="mx_DialPadModal_cancel" onClick={this.onCancelClick} />
</div>
<div className="mx_DialPadModal_header">
<div>
<span className="mx_DialPadModal_title">{_t("Dial pad")}</span>
<AccessibleButton className="mx_DialPadModal_cancel" onClick={this.onCancelClick} />
</div>
<form onSubmit={this.onFormSubmit}>
<Field className="mx_DialPadModal_field" id="dialpad_number"
value={this.state.value} autoFocus={true}
onChange={this.onChange}
/>
{dialPadField}
</form>
</div>
<div className="mx_DialPadModal_horizSep" />
<div className="mx_DialPadModal_dialPad">
<DialPad hasDialAndDelete={true}
<DialPad hasDial={true}
onDigitPress={this.onDigitPress}
onDeletePress={this.onDeletePress}
onDialPress={this.onDialPress}