diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 96b095016..231c5f8e9 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -428,5 +428,8 @@ "dialogClearAllDataDeletionFailedMultiple": "Data not deleted by those Service Nodes: $snodes$", "dialogClearAllDataDeletionQuestion": "Would you like to clear only this device, or delete your entire account?", "deviceOnly": "Device Only", - "entireAccount": "Entire Account" + "entireAccount": "Entire Account", + "recoveryPhraseSecureTitle": "You're almost finished!", + "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", + "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase" } diff --git a/stylesheets/_session.scss b/stylesheets/_session.scss index 11e6ffa17..4385acff8 100644 --- a/stylesheets/_session.scss +++ b/stylesheets/_session.scss @@ -224,7 +224,6 @@ textarea { border-radius: 2px; height: 33px; padding: 0px 18px; - // line-height: 33px; font-size: $session-font-sm; } @@ -1248,7 +1247,6 @@ input { margin: 15px calc(100% / 2 - 1px); width: 1px; - // z-index: -1; } } diff --git a/stylesheets/_session_constants.scss b/stylesheets/_session_constants.scss index 364edc36e..725ed6ce5 100644 --- a/stylesheets/_session_constants.scss +++ b/stylesheets/_session_constants.scss @@ -153,6 +153,27 @@ $session-font-h4: 16px; color: subtle($color); } +@mixin pulse-color($color, $duration, $repetition) { + animation: pulseColor $duration $repetition; + + @keyframes pulseColor { + 0% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba($color, 0.7); + } + + 70% { + transform: scale(1); + box-shadow: 0 0 0 10px rgba($color, 0); + } + + 100% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba($color, 0); + } + } +} + $session-subtle-factor: 0.6; @function subtle($color) { diff --git a/stylesheets/_session_conversation.scss b/stylesheets/_session_conversation.scss index 75011e6c3..0a85590c0 100644 --- a/stylesheets/_session_conversation.scss +++ b/stylesheets/_session_conversation.scss @@ -180,14 +180,21 @@ border-top: themed('sessionBorder'); } - & > .session-icon-button { + .session-icon-button { + // & > .session-icon-button { margin-right: $session-margin-sm; } .session-icon-button { display: flex; justify-content: center; align-items: center; - opacity: 0.8; + opacity: 0.7; + + &:hover { + opacity: 1; + transform: scale(0.93); + transition: $session-transition-duration; + } .send { padding: $session-margin-xs; @@ -329,6 +336,26 @@ } } +.send-message-button { + animation: fadein $session-transition-duration; + + &---scale { + animation: scaling 2s ease-in-out; + + @keyframes scaling { + 0% { + transform: scale(1); + } + 80% { + transform: scale(1.3); + } + 100% { + transform: scale(1); + } + } + } +} + .session-recording { height: $composition-container-height; display: flex; @@ -337,19 +364,21 @@ flex-grow: 1; outline: none; - $actions-element-size: 45px; + $actions-element-size: 30px; &--actions { display: flex; align-items: center; - justify-content: center; width: $actions-element-size; height: $actions-element-size; border-radius: 50%; + .session-button { + animation: fadein $session-transition-duration; + } + .session-icon-button { animation: fadein $session-transition-duration; - opacity: 1; border-radius: 50%; width: $actions-element-size; height: $actions-element-size; @@ -414,7 +443,11 @@ flex-shrink: 0; &.playback-timer { - margin-right: $session-margin-sm; + animation: fadein $session-transition-duration; + + @media (-webkit-min-device-pixel-ratio: 1.6) { + margin-left: auto; + } } &-light { @@ -422,23 +455,13 @@ width: $session-margin-sm; border-radius: 50%; background-color: $session-color-danger-alt; - margin-left: $session-margin-sm; + margin: 0 $session-margin-sm; - animation: pulseLight 4s infinite; + @include pulse-color($session-color-danger-alt, 1s, infinite); } } } -// box-sizing: border-box; -// position: absolute; -// z-index: 3; -// width: 4px; -// height: 5px; -// margin-left: 0px; -// top: 0px; -// background: #00f782; -// border-radius: 50px; - /* ************ */ /* AUDIO PLAYER */ /* ************ */ diff --git a/ts/components/dialog/DeleteAccountModal.tsx b/ts/components/dialog/DeleteAccountModal.tsx index b0e948019..40c4226f5 100644 --- a/ts/components/dialog/DeleteAccountModal.tsx +++ b/ts/components/dialog/DeleteAccountModal.tsx @@ -11,6 +11,7 @@ import { SessionWrapperModal } from '../session/SessionWrapperModal'; const deleteDbLocally = async () => { window?.log?.info('configuration message sent successfully. Deleting everything'); + window.persistStore?.purge(); await window.Signal.Logs.deleteAll(); await window.Signal.Data.removeAll(); await window.Signal.Data.close(); diff --git a/ts/components/dialog/SessionSeedModal.tsx b/ts/components/dialog/SessionSeedModal.tsx index 3a48bac69..3e61e716d 100644 --- a/ts/components/dialog/SessionSeedModal.tsx +++ b/ts/components/dialog/SessionSeedModal.tsx @@ -1,216 +1,198 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { SessionButton } from '../session/SessionButton'; import { ToastUtils, UserUtils } from '../../session/utils'; -import { withTheme } from 'styled-components'; import { PasswordUtil } from '../../util'; import { getPasswordHash } from '../../data/data'; import { QRCode } from 'react-qr-svg'; import { mn_decode } from '../../session/crypto/mnemonic'; import { SessionWrapperModal } from '../session/SessionWrapperModal'; import { SpacerLG, SpacerSM, SpacerXS } from '../basic/Text'; -import autoBind from 'auto-bind'; import { recoveryPhraseModal } from '../../state/ducks/modalDialog'; +import { useDispatch } from 'react-redux'; -interface State { - error: string; - loadingPassword: boolean; - loadingSeed: boolean; - recoveryPhrase: string; - hasPassword: boolean | null; +interface PasswordProps { + setPasswordValid: (val: boolean) => any; passwordHash: string; - passwordValid: boolean; } -class SessionSeedModalInner extends React.Component<{}, State> { - constructor(props: any) { - super(props); +const Password = (props: PasswordProps) => { + const { setPasswordValid, passwordHash } = props; + const i18n = window.i18n; + const [error, setError] = useState(''); + const dispatch = useDispatch(); - this.state = { - error: '', - loadingPassword: true, - loadingSeed: true, - recoveryPhrase: '', - hasPassword: null, - passwordHash: '', - passwordValid: false, - }; + const onClose = () => dispatch(recoveryPhraseModal(null)); - autoBind(this); - } + const confirmPassword = () => { + const passwordValue = jQuery('#seed-input-password').val(); + const isPasswordValid = PasswordUtil.matchesHash(passwordValue as string, passwordHash); - public componentDidMount() { - setTimeout(() => ($('#seed-input-password') as any).focus(), 100); + if (!passwordValue) { + setError('noGivenPassword'); + return false; + } - void this.checkHasPassword(); - void this.getRecoveryPhrase(); - } - - public render() { - const i18n = window.i18n; - - const { hasPassword, passwordValid } = this.state; - const loading = this.state.loadingPassword || this.state.loadingSeed; - const onClose = () => window.inboxStore?.dispatch(recoveryPhraseModal(null)); - - return ( - <> - {!loading && ( - - - - {hasPassword && !passwordValid ? ( - <>{this.renderPasswordView()} - ) : ( - <>{this.renderSeedView()} - )} - - )} - - ); - } - - private renderPasswordView() { - const error = this.state.error; - const i18n = window.i18n; - - const onClose = () => window.inboxStore?.dispatch(recoveryPhraseModal(null)); - - return ( - <> -

{i18n('showRecoveryPhrasePasswordRequest')}

- + if (passwordHash && !isPasswordValid) { + setError('invalidPassword'); + return false; + } - {error && ( - <> - -
{error}
- - )} + setPasswordValid(true); + setError(''); - + window.removeEventListener('keyup', onEnter); + return true; + }; -
- + const onEnter = (event: any) => { + if (event.key === 'Enter') { + confirmPassword(); + } + }; + + return ( + <> +

{i18n('showRecoveryPhrasePasswordRequest')}

+ + + {error && ( + <> + +
{error}
+ + )} - -
- - ); - } + - private renderSeedView() { - const i18n = window.i18n; - const bgColor = '#FFFFFF'; - const fgColor = '#1B1B1B'; +
+ - const hexEncodedSeed = mn_decode(this.state.recoveryPhrase, 'english'); + +
+ + ); +}; - return ( - <> -
-

{i18n('recoveryPhraseSavePromptMain')}

- +interface SeedProps { + recoveryPhrase: string; + onClickCopy?: () => any; +} - {this.state.recoveryPhrase} -
- -
- -
- -
- { - this.copyRecoveryPhrase(this.state.recoveryPhrase); - }} - /> -
- - ); - } - - private confirmPassword() { - const passwordHash = this.state.passwordHash; - const passwordValue = jQuery('#seed-input-password').val(); - const isPasswordValid = PasswordUtil.matchesHash(passwordValue as string, passwordHash); +const Seed = (props: SeedProps) => { + const { recoveryPhrase, onClickCopy } = props; + const i18n = window.i18n; + const bgColor = '#FFFFFF'; + const fgColor = '#1B1B1B'; + const dispatch = useDispatch(); - if (!passwordValue) { - this.setState({ - error: window.i18n('noGivenPassword'), - }); + const hexEncodedSeed = mn_decode(recoveryPhrase, 'english'); - return false; + const copyRecoveryPhrase = (recoveryPhraseToCopy: string) => { + window.clipboard.writeText(recoveryPhraseToCopy); + ToastUtils.pushCopiedToClipBoard(); + if (onClickCopy) { + onClickCopy(); } + dispatch(recoveryPhraseModal(null)); + }; + + return ( + <> +
+

{i18n('recoveryPhraseSavePromptMain')}

+ + + {recoveryPhrase} +
+ +
+ +
+ +
+ { + copyRecoveryPhrase(recoveryPhrase); + }} + /> +
+ + ); +}; - if (passwordHash && !isPasswordValid) { - this.setState({ - error: window.i18n('invalidPassword'), - }); - - return false; - } +interface ModalInnerProps { + onClickOk?: () => any; +} - this.setState({ - passwordValid: true, - error: '', - }); +const SessionSeedModalInner = (props: ModalInnerProps) => { + const { onClickOk } = props; + const [loadingPassword, setLoadingPassword] = useState(true); + const [loadingSeed, setLoadingSeed] = useState(true); + const [recoveryPhrase, setRecoveryPhrase] = useState(''); + const [hasPassword, setHasPassword] = useState(null); + const [passwordValid, setPasswordValid] = useState(false); + const [passwordHash, setPasswordHash] = useState(''); + const dispatch = useDispatch(); + + useEffect(() => { + setTimeout(() => ($('#seed-input-password') as any).focus(), 100); + void checkHasPassword(); + void getRecoveryPhrase(); + }, []); - window.removeEventListener('keyup', this.onEnter); + const i18n = window.i18n; - return true; - } + const onClose = () => dispatch(recoveryPhraseModal(null)); - private async checkHasPassword() { - if (!this.state.loadingPassword) { + const checkHasPassword = async () => { + if (!loadingPassword) { return; } const hash = await getPasswordHash(); - this.setState({ - hasPassword: !!hash, - passwordHash: hash || '', - loadingPassword: false, - }); - } - - private async getRecoveryPhrase() { - if (this.state.recoveryPhrase) { + setHasPassword(!!hash); + setPasswordHash(hash || ''); + setLoadingPassword(false); + }; + + const getRecoveryPhrase = async () => { + if (recoveryPhrase) { return false; } - - const recoveryPhrase = UserUtils.getCurrentRecoveryPhrase(); - - this.setState({ - recoveryPhrase, - loadingSeed: false, - }); + const newRecoveryPhrase = UserUtils.getCurrentRecoveryPhrase(); + setRecoveryPhrase(newRecoveryPhrase); + setLoadingSeed(false); return true; - } - - private copyRecoveryPhrase(recoveryPhrase: string) { - window.clipboard.writeText(recoveryPhrase); - - ToastUtils.pushCopiedToClipBoard(); - window.inboxStore?.dispatch(recoveryPhraseModal(null)); - } - - private onEnter(event: any) { - if (event.key === 'Enter') { - this.confirmPassword(); - } - } -} - -export const SessionSeedModal = withTheme(SessionSeedModalInner); + }; + + return ( + <> + {!loadingSeed && ( + + + + {hasPassword && !passwordValid ? ( + + ) : ( + + )} + + )} + : + + ); +}; + +export const SessionSeedModal = SessionSeedModalInner; diff --git a/ts/components/session/LeftPaneSectionHeader.tsx b/ts/components/session/LeftPaneSectionHeader.tsx index 78cec1c8c..fcff680a3 100644 --- a/ts/components/session/LeftPaneSectionHeader.tsx +++ b/ts/components/session/LeftPaneSectionHeader.tsx @@ -1,8 +1,13 @@ import React from 'react'; import classNames from 'classnames'; import { SessionIcon, SessionIconSize, SessionIconType } from './icon'; -import { useTheme } from 'styled-components'; -import { SessionButton } from './SessionButton'; +import styled, { useTheme } from 'styled-components'; +import { SessionButton, SessionButtonType } from './SessionButton'; +import { useDispatch, useSelector } from 'react-redux'; +import { disableRecoveryPhrasePrompt } from '../../state/ducks/userConfig'; +import { getShowRecoveryPhrasePrompt } from '../../state/selectors/userConfig'; +import { recoveryPhraseModal } from '../../state/ducks/modalDialog'; +import { Flex } from '../basic/Flex'; const Tab = ({ isSelected, @@ -40,22 +45,119 @@ type Props = { export const LeftPaneSectionHeader = (props: Props) => { const { label, buttonIcon, buttonClicked } = props; + const theme = useTheme(); + const showRecoveryPhrasePrompt = useSelector(getShowRecoveryPhrasePrompt); + + return ( + +
+ {label && } + {buttonIcon && ( + + + + )} +
+ {showRecoveryPhrasePrompt && } +
+ ); +}; + +export const LeftPaneBanner = () => { + const dispatch = useDispatch(); + + const showRecoveryPhraseModal = () => { + dispatch( + recoveryPhraseModal({ + onClickOk: () => { + dispatch(disableRecoveryPhrasePrompt()); + }, + }) + ); + }; + + const BannerInner = () => { + return ( + +

{window.i18n('recoveryPhraseRevealMessage')}

+ +
+ ); + }; const theme = useTheme(); return ( -
- {label && } - {buttonIcon && ( - - - - )} -
+ + + + + + {window.i18n('recoveryPhraseSecureTitle')} 90% + + + + + ); }; + +const StyledProgressBarContainer = styled.div` + width: 100%; + height: 5px; + flex-direction: row; + background: ${p => p.theme.colors.sessionBorderColor}; +`; + +const StyledProgressBarInner = styled.div` + background: ${p => p.theme.colors.accent}; + width: 90%; + transition: width 0.5s ease-in; + height: 100%; +`; + +export const StyledBannerTitle = styled.div` + line-height: 1.3; + font-size: ${p => p.theme.common.fonts.md}; + font-weight: bold; + margin: ${p => p.theme.common.margins.sm} ${p => p.theme.common.margins.sm} 0 + ${p => p.theme.common.margins.sm}; + + span { + color: ${p => p.theme.colors.textAccent}; + } +`; + +export const StyledLeftPaneBanner = styled.div` + background: ${p => p.theme.colors.recoveryPhraseBannerBackground}; + display: flex; + flex-direction: column; + border-bottom: ${p => p.theme.colors.sessionBorder}; +`; + +const StyledBannerInner = styled.div` + p { + margin: 0; + } + + .left-pane-banner___phrase { + margin-top: ${props => props.theme.common.margins.md}; + } + + .session-button { + margin-top: ${props => props.theme.common.margins.sm}; + } +`; diff --git a/ts/components/session/LeftPaneSettingSection.tsx b/ts/components/session/LeftPaneSettingSection.tsx index be32bc1ad..7c8b79e6f 100644 --- a/ts/components/session/LeftPaneSettingSection.tsx +++ b/ts/components/session/LeftPaneSettingSection.tsx @@ -1,7 +1,6 @@ +import React from 'react'; import classNames from 'classnames'; - import { SessionButton, SessionButtonColor, SessionButtonType } from './SessionButton'; - import { SessionIcon, SessionIconSize, SessionIconType } from './icon'; import { SessionSettingCategory } from './settings/SessionSettings'; import { LeftPaneSectionHeader } from './LeftPaneSectionHeader'; @@ -10,7 +9,6 @@ import { showSettingsSection } from '../../state/ducks/section'; import { getFocusedSettingsSection } from '../../state/selectors/section'; import { getTheme } from '../../state/selectors/theme'; import { recoveryPhraseModal, updateDeleteAccountModal } from '../../state/ducks/modalDialog'; -import React from 'react'; const getCategories = () => { return [ diff --git a/ts/components/session/SessionInboxView.tsx b/ts/components/session/SessionInboxView.tsx index 6937a19e3..36187b644 100644 --- a/ts/components/session/SessionInboxView.tsx +++ b/ts/components/session/SessionInboxView.tsx @@ -53,6 +53,7 @@ export class SessionInboxView extends React.Component { } const persistor = persistStore(this.store); + window.persistStore = persistor; return ( diff --git a/ts/components/session/conversation/SessionRecording.tsx b/ts/components/session/conversation/SessionRecording.tsx index 0efc3161c..7b6cf12dd 100644 --- a/ts/components/session/conversation/SessionRecording.tsx +++ b/ts/components/session/conversation/SessionRecording.tsx @@ -3,11 +3,11 @@ import classNames from 'classnames'; import moment from 'moment'; import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon'; -import { SessionButton, SessionButtonColor, SessionButtonType } from '../SessionButton'; import { Constants } from '../../../session'; import { ToastUtils } from '../../../session/utils'; import autoBind from 'auto-bind'; import MicRecorder from 'mic-recorder-to-mp3'; +import styled from 'styled-components'; interface Props { onExitVoiceNoteView: any; @@ -33,6 +33,24 @@ function getTimestamp(asInt = false) { return asInt ? Math.floor(timestamp) : timestamp; } +export interface StyledFlexWrapperProps { + flexDirection: 'row' | 'column'; + marginHorizontal: string; +} + +/** + * Generic wrapper for quickly passing in theme constant values. + */ +export const StyledFlexWrapper = styled.div` + display: flex; + flex-direction: ${(props: StyledFlexWrapperProps) => props.flexDirection}; + align-items: center; + + .session-button { + margin: ${(props: StyledFlexWrapperProps) => props.marginHorizontal}; + } +`; + class SessionRecordingInner extends React.Component { private recorder: any; private audioBlobMp3?: Blob; @@ -77,19 +95,12 @@ class SessionRecordingInner extends React.Component { // tslint:disable-next-line: cyclomatic-complexity public render() { - const { - actionHover, - isPlaying, - isPaused, - isRecording, - startTimestamp, - nowTimestamp, - } = this.state; - - const actionStopRecording = actionHover && isRecording; - const actionPlayAudio = !isRecording && !isPlaying; + const { isPlaying, isPaused, isRecording, startTimestamp, nowTimestamp } = this.state; + + const hasRecordingAndPaused = !isRecording && !isPlaying; + const hasRecording = !!this.audioElement?.duration && this.audioElement?.duration > 0; const actionPauseAudio = !isRecording && !isPaused && isPlaying; - const actionDefault = !actionStopRecording && !actionPlayAudio && !actionPauseAudio; + const actionDefault = !isRecording && !hasRecordingAndPaused && !actionPauseAudio; // if we are recording, we base the time recording on our state values // if we are playing ( audioElement?.currentTime is !== 0, use that instead) @@ -102,6 +113,14 @@ class SessionRecordingInner extends React.Component { 0; const displayTimeString = moment.utc(displayTimeMs).format('m:ss'); + const recordingDurationMs = this.audioElement?.duration + ? this.audioElement?.duration * 1000 + : 1; + + let remainingTimeString = ''; + if (recordingDurationMs !== undefined) { + remainingTimeString = ` / ${moment.utc(recordingDurationMs).format('m:ss')}`; + } const actionPauseFn = isPlaying ? this.pauseAudio : this.stopRecordingStream; @@ -112,28 +131,37 @@ class SessionRecordingInner extends React.Component { onMouseEnter={this.handleHoverActions} onMouseLeave={this.handleUnhoverActions} > - {actionStopRecording && ( - - )} - {actionPauseAudio && ( - - )} - {actionPlayAudio && ( - - )} + + {isRecording && ( + + )} + {actionPauseAudio && ( + + )} + {hasRecordingAndPaused && ( + + )} + {hasRecording && ( + + )} + {actionDefault && ( { )} -
- {displayTimeString} - {isRecording &&
} -
+ {hasRecording && !isRecording ? ( +
+ {displayTimeString + remainingTimeString} +
+ ) : null} + + {isRecording ? ( +
+ {displayTimeString} +
+
+ ) : null} {!isRecording && ( -
+
{ />
)} - -
- {isRecording ? ( - - ) : ( - - )} -
); } @@ -271,6 +295,9 @@ class SessionRecordingInner extends React.Component { this.props.onExitVoiceNoteView(); } + /** + * Sends the recorded voice message + */ private async onSendVoiceMessage() { if (!this.audioBlobMp3 || !this.audioBlobMp3.size) { window?.log?.info('Empty audio blob'); @@ -305,6 +332,9 @@ class SessionRecordingInner extends React.Component { }); } + /** + * Stops recording audio, sets recording state to stopped. + */ private async stopRecordingStream() { if (!this.recorder) { return; @@ -313,10 +343,39 @@ class SessionRecordingInner extends React.Component { this.recorder = undefined; this.audioBlobMp3 = blob; + this.updateAudioElementAndDuration(); + // Stop recording this.stopRecordingState(); } + /** + * Creates an audio element using the recorded audio blob. + * Updates the duration for displaying audio duration. + */ + private updateAudioElementAndDuration() { + // init audio element + const audioURL = window.URL.createObjectURL(this.audioBlobMp3); + this.audioElement = new Audio(audioURL); + + this.setState({ + recordDuration: this.audioElement.duration, + }); + + this.audioElement.loop = false; + this.audioElement.onended = () => { + this.pauseAudio(); + }; + + this.audioElement.oncanplaythrough = async () => { + const duration = this.state.recordDuration; + + if (duration && this.audioElement && this.audioElement.currentTime < duration) { + await this.audioElement?.play(); + } + }; + } + private async onKeyDown(event: any) { if (event.key === 'Escape') { await this.onDeleteVoiceMessage(); diff --git a/ts/components/session/icon/Icons.tsx b/ts/components/session/icon/Icons.tsx index a942fa1ab..4e0aa2c0e 100644 --- a/ts/components/session/icon/Icons.tsx +++ b/ts/components/session/icon/Icons.tsx @@ -12,6 +12,7 @@ export enum SessionIconType { CirclePlus = 'circlePlus', CircleElipses = 'circleElipses', Contacts = 'contacts', + Delete = 'delete', Ellipses = 'ellipses', Emoji = 'emoji', Error = 'error', @@ -127,6 +128,12 @@ export const icons = { viewBox: '4 4 7 7', ratio: 1, }, + [SessionIconType.Delete]: { + path: + 'M11.17 37.16h83.48a8.4 8.4 0 012 .16 5.93 5.93 0 012.88 1.56 5.43 5.43 0 011.64 3.34 7.65 7.65 0 01-.06 1.44L94 117.31V117.72a7.06 7.06 0 01-.2.9v.06a5.89 5.89 0 01-5.47 4.07H17.32a6.17 6.17 0 01-1.25-.19 6.17 6.17 0 01-1.16-.48 6.18 6.18 0 01-3.08-4.88l-7-73.49a7.69 7.69 0 01-.06-1.66 5.37 5.37 0 011.63-3.29 6 6 0 013-1.58 8.94 8.94 0 011.79-.13zM5.65 8.8h31.47V6a2.44 2.44 0 010-.27 6 6 0 011.76-4A6 6 0 0143.09 0h19.67a6 6 0 015.7 6v2.8h32.39a4.7 4.7 0 014.31 4.43v10.36a2.59 2.59 0 01-2.59 2.59H2.59A2.59 2.59 0 010 23.62V13.53a1.56 1.56 0 010-.31 4.72 4.72 0 013.88-4.34 10.4 10.4 0 011.77-.08zm42.1 52.7a4.77 4.77 0 019.49 0v37a4.77 4.77 0 01-9.49 0v-37zm23.73-.2a4.58 4.58 0 015-4.06 4.47 4.47 0 014.51 4.46l-2 37a4.57 4.57 0 01-5 4.06 4.47 4.47 0 01-4.51-4.46l2-37zM25 61.7a4.46 4.46 0 014.5-4.46 4.58 4.58 0 015 4.06l2 37a4.47 4.47 0 01-4.51 4.46 4.57 4.57 0 01-5-4.06l-2-37z', + viewBox: '0 0 105.16 122.88', + ratio: 1, + }, [SessionIconType.DoubleCheckCircleFilled]: { path: 'M7.91731278,0.313257194 C6.15053376,1.58392424 5,3.65760134 5,6 C5,6.343797 5.0247846,6.68180525 5.07266453,7.01233547 L5,7.085 L3.205,5.295 L2.5,6 L5,8.5 L5.33970233,8.16029767 C5.80439817,9.59399486 6.71914823,10.8250231 7.91731278,11.6867428 C7.31518343,11.8898758 6.67037399,12 6,12 C2.688,12 0,9.312 0,6 C0,2.688 2.688,0 6,0 C6.67037399,0 7.31518343,0.110124239 7.91731278,0.313257194 Z M12,0 C15.312,0 18,2.688 18,6 C18,9.312 15.312,12 12,12 C8.688,12 6,9.312 6,6 C6,2.688 8.688,0 12,0 Z M11,8.5 L15.5,4 L14.795,3.29 L11,7.085 L9.205,5.295 L8.5,6 L11,8.5 Z', diff --git a/ts/receiver/attachments.ts b/ts/receiver/attachments.ts index a9134a75e..a4b804fb0 100644 --- a/ts/receiver/attachments.ts +++ b/ts/receiver/attachments.ts @@ -171,12 +171,12 @@ async function processPreviews(message: MessageModel, convo: ConversationModel): const image = message.isTrustedForAttachmentDownload() ? await AttachmentDownloads.addJob(item.image, { - messageId: message.id, - type: 'preview', - index, - isOpenGroupV2, - openGroupV2Details, - }) + messageId: message.id, + type: 'preview', + index, + isOpenGroupV2, + openGroupV2Details, + }) : null; return { ...item, image }; diff --git a/ts/state/ducks/SessionTheme.tsx b/ts/state/ducks/SessionTheme.tsx index 1aba14216..587a9f5e3 100644 --- a/ts/state/ducks/SessionTheme.tsx +++ b/ts/state/ducks/SessionTheme.tsx @@ -10,8 +10,14 @@ const warning = '#e7b100'; const destructive = '#ff453a'; const accentLightTheme = '#00e97b'; const accentDarkTheme = '#00f782'; -const borderLightTheme = '#f1f1f1'; -const borderDarkTheme = '#ffffff0F'; +const borderLightThemeColor = '#f1f1f1'; +const borderDarkThemeColor = '#ffffff0F'; +const borderHighContrastLightTheme = '#afafaf'; +const borderHighContrastDarkTheme = '#484848'; + +// const borderAvatarColor = '#00000059'; +// const borderLightTheme = '#f1f1f1'; +// const borderDarkTheme = '#ffffff0F'; const common = { fonts: { @@ -53,6 +59,7 @@ export const lightTheme: DefaultTheme = { textColorSubtleNoOpacity: '#52514f', textColorOpposite: white, textHighlight: `${black}33`, + textAccent: '#00c769', // inbox inboxBackground: white, // buttons @@ -73,9 +80,12 @@ export const lightTheme: DefaultTheme = { conversationItemHasUnread: '#fcfcfc', conversationItemSelected: '#f0f0f0', clickableHovered: '#dfdfdf', - sessionBorder: `1px solid ${borderLightTheme}`, + sessionBorder: `1px solid ${borderLightThemeColor}`, + sessionBorderColor: borderLightThemeColor, + sessionBorderHighContrast: `1px solid ${borderHighContrastLightTheme}`, sessionUnreadBorder: `4px solid ${accentLightTheme}`, leftpaneOverlayBackground: white, + recoveryPhraseBannerBackground: white, // scrollbars scrollBarTrack: '#fcfcfc', scrollBarThumb: '#474646', @@ -109,6 +119,7 @@ export const darkTheme = { textColorSubtleNoOpacity: '#7f7d7d', textColorOpposite: black, textHighlight: `${accentDarkTheme}99`, + textAccent: accentDarkTheme, // inbox inboxBackground: 'linear-gradient(180deg, #171717 0%, #121212 100%)', // buttons @@ -129,9 +140,12 @@ export const darkTheme = { conversationItemHasUnread: '#2c2c2c', conversationItemSelected: '#404040', clickableHovered: '#414347', - sessionBorder: `1px solid ${borderDarkTheme}`, + sessionBorder: `1px solid ${borderDarkThemeColor}`, + sessionBorderColor: borderDarkThemeColor, + sessionBorderHighContrast: `1px solid ${borderHighContrastDarkTheme}`, sessionUnreadBorder: `4px solid ${accentDarkTheme}`, leftpaneOverlayBackground: 'linear-gradient(180deg, #171717 0%, #121212 100%)', + recoveryPhraseBannerBackground: '#1f1f1f', // scrollbars scrollBarTrack: '#1b1b1b', scrollBarThumb: '#474646', diff --git a/ts/state/ducks/userConfig.tsx b/ts/state/ducks/userConfig.tsx index 1c704e2b4..af64b2cb4 100644 --- a/ts/state/ducks/userConfig.tsx +++ b/ts/state/ducks/userConfig.tsx @@ -6,10 +6,12 @@ import { createSlice } from '@reduxjs/toolkit'; export interface UserConfigState { audioAutoplay: boolean; + showRecoveryPhrasePrompt: boolean; } export const initialUserConfigState = { audioAutoplay: false, + showRecoveryPhrasePrompt: true, }; const userConfigSlice = createSlice({ @@ -19,9 +21,12 @@ const userConfigSlice = createSlice({ toggleAudioAutoplay: state => { state.audioAutoplay = !state.audioAutoplay; }, + disableRecoveryPhrasePrompt: state => { + state.showRecoveryPhrasePrompt = false; + }, }, }); const { actions, reducer } = userConfigSlice; -export const { toggleAudioAutoplay } = actions; +export const { toggleAudioAutoplay, disableRecoveryPhrasePrompt } = actions; export const userConfigReducer = reducer; diff --git a/ts/state/selectors/userConfig.ts b/ts/state/selectors/userConfig.ts index 084f8d9ac..9c8641cb2 100644 --- a/ts/state/selectors/userConfig.ts +++ b/ts/state/selectors/userConfig.ts @@ -8,3 +8,8 @@ export const getAudioAutoplay = createSelector( getUserConfig, (state: UserConfigState): boolean => state.audioAutoplay ); + +export const getShowRecoveryPhrasePrompt = createSelector( + getUserConfig, + (state: UserConfigState): boolean => state.showRecoveryPhrasePrompt +); diff --git a/ts/styled.d.ts b/ts/styled.d.ts index 905375e34..953f901d3 100644 --- a/ts/styled.d.ts +++ b/ts/styled.d.ts @@ -39,6 +39,7 @@ declare module 'styled-components' { textColorSubtleNoOpacity: string; textColorOpposite: string; textHighlight: string; + textAccent: string; // inbox inboxBackground: string; // buttons @@ -60,8 +61,11 @@ declare module 'styled-components' { conversationItemSelected: string; clickableHovered: string; sessionBorder: string; + sessionBorderColor: string; + sessionBorderHighContrast: string; sessionUnreadBorder: string; leftpaneOverlayBackground: string; + recoveryPhraseBannerBackground: string; // scrollbars scrollBarTrack: string; scrollBarThumb: string; diff --git a/ts/window.d.ts b/ts/window.d.ts index b540c3d1f..7b7e58120 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -52,6 +52,7 @@ declare global { }; lokiSnodeAPI: LokiSnodeAPI; onLogin: any; + persistStore?: Persistor; resetDatabase: any; restart: any; getSeedNodeList: () => Array | undefined;