adding TheCard preview

This commit is contained in:
or-else
2026-01-17 12:55:45 +03:00
parent 930f4f0656
commit dea781b423
36 changed files with 7720 additions and 201 deletions
+97
View File
@@ -0,0 +1,97 @@
import React from 'react';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import { TheCard } from 'tinode-sdk';
import LetterTile from './letter-tile.jsx';
const messages = defineMessages({
unknown_name: {
id: 'unknown_name',
defaultMessage: 'Unknown',
description: 'Name to show when the name is missing'
}
});
class TheCardMini extends React.PureComponent {
constructor(props) {
super(props);
this.handleDownload = this.handleDownload.bind(this);
this.handleMessage = this.handleMessage.bind(this);
}
handleDownload(e) {
e.preventDefault();
try {
const vcard = TheCard.exportVCard(this.props.content);
const blob = new Blob([vcard], { type: 'text/vcard' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = (this.props.content.fn || 'contact') + '.vcf';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (err) {
console.error("Failed to download vcard", err);
if (this.props.onError) {
this.props.onError("Failed to download vcard", 'err');
}
}
}
handleMessage(e) {
// This is handled by the parent component via props.onAction which expects a click event.
}
render() {
const card = this.props.content;
if (!card) {
return null;
}
const uid = TheCard.getFirstTinodeID(card);
const contacts = TheCard.getEmails(card).concat(TheCard.getPhones(card));
const org = TheCard.getOrg(card);
return (
<div className="contact-card">
<div className="contact-body">
<div className="avatar-box">
<LetterTile
authorizeURL={this.props.authorizeURL}
topic={uid || 'usr123XXX'}
avatar={TheCard.getPhotoUrl(card) || true}
title={card.fn} />
</div>
<div className="name-box">
<div className="name">{card.fn || <FormattedMessage {...messages.unknown_name} />}</div>
{org && <div className="org">{org}</div>}
</div>
</div>
<div className="contact-actions">
{uid &&
<a className="flat-button" data-val={uid} onClick={this.props.onChatClick}>
<FormattedMessage id="chat_now" defaultMessage="Chat"
description="Label for message button in contact card" />
</a>
}
{uid && <div className="divider"></div>}
{contacts.length > 0 &&
<a className="flat-button" data-val={contacts.join(',')} onClick={this.props.onChatClick}>
<FormattedMessage id="find_user" defaultMessage="Find"
description="Label for search button in contact card" />
</a>
}
{contacts.length > 0 && <div className="divider"></div>}
<a className="flat-button" onClick={this.handleDownload}>
<FormattedMessage id="save_action" defaultMessage="Save" description="Label for Save button" />
</a>
</div>
</div>
);
}
};
export default injectIntl(TheCardMini);
+190
View File
@@ -0,0 +1,190 @@
import React from 'react';
import { defineMessages, FormattedMessage } from 'react-intl';
import { parsePhoneNumber } from 'libphonenumber-js/mobile';
import SendMessage from './send-message.jsx';
import LetterTile from './letter-tile.jsx';
import { TheCard } from 'tinode-sdk';
import { importVCard } from '../lib/blob-helpers.js';
const messages = defineMessages({
unknown_name: {
id: 'unknown_name',
defaultMessage: 'Unknown',
description: 'Name to show when the name is missing'
}
});
export default class TheCardPreview extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
content: null
};
importVCard(props.content.file)
.then(card => {
console.log('Imported vCard:', card);
this.setState({ content: card });
});
this.handleSendCard = this.handleSendCard.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
}
componentDidMount() {
document.addEventListener('keydown', this.handleKeyDown);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.handleKeyDown);
}
handleKeyDown(e) {
e.preventDefault();
if (e.key === 'Escape') {
this.props.onClose();
}
}
handleSendCard() {
this.props.onClose();
this.props.onSendMessage(this.props.content);
}
render() {
const card = this.state.content;
console.log('Rendering TheCardPreview for card:', JSON.stringify(card));
const fullName = TheCard.getFn(card);
const photoUrl = TheCard.getPhotoUrl(card) || null;
const note = card?.note;
const org = TheCard.getOrg(card);
// Get communication methods
const emails = TheCard.getComm(card, 'email') || [];
console.log('Emails:', emails);
const phones = TheCard.getComm(card, 'tel') || [];
console.log('Phones:', phones);
const tinodeIds = TheCard.getComm(card, 'tinode') || [];
console.log('Tinode IDs:', tinodeIds);
const urls = TheCard.getComm(card, 'http') || [];
console.log('URLs:', urls);
const contacts = [];
// Add emails
emails.forEach((email, idx) => {
const types = Array.isArray(email.des) ? email.des.join(', ') : '';
contacts.push(
<div className="group quoted" key={`email-${idx}`}>
<tt>{email.value}</tt>
<span className="small gray"> {types ? `(${types})` : ''}</span>
</div>
);
});
// Add phones
phones.forEach((phone, idx) => {
let val = phone.value;
try {
const number = parsePhoneNumber(phone.value);
val = number ? number.formatInternational() : phone.value;
} catch (err) {
// Use original value if parsing fails
}
const types = Array.isArray(phone.des) ? phone.des.join(', ') : '';
contacts.push(
<div className="group quoted" key={`tel-${idx}`}>
<tt>{val}</tt>
<span className="small gray"> {types ? `(${types})` : ''}</span>
</div>
);
});
// Add Tinode IDs
tinodeIds.forEach((tid, idx) => {
const types = Array.isArray(tid.des) ? tid.des.join(', ') : '';
contacts.push(
<div className="group quoted" key={`tinode-${idx}`}>
<tt>{tid.value}</tt>
<span className="small gray"> {types ? `(${types})` : ''}</span>
</div>
);
});
// Add URLs
urls.forEach((url, idx) => {
const types = Array.isArray(url.des) ? url.des.join(', ') : '';
contacts.push(
<div className="group quoted" key={`url-${idx}`}>
<tt>{url.value}</tt>
<span className="small gray"> {types ? `(${types})` : ''}</span>
</div>
);
});
return (
<div id="image-preview">
<div id="image-preview-caption-panel">
<span><FormattedMessage id="label_contact_card" defaultMessage="Contact Card"
description="Label for contact card preview" /></span>
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onClose();}}>
<i className="material-icons gray">close</i>
</a>
</div>
<div id="image-preview-container">
<div className="doc-card">
<div className="panel-form-column">
<center>
<div className="avatar-box">
<LetterTile
authorizeURL={this.props.tinode.authorizeURL}
avatar={photoUrl || true}
topic="usrXXX123"
title={fullName} />
</div>
</center>
<div className="group">
<div className="large ellipsized">{fullName || <FormattedMessage {...messages.unknown_name} />}</div>
</div>
{org ?
<div className="group">
<div>{org}</div>
</div>
: null}
{note ?
<div className="group">
<label className="small">
<FormattedMessage id="label_description" defaultMessage="Description"
description="Label for description" />
</label>
<div className="quoted">{note}</div>
</div>
: null}
{contacts.length > 0 ?
<div className="group">
<label className="small">
<FormattedMessage id="label_contacts" defaultMessage="Contacts"
description="Label for contact information" />
</label>
{contacts}
</div>
: null}
</div>
</div>
</div>
<SendMessage
noInput={true}
tinode={this.props.tinode}
reply={this.props.reply}
onCancelReply={this.props.onCancelReply}
onSendMessage={this.handleSendCard}
onError={this.props.onError} />
</div>
);
}
}