Finish new Message component, integrate into application

Also:
- New schema version 8 with video/image thumbnails, screenshots, sizes
- Upgrade messages not at current schema version when loading messages
  to show in conversation
- New MessageDetail react component
- New ConversationHeader react component
This commit is contained in:
Scott Nonnenberg 2018-07-09 14:29:13 -07:00
parent 69f11c4a7b
commit 3c69886320
102 changed files with 9644 additions and 7381 deletions

View file

@ -12,7 +12,10 @@ describe('i18n', function() {
});
it('returns message with multiple substitutions', function() {
const actual = i18n('theyChangedTheTimer', ['Someone', '5 minutes']);
assert.equal(actual, 'Someone set the timer to 5 minutes.');
assert.equal(
actual,
'Someone set the disappearing message timer to 5 minutes'
);
});
});

View file

@ -96,37 +96,7 @@
<p> {{ content }}</p>
</script>
<script type='text/x-tmpl-mustache' id='conversation'>
<div class='conversation-header {{ avatar.color }}'>
<div class='header-buttons left'>
<div class='vertical-align'>
<button class='back hide'></button>
</div>
</div>
<div class='header-buttons right'>
<div class='vertical-align'>
<div class='conversation-menu menu'>
<button class='hamburger' alt='conversation menu'></button>
<ul class='menu-list'>
<li class='disappearing-messages'>{{ disappearing-messages }}</li>
{{#group}}
<li class='show-members'>{{ show-members }}</li>
<!-- <li class='update-group'>Update group</li> -->
<!-- <li class='leave-group'>Leave group</li> -->
{{/group}}
{{^group}}
{{ ^isMe }}
<li class='show-identity'>{{ show-identity }}</li>
{{ /isMe }}
<li class='end-session'>{{ end-session }}</li>
{{/group}}
<li class='destroy'>{{ destroy }}</li>
</ul>
</div>
</div>
</div>
<span class='conversation-title'></span>
{{> avatar }}
</div>
<div class='conversation-header'></div>
<div class='main panel'>
<div class='discussion-container'>
<div class='bar-container hide'>
@ -172,62 +142,6 @@
<img src='{{ source }}' class='preview' />
<a class='x close' alt='remove attachment' href='#'></a>
</script>
<script type='text/x-tmpl-mustache' id='hasRetry'>
{{ messageNotSent }}
<span href='#' class='retry'>{{ resend }}</span>
</script>
<script type='text/x-tmpl-mustache' id='keychange'>
<span class='content' dir='auto'><span class='shield icon'></span> {{ content }}</span>
</script>
<script type='text/x-tmpl-mustache' id='verified-change'>
<span class='content' dir='auto'><span class='{{ icon }} icon'></span> {{ content }}</span>
</script>
<script type='text/x-tmpl-mustache' id='message'>
{{> avatar }}
<div class='bubble {{ avatar.color }}'>
<div class='sender' dir='auto'>
{{ sender }}
{{ #profileName }}
<span class='profileName'>{{ profileName }} </span>
{{ /profileName }}
</div>
<div class='tail-wrapper {{ innerBubbleClasses }}'>
<div class='inner-bubble'>
{{ #hasAttachments }}
<div class='attachments'></div>
{{ /hasAttachments }}
{{ #hasBody }}
<div class='content' dir='auto'>
{{ #message }}
<div class='body'></div>
{{ /message }}
</div>
{{ /hasBody }}
</div>
</div>
<div class='meta'>
<span class='timestamp' data-timestamp={{ timestamp }}></span>
<span class='status hide'></span>
<span class='timer'></span>
</div>
{{ #hoverIcon }}
<div class='menu-container menu'>
<div class='menu-anchor'>
<span class='dots-horizontal-icon'></span>
<ul class='menu-list'>
<li class='reply'>{{ reply }}</li>
</ul>
</div>
</div>
{{ /hoverIcon }}
</div>
</script>
<script type='text/x-tmpl-mustache' id='hourglass'>
<span class='hourglass'><span class='sand'></span></span>
</script>
<script type='text/x-tmpl-mustache' id='expirationTimerUpdate'>
<span class='content'><span class='icon clock'></span> {{ content }}</span>
</script>
<script type='text/x-tmpl-mustache' id='new-group-update'>
<div class='conversation-header'>
<button class='back'></button>
@ -304,48 +218,6 @@
<script type='text/x-tmpl-mustache' id='attachment-type-modal'>
Sorry, your attachment has a type, {{type}}, that is not currently supported.
</script>
<script type='text/x-tmpl-mustache' id='message-detail'>
<div class='container'>
<div class='message-container'></div>
<div class='info'>
<table>
{{ #errors }}
<tr>
<td class='label'>{{ errorLabel }}</td>
<td> <span class='error-message'>{{message}}</span> </td>
</tr>
{{ /errors }}
<tr>
<td class='label'>{{ sent }}</td>
<td> {{ sent_at }}</td>
</tr>
{{ #received_at }}
<tr>
<td class='label'>{{ received }}</td>
<td> {{ received_at }}</td>
</tr>
{{ /received_at }}
<tr> <td class='tofrom label'>{{tofrom}}</td> </tr>
</table>
<div class='contacts'>
</div>
</div>
</div>
</script>
<script type='text/x-tmpl-mustache' id='identity-key-send-error'>
<div class='container'>
<div class='explanation'>
{{ errorExplanation }}
</div>
<div class='safety-number'>
<button class='show-safety-number grey'>{{ showSafetyNumber }}</button>
</div>
<div class='actions'>
<button class='send-anyway grey'>{{ sendAnyway }}</button>
<button class='cancel grey'>{{ cancel }}</button>
</div>
</div>
</script>
<script type='text/x-tmpl-mustache' id='group-member-list'>
<div class='container'>
{{ #summary }} <div class='summary'>{{ summary }}</div>{{ /summary }}
@ -425,24 +297,6 @@
<span class='error-message'>{{message}}</span>
{{ /message }}
</script>
<script type='text/x-tmpl-mustache' id='contact-detail'>
<div class='clearfix'>
{{> avatar }}
<div class='contact-details'>
{{ #errors }}
<div class='error-icon-container'>
<span class='error-icon'></span>
</div>
{{ /errors }}
<span class='name' dir='auto'>{{ name }}</span>
{{ #errors }}
{{ #message }}
<p class='error-message'>{{message}}</p>
{{ /message }}
{{ /errors }}
</div>
</div>
</script>
<script type='text/x-tmpl-mustache' id='link_to_support'>
<a href='http://support.signal.org/hc/articles/213134107' target='_blank'>
{{ learnMore }}
@ -514,7 +368,6 @@
<script type='text/javascript' src='../js/views/timestamp_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/message_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/key_verification_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/message_detail_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/message_list_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/group_member_list_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/recorder_view.js' data-cover></script>
@ -528,12 +381,10 @@
<script type='text/javascript' src='../js/views/last_seen_indicator_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/scroll_down_button_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/banner_view.js' data-cover></script>
<script type="text/javascript" src='../js/views/identity_key_send_error_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/clear_data_view.js'></script>
<script type="text/javascript" src="views/whisper_view_test.js"></script>
<script type="text/javascript" src="views/group_update_view_test.js"></script>
<script type="text/javascript" src="views/message_view_test.js"></script>
<script type="text/javascript" src="views/attachment_view_test.js"></script>
<script type="text/javascript" src="views/timestamp_view_test.js"></script>
<script type="text/javascript" src="views/list_view_test.js"></script>

View file

@ -164,21 +164,21 @@
message = messages.add({ group_update: { left: 'Alice' } });
assert.equal(
message.getDescription(),
'Alice left the group.',
'Alice left the group',
'Notes one person leaving the group.'
);
message = messages.add({ group_update: { name: 'blerg' } });
assert.equal(
message.getDescription(),
"Updated the group. Title is now 'blerg'.",
"Title is now 'blerg'",
'Returns a single notice if only group_updates.name changes.'
);
message = messages.add({ group_update: { joined: ['Bob'] } });
assert.equal(
message.getDescription(),
'Updated the group. Bob joined the group.',
'Bob joined the group',
'Returns a single notice if only group_updates.joined changes.'
);
@ -187,7 +187,7 @@
});
assert.equal(
message.getDescription(),
'Updated the group. Bob, Alice, Eve joined the group.',
'Bob, Alice, Eve joined the group',
'Notes when >1 person joins the group.'
);
@ -196,7 +196,7 @@
});
assert.equal(
message.getDescription(),
"Updated the group. Title is now 'blerg'. Bob joined the group.",
"Title is now 'blerg', Bob joined the group",
'Notes when there are multiple changes to group_updates properties.'
);

View file

@ -278,6 +278,12 @@ describe('Message', () => {
return 'abc/abcdefg';
},
getRegionCode: () => 'US',
getAbsoluteAttachmentPath: () => 'some/path/on/disk',
makeObjectUrl: () => 'blob://FAKE',
revokeObjectUrl: () => null,
getImageDimensions: () => ({ height: 10, width: 15 }),
makeImageThumbnail: () => new Blob(),
makeVideoScreenshot: () => new Blob(),
};
const actual = await Message.upgradeSchema(input, context);
assert.deepEqual(actual, expected);

View file

@ -1,84 +0,0 @@
'use strict';
/* global window: false */
// Because we aren't hosting the Style Guide in Electron, we can't rely on preload.js
// to set things up for us. This gives us the minimum bar shims for everything it
// provdes.
//
// Remember, the idea here is just to enable visual testing, no full functionality. Most
// of thise can be very simple.
window.PROTO_ROOT = '/protos';
window.nodeSetImmediate = () => {};
window.libphonenumber = {
parse: number => ({
e164: number,
isValidNumber: true,
getCountryCode: () => '1',
getNationalNumber: () => number,
}),
isValidNumber: () => true,
getRegionCodeForNumber: () => '1',
format: number => number.e164,
PhoneNumberFormat: {},
};
window.Signal = {};
window.Signal.Backup = {};
window.Signal.Crypto = {};
window.Signal.Logs = {};
window.Signal.Migrations = {
getPlaceholderMigrations: () => [
{
migrate: (transaction, next) => {
console.log('migration version 1');
transaction.db.createObjectStore('conversations');
next();
},
version: 1,
},
{
migrate: (transaction, next) => {
console.log('migration version 2');
const messages = transaction.db.createObjectStore('messages');
messages.createIndex('expires_at', 'expireTimer', { unique: false });
next();
},
version: 2,
},
{
migrate: (transaction, next) => {
console.log('migration version 3');
transaction.db.createObjectStore('items');
next();
},
version: 3,
},
],
loadAttachmentData: attachment => Promise.resolve(attachment),
getAbsoluteAttachmentPath: path => path,
};
window.Signal.Components = {};
window.i18n = () => '';
// Ideally we don't need to add things here. We want to add them in StyleGuideUtil, which
// means that references to these things can't be early-bound, not capturing the direct
// reference to the function on file load.
window.Signal.Migrations.V17 = {};
window.Signal.OS = {};
window.Signal.Types = {};
window.Signal.Types.Attachment = {};
window.Signal.Types.Conversation = {};
window.Signal.Types.Errors = {};
window.Signal.Types.Message = {
initializeSchemaVersion: attributes => attributes,
};
window.Signal.Types.MIME = {};
window.Signal.Types.Settings = {};
window.Signal.Views = {};
window.Signal.Views.Initialization = {};
window.Signal.Workflow = {};

View file

@ -1,90 +0,0 @@
'use strict';
/* global window: false */
// Taken from background.html.
// Templates are here solely to support the Backbone views rendered in the Style Guide.
// Note: Any change here must be reflected in background.html to be reflected in the app
// and test/index.html to be reflected in the unit tests.
window.Whisper.View.Templates = {
hasRetry: `
{{ messageNotSent }} <span href='#' class='retry'>{{ resend }}</span>
`,
'some-failed': `
{{ someFailed }}
`,
keychange: `
<span class='content' dir='auto'>
<span class='shield icon'></span> {{ content }}
</span>
`,
'verified-change': `
<span class='content' dir='auto'>
<span class='{{ icon }} icon'></span> {{ content }}
</span>
`,
message: `
{{> avatar }}
<div class='bubble {{ avatar.color }}'>
<div class='sender' dir='auto'>
{{ sender }}
{{ #profileName }}
<span class='profileName'>{{ profileName }} </span>
{{ /profileName }}
</div>
<div class='tail-wrapper {{ innerBubbleClasses }}'>
<div class='inner-bubble'>
{{ #hasAttachments }}
<div class='attachments'></div>
{{ /hasAttachments }}
{{ #hasBody }}
<div class='content' dir='auto'>
{{ #message }}
<div class='body'></div>
{{ /message }}
</div>
{{ /hasBody }}
</div>
</div>
<div class='meta'>
<span class='timestamp' data-timestamp={{ timestamp }}></span>
<span class='status hide'></span>
<span class='timer'></span>
</div>
{{ #hoverIcon }}
<div class='menu-container menu'>
<div class='menu-anchor'>
<span class='dots-horizontal-icon'></span>
<ul class='menu-list'>
<li class='reply'>{{ reply }}</li>
</ul>
</div>
</div>
{{ /hoverIcon }}
</div>
`,
hourglass: `
<span class='hourglass'><span class='sand'></span></span>
`,
expirationTimerUpdate: `
<span class='content'><span class='icon clock'></span> {{ content }}</span>
`,
'file-view': `
<div class='icon {{ mediaType }}'></div>
<div class='text'>
<div class='fileName' title='{{ altText }}'>
{{ fileName }}
</div>
<div class='fileSize'>{{ fileSize }}</div>
</div>
`,
'error-icon': `
<span class='error-icon'>
</span>
{{ #message }}
<span class='error-message'>{{message}}</span>
{{ /message }}
`,
};

View file

@ -5,6 +5,22 @@
'use strict';
describe('AttachmentView', () => {
var convo, message;
before(async () => {
await clearDatabase();
convo = new Whisper.Conversation({ id: 'foo' });
message = convo.messageCollection.add({
conversationId: convo.id,
body: 'hello world',
type: 'outgoing',
source: '+14158675309',
received_at: Date.now(),
});
await storage.put('number_id', '+18088888888.1');
});
describe('with arbitrary files', () => {
it('should render a file view', () => {
const attachment = {

View file

@ -1,90 +0,0 @@
describe('MessageView', function() {
var convo, message;
before(async () => {
await clearDatabase();
convo = new Whisper.Conversation({ id: 'foo' });
message = convo.messageCollection.add({
conversationId: convo.id,
body: 'hello world',
type: 'outgoing',
source: '+14158675309',
received_at: Date.now(),
});
await storage.put('number_id', '+18088888888.1');
});
it('should display the message text', function() {
var view = new Whisper.MessageView({ model: message }).render();
assert.match(view.$el.text(), /hello world/);
});
it('should auto-update the message text', function() {
var view = new Whisper.MessageView({ model: message }).render();
message.set('body', 'goodbye world');
assert.match(view.$el.html(), /goodbye world/);
});
it('should have a nice timestamp', function() {
var view = new Whisper.MessageView({ model: message });
message.set({ sent_at: Date.now() - 5000 });
view.render();
assert.match(view.$el.html(), /now/);
message.set({ sent_at: Date.now() - 60000 });
view.render();
assert.match(view.$el.html(), /min/);
message.set({ sent_at: Date.now() - 3600000 });
view.render();
assert.match(view.$el.html(), /hour/);
});
it('should not imply messages are from the future', function() {
var view = new Whisper.MessageView({ model: message });
message.set({ sent_at: Date.now() + 60000 });
view.render();
assert.match(view.$el.html(), /now/);
});
it('should go away when the model is destroyed', function() {
var view = new Whisper.MessageView({ model: message });
var div = $('<div>').append(view.$el);
message.destroy();
assert.strictEqual(div.find(view.$el).length, 0);
});
it('allows links', function() {
var url = 'http://example.com';
message.set('body', url);
var view = new Whisper.MessageView({ model: message });
view.render();
var link = view.$el.find('.body a');
assert.strictEqual(link.length, 1);
assert.strictEqual(link.text(), url);
assert.strictEqual(link.attr('href'), url);
});
it('disallows xss', function() {
var xss = '<script>alert("pwnd")</script>';
message.set('body', xss);
var view = new Whisper.MessageView({ model: message });
view.render();
assert.include(view.$el.text(), xss); // should appear as escaped text
assert.strictEqual(view.$el.find('script').length, 0); // should not appear as html
});
it('supports emoji', function() {
message.set('body', 'I \u2764\uFE0F emoji!');
var view = new Whisper.MessageView({ model: message });
view.render();
var img = view.$el.find('.content img');
assert.strictEqual(img.length, 1);
assert.strictEqual(
img.attr('src'),
'node_modules/emoji-datasource-apple/img/apple/64/2764-fe0f.png'
);
assert.strictEqual(img.attr('title'), ':heart:');
assert.strictEqual(img.attr('class'), 'emoji');
});
});

View file

@ -10,7 +10,6 @@ describe('ScrollDownButtonView', function() {
var view = new Whisper.ScrollDownButtonView({ count: 1 });
view.render();
assert.equal(view.count, 1);
assert.match(view.$el.html(), /new-messages/);
assert.match(view.$el.html(), /New message below/);
});
@ -19,7 +18,6 @@ describe('ScrollDownButtonView', function() {
view.render();
assert.equal(view.count, 2);
assert.match(view.$el.html(), /new-messages/);
assert.match(view.$el.html(), /New messages below/);
});
@ -27,9 +25,9 @@ describe('ScrollDownButtonView', function() {
var view = new Whisper.ScrollDownButtonView();
view.render();
assert.equal(view.count, 0);
assert.notMatch(view.$el.html(), /new-messages/);
assert.notMatch(view.$el.html(), /New message below/);
view.increment(1);
assert.equal(view.count, 1);
assert.match(view.$el.html(), /new-messages/);
assert.match(view.$el.html(), /New message below/);
});
});