Merge pull request #1846 from warrickct/registration-progress-banner

Registration progress banner
pull/1873/head
Audric Ackermann 4 years ago committed by GitHub
commit d32562673d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -428,5 +428,8 @@
"dialogClearAllDataDeletionFailedMultiple": "Data not deleted by those Service Nodes: $snodes$", "dialogClearAllDataDeletionFailedMultiple": "Data not deleted by those Service Nodes: $snodes$",
"dialogClearAllDataDeletionQuestion": "Would you like to clear only this device, or delete your entire account?", "dialogClearAllDataDeletionQuestion": "Would you like to clear only this device, or delete your entire account?",
"deviceOnly": "Device Only", "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"
} }

@ -224,7 +224,6 @@ textarea {
border-radius: 2px; border-radius: 2px;
height: 33px; height: 33px;
padding: 0px 18px; padding: 0px 18px;
// line-height: 33px;
font-size: $session-font-sm; font-size: $session-font-sm;
} }
@ -1248,7 +1247,6 @@ input {
margin: 15px calc(100% / 2 - 1px); margin: 15px calc(100% / 2 - 1px);
width: 1px; width: 1px;
// z-index: -1;
} }
} }

@ -153,6 +153,27 @@ $session-font-h4: 16px;
color: subtle($color); 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; $session-subtle-factor: 0.6;
@function subtle($color) { @function subtle($color) {

@ -180,14 +180,21 @@
border-top: themed('sessionBorder'); border-top: themed('sessionBorder');
} }
& > .session-icon-button { .session-icon-button {
// & > .session-icon-button {
margin-right: $session-margin-sm; margin-right: $session-margin-sm;
} }
.session-icon-button { .session-icon-button {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
opacity: 0.8; opacity: 0.7;
&:hover {
opacity: 1;
transform: scale(0.93);
transition: $session-transition-duration;
}
.send { .send {
padding: $session-margin-xs; 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 { .session-recording {
height: $composition-container-height; height: $composition-container-height;
display: flex; display: flex;
@ -337,19 +364,21 @@
flex-grow: 1; flex-grow: 1;
outline: none; outline: none;
$actions-element-size: 45px; $actions-element-size: 30px;
&--actions { &--actions {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center;
width: $actions-element-size; width: $actions-element-size;
height: $actions-element-size; height: $actions-element-size;
border-radius: 50%; border-radius: 50%;
.session-button {
animation: fadein $session-transition-duration;
}
.session-icon-button { .session-icon-button {
animation: fadein $session-transition-duration; animation: fadein $session-transition-duration;
opacity: 1;
border-radius: 50%; border-radius: 50%;
width: $actions-element-size; width: $actions-element-size;
height: $actions-element-size; height: $actions-element-size;
@ -414,7 +443,11 @@
flex-shrink: 0; flex-shrink: 0;
&.playback-timer { &.playback-timer {
margin-right: $session-margin-sm; animation: fadein $session-transition-duration;
@media (-webkit-min-device-pixel-ratio: 1.6) {
margin-left: auto;
}
} }
&-light { &-light {
@ -422,23 +455,13 @@
width: $session-margin-sm; width: $session-margin-sm;
border-radius: 50%; border-radius: 50%;
background-color: $session-color-danger-alt; 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 */ /* AUDIO PLAYER */
/* ************ */ /* ************ */

@ -11,6 +11,7 @@ import { SessionWrapperModal } from '../session/SessionWrapperModal';
const deleteDbLocally = async () => { const deleteDbLocally = async () => {
window?.log?.info('configuration message sent successfully. Deleting everything'); window?.log?.info('configuration message sent successfully. Deleting everything');
window.persistStore?.purge();
await window.Signal.Logs.deleteAll(); await window.Signal.Logs.deleteAll();
await window.Signal.Data.removeAll(); await window.Signal.Data.removeAll();
await window.Signal.Data.close(); await window.Signal.Data.close();

@ -1,84 +1,55 @@
import React from 'react'; import React, { useEffect, useState } from 'react';
import { SessionButton } from '../session/SessionButton'; import { SessionButton } from '../session/SessionButton';
import { ToastUtils, UserUtils } from '../../session/utils'; import { ToastUtils, UserUtils } from '../../session/utils';
import { withTheme } from 'styled-components';
import { PasswordUtil } from '../../util'; import { PasswordUtil } from '../../util';
import { getPasswordHash } from '../../data/data'; import { getPasswordHash } from '../../data/data';
import { QRCode } from 'react-qr-svg'; import { QRCode } from 'react-qr-svg';
import { mn_decode } from '../../session/crypto/mnemonic'; import { mn_decode } from '../../session/crypto/mnemonic';
import { SessionWrapperModal } from '../session/SessionWrapperModal'; import { SessionWrapperModal } from '../session/SessionWrapperModal';
import { SpacerLG, SpacerSM, SpacerXS } from '../basic/Text'; import { SpacerLG, SpacerSM, SpacerXS } from '../basic/Text';
import autoBind from 'auto-bind';
import { recoveryPhraseModal } from '../../state/ducks/modalDialog'; import { recoveryPhraseModal } from '../../state/ducks/modalDialog';
import { useDispatch } from 'react-redux';
interface State { interface PasswordProps {
error: string; setPasswordValid: (val: boolean) => any;
loadingPassword: boolean;
loadingSeed: boolean;
recoveryPhrase: string;
hasPassword: boolean | null;
passwordHash: string; passwordHash: string;
passwordValid: boolean;
} }
class SessionSeedModalInner extends React.Component<{}, State> { const Password = (props: PasswordProps) => {
constructor(props: any) { const { setPasswordValid, passwordHash } = props;
super(props); const i18n = window.i18n;
const [error, setError] = useState('');
this.state = { const dispatch = useDispatch();
error: '',
loadingPassword: true,
loadingSeed: true,
recoveryPhrase: '',
hasPassword: null,
passwordHash: '',
passwordValid: false,
};
autoBind(this); const onClose = () => dispatch(recoveryPhraseModal(null));
}
public componentDidMount() { const confirmPassword = () => {
setTimeout(() => ($('#seed-input-password') as any).focus(), 100); const passwordValue = jQuery('#seed-input-password').val();
const isPasswordValid = PasswordUtil.matchesHash(passwordValue as string, passwordHash);
void this.checkHasPassword(); if (!passwordValue) {
void this.getRecoveryPhrase(); setError('noGivenPassword');
return false;
} }
public render() { if (passwordHash && !isPasswordValid) {
const i18n = window.i18n; setError('invalidPassword');
return false;
}
const { hasPassword, passwordValid } = this.state; setPasswordValid(true);
const loading = this.state.loadingPassword || this.state.loadingSeed; setError('');
const onClose = () => window.inboxStore?.dispatch(recoveryPhraseModal(null));
return ( window.removeEventListener('keyup', onEnter);
<> return true;
{!loading && ( };
<SessionWrapperModal
title={i18n('showRecoveryPhrase')}
onClose={onClose}
showExitIcon={true}
>
<SpacerSM />
{hasPassword && !passwordValid ? ( const onEnter = (event: any) => {
<>{this.renderPasswordView()}</> if (event.key === 'Enter') {
) : ( confirmPassword();
<>{this.renderSeedView()}</>
)}
</SessionWrapperModal>
)}
</>
);
} }
};
private renderPasswordView() {
const error = this.state.error;
const i18n = window.i18n;
const onClose = () => window.inboxStore?.dispatch(recoveryPhraseModal(null));
return ( return (
<> <>
@ -87,7 +58,7 @@ class SessionSeedModalInner extends React.Component<{}, State> {
type="password" type="password"
id="seed-input-password" id="seed-input-password"
placeholder={i18n('password')} placeholder={i18n('password')}
onKeyUp={this.onEnter} onKeyUp={onEnter}
/> />
{error && ( {error && (
@ -100,20 +71,36 @@ class SessionSeedModalInner extends React.Component<{}, State> {
<SpacerLG /> <SpacerLG />
<div className="session-modal__button-group"> <div className="session-modal__button-group">
<SessionButton text={i18n('ok')} onClick={this.confirmPassword} /> <SessionButton text={i18n('ok')} onClick={confirmPassword} />
<SessionButton text={i18n('cancel')} onClick={onClose} /> <SessionButton text={i18n('cancel')} onClick={onClose} />
</div> </div>
</> </>
); );
};
interface SeedProps {
recoveryPhrase: string;
onClickCopy?: () => any;
} }
private renderSeedView() { const Seed = (props: SeedProps) => {
const { recoveryPhrase, onClickCopy } = props;
const i18n = window.i18n; const i18n = window.i18n;
const bgColor = '#FFFFFF'; const bgColor = '#FFFFFF';
const fgColor = '#1B1B1B'; const fgColor = '#1B1B1B';
const dispatch = useDispatch();
const hexEncodedSeed = mn_decode(this.state.recoveryPhrase, 'english'); const hexEncodedSeed = mn_decode(recoveryPhrase, 'english');
const copyRecoveryPhrase = (recoveryPhraseToCopy: string) => {
window.clipboard.writeText(recoveryPhraseToCopy);
ToastUtils.pushCopiedToClipBoard();
if (onClickCopy) {
onClickCopy();
}
dispatch(recoveryPhraseModal(null));
};
return ( return (
<> <>
@ -121,7 +108,7 @@ class SessionSeedModalInner extends React.Component<{}, State> {
<p className="session-modal__description">{i18n('recoveryPhraseSavePromptMain')}</p> <p className="session-modal__description">{i18n('recoveryPhraseSavePromptMain')}</p>
<SpacerXS /> <SpacerXS />
<i className="session-modal__text-highlight">{this.state.recoveryPhrase}</i> <i className="session-modal__text-highlight">{recoveryPhrase}</i>
</div> </div>
<SpacerLG /> <SpacerLG />
<div className="qr-image"> <div className="qr-image">
@ -132,85 +119,80 @@ class SessionSeedModalInner extends React.Component<{}, State> {
<SessionButton <SessionButton
text={i18n('copy')} text={i18n('copy')}
onClick={() => { onClick={() => {
this.copyRecoveryPhrase(this.state.recoveryPhrase); copyRecoveryPhrase(recoveryPhrase);
}} }}
/> />
</div> </div>
</> </>
); );
} };
private confirmPassword() {
const passwordHash = this.state.passwordHash;
const passwordValue = jQuery('#seed-input-password').val();
const isPasswordValid = PasswordUtil.matchesHash(passwordValue as string, passwordHash);
if (!passwordValue) {
this.setState({
error: window.i18n('noGivenPassword'),
});
return false; interface ModalInnerProps {
onClickOk?: () => any;
} }
if (passwordHash && !isPasswordValid) { const SessionSeedModalInner = (props: ModalInnerProps) => {
this.setState({ const { onClickOk } = props;
error: window.i18n('invalidPassword'), const [loadingPassword, setLoadingPassword] = useState(true);
}); const [loadingSeed, setLoadingSeed] = useState(true);
const [recoveryPhrase, setRecoveryPhrase] = useState('');
const [hasPassword, setHasPassword] = useState<null | boolean>(null);
const [passwordValid, setPasswordValid] = useState(false);
const [passwordHash, setPasswordHash] = useState('');
const dispatch = useDispatch();
return false; useEffect(() => {
} setTimeout(() => ($('#seed-input-password') as any).focus(), 100);
void checkHasPassword();
this.setState({ void getRecoveryPhrase();
passwordValid: true, }, []);
error: '',
});
window.removeEventListener('keyup', this.onEnter); const i18n = window.i18n;
return true; const onClose = () => dispatch(recoveryPhraseModal(null));
}
private async checkHasPassword() { const checkHasPassword = async () => {
if (!this.state.loadingPassword) { if (!loadingPassword) {
return; return;
} }
const hash = await getPasswordHash(); const hash = await getPasswordHash();
this.setState({ setHasPassword(!!hash);
hasPassword: !!hash, setPasswordHash(hash || '');
passwordHash: hash || '', setLoadingPassword(false);
loadingPassword: false, };
});
}
private async getRecoveryPhrase() { const getRecoveryPhrase = async () => {
if (this.state.recoveryPhrase) { if (recoveryPhrase) {
return false; return false;
} }
const newRecoveryPhrase = UserUtils.getCurrentRecoveryPhrase();
const recoveryPhrase = UserUtils.getCurrentRecoveryPhrase(); setRecoveryPhrase(newRecoveryPhrase);
setLoadingSeed(false);
this.setState({
recoveryPhrase,
loadingSeed: false,
});
return true; return true;
} };
private copyRecoveryPhrase(recoveryPhrase: string) {
window.clipboard.writeText(recoveryPhrase);
ToastUtils.pushCopiedToClipBoard(); return (
window.inboxStore?.dispatch(recoveryPhraseModal(null)); <>
} {!loadingSeed && (
<SessionWrapperModal
title={i18n('showRecoveryPhrase')}
onClose={onClose}
showExitIcon={true}
>
<SpacerSM />
private onEnter(event: any) { {hasPassword && !passwordValid ? (
if (event.key === 'Enter') { <Password passwordHash={passwordHash} setPasswordValid={setPasswordValid} />
this.confirmPassword(); ) : (
} <Seed recoveryPhrase={recoveryPhrase} onClickCopy={onClickOk} />
} )}
} </SessionWrapperModal>
)}
:
</>
);
};
export const SessionSeedModal = withTheme(SessionSeedModalInner); export const SessionSeedModal = SessionSeedModalInner;

@ -1,8 +1,13 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { SessionIcon, SessionIconSize, SessionIconType } from './icon'; import { SessionIcon, SessionIconSize, SessionIconType } from './icon';
import { useTheme } from 'styled-components'; import styled, { useTheme } from 'styled-components';
import { SessionButton } from './SessionButton'; 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 = ({ const Tab = ({
isSelected, isSelected,
@ -40,14 +45,15 @@ type Props = {
export const LeftPaneSectionHeader = (props: Props) => { export const LeftPaneSectionHeader = (props: Props) => {
const { label, buttonIcon, buttonClicked } = props; const { label, buttonIcon, buttonClicked } = props;
const theme = useTheme(); const theme = useTheme();
const showRecoveryPhrasePrompt = useSelector(getShowRecoveryPhrasePrompt);
return ( return (
<Flex flexDirection="column">
<div className="module-left-pane__header"> <div className="module-left-pane__header">
{label && <Tab label={label} type={0} isSelected={true} key={label} />} {label && <Tab label={label} type={0} isSelected={true} key={label} />}
{buttonIcon && ( {buttonIcon && (
<SessionButton onClick={buttonClicked} key="compose" theme={theme}> <SessionButton onClick={buttonClicked} key="compose">
<SessionIcon <SessionIcon
iconType={buttonIcon} iconType={buttonIcon}
iconSize={SessionIconSize.Small} iconSize={SessionIconSize.Small}
@ -57,5 +63,101 @@ export const LeftPaneSectionHeader = (props: Props) => {
</SessionButton> </SessionButton>
)} )}
</div> </div>
{showRecoveryPhrasePrompt && <LeftPaneBanner />}
</Flex>
);
};
export const LeftPaneBanner = () => {
const dispatch = useDispatch();
const showRecoveryPhraseModal = () => {
dispatch(
recoveryPhraseModal({
onClickOk: () => {
dispatch(disableRecoveryPhrasePrompt());
},
})
); );
}; };
const BannerInner = () => {
return (
<StyledBannerInner>
<p>{window.i18n('recoveryPhraseRevealMessage')}</p>
<SessionButton
buttonType={SessionButtonType.Default}
text={window.i18n('recoveryPhraseRevealButtonText')}
onClick={showRecoveryPhraseModal}
/>
</StyledBannerInner>
);
};
const theme = useTheme();
return (
<StyledLeftPaneBanner>
<StyledProgressBarContainer>
<StyledProgressBarInner />
</StyledProgressBarContainer>
<StyledBannerTitle>
{window.i18n('recoveryPhraseSecureTitle')} <span>90%</span>
</StyledBannerTitle>
<Flex
flexDirection="column"
justifyContent="space-between"
padding={`${theme.common.margins.sm}`}
>
<BannerInner />
</Flex>
</StyledLeftPaneBanner>
);
};
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};
}
`;

@ -1,7 +1,6 @@
import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { SessionButton, SessionButtonColor, SessionButtonType } from './SessionButton'; import { SessionButton, SessionButtonColor, SessionButtonType } from './SessionButton';
import { SessionIcon, SessionIconSize, SessionIconType } from './icon'; import { SessionIcon, SessionIconSize, SessionIconType } from './icon';
import { SessionSettingCategory } from './settings/SessionSettings'; import { SessionSettingCategory } from './settings/SessionSettings';
import { LeftPaneSectionHeader } from './LeftPaneSectionHeader'; import { LeftPaneSectionHeader } from './LeftPaneSectionHeader';
@ -10,7 +9,6 @@ import { showSettingsSection } from '../../state/ducks/section';
import { getFocusedSettingsSection } from '../../state/selectors/section'; import { getFocusedSettingsSection } from '../../state/selectors/section';
import { getTheme } from '../../state/selectors/theme'; import { getTheme } from '../../state/selectors/theme';
import { recoveryPhraseModal, updateDeleteAccountModal } from '../../state/ducks/modalDialog'; import { recoveryPhraseModal, updateDeleteAccountModal } from '../../state/ducks/modalDialog';
import React from 'react';
const getCategories = () => { const getCategories = () => {
return [ return [

@ -53,6 +53,7 @@ export class SessionInboxView extends React.Component<any, State> {
} }
const persistor = persistStore(this.store); const persistor = persistStore(this.store);
window.persistStore = persistor;
return ( return (
<Provider store={this.store}> <Provider store={this.store}>

@ -3,11 +3,11 @@ import classNames from 'classnames';
import moment from 'moment'; import moment from 'moment';
import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon'; import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../SessionButton';
import { Constants } from '../../../session'; import { Constants } from '../../../session';
import { ToastUtils } from '../../../session/utils'; import { ToastUtils } from '../../../session/utils';
import autoBind from 'auto-bind'; import autoBind from 'auto-bind';
import MicRecorder from 'mic-recorder-to-mp3'; import MicRecorder from 'mic-recorder-to-mp3';
import styled from 'styled-components';
interface Props { interface Props {
onExitVoiceNoteView: any; onExitVoiceNoteView: any;
@ -33,6 +33,24 @@ function getTimestamp(asInt = false) {
return asInt ? Math.floor(timestamp) : timestamp; 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<Props, State> { class SessionRecordingInner extends React.Component<Props, State> {
private recorder: any; private recorder: any;
private audioBlobMp3?: Blob; private audioBlobMp3?: Blob;
@ -77,19 +95,12 @@ class SessionRecordingInner extends React.Component<Props, State> {
// tslint:disable-next-line: cyclomatic-complexity // tslint:disable-next-line: cyclomatic-complexity
public render() { public render() {
const { const { isPlaying, isPaused, isRecording, startTimestamp, nowTimestamp } = this.state;
actionHover,
isPlaying, const hasRecordingAndPaused = !isRecording && !isPlaying;
isPaused, const hasRecording = !!this.audioElement?.duration && this.audioElement?.duration > 0;
isRecording,
startTimestamp,
nowTimestamp,
} = this.state;
const actionStopRecording = actionHover && isRecording;
const actionPlayAudio = !isRecording && !isPlaying;
const actionPauseAudio = !isRecording && !isPaused && isPlaying; 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 recording, we base the time recording on our state values
// if we are playing ( audioElement?.currentTime is !== 0, use that instead) // if we are playing ( audioElement?.currentTime is !== 0, use that instead)
@ -102,6 +113,14 @@ class SessionRecordingInner extends React.Component<Props, State> {
0; 0;
const displayTimeString = moment.utc(displayTimeMs).format('m:ss'); 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; const actionPauseFn = isPlaying ? this.pauseAudio : this.stopRecordingStream;
@ -112,7 +131,8 @@ class SessionRecordingInner extends React.Component<Props, State> {
onMouseEnter={this.handleHoverActions} onMouseEnter={this.handleHoverActions}
onMouseLeave={this.handleUnhoverActions} onMouseLeave={this.handleUnhoverActions}
> >
{actionStopRecording && ( <StyledFlexWrapper flexDirection="row" marginHorizontal={Constants.UI.SPACING.marginXs}>
{isRecording && (
<SessionIconButton <SessionIconButton
iconType={SessionIconType.Pause} iconType={SessionIconType.Pause}
iconSize={SessionIconSize.Medium} iconSize={SessionIconSize.Medium}
@ -127,13 +147,21 @@ class SessionRecordingInner extends React.Component<Props, State> {
onClick={actionPauseFn} onClick={actionPauseFn}
/> />
)} )}
{actionPlayAudio && ( {hasRecordingAndPaused && (
<SessionIconButton <SessionIconButton
iconType={SessionIconType.Play} iconType={SessionIconType.Play}
iconSize={SessionIconSize.Medium} iconSize={SessionIconSize.Medium}
onClick={this.playAudio} onClick={this.playAudio}
/> />
)} )}
{hasRecording && (
<SessionIconButton
iconType={SessionIconType.Delete}
iconSize={SessionIconSize.Medium}
onClick={this.onDeleteVoiceMessage}
/>
)}
</StyledFlexWrapper>
{actionDefault && ( {actionDefault && (
<SessionIconButton <SessionIconButton
@ -143,13 +171,26 @@ class SessionRecordingInner extends React.Component<Props, State> {
)} )}
</div> </div>
{hasRecording && !isRecording ? (
<div className={classNames('session-recording--timer', !isRecording && 'playback-timer')}> <div className={classNames('session-recording--timer', !isRecording && 'playback-timer')}>
{displayTimeString + remainingTimeString}
</div>
) : null}
{isRecording ? (
<div className={classNames('session-recording--timer')}>
{displayTimeString} {displayTimeString}
{isRecording && <div className="session-recording--timer-light" />} <div className="session-recording--timer-light" />
</div> </div>
) : null}
{!isRecording && ( {!isRecording && (
<div className="send-message-button"> <div
className={classNames(
'send-message-button',
hasRecording && 'send-message-button---scale'
)}
>
<SessionIconButton <SessionIconButton
iconType={SessionIconType.Send} iconType={SessionIconType.Send}
iconSize={SessionIconSize.Large} iconSize={SessionIconSize.Large}
@ -158,23 +199,6 @@ class SessionRecordingInner extends React.Component<Props, State> {
/> />
</div> </div>
)} )}
<div className="session-recording--status">
{isRecording ? (
<SessionButton
text={window.i18n('recording')}
buttonType={SessionButtonType.Brand}
buttonColor={SessionButtonColor.Primary}
/>
) : (
<SessionButton
text={window.i18n('delete')}
buttonType={SessionButtonType.Brand}
buttonColor={SessionButtonColor.DangerAlt}
onClick={this.onDeleteVoiceMessage}
/>
)}
</div>
</div> </div>
); );
} }
@ -271,6 +295,9 @@ class SessionRecordingInner extends React.Component<Props, State> {
this.props.onExitVoiceNoteView(); this.props.onExitVoiceNoteView();
} }
/**
* Sends the recorded voice message
*/
private async onSendVoiceMessage() { private async onSendVoiceMessage() {
if (!this.audioBlobMp3 || !this.audioBlobMp3.size) { if (!this.audioBlobMp3 || !this.audioBlobMp3.size) {
window?.log?.info('Empty audio blob'); window?.log?.info('Empty audio blob');
@ -305,6 +332,9 @@ class SessionRecordingInner extends React.Component<Props, State> {
}); });
} }
/**
* Stops recording audio, sets recording state to stopped.
*/
private async stopRecordingStream() { private async stopRecordingStream() {
if (!this.recorder) { if (!this.recorder) {
return; return;
@ -313,10 +343,39 @@ class SessionRecordingInner extends React.Component<Props, State> {
this.recorder = undefined; this.recorder = undefined;
this.audioBlobMp3 = blob; this.audioBlobMp3 = blob;
this.updateAudioElementAndDuration();
// Stop recording // Stop recording
this.stopRecordingState(); 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) { private async onKeyDown(event: any) {
if (event.key === 'Escape') { if (event.key === 'Escape') {
await this.onDeleteVoiceMessage(); await this.onDeleteVoiceMessage();

@ -12,6 +12,7 @@ export enum SessionIconType {
CirclePlus = 'circlePlus', CirclePlus = 'circlePlus',
CircleElipses = 'circleElipses', CircleElipses = 'circleElipses',
Contacts = 'contacts', Contacts = 'contacts',
Delete = 'delete',
Ellipses = 'ellipses', Ellipses = 'ellipses',
Emoji = 'emoji', Emoji = 'emoji',
Error = 'error', Error = 'error',
@ -127,6 +128,12 @@ export const icons = {
viewBox: '4 4 7 7', viewBox: '4 4 7 7',
ratio: 1, 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]: { [SessionIconType.DoubleCheckCircleFilled]: {
path: 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', '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',

@ -10,8 +10,14 @@ const warning = '#e7b100';
const destructive = '#ff453a'; const destructive = '#ff453a';
const accentLightTheme = '#00e97b'; const accentLightTheme = '#00e97b';
const accentDarkTheme = '#00f782'; const accentDarkTheme = '#00f782';
const borderLightTheme = '#f1f1f1'; const borderLightThemeColor = '#f1f1f1';
const borderDarkTheme = '#ffffff0F'; const borderDarkThemeColor = '#ffffff0F';
const borderHighContrastLightTheme = '#afafaf';
const borderHighContrastDarkTheme = '#484848';
// const borderAvatarColor = '#00000059';
// const borderLightTheme = '#f1f1f1';
// const borderDarkTheme = '#ffffff0F';
const common = { const common = {
fonts: { fonts: {
@ -53,6 +59,7 @@ export const lightTheme: DefaultTheme = {
textColorSubtleNoOpacity: '#52514f', textColorSubtleNoOpacity: '#52514f',
textColorOpposite: white, textColorOpposite: white,
textHighlight: `${black}33`, textHighlight: `${black}33`,
textAccent: '#00c769',
// inbox // inbox
inboxBackground: white, inboxBackground: white,
// buttons // buttons
@ -73,9 +80,12 @@ export const lightTheme: DefaultTheme = {
conversationItemHasUnread: '#fcfcfc', conversationItemHasUnread: '#fcfcfc',
conversationItemSelected: '#f0f0f0', conversationItemSelected: '#f0f0f0',
clickableHovered: '#dfdfdf', clickableHovered: '#dfdfdf',
sessionBorder: `1px solid ${borderLightTheme}`, sessionBorder: `1px solid ${borderLightThemeColor}`,
sessionBorderColor: borderLightThemeColor,
sessionBorderHighContrast: `1px solid ${borderHighContrastLightTheme}`,
sessionUnreadBorder: `4px solid ${accentLightTheme}`, sessionUnreadBorder: `4px solid ${accentLightTheme}`,
leftpaneOverlayBackground: white, leftpaneOverlayBackground: white,
recoveryPhraseBannerBackground: white,
// scrollbars // scrollbars
scrollBarTrack: '#fcfcfc', scrollBarTrack: '#fcfcfc',
scrollBarThumb: '#474646', scrollBarThumb: '#474646',
@ -109,6 +119,7 @@ export const darkTheme = {
textColorSubtleNoOpacity: '#7f7d7d', textColorSubtleNoOpacity: '#7f7d7d',
textColorOpposite: black, textColorOpposite: black,
textHighlight: `${accentDarkTheme}99`, textHighlight: `${accentDarkTheme}99`,
textAccent: accentDarkTheme,
// inbox // inbox
inboxBackground: 'linear-gradient(180deg, #171717 0%, #121212 100%)', inboxBackground: 'linear-gradient(180deg, #171717 0%, #121212 100%)',
// buttons // buttons
@ -129,9 +140,12 @@ export const darkTheme = {
conversationItemHasUnread: '#2c2c2c', conversationItemHasUnread: '#2c2c2c',
conversationItemSelected: '#404040', conversationItemSelected: '#404040',
clickableHovered: '#414347', clickableHovered: '#414347',
sessionBorder: `1px solid ${borderDarkTheme}`, sessionBorder: `1px solid ${borderDarkThemeColor}`,
sessionBorderColor: borderDarkThemeColor,
sessionBorderHighContrast: `1px solid ${borderHighContrastDarkTheme}`,
sessionUnreadBorder: `4px solid ${accentDarkTheme}`, sessionUnreadBorder: `4px solid ${accentDarkTheme}`,
leftpaneOverlayBackground: 'linear-gradient(180deg, #171717 0%, #121212 100%)', leftpaneOverlayBackground: 'linear-gradient(180deg, #171717 0%, #121212 100%)',
recoveryPhraseBannerBackground: '#1f1f1f',
// scrollbars // scrollbars
scrollBarTrack: '#1b1b1b', scrollBarTrack: '#1b1b1b',
scrollBarThumb: '#474646', scrollBarThumb: '#474646',

@ -6,10 +6,12 @@ import { createSlice } from '@reduxjs/toolkit';
export interface UserConfigState { export interface UserConfigState {
audioAutoplay: boolean; audioAutoplay: boolean;
showRecoveryPhrasePrompt: boolean;
} }
export const initialUserConfigState = { export const initialUserConfigState = {
audioAutoplay: false, audioAutoplay: false,
showRecoveryPhrasePrompt: true,
}; };
const userConfigSlice = createSlice({ const userConfigSlice = createSlice({
@ -19,9 +21,12 @@ const userConfigSlice = createSlice({
toggleAudioAutoplay: state => { toggleAudioAutoplay: state => {
state.audioAutoplay = !state.audioAutoplay; state.audioAutoplay = !state.audioAutoplay;
}, },
disableRecoveryPhrasePrompt: state => {
state.showRecoveryPhrasePrompt = false;
},
}, },
}); });
const { actions, reducer } = userConfigSlice; const { actions, reducer } = userConfigSlice;
export const { toggleAudioAutoplay } = actions; export const { toggleAudioAutoplay, disableRecoveryPhrasePrompt } = actions;
export const userConfigReducer = reducer; export const userConfigReducer = reducer;

@ -8,3 +8,8 @@ export const getAudioAutoplay = createSelector(
getUserConfig, getUserConfig,
(state: UserConfigState): boolean => state.audioAutoplay (state: UserConfigState): boolean => state.audioAutoplay
); );
export const getShowRecoveryPhrasePrompt = createSelector(
getUserConfig,
(state: UserConfigState): boolean => state.showRecoveryPhrasePrompt
);

4
ts/styled.d.ts vendored

@ -39,6 +39,7 @@ declare module 'styled-components' {
textColorSubtleNoOpacity: string; textColorSubtleNoOpacity: string;
textColorOpposite: string; textColorOpposite: string;
textHighlight: string; textHighlight: string;
textAccent: string;
// inbox // inbox
inboxBackground: string; inboxBackground: string;
// buttons // buttons
@ -60,8 +61,11 @@ declare module 'styled-components' {
conversationItemSelected: string; conversationItemSelected: string;
clickableHovered: string; clickableHovered: string;
sessionBorder: string; sessionBorder: string;
sessionBorderColor: string;
sessionBorderHighContrast: string;
sessionUnreadBorder: string; sessionUnreadBorder: string;
leftpaneOverlayBackground: string; leftpaneOverlayBackground: string;
recoveryPhraseBannerBackground: string;
// scrollbars // scrollbars
scrollBarTrack: string; scrollBarTrack: string;
scrollBarThumb: string; scrollBarThumb: string;

1
ts/window.d.ts vendored

@ -52,6 +52,7 @@ declare global {
}; };
lokiSnodeAPI: LokiSnodeAPI; lokiSnodeAPI: LokiSnodeAPI;
onLogin: any; onLogin: any;
persistStore?: Persistor;
resetDatabase: any; resetDatabase: any;
restart: any; restart: any;
getSeedNodeList: () => Array<any> | undefined; getSeedNodeList: () => Array<any> | undefined;

Loading…
Cancel
Save