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":
|
||||
"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": {
|
||||
"message": "New messages below",
|
||||
"description":
|
||||
|
|
117
app/sql.js
117
app/sql.js
|
@ -16,6 +16,7 @@ const {
|
|||
isString,
|
||||
last,
|
||||
map,
|
||||
pick,
|
||||
} = require('lodash');
|
||||
|
||||
// To get long stack traces
|
||||
|
@ -93,9 +94,11 @@ module.exports = {
|
|||
getExpiredMessages,
|
||||
getOutgoingWithoutExpiresAt,
|
||||
getNextExpiringMessage,
|
||||
getMessagesByConversation,
|
||||
getNextTapToViewMessageToAgeOut,
|
||||
getTapToViewMessagesNeedingErase,
|
||||
getOlderMessagesByConversation,
|
||||
getNewerMessagesByConversation,
|
||||
getMessageMetricsForConversation,
|
||||
|
||||
getUnprocessedCount,
|
||||
getAllUnprocessed,
|
||||
|
@ -1840,7 +1843,7 @@ async function getUnreadByConversation(conversationId) {
|
|||
return map(rows, row => jsonToObject(row.json));
|
||||
}
|
||||
|
||||
async function getMessagesByConversation(
|
||||
async function getOlderMessagesByConversation(
|
||||
conversationId,
|
||||
{ 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));
|
||||
}
|
||||
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) {
|
||||
const rows = await db.all(
|
||||
|
|
|
@ -71,19 +71,6 @@
|
|||
<div class='lightbox-container'></div>
|
||||
</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'>
|
||||
<a target='_blank' href='https://signal.org/download/'>
|
||||
<button class='upgrade'>{{ upgrade }}</button>
|
||||
|
@ -106,12 +93,7 @@
|
|||
<script type='text/x-tmpl-mustache' id='conversation'>
|
||||
<div class='conversation-header'></div>
|
||||
<div class='main panel'>
|
||||
<div class='discussion-container'>
|
||||
<div class='bar-container hide'>
|
||||
<div class='bar active progress-bar-striped progress-bar'></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='timeline-placeholder'></div>
|
||||
<div class='bottom-bar' id='footer'>
|
||||
<div class='compose'>
|
||||
<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/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/file_input_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/message_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/recorder_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/conversation_view.js'></script>
|
||||
|
|
|
@ -476,6 +476,12 @@
|
|||
const initialState = {
|
||||
conversations: {
|
||||
conversationLookup: Signal.Util.makeLookup(conversations, 'id'),
|
||||
messagesByConversation: {},
|
||||
messagesLookup: {},
|
||||
selectedConversation: null,
|
||||
selectedMessage: null,
|
||||
selectedMessageCounter: 0,
|
||||
showArchived: false,
|
||||
},
|
||||
emojis: Signal.Emojis.getInitialState(),
|
||||
items: storage.getItemsState(),
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
'add remove change:unreadCount',
|
||||
_.debounce(this.updateUnreadCount.bind(this), 1000)
|
||||
);
|
||||
this.startPruning();
|
||||
},
|
||||
addActive(model) {
|
||||
if (model.get('active_at')) {
|
||||
|
@ -44,14 +43,6 @@
|
|||
}
|
||||
window.updateTrayIcon(newUnreadCount);
|
||||
},
|
||||
startPruning() {
|
||||
const halfHour = 30 * 60 * 1000;
|
||||
this.interval = setInterval(() => {
|
||||
this.forEach(conversation => {
|
||||
conversation.trigger('prune');
|
||||
});
|
||||
}, halfHour);
|
||||
},
|
||||
}))();
|
||||
|
||||
window.getInboxCollection = () => inboxCollection;
|
||||
|
|
|
@ -27,13 +27,7 @@
|
|||
};
|
||||
|
||||
const { Util } = window.Signal;
|
||||
const {
|
||||
Conversation,
|
||||
Contact,
|
||||
Errors,
|
||||
Message,
|
||||
PhoneNumber,
|
||||
} = window.Signal.Types;
|
||||
const { Conversation, Contact, Message, PhoneNumber } = window.Signal.Types;
|
||||
const {
|
||||
deleteAttachmentData,
|
||||
getAbsoluteAttachmentPath,
|
||||
|
@ -277,6 +271,7 @@
|
|||
|
||||
this.messageCollection.remove(id);
|
||||
existing.trigger('expired');
|
||||
existing.cleanup();
|
||||
};
|
||||
|
||||
// If a fetch is in progress, then we need to wait until that's complete to
|
||||
|
@ -288,18 +283,33 @@
|
|||
},
|
||||
|
||||
async onNewMessage(message) {
|
||||
await this.updateLastMessage();
|
||||
|
||||
// Clear typing indicator for a given contact if we receive a message from them
|
||||
const identifier = message.get
|
||||
? `${message.get('source')}.${message.get('sourceDevice')}`
|
||||
: `${message.source}.${message.sourceDevice}`;
|
||||
this.clearContactTypingTimer(identifier);
|
||||
|
||||
await this.updateLastMessage();
|
||||
},
|
||||
|
||||
addSingleMessage(message) {
|
||||
const { id } = message;
|
||||
const existing = this.messageCollection.get(id);
|
||||
|
||||
const model = this.messageCollection.add(message, { merge: true });
|
||||
model.setToExpire();
|
||||
|
||||
if (!existing) {
|
||||
const { messagesAdded } = window.reduxActions.conversations;
|
||||
const isNewMessage = true;
|
||||
messagesAdded(
|
||||
this.id,
|
||||
[model.getReduxData()],
|
||||
isNewMessage,
|
||||
document.hasFocus()
|
||||
);
|
||||
}
|
||||
|
||||
return model;
|
||||
},
|
||||
|
||||
|
@ -310,7 +320,12 @@
|
|||
const { format } = PhoneNumber;
|
||||
const regionCode = storage.get('regionCode');
|
||||
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 = {
|
||||
id: this.id,
|
||||
|
@ -321,7 +336,7 @@
|
|||
color,
|
||||
type: this.isPrivate() ? 'direct' : 'group',
|
||||
isMe: this.isMe(),
|
||||
isTyping: typingKeys.length > 0,
|
||||
typingContact: typingContact ? typingContact.format() : null,
|
||||
lastUpdated: this.get('timestamp'),
|
||||
name: this.getName(),
|
||||
profileName: this.getProfileName(),
|
||||
|
@ -894,6 +909,9 @@
|
|||
sendMessage(body, attachments, quote, preview, sticker) {
|
||||
this.clearTypingTimers();
|
||||
|
||||
const { clearUnreadMetrics } = window.reduxActions.conversations;
|
||||
clearUnreadMetrics(this.id);
|
||||
|
||||
const destination = this.id;
|
||||
const expireTimer = this.get('expireTimer');
|
||||
const recipients = this.getRecipients();
|
||||
|
@ -1202,7 +1220,7 @@
|
|||
return;
|
||||
}
|
||||
|
||||
const messages = await window.Signal.Data.getMessagesByConversation(
|
||||
const messages = await window.Signal.Data.getOlderMessagesByConversation(
|
||||
this.id,
|
||||
{ limit: 1, MessageCollection: Whisper.MessageCollection }
|
||||
);
|
||||
|
@ -1310,7 +1328,7 @@
|
|||
model.set({ id });
|
||||
|
||||
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 (receivedAt) {
|
||||
|
@ -1373,7 +1391,7 @@
|
|||
async endSession() {
|
||||
if (this.isPrivate()) {
|
||||
const now = Date.now();
|
||||
const message = this.messageCollection.add({
|
||||
const model = new Whisper.Message({
|
||||
conversationId: this.id,
|
||||
type: 'outgoing',
|
||||
sent_at: now,
|
||||
|
@ -1383,10 +1401,13 @@
|
|||
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.set({ id });
|
||||
model.set({ id });
|
||||
|
||||
const message = MessageController.register(model.id, model);
|
||||
this.addSingleMessage(message);
|
||||
|
||||
const options = this.getSendOptions();
|
||||
message.send(
|
||||
|
@ -1407,7 +1428,7 @@
|
|||
groupUpdate = this.pick(['name', 'avatar', 'members']);
|
||||
}
|
||||
const now = Date.now();
|
||||
const message = this.messageCollection.add({
|
||||
const model = new Whisper.Message({
|
||||
conversationId: this.id,
|
||||
type: 'outgoing',
|
||||
sent_at: now,
|
||||
|
@ -1415,10 +1436,14 @@
|
|||
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.set({ id });
|
||||
|
||||
model.set({ id });
|
||||
|
||||
const message = MessageController.register(model.id, model);
|
||||
this.addSingleMessage(message);
|
||||
|
||||
const options = this.getSendOptions();
|
||||
message.send(
|
||||
|
@ -1443,7 +1468,7 @@
|
|||
Conversation: Whisper.Conversation,
|
||||
});
|
||||
|
||||
const message = this.messageCollection.add({
|
||||
const model = new Whisper.Message({
|
||||
group_update: { left: 'You' },
|
||||
conversationId: this.id,
|
||||
type: 'outgoing',
|
||||
|
@ -1451,10 +1476,13 @@
|
|||
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.set({ id });
|
||||
model.set({ id });
|
||||
|
||||
const message = MessageController.register(model.id, model);
|
||||
this.addSingleMessage(message);
|
||||
|
||||
const options = this.getSendOptions();
|
||||
message.send(
|
||||
|
@ -1830,57 +1858,6 @@
|
|||
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) {
|
||||
return _.contains(this.get('members'), number);
|
||||
},
|
||||
|
@ -1908,10 +1885,6 @@
|
|||
},
|
||||
|
||||
async destroyMessages() {
|
||||
await window.Signal.Data.removeAllMessagesInConversation(this.id, {
|
||||
MessageCollection: Whisper.MessageCollection,
|
||||
});
|
||||
|
||||
this.messageCollection.reset([]);
|
||||
|
||||
this.set({
|
||||
|
@ -1922,6 +1895,10 @@
|
|||
await window.Signal.Data.updateConversation(this.id, this.attributes, {
|
||||
Conversation: Whisper.Conversation,
|
||||
});
|
||||
|
||||
await window.Signal.Data.removeAllMessagesInConversation(this.id, {
|
||||
MessageCollection: Whisper.MessageCollection,
|
||||
});
|
||||
},
|
||||
|
||||
getName() {
|
||||
|
@ -2102,10 +2079,6 @@
|
|||
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) {
|
||||
this.contactTypingTimers[identifier] = this.contactTypingTimers[
|
||||
identifier
|
||||
|
@ -2121,14 +2094,12 @@
|
|||
);
|
||||
if (!record) {
|
||||
// User was not previously typing before. State change!
|
||||
this.trigger('typing-update');
|
||||
this.trigger('change', this);
|
||||
}
|
||||
} else {
|
||||
delete this.contactTypingTimers[identifier];
|
||||
if (record) {
|
||||
// User was previously typing, and is no longer. State change!
|
||||
this.trigger('typing-update');
|
||||
this.trigger('change', this);
|
||||
}
|
||||
}
|
||||
|
@ -2143,7 +2114,6 @@
|
|||
delete this.contactTypingTimers[identifier];
|
||||
|
||||
// User was previously typing, but timed out or we received message. State change!
|
||||
this.trigger('typing-update');
|
||||
this.trigger('change', this);
|
||||
}
|
||||
},
|
||||
|
@ -2155,17 +2125,6 @@
|
|||
comparator(m) {
|
||||
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(' ');
|
||||
|
|
|
@ -100,69 +100,67 @@
|
|||
this.on('expired', this.onExpired);
|
||||
this.setToExpire();
|
||||
|
||||
this.on('change', this.generateProps);
|
||||
this.on('change', this.notifyRedux);
|
||||
},
|
||||
|
||||
const applicableConversationChanges =
|
||||
'change:color change:name change:number change:profileName change:profileAvatar';
|
||||
notifyRedux() {
|
||||
const { messageChanged } = window.reduxActions.conversations;
|
||||
|
||||
const conversation = this.getConversation();
|
||||
const fromContact = this.getIncomingContact();
|
||||
|
||||
this.listenTo(
|
||||
conversation,
|
||||
applicableConversationChanges,
|
||||
this.generateProps
|
||||
);
|
||||
if (fromContact) {
|
||||
this.listenTo(
|
||||
fromContact,
|
||||
applicableConversationChanges,
|
||||
this.generateProps
|
||||
);
|
||||
if (messageChanged) {
|
||||
const conversationId = this.get('conversationId');
|
||||
// Note: The clone is important for triggering a re-run of selectors
|
||||
messageChanged(this.id, conversationId, _.clone(this.attributes));
|
||||
}
|
||||
},
|
||||
|
||||
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
|
||||
generateProps() {
|
||||
getPropsForBubble() {
|
||||
if (this.isUnsupportedMessage()) {
|
||||
this.props = {
|
||||
return {
|
||||
type: 'unsupportedMessage',
|
||||
data: this.getPropsForUnsupportedMessage(),
|
||||
};
|
||||
} else if (this.isExpirationTimerUpdate()) {
|
||||
this.props = {
|
||||
return {
|
||||
type: 'timerNotification',
|
||||
data: this.getPropsForTimerNotification(),
|
||||
};
|
||||
} else if (this.isKeyChange()) {
|
||||
this.props = {
|
||||
return {
|
||||
type: 'safetyNumberNotification',
|
||||
data: this.getPropsForSafetyNumberNotification(),
|
||||
};
|
||||
} else if (this.isVerifiedChange()) {
|
||||
this.props = {
|
||||
return {
|
||||
type: 'verificationNotification',
|
||||
data: this.getPropsForVerificationNotification(),
|
||||
};
|
||||
} else if (this.isGroupUpdate()) {
|
||||
this.props = {
|
||||
return {
|
||||
type: 'groupNotification',
|
||||
data: this.getPropsForGroupNotification(),
|
||||
};
|
||||
} else if (this.isEndSession()) {
|
||||
this.props = {
|
||||
return {
|
||||
type: 'resetSessionNotification',
|
||||
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
|
||||
|
@ -269,6 +267,21 @@
|
|||
disableScroll: true,
|
||||
// To ensure that group avatar doesn't show up
|
||||
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,
|
||||
contacts: sortedContacts,
|
||||
|
@ -290,7 +303,7 @@
|
|||
const flag =
|
||||
textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return !!(this.get('flags') & flag);
|
||||
return Boolean(this.get('flags') & flag);
|
||||
},
|
||||
isKeyChange() {
|
||||
return this.get('type') === 'keychange';
|
||||
|
@ -353,12 +366,10 @@
|
|||
const conversation = this.getConversation();
|
||||
const isGroup = conversation && !conversation.isPrivate();
|
||||
const phoneNumber = this.get('key_changed');
|
||||
const showIdentity = id => this.trigger('show-identity', id);
|
||||
|
||||
return {
|
||||
isGroup,
|
||||
contact: this.findAndFormatContact(phoneNumber),
|
||||
showIdentity,
|
||||
};
|
||||
},
|
||||
getPropsForVerificationNotification() {
|
||||
|
@ -498,28 +509,6 @@
|
|||
isTapToViewExpired: isTapToView && this.get('isErased'),
|
||||
isTapToViewError:
|
||||
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,
|
||||
authorColor,
|
||||
referencedMessageNotFound,
|
||||
onClick: () => this.trigger('scroll-to-message'),
|
||||
};
|
||||
},
|
||||
getStatus(number) {
|
||||
|
@ -851,6 +841,8 @@
|
|||
this.cleanup();
|
||||
},
|
||||
async cleanup() {
|
||||
const { messageDeleted } = window.reduxActions.conversations;
|
||||
messageDeleted(this.id, this.get('conversationId'));
|
||||
MessageController.unregister(this.id);
|
||||
this.unload();
|
||||
await this.deleteData();
|
||||
|
@ -2193,74 +2185,5 @@
|
|||
|
||||
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) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const collection = await window.Signal.Data.getMessagesByConversation(
|
||||
const collection = await window.Signal.Data.getOlderMessagesByConversation(
|
||||
conversation.id,
|
||||
{
|
||||
limit: CHUNK_SIZE,
|
||||
|
|
|
@ -121,9 +121,11 @@ module.exports = {
|
|||
getExpiredMessages,
|
||||
getOutgoingWithoutExpiresAt,
|
||||
getNextExpiringMessage,
|
||||
getMessagesByConversation,
|
||||
getNextTapToViewMessageToAgeOut,
|
||||
getTapToViewMessagesNeedingErase,
|
||||
getOlderMessagesByConversation,
|
||||
getNewerMessagesByConversation,
|
||||
getMessageMetricsForConversation,
|
||||
|
||||
getUnprocessedCount,
|
||||
getAllUnprocessed,
|
||||
|
@ -779,17 +781,40 @@ async function getUnreadByConversation(conversationId, { MessageCollection }) {
|
|||
return new MessageCollection(messages);
|
||||
}
|
||||
|
||||
async function getMessagesByConversation(
|
||||
async function getOlderMessagesByConversation(
|
||||
conversationId,
|
||||
{ limit = 100, receivedAt = Number.MAX_VALUE, MessageCollection }
|
||||
) {
|
||||
const messages = await channels.getMessagesByConversation(conversationId, {
|
||||
limit,
|
||||
receivedAt,
|
||||
});
|
||||
const messages = await channels.getOlderMessagesByConversation(
|
||||
conversationId,
|
||||
{
|
||||
limit,
|
||||
receivedAt,
|
||||
}
|
||||
);
|
||||
|
||||
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(
|
||||
conversationId,
|
||||
|
@ -800,7 +825,7 @@ async function removeAllMessagesInConversation(
|
|||
// Yes, we really want the await in the loop. We're deleting 100 at a
|
||||
// time so we don't use too much memory.
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
messages = await getMessagesByConversation(conversationId, {
|
||||
messages = await getOlderMessagesByConversation(conversationId, {
|
||||
limit: 100,
|
||||
MessageCollection,
|
||||
});
|
||||
|
|
|
@ -28,51 +28,25 @@ const {
|
|||
ContactDetail,
|
||||
} = require('../../ts/components/conversation/ContactDetail');
|
||||
const { ContactListItem } = require('../../ts/components/ContactListItem');
|
||||
const { ContactName } = require('../../ts/components/conversation/ContactName');
|
||||
const {
|
||||
ConversationHeader,
|
||||
} = require('../../ts/components/conversation/ConversationHeader');
|
||||
const {
|
||||
EmbeddedContact,
|
||||
} = require('../../ts/components/conversation/EmbeddedContact');
|
||||
const { Emojify } = require('../../ts/components/conversation/Emojify');
|
||||
const {
|
||||
GroupNotification,
|
||||
} = require('../../ts/components/conversation/GroupNotification');
|
||||
const { Lightbox } = require('../../ts/components/Lightbox');
|
||||
const { LightboxGallery } = require('../../ts/components/LightboxGallery');
|
||||
const {
|
||||
MediaGallery,
|
||||
} = require('../../ts/components/conversation/media-gallery/MediaGallery');
|
||||
const { Message } = require('../../ts/components/conversation/Message');
|
||||
const { MessageBody } = require('../../ts/components/conversation/MessageBody');
|
||||
const {
|
||||
MessageDetail,
|
||||
} = require('../../ts/components/conversation/MessageDetail');
|
||||
const { Quote } = require('../../ts/components/conversation/Quote');
|
||||
const {
|
||||
ResetSessionNotification,
|
||||
} = require('../../ts/components/conversation/ResetSessionNotification');
|
||||
const {
|
||||
SafetyNumberNotification,
|
||||
} = require('../../ts/components/conversation/SafetyNumberNotification');
|
||||
const {
|
||||
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
|
||||
const { createTimeline } = require('../../ts/state/roots/createTimeline');
|
||||
const {
|
||||
createCompositionArea,
|
||||
} = require('../../ts/state/roots/createCompositionArea');
|
||||
|
@ -264,33 +238,23 @@ exports.setup = (options = {}) => {
|
|||
CaptionEditor,
|
||||
ContactDetail,
|
||||
ContactListItem,
|
||||
ContactName,
|
||||
ConversationHeader,
|
||||
EmbeddedContact,
|
||||
Emojify,
|
||||
GroupNotification,
|
||||
Lightbox,
|
||||
LightboxGallery,
|
||||
MediaGallery,
|
||||
Message,
|
||||
MessageBody,
|
||||
MessageDetail,
|
||||
Quote,
|
||||
ResetSessionNotification,
|
||||
SafetyNumberNotification,
|
||||
StagedLinkPreview,
|
||||
TimerNotification,
|
||||
Types: {
|
||||
Message: MediaGalleryMessage,
|
||||
},
|
||||
TypingBubble,
|
||||
UnsupportedMessage,
|
||||
VerificationNotification,
|
||||
};
|
||||
|
||||
const Roots = {
|
||||
createCompositionArea,
|
||||
createLeftPane,
|
||||
createTimeline,
|
||||
createStickerManager,
|
||||
createStickerPreviewModal,
|
||||
};
|
||||
|
|
|
@ -152,7 +152,7 @@
|
|||
silent: !status.shouldPlayNotificationSound,
|
||||
});
|
||||
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
|
||||
// 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({
|
||||
className: 'conversation-stack',
|
||||
lastConversation: null,
|
||||
open(conversation) {
|
||||
open(conversation, messageId) {
|
||||
const id = `conversation-${conversation.cid}`;
|
||||
if (id !== this.el.firstChild.id) {
|
||||
this.$el
|
||||
.first()
|
||||
.find('video, audio')
|
||||
.each(function pauseMedia() {
|
||||
this.pause();
|
||||
});
|
||||
let $el = this.$(`#${id}`);
|
||||
if ($el === null || $el.length === 0) {
|
||||
const view = new Whisper.ConversationView({
|
||||
model: conversation,
|
||||
window: this.model.window,
|
||||
});
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
$el = view.$el;
|
||||
if (id !== this.el.lastChild.id) {
|
||||
const view = new Whisper.ConversationView({
|
||||
model: conversation,
|
||||
window: this.model.window,
|
||||
});
|
||||
view.$el.appendTo(this.el);
|
||||
|
||||
if (this.lastConversation) {
|
||||
this.lastConversation.trigger(
|
||||
'unload',
|
||||
'opened another conversation'
|
||||
);
|
||||
}
|
||||
$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
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
},
|
||||
|
@ -122,11 +119,10 @@
|
|||
},
|
||||
setupLeftPane() {
|
||||
this.leftPaneView = new Whisper.ReactWrapperView({
|
||||
JSX: Signal.State.Roots.createLeftPane(window.reduxStore),
|
||||
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);
|
||||
},
|
||||
startConnectionListener() {
|
||||
|
@ -194,7 +190,7 @@
|
|||
openConversationExternal(id, messageId);
|
||||
}
|
||||
|
||||
this.conversation_stack.open(conversation);
|
||||
this.conversation_stack.open(conversation, messageId);
|
||||
this.focusConversation();
|
||||
},
|
||||
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",
|
||||
"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 .",
|
||||
"grunt": "grunt",
|
||||
"icon-gen": "electron-icon-maker --input=images/icon_1024.png --output=./build",
|
||||
|
@ -169,6 +169,7 @@
|
|||
"mocha-testcheck": "1.0.0-rc.0",
|
||||
"node-sass-import-once": "1.2.0",
|
||||
"nyc": "11.4.1",
|
||||
"patch-package": "6.1.2",
|
||||
"prettier": "1.12.0",
|
||||
"react-docgen-typescript": "1.2.6",
|
||||
"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() {
|
||||
background-color: $color-black;
|
||||
background-color: $color-gray-95;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,24 +66,21 @@
|
|||
}
|
||||
|
||||
.main.panel {
|
||||
.discussion-container {
|
||||
.timeline-placeholder {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.bar-container {
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
.timeline-wrapper {
|
||||
-webkit-padding-start: 0px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 10px 0 0 0;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
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 {
|
||||
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 {
|
||||
display: flex;
|
||||
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%;
|
||||
}
|
||||
|
||||
.conversation-stack {
|
||||
.conversation {
|
||||
display: none;
|
||||
}
|
||||
.conversation:first-child {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.tool-bar {
|
||||
color: $color-light-90;
|
||||
|
||||
|
|
|
@ -8,6 +8,22 @@
|
|||
|
||||
// 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 {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
|
@ -39,15 +55,19 @@
|
|||
|
||||
// Spec: container < 438px
|
||||
.module-message--incoming {
|
||||
margin-left: 0;
|
||||
margin-left: 16px;
|
||||
margin-right: 32px;
|
||||
}
|
||||
.module-message--outgoing {
|
||||
float: right;
|
||||
margin-right: 0;
|
||||
margin-right: 16px;
|
||||
margin-left: 32px;
|
||||
}
|
||||
|
||||
.module-message--incoming.module-message--group {
|
||||
margin-left: 44px;
|
||||
}
|
||||
|
||||
.module-message__buttons {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -165,6 +185,37 @@
|
|||
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
|
||||
.module-message__container--incoming {
|
||||
background-color: $color-conversation-grey;
|
||||
|
@ -704,10 +755,10 @@
|
|||
|
||||
.module-message__metadata__status-icon--sending {
|
||||
@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% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
|
@ -842,6 +893,9 @@
|
|||
}
|
||||
|
||||
.module-quote {
|
||||
// To leave room for image thumbnail
|
||||
min-height: 54px;
|
||||
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
border-top-left-radius: 10px;
|
||||
|
@ -1286,6 +1340,8 @@
|
|||
|
||||
.module-group-notification {
|
||||
margin-top: 14px;
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.3px;
|
||||
|
@ -2420,6 +2476,13 @@
|
|||
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 {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
|
@ -2999,9 +3062,16 @@
|
|||
// 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.
|
||||
|
||||
.module-spinner__container--small {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.module-spinner__circle--small {
|
||||
-webkit-mask: url('../images/spinner-track-24.svg') no-repeat center;
|
||||
-webkit-mask-size: 100%;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
.module-spinner__arc--small {
|
||||
-webkit-mask: url('../images/spinner-24.svg') no-repeat center;
|
||||
|
@ -3023,6 +3093,8 @@
|
|||
.module-spinner__arc--mini {
|
||||
-webkit-mask: url('../images/spinner-24.svg') no-repeat center;
|
||||
-webkit-mask-size: 100%;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.module-spinner__circle--incoming {
|
||||
|
@ -3032,6 +3104,13 @@
|
|||
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-message-body__highlight {
|
||||
|
@ -3306,10 +3385,31 @@
|
|||
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 {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.module-timeline__message-container {
|
||||
|
@ -4686,13 +4786,35 @@
|
|||
|
||||
.module-countdown {
|
||||
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;
|
||||
stroke: $color-white;
|
||||
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
|
||||
|
@ -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
|
||||
|
||||
.react-contextmenu {
|
||||
|
@ -5016,11 +5202,9 @@
|
|||
|
||||
// Spec: container < 438px
|
||||
.module-message--incoming {
|
||||
margin-left: 0;
|
||||
margin-right: auto;
|
||||
}
|
||||
.module-message--outgoing {
|
||||
margin-right: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
|
@ -5051,11 +5235,9 @@
|
|||
}
|
||||
|
||||
.module-message--incoming {
|
||||
margin-left: 0;
|
||||
margin-right: auto;
|
||||
}
|
||||
.module-message--outgoing {
|
||||
margin-right: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
|
|
|
@ -1509,6 +1509,13 @@ body.dark-theme {
|
|||
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 {
|
||||
|
|
|
@ -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/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/message_list_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/group_member_list_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/recorder_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/conversation_view.js' data-cover></script>
|
||||
|
@ -497,19 +495,14 @@
|
|||
<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/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/clear_data_view.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/timestamp_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/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>
|
||||
|
||||
|
|
|
@ -34,17 +34,19 @@ describe('KeyChangeListener', () => {
|
|||
});
|
||||
|
||||
after(async () => {
|
||||
await convo.destroyMessages();
|
||||
await window.Signal.Data.removeAllMessagesInConversation(convo.id, {
|
||||
MessageCollection: Whisper.MessageCollection,
|
||||
});
|
||||
await window.Signal.Data.saveConversation(convo.id);
|
||||
});
|
||||
|
||||
it('generates a key change notice in the private conversation with this contact', done => {
|
||||
convo.once('newmessage', async () => {
|
||||
await convo.fetchMessages();
|
||||
const message = convo.messageCollection.at(0);
|
||||
assert.strictEqual(message.get('type'), 'keychange');
|
||||
const original = convo.addKeyChange;
|
||||
convo.addKeyChange = keyChangedId => {
|
||||
assert.equal(address.getName(), keyChangedId);
|
||||
convo.addKeyChange = original;
|
||||
done();
|
||||
});
|
||||
};
|
||||
store.saveIdentity(address.toString(), newKey);
|
||||
});
|
||||
});
|
||||
|
@ -62,17 +64,20 @@ describe('KeyChangeListener', () => {
|
|||
});
|
||||
});
|
||||
after(async () => {
|
||||
await convo.destroyMessages();
|
||||
await window.Signal.Data.removeAllMessagesInConversation(convo.id, {
|
||||
MessageCollection: Whisper.MessageCollection,
|
||||
});
|
||||
await window.Signal.Data.saveConversation(convo.id);
|
||||
});
|
||||
|
||||
it('generates a key change notice in the group conversation with this contact', done => {
|
||||
convo.once('newmessage', async () => {
|
||||
await convo.fetchMessages();
|
||||
const message = convo.messageCollection.at(0);
|
||||
assert.strictEqual(message.get('type'), 'keychange');
|
||||
const original = convo.addKeyChange;
|
||||
convo.addKeyChange = keyChangedId => {
|
||||
assert.equal(address.getName(), keyChangedId);
|
||||
convo.addKeyChange = original;
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
store.saveIdentity(address.toString(), newKey);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -72,22 +72,6 @@ describe('Conversation', () => {
|
|||
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 () => {
|
||||
const convo = new Whisper.ConversationCollection().add({
|
||||
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'}
|
||||
unreadCount={4}
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
isTyping={true}
|
||||
typingContact={{
|
||||
name: 'Someone Here',
|
||||
}}
|
||||
onClick={result => console.log('onClick', result)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
|
@ -164,7 +166,9 @@
|
|||
conversationType={'direct'}
|
||||
unreadCount={4}
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
isTyping={true}
|
||||
typingContact={{
|
||||
name: 'Someone Here',
|
||||
}}
|
||||
lastMessage={{
|
||||
status: 'read',
|
||||
}}
|
||||
|
|
|
@ -23,7 +23,7 @@ export type PropsData = {
|
|||
unreadCount: number;
|
||||
isSelected: boolean;
|
||||
|
||||
isTyping: boolean;
|
||||
typingContact?: Object;
|
||||
lastMessage?: {
|
||||
status: 'sending' | 'sent' | 'delivered' | 'read' | 'error';
|
||||
text: string;
|
||||
|
@ -134,8 +134,8 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
|||
}
|
||||
|
||||
public renderMessage() {
|
||||
const { lastMessage, isTyping, unreadCount, i18n } = this.props;
|
||||
if (!lastMessage && !isTyping) {
|
||||
const { lastMessage, typingContact, unreadCount, i18n } = this.props;
|
||||
if (!lastMessage && !typingContact) {
|
||||
return null;
|
||||
}
|
||||
const text = lastMessage && lastMessage.text ? lastMessage.text : '';
|
||||
|
@ -150,15 +150,22 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
|||
: null
|
||||
)}
|
||||
>
|
||||
{isTyping ? (
|
||||
{typingContact ? (
|
||||
<TypingAnimation i18n={i18n} />
|
||||
) : (
|
||||
<MessageBody
|
||||
text={text}
|
||||
disableJumbomoji={true}
|
||||
disableLinks={true}
|
||||
i18n={i18n}
|
||||
/>
|
||||
<>
|
||||
{shouldShowDraft ? (
|
||||
<span className="module-conversation-list-item__message__draft-prefix">
|
||||
{i18n('ConversationListItem--draft-prefix')}
|
||||
</span>
|
||||
) : null}
|
||||
<MessageBody
|
||||
text={text}
|
||||
disableJumbomoji={true}
|
||||
disableLinks={true}
|
||||
i18n={i18n}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{lastMessage && lastMessage.status ? (
|
||||
|
|
|
@ -73,16 +73,25 @@ export class Countdown extends React.Component<Props, State> {
|
|||
const strokeDashoffset = ratio * CIRCUMFERENCE;
|
||||
|
||||
return (
|
||||
<svg className="module-countdown" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12,1 A11,11,0,1,1,1,12,11.013,11.013,0,0,1,12,1Z"
|
||||
className="module-countdown__path"
|
||||
style={{
|
||||
strokeDasharray: `${CIRCUMFERENCE}, ${CIRCUMFERENCE}`,
|
||||
strokeDashoffset,
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
<div className="module-countdown">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12,1 A11,11,0,1,1,1,12,11.013,11.013,0,0,1,12,1Z"
|
||||
className="module-countdown__back-path"
|
||||
style={{
|
||||
strokeDasharray: `${CIRCUMFERENCE}, ${CIRCUMFERENCE}`,
|
||||
}}
|
||||
/>
|
||||
<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,
|
||||
PropsData as ConversationListItemPropsType,
|
||||
} from './ConversationListItem';
|
||||
import {
|
||||
MessageSearchResult,
|
||||
PropsData as MessageSearchResultPropsType,
|
||||
} from './MessageSearchResult';
|
||||
import { MessageSearchResult } from './MessageSearchResult';
|
||||
import { StartNewConversation } from './StartNewConversation';
|
||||
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
|
|
@ -4,7 +4,7 @@ import classNames from 'classnames';
|
|||
interface Props {
|
||||
size?: string;
|
||||
svgSize: 'small' | 'normal';
|
||||
direction?: string;
|
||||
direction?: 'outgoing' | 'incoming' | 'on-background';
|
||||
}
|
||||
|
||||
export class Spinner extends React.Component<Props> {
|
||||
|
|
|
@ -23,7 +23,7 @@ const contact = {
|
|||
signalAccount: '+12025550000',
|
||||
};
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
|
@ -31,8 +31,8 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
|
@ -41,8 +41,8 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
|
@ -51,8 +51,8 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
|
@ -62,7 +62,7 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
|
@ -89,7 +89,7 @@ const contact = {
|
|||
signalAccount: '+12025550000',
|
||||
};
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
|
@ -97,8 +97,8 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
|
@ -107,7 +107,7 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
|
@ -131,15 +131,15 @@ const contact = {
|
|||
},
|
||||
};
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
i18n={util.i18n}
|
||||
timestamp={Date.now()}
|
||||
contact={contact}/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
|
@ -147,7 +147,7 @@ const contact = {
|
|||
i18n={util.i18n}
|
||||
timestamp={Date.now()}
|
||||
contact={contact}/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
|
@ -171,8 +171,8 @@ const contact = {
|
|||
},
|
||||
signalAccount: '+12025550000',
|
||||
};
|
||||
<util.ConversationContext theme={util.theme} type="group" ios={util.ios}>
|
||||
<li>
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
conversationType="group"
|
||||
|
@ -183,8 +183,8 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
|
@ -195,8 +195,8 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
|
@ -207,7 +207,7 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
|
@ -231,7 +231,7 @@ const contact = {
|
|||
},
|
||||
};
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
|
@ -239,8 +239,8 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
|
@ -249,8 +249,8 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
|
@ -259,8 +259,8 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
|
@ -270,7 +270,7 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
|
@ -292,7 +292,7 @@ const contact = {
|
|||
},
|
||||
};
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
|
@ -300,8 +300,8 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
|
@ -310,8 +310,8 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
|
@ -320,8 +320,8 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
|
@ -331,7 +331,7 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
|
@ -356,7 +356,7 @@ const contact = {
|
|||
signalAccount: '+12025551000',
|
||||
};
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
|
@ -364,8 +364,8 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
|
@ -374,8 +374,8 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
|
@ -384,8 +384,8 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
|
@ -395,7 +395,7 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
|
@ -414,7 +414,7 @@ const contact = {
|
|||
],
|
||||
};
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
|
@ -422,8 +422,8 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
|
@ -432,8 +432,8 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
|
@ -442,8 +442,8 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
|
@ -453,7 +453,7 @@ const contact = {
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
|
@ -462,7 +462,7 @@ const contact = {
|
|||
```jsx
|
||||
const contact = {};
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
|
@ -470,8 +470,8 @@ const contact = {};
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
|
@ -480,8 +480,8 @@ const contact = {};
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="incoming"
|
||||
|
@ -490,8 +490,8 @@ const contact = {};
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="green"
|
||||
direction="outgoing"
|
||||
|
@ -501,7 +501,7 @@ const contact = {};
|
|||
timestamp={Date.now()}
|
||||
contact={contact}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
||||
|
@ -542,7 +542,7 @@ const contactWithoutAccount = {
|
|||
},
|
||||
};
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
text="I want to introduce you to Someone..."
|
||||
authorColor="green"
|
||||
|
@ -551,8 +551,8 @@ const contactWithoutAccount = {
|
|||
timestamp={Date.now()}
|
||||
contact={contactWithAccount}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
text="I want to introduce you to Someone..."
|
||||
authorColor="green"
|
||||
|
@ -562,8 +562,8 @@ const contactWithoutAccount = {
|
|||
timestamp={Date.now()}
|
||||
contact={contactWithAccount}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
text="I want to introduce you to Someone..."
|
||||
authorColor="green"
|
||||
|
@ -572,8 +572,8 @@ const contactWithoutAccount = {
|
|||
timestamp={Date.now()}
|
||||
contact={contactWithAccount}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
text="I want to introduce you to Someone..."
|
||||
authorColor="green"
|
||||
|
@ -583,8 +583,8 @@ const contactWithoutAccount = {
|
|||
timestamp={Date.now()}
|
||||
contact={contactWithAccount}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
text="I want to introduce you to Someone..."
|
||||
authorColor="green"
|
||||
|
@ -594,8 +594,8 @@ const contactWithoutAccount = {
|
|||
timestamp={Date.now()}
|
||||
contact={contactWithoutAccount}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
text="I want to introduce you to Someone..."
|
||||
authorColor="green"
|
||||
|
@ -606,8 +606,8 @@ const contactWithoutAccount = {
|
|||
timestamp={Date.now()}
|
||||
contact={contactWithoutAccount}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
text="I want to introduce you to Someone..."
|
||||
authorColor="green"
|
||||
|
@ -617,8 +617,8 @@ const contactWithoutAccount = {
|
|||
timestamp={Date.now()}
|
||||
contact={contactWithoutAccount}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
text="I want to introduce you to Someone..."
|
||||
authorColor="green"
|
||||
|
@ -629,6 +629,6 @@ const contactWithoutAccount = {
|
|||
timestamp={Date.now()}
|
||||
contact={contactWithoutAccount}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="outgoing"
|
||||
|
@ -13,8 +13,8 @@
|
|||
expirationLength={10 * 1000}
|
||||
expirationTimestamp={Date.now() + 10 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="outgoing"
|
||||
status="delivered"
|
||||
|
@ -25,8 +25,8 @@
|
|||
expirationLength={30 * 1000}
|
||||
expirationTimestamp={Date.now() + 30 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="outgoing"
|
||||
|
@ -37,8 +37,8 @@
|
|||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() + 55 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="outgoing"
|
||||
|
@ -49,7 +49,7 @@
|
|||
expirationLength={5 * 60 * 1000}
|
||||
expirationTimestamp={Date.now() + 5 * 60 * 1000}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
|
@ -57,7 +57,7 @@
|
|||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="incoming"
|
||||
|
@ -67,8 +67,8 @@
|
|||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() + 60 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="outgoing"
|
||||
|
@ -79,8 +79,8 @@
|
|||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() + 60 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="incoming"
|
||||
|
@ -90,8 +90,8 @@
|
|||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() + 55 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="outgoing"
|
||||
|
@ -102,8 +102,8 @@
|
|||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() + 55 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="incoming"
|
||||
|
@ -113,8 +113,8 @@
|
|||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() + 30 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="outgoing"
|
||||
|
@ -125,8 +125,8 @@
|
|||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() + 30 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="incoming"
|
||||
|
@ -136,8 +136,8 @@
|
|||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() + 5 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="outgoing"
|
||||
|
@ -148,8 +148,8 @@
|
|||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() + 5 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="incoming"
|
||||
|
@ -159,8 +159,8 @@
|
|||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now()}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="outgoing"
|
||||
|
@ -171,8 +171,8 @@
|
|||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now()}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="incoming"
|
||||
|
@ -182,8 +182,8 @@
|
|||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() + 120 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="outgoing"
|
||||
|
@ -194,8 +194,8 @@
|
|||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() + 120 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="incoming"
|
||||
|
@ -205,8 +205,8 @@
|
|||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() - 20 * 1000}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
authorColor="blue"
|
||||
direction="outgoing"
|
||||
|
@ -217,6 +217,6 @@
|
|||
expirationLength={60 * 1000}
|
||||
expirationTimestamp={Date.now() - 20 * 1000}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
|
|
@ -15,6 +15,7 @@ interface Props {
|
|||
|
||||
overlayText?: string;
|
||||
|
||||
isSelected?: boolean;
|
||||
noBorder?: boolean;
|
||||
noBackground?: boolean;
|
||||
bottomOverlay?: boolean;
|
||||
|
@ -51,6 +52,7 @@ export class Image extends React.Component<Props> {
|
|||
darkOverlay,
|
||||
height,
|
||||
i18n,
|
||||
isSelected,
|
||||
noBackground,
|
||||
noBorder,
|
||||
onClick,
|
||||
|
@ -118,7 +120,7 @@ export class Image extends React.Component<Props> {
|
|||
alt={i18n('imageCaptionIconAlt')}
|
||||
/>
|
||||
) : null}
|
||||
{!noBorder ? (
|
||||
{!noBorder || isSelected ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-image__border-overlay',
|
||||
|
@ -128,7 +130,8 @@ export class Image extends React.Component<Props> {
|
|||
curveBottomRight ? 'module-image--curved-bottom-right' : null,
|
||||
smallCurveTopLeft ? 'module-image--small-curved-top-left' : 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}
|
||||
|
|
|
@ -21,6 +21,7 @@ interface Props {
|
|||
withContentBelow?: boolean;
|
||||
bottomOverlay?: boolean;
|
||||
isSticker?: boolean;
|
||||
isSelected?: boolean;
|
||||
stickerSize?: number;
|
||||
|
||||
i18n: LocalizerType;
|
||||
|
@ -37,6 +38,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
bottomOverlay,
|
||||
i18n,
|
||||
isSticker,
|
||||
isSelected,
|
||||
stickerSize,
|
||||
onError,
|
||||
onClick,
|
||||
|
@ -83,6 +85,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
curveBottomRight={curveBottomRight}
|
||||
attachment={attachments[0]}
|
||||
playIconOverlay={isVideoAttachment(attachments[0])}
|
||||
isSelected={isSelected}
|
||||
height={finalHeight}
|
||||
width={finalWidth}
|
||||
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
|
||||
const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200;
|
||||
const STICKER_SIZE = 128;
|
||||
const SELECTED_TIMEOUT = 1000;
|
||||
|
||||
interface LinkPreviewType {
|
||||
title: string;
|
||||
|
@ -54,6 +55,8 @@ export type PropsData = {
|
|||
text?: string;
|
||||
textPending?: boolean;
|
||||
isSticker: boolean;
|
||||
isSelected: boolean;
|
||||
isSelectedCounter: number;
|
||||
direction: 'incoming' | 'outgoing';
|
||||
timestamp: number;
|
||||
status?: 'sending' | 'sent' | 'delivered' | 'read' | 'error';
|
||||
|
@ -97,6 +100,8 @@ type PropsHousekeeping = {
|
|||
};
|
||||
|
||||
export type PropsActions = {
|
||||
clearSelectedMessage: () => unknown;
|
||||
|
||||
replyToMessage: (id: string) => void;
|
||||
retrySend: (id: string) => void;
|
||||
deleteMessage: (id: string) => void;
|
||||
|
@ -120,11 +125,10 @@ export type PropsActions = {
|
|||
displayTapToViewMessage: (messageId: string) => unknown;
|
||||
|
||||
openLink: (url: string) => void;
|
||||
scrollToMessage: (
|
||||
scrollToQuotedMessage: (
|
||||
options: {
|
||||
author: string;
|
||||
sentAt: number;
|
||||
referencedMessageNotFound: boolean;
|
||||
}
|
||||
) => void;
|
||||
};
|
||||
|
@ -135,6 +139,9 @@ interface State {
|
|||
expiring: boolean;
|
||||
expired: boolean;
|
||||
imageBroken: boolean;
|
||||
|
||||
isSelected: boolean;
|
||||
prevSelectedCounter: number;
|
||||
}
|
||||
|
||||
const EXPIRATION_CHECK_MINIMUM = 2000;
|
||||
|
@ -148,6 +155,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
public menuTriggerRef: Trigger | undefined;
|
||||
public expirationCheckInterval: any;
|
||||
public expiredTimeout: any;
|
||||
public selectedTimeout: any;
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
|
@ -160,10 +168,30 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
expiring: false,
|
||||
expired: 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() {
|
||||
this.startSelectedTimer();
|
||||
|
||||
const { expirationLength } = this.props;
|
||||
if (!expirationLength) {
|
||||
return;
|
||||
|
@ -180,6 +208,9 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.selectedTimeout) {
|
||||
clearInterval(this.selectedTimeout);
|
||||
}
|
||||
if (this.expirationCheckInterval) {
|
||||
clearInterval(this.expirationCheckInterval);
|
||||
}
|
||||
|
@ -189,9 +220,26 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
this.startSelectedTimer();
|
||||
|
||||
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() {
|
||||
const now = Date.now();
|
||||
const { isExpired, expirationTimestamp, expirationLength } = this.props;
|
||||
|
@ -379,7 +427,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
isSticker,
|
||||
text,
|
||||
} = this.props;
|
||||
const { imageBroken } = this.state;
|
||||
const { imageBroken, isSelected } = this.state;
|
||||
|
||||
if (!attachments || !attachments[0]) {
|
||||
return null;
|
||||
|
@ -422,6 +470,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
withContentAbove={isSticker || withContentAbove}
|
||||
withContentBelow={isSticker || withContentBelow}
|
||||
isSticker={isSticker}
|
||||
isSelected={isSticker && isSelected}
|
||||
stickerSize={STICKER_SIZE}
|
||||
bottomOverlay={bottomOverlay}
|
||||
i18n={i18n}
|
||||
|
@ -622,7 +671,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
disableScroll,
|
||||
i18n,
|
||||
quote,
|
||||
scrollToMessage,
|
||||
scrollToQuotedMessage,
|
||||
} = this.props;
|
||||
|
||||
if (!quote) {
|
||||
|
@ -633,15 +682,14 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
conversationType === 'group' && direction === 'incoming';
|
||||
const quoteColor =
|
||||
direction === 'incoming' ? authorColor : quote.authorColor;
|
||||
|
||||
const { referencedMessageNotFound } = quote;
|
||||
|
||||
const clickHandler = disableScroll
|
||||
? undefined
|
||||
: () => {
|
||||
scrollToMessage({
|
||||
scrollToQuotedMessage({
|
||||
author: quote.authorId,
|
||||
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
|
||||
public render() {
|
||||
const {
|
||||
authorPhoneNumber,
|
||||
authorColor,
|
||||
attachments,
|
||||
conversationType,
|
||||
direction,
|
||||
displayTapToViewMessage,
|
||||
id,
|
||||
|
@ -1211,6 +1271,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
timestamp,
|
||||
} = this.props;
|
||||
const { expired, expiring, imageBroken } = this.state;
|
||||
|
||||
const isAttachmentPending = this.isAttachmentPending();
|
||||
const isButton = isTapToView && !isTapToViewExpired && !isAttachmentPending;
|
||||
|
||||
|
@ -1236,7 +1297,8 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
className={classNames(
|
||||
'module-message',
|
||||
`module-message--${direction}`,
|
||||
expiring ? 'module-message--expired' : null
|
||||
expiring ? 'module-message--expired' : null,
|
||||
conversationType === 'group' ? 'module-message--group' : null
|
||||
)}
|
||||
>
|
||||
{this.renderError(direction === 'incoming')}
|
||||
|
@ -1271,6 +1333,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
>
|
||||
{this.renderAuthor()}
|
||||
{this.renderContents()}
|
||||
{this.renderSelectionHighlight()}
|
||||
{this.renderAvatar()}
|
||||
</div>
|
||||
{this.renderError(direction === 'outgoing')}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,4 @@
|
|||
import React from 'react';
|
||||
// import classNames from 'classnames';
|
||||
|
||||
import { ContactName } from './ContactName';
|
||||
import { Intl } from '../Intl';
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
### None
|
||||
### No new messages
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<ScrollDownButton
|
||||
count={0}
|
||||
withNewMessages={false}
|
||||
conversationId="id-1"
|
||||
scrollDown={id => console.log('scrollDown', id)}
|
||||
i18n={util.i18n}
|
||||
|
@ -11,28 +11,15 @@
|
|||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
### One
|
||||
### With new messages
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<ScrollDownButton
|
||||
count={1}
|
||||
withNewMessages={true}
|
||||
conversationId="id-2"
|
||||
scrollDown={id => console.log('scrollDown', id)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
### More than one
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<ScrollDownButton
|
||||
count={2}
|
||||
conversationId="id-3"
|
||||
scrollDown={id => console.log('scrollDown', id)}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
|
|
@ -4,7 +4,7 @@ import classNames from 'classnames';
|
|||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
type Props = {
|
||||
count: number;
|
||||
withNewMessages: boolean;
|
||||
conversationId: string;
|
||||
|
||||
scrollDown: (conversationId: string) => void;
|
||||
|
@ -14,21 +14,17 @@ type Props = {
|
|||
|
||||
export class ScrollDownButton extends React.Component<Props> {
|
||||
public render() {
|
||||
const { conversationId, count, i18n, scrollDown } = this.props;
|
||||
|
||||
let altText = i18n('scrollDown');
|
||||
if (count > 1) {
|
||||
altText = i18n('messagesBelow');
|
||||
} else if (count === 1) {
|
||||
altText = i18n('messageBelow');
|
||||
}
|
||||
const { conversationId, withNewMessages, i18n, scrollDown } = this.props;
|
||||
const altText = withNewMessages
|
||||
? i18n('messagesBelow')
|
||||
: i18n('scrollDown');
|
||||
|
||||
return (
|
||||
<div className="module-scroll-down">
|
||||
<button
|
||||
className={classNames(
|
||||
'module-scroll-down__button',
|
||||
count > 0 ? 'module-scroll-down__button--new-messages' : null
|
||||
withNewMessages ? 'module-scroll-down__button--new-messages' : null
|
||||
)}
|
||||
onClick={() => {
|
||||
scrollDown(conversationId);
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
```javascript
|
||||
const itemLookup = {
|
||||
## With oldest and newest
|
||||
|
||||
```jsx
|
||||
window.itemLookup = {
|
||||
'id-1': {
|
||||
type: 'message',
|
||||
data: {
|
||||
|
@ -15,12 +17,24 @@ const itemLookup = {
|
|||
type: 'message',
|
||||
data: {
|
||||
id: 'id-2',
|
||||
conversationType: 'group',
|
||||
direction: 'incoming',
|
||||
timestamp: Date.now(),
|
||||
authorColor: 'green',
|
||||
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': {
|
||||
type: 'message',
|
||||
data: {
|
||||
|
@ -155,25 +169,186 @@ const itemLookup = {
|
|||
},
|
||||
};
|
||||
|
||||
const actions = {
|
||||
window.actions = {
|
||||
// For messages
|
||||
downloadAttachment: options => console.log('onDownload', options),
|
||||
replyToitem: id => console.log('onReply', id),
|
||||
showMessageDetail: id => console.log('onShowDetail', 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 renderItem = id => {
|
||||
const item = itemLookup[id];
|
||||
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,
|
||||
|
||||
// Because we can't use ...item syntax
|
||||
return React.createElement(
|
||||
TimelineItem,
|
||||
util._.merge({ item, i18n: util.i18n }, actions)
|
||||
);
|
||||
renderItem: id => (
|
||||
<TimelineItem item={window.itemLookup[id]} i18n={util.i18n} {...actions} />
|
||||
),
|
||||
};
|
||||
|
||||
<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>;
|
||||
```
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { debounce, isNumber } from 'lodash';
|
||||
import React from 'react';
|
||||
import {
|
||||
AutoSizer,
|
||||
|
@ -6,24 +7,64 @@ import {
|
|||
List,
|
||||
} from 'react-virtualized';
|
||||
|
||||
import { ScrollDownButton } from './ScrollDownButton';
|
||||
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
import { PropsActions as MessageActionsType } from './Message';
|
||||
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>;
|
||||
|
||||
renderItem: (id: string) => JSX.Element;
|
||||
loadCountdownStart?: number;
|
||||
messageHeightChanges: boolean;
|
||||
oldestUnreadIndex?: number;
|
||||
resetCounter: number;
|
||||
scrollToIndex?: number;
|
||||
scrollToIndexCounter: number;
|
||||
totalUnread: number;
|
||||
};
|
||||
|
||||
type PropsHousekeeping = {
|
||||
type PropsHousekeepingType = {
|
||||
id: string;
|
||||
unreadCount?: number;
|
||||
typingContact?: Object;
|
||||
|
||||
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
|
||||
type RowRendererParamsType = {
|
||||
|
@ -34,37 +75,407 @@ type RowRendererParamsType = {
|
|||
parent: 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({
|
||||
defaultHeight: 85,
|
||||
defaultHeight: 64,
|
||||
fixedWidth: true,
|
||||
});
|
||||
public mostRecentWidth = 0;
|
||||
public mostRecentHeight = 0;
|
||||
public offsetFromBottom: number | undefined = 0;
|
||||
public resizeAllFlag = false;
|
||||
public listRef = React.createRef<any>();
|
||||
public visibleRows: VisibleRowsType | undefined;
|
||||
public loadCountdownTimeout: any;
|
||||
|
||||
public componentDidUpdate(prevProps: Props) {
|
||||
if (this.resizeAllFlag) {
|
||||
this.resizeAllFlag = false;
|
||||
this.cellSizeCache.clearAll();
|
||||
this.recomputeRowHeights();
|
||||
} else if (this.props.items !== prevProps.items) {
|
||||
const index = prevProps.items.length;
|
||||
this.cellSizeCache.clear(index, 0);
|
||||
this.recomputeRowHeights(index);
|
||||
}
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const { scrollToIndex } = this.props;
|
||||
const oneTimeScrollRow = this.getLastSeenIndicatorRow();
|
||||
|
||||
this.state = {
|
||||
atBottom: true,
|
||||
atTop: false,
|
||||
oneTimeScrollRow,
|
||||
propScrollToIndex: scrollToIndex,
|
||||
prevPropScrollToIndex: scrollToIndex,
|
||||
shouldShowScrollDownButton: false,
|
||||
areUnreadBelowCurrentPosition: false,
|
||||
};
|
||||
}
|
||||
|
||||
public resizeAll = () => {
|
||||
this.resizeAllFlag = false;
|
||||
this.cellSizeCache.clearAll();
|
||||
public static getDerivedStateFromProps(props: Props, state: State): State {
|
||||
if (
|
||||
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) => {
|
||||
if (this.listRef && this.listRef) {
|
||||
this.listRef.current.recomputeRowHeights(index);
|
||||
public getGrid = () => {
|
||||
const list = this.getList();
|
||||
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 = ({
|
||||
|
@ -73,8 +484,62 @@ export class Timeline extends React.PureComponent<Props> {
|
|||
parent,
|
||||
style,
|
||||
}: RowRendererParamsType) => {
|
||||
const { items, renderItem } = this.props;
|
||||
const messageId = items[index];
|
||||
const {
|
||||
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 (
|
||||
<CellMeasurer
|
||||
|
@ -85,16 +550,277 @@ export class Timeline extends React.PureComponent<Props> {
|
|||
rowIndex={index}
|
||||
width={this.mostRecentWidth}
|
||||
>
|
||||
<div className="module-timeline__message-container" style={style}>
|
||||
{renderItem(messageId)}
|
||||
</div>
|
||||
{rowContents}
|
||||
</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;
|
||||
|
||||
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 (
|
||||
<div className="module-timeline">
|
||||
<AutoSizer>
|
||||
|
@ -103,26 +829,41 @@ export class Timeline extends React.PureComponent<Props> {
|
|||
this.resizeAllFlag = true;
|
||||
|
||||
setTimeout(this.resizeAll, 0);
|
||||
} else if (
|
||||
this.mostRecentHeight &&
|
||||
this.mostRecentHeight !== height
|
||||
) {
|
||||
setTimeout(this.onHeightOnlyChange, 0);
|
||||
}
|
||||
|
||||
this.mostRecentWidth = width;
|
||||
this.mostRecentHeight = height;
|
||||
|
||||
return (
|
||||
<List
|
||||
deferredMeasurementCache={this.cellSizeCache}
|
||||
height={height}
|
||||
// This also registers us with parent InfiniteLoader
|
||||
// onRowsRendered={onRowsRendered}
|
||||
overscanRowCount={0}
|
||||
onScroll={this.onScroll as any}
|
||||
overscanRowCount={10}
|
||||
ref={this.listRef}
|
||||
rowCount={items.length}
|
||||
rowCount={rowCount}
|
||||
rowHeight={this.cellSizeCache.rowHeight}
|
||||
rowRenderer={this.rowRenderer}
|
||||
scrollToAlignment="start"
|
||||
scrollToIndex={scrollToIndex}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
{shouldShowScrollDownButton ? (
|
||||
<ScrollDownButton
|
||||
conversationId={id}
|
||||
withNewMessages={areUnreadBelowCurrentPosition}
|
||||
scrollDown={this.onClickScrollDownButton}
|
||||
i18n={i18n}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,51 @@
|
|||
### A plain message
|
||||
|
||||
```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,
|
||||
PropsData as MessageProps,
|
||||
} from './Message';
|
||||
import {
|
||||
PropsActions as UnsupportedMessageActionsType,
|
||||
PropsData as UnsupportedMessageProps,
|
||||
UnsupportedMessage,
|
||||
} from './UnsupportedMessage';
|
||||
import {
|
||||
PropsData as TimerNotificationProps,
|
||||
TimerNotification,
|
||||
|
@ -29,6 +34,10 @@ type MessageType = {
|
|||
type: 'message';
|
||||
data: MessageProps;
|
||||
};
|
||||
type UnsupportedMessageType = {
|
||||
type: 'unsupportedMessage';
|
||||
data: UnsupportedMessageProps;
|
||||
};
|
||||
type TimerNotificationType = {
|
||||
type: 'timerNotification';
|
||||
data: TimerNotificationProps;
|
||||
|
@ -49,22 +58,26 @@ type ResetSessionNotificationType = {
|
|||
type: 'resetSessionNotification';
|
||||
data: null;
|
||||
};
|
||||
export type TimelineItemType =
|
||||
| MessageType
|
||||
| UnsupportedMessageType
|
||||
| TimerNotificationType
|
||||
| SafetyNumberNotificationType
|
||||
| VerificationNotificationType
|
||||
| ResetSessionNotificationType
|
||||
| GroupNotificationType;
|
||||
|
||||
type PropsData = {
|
||||
item:
|
||||
| MessageType
|
||||
| TimerNotificationType
|
||||
| SafetyNumberNotificationType
|
||||
| VerificationNotificationType
|
||||
| ResetSessionNotificationType
|
||||
| GroupNotificationType;
|
||||
item?: TimelineItemType;
|
||||
};
|
||||
|
||||
type PropsHousekeeping = {
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
type PropsActions = MessageActionsType & SafetyNumberActionsType;
|
||||
type PropsActions = MessageActionsType &
|
||||
UnsupportedMessageActionsType &
|
||||
SafetyNumberActionsType;
|
||||
|
||||
type Props = PropsData & PropsHousekeeping & PropsActions;
|
||||
|
||||
|
@ -73,12 +86,18 @@ export class TimelineItem extends React.PureComponent<Props> {
|
|||
const { item, i18n } = this.props;
|
||||
|
||||
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') {
|
||||
return <Message {...this.props} {...item.data} i18n={i18n} />;
|
||||
}
|
||||
if (item.type === 'unsupportedMessage') {
|
||||
return <UnsupportedMessage {...this.props} {...item.data} i18n={i18n} />;
|
||||
}
|
||||
if (item.type === 'timerNotification') {
|
||||
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 { LocalizerType } from '../../types/Util';
|
||||
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
|
||||
export type PropsData = {
|
||||
type: 'fromOther' | 'fromMe' | 'fromSync';
|
||||
phoneNumber: string;
|
||||
|
@ -63,7 +61,9 @@ export class TimerNotification extends React.Component<Props> {
|
|||
? i18n('disappearingMessagesDisabled')
|
||||
: i18n('timerSetOnSync', [timespan]);
|
||||
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}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
|
@ -28,8 +28,8 @@ function getDecember1159() {
|
|||
text="500ms ago - all below 1 minute are 'now'"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
|
@ -38,8 +38,8 @@ function getDecember1159() {
|
|||
text="Five seconds ago"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
|
@ -48,8 +48,8 @@ function getDecember1159() {
|
|||
text="30 seconds ago"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
|
@ -58,8 +58,8 @@ function getDecember1159() {
|
|||
text="One minute ago - in minutes"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
|
@ -68,8 +68,8 @@ function getDecember1159() {
|
|||
text="30 minutes ago"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
|
@ -78,8 +78,8 @@ function getDecember1159() {
|
|||
text="45 minutes ago (used to round up to 1 hour with moment)"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
|
@ -88,8 +88,8 @@ function getDecember1159() {
|
|||
text="One hour ago - in hours"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
|
@ -98,8 +98,8 @@ function getDecember1159() {
|
|||
text="12:01am today"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
|
@ -108,8 +108,8 @@ function getDecember1159() {
|
|||
text="11:59pm yesterday - adds day name"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
|
@ -118,8 +118,8 @@ function getDecember1159() {
|
|||
text="24 hours ago"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
|
@ -128,8 +128,8 @@ function getDecember1159() {
|
|||
text="Two days ago"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
|
@ -138,8 +138,8 @@ function getDecember1159() {
|
|||
text="Seven days ago - adds month"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
|
@ -148,8 +148,8 @@ function getDecember1159() {
|
|||
text="Thirty days ago"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
|
@ -158,8 +158,8 @@ function getDecember1159() {
|
|||
text="January 1st at 12:01am"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
|
@ -168,8 +168,8 @@ function getDecember1159() {
|
|||
text="December 31st at 11:59pm - adds year"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<Message
|
||||
direction="incoming"
|
||||
status="delivered"
|
||||
|
@ -178,6 +178,6 @@ function getDecember1159() {
|
|||
text="One year ago"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>;
|
||||
```
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<TypingBubble conversationType="direct" i18n={util.i18n} />
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<TypingBubble color="teal" conversationType="direct" i18n={util.i18n} />
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
|
@ -15,24 +15,24 @@
|
|||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<li>
|
||||
<div className="module-message-container">
|
||||
<TypingBubble color="red" conversationType="group" i18n={util.i18n} />
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<TypingBubble
|
||||
color="purple"
|
||||
authorName="First Last"
|
||||
conversationType="group"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
</div>
|
||||
<div className="module-message-container">
|
||||
<TypingBubble
|
||||
avatarPath={util.gifObjectUrl}
|
||||
color="blue"
|
||||
conversationType="group"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
</div>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
|
|
@ -9,14 +9,14 @@ import { LocalizerType } from '../../types/Util';
|
|||
interface Props {
|
||||
avatarPath?: string;
|
||||
color: string;
|
||||
name: string;
|
||||
name?: string;
|
||||
phoneNumber: string;
|
||||
profileName: string;
|
||||
conversationType: string;
|
||||
profileName?: string;
|
||||
conversationType: 'group' | 'direct';
|
||||
i18n: LocalizerType;
|
||||
}
|
||||
|
||||
export class TypingBubble extends React.Component<Props> {
|
||||
export class TypingBubble extends React.PureComponent<Props> {
|
||||
public renderAvatar() {
|
||||
const {
|
||||
avatarPath,
|
||||
|
@ -49,10 +49,17 @@ export class TypingBubble extends React.Component<Props> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const { i18n, color } = this.props;
|
||||
const { i18n, color, conversationType } = this.props;
|
||||
const isGroup = conversationType === 'group';
|
||||
|
||||
return (
|
||||
<div className={classNames('module-message', 'module-message--incoming')}>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message',
|
||||
'module-message--incoming',
|
||||
isGroup ? 'module-message--group' : null
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__container',
|
||||
|
|
|
@ -18,14 +18,14 @@ export type PropsData = {
|
|||
contact: ContactType;
|
||||
};
|
||||
|
||||
type PropsHousekeeping = {
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
export type PropsActions = {
|
||||
downloadNewVersion: () => unknown;
|
||||
};
|
||||
|
||||
type PropsHousekeeping = {
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
type Props = PropsData & PropsHousekeeping & PropsActions;
|
||||
|
||||
export class UnsupportedMessage extends React.Component<Props> {
|
||||
|
|
|
@ -18,7 +18,7 @@ export function renderAvatar({
|
|||
contact: ContactType;
|
||||
i18n: LocalizerType;
|
||||
size: number;
|
||||
direction?: string;
|
||||
direction?: 'outgoing' | 'incoming';
|
||||
}) {
|
||||
const { avatar } = contact;
|
||||
|
||||
|
|
|
@ -1,4 +1,13 @@
|
|||
export function getMessageModel(attributes: any) {
|
||||
export function getSearchResultsProps(attributes: any) {
|
||||
// @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 { omit } from 'lodash';
|
||||
|
||||
import {
|
||||
difference,
|
||||
fromPairs,
|
||||
intersection,
|
||||
omit,
|
||||
orderBy,
|
||||
pick,
|
||||
uniq,
|
||||
values,
|
||||
without,
|
||||
} from 'lodash';
|
||||
import { trigger } from '../../shims/events';
|
||||
import { NoopActionType } from './noop';
|
||||
|
||||
|
@ -48,29 +56,65 @@ export type ConversationType = {
|
|||
lastUpdated: number;
|
||||
unreadCount: number;
|
||||
isSelected: boolean;
|
||||
isTyping: boolean;
|
||||
typingContact?: {
|
||||
avatarPath?: string;
|
||||
color: string;
|
||||
name?: string;
|
||||
phoneNumber: string;
|
||||
profileName?: string;
|
||||
};
|
||||
};
|
||||
export type ConversationLookupType = {
|
||||
[key: string]: ConversationType;
|
||||
};
|
||||
export type MessageType = {
|
||||
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 = {
|
||||
[key: string]: MessageType;
|
||||
};
|
||||
export type ConversationMessageType = {
|
||||
// And perhaps this could be part of our ConversationType? What if we moved all the selectors as part of this set of changes?
|
||||
// We have the infrastructure for it now...
|
||||
messages: Array<string>;
|
||||
heightChangeMessageIds: Array<string>;
|
||||
isLoadingMessages: boolean;
|
||||
isNearBottom?: boolean;
|
||||
loadCountdownStart?: number;
|
||||
messageIds: Array<string>;
|
||||
metrics: MessageMetricsType;
|
||||
resetCounter: number;
|
||||
scrollToMessageId?: string;
|
||||
scrollToMessageCounter: number;
|
||||
};
|
||||
|
||||
export type MessagesByConversationType = {
|
||||
[key: string]: ConversationMessageType;
|
||||
[key: string]: ConversationMessageType | null;
|
||||
};
|
||||
|
||||
export type ConversationsStateType = {
|
||||
conversationLookup: ConversationLookupType;
|
||||
selectedConversation?: string;
|
||||
selectedMessage?: string;
|
||||
selectedMessageCounter: number;
|
||||
showArchived: boolean;
|
||||
|
||||
// Note: it's very important that both of these locations are always kept up to date
|
||||
|
@ -100,15 +144,91 @@ type ConversationRemovedActionType = {
|
|||
id: string;
|
||||
};
|
||||
};
|
||||
type ConversationUnloadedActionType = {
|
||||
type: 'CONVERSATION_UNLOADED';
|
||||
payload: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
export type RemoveAllConversationsActionType = {
|
||||
type: 'CONVERSATIONS_REMOVE_ALL';
|
||||
payload: null;
|
||||
};
|
||||
export type MessageExpiredActionType = {
|
||||
type: 'MESSAGE_EXPIRED';
|
||||
export type MessageChangedActionType = {
|
||||
type: 'MESSAGE_CHANGED';
|
||||
payload: {
|
||||
id: 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 = {
|
||||
|
@ -128,14 +248,24 @@ type ShowArchivedConversationsActionType = {
|
|||
};
|
||||
|
||||
export type ConversationActionType =
|
||||
| AnyAction
|
||||
| ConversationAddedActionType
|
||||
| ConversationChangedActionType
|
||||
| ConversationRemovedActionType
|
||||
| ConversationUnloadedActionType
|
||||
| RemoveAllConversationsActionType
|
||||
| MessageExpiredActionType
|
||||
| MessageChangedActionType
|
||||
| MessageDeletedActionType
|
||||
| MessagesAddedActionType
|
||||
| MessagesResetActionType
|
||||
| SetMessagesLoadingActionType
|
||||
| SetIsNearBottomActionType
|
||||
| SetLoadCountdownStartActionType
|
||||
| ClearChangedMessagesActionType
|
||||
| ClearSelectedMessageActionType
|
||||
| ClearUnreadMetricsActionType
|
||||
| ScrollToMessageActionType
|
||||
| SelectedConversationChangedActionType
|
||||
| MessageExpiredActionType
|
||||
| MessageDeletedActionType
|
||||
| SelectedConversationChangedActionType
|
||||
| ShowInboxActionType
|
||||
| ShowArchivedConversationsActionType;
|
||||
|
@ -146,8 +276,19 @@ export const actions = {
|
|||
conversationAdded,
|
||||
conversationChanged,
|
||||
conversationRemoved,
|
||||
conversationUnloaded,
|
||||
removeAllConversations,
|
||||
messageExpired,
|
||||
messageDeleted,
|
||||
messageChanged,
|
||||
messagesAdded,
|
||||
messagesReset,
|
||||
setMessagesLoading,
|
||||
setLoadCountdownStart,
|
||||
setIsNearBottom,
|
||||
clearChangedMessages,
|
||||
clearSelectedMessage,
|
||||
clearUnreadMetrics,
|
||||
scrollToMessage,
|
||||
openConversationInternal,
|
||||
openConversationExternal,
|
||||
showInbox,
|
||||
|
@ -186,6 +327,14 @@ function conversationRemoved(id: string): ConversationRemovedActionType {
|
|||
},
|
||||
};
|
||||
}
|
||||
function conversationUnloaded(id: string): ConversationUnloadedActionType {
|
||||
return {
|
||||
type: 'CONVERSATION_UNLOADED',
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
};
|
||||
}
|
||||
function removeAllConversations(): RemoveAllConversationsActionType {
|
||||
return {
|
||||
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,
|
||||
conversationId: string
|
||||
): MessageExpiredActionType {
|
||||
): MessageDeletedActionType {
|
||||
return {
|
||||
type: 'MESSAGE_EXPIRED',
|
||||
type: 'MESSAGE_DELETED',
|
||||
payload: {
|
||||
id,
|
||||
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
|
||||
// trigger an 'openConversation' so we go through Whisper.events for all conversation
|
||||
// selection.
|
||||
// trigger an 'openConversation' so we go through Whisper.events for all
|
||||
// conversation selection. Internal just triggers the Whisper.event, and External
|
||||
// makes the changes to the store.
|
||||
function openConversationInternal(
|
||||
id: string,
|
||||
messageId?: string
|
||||
|
@ -251,12 +522,24 @@ function showArchivedConversations() {
|
|||
function getEmptyState(): ConversationsStateType {
|
||||
return {
|
||||
conversationLookup: {},
|
||||
showArchived: false,
|
||||
messagesLookup: {},
|
||||
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(
|
||||
state: ConversationsStateType = getEmptyState(),
|
||||
action: ConversationActionType
|
||||
|
@ -322,11 +605,421 @@ export function reducer(
|
|||
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') {
|
||||
return getEmptyState();
|
||||
}
|
||||
if (action.type === 'MESSAGE_EXPIRED') {
|
||||
// noop - for now this is only important for search
|
||||
if (action.type === 'MESSAGE_CHANGED') {
|
||||
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') {
|
||||
const { payload } = action;
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
import { AnyAction } from 'redux';
|
||||
import { omit, reject } from 'lodash';
|
||||
|
||||
import { normalize } from '../../types/PhoneNumber';
|
||||
import { trigger } from '../../shims/events';
|
||||
// import { getMessageModel } from '../../shims/Whisper';
|
||||
// import { cleanSearchTerm } from '../../util/cleanSearchTerm';
|
||||
import {
|
||||
searchConversations /*, searchMessages */,
|
||||
} from '../../../js/modules/data';
|
||||
import { cleanSearchTerm } from '../../util/cleanSearchTerm';
|
||||
import { searchConversations, searchMessages } from '../../../js/modules/data';
|
||||
import { makeLookup } from '../../util/makeLookup';
|
||||
|
||||
import {
|
||||
ConversationType,
|
||||
MessageExpiredActionType,
|
||||
MessageDeletedActionType,
|
||||
MessageSearchResultType,
|
||||
RemoveAllConversationsActionType,
|
||||
SelectedConversationChangedActionType,
|
||||
|
@ -64,11 +60,10 @@ type ClearSearchActionType = {
|
|||
};
|
||||
|
||||
export type SEARCH_TYPES =
|
||||
| AnyAction
|
||||
| SearchResultsFulfilledActionType
|
||||
| UpdateSearchTermActionType
|
||||
| ClearSearchActionType
|
||||
| MessageExpiredActionType
|
||||
| MessageDeletedActionType
|
||||
| RemoveAllConversationsActionType
|
||||
| SelectedConversationChangedActionType;
|
||||
|
||||
|
@ -101,9 +96,9 @@ async function doSearch(
|
|||
): Promise<SearchResultsPayloadType> {
|
||||
const { regionCode, ourNumber, noteToSelf } = options;
|
||||
|
||||
const [discussions /*, messages */] = await Promise.all([
|
||||
const [discussions, messages] = await Promise.all([
|
||||
queryConversationsAndContacts(query, { ourNumber, noteToSelf }),
|
||||
// queryMessages(query),
|
||||
queryMessages(query),
|
||||
]);
|
||||
const { conversations, contacts } = discussions;
|
||||
|
||||
|
@ -112,7 +107,7 @@ async function doSearch(
|
|||
normalizedPhoneNumber: normalize(query, { regionCode }),
|
||||
conversations,
|
||||
contacts,
|
||||
messages: [], // getMessageProps(messages) || [],
|
||||
messages,
|
||||
};
|
||||
}
|
||||
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>) => {
|
||||
// if (!messages || !messages.length) {
|
||||
// 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 [];
|
||||
// }
|
||||
// }
|
||||
return searchMessages(normalized);
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function queryConversationsAndContacts(
|
||||
providedQuery: string,
|
||||
|
@ -271,7 +252,7 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === 'MESSAGE_EXPIRED') {
|
||||
if (action.type === 'MESSAGE_DELETED') {
|
||||
const { messages, messageLookup } = state;
|
||||
if (!messages.length) {
|
||||
return state;
|
||||
|
|
|
@ -9,8 +9,8 @@ import { SmartTimeline } from '../smart/Timeline';
|
|||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||
const FilteredTimeline = SmartTimeline as any;
|
||||
|
||||
export const createTimeline = (store: Store) => (
|
||||
export const createTimeline = (store: Store, props: Object) => (
|
||||
<Provider store={store}>
|
||||
<FilteredTimeline />
|
||||
<FilteredTimeline {...props} />
|
||||
</Provider>
|
||||
);
|
||||
|
|
|
@ -6,12 +6,16 @@ import { LocalizerType } from '../../types/Util';
|
|||
import { StateType } from '../reducer';
|
||||
import {
|
||||
ConversationLookupType,
|
||||
ConversationMessageType,
|
||||
ConversationsStateType,
|
||||
ConversationType,
|
||||
MessageLookupType,
|
||||
MessagesByConversationType,
|
||||
MessageType,
|
||||
} 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';
|
||||
|
||||
|
@ -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(
|
||||
getConversations,
|
||||
(state: ConversationsStateType): boolean => {
|
||||
|
@ -160,9 +182,12 @@ export const getMe = createSelector(
|
|||
);
|
||||
|
||||
// This is where we will put Conversation selector logic, replicating what
|
||||
// is currently in models/conversation.getProps()
|
||||
// Blockers:
|
||||
// 1) contactTypingTimers - that UI-only state needs to be moved to redux
|
||||
// is currently in models/conversation.getProps()
|
||||
// What needs to happen to pull that selector logic here?
|
||||
// 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(
|
||||
conversation: ConversationType
|
||||
// regionCode: string,
|
||||
|
@ -180,6 +205,8 @@ export const getCachedSelectorForConversation = createSelector(
|
|||
getRegionCode,
|
||||
getUserNumber,
|
||||
(): CachedConversationSelectorType => {
|
||||
// Note: memoizee will check all parameters provided, and only run our selector
|
||||
// if any of them have changed.
|
||||
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.
|
||||
// Blockers:
|
||||
// 1) it's a lot of code to pull over - ~500 lines
|
||||
// 2) a couple places still rely on all that code - will need to move these to Roots:
|
||||
// - quote compose
|
||||
// - message details
|
||||
// For now we use a shim, as selector logic is still happening in the Backbone Model.
|
||||
// What needs to happen to pull that selector logic here?
|
||||
// 1) translate ~500 lines of selector logic into TypeScript
|
||||
// 2) other places still rely on that prop-gen code - need to put these under Roots:
|
||||
// - quote compose
|
||||
// - message details
|
||||
export function _messageSelector(
|
||||
message: MessageType
|
||||
// ourNumber: string,
|
||||
// regionCode: string,
|
||||
// conversation?: ConversationType,
|
||||
// sender?: ConversationType,
|
||||
// quoted?: ConversationType
|
||||
): MessageType {
|
||||
return message;
|
||||
message: MessageType,
|
||||
// @ts-ignore
|
||||
ourNumber: string,
|
||||
// @ts-ignore
|
||||
regionCode: string,
|
||||
// @ts-ignore
|
||||
conversation?: ConversationType,
|
||||
// @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
|
||||
// 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(
|
||||
getRegionCode,
|
||||
getUserNumber,
|
||||
(): CachedMessageSelectorType => {
|
||||
// Note: memoizee will check all parameters provided, and only run our selector
|
||||
// if any of them have changed.
|
||||
return memoizee(_messageSelector, { max: 500 });
|
||||
}
|
||||
);
|
||||
|
||||
type GetMessageByIdType = (id: string) => MessageType | undefined;
|
||||
type GetMessageByIdType = (id: string) => TimelineItemType | undefined;
|
||||
export const getMessageSelector = createSelector(
|
||||
getCachedSelectorForMessage,
|
||||
getMessages,
|
||||
getSelectedMessage,
|
||||
getConversationSelector,
|
||||
getRegionCode,
|
||||
getUserNumber,
|
||||
(
|
||||
selector: CachedMessageSelectorType,
|
||||
lookup: MessageLookupType
|
||||
messageSelector: CachedMessageSelectorType,
|
||||
messageLookup: MessageLookupType,
|
||||
selectedMessage: SelectedMessageType | undefined,
|
||||
conversationSelector: GetConversationByIdType,
|
||||
regionCode: string,
|
||||
ourNumber: string
|
||||
): GetMessageByIdType => {
|
||||
return (id: string) => {
|
||||
const message = lookup[id];
|
||||
const message = messageLookup[id];
|
||||
if (!message) {
|
||||
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 { createSelector } from 'reselect';
|
||||
import { getSearchResultsProps } from '../../shims/Whisper';
|
||||
|
||||
import { StateType } from '../reducer';
|
||||
|
||||
|
@ -79,14 +80,16 @@ export const getSearchResults = createSelector(
|
|||
),
|
||||
hideMessagesHeader: false,
|
||||
messages: state.messages.map(message => {
|
||||
const props = getSearchResultsProps(message);
|
||||
|
||||
if (message.id === selectedMessage) {
|
||||
return {
|
||||
...message,
|
||||
...props,
|
||||
isSelected: true,
|
||||
};
|
||||
}
|
||||
|
||||
return message;
|
||||
return props;
|
||||
}),
|
||||
regionCode: regionCode,
|
||||
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
|
||||
const FilteredSmartMainHeader = SmartMainHeader as any;
|
||||
|
||||
function renderMainHeader(): JSX.Element {
|
||||
return <FilteredSmartMainHeader />;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StateType) => {
|
||||
const showSearch = isSearching(state);
|
||||
|
||||
|
@ -25,7 +29,7 @@ const mapStateToProps = (state: StateType) => {
|
|||
searchResults,
|
||||
showArchived: getShowArchived(state),
|
||||
i18n: getIntl(state),
|
||||
renderMainHeader: () => <FilteredSmartMainHeader />,
|
||||
renderMainHeader,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { pick } from 'lodash';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
|
@ -5,32 +6,59 @@ import { Timeline } from '../../components/conversation/Timeline';
|
|||
import { StateType } from '../reducer';
|
||||
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { getConversationSelector } from '../selectors/conversations';
|
||||
import {
|
||||
getConversationMessagesSelector,
|
||||
getConversationSelector,
|
||||
} from '../selectors/conversations';
|
||||
|
||||
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()
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||
const FilteredSmartTimelineItem = SmartTimelineItem as any;
|
||||
const FilteredSmartTypingBubble = SmartTypingBubble as any;
|
||||
const FilteredSmartLastSeenIndicator = SmartLastSeenIndicator as any;
|
||||
const FilteredSmartTimelineLoadingRow = SmartTimelineLoadingRow as any;
|
||||
|
||||
type ExternalProps = {
|
||||
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) => {
|
||||
const { id } = props;
|
||||
function renderItem(messageId: string, actionProps: Object): JSX.Element {
|
||||
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 conversation = conversationSelector(id);
|
||||
const items: Array<string> = [];
|
||||
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||
const { id, ...actions } = props;
|
||||
|
||||
const conversation = getConversationSelector(state)(id);
|
||||
const conversationMessages = getConversationMessagesSelector(state)(id);
|
||||
|
||||
return {
|
||||
...conversation,
|
||||
items,
|
||||
id,
|
||||
...pick(conversation, ['unreadCount', 'typingContact']),
|
||||
...conversationMessages,
|
||||
i18n: getIntl(state),
|
||||
renderTimelineItem: (messageId: string) => {
|
||||
return <FilteredSmartTimelineItem id={messageId} />;
|
||||
},
|
||||
renderItem,
|
||||
renderLastSeenIndicator,
|
||||
renderLoadingRow,
|
||||
renderTypingBubble,
|
||||
...actions,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -14,9 +14,10 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
const { id } = props;
|
||||
|
||||
const messageSelector = getMessageSelector(state);
|
||||
const item = messageSelector(id);
|
||||
|
||||
return {
|
||||
...messageSelector(id),
|
||||
item,
|
||||
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;
|
||||
theme: 'light-theme' | 'dark-theme';
|
||||
type: 'private' | 'group';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -16,16 +15,17 @@ interface Props {
|
|||
*/
|
||||
export class ConversationContext extends React.Component<Props> {
|
||||
public render() {
|
||||
const { ios, theme, type } = this.props;
|
||||
const { ios, theme } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(theme || 'light-theme', ios ? 'ios-theme' : null)}
|
||||
style={{
|
||||
backgroundColor: theme === 'dark-theme' ? 'black' : undefined,
|
||||
}}
|
||||
>
|
||||
<div className={classNames('conversation', type || 'private')}>
|
||||
<div className="discussion-container" style={{ padding: '0.5em' }}>
|
||||
<ul className="message-list">{this.props.children}</ul>
|
||||
</div>
|
||||
<div className="timeline-placeholder">
|
||||
<div className="timeline-wrapper">{this.props.children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -25,7 +25,11 @@ describe('state/selectors/conversations', () => {
|
|||
lastUpdated: Date.now(),
|
||||
unreadCount: 1,
|
||||
isSelected: false,
|
||||
isTyping: false,
|
||||
typingContact: {
|
||||
name: 'Someone There',
|
||||
color: 'blue',
|
||||
phoneNumber: '+18005551111',
|
||||
},
|
||||
},
|
||||
id2: {
|
||||
id: 'id2',
|
||||
|
@ -40,7 +44,11 @@ describe('state/selectors/conversations', () => {
|
|||
lastUpdated: Date.now(),
|
||||
unreadCount: 1,
|
||||
isSelected: false,
|
||||
isTyping: false,
|
||||
typingContact: {
|
||||
name: 'Someone There',
|
||||
color: 'blue',
|
||||
phoneNumber: '+18005551111',
|
||||
},
|
||||
},
|
||||
id3: {
|
||||
id: 'id3',
|
||||
|
@ -55,7 +63,11 @@ describe('state/selectors/conversations', () => {
|
|||
lastUpdated: Date.now(),
|
||||
unreadCount: 1,
|
||||
isSelected: false,
|
||||
isTyping: false,
|
||||
typingContact: {
|
||||
name: 'Someone There',
|
||||
color: 'blue',
|
||||
phoneNumber: '+18005551111',
|
||||
},
|
||||
},
|
||||
id4: {
|
||||
id: 'id4',
|
||||
|
@ -70,7 +82,11 @@ describe('state/selectors/conversations', () => {
|
|||
lastUpdated: Date.now(),
|
||||
unreadCount: 1,
|
||||
isSelected: false,
|
||||
isTyping: false,
|
||||
typingContact: {
|
||||
name: 'Someone There',
|
||||
color: 'blue',
|
||||
phoneNumber: '+18005551111',
|
||||
},
|
||||
},
|
||||
id5: {
|
||||
id: 'id5',
|
||||
|
@ -85,7 +101,11 @@ describe('state/selectors/conversations', () => {
|
|||
lastUpdated: Date.now(),
|
||||
unreadCount: 1,
|
||||
isSelected: false,
|
||||
isTyping: false,
|
||||
typingContact: {
|
||||
name: 'Someone There',
|
||||
color: 'blue',
|
||||
phoneNumber: '+18005551111',
|
||||
},
|
||||
},
|
||||
};
|
||||
const comparator = _getConversationComparator(i18n, regionCode);
|
||||
|
|
|
@ -164,17 +164,17 @@
|
|||
"rule": "jQuery-load(",
|
||||
"path": "js/conversation_controller.js",
|
||||
"line": " async load() {",
|
||||
"lineNumber": 178,
|
||||
"lineNumber": 169,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-10-02T21:00:44.007Z"
|
||||
"updated": "2019-07-31T00:19:18.696Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-load(",
|
||||
"path": "js/conversation_controller.js",
|
||||
"line": " this._initialPromise = load();",
|
||||
"lineNumber": 213,
|
||||
"lineNumber": 204,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-10-02T21:00:44.007Z"
|
||||
"updated": "2019-07-31T00:19:18.696Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
|
@ -363,8 +363,8 @@
|
|||
"line": " this.$el.append(this.contactView.el);",
|
||||
"lineNumber": 46,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-10-02T21:18:39.026Z",
|
||||
"reasonDetail": "Operating on previously-existing DOM elements"
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Known DOM elements"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
|
@ -474,148 +474,139 @@
|
|||
"updated": "2018-09-15T00:38:04.183Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"rule": "jQuery-appendTo(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " let $el = this.$(`#${id}`);",
|
||||
"lineNumber": 34,
|
||||
"line": " view.$el.appendTo(this.el);",
|
||||
"lineNumber": 32,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Known DOM elements"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " this.$('.message').text(message);",
|
||||
"lineNumber": 61,
|
||||
"lineNumber": 58,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Hardcoded selector"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " el: this.$('.conversation-stack'),",
|
||||
"lineNumber": 78,
|
||||
"lineNumber": 75,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Hardcoded selector"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-prependTo(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " this.appLoadingScreen.$el.prependTo(this.el);",
|
||||
"lineNumber": 85,
|
||||
"lineNumber": 82,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Known DOM elements"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " .append(this.networkStatusView.render().el);",
|
||||
"lineNumber": 100,
|
||||
"lineNumber": 97,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Known DOM elements"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-prependTo(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " banner.$el.prependTo(this.$el);",
|
||||
"lineNumber": 104,
|
||||
"lineNumber": 101,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Known DOM elements"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-appendTo(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " toast.$el.appendTo(this.$el);",
|
||||
"lineNumber": 110,
|
||||
"lineNumber": 107,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-05-10T00:25:51.515Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Known DOM elements"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
||||
"lineNumber": 130,
|
||||
"lineNumber": 126,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-03-08T23:49:08.796Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Known DOM elements"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
||||
"lineNumber": 130,
|
||||
"lineNumber": 126,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-03-08T23:49:08.796Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Known DOM elements"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " if (e && this.$(e.target).closest('.placeholder').length) {",
|
||||
"lineNumber": 171,
|
||||
"lineNumber": 167,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-03-08T23:49:08.796Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Known DOM elements"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " this.$('#header, .gutter').addClass('inactive');",
|
||||
"lineNumber": 175,
|
||||
"lineNumber": 171,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-03-08T23:49:08.796Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Hardcoded selector"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " this.$('.conversation-stack').addClass('inactive');",
|
||||
"lineNumber": 179,
|
||||
"lineNumber": 175,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-03-08T23:49:08.796Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Hardcoded selector"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " this.$('.conversation:first .menu').trigger('close');",
|
||||
"lineNumber": 181,
|
||||
"lineNumber": 177,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-03-08T23:49:08.796Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Hardcoded selector"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {",
|
||||
"lineNumber": 201,
|
||||
"lineNumber": 197,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-03-08T23:49:08.796Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Known DOM elements"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " this.$('.conversation:first .recorder').trigger('close');",
|
||||
"lineNumber": 204,
|
||||
"lineNumber": 200,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-03-08T23:49:08.796Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Hardcoded selector"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
|
@ -722,8 +713,8 @@
|
|||
"line": " new QRCode(this.$('.qr')[0]).makeCode(",
|
||||
"lineNumber": 39,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Hardcoded selector"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-wrap(",
|
||||
|
@ -731,7 +722,7 @@
|
|||
"line": " dcodeIO.ByteBuffer.wrap(this.ourKey).toString('base64')",
|
||||
"lineNumber": 40,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-09-19T18:13:29.628Z"
|
||||
"updated": "2019-07-31T00:19:18.696Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-insertBefore(",
|
||||
|
@ -739,8 +730,8 @@
|
|||
"line": " dialog.$el.insertBefore(this.el);",
|
||||
"lineNumber": 75,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Known DOM elements"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
|
@ -748,8 +739,8 @@
|
|||
"line": " this.$('button.verify').attr('disabled', true);",
|
||||
"lineNumber": 79,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Hardcoded selector"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
|
@ -757,8 +748,8 @@
|
|||
"line": " this.$('button.verify').removeAttr('disabled');",
|
||||
"lineNumber": 110,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Hardcoded selector"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
|
@ -778,105 +769,6 @@
|
|||
"updated": "2018-09-15T00:38:04.183Z",
|
||||
"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-$(",
|
||||
"path": "js/views/phone-input-view.js",
|
||||
|
@ -1453,6 +1345,45 @@
|
|||
"reasonCategory": "falseMatch",
|
||||
"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(",
|
||||
"path": "node_modules/archiver-utils/node_modules/lodash/after.js",
|
||||
|
@ -3590,20 +3521,36 @@
|
|||
"updated": "2018-09-19T18:13:29.628Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-wrap(",
|
||||
"path": "node_modules/extglob/index.js",
|
||||
"line": " o[id] = wrap(inner, prefix, opts.escape);",
|
||||
"lineNumber": 85,
|
||||
"rule": "jQuery-load(",
|
||||
"path": "node_modules/extglob/node_modules/debug/src/browser.js",
|
||||
"line": "function load() {",
|
||||
"lineNumber": 150,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-09-19T18:13:29.628Z"
|
||||
"updated": "2019-07-31T00:19:18.696Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-wrap(",
|
||||
"path": "node_modules/extglob/index.js",
|
||||
"line": "function wrap(inner, prefix, esc) {",
|
||||
"lineNumber": 119,
|
||||
"rule": "jQuery-load(",
|
||||
"path": "node_modules/extglob/node_modules/debug/src/browser.js",
|
||||
"line": "exports.enable(load());",
|
||||
"lineNumber": 168,
|
||||
"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",
|
||||
|
@ -5219,46 +5166,6 @@
|
|||
"reasonCategory": "falseMatch",
|
||||
"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",
|
||||
"path": "node_modules/min-document/serialize.js",
|
||||
|
@ -7161,6 +7068,62 @@
|
|||
"reasonCategory": "falseMatch",
|
||||
"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",
|
||||
"path": "node_modules/thenify/index.js",
|
||||
|
@ -7849,8 +7812,8 @@
|
|||
"line": " this.menuTriggerRef = react_1.default.createRef();",
|
||||
"lineNumber": 14,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-03-09T00:08:44.242Z",
|
||||
"reasonDetail": "Used only to trigger menu display"
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Used to reference popup menu"
|
||||
},
|
||||
{
|
||||
"rule": "React-createRef",
|
||||
|
@ -7858,17 +7821,17 @@
|
|||
"line": " this.menuTriggerRef = React.createRef();",
|
||||
"lineNumber": 59,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-03-09T00:08:44.242Z",
|
||||
"reasonDetail": "Used only to trigger menu display"
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Used to reference popup menu"
|
||||
},
|
||||
{
|
||||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/Timeline.js",
|
||||
"line": " this.listRef = react_1.default.createRef();",
|
||||
"lineNumber": 17,
|
||||
"lineNumber": 27,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-04-17T18:44:33.207Z",
|
||||
"reasonDetail": "Necessary to interact with child react-virtualized/List"
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"reasonDetail": "Timeline needs to interact with its child List directly"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-wrap(",
|
||||
|
|
|
@ -93,7 +93,13 @@
|
|||
"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
|
||||
"no-implicit-dependencies": [true, ["dashdash", "electron"]],
|
||||
|
|
74
yarn.lock
74
yarn.lock
|
@ -313,6 +313,11 @@
|
|||
dependencies:
|
||||
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:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f"
|
||||
|
@ -2067,7 +2072,7 @@ cross-spawn@^4:
|
|||
lru-cache "^4.0.1"
|
||||
which "^1.2.9"
|
||||
|
||||
cross-spawn@^6.0.0:
|
||||
cross-spawn@^6.0.0, cross-spawn@^6.0.5:
|
||||
version "6.0.5"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
|
||||
integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
|
||||
|
@ -3526,6 +3531,14 @@ find-up@^3.0.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "0.3.0"
|
||||
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"
|
||||
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:
|
||||
version "7.0.1"
|
||||
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"
|
||||
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:
|
||||
version "5.0.15"
|
||||
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"
|
||||
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:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439"
|
||||
|
@ -6935,6 +6976,25 @@ pascalcase@^0.1.1:
|
|||
version "0.1.1"
|
||||
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:
|
||||
version "0.0.0"
|
||||
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:
|
||||
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:
|
||||
version "2.4.5"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da"
|
||||
|
@ -8791,6 +8858,11 @@ slash@^1.0.0:
|
|||
version "1.0.0"
|
||||
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:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d"
|
||||
|
|
Loading…
Add table
Reference in a new issue