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$",
"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"
}

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

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

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

@ -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();

@ -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 && (
<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}
/>
if (passwordHash && !isPasswordValid) {
setError('invalidPassword');
return false;
}
{error && (
<>
<SpacerXS />
<div className="session-label danger">{error}</div>
</>
)}
setPasswordValid(true);
setError('');
<SpacerLG />
window.removeEventListener('keyup', onEnter);
return true;
};
<div className="session-modal__button-group">
<SessionButton text={i18n('ok')} onClick={this.confirmPassword} />
const onEnter = (event: any) => {
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} />
</div>
</>
);
}
<SpacerLG />
private renderSeedView() {
const i18n = window.i18n;
const bgColor = '#FFFFFF';
const fgColor = '#1B1B1B';
<div className="session-modal__button-group">
<SessionButton text={i18n('ok')} onClick={confirmPassword} />
const hexEncodedSeed = mn_decode(this.state.recoveryPhrase, 'english');
<SessionButton text={i18n('cancel')} onClick={onClose} />
</div>
</>
);
};
return (
<>
<div className="session-modal__centered text-center">
<p className="session-modal__description">{i18n('recoveryPhraseSavePromptMain')}</p>
<SpacerXS />
interface SeedProps {
recoveryPhrase: string;
onClickCopy?: () => any;
}
<i className="session-modal__text-highlight">{this.state.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={() => {
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);
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 (
<>
<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) {
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 | 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() {
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 && (
<SessionWrapperModal
title={i18n('showRecoveryPhrase')}
onClose={onClose}
showExitIcon={true}
>
<SpacerSM />
{hasPassword && !passwordValid ? (
<Password passwordHash={passwordHash} setPasswordValid={setPasswordValid} />
) : (
<Seed recoveryPhrase={recoveryPhrase} onClickCopy={onClickOk} />
)}
</SessionWrapperModal>
)}
:
</>
);
};
export const SessionSeedModal = SessionSeedModalInner;

@ -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 (
<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();
return (
<div className="module-left-pane__header">
{label && <Tab label={label} type={0} isSelected={true} key={label} />}
{buttonIcon && (
<SessionButton onClick={buttonClicked} key="compose" theme={theme}>
<SessionIcon
iconType={buttonIcon}
iconSize={SessionIconSize.Small}
iconColor="white"
theme={theme}
/>
</SessionButton>
)}
</div>
<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 { 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 [

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

@ -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<Props, State> {
private recorder: any;
private audioBlobMp3?: Blob;
@ -77,19 +95,12 @@ class SessionRecordingInner extends React.Component<Props, State> {
// 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<Props, State> {
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<Props, State> {
onMouseEnter={this.handleHoverActions}
onMouseLeave={this.handleUnhoverActions}
>
{actionStopRecording && (
<SessionIconButton
iconType={SessionIconType.Pause}
iconSize={SessionIconSize.Medium}
iconColor={Constants.UI.COLORS.DANGER_ALT}
onClick={actionPauseFn}
/>
)}
{actionPauseAudio && (
<SessionIconButton
iconType={SessionIconType.Pause}
iconSize={SessionIconSize.Medium}
onClick={actionPauseFn}
/>
)}
{actionPlayAudio && (
<SessionIconButton
iconType={SessionIconType.Play}
iconSize={SessionIconSize.Medium}
onClick={this.playAudio}
/>
)}
<StyledFlexWrapper flexDirection="row" marginHorizontal={Constants.UI.SPACING.marginXs}>
{isRecording && (
<SessionIconButton
iconType={SessionIconType.Pause}
iconSize={SessionIconSize.Medium}
iconColor={Constants.UI.COLORS.DANGER_ALT}
onClick={actionPauseFn}
/>
)}
{actionPauseAudio && (
<SessionIconButton
iconType={SessionIconType.Pause}
iconSize={SessionIconSize.Medium}
onClick={actionPauseFn}
/>
)}
{hasRecordingAndPaused && (
<SessionIconButton
iconType={SessionIconType.Play}
iconSize={SessionIconSize.Medium}
onClick={this.playAudio}
/>
)}
{hasRecording && (
<SessionIconButton
iconType={SessionIconType.Delete}
iconSize={SessionIconSize.Medium}
onClick={this.onDeleteVoiceMessage}
/>
)}
</StyledFlexWrapper>
{actionDefault && (
<SessionIconButton
@ -143,13 +171,26 @@ class SessionRecordingInner extends React.Component<Props, State> {
)}
</div>
<div className={classNames('session-recording--timer', !isRecording && 'playback-timer')}>
{displayTimeString}
{isRecording && <div className="session-recording--timer-light" />}
</div>
{hasRecording && !isRecording ? (
<div className={classNames('session-recording--timer', !isRecording && 'playback-timer')}>
{displayTimeString + remainingTimeString}
</div>
) : null}
{isRecording ? (
<div className={classNames('session-recording--timer')}>
{displayTimeString}
<div className="session-recording--timer-light" />
</div>
) : null}
{!isRecording && (
<div className="send-message-button">
<div
className={classNames(
'send-message-button',
hasRecording && 'send-message-button---scale'
)}
>
<SessionIconButton
iconType={SessionIconType.Send}
iconSize={SessionIconSize.Large}
@ -158,23 +199,6 @@ class SessionRecordingInner extends React.Component<Props, State> {
/>
</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>
);
}
@ -271,6 +295,9 @@ class SessionRecordingInner extends React.Component<Props, State> {
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<Props, State> {
});
}
/**
* Stops recording audio, sets recording state to stopped.
*/
private async stopRecordingStream() {
if (!this.recorder) {
return;
@ -313,10 +343,39 @@ class SessionRecordingInner extends React.Component<Props, State> {
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();

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

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

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

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

@ -8,3 +8,8 @@ export const getAudioAutoplay = createSelector(
getUserConfig,
(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;
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;

1
ts/window.d.ts vendored

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

Loading…
Cancel
Save