Typing Indicators
This commit is contained in:
parent
99252702e1
commit
79a861a870
23 changed files with 906 additions and 121 deletions
|
@ -653,6 +653,10 @@
|
|||
"selectAContact": {
|
||||
"message": "Select a contact or group to start chatting."
|
||||
},
|
||||
"typingAlt": {
|
||||
"message": "Typing animation for this conversation",
|
||||
"description": "Used as the 'title' attibute for the typing animation"
|
||||
},
|
||||
"contactAvatarAlt": {
|
||||
"message": "Avatar for contact $name$",
|
||||
"description": "Used in the alt tag for the image avatar of a contact",
|
||||
|
|
|
@ -140,6 +140,10 @@
|
|||
</div>
|
||||
</div>
|
||||
</script>
|
||||
<script type='text/x-tmpl-mustache' id='message-list'>
|
||||
<div class='messages'></div>
|
||||
<div class='typing-container'></div>
|
||||
</script>
|
||||
<script type='text/x-tmpl-mustache' id='recorder'>
|
||||
<button class='finish'><span class='icon'></span></button>
|
||||
<span class='time'>0:00</span>
|
||||
|
|
|
@ -664,6 +664,7 @@
|
|||
messageReceiver.addEventListener('reconnect', onReconnect);
|
||||
messageReceiver.addEventListener('progress', onProgress);
|
||||
messageReceiver.addEventListener('configuration', onConfiguration);
|
||||
messageReceiver.addEventListener('typing', onTyping);
|
||||
|
||||
window.textsecure.messaging = new textsecure.MessageSender(
|
||||
USERNAME,
|
||||
|
@ -790,10 +791,14 @@
|
|||
}
|
||||
function onConfiguration(ev) {
|
||||
const { configuration } = ev;
|
||||
const {
|
||||
readReceipts,
|
||||
typingIndicators,
|
||||
unidentifiedDeliveryIndicators,
|
||||
} = configuration;
|
||||
|
||||
storage.put('read-receipt-setting', configuration.readReceipts);
|
||||
storage.put('read-receipt-setting', readReceipts);
|
||||
|
||||
const { unidentifiedDeliveryIndicators } = configuration;
|
||||
if (
|
||||
unidentifiedDeliveryIndicators === true ||
|
||||
unidentifiedDeliveryIndicators === false
|
||||
|
@ -803,9 +808,34 @@
|
|||
unidentifiedDeliveryIndicators
|
||||
);
|
||||
}
|
||||
|
||||
if (typingIndicators === true || typingIndicators === false) {
|
||||
storage.put('typingIndicators', typingIndicators);
|
||||
}
|
||||
|
||||
ev.confirm();
|
||||
}
|
||||
|
||||
function onTyping(ev) {
|
||||
const { typing, sender, senderDevice } = ev;
|
||||
const { groupId, started } = typing || {};
|
||||
|
||||
// We don't do anything with incoming typing messages if the setting is disabled
|
||||
if (!storage.get('typingIndicators')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const conversation = ConversationController.get(groupId || sender);
|
||||
|
||||
if (conversation) {
|
||||
conversation.notifyTyping({
|
||||
isTyping: started,
|
||||
sender,
|
||||
senderDevice,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function onContactReceived(ev) {
|
||||
const details = ev.contactDetails;
|
||||
|
||||
|
|
|
@ -131,12 +131,93 @@
|
|||
this.unset('tokens');
|
||||
this.unset('lastMessage');
|
||||
this.unset('lastMessageStatus');
|
||||
|
||||
this.typingRefreshTimer = null;
|
||||
this.typingPauseTimer = null;
|
||||
},
|
||||
|
||||
isMe() {
|
||||
return this.id === this.ourNumber;
|
||||
},
|
||||
|
||||
bumpTyping() {
|
||||
// We don't send typing messages if the setting is disabled
|
||||
if (!storage.get('typingIndicators')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.typingRefreshTimer) {
|
||||
const isTyping = true;
|
||||
this.setTypingRefreshTimer();
|
||||
this.sendTypingMessage(isTyping);
|
||||
}
|
||||
|
||||
this.setTypingPauseTimer();
|
||||
},
|
||||
|
||||
setTypingRefreshTimer() {
|
||||
if (this.typingRefreshTimer) {
|
||||
clearTimeout(this.typingRefreshTimer);
|
||||
}
|
||||
this.typingRefreshTimer = setTimeout(
|
||||
this.onTypingRefreshTimeout.bind(this),
|
||||
10 * 1000
|
||||
);
|
||||
},
|
||||
|
||||
onTypingRefreshTimeout() {
|
||||
const isTyping = true;
|
||||
this.sendTypingMessage(isTyping);
|
||||
|
||||
// This timer will continue to reset itself until the pause timer stops it
|
||||
this.setTypingRefreshTimer();
|
||||
},
|
||||
|
||||
setTypingPauseTimer() {
|
||||
if (this.typingPauseTimer) {
|
||||
clearTimeout(this.typingPauseTimer);
|
||||
}
|
||||
this.typingPauseTimer = setTimeout(
|
||||
this.onTypingPauseTimeout.bind(this),
|
||||
3 * 1000
|
||||
);
|
||||
},
|
||||
|
||||
onTypingPauseTimeout() {
|
||||
const isTyping = false;
|
||||
this.sendTypingMessage(isTyping);
|
||||
|
||||
this.clearTypingTimers();
|
||||
},
|
||||
|
||||
clearTypingTimers() {
|
||||
if (this.typingPauseTimer) {
|
||||
clearTimeout(this.typingPauseTimer);
|
||||
this.typingPauseTimer = null;
|
||||
}
|
||||
if (this.typingRefreshTimer) {
|
||||
clearTimeout(this.typingRefreshTimer);
|
||||
this.typingRefreshTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
sendTypingMessage(isTyping) {
|
||||
const groupId = !this.isPrivate() ? this.id : null;
|
||||
const recipientId = this.isPrivate() ? this.id : null;
|
||||
|
||||
const sendOptions = this.getSendOptions();
|
||||
this.wrapSend(
|
||||
textsecure.messaging.sendTypingMessage(
|
||||
{
|
||||
groupId,
|
||||
isTyping,
|
||||
recipientId,
|
||||
},
|
||||
sendOptions
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async cleanup() {
|
||||
await window.Signal.Types.Conversation.deleteExternalFiles(
|
||||
this.attributes,
|
||||
|
@ -189,6 +270,12 @@
|
|||
},
|
||||
|
||||
addSingleMessage(message) {
|
||||
// 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);
|
||||
|
||||
const model = this.messageCollection.add(message, { merge: true });
|
||||
model.setToExpire();
|
||||
return model;
|
||||
|
@ -211,6 +298,8 @@
|
|||
};
|
||||
},
|
||||
getPropsForListItem() {
|
||||
const typingKeys = Object.keys(this.contactTypingTimers || {});
|
||||
|
||||
const result = {
|
||||
...this.format(),
|
||||
conversationType: this.isPrivate() ? 'direct' : 'group',
|
||||
|
@ -219,6 +308,7 @@
|
|||
unreadCount: this.get('unreadCount') || 0,
|
||||
isSelected: this.isSelected,
|
||||
|
||||
isTyping: typingKeys.length > 0,
|
||||
lastMessage: {
|
||||
status: this.lastMessageStatus,
|
||||
text: this.lastMessage,
|
||||
|
@ -698,6 +788,8 @@
|
|||
},
|
||||
|
||||
sendMessage(body, attachments, quote) {
|
||||
this.clearTypingTimers();
|
||||
|
||||
const destination = this.id;
|
||||
const expireTimer = this.get('expireTimer');
|
||||
const recipients = this.getRecipients();
|
||||
|
@ -1753,6 +1845,69 @@
|
|||
})
|
||||
);
|
||||
},
|
||||
|
||||
notifyTyping(options = {}) {
|
||||
const { isTyping, sender, senderDevice } = options;
|
||||
|
||||
// We don't do anything with typing messages from our other devices
|
||||
if (sender === this.ourNumber) {
|
||||
return;
|
||||
}
|
||||
|
||||
const identifier = `${sender}.${senderDevice}`;
|
||||
|
||||
this.contactTypingTimers = this.contactTypingTimers || {};
|
||||
const record = this.contactTypingTimers[identifier];
|
||||
|
||||
if (record) {
|
||||
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
|
||||
] || {
|
||||
timestamp: Date.now(),
|
||||
sender,
|
||||
senderDevice,
|
||||
};
|
||||
|
||||
this.contactTypingTimers[identifier].timer = setTimeout(
|
||||
this.clearContactTypingTimer.bind(this, identifier),
|
||||
15 * 1000
|
||||
);
|
||||
if (!record) {
|
||||
// User was not previously typing before. State change!
|
||||
this.trigger('typing-update');
|
||||
this.trigger('change');
|
||||
}
|
||||
} else {
|
||||
delete this.contactTypingTimers[identifier];
|
||||
if (record) {
|
||||
// User was previously typing, and is no longer. State change!
|
||||
this.trigger('typing-update');
|
||||
this.trigger('change');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
clearContactTypingTimer(identifier) {
|
||||
this.contactTypingTimers = this.contactTypingTimers || {};
|
||||
const record = this.contactTypingTimers[identifier];
|
||||
|
||||
if (record) {
|
||||
clearTimeout(record.timer);
|
||||
delete this.contactTypingTimers[identifier];
|
||||
|
||||
// User was previously typing, but timed out or we received message. State change!
|
||||
this.trigger('typing-update');
|
||||
this.trigger('change');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Whisper.ConversationCollection = Backbone.Collection.extend({
|
||||
|
|
|
@ -55,6 +55,9 @@ const {
|
|||
const {
|
||||
TimerNotification,
|
||||
} = require('../../ts/components/conversation/TimerNotification');
|
||||
const {
|
||||
TypingBubble,
|
||||
} = require('../../ts/components/conversation/TypingBubble');
|
||||
const {
|
||||
VerificationNotification,
|
||||
} = require('../../ts/components/conversation/VerificationNotification');
|
||||
|
@ -191,6 +194,7 @@ exports.setup = (options = {}) => {
|
|||
Types: {
|
||||
Message: MediaGalleryMessage,
|
||||
},
|
||||
TypingBubble,
|
||||
VerificationNotification,
|
||||
};
|
||||
|
||||
|
|
|
@ -695,6 +695,7 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
|||
messageArray,
|
||||
timestamp,
|
||||
silent,
|
||||
online,
|
||||
{ accessKey } = {}
|
||||
) {
|
||||
const jsonData = { messages: messageArray, timestamp };
|
||||
|
@ -702,6 +703,9 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
|||
if (silent) {
|
||||
jsonData.silent = true;
|
||||
}
|
||||
if (online) {
|
||||
jsonData.online = true;
|
||||
}
|
||||
|
||||
return _ajax({
|
||||
call: 'messages',
|
||||
|
@ -714,12 +718,21 @@ function initialize({ url, cdnUrl, certificateAuthority, proxyUrl }) {
|
|||
});
|
||||
}
|
||||
|
||||
function sendMessages(destination, messageArray, timestamp, silent) {
|
||||
function sendMessages(
|
||||
destination,
|
||||
messageArray,
|
||||
timestamp,
|
||||
silent,
|
||||
online
|
||||
) {
|
||||
const jsonData = { messages: messageArray, timestamp };
|
||||
|
||||
if (silent) {
|
||||
jsonData.silent = true;
|
||||
}
|
||||
if (online) {
|
||||
jsonData.online = true;
|
||||
}
|
||||
|
||||
return _ajax({
|
||||
call: 'messages',
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
/* global $: false */
|
||||
/* global _: false */
|
||||
/* global emojiData: false */
|
||||
/* global EmojiPanel: false */
|
||||
/* global extension: false */
|
||||
/* global i18n: false */
|
||||
/* global Signal: false */
|
||||
/* global storage: false */
|
||||
/* global Whisper: false */
|
||||
/* global
|
||||
$,
|
||||
_,
|
||||
emojiData,
|
||||
EmojiPanel,
|
||||
extension,
|
||||
i18n,
|
||||
Signal,
|
||||
storage,
|
||||
Whisper,
|
||||
ConversationController
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
|
@ -80,6 +83,7 @@
|
|||
this.listenTo(this.model, 'newmessage', this.addMessage);
|
||||
this.listenTo(this.model, 'opened', this.onOpened);
|
||||
this.listenTo(this.model, 'prune', this.onPrune);
|
||||
this.listenTo(this.model, 'typing-update', this.renderTypingBubble);
|
||||
this.listenTo(
|
||||
this.model.messageCollection,
|
||||
'show-identity',
|
||||
|
@ -236,6 +240,7 @@
|
|||
'submit .send': 'checkUnverifiedSendMessage',
|
||||
'input .send-message': 'updateMessageFieldSize',
|
||||
'keydown .send-message': 'updateMessageFieldSize',
|
||||
'keyup .send-message': 'maybeBumpTyping',
|
||||
click: 'onClick',
|
||||
'click .bottom-bar': 'focusMessageField',
|
||||
'click .capture-audio .microphone': 'captureAudio',
|
||||
|
@ -421,6 +426,43 @@
|
|||
}
|
||||
},
|
||||
|
||||
renderTypingBubble() {
|
||||
const timers = this.model.contactTypingTimers || {};
|
||||
const records = _.values(timers);
|
||||
const mostRecent = _.first(_.sortBy(records, 'timestamp'));
|
||||
|
||||
if (!mostRecent && this.typingBubbleView) {
|
||||
this.typingBubbleView.remove();
|
||||
this.typingBubbleView = null;
|
||||
}
|
||||
if (!mostRecent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { sender } = mostRecent;
|
||||
const contact = ConversationController.getOrCreate(sender, 'private');
|
||||
const props = {
|
||||
...contact.format(),
|
||||
conversationType: this.model.isPrivate() ? 'direct' : 'group',
|
||||
};
|
||||
|
||||
if (this.typingBubbleView) {
|
||||
this.typingBubbleView.update(props);
|
||||
return;
|
||||
}
|
||||
|
||||
this.typingBubbleView = new Whisper.ReactWrapperView({
|
||||
className: 'message-wrapper typing-bubble-wrapper',
|
||||
Component: Signal.Components.TypingBubble,
|
||||
props,
|
||||
});
|
||||
this.typingBubbleView.$el.appendTo(this.$('.typing-container'));
|
||||
|
||||
if (this.view.atBottom()) {
|
||||
this.typingBubbleView.el.scrollIntoView();
|
||||
}
|
||||
},
|
||||
|
||||
toggleMicrophone() {
|
||||
if (
|
||||
this.$('.send-message').val().length > 0 ||
|
||||
|
@ -538,6 +580,7 @@
|
|||
this.view.resetScrollPosition();
|
||||
this.$el.trigger('force-resize');
|
||||
this.focusMessageField();
|
||||
this.renderTypingBubble();
|
||||
|
||||
if (this.inProgressFetch) {
|
||||
// eslint-disable-next-line more/no-then
|
||||
|
@ -1492,6 +1535,7 @@
|
|||
async sendMessage(e) {
|
||||
this.removeLastSeenIndicator();
|
||||
this.closeEmojiPanel();
|
||||
this.model.clearTypingTimers();
|
||||
|
||||
let toast;
|
||||
if (extension.expired()) {
|
||||
|
@ -1543,6 +1587,15 @@
|
|||
}
|
||||
},
|
||||
|
||||
// Called whenever the user changes the message composition field. But only
|
||||
// fires if there's content in the message field after the change.
|
||||
maybeBumpTyping() {
|
||||
const messageText = this.$messageField.val();
|
||||
if (messageText.length) {
|
||||
this.model.bumpTyping();
|
||||
}
|
||||
},
|
||||
|
||||
updateMessageFieldSize(event) {
|
||||
const keyCode = event.which || event.keyCode;
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global Whisper, _ */
|
||||
/* global Whisper, Backbone, _, $ */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
|
@ -6,15 +6,36 @@
|
|||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
Whisper.MessageListView = Whisper.ListView.extend({
|
||||
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() {
|
||||
Whisper.ListView.prototype.initialize.call(this);
|
||||
this.listenTo(this.collection, 'add', this.addOne);
|
||||
this.listenTo(this.collection, 'reset', this.addAll);
|
||||
|
||||
this.render();
|
||||
|
||||
this.triggerLazyScroll = _.debounce(() => {
|
||||
this.$el.trigger('lazyScroll');
|
||||
|
@ -78,10 +99,10 @@
|
|||
|
||||
if (index === this.collection.length - 1) {
|
||||
// add to the bottom.
|
||||
this.$el.append(view.el);
|
||||
this.$messages.append(view.el);
|
||||
} else if (index === 0) {
|
||||
// add to top
|
||||
this.$el.prepend(view.el);
|
||||
this.$messages.prepend(view.el);
|
||||
} else {
|
||||
// insert
|
||||
const next = this.$(`#${this.collection.at(index + 1).id}`);
|
||||
|
@ -92,7 +113,7 @@
|
|||
view.$el.insertAfter(prev);
|
||||
} else {
|
||||
// scan for the right spot
|
||||
const elements = this.$el.children();
|
||||
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);
|
||||
|
@ -103,7 +124,7 @@
|
|||
}
|
||||
}
|
||||
} else {
|
||||
this.$el.append(view.el);
|
||||
this.$messages.append(view.el);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -935,6 +935,8 @@ MessageReceiver.prototype.extend({
|
|||
return this.handleCallMessage(envelope, content.callMessage);
|
||||
} else if (content.receiptMessage) {
|
||||
return this.handleReceiptMessage(envelope, content.receiptMessage);
|
||||
} else if (content.typingMessage) {
|
||||
return this.handleTypingMessage(envelope, content.typingMessage);
|
||||
}
|
||||
this.removeFromCache(envelope);
|
||||
throw new Error('Unsupported content message');
|
||||
|
@ -974,6 +976,43 @@ MessageReceiver.prototype.extend({
|
|||
}
|
||||
return Promise.all(results);
|
||||
},
|
||||
handleTypingMessage(envelope, typingMessage) {
|
||||
const ev = new Event('typing');
|
||||
|
||||
this.removeFromCache(envelope);
|
||||
|
||||
if (envelope.timestamp && typingMessage.timestamp) {
|
||||
const envelopeTimestamp = envelope.timestamp.toNumber();
|
||||
const typingTimestamp = typingMessage.timestamp.toNumber();
|
||||
|
||||
if (typingTimestamp !== envelopeTimestamp) {
|
||||
window.log.warn(
|
||||
`Typing message envelope timestamp (${envelopeTimestamp}) did not match typing timestamp (${typingTimestamp})`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
ev.sender = envelope.source;
|
||||
ev.senderDevice = envelope.sourceDevice;
|
||||
ev.typing = {
|
||||
typingMessage,
|
||||
timestamp: typingMessage.timestamp
|
||||
? typingMessage.timestamp.toNumber()
|
||||
: Date.now(),
|
||||
groupId: typingMessage.groupId
|
||||
? typingMessage.groupId.toString('binary')
|
||||
: null,
|
||||
started:
|
||||
typingMessage.action ===
|
||||
textsecure.protobuf.TypingMessage.Action.STARTED,
|
||||
stopped:
|
||||
typingMessage.action ===
|
||||
textsecure.protobuf.TypingMessage.Action.STOPPED,
|
||||
};
|
||||
|
||||
return this.dispatchEvent(ev);
|
||||
},
|
||||
handleNullMessage(envelope) {
|
||||
window.log.info('null message from', this.getEnvelopeId(envelope));
|
||||
this.removeFromCache(envelope);
|
||||
|
|
|
@ -30,9 +30,10 @@ function OutgoingMessage(
|
|||
this.failoverNumbers = [];
|
||||
this.unidentifiedDeliveries = [];
|
||||
|
||||
const { numberInfo, senderCertificate } = options;
|
||||
const { numberInfo, senderCertificate, online } = options;
|
||||
this.numberInfo = numberInfo;
|
||||
this.senderCertificate = senderCertificate;
|
||||
this.online = online;
|
||||
}
|
||||
|
||||
OutgoingMessage.prototype = {
|
||||
|
@ -192,6 +193,7 @@ OutgoingMessage.prototype = {
|
|||
jsonData,
|
||||
timestamp,
|
||||
this.silent,
|
||||
this.online,
|
||||
{ accessKey }
|
||||
);
|
||||
} else {
|
||||
|
@ -199,7 +201,8 @@ OutgoingMessage.prototype = {
|
|||
number,
|
||||
jsonData,
|
||||
timestamp,
|
||||
this.silent
|
||||
this.silent,
|
||||
this.online
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -316,10 +316,31 @@ MessageSender.prototype = {
|
|||
});
|
||||
},
|
||||
|
||||
sendMessageProtoAndWait(timestamp, numbers, message, silent, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const callback = result => {
|
||||
if (result && result.errors && result.errors.length > 0) {
|
||||
return reject(result);
|
||||
}
|
||||
|
||||
return resolve(result);
|
||||
};
|
||||
|
||||
this.sendMessageProto(
|
||||
timestamp,
|
||||
numbers,
|
||||
message,
|
||||
callback,
|
||||
silent,
|
||||
options
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
sendIndividualProto(number, proto, timestamp, silent, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const callback = res => {
|
||||
if (res.errors.length > 0) {
|
||||
if (res && res.errors && res.errors.length > 0) {
|
||||
reject(res);
|
||||
} else {
|
||||
resolve(res);
|
||||
|
@ -447,6 +468,7 @@ MessageSender.prototype = {
|
|||
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
sendRequestGroupSyncMessage(options) {
|
||||
const myNumber = textsecure.storage.user.getNumber();
|
||||
const myDevice = textsecure.storage.user.getDeviceId();
|
||||
|
@ -494,6 +516,55 @@ MessageSender.prototype = {
|
|||
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
async sendTypingMessage(options = {}, sendOptions = {}) {
|
||||
const ACTION_ENUM = textsecure.protobuf.TypingMessage.Action;
|
||||
const { recipientId, groupId, isTyping, timestamp } = options;
|
||||
|
||||
// We don't want to send typing messages to our other devices, but we will
|
||||
// in the group case.
|
||||
const myNumber = textsecure.storage.user.getNumber();
|
||||
if (recipientId && myNumber === recipientId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!recipientId && !groupId) {
|
||||
throw new Error('Need to provide either recipientId or groupId!');
|
||||
}
|
||||
|
||||
const recipients = groupId
|
||||
? await textsecure.storage.groups.getNumbers(groupId)
|
||||
: [recipientId];
|
||||
const groupIdBuffer = groupId
|
||||
? window.Signal.Crypto.fromEncodedBinaryToArrayBuffer(groupId)
|
||||
: null;
|
||||
|
||||
const action = isTyping ? ACTION_ENUM.STARTED : ACTION_ENUM.STOPPED;
|
||||
const finalTimestamp = timestamp || Date.now();
|
||||
|
||||
const typingMessage = new textsecure.protobuf.TypingMessage();
|
||||
typingMessage.groupId = groupIdBuffer;
|
||||
typingMessage.action = action;
|
||||
typingMessage.timestamp = finalTimestamp;
|
||||
|
||||
const contentMessage = new textsecure.protobuf.Content();
|
||||
contentMessage.typingMessage = typingMessage;
|
||||
|
||||
const silent = true;
|
||||
const online = true;
|
||||
|
||||
return this.sendMessageProtoAndWait(
|
||||
finalTimestamp,
|
||||
recipients,
|
||||
contentMessage,
|
||||
silent,
|
||||
{
|
||||
...sendOptions,
|
||||
online,
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
sendDeliveryReceipt(recipientId, timestamp, options) {
|
||||
const myNumber = textsecure.storage.user.getNumber();
|
||||
const myDevice = textsecure.storage.user.getDeviceId();
|
||||
|
@ -517,6 +588,7 @@ MessageSender.prototype = {
|
|||
options
|
||||
);
|
||||
},
|
||||
|
||||
sendReadReceipts(sender, timestamps, options) {
|
||||
const receiptMessage = new textsecure.protobuf.ReceiptMessage();
|
||||
receiptMessage.type = textsecure.protobuf.ReceiptMessage.Type.READ;
|
||||
|
@ -971,6 +1043,7 @@ textsecure.MessageSender = function MessageSenderWrapper(
|
|||
this.sendMessage = sender.sendMessage.bind(sender);
|
||||
this.resetSession = sender.resetSession.bind(sender);
|
||||
this.sendMessageToGroup = sender.sendMessageToGroup.bind(sender);
|
||||
this.sendTypingMessage = sender.sendTypingMessage.bind(sender);
|
||||
this.createGroup = sender.createGroup.bind(sender);
|
||||
this.updateGroup = sender.updateGroup.bind(sender);
|
||||
this.addNumberToGroup = sender.addNumberToGroup.bind(sender);
|
||||
|
|
|
@ -32,6 +32,7 @@ message Content {
|
|||
optional CallMessage callMessage = 3;
|
||||
optional NullMessage nullMessage = 4;
|
||||
optional ReceiptMessage receiptMessage = 5;
|
||||
optional TypingMessage typingMessage = 6;
|
||||
}
|
||||
|
||||
message CallMessage {
|
||||
|
@ -180,6 +181,17 @@ message ReceiptMessage {
|
|||
repeated uint64 timestamp = 2;
|
||||
}
|
||||
|
||||
message TypingMessage {
|
||||
enum Action {
|
||||
STARTED = 0;
|
||||
STOPPED = 1;
|
||||
}
|
||||
|
||||
optional uint64 timestamp = 1;
|
||||
optional Action action = 2;
|
||||
optional bytes groupId = 3;
|
||||
}
|
||||
|
||||
message Verified {
|
||||
enum State {
|
||||
DEFAULT = 0;
|
||||
|
@ -241,6 +253,7 @@ message SyncMessage {
|
|||
message Configuration {
|
||||
optional bool readReceipts = 1;
|
||||
optional bool unidentifiedDeliveryIndicators = 2;
|
||||
optional bool typingIndicators = 3;
|
||||
}
|
||||
|
||||
optional Sent sent = 1;
|
||||
|
|
|
@ -136,14 +136,14 @@
|
|||
.message-list {
|
||||
list-style: none;
|
||||
|
||||
.message-wrapper {
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
|
||||
.message-wrapper {
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
visibility: hidden;
|
||||
display: block;
|
||||
|
@ -158,12 +158,16 @@
|
|||
.group {
|
||||
.message-container,
|
||||
.message-list {
|
||||
li .message-wrapper {
|
||||
.message-wrapper {
|
||||
margin-left: 44px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.typing-bubble-wrapper {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.contact-detail-pane {
|
||||
overflow-y: scroll;
|
||||
padding-top: 40px;
|
||||
|
|
|
@ -500,11 +500,18 @@
|
|||
|
||||
.module-message__author-avatar {
|
||||
position: absolute;
|
||||
// This accounts for the weird extra 3px we get at the bottom of messages
|
||||
bottom: 0px;
|
||||
right: calc(100% + 4px);
|
||||
}
|
||||
|
||||
.module-message__typing-container {
|
||||
height: 16px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// Module: Expire Timer
|
||||
|
||||
.module-expire-timer {
|
||||
|
@ -1774,8 +1781,6 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.module-conversation-list-item__message__text {
|
||||
|
@ -2161,6 +2166,93 @@
|
|||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
// Module: Typing Animation
|
||||
|
||||
.module-typing-animation {
|
||||
display: inline-flex;
|
||||
flex-directin: row;
|
||||
align-items: center;
|
||||
|
||||
height: 8px;
|
||||
width: 38px;
|
||||
padding-left: 1px;
|
||||
padding-right: 1px;
|
||||
}
|
||||
|
||||
.module-typing-animation__dot {
|
||||
border-radius: 50%;
|
||||
background-color: $color-gray-60;
|
||||
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.module-typing-animation__dot--light {
|
||||
border-radius: 50%;
|
||||
background-color: $color-white;
|
||||
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
@keyframes typing-animation-first {
|
||||
0% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
20% {
|
||||
transform: scale(1.3);
|
||||
opacity: 1;
|
||||
}
|
||||
40% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes typing-animation-second {
|
||||
10% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
30% {
|
||||
transform: scale(1.3);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes typing-animation-third {
|
||||
20% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1.3);
|
||||
opacity: 1;
|
||||
}
|
||||
60% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.module-typing-animation__dot--first {
|
||||
animation: typing-animation-first 1600ms ease infinite;
|
||||
}
|
||||
|
||||
.module-typing-animation__dot--second {
|
||||
animation: typing-animation-second 1600ms ease infinite;
|
||||
}
|
||||
|
||||
.module-typing-animation__dot--third {
|
||||
animation: typing-animation-third 1600ms ease infinite;
|
||||
}
|
||||
|
||||
.module-typing-animation__spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
// Third-party module: react-contextmenu
|
||||
|
|
|
@ -1368,6 +1368,16 @@ body.dark-theme {
|
|||
color: $color-dark-05;
|
||||
}
|
||||
|
||||
// Module: Typing Animation
|
||||
|
||||
.module-typing-animation__dot {
|
||||
background-color: $color-white;
|
||||
}
|
||||
|
||||
.module-typing-animation__dot--light {
|
||||
background-color: $color-white;
|
||||
}
|
||||
|
||||
// Third-party module: react-contextmenu
|
||||
|
||||
.react-contextmenu {
|
||||
|
|
|
@ -112,6 +112,40 @@
|
|||
</util.LeftPaneContext>
|
||||
```
|
||||
|
||||
#### Is typing
|
||||
|
||||
```jsx
|
||||
<util.LeftPaneContext theme={util.theme}>
|
||||
<div>
|
||||
<ConversationListItem
|
||||
phoneNumber="(202) 555-0011"
|
||||
conversationType={'direct'}
|
||||
unreadCount={4}
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
isTyping={true}
|
||||
onClick={() => console.log('onClick')}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ConversationListItem
|
||||
phoneNumber="(202) 555-0011"
|
||||
conversationType={'direct'}
|
||||
unreadCount={4}
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
isTyping={true}
|
||||
lastMessage={{
|
||||
status: 'read',
|
||||
}}
|
||||
onClick={() => console.log('onClick')}
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</div>
|
||||
</util.LeftPaneContext>
|
||||
```
|
||||
|
||||
#### Selected
|
||||
|
||||
#### With unread
|
||||
|
||||
```jsx
|
||||
|
|
|
@ -5,6 +5,8 @@ import { Avatar } from './Avatar';
|
|||
import { MessageBody } from './conversation/MessageBody';
|
||||
import { Timestamp } from './conversation/Timestamp';
|
||||
import { ContactName } from './conversation/ContactName';
|
||||
import { TypingAnimation } from './conversation/TypingAnimation';
|
||||
|
||||
import { Localizer } from '../types/Util';
|
||||
|
||||
interface Props {
|
||||
|
@ -19,6 +21,7 @@ interface Props {
|
|||
unreadCount: number;
|
||||
isSelected: boolean;
|
||||
|
||||
isTyping: boolean;
|
||||
lastMessage?: {
|
||||
status: 'sending' | 'sent' | 'delivered' | 'read' | 'error';
|
||||
text: string;
|
||||
|
@ -118,9 +121,9 @@ export class ConversationListItem extends React.Component<Props> {
|
|||
}
|
||||
|
||||
public renderMessage() {
|
||||
const { lastMessage, unreadCount, i18n } = this.props;
|
||||
const { lastMessage, isTyping, unreadCount, i18n } = this.props;
|
||||
|
||||
if (!lastMessage) {
|
||||
if (!lastMessage && !isTyping) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -134,14 +137,18 @@ export class ConversationListItem extends React.Component<Props> {
|
|||
: null
|
||||
)}
|
||||
>
|
||||
<MessageBody
|
||||
text={lastMessage.text || ''}
|
||||
disableJumbomoji={true}
|
||||
disableLinks={true}
|
||||
i18n={i18n}
|
||||
/>
|
||||
{isTyping ? (
|
||||
<TypingAnimation i18n={i18n} />
|
||||
) : (
|
||||
<MessageBody
|
||||
text={lastMessage && lastMessage.text ? lastMessage.text : ''}
|
||||
disableJumbomoji={true}
|
||||
disableLinks={true}
|
||||
i18n={i18n}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{lastMessage.status ? (
|
||||
{lastMessage && lastMessage.status ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-conversation-list-item__message__status-icon',
|
||||
|
|
22
ts/components/conversation/TypingAnimation.md
Normal file
22
ts/components/conversation/TypingAnimation.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
### Conversation List
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<TypingAnimation i18n={util.i18n} />
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
### Dark background
|
||||
|
||||
Note: background color is 'steel'
|
||||
|
||||
```jsx
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: '#6b6b78',
|
||||
padding: '2em',
|
||||
}}
|
||||
>
|
||||
<TypingAnimation color="light" i18n={util.i18n} />
|
||||
</div>
|
||||
```
|
43
ts/components/conversation/TypingAnimation.tsx
Normal file
43
ts/components/conversation/TypingAnimation.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Localizer } from '../../types/Util';
|
||||
|
||||
interface Props {
|
||||
i18n: Localizer;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export class TypingAnimation extends React.Component<Props> {
|
||||
public render() {
|
||||
const { i18n, color } = this.props;
|
||||
|
||||
return (
|
||||
<div className="module-typing-animation" title={i18n('typingAlt')}>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-typing-animation__dot',
|
||||
'module-typing-animation__dot--first',
|
||||
color ? `module-typing-animation__dot--${color}` : null
|
||||
)}
|
||||
/>
|
||||
<div className="module-typing-animation__spacer" />
|
||||
<div
|
||||
className={classNames(
|
||||
'module-typing-animation__dot',
|
||||
'module-typing-animation__dot--second',
|
||||
color ? `module-typing-animation__dot--${color}` : null
|
||||
)}
|
||||
/>
|
||||
<div className="module-typing-animation__spacer" />
|
||||
<div
|
||||
className={classNames(
|
||||
'module-typing-animation__dot',
|
||||
'module-typing-animation__dot--third',
|
||||
color ? `module-typing-animation__dot--${color}` : null
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
38
ts/components/conversation/TypingBubble.md
Normal file
38
ts/components/conversation/TypingBubble.md
Normal file
|
@ -0,0 +1,38 @@
|
|||
### In message bubble
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<li>
|
||||
<TypingBubble conversationType="direct" i18n={util.i18n} />
|
||||
</li>
|
||||
<li>
|
||||
<TypingBubble color="teal" conversationType="direct" i18n={util.i18n} />
|
||||
</li>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
### In message bubble, group conversation
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<li>
|
||||
<TypingBubble color="red" conversationType="group" i18n={util.i18n} />
|
||||
</li>
|
||||
<li>
|
||||
<TypingBubble
|
||||
color="purple"
|
||||
authorName="First Last"
|
||||
conversationType="group"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<TypingBubble
|
||||
avatarPath={util.gifObjectUrl}
|
||||
color="blue"
|
||||
conversationType="group"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</li>
|
||||
</util.ConversationContext>
|
||||
```
|
71
ts/components/conversation/TypingBubble.tsx
Normal file
71
ts/components/conversation/TypingBubble.tsx
Normal file
|
@ -0,0 +1,71 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { TypingAnimation } from './TypingAnimation';
|
||||
import { Avatar } from '../Avatar';
|
||||
|
||||
import { Localizer } from '../../types/Util';
|
||||
|
||||
interface Props {
|
||||
avatarPath?: string;
|
||||
color: string;
|
||||
name: string;
|
||||
phoneNumber: string;
|
||||
profileName: string;
|
||||
conversationType: string;
|
||||
i18n: Localizer;
|
||||
}
|
||||
|
||||
export class TypingBubble extends React.Component<Props> {
|
||||
public renderAvatar() {
|
||||
const {
|
||||
avatarPath,
|
||||
color,
|
||||
name,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
conversationType,
|
||||
i18n,
|
||||
} = this.props;
|
||||
|
||||
if (conversationType !== 'group') {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-message__author-avatar">
|
||||
<Avatar
|
||||
avatarPath={avatarPath}
|
||||
color={color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
name={name}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
size={36}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { i18n, color } = this.props;
|
||||
|
||||
return (
|
||||
<div className={classNames('module-message', 'module-message--incoming')}>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-message__container',
|
||||
'module-message__container--incoming',
|
||||
`module-message__container--incoming-${color}`
|
||||
)}
|
||||
>
|
||||
<div className="module-message__typing-container">
|
||||
<TypingAnimation color="light" i18n={i18n} />
|
||||
</div>
|
||||
{this.renderAvatar()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -105,3 +105,10 @@ export { theme, ios, locale, i18n };
|
|||
// Telling Lodash to relinquish _ for use by underscore
|
||||
// @ts-ignore
|
||||
_.noConflict();
|
||||
|
||||
// @ts-ignore
|
||||
window.log = {
|
||||
info: console.log,
|
||||
error: console.log,
|
||||
war: console.log,
|
||||
};
|
||||
|
|
|
@ -244,7 +244,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "js/background.js",
|
||||
"line": " wrap(",
|
||||
"lineNumber": 739,
|
||||
"lineNumber": 740,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-10-18T22:23:00.485Z"
|
||||
},
|
||||
|
@ -252,7 +252,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "js/background.js",
|
||||
"line": " await wrap(",
|
||||
"lineNumber": 1240,
|
||||
"lineNumber": 1270,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-10-26T22:43:23.229Z"
|
||||
},
|
||||
|
@ -667,7 +667,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " template: $('#conversation').html(),",
|
||||
"lineNumber": 70,
|
||||
"lineNumber": 73,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -676,7 +676,7 @@
|
|||
"rule": "jQuery-html(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " template: $('#conversation').html(),",
|
||||
"lineNumber": 70,
|
||||
"lineNumber": 73,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-15T00:38:04.183Z",
|
||||
"reasonDetail": "Getting the value, not setting it"
|
||||
|
@ -685,34 +685,34 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.loadingScreen.$el.prependTo(this.$('.discussion-container'));",
|
||||
"lineNumber": 139,
|
||||
"lineNumber": 143,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"updated": "2018-11-14T19:09:08.182Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-prependTo(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.loadingScreen.$el.prependTo(this.$('.discussion-container'));",
|
||||
"lineNumber": 139,
|
||||
"lineNumber": 143,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
"updated": "2018-11-14T19:07:46.079Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " el: this.$('form.send'),",
|
||||
"lineNumber": 143,
|
||||
"lineNumber": 147,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"updated": "2018-11-14T19:07:46.079Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.conversation-header').append(this.titleView.el);",
|
||||
"lineNumber": 201,
|
||||
"lineNumber": 205,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -721,7 +721,7 @@
|
|||
"rule": "jQuery-append(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.conversation-header').append(this.titleView.el);",
|
||||
"lineNumber": 201,
|
||||
"lineNumber": 205,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
|
@ -730,7 +730,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.discussion-container').append(this.view.el);",
|
||||
"lineNumber": 207,
|
||||
"lineNumber": 211,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -739,7 +739,7 @@
|
|||
"rule": "jQuery-append(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.discussion-container').append(this.view.el);",
|
||||
"lineNumber": 207,
|
||||
"lineNumber": 211,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
|
@ -748,7 +748,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$messageField = this.$('.send-message');",
|
||||
"lineNumber": 210,
|
||||
"lineNumber": 214,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -757,7 +757,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.send-message').focus(this.focusBottomBar.bind(this));",
|
||||
"lineNumber": 228,
|
||||
"lineNumber": 232,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -766,7 +766,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$emojiPanelContainer = this.$('.emoji-panel-container');",
|
||||
"lineNumber": 231,
|
||||
"lineNumber": 235,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -775,7 +775,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " const container = this.$('.discussion-container');",
|
||||
"lineNumber": 416,
|
||||
"lineNumber": 421,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -784,16 +784,34 @@
|
|||
"rule": "jQuery-append(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " container.append(this.banner.el);",
|
||||
"lineNumber": 417,
|
||||
"lineNumber": 422,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.typingBubbleView.$el.appendTo(this.$('.typing-container'));",
|
||||
"lineNumber": 459,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-11-14T18:51:15.180Z",
|
||||
"reasonDetail": "$() parameter is a hard-coded string"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-appendTo(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.typingBubbleView.$el.appendTo(this.$('.typing-container'));",
|
||||
"lineNumber": 459,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-11-14T18:51:15.180Z",
|
||||
"reasonDetail": "Both parameters are known elements from the DOM"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.send-message').val().length > 0 ||",
|
||||
"lineNumber": 426,
|
||||
"lineNumber": 468,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -802,7 +820,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.capture-audio').hide();",
|
||||
"lineNumber": 429,
|
||||
"lineNumber": 471,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -811,7 +829,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.capture-audio').show();",
|
||||
"lineNumber": 431,
|
||||
"lineNumber": 473,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -820,7 +838,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " if (this.$('.send-message').val().length > 2000) {",
|
||||
"lineNumber": 435,
|
||||
"lineNumber": 477,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -829,7 +847,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.android-length-warning').hide();",
|
||||
"lineNumber": 438,
|
||||
"lineNumber": 480,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -838,7 +856,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " view.$el.appendTo(this.$('.capture-audio'));",
|
||||
"lineNumber": 458,
|
||||
"lineNumber": 500,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -847,7 +865,7 @@
|
|||
"rule": "jQuery-appendTo(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " view.$el.appendTo(this.$('.capture-audio'));",
|
||||
"lineNumber": 458,
|
||||
"lineNumber": 500,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
|
@ -856,7 +874,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.send-message').attr('disabled', true);",
|
||||
"lineNumber": 460,
|
||||
"lineNumber": 502,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -865,7 +883,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.bottom-bar form').submit();",
|
||||
"lineNumber": 467,
|
||||
"lineNumber": 509,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -874,7 +892,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.send-message').removeAttr('disabled');",
|
||||
"lineNumber": 470,
|
||||
"lineNumber": 512,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -883,7 +901,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.bottom-bar form').removeClass('active');",
|
||||
"lineNumber": 476,
|
||||
"lineNumber": 518,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -892,7 +910,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.bottom-bar form').addClass('active');",
|
||||
"lineNumber": 479,
|
||||
"lineNumber": 521,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -901,7 +919,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " const container = this.$('.discussion-container');",
|
||||
"lineNumber": 566,
|
||||
"lineNumber": 609,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -910,7 +928,7 @@
|
|||
"rule": "jQuery-append(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " container.append(this.scrollDownButton.el);",
|
||||
"lineNumber": 567,
|
||||
"lineNumber": 610,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
|
@ -919,7 +937,7 @@
|
|||
"rule": "jQuery-appendTo(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " toast.$el.appendTo(this.$el);",
|
||||
"lineNumber": 594,
|
||||
"lineNumber": 637,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
|
@ -928,7 +946,7 @@
|
|||
"rule": "jQuery-appendTo(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " toast.$el.appendTo(this.$el);",
|
||||
"lineNumber": 627,
|
||||
"lineNumber": 670,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
|
@ -937,7 +955,7 @@
|
|||
"rule": "jQuery-appendTo(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " toast.$el.appendTo(this.$el);",
|
||||
"lineNumber": 631,
|
||||
"lineNumber": 674,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
|
@ -946,7 +964,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " const el = this.$(`#${databaseId}`);",
|
||||
"lineNumber": 638,
|
||||
"lineNumber": 681,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -955,7 +973,7 @@
|
|||
"rule": "jQuery-appendTo(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " toast.$el.appendTo(this.$el);",
|
||||
"lineNumber": 641,
|
||||
"lineNumber": 684,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
|
@ -964,7 +982,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " lastSeenEl.insertBefore(this.$(`#${oldestUnread.get('id')}`));",
|
||||
"lineNumber": 818,
|
||||
"lineNumber": 861,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -973,7 +991,7 @@
|
|||
"rule": "jQuery-insertBefore(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " lastSeenEl.insertBefore(this.$(`#${oldestUnread.get('id')}`));",
|
||||
"lineNumber": 818,
|
||||
"lineNumber": 861,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
|
@ -982,7 +1000,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.bar-container').show();",
|
||||
"lineNumber": 873,
|
||||
"lineNumber": 916,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -991,7 +1009,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.bar-container').hide();",
|
||||
"lineNumber": 885,
|
||||
"lineNumber": 928,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -1000,7 +1018,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " const el = this.$(`#${message.id}`);",
|
||||
"lineNumber": 982,
|
||||
"lineNumber": 1025,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -1009,7 +1027,7 @@
|
|||
"rule": "jQuery-prepend(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$el.prepend(dialog.el);",
|
||||
"lineNumber": 1055,
|
||||
"lineNumber": 1098,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
|
@ -1018,7 +1036,7 @@
|
|||
"rule": "jQuery-appendTo(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " toast.$el.appendTo(this.$el);",
|
||||
"lineNumber": 1078,
|
||||
"lineNumber": 1121,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-10-11T19:22:47.331Z",
|
||||
"reasonDetail": "Operating on already-existing DOM elements"
|
||||
|
@ -1027,7 +1045,7 @@
|
|||
"rule": "jQuery-prepend(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$el.prepend(dialog.el);",
|
||||
"lineNumber": 1106,
|
||||
"lineNumber": 1149,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
|
@ -1036,7 +1054,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " view.$el.insertBefore(this.$('.panel').first());",
|
||||
"lineNumber": 1240,
|
||||
"lineNumber": 1283,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -1045,7 +1063,7 @@
|
|||
"rule": "jQuery-insertBefore(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " view.$el.insertBefore(this.$('.panel').first());",
|
||||
"lineNumber": 1240,
|
||||
"lineNumber": 1283,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
|
@ -1054,7 +1072,7 @@
|
|||
"rule": "jQuery-prepend(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$el.prepend(dialog.el);",
|
||||
"lineNumber": 1318,
|
||||
"lineNumber": 1361,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
|
@ -1063,7 +1081,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.send').prepend(this.quoteView.el);",
|
||||
"lineNumber": 1488,
|
||||
"lineNumber": 1531,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -1072,7 +1090,7 @@
|
|||
"rule": "jQuery-prepend(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.send').prepend(this.quoteView.el);",
|
||||
"lineNumber": 1488,
|
||||
"lineNumber": 1531,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
|
@ -1081,7 +1099,7 @@
|
|||
"rule": "jQuery-appendTo(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " toast.$el.appendTo(this.$el);",
|
||||
"lineNumber": 1511,
|
||||
"lineNumber": 1555,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
|
@ -1090,7 +1108,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.bottom-bar form').submit();",
|
||||
"lineNumber": 1557,
|
||||
"lineNumber": 1610,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -1099,7 +1117,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " const $attachmentPreviews = this.$('.attachment-previews');",
|
||||
"lineNumber": 1566,
|
||||
"lineNumber": 1619,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -1108,7 +1126,7 @@
|
|||
"rule": "jQuery-$(",
|
||||
"path": "js/views/conversation_view.js",
|
||||
"line": " this.$('.panel').css('display') === 'none'",
|
||||
"lineNumber": 1597,
|
||||
"lineNumber": 1650,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
|
@ -1651,68 +1669,95 @@
|
|||
"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.$el.append(view.el);",
|
||||
"lineNumber": 81,
|
||||
"line": " this.$messages.append(view.el);",
|
||||
"lineNumber": 102,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
"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.$el.prepend(view.el);",
|
||||
"lineNumber": 84,
|
||||
"line": " this.$messages.prepend(view.el);",
|
||||
"lineNumber": 105,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
"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": 87,
|
||||
"lineNumber": 108,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T21:59:32.770Z",
|
||||
"reasonDetail": "Protected from arbitrary input"
|
||||
"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": 90,
|
||||
"lineNumber": 111,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
"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": 92,
|
||||
"lineNumber": 113,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
"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": 101,
|
||||
"lineNumber": 122,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
"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.$el.append(view.el);",
|
||||
"lineNumber": 106,
|
||||
"line": " this.$messages.append(view.el);",
|
||||
"lineNumber": 127,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2018-09-19T18:13:29.628Z",
|
||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||
"updated": "2018-11-14T18:51:15.180Z",
|
||||
"reasonDetail": "view.el is a known DOM element"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
|
@ -6825,4 +6870,4 @@
|
|||
"updated": "2018-09-17T20:50:40.689Z",
|
||||
"reasonDetail": "Hard-coded value"
|
||||
}
|
||||
]
|
||||
]
|
Loading…
Add table
Reference in a new issue