diff --git a/_locales/en/messages.json b/_locales/en/messages.json index f431e22ce..15c65f464 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1656,5 +1656,18 @@ "editProfileDisplayNameWarning": { "message": "Note: Your display name will be visible to your contacts", "description": "Shown to the user as a warning about setting display name" + }, + + "copyPublicKey": { + "message": "Copy public key", + "description": "Button action that the user can click to copy their public keys" + }, + "copiedPublicKey": { + "message": "Copied public key", + "description": "A toast message telling the user that the key was copied" + }, + "editDisplayName": { + "message": "Edit display name", + "description": "Button action that the user can click to edit their display name" } } diff --git a/app/profile_images.js b/app/profile_images.js index 639e6a7f2..d416894a2 100644 --- a/app/profile_images.js +++ b/app/profile_images.js @@ -49,7 +49,7 @@ const generateImage = pubKey => { */ const png = new Identicon(sha224(pubKey), { margin: 0.2, - background: [0,0,0,0], + background: [0, 0, 0, 0], }).toString(); fs.writeFileSync(imagePath, png, 'base64'); return imagePath diff --git a/background.html b/background.html index f5a05f2e8..d011f79f9 100644 --- a/background.html +++ b/background.html @@ -71,7 +71,6 @@ -
@@ -88,6 +87,16 @@
+ + diff --git a/js/background.js b/js/background.js index fc0c6d215..ebc613361 100644 --- a/js/background.js +++ b/js/background.js @@ -580,7 +580,6 @@ nickname: displayName, onOk: async (newName) => { await storage.setProfileName(newName); - appView.inboxView.trigger('updateProfile'); // Update the conversation if we have it const conversation = ConversationController.get(ourNumber); diff --git a/js/models/conversations.js b/js/models/conversations.js index 946dd04c5..6fe724ead 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -869,6 +869,11 @@ this.set({ id: this.id }); return 'Invalid ID Length'; } + + // Check if the id is prefixed by 05 + if (!/^05/.test(this.id)) { + return 'Invalid Pubkey Format'; + } } return null; @@ -1616,12 +1621,12 @@ await this.updateProfile(); }, async setProfile(profile) { - if (_.isEqual(this.get('profile'), profile)) return; - - this.set({ profile }); - await window.Signal.Data.updateConversation(this.id, this.attributes, { - Conversation: Whisper.Conversation, - }); + if (!_.isEqual(this.get('profile'), profile)) { + this.set({ profile }); + await window.Signal.Data.updateConversation(this.id, this.attributes, { + Conversation: Whisper.Conversation, + }); + } await this.updateProfile(); }, diff --git a/js/modules/signal.js b/js/modules/signal.js index 4ed80eba7..b7755f25b 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -43,7 +43,6 @@ const { MediaGallery, } = require('../../ts/components/conversation/media-gallery/MediaGallery'); const { MainHeader } = require('../../ts/components/MainHeader'); -const { IdentityKeyHeader } = require('../../ts/components/IdentityKeyHeader'); const { Message } = require('../../ts/components/conversation/Message'); const { MessageBody } = require('../../ts/components/conversation/MessageBody'); const { @@ -185,7 +184,6 @@ exports.setup = (options = {}) => { Lightbox, LightboxGallery, MainHeader, - IdentityKeyHeader, MediaGallery, Message, MessageBody, diff --git a/js/views/conversation_search_view.js b/js/views/conversation_search_view.js index 9f9b33505..eb1d3a398 100644 --- a/js/views/conversation_search_view.js +++ b/js/views/conversation_search_view.js @@ -73,7 +73,7 @@ // Update the contact model this.new_contact_view.model.set('id', query); - this.new_contact_view.render().$el.show(); + this.new_contact_view.render().$el.hide(); this.new_contact_view.validate(); this.hideHints(); @@ -89,10 +89,9 @@ // This will allow us to show the last message when searching this.typeahead_view.collection.forEach(c => c.updateLastMessage()); - // Check if the query is in the model list - // If it is then hide the new contact view - const modelExists = this.typeahead_view.collection.find(item => item.get('id') === query); - if (modelExists) this.new_contact_view.$el.hide(); + // Show the new contact view if we already have results + if (this.typeahead_view.collection.length === 0) + this.new_contact_view.$el.show(); }) ); /* eslint-enable more/no-then */ diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index 74980bae6..643cd135e 100644 --- a/js/views/inbox_view.js +++ b/js/views/inbox_view.js @@ -5,9 +5,7 @@ /* global i18n: false */ /* global Whisper: false */ /* global textsecure: false */ -/* global Signal: false */ -/* global StringView: false */ -/* global storage: false */ +/* global clipboard: false */ // eslint-disable-next-line func-names (function() { @@ -104,26 +102,10 @@ // eslint-disable-next-line no-new new Whisper.FontSizeView({ el: this.$el }); - const ourNumber = textsecure.storage.user.getNumber(); - const me = ConversationController.getOrCreate(ourNumber, 'private'); - this.mainHeaderView = new Whisper.ReactWrapperView({ - className: 'main-header-wrapper', - Component: Signal.Components.MainHeader, - props: me.format(), + this.mainHeaderView = new Whisper.MainHeaderView({ + el: this.$('.main-header-placeholder'), + items: this.getMainHeaderItems(), }); - const update = () => this.mainHeaderView.update(me.format()); - this.listenTo(me, 'change', update); - this.$('.main-header-placeholder').append(this.mainHeaderView.el); - - this.identityKeyView = new Whisper.ReactWrapperView({ - className: 'identity-key-wrapper', - Component: Signal.Components.IdentityKeyHeader, - props: this._getIdentityKeyViewProps(), - }); - this.on('updateProfile', () => { - this.identityKeyView.update(this._getIdentityKeyViewProps()); - }) - this.$('.identity-key-placeholder').append(this.identityKeyView.el); this.conversation_stack = new Whisper.ConversationStack({ el: this.$('.conversation-stack'), @@ -221,20 +203,6 @@ this.$el.addClass('expired'); } }, - _getIdentityKeyViewProps() { - const identityKey = textsecure.storage.get('identityKey').pubKey; - const pubKey = StringView.arrayBufferToHex(identityKey); - const profile = storage.getLocalProfile(); - const name = profile && profile.name && profile.name.displayName; - - return { - identityKey: pubKey, - name, - onEditProfile: async () => { - window.Whisper.events.trigger('onEditProfile'); - }, - } - }, render_attributes() { return { welcomeToSignal: i18n('welcomeToSignal'), @@ -357,6 +325,30 @@ onClick(e) { this.closeRecording(e); }, + getMainHeaderItems() { + return [ + this._mainHeaderItem('copyPublicKey', () => { + const ourNumber = textsecure.storage.user.getNumber(); + clipboard.writeText(ourNumber); + + const toast = new Whisper.MessageToastView({ + message: i18n('copiedPublicKey'), + }); + toast.$el.appendTo(this.$('.gutter')); + toast.render(); + }), + this._mainHeaderItem('editDisplayName', () => { + window.Whisper.events.trigger('onEditProfile'); + }), + ]; + }, + _mainHeaderItem(textKey, onClick) { + return { + id: textKey, + text: i18n(textKey), + onClick, + }; + }, }); Whisper.ExpiredAlertBanner = Whisper.View.extend({ diff --git a/js/views/main_header_view.js b/js/views/main_header_view.js new file mode 100644 index 000000000..6366f914d --- /dev/null +++ b/js/views/main_header_view.js @@ -0,0 +1,57 @@ +/* global Whisper, textsecure, ConversationController, Signal, i18n */ + +// eslint-disable-next-line func-names +(function() { + 'use strict'; + + window.Whisper = window.Whisper || {}; + + Whisper.MainHeaderView = Whisper.View.extend({ + templateName: 'main-header-placeholder', + events: { + 'click .main-header-title-wrapper': 'onClick', + 'click .edit-name': 'onEditProfile', + 'click .copy-key': 'onCopyKey', + }, + initialize(options) { + this.items = options.items || []; + + this.ourNumber = textsecure.storage.user.getNumber(); + const me = ConversationController.getOrCreate(this.ourNumber, 'private'); + + this.mainHeaderView = new Whisper.ReactWrapperView({ + className: 'main-header-wrapper', + Component: Signal.Components.MainHeader, + props: me.format(), + }); + const update = () => this.mainHeaderView.update(me.format()); + this.listenTo(me, 'change', update); + + this.render(); + this.$('.main-header-title-wrapper').prepend(this.mainHeaderView.el); + + this.$toggle = this.$('.main-header-content-toggle'); + this.$content = this.$('.main-header-content-wrapper'); + this.$content.hide(); + + this.registerCallbacks(); + }, + registerCallbacks() { + this.items.forEach(item => { + if (item.onClick) { + this.$(`#${item.id}`).click(item.onClick); + } + }) + }, + render_attributes() { + return { + items: this.items, + }; + }, + onClick() { + // Toggle section visibility + this.$content.slideToggle('fast'); + this.$toggle.toggleClass('main-header-content-toggle-visible'); + }, + }); + })(); diff --git a/js/views/toast_view.js b/js/views/toast_view.js index ae90feb25..599e65370 100644 --- a/js/views/toast_view.js +++ b/js/views/toast_view.js @@ -28,4 +28,13 @@ setTimeout(this.close.bind(this), 2000); }, }); + + Whisper.MessageToastView = Whisper.ToastView.extend({ + initialize(options) { + this.message = options.message || '-'; + }, + render_attributes() { + return { toastMessage: this.message }; + }, + }) })(); diff --git a/preload.js b/preload.js index f868614a2..9c5ea103e 100644 --- a/preload.js +++ b/preload.js @@ -7,6 +7,7 @@ const semver = require('semver'); const { deferredToPromise } = require('./js/modules/deferred_to_promise'); const { app } = electron.remote; +const { clipboard } = electron; window.PROTO_ROOT = 'protos'; const config = require('url').parse(window.location.toString(), true).query; @@ -277,6 +278,8 @@ window.React = require('react'); window.ReactDOM = require('react-dom'); window.moment = require('moment'); +window.clipboard = clipboard; + const Signal = require('./js/modules/signal'); const i18n = require('./js/modules/i18n'); const Attachments = require('./app/attachments'); diff --git a/stylesheets/_index.scss b/stylesheets/_index.scss index 711ee8152..344eab95b 100644 --- a/stylesheets/_index.scss +++ b/stylesheets/_index.scss @@ -33,6 +33,7 @@ flex-direction: column; float: left; width: 300px; + position: relative; .tool-bar { margin-top: 8px; @@ -53,6 +54,10 @@ } } + .toast { + bottom: 78px; + } + .content { overflow-y: auto; max-height: calc(100% - 88px); @@ -142,51 +147,9 @@ } } -.identity-key-wrapper { - background-color: $color-black-008-no-tranparency; - display: flex; - flex: 1; - height: $header-height; - padding-left: 16px; - padding-right: 16px; -} - -.identity-key-container { - display: flex; - flex: 1; - flex-direction: row; - align-items: center; - justify-content: space-around; - white-space: nowrap; - overflow-x: hidden; -} - -.identity-key-text-container { - flex: 1; - text-align: center; - flex-direction: column; -} - -.identity-key-container div { - overflow-x: hidden; - text-overflow: ellipsis; -} - -.identity-key_bold { - font-weight: bold; -} - -.identity-key-wrapper__pencil-icon { - @include color-svg('../images/lead-pencil.svg', $color-gray-60); - height: 20px; - width: 20px; - margin-left: 4px; - cursor: pointer; -} - .underneathIdentityWrapper { position: absolute; - top: $header-height; + top: 0; bottom: 0; left: 300px; right: 0; diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index a2ecc0b96..2e534ab57 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -2180,15 +2180,73 @@ // Module: Main Header +.main-header-title-wrapper { + flex: 1; + flex-direction: row; + display: flex; + align-items: center; + cursor: pointer; + + &:hover { + background-color: $color-dark-75; + } +} + +.main-header-content-wrapper { + color: $color-dark-05; + + div { + padding: 12px; + background-color: $color-dark-90; + + cursor: pointer; + + &:hover { + background-color: $color-dark-75; + } + } +} + +.main-header-wrapper { + overflow-x: hidden; + flex: 1; +} + .module-main-header { height: $header-height; - margin-left: 16px; + padding-left: 16px; display: flex; flex-direction: row; align-items: center; } +.main-header-content-toggle { + width: 3em; + line-height: 3em; + font-weight: bold; + overflow: hidden; + user-select: none; + color: white; + text-align: center; + + &:after { + -webkit-transition: all .35s; + -o-transition: all .35s; + transition: all .35s; + width: 3em; + line-height: 3em; + height: 3em; + content: '\25BE'; + display: inline-block; + } +} + +.main-header-content-toggle-visible::after { + transform: rotate(180deg); +} + + .module-main-header__app-name { font-size: 16px; line-height: 24px; @@ -2197,6 +2255,14 @@ color: $color-dark-05; } +.module-main-header__contact-name { + font-weight: 300; + margin-left: 12px; + color: $color-dark-05; + overflow-x: auto; + flex: 1; +} + // Third-party module: react-contextmenu .react-contextmenu { diff --git a/stylesheets/_theme_dark.scss b/stylesheets/_theme_dark.scss index 8cfea9fe2..7d9137355 100644 --- a/stylesheets/_theme_dark.scss +++ b/stylesheets/_theme_dark.scss @@ -6,16 +6,6 @@ body.dark-theme { } .dark-theme { - // identity key - .identity-key-wrapper { - background-color:$color-gray-85; - } - - .identity-key-wrapper__pencil-icon { - @include color-svg('../images/lead-pencil.svg', $color-gray-25); - } - - // _conversation .conversation { diff --git a/stylesheets/_variables.scss b/stylesheets/_variables.scss index f7bb40910..98236c7d8 100644 --- a/stylesheets/_variables.scss +++ b/stylesheets/_variables.scss @@ -115,6 +115,7 @@ $color-dark-60: #797a7c; $color-dark-70: #414347; $color-dark-75: #292c33; $color-dark-85: #1a1c20; +$color-dark-90: #121417; $color-black-008: rgba($color-black, 0.08); $color-black-008-no-tranparency: #ededed; $color-black-016-no-tranparency: #d9d9d9; diff --git a/test/index.html b/test/index.html index 9dbcf2fa2..fdf60a6cc 100644 --- a/test/index.html +++ b/test/index.html @@ -374,6 +374,7 @@ + diff --git a/ts/components/IdentityKeyHeader.tsx b/ts/components/IdentityKeyHeader.tsx deleted file mode 100644 index a345cef0e..000000000 --- a/ts/components/IdentityKeyHeader.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; - -interface Props { - identityKey: string; - name?: string; - onEditProfile: () => void; -} - -export class IdentityKeyHeader extends React.Component { - public render() { - const { - name, - identityKey, - onEditProfile, - } = this.props; - - return ( -
-
-
- Your identity key: {identityKey} -
- {!!name && -
- Your display name: {name} -
- } -
-
-
- ); - } -} diff --git a/ts/components/MainHeader.tsx b/ts/components/MainHeader.tsx index d49911daa..0f81533bb 100644 --- a/ts/components/MainHeader.tsx +++ b/ts/components/MainHeader.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Avatar } from './Avatar'; +import { ContactName } from './conversation/ContactName'; import { Localizer } from '../types/Util'; @@ -25,10 +26,11 @@ export class MainHeader extends React.Component { name, phoneNumber, profileName, + onClick } = this.props; return ( -
+
{ profileName={profileName} size={28} /> -
Loki Messenger
+
+ +
); } diff --git a/ts/components/conversation/ContactName.tsx b/ts/components/conversation/ContactName.tsx index dbe17c24a..ef2755207 100644 --- a/ts/components/conversation/ContactName.tsx +++ b/ts/components/conversation/ContactName.tsx @@ -28,7 +28,7 @@ export class ContactName extends React.Component { return ( {profileElement} - +