Visuals for embedded contacts as well as contact detail screen
parent
3ea3e4e256
commit
41be7f126b
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="20px" height="19px" viewBox="0 0 20 19" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 49.3 (51167) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Shape</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Key-View" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="1.1a-Received-Contact-(iOS)" transform="translate(-879.000000, -412.000000)" fill-rule="nonzero" stroke="#000000" stroke-width="0.833333333">
|
||||
<g id="Window" transform="translate(320.000000, 135.000000)">
|
||||
<g id="message" transform="translate(473.000000, 192.000000)">
|
||||
<g id="Group-4" transform="translate(0.000000, 72.000000)">
|
||||
<g id="Group" transform="translate(86.000000, 11.000000)">
|
||||
<path d="M0.503004576,20.5808657 C2.44736376,20.5304175 4.1383603,19.7106004 5.53209251,18.5049654 L5.72548696,18.3376713 L5.96221887,18.4343498 C7.23137666,18.9526585 8.59754256,19.2261905 10,19.2261905 C15.3043498,19.2261905 19.5833333,15.4516724 19.5833333,10.8214286 C19.5833333,6.19118476 15.3043498,2.41666667 10,2.41666667 C4.69565023,2.41666667 0.416666667,6.19118476 0.416666667,10.8214286 C0.416666667,12.7612904 1.1698162,14.5979981 2.53343385,16.082656 L2.68151739,16.243884 L2.63276552,16.4573002 C2.40695363,17.4458143 1.9438817,18.4456758 1.33263084,19.4122874 C1.02870151,19.8929111 0.697423786,20.342753 0.503004576,20.5808657 Z" id="Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="18px" height="17px" viewBox="0 0 18 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 49.3 (51167) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Shape</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Key-View" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="1.1b-Received-Contact-(Android-Light)" transform="translate(-888.000000, -377.000000)" fill="#000000" fill-rule="nonzero">
|
||||
<g id="Window" transform="translate(320.000000, 135.000000)">
|
||||
<g id="Group-4" transform="translate(500.000000, 166.000000)">
|
||||
<g id="Group-3" transform="translate(0.000000, 64.000000)">
|
||||
<g id="Group-2" transform="translate(68.000000, 12.000000)">
|
||||
<path d="M9,0 C4.02890625,0 0,3.5328125 0,7.89285714 C0,9.775 0.75234375,11.4977679 2.00390625,12.8524554 C1.56445312,14.7649554 0.094921875,16.46875 0.07734375,16.4877232 C0,16.575 -0.02109375,16.7040179 0.024609375,16.8178571 C0.0703125,16.9316964 0.16875,17 0.28125,17 C2.61210937,17 4.359375,15.7933036 5.22421875,15.0495536 C6.37382813,15.5162946 7.65,15.7857143 9,15.7857143 C13.9710937,15.7857143 18,12.2529018 18,7.89285714 C18,3.5328125 13.9710937,0 9,0 Z" id="Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,173 @@
|
||||
### With all data types
|
||||
|
||||
```jsx
|
||||
const contact = {
|
||||
avatar: {
|
||||
avatar: {
|
||||
path: util.gifObjectUrl,
|
||||
},
|
||||
},
|
||||
name: {
|
||||
displayName: 'Someone Somewhere',
|
||||
},
|
||||
number: [
|
||||
{
|
||||
value: '(202) 555-0000',
|
||||
type: 3,
|
||||
},
|
||||
{
|
||||
value: '(202) 555-0001',
|
||||
type: 4,
|
||||
label: 'My favorite custom label',
|
||||
},
|
||||
],
|
||||
email: [
|
||||
{
|
||||
value: 'someone@somewhere.com',
|
||||
type: 2,
|
||||
},
|
||||
|
||||
{
|
||||
value: 'someone2@somewhere.com',
|
||||
type: 4,
|
||||
label: 'My second-favorite custom label',
|
||||
},
|
||||
],
|
||||
address: [
|
||||
{
|
||||
street: '5 Pike Place',
|
||||
city: 'Seattle',
|
||||
region: 'WA',
|
||||
postcode: '98101',
|
||||
type: 1,
|
||||
},
|
||||
{
|
||||
street: '10 Pike Place',
|
||||
pobox: '3242',
|
||||
neighborhood: 'Downtown',
|
||||
city: 'Seattle',
|
||||
region: 'WA',
|
||||
postcode: '98101',
|
||||
country: 'United States',
|
||||
type: 3,
|
||||
label: 'My favorite spot!',
|
||||
},
|
||||
],
|
||||
};
|
||||
<ContactDetail
|
||||
contact={contact}
|
||||
hasSignalAccount={true}
|
||||
i18n={util.i18n}
|
||||
onSendMessage={() => console.log('onSendMessage')}
|
||||
/>;
|
||||
```
|
||||
|
||||
### With default avatar
|
||||
|
||||
```jsx
|
||||
const contact = {
|
||||
name: {
|
||||
displayName: 'Someone Somewhere',
|
||||
},
|
||||
number: [
|
||||
{
|
||||
value: '(202) 555-0000',
|
||||
type: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
<ContactDetail
|
||||
contact={contact}
|
||||
hasSignalAccount={true}
|
||||
i18n={util.i18n}
|
||||
onSendMessage={() => console.log('onSendMessage')}
|
||||
/>;
|
||||
```
|
||||
|
||||
### Without a Signal account
|
||||
|
||||
```jsx
|
||||
const contact = {
|
||||
avatar: {
|
||||
avatar: {
|
||||
path: util.gifObjectUrl,
|
||||
},
|
||||
},
|
||||
name: {
|
||||
displayName: 'Someone Somewhere',
|
||||
},
|
||||
number: [
|
||||
{
|
||||
value: '(202) 555-0001',
|
||||
type: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
<ContactDetail
|
||||
contact={contact}
|
||||
hasSignalAccount={false}
|
||||
i18n={util.i18n}
|
||||
onSendMessage={() => console.log('onSendMessage')}
|
||||
/>;
|
||||
```
|
||||
|
||||
### No phone or email, partial addresses
|
||||
|
||||
```jsx
|
||||
const contact = {
|
||||
avatar: {
|
||||
avatar: {
|
||||
path: util.gifObjectUrl,
|
||||
},
|
||||
},
|
||||
name: {
|
||||
displayName: 'Someone Somewhere',
|
||||
},
|
||||
address: [
|
||||
{
|
||||
type: 1,
|
||||
neighborhood: 'Greenwood',
|
||||
region: 'WA',
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
city: 'Seattle',
|
||||
region: 'WA',
|
||||
},
|
||||
{
|
||||
type: 3,
|
||||
label: 'My label',
|
||||
region: 'WA',
|
||||
},
|
||||
{
|
||||
type: 1,
|
||||
label: 'My label',
|
||||
postcode: '98101',
|
||||
region: 'WA',
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
label: 'My label',
|
||||
postcode: '98101',
|
||||
},
|
||||
],
|
||||
};
|
||||
<ContactDetail
|
||||
contact={contact}
|
||||
hasSignalAccount={false}
|
||||
i18n={util.i18n}
|
||||
onSendMessage={() => console.log('onSendMessage')}
|
||||
/>;
|
||||
```
|
||||
|
||||
### Empty contact
|
||||
|
||||
```jsx
|
||||
const contact = {};
|
||||
<ContactDetail
|
||||
contact={contact}
|
||||
hasSignalAccount={false}
|
||||
i18n={util.i18n}
|
||||
onSendMessage={() => console.log('onSendMessage')}
|
||||
/>;
|
||||
```
|
@ -0,0 +1,264 @@
|
||||
import React from 'react';
|
||||
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
|
||||
type Localizer = (key: string, values?: Array<string>) => string;
|
||||
|
||||
interface Props {
|
||||
contact: Contact;
|
||||
hasSignalAccount: boolean;
|
||||
i18n: Localizer;
|
||||
onSendMessage: () => void;
|
||||
}
|
||||
|
||||
interface Contact {
|
||||
name: Name;
|
||||
number?: Array<Phone>;
|
||||
email?: Array<Email>;
|
||||
address?: Array<PostalAddress>;
|
||||
avatar?: Avatar;
|
||||
organization?: string;
|
||||
}
|
||||
|
||||
interface Name {
|
||||
givenName?: string;
|
||||
familyName?: string;
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
middleName?: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
enum ContactType {
|
||||
HOME = 1,
|
||||
MOBILE = 2,
|
||||
WORK = 3,
|
||||
CUSTOM = 4,
|
||||
}
|
||||
|
||||
enum AddressType {
|
||||
HOME = 1,
|
||||
WORK = 2,
|
||||
CUSTOM = 3,
|
||||
}
|
||||
|
||||
interface Phone {
|
||||
value: string;
|
||||
type: ContactType;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface Email {
|
||||
value: string;
|
||||
type: ContactType;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface PostalAddress {
|
||||
type: AddressType;
|
||||
label?: string;
|
||||
street?: string;
|
||||
pobox?: string;
|
||||
neighborhood?: string;
|
||||
city?: string;
|
||||
region?: string;
|
||||
postcode?: string;
|
||||
country?: string;
|
||||
}
|
||||
|
||||
interface Avatar {
|
||||
avatar: Attachment;
|
||||
isProfile: boolean;
|
||||
}
|
||||
|
||||
interface Attachment {
|
||||
path: string;
|
||||
}
|
||||
|
||||
function getLabelForContactMethod(method: Phone | Email, i18n: Localizer) {
|
||||
switch (method.type) {
|
||||
case ContactType.CUSTOM:
|
||||
return method.label;
|
||||
case ContactType.HOME:
|
||||
return i18n('home');
|
||||
case ContactType.MOBILE:
|
||||
return i18n('mobile');
|
||||
case ContactType.WORK:
|
||||
return i18n('work');
|
||||
default:
|
||||
return missingCaseError(method.type);
|
||||
}
|
||||
}
|
||||
|
||||
function getLabelForAddress(address: PostalAddress, i18n: Localizer) {
|
||||
switch (address.type) {
|
||||
case AddressType.CUSTOM:
|
||||
return address.label;
|
||||
case AddressType.HOME:
|
||||
return i18n('home');
|
||||
case AddressType.WORK:
|
||||
return i18n('work');
|
||||
default:
|
||||
return missingCaseError(address.type);
|
||||
}
|
||||
}
|
||||
|
||||
function getInitials(name: string): string {
|
||||
return name.trim()[0] || '#';
|
||||
}
|
||||
|
||||
function getName(contact: Contact): string {
|
||||
const { name, organization } = contact;
|
||||
return (name && name.displayName) || organization || '';
|
||||
}
|
||||
|
||||
export class ContactDetail extends React.Component<Props, {}> {
|
||||
public renderAvatar() {
|
||||
const { contact } = this.props;
|
||||
const { avatar } = contact;
|
||||
|
||||
const path = avatar && avatar.avatar && avatar.avatar.path;
|
||||
if (!path) {
|
||||
const name = getName(contact);
|
||||
const initials = getInitials(name);
|
||||
return (
|
||||
<div className="image-container">
|
||||
<div className="default-avatar">{initials}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="image-container">
|
||||
<img src={path} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public renderName() {
|
||||
const { contact } = this.props;
|
||||
|
||||
return <div className="contact-name">{getName(contact)}</div>;
|
||||
}
|
||||
|
||||
public renderContactShorthand() {
|
||||
const { contact } = this.props;
|
||||
const { number, email } = contact;
|
||||
const firstNumber = number && number[0] && number[0].value;
|
||||
const firstEmail = email && email[0] && email[0].value;
|
||||
|
||||
return <div className="contact-method">{firstNumber || firstEmail}</div>;
|
||||
}
|
||||
|
||||
public renderSendMessage() {
|
||||
const { hasSignalAccount, i18n, onSendMessage } = this.props;
|
||||
|
||||
if (!hasSignalAccount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We don't want the overall click handler for this element to fire, so we stop
|
||||
// propagation before handing control to the caller's callback.
|
||||
const onClick = (e: React.MouseEvent<{}>): void => {
|
||||
e.stopPropagation();
|
||||
onSendMessage();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="send-message" onClick={onClick}>
|
||||
<button className="inner">
|
||||
<div className="icon bubble-icon" />
|
||||
{i18n('sendMessageToContact')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public renderAdditionalContact(
|
||||
items: Array<Phone | Email> | undefined,
|
||||
i18n: Localizer
|
||||
) {
|
||||
if (!items || items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return items.map((item: Phone | Email) => {
|
||||
return (
|
||||
<div key={item.value} className="additional-contact">
|
||||
<div className="type">{getLabelForContactMethod(item, i18n)}</div>
|
||||
{item.value}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public renderAddressLineIfTruthy(value: string | undefined) {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
|
||||
public renderPOBox(poBox: string | undefined, i18n: Localizer) {
|
||||
if (!poBox) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{i18n('poBox')} {poBox}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public renderAddressLineTwo(address: PostalAddress) {
|
||||
if (address.city || address.region || address.postcode) {
|
||||
return (
|
||||
<div>
|
||||
{address.city} {address.region} {address.postcode}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public renderAddresses(
|
||||
addresses: Array<PostalAddress> | undefined,
|
||||
i18n: Localizer
|
||||
) {
|
||||
if (!addresses || addresses.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return addresses.map((address: PostalAddress, index: number) => {
|
||||
return (
|
||||
<div key={index} className="additional-contact">
|
||||
<div className="type">{getLabelForAddress(address, i18n)}</div>
|
||||
{this.renderAddressLineIfTruthy(address.street)}
|
||||
{this.renderPOBox(address.pobox, i18n)}
|
||||
{this.renderAddressLineIfTruthy(address.neighborhood)}
|
||||
{this.renderAddressLineTwo(address)}
|
||||
{this.renderAddressLineIfTruthy(address.country)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { contact, i18n } = this.props;
|
||||
|
||||
return (
|
||||
<div className="contact-detail">
|
||||
{this.renderAvatar()}
|
||||
{this.renderName()}
|
||||
{this.renderContactShorthand()}
|
||||
{this.renderSendMessage()}
|
||||
{this.renderAdditionalContact(contact.number, i18n)}
|
||||
{this.renderAdditionalContact(contact.email, i18n)}
|
||||
{this.renderAddresses(contact.address, i18n)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,244 @@
|
||||
### With a contact
|
||||
|
||||
#### Including all data types
|
||||
|
||||
```jsx
|
||||
const outgoing = new Whisper.Message({
|
||||
type: 'outgoing',
|
||||
sent_at: Date.now() - 18000000,
|
||||
contact: [
|
||||
{
|
||||
name: {
|
||||
displayName: 'Someone Somewhere',
|
||||
},
|
||||
number: [
|
||||
{
|
||||
value: util.CONTACTS[0].id,
|
||||
type: 1,
|
||||
},
|
||||
],
|
||||
avatar: {
|
||||
avatar: {
|
||||
path: util.gifObjectUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const incoming = new Whisper.Message(
|
||||
Object.assign({}, outgoing.attributes, {
|
||||
source: '+12025550011',
|
||||
type: 'incoming',
|
||||
})
|
||||
);
|
||||
const View = Whisper.MessageView;
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<util.BackboneWrapper View={View} options={{ model: incoming }} />
|
||||
<util.BackboneWrapper View={View} options={{ model: outgoing }} />
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
#### In group conversation
|
||||
|
||||
```jsx
|
||||
const outgoing = new Whisper.Message({
|
||||
type: 'outgoing',
|
||||
sent_at: Date.now() - 18000000,
|
||||
contact: [
|
||||
{
|
||||
name: {
|
||||
displayName: 'Someone Somewhere',
|
||||
},
|
||||
number: [
|
||||
{
|
||||
value: util.CONTACTS[0].id,
|
||||
type: 1,
|
||||
},
|
||||
],
|
||||
avatar: {
|
||||
avatar: {
|
||||
path: util.gifObjectUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const incoming = new Whisper.Message(
|
||||
Object.assign({}, outgoing.attributes, {
|
||||
source: '+12025550011',
|
||||
type: 'incoming',
|
||||
})
|
||||
);
|
||||
const View = Whisper.MessageView;
|
||||
<util.ConversationContext theme={util.theme} type="group">
|
||||
<util.BackboneWrapper View={View} options={{ model: incoming }} />
|
||||
<util.BackboneWrapper View={View} options={{ model: outgoing }} />
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
#### If contact has no signal account
|
||||
|
||||
```jsx
|
||||
const outgoing = new Whisper.Message({
|
||||
type: 'outgoing',
|
||||
sent_at: Date.now() - 18000000,
|
||||
contact: [
|
||||
{
|
||||
name: {
|
||||
displayName: 'Someone Somewhere',
|
||||
},
|
||||
number: [
|
||||
{
|
||||
value: '+12025551000',
|
||||
type: 1,
|
||||
},
|
||||
],
|
||||
avatar: {
|
||||
avatar: {
|
||||
path: util.gifObjectUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const incoming = new Whisper.Message(
|
||||
Object.assign({}, outgoing.attributes, {
|
||||
source: '+12025550011',
|
||||
type: 'incoming',
|
||||
})
|
||||
);
|
||||
const View = Whisper.MessageView;
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<util.BackboneWrapper View={View} options={{ model: incoming }} />
|
||||
<util.BackboneWrapper View={View} options={{ model: outgoing }} />
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
#### With organization name instead of name
|
||||
|
||||
```jsx
|
||||
const outgoing = new Whisper.Message({
|
||||
type: 'outgoing',
|
||||
sent_at: Date.now() - 18000000,
|
||||
contact: [
|
||||
{
|
||||
organization: 'United Somewheres, Inc.',
|
||||
email: [
|
||||
{
|
||||
value: 'someone@somewheres.com',
|
||||
type: 2,
|
||||
},
|
||||
],
|
||||
avatar: {
|
||||
avatar: {
|
||||
path: util.gifObjectUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const incoming = new Whisper.Message(
|
||||
Object.assign({}, outgoing.attributes, {
|
||||
source: '+12025550011',
|
||||
type: 'incoming',
|
||||
})
|
||||
);
|
||||
const View = Whisper.MessageView;
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<util.BackboneWrapper View={View} options={{ model: incoming }} />
|
||||
<util.BackboneWrapper View={View} options={{ model: outgoing }} />
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
#### Default avatar
|
||||
|
||||
```jsx
|
||||
const outgoing = new Whisper.Message({
|
||||
type: 'outgoing',
|
||||
sent_at: Date.now() - 18000000,
|
||||
contact: [
|
||||
{
|
||||
name: {
|
||||
displayName: 'Someone Somewhere',
|
||||
},
|
||||
number: [
|
||||
{
|
||||
value: util.CONTACTS[0].id,
|
||||
type: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
const incoming = new Whisper.Message(
|
||||
Object.assign({}, outgoing.attributes, {
|
||||
source: '+12025550011',
|
||||
type: 'incoming',
|
||||
})
|
||||
);
|
||||
const View = Whisper.MessageView;
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<util.BackboneWrapper View={View} options={{ model: incoming }} />
|
||||
<util.BackboneWrapper View={View} options={{ model: outgoing }} />
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
#### Empty contact
|
||||
|
||||
```jsx
|
||||
const outgoing = new Whisper.Message({
|
||||
type: 'outgoing',
|
||||
sent_at: Date.now() - 18000000,
|
||||
contact: [{}],
|
||||
});
|
||||
const incoming = new Whisper.Message(
|
||||
Object.assign({}, outgoing.attributes, {
|
||||
source: '+12025550011',
|
||||
type: 'incoming',
|
||||
})
|
||||
);
|
||||
const View = Whisper.MessageView;
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<util.BackboneWrapper View={View} options={{ model: incoming }} />
|
||||
<util.BackboneWrapper View={View} options={{ model: outgoing }} />
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
#### Contact with caption (cannot currently be sent)
|
||||
|
||||
```jsx
|
||||
const outgoing = new Whisper.Message({
|
||||
type: 'outgoing',
|
||||
sent_at: Date.now() - 18000000,
|
||||
body: 'I want to introduce you to Someone...',
|
||||
contact: [
|
||||
{
|
||||
name: {
|
||||
displayName: 'Someone Somewhere',
|
||||
},
|
||||
number: [
|
||||
{
|
||||
value: util.CONTACTS[0].id,
|
||||
type: 1,
|
||||
},
|
||||
],
|
||||
avatar: {
|
||||
avatar: {
|
||||
path: util.gifObjectUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const incoming = new Whisper.Message(
|
||||
Object.assign({}, outgoing.attributes, {
|
||||
source: '+12025550011',
|
||||
type: 'incoming',
|
||||
})
|
||||
);
|
||||
const View = Whisper.MessageView;
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<util.BackboneWrapper View={View} options={{ model: incoming }} />
|
||||
<util.BackboneWrapper View={View} options={{ model: outgoing }} />
|
||||
</util.ConversationContext>;
|
||||
```
|
@ -0,0 +1,162 @@
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
contact: Contact;
|
||||
hasSignalAccount: boolean;
|
||||
i18n: (key: string, values?: Array<string>) => string;
|
||||
onSendMessage: () => void;
|
||||
onOpenContact: () => void;
|
||||
}
|
||||
|
||||
interface Contact {
|
||||
name: Name;
|
||||
number?: Array<Phone>;
|
||||
email?: Array<Email>;
|
||||
address?: Array<PostalAddress>;
|
||||
avatar?: Avatar;
|
||||
organization?: string;
|
||||
}
|
||||
|
||||
interface Name {
|
||||
givenName?: string;
|
||||
familyName?: string;
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
middleName?: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
enum ContactType {
|
||||
HOME = 1,
|
||||
MOBILE = 2,
|
||||
WORK = 3,
|
||||
CUSTOM = 4,
|
||||
}
|
||||
|
||||
enum AddressType {
|
||||
HOME = 1,
|
||||
WORK = 2,
|
||||
CUSTOM = 3,
|
||||
}
|
||||
|
||||
interface Phone {
|
||||
value: string;
|
||||
type: ContactType;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface Email {
|
||||
value: string;
|
||||
type: ContactType;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface PostalAddress {
|
||||
type: AddressType;
|
||||
label?: string;
|
||||
street?: string;
|
||||
pobox?: string;
|
||||
neighborhood?: string;
|
||||
city?: string;
|
||||
region?: string;
|
||||
postcode?: string;
|
||||
country?: string;
|
||||
}
|
||||
|
||||
interface Avatar {
|
||||
avatar: Attachment;
|
||||
isProfile: boolean;
|
||||
}
|
||||
|
||||
interface Attachment {
|
||||
path: string;
|
||||
}
|
||||
|
||||
function getInitials(name: string): string {
|
||||
return name.trim()[0] || '#';
|
||||
}
|
||||
|
||||
function getName(contact: Contact): string {
|
||||
const { name, organization } = contact;
|
||||
return (name && name.displayName) || organization || '';
|
||||
}
|
||||
|
||||
export class EmbeddedContact extends React.Component<Props, {}> {
|
||||
public renderAvatar() {
|
||||
const { contact } = this.props;
|
||||
const { avatar } = contact;
|
||||
|
||||
const path = avatar && avatar.avatar && avatar.avatar.path;
|
||||
if (!path) {
|
||||
const name = getName(contact);
|
||||
const initials = getInitials(name);
|
||||
return (
|
||||
<div className="image-container">
|
||||
<div className="default-avatar">{initials}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="image-container">
|
||||
<img src={path} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public renderName() {
|
||||
const { contact } = this.props;
|
||||
|
||||
return <div className="contact-name">{getName(contact)}</div>;
|
||||
}
|
||||
|
||||
public renderContactShorthand() {
|
||||
const { contact } = this.props;
|
||||
const { number, email } = contact;
|
||||
const firstNumber = number && number[0] && number[0].value;
|
||||
const firstEmail = email && email[0] && email[0].value;
|
||||
|
||||
return <div className="contact-method">{firstNumber || firstEmail}</div>;
|
||||
}
|
||||
|
||||
public renderSendMessage() {
|
||||
const { hasSignalAccount, i18n, onSendMessage } = this.props;
|
||||
|
||||
if (!hasSignalAccount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We don't want the overall click handler for this element to fire, so we stop
|
||||
// propagation before handing control to the caller's callback.
|
||||
const onClick = (e: React.MouseEvent<{}>): void => {
|
||||
e.stopPropagation();
|
||||
onSendMessage();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="send-message" onClick={onClick}>
|
||||
<button className="inner">
|
||||
<div className="icon bubble-icon" />
|
||||
{i18n('sendMessageToContact')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { onOpenContact } = this.props;
|
||||
|
||||
return (
|
||||
<div className="embedded-contact" onClick={onOpenContact}>
|
||||
<div className="first-line">
|
||||
{this.renderAvatar()}
|
||||
<div className="text-container">
|
||||
{this.renderName()}
|
||||
{this.renderContactShorthand()}
|
||||
</div>
|
||||
</div>
|
||||
{this.renderSendMessage()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue