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,216 +1,198 @@
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('');
const dispatch = useDispatch();
this.state = { const onClose = () => dispatch(recoveryPhraseModal(null));
error: '',
loadingPassword: true,
loadingSeed: true,
recoveryPhrase: '',
hasPassword: null,
passwordHash: '',
passwordValid: false,
};
autoBind(this); const confirmPassword = () => {
} const passwordValue = jQuery('#seed-input-password').val();
const isPasswordValid = PasswordUtil.matchesHash(passwordValue as string, passwordHash);
public componentDidMount() { if (!passwordValue) {
setTimeout(() => ($('#seed-input-password') as any).focus(), 100); setError('noGivenPassword');
return false;
}
void this.checkHasPassword(); if (passwordHash && !isPasswordValid) {
void this.getRecoveryPhrase(); setError('invalidPassword');
} return false;
}
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 && (
<SessionWrapperModal
title={i18n('showRecoveryPhrase')}
onClose={onClose}
showExitIcon={true}
>
<SpacerSM />
{hasPassword && !passwordValid ? (
<>{this.renderPasswordView()}</>
) : (
<>{this.renderSeedView()}</>
)}
</SessionWrapperModal>
)}
</>
);
}
private renderPasswordView() {
const error = this.state.error;
const i18n = window.i18n;
const onClose = () => window.inboxStore?.dispatch(recoveryPhraseModal(null));
return (
<>
<p>{i18n('showRecoveryPhrasePasswordRequest')}</p>
<input
type="password"
id="seed-input-password"
placeholder={i18n('password')}
onKeyUp={this.onEnter}
/>
{error && ( setPasswordValid(true);
<> setError('');
<SpacerXS />
<div className="session-label danger">{error}</div>
</>
)}
<SpacerLG /> window.removeEventListener('keyup', onEnter);
return true;
};
<div className="session-modal__button-group"> const onEnter = (event: any) => {
<SessionButton text={i18n('ok')} onClick={this.confirmPassword} /> if (event.key === 'Enter') {
confirmPassword();
}
};
return (
<>
<p>{i18n('showRecoveryPhrasePasswordRequest')}</p>
<input
type="password"
id="seed-input-password"
placeholder={i18n('password')}
onKeyUp={onEnter}
/>
{error && (
<>
<SpacerXS />
<div className="session-label danger">{error}</div>
</>
)}
<SessionButton text={i18n('cancel')} onClick={onClose} /> <SpacerLG />
</div>
</>
);
}
private renderSeedView() { <div className="session-modal__button-group">
const i18n = window.i18n; <SessionButton text={i18n('ok')} onClick={confirmPassword} />
const bgColor = '#FFFFFF';
const fgColor = '#1B1B1B';
const hexEncodedSeed = mn_decode(this.state.recoveryPhrase, 'english'); <SessionButton text={i18n('cancel')} onClick={onClose} />
</div>
</>
);
};
return ( interface SeedProps {
<> recoveryPhrase: string;
<div className="session-modal__centered text-center"> onClickCopy?: () => any;
<p className="session-modal__description">{i18n('recoveryPhraseSavePromptMain')}</p> }
<SpacerXS />
<i className="session-modal__text-highlight">{this.state.recoveryPhrase}</i> const Seed = (props: SeedProps) => {
</div> const { recoveryPhrase, onClickCopy } = props;
<SpacerLG /> const i18n = window.i18n;
<div className="qr-image"> const bgColor = '#FFFFFF';
<QRCode value={hexEncodedSeed} bgColor={bgColor} fgColor={fgColor} level="L" /> const fgColor = '#1B1B1B';
</div> const dispatch = useDispatch();
<SpacerLG />
<div className="session-modal__button-group">
<SessionButton
text={i18n('copy')}
onClick={() => {
this.copyRecoveryPhrase(this.state.recoveryPhrase);
}}
/>
</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) { const hexEncodedSeed = mn_decode(recoveryPhrase, 'english');
this.setState({
error: window.i18n('noGivenPassword'),
});
return false; const copyRecoveryPhrase = (recoveryPhraseToCopy: string) => {
window.clipboard.writeText(recoveryPhraseToCopy);
ToastUtils.pushCopiedToClipBoard();
if (onClickCopy) {
onClickCopy();
} }
dispatch(recoveryPhraseModal(null));
};
return (
<>
<div className="session-modal__centered text-center">
<p className="session-modal__description">{i18n('recoveryPhraseSavePromptMain')}</p>
<SpacerXS />
<i className="session-modal__text-highlight">{recoveryPhrase}</i>
</div>
<SpacerLG />
<div className="qr-image">
<QRCode value={hexEncodedSeed} bgColor={bgColor} fgColor={fgColor} level="L" />
</div>
<SpacerLG />
<div className="session-modal__button-group">
<SessionButton
text={i18n('copy')}
onClick={() => {
copyRecoveryPhrase(recoveryPhrase);
}}
/>
</div>
</>
);
};
if (passwordHash && !isPasswordValid) { interface ModalInnerProps {
this.setState({ onClickOk?: () => any;
error: window.i18n('invalidPassword'), }
});
return false;
}
this.setState({ const SessionSeedModalInner = (props: ModalInnerProps) => {
passwordValid: true, const { onClickOk } = props;
error: '', 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();
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() { 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, };
});
} const getRecoveryPhrase = async () => {
if (recoveryPhrase) {
private async getRecoveryPhrase() {
if (this.state.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) { return (
window.clipboard.writeText(recoveryPhrase); <>
{!loadingSeed && (
ToastUtils.pushCopiedToClipBoard(); <SessionWrapperModal
window.inboxStore?.dispatch(recoveryPhraseModal(null)); title={i18n('showRecoveryPhrase')}
} onClose={onClose}
showExitIcon={true}
private onEnter(event: any) { >
if (event.key === 'Enter') { <SpacerSM />
this.confirmPassword();
} {hasPassword && !passwordValid ? (
} <Password passwordHash={passwordHash} setPasswordValid={setPasswordValid} />
} ) : (
<Seed recoveryPhrase={recoveryPhrase} onClickCopy={onClickOk} />
export const SessionSeedModal = withTheme(SessionSeedModalInner); )}
</SessionWrapperModal>
)}
:
</>
);
};
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,22 +45,119 @@ 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 showRecoveryPhrasePrompt = useSelector(getShowRecoveryPhrasePrompt);
return (
<Flex flexDirection="column">
<div className="module-left-pane__header">
{label && <Tab label={label} type={0} isSelected={true} key={label} />}
{buttonIcon && (
<SessionButton onClick={buttonClicked} key="compose">
<SessionIcon
iconType={buttonIcon}
iconSize={SessionIconSize.Small}
iconColor="white"
theme={theme}
/>
</SessionButton>
)}
</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(); const theme = useTheme();
return ( return (
<div className="module-left-pane__header"> <StyledLeftPaneBanner>
{label && <Tab label={label} type={0} isSelected={true} key={label} />} <StyledProgressBarContainer>
{buttonIcon && ( <StyledProgressBarInner />
<SessionButton onClick={buttonClicked} key="compose" theme={theme}> </StyledProgressBarContainer>
<SessionIcon <StyledBannerTitle>
iconType={buttonIcon} {window.i18n('recoveryPhraseSecureTitle')} <span>90%</span>
iconSize={SessionIconSize.Small} </StyledBannerTitle>
iconColor="white" <Flex
theme={theme} flexDirection="column"
/> justifyContent="space-between"
</SessionButton> padding={`${theme.common.margins.sm}`}
)} >
</div> <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,28 +131,37 @@ 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}>
<SessionIconButton {isRecording && (
iconType={SessionIconType.Pause} <SessionIconButton
iconSize={SessionIconSize.Medium} iconType={SessionIconType.Pause}
iconColor={Constants.UI.COLORS.DANGER_ALT} iconSize={SessionIconSize.Medium}
onClick={actionPauseFn} iconColor={Constants.UI.COLORS.DANGER_ALT}
/> onClick={actionPauseFn}
)} />
{actionPauseAudio && ( )}
<SessionIconButton {actionPauseAudio && (
iconType={SessionIconType.Pause} <SessionIconButton
iconSize={SessionIconSize.Medium} iconType={SessionIconType.Pause}
onClick={actionPauseFn} iconSize={SessionIconSize.Medium}
/> onClick={actionPauseFn}
)} />
{actionPlayAudio && ( )}
<SessionIconButton {hasRecordingAndPaused && (
iconType={SessionIconType.Play} <SessionIconButton
iconSize={SessionIconSize.Medium} iconType={SessionIconType.Play}
onClick={this.playAudio} iconSize={SessionIconSize.Medium}
/> 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>
<div className={classNames('session-recording--timer', !isRecording && 'playback-timer')}> {hasRecording && !isRecording ? (
{displayTimeString} <div className={classNames('session-recording--timer', !isRecording && 'playback-timer')}>
{isRecording && <div className="session-recording--timer-light" />} {displayTimeString + remainingTimeString}
</div> </div>
) : null}
{isRecording ? (
<div className={classNames('session-recording--timer')}>
{displayTimeString}
<div className="session-recording--timer-light" />
</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',

@ -171,12 +171,12 @@ async function processPreviews(message: MessageModel, convo: ConversationModel):
const image = message.isTrustedForAttachmentDownload() const image = message.isTrustedForAttachmentDownload()
? await AttachmentDownloads.addJob(item.image, { ? await AttachmentDownloads.addJob(item.image, {
messageId: message.id, messageId: message.id,
type: 'preview', type: 'preview',
index, index,
isOpenGroupV2, isOpenGroupV2,
openGroupV2Details, openGroupV2Details,
}) })
: null; : null;
return { ...item, image }; return { ...item, image };

@ -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