diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 0eadf0ba3..880c5f26f 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1,4 +1,8 @@ { + "me": { + "message": "Me", + "description": "The label for yourself when shown in a group member list" + }, "youLeftTheGroup": { "message": "You left the group", "description": "Displayed when a user can't send a message because they have left the group" @@ -172,15 +176,40 @@ "message": "Send a message", "description": "Placeholder text in the message entry field" }, - "members": { - "message": "Members" + "groupMembers": { + "message": "Group members" + }, + "showMembers": { + "message": "Show members" }, "resetSession": { "message": "Reset session", "description": "This is a menu item for resetting the session, using the imperative case, as in a command." }, "showSafetyNumber": { - "message": "Show safety number" + "message": "Show safety number" + }, + "markAsNotVerified": { + "message": "Mark as not verified" + }, + "verifyHelp": { + "message": "If you wish to verify the security of your end-to-end encryption with $name$, compare the numbers above with the numbers on their device.", + "placeholders": { + "name": { + "content": "$1", + "example": "John" + } + } + }, + "isVerified": { + "message": "$name$ is verified", + "description": "If the user has manually marked a contact's safety number as verified, this string is shown on the 'Show Safety Number' screen", + "placeholders": { + "name": { + "content": "$1", + "example": "John" + } + } }, "theirIdentityUnknown": { "message": "You haven't exchanged any messages with this contact yet. Your safety number with them will be available after the first message." diff --git a/background.html b/background.html index 7b01094f4..b6e4913a6 100644 --- a/background.html +++ b/background.html @@ -66,6 +66,17 @@ + diff --git a/js/models/conversations.js b/js/models/conversations.js index a6b9a5417..fbc1f6edd 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -37,6 +37,8 @@ }, initialize: function() { + this.ourNumber = textsecure.storage.user.getNumber(); + this.contactCollection = new Backbone.Collection(); this.messageCollection = new Whisper.MessageCollection([], { conversation: this @@ -48,6 +50,84 @@ this.on('destroy', this.revokeAvatarUrl); }, + updateVerified: function() { + // TODO: replace this with the real call + function checkTrustStore() { + return Promise.resolve('default'); + } + + if (this.isPrivate()) { + return Promise.all([ + checkTrustStore(this.id), + this.fetch() + ]).then(function(results) { + var trust = results[0]; + return this.save({verified: trust}); + }); + } else { + return this.fetchContacts().then(function() { + return Promise.all(this.contactCollection.map(function(contact) { + if (contact.id !== this.myNumber) { + return contact.updateVerified(); + } + }.bind(this))); + }.bind(this)); + } + }, + + isVerified: function() { + if (this.isPrivate()) { + return this.get('verified') === 'verified'; + } else { + return this.contactCollection.every(function(contact) { + if (contact.id === this.myNumber) { + return true; + } else { + return contact.isVerified(); + } + }.bind(this)); + } + }, + isConflict: function() { + if (this.isPrivate()) { + var verified = this.get('verified'); + return verified !== 'verified' && verified !== 'default'; + } else { + return Boolean(this.getConflicts().length); + } + }, + getConflicts: function() { + if (this.isPrivate()) { + return this.isConflict() ? [this] : []; + } else { + return this.contactCollection.filter(function(contact) { + if (contact.id === this.myNumber) { + return false; + } else { + return contact.isConflict(); + } + }.bind(this)); + } + }, + onMemberVerifiedChange: function() { + // If the verified state of a member changes, our aggregate state changes. + // We trigger both events to replicate the behavior of Backbone.Model.set() + this.trigger('change:verified'); + this.trigger('change'); + }, + toggleVerified: function() { + if (!this.isPrivate()) { + throw new Error('You cannot verify a group conversation. ' + + 'You must verify individual contacts.'); + } + + if (this.isVerified()) { + this.save({verified: 'default'}); + } else { + this.save({verified: 'verified'}); + } + }, + addKeyChange: function(id) { console.log('adding key change advisory for', this.id, this.get('timestamp')); var timestamp = Date.now(); @@ -373,12 +453,14 @@ } else { var promises = []; var members = this.get('members') || []; + this.contactCollection.reset( members.map(function(number) { var c = ConversationController.create({ id : number, type : 'private' }); + this.listenTo(c, 'change:verified', this.onMemberVerifiedChange); promises.push(new Promise(function(resolve) { c.fetch().always(resolve); })); diff --git a/js/views/contact_list_view.js b/js/views/contact_list_view.js index 4e76d9037..e23b8114b 100644 --- a/js/views/contact_list_view.js +++ b/js/views/contact_list_view.js @@ -12,12 +12,41 @@ tagName: 'div', className: 'contact', templateName: 'contact', + events: { + 'click': 'showIdentity' + }, + initialize: function(options) { + this.ourNumber = textsecure.storage.user.getNumber(); + this.listenBack = options.listenBack; + + this.listenTo(this.model, 'change', this.render); + }, render_attributes: function() { + if (this.model.id === this.ourNumber) { + return { + class: 'not-clickable', + title: i18n('me'), + number: this.model.getNumber(), + avatar: this.model.getAvatar() + }; + } + return { + class: '', title: this.model.getTitle(), number: this.model.getNumber(), - avatar: this.model.getAvatar() + avatar: this.model.getAvatar(), + verified: this.model.isVerified() }; + }, + showIdentity: function() { + if (this.model.id === this.ourNumber) { + return; + } + var view = new Whisper.KeyVerificationPanelView({ + model: this.model + }); + this.listenBack(view); } }) }); diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index c50c553d5..f0fa05ab0 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -57,6 +57,17 @@ } }); + Whisper.ConversationTitleView = Whisper.View.extend({ + templateName: 'conversation-title', + render_attributes: function() { + return { + verified: this.model.isVerified(), + name: this.model.getName(), + number: this.model.getNumber(), + }; + } + }); + Whisper.ConversationView = Whisper.View.extend({ className: function() { return [ 'conversation', this.model.get('type') ].join(' '); @@ -68,13 +79,11 @@ render_attributes: function() { return { group: this.model.get('type') === 'group', - name: this.model.getName(), - number: this.model.getNumber(), avatar: this.model.getAvatar(), expireTimer: this.model.get('expireTimer'), - 'view-members' : i18n('members'), + 'show-members' : i18n('showMembers'), 'end-session' : i18n('resetSession'), - 'show-identity' : i18n('showSafetyNumber'), + 'show-identity' : i18n('showSafetyNumber'), 'destroy' : i18n('deleteMessages'), 'send-message' : i18n('sendMessage'), 'disappearing-messages': i18n('disappearingMessages'), @@ -84,6 +93,7 @@ }, initialize: function(options) { this.listenTo(this.model, 'destroy', this.stopListening); + this.listenTo(this.model, 'change', this.updateTitle); this.listenTo(this.model, 'change:color', this.updateColor); this.listenTo(this.model, 'change:name', this.updateTitle); this.listenTo(this.model, 'newmessage', this.addMessage); @@ -103,6 +113,13 @@ window: this.window }); + this.titleView = new Whisper.ConversationTitleView({ + el: this.$('.conversation-title'), + model: this.model + }); + this.titleView.render(); + this.titleView.render(); + this.view = new Whisper.MessageListView({ collection: this.model.messageCollection, window: this.window @@ -145,7 +162,7 @@ 'click .leave-group': 'leaveGroup', 'click .update-group': 'newGroupUpdate', 'click .show-identity': 'showIdentity', - 'click .view-members': 'viewMembers', + 'click .show-members': 'showMembers', 'click .conversation-menu .hamburger': 'toggleMenu', 'click .openInbox' : 'openInbox', 'click' : 'onClick', @@ -167,6 +184,10 @@ 'force-resize': 'forceUpdateMessageFieldSize', 'show-identity': 'showIdentity' }, + + updateTitle: function() { + this.titleView.render(); + }, enableDisappearingMessages: function() { if (!this.model.get('expireTimer')) { this.model.updateExpirationTimer( @@ -416,9 +437,12 @@ this.model.messageCollection.add(message, {merge: true}); }, - viewMembers: function() { + showMembers: function() { return this.model.fetchContacts().then(function() { - var view = new Whisper.GroupMemberList({ model: this.model }); + var view = new Whisper.GroupMemberList({ + model: this.model, + listenBack: this.listenBack.bind(this) + }); this.listenBack(view); }.bind(this)); }, @@ -515,16 +539,25 @@ }, listenBack: function(view) { - this.panel = view; - this.$('.main.panel, .header-buttons.right').hide(); - this.$('.back').show(); - view.$el.insertBefore(this.$('.panel')); + this.panels = this.panels || []; + this.panels.unshift(view); + + if (this.panels.length === 1) { + this.$('.main.panel, .header-buttons.right').hide(); + this.$('.back').show(); + } + + view.$el.insertBefore(this.$('.panel').first()); }, resetPanel: function() { - this.panel.remove(); - this.$('.main.panel, .header-buttons.right').show(); - this.$('.back').hide(); - this.$el.trigger('force-resize'); + var view = this.panels.shift(); + view.remove(); + + if (this.panels.length === 0) { + this.$('.main.panel, .header-buttons.right').show(); + this.$('.back').hide(); + this.$el.trigger('force-resize'); + } }, closeMenu: function(e) { @@ -614,10 +647,6 @@ }); }, - updateTitle: function() { - this.$('.conversation-title').text(this.model.getTitle()); - }, - updateColor: function(model, color) { var header = this.$('.conversation-header'); header.removeClass(Whisper.Conversation.COLORS); diff --git a/js/views/group_member_list_view.js b/js/views/group_member_list_view.js index 02432b17d..065f63cbf 100644 --- a/js/views/group_member_list_view.js +++ b/js/views/group_member_list_view.js @@ -13,17 +13,22 @@ Whisper.GroupMemberList = Whisper.View.extend({ className: 'group-member-list panel', templateName: 'group-member-list', - initialize: function() { + initialize: function(options) { this.render(); + console.log('GroupMemberList', options); + this.member_list_view = new Whisper.ContactListView({ collection: this.model.contactCollection, - className: 'members' + className: 'members', + toInclude: { + listenBack: options.listenBack + } }); this.member_list_view.render(); this.$('.container').append(this.member_list_view.el); }, render_attributes: { - members: i18n('members') + members: i18n('groupMembers') } }); })(); diff --git a/js/views/key_verification_view.js b/js/views/key_verification_view.js index e10da5f43..f950de1d7 100644 --- a/js/views/key_verification_view.js +++ b/js/views/key_verification_view.js @@ -5,25 +5,30 @@ 'use strict'; window.Whisper = window.Whisper || {}; - // TODO; find all uses of that removed panel - // Add the Verify functionality to this view Whisper.KeyVerificationPanelView = Whisper.View.extend({ className: 'key-verification panel', templateName: 'key-verification', + events: { + 'click button.verify': 'toggleVerified', + }, initialize: function(options) { this.our_number = textsecure.storage.user.getNumber(); if (options.newKey) { this.their_key = options.newKey; } + Promise.all([ this.loadTheirKey(), this.loadOurKey(), ]).then(this.generateSecurityNumber.bind(this)) + .then(function() { + this.listenTo(this.model, 'change', this.render); + }.bind(this)) .then(this.render.bind(this)); //.then(this.makeQRCode.bind(this)); }, makeQRCode: function() { - // Per Lilia: We can't turn this on until it geneates a Latin1 string, as is + // Per Lilia: We can't turn this on until it generates a Latin1 string, as is // required by the mobile clients. new QRCode(this.$('.qr')[0]).makeCode( dcodeIO.ByteBuffer.wrap(this.our_key).toString('base64') @@ -58,6 +63,9 @@ this.securityNumber = securityNumber; }.bind(this)); }, + toggleVerified: function() { + this.model.toggleVerified(); + }, render_attributes: function() { var s = this.securityNumber; var chunks = []; @@ -67,10 +75,15 @@ var yourSafetyNumberWith = i18n( 'yourSafetyNumberWith', this.model.getTitle() ); + console.log('this.model',this.model); + var verifyButton = this.model.isVerified() ? i18n('markAsNotVerified') : i18n('verify'); + return { learnMore : i18n('learnMore'), their_key_unknown : i18n('theirIdentityUnknown'), yourSafetyNumberWith : i18n('yourSafetyNumberWith', this.model.getTitle()), + verifyHelp : i18n('verifyHelp', this.model.getTitle()), + verifyButton : verifyButton, has_their_key : this.their_key !== undefined, chunks : chunks, }; diff --git a/js/views/list_view.js b/js/views/list_view.js index 1de9d5c7c..b80204f38 100644 --- a/js/views/list_view.js +++ b/js/views/list_view.js @@ -13,13 +13,15 @@ tagName: 'ul', itemView: Backbone.View, initialize: function(options) { + this.options = options || {}; this.listenTo(this.collection, 'add', this.addOne); this.listenTo(this.collection, 'reset', this.addAll); }, addOne: function(model) { if (this.itemView) { - var view = new this.itemView({model: model}); + var options = Object.assign({}, this.options.toInclude, {model: model}); + var view = new this.itemView(options); this.$el.append(view.render().el); this.$el.trigger('add'); } diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss index 27be2fbf7..8b4dd6d39 100644 --- a/stylesheets/_conversation.scss +++ b/stylesheets/_conversation.scss @@ -14,6 +14,13 @@ padding: 0 5px 0 4px; } } +.conversation-title .verified { + &:before { + content:"\00b7"; // · + font-weight: bold; + padding: 0 5px 0 4px; + } +} .conversation { background-color: white; @@ -101,6 +108,15 @@ max-width: 100%; } } + + div.verify { + text-align: center; + } + button.verify { + border-radius: 5px; + font-weight: bold; + padding: 10px; + } } .message-detail { diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss index 0414f0c3f..507d54629 100644 --- a/stylesheets/_global.scss +++ b/stylesheets/_global.scss @@ -342,6 +342,7 @@ $avatar-size: 44px; margin: 0 0 0 $left-margin; width: calc(100% - #{$avatar-size} - #{$left-margin} - #{(4/14) + em}); text-align: left; + cursor: pointer; p { overflow-x: hidden; @@ -361,6 +362,10 @@ $avatar-size: 44px; color: $grey; font-size: $font-size-small; } + + &.not-clickable { + cursor: default; + } } diff --git a/stylesheets/manifest.css b/stylesheets/manifest.css index 6b178efad..02c8984e1 100644 --- a/stylesheets/manifest.css +++ b/stylesheets/manifest.css @@ -332,7 +332,8 @@ button.hamburger { display: inline-block; margin: 0 0 0 8px; width: calc(100% - 44px - 8px - 0.28571em); - text-align: left; } + text-align: left; + cursor: pointer; } .contact-details p { overflow-x: hidden; text-overflow: ellipsis; } @@ -346,6 +347,8 @@ button.hamburger { .contact-details .number { color: #616161; font-size: 0.92857em; } + .contact-details.not-clickable { + cursor: default; } .recipients-input { position: relative; } @@ -988,6 +991,11 @@ input.search { font-weight: bold; padding: 0 5px 0 4px; } +.conversation-title .verified:before { + content: "\00b7"; + font-weight: bold; + padding: 0 5px 0 4px; } + .conversation { background-color: white; height: 100%; } @@ -1052,6 +1060,12 @@ input.search { .key-verification .qr img { display: inline-block; max-width: 100%; } +.key-verification div.verify { + text-align: center; } +.key-verification button.verify { + border-radius: 5px; + font-weight: bold; + padding: 10px; } .message-detail { background-color: #eee; } diff --git a/test/index.html b/test/index.html index 14f35e60d..fb6b091e7 100644 --- a/test/index.html +++ b/test/index.html @@ -92,14 +92,15 @@