Files
im_webapp/umd/src_widgets_audio-recorder_jsx.dev.js
T
Your NameandClaude Sonnet 5 d759f67b11 第八轮最小粒度测试:修复CSS选择器/JSX结构不匹配的一整类bug,清理Tinode品牌残留
第八轮改用"读CSS选择器+核对JSX结构"的方式复测全部11章,命中了7轮纯接口测试从未发现的一类
问题——选择器写对了但目标元素的实际DOM结构/className对不上,样式静默失效:

- 语音通话最小化胶囊按钮尺寸被同特异度规则覆盖;移动端悬浮通话窗口left/right/width三值同时
  生效导致过度约束;来电全屏页认证徽章/姓名标题缺少专属样式覆盖
- 我的收藏-语音卡片三列grid对应实际只有1个子元素,播放器被压缩到卡片左侧一角;链接/记录头部
  完全没有基础样式,退化成浏览器默认外观
- 多选模式下"收到的"消息气泡被flex auto-margin一起拖到最右侧;.avatar-box/#send-message-panel
  两处逗号断句错误导致后半句选择器丢失祖先限定
- 按住说话悬浮反馈渲染成内联胶囊,而参考图要求的居中悬浮卡片样式其实已经存在(挂在了另一个非
  生产demo组件上);批量转发确认框沿用了80%视口高度;audio-player.jsx的playback类点在canvas
  上而非包裹层,波形和计时器可能纵向堆叠
- "消息已撤回"占位符误拼了日期分隔条的class而非专属的deleted

品牌清理:浏览器标签页标题、未读数标题更新均硬编码"Tinode";LINK_CONTACT_US等三个链接常量
指向开源Tinode项目自己的域名(联系我们改为跟用户条款/隐私协议一致的toast,不再发起外部mailto)。

以及:空会话丢失.chat-box样式类、青少年模式PIN定位、若干收藏/文件类型展示细节。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 17:09:42 +08:00

424 lines
16 KiB
JavaScript

