Virtualize Messages List - only render what's visible
This commit is contained in:
parent
a976cfe6b6
commit
5ebd8bc690
73 changed files with 4717 additions and 2745 deletions
|
@ -309,11 +309,6 @@
|
||||||
"description":
|
"description":
|
||||||
"Alt text for button to take user down to bottom of conversation, shown when user scrolls up"
|
"Alt text for button to take user down to bottom of conversation, shown when user scrolls up"
|
||||||
},
|
},
|
||||||
"messageBelow": {
|
|
||||||
"message": "New message below",
|
|
||||||
"description":
|
|
||||||
"Alt text for button to take user down to bottom of conversation with a new message out of screen"
|
|
||||||
},
|
|
||||||
"messagesBelow": {
|
"messagesBelow": {
|
||||||
"message": "New messages below",
|
"message": "New messages below",
|
||||||
"description":
|
"description":
|
||||||
|
|
117
app/sql.js
117
app/sql.js
|
@ -16,6 +16,7 @@ const {
|
||||||
isString,
|
isString,
|
||||||
last,
|
last,
|
||||||
map,
|
map,
|
||||||
|
pick,
|
||||||
} = require('lodash');
|
} = require('lodash');
|
||||||
|
|
||||||
// To get long stack traces
|
// To get long stack traces
|
||||||
|
@ -93,9 +94,11 @@ module.exports = {
|
||||||
getExpiredMessages,
|
getExpiredMessages,
|
||||||
getOutgoingWithoutExpiresAt,
|
getOutgoingWithoutExpiresAt,
|
||||||
getNextExpiringMessage,
|
getNextExpiringMessage,
|
||||||
getMessagesByConversation,
|
|
||||||
getNextTapToViewMessageToAgeOut,
|
getNextTapToViewMessageToAgeOut,
|
||||||
getTapToViewMessagesNeedingErase,
|
getTapToViewMessagesNeedingErase,
|
||||||
|
getOlderMessagesByConversation,
|
||||||
|
getNewerMessagesByConversation,
|
||||||
|
getMessageMetricsForConversation,
|
||||||
|
|
||||||
getUnprocessedCount,
|
getUnprocessedCount,
|
||||||
getAllUnprocessed,
|
getAllUnprocessed,
|
||||||
|
@ -1840,7 +1843,7 @@ async function getUnreadByConversation(conversationId) {
|
||||||
return map(rows, row => jsonToObject(row.json));
|
return map(rows, row => jsonToObject(row.json));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMessagesByConversation(
|
async function getOlderMessagesByConversation(
|
||||||
conversationId,
|
conversationId,
|
||||||
{ limit = 100, receivedAt = Number.MAX_VALUE } = {}
|
{ limit = 100, receivedAt = Number.MAX_VALUE } = {}
|
||||||
) {
|
) {
|
||||||
|
@ -1857,8 +1860,118 @@ async function getMessagesByConversation(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return map(rows.reverse(), row => jsonToObject(row.json));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getNewerMessagesByConversation(
|
||||||
|
conversationId,
|
||||||
|
{ limit = 100, receivedAt = 0 } = {}
|
||||||
|
) {
|
||||||
|
const rows = await db.all(
|
||||||
|
`SELECT json FROM messages WHERE
|
||||||
|
conversationId = $conversationId AND
|
||||||
|
received_at > $received_at
|
||||||
|
ORDER BY received_at ASC
|
||||||
|
LIMIT $limit;`,
|
||||||
|
{
|
||||||
|
$conversationId: conversationId,
|
||||||
|
$received_at: receivedAt,
|
||||||
|
$limit: limit,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return map(rows, row => jsonToObject(row.json));
|
return map(rows, row => jsonToObject(row.json));
|
||||||
}
|
}
|
||||||
|
async function getOldestMessageForConversation(conversationId) {
|
||||||
|
const row = await db.get(
|
||||||
|
`SELECT * FROM messages WHERE
|
||||||
|
conversationId = $conversationId
|
||||||
|
ORDER BY received_at ASC
|
||||||
|
LIMIT 1;`,
|
||||||
|
{
|
||||||
|
$conversationId: conversationId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
async function getNewestMessageForConversation(conversationId) {
|
||||||
|
const row = await db.get(
|
||||||
|
`SELECT * FROM messages WHERE
|
||||||
|
conversationId = $conversationId
|
||||||
|
ORDER BY received_at DESC
|
||||||
|
LIMIT 1;`,
|
||||||
|
{
|
||||||
|
$conversationId: conversationId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
async function getOldestUnreadMessageForConversation(conversationId) {
|
||||||
|
const row = await db.get(
|
||||||
|
`SELECT * FROM messages WHERE
|
||||||
|
conversationId = $conversationId AND
|
||||||
|
unread = 1
|
||||||
|
ORDER BY received_at ASC
|
||||||
|
LIMIT 1;`,
|
||||||
|
{
|
||||||
|
$conversationId: conversationId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTotalUnreadForConversation(conversationId) {
|
||||||
|
const row = await db.get(
|
||||||
|
`SELECT count(id) from messages WHERE
|
||||||
|
conversationId = $conversationId AND
|
||||||
|
unread = 1;
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
$conversationId: conversationId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
throw new Error('getTotalUnreadForConversation: Unable to get count');
|
||||||
|
}
|
||||||
|
|
||||||
|
return row['count(id)'];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getMessageMetricsForConversation(conversationId) {
|
||||||
|
const results = await Promise.all([
|
||||||
|
getOldestMessageForConversation(conversationId),
|
||||||
|
getNewestMessageForConversation(conversationId),
|
||||||
|
getOldestUnreadMessageForConversation(conversationId),
|
||||||
|
getTotalUnreadForConversation(conversationId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [oldest, newest, oldestUnread, totalUnread] = results;
|
||||||
|
|
||||||
|
return {
|
||||||
|
oldest: oldest ? pick(oldest, ['received_at', 'id']) : null,
|
||||||
|
newest: newest ? pick(newest, ['received_at', 'id']) : null,
|
||||||
|
oldestUnread: oldestUnread
|
||||||
|
? pick(oldestUnread, ['received_at', 'id'])
|
||||||
|
: null,
|
||||||
|
totalUnread,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function getMessagesBySentAt(sentAt) {
|
async function getMessagesBySentAt(sentAt) {
|
||||||
const rows = await db.all(
|
const rows = await db.all(
|
||||||
|
|
|
@ -71,19 +71,6 @@
|
||||||
<div class='lightbox-container'></div>
|
<div class='lightbox-container'></div>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script type='text/x-tmpl-mustache' id='scroll-down-button-view'>
|
|
||||||
<button class='text module-scroll-down__button {{ buttonClass }}' alt='{{ moreBelow }}'>
|
|
||||||
<div class='module-scroll-down__icon'></div>
|
|
||||||
</button>
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script type='text/x-tmpl-mustache' id='last-seen-indicator-view'>
|
|
||||||
<div class='module-last-seen-indicator__bar'/>
|
|
||||||
<div class='module-last-seen-indicator__text'>
|
|
||||||
{{ unreadMessages }}
|
|
||||||
</div>
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script type='text/x-tmpl-mustache' id='expired_alert'>
|
<script type='text/x-tmpl-mustache' id='expired_alert'>
|
||||||
<a target='_blank' href='https://signal.org/download/'>
|
<a target='_blank' href='https://signal.org/download/'>
|
||||||
<button class='upgrade'>{{ upgrade }}</button>
|
<button class='upgrade'>{{ upgrade }}</button>
|
||||||
|
@ -106,12 +93,7 @@
|
||||||
<script type='text/x-tmpl-mustache' id='conversation'>
|
<script type='text/x-tmpl-mustache' id='conversation'>
|
||||||
<div class='conversation-header'></div>
|
<div class='conversation-header'></div>
|
||||||
<div class='main panel'>
|
<div class='main panel'>
|
||||||
<div class='discussion-container'>
|
<div class='timeline-placeholder'></div>
|
||||||
<div class='bar-container hide'>
|
|
||||||
<div class='bar active progress-bar-striped progress-bar'></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='bottom-bar' id='footer'>
|
<div class='bottom-bar' id='footer'>
|
||||||
<div class='compose'>
|
<div class='compose'>
|
||||||
<form class='send clearfix file-input'>
|
<form class='send clearfix file-input'>
|
||||||
|
@ -488,15 +470,11 @@
|
||||||
|
|
||||||
<script type='text/javascript' src='js/views/react_wrapper_view.js'></script>
|
<script type='text/javascript' src='js/views/react_wrapper_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/whisper_view.js'></script>
|
<script type='text/javascript' src='js/views/whisper_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/last_seen_indicator_view.js'></script>
|
|
||||||
<script type='text/javascript' src='js/views/scroll_down_button_view.js'></script>
|
|
||||||
<script type='text/javascript' src='js/views/toast_view.js'></script>
|
<script type='text/javascript' src='js/views/toast_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/file_input_view.js'></script>
|
<script type='text/javascript' src='js/views/file_input_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/list_view.js'></script>
|
<script type='text/javascript' src='js/views/list_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/contact_list_view.js'></script>
|
<script type='text/javascript' src='js/views/contact_list_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/message_view.js'></script>
|
|
||||||
<script type='text/javascript' src='js/views/key_verification_view.js'></script>
|
<script type='text/javascript' src='js/views/key_verification_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/message_list_view.js'></script>
|
|
||||||
<script type='text/javascript' src='js/views/group_member_list_view.js'></script>
|
<script type='text/javascript' src='js/views/group_member_list_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/recorder_view.js'></script>
|
<script type='text/javascript' src='js/views/recorder_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/conversation_view.js'></script>
|
<script type='text/javascript' src='js/views/conversation_view.js'></script>
|
||||||
|
|
|
@ -476,6 +476,12 @@
|
||||||
const initialState = {
|
const initialState = {
|
||||||
conversations: {
|
conversations: {
|
||||||
conversationLookup: Signal.Util.makeLookup(conversations, 'id'),
|
conversationLookup: Signal.Util.makeLookup(conversations, 'id'),
|
||||||
|
messagesByConversation: {},
|
||||||
|
messagesLookup: {},
|
||||||
|
selectedConversation: null,
|
||||||
|
selectedMessage: null,
|
||||||
|
selectedMessageCounter: 0,
|
||||||
|
showArchived: false,
|
||||||
},
|
},
|
||||||
emojis: Signal.Emojis.getInitialState(),
|
emojis: Signal.Emojis.getInitialState(),
|
||||||
items: storage.getItemsState(),
|
items: storage.getItemsState(),
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
'add remove change:unreadCount',
|
'add remove change:unreadCount',
|
||||||
_.debounce(this.updateUnreadCount.bind(this), 1000)
|
_.debounce(this.updateUnreadCount.bind(this), 1000)
|
||||||
);
|
);
|
||||||
this.startPruning();
|
|
||||||
},
|
},
|
||||||
addActive(model) {
|
addActive(model) {
|
||||||
if (model.get('active_at')) {
|
if (model.get('active_at')) {
|
||||||
|
@ -44,14 +43,6 @@
|
||||||
}
|
}
|
||||||
window.updateTrayIcon(newUnreadCount);
|
window.updateTrayIcon(newUnreadCount);
|
||||||
},
|
},
|
||||||
startPruning() {
|
|
||||||
const halfHour = 30 * 60 * 1000;
|
|
||||||
this.interval = setInterval(() => {
|
|
||||||
this.forEach(conversation => {
|
|
||||||
conversation.trigger('prune');
|
|
||||||
});
|
|
||||||
}, halfHour);
|
|
||||||
},
|
|
||||||
}))();
|
}))();
|
||||||
|
|
||||||
window.getInboxCollection = () => inboxCollection;
|
window.getInboxCollection = () => inboxCollection;
|
||||||
|
|
|
@ -27,13 +27,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const { Util } = window.Signal;
|
const { Util } = window.Signal;
|
||||||
const {
|
const { Conversation, Contact, Message, PhoneNumber } = window.Signal.Types;
|
||||||
Conversation,
|
|
||||||
Contact,
|
|
||||||
Errors,
|
|
||||||
Message,
|
|
||||||
PhoneNumber,
|
|
||||||
} = window.Signal.Types;
|
|
||||||
const {
|
const {
|
||||||
deleteAttachmentData,
|
deleteAttachmentData,
|
||||||
getAbsoluteAttachmentPath,
|
getAbsoluteAttachmentPath,
|
||||||
|
@ -277,6 +271,7 @@
|
||||||
|
|
||||||
this.messageCollection.remove(id);
|
this.messageCollection.remove(id);
|
||||||
existing.trigger('expired');
|
existing.trigger('expired');
|
||||||
|
existing.cleanup();
|
||||||
};
|
};
|
||||||
|
|
||||||
// If a fetch is in progress, then we need to wait until that's complete to
|
// If a fetch is in progress, then we need to wait until that's complete to
|
||||||
|
@ -288,18 +283,33 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
async onNewMessage(message) {
|
async onNewMessage(message) {
|
||||||
await this.updateLastMessage();
|
|
||||||
|
|
||||||
// Clear typing indicator for a given contact if we receive a message from them
|
// Clear typing indicator for a given contact if we receive a message from them
|
||||||
const identifier = message.get
|
const identifier = message.get
|
||||||
? `${message.get('source')}.${message.get('sourceDevice')}`
|
? `${message.get('source')}.${message.get('sourceDevice')}`
|
||||||
: `${message.source}.${message.sourceDevice}`;
|
: `${message.source}.${message.sourceDevice}`;
|
||||||
this.clearContactTypingTimer(identifier);
|
this.clearContactTypingTimer(identifier);
|
||||||
|
|
||||||
|
await this.updateLastMessage();
|
||||||
},
|
},
|
||||||
|
|
||||||
addSingleMessage(message) {
|
addSingleMessage(message) {
|
||||||
|
const { id } = message;
|
||||||
|
const existing = this.messageCollection.get(id);
|
||||||
|
|
||||||
const model = this.messageCollection.add(message, { merge: true });
|
const model = this.messageCollection.add(message, { merge: true });
|
||||||
model.setToExpire();
|
model.setToExpire();
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
const { messagesAdded } = window.reduxActions.conversations;
|
||||||
|
const isNewMessage = true;
|
||||||
|
messagesAdded(
|
||||||
|
this.id,
|
||||||
|
[model.getReduxData()],
|
||||||
|
isNewMessage,
|
||||||
|
document.hasFocus()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return model;
|
return model;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -310,7 +320,12 @@
|
||||||
const { format } = PhoneNumber;
|
const { format } = PhoneNumber;
|
||||||
const regionCode = storage.get('regionCode');
|
const regionCode = storage.get('regionCode');
|
||||||
const color = this.getColor();
|
const color = this.getColor();
|
||||||
const typingKeys = Object.keys(this.contactTypingTimers || {});
|
|
||||||
|
const typingValues = _.values(this.contactTypingTimers || {});
|
||||||
|
const typingMostRecent = _.first(_.sortBy(typingValues, 'timestamp'));
|
||||||
|
const typingContact = typingMostRecent
|
||||||
|
? ConversationController.getOrCreate(typingMostRecent.sender, 'private')
|
||||||
|
: null;
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
@ -321,7 +336,7 @@
|
||||||
color,
|
color,
|
||||||
type: this.isPrivate() ? 'direct' : 'group',
|
type: this.isPrivate() ? 'direct' : 'group',
|
||||||
isMe: this.isMe(),
|
isMe: this.isMe(),
|
||||||
isTyping: typingKeys.length > 0,
|
typingContact: typingContact ? typingContact.format() : null,
|
||||||
lastUpdated: this.get('timestamp'),
|
lastUpdated: this.get('timestamp'),
|
||||||
name: this.getName(),
|
name: this.getName(),
|
||||||
profileName: this.getProfileName(),
|
profileName: this.getProfileName(),
|
||||||
|
@ -894,6 +909,9 @@
|
||||||
sendMessage(body, attachments, quote, preview, sticker) {
|
sendMessage(body, attachments, quote, preview, sticker) {
|
||||||
this.clearTypingTimers();
|
this.clearTypingTimers();
|
||||||
|
|
||||||
|
const { clearUnreadMetrics } = window.reduxActions.conversations;
|
||||||
|
clearUnreadMetrics(this.id);
|
||||||
|
|
||||||
const destination = this.id;
|
const destination = this.id;
|
||||||
const expireTimer = this.get('expireTimer');
|
const expireTimer = this.get('expireTimer');
|
||||||
const recipients = this.getRecipients();
|
const recipients = this.getRecipients();
|
||||||
|
@ -1202,7 +1220,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = await window.Signal.Data.getMessagesByConversation(
|
const messages = await window.Signal.Data.getOlderMessagesByConversation(
|
||||||
this.id,
|
this.id,
|
||||||
{ limit: 1, MessageCollection: Whisper.MessageCollection }
|
{ limit: 1, MessageCollection: Whisper.MessageCollection }
|
||||||
);
|
);
|
||||||
|
@ -1310,7 +1328,7 @@
|
||||||
model.set({ id });
|
model.set({ id });
|
||||||
|
|
||||||
const message = MessageController.register(id, model);
|
const message = MessageController.register(id, model);
|
||||||
this.messageCollection.add(message);
|
this.addSingleMessage(message);
|
||||||
|
|
||||||
// if change was made remotely, don't send it to the number/group
|
// if change was made remotely, don't send it to the number/group
|
||||||
if (receivedAt) {
|
if (receivedAt) {
|
||||||
|
@ -1373,7 +1391,7 @@
|
||||||
async endSession() {
|
async endSession() {
|
||||||
if (this.isPrivate()) {
|
if (this.isPrivate()) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const message = this.messageCollection.add({
|
const model = new Whisper.Message({
|
||||||
conversationId: this.id,
|
conversationId: this.id,
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
sent_at: now,
|
sent_at: now,
|
||||||
|
@ -1383,10 +1401,13 @@
|
||||||
flags: textsecure.protobuf.DataMessage.Flags.END_SESSION,
|
flags: textsecure.protobuf.DataMessage.Flags.END_SESSION,
|
||||||
});
|
});
|
||||||
|
|
||||||
const id = await window.Signal.Data.saveMessage(message.attributes, {
|
const id = await window.Signal.Data.saveMessage(model.attributes, {
|
||||||
Message: Whisper.Message,
|
Message: Whisper.Message,
|
||||||
});
|
});
|
||||||
message.set({ id });
|
model.set({ id });
|
||||||
|
|
||||||
|
const message = MessageController.register(model.id, model);
|
||||||
|
this.addSingleMessage(message);
|
||||||
|
|
||||||
const options = this.getSendOptions();
|
const options = this.getSendOptions();
|
||||||
message.send(
|
message.send(
|
||||||
|
@ -1407,7 +1428,7 @@
|
||||||
groupUpdate = this.pick(['name', 'avatar', 'members']);
|
groupUpdate = this.pick(['name', 'avatar', 'members']);
|
||||||
}
|
}
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const message = this.messageCollection.add({
|
const model = new Whisper.Message({
|
||||||
conversationId: this.id,
|
conversationId: this.id,
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
sent_at: now,
|
sent_at: now,
|
||||||
|
@ -1415,10 +1436,14 @@
|
||||||
group_update: groupUpdate,
|
group_update: groupUpdate,
|
||||||
});
|
});
|
||||||
|
|
||||||
const id = await window.Signal.Data.saveMessage(message.attributes, {
|
const id = await window.Signal.Data.saveMessage(model.attributes, {
|
||||||
Message: Whisper.Message,
|
Message: Whisper.Message,
|
||||||
});
|
});
|
||||||
message.set({ id });
|
|
||||||
|
model.set({ id });
|
||||||
|
|
||||||
|
const message = MessageController.register(model.id, model);
|
||||||
|
this.addSingleMessage(message);
|
||||||
|
|
||||||
const options = this.getSendOptions();
|
const options = this.getSendOptions();
|
||||||
message.send(
|
message.send(
|
||||||
|
@ -1443,7 +1468,7 @@
|
||||||
Conversation: Whisper.Conversation,
|
Conversation: Whisper.Conversation,
|
||||||
});
|
});
|
||||||
|
|
||||||
const message = this.messageCollection.add({
|
const model = new Whisper.Message({
|
||||||
group_update: { left: 'You' },
|
group_update: { left: 'You' },
|
||||||
conversationId: this.id,
|
conversationId: this.id,
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
|
@ -1451,10 +1476,13 @@
|
||||||
received_at: now,
|
received_at: now,
|
||||||
});
|
});
|
||||||
|
|
||||||
const id = await window.Signal.Data.saveMessage(message.attributes, {
|
const id = await window.Signal.Data.saveMessage(model.attributes, {
|
||||||
Message: Whisper.Message,
|
Message: Whisper.Message,
|
||||||
});
|
});
|
||||||
message.set({ id });
|
model.set({ id });
|
||||||
|
|
||||||
|
const message = MessageController.register(model.id, model);
|
||||||
|
this.addSingleMessage(message);
|
||||||
|
|
||||||
const options = this.getSendOptions();
|
const options = this.getSendOptions();
|
||||||
message.send(
|
message.send(
|
||||||
|
@ -1830,57 +1858,6 @@
|
||||||
this.set({ accessKey });
|
this.set({ accessKey });
|
||||||
},
|
},
|
||||||
|
|
||||||
async upgradeMessages(messages) {
|
|
||||||
for (let max = messages.length, i = 0; i < max; i += 1) {
|
|
||||||
const message = messages.at(i);
|
|
||||||
const { attributes } = message;
|
|
||||||
const { schemaVersion } = attributes;
|
|
||||||
|
|
||||||
if (schemaVersion < Message.VERSION_NEEDED_FOR_DISPLAY) {
|
|
||||||
// Yep, we really do want to wait for each of these
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
const upgradedMessage = await upgradeMessageSchema(attributes);
|
|
||||||
message.set(upgradedMessage);
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await window.Signal.Data.saveMessage(upgradedMessage, {
|
|
||||||
Message: Whisper.Message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchMessages() {
|
|
||||||
if (!this.id) {
|
|
||||||
throw new Error('This conversation has no id!');
|
|
||||||
}
|
|
||||||
if (this.inProgressFetch) {
|
|
||||||
window.log.warn('Attempting to start a parallel fetchMessages() call');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.inProgressFetch = this.messageCollection.fetchConversation(
|
|
||||||
this.id,
|
|
||||||
undefined,
|
|
||||||
this.get('unreadCount')
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.inProgressFetch;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// We are now doing the work to upgrade messages before considering the load from
|
|
||||||
// the database complete. Note that we do save messages back, so it is a
|
|
||||||
// one-time hit. We do this so we have guarantees about message structure.
|
|
||||||
await this.upgradeMessages(this.messageCollection);
|
|
||||||
} catch (error) {
|
|
||||||
window.log.error(
|
|
||||||
'fetchMessages: failed to upgrade messages',
|
|
||||||
Errors.toLogFormat(error)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.inProgressFetch = null;
|
|
||||||
},
|
|
||||||
|
|
||||||
hasMember(number) {
|
hasMember(number) {
|
||||||
return _.contains(this.get('members'), number);
|
return _.contains(this.get('members'), number);
|
||||||
},
|
},
|
||||||
|
@ -1908,10 +1885,6 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
async destroyMessages() {
|
async destroyMessages() {
|
||||||
await window.Signal.Data.removeAllMessagesInConversation(this.id, {
|
|
||||||
MessageCollection: Whisper.MessageCollection,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.messageCollection.reset([]);
|
this.messageCollection.reset([]);
|
||||||
|
|
||||||
this.set({
|
this.set({
|
||||||
|
@ -1922,6 +1895,10 @@
|
||||||
await window.Signal.Data.updateConversation(this.id, this.attributes, {
|
await window.Signal.Data.updateConversation(this.id, this.attributes, {
|
||||||
Conversation: Whisper.Conversation,
|
Conversation: Whisper.Conversation,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await window.Signal.Data.removeAllMessagesInConversation(this.id, {
|
||||||
|
MessageCollection: Whisper.MessageCollection,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
|
@ -2102,10 +2079,6 @@
|
||||||
clearTimeout(record.timer);
|
clearTimeout(record.timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: We trigger two events because:
|
|
||||||
// 'typing-update' is a surgical update ConversationView does for in-convo bubble
|
|
||||||
// 'change' causes a re-render of this conversation's list item in the left pane
|
|
||||||
|
|
||||||
if (isTyping) {
|
if (isTyping) {
|
||||||
this.contactTypingTimers[identifier] = this.contactTypingTimers[
|
this.contactTypingTimers[identifier] = this.contactTypingTimers[
|
||||||
identifier
|
identifier
|
||||||
|
@ -2121,14 +2094,12 @@
|
||||||
);
|
);
|
||||||
if (!record) {
|
if (!record) {
|
||||||
// User was not previously typing before. State change!
|
// User was not previously typing before. State change!
|
||||||
this.trigger('typing-update');
|
|
||||||
this.trigger('change', this);
|
this.trigger('change', this);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
delete this.contactTypingTimers[identifier];
|
delete this.contactTypingTimers[identifier];
|
||||||
if (record) {
|
if (record) {
|
||||||
// User was previously typing, and is no longer. State change!
|
// User was previously typing, and is no longer. State change!
|
||||||
this.trigger('typing-update');
|
|
||||||
this.trigger('change', this);
|
this.trigger('change', this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2143,7 +2114,6 @@
|
||||||
delete this.contactTypingTimers[identifier];
|
delete this.contactTypingTimers[identifier];
|
||||||
|
|
||||||
// User was previously typing, but timed out or we received message. State change!
|
// User was previously typing, but timed out or we received message. State change!
|
||||||
this.trigger('typing-update');
|
|
||||||
this.trigger('change', this);
|
this.trigger('change', this);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -2155,17 +2125,6 @@
|
||||||
comparator(m) {
|
comparator(m) {
|
||||||
return -m.get('timestamp');
|
return -m.get('timestamp');
|
||||||
},
|
},
|
||||||
|
|
||||||
async destroyAll() {
|
|
||||||
await Promise.all(
|
|
||||||
this.models.map(conversation =>
|
|
||||||
window.Signal.Data.removeConversation(conversation.id, {
|
|
||||||
Conversation: Whisper.Conversation,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
this.reset([]);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Whisper.Conversation.COLORS = COLORS.concat(['grey', 'default']).join(' ');
|
Whisper.Conversation.COLORS = COLORS.concat(['grey', 'default']).join(' ');
|
||||||
|
|
|
@ -100,69 +100,67 @@
|
||||||
this.on('expired', this.onExpired);
|
this.on('expired', this.onExpired);
|
||||||
this.setToExpire();
|
this.setToExpire();
|
||||||
|
|
||||||
this.on('change', this.generateProps);
|
this.on('change', this.notifyRedux);
|
||||||
|
},
|
||||||
|
|
||||||
const applicableConversationChanges =
|
notifyRedux() {
|
||||||
'change:color change:name change:number change:profileName change:profileAvatar';
|
const { messageChanged } = window.reduxActions.conversations;
|
||||||
|
|
||||||
const conversation = this.getConversation();
|
if (messageChanged) {
|
||||||
const fromContact = this.getIncomingContact();
|
const conversationId = this.get('conversationId');
|
||||||
|
// Note: The clone is important for triggering a re-run of selectors
|
||||||
this.listenTo(
|
messageChanged(this.id, conversationId, _.clone(this.attributes));
|
||||||
conversation,
|
|
||||||
applicableConversationChanges,
|
|
||||||
this.generateProps
|
|
||||||
);
|
|
||||||
if (fromContact) {
|
|
||||||
this.listenTo(
|
|
||||||
fromContact,
|
|
||||||
applicableConversationChanges,
|
|
||||||
this.generateProps
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
this.generateProps();
|
getReduxData() {
|
||||||
|
const contact = this.getPropsForEmbeddedContact();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...this.attributes,
|
||||||
|
// We need this in the reducer to detect if the message's height has changed
|
||||||
|
hasSignalAccount: contact ? Boolean(contact.signalAccount) : null,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
// Top-level prop generation for the message bubble
|
// Top-level prop generation for the message bubble
|
||||||
generateProps() {
|
getPropsForBubble() {
|
||||||
if (this.isUnsupportedMessage()) {
|
if (this.isUnsupportedMessage()) {
|
||||||
this.props = {
|
return {
|
||||||
type: 'unsupportedMessage',
|
type: 'unsupportedMessage',
|
||||||
data: this.getPropsForUnsupportedMessage(),
|
data: this.getPropsForUnsupportedMessage(),
|
||||||
};
|
};
|
||||||
} else if (this.isExpirationTimerUpdate()) {
|
} else if (this.isExpirationTimerUpdate()) {
|
||||||
this.props = {
|
return {
|
||||||
type: 'timerNotification',
|
type: 'timerNotification',
|
||||||
data: this.getPropsForTimerNotification(),
|
data: this.getPropsForTimerNotification(),
|
||||||
};
|
};
|
||||||
} else if (this.isKeyChange()) {
|
} else if (this.isKeyChange()) {
|
||||||
this.props = {
|
return {
|
||||||
type: 'safetyNumberNotification',
|
type: 'safetyNumberNotification',
|
||||||
data: this.getPropsForSafetyNumberNotification(),
|
data: this.getPropsForSafetyNumberNotification(),
|
||||||
};
|
};
|
||||||
} else if (this.isVerifiedChange()) {
|
} else if (this.isVerifiedChange()) {
|
||||||
this.props = {
|
return {
|
||||||
type: 'verificationNotification',
|
type: 'verificationNotification',
|
||||||
data: this.getPropsForVerificationNotification(),
|
data: this.getPropsForVerificationNotification(),
|
||||||
};
|
};
|
||||||
} else if (this.isGroupUpdate()) {
|
} else if (this.isGroupUpdate()) {
|
||||||
this.props = {
|
return {
|
||||||
type: 'groupNotification',
|
type: 'groupNotification',
|
||||||
data: this.getPropsForGroupNotification(),
|
data: this.getPropsForGroupNotification(),
|
||||||
};
|
};
|
||||||
} else if (this.isEndSession()) {
|
} else if (this.isEndSession()) {
|
||||||
this.props = {
|
return {
|
||||||
type: 'resetSessionNotification',
|
type: 'resetSessionNotification',
|
||||||
data: this.getPropsForResetSessionNotification(),
|
data: this.getPropsForResetSessionNotification(),
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
this.propsForSearchResult = this.getPropsForSearchResult();
|
|
||||||
this.props = {
|
|
||||||
type: 'message',
|
|
||||||
data: this.getPropsForMessage(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
data: this.getPropsForMessage(),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
// Other top-level prop-generation
|
// Other top-level prop-generation
|
||||||
|
@ -269,6 +267,21 @@
|
||||||
disableScroll: true,
|
disableScroll: true,
|
||||||
// To ensure that group avatar doesn't show up
|
// To ensure that group avatar doesn't show up
|
||||||
conversationType: 'direct',
|
conversationType: 'direct',
|
||||||
|
downloadNewVersion: () => {
|
||||||
|
this.trigger('download-new-version');
|
||||||
|
},
|
||||||
|
deleteMessage: messageId => {
|
||||||
|
this.trigger('delete', messageId);
|
||||||
|
},
|
||||||
|
showVisualAttachment: options => {
|
||||||
|
this.trigger('show-visual-attachment', options);
|
||||||
|
},
|
||||||
|
displayTapToViewMessage: messageId => {
|
||||||
|
this.trigger('display-tap-to-view-message', messageId);
|
||||||
|
},
|
||||||
|
openLink: url => {
|
||||||
|
this.trigger('navigate-to', url);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
errors,
|
errors,
|
||||||
contacts: sortedContacts,
|
contacts: sortedContacts,
|
||||||
|
@ -290,7 +303,7 @@
|
||||||
const flag =
|
const flag =
|
||||||
textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
|
textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
|
||||||
// eslint-disable-next-line no-bitwise
|
// eslint-disable-next-line no-bitwise
|
||||||
return !!(this.get('flags') & flag);
|
return Boolean(this.get('flags') & flag);
|
||||||
},
|
},
|
||||||
isKeyChange() {
|
isKeyChange() {
|
||||||
return this.get('type') === 'keychange';
|
return this.get('type') === 'keychange';
|
||||||
|
@ -353,12 +366,10 @@
|
||||||
const conversation = this.getConversation();
|
const conversation = this.getConversation();
|
||||||
const isGroup = conversation && !conversation.isPrivate();
|
const isGroup = conversation && !conversation.isPrivate();
|
||||||
const phoneNumber = this.get('key_changed');
|
const phoneNumber = this.get('key_changed');
|
||||||
const showIdentity = id => this.trigger('show-identity', id);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isGroup,
|
isGroup,
|
||||||
contact: this.findAndFormatContact(phoneNumber),
|
contact: this.findAndFormatContact(phoneNumber),
|
||||||
showIdentity,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getPropsForVerificationNotification() {
|
getPropsForVerificationNotification() {
|
||||||
|
@ -498,28 +509,6 @@
|
||||||
isTapToViewExpired: isTapToView && this.get('isErased'),
|
isTapToViewExpired: isTapToView && this.get('isErased'),
|
||||||
isTapToViewError:
|
isTapToViewError:
|
||||||
isTapToView && this.isIncoming() && this.get('isTapToViewInvalid'),
|
isTapToView && this.isIncoming() && this.get('isTapToViewInvalid'),
|
||||||
|
|
||||||
replyToMessage: id => this.trigger('reply', id),
|
|
||||||
retrySend: id => this.trigger('retry', id),
|
|
||||||
deleteMessage: id => this.trigger('delete', id),
|
|
||||||
showMessageDetail: id => this.trigger('show-message-detail', id),
|
|
||||||
|
|
||||||
openConversation: conversationId =>
|
|
||||||
this.trigger('open-conversation', conversationId),
|
|
||||||
showContactDetail: contactOptions =>
|
|
||||||
this.trigger('show-contact-detail', contactOptions),
|
|
||||||
|
|
||||||
showVisualAttachment: lightboxOptions =>
|
|
||||||
this.trigger('show-lightbox', lightboxOptions),
|
|
||||||
downloadAttachment: downloadOptions =>
|
|
||||||
this.trigger('download', downloadOptions),
|
|
||||||
displayTapToViewMessage: messageId =>
|
|
||||||
this.trigger('display-tap-to-view-message', messageId),
|
|
||||||
|
|
||||||
openLink: url => this.trigger('navigate-to', url),
|
|
||||||
downloadNewVersion: () => this.trigger('download-new-version'),
|
|
||||||
scrollToMessage: scrollOptions =>
|
|
||||||
this.trigger('scroll-to-message', scrollOptions),
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -692,6 +681,7 @@
|
||||||
authorName,
|
authorName,
|
||||||
authorColor,
|
authorColor,
|
||||||
referencedMessageNotFound,
|
referencedMessageNotFound,
|
||||||
|
onClick: () => this.trigger('scroll-to-message'),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getStatus(number) {
|
getStatus(number) {
|
||||||
|
@ -851,6 +841,8 @@
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
},
|
},
|
||||||
async cleanup() {
|
async cleanup() {
|
||||||
|
const { messageDeleted } = window.reduxActions.conversations;
|
||||||
|
messageDeleted(this.id, this.get('conversationId'));
|
||||||
MessageController.unregister(this.id);
|
MessageController.unregister(this.id);
|
||||||
this.unload();
|
this.unload();
|
||||||
await this.deleteData();
|
await this.deleteData();
|
||||||
|
@ -2193,74 +2185,5 @@
|
||||||
|
|
||||||
return (left.get('received_at') || 0) - (right.get('received_at') || 0);
|
return (left.get('received_at') || 0) - (right.get('received_at') || 0);
|
||||||
},
|
},
|
||||||
initialize(models, options) {
|
|
||||||
if (options) {
|
|
||||||
this.conversation = options.conversation;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async destroyAll() {
|
|
||||||
await Promise.all(
|
|
||||||
this.models.map(message =>
|
|
||||||
window.Signal.Data.removeMessage(message.id, {
|
|
||||||
Message: Whisper.Message,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
this.reset([]);
|
|
||||||
},
|
|
||||||
|
|
||||||
getLoadedUnreadCount() {
|
|
||||||
return this.reduce((total, model) => {
|
|
||||||
const unread = model.get('unread') && model.isIncoming();
|
|
||||||
return total + (unread ? 1 : 0);
|
|
||||||
}, 0);
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchConversation(conversationId, limit = 100, unreadCount = 0) {
|
|
||||||
const startingLoadedUnread =
|
|
||||||
unreadCount > 0 ? this.getLoadedUnreadCount() : 0;
|
|
||||||
|
|
||||||
// We look for older messages if we've fetched once already
|
|
||||||
const receivedAt =
|
|
||||||
this.length === 0 ? Number.MAX_VALUE : this.at(0).get('received_at');
|
|
||||||
|
|
||||||
const messages = await window.Signal.Data.getMessagesByConversation(
|
|
||||||
conversationId,
|
|
||||||
{
|
|
||||||
limit,
|
|
||||||
receivedAt,
|
|
||||||
MessageCollection: Whisper.MessageCollection,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const models = messages
|
|
||||||
.filter(message => Boolean(message.id))
|
|
||||||
.map(message => MessageController.register(message.id, message));
|
|
||||||
const eliminated = messages.length - models.length;
|
|
||||||
if (eliminated > 0) {
|
|
||||||
window.log.warn(
|
|
||||||
`fetchConversation: Eliminated ${eliminated} messages without an id`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.add(models);
|
|
||||||
|
|
||||||
if (unreadCount <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const loadedUnread = this.getLoadedUnreadCount();
|
|
||||||
if (loadedUnread >= unreadCount) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (startingLoadedUnread === loadedUnread) {
|
|
||||||
// that fetch didn't get us any more unread. stop fetching more.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.log.info(
|
|
||||||
'fetchConversation: doing another fetch to get all unread'
|
|
||||||
);
|
|
||||||
await this.fetchConversation(conversationId, limit, unreadCount);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -696,7 +696,7 @@ async function exportConversation(conversation, options = {}) {
|
||||||
|
|
||||||
while (!complete) {
|
while (!complete) {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
const collection = await window.Signal.Data.getMessagesByConversation(
|
const collection = await window.Signal.Data.getOlderMessagesByConversation(
|
||||||
conversation.id,
|
conversation.id,
|
||||||
{
|
{
|
||||||
limit: CHUNK_SIZE,
|
limit: CHUNK_SIZE,
|
||||||
|
|
|
@ -121,9 +121,11 @@ module.exports = {
|
||||||
getExpiredMessages,
|
getExpiredMessages,
|
||||||
getOutgoingWithoutExpiresAt,
|
getOutgoingWithoutExpiresAt,
|
||||||
getNextExpiringMessage,
|
getNextExpiringMessage,
|
||||||
getMessagesByConversation,
|
|
||||||
getNextTapToViewMessageToAgeOut,
|
getNextTapToViewMessageToAgeOut,
|
||||||
getTapToViewMessagesNeedingErase,
|
getTapToViewMessagesNeedingErase,
|
||||||
|
getOlderMessagesByConversation,
|
||||||
|
getNewerMessagesByConversation,
|
||||||
|
getMessageMetricsForConversation,
|
||||||
|
|
||||||
getUnprocessedCount,
|
getUnprocessedCount,
|
||||||
getAllUnprocessed,
|
getAllUnprocessed,
|
||||||
|
@ -779,17 +781,40 @@ async function getUnreadByConversation(conversationId, { MessageCollection }) {
|
||||||
return new MessageCollection(messages);
|
return new MessageCollection(messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMessagesByConversation(
|
async function getOlderMessagesByConversation(
|
||||||
conversationId,
|
conversationId,
|
||||||
{ limit = 100, receivedAt = Number.MAX_VALUE, MessageCollection }
|
{ limit = 100, receivedAt = Number.MAX_VALUE, MessageCollection }
|
||||||
) {
|
) {
|
||||||
const messages = await channels.getMessagesByConversation(conversationId, {
|
const messages = await channels.getOlderMessagesByConversation(
|
||||||
limit,
|
conversationId,
|
||||||
receivedAt,
|
{
|
||||||
});
|
limit,
|
||||||
|
receivedAt,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return new MessageCollection(messages);
|
return new MessageCollection(messages);
|
||||||
}
|
}
|
||||||
|
async function getNewerMessagesByConversation(
|
||||||
|
conversationId,
|
||||||
|
{ limit = 100, receivedAt = 0, MessageCollection }
|
||||||
|
) {
|
||||||
|
const messages = await channels.getNewerMessagesByConversation(
|
||||||
|
conversationId,
|
||||||
|
{
|
||||||
|
limit,
|
||||||
|
receivedAt,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return new MessageCollection(messages);
|
||||||
|
}
|
||||||
|
async function getMessageMetricsForConversation(conversationId) {
|
||||||
|
const result = await channels.getMessageMetricsForConversation(
|
||||||
|
conversationId
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
async function removeAllMessagesInConversation(
|
async function removeAllMessagesInConversation(
|
||||||
conversationId,
|
conversationId,
|
||||||
|
@ -800,7 +825,7 @@ async function removeAllMessagesInConversation(
|
||||||
// Yes, we really want the await in the loop. We're deleting 100 at a
|
// Yes, we really want the await in the loop. We're deleting 100 at a
|
||||||
// time so we don't use too much memory.
|
// time so we don't use too much memory.
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
messages = await getMessagesByConversation(conversationId, {
|
messages = await getOlderMessagesByConversation(conversationId, {
|
||||||
limit: 100,
|
limit: 100,
|
||||||
MessageCollection,
|
MessageCollection,
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,51 +28,25 @@ const {
|
||||||
ContactDetail,
|
ContactDetail,
|
||||||
} = require('../../ts/components/conversation/ContactDetail');
|
} = require('../../ts/components/conversation/ContactDetail');
|
||||||
const { ContactListItem } = require('../../ts/components/ContactListItem');
|
const { ContactListItem } = require('../../ts/components/ContactListItem');
|
||||||
const { ContactName } = require('../../ts/components/conversation/ContactName');
|
|
||||||
const {
|
const {
|
||||||
ConversationHeader,
|
ConversationHeader,
|
||||||
} = require('../../ts/components/conversation/ConversationHeader');
|
} = require('../../ts/components/conversation/ConversationHeader');
|
||||||
const {
|
|
||||||
EmbeddedContact,
|
|
||||||
} = require('../../ts/components/conversation/EmbeddedContact');
|
|
||||||
const { Emojify } = require('../../ts/components/conversation/Emojify');
|
const { Emojify } = require('../../ts/components/conversation/Emojify');
|
||||||
const {
|
|
||||||
GroupNotification,
|
|
||||||
} = require('../../ts/components/conversation/GroupNotification');
|
|
||||||
const { Lightbox } = require('../../ts/components/Lightbox');
|
const { Lightbox } = require('../../ts/components/Lightbox');
|
||||||
const { LightboxGallery } = require('../../ts/components/LightboxGallery');
|
const { LightboxGallery } = require('../../ts/components/LightboxGallery');
|
||||||
const {
|
const {
|
||||||
MediaGallery,
|
MediaGallery,
|
||||||
} = require('../../ts/components/conversation/media-gallery/MediaGallery');
|
} = require('../../ts/components/conversation/media-gallery/MediaGallery');
|
||||||
const { Message } = require('../../ts/components/conversation/Message');
|
|
||||||
const { MessageBody } = require('../../ts/components/conversation/MessageBody');
|
|
||||||
const {
|
const {
|
||||||
MessageDetail,
|
MessageDetail,
|
||||||
} = require('../../ts/components/conversation/MessageDetail');
|
} = require('../../ts/components/conversation/MessageDetail');
|
||||||
const { Quote } = require('../../ts/components/conversation/Quote');
|
const { Quote } = require('../../ts/components/conversation/Quote');
|
||||||
const {
|
|
||||||
ResetSessionNotification,
|
|
||||||
} = require('../../ts/components/conversation/ResetSessionNotification');
|
|
||||||
const {
|
|
||||||
SafetyNumberNotification,
|
|
||||||
} = require('../../ts/components/conversation/SafetyNumberNotification');
|
|
||||||
const {
|
const {
|
||||||
StagedLinkPreview,
|
StagedLinkPreview,
|
||||||
} = require('../../ts/components/conversation/StagedLinkPreview');
|
} = require('../../ts/components/conversation/StagedLinkPreview');
|
||||||
const {
|
|
||||||
TimerNotification,
|
|
||||||
} = require('../../ts/components/conversation/TimerNotification');
|
|
||||||
const {
|
|
||||||
TypingBubble,
|
|
||||||
} = require('../../ts/components/conversation/TypingBubble');
|
|
||||||
const {
|
|
||||||
UnsupportedMessage,
|
|
||||||
} = require('../../ts/components/conversation/UnsupportedMessage');
|
|
||||||
const {
|
|
||||||
VerificationNotification,
|
|
||||||
} = require('../../ts/components/conversation/VerificationNotification');
|
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
const { createTimeline } = require('../../ts/state/roots/createTimeline');
|
||||||
const {
|
const {
|
||||||
createCompositionArea,
|
createCompositionArea,
|
||||||
} = require('../../ts/state/roots/createCompositionArea');
|
} = require('../../ts/state/roots/createCompositionArea');
|
||||||
|
@ -264,33 +238,23 @@ exports.setup = (options = {}) => {
|
||||||
CaptionEditor,
|
CaptionEditor,
|
||||||
ContactDetail,
|
ContactDetail,
|
||||||
ContactListItem,
|
ContactListItem,
|
||||||
ContactName,
|
|
||||||
ConversationHeader,
|
ConversationHeader,
|
||||||
EmbeddedContact,
|
|
||||||
Emojify,
|
Emojify,
|
||||||
GroupNotification,
|
|
||||||
Lightbox,
|
Lightbox,
|
||||||
LightboxGallery,
|
LightboxGallery,
|
||||||
MediaGallery,
|
MediaGallery,
|
||||||
Message,
|
|
||||||
MessageBody,
|
|
||||||
MessageDetail,
|
MessageDetail,
|
||||||
Quote,
|
Quote,
|
||||||
ResetSessionNotification,
|
|
||||||
SafetyNumberNotification,
|
|
||||||
StagedLinkPreview,
|
StagedLinkPreview,
|
||||||
TimerNotification,
|
|
||||||
Types: {
|
Types: {
|
||||||
Message: MediaGalleryMessage,
|
Message: MediaGalleryMessage,
|
||||||
},
|
},
|
||||||
TypingBubble,
|
|
||||||
UnsupportedMessage,
|
|
||||||
VerificationNotification,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Roots = {
|
const Roots = {
|
||||||
createCompositionArea,
|
createCompositionArea,
|
||||||
createLeftPane,
|
createLeftPane,
|
||||||
|
createTimeline,
|
||||||
createStickerManager,
|
createStickerManager,
|
||||||
createStickerPreviewModal,
|
createStickerPreviewModal,
|
||||||
};
|
};
|
||||||
|
|
|
@ -152,7 +152,7 @@
|
||||||
silent: !status.shouldPlayNotificationSound,
|
silent: !status.shouldPlayNotificationSound,
|
||||||
});
|
});
|
||||||
this.lastNotification.onclick = () =>
|
this.lastNotification.onclick = () =>
|
||||||
this.trigger('click', last.conversationId, last.id);
|
this.trigger('click', last.conversationId, last.messageId);
|
||||||
|
|
||||||
// We continue to build up more and more messages for our notifications
|
// We continue to build up more and more messages for our notifications
|
||||||
// until the user comes back to our app or closes the app. Then we’ll
|
// until the user comes back to our app or closes the app. Then we’ll
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,32 +0,0 @@
|
||||||
/* global Backbone, Whisper */
|
|
||||||
|
|
||||||
// eslint-disable-next-line func-names
|
|
||||||
(function() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
window.Whisper = window.Whisper || {};
|
|
||||||
|
|
||||||
Whisper.GroupUpdateView = Backbone.View.extend({
|
|
||||||
tagName: 'div',
|
|
||||||
className: 'group-update',
|
|
||||||
render() {
|
|
||||||
// TODO l10n
|
|
||||||
if (this.model.left) {
|
|
||||||
this.$el.text(`${this.model.left} left the group`);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
const messages = ['Updated the group.'];
|
|
||||||
if (this.model.name) {
|
|
||||||
messages.push(`Title is now '${this.model.name}'.`);
|
|
||||||
}
|
|
||||||
if (this.model.joined) {
|
|
||||||
messages.push(`${this.model.joined.join(', ')} joined the group`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$el.text(messages.join(' '));
|
|
||||||
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})();
|
|
|
@ -22,31 +22,28 @@
|
||||||
Whisper.ConversationStack = Whisper.View.extend({
|
Whisper.ConversationStack = Whisper.View.extend({
|
||||||
className: 'conversation-stack',
|
className: 'conversation-stack',
|
||||||
lastConversation: null,
|
lastConversation: null,
|
||||||
open(conversation) {
|
open(conversation, messageId) {
|
||||||
const id = `conversation-${conversation.cid}`;
|
const id = `conversation-${conversation.cid}`;
|
||||||
if (id !== this.el.firstChild.id) {
|
if (id !== this.el.lastChild.id) {
|
||||||
this.$el
|
const view = new Whisper.ConversationView({
|
||||||
.first()
|
model: conversation,
|
||||||
.find('video, audio')
|
window: this.model.window,
|
||||||
.each(function pauseMedia() {
|
});
|
||||||
this.pause();
|
view.$el.appendTo(this.el);
|
||||||
});
|
|
||||||
let $el = this.$(`#${id}`);
|
if (this.lastConversation) {
|
||||||
if ($el === null || $el.length === 0) {
|
this.lastConversation.trigger(
|
||||||
const view = new Whisper.ConversationView({
|
'unload',
|
||||||
model: conversation,
|
'opened another conversation'
|
||||||
window: this.model.window,
|
);
|
||||||
});
|
|
||||||
// eslint-disable-next-line prefer-destructuring
|
|
||||||
$el = view.$el;
|
|
||||||
}
|
}
|
||||||
$el.prependTo(this.el);
|
|
||||||
|
this.lastConversation = conversation;
|
||||||
|
conversation.trigger('opened', messageId);
|
||||||
|
} else if (messageId) {
|
||||||
|
conversation.trigger('scroll-to-message', messageId);
|
||||||
}
|
}
|
||||||
conversation.trigger('opened');
|
|
||||||
if (this.lastConversation) {
|
|
||||||
this.lastConversation.trigger('backgrounded');
|
|
||||||
}
|
|
||||||
this.lastConversation = conversation;
|
|
||||||
// Make sure poppers are positioned properly
|
// Make sure poppers are positioned properly
|
||||||
window.dispatchEvent(new Event('resize'));
|
window.dispatchEvent(new Event('resize'));
|
||||||
},
|
},
|
||||||
|
@ -122,11 +119,10 @@
|
||||||
},
|
},
|
||||||
setupLeftPane() {
|
setupLeftPane() {
|
||||||
this.leftPaneView = new Whisper.ReactWrapperView({
|
this.leftPaneView = new Whisper.ReactWrapperView({
|
||||||
JSX: Signal.State.Roots.createLeftPane(window.reduxStore),
|
|
||||||
className: 'left-pane-wrapper',
|
className: 'left-pane-wrapper',
|
||||||
|
JSX: Signal.State.Roots.createLeftPane(window.reduxStore),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Finally, add it to the DOM
|
|
||||||
this.$('.left-pane-placeholder').append(this.leftPaneView.el);
|
this.$('.left-pane-placeholder').append(this.leftPaneView.el);
|
||||||
},
|
},
|
||||||
startConnectionListener() {
|
startConnectionListener() {
|
||||||
|
@ -194,7 +190,7 @@
|
||||||
openConversationExternal(id, messageId);
|
openConversationExternal(id, messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.conversation_stack.open(conversation);
|
this.conversation_stack.open(conversation, messageId);
|
||||||
this.focusConversation();
|
this.focusConversation();
|
||||||
},
|
},
|
||||||
closeRecording(e) {
|
closeRecording(e) {
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
/* global Whisper, i18n */
|
|
||||||
|
|
||||||
// eslint-disable-next-line func-names
|
|
||||||
(function() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
window.Whisper = window.Whisper || {};
|
|
||||||
|
|
||||||
Whisper.LastSeenIndicatorView = Whisper.View.extend({
|
|
||||||
className: 'module-last-seen-indicator',
|
|
||||||
templateName: 'last-seen-indicator-view',
|
|
||||||
initialize(options = {}) {
|
|
||||||
this.count = options.count || 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
increment(count) {
|
|
||||||
this.count += count;
|
|
||||||
this.render();
|
|
||||||
},
|
|
||||||
|
|
||||||
getCount() {
|
|
||||||
return this.count;
|
|
||||||
},
|
|
||||||
|
|
||||||
render_attributes() {
|
|
||||||
const unreadMessages =
|
|
||||||
this.count === 1
|
|
||||||
? i18n('unreadMessage')
|
|
||||||
: i18n('unreadMessages', [this.count]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
unreadMessages,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})();
|
|
|
@ -1,143 +0,0 @@
|
||||||
/* global Whisper, Backbone, _, $ */
|
|
||||||
|
|
||||||
// eslint-disable-next-line func-names
|
|
||||||
(function() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
window.Whisper = window.Whisper || {};
|
|
||||||
|
|
||||||
Whisper.MessageListView = Backbone.View.extend({
|
|
||||||
tagName: 'ul',
|
|
||||||
className: 'message-list',
|
|
||||||
|
|
||||||
template: $('#message-list').html(),
|
|
||||||
itemView: Whisper.MessageView,
|
|
||||||
|
|
||||||
events: {
|
|
||||||
scroll: 'onScroll',
|
|
||||||
},
|
|
||||||
|
|
||||||
// Here we reimplement Whisper.ListView so we can override addAll
|
|
||||||
render() {
|
|
||||||
this.addAll();
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
// The key is that we don't erase all inner HTML, we re-render our template.
|
|
||||||
// And then we keep a reference to .messages
|
|
||||||
addAll() {
|
|
||||||
Whisper.View.prototype.render.call(this);
|
|
||||||
this.$messages = this.$('.messages');
|
|
||||||
this.collection.each(this.addOne, this);
|
|
||||||
},
|
|
||||||
|
|
||||||
initialize() {
|
|
||||||
this.listenTo(this.collection, 'add', this.addOne);
|
|
||||||
this.listenTo(this.collection, 'reset', this.addAll);
|
|
||||||
|
|
||||||
this.render();
|
|
||||||
|
|
||||||
this.triggerLazyScroll = _.debounce(() => {
|
|
||||||
this.$el.trigger('lazyScroll');
|
|
||||||
}, 500);
|
|
||||||
},
|
|
||||||
onScroll() {
|
|
||||||
this.measureScrollPosition();
|
|
||||||
if (this.$el.scrollTop() === 0) {
|
|
||||||
this.$el.trigger('loadMore');
|
|
||||||
}
|
|
||||||
if (this.atBottom()) {
|
|
||||||
this.$el.trigger('atBottom');
|
|
||||||
} else if (this.bottomOffset > this.outerHeight) {
|
|
||||||
this.$el.trigger('farFromBottom');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.triggerLazyScroll();
|
|
||||||
},
|
|
||||||
atBottom() {
|
|
||||||
return this.bottomOffset < 30;
|
|
||||||
},
|
|
||||||
measureScrollPosition() {
|
|
||||||
if (this.el.scrollHeight === 0) {
|
|
||||||
// hidden
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.outerHeight = this.$el.outerHeight();
|
|
||||||
this.scrollPosition = this.$el.scrollTop() + this.outerHeight;
|
|
||||||
this.scrollHeight = this.el.scrollHeight;
|
|
||||||
this.bottomOffset = this.scrollHeight - this.scrollPosition;
|
|
||||||
},
|
|
||||||
resetScrollPosition() {
|
|
||||||
this.$el.scrollTop(this.scrollPosition - this.$el.outerHeight());
|
|
||||||
},
|
|
||||||
restoreBottomOffset() {
|
|
||||||
if (_.isNumber(this.bottomOffset)) {
|
|
||||||
// + 10 is necessary to account for padding
|
|
||||||
const height = this.$el.height() + 10;
|
|
||||||
|
|
||||||
const topOfBottomScreen = this.el.scrollHeight - height;
|
|
||||||
this.$el.scrollTop(topOfBottomScreen - this.bottomOffset);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scrollToBottomIfNeeded() {
|
|
||||||
// This is counter-intuitive. Our current bottomOffset is reflective of what
|
|
||||||
// we last measured, not necessarily the current state. And this is called
|
|
||||||
// after we just made a change to the DOM: inserting a message, or an image
|
|
||||||
// finished loading. So if we were near the bottom before, we _need_ to be
|
|
||||||
// at the bottom again. So we scroll to the bottom.
|
|
||||||
if (this.atBottom()) {
|
|
||||||
this.scrollToBottom();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scrollToBottom() {
|
|
||||||
this.$el.scrollTop(this.el.scrollHeight);
|
|
||||||
this.measureScrollPosition();
|
|
||||||
},
|
|
||||||
addOne(model) {
|
|
||||||
// eslint-disable-next-line new-cap
|
|
||||||
const view = new this.itemView({ model }).render();
|
|
||||||
this.listenTo(view, 'beforeChangeHeight', this.measureScrollPosition);
|
|
||||||
this.listenTo(view, 'afterChangeHeight', this.scrollToBottomIfNeeded);
|
|
||||||
|
|
||||||
const index = this.collection.indexOf(model);
|
|
||||||
this.measureScrollPosition();
|
|
||||||
|
|
||||||
if (model.get('unread') && !this.atBottom()) {
|
|
||||||
this.$el.trigger('newOffscreenMessage');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index === this.collection.length - 1) {
|
|
||||||
// add to the bottom.
|
|
||||||
this.$messages.append(view.el);
|
|
||||||
} else if (index === 0) {
|
|
||||||
// add to top
|
|
||||||
this.$messages.prepend(view.el);
|
|
||||||
} else {
|
|
||||||
// insert
|
|
||||||
const next = this.$(`#${this.collection.at(index + 1).id}`);
|
|
||||||
const prev = this.$(`#${this.collection.at(index - 1).id}`);
|
|
||||||
if (next.length > 0) {
|
|
||||||
view.$el.insertBefore(next);
|
|
||||||
} else if (prev.length > 0) {
|
|
||||||
view.$el.insertAfter(prev);
|
|
||||||
} else {
|
|
||||||
// scan for the right spot
|
|
||||||
const elements = this.$messages.children();
|
|
||||||
if (elements.length > 0) {
|
|
||||||
for (let i = 0; i < elements.length; i += 1) {
|
|
||||||
const m = this.collection.get(elements[i].id);
|
|
||||||
const mIndex = this.collection.indexOf(m);
|
|
||||||
if (mIndex > index) {
|
|
||||||
view.$el.insertBefore(elements[i]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.$messages.append(view.el);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.scrollToBottomIfNeeded();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})();
|
|
|
@ -1,149 +0,0 @@
|
||||||
/* global Whisper: false */
|
|
||||||
|
|
||||||
// eslint-disable-next-line func-names
|
|
||||||
(function() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
window.Whisper = window.Whisper || {};
|
|
||||||
|
|
||||||
Whisper.MessageView = Whisper.View.extend({
|
|
||||||
tagName: 'li',
|
|
||||||
id() {
|
|
||||||
return this.model.id;
|
|
||||||
},
|
|
||||||
initialize() {
|
|
||||||
this.listenTo(this.model, 'change', this.onChange);
|
|
||||||
this.listenTo(this.model, 'destroy', this.onDestroy);
|
|
||||||
this.listenTo(this.model, 'unload', this.onUnload);
|
|
||||||
this.listenTo(this.model, 'expired', this.onExpired);
|
|
||||||
|
|
||||||
this.updateHiddenSticker();
|
|
||||||
},
|
|
||||||
updateHiddenSticker() {
|
|
||||||
const sticker = this.model.get('sticker');
|
|
||||||
this.isHiddenSticker = sticker && (!sticker.data || !sticker.data.path);
|
|
||||||
},
|
|
||||||
onChange() {
|
|
||||||
this.addId();
|
|
||||||
},
|
|
||||||
addId() {
|
|
||||||
// The ID is important for other items inserting themselves into the DOM. Because
|
|
||||||
// of ReactWrapperView and this view, there are two layers of DOM elements
|
|
||||||
// between the parent and the elements returned by the React component, so this is
|
|
||||||
// necessary.
|
|
||||||
const { id } = this.model;
|
|
||||||
this.$el.attr('id', id);
|
|
||||||
},
|
|
||||||
onExpired() {
|
|
||||||
setTimeout(() => this.onUnload(), 1000);
|
|
||||||
},
|
|
||||||
onUnload() {
|
|
||||||
if (this.childView) {
|
|
||||||
this.childView.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.remove();
|
|
||||||
},
|
|
||||||
onDestroy() {
|
|
||||||
this.onUnload();
|
|
||||||
},
|
|
||||||
getRenderInfo() {
|
|
||||||
const { Components } = window.Signal;
|
|
||||||
const { type, data: props } = this.model.props;
|
|
||||||
|
|
||||||
if (type === 'unsupportedMessage') {
|
|
||||||
return {
|
|
||||||
Component: Components.UnsupportedMessage,
|
|
||||||
props,
|
|
||||||
};
|
|
||||||
} else if (type === 'timerNotification') {
|
|
||||||
return {
|
|
||||||
Component: Components.TimerNotification,
|
|
||||||
props,
|
|
||||||
};
|
|
||||||
} else if (type === 'safetyNumberNotification') {
|
|
||||||
return {
|
|
||||||
Component: Components.SafetyNumberNotification,
|
|
||||||
props,
|
|
||||||
};
|
|
||||||
} else if (type === 'verificationNotification') {
|
|
||||||
return {
|
|
||||||
Component: Components.VerificationNotification,
|
|
||||||
props,
|
|
||||||
};
|
|
||||||
} else if (type === 'groupNotification') {
|
|
||||||
return {
|
|
||||||
Component: Components.GroupNotification,
|
|
||||||
props,
|
|
||||||
};
|
|
||||||
} else if (type === 'resetSessionNotification') {
|
|
||||||
return {
|
|
||||||
Component: Components.ResetSessionNotification,
|
|
||||||
props,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
Component: Components.Message,
|
|
||||||
props,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
render() {
|
|
||||||
this.addId();
|
|
||||||
|
|
||||||
if (this.childView) {
|
|
||||||
this.childView.remove();
|
|
||||||
this.childView = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { Component, props } = this.getRenderInfo();
|
|
||||||
this.childView = new Whisper.ReactWrapperView({
|
|
||||||
className: 'message-wrapper',
|
|
||||||
Component,
|
|
||||||
props,
|
|
||||||
});
|
|
||||||
|
|
||||||
const update = () => {
|
|
||||||
const info = this.getRenderInfo();
|
|
||||||
this.childView.update(info.props, () => {
|
|
||||||
if (!this.isHiddenSticker) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateHiddenSticker();
|
|
||||||
|
|
||||||
if (!this.isHiddenSticker) {
|
|
||||||
this.model.trigger('height-changed');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.listenTo(this.model, 'change', update);
|
|
||||||
this.listenTo(this.model, 'expired', update);
|
|
||||||
|
|
||||||
const applicableConversationChanges =
|
|
||||||
'change:color change:name change:number change:profileName change:profileAvatar';
|
|
||||||
|
|
||||||
this.conversation = this.model.getConversation();
|
|
||||||
this.listenTo(this.conversation, applicableConversationChanges, update);
|
|
||||||
|
|
||||||
this.fromContact = this.model.getIncomingContact();
|
|
||||||
if (this.fromContact) {
|
|
||||||
this.listenTo(this.fromContact, applicableConversationChanges, update);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.quotedContact = this.model.getQuoteContact();
|
|
||||||
if (this.quotedContact) {
|
|
||||||
this.listenTo(
|
|
||||||
this.quotedContact,
|
|
||||||
applicableConversationChanges,
|
|
||||||
update
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$el.append(this.childView.el);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})();
|
|
|
@ -1,39 +0,0 @@
|
||||||
/* global Whisper, i18n */
|
|
||||||
|
|
||||||
// eslint-disable-next-line func-names
|
|
||||||
(function() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
window.Whisper = window.Whisper || {};
|
|
||||||
|
|
||||||
Whisper.ScrollDownButtonView = Whisper.View.extend({
|
|
||||||
className: 'module-scroll-down',
|
|
||||||
templateName: 'scroll-down-button-view',
|
|
||||||
|
|
||||||
initialize(options = {}) {
|
|
||||||
this.count = options.count || 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
increment(count = 0) {
|
|
||||||
this.count += count;
|
|
||||||
this.render();
|
|
||||||
},
|
|
||||||
|
|
||||||
render_attributes() {
|
|
||||||
const buttonClass =
|
|
||||||
this.count > 0 ? 'module-scroll-down__button--new-messages' : '';
|
|
||||||
|
|
||||||
let moreBelow = i18n('scrollDown');
|
|
||||||
if (this.count > 1) {
|
|
||||||
moreBelow = i18n('messagesBelow');
|
|
||||||
} else if (this.count === 1) {
|
|
||||||
moreBelow = i18n('messageBelow');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
buttonClass,
|
|
||||||
moreBelow,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})();
|
|
|
@ -12,7 +12,7 @@
|
||||||
},
|
},
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "electron-builder install-app-deps && rimraf node_modules/dtrace-provider",
|
"postinstall": "patch-package && electron-builder install-app-deps && rimraf node_modules/dtrace-provider",
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"grunt": "grunt",
|
"grunt": "grunt",
|
||||||
"icon-gen": "electron-icon-maker --input=images/icon_1024.png --output=./build",
|
"icon-gen": "electron-icon-maker --input=images/icon_1024.png --output=./build",
|
||||||
|
@ -169,6 +169,7 @@
|
||||||
"mocha-testcheck": "1.0.0-rc.0",
|
"mocha-testcheck": "1.0.0-rc.0",
|
||||||
"node-sass-import-once": "1.2.0",
|
"node-sass-import-once": "1.2.0",
|
||||||
"nyc": "11.4.1",
|
"nyc": "11.4.1",
|
||||||
|
"patch-package": "6.1.2",
|
||||||
"prettier": "1.12.0",
|
"prettier": "1.12.0",
|
||||||
"react-docgen-typescript": "1.2.6",
|
"react-docgen-typescript": "1.2.6",
|
||||||
"react-styleguidist": "7.0.1",
|
"react-styleguidist": "7.0.1",
|
||||||
|
|
357
patches/react-virtualized+9.21.0.patch
Normal file
357
patches/react-virtualized+9.21.0.patch
Normal file
|
@ -0,0 +1,357 @@
|
||||||
|
diff --git a/node_modules/react-virtualized/dist/commonjs/CellMeasurer/CellMeasurer.js b/node_modules/react-virtualized/dist/commonjs/CellMeasurer/CellMeasurer.js
|
||||||
|
index d9716a0..e7a9f9f 100644
|
||||||
|
--- a/node_modules/react-virtualized/dist/commonjs/CellMeasurer/CellMeasurer.js
|
||||||
|
+++ b/node_modules/react-virtualized/dist/commonjs/CellMeasurer/CellMeasurer.js
|
||||||
|
@@ -166,13 +166,19 @@ var CellMeasurer = function (_React$PureComponent) {
|
||||||
|
height = _getCellMeasurements2.height,
|
||||||
|
width = _getCellMeasurements2.width;
|
||||||
|
|
||||||
|
+
|
||||||
|
cache.set(rowIndex, columnIndex, width, height);
|
||||||
|
|
||||||
|
// If size has changed, let Grid know to re-render.
|
||||||
|
if (parent && typeof parent.invalidateCellSizeAfterRender === 'function') {
|
||||||
|
+ const heightChange = height - cache.defaultHeight;
|
||||||
|
+ const widthChange = width - cache.defaultWidth;
|
||||||
|
+
|
||||||
|
parent.invalidateCellSizeAfterRender({
|
||||||
|
columnIndex: columnIndex,
|
||||||
|
- rowIndex: rowIndex
|
||||||
|
+ rowIndex: rowIndex,
|
||||||
|
+ heightChange: heightChange,
|
||||||
|
+ widthChange: widthChange,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
diff --git a/node_modules/react-virtualized/dist/commonjs/Grid/Grid.js b/node_modules/react-virtualized/dist/commonjs/Grid/Grid.js
|
||||||
|
index e1b959a..1e3a269 100644
|
||||||
|
--- a/node_modules/react-virtualized/dist/commonjs/Grid/Grid.js
|
||||||
|
+++ b/node_modules/react-virtualized/dist/commonjs/Grid/Grid.js
|
||||||
|
@@ -132,6 +132,9 @@ var Grid = function (_React$PureComponent) {
|
||||||
|
_this._renderedRowStopIndex = 0;
|
||||||
|
_this._styleCache = {};
|
||||||
|
_this._cellCache = {};
|
||||||
|
+ _this._cellUpdates = [];
|
||||||
|
+ this._hasScrolledToColumnTarget = false;
|
||||||
|
+ this._hasScrolledToRowTarget = false;
|
||||||
|
|
||||||
|
_this._debounceScrollEndedCallback = function () {
|
||||||
|
_this._disablePointerEventsTimeoutId = null;
|
||||||
|
@@ -345,7 +348,11 @@ var Grid = function (_React$PureComponent) {
|
||||||
|
scrollLeft: scrollLeft,
|
||||||
|
scrollTop: scrollTop,
|
||||||
|
totalColumnsWidth: totalColumnsWidth,
|
||||||
|
- totalRowsHeight: totalRowsHeight
|
||||||
|
+ totalRowsHeight: totalRowsHeight,
|
||||||
|
+ scrollToColumn: this.props.scrollToColumn,
|
||||||
|
+ _hasScrolledToColumnTarget: this._hasScrolledToColumnTarget,
|
||||||
|
+ scrollToRow: this.props.scrollToRow,
|
||||||
|
+ _hasScrolledToRowTarget: this._hasScrolledToRowTarget,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -362,6 +369,9 @@ var Grid = function (_React$PureComponent) {
|
||||||
|
value: function invalidateCellSizeAfterRender(_ref3) {
|
||||||
|
var columnIndex = _ref3.columnIndex,
|
||||||
|
rowIndex = _ref3.rowIndex;
|
||||||
|
+ if (!this._disableCellUpdates) {
|
||||||
|
+ this._cellUpdates.push(_ref3);
|
||||||
|
+ }
|
||||||
|
|
||||||
|
this._deferredInvalidateColumnIndex = typeof this._deferredInvalidateColumnIndex === 'number' ? Math.min(this._deferredInvalidateColumnIndex, columnIndex) : columnIndex;
|
||||||
|
this._deferredInvalidateRowIndex = typeof this._deferredInvalidateRowIndex === 'number' ? Math.min(this._deferredInvalidateRowIndex, rowIndex) : rowIndex;
|
||||||
|
@@ -381,8 +391,12 @@ var Grid = function (_React$PureComponent) {
|
||||||
|
rowCount = _props2.rowCount;
|
||||||
|
var instanceProps = this.state.instanceProps;
|
||||||
|
|
||||||
|
- instanceProps.columnSizeAndPositionManager.getSizeAndPositionOfCell(columnCount - 1);
|
||||||
|
- instanceProps.rowSizeAndPositionManager.getSizeAndPositionOfCell(rowCount - 1);
|
||||||
|
+ if (columnCount > 0) {
|
||||||
|
+ instanceProps.columnSizeAndPositionManager.getSizeAndPositionOfCell(columnCount - 1);
|
||||||
|
+ }
|
||||||
|
+ if (rowCount > 0) {
|
||||||
|
+ instanceProps.rowSizeAndPositionManager.getSizeAndPositionOfCell(rowCount - 1);
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@@ -415,6 +429,15 @@ var Grid = function (_React$PureComponent) {
|
||||||
|
this._recomputeScrollLeftFlag = scrollToColumn >= 0 && (this.state.scrollDirectionHorizontal === _defaultOverscanIndicesGetter.SCROLL_DIRECTION_FORWARD ? columnIndex <= scrollToColumn : columnIndex >= scrollToColumn);
|
||||||
|
this._recomputeScrollTopFlag = scrollToRow >= 0 && (this.state.scrollDirectionVertical === _defaultOverscanIndicesGetter.SCROLL_DIRECTION_FORWARD ? rowIndex <= scrollToRow : rowIndex >= scrollToRow);
|
||||||
|
|
||||||
|
+ // Global notification that we should retry our scroll to props-requested indices
|
||||||
|
+ this._hasScrolledToColumnTarget = false;
|
||||||
|
+ this._hasScrolledToRowTarget = false;
|
||||||
|
+
|
||||||
|
+ // Disable cell updates for global reset
|
||||||
|
+ if (rowIndex >= this.props.rowCount - 1 || columnIndex >= this.props.columnCount - 1) {
|
||||||
|
+ this._disableCellUpdates = true;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
// Clear cell cache in case we are scrolling;
|
||||||
|
// Invalid row heights likely mean invalid cached content as well.
|
||||||
|
this._styleCache = {};
|
||||||
|
@@ -526,7 +549,11 @@ var Grid = function (_React$PureComponent) {
|
||||||
|
scrollLeft: scrollLeft || 0,
|
||||||
|
scrollTop: scrollTop || 0,
|
||||||
|
totalColumnsWidth: instanceProps.columnSizeAndPositionManager.getTotalSize(),
|
||||||
|
- totalRowsHeight: instanceProps.rowSizeAndPositionManager.getTotalSize()
|
||||||
|
+ totalRowsHeight: instanceProps.rowSizeAndPositionManager.getTotalSize(),
|
||||||
|
+ scrollToColumn: scrollToColumn,
|
||||||
|
+ _hasScrolledToColumnTarget: this._hasScrolledToColumnTarget,
|
||||||
|
+ scrollToRow: scrollToRow,
|
||||||
|
+ _hasScrolledToRowTarget: this._hasScrolledToRowTarget,
|
||||||
|
});
|
||||||
|
|
||||||
|
this._maybeCallOnScrollbarPresenceChange();
|
||||||
|
@@ -584,6 +611,51 @@ var Grid = function (_React$PureComponent) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+ if (scrollToColumn >= 0) {
|
||||||
|
+ const scrollRight = scrollLeft + width;
|
||||||
|
+ const targetColumn = instanceProps.columnSizeAndPositionManager.getSizeAndPositionOfCell(scrollToColumn);
|
||||||
|
+
|
||||||
|
+ let isVisible = false;
|
||||||
|
+ if (targetColumn.size <= width) {
|
||||||
|
+ const targetColumnRight = targetColumn.offset + targetColumn.size;
|
||||||
|
+ isVisible = (targetColumn.offset >= scrollLeft && targetColumnRight <= scrollRight);
|
||||||
|
+ } else {
|
||||||
|
+ isVisible = (targetColumn.offset >= scrollLeft && targetColumn.offset <= scrollRight);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (isVisible) {
|
||||||
|
+ const totalColumnsWidth = instanceProps.columnSizeAndPositionManager.getTotalSize();
|
||||||
|
+ const maxScroll = totalColumnsWidth - width;
|
||||||
|
+ this._hasScrolledToColumnTarget = (scrollLeft >= maxScroll || targetColumn.offset === scrollLeft);
|
||||||
|
+ } else if (scrollToColumn !== prevProps.scrollToColumn) {
|
||||||
|
+ this._hasScrolledToColumnTarget = false;
|
||||||
|
+ }
|
||||||
|
+ } else {
|
||||||
|
+ this._hasScrolledToColumnTarget = false;
|
||||||
|
+ }
|
||||||
|
+ if (scrollToRow >= 0) {
|
||||||
|
+ const scrollBottom = scrollTop + height;
|
||||||
|
+ const targetRow = instanceProps.rowSizeAndPositionManager.getSizeAndPositionOfCell(scrollToRow);
|
||||||
|
+
|
||||||
|
+ let isVisible = false;
|
||||||
|
+ if (targetRow.size <= height) {
|
||||||
|
+ const targetRowBottom = targetRow.offset + targetRow.size;
|
||||||
|
+ isVisible = (targetRow.offset >= scrollTop && targetRowBottom <= scrollBottom);
|
||||||
|
+ } else {
|
||||||
|
+ isVisible = (targetRow.offset >= scrollTop && targetRow.offset <= scrollBottom);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (isVisible) {
|
||||||
|
+ const totalRowsHeight = instanceProps.rowSizeAndPositionManager.getTotalSize();
|
||||||
|
+ const maxScroll = totalRowsHeight - height;
|
||||||
|
+ this._hasScrolledToRowTarget = (scrollTop >= maxScroll || targetRow.offset === scrollTop);
|
||||||
|
+ } else if (scrollToRow !== prevProps.scrollToRow) {
|
||||||
|
+ this._hasScrolledToRowTarget = false;
|
||||||
|
+ }
|
||||||
|
+ } else {
|
||||||
|
+ this._hasScrolledToRowTarget = false;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
// Special case where the previous size was 0:
|
||||||
|
// In this case we don't show any windowed cells at all.
|
||||||
|
// So we should always recalculate offset afterwards.
|
||||||
|
@@ -594,6 +666,8 @@ var Grid = function (_React$PureComponent) {
|
||||||
|
if (this._recomputeScrollLeftFlag) {
|
||||||
|
this._recomputeScrollLeftFlag = false;
|
||||||
|
this._updateScrollLeftForScrollToColumn(this.props);
|
||||||
|
+ } else if (this.props.scrollToColumn >= 0 && !this._hasScrolledToColumnTarget) {
|
||||||
|
+ this._updateScrollLeftForScrollToColumn(this.props);
|
||||||
|
} else {
|
||||||
|
(0, _updateScrollIndexHelper2.default)({
|
||||||
|
cellSizeAndPositionManager: instanceProps.columnSizeAndPositionManager,
|
||||||
|
@@ -616,6 +690,8 @@ var Grid = function (_React$PureComponent) {
|
||||||
|
if (this._recomputeScrollTopFlag) {
|
||||||
|
this._recomputeScrollTopFlag = false;
|
||||||
|
this._updateScrollTopForScrollToRow(this.props);
|
||||||
|
+ } else if (this.props.scrollToRow >= 0 && !this._hasScrolledToRowTarget) {
|
||||||
|
+ this._updateScrollTopForScrollToRow(this.props);
|
||||||
|
} else {
|
||||||
|
(0, _updateScrollIndexHelper2.default)({
|
||||||
|
cellSizeAndPositionManager: instanceProps.rowSizeAndPositionManager,
|
||||||
|
@@ -630,11 +706,56 @@ var Grid = function (_React$PureComponent) {
|
||||||
|
size: height,
|
||||||
|
sizeJustIncreasedFromZero: sizeJustIncreasedFromZero,
|
||||||
|
updateScrollIndexCallback: function updateScrollIndexCallback() {
|
||||||
|
- return _this2._updateScrollTopForScrollToRow(_this2.props);
|
||||||
|
+ _this2._updateScrollLeftForScrollToColumn(_this2.props);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
+ this._disableCellUpdates = false;
|
||||||
|
+ if (scrollPositionChangeReason !== SCROLL_POSITION_CHANGE_REASONS.OBSERVED) {
|
||||||
|
+ this._cellUpdates = [];
|
||||||
|
+ }
|
||||||
|
+ if (this._cellUpdates.length) {
|
||||||
|
+ const currentScrollTop = this.state.scrollTop;
|
||||||
|
+ const currentScrollBottom = currentScrollTop + height;
|
||||||
|
+ const currentScrollLeft = this.state.scrollLeft;
|
||||||
|
+ const currentScrollRight = currentScrollLeft + width;
|
||||||
|
+
|
||||||
|
+ let item;
|
||||||
|
+ let verticalDelta = 0;
|
||||||
|
+ let horizontalDelta = 0;
|
||||||
|
+
|
||||||
|
+ while (item = this._cellUpdates.shift()) {
|
||||||
|
+ const rowData = instanceProps.rowSizeAndPositionManager.getSizeAndPositionOfCell(item.rowIndex);
|
||||||
|
+ const columnData = instanceProps.columnSizeAndPositionManager.getSizeAndPositionOfCell(item.columnIndex);
|
||||||
|
+
|
||||||
|
+ const bottomOfItem = rowData.offset + rowData.size;
|
||||||
|
+ const rightSideOfItem = columnData.offset + columnData.size;
|
||||||
|
+
|
||||||
|
+ if (bottomOfItem < currentScrollBottom) {
|
||||||
|
+ verticalDelta += item.heightChange;
|
||||||
|
+ }
|
||||||
|
+ if (rightSideOfItem < currentScrollRight) {
|
||||||
|
+ horizontalDelta += item.widthChange;
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (this.props.scrollToRow >= 0 && !this._hasScrolledToRowTarget) {
|
||||||
|
+ verticalDelta = 0;
|
||||||
|
+ }
|
||||||
|
+ if (this.props.scrollToColumn >= 0 && !this._hasScrolledToColumnTarget) {
|
||||||
|
+ horizontalDelta = 0;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (verticalDelta !== 0 || horizontalDelta !== 0) {
|
||||||
|
+ this.setState(Grid._getScrollToPositionStateUpdate({
|
||||||
|
+ prevState: this.state,
|
||||||
|
+ scrollTop: scrollTop + verticalDelta,
|
||||||
|
+ scrollLeft: scrollLeft + horizontalDelta,
|
||||||
|
+ }));
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
// Update onRowsRendered callback if start/stop indices have changed
|
||||||
|
this._invokeOnGridRenderedHelper();
|
||||||
|
|
||||||
|
@@ -647,7 +768,11 @@ var Grid = function (_React$PureComponent) {
|
||||||
|
scrollLeft: scrollLeft,
|
||||||
|
scrollTop: scrollTop,
|
||||||
|
totalColumnsWidth: totalColumnsWidth,
|
||||||
|
- totalRowsHeight: totalRowsHeight
|
||||||
|
+ totalRowsHeight: totalRowsHeight,
|
||||||
|
+ scrollToColumn: scrollToColumn,
|
||||||
|
+ _hasScrolledToColumnTarget: this._hasScrolledToColumnTarget,
|
||||||
|
+ scrollToRow: scrollToRow,
|
||||||
|
+ _hasScrolledToRowTarget: this._hasScrolledToRowTarget,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -962,7 +1087,11 @@ var Grid = function (_React$PureComponent) {
|
||||||
|
var scrollLeft = _ref6.scrollLeft,
|
||||||
|
scrollTop = _ref6.scrollTop,
|
||||||
|
totalColumnsWidth = _ref6.totalColumnsWidth,
|
||||||
|
- totalRowsHeight = _ref6.totalRowsHeight;
|
||||||
|
+ totalRowsHeight = _ref6.totalRowsHeight,
|
||||||
|
+ scrollToColumn = _ref6.scrollToColumn,
|
||||||
|
+ _hasScrolledToColumnTarget = _ref6._hasScrolledToColumnTarget,
|
||||||
|
+ scrollToRow = _ref6.scrollToRow,
|
||||||
|
+ _hasScrolledToRowTarget = _ref6._hasScrolledToRowTarget;
|
||||||
|
|
||||||
|
this._onScrollMemoizer({
|
||||||
|
callback: function callback(_ref7) {
|
||||||
|
@@ -973,19 +1102,26 @@ var Grid = function (_React$PureComponent) {
|
||||||
|
onScroll = _props7.onScroll,
|
||||||
|
width = _props7.width;
|
||||||
|
|
||||||
|
-
|
||||||
|
onScroll({
|
||||||
|
clientHeight: height,
|
||||||
|
clientWidth: width,
|
||||||
|
scrollHeight: totalRowsHeight,
|
||||||
|
scrollLeft: scrollLeft,
|
||||||
|
scrollTop: scrollTop,
|
||||||
|
- scrollWidth: totalColumnsWidth
|
||||||
|
+ scrollWidth: totalColumnsWidth,
|
||||||
|
+ scrollToColumn: scrollToColumn,
|
||||||
|
+ _hasScrolledToColumnTarget: _hasScrolledToColumnTarget,
|
||||||
|
+ scrollToRow: scrollToRow,
|
||||||
|
+ _hasScrolledToRowTarget: _hasScrolledToRowTarget,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
indices: {
|
||||||
|
scrollLeft: scrollLeft,
|
||||||
|
- scrollTop: scrollTop
|
||||||
|
+ scrollTop: scrollTop,
|
||||||
|
+ scrollToColumn: scrollToColumn,
|
||||||
|
+ _hasScrolledToColumnTarget: _hasScrolledToColumnTarget,
|
||||||
|
+ scrollToRow: scrollToRow,
|
||||||
|
+ _hasScrolledToRowTarget: _hasScrolledToRowTarget,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
diff --git a/node_modules/react-virtualized/dist/commonjs/Grid/accessibilityOverscanIndicesGetter.js b/node_modules/react-virtualized/dist/commonjs/Grid/accessibilityOverscanIndicesGetter.js
|
||||||
|
index 70b0abe..2f04448 100644
|
||||||
|
--- a/node_modules/react-virtualized/dist/commonjs/Grid/accessibilityOverscanIndicesGetter.js
|
||||||
|
+++ b/node_modules/react-virtualized/dist/commonjs/Grid/accessibilityOverscanIndicesGetter.js
|
||||||
|
@@ -32,15 +32,8 @@ function defaultOverscanIndicesGetter(_ref) {
|
||||||
|
// For more info see issues #625
|
||||||
|
overscanCellsCount = Math.max(1, overscanCellsCount);
|
||||||
|
|
||||||
|
- if (scrollDirection === SCROLL_DIRECTION_FORWARD) {
|
||||||
|
- return {
|
||||||
|
- overscanStartIndex: Math.max(0, startIndex - 1),
|
||||||
|
- overscanStopIndex: Math.min(cellCount - 1, stopIndex + overscanCellsCount)
|
||||||
|
- };
|
||||||
|
- } else {
|
||||||
|
- return {
|
||||||
|
- overscanStartIndex: Math.max(0, startIndex - overscanCellsCount),
|
||||||
|
- overscanStopIndex: Math.min(cellCount - 1, stopIndex + 1)
|
||||||
|
- };
|
||||||
|
- }
|
||||||
|
+ return {
|
||||||
|
+ overscanStartIndex: Math.max(0, startIndex - overscanCellsCount),
|
||||||
|
+ overscanStopIndex: Math.min(cellCount - 1, stopIndex + overscanCellsCount),
|
||||||
|
+ };
|
||||||
|
}
|
||||||
|
diff --git a/node_modules/react-virtualized/dist/commonjs/Grid/defaultOverscanIndicesGetter.js b/node_modules/react-virtualized/dist/commonjs/Grid/defaultOverscanIndicesGetter.js
|
||||||
|
index d5f6d04..e01e69a 100644
|
||||||
|
--- a/node_modules/react-virtualized/dist/commonjs/Grid/defaultOverscanIndicesGetter.js
|
||||||
|
+++ b/node_modules/react-virtualized/dist/commonjs/Grid/defaultOverscanIndicesGetter.js
|
||||||
|
@@ -27,15 +27,8 @@ function defaultOverscanIndicesGetter(_ref) {
|
||||||
|
startIndex = _ref.startIndex,
|
||||||
|
stopIndex = _ref.stopIndex;
|
||||||
|
|
||||||
|
- if (scrollDirection === SCROLL_DIRECTION_FORWARD) {
|
||||||
|
- return {
|
||||||
|
- overscanStartIndex: Math.max(0, startIndex),
|
||||||
|
- overscanStopIndex: Math.min(cellCount - 1, stopIndex + overscanCellsCount)
|
||||||
|
- };
|
||||||
|
- } else {
|
||||||
|
- return {
|
||||||
|
- overscanStartIndex: Math.max(0, startIndex - overscanCellsCount),
|
||||||
|
- overscanStopIndex: Math.min(cellCount - 1, stopIndex)
|
||||||
|
- };
|
||||||
|
- }
|
||||||
|
+ return {
|
||||||
|
+ overscanStartIndex: Math.max(0, startIndex - overscanCellsCount),
|
||||||
|
+ overscanStopIndex: Math.min(cellCount - 1, stopIndex + overscanCellsCount),
|
||||||
|
+ };
|
||||||
|
}
|
||||||
|
diff --git a/node_modules/react-virtualized/dist/commonjs/List/List.js b/node_modules/react-virtualized/dist/commonjs/List/List.js
|
||||||
|
index b5ad0eb..efb2cd7 100644
|
||||||
|
--- a/node_modules/react-virtualized/dist/commonjs/List/List.js
|
||||||
|
+++ b/node_modules/react-virtualized/dist/commonjs/List/List.js
|
||||||
|
@@ -112,13 +112,8 @@ var List = function (_React$PureComponent) {
|
||||||
|
}, _this._setRef = function (ref) {
|
||||||
|
_this.Grid = ref;
|
||||||
|
}, _this._onScroll = function (_ref3) {
|
||||||
|
- var clientHeight = _ref3.clientHeight,
|
||||||
|
- scrollHeight = _ref3.scrollHeight,
|
||||||
|
- scrollTop = _ref3.scrollTop;
|
||||||
|
var onScroll = _this.props.onScroll;
|
||||||
|
-
|
||||||
|
-
|
||||||
|
- onScroll({ clientHeight: clientHeight, scrollHeight: scrollHeight, scrollTop: scrollTop });
|
||||||
|
+ onScroll(_ref3);
|
||||||
|
}, _this._onSectionRendered = function (_ref4) {
|
||||||
|
var rowOverscanStartIndex = _ref4.rowOverscanStartIndex,
|
||||||
|
rowOverscanStopIndex = _ref4.rowOverscanStopIndex,
|
|
@ -32,7 +32,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@include dark-theme() {
|
@include dark-theme() {
|
||||||
background-color: $color-black;
|
background-color: $color-gray-95;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,24 +66,21 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.main.panel {
|
.main.panel {
|
||||||
.discussion-container {
|
.timeline-placeholder {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
.bar-container {
|
.timeline-wrapper {
|
||||||
height: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-list {
|
|
||||||
-webkit-padding-start: 0px;
|
-webkit-padding-start: 0px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 10px 0 0 0;
|
padding: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
@ -177,38 +174,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-container,
|
|
||||||
.message-list {
|
|
||||||
list-style: none;
|
|
||||||
|
|
||||||
.message-wrapper {
|
|
||||||
margin-left: 16px;
|
|
||||||
margin-right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
visibility: hidden;
|
|
||||||
display: block;
|
|
||||||
font-size: 0;
|
|
||||||
content: ' ';
|
|
||||||
clear: both;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.group {
|
|
||||||
.message-container,
|
|
||||||
.message-list {
|
|
||||||
.message-wrapper {
|
|
||||||
margin-left: 44px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.typing-bubble-wrapper {
|
.typing-bubble-wrapper {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
@ -282,31 +247,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-previews {
|
|
||||||
padding: 0 36px;
|
|
||||||
margin-bottom: 3px;
|
|
||||||
|
|
||||||
.attachment-preview {
|
|
||||||
padding: 13px 10px 0;
|
|
||||||
}
|
|
||||||
img {
|
|
||||||
border: 2px solid #ddd;
|
|
||||||
border-radius: $border-radius;
|
|
||||||
max-height: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close {
|
|
||||||
position: absolute;
|
|
||||||
top: 5px;
|
|
||||||
right: 2px;
|
|
||||||
background: #999;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: $grey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex {
|
.flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -462,63 +402,3 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-last-seen-indicator {
|
|
||||||
padding-top: 25px;
|
|
||||||
padding-bottom: 35px;
|
|
||||||
margin-left: 28px;
|
|
||||||
margin-right: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-last-seen-indicator__bar {
|
|
||||||
background-color: $color-light-60;
|
|
||||||
width: 100%;
|
|
||||||
height: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-last-seen-indicator__text {
|
|
||||||
margin-top: 3px;
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 16px;
|
|
||||||
letter-spacing: 0.3px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
color: $color-light-90;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-scroll-down {
|
|
||||||
z-index: 100;
|
|
||||||
position: absolute;
|
|
||||||
right: 20px;
|
|
||||||
bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-scroll-down__button {
|
|
||||||
height: 44px;
|
|
||||||
width: 44px;
|
|
||||||
border-radius: 22px;
|
|
||||||
text-align: center;
|
|
||||||
background-color: $color-light-35;
|
|
||||||
border: none;
|
|
||||||
box-shadow: 0px 3px 5px 0px rgba(0, 0, 0, 0.2);
|
|
||||||
outline: none;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $color-light-45;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-scroll-down__button--new-messages {
|
|
||||||
background-color: $color-signal-blue;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #1472bd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-scroll-down__icon {
|
|
||||||
@include color-svg('../images/down.svg', $color-white);
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
|
@ -81,15 +81,6 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation-stack {
|
|
||||||
.conversation {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.conversation:first-child {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-bar {
|
.tool-bar {
|
||||||
color: $color-light-90;
|
color: $color-light-90;
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,22 @@
|
||||||
|
|
||||||
// Module: Message
|
// Module: Message
|
||||||
|
|
||||||
|
// Note: this does the same thing as module-timeline__message-container but
|
||||||
|
// can be used outside tht Timeline contact more easily.
|
||||||
|
.module-message-container {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
visibility: hidden;
|
||||||
|
display: block;
|
||||||
|
font-size: 0;
|
||||||
|
content: ' ';
|
||||||
|
clear: both;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.module-message {
|
.module-message {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
@ -39,15 +55,19 @@
|
||||||
|
|
||||||
// Spec: container < 438px
|
// Spec: container < 438px
|
||||||
.module-message--incoming {
|
.module-message--incoming {
|
||||||
margin-left: 0;
|
margin-left: 16px;
|
||||||
margin-right: 32px;
|
margin-right: 32px;
|
||||||
}
|
}
|
||||||
.module-message--outgoing {
|
.module-message--outgoing {
|
||||||
float: right;
|
float: right;
|
||||||
margin-right: 0;
|
margin-right: 16px;
|
||||||
margin-left: 32px;
|
margin-left: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-message--incoming.module-message--group {
|
||||||
|
margin-left: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
.module-message__buttons {
|
.module-message__buttons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -165,6 +185,37 @@
|
||||||
background-color: $color-light-10;
|
background-color: $color-light-10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-message__container__selection {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
border-radius: 16px;
|
||||||
|
|
||||||
|
background-color: $color-black;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
animation: message--selected 1s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes message--selected {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// In case the color gets messed up
|
// In case the color gets messed up
|
||||||
.module-message__container--incoming {
|
.module-message__container--incoming {
|
||||||
background-color: $color-conversation-grey;
|
background-color: $color-conversation-grey;
|
||||||
|
@ -704,10 +755,10 @@
|
||||||
|
|
||||||
.module-message__metadata__status-icon--sending {
|
.module-message__metadata__status-icon--sending {
|
||||||
@include color-svg('../images/sending.svg', $color-gray-60);
|
@include color-svg('../images/sending.svg', $color-gray-60);
|
||||||
animation: module-message__metdata__status-icon--spinning 4s linear infinite;
|
animation: module-message__metadata__status-icon--spinning 4s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes module-message__metdata__status-icon--spinning {
|
@keyframes module-message__metadata__status-icon--spinning {
|
||||||
100% {
|
100% {
|
||||||
-webkit-transform: rotate(360deg);
|
-webkit-transform: rotate(360deg);
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
|
@ -842,6 +893,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-quote {
|
.module-quote {
|
||||||
|
// To leave room for image thumbnail
|
||||||
|
min-height: 54px;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border-top-left-radius: 10px;
|
border-top-left-radius: 10px;
|
||||||
|
@ -1286,6 +1340,8 @@
|
||||||
|
|
||||||
.module-group-notification {
|
.module-group-notification {
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
|
margin-left: 1em;
|
||||||
|
margin-right: 1em;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
letter-spacing: 0.3px;
|
letter-spacing: 0.3px;
|
||||||
|
@ -2420,6 +2476,13 @@
|
||||||
background-color: $color-black-02;
|
background-color: $color-black-02;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-image__border-overlay--selected {
|
||||||
|
background-color: $color-black;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
animation: message--selected 1s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
.module-image__loading-placeholder {
|
.module-image__loading-placeholder {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -2999,9 +3062,16 @@
|
||||||
// In these --small and --mini sizes, we're exploding our @color-svg mixin so we don't
|
// In these --small and --mini sizes, we're exploding our @color-svg mixin so we don't
|
||||||
// have to duplicate our background colors for the dark/ios/size matrix.
|
// have to duplicate our background colors for the dark/ios/size matrix.
|
||||||
|
|
||||||
|
.module-spinner__container--small {
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
.module-spinner__circle--small {
|
.module-spinner__circle--small {
|
||||||
-webkit-mask: url('../images/spinner-track-24.svg') no-repeat center;
|
-webkit-mask: url('../images/spinner-track-24.svg') no-repeat center;
|
||||||
-webkit-mask-size: 100%;
|
-webkit-mask-size: 100%;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
}
|
}
|
||||||
.module-spinner__arc--small {
|
.module-spinner__arc--small {
|
||||||
-webkit-mask: url('../images/spinner-24.svg') no-repeat center;
|
-webkit-mask: url('../images/spinner-24.svg') no-repeat center;
|
||||||
|
@ -3023,6 +3093,8 @@
|
||||||
.module-spinner__arc--mini {
|
.module-spinner__arc--mini {
|
||||||
-webkit-mask: url('../images/spinner-24.svg') no-repeat center;
|
-webkit-mask: url('../images/spinner-24.svg') no-repeat center;
|
||||||
-webkit-mask-size: 100%;
|
-webkit-mask-size: 100%;
|
||||||
|
height: 14px;
|
||||||
|
width: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-spinner__circle--incoming {
|
.module-spinner__circle--incoming {
|
||||||
|
@ -3032,6 +3104,13 @@
|
||||||
background-color: $color-white;
|
background-color: $color-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-spinner__circle--on-background {
|
||||||
|
background-color: $color-gray-05;
|
||||||
|
}
|
||||||
|
.module-spinner__arc--on-background {
|
||||||
|
background-color: $color-gray-60;
|
||||||
|
}
|
||||||
|
|
||||||
// Module: Highlighted Message Body
|
// Module: Highlighted Message Body
|
||||||
|
|
||||||
.module-message-body__highlight {
|
.module-message-body__highlight {
|
||||||
|
@ -3306,10 +3385,31 @@
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Module: Timeline Loading Row
|
||||||
|
|
||||||
|
.module-timeline-loading-row {
|
||||||
|
height: 48px;
|
||||||
|
padding: 12px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: columns;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
color: $color-gray-75;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
color: $color-gray-25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Module: Timeline
|
// Module: Timeline
|
||||||
|
|
||||||
.module-timeline {
|
.module-timeline {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-timeline__message-container {
|
.module-timeline__message-container {
|
||||||
|
@ -4686,13 +4786,35 @@
|
||||||
|
|
||||||
.module-countdown {
|
.module-countdown {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-countdown__path {
|
// Note: the colors here should match the module-spinner's on-background colors
|
||||||
|
.module-countdown__front-path {
|
||||||
fill-opacity: 0;
|
fill-opacity: 0;
|
||||||
stroke: $color-white;
|
|
||||||
stroke-width: 2;
|
stroke-width: 2;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
stroke: $color-gray-60;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
stroke: $color-gray-25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-countdown__back-path {
|
||||||
|
fill-opacity: 0;
|
||||||
|
stroke-width: 2;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
stroke: $color-gray-05;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
stroke: $color-gray-75;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Module: CompositionInput
|
// Module: CompositionInput
|
||||||
|
@ -4913,6 +5035,70 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Module: Last Seen Indicator
|
||||||
|
|
||||||
|
.module-last-seen-indicator {
|
||||||
|
padding-top: 25px;
|
||||||
|
padding-bottom: 35px;
|
||||||
|
margin-left: 28px;
|
||||||
|
margin-right: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-last-seen-indicator__bar {
|
||||||
|
background-color: $color-light-60;
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-last-seen-indicator__text {
|
||||||
|
margin-top: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
color: $color-light-90;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module: Scroll Down Button
|
||||||
|
|
||||||
|
.module-scroll-down {
|
||||||
|
z-index: 100;
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-scroll-down__button {
|
||||||
|
height: 44px;
|
||||||
|
width: 44px;
|
||||||
|
border-radius: 22px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: $color-light-35;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0px 3px 5px 0px rgba(0, 0, 0, 0.2);
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $color-light-45;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-scroll-down__button--new-messages {
|
||||||
|
background-color: $color-signal-blue;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #1472bd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-scroll-down__icon {
|
||||||
|
@include color-svg('../images/down.svg', $color-white);
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
// Third-party module: react-contextmenu
|
// Third-party module: react-contextmenu
|
||||||
|
|
||||||
.react-contextmenu {
|
.react-contextmenu {
|
||||||
|
@ -5016,11 +5202,9 @@
|
||||||
|
|
||||||
// Spec: container < 438px
|
// Spec: container < 438px
|
||||||
.module-message--incoming {
|
.module-message--incoming {
|
||||||
margin-left: 0;
|
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
.module-message--outgoing {
|
.module-message--outgoing {
|
||||||
margin-right: 0;
|
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5051,11 +5235,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-message--incoming {
|
.module-message--incoming {
|
||||||
margin-left: 0;
|
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
.module-message--outgoing {
|
.module-message--outgoing {
|
||||||
margin-right: 0;
|
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1509,6 +1509,13 @@ body.dark-theme {
|
||||||
background-color: $color-gray-05;
|
background-color: $color-gray-05;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-spinner__circle--on-background {
|
||||||
|
background-color: $color-gray-75;
|
||||||
|
}
|
||||||
|
.module-spinner__arc--on-background {
|
||||||
|
background-color: $color-gray-25;
|
||||||
|
}
|
||||||
|
|
||||||
// Module: Caption Editor
|
// Module: Caption Editor
|
||||||
|
|
||||||
.module-caption-editor {
|
.module-caption-editor {
|
||||||
|
|
|
@ -487,9 +487,7 @@
|
||||||
<script type='text/javascript' src='../js/views/file_input_view.js' data-cover></script>
|
<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/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/contact_list_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/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/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/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/conversation_view.js' data-cover></script>
|
||||||
|
@ -497,19 +495,14 @@
|
||||||
<script type='text/javascript' src='../js/views/network_status_view.js'></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>
|
<script type='text/javascript' src='../js/views/confirmation_dialog_view.js' data-cover></script>
|
||||||
<script type='text/javascript' src='../js/views/identicon_svg_view.js' data-cover></script>
|
<script type='text/javascript' src='../js/views/identicon_svg_view.js' data-cover></script>
|
||||||
<script type='text/javascript' src='../js/views/last_seen_indicator_view.js' data-cover></script>
|
|
||||||
<script type='text/javascript' src='../js/views/scroll_down_button_view.js' data-cover></script>
|
|
||||||
<script type='text/javascript' src='../js/views/banner_view.js' data-cover></script>
|
<script type='text/javascript' src='../js/views/banner_view.js' data-cover></script>
|
||||||
<script type='text/javascript' src='../js/views/clear_data_view.js'></script>
|
<script type='text/javascript' src='../js/views/clear_data_view.js'></script>
|
||||||
|
|
||||||
<script type="text/javascript" src="metadata/SecretSessionCipher_test.js"></script>
|
<script type="text/javascript" src="metadata/SecretSessionCipher_test.js"></script>
|
||||||
|
|
||||||
<script type="text/javascript" src="views/whisper_view_test.js"></script>
|
<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/list_view_test.js"></script>
|
||||||
<script type="text/javascript" src="views/network_status_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/messages_test.js"></script>
|
<script type="text/javascript" src="models/messages_test.js"></script>
|
||||||
|
|
||||||
|
|
|
@ -34,17 +34,19 @@ describe('KeyChangeListener', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
after(async () => {
|
after(async () => {
|
||||||
await convo.destroyMessages();
|
await window.Signal.Data.removeAllMessagesInConversation(convo.id, {
|
||||||
|
MessageCollection: Whisper.MessageCollection,
|
||||||
|
});
|
||||||
await window.Signal.Data.saveConversation(convo.id);
|
await window.Signal.Data.saveConversation(convo.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('generates a key change notice in the private conversation with this contact', done => {
|
it('generates a key change notice in the private conversation with this contact', done => {
|
||||||
convo.once('newmessage', async () => {
|
const original = convo.addKeyChange;
|
||||||
await convo.fetchMessages();
|
convo.addKeyChange = keyChangedId => {
|
||||||
const message = convo.messageCollection.at(0);
|
assert.equal(address.getName(), keyChangedId);
|
||||||
assert.strictEqual(message.get('type'), 'keychange');
|
convo.addKeyChange = original;
|
||||||
done();
|
done();
|
||||||
});
|
};
|
||||||
store.saveIdentity(address.toString(), newKey);
|
store.saveIdentity(address.toString(), newKey);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -62,17 +64,20 @@ describe('KeyChangeListener', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
after(async () => {
|
after(async () => {
|
||||||
await convo.destroyMessages();
|
await window.Signal.Data.removeAllMessagesInConversation(convo.id, {
|
||||||
|
MessageCollection: Whisper.MessageCollection,
|
||||||
|
});
|
||||||
await window.Signal.Data.saveConversation(convo.id);
|
await window.Signal.Data.saveConversation(convo.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('generates a key change notice in the group conversation with this contact', done => {
|
it('generates a key change notice in the group conversation with this contact', done => {
|
||||||
convo.once('newmessage', async () => {
|
const original = convo.addKeyChange;
|
||||||
await convo.fetchMessages();
|
convo.addKeyChange = keyChangedId => {
|
||||||
const message = convo.messageCollection.at(0);
|
assert.equal(address.getName(), keyChangedId);
|
||||||
assert.strictEqual(message.get('type'), 'keychange');
|
convo.addKeyChange = original;
|
||||||
done();
|
done();
|
||||||
});
|
};
|
||||||
|
|
||||||
store.saveIdentity(address.toString(), newKey);
|
store.saveIdentity(address.toString(), newKey);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -72,22 +72,6 @@ describe('Conversation', () => {
|
||||||
assert.strictEqual(convo.contactCollection.at('2').get('name'), 'C');
|
assert.strictEqual(convo.contactCollection.at('2').get('name'), 'C');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('contains its own messages', async () => {
|
|
||||||
const convo = new Whisper.ConversationCollection().add({
|
|
||||||
id: '+18085555555',
|
|
||||||
});
|
|
||||||
await convo.fetchMessages();
|
|
||||||
assert.notEqual(convo.messageCollection.length, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('contains only its own messages', async () => {
|
|
||||||
const convo = new Whisper.ConversationCollection().add({
|
|
||||||
id: '+18085556666',
|
|
||||||
});
|
|
||||||
await convo.fetchMessages();
|
|
||||||
assert.strictEqual(convo.messageCollection.length, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adds conversation to message collection upon leaving group', async () => {
|
it('adds conversation to message collection upon leaving group', async () => {
|
||||||
const convo = new Whisper.ConversationCollection().add({
|
const convo = new Whisper.ConversationCollection().add({
|
||||||
type: 'group',
|
type: 'group',
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
/* global Whisper */
|
|
||||||
|
|
||||||
describe('LastSeenIndicatorView', () => {
|
|
||||||
it('renders provided count', () => {
|
|
||||||
const view = new Whisper.LastSeenIndicatorView({ count: 10 });
|
|
||||||
assert.equal(view.count, 10);
|
|
||||||
|
|
||||||
view.render();
|
|
||||||
assert.match(view.$el.html(), /10 Unread Messages/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders count of 1', () => {
|
|
||||||
const view = new Whisper.LastSeenIndicatorView({ count: 1 });
|
|
||||||
assert.equal(view.count, 1);
|
|
||||||
|
|
||||||
view.render();
|
|
||||||
assert.match(view.$el.html(), /1 Unread Message/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('increments count', () => {
|
|
||||||
const view = new Whisper.LastSeenIndicatorView({ count: 4 });
|
|
||||||
|
|
||||||
assert.equal(view.count, 4);
|
|
||||||
view.render();
|
|
||||||
assert.match(view.$el.html(), /4 Unread Messages/);
|
|
||||||
|
|
||||||
view.increment(3);
|
|
||||||
assert.equal(view.count, 7);
|
|
||||||
view.render();
|
|
||||||
assert.match(view.$el.html(), /7 Unread Messages/);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,35 +0,0 @@
|
||||||
/* global Whisper */
|
|
||||||
|
|
||||||
describe('ScrollDownButtonView', () => {
|
|
||||||
it('renders with count = 0', () => {
|
|
||||||
const view = new Whisper.ScrollDownButtonView();
|
|
||||||
view.render();
|
|
||||||
assert.equal(view.count, 0);
|
|
||||||
assert.match(view.$el.html(), /Scroll to bottom/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders with count = 1', () => {
|
|
||||||
const view = new Whisper.ScrollDownButtonView({ count: 1 });
|
|
||||||
view.render();
|
|
||||||
assert.equal(view.count, 1);
|
|
||||||
assert.match(view.$el.html(), /New message below/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders with count = 2', () => {
|
|
||||||
const view = new Whisper.ScrollDownButtonView({ count: 2 });
|
|
||||||
view.render();
|
|
||||||
assert.equal(view.count, 2);
|
|
||||||
|
|
||||||
assert.match(view.$el.html(), /New messages below/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('increments count and re-renders', () => {
|
|
||||||
const view = new Whisper.ScrollDownButtonView();
|
|
||||||
view.render();
|
|
||||||
assert.equal(view.count, 0);
|
|
||||||
assert.notMatch(view.$el.html(), /New message below/);
|
|
||||||
view.increment(1);
|
|
||||||
assert.equal(view.count, 1);
|
|
||||||
assert.match(view.$el.html(), /New message below/);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -152,7 +152,9 @@
|
||||||
conversationType={'direct'}
|
conversationType={'direct'}
|
||||||
unreadCount={4}
|
unreadCount={4}
|
||||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||||
isTyping={true}
|
typingContact={{
|
||||||
|
name: 'Someone Here',
|
||||||
|
}}
|
||||||
onClick={result => console.log('onClick', result)}
|
onClick={result => console.log('onClick', result)}
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
|
@ -164,7 +166,9 @@
|
||||||
conversationType={'direct'}
|
conversationType={'direct'}
|
||||||
unreadCount={4}
|
unreadCount={4}
|
||||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||||
isTyping={true}
|
typingContact={{
|
||||||
|
name: 'Someone Here',
|
||||||
|
}}
|
||||||
lastMessage={{
|
lastMessage={{
|
||||||
status: 'read',
|
status: 'read',
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -23,7 +23,7 @@ export type PropsData = {
|
||||||
unreadCount: number;
|
unreadCount: number;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
|
|
||||||
isTyping: boolean;
|
typingContact?: Object;
|
||||||
lastMessage?: {
|
lastMessage?: {
|
||||||
status: 'sending' | 'sent' | 'delivered' | 'read' | 'error';
|
status: 'sending' | 'sent' | 'delivered' | 'read' | 'error';
|
||||||
text: string;
|
text: string;
|
||||||
|
@ -134,8 +134,8 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderMessage() {
|
public renderMessage() {
|
||||||
const { lastMessage, isTyping, unreadCount, i18n } = this.props;
|
const { lastMessage, typingContact, unreadCount, i18n } = this.props;
|
||||||
if (!lastMessage && !isTyping) {
|
if (!lastMessage && !typingContact) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const text = lastMessage && lastMessage.text ? lastMessage.text : '';
|
const text = lastMessage && lastMessage.text ? lastMessage.text : '';
|
||||||
|
@ -150,15 +150,22 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
||||||
: null
|
: null
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isTyping ? (
|
{typingContact ? (
|
||||||
<TypingAnimation i18n={i18n} />
|
<TypingAnimation i18n={i18n} />
|
||||||
) : (
|
) : (
|
||||||
<MessageBody
|
<>
|
||||||
text={text}
|
{shouldShowDraft ? (
|
||||||
disableJumbomoji={true}
|
<span className="module-conversation-list-item__message__draft-prefix">
|
||||||
disableLinks={true}
|
{i18n('ConversationListItem--draft-prefix')}
|
||||||
i18n={i18n}
|
</span>
|
||||||
/>
|
) : null}
|
||||||
|
<MessageBody
|
||||||
|
text={text}
|
||||||
|
disableJumbomoji={true}
|
||||||
|
disableLinks={true}
|
||||||
|
i18n={i18n}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{lastMessage && lastMessage.status ? (
|
{lastMessage && lastMessage.status ? (
|
||||||
|
|
|
@ -73,16 +73,25 @@ export class Countdown extends React.Component<Props, State> {
|
||||||
const strokeDashoffset = ratio * CIRCUMFERENCE;
|
const strokeDashoffset = ratio * CIRCUMFERENCE;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg className="module-countdown" viewBox="0 0 24 24">
|
<div className="module-countdown">
|
||||||
<path
|
<svg viewBox="0 0 24 24">
|
||||||
d="M12,1 A11,11,0,1,1,1,12,11.013,11.013,0,0,1,12,1Z"
|
<path
|
||||||
className="module-countdown__path"
|
d="M12,1 A11,11,0,1,1,1,12,11.013,11.013,0,0,1,12,1Z"
|
||||||
style={{
|
className="module-countdown__back-path"
|
||||||
strokeDasharray: `${CIRCUMFERENCE}, ${CIRCUMFERENCE}`,
|
style={{
|
||||||
strokeDashoffset,
|
strokeDasharray: `${CIRCUMFERENCE}, ${CIRCUMFERENCE}`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</svg>
|
<path
|
||||||
|
d="M12,1 A11,11,0,1,1,1,12,11.013,11.013,0,0,1,12,1Z"
|
||||||
|
className="module-countdown__front-path"
|
||||||
|
style={{
|
||||||
|
strokeDasharray: `${CIRCUMFERENCE}, ${CIRCUMFERENCE}`,
|
||||||
|
strokeDashoffset,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,7 @@ import {
|
||||||
ConversationListItem,
|
ConversationListItem,
|
||||||
PropsData as ConversationListItemPropsType,
|
PropsData as ConversationListItemPropsType,
|
||||||
} from './ConversationListItem';
|
} from './ConversationListItem';
|
||||||
import {
|
import { MessageSearchResult } from './MessageSearchResult';
|
||||||
MessageSearchResult,
|
|
||||||
PropsData as MessageSearchResultPropsType,
|
|
||||||
} from './MessageSearchResult';
|
|
||||||
import { StartNewConversation } from './StartNewConversation';
|
import { StartNewConversation } from './StartNewConversation';
|
||||||
|
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
|
|
|
@ -4,7 +4,7 @@ import classNames from 'classnames';
|
||||||
interface Props {
|
interface Props {
|
||||||
size?: string;
|
size?: string;
|
||||||
svgSize: 'small' | 'normal';
|
svgSize: 'small' | 'normal';
|
||||||
direction?: string;
|
direction?: 'outgoing' | 'incoming' | 'on-background';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Spinner extends React.Component<Props> {
|
export class Spinner extends React.Component<Props> {
|
||||||
|
|
|
@ -23,7 +23,7 @@ const contact = {
|
||||||
signalAccount: '+12025550000',
|
signalAccount: '+12025550000',
|
||||||
};
|
};
|
||||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
|
@ -31,8 +31,8 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -41,8 +41,8 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
|
@ -51,8 +51,8 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -62,7 +62,7 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
</util.ConversationContext>;
|
</util.ConversationContext>;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ const contact = {
|
||||||
signalAccount: '+12025550000',
|
signalAccount: '+12025550000',
|
||||||
};
|
};
|
||||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
|
@ -97,8 +97,8 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -107,7 +107,7 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
</util.ConversationContext>;
|
</util.ConversationContext>;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -131,15 +131,15 @@ const contact = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}/>
|
contact={contact}/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -147,7 +147,7 @@ const contact = {
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}/>
|
contact={contact}/>
|
||||||
</li>
|
</div>
|
||||||
</util.ConversationContext>;
|
</util.ConversationContext>;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -171,8 +171,8 @@ const contact = {
|
||||||
},
|
},
|
||||||
signalAccount: '+12025550000',
|
signalAccount: '+12025550000',
|
||||||
};
|
};
|
||||||
<util.ConversationContext theme={util.theme} type="group" ios={util.ios}>
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
conversationType="group"
|
conversationType="group"
|
||||||
|
@ -183,8 +183,8 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
|
@ -195,8 +195,8 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -207,7 +207,7 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
</util.ConversationContext>;
|
</util.ConversationContext>;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -231,7 +231,7 @@ const contact = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
|
@ -239,8 +239,8 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -249,8 +249,8 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
|
@ -259,8 +259,8 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -270,7 +270,7 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
</util.ConversationContext>;
|
</util.ConversationContext>;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -292,7 +292,7 @@ const contact = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
|
@ -300,8 +300,8 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -310,8 +310,8 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
|
@ -320,8 +320,8 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -331,7 +331,7 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
</util.ConversationContext>;
|
</util.ConversationContext>;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -356,7 +356,7 @@ const contact = {
|
||||||
signalAccount: '+12025551000',
|
signalAccount: '+12025551000',
|
||||||
};
|
};
|
||||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
|
@ -364,8 +364,8 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -374,8 +374,8 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
|
@ -384,8 +384,8 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -395,7 +395,7 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
</util.ConversationContext>;
|
</util.ConversationContext>;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -414,7 +414,7 @@ const contact = {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
|
@ -422,8 +422,8 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -432,8 +432,8 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
|
@ -442,8 +442,8 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -453,7 +453,7 @@ const contact = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
</util.ConversationContext>;
|
</util.ConversationContext>;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -462,7 +462,7 @@ const contact = {
|
||||||
```jsx
|
```jsx
|
||||||
const contact = {};
|
const contact = {};
|
||||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
|
@ -470,8 +470,8 @@ const contact = {};
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -480,8 +480,8 @@ const contact = {};
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
|
@ -490,8 +490,8 @@ const contact = {};
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -501,7 +501,7 @@ const contact = {};
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
</util.ConversationContext>;
|
</util.ConversationContext>;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -542,7 +542,7 @@ const contactWithoutAccount = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
text="I want to introduce you to Someone..."
|
text="I want to introduce you to Someone..."
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
|
@ -551,8 +551,8 @@ const contactWithoutAccount = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contactWithAccount}
|
contact={contactWithAccount}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
text="I want to introduce you to Someone..."
|
text="I want to introduce you to Someone..."
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
|
@ -562,8 +562,8 @@ const contactWithoutAccount = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contactWithAccount}
|
contact={contactWithAccount}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
text="I want to introduce you to Someone..."
|
text="I want to introduce you to Someone..."
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
|
@ -572,8 +572,8 @@ const contactWithoutAccount = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contactWithAccount}
|
contact={contactWithAccount}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
text="I want to introduce you to Someone..."
|
text="I want to introduce you to Someone..."
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
|
@ -583,8 +583,8 @@ const contactWithoutAccount = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contactWithAccount}
|
contact={contactWithAccount}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
text="I want to introduce you to Someone..."
|
text="I want to introduce you to Someone..."
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
|
@ -594,8 +594,8 @@ const contactWithoutAccount = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contactWithoutAccount}
|
contact={contactWithoutAccount}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
text="I want to introduce you to Someone..."
|
text="I want to introduce you to Someone..."
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
|
@ -606,8 +606,8 @@ const contactWithoutAccount = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contactWithoutAccount}
|
contact={contactWithoutAccount}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
text="I want to introduce you to Someone..."
|
text="I want to introduce you to Someone..."
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
|
@ -617,8 +617,8 @@ const contactWithoutAccount = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contactWithoutAccount}
|
contact={contactWithoutAccount}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
text="I want to introduce you to Someone..."
|
text="I want to introduce you to Someone..."
|
||||||
authorColor="green"
|
authorColor="green"
|
||||||
|
@ -629,6 +629,6 @@ const contactWithoutAccount = {
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
contact={contactWithoutAccount}
|
contact={contactWithoutAccount}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
</util.ConversationContext>;
|
</util.ConversationContext>;
|
||||||
```
|
```
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="blue"
|
authorColor="blue"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -13,8 +13,8 @@
|
||||||
expirationLength={10 * 1000}
|
expirationLength={10 * 1000}
|
||||||
expirationTimestamp={Date.now() + 10 * 1000}
|
expirationTimestamp={Date.now() + 10 * 1000}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
status="delivered"
|
status="delivered"
|
||||||
|
@ -25,8 +25,8 @@
|
||||||
expirationLength={30 * 1000}
|
expirationLength={30 * 1000}
|
||||||
expirationTimestamp={Date.now() + 30 * 1000}
|
expirationTimestamp={Date.now() + 30 * 1000}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="blue"
|
authorColor="blue"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -37,8 +37,8 @@
|
||||||
expirationLength={60 * 1000}
|
expirationLength={60 * 1000}
|
||||||
expirationTimestamp={Date.now() + 55 * 1000}
|
expirationTimestamp={Date.now() + 55 * 1000}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="blue"
|
authorColor="blue"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
expirationLength={5 * 60 * 1000}
|
expirationLength={5 * 60 * 1000}
|
||||||
expirationTimestamp={Date.now() + 5 * 60 * 1000}
|
expirationTimestamp={Date.now() + 5 * 60 * 1000}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
</util.ConversationContext>
|
</util.ConversationContext>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="blue"
|
authorColor="blue"
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
|
@ -67,8 +67,8 @@
|
||||||
expirationLength={60 * 1000}
|
expirationLength={60 * 1000}
|
||||||
expirationTimestamp={Date.now() + 60 * 1000}
|
expirationTimestamp={Date.now() + 60 * 1000}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="blue"
|
authorColor="blue"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -79,8 +79,8 @@
|
||||||
expirationLength={60 * 1000}
|
expirationLength={60 * 1000}
|
||||||
expirationTimestamp={Date.now() + 60 * 1000}
|
expirationTimestamp={Date.now() + 60 * 1000}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="blue"
|
authorColor="blue"
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
|
@ -90,8 +90,8 @@
|
||||||
expirationLength={60 * 1000}
|
expirationLength={60 * 1000}
|
||||||
expirationTimestamp={Date.now() + 55 * 1000}
|
expirationTimestamp={Date.now() + 55 * 1000}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="blue"
|
authorColor="blue"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -102,8 +102,8 @@
|
||||||
expirationLength={60 * 1000}
|
expirationLength={60 * 1000}
|
||||||
expirationTimestamp={Date.now() + 55 * 1000}
|
expirationTimestamp={Date.now() + 55 * 1000}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="blue"
|
authorColor="blue"
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
|
@ -113,8 +113,8 @@
|
||||||
expirationLength={60 * 1000}
|
expirationLength={60 * 1000}
|
||||||
expirationTimestamp={Date.now() + 30 * 1000}
|
expirationTimestamp={Date.now() + 30 * 1000}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="blue"
|
authorColor="blue"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -125,8 +125,8 @@
|
||||||
expirationLength={60 * 1000}
|
expirationLength={60 * 1000}
|
||||||
expirationTimestamp={Date.now() + 30 * 1000}
|
expirationTimestamp={Date.now() + 30 * 1000}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="blue"
|
authorColor="blue"
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
|
@ -136,8 +136,8 @@
|
||||||
expirationLength={60 * 1000}
|
expirationLength={60 * 1000}
|
||||||
expirationTimestamp={Date.now() + 5 * 1000}
|
expirationTimestamp={Date.now() + 5 * 1000}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="blue"
|
authorColor="blue"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -148,8 +148,8 @@
|
||||||
expirationLength={60 * 1000}
|
expirationLength={60 * 1000}
|
||||||
expirationTimestamp={Date.now() + 5 * 1000}
|
expirationTimestamp={Date.now() + 5 * 1000}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="blue"
|
authorColor="blue"
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
|
@ -159,8 +159,8 @@
|
||||||
expirationLength={60 * 1000}
|
expirationLength={60 * 1000}
|
||||||
expirationTimestamp={Date.now()}
|
expirationTimestamp={Date.now()}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="blue"
|
authorColor="blue"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -171,8 +171,8 @@
|
||||||
expirationLength={60 * 1000}
|
expirationLength={60 * 1000}
|
||||||
expirationTimestamp={Date.now()}
|
expirationTimestamp={Date.now()}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="blue"
|
authorColor="blue"
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
|
@ -182,8 +182,8 @@
|
||||||
expirationLength={60 * 1000}
|
expirationLength={60 * 1000}
|
||||||
expirationTimestamp={Date.now() + 120 * 1000}
|
expirationTimestamp={Date.now() + 120 * 1000}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="blue"
|
authorColor="blue"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -194,8 +194,8 @@
|
||||||
expirationLength={60 * 1000}
|
expirationLength={60 * 1000}
|
||||||
expirationTimestamp={Date.now() + 120 * 1000}
|
expirationTimestamp={Date.now() + 120 * 1000}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="blue"
|
authorColor="blue"
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
|
@ -205,8 +205,8 @@
|
||||||
expirationLength={60 * 1000}
|
expirationLength={60 * 1000}
|
||||||
expirationTimestamp={Date.now() - 20 * 1000}
|
expirationTimestamp={Date.now() - 20 * 1000}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
authorColor="blue"
|
authorColor="blue"
|
||||||
direction="outgoing"
|
direction="outgoing"
|
||||||
|
@ -217,6 +217,6 @@
|
||||||
expirationLength={60 * 1000}
|
expirationLength={60 * 1000}
|
||||||
expirationTimestamp={Date.now() - 20 * 1000}
|
expirationTimestamp={Date.now() - 20 * 1000}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
</util.ConversationContext>
|
</util.ConversationContext>
|
||||||
```
|
```
|
||||||
|
|
|
@ -15,6 +15,7 @@ interface Props {
|
||||||
|
|
||||||
overlayText?: string;
|
overlayText?: string;
|
||||||
|
|
||||||
|
isSelected?: boolean;
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
noBackground?: boolean;
|
noBackground?: boolean;
|
||||||
bottomOverlay?: boolean;
|
bottomOverlay?: boolean;
|
||||||
|
@ -51,6 +52,7 @@ export class Image extends React.Component<Props> {
|
||||||
darkOverlay,
|
darkOverlay,
|
||||||
height,
|
height,
|
||||||
i18n,
|
i18n,
|
||||||
|
isSelected,
|
||||||
noBackground,
|
noBackground,
|
||||||
noBorder,
|
noBorder,
|
||||||
onClick,
|
onClick,
|
||||||
|
@ -118,7 +120,7 @@ export class Image extends React.Component<Props> {
|
||||||
alt={i18n('imageCaptionIconAlt')}
|
alt={i18n('imageCaptionIconAlt')}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{!noBorder ? (
|
{!noBorder || isSelected ? (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-image__border-overlay',
|
'module-image__border-overlay',
|
||||||
|
@ -128,7 +130,8 @@ export class Image extends React.Component<Props> {
|
||||||
curveBottomRight ? 'module-image--curved-bottom-right' : null,
|
curveBottomRight ? 'module-image--curved-bottom-right' : null,
|
||||||
smallCurveTopLeft ? 'module-image--small-curved-top-left' : null,
|
smallCurveTopLeft ? 'module-image--small-curved-top-left' : null,
|
||||||
softCorners ? 'module-image--soft-corners' : null,
|
softCorners ? 'module-image--soft-corners' : null,
|
||||||
darkOverlay ? 'module-image__border-overlay--dark' : null
|
darkOverlay ? 'module-image__border-overlay--dark' : null,
|
||||||
|
isSelected ? 'module-image__border-overlay--selected' : null
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -21,6 +21,7 @@ interface Props {
|
||||||
withContentBelow?: boolean;
|
withContentBelow?: boolean;
|
||||||
bottomOverlay?: boolean;
|
bottomOverlay?: boolean;
|
||||||
isSticker?: boolean;
|
isSticker?: boolean;
|
||||||
|
isSelected?: boolean;
|
||||||
stickerSize?: number;
|
stickerSize?: number;
|
||||||
|
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
@ -37,6 +38,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
bottomOverlay,
|
bottomOverlay,
|
||||||
i18n,
|
i18n,
|
||||||
isSticker,
|
isSticker,
|
||||||
|
isSelected,
|
||||||
stickerSize,
|
stickerSize,
|
||||||
onError,
|
onError,
|
||||||
onClick,
|
onClick,
|
||||||
|
@ -83,6 +85,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||||
curveBottomRight={curveBottomRight}
|
curveBottomRight={curveBottomRight}
|
||||||
attachment={attachments[0]}
|
attachment={attachments[0]}
|
||||||
playIconOverlay={isVideoAttachment(attachments[0])}
|
playIconOverlay={isVideoAttachment(attachments[0])}
|
||||||
|
isSelected={isSelected}
|
||||||
height={finalHeight}
|
height={finalHeight}
|
||||||
width={finalWidth}
|
width={finalWidth}
|
||||||
url={getUrl(attachments[0])}
|
url={getUrl(attachments[0])}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -40,6 +40,7 @@ interface Trigger {
|
||||||
// Same as MIN_WIDTH in ImageGrid.tsx
|
// Same as MIN_WIDTH in ImageGrid.tsx
|
||||||
const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200;
|
const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200;
|
||||||
const STICKER_SIZE = 128;
|
const STICKER_SIZE = 128;
|
||||||
|
const SELECTED_TIMEOUT = 1000;
|
||||||
|
|
||||||
interface LinkPreviewType {
|
interface LinkPreviewType {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -54,6 +55,8 @@ export type PropsData = {
|
||||||
text?: string;
|
text?: string;
|
||||||
textPending?: boolean;
|
textPending?: boolean;
|
||||||
isSticker: boolean;
|
isSticker: boolean;
|
||||||
|
isSelected: boolean;
|
||||||
|
isSelectedCounter: number;
|
||||||
direction: 'incoming' | 'outgoing';
|
direction: 'incoming' | 'outgoing';
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
status?: 'sending' | 'sent' | 'delivered' | 'read' | 'error';
|
status?: 'sending' | 'sent' | 'delivered' | 'read' | 'error';
|
||||||
|
@ -97,6 +100,8 @@ type PropsHousekeeping = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PropsActions = {
|
export type PropsActions = {
|
||||||
|
clearSelectedMessage: () => unknown;
|
||||||
|
|
||||||
replyToMessage: (id: string) => void;
|
replyToMessage: (id: string) => void;
|
||||||
retrySend: (id: string) => void;
|
retrySend: (id: string) => void;
|
||||||
deleteMessage: (id: string) => void;
|
deleteMessage: (id: string) => void;
|
||||||
|
@ -120,11 +125,10 @@ export type PropsActions = {
|
||||||
displayTapToViewMessage: (messageId: string) => unknown;
|
displayTapToViewMessage: (messageId: string) => unknown;
|
||||||
|
|
||||||
openLink: (url: string) => void;
|
openLink: (url: string) => void;
|
||||||
scrollToMessage: (
|
scrollToQuotedMessage: (
|
||||||
options: {
|
options: {
|
||||||
author: string;
|
author: string;
|
||||||
sentAt: number;
|
sentAt: number;
|
||||||
referencedMessageNotFound: boolean;
|
|
||||||
}
|
}
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
@ -135,6 +139,9 @@ interface State {
|
||||||
expiring: boolean;
|
expiring: boolean;
|
||||||
expired: boolean;
|
expired: boolean;
|
||||||
imageBroken: boolean;
|
imageBroken: boolean;
|
||||||
|
|
||||||
|
isSelected: boolean;
|
||||||
|
prevSelectedCounter: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EXPIRATION_CHECK_MINIMUM = 2000;
|
const EXPIRATION_CHECK_MINIMUM = 2000;
|
||||||
|
@ -148,6 +155,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
public menuTriggerRef: Trigger | undefined;
|
public menuTriggerRef: Trigger | undefined;
|
||||||
public expirationCheckInterval: any;
|
public expirationCheckInterval: any;
|
||||||
public expiredTimeout: any;
|
public expiredTimeout: any;
|
||||||
|
public selectedTimeout: any;
|
||||||
|
|
||||||
public constructor(props: Props) {
|
public constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -160,10 +168,30 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
expiring: false,
|
expiring: false,
|
||||||
expired: false,
|
expired: false,
|
||||||
imageBroken: false,
|
imageBroken: false,
|
||||||
|
|
||||||
|
isSelected: props.isSelected,
|
||||||
|
prevSelectedCounter: props.isSelectedCounter,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static getDerivedStateFromProps(props: Props, state: State): State {
|
||||||
|
if (
|
||||||
|
props.isSelected &&
|
||||||
|
props.isSelectedCounter !== state.prevSelectedCounter
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isSelected: props.isSelected,
|
||||||
|
prevSelectedCounter: props.isSelectedCounter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
|
this.startSelectedTimer();
|
||||||
|
|
||||||
const { expirationLength } = this.props;
|
const { expirationLength } = this.props;
|
||||||
if (!expirationLength) {
|
if (!expirationLength) {
|
||||||
return;
|
return;
|
||||||
|
@ -180,6 +208,9 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
|
if (this.selectedTimeout) {
|
||||||
|
clearInterval(this.selectedTimeout);
|
||||||
|
}
|
||||||
if (this.expirationCheckInterval) {
|
if (this.expirationCheckInterval) {
|
||||||
clearInterval(this.expirationCheckInterval);
|
clearInterval(this.expirationCheckInterval);
|
||||||
}
|
}
|
||||||
|
@ -189,9 +220,26 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate() {
|
public componentDidUpdate() {
|
||||||
|
this.startSelectedTimer();
|
||||||
|
|
||||||
this.checkExpired();
|
this.checkExpired();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public startSelectedTimer() {
|
||||||
|
const { isSelected } = this.state;
|
||||||
|
if (!isSelected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.selectedTimeout) {
|
||||||
|
this.selectedTimeout = setTimeout(() => {
|
||||||
|
this.selectedTimeout = undefined;
|
||||||
|
this.setState({ isSelected: false });
|
||||||
|
this.props.clearSelectedMessage();
|
||||||
|
}, SELECTED_TIMEOUT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public checkExpired() {
|
public checkExpired() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const { isExpired, expirationTimestamp, expirationLength } = this.props;
|
const { isExpired, expirationTimestamp, expirationLength } = this.props;
|
||||||
|
@ -379,7 +427,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
isSticker,
|
isSticker,
|
||||||
text,
|
text,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { imageBroken } = this.state;
|
const { imageBroken, isSelected } = this.state;
|
||||||
|
|
||||||
if (!attachments || !attachments[0]) {
|
if (!attachments || !attachments[0]) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -422,6 +470,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
withContentAbove={isSticker || withContentAbove}
|
withContentAbove={isSticker || withContentAbove}
|
||||||
withContentBelow={isSticker || withContentBelow}
|
withContentBelow={isSticker || withContentBelow}
|
||||||
isSticker={isSticker}
|
isSticker={isSticker}
|
||||||
|
isSelected={isSticker && isSelected}
|
||||||
stickerSize={STICKER_SIZE}
|
stickerSize={STICKER_SIZE}
|
||||||
bottomOverlay={bottomOverlay}
|
bottomOverlay={bottomOverlay}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
@ -622,7 +671,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
disableScroll,
|
disableScroll,
|
||||||
i18n,
|
i18n,
|
||||||
quote,
|
quote,
|
||||||
scrollToMessage,
|
scrollToQuotedMessage,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!quote) {
|
if (!quote) {
|
||||||
|
@ -633,15 +682,14 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
conversationType === 'group' && direction === 'incoming';
|
conversationType === 'group' && direction === 'incoming';
|
||||||
const quoteColor =
|
const quoteColor =
|
||||||
direction === 'incoming' ? authorColor : quote.authorColor;
|
direction === 'incoming' ? authorColor : quote.authorColor;
|
||||||
|
|
||||||
const { referencedMessageNotFound } = quote;
|
const { referencedMessageNotFound } = quote;
|
||||||
|
|
||||||
const clickHandler = disableScroll
|
const clickHandler = disableScroll
|
||||||
? undefined
|
? undefined
|
||||||
: () => {
|
: () => {
|
||||||
scrollToMessage({
|
scrollToQuotedMessage({
|
||||||
author: quote.authorId,
|
author: quote.authorId,
|
||||||
sentAt: quote.sentAt,
|
sentAt: quote.sentAt,
|
||||||
referencedMessageNotFound,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1195,12 +1243,24 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public renderSelectionHighlight() {
|
||||||
|
const { isSticker } = this.props;
|
||||||
|
const { isSelected } = this.state;
|
||||||
|
|
||||||
|
if (!isSelected || isSticker) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="module-message__container__selection" />;
|
||||||
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line cyclomatic-complexity
|
// tslint:disable-next-line cyclomatic-complexity
|
||||||
public render() {
|
public render() {
|
||||||
const {
|
const {
|
||||||
authorPhoneNumber,
|
authorPhoneNumber,
|
||||||
authorColor,
|
authorColor,
|
||||||
attachments,
|
attachments,
|
||||||
|
conversationType,
|
||||||
direction,
|
direction,
|
||||||
displayTapToViewMessage,
|
displayTapToViewMessage,
|
||||||
id,
|
id,
|
||||||
|
@ -1211,6 +1271,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
timestamp,
|
timestamp,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { expired, expiring, imageBroken } = this.state;
|
const { expired, expiring, imageBroken } = this.state;
|
||||||
|
|
||||||
const isAttachmentPending = this.isAttachmentPending();
|
const isAttachmentPending = this.isAttachmentPending();
|
||||||
const isButton = isTapToView && !isTapToViewExpired && !isAttachmentPending;
|
const isButton = isTapToView && !isTapToViewExpired && !isAttachmentPending;
|
||||||
|
|
||||||
|
@ -1236,7 +1297,8 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-message',
|
'module-message',
|
||||||
`module-message--${direction}`,
|
`module-message--${direction}`,
|
||||||
expiring ? 'module-message--expired' : null
|
expiring ? 'module-message--expired' : null,
|
||||||
|
conversationType === 'group' ? 'module-message--group' : null
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{this.renderError(direction === 'incoming')}
|
{this.renderError(direction === 'incoming')}
|
||||||
|
@ -1271,6 +1333,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
>
|
>
|
||||||
{this.renderAuthor()}
|
{this.renderAuthor()}
|
||||||
{this.renderContents()}
|
{this.renderContents()}
|
||||||
|
{this.renderSelectionHighlight()}
|
||||||
{this.renderAvatar()}
|
{this.renderAvatar()}
|
||||||
</div>
|
</div>
|
||||||
{this.renderError(direction === 'outgoing')}
|
{this.renderError(direction === 'outgoing')}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,4 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
// import classNames from 'classnames';
|
|
||||||
|
|
||||||
import { ContactName } from './ContactName';
|
import { ContactName } from './ContactName';
|
||||||
import { Intl } from '../Intl';
|
import { Intl } from '../Intl';
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
### None
|
### No new messages
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
<ScrollDownButton
|
<ScrollDownButton
|
||||||
count={0}
|
withNewMessages={false}
|
||||||
conversationId="id-1"
|
conversationId="id-1"
|
||||||
scrollDown={id => console.log('scrollDown', id)}
|
scrollDown={id => console.log('scrollDown', id)}
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
|
@ -11,28 +11,15 @@
|
||||||
</util.ConversationContext>
|
</util.ConversationContext>
|
||||||
```
|
```
|
||||||
|
|
||||||
### One
|
### With new messages
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
<ScrollDownButton
|
<ScrollDownButton
|
||||||
count={1}
|
withNewMessages={true}
|
||||||
conversationId="id-2"
|
conversationId="id-2"
|
||||||
scrollDown={id => console.log('scrollDown', id)}
|
scrollDown={id => console.log('scrollDown', id)}
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
</util.ConversationContext>
|
</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>
|
|
||||||
```
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import classNames from 'classnames';
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
count: number;
|
withNewMessages: boolean;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
|
|
||||||
scrollDown: (conversationId: string) => void;
|
scrollDown: (conversationId: string) => void;
|
||||||
|
@ -14,21 +14,17 @@ type Props = {
|
||||||
|
|
||||||
export class ScrollDownButton extends React.Component<Props> {
|
export class ScrollDownButton extends React.Component<Props> {
|
||||||
public render() {
|
public render() {
|
||||||
const { conversationId, count, i18n, scrollDown } = this.props;
|
const { conversationId, withNewMessages, i18n, scrollDown } = this.props;
|
||||||
|
const altText = withNewMessages
|
||||||
let altText = i18n('scrollDown');
|
? i18n('messagesBelow')
|
||||||
if (count > 1) {
|
: i18n('scrollDown');
|
||||||
altText = i18n('messagesBelow');
|
|
||||||
} else if (count === 1) {
|
|
||||||
altText = i18n('messageBelow');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="module-scroll-down">
|
<div className="module-scroll-down">
|
||||||
<button
|
<button
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-scroll-down__button',
|
'module-scroll-down__button',
|
||||||
count > 0 ? 'module-scroll-down__button--new-messages' : null
|
withNewMessages ? 'module-scroll-down__button--new-messages' : null
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
scrollDown(conversationId);
|
scrollDown(conversationId);
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
```javascript
|
## With oldest and newest
|
||||||
const itemLookup = {
|
|
||||||
|
```jsx
|
||||||
|
window.itemLookup = {
|
||||||
'id-1': {
|
'id-1': {
|
||||||
type: 'message',
|
type: 'message',
|
||||||
data: {
|
data: {
|
||||||
|
@ -15,12 +17,24 @@ const itemLookup = {
|
||||||
type: 'message',
|
type: 'message',
|
||||||
data: {
|
data: {
|
||||||
id: 'id-2',
|
id: 'id-2',
|
||||||
|
conversationType: 'group',
|
||||||
direction: 'incoming',
|
direction: 'incoming',
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
authorColor: 'green',
|
authorColor: 'green',
|
||||||
text: 'Hello there from the new world! http://somewhere.com',
|
text: 'Hello there from the new world! http://somewhere.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'id-2.5': {
|
||||||
|
type: 'unsupportedMessage',
|
||||||
|
data: {
|
||||||
|
id: 'id-2.5',
|
||||||
|
canProcessNow: false,
|
||||||
|
contact: {
|
||||||
|
phoneNumber: '(202) 555-1000',
|
||||||
|
profileName: 'Mr. Pig',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
'id-3': {
|
'id-3': {
|
||||||
type: 'message',
|
type: 'message',
|
||||||
data: {
|
data: {
|
||||||
|
@ -155,25 +169,186 @@ const itemLookup = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const actions = {
|
window.actions = {
|
||||||
|
// For messages
|
||||||
downloadAttachment: options => console.log('onDownload', options),
|
downloadAttachment: options => console.log('onDownload', options),
|
||||||
replyToitem: id => console.log('onReply', id),
|
replyToitem: id => console.log('onReply', id),
|
||||||
showMessageDetail: id => console.log('onShowDetail', id),
|
showMessageDetail: id => console.log('onShowDetail', id),
|
||||||
deleteMessage: id => console.log('onDelete', id),
|
deleteMessage: id => console.log('onDelete', id),
|
||||||
|
downloadNewVersion: () => console.log('downloadNewVersion'),
|
||||||
|
|
||||||
|
// For Timeline
|
||||||
|
clearChangedMessages: (...args) => console.log('clearChangedMessages', args),
|
||||||
|
setLoadCountdownStart: (...args) =>
|
||||||
|
console.log('setLoadCountdownStart', args),
|
||||||
|
|
||||||
|
loadAndScroll: (...args) => console.log('loadAndScroll', args),
|
||||||
|
loadOlderMessages: (...args) => console.log('loadOlderMessages', args),
|
||||||
|
loadNewerMessages: (...args) => console.log('loadNewerMessages', args),
|
||||||
|
loadNewestMessages: (...args) => console.log('loadNewestMessages', args),
|
||||||
|
markMessageRead: (...args) => console.log('markMessageRead', args),
|
||||||
};
|
};
|
||||||
|
|
||||||
const items = util._.keys(itemLookup);
|
const props = {
|
||||||
const renderItem = id => {
|
id: 'conversationId-1',
|
||||||
const item = itemLookup[id];
|
haveNewest: true,
|
||||||
|
haveOldest: true,
|
||||||
|
isLoadingMessages: false,
|
||||||
|
items: util._.keys(window.itemLookup),
|
||||||
|
messagesHaveChanged: false,
|
||||||
|
oldestUnreadIndex: null,
|
||||||
|
resetCounter: 0,
|
||||||
|
scrollToIndex: null,
|
||||||
|
scrollToIndexCounter: 0,
|
||||||
|
totalUnread: 0,
|
||||||
|
|
||||||
// Because we can't use ...item syntax
|
renderItem: id => (
|
||||||
return React.createElement(
|
<TimelineItem item={window.itemLookup[id]} i18n={util.i18n} {...actions} />
|
||||||
TimelineItem,
|
),
|
||||||
util._.merge({ item, i18n: util.i18n }, actions)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
<div style={{ height: '300px' }}>
|
<div style={{ height: '300px' }}>
|
||||||
<Timeline items={items} renderItem={renderItem} i18n={util.i18n} />
|
<Timeline {...props} {...window.actions} i18n={util.i18n} />
|
||||||
|
</div>;
|
||||||
|
```
|
||||||
|
|
||||||
|
## With last seen indicator
|
||||||
|
|
||||||
|
```
|
||||||
|
const props = {
|
||||||
|
id: 'conversationId-1',
|
||||||
|
haveNewest: true,
|
||||||
|
haveOldest: true,
|
||||||
|
isLoadingMessages: false,
|
||||||
|
items: util._.keys(window.itemLookup),
|
||||||
|
messagesHaveChanged: false,
|
||||||
|
oldestUnreadIndex: 2,
|
||||||
|
resetCounter: 0,
|
||||||
|
scrollToIndex: null,
|
||||||
|
scrollToIndexCounter: 0,
|
||||||
|
totalUnread: 2,
|
||||||
|
|
||||||
|
renderItem: id => (
|
||||||
|
<TimelineItem item={window.itemLookup[id]} i18n={util.i18n} {...actions} />
|
||||||
|
),
|
||||||
|
renderLastSeenIndicator: () => (
|
||||||
|
<LastSeenIndicator count={2} i18n={util.i18n} />
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
<div style={{ height: '300px' }}>
|
||||||
|
<Timeline {...props} {...window.actions} i18n={util.i18n} />
|
||||||
|
</div>;
|
||||||
|
```
|
||||||
|
|
||||||
|
## With target index = 0
|
||||||
|
|
||||||
|
```
|
||||||
|
const props = {
|
||||||
|
id: 'conversationId-1',
|
||||||
|
haveNewest: true,
|
||||||
|
haveOldest: true,
|
||||||
|
isLoadingMessages: false,
|
||||||
|
items: util._.keys(window.itemLookup),
|
||||||
|
messagesHaveChanged: false,
|
||||||
|
oldestUnreadIndex: null,
|
||||||
|
resetCounter: 0,
|
||||||
|
scrollToIndex: 0,
|
||||||
|
scrollToIndexCounter: 0,
|
||||||
|
totalUnread: 0,
|
||||||
|
|
||||||
|
renderItem: id => (
|
||||||
|
<TimelineItem item={window.itemLookup[id]} i18n={util.i18n} {...actions} />
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
<div style={{ height: '300px' }}>
|
||||||
|
<Timeline {...props} {...window.actions} i18n={util.i18n} />
|
||||||
|
</div>;
|
||||||
|
```
|
||||||
|
|
||||||
|
## With typing indicator
|
||||||
|
|
||||||
|
```
|
||||||
|
const props = {
|
||||||
|
id: 'conversationId-1',
|
||||||
|
haveNewest: true,
|
||||||
|
haveOldest: true,
|
||||||
|
isLoadingMessages: false,
|
||||||
|
items: util._.keys(window.itemLookup),
|
||||||
|
messagesHaveChanged: false,
|
||||||
|
oldestUnreadIndex: null,
|
||||||
|
resetCounter: 0,
|
||||||
|
scrollToIndex: null,
|
||||||
|
scrollToIndexCounter: 0,
|
||||||
|
totalUnread: 0,
|
||||||
|
|
||||||
|
typingContact: true,
|
||||||
|
|
||||||
|
renderItem: id => (
|
||||||
|
<TimelineItem item={window.itemLookup[id]} i18n={util.i18n} {...actions} />
|
||||||
|
),
|
||||||
|
renderTypingBubble: () => (
|
||||||
|
<TypingBubble color="red" conversationType="direct" phoneNumber="+18005552222" i18n={util.i18n} />
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
<div style={{ height: '300px' }}>
|
||||||
|
<Timeline {...props} {...window.actions} i18n={util.i18n} />
|
||||||
|
</div>;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Without newest message
|
||||||
|
|
||||||
|
```
|
||||||
|
const props = {
|
||||||
|
id: 'conversationId-1',
|
||||||
|
haveNewest: false,
|
||||||
|
haveOldest: true,
|
||||||
|
isLoadingMessages: false,
|
||||||
|
items: util._.keys(window.itemLookup),
|
||||||
|
messagesHaveChanged: false,
|
||||||
|
oldestUnreadIndex: null,
|
||||||
|
resetCounter: 0,
|
||||||
|
scrollToIndex: 3,
|
||||||
|
scrollToIndexCounter: 0,
|
||||||
|
totalUnread: 0,
|
||||||
|
|
||||||
|
renderItem: id => (
|
||||||
|
<TimelineItem item={window.itemLookup[id]} i18n={util.i18n} {...actions} />
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
<div style={{ height: '300px' }}>
|
||||||
|
<Timeline {...props} {...window.actions} i18n={util.i18n} />
|
||||||
|
</div>;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Without oldest message
|
||||||
|
|
||||||
|
```
|
||||||
|
const props = {
|
||||||
|
id: 'conversationId-1',
|
||||||
|
haveNewest: true,
|
||||||
|
haveOldest: false,
|
||||||
|
isLoadingMessages: false,
|
||||||
|
items: util._.keys(window.itemLookup),
|
||||||
|
messagesHaveChanged: false,
|
||||||
|
oldestUnreadIndex: null,
|
||||||
|
resetCounter: 0,
|
||||||
|
scrollToIndex: null,
|
||||||
|
scrollToIndexCounter: 0,
|
||||||
|
totalUnread: 0,
|
||||||
|
|
||||||
|
renderItem: id => (
|
||||||
|
<TimelineItem item={window.itemLookup[id]} i18n={util.i18n} {...actions} />
|
||||||
|
),
|
||||||
|
renderLoadingRow: () => (
|
||||||
|
<TimelineLoadingRow state="idle" />
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
<div style={{ height: '300px' }}>
|
||||||
|
<Timeline {...props} {...window.actions} i18n={util.i18n} />
|
||||||
</div>;
|
</div>;
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { debounce, isNumber } from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
AutoSizer,
|
AutoSizer,
|
||||||
|
@ -6,24 +7,64 @@ import {
|
||||||
List,
|
List,
|
||||||
} from 'react-virtualized';
|
} from 'react-virtualized';
|
||||||
|
|
||||||
|
import { ScrollDownButton } from './ScrollDownButton';
|
||||||
|
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
import { PropsActions as MessageActionsType } from './Message';
|
import { PropsActions as MessageActionsType } from './Message';
|
||||||
import { PropsActions as SafetyNumberActionsType } from './SafetyNumberNotification';
|
import { PropsActions as SafetyNumberActionsType } from './SafetyNumberNotification';
|
||||||
|
|
||||||
type PropsData = {
|
const AT_BOTTOM_THRESHOLD = 1;
|
||||||
|
const NEAR_BOTTOM_THRESHOLD = 15;
|
||||||
|
const AT_TOP_THRESHOLD = 10;
|
||||||
|
const LOAD_MORE_THRESHOLD = 30;
|
||||||
|
const SCROLL_DOWN_BUTTON_THRESHOLD = 8;
|
||||||
|
export const LOAD_COUNTDOWN = 2 * 1000;
|
||||||
|
|
||||||
|
export type PropsDataType = {
|
||||||
|
haveNewest: boolean;
|
||||||
|
haveOldest: boolean;
|
||||||
|
isLoadingMessages: boolean;
|
||||||
items: Array<string>;
|
items: Array<string>;
|
||||||
|
loadCountdownStart?: number;
|
||||||
renderItem: (id: string) => JSX.Element;
|
messageHeightChanges: boolean;
|
||||||
|
oldestUnreadIndex?: number;
|
||||||
|
resetCounter: number;
|
||||||
|
scrollToIndex?: number;
|
||||||
|
scrollToIndexCounter: number;
|
||||||
|
totalUnread: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PropsHousekeeping = {
|
type PropsHousekeepingType = {
|
||||||
|
id: string;
|
||||||
|
unreadCount?: number;
|
||||||
|
typingContact?: Object;
|
||||||
|
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
|
||||||
|
renderItem: (id: string, actions: Object) => JSX.Element;
|
||||||
|
renderLastSeenIndicator: (id: string) => JSX.Element;
|
||||||
|
renderLoadingRow: (id: string) => JSX.Element;
|
||||||
|
renderTypingBubble: (id: string) => JSX.Element;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PropsActions = MessageActionsType & SafetyNumberActionsType;
|
type PropsActionsType = {
|
||||||
|
clearChangedMessages: (conversationId: string) => unknown;
|
||||||
|
setLoadCountdownStart: (
|
||||||
|
conversationId: string,
|
||||||
|
loadCountdownStart?: number
|
||||||
|
) => unknown;
|
||||||
|
setIsNearBottom: (conversationId: string, isNearBottom: boolean) => unknown;
|
||||||
|
|
||||||
type Props = PropsData & PropsHousekeeping & PropsActions;
|
loadAndScroll: (messageId: string) => unknown;
|
||||||
|
loadOlderMessages: (messageId: string) => unknown;
|
||||||
|
loadNewerMessages: (messageId: string) => unknown;
|
||||||
|
loadNewestMessages: (messageId: string) => unknown;
|
||||||
|
markMessageRead: (messageId: string) => unknown;
|
||||||
|
} & MessageActionsType &
|
||||||
|
SafetyNumberActionsType;
|
||||||
|
|
||||||
|
type Props = PropsDataType & PropsHousekeepingType & PropsActionsType;
|
||||||
|
|
||||||
// from https://github.com/bvaughn/react-virtualized/blob/fb3484ed5dcc41bffae8eab029126c0fb8f7abc0/source/List/types.js#L5
|
// from https://github.com/bvaughn/react-virtualized/blob/fb3484ed5dcc41bffae8eab029126c0fb8f7abc0/source/List/types.js#L5
|
||||||
type RowRendererParamsType = {
|
type RowRendererParamsType = {
|
||||||
|
@ -34,37 +75,407 @@ type RowRendererParamsType = {
|
||||||
parent: Object;
|
parent: Object;
|
||||||
style: Object;
|
style: Object;
|
||||||
};
|
};
|
||||||
|
type OnScrollParamsType = {
|
||||||
|
scrollTop: number;
|
||||||
|
clientHeight: number;
|
||||||
|
scrollHeight: number;
|
||||||
|
|
||||||
export class Timeline extends React.PureComponent<Props> {
|
clientWidth: number;
|
||||||
|
scrollWidth?: number;
|
||||||
|
scrollLeft?: number;
|
||||||
|
scrollToColumn?: number;
|
||||||
|
_hasScrolledToColumnTarget?: boolean;
|
||||||
|
scrollToRow?: number;
|
||||||
|
_hasScrolledToRowTarget?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type VisibleRowsType = {
|
||||||
|
newest?: {
|
||||||
|
id: string;
|
||||||
|
offsetTop: number;
|
||||||
|
row: number;
|
||||||
|
};
|
||||||
|
oldest?: {
|
||||||
|
id: string;
|
||||||
|
offsetTop: number;
|
||||||
|
row: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
atBottom: boolean;
|
||||||
|
atTop: boolean;
|
||||||
|
oneTimeScrollRow?: number;
|
||||||
|
|
||||||
|
prevPropScrollToIndex?: number;
|
||||||
|
prevPropScrollToIndexCounter?: number;
|
||||||
|
propScrollToIndex?: number;
|
||||||
|
|
||||||
|
shouldShowScrollDownButton: boolean;
|
||||||
|
areUnreadBelowCurrentPosition: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Timeline extends React.PureComponent<Props, State> {
|
||||||
public cellSizeCache = new CellMeasurerCache({
|
public cellSizeCache = new CellMeasurerCache({
|
||||||
defaultHeight: 85,
|
defaultHeight: 64,
|
||||||
fixedWidth: true,
|
fixedWidth: true,
|
||||||
});
|
});
|
||||||
public mostRecentWidth = 0;
|
public mostRecentWidth = 0;
|
||||||
|
public mostRecentHeight = 0;
|
||||||
|
public offsetFromBottom: number | undefined = 0;
|
||||||
public resizeAllFlag = false;
|
public resizeAllFlag = false;
|
||||||
public listRef = React.createRef<any>();
|
public listRef = React.createRef<any>();
|
||||||
|
public visibleRows: VisibleRowsType | undefined;
|
||||||
|
public loadCountdownTimeout: any;
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: Props) {
|
constructor(props: Props) {
|
||||||
if (this.resizeAllFlag) {
|
super(props);
|
||||||
this.resizeAllFlag = false;
|
|
||||||
this.cellSizeCache.clearAll();
|
const { scrollToIndex } = this.props;
|
||||||
this.recomputeRowHeights();
|
const oneTimeScrollRow = this.getLastSeenIndicatorRow();
|
||||||
} else if (this.props.items !== prevProps.items) {
|
|
||||||
const index = prevProps.items.length;
|
this.state = {
|
||||||
this.cellSizeCache.clear(index, 0);
|
atBottom: true,
|
||||||
this.recomputeRowHeights(index);
|
atTop: false,
|
||||||
}
|
oneTimeScrollRow,
|
||||||
|
propScrollToIndex: scrollToIndex,
|
||||||
|
prevPropScrollToIndex: scrollToIndex,
|
||||||
|
shouldShowScrollDownButton: false,
|
||||||
|
areUnreadBelowCurrentPosition: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public resizeAll = () => {
|
public static getDerivedStateFromProps(props: Props, state: State): State {
|
||||||
this.resizeAllFlag = false;
|
if (
|
||||||
this.cellSizeCache.clearAll();
|
isNumber(props.scrollToIndex) &&
|
||||||
|
(props.scrollToIndex !== state.prevPropScrollToIndex ||
|
||||||
|
props.scrollToIndexCounter !== state.prevPropScrollToIndexCounter)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
propScrollToIndex: props.scrollToIndex,
|
||||||
|
prevPropScrollToIndex: props.scrollToIndex,
|
||||||
|
prevPropScrollToIndexCounter: props.scrollToIndexCounter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getList = () => {
|
||||||
|
if (!this.listRef) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { current } = this.listRef;
|
||||||
|
|
||||||
|
return current;
|
||||||
};
|
};
|
||||||
|
|
||||||
public recomputeRowHeights = (index?: number) => {
|
public getGrid = () => {
|
||||||
if (this.listRef && this.listRef) {
|
const list = this.getList();
|
||||||
this.listRef.current.recomputeRowHeights(index);
|
if (!list) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return list.Grid;
|
||||||
|
};
|
||||||
|
|
||||||
|
public getScrollContainer = () => {
|
||||||
|
const grid = this.getGrid();
|
||||||
|
if (!grid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return grid._scrollingContainer as HTMLDivElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
public scrollToRow = (row: number) => {
|
||||||
|
const list = this.getList();
|
||||||
|
if (!list) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.scrollToRow(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
public recomputeRowHeights = (row?: number) => {
|
||||||
|
const list = this.getList();
|
||||||
|
if (!list) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.recomputeRowHeights(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
public onHeightOnlyChange = () => {
|
||||||
|
const grid = this.getGrid();
|
||||||
|
const scrollContainer = this.getScrollContainer();
|
||||||
|
if (!grid || !scrollContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNumber(this.offsetFromBottom)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { clientHeight, scrollHeight, scrollTop } = scrollContainer;
|
||||||
|
const newOffsetFromBottom = Math.max(
|
||||||
|
0,
|
||||||
|
scrollHeight - clientHeight - scrollTop
|
||||||
|
);
|
||||||
|
const delta = newOffsetFromBottom - this.offsetFromBottom;
|
||||||
|
|
||||||
|
grid.scrollToPosition({ scrollTop: scrollContainer.scrollTop + delta });
|
||||||
|
};
|
||||||
|
|
||||||
|
public resizeAll = () => {
|
||||||
|
this.offsetFromBottom = undefined;
|
||||||
|
this.resizeAllFlag = false;
|
||||||
|
this.cellSizeCache.clearAll();
|
||||||
|
|
||||||
|
const rowCount = this.getRowCount();
|
||||||
|
this.recomputeRowHeights(rowCount - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
public onScroll = (data: OnScrollParamsType) => {
|
||||||
|
// Ignore scroll events generated as react-virtualized recursively scrolls and
|
||||||
|
// re-measures to get us where we want to go.
|
||||||
|
if (
|
||||||
|
isNumber(data.scrollToRow) &&
|
||||||
|
data.scrollToRow >= 0 &&
|
||||||
|
!data._hasScrolledToRowTarget
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sometimes react-virtualized ends up with some incorrect math - we've scrolled below
|
||||||
|
// what should be possible. In this case, we leave everything the same and ask
|
||||||
|
// react-virtualized to try again. Without this, we'll set atBottom to true and
|
||||||
|
// pop the user back down to the bottom.
|
||||||
|
const { clientHeight, scrollHeight, scrollTop } = data;
|
||||||
|
if (scrollTop + clientHeight > scrollHeight) {
|
||||||
|
this.resizeAll();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateScrollMetrics(data);
|
||||||
|
this.updateWithVisibleRows();
|
||||||
|
};
|
||||||
|
|
||||||
|
// tslint:disable-next-line member-ordering
|
||||||
|
public updateScrollMetrics = debounce(
|
||||||
|
(data: OnScrollParamsType) => {
|
||||||
|
const { clientHeight, clientWidth, scrollHeight, scrollTop } = data;
|
||||||
|
|
||||||
|
if (clientHeight <= 0 || scrollHeight <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
haveNewest,
|
||||||
|
haveOldest,
|
||||||
|
id,
|
||||||
|
setIsNearBottom,
|
||||||
|
setLoadCountdownStart,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.mostRecentHeight &&
|
||||||
|
clientHeight !== this.mostRecentHeight &&
|
||||||
|
this.mostRecentWidth &&
|
||||||
|
clientWidth === this.mostRecentWidth
|
||||||
|
) {
|
||||||
|
this.onHeightOnlyChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've scrolled, we want to reset these
|
||||||
|
const oneTimeScrollRow = undefined;
|
||||||
|
const propScrollToIndex = undefined;
|
||||||
|
|
||||||
|
this.offsetFromBottom = Math.max(
|
||||||
|
0,
|
||||||
|
scrollHeight - clientHeight - scrollTop
|
||||||
|
);
|
||||||
|
|
||||||
|
const atBottom =
|
||||||
|
haveNewest && this.offsetFromBottom <= AT_BOTTOM_THRESHOLD;
|
||||||
|
const isNearBottom =
|
||||||
|
haveNewest && this.offsetFromBottom <= NEAR_BOTTOM_THRESHOLD;
|
||||||
|
const atTop = scrollTop <= AT_TOP_THRESHOLD;
|
||||||
|
const loadCountdownStart = atTop && !haveOldest ? Date.now() : undefined;
|
||||||
|
|
||||||
|
if (this.loadCountdownTimeout) {
|
||||||
|
clearTimeout(this.loadCountdownTimeout);
|
||||||
|
this.loadCountdownTimeout = null;
|
||||||
|
}
|
||||||
|
if (isNumber(loadCountdownStart)) {
|
||||||
|
this.loadCountdownTimeout = setTimeout(
|
||||||
|
this.loadOlderMessages,
|
||||||
|
LOAD_COUNTDOWN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadCountdownStart !== this.props.loadCountdownStart) {
|
||||||
|
setLoadCountdownStart(id, loadCountdownStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsNearBottom(id, isNearBottom);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
atBottom,
|
||||||
|
atTop,
|
||||||
|
oneTimeScrollRow,
|
||||||
|
propScrollToIndex,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
50,
|
||||||
|
{ maxWait: 50 }
|
||||||
|
);
|
||||||
|
|
||||||
|
public updateVisibleRows = () => {
|
||||||
|
let newest;
|
||||||
|
let oldest;
|
||||||
|
|
||||||
|
const scrollContainer = this.getScrollContainer();
|
||||||
|
if (!scrollContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollContainer.clientHeight === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleTop = scrollContainer.scrollTop;
|
||||||
|
const visibleBottom = visibleTop + scrollContainer.clientHeight;
|
||||||
|
|
||||||
|
const innerScrollContainer = scrollContainer.children[0];
|
||||||
|
if (!innerScrollContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { children } = innerScrollContainer;
|
||||||
|
|
||||||
|
for (let i = children.length - 1; i >= 0; i -= 1) {
|
||||||
|
const child = children[i] as HTMLDivElement;
|
||||||
|
const { id, offsetTop, offsetHeight } = child;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bottom = offsetTop + offsetHeight;
|
||||||
|
|
||||||
|
if (bottom - AT_BOTTOM_THRESHOLD <= visibleBottom) {
|
||||||
|
const row = parseInt(child.getAttribute('data-row') || '-1', 10);
|
||||||
|
newest = { offsetTop, row, id };
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const max = children.length;
|
||||||
|
for (let i = 0; i < max; i += 1) {
|
||||||
|
const child = children[i] as HTMLDivElement;
|
||||||
|
const { offsetTop, id } = child;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offsetTop + AT_TOP_THRESHOLD >= visibleTop) {
|
||||||
|
const row = parseInt(child.getAttribute('data-row') || '-1', 10);
|
||||||
|
oldest = { offsetTop, row, id };
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.visibleRows = { newest, oldest };
|
||||||
|
};
|
||||||
|
|
||||||
|
// tslint:disable-next-line member-ordering cyclomatic-complexity
|
||||||
|
public updateWithVisibleRows = debounce(
|
||||||
|
() => {
|
||||||
|
const {
|
||||||
|
unreadCount,
|
||||||
|
haveNewest,
|
||||||
|
isLoadingMessages,
|
||||||
|
items,
|
||||||
|
loadNewerMessages,
|
||||||
|
markMessageRead,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (!items || items.length < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateVisibleRows();
|
||||||
|
if (!this.visibleRows) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { newest } = this.visibleRows;
|
||||||
|
if (!newest || !newest.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
markMessageRead(newest.id);
|
||||||
|
|
||||||
|
const rowCount = this.getRowCount();
|
||||||
|
|
||||||
|
const lastId = items[items.length - 1];
|
||||||
|
if (
|
||||||
|
!isLoadingMessages &&
|
||||||
|
!haveNewest &&
|
||||||
|
newest.row > rowCount - LOAD_MORE_THRESHOLD
|
||||||
|
) {
|
||||||
|
loadNewerMessages(lastId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastIndex = items.length - 1;
|
||||||
|
const lastItemRow = this.fromItemIndexToRow(lastIndex);
|
||||||
|
const areUnreadBelowCurrentPosition = Boolean(
|
||||||
|
isNumber(unreadCount) &&
|
||||||
|
unreadCount > 0 &&
|
||||||
|
(!haveNewest || newest.row < lastItemRow)
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldShowScrollDownButton = Boolean(
|
||||||
|
!haveNewest ||
|
||||||
|
areUnreadBelowCurrentPosition ||
|
||||||
|
newest.row < rowCount - SCROLL_DOWN_BUTTON_THRESHOLD
|
||||||
|
);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
shouldShowScrollDownButton,
|
||||||
|
areUnreadBelowCurrentPosition,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
500,
|
||||||
|
{ maxWait: 500 }
|
||||||
|
);
|
||||||
|
|
||||||
|
public loadOlderMessages = () => {
|
||||||
|
const {
|
||||||
|
haveOldest,
|
||||||
|
isLoadingMessages,
|
||||||
|
items,
|
||||||
|
loadOlderMessages,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (this.loadCountdownTimeout) {
|
||||||
|
clearTimeout(this.loadCountdownTimeout);
|
||||||
|
this.loadCountdownTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoadingMessages || haveOldest || !items || items.length < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldestId = items[0];
|
||||||
|
loadOlderMessages(oldestId);
|
||||||
};
|
};
|
||||||
|
|
||||||
public rowRenderer = ({
|
public rowRenderer = ({
|
||||||
|
@ -73,8 +484,62 @@ export class Timeline extends React.PureComponent<Props> {
|
||||||
parent,
|
parent,
|
||||||
style,
|
style,
|
||||||
}: RowRendererParamsType) => {
|
}: RowRendererParamsType) => {
|
||||||
const { items, renderItem } = this.props;
|
const {
|
||||||
const messageId = items[index];
|
id,
|
||||||
|
haveOldest,
|
||||||
|
items,
|
||||||
|
renderItem,
|
||||||
|
renderLoadingRow,
|
||||||
|
renderLastSeenIndicator,
|
||||||
|
renderTypingBubble,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const row = index;
|
||||||
|
const oldestUnreadRow = this.getLastSeenIndicatorRow();
|
||||||
|
const typingBubbleRow = this.getTypingBubbleRow();
|
||||||
|
let rowContents;
|
||||||
|
|
||||||
|
if (!haveOldest && row === 0) {
|
||||||
|
rowContents = (
|
||||||
|
<div data-row={row} style={style}>
|
||||||
|
{renderLoadingRow(id)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (oldestUnreadRow === row) {
|
||||||
|
rowContents = (
|
||||||
|
<div data-row={row} style={style}>
|
||||||
|
{renderLastSeenIndicator(id)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (typingBubbleRow === row) {
|
||||||
|
rowContents = (
|
||||||
|
<div
|
||||||
|
data-row={row}
|
||||||
|
className="module-timeline__message-container"
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{renderTypingBubble(id)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const itemIndex = this.fromRowToItemIndex(row);
|
||||||
|
if (typeof itemIndex !== 'number') {
|
||||||
|
throw new Error(
|
||||||
|
`Attempted to render item with undefined index - row ${row}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const messageId = items[itemIndex];
|
||||||
|
rowContents = (
|
||||||
|
<div
|
||||||
|
id={messageId}
|
||||||
|
data-row={row}
|
||||||
|
className="module-timeline__message-container"
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{renderItem(messageId, this.props)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CellMeasurer
|
<CellMeasurer
|
||||||
|
@ -85,16 +550,277 @@ export class Timeline extends React.PureComponent<Props> {
|
||||||
rowIndex={index}
|
rowIndex={index}
|
||||||
width={this.mostRecentWidth}
|
width={this.mostRecentWidth}
|
||||||
>
|
>
|
||||||
<div className="module-timeline__message-container" style={style}>
|
{rowContents}
|
||||||
{renderItem(messageId)}
|
|
||||||
</div>
|
|
||||||
</CellMeasurer>
|
</CellMeasurer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
public fromItemIndexToRow(index: number) {
|
||||||
|
const { haveOldest, oldestUnreadIndex } = this.props;
|
||||||
|
|
||||||
|
let addition = 0;
|
||||||
|
|
||||||
|
if (!haveOldest) {
|
||||||
|
addition += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNumber(oldestUnreadIndex) && index >= oldestUnreadIndex) {
|
||||||
|
addition += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return index + addition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRowCount() {
|
||||||
|
const { haveOldest, oldestUnreadIndex, typingContact } = this.props;
|
||||||
const { items } = this.props;
|
const { items } = this.props;
|
||||||
|
|
||||||
|
if (!items || items.length < 1) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let extraRows = 0;
|
||||||
|
|
||||||
|
if (!haveOldest) {
|
||||||
|
extraRows += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNumber(oldestUnreadIndex)) {
|
||||||
|
extraRows += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typingContact) {
|
||||||
|
extraRows += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.length + extraRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public fromRowToItemIndex(row: number): number | undefined {
|
||||||
|
const { haveOldest, items } = this.props;
|
||||||
|
|
||||||
|
let subtraction = 0;
|
||||||
|
|
||||||
|
if (!haveOldest) {
|
||||||
|
subtraction += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldestUnreadRow = this.getLastSeenIndicatorRow();
|
||||||
|
if (isNumber(oldestUnreadRow) && row > oldestUnreadRow) {
|
||||||
|
subtraction += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = row - subtraction;
|
||||||
|
if (index < 0 || index >= items.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLastSeenIndicatorRow() {
|
||||||
|
const { oldestUnreadIndex } = this.props;
|
||||||
|
if (!isNumber(oldestUnreadIndex)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.fromItemIndexToRow(oldestUnreadIndex) - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTypingBubbleRow() {
|
||||||
|
const { items } = this.props;
|
||||||
|
if (!items || items.length < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const last = items.length - 1;
|
||||||
|
|
||||||
|
return this.fromItemIndexToRow(last) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public onScrollToMessage = (messageId: string) => {
|
||||||
|
const { isLoadingMessages, items, loadAndScroll } = this.props;
|
||||||
|
const index = items.findIndex(item => item === messageId);
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
const row = this.fromItemIndexToRow(index);
|
||||||
|
this.setState({
|
||||||
|
oneTimeScrollRow: row,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoadingMessages) {
|
||||||
|
loadAndScroll(messageId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public scrollToBottom = () => {
|
||||||
|
this.setState({
|
||||||
|
propScrollToIndex: undefined,
|
||||||
|
oneTimeScrollRow: undefined,
|
||||||
|
atBottom: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public onClickScrollDownButton = () => {
|
||||||
|
const {
|
||||||
|
haveNewest,
|
||||||
|
isLoadingMessages,
|
||||||
|
items,
|
||||||
|
loadNewestMessages,
|
||||||
|
} = this.props;
|
||||||
|
const lastId = items[items.length - 1];
|
||||||
|
const lastSeenIndicatorRow = this.getLastSeenIndicatorRow();
|
||||||
|
|
||||||
|
if (!this.visibleRows) {
|
||||||
|
if (haveNewest) {
|
||||||
|
this.scrollToBottom();
|
||||||
|
} else if (!isLoadingMessages) {
|
||||||
|
loadNewestMessages(lastId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { newest } = this.visibleRows;
|
||||||
|
|
||||||
|
if (
|
||||||
|
newest &&
|
||||||
|
isNumber(lastSeenIndicatorRow) &&
|
||||||
|
newest.row < lastSeenIndicatorRow
|
||||||
|
) {
|
||||||
|
this.setState({
|
||||||
|
oneTimeScrollRow: lastSeenIndicatorRow,
|
||||||
|
});
|
||||||
|
} else if (haveNewest) {
|
||||||
|
this.scrollToBottom();
|
||||||
|
} else if (!isLoadingMessages) {
|
||||||
|
loadNewestMessages(lastId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public componentDidUpdate(prevProps: Props) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
clearChangedMessages,
|
||||||
|
items,
|
||||||
|
messageHeightChanges,
|
||||||
|
oldestUnreadIndex,
|
||||||
|
resetCounter,
|
||||||
|
scrollToIndex,
|
||||||
|
typingContact,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
// There are a number of situations which can necessitate that we drop our row height
|
||||||
|
// cache and start over. It can cause the scroll position to do weird things, so we
|
||||||
|
// try to minimize those situations. In some cases we could reset a smaller set
|
||||||
|
// of cached row data, but we currently don't have an API for that. We'd need to
|
||||||
|
// create it.
|
||||||
|
if (
|
||||||
|
!prevProps.items ||
|
||||||
|
prevProps.items.length === 0 ||
|
||||||
|
resetCounter !== prevProps.resetCounter
|
||||||
|
) {
|
||||||
|
const oneTimeScrollRow = this.getLastSeenIndicatorRow();
|
||||||
|
this.setState({
|
||||||
|
oneTimeScrollRow,
|
||||||
|
atBottom: true,
|
||||||
|
propScrollToIndex: scrollToIndex,
|
||||||
|
prevPropScrollToIndex: scrollToIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (prevProps.items && prevProps.items.length > 0) {
|
||||||
|
this.resizeAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
} else if (!typingContact && prevProps.typingContact) {
|
||||||
|
this.resizeAll();
|
||||||
|
} else if (oldestUnreadIndex !== prevProps.oldestUnreadIndex) {
|
||||||
|
this.resizeAll();
|
||||||
|
} else if (
|
||||||
|
items &&
|
||||||
|
items.length > 0 &&
|
||||||
|
prevProps.items &&
|
||||||
|
prevProps.items.length > 0 &&
|
||||||
|
items !== prevProps.items
|
||||||
|
) {
|
||||||
|
if (this.state.atTop) {
|
||||||
|
const oldFirstIndex = 0;
|
||||||
|
const oldFirstId = prevProps.items[oldFirstIndex];
|
||||||
|
|
||||||
|
const newIndex = items.findIndex(item => item === oldFirstId);
|
||||||
|
if (newIndex < 0) {
|
||||||
|
this.resizeAll();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRow = this.fromItemIndexToRow(newIndex);
|
||||||
|
this.resizeAll();
|
||||||
|
this.setState({ oneTimeScrollRow: newRow });
|
||||||
|
} else {
|
||||||
|
const oldLastIndex = prevProps.items.length - 1;
|
||||||
|
const oldLastId = prevProps.items[oldLastIndex];
|
||||||
|
|
||||||
|
const newIndex = items.findIndex(item => item === oldLastId);
|
||||||
|
if (newIndex < 0) {
|
||||||
|
this.resizeAll();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexDelta = newIndex - oldLastIndex;
|
||||||
|
|
||||||
|
// If we've just added to the end of the list, then the index of the last id's
|
||||||
|
// index won't have changed, and we can rely on List's detection that items is
|
||||||
|
// different for the necessary re-render.
|
||||||
|
if (indexDelta !== 0) {
|
||||||
|
this.resizeAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (messageHeightChanges) {
|
||||||
|
this.resizeAll();
|
||||||
|
clearChangedMessages(id);
|
||||||
|
} else if (this.resizeAllFlag) {
|
||||||
|
this.resizeAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getScrollTarget = () => {
|
||||||
|
const { oneTimeScrollRow, atBottom, propScrollToIndex } = this.state;
|
||||||
|
|
||||||
|
const rowCount = this.getRowCount();
|
||||||
|
const targetMessage = isNumber(propScrollToIndex)
|
||||||
|
? this.fromItemIndexToRow(propScrollToIndex)
|
||||||
|
: undefined;
|
||||||
|
const scrollToBottom = atBottom ? rowCount - 1 : undefined;
|
||||||
|
|
||||||
|
if (isNumber(targetMessage)) {
|
||||||
|
return targetMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNumber(oneTimeScrollRow)) {
|
||||||
|
return oneTimeScrollRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
return scrollToBottom;
|
||||||
|
};
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { i18n, id, items } = this.props;
|
||||||
|
const {
|
||||||
|
shouldShowScrollDownButton,
|
||||||
|
areUnreadBelowCurrentPosition,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
if (!items || items.length < 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowCount = this.getRowCount();
|
||||||
|
const scrollToIndex = this.getScrollTarget();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="module-timeline">
|
<div className="module-timeline">
|
||||||
<AutoSizer>
|
<AutoSizer>
|
||||||
|
@ -103,26 +829,41 @@ export class Timeline extends React.PureComponent<Props> {
|
||||||
this.resizeAllFlag = true;
|
this.resizeAllFlag = true;
|
||||||
|
|
||||||
setTimeout(this.resizeAll, 0);
|
setTimeout(this.resizeAll, 0);
|
||||||
|
} else if (
|
||||||
|
this.mostRecentHeight &&
|
||||||
|
this.mostRecentHeight !== height
|
||||||
|
) {
|
||||||
|
setTimeout(this.onHeightOnlyChange, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.mostRecentWidth = width;
|
this.mostRecentWidth = width;
|
||||||
|
this.mostRecentHeight = height;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List
|
<List
|
||||||
deferredMeasurementCache={this.cellSizeCache}
|
deferredMeasurementCache={this.cellSizeCache}
|
||||||
height={height}
|
height={height}
|
||||||
// This also registers us with parent InfiniteLoader
|
onScroll={this.onScroll as any}
|
||||||
// onRowsRendered={onRowsRendered}
|
overscanRowCount={10}
|
||||||
overscanRowCount={0}
|
|
||||||
ref={this.listRef}
|
ref={this.listRef}
|
||||||
rowCount={items.length}
|
rowCount={rowCount}
|
||||||
rowHeight={this.cellSizeCache.rowHeight}
|
rowHeight={this.cellSizeCache.rowHeight}
|
||||||
rowRenderer={this.rowRenderer}
|
rowRenderer={this.rowRenderer}
|
||||||
|
scrollToAlignment="start"
|
||||||
|
scrollToIndex={scrollToIndex}
|
||||||
width={width}
|
width={width}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</AutoSizer>
|
</AutoSizer>
|
||||||
|
{shouldShowScrollDownButton ? (
|
||||||
|
<ScrollDownButton
|
||||||
|
conversationId={id}
|
||||||
|
withNewMessages={areUnreadBelowCurrentPosition}
|
||||||
|
scrollDown={this.onClickScrollDownButton}
|
||||||
|
i18n={i18n}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,51 @@
|
||||||
|
### A plain message
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
const item = {} < TimelineItem;
|
const item = {
|
||||||
|
type: 'message',
|
||||||
|
data: {
|
||||||
|
id: 'id-1',
|
||||||
|
direction: 'incoming',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
authorPhoneNumber: '(202) 555-2001',
|
||||||
|
authorColor: 'green',
|
||||||
|
text: '🔥',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
<TimelineItem item={item} i18n={util.i18n} />;
|
||||||
|
```
|
||||||
|
|
||||||
|
### A notification
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const item = {
|
||||||
|
type: 'timerNotification',
|
||||||
|
data: {
|
||||||
|
type: 'fromOther',
|
||||||
|
phoneNumber: '(202) 555-0000',
|
||||||
|
timespan: '1 hour',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
<TimelineItem item={item} i18n={util.i18n} />;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unknown type
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const item = {
|
||||||
|
type: 'random',
|
||||||
|
data: {
|
||||||
|
somethin: 'somethin',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
<TimelineItem item={item} i18n={util.i18n} />;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Missing itme
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<TimelineItem item={null} i18n={util.i18n} />
|
||||||
```
|
```
|
||||||
|
|
|
@ -6,6 +6,11 @@ import {
|
||||||
PropsActions as MessageActionsType,
|
PropsActions as MessageActionsType,
|
||||||
PropsData as MessageProps,
|
PropsData as MessageProps,
|
||||||
} from './Message';
|
} from './Message';
|
||||||
|
import {
|
||||||
|
PropsActions as UnsupportedMessageActionsType,
|
||||||
|
PropsData as UnsupportedMessageProps,
|
||||||
|
UnsupportedMessage,
|
||||||
|
} from './UnsupportedMessage';
|
||||||
import {
|
import {
|
||||||
PropsData as TimerNotificationProps,
|
PropsData as TimerNotificationProps,
|
||||||
TimerNotification,
|
TimerNotification,
|
||||||
|
@ -29,6 +34,10 @@ type MessageType = {
|
||||||
type: 'message';
|
type: 'message';
|
||||||
data: MessageProps;
|
data: MessageProps;
|
||||||
};
|
};
|
||||||
|
type UnsupportedMessageType = {
|
||||||
|
type: 'unsupportedMessage';
|
||||||
|
data: UnsupportedMessageProps;
|
||||||
|
};
|
||||||
type TimerNotificationType = {
|
type TimerNotificationType = {
|
||||||
type: 'timerNotification';
|
type: 'timerNotification';
|
||||||
data: TimerNotificationProps;
|
data: TimerNotificationProps;
|
||||||
|
@ -49,22 +58,26 @@ type ResetSessionNotificationType = {
|
||||||
type: 'resetSessionNotification';
|
type: 'resetSessionNotification';
|
||||||
data: null;
|
data: null;
|
||||||
};
|
};
|
||||||
|
export type TimelineItemType =
|
||||||
|
| MessageType
|
||||||
|
| UnsupportedMessageType
|
||||||
|
| TimerNotificationType
|
||||||
|
| SafetyNumberNotificationType
|
||||||
|
| VerificationNotificationType
|
||||||
|
| ResetSessionNotificationType
|
||||||
|
| GroupNotificationType;
|
||||||
|
|
||||||
type PropsData = {
|
type PropsData = {
|
||||||
item:
|
item?: TimelineItemType;
|
||||||
| MessageType
|
|
||||||
| TimerNotificationType
|
|
||||||
| SafetyNumberNotificationType
|
|
||||||
| VerificationNotificationType
|
|
||||||
| ResetSessionNotificationType
|
|
||||||
| GroupNotificationType;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type PropsHousekeeping = {
|
type PropsHousekeeping = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PropsActions = MessageActionsType & SafetyNumberActionsType;
|
type PropsActions = MessageActionsType &
|
||||||
|
UnsupportedMessageActionsType &
|
||||||
|
SafetyNumberActionsType;
|
||||||
|
|
||||||
type Props = PropsData & PropsHousekeeping & PropsActions;
|
type Props = PropsData & PropsHousekeeping & PropsActions;
|
||||||
|
|
||||||
|
@ -73,12 +86,18 @@ export class TimelineItem extends React.PureComponent<Props> {
|
||||||
const { item, i18n } = this.props;
|
const { item, i18n } = this.props;
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
throw new Error('TimelineItem: Item was not provided!');
|
// tslint:disable-next-line:no-console
|
||||||
|
console.warn('TimelineItem: item provided was falsey');
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.type === 'message') {
|
if (item.type === 'message') {
|
||||||
return <Message {...this.props} {...item.data} i18n={i18n} />;
|
return <Message {...this.props} {...item.data} i18n={i18n} />;
|
||||||
}
|
}
|
||||||
|
if (item.type === 'unsupportedMessage') {
|
||||||
|
return <UnsupportedMessage {...this.props} {...item.data} i18n={i18n} />;
|
||||||
|
}
|
||||||
if (item.type === 'timerNotification') {
|
if (item.type === 'timerNotification') {
|
||||||
return <TimerNotification {...this.props} {...item.data} i18n={i18n} />;
|
return <TimerNotification {...this.props} {...item.data} i18n={i18n} />;
|
||||||
}
|
}
|
||||||
|
|
28
ts/components/conversation/TimelineLoadingRow.md
Normal file
28
ts/components/conversation/TimelineLoadingRow.md
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
### Idle
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
|
<TimelineLoadingRow state="idle" />
|
||||||
|
</util.ConversationContext>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Countdown
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
|
<TimelineLoadingRow
|
||||||
|
state="countdown"
|
||||||
|
duration={30000}
|
||||||
|
expiresAt={Date.now() + 20000}
|
||||||
|
onComplete={() => console.log('onComplete')}
|
||||||
|
/>
|
||||||
|
</util.ConversationContext>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Loading
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
|
<TimelineLoadingRow state="loading" />
|
||||||
|
</util.ConversationContext>
|
||||||
|
```
|
48
ts/components/conversation/TimelineLoadingRow.tsx
Normal file
48
ts/components/conversation/TimelineLoadingRow.tsx
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { isNumber } from 'lodash';
|
||||||
|
|
||||||
|
import { Countdown } from '../Countdown';
|
||||||
|
import { Spinner } from '../Spinner';
|
||||||
|
|
||||||
|
export type STATE_ENUM = 'idle' | 'countdown' | 'loading';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
state: STATE_ENUM;
|
||||||
|
duration?: number;
|
||||||
|
expiresAt?: number;
|
||||||
|
onComplete?: () => unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FAKE_DURATION = 1000;
|
||||||
|
|
||||||
|
export class TimelineLoadingRow extends React.PureComponent<Props> {
|
||||||
|
public renderContents() {
|
||||||
|
const { state, duration, expiresAt, onComplete } = this.props;
|
||||||
|
|
||||||
|
if (state === 'idle') {
|
||||||
|
const fakeExpiresAt = Date.now() - FAKE_DURATION;
|
||||||
|
|
||||||
|
return <Countdown duration={FAKE_DURATION} expiresAt={fakeExpiresAt} />;
|
||||||
|
} else if (
|
||||||
|
state === 'countdown' &&
|
||||||
|
isNumber(duration) &&
|
||||||
|
isNumber(expiresAt)
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<Countdown
|
||||||
|
duration={duration}
|
||||||
|
expiresAt={expiresAt}
|
||||||
|
onComplete={onComplete}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Spinner size="24" svgSize="small" direction="on-background" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<div className="module-timeline-loading-row">{this.renderContents()}</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,8 +5,6 @@ import { ContactName } from './ContactName';
|
||||||
import { Intl } from '../Intl';
|
import { Intl } from '../Intl';
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
|
||||||
|
|
||||||
export type PropsData = {
|
export type PropsData = {
|
||||||
type: 'fromOther' | 'fromMe' | 'fromSync';
|
type: 'fromOther' | 'fromMe' | 'fromSync';
|
||||||
phoneNumber: string;
|
phoneNumber: string;
|
||||||
|
@ -63,7 +61,9 @@ export class TimerNotification extends React.Component<Props> {
|
||||||
? i18n('disappearingMessagesDisabled')
|
? i18n('disappearingMessagesDisabled')
|
||||||
: i18n('timerSetOnSync', [timespan]);
|
: i18n('timerSetOnSync', [timespan]);
|
||||||
default:
|
default:
|
||||||
throw missingCaseError(type);
|
console.warn('TimerNotification: unsupported type provided:', type);
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ function getDecember1159() {
|
||||||
}
|
}
|
||||||
|
|
||||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
status="delivered"
|
status="delivered"
|
||||||
|
@ -28,8 +28,8 @@ function getDecember1159() {
|
||||||
text="500ms ago - all below 1 minute are 'now'"
|
text="500ms ago - all below 1 minute are 'now'"
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
status="delivered"
|
status="delivered"
|
||||||
|
@ -38,8 +38,8 @@ function getDecember1159() {
|
||||||
text="Five seconds ago"
|
text="Five seconds ago"
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
status="delivered"
|
status="delivered"
|
||||||
|
@ -48,8 +48,8 @@ function getDecember1159() {
|
||||||
text="30 seconds ago"
|
text="30 seconds ago"
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
status="delivered"
|
status="delivered"
|
||||||
|
@ -58,8 +58,8 @@ function getDecember1159() {
|
||||||
text="One minute ago - in minutes"
|
text="One minute ago - in minutes"
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
status="delivered"
|
status="delivered"
|
||||||
|
@ -68,8 +68,8 @@ function getDecember1159() {
|
||||||
text="30 minutes ago"
|
text="30 minutes ago"
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
status="delivered"
|
status="delivered"
|
||||||
|
@ -78,8 +78,8 @@ function getDecember1159() {
|
||||||
text="45 minutes ago (used to round up to 1 hour with moment)"
|
text="45 minutes ago (used to round up to 1 hour with moment)"
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
status="delivered"
|
status="delivered"
|
||||||
|
@ -88,8 +88,8 @@ function getDecember1159() {
|
||||||
text="One hour ago - in hours"
|
text="One hour ago - in hours"
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
status="delivered"
|
status="delivered"
|
||||||
|
@ -98,8 +98,8 @@ function getDecember1159() {
|
||||||
text="12:01am today"
|
text="12:01am today"
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
status="delivered"
|
status="delivered"
|
||||||
|
@ -108,8 +108,8 @@ function getDecember1159() {
|
||||||
text="11:59pm yesterday - adds day name"
|
text="11:59pm yesterday - adds day name"
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
status="delivered"
|
status="delivered"
|
||||||
|
@ -118,8 +118,8 @@ function getDecember1159() {
|
||||||
text="24 hours ago"
|
text="24 hours ago"
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
status="delivered"
|
status="delivered"
|
||||||
|
@ -128,8 +128,8 @@ function getDecember1159() {
|
||||||
text="Two days ago"
|
text="Two days ago"
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
status="delivered"
|
status="delivered"
|
||||||
|
@ -138,8 +138,8 @@ function getDecember1159() {
|
||||||
text="Seven days ago - adds month"
|
text="Seven days ago - adds month"
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
status="delivered"
|
status="delivered"
|
||||||
|
@ -148,8 +148,8 @@ function getDecember1159() {
|
||||||
text="Thirty days ago"
|
text="Thirty days ago"
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
status="delivered"
|
status="delivered"
|
||||||
|
@ -158,8 +158,8 @@ function getDecember1159() {
|
||||||
text="January 1st at 12:01am"
|
text="January 1st at 12:01am"
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
status="delivered"
|
status="delivered"
|
||||||
|
@ -168,8 +168,8 @@ function getDecember1159() {
|
||||||
text="December 31st at 11:59pm - adds year"
|
text="December 31st at 11:59pm - adds year"
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<Message
|
<Message
|
||||||
direction="incoming"
|
direction="incoming"
|
||||||
status="delivered"
|
status="delivered"
|
||||||
|
@ -178,6 +178,6 @@ function getDecember1159() {
|
||||||
text="One year ago"
|
text="One year ago"
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
</util.ConversationContext>;
|
</util.ConversationContext>;
|
||||||
```
|
```
|
||||||
|
|
|
@ -2,12 +2,12 @@
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<TypingBubble conversationType="direct" i18n={util.i18n} />
|
<TypingBubble conversationType="direct" i18n={util.i18n} />
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<TypingBubble color="teal" conversationType="direct" i18n={util.i18n} />
|
<TypingBubble color="teal" conversationType="direct" i18n={util.i18n} />
|
||||||
</li>
|
</div>
|
||||||
</util.ConversationContext>
|
</util.ConversationContext>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -15,24 +15,24 @@
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<TypingBubble color="red" conversationType="group" i18n={util.i18n} />
|
<TypingBubble color="red" conversationType="group" i18n={util.i18n} />
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<TypingBubble
|
<TypingBubble
|
||||||
color="purple"
|
color="purple"
|
||||||
authorName="First Last"
|
authorName="First Last"
|
||||||
conversationType="group"
|
conversationType="group"
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
<div className="module-message-container">
|
||||||
<TypingBubble
|
<TypingBubble
|
||||||
avatarPath={util.gifObjectUrl}
|
avatarPath={util.gifObjectUrl}
|
||||||
color="blue"
|
color="blue"
|
||||||
conversationType="group"
|
conversationType="group"
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
/>
|
/>
|
||||||
</li>
|
</div>
|
||||||
</util.ConversationContext>
|
</util.ConversationContext>
|
||||||
```
|
```
|
||||||
|
|
|
@ -9,14 +9,14 @@ import { LocalizerType } from '../../types/Util';
|
||||||
interface Props {
|
interface Props {
|
||||||
avatarPath?: string;
|
avatarPath?: string;
|
||||||
color: string;
|
color: string;
|
||||||
name: string;
|
name?: string;
|
||||||
phoneNumber: string;
|
phoneNumber: string;
|
||||||
profileName: string;
|
profileName?: string;
|
||||||
conversationType: string;
|
conversationType: 'group' | 'direct';
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TypingBubble extends React.Component<Props> {
|
export class TypingBubble extends React.PureComponent<Props> {
|
||||||
public renderAvatar() {
|
public renderAvatar() {
|
||||||
const {
|
const {
|
||||||
avatarPath,
|
avatarPath,
|
||||||
|
@ -49,10 +49,17 @@ export class TypingBubble extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { i18n, color } = this.props;
|
const { i18n, color, conversationType } = this.props;
|
||||||
|
const isGroup = conversationType === 'group';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('module-message', 'module-message--incoming')}>
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'module-message',
|
||||||
|
'module-message--incoming',
|
||||||
|
isGroup ? 'module-message--group' : null
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-message__container',
|
'module-message__container',
|
||||||
|
|
|
@ -18,14 +18,14 @@ export type PropsData = {
|
||||||
contact: ContactType;
|
contact: ContactType;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PropsHousekeeping = {
|
|
||||||
i18n: LocalizerType;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PropsActions = {
|
export type PropsActions = {
|
||||||
downloadNewVersion: () => unknown;
|
downloadNewVersion: () => unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PropsHousekeeping = {
|
||||||
|
i18n: LocalizerType;
|
||||||
|
};
|
||||||
|
|
||||||
type Props = PropsData & PropsHousekeeping & PropsActions;
|
type Props = PropsData & PropsHousekeeping & PropsActions;
|
||||||
|
|
||||||
export class UnsupportedMessage extends React.Component<Props> {
|
export class UnsupportedMessage extends React.Component<Props> {
|
||||||
|
|
|
@ -18,7 +18,7 @@ export function renderAvatar({
|
||||||
contact: ContactType;
|
contact: ContactType;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
size: number;
|
size: number;
|
||||||
direction?: string;
|
direction?: 'outgoing' | 'incoming';
|
||||||
}) {
|
}) {
|
||||||
const { avatar } = contact;
|
const { avatar } = contact;
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,13 @@
|
||||||
export function getMessageModel(attributes: any) {
|
export function getSearchResultsProps(attributes: any) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return new window.Whisper.Message(attributes);
|
const model = new window.Whisper.Message(attributes);
|
||||||
|
|
||||||
|
return model.getPropsForSearchResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBubbleProps(attributes: any) {
|
||||||
|
// @ts-ignore
|
||||||
|
const model = new window.Whisper.Message(attributes);
|
||||||
|
|
||||||
|
return model.getPropsForBubble();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
import { AnyAction } from 'redux';
|
import {
|
||||||
import { omit } from 'lodash';
|
difference,
|
||||||
|
fromPairs,
|
||||||
|
intersection,
|
||||||
|
omit,
|
||||||
|
orderBy,
|
||||||
|
pick,
|
||||||
|
uniq,
|
||||||
|
values,
|
||||||
|
without,
|
||||||
|
} from 'lodash';
|
||||||
import { trigger } from '../../shims/events';
|
import { trigger } from '../../shims/events';
|
||||||
import { NoopActionType } from './noop';
|
import { NoopActionType } from './noop';
|
||||||
|
|
||||||
|
@ -48,29 +56,65 @@ export type ConversationType = {
|
||||||
lastUpdated: number;
|
lastUpdated: number;
|
||||||
unreadCount: number;
|
unreadCount: number;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
isTyping: boolean;
|
typingContact?: {
|
||||||
|
avatarPath?: string;
|
||||||
|
color: string;
|
||||||
|
name?: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
profileName?: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
export type ConversationLookupType = {
|
export type ConversationLookupType = {
|
||||||
[key: string]: ConversationType;
|
[key: string]: ConversationType;
|
||||||
};
|
};
|
||||||
export type MessageType = {
|
export type MessageType = {
|
||||||
id: string;
|
id: string;
|
||||||
|
conversationId: string;
|
||||||
|
source: string;
|
||||||
|
type: 'incoming' | 'outgoing' | 'group' | 'keychange' | 'verified-change';
|
||||||
|
quote?: { author: string };
|
||||||
|
received_at: number;
|
||||||
|
hasSignalAccount?: boolean;
|
||||||
|
|
||||||
|
// No need to go beyond this; unused at this stage, since this goes into
|
||||||
|
// a reducer still in plain JavaScript and comes out well-formed
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MessagePointerType = {
|
||||||
|
id: string;
|
||||||
|
received_at: number;
|
||||||
|
};
|
||||||
|
type MessageMetricsType = {
|
||||||
|
newest?: MessagePointerType;
|
||||||
|
oldest?: MessagePointerType;
|
||||||
|
oldestUnread?: MessagePointerType;
|
||||||
|
totalUnread: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type MessageLookupType = {
|
export type MessageLookupType = {
|
||||||
[key: string]: MessageType;
|
[key: string]: MessageType;
|
||||||
};
|
};
|
||||||
export type ConversationMessageType = {
|
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?
|
heightChangeMessageIds: Array<string>;
|
||||||
// We have the infrastructure for it now...
|
isLoadingMessages: boolean;
|
||||||
messages: Array<string>;
|
isNearBottom?: boolean;
|
||||||
|
loadCountdownStart?: number;
|
||||||
|
messageIds: Array<string>;
|
||||||
|
metrics: MessageMetricsType;
|
||||||
|
resetCounter: number;
|
||||||
|
scrollToMessageId?: string;
|
||||||
|
scrollToMessageCounter: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MessagesByConversationType = {
|
export type MessagesByConversationType = {
|
||||||
[key: string]: ConversationMessageType;
|
[key: string]: ConversationMessageType | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConversationsStateType = {
|
export type ConversationsStateType = {
|
||||||
conversationLookup: ConversationLookupType;
|
conversationLookup: ConversationLookupType;
|
||||||
selectedConversation?: string;
|
selectedConversation?: string;
|
||||||
|
selectedMessage?: string;
|
||||||
|
selectedMessageCounter: number;
|
||||||
showArchived: boolean;
|
showArchived: boolean;
|
||||||
|
|
||||||
// Note: it's very important that both of these locations are always kept up to date
|
// Note: it's very important that both of these locations are always kept up to date
|
||||||
|
@ -100,15 +144,91 @@ type ConversationRemovedActionType = {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
type ConversationUnloadedActionType = {
|
||||||
|
type: 'CONVERSATION_UNLOADED';
|
||||||
|
payload: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
export type RemoveAllConversationsActionType = {
|
export type RemoveAllConversationsActionType = {
|
||||||
type: 'CONVERSATIONS_REMOVE_ALL';
|
type: 'CONVERSATIONS_REMOVE_ALL';
|
||||||
payload: null;
|
payload: null;
|
||||||
};
|
};
|
||||||
export type MessageExpiredActionType = {
|
export type MessageChangedActionType = {
|
||||||
type: 'MESSAGE_EXPIRED';
|
type: 'MESSAGE_CHANGED';
|
||||||
payload: {
|
payload: {
|
||||||
id: string;
|
id: string;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
|
data: MessageType;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type MessageDeletedActionType = {
|
||||||
|
type: 'MESSAGE_DELETED';
|
||||||
|
payload: {
|
||||||
|
id: string;
|
||||||
|
conversationId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type MessagesAddedActionType = {
|
||||||
|
type: 'MESSAGES_ADDED';
|
||||||
|
payload: {
|
||||||
|
conversationId: string;
|
||||||
|
messages: Array<MessageType>;
|
||||||
|
isNewMessage: boolean;
|
||||||
|
isFocused: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type MessagesResetActionType = {
|
||||||
|
type: 'MESSAGES_RESET';
|
||||||
|
payload: {
|
||||||
|
conversationId: string;
|
||||||
|
messages: Array<MessageType>;
|
||||||
|
metrics: MessageMetricsType;
|
||||||
|
scrollToMessageId?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type SetMessagesLoadingActionType = {
|
||||||
|
type: 'SET_MESSAGES_LOADING';
|
||||||
|
payload: {
|
||||||
|
conversationId: string;
|
||||||
|
isLoadingMessages: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type SetLoadCountdownStartActionType = {
|
||||||
|
type: 'SET_LOAD_COUNTDOWN_START';
|
||||||
|
payload: {
|
||||||
|
conversationId: string;
|
||||||
|
loadCountdownStart?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type SetIsNearBottomActionType = {
|
||||||
|
type: 'SET_NEAR_BOTTOM';
|
||||||
|
payload: {
|
||||||
|
conversationId: string;
|
||||||
|
isNearBottom: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type ScrollToMessageActionType = {
|
||||||
|
type: 'SCROLL_TO_MESSAGE';
|
||||||
|
payload: {
|
||||||
|
conversationId: string;
|
||||||
|
messageId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type ClearChangedMessagesActionType = {
|
||||||
|
type: 'CLEAR_CHANGED_MESSAGES';
|
||||||
|
payload: {
|
||||||
|
conversationId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type ClearSelectedMessageActionType = {
|
||||||
|
type: 'CLEAR_SELECTED_MESSAGE';
|
||||||
|
payload: null;
|
||||||
|
};
|
||||||
|
export type ClearUnreadMetricsActionType = {
|
||||||
|
type: 'CLEAR_UNREAD_METRICS';
|
||||||
|
payload: {
|
||||||
|
conversationId: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type SelectedConversationChangedActionType = {
|
export type SelectedConversationChangedActionType = {
|
||||||
|
@ -128,14 +248,24 @@ type ShowArchivedConversationsActionType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConversationActionType =
|
export type ConversationActionType =
|
||||||
| AnyAction
|
|
||||||
| ConversationAddedActionType
|
| ConversationAddedActionType
|
||||||
| ConversationChangedActionType
|
| ConversationChangedActionType
|
||||||
| ConversationRemovedActionType
|
| ConversationRemovedActionType
|
||||||
|
| ConversationUnloadedActionType
|
||||||
| RemoveAllConversationsActionType
|
| RemoveAllConversationsActionType
|
||||||
| MessageExpiredActionType
|
| MessageChangedActionType
|
||||||
|
| MessageDeletedActionType
|
||||||
|
| MessagesAddedActionType
|
||||||
|
| MessagesResetActionType
|
||||||
|
| SetMessagesLoadingActionType
|
||||||
|
| SetIsNearBottomActionType
|
||||||
|
| SetLoadCountdownStartActionType
|
||||||
|
| ClearChangedMessagesActionType
|
||||||
|
| ClearSelectedMessageActionType
|
||||||
|
| ClearUnreadMetricsActionType
|
||||||
|
| ScrollToMessageActionType
|
||||||
| SelectedConversationChangedActionType
|
| SelectedConversationChangedActionType
|
||||||
| MessageExpiredActionType
|
| MessageDeletedActionType
|
||||||
| SelectedConversationChangedActionType
|
| SelectedConversationChangedActionType
|
||||||
| ShowInboxActionType
|
| ShowInboxActionType
|
||||||
| ShowArchivedConversationsActionType;
|
| ShowArchivedConversationsActionType;
|
||||||
|
@ -146,8 +276,19 @@ export const actions = {
|
||||||
conversationAdded,
|
conversationAdded,
|
||||||
conversationChanged,
|
conversationChanged,
|
||||||
conversationRemoved,
|
conversationRemoved,
|
||||||
|
conversationUnloaded,
|
||||||
removeAllConversations,
|
removeAllConversations,
|
||||||
messageExpired,
|
messageDeleted,
|
||||||
|
messageChanged,
|
||||||
|
messagesAdded,
|
||||||
|
messagesReset,
|
||||||
|
setMessagesLoading,
|
||||||
|
setLoadCountdownStart,
|
||||||
|
setIsNearBottom,
|
||||||
|
clearChangedMessages,
|
||||||
|
clearSelectedMessage,
|
||||||
|
clearUnreadMetrics,
|
||||||
|
scrollToMessage,
|
||||||
openConversationInternal,
|
openConversationInternal,
|
||||||
openConversationExternal,
|
openConversationExternal,
|
||||||
showInbox,
|
showInbox,
|
||||||
|
@ -186,6 +327,14 @@ function conversationRemoved(id: string): ConversationRemovedActionType {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function conversationUnloaded(id: string): ConversationUnloadedActionType {
|
||||||
|
return {
|
||||||
|
type: 'CONVERSATION_UNLOADED',
|
||||||
|
payload: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
function removeAllConversations(): RemoveAllConversationsActionType {
|
function removeAllConversations(): RemoveAllConversationsActionType {
|
||||||
return {
|
return {
|
||||||
type: 'CONVERSATIONS_REMOVE_ALL',
|
type: 'CONVERSATIONS_REMOVE_ALL',
|
||||||
|
@ -193,22 +342,144 @@ function removeAllConversations(): RemoveAllConversationsActionType {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function messageExpired(
|
function messageChanged(
|
||||||
|
id: string,
|
||||||
|
conversationId: string,
|
||||||
|
data: MessageType
|
||||||
|
): MessageChangedActionType {
|
||||||
|
return {
|
||||||
|
type: 'MESSAGE_CHANGED',
|
||||||
|
payload: {
|
||||||
|
id,
|
||||||
|
conversationId,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function messageDeleted(
|
||||||
id: string,
|
id: string,
|
||||||
conversationId: string
|
conversationId: string
|
||||||
): MessageExpiredActionType {
|
): MessageDeletedActionType {
|
||||||
return {
|
return {
|
||||||
type: 'MESSAGE_EXPIRED',
|
type: 'MESSAGE_DELETED',
|
||||||
payload: {
|
payload: {
|
||||||
id,
|
id,
|
||||||
conversationId,
|
conversationId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function messagesAdded(
|
||||||
|
conversationId: string,
|
||||||
|
messages: Array<MessageType>,
|
||||||
|
isNewMessage: boolean,
|
||||||
|
isFocused: boolean
|
||||||
|
): MessagesAddedActionType {
|
||||||
|
return {
|
||||||
|
type: 'MESSAGES_ADDED',
|
||||||
|
payload: {
|
||||||
|
conversationId,
|
||||||
|
messages,
|
||||||
|
isNewMessage,
|
||||||
|
isFocused,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function messagesReset(
|
||||||
|
conversationId: string,
|
||||||
|
messages: Array<MessageType>,
|
||||||
|
metrics: MessageMetricsType,
|
||||||
|
scrollToMessageId?: string
|
||||||
|
): MessagesResetActionType {
|
||||||
|
return {
|
||||||
|
type: 'MESSAGES_RESET',
|
||||||
|
payload: {
|
||||||
|
conversationId,
|
||||||
|
messages,
|
||||||
|
metrics,
|
||||||
|
scrollToMessageId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function setMessagesLoading(
|
||||||
|
conversationId: string,
|
||||||
|
isLoadingMessages: boolean
|
||||||
|
): SetMessagesLoadingActionType {
|
||||||
|
return {
|
||||||
|
type: 'SET_MESSAGES_LOADING',
|
||||||
|
payload: {
|
||||||
|
conversationId,
|
||||||
|
isLoadingMessages,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function setLoadCountdownStart(
|
||||||
|
conversationId: string,
|
||||||
|
loadCountdownStart?: number
|
||||||
|
): SetLoadCountdownStartActionType {
|
||||||
|
return {
|
||||||
|
type: 'SET_LOAD_COUNTDOWN_START',
|
||||||
|
payload: {
|
||||||
|
conversationId,
|
||||||
|
loadCountdownStart,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function setIsNearBottom(
|
||||||
|
conversationId: string,
|
||||||
|
isNearBottom: boolean
|
||||||
|
): SetIsNearBottomActionType {
|
||||||
|
return {
|
||||||
|
type: 'SET_NEAR_BOTTOM',
|
||||||
|
payload: {
|
||||||
|
conversationId,
|
||||||
|
isNearBottom,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function clearChangedMessages(
|
||||||
|
conversationId: string
|
||||||
|
): ClearChangedMessagesActionType {
|
||||||
|
return {
|
||||||
|
type: 'CLEAR_CHANGED_MESSAGES',
|
||||||
|
payload: {
|
||||||
|
conversationId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function clearSelectedMessage(): ClearSelectedMessageActionType {
|
||||||
|
return {
|
||||||
|
type: 'CLEAR_SELECTED_MESSAGE',
|
||||||
|
payload: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function clearUnreadMetrics(
|
||||||
|
conversationId: string
|
||||||
|
): ClearUnreadMetricsActionType {
|
||||||
|
return {
|
||||||
|
type: 'CLEAR_UNREAD_METRICS',
|
||||||
|
payload: {
|
||||||
|
conversationId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToMessage(
|
||||||
|
conversationId: string,
|
||||||
|
messageId: string
|
||||||
|
): ScrollToMessageActionType {
|
||||||
|
return {
|
||||||
|
type: 'SCROLL_TO_MESSAGE',
|
||||||
|
payload: {
|
||||||
|
conversationId,
|
||||||
|
messageId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Note: we need two actions here to simplify. Operations outside of the left pane can
|
// Note: we need two actions here to simplify. Operations outside of the left pane can
|
||||||
// trigger an 'openConversation' so we go through Whisper.events for all conversation
|
// trigger an 'openConversation' so we go through Whisper.events for all
|
||||||
// selection.
|
// conversation selection. Internal just triggers the Whisper.event, and External
|
||||||
|
// makes the changes to the store.
|
||||||
function openConversationInternal(
|
function openConversationInternal(
|
||||||
id: string,
|
id: string,
|
||||||
messageId?: string
|
messageId?: string
|
||||||
|
@ -251,12 +522,24 @@ function showArchivedConversations() {
|
||||||
function getEmptyState(): ConversationsStateType {
|
function getEmptyState(): ConversationsStateType {
|
||||||
return {
|
return {
|
||||||
conversationLookup: {},
|
conversationLookup: {},
|
||||||
showArchived: false,
|
|
||||||
messagesLookup: {},
|
|
||||||
messagesByConversation: {},
|
messagesByConversation: {},
|
||||||
|
messagesLookup: {},
|
||||||
|
selectedMessageCounter: 0,
|
||||||
|
showArchived: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasMessageHeightChanged(
|
||||||
|
message: MessageType,
|
||||||
|
previous: MessageType
|
||||||
|
): Boolean {
|
||||||
|
return (
|
||||||
|
Boolean(message.hasSignalAccount || previous.hasSignalAccount) &&
|
||||||
|
message.hasSignalAccount !== previous.hasSignalAccount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// tslint:disable-next-line cyclomatic-complexity max-func-body-length
|
||||||
export function reducer(
|
export function reducer(
|
||||||
state: ConversationsStateType = getEmptyState(),
|
state: ConversationsStateType = getEmptyState(),
|
||||||
action: ConversationActionType
|
action: ConversationActionType
|
||||||
|
@ -322,11 +605,421 @@ export function reducer(
|
||||||
conversationLookup: omit(conversationLookup, [id]),
|
conversationLookup: omit(conversationLookup, [id]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (action.type === 'CONVERSATION_UNLOADED') {
|
||||||
|
const { payload } = action;
|
||||||
|
const { id } = payload;
|
||||||
|
const existingConversation = state.messagesByConversation[id];
|
||||||
|
if (!existingConversation) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { messageIds } = existingConversation;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
messagesLookup: omit(state.messagesLookup, messageIds),
|
||||||
|
messagesByConversation: omit(state.messagesByConversation, [id]),
|
||||||
|
};
|
||||||
|
}
|
||||||
if (action.type === 'CONVERSATIONS_REMOVE_ALL') {
|
if (action.type === 'CONVERSATIONS_REMOVE_ALL') {
|
||||||
return getEmptyState();
|
return getEmptyState();
|
||||||
}
|
}
|
||||||
if (action.type === 'MESSAGE_EXPIRED') {
|
if (action.type === 'MESSAGE_CHANGED') {
|
||||||
// noop - for now this is only important for search
|
const { id, conversationId, data } = action.payload;
|
||||||
|
const existingConversation = state.messagesByConversation[conversationId];
|
||||||
|
|
||||||
|
// We don't keep track of messages unless their conversation is loaded...
|
||||||
|
if (!existingConversation) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
// ...and we've already loaded that message once
|
||||||
|
const existingMessage = state.messagesLookup[id];
|
||||||
|
if (!existingMessage) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for changes which could affect height - that's why we need this
|
||||||
|
// heightChangeMessageIds field. It tells Timeline to recalculate all of its heights
|
||||||
|
const hasHeightChanged = hasMessageHeightChanged(data, existingMessage);
|
||||||
|
|
||||||
|
const { heightChangeMessageIds } = existingConversation;
|
||||||
|
const updatedChanges = hasHeightChanged
|
||||||
|
? uniq([...heightChangeMessageIds, id])
|
||||||
|
: heightChangeMessageIds;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
messagesLookup: {
|
||||||
|
...state.messagesLookup,
|
||||||
|
[id]: data,
|
||||||
|
},
|
||||||
|
messagesByConversation: {
|
||||||
|
...state.messagesByConversation,
|
||||||
|
[conversationId]: {
|
||||||
|
...existingConversation,
|
||||||
|
heightChangeMessageIds: updatedChanges,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action.type === 'MESSAGES_RESET') {
|
||||||
|
const {
|
||||||
|
conversationId,
|
||||||
|
messages,
|
||||||
|
metrics,
|
||||||
|
scrollToMessageId,
|
||||||
|
} = action.payload;
|
||||||
|
const { messagesByConversation, messagesLookup } = state;
|
||||||
|
|
||||||
|
const existingConversation = messagesByConversation[conversationId];
|
||||||
|
const resetCounter = existingConversation
|
||||||
|
? existingConversation.resetCounter + 1
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const sorted = orderBy(messages, ['received_at'], ['ASC']);
|
||||||
|
const messageIds = sorted.map(message => message.id);
|
||||||
|
|
||||||
|
const lookup = fromPairs(messages.map(message => [message.id, message]));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedMessage: scrollToMessageId,
|
||||||
|
selectedMessageCounter: state.selectedMessageCounter + 1,
|
||||||
|
messagesLookup: {
|
||||||
|
...messagesLookup,
|
||||||
|
...lookup,
|
||||||
|
},
|
||||||
|
messagesByConversation: {
|
||||||
|
...messagesByConversation,
|
||||||
|
[conversationId]: {
|
||||||
|
isLoadingMessages: false,
|
||||||
|
scrollToMessageId,
|
||||||
|
scrollToMessageCounter: 0,
|
||||||
|
messageIds,
|
||||||
|
metrics,
|
||||||
|
resetCounter,
|
||||||
|
heightChangeMessageIds: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action.type === 'SET_MESSAGES_LOADING') {
|
||||||
|
const { payload } = action;
|
||||||
|
const { conversationId, isLoadingMessages } = payload;
|
||||||
|
|
||||||
|
const { messagesByConversation } = state;
|
||||||
|
const existingConversation = messagesByConversation[conversationId];
|
||||||
|
|
||||||
|
if (!existingConversation) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
messagesByConversation: {
|
||||||
|
...messagesByConversation,
|
||||||
|
[conversationId]: {
|
||||||
|
...existingConversation,
|
||||||
|
loadCountdownStart: undefined,
|
||||||
|
isLoadingMessages,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action.type === 'SET_LOAD_COUNTDOWN_START') {
|
||||||
|
const { payload } = action;
|
||||||
|
const { conversationId, loadCountdownStart } = payload;
|
||||||
|
|
||||||
|
const { messagesByConversation } = state;
|
||||||
|
const existingConversation = messagesByConversation[conversationId];
|
||||||
|
|
||||||
|
if (!existingConversation) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
messagesByConversation: {
|
||||||
|
...messagesByConversation,
|
||||||
|
[conversationId]: {
|
||||||
|
...existingConversation,
|
||||||
|
loadCountdownStart,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action.type === 'SET_NEAR_BOTTOM') {
|
||||||
|
const { payload } = action;
|
||||||
|
const { conversationId, isNearBottom } = payload;
|
||||||
|
|
||||||
|
const { messagesByConversation } = state;
|
||||||
|
const existingConversation = messagesByConversation[conversationId];
|
||||||
|
|
||||||
|
if (!existingConversation) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
messagesByConversation: {
|
||||||
|
...messagesByConversation,
|
||||||
|
[conversationId]: {
|
||||||
|
...existingConversation,
|
||||||
|
isNearBottom,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action.type === 'SCROLL_TO_MESSAGE') {
|
||||||
|
const { payload } = action;
|
||||||
|
const { conversationId, messageId } = payload;
|
||||||
|
|
||||||
|
const { messagesByConversation, messagesLookup } = state;
|
||||||
|
const existingConversation = messagesByConversation[conversationId];
|
||||||
|
|
||||||
|
if (!existingConversation) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
if (!messagesLookup[messageId]) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
if (!existingConversation.messageIds.includes(messageId)) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedMessage: messageId,
|
||||||
|
selectedMessageCounter: state.selectedMessageCounter + 1,
|
||||||
|
messagesByConversation: {
|
||||||
|
...messagesByConversation,
|
||||||
|
[conversationId]: {
|
||||||
|
...existingConversation,
|
||||||
|
isLoadingMessages: false,
|
||||||
|
scrollToMessageId: messageId,
|
||||||
|
scrollToMessageCounter:
|
||||||
|
existingConversation.scrollToMessageCounter + 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action.type === 'MESSAGE_DELETED') {
|
||||||
|
const { id, conversationId } = action.payload;
|
||||||
|
const { messagesByConversation, messagesLookup } = state;
|
||||||
|
|
||||||
|
const existingConversation = messagesByConversation[conversationId];
|
||||||
|
if (!existingConversation) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assuming that we always have contiguous groups of messages in memory, the removal
|
||||||
|
// of one message at one end of our message set be replaced with the message right
|
||||||
|
// next to it.
|
||||||
|
const oldIds = existingConversation.messageIds;
|
||||||
|
let { newest, oldest } = existingConversation.metrics;
|
||||||
|
|
||||||
|
if (oldIds.length > 1) {
|
||||||
|
const firstId = oldIds[0];
|
||||||
|
const lastId = oldIds[oldIds.length - 1];
|
||||||
|
|
||||||
|
if (oldest && oldest.id === firstId && firstId === id) {
|
||||||
|
const second = messagesLookup[oldIds[1]];
|
||||||
|
oldest = second ? pick(second, ['id', 'received_at']) : undefined;
|
||||||
|
}
|
||||||
|
if (newest && newest.id === lastId && lastId === id) {
|
||||||
|
const penultimate = messagesLookup[oldIds[oldIds.length - 2]];
|
||||||
|
newest = penultimate
|
||||||
|
? pick(penultimate, ['id', 'received_at'])
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removing it from our caches
|
||||||
|
const messageIds = without(existingConversation.messageIds, id);
|
||||||
|
const heightChangeMessageIds = without(
|
||||||
|
existingConversation.heightChangeMessageIds,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
messagesLookup: omit(messagesLookup, id),
|
||||||
|
messagesByConversation: {
|
||||||
|
[conversationId]: {
|
||||||
|
...existingConversation,
|
||||||
|
messageIds,
|
||||||
|
heightChangeMessageIds,
|
||||||
|
metrics: {
|
||||||
|
...existingConversation.metrics,
|
||||||
|
oldest,
|
||||||
|
newest,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action.type === 'MESSAGES_ADDED') {
|
||||||
|
const {
|
||||||
|
conversationId,
|
||||||
|
isFocused,
|
||||||
|
isNewMessage,
|
||||||
|
messages,
|
||||||
|
} = action.payload;
|
||||||
|
const { messagesByConversation, messagesLookup } = state;
|
||||||
|
|
||||||
|
const existingConversation = messagesByConversation[conversationId];
|
||||||
|
if (!existingConversation) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
newest,
|
||||||
|
oldest,
|
||||||
|
oldestUnread,
|
||||||
|
totalUnread,
|
||||||
|
} = existingConversation.metrics;
|
||||||
|
|
||||||
|
const existingTotal = existingConversation.messageIds.length;
|
||||||
|
if (isNewMessage && existingTotal > 0) {
|
||||||
|
const lastMessageId = existingConversation.messageIds[existingTotal - 1];
|
||||||
|
|
||||||
|
// If our messages in memory don't include the most recent messages, then we
|
||||||
|
// won't add new messages to our message list.
|
||||||
|
const haveLatest = newest && newest.id === lastMessageId;
|
||||||
|
if (!haveLatest) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newIds = messages.map(message => message.id);
|
||||||
|
const newChanges = intersection(newIds, existingConversation.messageIds);
|
||||||
|
const heightChangeMessageIds = uniq([
|
||||||
|
...newChanges,
|
||||||
|
...existingConversation.heightChangeMessageIds,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const lookup = fromPairs(
|
||||||
|
existingConversation.messageIds.map(id => [id, messagesLookup[id]])
|
||||||
|
);
|
||||||
|
|
||||||
|
messages.forEach(message => {
|
||||||
|
lookup[message.id] = message;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sorted = orderBy(values(lookup), ['received_at'], ['ASC']);
|
||||||
|
const messageIds = sorted.map(message => message.id);
|
||||||
|
|
||||||
|
const first = sorted[0];
|
||||||
|
const last = sorted.length > 0 ? sorted[sorted.length - 1] : null;
|
||||||
|
|
||||||
|
if (first && oldest && first.received_at < oldest.received_at) {
|
||||||
|
oldest = pick(first, ['id', 'received_at']);
|
||||||
|
}
|
||||||
|
if (last && newest && last.received_at > newest.received_at) {
|
||||||
|
newest = pick(last, ['id', 'received_at']);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newMessageIds = difference(newIds, existingConversation.messageIds);
|
||||||
|
const { isNearBottom } = existingConversation;
|
||||||
|
|
||||||
|
if ((!isNearBottom || !isFocused) && !oldestUnread) {
|
||||||
|
const oldestId = newMessageIds.find(messageId => {
|
||||||
|
const message = lookup[messageId];
|
||||||
|
|
||||||
|
return Boolean(message.unread);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (oldestId) {
|
||||||
|
oldestUnread = pick(lookup[oldestId], [
|
||||||
|
'id',
|
||||||
|
'received_at',
|
||||||
|
]) as MessagePointerType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldestUnread) {
|
||||||
|
const newUnread: number = newMessageIds.reduce((sum, messageId) => {
|
||||||
|
const message = lookup[messageId];
|
||||||
|
|
||||||
|
return sum + (message && message.unread ? 1 : 0);
|
||||||
|
}, 0);
|
||||||
|
totalUnread = (totalUnread || 0) + newUnread;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
messagesLookup: {
|
||||||
|
...messagesLookup,
|
||||||
|
...lookup,
|
||||||
|
},
|
||||||
|
messagesByConversation: {
|
||||||
|
...messagesByConversation,
|
||||||
|
[conversationId]: {
|
||||||
|
...existingConversation,
|
||||||
|
isLoadingMessages: false,
|
||||||
|
messageIds,
|
||||||
|
heightChangeMessageIds,
|
||||||
|
scrollToMessageId: undefined,
|
||||||
|
metrics: {
|
||||||
|
...existingConversation.metrics,
|
||||||
|
newest,
|
||||||
|
oldest,
|
||||||
|
totalUnread,
|
||||||
|
oldestUnread,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action.type === 'CLEAR_SELECTED_MESSAGE') {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedMessage: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action.type === 'CLEAR_CHANGED_MESSAGES') {
|
||||||
|
const { payload } = action;
|
||||||
|
const { conversationId } = payload;
|
||||||
|
const existingConversation = state.messagesByConversation[conversationId];
|
||||||
|
|
||||||
|
if (!existingConversation) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
messagesByConversation: {
|
||||||
|
...state.messagesByConversation,
|
||||||
|
[conversationId]: {
|
||||||
|
...existingConversation,
|
||||||
|
heightChangeMessageIds: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action.type === 'CLEAR_UNREAD_METRICS') {
|
||||||
|
const { payload } = action;
|
||||||
|
const { conversationId } = payload;
|
||||||
|
const existingConversation = state.messagesByConversation[conversationId];
|
||||||
|
|
||||||
|
if (!existingConversation) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
messagesByConversation: {
|
||||||
|
...state.messagesByConversation,
|
||||||
|
[conversationId]: {
|
||||||
|
...existingConversation,
|
||||||
|
metrics: {
|
||||||
|
...existingConversation.metrics,
|
||||||
|
oldestUnread: undefined,
|
||||||
|
totalUnread: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (action.type === 'SELECTED_CONVERSATION_CHANGED') {
|
if (action.type === 'SELECTED_CONVERSATION_CHANGED') {
|
||||||
const { payload } = action;
|
const { payload } = action;
|
||||||
|
|
|
@ -1,18 +1,14 @@
|
||||||
import { AnyAction } from 'redux';
|
|
||||||
import { omit, reject } from 'lodash';
|
import { omit, reject } from 'lodash';
|
||||||
|
|
||||||
import { normalize } from '../../types/PhoneNumber';
|
import { normalize } from '../../types/PhoneNumber';
|
||||||
import { trigger } from '../../shims/events';
|
import { trigger } from '../../shims/events';
|
||||||
// import { getMessageModel } from '../../shims/Whisper';
|
import { cleanSearchTerm } from '../../util/cleanSearchTerm';
|
||||||
// import { cleanSearchTerm } from '../../util/cleanSearchTerm';
|
import { searchConversations, searchMessages } from '../../../js/modules/data';
|
||||||
import {
|
|
||||||
searchConversations /*, searchMessages */,
|
|
||||||
} from '../../../js/modules/data';
|
|
||||||
import { makeLookup } from '../../util/makeLookup';
|
import { makeLookup } from '../../util/makeLookup';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ConversationType,
|
ConversationType,
|
||||||
MessageExpiredActionType,
|
MessageDeletedActionType,
|
||||||
MessageSearchResultType,
|
MessageSearchResultType,
|
||||||
RemoveAllConversationsActionType,
|
RemoveAllConversationsActionType,
|
||||||
SelectedConversationChangedActionType,
|
SelectedConversationChangedActionType,
|
||||||
|
@ -64,11 +60,10 @@ type ClearSearchActionType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SEARCH_TYPES =
|
export type SEARCH_TYPES =
|
||||||
| AnyAction
|
|
||||||
| SearchResultsFulfilledActionType
|
| SearchResultsFulfilledActionType
|
||||||
| UpdateSearchTermActionType
|
| UpdateSearchTermActionType
|
||||||
| ClearSearchActionType
|
| ClearSearchActionType
|
||||||
| MessageExpiredActionType
|
| MessageDeletedActionType
|
||||||
| RemoveAllConversationsActionType
|
| RemoveAllConversationsActionType
|
||||||
| SelectedConversationChangedActionType;
|
| SelectedConversationChangedActionType;
|
||||||
|
|
||||||
|
@ -101,9 +96,9 @@ async function doSearch(
|
||||||
): Promise<SearchResultsPayloadType> {
|
): Promise<SearchResultsPayloadType> {
|
||||||
const { regionCode, ourNumber, noteToSelf } = options;
|
const { regionCode, ourNumber, noteToSelf } = options;
|
||||||
|
|
||||||
const [discussions /*, messages */] = await Promise.all([
|
const [discussions, messages] = await Promise.all([
|
||||||
queryConversationsAndContacts(query, { ourNumber, noteToSelf }),
|
queryConversationsAndContacts(query, { ourNumber, noteToSelf }),
|
||||||
// queryMessages(query),
|
queryMessages(query),
|
||||||
]);
|
]);
|
||||||
const { conversations, contacts } = discussions;
|
const { conversations, contacts } = discussions;
|
||||||
|
|
||||||
|
@ -112,7 +107,7 @@ async function doSearch(
|
||||||
normalizedPhoneNumber: normalize(query, { regionCode }),
|
normalizedPhoneNumber: normalize(query, { regionCode }),
|
||||||
conversations,
|
conversations,
|
||||||
contacts,
|
contacts,
|
||||||
messages: [], // getMessageProps(messages) || [],
|
messages,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function clearSearch(): ClearSearchActionType {
|
function clearSearch(): ClearSearchActionType {
|
||||||
|
@ -146,29 +141,15 @@ function startNewConversation(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions for search
|
async function queryMessages(query: string) {
|
||||||
|
try {
|
||||||
|
const normalized = cleanSearchTerm(query);
|
||||||
|
|
||||||
// const getMessageProps = (messages: Array<MessageSearchResultType>) => {
|
return searchMessages(normalized);
|
||||||
// if (!messages || !messages.length) {
|
} catch (e) {
|
||||||
// return [];
|
return [];
|
||||||
// }
|
}
|
||||||
|
}
|
||||||
// return messages.map(message => {
|
|
||||||
// const model = getMessageModel(message);
|
|
||||||
|
|
||||||
// return model.propsForSearchResult;
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
|
|
||||||
// async function queryMessages(query: string) {
|
|
||||||
// try {
|
|
||||||
// const normalized = cleanSearchTerm(query);
|
|
||||||
|
|
||||||
// return searchMessages(normalized);
|
|
||||||
// } catch (e) {
|
|
||||||
// return [];
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
async function queryConversationsAndContacts(
|
async function queryConversationsAndContacts(
|
||||||
providedQuery: string,
|
providedQuery: string,
|
||||||
|
@ -271,7 +252,7 @@ export function reducer(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.type === 'MESSAGE_EXPIRED') {
|
if (action.type === 'MESSAGE_DELETED') {
|
||||||
const { messages, messageLookup } = state;
|
const { messages, messageLookup } = state;
|
||||||
if (!messages.length) {
|
if (!messages.length) {
|
||||||
return state;
|
return state;
|
||||||
|
|
|
@ -9,8 +9,8 @@ import { SmartTimeline } from '../smart/Timeline';
|
||||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||||
const FilteredTimeline = SmartTimeline as any;
|
const FilteredTimeline = SmartTimeline as any;
|
||||||
|
|
||||||
export const createTimeline = (store: Store) => (
|
export const createTimeline = (store: Store, props: Object) => (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<FilteredTimeline />
|
<FilteredTimeline {...props} />
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,12 +6,16 @@ import { LocalizerType } from '../../types/Util';
|
||||||
import { StateType } from '../reducer';
|
import { StateType } from '../reducer';
|
||||||
import {
|
import {
|
||||||
ConversationLookupType,
|
ConversationLookupType,
|
||||||
|
ConversationMessageType,
|
||||||
ConversationsStateType,
|
ConversationsStateType,
|
||||||
ConversationType,
|
ConversationType,
|
||||||
MessageLookupType,
|
MessageLookupType,
|
||||||
MessagesByConversationType,
|
MessagesByConversationType,
|
||||||
MessageType,
|
MessageType,
|
||||||
} from '../ducks/conversations';
|
} from '../ducks/conversations';
|
||||||
|
import { getBubbleProps } from '../../shims/Whisper';
|
||||||
|
import { PropsDataType as TimelinePropsType } from '../../components/conversation/Timeline';
|
||||||
|
import { TimelineItemType } from '../../components/conversation/TimelineItem';
|
||||||
|
|
||||||
import { getIntl, getRegionCode, getUserNumber } from './user';
|
import { getIntl, getRegionCode, getUserNumber } from './user';
|
||||||
|
|
||||||
|
@ -32,6 +36,24 @@ export const getSelectedConversation = createSelector(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
type SelectedMessageType = {
|
||||||
|
id: string;
|
||||||
|
counter: number;
|
||||||
|
};
|
||||||
|
export const getSelectedMessage = createSelector(
|
||||||
|
getConversations,
|
||||||
|
(state: ConversationsStateType): SelectedMessageType | undefined => {
|
||||||
|
if (!state.selectedMessage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: state.selectedMessage,
|
||||||
|
counter: state.selectedMessageCounter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const getShowArchived = createSelector(
|
export const getShowArchived = createSelector(
|
||||||
getConversations,
|
getConversations,
|
||||||
(state: ConversationsStateType): boolean => {
|
(state: ConversationsStateType): boolean => {
|
||||||
|
@ -160,9 +182,12 @@ export const getMe = createSelector(
|
||||||
);
|
);
|
||||||
|
|
||||||
// This is where we will put Conversation selector logic, replicating what
|
// This is where we will put Conversation selector logic, replicating what
|
||||||
// is currently in models/conversation.getProps()
|
// is currently in models/conversation.getProps()
|
||||||
// Blockers:
|
// What needs to happen to pull that selector logic here?
|
||||||
// 1) contactTypingTimers - that UI-only state needs to be moved to redux
|
// 1) contactTypingTimers - that UI-only state needs to be moved to redux
|
||||||
|
// 2) all of the message selectors need to be reselect-based; today those
|
||||||
|
// Backbone-based prop-generation functions expect to get Conversation information
|
||||||
|
// directly via ConversationController
|
||||||
export function _conversationSelector(
|
export function _conversationSelector(
|
||||||
conversation: ConversationType
|
conversation: ConversationType
|
||||||
// regionCode: string,
|
// regionCode: string,
|
||||||
|
@ -180,6 +205,8 @@ export const getCachedSelectorForConversation = createSelector(
|
||||||
getRegionCode,
|
getRegionCode,
|
||||||
getUserNumber,
|
getUserNumber,
|
||||||
(): CachedConversationSelectorType => {
|
(): CachedConversationSelectorType => {
|
||||||
|
// Note: memoizee will check all parameters provided, and only run our selector
|
||||||
|
// if any of them have changed.
|
||||||
return memoizee(_conversationSelector, { max: 100 });
|
return memoizee(_conversationSelector, { max: 100 });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -203,49 +230,200 @@ export const getConversationSelector = createSelector(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// For now we pass through, as selector logic is still happening in the Backbone Model.
|
// For now we use a shim, as selector logic is still happening in the Backbone Model.
|
||||||
// Blockers:
|
// What needs to happen to pull that selector logic here?
|
||||||
// 1) it's a lot of code to pull over - ~500 lines
|
// 1) translate ~500 lines of selector logic into TypeScript
|
||||||
// 2) a couple places still rely on all that code - will need to move these to Roots:
|
// 2) other places still rely on that prop-gen code - need to put these under Roots:
|
||||||
// - quote compose
|
// - quote compose
|
||||||
// - message details
|
// - message details
|
||||||
export function _messageSelector(
|
export function _messageSelector(
|
||||||
message: MessageType
|
message: MessageType,
|
||||||
// ourNumber: string,
|
// @ts-ignore
|
||||||
// regionCode: string,
|
ourNumber: string,
|
||||||
// conversation?: ConversationType,
|
// @ts-ignore
|
||||||
// sender?: ConversationType,
|
regionCode: string,
|
||||||
// quoted?: ConversationType
|
// @ts-ignore
|
||||||
): MessageType {
|
conversation?: ConversationType,
|
||||||
return message;
|
// @ts-ignore
|
||||||
|
author?: ConversationType,
|
||||||
|
// @ts-ignore
|
||||||
|
quoted?: ConversationType,
|
||||||
|
selectedMessageId?: string,
|
||||||
|
selectedMessageCounter?: number
|
||||||
|
): TimelineItemType {
|
||||||
|
// Note: We don't use all of those parameters here, but the shim we call does.
|
||||||
|
// We want to call this function again if any of those parameters change.
|
||||||
|
const props = getBubbleProps(message);
|
||||||
|
|
||||||
|
if (selectedMessageId === message.id) {
|
||||||
|
return {
|
||||||
|
...props,
|
||||||
|
data: {
|
||||||
|
...props.data,
|
||||||
|
isSelected: true,
|
||||||
|
isSelectedCounter: selectedMessageCounter,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return props;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A little optimization to reset our selector cache whenever high-level application data
|
// A little optimization to reset our selector cache whenever high-level application data
|
||||||
// changes: regionCode and userNumber.
|
// changes: regionCode and userNumber.
|
||||||
type CachedMessageSelectorType = (message: MessageType) => MessageType;
|
type CachedMessageSelectorType = (
|
||||||
|
message: MessageType,
|
||||||
|
ourNumber: string,
|
||||||
|
regionCode: string,
|
||||||
|
conversation?: ConversationType,
|
||||||
|
author?: ConversationType,
|
||||||
|
quoted?: ConversationType,
|
||||||
|
selectedMessageId?: string,
|
||||||
|
selectedMessageCounter?: number
|
||||||
|
) => TimelineItemType;
|
||||||
export const getCachedSelectorForMessage = createSelector(
|
export const getCachedSelectorForMessage = createSelector(
|
||||||
getRegionCode,
|
getRegionCode,
|
||||||
getUserNumber,
|
getUserNumber,
|
||||||
(): CachedMessageSelectorType => {
|
(): CachedMessageSelectorType => {
|
||||||
|
// Note: memoizee will check all parameters provided, and only run our selector
|
||||||
|
// if any of them have changed.
|
||||||
return memoizee(_messageSelector, { max: 500 });
|
return memoizee(_messageSelector, { max: 500 });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
type GetMessageByIdType = (id: string) => MessageType | undefined;
|
type GetMessageByIdType = (id: string) => TimelineItemType | undefined;
|
||||||
export const getMessageSelector = createSelector(
|
export const getMessageSelector = createSelector(
|
||||||
getCachedSelectorForMessage,
|
getCachedSelectorForMessage,
|
||||||
getMessages,
|
getMessages,
|
||||||
|
getSelectedMessage,
|
||||||
|
getConversationSelector,
|
||||||
|
getRegionCode,
|
||||||
|
getUserNumber,
|
||||||
(
|
(
|
||||||
selector: CachedMessageSelectorType,
|
messageSelector: CachedMessageSelectorType,
|
||||||
lookup: MessageLookupType
|
messageLookup: MessageLookupType,
|
||||||
|
selectedMessage: SelectedMessageType | undefined,
|
||||||
|
conversationSelector: GetConversationByIdType,
|
||||||
|
regionCode: string,
|
||||||
|
ourNumber: string
|
||||||
): GetMessageByIdType => {
|
): GetMessageByIdType => {
|
||||||
return (id: string) => {
|
return (id: string) => {
|
||||||
const message = lookup[id];
|
const message = messageLookup[id];
|
||||||
if (!message) {
|
if (!message) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return selector(message);
|
const { conversationId, source, type, quote } = message;
|
||||||
|
const conversation = conversationSelector(conversationId);
|
||||||
|
let author: ConversationType | undefined;
|
||||||
|
let quoted: ConversationType | undefined;
|
||||||
|
|
||||||
|
if (type === 'incoming') {
|
||||||
|
author = conversationSelector(source);
|
||||||
|
} else if (type === 'outgoing') {
|
||||||
|
author = conversationSelector(ourNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quote) {
|
||||||
|
quoted = conversationSelector(quote.author);
|
||||||
|
}
|
||||||
|
|
||||||
|
return messageSelector(
|
||||||
|
message,
|
||||||
|
ourNumber,
|
||||||
|
regionCode,
|
||||||
|
conversation,
|
||||||
|
author,
|
||||||
|
quoted,
|
||||||
|
selectedMessage ? selectedMessage.id : undefined,
|
||||||
|
selectedMessage ? selectedMessage.counter : undefined
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export function _conversationMessagesSelector(
|
||||||
|
conversation: ConversationMessageType
|
||||||
|
): TimelinePropsType {
|
||||||
|
const {
|
||||||
|
heightChangeMessageIds,
|
||||||
|
isLoadingMessages,
|
||||||
|
loadCountdownStart,
|
||||||
|
messageIds,
|
||||||
|
metrics,
|
||||||
|
resetCounter,
|
||||||
|
scrollToMessageId,
|
||||||
|
scrollToMessageCounter,
|
||||||
|
} = conversation;
|
||||||
|
|
||||||
|
const firstId = messageIds[0];
|
||||||
|
const lastId =
|
||||||
|
messageIds.length === 0 ? undefined : messageIds[messageIds.length - 1];
|
||||||
|
|
||||||
|
const { oldestUnread } = metrics;
|
||||||
|
|
||||||
|
const haveNewest = !metrics.newest || !lastId || lastId === metrics.newest.id;
|
||||||
|
const haveOldest =
|
||||||
|
!metrics.oldest || !firstId || firstId === metrics.oldest.id;
|
||||||
|
|
||||||
|
const items = messageIds;
|
||||||
|
const messageHeightChanges = Boolean(
|
||||||
|
heightChangeMessageIds && heightChangeMessageIds.length
|
||||||
|
);
|
||||||
|
const oldestUnreadIndex = oldestUnread
|
||||||
|
? messageIds.findIndex(id => id === oldestUnread.id)
|
||||||
|
: undefined;
|
||||||
|
const scrollToIndex = scrollToMessageId
|
||||||
|
? messageIds.findIndex(id => id === scrollToMessageId)
|
||||||
|
: undefined;
|
||||||
|
const { totalUnread } = metrics;
|
||||||
|
|
||||||
|
return {
|
||||||
|
haveNewest,
|
||||||
|
haveOldest,
|
||||||
|
isLoadingMessages,
|
||||||
|
loadCountdownStart,
|
||||||
|
items,
|
||||||
|
messageHeightChanges,
|
||||||
|
oldestUnreadIndex:
|
||||||
|
oldestUnreadIndex && oldestUnreadIndex >= 0
|
||||||
|
? oldestUnreadIndex
|
||||||
|
: undefined,
|
||||||
|
resetCounter,
|
||||||
|
scrollToIndex:
|
||||||
|
scrollToIndex && scrollToIndex >= 0 ? scrollToIndex : undefined,
|
||||||
|
scrollToIndexCounter: scrollToMessageCounter,
|
||||||
|
totalUnread,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type CachedConversationMessagesSelectorType = (
|
||||||
|
conversation: ConversationMessageType
|
||||||
|
) => TimelinePropsType;
|
||||||
|
export const getCachedSelectorForConversationMessages = createSelector(
|
||||||
|
getRegionCode,
|
||||||
|
getUserNumber,
|
||||||
|
(): CachedConversationMessagesSelectorType => {
|
||||||
|
// Note: memoizee will check all parameters provided, and only run our selector
|
||||||
|
// if any of them have changed.
|
||||||
|
return memoizee(_conversationMessagesSelector, { max: 50 });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getConversationMessagesSelector = createSelector(
|
||||||
|
getCachedSelectorForConversationMessages,
|
||||||
|
getMessagesByConversation,
|
||||||
|
(
|
||||||
|
conversationMessagesSelector: CachedConversationMessagesSelectorType,
|
||||||
|
messagesByConversation: MessagesByConversationType
|
||||||
|
) => {
|
||||||
|
return (id: string): TimelinePropsType | undefined => {
|
||||||
|
const conversation = messagesByConversation[id];
|
||||||
|
if (!conversation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return conversationMessagesSelector(conversation);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { compact } from 'lodash';
|
import { compact } from 'lodash';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import { getSearchResultsProps } from '../../shims/Whisper';
|
||||||
|
|
||||||
import { StateType } from '../reducer';
|
import { StateType } from '../reducer';
|
||||||
|
|
||||||
|
@ -79,14 +80,16 @@ export const getSearchResults = createSelector(
|
||||||
),
|
),
|
||||||
hideMessagesHeader: false,
|
hideMessagesHeader: false,
|
||||||
messages: state.messages.map(message => {
|
messages: state.messages.map(message => {
|
||||||
|
const props = getSearchResultsProps(message);
|
||||||
|
|
||||||
if (message.id === selectedMessage) {
|
if (message.id === selectedMessage) {
|
||||||
return {
|
return {
|
||||||
...message,
|
...props,
|
||||||
isSelected: true,
|
isSelected: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return message;
|
return props;
|
||||||
}),
|
}),
|
||||||
regionCode: regionCode,
|
regionCode: regionCode,
|
||||||
searchTerm: state.query,
|
searchTerm: state.query,
|
||||||
|
|
32
ts/state/smart/LastSeenIndicator.tsx
Normal file
32
ts/state/smart/LastSeenIndicator.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { mapDispatchToProps } from '../actions';
|
||||||
|
|
||||||
|
import { LastSeenIndicator } from '../../components/conversation/LastSeenIndicator';
|
||||||
|
|
||||||
|
import { StateType } from '../reducer';
|
||||||
|
import { getIntl } from '../selectors/user';
|
||||||
|
import { getConversationMessagesSelector } from '../selectors/conversations';
|
||||||
|
|
||||||
|
type ExternalProps = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
|
const { id } = props;
|
||||||
|
|
||||||
|
const conversation = getConversationMessagesSelector(state)(id);
|
||||||
|
if (!conversation) {
|
||||||
|
throw new Error(`Did not find conversation ${id} in state!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { totalUnread } = conversation;
|
||||||
|
|
||||||
|
return {
|
||||||
|
count: totalUnread,
|
||||||
|
i18n: getIntl(state),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||||
|
|
||||||
|
export const SmartLastSeenIndicator = smart(LastSeenIndicator);
|
|
@ -14,6 +14,10 @@ import { SmartMainHeader } from './MainHeader';
|
||||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||||
const FilteredSmartMainHeader = SmartMainHeader as any;
|
const FilteredSmartMainHeader = SmartMainHeader as any;
|
||||||
|
|
||||||
|
function renderMainHeader(): JSX.Element {
|
||||||
|
return <FilteredSmartMainHeader />;
|
||||||
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: StateType) => {
|
const mapStateToProps = (state: StateType) => {
|
||||||
const showSearch = isSearching(state);
|
const showSearch = isSearching(state);
|
||||||
|
|
||||||
|
@ -25,7 +29,7 @@ const mapStateToProps = (state: StateType) => {
|
||||||
searchResults,
|
searchResults,
|
||||||
showArchived: getShowArchived(state),
|
showArchived: getShowArchived(state),
|
||||||
i18n: getIntl(state),
|
i18n: getIntl(state),
|
||||||
renderMainHeader: () => <FilteredSmartMainHeader />,
|
renderMainHeader,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { pick } from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { mapDispatchToProps } from '../actions';
|
import { mapDispatchToProps } from '../actions';
|
||||||
|
@ -5,32 +6,59 @@ import { Timeline } from '../../components/conversation/Timeline';
|
||||||
import { StateType } from '../reducer';
|
import { StateType } from '../reducer';
|
||||||
|
|
||||||
import { getIntl } from '../selectors/user';
|
import { getIntl } from '../selectors/user';
|
||||||
import { getConversationSelector } from '../selectors/conversations';
|
import {
|
||||||
|
getConversationMessagesSelector,
|
||||||
|
getConversationSelector,
|
||||||
|
} from '../selectors/conversations';
|
||||||
|
|
||||||
import { SmartTimelineItem } from './TimelineItem';
|
import { SmartTimelineItem } from './TimelineItem';
|
||||||
|
import { SmartTypingBubble } from './TypingBubble';
|
||||||
|
import { SmartLastSeenIndicator } from './LastSeenIndicator';
|
||||||
|
import { SmartTimelineLoadingRow } from './TimelineLoadingRow';
|
||||||
|
|
||||||
// Workaround: A react component's required properties are filtering up through connect()
|
// Workaround: A react component's required properties are filtering up through connect()
|
||||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||||
const FilteredSmartTimelineItem = SmartTimelineItem as any;
|
const FilteredSmartTimelineItem = SmartTimelineItem as any;
|
||||||
|
const FilteredSmartTypingBubble = SmartTypingBubble as any;
|
||||||
|
const FilteredSmartLastSeenIndicator = SmartLastSeenIndicator as any;
|
||||||
|
const FilteredSmartTimelineLoadingRow = SmartTimelineLoadingRow as any;
|
||||||
|
|
||||||
type ExternalProps = {
|
type ExternalProps = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
|
// Note: most action creators are not wired into redux; for now they
|
||||||
|
// are provided by ConversationView in setupTimeline().
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
function renderItem(messageId: string, actionProps: Object): JSX.Element {
|
||||||
const { id } = props;
|
return <FilteredSmartTimelineItem {...actionProps} id={messageId} />;
|
||||||
|
}
|
||||||
|
function renderLastSeenIndicator(id: string): JSX.Element {
|
||||||
|
return <FilteredSmartLastSeenIndicator id={id} />;
|
||||||
|
}
|
||||||
|
function renderLoadingRow(id: string): JSX.Element {
|
||||||
|
return <FilteredSmartTimelineLoadingRow id={id} />;
|
||||||
|
}
|
||||||
|
function renderTypingBubble(id: string): JSX.Element {
|
||||||
|
return <FilteredSmartTypingBubble id={id} />;
|
||||||
|
}
|
||||||
|
|
||||||
const conversationSelector = getConversationSelector(state);
|
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
const conversation = conversationSelector(id);
|
const { id, ...actions } = props;
|
||||||
const items: Array<string> = [];
|
|
||||||
|
const conversation = getConversationSelector(state)(id);
|
||||||
|
const conversationMessages = getConversationMessagesSelector(state)(id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...conversation,
|
id,
|
||||||
items,
|
...pick(conversation, ['unreadCount', 'typingContact']),
|
||||||
|
...conversationMessages,
|
||||||
i18n: getIntl(state),
|
i18n: getIntl(state),
|
||||||
renderTimelineItem: (messageId: string) => {
|
renderItem,
|
||||||
return <FilteredSmartTimelineItem id={messageId} />;
|
renderLastSeenIndicator,
|
||||||
},
|
renderLoadingRow,
|
||||||
|
renderTypingBubble,
|
||||||
|
...actions,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,10 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
const { id } = props;
|
const { id } = props;
|
||||||
|
|
||||||
const messageSelector = getMessageSelector(state);
|
const messageSelector = getMessageSelector(state);
|
||||||
|
const item = messageSelector(id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...messageSelector(id),
|
item,
|
||||||
i18n: getIntl(state),
|
i18n: getIntl(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
50
ts/state/smart/TimelineLoadingRow.tsx
Normal file
50
ts/state/smart/TimelineLoadingRow.tsx
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { mapDispatchToProps } from '../actions';
|
||||||
|
import { isNumber } from 'lodash';
|
||||||
|
|
||||||
|
import {
|
||||||
|
STATE_ENUM,
|
||||||
|
TimelineLoadingRow,
|
||||||
|
} from '../../components/conversation/TimelineLoadingRow';
|
||||||
|
import { LOAD_COUNTDOWN } from '../../components/conversation/Timeline';
|
||||||
|
|
||||||
|
import { StateType } from '../reducer';
|
||||||
|
import { getIntl } from '../selectors/user';
|
||||||
|
import { getConversationMessagesSelector } from '../selectors/conversations';
|
||||||
|
|
||||||
|
type ExternalProps = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
|
const { id } = props;
|
||||||
|
|
||||||
|
const conversation = getConversationMessagesSelector(state)(id);
|
||||||
|
if (!conversation) {
|
||||||
|
throw new Error(`Did not find conversation ${id} in state!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isLoadingMessages, loadCountdownStart } = conversation;
|
||||||
|
|
||||||
|
const loadingState: STATE_ENUM = isLoadingMessages
|
||||||
|
? 'loading'
|
||||||
|
: isNumber(loadCountdownStart)
|
||||||
|
? 'countdown'
|
||||||
|
: 'idle';
|
||||||
|
const duration = loadingState === 'countdown' ? LOAD_COUNTDOWN : undefined;
|
||||||
|
const expiresAt =
|
||||||
|
loadingState === 'countdown' && loadCountdownStart
|
||||||
|
? loadCountdownStart + LOAD_COUNTDOWN
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
state: loadingState,
|
||||||
|
duration,
|
||||||
|
expiresAt,
|
||||||
|
i18n: getIntl(state),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||||
|
|
||||||
|
export const SmartTimelineLoadingRow = smart(TimelineLoadingRow);
|
30
ts/state/smart/TypingBubble.tsx
Normal file
30
ts/state/smart/TypingBubble.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { mapDispatchToProps } from '../actions';
|
||||||
|
import { TypingBubble } from '../../components/conversation/TypingBubble';
|
||||||
|
import { StateType } from '../reducer';
|
||||||
|
|
||||||
|
import { getIntl } from '../selectors/user';
|
||||||
|
import { getConversationSelector } from '../selectors/conversations';
|
||||||
|
|
||||||
|
type ExternalProps = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
|
const { id } = props;
|
||||||
|
|
||||||
|
const conversation = getConversationSelector(state)(id);
|
||||||
|
if (!conversation) {
|
||||||
|
throw new Error(`Did not find conversation ${id} in state!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...conversation.typingContact,
|
||||||
|
conversationType: conversation.type,
|
||||||
|
i18n: getIntl(state),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||||
|
|
||||||
|
export const SmartTypingBubble = smart(TypingBubble);
|
|
@ -7,7 +7,6 @@ interface Props {
|
||||||
*/
|
*/
|
||||||
ios: boolean;
|
ios: boolean;
|
||||||
theme: 'light-theme' | 'dark-theme';
|
theme: 'light-theme' | 'dark-theme';
|
||||||
type: 'private' | 'group';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -16,16 +15,17 @@ interface Props {
|
||||||
*/
|
*/
|
||||||
export class ConversationContext extends React.Component<Props> {
|
export class ConversationContext extends React.Component<Props> {
|
||||||
public render() {
|
public render() {
|
||||||
const { ios, theme, type } = this.props;
|
const { ios, theme } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(theme || 'light-theme', ios ? 'ios-theme' : null)}
|
className={classNames(theme || 'light-theme', ios ? 'ios-theme' : null)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: theme === 'dark-theme' ? 'black' : undefined,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className={classNames('conversation', type || 'private')}>
|
<div className="timeline-placeholder">
|
||||||
<div className="discussion-container" style={{ padding: '0.5em' }}>
|
<div className="timeline-wrapper">{this.props.children}</div>
|
||||||
<ul className="message-list">{this.props.children}</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -25,7 +25,11 @@ describe('state/selectors/conversations', () => {
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
isTyping: false,
|
typingContact: {
|
||||||
|
name: 'Someone There',
|
||||||
|
color: 'blue',
|
||||||
|
phoneNumber: '+18005551111',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
id2: {
|
id2: {
|
||||||
id: 'id2',
|
id: 'id2',
|
||||||
|
@ -40,7 +44,11 @@ describe('state/selectors/conversations', () => {
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
isTyping: false,
|
typingContact: {
|
||||||
|
name: 'Someone There',
|
||||||
|
color: 'blue',
|
||||||
|
phoneNumber: '+18005551111',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
id3: {
|
id3: {
|
||||||
id: 'id3',
|
id: 'id3',
|
||||||
|
@ -55,7 +63,11 @@ describe('state/selectors/conversations', () => {
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
isTyping: false,
|
typingContact: {
|
||||||
|
name: 'Someone There',
|
||||||
|
color: 'blue',
|
||||||
|
phoneNumber: '+18005551111',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
id4: {
|
id4: {
|
||||||
id: 'id4',
|
id: 'id4',
|
||||||
|
@ -70,7 +82,11 @@ describe('state/selectors/conversations', () => {
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
isTyping: false,
|
typingContact: {
|
||||||
|
name: 'Someone There',
|
||||||
|
color: 'blue',
|
||||||
|
phoneNumber: '+18005551111',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
id5: {
|
id5: {
|
||||||
id: 'id5',
|
id: 'id5',
|
||||||
|
@ -85,7 +101,11 @@ describe('state/selectors/conversations', () => {
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
isTyping: false,
|
typingContact: {
|
||||||
|
name: 'Someone There',
|
||||||
|
color: 'blue',
|
||||||
|
phoneNumber: '+18005551111',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const comparator = _getConversationComparator(i18n, regionCode);
|
const comparator = _getConversationComparator(i18n, regionCode);
|
||||||
|
|
|
@ -164,17 +164,17 @@
|
||||||
"rule": "jQuery-load(",
|
"rule": "jQuery-load(",
|
||||||
"path": "js/conversation_controller.js",
|
"path": "js/conversation_controller.js",
|
||||||
"line": " async load() {",
|
"line": " async load() {",
|
||||||
"lineNumber": 178,
|
"lineNumber": 169,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-10-02T21:00:44.007Z"
|
"updated": "2019-07-31T00:19:18.696Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-load(",
|
"rule": "jQuery-load(",
|
||||||
"path": "js/conversation_controller.js",
|
"path": "js/conversation_controller.js",
|
||||||
"line": " this._initialPromise = load();",
|
"line": " this._initialPromise = load();",
|
||||||
"lineNumber": 213,
|
"lineNumber": 204,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-10-02T21:00:44.007Z"
|
"updated": "2019-07-31T00:19:18.696Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
|
@ -363,8 +363,8 @@
|
||||||
"line": " this.$el.append(this.contactView.el);",
|
"line": " this.$el.append(this.contactView.el);",
|
||||||
"lineNumber": 46,
|
"lineNumber": 46,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-10-02T21:18:39.026Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Operating on previously-existing DOM elements"
|
"reasonDetail": "Known DOM elements"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
|
@ -474,148 +474,139 @@
|
||||||
"updated": "2018-09-15T00:38:04.183Z"
|
"updated": "2018-09-15T00:38:04.183Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-appendTo(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " let $el = this.$(`#${id}`);",
|
"line": " view.$el.appendTo(this.el);",
|
||||||
"lineNumber": 34,
|
"lineNumber": 32,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T21:59:32.770Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Known DOM elements"
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-prependTo(",
|
|
||||||
"path": "js/views/inbox_view.js",
|
|
||||||
"line": " $el.prependTo(this.el);",
|
|
||||||
"lineNumber": 43,
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2018-09-19T18:13:29.628Z",
|
|
||||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.message').text(message);",
|
"line": " this.$('.message').text(message);",
|
||||||
"lineNumber": 61,
|
"lineNumber": 58,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T21:59:32.770Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Hardcoded selector"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " el: this.$('.conversation-stack'),",
|
"line": " el: this.$('.conversation-stack'),",
|
||||||
"lineNumber": 78,
|
"lineNumber": 75,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T21:59:32.770Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Hardcoded selector"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-prependTo(",
|
"rule": "jQuery-prependTo(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.appLoadingScreen.$el.prependTo(this.el);",
|
"line": " this.appLoadingScreen.$el.prependTo(this.el);",
|
||||||
"lineNumber": 85,
|
"lineNumber": 82,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T18:13:29.628Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
"reasonDetail": "Known DOM elements"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " .append(this.networkStatusView.render().el);",
|
"line": " .append(this.networkStatusView.render().el);",
|
||||||
"lineNumber": 100,
|
"lineNumber": 97,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T18:13:29.628Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
"reasonDetail": "Known DOM elements"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-prependTo(",
|
"rule": "jQuery-prependTo(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " banner.$el.prependTo(this.$el);",
|
"line": " banner.$el.prependTo(this.$el);",
|
||||||
"lineNumber": 104,
|
"lineNumber": 101,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T18:13:29.628Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
"reasonDetail": "Known DOM elements"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-appendTo(",
|
"rule": "jQuery-appendTo(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " toast.$el.appendTo(this.$el);",
|
"line": " toast.$el.appendTo(this.$el);",
|
||||||
"lineNumber": 110,
|
"lineNumber": 107,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-05-10T00:25:51.515Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
"reasonDetail": "Known DOM elements"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
||||||
"lineNumber": 130,
|
"lineNumber": 126,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-08T23:49:08.796Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Known DOM elements"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
||||||
"lineNumber": 130,
|
"lineNumber": 126,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-08T23:49:08.796Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Known DOM elements"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " if (e && this.$(e.target).closest('.placeholder').length) {",
|
"line": " if (e && this.$(e.target).closest('.placeholder').length) {",
|
||||||
"lineNumber": 171,
|
"lineNumber": 167,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-08T23:49:08.796Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Known DOM elements"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('#header, .gutter').addClass('inactive');",
|
"line": " this.$('#header, .gutter').addClass('inactive');",
|
||||||
"lineNumber": 175,
|
"lineNumber": 171,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-08T23:49:08.796Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Hardcoded selector"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.conversation-stack').addClass('inactive');",
|
"line": " this.$('.conversation-stack').addClass('inactive');",
|
||||||
"lineNumber": 179,
|
"lineNumber": 175,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-08T23:49:08.796Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Hardcoded selector"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.conversation:first .menu').trigger('close');",
|
"line": " this.$('.conversation:first .menu').trigger('close');",
|
||||||
"lineNumber": 181,
|
"lineNumber": 177,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-08T23:49:08.796Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Hardcoded selector"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {",
|
"line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {",
|
||||||
"lineNumber": 201,
|
"lineNumber": 197,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-08T23:49:08.796Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Known DOM elements"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.conversation:first .recorder').trigger('close');",
|
"line": " this.$('.conversation:first .recorder').trigger('close');",
|
||||||
"lineNumber": 204,
|
"lineNumber": 200,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-08T23:49:08.796Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Hardcoded selector"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
|
@ -722,8 +713,8 @@
|
||||||
"line": " new QRCode(this.$('.qr')[0]).makeCode(",
|
"line": " new QRCode(this.$('.qr')[0]).makeCode(",
|
||||||
"lineNumber": 39,
|
"lineNumber": 39,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T21:59:32.770Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Hardcoded selector"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
|
@ -731,7 +722,7 @@
|
||||||
"line": " dcodeIO.ByteBuffer.wrap(this.ourKey).toString('base64')",
|
"line": " dcodeIO.ByteBuffer.wrap(this.ourKey).toString('base64')",
|
||||||
"lineNumber": 40,
|
"lineNumber": 40,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2019-07-31T00:19:18.696Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-insertBefore(",
|
"rule": "jQuery-insertBefore(",
|
||||||
|
@ -739,8 +730,8 @@
|
||||||
"line": " dialog.$el.insertBefore(this.el);",
|
"line": " dialog.$el.insertBefore(this.el);",
|
||||||
"lineNumber": 75,
|
"lineNumber": 75,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T18:13:29.628Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
"reasonDetail": "Known DOM elements"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
|
@ -748,8 +739,8 @@
|
||||||
"line": " this.$('button.verify').attr('disabled', true);",
|
"line": " this.$('button.verify').attr('disabled', true);",
|
||||||
"lineNumber": 79,
|
"lineNumber": 79,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T21:59:32.770Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Hardcoded selector"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
|
@ -757,8 +748,8 @@
|
||||||
"line": " this.$('button.verify').removeAttr('disabled');",
|
"line": " this.$('button.verify').removeAttr('disabled');",
|
||||||
"lineNumber": 110,
|
"lineNumber": 110,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T21:59:32.770Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Hardcoded selector"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
|
@ -778,105 +769,6 @@
|
||||||
"updated": "2018-09-15T00:38:04.183Z",
|
"updated": "2018-09-15T00:38:04.183Z",
|
||||||
"reasonDetail": "Hard-coded value"
|
"reasonDetail": "Hard-coded value"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"rule": "jQuery-$(",
|
|
||||||
"path": "js/views/message_list_view.js",
|
|
||||||
"line": " template: $('#message-list').html(),",
|
|
||||||
"lineNumber": 13,
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2018-11-14T18:51:15.180Z",
|
|
||||||
"reasonDetail": "Parameter is a hard-coded string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-html(",
|
|
||||||
"path": "js/views/message_list_view.js",
|
|
||||||
"line": " template: $('#message-list').html(),",
|
|
||||||
"lineNumber": 13,
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2018-11-14T18:51:15.180Z",
|
|
||||||
"reasonDetail": "This is run at JS load time, which means we control the contents of the target element"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-$(",
|
|
||||||
"path": "js/views/message_list_view.js",
|
|
||||||
"line": " this.$messages = this.$('.messages');",
|
|
||||||
"lineNumber": 30,
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2018-11-14T18:51:15.180Z",
|
|
||||||
"reasonDetail": "Parameter is a hard-coded string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-append(",
|
|
||||||
"path": "js/views/message_list_view.js",
|
|
||||||
"line": " this.$messages.append(view.el);",
|
|
||||||
"lineNumber": 111,
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2018-11-14T18:51:15.180Z",
|
|
||||||
"reasonDetail": "view.el is a known DOM element"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-prepend(",
|
|
||||||
"path": "js/views/message_list_view.js",
|
|
||||||
"line": " this.$messages.prepend(view.el);",
|
|
||||||
"lineNumber": 114,
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2018-11-14T18:51:15.180Z",
|
|
||||||
"reasonDetail": "view.el is a known DOM element"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-$(",
|
|
||||||
"path": "js/views/message_list_view.js",
|
|
||||||
"line": " const next = this.$(`#${this.collection.at(index + 1).id}`);",
|
|
||||||
"lineNumber": 117,
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2018-11-14T18:51:15.180Z",
|
|
||||||
"reasonDetail": "Message ids are GUIDs, and therefore the resultant string for $() is an id"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-insertBefore(",
|
|
||||||
"path": "js/views/message_list_view.js",
|
|
||||||
"line": " view.$el.insertBefore(next);",
|
|
||||||
"lineNumber": 120,
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2018-11-14T18:51:15.180Z",
|
|
||||||
"reasonDetail": "next is a known DOM element"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-insertAfter(",
|
|
||||||
"path": "js/views/message_list_view.js",
|
|
||||||
"line": " view.$el.insertAfter(prev);",
|
|
||||||
"lineNumber": 122,
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2018-11-14T18:51:15.180Z",
|
|
||||||
"reasonDetail": "prev is a known DOM element"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-insertBefore(",
|
|
||||||
"path": "js/views/message_list_view.js",
|
|
||||||
"line": " view.$el.insertBefore(elements[i]);",
|
|
||||||
"lineNumber": 131,
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2018-11-14T18:51:15.180Z",
|
|
||||||
"reasonDetail": "elements[i] is a known DOM element"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-append(",
|
|
||||||
"path": "js/views/message_list_view.js",
|
|
||||||
"line": " this.$messages.append(view.el);",
|
|
||||||
"lineNumber": 136,
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2018-11-14T18:51:15.180Z",
|
|
||||||
"reasonDetail": "view.el is a known DOM element"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-append(",
|
|
||||||
"path": "js/views/message_view.js",
|
|
||||||
"line": " this.$el.append(this.childView.el);",
|
|
||||||
"lineNumber": 144,
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2018-09-19T18:13:29.628Z",
|
|
||||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/phone-input-view.js",
|
"path": "js/views/phone-input-view.js",
|
||||||
|
@ -1453,6 +1345,45 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-15T00:38:04.183Z"
|
"updated": "2018-09-15T00:38:04.183Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-$(",
|
||||||
|
"path": "node_modules/@yarnpkg/lockfile/index.js",
|
||||||
|
"lineNumber": 6546,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-07-31T00:19:18.696Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-load(",
|
||||||
|
"path": "node_modules/@yarnpkg/lockfile/index.js",
|
||||||
|
"line": "function load() {",
|
||||||
|
"lineNumber": 8470,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-07-31T00:19:18.696Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-load(",
|
||||||
|
"path": "node_modules/@yarnpkg/lockfile/index.js",
|
||||||
|
"line": "exports.enable(load());",
|
||||||
|
"lineNumber": 8488,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-07-31T00:19:18.696Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-load(",
|
||||||
|
"path": "node_modules/@yarnpkg/lockfile/index.js",
|
||||||
|
"line": "function load() {",
|
||||||
|
"lineNumber": 8689,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-07-31T00:19:18.696Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-load(",
|
||||||
|
"path": "node_modules/@yarnpkg/lockfile/index.js",
|
||||||
|
"line": "exports.enable(load());",
|
||||||
|
"lineNumber": 8713,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-07-31T00:19:18.696Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-after(",
|
"rule": "jQuery-after(",
|
||||||
"path": "node_modules/archiver-utils/node_modules/lodash/after.js",
|
"path": "node_modules/archiver-utils/node_modules/lodash/after.js",
|
||||||
|
@ -3590,20 +3521,36 @@
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2018-09-19T18:13:29.628Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-load(",
|
||||||
"path": "node_modules/extglob/index.js",
|
"path": "node_modules/extglob/node_modules/debug/src/browser.js",
|
||||||
"line": " o[id] = wrap(inner, prefix, opts.escape);",
|
"line": "function load() {",
|
||||||
"lineNumber": 85,
|
"lineNumber": 150,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2019-07-31T00:19:18.696Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-load(",
|
||||||
"path": "node_modules/extglob/index.js",
|
"path": "node_modules/extglob/node_modules/debug/src/browser.js",
|
||||||
"line": "function wrap(inner, prefix, esc) {",
|
"line": "exports.enable(load());",
|
||||||
"lineNumber": 119,
|
"lineNumber": 168,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2019-07-31T00:19:18.696Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-load(",
|
||||||
|
"path": "node_modules/extglob/node_modules/debug/src/node.js",
|
||||||
|
"line": "function load() {",
|
||||||
|
"lineNumber": 156,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-07-31T00:19:18.696Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-load(",
|
||||||
|
"path": "node_modules/extglob/node_modules/debug/src/node.js",
|
||||||
|
"line": "exports.enable(load());",
|
||||||
|
"lineNumber": 248,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-07-31T00:19:18.696Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "DOM-innerHTML",
|
"rule": "DOM-innerHTML",
|
||||||
|
@ -5219,46 +5166,6 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:06:35.446Z"
|
"updated": "2018-09-19T18:06:35.446Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"rule": "jQuery-before(",
|
|
||||||
"path": "node_modules/micromatch/node_modules/braces/index.js",
|
|
||||||
"line": " str = tokens.before(str, es6Regex());",
|
|
||||||
"lineNumber": 92,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-15T00:38:04.183Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "node_modules/micromatch/node_modules/braces/index.js",
|
|
||||||
"line": " return braces(str.replace(outter, wrap(segs, '|')), opts);",
|
|
||||||
"lineNumber": 121,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "node_modules/micromatch/node_modules/braces/index.js",
|
|
||||||
"line": " segs[0] = wrap(segs[0], '\\\\');",
|
|
||||||
"lineNumber": 126,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-after(",
|
|
||||||
"path": "node_modules/micromatch/node_modules/braces/index.js",
|
|
||||||
"line": " arr.push(es6 ? tokens.after(val) : val);",
|
|
||||||
"lineNumber": 150,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-15T00:38:04.183Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-wrap(",
|
|
||||||
"path": "node_modules/micromatch/node_modules/braces/index.js",
|
|
||||||
"line": "function wrap(val, ch) {",
|
|
||||||
"lineNumber": 216,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "DOM-innerHTML",
|
"rule": "DOM-innerHTML",
|
||||||
"path": "node_modules/min-document/serialize.js",
|
"path": "node_modules/min-document/serialize.js",
|
||||||
|
@ -7161,6 +7068,62 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-09-19T18:13:29.628Z"
|
"updated": "2018-09-19T18:13:29.628Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-before(",
|
||||||
|
"path": "node_modules/test-exclude/node_modules/braces/index.js",
|
||||||
|
"line": " str = tokens.before(str, es6Regex());",
|
||||||
|
"lineNumber": 92,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-07-31T00:19:18.696Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "node_modules/test-exclude/node_modules/braces/index.js",
|
||||||
|
"line": " return braces(str.replace(outter, wrap(segs, '|')), opts);",
|
||||||
|
"lineNumber": 121,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-07-31T00:19:18.696Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "node_modules/test-exclude/node_modules/braces/index.js",
|
||||||
|
"line": " segs[0] = wrap(segs[0], '\\\\');",
|
||||||
|
"lineNumber": 126,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-07-31T00:19:18.696Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-after(",
|
||||||
|
"path": "node_modules/test-exclude/node_modules/braces/index.js",
|
||||||
|
"line": " arr.push(es6 ? tokens.after(val) : val);",
|
||||||
|
"lineNumber": 150,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-07-31T00:19:18.696Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "node_modules/test-exclude/node_modules/braces/index.js",
|
||||||
|
"line": "function wrap(val, ch) {",
|
||||||
|
"lineNumber": 216,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-07-31T00:19:18.696Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "node_modules/test-exclude/node_modules/extglob/index.js",
|
||||||
|
"line": " o[id] = wrap(inner, prefix, opts.escape);",
|
||||||
|
"lineNumber": 85,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-07-31T00:19:18.696Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-wrap(",
|
||||||
|
"path": "node_modules/test-exclude/node_modules/extglob/index.js",
|
||||||
|
"line": "function wrap(inner, prefix, esc) {",
|
||||||
|
"lineNumber": 119,
|
||||||
|
"reasonCategory": "falseMatch",
|
||||||
|
"updated": "2019-07-31T00:19:18.696Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "eval",
|
"rule": "eval",
|
||||||
"path": "node_modules/thenify/index.js",
|
"path": "node_modules/thenify/index.js",
|
||||||
|
@ -7849,8 +7812,8 @@
|
||||||
"line": " this.menuTriggerRef = react_1.default.createRef();",
|
"line": " this.menuTriggerRef = react_1.default.createRef();",
|
||||||
"lineNumber": 14,
|
"lineNumber": 14,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-09T00:08:44.242Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Used only to trigger menu display"
|
"reasonDetail": "Used to reference popup menu"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
|
@ -7858,17 +7821,17 @@
|
||||||
"line": " this.menuTriggerRef = React.createRef();",
|
"line": " this.menuTriggerRef = React.createRef();",
|
||||||
"lineNumber": 59,
|
"lineNumber": 59,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-09T00:08:44.242Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Used only to trigger menu display"
|
"reasonDetail": "Used to reference popup menu"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/Timeline.js",
|
"path": "ts/components/conversation/Timeline.js",
|
||||||
"line": " this.listRef = react_1.default.createRef();",
|
"line": " this.listRef = react_1.default.createRef();",
|
||||||
"lineNumber": 17,
|
"lineNumber": 27,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-04-17T18:44:33.207Z",
|
"updated": "2019-07-31T00:19:18.696Z",
|
||||||
"reasonDetail": "Necessary to interact with child react-virtualized/List"
|
"reasonDetail": "Timeline needs to interact with its child List directly"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
|
|
|
@ -93,7 +93,13 @@
|
||||||
"allow-pascal-case"
|
"allow-pascal-case"
|
||||||
],
|
],
|
||||||
|
|
||||||
"function-name": [true, { "function-regex": "^_?[a-z][\\w\\d]+$" }],
|
"function-name": [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
"function-regex": "^_?[a-z][\\w\\d]+$",
|
||||||
|
"static-method-regex": "^_?[a-z][\\w\\d]+$"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
// Adding select dev dependencies here for now, may turn on all in the future
|
// Adding select dev dependencies here for now, may turn on all in the future
|
||||||
"no-implicit-dependencies": [true, ["dashdash", "electron"]],
|
"no-implicit-dependencies": [true, ["dashdash", "electron"]],
|
||||||
|
|
74
yarn.lock
74
yarn.lock
|
@ -313,6 +313,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
common-tags "^1.7.2"
|
common-tags "^1.7.2"
|
||||||
|
|
||||||
|
"@yarnpkg/lockfile@^1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
|
||||||
|
integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==
|
||||||
|
|
||||||
abbrev@1:
|
abbrev@1:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f"
|
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f"
|
||||||
|
@ -2067,7 +2072,7 @@ cross-spawn@^4:
|
||||||
lru-cache "^4.0.1"
|
lru-cache "^4.0.1"
|
||||||
which "^1.2.9"
|
which "^1.2.9"
|
||||||
|
|
||||||
cross-spawn@^6.0.0:
|
cross-spawn@^6.0.0, cross-spawn@^6.0.5:
|
||||||
version "6.0.5"
|
version "6.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
|
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
|
||||||
integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
|
integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
|
||||||
|
@ -3526,6 +3531,14 @@ find-up@^3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
locate-path "^3.0.0"
|
locate-path "^3.0.0"
|
||||||
|
|
||||||
|
find-yarn-workspace-root@^1.2.1:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-1.2.1.tgz#40eb8e6e7c2502ddfaa2577c176f221422f860db"
|
||||||
|
integrity sha512-dVtfb0WuQG+8Ag2uWkbG79hOUzEsRrhBzgfn86g2sJPkzmcpGdghbNTfUKGTxymFrY/tLIodDzLoW9nOJ4FY8Q==
|
||||||
|
dependencies:
|
||||||
|
fs-extra "^4.0.3"
|
||||||
|
micromatch "^3.1.4"
|
||||||
|
|
||||||
findup-sync@~0.3.0:
|
findup-sync@~0.3.0:
|
||||||
version "0.3.0"
|
version "0.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.3.0.tgz#37930aa5d816b777c03445e1966cc6790a4c0b16"
|
resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.3.0.tgz#37930aa5d816b777c03445e1966cc6790a4c0b16"
|
||||||
|
@ -3684,6 +3697,15 @@ fs-extra@^2.0.0:
|
||||||
graceful-fs "^4.1.2"
|
graceful-fs "^4.1.2"
|
||||||
jsonfile "^2.1.0"
|
jsonfile "^2.1.0"
|
||||||
|
|
||||||
|
fs-extra@^4.0.3:
|
||||||
|
version "4.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94"
|
||||||
|
integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==
|
||||||
|
dependencies:
|
||||||
|
graceful-fs "^4.1.2"
|
||||||
|
jsonfile "^4.0.0"
|
||||||
|
universalify "^0.1.0"
|
||||||
|
|
||||||
fs-extra@^7.0.1:
|
fs-extra@^7.0.1:
|
||||||
version "7.0.1"
|
version "7.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
|
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
|
||||||
|
@ -3916,6 +3938,18 @@ glob@^7.0.3, glob@~7.0.0:
|
||||||
once "^1.3.0"
|
once "^1.3.0"
|
||||||
path-is-absolute "^1.0.0"
|
path-is-absolute "^1.0.0"
|
||||||
|
|
||||||
|
glob@^7.1.3:
|
||||||
|
version "7.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255"
|
||||||
|
integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==
|
||||||
|
dependencies:
|
||||||
|
fs.realpath "^1.0.0"
|
||||||
|
inflight "^1.0.4"
|
||||||
|
inherits "2"
|
||||||
|
minimatch "^3.0.4"
|
||||||
|
once "^1.3.0"
|
||||||
|
path-is-absolute "^1.0.0"
|
||||||
|
|
||||||
glob@~5.0.0:
|
glob@~5.0.0:
|
||||||
version "5.0.15"
|
version "5.0.15"
|
||||||
resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
|
resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
|
||||||
|
@ -5402,6 +5436,13 @@ kind-of@^6.0.0, kind-of@^6.0.2:
|
||||||
version "6.0.2"
|
version "6.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
|
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
|
||||||
|
|
||||||
|
klaw-sync@^6.0.0:
|
||||||
|
version "6.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c"
|
||||||
|
integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==
|
||||||
|
dependencies:
|
||||||
|
graceful-fs "^4.1.11"
|
||||||
|
|
||||||
klaw@^1.0.0:
|
klaw@^1.0.0:
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439"
|
resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439"
|
||||||
|
@ -6935,6 +6976,25 @@ pascalcase@^0.1.1:
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
|
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
|
||||||
|
|
||||||
|
patch-package@6.1.2:
|
||||||
|
version "6.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-6.1.2.tgz#9ed0b3defb5c34ecbef3f334ddfb13e01b3d3ff6"
|
||||||
|
integrity sha512-5GnzR8lEyeleeariG+hGabUnD2b1yL7AIGFjlLo95zMGRWhZCel58IpeKD46wwPb7i+uNhUI8unV56ogk8Bgqg==
|
||||||
|
dependencies:
|
||||||
|
"@yarnpkg/lockfile" "^1.1.0"
|
||||||
|
chalk "^2.4.2"
|
||||||
|
cross-spawn "^6.0.5"
|
||||||
|
find-yarn-workspace-root "^1.2.1"
|
||||||
|
fs-extra "^7.0.1"
|
||||||
|
is-ci "^2.0.0"
|
||||||
|
klaw-sync "^6.0.0"
|
||||||
|
minimist "^1.2.0"
|
||||||
|
rimraf "^2.6.3"
|
||||||
|
semver "^5.6.0"
|
||||||
|
slash "^2.0.0"
|
||||||
|
tmp "^0.0.33"
|
||||||
|
update-notifier "^2.5.0"
|
||||||
|
|
||||||
path-browserify@0.0.0:
|
path-browserify@0.0.0:
|
||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a"
|
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a"
|
||||||
|
@ -8514,6 +8574,13 @@ rimraf@2.6.2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
glob "^7.0.5"
|
glob "^7.0.5"
|
||||||
|
|
||||||
|
rimraf@^2.6.3:
|
||||||
|
version "2.6.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
|
||||||
|
integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
|
||||||
|
dependencies:
|
||||||
|
glob "^7.1.3"
|
||||||
|
|
||||||
rimraf@~2.4.0:
|
rimraf@~2.4.0:
|
||||||
version "2.4.5"
|
version "2.4.5"
|
||||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da"
|
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da"
|
||||||
|
@ -8791,6 +8858,11 @@ slash@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
|
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
|
||||||
|
|
||||||
|
slash@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
|
||||||
|
integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
|
||||||
|
|
||||||
slice-ansi@1.0.0:
|
slice-ansi@1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d"
|
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue