New React component: ConversationListItem, installed in left pane
When collecting a conversation's last message, we grab that message's status as well (if outgoing) and show it.pull/1/head
parent
7e2d7b5e60
commit
675e34fc8d
@ -0,0 +1,186 @@
|
|||||||
|
#### With name and profile
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<ConversationListItem
|
||||||
|
name="Someone 🔥 Somewhere"
|
||||||
|
phoneNumber="(202) 555-0011"
|
||||||
|
avatarPath={util.gifObjectUrl}
|
||||||
|
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||||
|
lastMessage={{
|
||||||
|
text: "What's going on?",
|
||||||
|
status: 'sent',
|
||||||
|
}}
|
||||||
|
onClick={() => console.log('onClick')}
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Profile, with name, no avatar
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<ConversationListItem
|
||||||
|
phoneNumber="(202) 555-0011"
|
||||||
|
name="Mr. Fire🔥"
|
||||||
|
color="green"
|
||||||
|
lastMessage={{
|
||||||
|
text: 'Just a second',
|
||||||
|
status: 'read',
|
||||||
|
}}
|
||||||
|
onClick={() => console.log('onClick')}
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### With unread
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<ConversationListItem
|
||||||
|
phoneNumber="(202) 555-0011"
|
||||||
|
hasUnread={true}
|
||||||
|
lastMessage={{
|
||||||
|
text: 'Hey there!',
|
||||||
|
status: 'sending',
|
||||||
|
}}
|
||||||
|
onClick={() => console.log('onClick')}
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Selected
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<ConversationListItem
|
||||||
|
phoneNumber="(202) 555-0011"
|
||||||
|
isSelected={true}
|
||||||
|
lastMessage={{
|
||||||
|
text: 'Hey there!',
|
||||||
|
}}
|
||||||
|
onClick={() => console.log('onClick')}
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### With emoji/links in message, no status
|
||||||
|
|
||||||
|
We don't want Jumbomoji or links.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<div>
|
||||||
|
<ConversationListItem
|
||||||
|
phoneNumber="(202) 555-0011"
|
||||||
|
lastMessage={{
|
||||||
|
text: 'Download at http://signal.org',
|
||||||
|
}}
|
||||||
|
onClick={() => console.log('onClick')}
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
<ConversationListItem
|
||||||
|
phoneNumber="(202) 555-0011"
|
||||||
|
lastMessage={{
|
||||||
|
text: '🔥',
|
||||||
|
}}
|
||||||
|
onClick={() => console.log('onClick')}
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Long content
|
||||||
|
|
||||||
|
We only show one line.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<div>
|
||||||
|
<ConversationListItem
|
||||||
|
phoneNumber="(202) 555-0011"
|
||||||
|
name="Long contact name. Esquire. The third. And stuff. And more! And more!"
|
||||||
|
lastMessage={{
|
||||||
|
text: 'Normal message',
|
||||||
|
}}
|
||||||
|
onClick={() => console.log('onClick')}
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
<ConversationListItem
|
||||||
|
phoneNumber="(202) 555-0011"
|
||||||
|
lastMessage={{
|
||||||
|
text:
|
||||||
|
"Long line. This is a really really really long line. Really really long. Because that's just how it is",
|
||||||
|
}}
|
||||||
|
onClick={() => console.log('onClick')}
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
<ConversationListItem
|
||||||
|
phoneNumber="(202) 555-0011"
|
||||||
|
lastMessage={{
|
||||||
|
text:
|
||||||
|
"Long line. This is a really really really long line. Really really long. Because that's just how it is",
|
||||||
|
status: 'read',
|
||||||
|
}}
|
||||||
|
onClick={() => console.log('onClick')}
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConversationListItem
|
||||||
|
phoneNumber="(202) 555-0011"
|
||||||
|
lastMessage={{
|
||||||
|
text:
|
||||||
|
"Many lines. This is a many-line message.\nLine 2 is really exciting but it shouldn't be seen.\nLine three is even better.\nLine 4, well.",
|
||||||
|
}}
|
||||||
|
onClick={() => console.log('onClick')}
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
<ConversationListItem
|
||||||
|
phoneNumber="(202) 555-0011"
|
||||||
|
lastMessage={{
|
||||||
|
text:
|
||||||
|
"Many lines. This is a many-line message.\nLine 2 is really exciting but it shouldn't be seen.\nLine three is even better.\nLine 4, well.",
|
||||||
|
status: 'delivered',
|
||||||
|
}}
|
||||||
|
onClick={() => console.log('onClick')}
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### With various ages
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<div>
|
||||||
|
<ConversationListItem
|
||||||
|
phoneNumber="(202) 555-0011"
|
||||||
|
lastUpdated={Date.now() - 5 * 60 * 60 * 1000}
|
||||||
|
lastMessage={{
|
||||||
|
text: 'Five hours ago',
|
||||||
|
}}
|
||||||
|
onClick={() => console.log('onClick')}
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
<ConversationListItem
|
||||||
|
phoneNumber="(202) 555-0011"
|
||||||
|
lastUpdated={Date.now() - 24 * 60 * 60 * 1000}
|
||||||
|
lastMessage={{
|
||||||
|
text: 'One day ago',
|
||||||
|
}}
|
||||||
|
onClick={() => console.log('onClick')}
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
<ConversationListItem
|
||||||
|
phoneNumber="(202) 555-0011"
|
||||||
|
lastUpdated={Date.now() - 7 * 24 * 60 * 60 * 1000}
|
||||||
|
lastMessage={{
|
||||||
|
text: 'One week ago',
|
||||||
|
}}
|
||||||
|
onClick={() => console.log('onClick')}
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
<ConversationListItem
|
||||||
|
phoneNumber="(202) 555-0011"
|
||||||
|
lastUpdated={Date.now() - 365 * 24 * 60 * 60 * 1000}
|
||||||
|
lastMessage={{
|
||||||
|
text: 'One year ago',
|
||||||
|
}}
|
||||||
|
onClick={() => console.log('onClick')}
|
||||||
|
i18n={util.i18n}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
```
|
@ -0,0 +1,159 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { MessageBody } from './conversation/MessageBody';
|
||||||
|
import { Timestamp } from './conversation/Timestamp';
|
||||||
|
import { ContactName } from './conversation/ContactName';
|
||||||
|
import { Localizer } from '../types/Util';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
phoneNumber: string;
|
||||||
|
profileName?: string;
|
||||||
|
name?: string;
|
||||||
|
color?: string;
|
||||||
|
avatarPath?: string;
|
||||||
|
|
||||||
|
lastUpdated: number;
|
||||||
|
hasUnread: boolean;
|
||||||
|
isSelected: boolean;
|
||||||
|
|
||||||
|
lastMessage?: {
|
||||||
|
status: 'sending' | 'sent' | 'delivered' | 'read' | 'error';
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
i18n: Localizer;
|
||||||
|
onClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitial(name: string): string {
|
||||||
|
return name.trim()[0] || '#';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ConversationListItem extends React.Component<Props> {
|
||||||
|
public renderAvatar() {
|
||||||
|
const {
|
||||||
|
avatarPath,
|
||||||
|
color,
|
||||||
|
i18n,
|
||||||
|
name,
|
||||||
|
phoneNumber,
|
||||||
|
profileName,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (!avatarPath) {
|
||||||
|
const initial = getInitial(name || '');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'module-conversation-list-item__avatar',
|
||||||
|
'module-conversation-list-item__default-avatar',
|
||||||
|
`module-conversation-list-item__default-avatar--${color}`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{initial}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = `${name || phoneNumber}${
|
||||||
|
!name && profileName ? ` ~${profileName}` : ''
|
||||||
|
}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
className="module-conversation-list-item__avatar"
|
||||||
|
alt={i18n('contactAvatarAlt', [title])}
|
||||||
|
src={avatarPath}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public renderHeader() {
|
||||||
|
const { i18n, lastUpdated, name, phoneNumber, profileName } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="module-conversation-list-item__header">
|
||||||
|
<div className="module-conversation-list-item__header__name">
|
||||||
|
<ContactName
|
||||||
|
phoneNumber={phoneNumber}
|
||||||
|
name={name}
|
||||||
|
profileName={profileName}
|
||||||
|
i18n={i18n}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="module-conversation-list-item__header__date">
|
||||||
|
<Timestamp
|
||||||
|
timestamp={lastUpdated}
|
||||||
|
extended={false}
|
||||||
|
module="module-conversation-list-item__header__timestamp"
|
||||||
|
i18n={i18n}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public renderMessage() {
|
||||||
|
const { lastMessage, hasUnread, i18n } = this.props;
|
||||||
|
|
||||||
|
if (!lastMessage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="module-conversation-list-item__message">
|
||||||
|
{lastMessage.text ? (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'module-conversation-list-item__message__text',
|
||||||
|
hasUnread
|
||||||
|
? 'module-conversation-list-item__message__text--has-unread'
|
||||||
|
: null
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<MessageBody
|
||||||
|
text={lastMessage.text}
|
||||||
|
disableJumbomoji={true}
|
||||||
|
disableLinks={true}
|
||||||
|
i18n={i18n}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{lastMessage.status ? (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'module-conversation-list-item__message__status-icon',
|
||||||
|
`module-conversation-list-item__message__status-icon--${
|
||||||
|
lastMessage.status
|
||||||
|
}`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { hasUnread, onClick, isSelected } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
onClick={onClick}
|
||||||
|
className={classNames(
|
||||||
|
'module-conversation-list-item',
|
||||||
|
hasUnread ? 'module-conversation-list-item--has-unread' : null,
|
||||||
|
isSelected ? 'module-conversation-list-item--is-selected' : null
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{this.renderAvatar()}
|
||||||
|
<div className="module-conversation-list-item__content">
|
||||||
|
{this.renderHeader()}
|
||||||
|
{this.renderMessage()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,127 +0,0 @@
|
|||||||
export function messageSelector({ model, view }: { model: any; view: any }) {
|
|
||||||
// tslint:disable-next-line
|
|
||||||
console.log({ model, view });
|
|
||||||
|
|
||||||
return null;
|
|
||||||
// const avatar = this.model.getAvatar();
|
|
||||||
// const avatarPath = avatar && avatar.url;
|
|
||||||
// const color = avatar && avatar.color;
|
|
||||||
// const isMe = this.ourNumber === this.model.id;
|
|
||||||
|
|
||||||
// const attachments = this.model.get('attachments') || [];
|
|
||||||
// const loadedAttachmentViews = Promise.all(
|
|
||||||
// attachments.map(
|
|
||||||
// attachment =>
|
|
||||||
// new Promise(async resolve => {
|
|
||||||
// const attachmentWithData = await loadAttachmentData(attachment);
|
|
||||||
// const view = new Whisper.AttachmentView({
|
|
||||||
// model: attachmentWithData,
|
|
||||||
// timestamp: this.model.get('sent_at'),
|
|
||||||
// });
|
|
||||||
|
|
||||||
// this.listenTo(view, 'update', () => {
|
|
||||||
// // NOTE: Can we do without `updated` flag now that we use promises?
|
|
||||||
// view.updated = true;
|
|
||||||
// resolve(view);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// view.render();
|
|
||||||
// })
|
|
||||||
// )
|
|
||||||
// );
|
|
||||||
|
|
||||||
// Wiring up TimerNotification
|
|
||||||
|
|
||||||
// this.conversation = this.model.getExpirationTimerUpdateSource();
|
|
||||||
// this.listenTo(this.conversation, 'change', this.render);
|
|
||||||
// this.listenTo(this.model, 'unload', this.remove);
|
|
||||||
// this.listenTo(this.model, 'change', this.onChange);
|
|
||||||
|
|
||||||
// Wiring up SafetyNumberNotification
|
|
||||||
|
|
||||||
// this.conversation = this.model.getModelForKeyChange();
|
|
||||||
// this.listenTo(this.conversation, 'change', this.render);
|
|
||||||
// this.listenTo(this.model, 'unload', this.remove);
|
|
||||||
|
|
||||||
// Wiring up VerificationNotification
|
|
||||||
|
|
||||||
// this.conversation = this.model.getModelForVerifiedChange();
|
|
||||||
// this.listenTo(this.conversation, 'change', this.render);
|
|
||||||
// this.listenTo(this.model, 'unload', this.remove);
|
|
||||||
|
|
||||||
// this.contactView = new Whisper.ReactWrapperView({
|
|
||||||
// className: 'contact-wrapper',
|
|
||||||
// Component: window.Signal.Components.ContactListItem,
|
|
||||||
// props: {
|
|
||||||
// isMe,
|
|
||||||
// color,
|
|
||||||
// avatarPath,
|
|
||||||
// phoneNumber: model.getNumber(),
|
|
||||||
// name: model.getName(),
|
|
||||||
// profileName: model.getProfileName(),
|
|
||||||
// verified: model.isVerified(),
|
|
||||||
// onClick: showIdentity,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// this.$el.append(this.contactView.el);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We actually don't listen to the model telling us that it's gone if it's disappearing
|
|
||||||
// onDestroy() {
|
|
||||||
// if (this.$el.hasClass('expired')) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// this.onUnload();
|
|
||||||
// },
|
|
||||||
|
|
||||||
// The backflips required to maintain scroll position when loading images
|
|
||||||
// Key is only adding the img to the DOM when the image has loaded.
|
|
||||||
//
|
|
||||||
// How might we get similar behavior with React?
|
|
||||||
//
|
|
||||||
// this.trigger('beforeChangeHeight');
|
|
||||||
// this.$('.attachments').append(view.el);
|
|
||||||
// view.setElement(view.el);
|
|
||||||
// this.trigger('afterChangeHeight');
|
|
||||||
|
|
||||||
// Timer code
|
|
||||||
|
|
||||||
// if (this.model.isExpired()) {
|
|
||||||
// return this;
|
|
||||||
// }
|
|
||||||
// if (this.model.isExpiring()) {
|
|
||||||
// this.render();
|
|
||||||
// const totalTime = this.model.get('expireTimer') * 1000;
|
|
||||||
// const remainingTime = this.model.msTilExpire();
|
|
||||||
// const elapsed = (totalTime - remainingTime) / totalTime;
|
|
||||||
// this.$('.sand').css('transform', `translateY(${elapsed * 100}%)`);
|
|
||||||
// this.$el.css('display', 'inline-block');
|
|
||||||
// this.timeout = setTimeout(
|
|
||||||
// this.update.bind(this),
|
|
||||||
// Math.max(totalTime / 100, 500)
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Expiring message
|
|
||||||
|
|
||||||
// this.$el.addClass('expired');
|
|
||||||
// this.$el.find('.bubble').one('webkitAnimationEnd animationend', e => {
|
|
||||||
// if (e.target === this.$('.bubble')[0]) {
|
|
||||||
// this.remove();
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // Failsafe: if in the background, animation events don't fire
|
|
||||||
// setTimeout(this.remove.bind(this), 1000);
|
|
||||||
|
|
||||||
// Retrying a message
|
|
||||||
// retryMessage() {
|
|
||||||
// const retrys = _.filter(
|
|
||||||
// this.model.get('errors'),
|
|
||||||
// this.model.isReplayableError.bind(this.model)
|
|
||||||
// );
|
|
||||||
// _.map(retrys, 'number').forEach(number => {
|
|
||||||
// this.model.resend(number);
|
|
||||||
// });
|
|
||||||
// },
|
|
Loading…
Reference in New Issue