Refactor messages model; New timeline react components
This commit is contained in:
parent
d342b23cbc
commit
c41bc53614
31 changed files with 1463 additions and 3395 deletions
File diff suppressed because it is too large
Load diff
|
@ -1187,7 +1187,15 @@
|
|||
this.listenBack(view);
|
||||
},
|
||||
|
||||
forceSend({ contact, message }) {
|
||||
forceSend({ contactId, messageId }) {
|
||||
const contact = ConversationController.get(contactId);
|
||||
const message = this.model.messageCollection.get(messageId);
|
||||
if (!message) {
|
||||
throw new Error(
|
||||
`deleteMessage: Did not find message for id ${messageId}`
|
||||
);
|
||||
}
|
||||
|
||||
const dialog = new Whisper.ConfirmationDialogView({
|
||||
message: i18n('identityKeyErrorOnSend', [
|
||||
contact.getTitle(),
|
||||
|
|
|
@ -43,37 +43,38 @@
|
|||
},
|
||||
getRenderInfo() {
|
||||
const { Components } = window.Signal;
|
||||
const { type, data: props } = this.model.props;
|
||||
|
||||
if (this.model.propsForTimerNotification) {
|
||||
if (type === 'timerNotification') {
|
||||
return {
|
||||
Component: Components.TimerNotification,
|
||||
props: this.model.propsForTimerNotification,
|
||||
props,
|
||||
};
|
||||
} else if (this.model.propsForSafetyNumberNotification) {
|
||||
} else if (type === 'safetyNumberNotification') {
|
||||
return {
|
||||
Component: Components.SafetyNumberNotification,
|
||||
props: this.model.propsForSafetyNumberNotification,
|
||||
props,
|
||||
};
|
||||
} else if (this.model.propsForVerificationNotification) {
|
||||
} else if (type === 'verificationNotification') {
|
||||
return {
|
||||
Component: Components.VerificationNotification,
|
||||
props: this.model.propsForVerificationNotification,
|
||||
props,
|
||||
};
|
||||
} else if (this.model.propsForResetSessionNotification) {
|
||||
return {
|
||||
Component: Components.ResetSessionNotification,
|
||||
props: this.model.propsForResetSessionNotification,
|
||||
};
|
||||
} else if (this.model.propsForGroupNotification) {
|
||||
} else if (type === 'groupNotification') {
|
||||
return {
|
||||
Component: Components.GroupNotification,
|
||||
props: this.model.propsForGroupNotification,
|
||||
props,
|
||||
};
|
||||
} else if (type === 'resetSessionNotification') {
|
||||
return {
|
||||
Component: Components.ResetSessionNotification,
|
||||
props,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
Component: Components.Message,
|
||||
props: this.model.propsForMessage,
|
||||
props,
|
||||
};
|
||||
},
|
||||
render() {
|
||||
|
|
|
@ -72,6 +72,7 @@
|
|||
"js-yaml": "3.13.0",
|
||||
"linkify-it": "2.0.3",
|
||||
"lodash": "4.17.11",
|
||||
"memoizee": "0.4.14",
|
||||
"mkdirp": "0.5.1",
|
||||
"moment": "2.21.0",
|
||||
"mustache": "2.3.0",
|
||||
|
@ -115,6 +116,7 @@
|
|||
"@types/js-yaml": "3.12.0",
|
||||
"@types/linkify-it": "2.0.3",
|
||||
"@types/lodash": "4.14.106",
|
||||
"@types/memoizee": "0.4.2",
|
||||
"@types/mkdirp": "0.5.2",
|
||||
"@types/mocha": "5.0.0",
|
||||
"@types/pify": "3.0.2",
|
||||
|
|
|
@ -3105,6 +3105,20 @@
|
|||
font-size: 13px;
|
||||
}
|
||||
|
||||
// Module: Timeline
|
||||
|
||||
.module-timeline {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.module-timeline__message-container {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.ReactVirtualized__List {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// Third-party module: react-contextmenu
|
||||
|
||||
.react-contextmenu {
|
||||
|
|
2712
test/fixtures.js
2712
test/fixtures.js
File diff suppressed because it is too large
Load diff
|
@ -1,38 +0,0 @@
|
|||
/* global $, ConversationController, textsecure, Whisper */
|
||||
|
||||
'use strict';
|
||||
|
||||
describe('Fixtures', () => {
|
||||
before(async () => {
|
||||
// NetworkStatusView checks this method every five seconds while showing
|
||||
window.getSocketStatus = () => WebSocket.OPEN;
|
||||
|
||||
await clearDatabase();
|
||||
await textsecure.storage.user.setNumberAndDeviceId(
|
||||
'+17015552000',
|
||||
2,
|
||||
'testDevice'
|
||||
);
|
||||
|
||||
await ConversationController.getOrCreateAndWait(
|
||||
textsecure.storage.user.getNumber(),
|
||||
'private'
|
||||
);
|
||||
});
|
||||
|
||||
it('renders', async () => {
|
||||
await Whisper.Fixtures().saveAll();
|
||||
|
||||
ConversationController.reset();
|
||||
await ConversationController.load();
|
||||
|
||||
let view = new Whisper.InboxView({ window });
|
||||
view.onEmpty();
|
||||
view.$el.prependTo($('#render-light-theme'));
|
||||
|
||||
view = new Whisper.InboxView({ window });
|
||||
view.$el.removeClass('light-theme').addClass('dark-theme');
|
||||
view.onEmpty();
|
||||
view.$el.prependTo($('#render-dark-theme'));
|
||||
});
|
||||
});
|
|
@ -7,15 +7,8 @@
|
|||
<link rel="stylesheet" href="../stylesheets/manifest.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="mocha">
|
||||
</div>
|
||||
<div id="tests">
|
||||
</div>
|
||||
<div id="render-light-theme" class='index' style="width: 800; height: 500; margin:10px; border: solid 1px black;">
|
||||
</div>
|
||||
<div id="render-dark-theme" class='index' style="width: 800; height: 500; margin:10px; border: solid 1px black;">
|
||||
</div>
|
||||
</div>
|
||||
<div id="mocha"></div>
|
||||
<div id="tests"></div>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='app-loading-screen'>
|
||||
<div class='content'>
|
||||
|
@ -493,14 +486,12 @@
|
|||
<script type='text/javascript' src='../js/views/file_input_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/list_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/contact_list_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/timestamp_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/message_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/key_verification_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/message_list_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/group_member_list_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/recorder_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/conversation_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/hint_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/inbox_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/network_status_view.js'></script>
|
||||
<script type='text/javascript' src='../js/views/confirmation_dialog_view.js' data-cover></script>
|
||||
|
@ -515,12 +506,10 @@
|
|||
<script type="text/javascript" src="views/whisper_view_test.js"></script>
|
||||
<script type="text/javascript" src="views/timestamp_view_test.js"></script>
|
||||
<script type="text/javascript" src="views/list_view_test.js"></script>
|
||||
<script type="text/javascript" src="views/inbox_view_test.js"></script>
|
||||
<script type="text/javascript" src="views/network_status_view_test.js"></script>
|
||||
<script type="text/javascript" src="views/last_seen_indicator_view_test.js"></script>
|
||||
<script type='text/javascript' src='views/scroll_down_button_view_test.js'></script>
|
||||
|
||||
<script type="text/javascript" src="models/conversations_test.js"></script>
|
||||
<script type="text/javascript" src="models/messages_test.js"></script>
|
||||
|
||||
<script type="text/javascript" src="libphonenumber_util_test.js"></script>
|
||||
|
@ -534,9 +523,6 @@
|
|||
<script type="text/javascript" src="i18n_test.js"></script>
|
||||
<script type="text/javascript" src="spellcheck_test.js"></script>
|
||||
|
||||
<script type="text/javascript" src="fixtures.js"></script>
|
||||
<script type="text/javascript" src="fixtures_test.js"></script>
|
||||
|
||||
<!-- Comment out to turn off code coverage. Useful for getting real callstacks. -->
|
||||
<!-- NOTE: blanket doesn't support modern syntax and will choke until we find a replacement. :0( -->
|
||||
<!-- <script type="text/javascript" src="blanket_mocha.js"></script> -->
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
/* global ConversationController, textsecure, Whisper */
|
||||
|
||||
describe('InboxView', () => {
|
||||
let inboxView;
|
||||
let conversation;
|
||||
|
||||
before(async () => {
|
||||
ConversationController.reset();
|
||||
await ConversationController.load();
|
||||
await textsecure.storage.user.setNumberAndDeviceId(
|
||||
'18005554444',
|
||||
1,
|
||||
'Home Office'
|
||||
);
|
||||
await ConversationController.getOrCreateAndWait(
|
||||
textsecure.storage.user.getNumber(),
|
||||
'private'
|
||||
);
|
||||
inboxView = new Whisper.InboxView({
|
||||
model: {},
|
||||
window,
|
||||
initialLoadComplete() {},
|
||||
}).render();
|
||||
|
||||
conversation = new Whisper.Conversation({
|
||||
id: '1234',
|
||||
type: 'private',
|
||||
});
|
||||
});
|
||||
|
||||
describe('the conversation stack', () => {
|
||||
it('should be rendered', () => {
|
||||
assert.ok(inboxView.$('.conversation-stack').length === 1);
|
||||
});
|
||||
|
||||
describe('opening a conversation', () => {
|
||||
let triggeredOpenedCount = 0;
|
||||
|
||||
before(() => {
|
||||
conversation.on('opened', () => {
|
||||
triggeredOpenedCount += 1;
|
||||
});
|
||||
|
||||
inboxView.conversation_stack.open(conversation);
|
||||
});
|
||||
|
||||
it('should trigger an opened event', () => {
|
||||
assert.ok(triggeredOpenedCount === 1);
|
||||
});
|
||||
|
||||
describe('and then opening it again immediately', () => {
|
||||
before(() => {
|
||||
inboxView.conversation_stack.open(conversation);
|
||||
});
|
||||
|
||||
it('should trigger the opened event again', () => {
|
||||
assert.ok(triggeredOpenedCount === 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -70,13 +70,17 @@ export class LeftPane extends React.Component<Props> {
|
|||
: conversations[index];
|
||||
|
||||
return (
|
||||
<ConversationListItem
|
||||
<div
|
||||
key={key}
|
||||
className="module-left-pane__conversation-container"
|
||||
style={style}
|
||||
>
|
||||
<ConversationListItem
|
||||
{...conversation}
|
||||
onClick={openConversationInternal}
|
||||
i18n={i18n}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -21,10 +21,15 @@ interface Change {
|
|||
contacts?: Array<Contact>;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
export type PropsData = {
|
||||
changes: Array<Change>;
|
||||
};
|
||||
|
||||
type PropsHousekeeping = {
|
||||
i18n: LocalizerType;
|
||||
}
|
||||
};
|
||||
|
||||
type Props = PropsData & PropsHousekeeping;
|
||||
|
||||
export class GroupNotification extends React.Component<Props> {
|
||||
public renderChange(change: Change) {
|
||||
|
|
15
ts/components/conversation/LastSeenIndicator.md
Normal file
15
ts/components/conversation/LastSeenIndicator.md
Normal file
|
@ -0,0 +1,15 @@
|
|||
### One
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<LastSeenIndicator count={1} i18n={util.i18n} />
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
### More than one
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<LastSeenIndicator count={2} i18n={util.i18n} />
|
||||
</util.ConversationContext>
|
||||
```
|
26
ts/components/conversation/LastSeenIndicator.tsx
Normal file
26
ts/components/conversation/LastSeenIndicator.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
type Props = {
|
||||
count: number;
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
export class LastSeenIndicator extends React.Component<Props> {
|
||||
public render() {
|
||||
const { count, i18n } = this.props;
|
||||
|
||||
const message =
|
||||
count === 1
|
||||
? i18n('unreadMessage')
|
||||
: i18n('unreadMessages', [String(count)]);
|
||||
|
||||
return (
|
||||
<div className="module-last-seen-indicator">
|
||||
<div className="module-last-seen-indicator__bar" />
|
||||
<div className="module-last-seen-indicator__text">{message}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Avatar } from '../Avatar';
|
||||
|
@ -46,7 +47,7 @@ interface LinkPreviewType {
|
|||
image?: AttachmentType;
|
||||
}
|
||||
|
||||
type PropsData = {
|
||||
export type PropsData = {
|
||||
id: string;
|
||||
text?: string;
|
||||
textPending?: boolean;
|
||||
|
@ -863,7 +864,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
const isDangerous = isFileDangerous(fileName || '');
|
||||
const multipleAttachments = attachments && attachments.length > 1;
|
||||
|
||||
return (
|
||||
const menu = (
|
||||
<ContextMenu id={triggerId}>
|
||||
{!multipleAttachments && attachments && attachments[0] ? (
|
||||
<MenuItem
|
||||
|
@ -925,6 +926,8 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
</MenuItem>
|
||||
</ContextMenu>
|
||||
);
|
||||
|
||||
return ReactDOM.createPortal(menu, document.body);
|
||||
}
|
||||
|
||||
public getWidth(): number | undefined {
|
||||
|
|
|
@ -12,7 +12,7 @@ interface ContactType {
|
|||
name?: string;
|
||||
}
|
||||
|
||||
type PropsData = {
|
||||
export type PropsData = {
|
||||
isGroup: boolean;
|
||||
contact: ContactType;
|
||||
};
|
||||
|
|
38
ts/components/conversation/ScrollDownButton.md
Normal file
38
ts/components/conversation/ScrollDownButton.md
Normal file
|
@ -0,0 +1,38 @@
|
|||
### None
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<ScrollDownButton
|
||||
count={0}
|
||||
conversationId="id-1"
|
||||
scrollDown={id => console.log('scrollDown', id)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
### One
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<ScrollDownButton
|
||||
count={1}
|
||||
conversationId="id-2"
|
||||
scrollDown={id => console.log('scrollDown', id)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
### More than one
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<ScrollDownButton
|
||||
count={2}
|
||||
conversationId="id-3"
|
||||
scrollDown={id => console.log('scrollDown', id)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</util.ConversationContext>
|
||||
```
|
43
ts/components/conversation/ScrollDownButton.tsx
Normal file
43
ts/components/conversation/ScrollDownButton.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
type Props = {
|
||||
count: number;
|
||||
conversationId: string;
|
||||
|
||||
scrollDown: (conversationId: string) => void;
|
||||
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
export class ScrollDownButton extends React.Component<Props> {
|
||||
public render() {
|
||||
const { conversationId, count, i18n, scrollDown } = this.props;
|
||||
|
||||
let altText = i18n('scrollDown');
|
||||
if (count > 1) {
|
||||
altText = i18n('messagesBelow');
|
||||
} else if (count === 1) {
|
||||
altText = i18n('messageBelow');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-scroll-down">
|
||||
<button
|
||||
className={classNames(
|
||||
'module-scroll-down__button',
|
||||
count > 0 ? 'module-scroll-down__button--new-messages' : null
|
||||
)}
|
||||
onClick={() => {
|
||||
scrollDown(conversationId);
|
||||
}}
|
||||
title={altText}
|
||||
>
|
||||
<div className="module-scroll-down__icon" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
179
ts/components/conversation/Timeline.md
Normal file
179
ts/components/conversation/Timeline.md
Normal file
|
@ -0,0 +1,179 @@
|
|||
```javascript
|
||||
const itemLookup = {
|
||||
'id-1': {
|
||||
type: 'message',
|
||||
data: {
|
||||
id: 'id-1',
|
||||
direction: 'incoming',
|
||||
timestamp: Date.now(),
|
||||
authorPhoneNumber: '(202) 555-2001',
|
||||
authorColor: 'green',
|
||||
text: '🔥',
|
||||
},
|
||||
},
|
||||
'id-2': {
|
||||
type: 'message',
|
||||
data: {
|
||||
id: 'id-2',
|
||||
direction: 'incoming',
|
||||
timestamp: Date.now(),
|
||||
authorColor: 'green',
|
||||
text: 'Hello there from the new world! http://somewhere.com',
|
||||
},
|
||||
},
|
||||
'id-3': {
|
||||
type: 'message',
|
||||
data: {
|
||||
id: 'id-3',
|
||||
collapseMetadata: true,
|
||||
direction: 'incoming',
|
||||
timestamp: Date.now(),
|
||||
authorColor: 'red',
|
||||
text: 'Hello there from the new world!',
|
||||
},
|
||||
},
|
||||
'id-4': {
|
||||
type: 'timerNotification',
|
||||
data: {
|
||||
type: 'fromMe',
|
||||
timespan: '5 minutes',
|
||||
},
|
||||
},
|
||||
'id-5': {
|
||||
type: 'timerNotification',
|
||||
data: {
|
||||
type: 'fromOther',
|
||||
phoneNumber: '(202) 555-0000',
|
||||
timespan: '1 hour',
|
||||
},
|
||||
},
|
||||
'id-6': {
|
||||
type: 'safetyNumberNotification',
|
||||
data: {
|
||||
contact: {
|
||||
id: '+1202555000',
|
||||
phoneNumber: '(202) 555-0000',
|
||||
profileName: 'Mr. Fire',
|
||||
},
|
||||
},
|
||||
},
|
||||
'id-7': {
|
||||
type: 'verificationNotification',
|
||||
data: {
|
||||
contact: {
|
||||
phoneNumber: '(202) 555-0001',
|
||||
name: 'Mrs. Ice',
|
||||
},
|
||||
isLocal: true,
|
||||
type: 'markVerified',
|
||||
},
|
||||
},
|
||||
'id-8': {
|
||||
type: 'groupNotification',
|
||||
data: {
|
||||
changes: [
|
||||
{
|
||||
type: 'name',
|
||||
newName: 'Squirrels and their uses',
|
||||
},
|
||||
{
|
||||
type: 'add',
|
||||
contacts: [
|
||||
{
|
||||
phoneNumber: '(202) 555-0002',
|
||||
},
|
||||
{
|
||||
phoneNumber: '(202) 555-0003',
|
||||
profileName: 'Ms. Water',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
isMe: false,
|
||||
},
|
||||
},
|
||||
'id-9': {
|
||||
type: 'resetSessionNotification',
|
||||
data: null,
|
||||
},
|
||||
'id-10': {
|
||||
type: 'message',
|
||||
data: {
|
||||
id: 'id-6',
|
||||
direction: 'outgoing',
|
||||
timestamp: Date.now(),
|
||||
status: 'sent',
|
||||
authorColor: 'pink',
|
||||
text: '🔥',
|
||||
},
|
||||
},
|
||||
'id-11': {
|
||||
type: 'message',
|
||||
data: {
|
||||
id: 'id-7',
|
||||
direction: 'outgoing',
|
||||
timestamp: Date.now(),
|
||||
status: 'read',
|
||||
authorColor: 'pink',
|
||||
text: 'Hello there from the new world! http://somewhere.com',
|
||||
},
|
||||
},
|
||||
'id-12': {
|
||||
type: 'message',
|
||||
data: {
|
||||
id: 'id-8',
|
||||
collapseMetadata: true,
|
||||
direction: 'outgoing',
|
||||
status: 'sent',
|
||||
timestamp: Date.now(),
|
||||
text: 'Hello there from the new world! 🔥',
|
||||
},
|
||||
},
|
||||
'id-13': {
|
||||
type: 'message',
|
||||
data: {
|
||||
id: 'id-9',
|
||||
direction: 'outgoing',
|
||||
status: 'sent',
|
||||
timestamp: Date.now(),
|
||||
authorColor: 'blue',
|
||||
text:
|
||||
'Hello there from the new world! And this is multiple lines of text. Lines and lines and lines.',
|
||||
},
|
||||
},
|
||||
'id-14': {
|
||||
type: 'message',
|
||||
data: {
|
||||
id: 'id-10',
|
||||
direction: 'outgoing',
|
||||
status: 'read',
|
||||
timestamp: Date.now(),
|
||||
collapseMetadata: true,
|
||||
text:
|
||||
'Hello there from the new world! And this is multiple lines of text. Lines and lines and lines.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const actions = {
|
||||
downloadAttachment: options => console.log('onDownload', options),
|
||||
replyToitem: id => console.log('onReply', id),
|
||||
showMessageDetail: id => console.log('onShowDetail', id),
|
||||
deleteMessage: id => console.log('onDelete', id),
|
||||
};
|
||||
|
||||
const items = util._.keys(itemLookup);
|
||||
const renderItem = id => {
|
||||
const item = itemLookup[id];
|
||||
|
||||
// Because we can't use ...item syntax
|
||||
return React.createElement(
|
||||
TimelineItem,
|
||||
util._.merge({ item, i18n: util.i18n }, actions)
|
||||
);
|
||||
};
|
||||
|
||||
<div style={{ height: '300px' }}>
|
||||
<Timeline items={items} renderItem={renderItem} i18n={util.i18n} />
|
||||
</div>;
|
||||
```
|
129
ts/components/conversation/Timeline.tsx
Normal file
129
ts/components/conversation/Timeline.tsx
Normal file
|
@ -0,0 +1,129 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
AutoSizer,
|
||||
CellMeasurer,
|
||||
CellMeasurerCache,
|
||||
List,
|
||||
} from 'react-virtualized';
|
||||
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
import { PropsActions as MessageActionsType } from './Message';
|
||||
import { PropsActions as SafetyNumberActionsType } from './SafetyNumberNotification';
|
||||
|
||||
type PropsData = {
|
||||
items: Array<string>;
|
||||
|
||||
renderItem: (id: string) => JSX.Element;
|
||||
};
|
||||
|
||||
type PropsHousekeeping = {
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
type PropsActions = MessageActionsType & SafetyNumberActionsType;
|
||||
|
||||
type Props = PropsData & PropsHousekeeping & PropsActions;
|
||||
|
||||
// from https://github.com/bvaughn/react-virtualized/blob/fb3484ed5dcc41bffae8eab029126c0fb8f7abc0/source/List/types.js#L5
|
||||
type RowRendererParamsType = {
|
||||
index: number;
|
||||
isScrolling: boolean;
|
||||
isVisible: boolean;
|
||||
key: string;
|
||||
parent: Object;
|
||||
style: Object;
|
||||
};
|
||||
|
||||
export class Timeline extends React.PureComponent<Props> {
|
||||
public cellSizeCache = new CellMeasurerCache({
|
||||
defaultHeight: 85,
|
||||
fixedWidth: true,
|
||||
});
|
||||
public mostRecentWidth = 0;
|
||||
public resizeAllFlag = false;
|
||||
public listRef = React.createRef<any>();
|
||||
|
||||
public componentDidUpdate(prevProps: Props) {
|
||||
if (this.resizeAllFlag) {
|
||||
this.resizeAllFlag = false;
|
||||
this.cellSizeCache.clearAll();
|
||||
this.recomputeRowHeights();
|
||||
} else if (this.props.items !== prevProps.items) {
|
||||
const index = prevProps.items.length;
|
||||
this.cellSizeCache.clear(index, 0);
|
||||
this.recomputeRowHeights(index);
|
||||
}
|
||||
}
|
||||
|
||||
public resizeAll = () => {
|
||||
this.resizeAllFlag = false;
|
||||
this.cellSizeCache.clearAll();
|
||||
};
|
||||
|
||||
public recomputeRowHeights = (index?: number) => {
|
||||
if (this.listRef && this.listRef) {
|
||||
this.listRef.current.recomputeRowHeights(index);
|
||||
}
|
||||
};
|
||||
|
||||
public rowRenderer = ({
|
||||
index,
|
||||
key,
|
||||
parent,
|
||||
style,
|
||||
}: RowRendererParamsType) => {
|
||||
const { items, renderItem } = this.props;
|
||||
const messageId = items[index];
|
||||
|
||||
return (
|
||||
<CellMeasurer
|
||||
cache={this.cellSizeCache}
|
||||
columnIndex={0}
|
||||
key={key}
|
||||
parent={parent}
|
||||
rowIndex={index}
|
||||
width={this.mostRecentWidth}
|
||||
>
|
||||
<div className="module-timeline__message-container" style={style}>
|
||||
{renderItem(messageId)}
|
||||
</div>
|
||||
</CellMeasurer>
|
||||
);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { items } = this.props;
|
||||
|
||||
return (
|
||||
<div className="module-timeline">
|
||||
<AutoSizer>
|
||||
{({ height, width }) => {
|
||||
if (this.mostRecentWidth && this.mostRecentWidth !== width) {
|
||||
this.resizeAllFlag = true;
|
||||
|
||||
setTimeout(this.resizeAll, 0);
|
||||
}
|
||||
|
||||
this.mostRecentWidth = width;
|
||||
|
||||
return (
|
||||
<List
|
||||
deferredMeasurementCache={this.cellSizeCache}
|
||||
height={height}
|
||||
// This also registers us with parent InfiniteLoader
|
||||
// onRowsRendered={onRowsRendered}
|
||||
overscanRowCount={0}
|
||||
ref={this.listRef}
|
||||
rowCount={items.length}
|
||||
rowHeight={this.cellSizeCache.rowHeight}
|
||||
rowRenderer={this.rowRenderer}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
3
ts/components/conversation/TimelineItem.md
Normal file
3
ts/components/conversation/TimelineItem.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
```jsx
|
||||
const item = {} < TimelineItem;
|
||||
```
|
106
ts/components/conversation/TimelineItem.tsx
Normal file
106
ts/components/conversation/TimelineItem.tsx
Normal file
|
@ -0,0 +1,106 @@
|
|||
import React from 'react';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
import {
|
||||
Message,
|
||||
PropsActions as MessageActionsType,
|
||||
PropsData as MessageProps,
|
||||
} from './Message';
|
||||
import {
|
||||
PropsData as TimerNotificationProps,
|
||||
TimerNotification,
|
||||
} from './TimerNotification';
|
||||
import {
|
||||
PropsActions as SafetyNumberActionsType,
|
||||
PropsData as SafetyNumberNotificationProps,
|
||||
SafetyNumberNotification,
|
||||
} from './SafetyNumberNotification';
|
||||
import {
|
||||
PropsData as VerificationNotificationProps,
|
||||
VerificationNotification,
|
||||
} from './VerificationNotification';
|
||||
import {
|
||||
GroupNotification,
|
||||
PropsData as GroupNotificationProps,
|
||||
} from './GroupNotification';
|
||||
import { ResetSessionNotification } from './ResetSessionNotification';
|
||||
|
||||
type MessageType = {
|
||||
type: 'message';
|
||||
data: MessageProps;
|
||||
};
|
||||
type TimerNotificationType = {
|
||||
type: 'timerNotification';
|
||||
data: TimerNotificationProps;
|
||||
};
|
||||
type SafetyNumberNotificationType = {
|
||||
type: 'safetyNumberNotification';
|
||||
data: SafetyNumberNotificationProps;
|
||||
};
|
||||
type VerificationNotificationType = {
|
||||
type: 'verificationNotification';
|
||||
data: VerificationNotificationProps;
|
||||
};
|
||||
type GroupNotificationType = {
|
||||
type: 'groupNotification';
|
||||
data: GroupNotificationProps;
|
||||
};
|
||||
type ResetSessionNotificationType = {
|
||||
type: 'resetSessionNotification';
|
||||
data: null;
|
||||
};
|
||||
|
||||
type PropsData = {
|
||||
item:
|
||||
| MessageType
|
||||
| TimerNotificationType
|
||||
| SafetyNumberNotificationType
|
||||
| VerificationNotificationType
|
||||
| ResetSessionNotificationType
|
||||
| GroupNotificationType;
|
||||
};
|
||||
|
||||
type PropsHousekeeping = {
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
type PropsActions = MessageActionsType & SafetyNumberActionsType;
|
||||
|
||||
type Props = PropsData & PropsHousekeeping & PropsActions;
|
||||
|
||||
export class TimelineItem extends React.PureComponent<Props> {
|
||||
public render() {
|
||||
const { item, i18n } = this.props;
|
||||
|
||||
if (!item) {
|
||||
throw new Error('TimelineItem: Item was not provided!');
|
||||
}
|
||||
|
||||
if (item.type === 'message') {
|
||||
return <Message {...this.props} {...item.data} i18n={i18n} />;
|
||||
}
|
||||
if (item.type === 'timerNotification') {
|
||||
return <TimerNotification {...this.props} {...item.data} i18n={i18n} />;
|
||||
}
|
||||
if (item.type === 'safetyNumberNotification') {
|
||||
return (
|
||||
<SafetyNumberNotification {...this.props} {...item.data} i18n={i18n} />
|
||||
);
|
||||
}
|
||||
if (item.type === 'verificationNotification') {
|
||||
return (
|
||||
<VerificationNotification {...this.props} {...item.data} i18n={i18n} />
|
||||
);
|
||||
}
|
||||
if (item.type === 'groupNotification') {
|
||||
return <GroupNotification {...this.props} {...item.data} i18n={i18n} />;
|
||||
}
|
||||
if (item.type === 'resetSessionNotification') {
|
||||
return (
|
||||
<ResetSessionNotification {...this.props} {...item.data} i18n={i18n} />
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error('TimelineItem: Unknown type!');
|
||||
}
|
||||
}
|
|
@ -7,15 +7,20 @@ import { LocalizerType } from '../../types/Util';
|
|||
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
|
||||
interface Props {
|
||||
export type PropsData = {
|
||||
type: 'fromOther' | 'fromMe' | 'fromSync';
|
||||
phoneNumber: string;
|
||||
profileName?: string;
|
||||
name?: string;
|
||||
disabled: boolean;
|
||||
timespan: string;
|
||||
};
|
||||
|
||||
type PropsHousekeeping = {
|
||||
i18n: LocalizerType;
|
||||
}
|
||||
};
|
||||
|
||||
type Props = PropsData & PropsHousekeeping;
|
||||
|
||||
export class TimerNotification extends React.Component<Props> {
|
||||
public renderContents() {
|
||||
|
|
|
@ -13,12 +13,17 @@ interface Contact {
|
|||
name?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
export type PropsData = {
|
||||
type: 'markVerified' | 'markNotVerified';
|
||||
isLocal: boolean;
|
||||
contact: Contact;
|
||||
};
|
||||
|
||||
type PropsHousekeeping = {
|
||||
i18n: LocalizerType;
|
||||
}
|
||||
};
|
||||
|
||||
type Props = PropsData & PropsHousekeeping;
|
||||
|
||||
export class VerificationNotification extends React.Component<Props> {
|
||||
public getStringId() {
|
||||
|
|
|
@ -5,7 +5,7 @@ import { NoopActionType } from './noop';
|
|||
|
||||
// State
|
||||
|
||||
export type MessageType = {
|
||||
export type MessageSearchResultType = {
|
||||
id: string;
|
||||
conversationId: string;
|
||||
receivedAt: number;
|
||||
|
@ -52,11 +52,29 @@ export type ConversationType = {
|
|||
export type ConversationLookupType = {
|
||||
[key: string]: ConversationType;
|
||||
};
|
||||
export type MessageType = {
|
||||
id: string;
|
||||
};
|
||||
export type MessageLookupType = {
|
||||
[key: string]: MessageType;
|
||||
};
|
||||
export type ConversationMessageType = {
|
||||
// And perhaps this could be part of our ConversationType? What if we moved all the selectors as part of this set of changes?
|
||||
// We have the infrastructure for it now...
|
||||
messages: Array<string>;
|
||||
};
|
||||
export type MessagesByConversationType = {
|
||||
[key: string]: ConversationMessageType;
|
||||
};
|
||||
|
||||
export type ConversationsStateType = {
|
||||
conversationLookup: ConversationLookupType;
|
||||
selectedConversation?: string;
|
||||
showArchived: boolean;
|
||||
|
||||
// Note: it's very important that both of these locations are always kept up to date
|
||||
messagesLookup: MessageLookupType;
|
||||
messagesByConversation: MessagesByConversationType;
|
||||
};
|
||||
|
||||
// Actions
|
||||
|
@ -232,6 +250,8 @@ function getEmptyState(): ConversationsStateType {
|
|||
return {
|
||||
conversationLookup: {},
|
||||
showArchived: false,
|
||||
messagesLookup: {},
|
||||
messagesByConversation: {},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import { makeLookup } from '../../util/makeLookup';
|
|||
import {
|
||||
ConversationType,
|
||||
MessageExpiredActionType,
|
||||
MessageType,
|
||||
MessageSearchResultType,
|
||||
RemoveAllConversationsActionType,
|
||||
SelectedConversationChangedActionType,
|
||||
} from './conversations';
|
||||
|
@ -23,10 +23,10 @@ export type SearchStateType = {
|
|||
query: string;
|
||||
normalizedPhoneNumber?: string;
|
||||
// We need to store messages here, because they aren't anywhere else in state
|
||||
messages: Array<MessageType>;
|
||||
messages: Array<MessageSearchResultType>;
|
||||
selectedMessage?: string;
|
||||
messageLookup: {
|
||||
[key: string]: MessageType;
|
||||
[key: string]: MessageSearchResultType;
|
||||
};
|
||||
// For conversations we store just the id, and pull conversation props in the selector
|
||||
conversations: Array<string>;
|
||||
|
@ -38,7 +38,7 @@ export type SearchStateType = {
|
|||
type SearchResultsPayloadType = {
|
||||
query: string;
|
||||
normalizedPhoneNumber?: string;
|
||||
messages: Array<MessageType>;
|
||||
messages: Array<MessageSearchResultType>;
|
||||
conversations: Array<string>;
|
||||
contacts: Array<string>;
|
||||
};
|
||||
|
@ -146,7 +146,7 @@ function startNewConversation(
|
|||
|
||||
// Helper functions for search
|
||||
|
||||
// const getMessageProps = (messages: Array<MessageType>) => {
|
||||
// const getMessageProps = (messages: Array<MessageSearchResultType>) => {
|
||||
// if (!messages || !messages.length) {
|
||||
// return [];
|
||||
// }
|
||||
|
|
16
ts/state/roots/createTimeline.tsx
Normal file
16
ts/state/roots/createTimeline.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { Store } from 'redux';
|
||||
|
||||
import { SmartTimeline } from '../smart/Timeline';
|
||||
|
||||
// Workaround: A react component's required properties are filtering up through connect()
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||
const FilteredTimeline = SmartTimeline as any;
|
||||
|
||||
export const createTimeline = (store: Store) => (
|
||||
<Provider store={store}>
|
||||
<FilteredTimeline />
|
||||
</Provider>
|
||||
);
|
|
@ -1,3 +1,4 @@
|
|||
import memoizee from 'memoizee';
|
||||
import { createSelector } from 'reselect';
|
||||
import { format } from '../../types/PhoneNumber';
|
||||
|
||||
|
@ -7,6 +8,9 @@ import {
|
|||
ConversationLookupType,
|
||||
ConversationsStateType,
|
||||
ConversationType,
|
||||
MessageLookupType,
|
||||
MessagesByConversationType,
|
||||
MessageType,
|
||||
} from '../ducks/conversations';
|
||||
|
||||
import { getIntl, getRegionCode, getUserNumber } from './user';
|
||||
|
@ -35,6 +39,19 @@ export const getShowArchived = createSelector(
|
|||
}
|
||||
);
|
||||
|
||||
export const getMessages = createSelector(
|
||||
getConversations,
|
||||
(state: ConversationsStateType): MessageLookupType => {
|
||||
return state.messagesLookup;
|
||||
}
|
||||
);
|
||||
export const getMessagesByConversation = createSelector(
|
||||
getConversations,
|
||||
(state: ConversationsStateType): MessagesByConversationType => {
|
||||
return state.messagesByConversation;
|
||||
}
|
||||
);
|
||||
|
||||
function getConversationTitle(
|
||||
conversation: ConversationType,
|
||||
options: { i18n: LocalizerType; ourRegionCode: string }
|
||||
|
@ -140,3 +157,94 @@ export const getMe = createSelector(
|
|||
return lookup[ourNumber];
|
||||
}
|
||||
);
|
||||
|
||||
// This is where we will put Conversation selector logic, replicating what
|
||||
// is currently in models/conversation.getProps()
|
||||
// Blockers:
|
||||
// 1) contactTypingTimers - that UI-only state needs to be moved to redux
|
||||
export function _conversationSelector(
|
||||
conversation: ConversationType
|
||||
// regionCode: string,
|
||||
// userNumber: string
|
||||
): ConversationType {
|
||||
return conversation;
|
||||
}
|
||||
|
||||
// A little optimization to reset our selector cache when high-level application data
|
||||
// changes: regionCode and userNumber.
|
||||
type CachedConversationSelectorType = (
|
||||
conversation: ConversationType
|
||||
) => ConversationType;
|
||||
export const getCachedSelectorForConversation = createSelector(
|
||||
getRegionCode,
|
||||
getUserNumber,
|
||||
(): CachedConversationSelectorType => {
|
||||
return memoizee(_conversationSelector, { max: 100 });
|
||||
}
|
||||
);
|
||||
|
||||
type GetConversationByIdType = (id: string) => ConversationType | undefined;
|
||||
export const getConversationSelector = createSelector(
|
||||
getCachedSelectorForConversation,
|
||||
getConversationLookup,
|
||||
(
|
||||
selector: CachedConversationSelectorType,
|
||||
lookup: ConversationLookupType
|
||||
): GetConversationByIdType => {
|
||||
return (id: string) => {
|
||||
const conversation = lookup[id];
|
||||
if (!conversation) {
|
||||
return;
|
||||
}
|
||||
|
||||
return selector(conversation);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// For now we pass through, as selector logic is still happening in the Backbone Model.
|
||||
// Blockers:
|
||||
// 1) it's a lot of code to pull over - ~500 lines
|
||||
// 2) a couple places still rely on all that code - will need to move these to Roots:
|
||||
// - quote compose
|
||||
// - message details
|
||||
export function _messageSelector(
|
||||
message: MessageType
|
||||
// ourNumber: string,
|
||||
// regionCode: string,
|
||||
// conversation?: ConversationType,
|
||||
// sender?: ConversationType,
|
||||
// quoted?: ConversationType
|
||||
): MessageType {
|
||||
return message;
|
||||
}
|
||||
|
||||
// A little optimization to reset our selector cache whenever high-level application data
|
||||
// changes: regionCode and userNumber.
|
||||
type CachedMessageSelectorType = (message: MessageType) => MessageType;
|
||||
export const getCachedSelectorForMessage = createSelector(
|
||||
getRegionCode,
|
||||
getUserNumber,
|
||||
(): CachedMessageSelectorType => {
|
||||
return memoizee(_messageSelector, { max: 500 });
|
||||
}
|
||||
);
|
||||
|
||||
type GetMessageByIdType = (id: string) => MessageType | undefined;
|
||||
export const getMessageSelector = createSelector(
|
||||
getCachedSelectorForMessage,
|
||||
getMessages,
|
||||
(
|
||||
selector: CachedMessageSelectorType,
|
||||
lookup: MessageLookupType
|
||||
): GetMessageByIdType => {
|
||||
return (id: string) => {
|
||||
const message = lookup[id];
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
return selector(message);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
39
ts/state/smart/Timeline.tsx
Normal file
39
ts/state/smart/Timeline.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import { Timeline } from '../../components/conversation/Timeline';
|
||||
import { StateType } from '../reducer';
|
||||
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { getConversationSelector } from '../selectors/conversations';
|
||||
|
||||
import { SmartTimelineItem } from './TimelineItem';
|
||||
|
||||
// Workaround: A react component's required properties are filtering up through connect()
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||
const FilteredSmartTimelineItem = SmartTimelineItem as any;
|
||||
|
||||
type ExternalProps = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||
const { id } = props;
|
||||
|
||||
const conversationSelector = getConversationSelector(state);
|
||||
const conversation = conversationSelector(id);
|
||||
const items: Array<string> = [];
|
||||
|
||||
return {
|
||||
...conversation,
|
||||
items,
|
||||
i18n: getIntl(state),
|
||||
renderTimelineItem: (messageId: string) => {
|
||||
return <FilteredSmartTimelineItem id={messageId} />;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export const SmartTimeline = smart(Timeline);
|
26
ts/state/smart/TimelineItem.tsx
Normal file
26
ts/state/smart/TimelineItem.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import { TimelineItem } from '../../components/conversation/TimelineItem';
|
||||
import { StateType } from '../reducer';
|
||||
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { getMessageSelector } from '../selectors/conversations';
|
||||
|
||||
type ExternalProps = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||
const { id } = props;
|
||||
|
||||
const messageSelector = getMessageSelector(state);
|
||||
|
||||
return {
|
||||
...messageSelector(id),
|
||||
i18n: getIntl(state),
|
||||
};
|
||||
};
|
||||
|
||||
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export const SmartTimelineItem = smart(TimelineItem);
|
|
@ -896,7 +896,7 @@
|
|||
"rule": "jQuery-append(",
|
||||
"path": "js/views/message_view.js",
|
||||
"line": " this.$el.append(this.childView.el);",
|
||||
"lineNumber": 122,
|
||||
"lineNumber": 123,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
|
@ -6250,5 +6250,14 @@
|
|||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-03-09T00:08:44.242Z",
|
||||
"reasonDetail": "Used only to trigger menu display"
|
||||
},
|
||||
{
|
||||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/Timeline.js",
|
||||
"line": " this.listRef = react_1.default.createRef();",
|
||||
"lineNumber": 17,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-04-17T18:44:33.207Z",
|
||||
"reasonDetail": "Necessary to interact with child react-virtualized/List"
|
||||
}
|
||||
]
|
92
yarn.lock
92
yarn.lock
|
@ -160,6 +160,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/long/-/long-3.0.32.tgz#f4e5af31e9e9b196d8e5fca8a5e2e20aa3d60b69"
|
||||
integrity sha512-ZXyOOm83p7X8p3s0IYM3VeueNmHpkk/yMlP8CLeOnEcu6hIwPH7YjZBvhQkR0ZFS2DqZAxKtJ/M5fcuv3OU5BA==
|
||||
|
||||
"@types/memoizee@0.4.2":
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/memoizee/-/memoizee-0.4.2.tgz#a500158999a8144a9b46cf9a9fb49b15f1853573"
|
||||
integrity sha512-bhdZXZWKfpkQuuiQjVjnPiNeBHpIAC6rfOFqlJXKD3VC35mCcolfVfXYTnk9Ppee5Mkmmz3Llgec7xCdJAbzWw==
|
||||
|
||||
"@types/minimatch@*":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
|
||||
|
@ -2202,6 +2207,13 @@ cyclist@~0.2.2:
|
|||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
|
||||
|
||||
d@1:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
|
||||
integrity sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=
|
||||
dependencies:
|
||||
es5-ext "^0.10.9"
|
||||
|
||||
dashdash@1.14.1, dashdash@^1.12.0:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
|
||||
|
@ -2835,6 +2847,24 @@ es-to-primitive@^1.1.1:
|
|||
is-date-object "^1.0.1"
|
||||
is-symbol "^1.0.1"
|
||||
|
||||
es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.45, es5-ext@^0.10.9, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46:
|
||||
version "0.10.49"
|
||||
resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.49.tgz#059a239de862c94494fec28f8150c977028c6c5e"
|
||||
integrity sha512-3NMEhi57E31qdzmYp2jwRArIUsj1HI/RxbQ4bgnSB+AIKIxsAmTiK83bYMifIcpWvEc3P1X30DhUKOqEtF/kvg==
|
||||
dependencies:
|
||||
es6-iterator "~2.0.3"
|
||||
es6-symbol "~3.1.1"
|
||||
next-tick "^1.0.0"
|
||||
|
||||
es6-iterator@^2.0.1, es6-iterator@~2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
|
||||
integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c=
|
||||
dependencies:
|
||||
d "1"
|
||||
es5-ext "^0.10.35"
|
||||
es6-symbol "^3.1.1"
|
||||
|
||||
es6-object-assign@~1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c"
|
||||
|
@ -2857,6 +2887,24 @@ es6-promisify@^5.0.0:
|
|||
dependencies:
|
||||
es6-promise "^4.0.3"
|
||||
|
||||
es6-symbol@^3.1.1, es6-symbol@~3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77"
|
||||
integrity sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=
|
||||
dependencies:
|
||||
d "1"
|
||||
es5-ext "~0.10.14"
|
||||
|
||||
es6-weak-map@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f"
|
||||
integrity sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=
|
||||
dependencies:
|
||||
d "1"
|
||||
es5-ext "^0.10.14"
|
||||
es6-iterator "^2.0.1"
|
||||
es6-symbol "^3.1.1"
|
||||
|
||||
escape-html@~1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
||||
|
@ -3045,6 +3093,14 @@ etag@~1.8.1:
|
|||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||
|
||||
event-emitter@^0.3.5:
|
||||
version "0.3.5"
|
||||
resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
|
||||
integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=
|
||||
dependencies:
|
||||
d "1"
|
||||
es5-ext "~0.10.14"
|
||||
|
||||
eventemitter2@~0.4.13:
|
||||
version "0.4.14"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-0.4.14.tgz#8f61b75cde012b2e9eb284d4545583b5643b61ab"
|
||||
|
@ -4838,7 +4894,7 @@ is-primitive@^2.0.0:
|
|||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
|
||||
|
||||
is-promise@^2.1.0:
|
||||
is-promise@^2.1, is-promise@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
|
||||
|
||||
|
@ -5553,6 +5609,13 @@ lru-cache@^4.1.2:
|
|||
pseudomap "^1.0.2"
|
||||
yallist "^3.0.2"
|
||||
|
||||
lru-queue@0.1:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3"
|
||||
integrity sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=
|
||||
dependencies:
|
||||
es5-ext "~0.10.2"
|
||||
|
||||
macaddress@^0.2.8:
|
||||
version "0.2.8"
|
||||
resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12"
|
||||
|
@ -5653,6 +5716,20 @@ mem@^4.0.0:
|
|||
mimic-fn "^2.0.0"
|
||||
p-is-promise "^2.0.0"
|
||||
|
||||
memoizee@0.4.14:
|
||||
version "0.4.14"
|
||||
resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.14.tgz#07a00f204699f9a95c2d9e77218271c7cd610d57"
|
||||
integrity sha512-/SWFvWegAIYAO4NQMpcX+gcra0yEZu4OntmUdrBaWrJncxOqAziGFlHxc7yjKVK2uu3lpPW27P27wkR82wA8mg==
|
||||
dependencies:
|
||||
d "1"
|
||||
es5-ext "^0.10.45"
|
||||
es6-weak-map "^2.0.2"
|
||||
event-emitter "^0.3.5"
|
||||
is-promise "^2.1"
|
||||
lru-queue "0.1"
|
||||
next-tick "1"
|
||||
timers-ext "^0.1.5"
|
||||
|
||||
memory-fs@^0.4.0, memory-fs@~0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
|
||||
|
@ -6076,6 +6153,11 @@ netmask@^1.0.6:
|
|||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/netmask/-/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35"
|
||||
|
||||
next-tick@1, next-tick@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
|
||||
integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
|
||||
|
||||
nice-try@^1.0.4:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
|
||||
|
@ -9260,6 +9342,14 @@ timers-browserify@^2.0.4:
|
|||
dependencies:
|
||||
setimmediate "^1.0.4"
|
||||
|
||||
timers-ext@^0.1.5:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.7.tgz#6f57ad8578e07a3fb9f91d9387d65647555e25c6"
|
||||
integrity sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==
|
||||
dependencies:
|
||||
es5-ext "~0.10.46"
|
||||
next-tick "1"
|
||||
|
||||
tiny-lr@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/tiny-lr/-/tiny-lr-0.2.1.tgz#b3fdba802e5d56a33c2f6f10794b32e477ac729d"
|
||||
|
|
Loading…
Reference in a new issue