"use strict";
(self["webpackChunktinode_webapp"] = self["webpackChunktinode_webapp"] || []).push([["src_widgets_audio-recorder_jsx"],{
/***/ "./src/widgets/audio-recorder.jsx":
/*!****************************************!*\
!*** ./src/widgets/audio-recorder.jsx ***!
\****************************************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "react");
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react-intl */ "react-intl");
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var _audio_player_jsx__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./audio-player.jsx */ "./src/widgets/audio-player.jsx");
/* harmony import */ var webm_duration_fix__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! webm-duration-fix */ "./node_modules/webm-duration-fix/lib/index.js");
/* harmony import */ var webm_duration_fix__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(webm_duration_fix__WEBPACK_IMPORTED_MODULE_3__);
/* harmony import */ var _lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../lib/blob-helpers.js */ "./src/lib/blob-helpers.js");
/* harmony import */ var _lib_strformat__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../lib/strformat */ "./src/lib/strformat.js");
/* harmony import */ var _config_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../config.js */ "./src/config.js");
/* harmony import */ var react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! react/jsx-dev-runtime */ "./node_modules/react/jsx-dev-runtime.js");
const BUFFER_SIZE = 256;
const CANVAS_UPSCALING = 2.0;
const LINE_WIDTH = 3 * CANVAS_UPSCALING;
const SPACING = 2 * CANVAS_UPSCALING;
const MILLIS_PER_BAR = 100;
const BAR_COLOR = '#BBBD';
const BAR_SCALE = 64.0;
const VISUALIZATION_BARS = 96;
const MAX_SAMPLES_PER_BAR = 10;
const DEFAULT_AUDIO_MIME_TYPE = 'audio/webm';
const SAFARI_AUDIO_MIME_TYPE = 'audio/mp4';
const AUDIO_MIME_TYPES = [DEFAULT_AUDIO_MIME_TYPE, SAFARI_AUDIO_MIME_TYPE, ''];
const messages = (0,react_intl__WEBPACK_IMPORTED_MODULE_1__.defineMessages)({
icon_title_delete: {
id: 'icon_title_delete',
defaultMessage: 'Delete recording',
description: 'Icon tool tip for deleting recorded audio'
},
icon_title_pause: {
id: 'icon_title_pause',
defaultMessage: 'Pause playback',
description: 'Icon tool tip for pausing audio playback'
},
icon_title_resume: {
id: 'icon_title_resume',
defaultMessage: 'Resume playback',
description: 'Icon tool tip for resuming audio playback'
},
icon_title_send: {
id: 'icon_title_send',
defaultMessage: 'Send message',
description: 'Icon tool tip for sending a message'
},
failed_to_init_audio: {
id: 'failed_to_init_audio',
defaultMessage: 'Failed to initialize audio recording',
description: 'Error message when audio is not available'
}
});
class AudioRecorder extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComponent) {
constructor(props) {
super(props);
this.state = {
enabled: true,
audioRecord: null,
recording: true,
paused: false,
duration: '0:00',
blobUrl: null,
preview: null
};
this.visualize = this.visualize.bind(this);
this.initMediaRecording = this.initMediaRecording.bind(this);
this.initCanvas = this.initCanvas.bind(this);
this.getRecording = this.getRecording.bind(this);
this.cleanUp = this.cleanUp.bind(this);
this.handleResume = this.handleResume.bind(this);
this.handlePause = this.handlePause.bind(this);
this.handleDelete = this.handleDelete.bind(this);
this.handleDone = this.handleDone.bind(this);
this.durationMillis = 0;
this.startedOn = null;
this.viewBuffer = [];
this.canvasRef = react__WEBPACK_IMPORTED_MODULE_0___default().createRef();
this.recordingTimestamp = 0;
}
componentDidMount() {
this.stream = null;
this.mediaRecorder = null;
this.audioContext = null;
this.audioInput = null;
this.analyser = null;
this.audioChunks = [];
try {
navigator.mediaDevices.getUserMedia({
audio: true,
video: false
}).then(this.initMediaRecording, this.props.onError);
} catch (err) {
this.props.onError(err);
}
}
componentWillUnmount() {
this._unmounted = true;
this.startedOn = null;
if (this.stream) {
this.cleanUp();
}
}
visualize() {
this.initCanvas();
const pcmData = new Uint8Array(this.analyser.frequencyBinCount);
const width = this.canvasWidth;
const height = this.canvasHeight;
const viewLength = width / (LINE_WIDTH + SPACING) | 0;
const viewDuration = MILLIS_PER_BAR * viewLength;
this.canvasContext.lineWidth = LINE_WIDTH;
this.canvasContext.strokeStyle = BAR_COLOR;
let prevBarCount = 0;
let volume = 0.0;
let countPerBar = 0;
const drawFrame = _ => {
if (!this.startedOn) {
return;
}
window.requestAnimationFrame(drawFrame);
const duration = this.durationMillis + (Date.now() - this.startedOn);
this.setState({
duration: (0,_lib_strformat__WEBPACK_IMPORTED_MODULE_5__.secondsToTime)(duration / 1000)
});
if (duration > _config_js__WEBPACK_IMPORTED_MODULE_6__.MAX_DURATION) {
this.startedOn = null;
this.mediaRecorder.pause();
this.durationMillis = duration;
this.setState({
enabled: false,
recording: false,
duration: (0,_lib_strformat__WEBPACK_IMPORTED_MODULE_5__.secondsToTime)(this.durationMillis / 1000)
});
}
this.analyser.getByteTimeDomainData(pcmData);
let amp = 0.0;
for (const amplitude of pcmData) {
amp += (amplitude - 127) ** 2;
}
volume += Math.sqrt(amp / pcmData.length);
countPerBar++;
let barCount = duration / MILLIS_PER_BAR | 0;
const dx = viewDuration > duration ? 0 : (duration - MILLIS_PER_BAR * barCount) / MILLIS_PER_BAR * (LINE_WIDTH + SPACING);
if (prevBarCount != barCount) {
prevBarCount = barCount;
this.viewBuffer.push(volume / countPerBar);
volume = 0.0;
countPerBar = 0;
if (this.viewBuffer.length > viewLength) {
this.viewBuffer.shift();
}
}
this.canvasContext.clearRect(0, 0, width, height);
this.canvasContext.beginPath();
for (let i = 0; i < this.viewBuffer.length; i++) {
let x = i * (LINE_WIDTH + SPACING) - dx;
let y = Math.max(Math.min(this.viewBuffer[i] / BAR_SCALE, 0.9) * height, 1);
this.canvasContext.moveTo(x, (height - y) * 0.5);
this.canvasContext.lineTo(x, (height + y) * 0.5);
}
this.canvasContext.stroke();
const now = new Date().getTime();
if (now - this.recordingTimestamp > _config_js__WEBPACK_IMPORTED_MODULE_6__.KEYPRESS_DELAY) {
this.props.onRecordingProgress();
this.recordingTimestamp = now;
}
};
drawFrame();
}
handlePause(e) {
e.preventDefault();
this.mediaRecorder.pause();
this.mediaRecorder.requestData();
this.durationMillis += Date.now() - this.startedOn;
this.startedOn = null;
this.setState({
recording: false
});
}
handleResume(e) {
e.preventDefault();
if (this.state.enabled) {
this.startedOn = Date.now();
this.mediaRecorder.resume();
this.setState({
recording: true
}, this.visualize);
}
}
handleDelete(e) {
e.preventDefault();
this.durationMillis = 0;
this.startedOn = null;
if (this.mediaRecorder) {
this.mediaRecorder.stop();
}
this.cleanUp();
this.setState({
recording: false
});
if (!this.mediaRecorder && this.props.onDeleted) {
this.props.onDeleted();
}
}
handleDone(e) {
e.preventDefault();
this.setState({
recording: false
});
if (this.startedOn) {
this.durationMillis += Date.now() - this.startedOn;
this.startedOn = null;
}
if (this.mediaRecorder) {
this.mediaRecorder.stop();
} else if (this.props.onDeleted) {
this.props.onDeleted();
}
}
initCanvas() {
this.canvasRef.current.width = this.canvasRef.current.offsetWidth * CANVAS_UPSCALING;
this.canvasRef.current.height = this.canvasRef.current.offsetHeight * CANVAS_UPSCALING;
this.canvasContext = this.canvasRef.current.getContext('2d');
this.canvasContext.lineCap = 'round';
this.canvasContext.translate(0.5, 0.5);
this.canvasWidth = this.canvasRef.current.width;
this.canvasHeight = this.canvasRef.current.height;
}
initMediaRecording(stream) {
if (this._unmounted) {
stream.getTracks().forEach(t => t.stop());
return;
}
this.stream = stream;
AUDIO_MIME_TYPES.some(mimeType => {
if (MediaRecorder.isTypeSupported(mimeType)) {
this.mediaRecorder = new MediaRecorder(stream, {
mimeType: mimeType,
audioBitsPerSecond: 24_000
});
return true;
}
return false;
});
if (!this.mediaRecorder) {
console.warn('MediaRecorder failed to initialize: no supported audio formats');
this.props.onError(this.props.intl.formatMessage(messages.failed_to_init_audio));
return;
}
this.audioContext = new AudioContext();
this.audioInput = this.audioContext.createMediaStreamSource(stream);
if (!this.audioInput) {
console.warn('createMediaStreamSource returned null: audio input unavailable');
this.props.onError(this.props.intl.formatMessage(messages.failed_to_init_audio));
return;
}
this.analyser = this.audioContext.createAnalyser();
this.analyser.fftSize = BUFFER_SIZE;
this.audioInput.connect(this.analyser);
this.mediaRecorder.onstop = _ => {
if (this.durationMillis > _config_js__WEBPACK_IMPORTED_MODULE_6__.MIN_DURATION) {
this.getRecording(this.mediaRecorder.mimeType).then(result => this.props.onFinished(result.url, result.preview, this.durationMillis));
} else {
this.props.onDeleted();
}
this.cleanUp();
};
this.mediaRecorder.ondataavailable = e => {
if (e.data.size > 0) {
this.audioChunks.push(e.data);
}
if (this.mediaRecorder.state != 'inactive') {
this.getRecording(this.mediaRecorder.mimeType).then(result => {
this.setState({
blobUrl: result.url,
preview: result.preview
});
});
}
};
this.durationMillis = 0;
this.startedOn = Date.now();
this.mediaRecorder.start();
this.visualize();
this.props.onRecordingProgress();
this.recordingTimestamp = this.startedOn;
}
getRecording(mimeType) {
mimeType = mimeType || DEFAULT_AUDIO_MIME_TYPE;
let blob = new Blob(this.audioChunks, {
type: mimeType
});
const result = mimeType == DEFAULT_AUDIO_MIME_TYPE ? webm_duration_fix__WEBPACK_IMPORTED_MODULE_3___default()(blob, mimeType) : Promise.resolve(blob);
return result.then(fixedBlob => {
blob = fixedBlob;
return fixedBlob.arrayBuffer();
}).then(arrayBuff => this.audioContext.decodeAudioData(arrayBuff)).then(decoded => this.createPreview(decoded)).then(preview => ({
url: window.URL.createObjectURL(blob),
preview: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_4__.intArrayToBase64)(preview)
}));
}
createPreview(audio) {
const data = audio.getChannelData(0);
const viewLength = Math.min(data.length, VISUALIZATION_BARS);
const totalSPB = data.length / viewLength | 0;
const samplingRate = Math.max(1, totalSPB / MAX_SAMPLES_PER_BAR | 0);
let buffer = [];
let max = -1;
for (let i = 0; i < viewLength; i++) {
let amplitude = 0;
let count = 0;
for (let j = 0; j < totalSPB; j += samplingRate) {
amplitude += data[totalSPB * i + j] ** 2;
count++;
}
const val = Math.sqrt(amplitude / count);
buffer.push(val);
max = Math.max(max, val);
}
if (max > 0) {
buffer = buffer.map(a => 100 * a / max | 0);
}
return buffer;
}
cleanUp() {
if (this.audioInput) {
this.audioInput.disconnect();
}
if (this.stream) {
this.stream.getTracks().forEach(track => track.stop());
}
}
render() {
const {
formatMessage
} = this.props.intl;
const resumeClass = 'material-icons ' + (this.state.enabled ? 'red' : 'gray');
if (this.props.holdMode) {
return (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("div", {
className: 'compose-record-overlay' + (this.props.cancelling ? ' cancel' : ''),
role: "status",
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("i", {
className: "material-icons",
children: this.props.cancelling ? 'delete_outline' : 'mic'
}, void 0, false), this.state.recording ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("canvas", {
ref: this.canvasRef
}, void 0, false) : null, (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("strong", {
children: this.props.cancelling ? '松开手指,取消发送' : '手指上滑,取消发送'
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("small", {
children: this.state.duration
}, void 0, false)]
}, void 0, true);
}
return (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("div", {
className: "audio",
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("a", {
href: "#",
onClick: this.handleDelete,
title: formatMessage(messages.icon_title_delete),
children: (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("i", {
className: "material-icons gray",
children: "delete_outline"
}, void 0, false)
}, void 0, false), this.state.recording ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("canvas", {
ref: this.canvasRef
}, void 0, false) : (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)(_audio_player_jsx__WEBPACK_IMPORTED_MODULE_2__["default"], {
src: this.state.blobUrl,
preview: this.state.preview,
duration: this.durationMillis,
short: true
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("div", {
className: "duration",
children: this.state.duration
}, void 0, false), this.state.recording ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("a", {
href: "#",
onClick: this.handlePause,
title: formatMessage(messages.icon_title_pause),
children: (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("i", {
className: "material-icons",
children: "pause_circle_outline"
}, void 0, false)
}, void 0, false) : (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("a", {
href: "#",
onClick: this.handleResume,
title: formatMessage(messages.icon_title_resume),
children: (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("i", {
className: resumeClass,
children: "radio_button_checked"
}, void 0, false)
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("a", {
href: "#",
onClick: this.handleDone,
title: formatMessage(messages.icon_title_send),
children: (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("i", {
className: "material-icons",
children: "send"
}, void 0, false)
}, void 0, false)]
}, void 0, true);
}
}
/* harmony default export */ __webpack_exports__["default"] = ((0,react_intl__WEBPACK_IMPORTED_MODULE_1__.injectIntl)(AudioRecorder, {
forwardRef: true
}));
/***/ })
}]);
//# sourceMappingURL=src_widgets_audio-recorder_jsx.dev.js.map