Merge remote-tracking branch 'upstream/develop' into fix/12652/screen-share

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
Šimon Brandner 2021-07-27 16:25:38 +02:00
commit 24f0c760ce
No known key found for this signature in database
GPG key ID: 55C211A1226CB17D
70 changed files with 1590 additions and 263 deletions

View file

@ -22,6 +22,7 @@ import MemberAvatar from '../avatars/MemberAvatar';
import { _t } from '../../../languageHandler';
import { useStateToggle } from "../../../hooks/useStateToggle";
import AccessibleButton from "./AccessibleButton";
import { Layout } from '../../../settings/Layout';
interface IProps {
// An array of member events to summarise
@ -38,6 +39,8 @@ interface IProps {
children: ReactNode[];
// Called when the event list expansion is toggled
onToggle?(): void;
// The layout currently used
layout?: Layout;
}
const EventListSummary: React.FC<IProps> = ({
@ -48,6 +51,7 @@ const EventListSummary: React.FC<IProps> = ({
startExpanded,
summaryMembers = [],
summaryText,
layout,
}) => {
const [expanded, toggleExpanded] = useStateToggle(startExpanded);
@ -63,7 +67,7 @@ const EventListSummary: React.FC<IProps> = ({
// If we are only given few events then just pass them through
if (events.length < threshold) {
return (
<li className="mx_EventListSummary" data-scroll-tokens={eventIds} data-expanded={true}>
<li className="mx_EventListSummary" data-scroll-tokens={eventIds} data-expanded={true} data-layout={layout}>
{ children }
</li>
);
@ -92,7 +96,7 @@ const EventListSummary: React.FC<IProps> = ({
}
return (
<li className="mx_EventListSummary" data-scroll-tokens={eventIds} data-expanded={expanded + ""}>
<li className="mx_EventListSummary" data-scroll-tokens={eventIds} data-expanded={expanded + ""} data-layout={layout}>
<AccessibleButton className="mx_EventListSummary_toggle" onClick={toggleExpanded} aria-expanded={expanded}>
{ expanded ? _t('collapse') : _t('expand') }
</AccessibleButton>
@ -103,6 +107,7 @@ const EventListSummary: React.FC<IProps> = ({
EventListSummary.defaultProps = {
startExpanded: false,
layout: Layout.Group,
};
export default EventListSummary;

View file

@ -25,12 +25,15 @@ import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import { isValid3pidInvite } from "../../../RoomInvite";
import EventListSummary from "./EventListSummary";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { Layout } from '../../../settings/Layout';
interface IProps extends Omit<ComponentProps<typeof EventListSummary>, "summaryText" | "summaryMembers"> {
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
summaryLength?: number;
// The maximum number of avatars to display in the summary
avatarsMaxLength?: number;
// The currently selected layout
layout: Layout;
}
interface IUserEvents {
@ -67,6 +70,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
summaryLength: 1,
threshold: 3,
avatarsMaxLength: 5,
layout: Layout.Group,
};
shouldComponentUpdate(nextProps) {
@ -453,6 +457,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
startExpanded={this.props.startExpanded}
children={this.props.children}
summaryMembers={[...latestUserAvatarMember.values()]}
layout={this.props.layout}
summaryText={this.generateSummary(aggregate.names, orderedTransitionSequences)} />;
}
}

View file

@ -25,6 +25,7 @@ import { CallErrorCode, CallState } from 'matrix-js-sdk/src/webrtc/call';
import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip';
import classNames from 'classnames';
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
interface IProps {
mxEvent: MatrixEvent;
@ -69,6 +70,18 @@ export default class CallEvent extends React.Component<IProps, IState> {
this.setState({ callState: newState });
};
private renderCallBackButton(text: string): JSX.Element {
return (
<AccessibleButton
className="mx_CallEvent_content_button mx_CallEvent_content_button_callBack"
onClick={this.props.callEventGrouper.callBack}
kind="primary"
>
<span> { text } </span>
</AccessibleButton>
);
}
private renderContent(state: CallState | CustomCallState): JSX.Element {
if (state === CallState.Ringing) {
const silenceClass = classNames({
@ -103,8 +116,18 @@ export default class CallEvent extends React.Component<IProps, IState> {
}
if (state === CallState.Ended) {
const hangupReason = this.props.callEventGrouper.hangupReason;
const gotRejected = this.props.callEventGrouper.gotRejected;
const rejectParty = this.props.callEventGrouper.rejectParty;
if ([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason) {
if (gotRejected) {
const weDeclinedCall = MatrixClientPeg.get().getUserId() === rejectParty;
return (
<div className="mx_CallEvent_content">
{ weDeclinedCall ? _t("You declined this call") : _t("They declined this call") }
{ this.renderCallBackButton(weDeclinedCall ? _t("Call back") : _t("Call again")) }
</div>
);
} else if (([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason)) {
// workaround for https://github.com/vector-im/element-web/issues/5178
// it seems Android randomly sets a reason of "user hangup" which is
// interpreted as an error code :(
@ -116,6 +139,13 @@ export default class CallEvent extends React.Component<IProps, IState> {
{ _t("This call has ended") }
</div>
);
} else if (hangupReason === CallErrorCode.InviteTimeout) {
return (
<div className="mx_CallEvent_content">
{ _t("They didn't pick up") }
{ this.renderCallBackButton(_t("Call again")) }
</div>
);
}
let reason;
@ -133,8 +163,6 @@ export default class CallEvent extends React.Component<IProps, IState> {
// (as opposed to an error code they gave but we don't know about,
// in which case we show the error code)
reason = _t("An unknown error occurred");
} else if (hangupReason === CallErrorCode.InviteTimeout) {
reason = _t("No answer");
} else if (hangupReason === CallErrorCode.UserBusy) {
reason = _t("The user you called is busy.");
} else {
@ -163,13 +191,7 @@ export default class CallEvent extends React.Component<IProps, IState> {
return (
<div className="mx_CallEvent_content">
{ _t("You missed this call") }
<AccessibleButton
className="mx_CallEvent_content_button mx_CallEvent_content_button_callBack"
onClick={this.props.callEventGrouper.callBack}
kind="primary"
>
<span> { _t("Call back") } </span>
</AccessibleButton>
{ this.renderCallBackButton(_t("Call back")) }
</div>
);
}
@ -186,11 +208,17 @@ export default class CallEvent extends React.Component<IProps, IState> {
const sender = event.sender ? event.sender.name : event.getSender();
const isVoice = this.props.callEventGrouper.isVoice;
const callType = isVoice ? _t("Voice call") : _t("Video call");
const content = this.renderContent(this.state.callState);
const callState = this.state.callState;
const hangupReason = this.props.callEventGrouper.hangupReason;
const content = this.renderContent(callState);
const className = classNames({
mx_CallEvent: true,
mx_CallEvent_voice: isVoice,
mx_CallEvent_video: !isVoice,
mx_CallEvent_missed: (
callState === CustomCallState.Missed ||
(callState === CallState.Ended && hangupReason === CallErrorCode.InviteTimeout)
),
});
return (

View file

@ -896,6 +896,7 @@ export default class EventTile extends React.Component<IProps, IState> {
mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2E_STATE.UNKNOWN,
mx_EventTile_bad: isEncryptionFailure,
mx_EventTile_emote: msgtype === 'm.emote',
mx_EventTile_noSender: this.props.hideSender,
});
// If the tile is in the Sending state, don't speak the message.
@ -1165,8 +1166,9 @@ export default class EventTile extends React.Component<IProps, IState> {
/>
{ keyRequestInfo }
{ actionBar }
{ this.props.layout === Layout.IRC && (reactionsRow) }
</div>
{ reactionsRow }
{ this.props.layout !== Layout.IRC && (reactionsRow) }
{ msgOption }
</>)
);

View file

@ -146,7 +146,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
document.addEventListener("mousemove", this.onMoving);
document.addEventListener("mouseup", this.onEndMoving);
window.addEventListener("resize", this.snap);
window.addEventListener("resize", this.onResize);
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
}
@ -156,7 +156,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
document.removeEventListener("mousemove", this.onMoving);
document.removeEventListener("mouseup", this.onEndMoving);
window.removeEventListener("resize", this.snap);
window.removeEventListener("resize", this.onResize);
if (this.roomStoreToken) {
this.roomStoreToken.remove();
}
@ -164,6 +164,10 @@ export default class CallPreview extends React.Component<IProps, IState> {
SettingsStore.unwatchSetting(this.settingsWatcherRef);
}
private onResize = (): void => {
this.snap(false);
};
private animationCallback = () => {
// If the PiP isn't being dragged and there is only a tiny difference in
// the desiredTranslation and translation, quit the animationCallback
@ -207,7 +211,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
}
}
private snap = () => {
private snap(animate?: boolean): void {
const translationX = this.desiredTranslationX;
const translationY = this.desiredTranslationY;
// We subtract the PiP size from the window size in order to calculate
@ -236,10 +240,17 @@ export default class CallPreview extends React.Component<IProps, IState> {
this.desiredTranslationY = PADDING.top;
}
// We start animating here because we want the PiP to move when we're
// resizing the window
this.scheduledUpdate.mark();
};
if (animate) {
// We start animating here because we want the PiP to move when we're
// resizing the window
this.scheduledUpdate.mark();
} else {
this.setState({
translationX: this.desiredTranslationX,
translationY: this.desiredTranslationY,
});
}
}
private onRoomViewStoreUpdate = () => {
if (RoomViewStore.getRoomId() === this.state.roomId) return;
@ -310,7 +321,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
private onEndMoving = () => {
this.moving = false;
this.snap();
this.snap(true);
};
public render() {
@ -333,6 +344,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
secondaryCall={this.state.secondaryCall}
pipMode={true}
onMouseDownOnHeader={this.onStartMoving}
onResize={this.onResize}
/>
</div>
);

View file

@ -16,7 +16,7 @@ limitations under the License.
import classnames from 'classnames';
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import React, { createRef } from 'react';
import React from 'react';
import SettingsStore from "../../../settings/SettingsStore";
import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed';
import { logger } from 'matrix-js-sdk/src/logger';
@ -48,7 +48,7 @@ interface IState {
@replaceableComponent("views.voip.VideoFeed")
export default class VideoFeed extends React.Component<IProps, IState> {
private element = createRef<HTMLVideoElement>();
private element: HTMLVideoElement;
constructor(props: IProps) {
super(props);
@ -66,10 +66,10 @@ export default class VideoFeed extends React.Component<IProps, IState> {
componentWillUnmount() {
this.updateFeed(this.props.feed, null);
this.element.current?.removeEventListener('resize', this.onResize);
}
componentDidUpdate(prevProps: IProps) {
this.element?.addEventListener('resize', this.onResize);
this.updateFeed(prevProps.feed, this.props.feed);
}
@ -80,6 +80,13 @@ export default class VideoFeed extends React.Component<IProps, IState> {
};
}
private setElementRef = (element: HTMLVideoElement): void => {
if (!element) element.removeEventListener('resize', this.onResize);
this.element = element;
element.addEventListener('resize', this.onResize);
};
private updateFeed(oldFeed: CallFeed, newFeed: CallFeed) {
if (oldFeed === newFeed) return;
@ -94,7 +101,7 @@ export default class VideoFeed extends React.Component<IProps, IState> {
}
private playMedia() {
const element = this.element.current;
const element = this.element;
if (!element) return;
// We play audio in AudioFeed, not here
element.muted = true;
@ -117,7 +124,7 @@ export default class VideoFeed extends React.Component<IProps, IState> {
}
private stopMedia() {
const element = this.element.current;
const element = this.element;
if (!element) return;
element.pause();
@ -175,7 +182,7 @@ export default class VideoFeed extends React.Component<IProps, IState> {
);
} else {
return (
<video className={classnames(videoClasses)} ref={this.element} />
<video className={classnames(videoClasses)} ref={this.setElementRef} />
);
}
}