diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 139821a3b..60b5ddc0c 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -952,6 +952,9 @@ "cancel": { "message": "Cancel" }, + "copy": { + "message": "Copy" + }, "skip": { "message": "Skip" }, @@ -2161,6 +2164,9 @@ "description": "Button action that the user can click to view their unique seed" }, + "yourSessionID": { + "message": "Your Session ID" + }, "setAccountPasswordTitle": { "message": "Set Account Password", "description": "Prompt for user to set account password in settings view" @@ -2255,6 +2261,11 @@ "description": "A toast message telling the user that the mnemonic seed was copied" }, + "copiedSessionID": { + "message": "Copied Session ID to clipboard", + "description": + "A toast message telling the user that their Session ID was copied" + }, "passwordViewTitle": { "message": "Type in your password", @@ -2423,7 +2434,7 @@ }, "editProfileModalTitle": { - "message": "Edit Profile", + "message": "Profile", "description": "Title for the Edit Profile modal" }, diff --git a/js/background.js b/js/background.js index 1e057c046..ba1ce12d1 100644 --- a/js/background.js +++ b/js/background.js @@ -820,8 +820,107 @@ confirmDialog.render(); }; + + window.showQRDialog = window.owsDesktopApp.appView.showQRDialog; window.showSeedDialog = window.owsDesktopApp.appView.showSeedDialog; window.showPasswordDialog = window.owsDesktopApp.appView.showPasswordDialog; + window.showEditProfileDialog = async () => { + const ourNumber = window.storage.get('primaryDevicePubKey'); + const conversation = await ConversationController.getOrCreateAndWait( + ourNumber, + 'private' + ); + + const readFile = attachment => + new Promise((resolve, reject) => { + const fileReader = new FileReader(); + fileReader.onload = e => { + const data = e.target.result; + resolve({ + ...attachment, + data, + size: data.byteLength, + }); + }; + fileReader.onerror = reject; + fileReader.onabort = reject; + fileReader.readAsArrayBuffer(attachment.file); + }); + + const avatarPath = conversation.getAvatarPath(); + const profile = conversation.getLokiProfile(); + const displayName = profile && profile.displayName; + + if (appView) { + appView.showEditProfileDialog({ + profileName: displayName, + pubkey: ourNumber, + avatarPath, + avatarColor: conversation.getColor(), + onOk: async (newName, avatar) => { + let newAvatarPath = ''; + let url = null; + let profileKey = null; + if (avatar) { + const data = await readFile({ file: avatar }); + + // For simplicity we use the same attachment pointer that would send to + // others, which means we need to wait for the database response. + // To avoid the wait, we create a temporary url for the local image + // and use it until we the the response from the server + const tempUrl = window.URL.createObjectURL(avatar); + conversation.setLokiProfile({ displayName: newName }); + conversation.set('avatar', tempUrl); + + // Encrypt with a new key every time + profileKey = libsignal.crypto.getRandomBytes(32); + const encryptedData = await textsecure.crypto.encryptProfile( + data.data, + profileKey + ); + + const avatarPointer = await textsecure.messaging.uploadAvatar({ + ...data, + data: encryptedData, + size: encryptedData.byteLength, + }); + + ({ url } = avatarPointer); + + storage.put('profileKey', profileKey); + + conversation.set('avatarPointer', url); + + const upgraded = await Signal.Migrations.processNewAttachment({ + isRaw: true, + data: data.data, + url, + }); + newAvatarPath = upgraded.path; + } + + // Replace our temporary image with the attachment pointer from the server: + conversation.set('avatar', null); + conversation.setLokiProfile({ + displayName: newName, + avatar: newAvatarPath, + }); + // inform all your registered public servers + // could put load on all the servers + // if they just keep changing their names without sending messages + // so we could disable this here + // or least it enable for the quickest response + window.lokiPublicChatAPI.setProfileName(newName); + window + .getConversations() + .filter(convo => convo.isPublic() && !convo.isRss()) + .forEach(convo => + convo.trigger('ourAvatarChanged', { url, profileKey }) + ); + }, + }); + } + }; window.generateID = () => Math.random() @@ -1031,104 +1130,6 @@ }); }); - Whisper.events.on('onEditProfile', async () => { - const ourNumber = window.storage.get('primaryDevicePubKey'); - const conversation = await ConversationController.getOrCreateAndWait( - ourNumber, - 'private' - ); - - const readFile = attachment => - new Promise((resolve, reject) => { - const fileReader = new FileReader(); - fileReader.onload = e => { - const data = e.target.result; - resolve({ - ...attachment, - data, - size: data.byteLength, - }); - }; - fileReader.onerror = reject; - fileReader.onabort = reject; - fileReader.readAsArrayBuffer(attachment.file); - }); - - const avatarPath = conversation.getAvatarPath(); - const profile = conversation.getLokiProfile(); - const displayName = profile && profile.displayName; - - if (appView) { - appView.showEditProfileDialog({ - profileName: displayName, - pubkey: ourNumber, - avatarPath, - avatarColor: conversation.getColor(), - onOk: async (newName, avatar) => { - let newAvatarPath = ''; - let url = null; - let profileKey = null; - if (avatar) { - const data = await readFile({ file: avatar }); - - // For simplicity we use the same attachment pointer that would send to - // others, which means we need to wait for the database response. - // To avoid the wait, we create a temporary url for the local image - // and use it until we the the response from the server - const tempUrl = window.URL.createObjectURL(avatar); - conversation.setLokiProfile({ displayName: newName }); - conversation.set('avatar', tempUrl); - - // Encrypt with a new key every time - profileKey = libsignal.crypto.getRandomBytes(32); - const encryptedData = await textsecure.crypto.encryptProfile( - data.data, - profileKey - ); - - const avatarPointer = await textsecure.messaging.uploadAvatar({ - ...data, - data: encryptedData, - size: encryptedData.byteLength, - }); - - ({ url } = avatarPointer); - - storage.put('profileKey', profileKey); - - conversation.set('avatarPointer', url); - - const upgraded = await Signal.Migrations.processNewAttachment({ - isRaw: true, - data: data.data, - url, - }); - newAvatarPath = upgraded.path; - } - - // Replace our temporary image with the attachment pointer from the server: - conversation.set('avatar', null); - conversation.setLokiProfile({ - displayName: newName, - avatar: newAvatarPath, - }); - // inform all your registered public servers - // could put load on all the servers - // if they just keep changing their names without sending messages - // so we could disable this here - // or least it enable for the quickest response - window.lokiPublicChatAPI.setProfileName(newName); - window - .getConversations() - .filter(convo => convo.isPublic() && !convo.isRss()) - .forEach(convo => - convo.trigger('ourAvatarChanged', { url, profileKey }) - ); - }, - }); - } - }); - Whisper.events.on('onShowUserDetails', async ({ userPubKey }) => { const conversation = await ConversationController.getOrCreateAndWait( userPubKey, diff --git a/stylesheets/_session.scss b/stylesheets/_session.scss index 60744e8d6..18a4339d9 100644 --- a/stylesheets/_session.scss +++ b/stylesheets/_session.scss @@ -27,7 +27,7 @@ font-weight: bold; } @font-face { - font-family: 'SFPro'; + font-family: 'SF Pro Text'; src: url('../fonts/SFProText-Regular.ttf') format('truetype'); } @@ -87,6 +87,12 @@ $session-margin-sm: 10px; $session-margin-md: 15px; $session-margin-lg: 20px; +$session-font-xs: 11px; +$session-font-sm: 13px; +$session-font-md: 15px; +$session-font-lg: 18px; +$session-font-xl: 24px; + $session-search-input-height: 34px; $main-view-header-height: 85px; @@ -322,7 +328,7 @@ $session_message-container-border-radius: 5px; height: 45px; line-height: 40px; padding: 0; - font-size: 15px; + font-size: $session-font-md; font-family: $session-font-family; border-radius: 500px; @@ -340,7 +346,7 @@ $session_message-container-border-radius: 5px; height: 33px; padding: 0px 18px; line-height: 33px; - font-size: 13px; + font-size: $session-font-sm; } &.square, @@ -415,7 +421,7 @@ $session_message-container-border-radius: 5px; .notification-count { position: absolute; - font-size: 12px; + font-size: $session-font-xs; font-family: $session-font-family; top: 20px; right: 20px; @@ -581,8 +587,8 @@ label { } .title { - font-size: 15px; - line-height: 13px; + font-size: $session-font-md; + line-height: $session-font-sm; margin-bottom: $session-margin-sm; padding-top: 0px; color: $session-color-white; @@ -590,7 +596,7 @@ label { } .description { - font-size: 12px; + font-size: $session-font-xs; @include session-color-subtle($session-color-white); } @@ -640,15 +646,30 @@ label { display: flex; flex-direction: row; justify-content: space-between; + align-items: center; padding: $session-margin-lg; font-family: 'Wasa'; text-align: center; line-height: 18px; - font-size: 15px; + font-size: $session-font-md; font-weight: 700; + &.reverse { + flex-direction: row-reverse; + + .session-modal__header__close > div { + float: right; + } + + .session-modal__header__icons > div { + float: left; + padding-left: 0px; + padding-right: 10px; + } + } + &__icons, &__close { width: 60px; @@ -668,8 +689,8 @@ label { &__body { padding: 0px $session-margin-lg $session-margin-lg $session-margin-lg; font-family: 'Wasa'; - line-height: 16px; - font-size: 13px; + line-height: $session-font-md; + font-size: $session-font-sm; .message { text-align: center; @@ -703,7 +724,7 @@ label { font-family: monospace; font-style: normal; - font-size: 11px; + font-size: $session-font-xs; } } @@ -715,7 +736,7 @@ label { } &-main-message { - font-size: 15px; + font-size: $session-font-md; } &-sub-message { margin-top: 5px; @@ -775,7 +796,7 @@ label { color: $session-color-white; font-family: 'Wasa'; - font-size: 12px; + font-size: $session-font-sm; line-height: $session-icon-size-sm; font-weight: 700; @@ -811,7 +832,7 @@ label { color: $session-color-white; font-family: 'Wasa'; - font-size: 10px; + font-size: $session-font-xs; line-height: $session-icon-size-sm; font-weight: 700; @@ -837,10 +858,132 @@ label { } } -.edit-profile-dialog .image-upload-section { - position: absolute; - margin-top: 50px; - margin-left: 75px; +.edit-profile-dialog { + .session-modal__header__title { + font-size: $session-font-lg; + } + + .session-modal { + width: $session-modal-size-md; + + &__header { + height: 68.45px; + } + } + + .avatar-center-inner { + position: relative; + + .module-avatar { + box-shadow: 0 0 23px 0 rgba($session-color-black, 0.78); + } + + .qr-view-button { + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + position: absolute; + right: -3px; + height: 26px; + width: 26px; + border-radius: 50%; + padding-top: 3px; + background-color: $session-color-white; + transition: $session-transition-duration; + + &:hover{ + filter: brightness(90%); + } + + .session-icon-button { + opacity: 1; + } + } + } + + .image-upload-section { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + cursor: pointer; + width: 80px; + height: 80px; + border-radius: 100%; + background-color: rgba($session-color-black, 0.72); + box-shadow: 0px 0px 3px 0.5px rgba(0,0,0,0.75); + opacity: 0; + transition: $session-transition-duration; + + &:after{ + content: "Edit"; + } + + &:hover{ + opacity: 1; + } + } + + .qr-image { + display: flex; + justify-content: center; + + svg { + width: 135px; + height: 135px; + border-radius: 3px; + padding: $session-margin-xs; + background-color: $session-color-white; + } + } + + .session-id-section { + .panel-text-divider { + margin-top: 35px; + margin-bottom: 35px; + } + + &-display{ + text-align: center; + word-break: break-all; + font-size: $session-font-md; + padding: 0px $session-margin-lg; + font-family: "SF Pro Text"; + font-weight: 100; + color: rgba($session-color-white, 0.80); + font-size: $session-font-md; + padding: 0px $session-margin-sm; + } + } + + .profile-name { + display: flex; + justify-content: center; + margin-top: $session-margin-lg; + + &-input { + height: 38px; + width: 142px; + border-radius: 5px; + text-align: center; + font-size: $session-font-md; + background-color: $session-shade-5 !important; + } + + &-uneditable { + display: flex; + align-items: center; + justify-content: center; + margin-left: $session-margin-lg; + + p { + font-size: $session-font-md; + padding: 0px $session-margin-sm; + } + + } + } } .conversation-loader { @@ -932,7 +1075,7 @@ label { &-title { line-height: $main-view-header-height; font-weight: bold; - font-size: 18px; + font-size: $session-font-lg; text-align: center; flex-grow: 1; } @@ -944,7 +1087,7 @@ label { } &-item { - font-size: 15px; + font-size: $session-font-md; color: $session-color-white; background-color: $session-shade-1; @@ -963,12 +1106,12 @@ label { &__title { line-height: 1.7; - font-size: 16px; + font-size: $session-font-lg; font-weight: bold; } &__description { - font-size: 13px; + font-size: $session-font-sm; font-weight: 100; @include session-color-subtle($session-color-white); } @@ -1014,7 +1157,7 @@ label { border: none; border-radius: 2px; text-align: center; - font-size: 25px; + font-size: $session-font-xl; letter-spacing: 5px; font-family: 'SF Pro Text'; } @@ -1025,6 +1168,8 @@ label { #qr svg { width: $session-modal-size-sm; height: $session-modal-size-sm; + padding: $session-margin-xs; + background-color: $session-color-white; border-radius: 3px; } @@ -1059,7 +1204,7 @@ label { border: none; margin: 0px; padding: 0px $session-margin-lg; - font-size: 15px; + font-size: $session-font-md; line-height: 60px; @include session-color-subtle($session-color-white); @@ -1173,7 +1318,7 @@ input { width: 600px; &__header { - font-size: 17px; + font-size: $session-font-lg; } &__body > div:first-child { diff --git a/ts/components/EditProfileDialog.tsx b/ts/components/EditProfileDialog.tsx index 64b4d0de1..0afdf6607 100644 --- a/ts/components/EditProfileDialog.tsx +++ b/ts/components/EditProfileDialog.tsx @@ -1,8 +1,10 @@ import React from 'react'; -import classNames from 'classnames'; +import { QRCode } from 'react-qr-svg'; + import { Avatar } from './Avatar'; -import { SessionButton, SessionButtonColor } from './session/SessionButton'; +import { SessionButton, SessionButtonColor, SessionButtonType } from './session/SessionButton'; + import { SessionIconButton, SessionIconSize, @@ -28,9 +30,9 @@ interface Props { interface State { profileName: string; - errorDisplayed: boolean; - errorMessage: string; + setProfileName: string; avatar: string; + mode: 'default' | 'edit' | 'qr'; } export class EditProfileDialog extends React.Component { @@ -42,15 +44,14 @@ export class EditProfileDialog extends React.Component { this.onNameEdited = this.onNameEdited.bind(this); this.closeDialog = this.closeDialog.bind(this); this.onClickOK = this.onClickOK.bind(this); - this.showError = this.showError.bind(this); this.onKeyUp = this.onKeyUp.bind(this); this.onFileSelected = this.onFileSelected.bind(this); this.state = { profileName: this.props.profileName, - errorDisplayed: false, - errorMessage: 'placeholder', + setProfileName: this.props.profileName, avatar: this.props.avatarPath, + mode: 'default', }; this.inputEl = React.createRef(); @@ -61,25 +62,81 @@ export class EditProfileDialog extends React.Component { public render() { const i18n = this.props.i18n; - const cancelText = i18n('cancel'); - const okText = i18n('ok'); - const placeholderText = i18n('profileName'); + const viewDefault = this.state.mode === 'default'; + const viewEdit = this.state.mode === 'edit'; + const viewQR = this.state.mode === 'qr'; + const sessionID = window.textsecure.storage.user.getNumber(); - const errorMessageClasses = classNames( - 'error-message', - this.state.errorDisplayed ? 'error-shown' : 'error-faded' - ); + const backButton = (viewEdit || viewQR) + ? [{ + iconType: SessionIconType.Chevron, + iconRotation: 90, + onClick: () => this.setState({mode: 'default'}), + }] + : undefined; return ( +
+ + {viewQR && this.renderQRView(sessionID)} + {viewDefault && this.renderDefaultView()} + {viewEdit && this.renderEditView()} + +
+
+ {window.i18n('yourSessionID')} +
+

+ { sessionID } +

+ +
+ + { (viewDefault || viewQR) ? ( + this.copySessionID(sessionID)} + /> + ) : ( + + )} + +
+ +
+
+ ); + } + + private renderProfileHeader() { + return ( + <>
{this.renderAvatar()} -
+
{ + const el = this.inputEl.current; + if (el) { + el.click(); + } + }} + > { name="name" onChange={this.onFileSelected} /> - +
+
this.setState({mode: 'qr'})} + > { - const el = this.inputEl.current; - if (el) { - el.click(); - } - }} + iconType={SessionIconType.QR} + iconSize={SessionIconSize.Small} + iconColor={"#000000"} />
+ + ); + } -
- - -
{i18n('editProfileDisplayNameWarning')}
- {this.state.errorMessage} - -
- -
- + { this.renderProfileHeader() } + +
+

{ this.state.setProfileName }

+ this.setState({mode: 'edit'})} /> - + + ); + } + + + + private renderEditView() { + const placeholderText = window.i18n('displayName'); + + return ( + <> + { this.renderProfileHeader() } +
+
- + + ); + } + + private renderQRView(sessionID: string){ + const bgColor = '#FFFFFF'; + const fgColor = '#1B1B1B'; + + return ( +
+ +
); } @@ -189,29 +267,20 @@ export class EditProfileDialog extends React.Component { } } - private showError(msg: string) { - if (this.state.errorDisplayed) { - return; - } + private copySessionID(sessionID: string) { + window.clipboard.writeText(sessionID); - this.setState({ - errorDisplayed: true, - errorMessage: msg, + window.pushToast({ + title: window.i18n('copiedSessionID'), + type: 'success', + id: 'copiedSessionID', }); - - setTimeout(() => { - this.setState({ - errorDisplayed: false, - }); - }, 3000); } private onClickOK() { const newName = this.state.profileName.trim(); if (newName === '') { - this.showError(this.props.i18n('emptyProfileNameError')); - return; } @@ -224,7 +293,11 @@ export class EditProfileDialog extends React.Component { : null; this.props.onOk(newName, avatar); - this.closeDialog(); + + this.setState({ + mode: 'default', + setProfileName: this.state.profileName, + }); } private closeDialog() { diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index 983958eb7..602c8cb2b 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -40,9 +40,9 @@ const Section = ({ }) => { const handleClick = onSelect ? () => { - if (type !== SectionType.Profile) { - onSelect(type); - } + type === SectionType.Profile + ? window.showEditProfileDialog() + : onSelect(type); } : undefined; diff --git a/ts/components/session/SessionModal.tsx b/ts/components/session/SessionModal.tsx index d6502e163..c9078d11d 100644 --- a/ts/components/session/SessionModal.tsx +++ b/ts/components/session/SessionModal.tsx @@ -1,6 +1,8 @@ import React from 'react'; +import classNames from 'classnames'; import { SessionIconButton, SessionIconSize, SessionIconType } from './icon/'; +import { SessionButton, SessionButtonType, SessionButtonColor } from './SessionButton'; interface Props { title: string; @@ -8,9 +10,17 @@ interface Props { onOk: any; showExitIcon?: boolean; showHeader?: boolean; + headerReverse?: boolean; //Maximum of two icons in header headerIconButtons?: Array<{ - type: SessionIconType; + iconType: SessionIconType; + iconRotation: number; + onClick?: any; + }>; + headerButtons?: Array<{ + buttonType: SessionButtonType; + buttonColor: SessionButtonColor; + text: string; onClick?: any; }>; } @@ -23,6 +33,7 @@ export class SessionModal extends React.PureComponent { public static defaultProps = { showExitIcon: true, showHeader: true, + headerReverse: false, }; constructor(props: any) { @@ -38,14 +49,14 @@ export class SessionModal extends React.PureComponent { } public render() { - const { title, headerIconButtons, showExitIcon, showHeader } = this.props; + const { title, headerIconButtons, showExitIcon, showHeader, headerReverse } = this.props; const { isVisible } = this.state; return isVisible ? (
{showHeader ? ( <> -
+
{showExitIcon ? ( { ? headerIconButtons.map((iconItem: any) => { return ( ); }) diff --git a/ts/components/session/SessionQRModal.tsx b/ts/components/session/SessionQRModal.tsx index b930daf0d..97f613c8d 100644 --- a/ts/components/session/SessionQRModal.tsx +++ b/ts/components/session/SessionQRModal.tsx @@ -17,11 +17,9 @@ export class SessionQRModal extends React.Component { public render() { const { value, onClose } = this.props; - const theme = window.Events.getThemeSetting(); - // Foreground equivalent to .session-modal background color - const bgColor = 'rgba(0, 0, 0, 0)'; - const fgColor = theme === 'dark' ? '#FFFFFF' : '#1B1B1B'; + const bgColor = '#FFFFFF'; + const fgColor = '#1B1B1B'; return ( { return (
- + {shouldRenderPasswordLock ? ( this.renderPasswordLock() ) : ( @@ -294,7 +294,7 @@ export class SettingsView extends React.Component { { id: 'theme-setting', title: window.i18n('themeToggleTitle'), - description: 'Choose the theme best suited to you', + description: window.i18n('themeToggleDescription'), hidden: true, comparisonValue: 'light', type: SessionSettingType.Toggle, diff --git a/ts/components/session/settings/SessionSettingsHeader.tsx b/ts/components/session/settings/SessionSettingsHeader.tsx index 538d7f8a0..5d3bf106b 100644 --- a/ts/components/session/settings/SessionSettingsHeader.tsx +++ b/ts/components/session/settings/SessionSettingsHeader.tsx @@ -4,11 +4,19 @@ import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon'; import { SessionSettingCategory, SettingsViewProps } from './SessionSettings'; import { SessionButton } from '../SessionButton'; -export class SettingsHeader extends React.Component { +interface Props extends SettingsViewProps{ + disableLinkDeviceButton: boolean | null; +} + +export class SettingsHeader extends React.Component { + public static defaultProps = { + disableLinkDeviceButton: true, + } + public constructor(props: any) { super(props); this.state = { - disableLinkDeviceButton: true, + disableLinkDeviceButton: this.props.disableLinkDeviceButton, }; this.showAddLinkedDeviceModal = this.showAddLinkedDeviceModal.bind(this); } diff --git a/ts/global.d.ts b/ts/global.d.ts index 994356b3d..d1f16e980 100644 --- a/ts/global.d.ts +++ b/ts/global.d.ts @@ -28,8 +28,10 @@ interface Window { pushToast: any; confirmationDialog: any; + showQRDialog: any; showSeedDialog: any; showPasswordDialog: any; + showEditProfileDialog: any; deleteAccount: any;