Files
im_webapp/src/widgets/call-incoming.jsx
T
Your NameandClaude Sonnet 5 7db687602b 修复移动端语音通话胶囊被撑成方块、来电页认证徽章在长姓名截断时完全不可见
- #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>
2026-09-05 18:19:58 +08:00

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>
);
}
};