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
+79 -68
View File
@@ -2035,6 +2035,12 @@ img.channel {
border-radius: 0.5rem;
}
.doc-card .avatar-box {
width: 10rem;
height: 10rem;
font-size: 4.5rem;
}
/* Video-call related styles */
#video-container {
@@ -3550,6 +3556,79 @@ ul.tabbar li.active > a {
position: relative;
right: 0;
}
/* END of pinned messages */
/* Contact attachments */
.contact-card {
position: relative;
background-color: var(--clr-background-off-white);
border-radius: 0.5rem;
display: flex;
flex-direction: column;
min-width: 14rem;
}
.contact-card .avatar-box {
position: relative;
width: 1.8rem;
height: 1.8rem;
font-size: 1rem;
margin-right: 0.75rem;
margin-bottom: 0;
}
.contact-card .contact-body {
align-items: center;
background-color: var(--clr-background-blue-gray-50);
display: flex;
padding: 0.75rem;
border-radius: 0.5rem 0.5rem 0 0;
border-bottom: none;
}
.contact-card .contact-body .name-box {
display: flex;
flex-direction: column;
overflow: hidden;
}
.contact-card .contact-body .name-box .name {
color: var(--clr-text-primary);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.contact-card .contact-body .name-box .org {
color: var(--clr-text-secondary);
font-size: 90%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 300;
}
.contact-card .contact-actions {
display: flex;
justify-content: space-evenly;
align-items: center;
padding: 0 0.5rem;
border-radius: 0 0 0.5rem 0.5rem;
border-top: none;
background-color: var(--clr-bright);
}
.contact-card .contact-actions a {
cursor: pointer;
}
.contact-card .contact-actions .divider {
height: 1.8rem;
opacity: 0.5;
}
/* END of contact attachments */
/*
Drag-and-drop area.
@@ -3792,71 +3871,3 @@ div.toast.show {
border-radius: unset;
}
}
.contact-card {
position: relative;
background-color: var(--clr-background-off-white);
border-radius: 0.5rem;
display: flex;
flex-direction: column;
}
.contact-card .avatar-box {
position: relative;
width: 1.8rem;
height: 1.8rem;
font-size: 1rem;
margin-right: 0.75rem;
margin-bottom: 0;
}
.contact-card .contact-body {
align-items: center;
background-color: var(--clr-background-blue-gray-50);
display: flex;
padding: 0.75rem;
border-radius: 0.5rem 0.5rem 0 0;
border-bottom: none;
}
.contact-card .contact-body .name-box {
display: flex;
flex-direction: column;
overflow: hidden;
}
.contact-card .contact-body .name-box .name {
color: var(--clr-text-primary);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.contact-card .contact-body .name-box .org {
color: var(--clr-text-secondary);
font-size: 90%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 300;
}
.contact-card .contact-actions {
display: flex;
justify-content: space-evenly;
align-items: center;
padding: 0 0.5rem;
border-radius: 0 0 0.5rem 0.5rem;
border-top: none;
background-color: var(--clr-bright);
}
.contact-card .contact-actions a {
cursor: pointer;
}
.contact-card .contact-actions .divider {
height: 1.8rem;
opacity: 0.5;
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+28
View File
@@ -1941,5 +1941,33 @@
"description": "Label for search button in contact card",
"missing": true,
"obsolete": false
},
"label_contact_card": {
"translation": "",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": true,
"obsolete": false
},
"label_contacts": {
"translation": "",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": true,
"obsolete": false
},
"label_full_name": {
"translation": "",
"defaultMessage": "Full name",
"description": "Label for full name",
"missing": true,
"obsolete": true
},
"label_organization": {
"translation": "",
"defaultMessage": "Organization",
"description": "Label for organization",
"missing": true,
"obsolete": true
}
}
+28
View File
@@ -1941,5 +1941,33 @@
"description": "Label for search button in contact card",
"missing": true,
"obsolete": false
},
"label_contact_card": {
"translation": "",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": true,
"obsolete": false
},
"label_contacts": {
"translation": "",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": true,
"obsolete": false
},
"label_full_name": {
"translation": "",
"defaultMessage": "Full name",
"description": "Label for full name",
"missing": true,
"obsolete": true
},
"label_organization": {
"translation": "",
"defaultMessage": "Organization",
"description": "Label for organization",
"missing": true,
"obsolete": true
}
}
+28
View File
@@ -1941,5 +1941,33 @@
"description": "Label for search button in contact card",
"missing": false,
"obsolete": false
},
"label_contact_card": {
"translation": "Contact Card",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": false,
"obsolete": false
},
"label_contacts": {
"translation": "Contacts",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": false,
"obsolete": false
},
"label_full_name": {
"translation": "Full name",
"defaultMessage": "Full name",
"description": "Label for full name",
"missing": false,
"obsolete": true
},
"label_organization": {
"translation": "Organization",
"defaultMessage": "Organization",
"description": "Label for organization",
"missing": false,
"obsolete": true
}
}
+28
View File
@@ -1941,5 +1941,33 @@
"description": "Label for search button in contact card",
"missing": true,
"obsolete": false
},
"label_contact_card": {
"translation": "",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": true,
"obsolete": false
},
"label_contacts": {
"translation": "",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": true,
"obsolete": false
},
"label_full_name": {
"translation": "",
"defaultMessage": "Full name",
"description": "Label for full name",
"missing": true,
"obsolete": true
},
"label_organization": {
"translation": "",
"defaultMessage": "Organization",
"description": "Label for organization",
"missing": true,
"obsolete": true
}
}
+8
View File
@@ -434,6 +434,14 @@
"defaultMessage": "Theme:",
"description": "Label for selecting color scheme (dark, light) in Settings"
},
"label_contact_card": {
"defaultMessage": "Contact Card",
"description": "Label for contact card preview"
},
"label_contacts": {
"defaultMessage": "Contacts",
"description": "Label for contact information"
},
"label_content_type": {
"defaultMessage": "Content type:",
"description": "Label for file content type (mime)"
+28
View File
@@ -1941,5 +1941,33 @@
"description": "Label for search button in contact card",
"missing": true,
"obsolete": false
},
"label_contact_card": {
"translation": "",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": true,
"obsolete": false
},
"label_contacts": {
"translation": "",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": true,
"obsolete": false
},
"label_full_name": {
"translation": "",
"defaultMessage": "Full name",
"description": "Label for full name",
"missing": true,
"obsolete": true
},
"label_organization": {
"translation": "",
"defaultMessage": "Organization",
"description": "Label for organization",
"missing": true,
"obsolete": true
}
}
+28
View File
@@ -1941,5 +1941,33 @@
"description": "Label for search button in contact card",
"missing": true,
"obsolete": false
},
"label_contact_card": {
"translation": "",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": true,
"obsolete": false
},
"label_contacts": {
"translation": "",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": true,
"obsolete": false
},
"label_full_name": {
"translation": "",
"defaultMessage": "Full name",
"description": "Label for full name",
"missing": true,
"obsolete": true
},
"label_organization": {
"translation": "",
"defaultMessage": "Organization",
"description": "Label for organization",
"missing": true,
"obsolete": true
}
}
+28
View File
@@ -1941,5 +1941,33 @@
"description": "Label for search button in contact card",
"missing": true,
"obsolete": false
},
"label_contact_card": {
"translation": "",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": true,
"obsolete": false
},
"label_contacts": {
"translation": "",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": true,
"obsolete": false
},
"label_full_name": {
"translation": "",
"defaultMessage": "Full name",
"description": "Label for full name",
"missing": true,
"obsolete": true
},
"label_organization": {
"translation": "",
"defaultMessage": "Organization",
"description": "Label for organization",
"missing": true,
"obsolete": true
}
}
+28
View File
@@ -1941,5 +1941,33 @@
"description": "Label for search button in contact card",
"missing": true,
"obsolete": false
},
"label_contact_card": {
"translation": "",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": true,
"obsolete": false
},
"label_contacts": {
"translation": "",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": true,
"obsolete": false
},
"label_full_name": {
"translation": "",
"defaultMessage": "Full name",
"description": "Label for full name",
"missing": true,
"obsolete": true
},
"label_organization": {
"translation": "",
"defaultMessage": "Organization",
"description": "Label for organization",
"missing": true,
"obsolete": true
}
}
+28
View File
@@ -1941,5 +1941,33 @@
"description": "Label for search button in contact card",
"missing": true,
"obsolete": false
},
"label_contact_card": {
"translation": "",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": true,
"obsolete": false
},
"label_contacts": {
"translation": "",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": true,
"obsolete": false
},
"label_full_name": {
"translation": "",
"defaultMessage": "Full name",
"description": "Label for full name",
"missing": true,
"obsolete": true
},
"label_organization": {
"translation": "",
"defaultMessage": "Organization",
"description": "Label for organization",
"missing": true,
"obsolete": true
}
}
+28
View File
@@ -1941,5 +1941,33 @@
"description": "Label for search button in contact card",
"missing": true,
"obsolete": false
},
"label_contact_card": {
"translation": "",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": true,
"obsolete": false
},
"label_contacts": {
"translation": "",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": true,
"obsolete": false
},
"label_full_name": {
"translation": "",
"defaultMessage": "Full name",
"description": "Label for full name",
"missing": true,
"obsolete": true
},
"label_organization": {
"translation": "",
"defaultMessage": "Organization",
"description": "Label for organization",
"missing": true,
"obsolete": true
}
}
+28
View File
@@ -1941,5 +1941,33 @@
"description": "Label for search button in contact card",
"missing": true,
"obsolete": false
},
"label_contact_card": {
"translation": "",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": true,
"obsolete": false
},
"label_contacts": {
"translation": "",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": true,
"obsolete": false
},
"label_full_name": {
"translation": "",
"defaultMessage": "Full name",
"description": "Label for full name",
"missing": true,
"obsolete": true
},
"label_organization": {
"translation": "",
"defaultMessage": "Organization",
"description": "Label for organization",
"missing": true,
"obsolete": true
}
}
+28
View File
@@ -1941,5 +1941,33 @@
"description": "Label for search button in contact card",
"missing": true,
"obsolete": false
},
"label_contact_card": {
"translation": "",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": true,
"obsolete": false
},
"label_contacts": {
"translation": "",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": true,
"obsolete": false
},
"label_full_name": {
"translation": "",
"defaultMessage": "Full name",
"description": "Label for full name",
"missing": true,
"obsolete": true
},
"label_organization": {
"translation": "",
"defaultMessage": "Organization",
"description": "Label for organization",
"missing": true,
"obsolete": true
}
}
+28
View File
@@ -1941,5 +1941,33 @@
"description": "Label for search button in contact card",
"missing": true,
"obsolete": false
},
"label_contact_card": {
"translation": "",
"defaultMessage": "Contact Card",
"description": "Label for contact card preview",
"missing": true,
"obsolete": false
},
"label_contacts": {
"translation": "",
"defaultMessage": "Contacts",
"description": "Label for contact information",
"missing": true,
"obsolete": false
},
"label_full_name": {
"translation": "",
"defaultMessage": "Full name",
"description": "Label for full name",
"missing": true,
"obsolete": true
},
"label_organization": {
"translation": "",
"defaultMessage": "Organization",
"description": "Label for organization",
"missing": true,
"obsolete": true
}
}
+19
View File
@@ -1,4 +1,5 @@
// File and image helper functions.
import { TheCard } from 'tinode-sdk';
// Supported image MIME types and corresponding file extensions.
export const SUPPORTED_IMAGE_FORMATS = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml'];
@@ -305,3 +306,21 @@ export function base64ToIntArray(b64) {
} catch (err) {}
return null;
}
export function importVCard(fileOrBlob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = _ => {
reject(reader.error);
};
reader.onload = _ => {
const card = TheCard.importVCard(reader.result);
if (!card) {
reject(new Error("Failed to parse vCard"));
return;
}
resolve(card);
};
reader.readAsText(fileOrBlob);
});
}
+2 -2
View File
@@ -8,7 +8,7 @@ import CallMessage from '../widgets/call-message.jsx'
import CallStatus from '../widgets/call-status.jsx';
import InlineVideo from '../widgets/inline-video.jsx';
import LazyImage from '../widgets/lazy-image.jsx';
import TheCardViewer from '../widgets/the-card-viewer.jsx';
import TheCardMini from '../widgets/the-card-mini.jsx';
import UploadingImage from '../widgets/uploading-image.jsx'
import { BROKEN_IMAGE_SIZE, CLICKABLE_URL_SCHEMES, IMAGE_THUMBNAIL_DIM, NO_DIMENSIONS_VIDEO,
@@ -135,7 +135,7 @@ export function fullFormatter(style, data, values, key, stack) {
break;
case 'TC':
// TheCard
el = TheCardViewer;
el = TheCardMini;
attr.content = data;
attr.authorizeURL = this.authorizeURL;
attr.onChatClick = (e) => this.onHandleClick(e, 'contact_chat');
+35 -35
View File
@@ -19,13 +19,14 @@ import LogoView from './logo-view.jsx';
import MetaMessage from '../widgets/meta-message.jsx';
import PinnedMessages from '../widgets/pinned-messages.jsx';
import SendMessage from '../widgets/send-message.jsx';
import TheCardPreview from '../widgets/the-card-preview.jsx';
import VideoPreview from '../widgets/video-preview.jsx';
import { DEFAULT_P2P_ACCESS_MODE, EDIT_PREVIEW_LENGTH, IMAGE_PREVIEW_DIM, IMMEDIATE_P2P_SUBSCRIPTION,
DRAFTY_FR_MIME_TYPE_LEGACY, KEYPRESS_DELAY, MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM,
MAX_INBAND_ATTACHMENT_SIZE, READ_DELAY, QUOTED_REPLY_LENGTH, VIDEO_PREVIEW_DIM } from '../config.js';
import { CALL_STATE_OUTGOING_INITATED, CALL_STATE_IN_PROGRESS } from '../constants.js';
import { blobToBase64, fileToBase64, imageScaled, makeImageUrl } from '../lib/blob-helpers.js';
import { blobToBase64, fileToBase64, imageScaled, importVCard, makeImageUrl } from '../lib/blob-helpers.js';
import HashNavigation from '../lib/navigation.js';
import { bytesToHumanSize, relativeDateFormat, shortDateFormat } from '../lib/strformat.js';
@@ -1103,16 +1104,13 @@ class MessagesView extends React.Component {
return false;
}
const reader = new FileReader();
reader.onload = (e) => {
const card = TheCard.importVCard(e.target.result);
if (!card) {
// Failed to parse vCard.
this.props.onError(this.props.intl.formatMessage(messages.cannot_parse_vcard), 'err');
}
this.sendMessage(Drafty.appendTheCard(null, card));
};
reader.readAsText(file);
importVCard(file)
.then(card => {
this.sendMessage(Drafty.appendTheCard(null, card));
})
.catch(err => {
this.props.onError(this.props.intl.formatMessage(messages.cannot_parse_vcard, {error: err.message}), 'err');
});
return true;
}
@@ -1523,7 +1521,7 @@ class MessagesView extends React.Component {
// P2P call is an overlay, not a separate component because it cannot be
// re-mounted between full screen/minimized.
const overlay = this.state.rtcPanel ? (
const overlay = this.state.rtcPanel &&
<CallPanel
topic={this.state.topic}
seq={this.props.callSeq}
@@ -1533,23 +1531,20 @@ class MessagesView extends React.Component {
title={this.state.title}
avatar={this.state.avatar || true}
minimized={this.state.minimizedCallPanel}
onError={this.props.onError}
onHangup={this.handleCallHangup}
onToggleMinimize={this.handleCallPanelToggle}
onInvite={this.props.onCallInvite}
onSendOffer={this.props.onCallSendOffer}
onIceCandidate={this.props.onCallIceCandidate}
onSendAnswer={this.props.onCallSendAnswer} />
) : null;
onSendAnswer={this.props.onCallSendAnswer} />;
let component;
if (!this.state.topic) {
component = (
<LogoView
component = <LogoView
serverVersion={this.props.serverVersion}
serverAddress={this.props.serverAddress} />
);
serverAddress={this.props.serverAddress} />;
} else {
let component2;
if (this.state.imagePreview) {
@@ -1565,43 +1560,48 @@ class MessagesView extends React.Component {
);
} else if (this.state.videoPreview) {
// Preview video.
component2 = (
<VideoPreview
component2 = <VideoPreview
content={this.state.videoPreview}
tinode={this.props.tinode}
reply={this.state.reply}
onError={this.props.onError}
onCancelReply={this.handleCancelReply}
onClose={this.handleClosePreview}
onSendMessage={this.sendVideoAttachment} />
);
onSendMessage={this.sendVideoAttachment} />;
} else if (this.state.imagePostview) {
// Expand received image.
component2 = (
<ImagePreview
component2 = <ImagePreview
content={this.state.imagePostview}
onClose={this.handleClosePreview} />
);
onClose={this.handleClosePreview} />;
} else if (this.state.videoPostview) {
// Play received video.
component2 = (
<VideoPreview
component2 = <VideoPreview
content={this.state.videoPostview}
tinode={this.props.tinode}
onError={this.props.onError}
onClose={this.handleClosePreview} />
);
onClose={this.handleClosePreview} />;
} else if (this.state.docPreview) {
// Preview attachment before sending.
component2 = (
<DocPreview
if (TheCard.isFileSupported(this.state.docPreview.type, this.state.docPreview.name)) {
component2 = <TheCardPreview
content={this.state.docPreview}
tinode={this.props.tinode}
reply={this.state.reply}
onCancelReply={this.handleCancelReply}
onClose={this.handleClosePreview}
onSendMessage={this.sendFileAttachment} />
);
onSendMessage={this.sendFileAttachment} />;
} else {
component2 = <DocPreview
content={this.state.docPreview}
tinode={this.props.tinode}
reply={this.state.reply}
onCancelReply={this.handleCancelReply}
onClose={this.handleClosePreview}
onSendMessage={this.sendFileAttachment} />;
}
} else {
const topic = this.props.tinode.getTopic(this.state.topic);
const isChannel = topic.isChannelType() || topic.chan;
+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>
);
}
}
+2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+4120 -71
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+105 -15
View File
@@ -4139,14 +4139,15 @@ TheCard.exportVCard = function (card) {
}
if (Array.isArray(card.comm)) {
card.comm.forEach(comm => {
const types = comm.des.join(',').toUpperCase();
if (comm.proto === 'tel') {
vcard += `TEL;TYPE=${comm.des.toUpperCase()}:${comm.value}\r\n`;
vcard += `TEL;TYPE=${types}:${comm.value}\r\n`;
} else if (comm.proto === 'email') {
vcard += `EMAIL;TYPE=${comm.des.toUpperCase()}:${comm.value}\r\n`;
vcard += `EMAIL;TYPE=${types}:${comm.value}\r\n`;
} else if (comm.proto === 'tinode') {
vcard += `IMPP;TYPE=${comm.des.toUpperCase()};tinode:${comm.value}\r\n`;
vcard += `IMPP;TYPE=${types};tinode:${comm.value}\r\n`;
} else if (comm.proto === 'http') {
vcard += `URL;TYPE=${comm.des.toUpperCase()}:${comm.value}\r\n`;
vcard += `URL;TYPE=${types}:${comm.value}\r\n`;
}
});
}
@@ -4157,8 +4158,25 @@ TheCard.importVCard = function (vcardStr) {
if (!vcardStr || typeof vcardStr !== 'string') {
return null;
}
const lines = vcardStr.split(/\r\n|\n/);
const rawLines = vcardStr.split(/\r\n|\n/);
const lines = [];
let currentLine = '';
for (let i = 0; i < rawLines.length; i++) {
const line = rawLines[i];
if (line.startsWith(' ') || line.startsWith('\t')) {
currentLine += line.substring(1);
} else {
if (currentLine) {
lines.push(currentLine);
}
currentLine = line;
}
}
if (currentLine) {
lines.push(currentLine);
}
let card = {};
const commMap = new Map();
lines.forEach(line => {
const [keyPart, ...valueParts] = line.split(':');
if (!keyPart || valueParts.length === 0) {
@@ -4219,21 +4237,83 @@ TheCard.importVCard = function (vcardStr) {
};
}
} else if (key === 'TEL') {
const des = keyPart.split(';').find(param => param.trim().toUpperCase().startsWith('TYPE='))?.split('=')[1].trim().toLowerCase() || 'voice';
card = TheCard.addPhone(card, value, des);
const typeParams = keyPart.split(';').filter(param => param.trim().toUpperCase().startsWith('TYPE='));
const types = typeParams.flatMap(param => {
const equalIndex = param.indexOf('=');
const valuesPart = param.substring(equalIndex + 1);
return valuesPart.split(',').map(t => {
const cleaned = t.trim().toLowerCase();
return cleaned.startsWith('type=') ? cleaned.substring(5) : cleaned;
});
}).filter(t => t !== 'internet');
const mapKey = `tel|${value}`;
if (!commMap.has(mapKey)) {
commMap.set(mapKey, new Set());
}
types.forEach(t => commMap.get(mapKey).add(t));
} else if (key === 'EMAIL') {
const des = keyPart.split(';').find(param => param.trim().toUpperCase().startsWith('TYPE='))?.split('=')[1].trim().toLowerCase() || 'home';
card = TheCard.addEmail(card, value, des);
const typeParams = keyPart.split(';').filter(param => param.trim().toUpperCase().startsWith('TYPE='));
const types = typeParams.flatMap(param => {
const equalIndex = param.indexOf('=');
const valuesPart = param.substring(equalIndex + 1);
return valuesPart.split(',').map(t => {
const cleaned = t.trim().toLowerCase();
return cleaned.startsWith('type=') ? cleaned.substring(5) : cleaned;
});
}).filter(t => t !== 'internet');
const mapKey = `email|${value}`;
if (!commMap.has(mapKey)) {
commMap.set(mapKey, new Set());
}
types.forEach(t => commMap.get(mapKey).add(t));
} else if (key === 'IMPP') {
const des = keyPart.split(';').find(param => param.trim().toUpperCase().startsWith('TYPE='))?.split('=')[1].trim().toLowerCase() || 'home';
const typeParams = keyPart.split(';').filter(param => param.trim().toUpperCase().startsWith('TYPE='));
const types = typeParams.flatMap(param => {
const equalIndex = param.indexOf('=');
const valuesPart = param.substring(equalIndex + 1);
return valuesPart.split(',').map(t => {
const cleaned = t.trim().toLowerCase();
return cleaned.startsWith('type=') ? cleaned.substring(5) : cleaned;
});
}).filter(t => t !== 'internet');
const tinodeID = value.replace(/^tinode:/, '');
card = TheCard.addTinodeID(card, tinodeID, des);
const mapKey = `tinode|${tinodeID}`;
if (!commMap.has(mapKey)) {
commMap.set(mapKey, new Set());
}
types.forEach(t => commMap.get(mapKey).add(t));
} else if (key === 'URL') {
const typeParams = keyPart.split(';').filter(param => param.trim().toUpperCase().startsWith('TYPE='));
const types = typeParams.flatMap(param => {
const equalIndex = param.indexOf('=');
const valuesPart = param.substring(equalIndex + 1);
return valuesPart.split(',').map(t => {
const cleaned = t.trim().toLowerCase();
return cleaned.startsWith('type=') ? cleaned.substring(5) : cleaned;
});
}).filter(t => t !== 'internet');
const mapKey = `http|${value}`;
if (!commMap.has(mapKey)) {
commMap.set(mapKey, new Set());
}
types.forEach(t => commMap.get(mapKey).add(t));
}
});
if (commMap.size > 0) {
card.comm = [];
commMap.forEach((types, key) => {
const [proto, value] = key.split('|', 2);
card.comm.push({
proto: proto,
des: Array.from(types),
value: value
});
});
}
return card;
};
TheCard.isFileSupported = function (type, name) {
return type == 'text/vcard' || (name || '').endsWith('.vcf') || (name || '').endsWith('.vcard');
return type == 'text/vcard' || (name || '').endsWith('.vcf') || (name || '').endsWith('.vcr') || (name || '').endsWith('.vcard');
};
function theCard(fn, imageUrl, imageMimeType, note) {
let card = null;
@@ -4275,11 +4355,14 @@ function addOrSetComm(card, proto, value, type, setOnly) {
card = card || {};
card.comm = card.comm || [];
if (setOnly) {
card.comm = card.comm.filter(c => !(c.proto == proto && c.des == type));
card.comm = card.comm.filter(c => {
if (c.proto != proto) return true;
return !c.des.includes(type);
});
}
card.comm.push({
proto: proto,
des: type,
des: [type],
value: value
});
}
@@ -4287,7 +4370,14 @@ function addOrSetComm(card, proto, value, type, setOnly) {
}
function clearComm(card, proto, value, type) {
if (card && Array.isArray(card.comm)) {
card.comm = card.comm.filter(c => !(c.proto == proto && (!type || c.des == type) && (!value || c.value == value)));
card.comm = card.comm.filter(c => {
if (c.proto != proto) return true;
if (value && c.value != value) return true;
if (type) {
return !c.des.includes(type);
}
return false;
});
}
return card;
}
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long