Files
im_webapp/src/widgets/attachment.jsx
T
Your NameandClaude Sonnet 5 251c9de87c Convert im_webapp from static mock prototypes to real backend integration
Implements all 11 product chapters (auth, messaging, contacts, groups,
files/favorites, calls, workplace/reports, settings, security) against the
real /v1/im REST API and native Tinode WS protocol, replacing the hardcoded
mock data the ba8f48ee prototype commit shipped with. Adds src/lib/im-api.js
as the REST client, plus supporting fixes across messages-view.jsx and the
attachment/upload/download pipeline (wrong SDK auth headers, group topic
naming, seq/timezone edge cases) found via live end-to-end testing against
the real backend.

Backup checkpoint before reconciling with origin/dev's parallel rewrite.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 11:40:53 +08:00

101 lines
3.5 KiB
React

import React from 'react';
import { FormattedMessage } from 'react-intl';
import FileProgress from './file-progress.jsx';
import { createFileDownloader } from '../lib/im-api.js';
import { bytesToHumanSize } from '../lib/strformat.js';
import { isUrlRelative, sanitizeUrl } from '../lib/utils.js';
export default class Attachment extends React.Component {
constructor(props) {
super(props);
this.state = {
downloader: null,
progress: 0
};
this.downloadFile = this.downloadFile.bind(this);
this.handleCancel = this.handleCancel.bind(this);
}
downloadFile(url, filename, mimetype) {
if (!url) {
this.props.onError("Invalid download URL '" + url + "'");
return;
}
// Same wrong-header bug as content-library-view.jsx's DownloadPage (X-Tinode-APIKey/
// X-Tinode-Auth, neither recognized by this backend) -- confirmed live: clicking "保存" on any
// real chat file attachment 403'd with "valid API key required". createFileDownloader() is the
// fixed-headers drop-in with the same download()/cancel() interface.
const downloader = createFileDownloader(this.props.tinode);
this.setState({downloader: downloader});
downloader.download(url, filename, mimetype,
loaded => this.setState({progress: loaded / this.props.size}),
err => this.props.onError(err, 'err')
)
.then(_ => this.setState({downloader: null, progress: 0}))
.catch(err => {
if (err) {
this.props.onError("Error downloading file: " + err.message, 'err');
}
this.setState({downloader: null, progress: 0});
});
}
handleCancel() {
if (this.props.uploading) {
this.props.onCancelUpload();
} else if (this.state.downloader) {
this.state.downloader.cancel();
}
}
render() {
let filename = this.props.filename || 'file_attachment';
if (filename.length > 36) {
filename = filename.substr(0, 16) + '...' + filename.substr(-16);
}
let size = this.props.size > 0 ?
<span className="small gray">({bytesToHumanSize(this.props.size)})</span> :
null;
// Detect if the download URL is relative or absolute.
// If the URL is relative use LargeFileHelper to attach authentication
// credentials to the request.
let url, helperFunc;
if (!this.props.uploading && !this.state.downloader && isUrlRelative(this.props.downloadUrl)) {
// Relative URL. Use download helper.
url = '#';
helperFunc = (e) => {
e.preventDefault();
this.downloadFile(this.props.downloadUrl, this.props.filename, this.props.mimetype);
};
} else {
url = sanitizeUrl(this.props.downloadUrl);
helperFunc = null;
}
const downloadWidget = <><i className="material-icons">file_download</i> <FormattedMessage id="save_attachment"
defaultMessage="save" description="Call to save an attachment" /></>;
return (
<div className="attachment">
<div><i className="material-icons big gray">insert_drive_file</i></div>
<div className="flex-column">
<div>{filename} {size}</div>
{this.props.uploading || this.state.downloader ?
<FileProgress progress={this.props.uploading ? this.props.progress : this.state.progress}
onCancel={this.handleCancel} />
:
<div>{url ?
<a href={url} download={this.props.filename} onClick={helperFunc}>{downloadWidget}</a>
:
<span className="light-gray">{downloadWidget}</span>
}</div>
}
</div>
</div>
);
}
};