Files
ThreadNet-Web/src/components/views/messages/LegacyCallEvent.tsx
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

300 lines
13 KiB
TypeScript
Raw Normal View History

2021-05-29 21:16:02 +02:00
/*
2024-09-09 14:57:16 +01:00
Copyright 2024 New Vector Ltd.
2021-05-29 21:16:02 +02:00
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
2024-09-09 14:57:16 +01:00
Please see LICENSE files in the repository root for full details.
2021-05-29 21:16:02 +02:00
*/
2021-08-05 11:47:58 +02:00
import React, { createRef } from "react";
2025-02-05 13:25:06 +00:00
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
2021-10-22 17:23:32 -05:00
import { CallErrorCode, CallState } from "matrix-js-sdk/src/webrtc/call";
import classNames from "classnames";
2021-08-27 16:23:06 +02:00
import { _t } from "../../../languageHandler";
import MemberAvatar from "../avatars/MemberAvatar";
2025-02-05 13:25:06 +00:00
import type LegacyCallEventGrouper from "../../structures/LegacyCallEventGrouper";
import { LegacyCallEventGrouperEvent } from "../../structures/LegacyCallEventGrouper";
2021-06-21 16:49:10 +02:00
import AccessibleButton from "../elements/AccessibleButton";
2021-06-01 14:46:41 +02:00
import InfoTooltip, { InfoTooltipKind } from "../elements/InfoTooltip";
2022-11-29 16:21:51 -05:00
import { formatPreciseDuration } from "../../../DateUtils";
2021-08-27 16:23:06 +02:00
import Clock from "../audio_messages/Clock";
2021-05-29 21:16:02 +02:00
2021-08-16 10:08:30 +02:00
const MAX_NON_NARROW_WIDTH = (450 / 70) * 100;
2021-08-05 11:47:58 +02:00
2021-05-29 21:16:02 +02:00
interface IProps {
mxEvent: MatrixEvent;
callEventGrouper: LegacyCallEventGrouper;
timestamp?: JSX.Element;
2021-05-29 21:16:02 +02:00
}
2021-06-01 09:30:37 +02:00
interface IState {
callState?: CallState;
2021-06-19 20:02:51 +02:00
silenced: boolean;
2021-08-05 11:47:58 +02:00
narrow: boolean;
2021-08-27 16:23:06 +02:00
length: number;
2021-06-01 09:30:37 +02:00
}
export default class LegacyCallEvent extends React.PureComponent<IProps, IState> {
2021-08-05 11:47:58 +02:00
private wrapperElement = createRef<HTMLDivElement>();
private resizeObserver?: ResizeObserver;
2021-08-05 11:47:58 +02:00
public constructor(props: IProps) {
2021-06-01 09:30:37 +02:00
super(props);
this.state = {
2021-06-02 10:18:32 +02:00
callState: this.props.callEventGrouper.state,
2021-06-19 20:02:51 +02:00
silenced: false,
2021-08-05 11:47:58 +02:00
narrow: false,
2021-08-27 16:23:06 +02:00
length: 0,
2021-07-02 13:20:02 +02:00
};
2021-06-01 09:30:37 +02:00
}
public componentDidMount(): void {
this.props.callEventGrouper.addListener(LegacyCallEventGrouperEvent.StateChanged, this.onStateChanged);
this.props.callEventGrouper.addListener(LegacyCallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
this.props.callEventGrouper.addListener(LegacyCallEventGrouperEvent.LengthChanged, this.onLengthChanged);
2021-08-05 11:47:58 +02:00
2022-02-18 13:51:27 +00:00
this.resizeObserver = new ResizeObserver(this.resizeObserverCallback);
2024-10-16 16:43:07 +01:00
if (this.wrapperElement.current) this.resizeObserver.observe(this.wrapperElement.current);
2021-06-01 09:30:37 +02:00
}
public componentWillUnmount(): void {
this.props.callEventGrouper.removeListener(LegacyCallEventGrouperEvent.StateChanged, this.onStateChanged);
this.props.callEventGrouper.removeListener(LegacyCallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
this.props.callEventGrouper.removeListener(LegacyCallEventGrouperEvent.LengthChanged, this.onLengthChanged);
2021-08-05 11:47:58 +02:00
this.resizeObserver?.disconnect();
2021-06-01 09:30:37 +02:00
}
2021-08-27 16:23:06 +02:00
private onLengthChanged = (length: number): void => {
this.setState({ length });
};
2021-08-05 11:47:58 +02:00
private resizeObserverCallback = (entries: ResizeObserverEntry[]): void => {
const wrapperElementEntry = entries.find((entry) => entry.target === this.wrapperElement.current);
if (!wrapperElementEntry) return;
this.setState({ narrow: wrapperElementEntry.contentRect.width < MAX_NON_NARROW_WIDTH });
};
private onSilencedChanged = (newState: boolean): void => {
2021-06-19 20:02:51 +02:00
this.setState({ silenced: newState });
};
private onStateChanged = (newState: CallState): void => {
2021-07-02 13:20:02 +02:00
this.setState({ callState: newState });
2021-06-19 20:02:51 +02:00
};
2021-06-01 09:30:37 +02:00
2021-07-25 10:30:53 +02:00
private renderCallBackButton(text: string): JSX.Element {
return (
<AccessibleButton
className="mx_LegacyCallEvent_content_button mx_LegacyCallEvent_content_button_callBack"
2021-07-25 10:30:53 +02:00
onClick={this.props.callEventGrouper.callBack}
kind="primary"
>
<span> {text} </span>
</AccessibleButton>
);
}
2021-08-05 11:47:58 +02:00
private renderSilenceIcon(): JSX.Element {
const silenceClass = classNames({
mx_LegacyCallEvent_iconButton: true,
mx_LegacyCallEvent_unSilence: this.state.silenced,
mx_LegacyCallEvent_silence: !this.state.silenced,
2021-08-05 11:47:58 +02:00
});
return (
<AccessibleButton
2021-08-05 11:47:58 +02:00
className={silenceClass}
onClick={this.props.callEventGrouper.toggleSilenced}
title={this.state.silenced ? _t("voip|unsilence") : _t("voip|silence")}
2021-08-05 11:47:58 +02:00
/>
);
}
private renderContent(): JSX.Element {
if (this.state.callState === CallState.Ringing) {
2021-08-05 11:47:58 +02:00
let silenceIcon;
if (!this.state.narrow) {
silenceIcon = this.renderSilenceIcon();
}
2021-06-19 20:02:51 +02:00
2021-06-01 14:46:41 +02:00
return (
<div className="mx_LegacyCallEvent_content">
2021-08-05 11:47:58 +02:00
{silenceIcon}
2021-06-21 16:49:10 +02:00
<AccessibleButton
className="mx_LegacyCallEvent_content_button mx_LegacyCallEvent_content_button_reject"
2021-07-20 13:20:30 +02:00
onClick={this.props.callEventGrouper.rejectCall}
2021-06-01 10:03:17 +02:00
kind="danger"
2021-06-21 16:49:10 +02:00
>
<span> {_t("action|decline")} </span>
2021-06-21 16:49:10 +02:00
</AccessibleButton>
<AccessibleButton
className="mx_LegacyCallEvent_content_button mx_LegacyCallEvent_content_button_answer"
2021-07-20 13:20:30 +02:00
onClick={this.props.callEventGrouper.answerCall}
2021-06-01 10:03:17 +02:00
kind="primary"
2021-06-21 16:49:10 +02:00
>
<span> {_t("action|accept")} </span>
2021-06-21 16:49:10 +02:00
</AccessibleButton>
{this.props.timestamp}
2021-06-01 10:03:17 +02:00
</div>
);
2021-06-01 14:46:41 +02:00
}
if (this.state.callState === CallState.Ended) {
2021-06-02 10:18:32 +02:00
const hangupReason = this.props.callEventGrouper.hangupReason;
2021-07-25 10:30:53 +02:00
const gotRejected = this.props.callEventGrouper.gotRejected;
2021-06-01 14:46:41 +02:00
2021-07-25 10:30:53 +02:00
if (gotRejected) {
return (
<div className="mx_LegacyCallEvent_content">
{_t("timeline|m.call.invite|declined")}
{this.renderCallBackButton(_t("timeline|m.call.invite|call_back_prompt"))}
{this.props.timestamp}
2021-07-25 10:30:53 +02:00
</div>
);
} else if (hangupReason === CallErrorCode.AnsweredElsewhere) {
return (
<div className="mx_LegacyCallEvent_content">
{_t("timeline|m.call.invite|answered_elsewhere")}
{this.props.timestamp}
</div>
);
} else if (this.props.callEventGrouper.callWasMissed) {
return (
<div className="mx_LegacyCallEvent_content">
{_t("timeline|m.call.invite|missed_call")}
{this.renderCallBackButton(_t("timeline|m.call.invite|call_back_prompt"))}
{this.props.timestamp}
</div>
);
} else if (!hangupReason || [CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) {
2021-06-01 14:46:41 +02:00
// 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 :(
// https://github.com/vector-im/riot-android/issues/2623
// Also the correct hangup code as of VoIP v1 (with underscore)
// Also, if we don't have a reason
2022-11-29 16:21:51 -05:00
const duration = this.props.callEventGrouper.duration!;
let text = _t("timeline|m.call.hangup|dm");
2021-08-06 14:38:53 +02:00
if (duration) {
2022-11-29 16:21:51 -05:00
text += " • " + formatPreciseDuration(duration);
2021-08-06 13:59:26 +02:00
}
2021-06-01 14:46:41 +02:00
return (
<div className="mx_LegacyCallEvent_content">
2021-08-06 13:59:26 +02:00
{text}
{this.props.timestamp}
2021-06-01 14:46:41 +02:00
</div>
);
} else if (hangupReason === CallErrorCode.InviteTimeout) {
return (
<div className="mx_LegacyCallEvent_content">
{_t("timeline|m.call.invite|no_answer")}
{this.renderCallBackButton(_t("timeline|m.call.invite|call_back_prompt"))}
{this.props.timestamp}
</div>
);
2021-06-01 14:46:41 +02:00
}
let reason;
2021-06-02 10:42:58 +02:00
if (hangupReason === CallErrorCode.IceFailed) {
2021-06-01 14:46:41 +02:00
// We couldn't establish a connection at all
reason = _t("timeline|m.call.invite|failed_connect_media");
2021-06-01 14:46:41 +02:00
} else if (hangupReason === "ice_timeout") {
// We established a connection but it died
reason = _t("timeline|m.call.invite|failed_connection");
2021-06-02 10:42:58 +02:00
} else if (hangupReason === CallErrorCode.NoUserMedia) {
2021-06-01 14:46:41 +02:00
// The other side couldn't open capture devices
reason = _t("timeline|m.call.invite|failed_opponent_media");
2021-06-01 14:46:41 +02:00
} else if (hangupReason === "unknown_error") {
// An error code the other side doesn't have a way to express
// (as opposed to an error code they gave but we don't know about,
// in which case we show the error code)
reason = _t("timeline|m.call.invite|unknown_error");
2021-06-02 10:42:58 +02:00
} else if (hangupReason === CallErrorCode.UserBusy) {
reason = _t("voip|user_busy_description");
2021-06-01 14:46:41 +02:00
} else {
reason = _t("timeline|m.call.invite|unknown_failure", { reason: hangupReason });
2021-06-01 14:46:41 +02:00
}
return (
<div className="mx_LegacyCallEvent_content">
2021-06-01 14:46:41 +02:00
<InfoTooltip
tooltip={reason}
className="mx_LegacyCallEvent_content_tooltip"
2021-06-01 14:46:41 +02:00
kind={InfoTooltipKind.Warning}
/>
{_t("timeline|m.call.invite|failed_connection")}
{this.renderCallBackButton(_t("action|retry"))}
{this.props.timestamp}
2021-06-01 14:46:41 +02:00
</div>
);
}
if (this.state.callState === CallState.Connected) {
2021-06-01 14:46:41 +02:00
return (
<div className="mx_LegacyCallEvent_content">
<Clock seconds={this.state.length} aria-live="off" />
{this.props.timestamp}
2021-08-27 16:23:06 +02:00
</div>
);
}
if (this.state.callState === CallState.Connecting) {
2021-08-27 16:23:06 +02:00
return (
<div className="mx_LegacyCallEvent_content">
{_t("voip|connecting")}
{this.props.timestamp}
2021-06-01 10:33:44 +02:00
</div>
);
2021-06-01 14:46:41 +02:00
}
2021-06-01 07:55:55 +02:00
2021-06-01 14:46:41 +02:00
return (
<div className="mx_LegacyCallEvent_content">
{_t("timeline|m.call.invite|unknown_state")}
{this.props.timestamp}
2021-06-01 14:46:41 +02:00
</div>
);
}
public render(): React.ReactNode {
2021-06-01 14:46:41 +02:00
const event = this.props.mxEvent;
const sender = event.sender ? event.sender.name : event.getSender();
2021-06-17 17:55:18 +02:00
const isVoice = this.props.callEventGrouper.isVoice;
const callType = isVoice ? _t("voip|voice_call") : _t("voip|video_call");
const callState = this.state.callState;
const hangupReason = this.props.callEventGrouper.hangupReason;
const content = this.renderContent();
const className = classNames("mx_LegacyCallEvent", {
mx_LegacyCallEvent_voice: isVoice,
mx_LegacyCallEvent_video: !isVoice,
mx_LegacyCallEvent_narrow: this.state.narrow,
mx_LegacyCallEvent_missed: this.props.callEventGrouper.callWasMissed,
mx_LegacyCallEvent_noAnswer: callState === CallState.Ended && hangupReason === CallErrorCode.InviteTimeout,
mx_LegacyCallEvent_rejected: callState === CallState.Ended && this.props.callEventGrouper.gotRejected,
2021-07-02 13:20:02 +02:00
});
2021-08-05 11:47:58 +02:00
let silenceIcon;
if (this.state.narrow && this.state.callState === CallState.Ringing) {
silenceIcon = this.renderSilenceIcon();
}
2021-06-01 14:46:41 +02:00
2021-05-29 21:16:02 +02:00
return (
<div className="mx_LegacyCallEvent_wrapper" ref={this.wrapperElement}>
2021-08-05 11:47:58 +02:00
<div className={className}>
{silenceIcon}
<div className="mx_LegacyCallEvent_info">
<MemberAvatar member={event.sender} size="32px" />
<div className="mx_LegacyCallEvent_info_basic">
<div className="mx_LegacyCallEvent_sender">{sender}</div>
<div className="mx_LegacyCallEvent_type">
<div className="mx_LegacyCallEvent_type_icon" />
2021-08-05 11:47:58 +02:00
{callType}
</div>
2021-06-01 07:55:55 +02:00
</div>
</div>
2021-08-05 11:47:58 +02:00
{content}
2021-05-29 21:16:02 +02:00
</div>
</div>
);
}
}