- #video-container.minimized 的移动端媒体查询规则(宽度100%/高度11rem/方角,本是给视频悬浮窗 修的)和 #video-container.audio-minimized(语音胶囊自己的auto/auto/无阴影)特异度相同 (1,1,0),前者写在文件更后面、源码顺序上赢了——语音通话点最小化后,移动端下胶囊被强行撑成 贯穿屏幕顶部的方形透明条,胶囊本体和还原按钮挤在左上角。补 :not(.audio-minimized) 排除。 - 来电全屏页认证徽章原来和姓名文字共用同一个 white-space:nowrap+ellipsis 的<h1>,姓名够长 触发省略号时,排在姓名后面的徽章(同一行内容里靠后的部分)跟着一起被截断出可视区域之外—— 不是"字体显示不全",是整个徽章都看不见。改成flex布局:姓名单独一个可截断的flex子项, 徽章作为flex:none的兄弟节点,不参与省略号裁切,姓名截多短徽章都能跟在后面。 均已用真实DOM结构/getComputedStyle确认修复生效。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
161 lines
5.0 KiB
React
161 lines
5.0 KiB
React
// IncomingCall: displays Accept & Reject buttons for incoming calls.
|
|
import React from 'react';
|
|
|
|
import BadgeList from './badge-list.jsx';
|
|
import LetterTile from './letter-tile.jsx';
|
|
|
|
import { MAX_TITLE_LENGTH, MAX_PEER_TITLE_LENGTH } from '../config.js';
|
|
import { CALL_STATE_INCOMING_RECEIVED } from '../constants.js';
|
|
|
|
import { makeImageUrl } from '../lib/blob-helpers.js';
|
|
import { truncateString } from '../lib/strformat.js';
|
|
|
|
const RING_SOUND = new Audio('audio/call-in.m4a');
|
|
|
|
export default class CallIncoming extends React.Component {
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
topic: null,
|
|
fullName: undefined,
|
|
avatar: null,
|
|
trustedBadges: [],
|
|
previousMetaDesc: undefined,
|
|
};
|
|
|
|
this.resetDesc = this.resetDesc.bind(this);
|
|
this.onMetaDesc = this.onMetaDesc.bind(this);
|
|
this.handleRejectCall = this.handleRejectCall.bind(this);
|
|
this.handleAcceptCall = this.handleAcceptCall.bind(this);
|
|
this.ringTimer = null;
|
|
}
|
|
|
|
componentDidMount() {
|
|
const topic = this.props.tinode.getTopic(this.props.topic);
|
|
if (!topic) {
|
|
return;
|
|
}
|
|
|
|
this.resetDesc(topic);
|
|
if (this.props.callState == CALL_STATE_INCOMING_RECEIVED) {
|
|
// play() throws if the user did not click the app first: https://goo.gl/xX8pDD.
|
|
RING_SOUND.play().catch(_ => {});
|
|
this.ringTimer = setInterval(_ => {RING_SOUND.play().catch(_ => {})}, 2000);
|
|
this.props.onRinging(this.props.topic, this.props.seq);
|
|
}
|
|
}
|
|
|
|
// No need to separately handle component mount.
|
|
componentDidUpdate(props) {
|
|
const topic = this.props.tinode.getTopic(props.topic);
|
|
if (!topic) {
|
|
return;
|
|
}
|
|
|
|
if (this.onMetaDesc != topic.onMetaDesc) {
|
|
this.previousMetaDesc = topic.onMetaDesc;
|
|
topic.onMetaDesc = this.onMetaDesc;
|
|
}
|
|
|
|
if (this.state.topic != props.topic) {
|
|
this.setState({topic: props.topic});
|
|
this.resetDesc(topic, props);
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
if (this.ringTimer != null) {
|
|
clearInterval(this.ringTimer);
|
|
RING_SOUND.pause();
|
|
}
|
|
const topic = this.props.tinode.getTopic(this.props.topic);
|
|
if (!topic) {
|
|
return;
|
|
}
|
|
this.setState({topic: null});
|
|
topic.onMetaDesc = this.previousMetaDesc;
|
|
}
|
|
|
|
resetDesc(topic) {
|
|
const badges = [];
|
|
if (topic.trusted) {
|
|
for (const [key, val] of Object.entries(topic.trusted)) {
|
|
if (val) {
|
|
badges.push(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.setState({
|
|
fullName: truncateString(topic.public ? topic.public.fn : undefined, MAX_TITLE_LENGTH),
|
|
avatar: makeImageUrl(topic.public ? topic.public.photo : null),
|
|
trustedBadges: badges,
|
|
});
|
|
}
|
|
|
|
onMetaDesc(desc) {
|
|
const topic = this.props.tinode.getTopic(this.props.topic);
|
|
if (!topic) {
|
|
return;
|
|
}
|
|
this.resetDesc(topic);
|
|
|
|
if (this.previousMetaDesc && this.previousMetaDesc != this.onMetaDesc) {
|
|
this.previousMetaDesc(desc);
|
|
}
|
|
}
|
|
|
|
handleAcceptCall() {
|
|
this.props.onAcceptCall(this.props.topic);
|
|
}
|
|
|
|
handleRejectCall() {
|
|
this.props.onReject(this.props.topic, this.props.seq);
|
|
this.props.onClose();
|
|
}
|
|
|
|
render() {
|
|
const isAudioOnly = !!this.props.audioOnly;
|
|
const ringing = this.props.callState == CALL_STATE_INCOMING_RECEIVED;
|
|
const surfaceClass = isAudioOnly ? 'call-audio-surface' : 'call-video-surface';
|
|
const photoStyle = this.state.avatar ? {'--incoming-call-photo': `url(${this.state.avatar})`} : null;
|
|
return (
|
|
<div className={`call-fullscreen incoming-call-overlay ${surfaceClass} incoming`}>
|
|
<div className="call-ambient" />
|
|
{this.state.avatar &&
|
|
<div className="incoming-call-photo-blur" style={photoStyle} />}
|
|
<div className="call-peer-card">
|
|
<div className="avatar-box call-product-avatar">
|
|
<LetterTile
|
|
authorizeURL={this.props.tinode.authorizeURL}
|
|
avatar={this.state.avatar || true}
|
|
topic={this.props.topic}
|
|
title={this.state.fullName} />
|
|
</div>
|
|
<h1><span>{truncateString(this.state.fullName, MAX_PEER_TITLE_LENGTH)}</span>
|
|
<BadgeList short={true} trustedBadges={this.state.trustedBadges} /></h1>
|
|
<p>{isAudioOnly ? '对方邀请你语音通话...' : '对方邀请你视频通话...'}</p>
|
|
{ringing && <span className="call-ringing-dots"><i /><i /><i /></span>}
|
|
</div>
|
|
<footer className="call-incoming-actions">
|
|
{ringing ?
|
|
<>
|
|
<button type="button" className="call-control danger" onClick={this.handleRejectCall}>
|
|
<span><i className="material-icons">call_end</i></span>
|
|
<small>拒绝</small>
|
|
</button>
|
|
<button type="button" className="call-control" onClick={this.handleAcceptCall}>
|
|
<span><i className="material-icons">{isAudioOnly ? 'call' : 'videocam'}</i></span>
|
|
<small>接听</small>
|
|
</button>
|
|
</>
|
|
:
|
|
null
|
|
}
|
|
</footer>
|
|
</div>
|
|
);
|
|
}
|
|
};
|