Typing Indicators

This commit is contained in:
Scott Nonnenberg 2018-11-14 11:10:32 -08:00
parent 99252702e1
commit 79a861a870
23 changed files with 906 additions and 121 deletions

View File

@ -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",

View File

@ -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>

View File

@ -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;

View File

@ -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({

View File

@ -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,
};

View File

@ -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',

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -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
);
}

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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',

View 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>
```

View 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>
);
}
}

View 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>
```

View 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>
);
}
}

View File

@ -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,
};

View File

@ -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"
}
]
]