diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index 96b095016..231c5f8e9 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -428,5 +428,8 @@
"dialogClearAllDataDeletionFailedMultiple": "Data not deleted by those Service Nodes: $snodes$",
"dialogClearAllDataDeletionQuestion": "Would you like to clear only this device, or delete your entire account?",
"deviceOnly": "Device Only",
- "entireAccount": "Entire Account"
+ "entireAccount": "Entire Account",
+ "recoveryPhraseSecureTitle": "You're almost finished!",
+ "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.",
+ "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase"
}
diff --git a/stylesheets/_session.scss b/stylesheets/_session.scss
index 11e6ffa17..4385acff8 100644
--- a/stylesheets/_session.scss
+++ b/stylesheets/_session.scss
@@ -224,7 +224,6 @@ textarea {
border-radius: 2px;
height: 33px;
padding: 0px 18px;
- // line-height: 33px;
font-size: $session-font-sm;
}
@@ -1248,7 +1247,6 @@ input {
margin: 15px calc(100% / 2 - 1px);
width: 1px;
- // z-index: -1;
}
}
diff --git a/stylesheets/_session_constants.scss b/stylesheets/_session_constants.scss
index 364edc36e..725ed6ce5 100644
--- a/stylesheets/_session_constants.scss
+++ b/stylesheets/_session_constants.scss
@@ -153,6 +153,27 @@ $session-font-h4: 16px;
color: subtle($color);
}
+@mixin pulse-color($color, $duration, $repetition) {
+ animation: pulseColor $duration $repetition;
+
+ @keyframes pulseColor {
+ 0% {
+ transform: scale(0.95);
+ box-shadow: 0 0 0 0 rgba($color, 0.7);
+ }
+
+ 70% {
+ transform: scale(1);
+ box-shadow: 0 0 0 10px rgba($color, 0);
+ }
+
+ 100% {
+ transform: scale(0.95);
+ box-shadow: 0 0 0 0 rgba($color, 0);
+ }
+ }
+}
+
$session-subtle-factor: 0.6;
@function subtle($color) {
diff --git a/stylesheets/_session_conversation.scss b/stylesheets/_session_conversation.scss
index 75011e6c3..0a85590c0 100644
--- a/stylesheets/_session_conversation.scss
+++ b/stylesheets/_session_conversation.scss
@@ -180,14 +180,21 @@
border-top: themed('sessionBorder');
}
- & > .session-icon-button {
+ .session-icon-button {
+ // & > .session-icon-button {
margin-right: $session-margin-sm;
}
.session-icon-button {
display: flex;
justify-content: center;
align-items: center;
- opacity: 0.8;
+ opacity: 0.7;
+
+ &:hover {
+ opacity: 1;
+ transform: scale(0.93);
+ transition: $session-transition-duration;
+ }
.send {
padding: $session-margin-xs;
@@ -329,6 +336,26 @@
}
}
+.send-message-button {
+ animation: fadein $session-transition-duration;
+
+ &---scale {
+ animation: scaling 2s ease-in-out;
+
+ @keyframes scaling {
+ 0% {
+ transform: scale(1);
+ }
+ 80% {
+ transform: scale(1.3);
+ }
+ 100% {
+ transform: scale(1);
+ }
+ }
+ }
+}
+
.session-recording {
height: $composition-container-height;
display: flex;
@@ -337,19 +364,21 @@
flex-grow: 1;
outline: none;
- $actions-element-size: 45px;
+ $actions-element-size: 30px;
&--actions {
display: flex;
align-items: center;
- justify-content: center;
width: $actions-element-size;
height: $actions-element-size;
border-radius: 50%;
+ .session-button {
+ animation: fadein $session-transition-duration;
+ }
+
.session-icon-button {
animation: fadein $session-transition-duration;
- opacity: 1;
border-radius: 50%;
width: $actions-element-size;
height: $actions-element-size;
@@ -414,7 +443,11 @@
flex-shrink: 0;
&.playback-timer {
- margin-right: $session-margin-sm;
+ animation: fadein $session-transition-duration;
+
+ @media (-webkit-min-device-pixel-ratio: 1.6) {
+ margin-left: auto;
+ }
}
&-light {
@@ -422,23 +455,13 @@
width: $session-margin-sm;
border-radius: 50%;
background-color: $session-color-danger-alt;
- margin-left: $session-margin-sm;
+ margin: 0 $session-margin-sm;
- animation: pulseLight 4s infinite;
+ @include pulse-color($session-color-danger-alt, 1s, infinite);
}
}
}
-// box-sizing: border-box;
-// position: absolute;
-// z-index: 3;
-// width: 4px;
-// height: 5px;
-// margin-left: 0px;
-// top: 0px;
-// background: #00f782;
-// border-radius: 50px;
-
/* ************ */
/* AUDIO PLAYER */
/* ************ */
diff --git a/ts/components/dialog/DeleteAccountModal.tsx b/ts/components/dialog/DeleteAccountModal.tsx
index b0e948019..40c4226f5 100644
--- a/ts/components/dialog/DeleteAccountModal.tsx
+++ b/ts/components/dialog/DeleteAccountModal.tsx
@@ -11,6 +11,7 @@ import { SessionWrapperModal } from '../session/SessionWrapperModal';
const deleteDbLocally = async () => {
window?.log?.info('configuration message sent successfully. Deleting everything');
+ window.persistStore?.purge();
await window.Signal.Logs.deleteAll();
await window.Signal.Data.removeAll();
await window.Signal.Data.close();
diff --git a/ts/components/dialog/SessionSeedModal.tsx b/ts/components/dialog/SessionSeedModal.tsx
index 3a48bac69..3e61e716d 100644
--- a/ts/components/dialog/SessionSeedModal.tsx
+++ b/ts/components/dialog/SessionSeedModal.tsx
@@ -1,216 +1,198 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
import { SessionButton } from '../session/SessionButton';
import { ToastUtils, UserUtils } from '../../session/utils';
-import { withTheme } from 'styled-components';
import { PasswordUtil } from '../../util';
import { getPasswordHash } from '../../data/data';
import { QRCode } from 'react-qr-svg';
import { mn_decode } from '../../session/crypto/mnemonic';
import { SessionWrapperModal } from '../session/SessionWrapperModal';
import { SpacerLG, SpacerSM, SpacerXS } from '../basic/Text';
-import autoBind from 'auto-bind';
import { recoveryPhraseModal } from '../../state/ducks/modalDialog';
+import { useDispatch } from 'react-redux';
-interface State {
- error: string;
- loadingPassword: boolean;
- loadingSeed: boolean;
- recoveryPhrase: string;
- hasPassword: boolean | null;
+interface PasswordProps {
+ setPasswordValid: (val: boolean) => any;
passwordHash: string;
- passwordValid: boolean;
}
-class SessionSeedModalInner extends React.Component<{}, State> {
- constructor(props: any) {
- super(props);
+const Password = (props: PasswordProps) => {
+ const { setPasswordValid, passwordHash } = props;
+ const i18n = window.i18n;
+ const [error, setError] = useState('');
+ const dispatch = useDispatch();
- this.state = {
- error: '',
- loadingPassword: true,
- loadingSeed: true,
- recoveryPhrase: '',
- hasPassword: null,
- passwordHash: '',
- passwordValid: false,
- };
+ const onClose = () => dispatch(recoveryPhraseModal(null));
- autoBind(this);
- }
+ const confirmPassword = () => {
+ const passwordValue = jQuery('#seed-input-password').val();
+ const isPasswordValid = PasswordUtil.matchesHash(passwordValue as string, passwordHash);
- public componentDidMount() {
- setTimeout(() => ($('#seed-input-password') as any).focus(), 100);
+ if (!passwordValue) {
+ setError('noGivenPassword');
+ return false;
+ }
- void this.checkHasPassword();
- void this.getRecoveryPhrase();
- }
-
- public render() {
- const i18n = window.i18n;
-
- const { hasPassword, passwordValid } = this.state;
- const loading = this.state.loadingPassword || this.state.loadingSeed;
- const onClose = () => window.inboxStore?.dispatch(recoveryPhraseModal(null));
-
- return (
- <>
- {!loading && (
-
-
-
- {hasPassword && !passwordValid ? (
- <>{this.renderPasswordView()}>
- ) : (
- <>{this.renderSeedView()}>
- )}
-
- )}
- >
- );
- }
-
- private renderPasswordView() {
- const error = this.state.error;
- const i18n = window.i18n;
-
- const onClose = () => window.inboxStore?.dispatch(recoveryPhraseModal(null));
-
- return (
- <>
-
{i18n('showRecoveryPhrasePasswordRequest')}
-
+ if (passwordHash && !isPasswordValid) {
+ setError('invalidPassword');
+ return false;
+ }
- {error && (
- <>
-
- {error}
- >
- )}
+ setPasswordValid(true);
+ setError('');
-
+ window.removeEventListener('keyup', onEnter);
+ return true;
+ };
-
-
+ const onEnter = (event: any) => {
+ if (event.key === 'Enter') {
+ confirmPassword();
+ }
+ };
+
+ return (
+ <>
+
{i18n('showRecoveryPhrasePasswordRequest')}
+
+
+ {error && (
+ <>
+
+
{error}
+ >
+ )}
-
-
- >
- );
- }
+
- private renderSeedView() {
- const i18n = window.i18n;
- const bgColor = '#FFFFFF';
- const fgColor = '#1B1B1B';
+
+
- const hexEncodedSeed = mn_decode(this.state.recoveryPhrase, 'english');
+
+
+ >
+ );
+};
- return (
- <>
-
-
{i18n('recoveryPhraseSavePromptMain')}
-
+interface SeedProps {
+ recoveryPhrase: string;
+ onClickCopy?: () => any;
+}
-
{this.state.recoveryPhrase}
-
-
-
-
-
-
-
- {
- this.copyRecoveryPhrase(this.state.recoveryPhrase);
- }}
- />
-
- >
- );
- }
-
- private confirmPassword() {
- const passwordHash = this.state.passwordHash;
- const passwordValue = jQuery('#seed-input-password').val();
- const isPasswordValid = PasswordUtil.matchesHash(passwordValue as string, passwordHash);
+const Seed = (props: SeedProps) => {
+ const { recoveryPhrase, onClickCopy } = props;
+ const i18n = window.i18n;
+ const bgColor = '#FFFFFF';
+ const fgColor = '#1B1B1B';
+ const dispatch = useDispatch();
- if (!passwordValue) {
- this.setState({
- error: window.i18n('noGivenPassword'),
- });
+ const hexEncodedSeed = mn_decode(recoveryPhrase, 'english');
- return false;
+ const copyRecoveryPhrase = (recoveryPhraseToCopy: string) => {
+ window.clipboard.writeText(recoveryPhraseToCopy);
+ ToastUtils.pushCopiedToClipBoard();
+ if (onClickCopy) {
+ onClickCopy();
}
+ dispatch(recoveryPhraseModal(null));
+ };
+
+ return (
+ <>
+
+
{i18n('recoveryPhraseSavePromptMain')}
+
+
+
{recoveryPhrase}
+
+
+
+
+
+
+
+ {
+ copyRecoveryPhrase(recoveryPhrase);
+ }}
+ />
+
+ >
+ );
+};
- if (passwordHash && !isPasswordValid) {
- this.setState({
- error: window.i18n('invalidPassword'),
- });
-
- return false;
- }
+interface ModalInnerProps {
+ onClickOk?: () => any;
+}
- this.setState({
- passwordValid: true,
- error: '',
- });
+const SessionSeedModalInner = (props: ModalInnerProps) => {
+ const { onClickOk } = props;
+ const [loadingPassword, setLoadingPassword] = useState(true);
+ const [loadingSeed, setLoadingSeed] = useState(true);
+ const [recoveryPhrase, setRecoveryPhrase] = useState('');
+ const [hasPassword, setHasPassword] = useState(null);
+ const [passwordValid, setPasswordValid] = useState(false);
+ const [passwordHash, setPasswordHash] = useState('');
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ setTimeout(() => ($('#seed-input-password') as any).focus(), 100);
+ void checkHasPassword();
+ void getRecoveryPhrase();
+ }, []);
- window.removeEventListener('keyup', this.onEnter);
+ const i18n = window.i18n;
- return true;
- }
+ const onClose = () => dispatch(recoveryPhraseModal(null));
- private async checkHasPassword() {
- if (!this.state.loadingPassword) {
+ const checkHasPassword = async () => {
+ if (!loadingPassword) {
return;
}
const hash = await getPasswordHash();
- this.setState({
- hasPassword: !!hash,
- passwordHash: hash || '',
- loadingPassword: false,
- });
- }
-
- private async getRecoveryPhrase() {
- if (this.state.recoveryPhrase) {
+ setHasPassword(!!hash);
+ setPasswordHash(hash || '');
+ setLoadingPassword(false);
+ };
+
+ const getRecoveryPhrase = async () => {
+ if (recoveryPhrase) {
return false;
}
-
- const recoveryPhrase = UserUtils.getCurrentRecoveryPhrase();
-
- this.setState({
- recoveryPhrase,
- loadingSeed: false,
- });
+ const newRecoveryPhrase = UserUtils.getCurrentRecoveryPhrase();
+ setRecoveryPhrase(newRecoveryPhrase);
+ setLoadingSeed(false);
return true;
- }
-
- private copyRecoveryPhrase(recoveryPhrase: string) {
- window.clipboard.writeText(recoveryPhrase);
-
- ToastUtils.pushCopiedToClipBoard();
- window.inboxStore?.dispatch(recoveryPhraseModal(null));
- }
-
- private onEnter(event: any) {
- if (event.key === 'Enter') {
- this.confirmPassword();
- }
- }
-}
-
-export const SessionSeedModal = withTheme(SessionSeedModalInner);
+ };
+
+ return (
+ <>
+ {!loadingSeed && (
+
+
+
+ {hasPassword && !passwordValid ? (
+
+ ) : (
+
+ )}
+
+ )}
+ :
+ >
+ );
+};
+
+export const SessionSeedModal = SessionSeedModalInner;
diff --git a/ts/components/session/LeftPaneSectionHeader.tsx b/ts/components/session/LeftPaneSectionHeader.tsx
index 78cec1c8c..fcff680a3 100644
--- a/ts/components/session/LeftPaneSectionHeader.tsx
+++ b/ts/components/session/LeftPaneSectionHeader.tsx
@@ -1,8 +1,13 @@
import React from 'react';
import classNames from 'classnames';
import { SessionIcon, SessionIconSize, SessionIconType } from './icon';
-import { useTheme } from 'styled-components';
-import { SessionButton } from './SessionButton';
+import styled, { useTheme } from 'styled-components';
+import { SessionButton, SessionButtonType } from './SessionButton';
+import { useDispatch, useSelector } from 'react-redux';
+import { disableRecoveryPhrasePrompt } from '../../state/ducks/userConfig';
+import { getShowRecoveryPhrasePrompt } from '../../state/selectors/userConfig';
+import { recoveryPhraseModal } from '../../state/ducks/modalDialog';
+import { Flex } from '../basic/Flex';
const Tab = ({
isSelected,
@@ -40,22 +45,119 @@ type Props = {
export const LeftPaneSectionHeader = (props: Props) => {
const { label, buttonIcon, buttonClicked } = props;
+ const theme = useTheme();
+ const showRecoveryPhrasePrompt = useSelector(getShowRecoveryPhrasePrompt);
+
+ return (
+
+
+ {label && }
+ {buttonIcon && (
+
+
+
+ )}
+
+ {showRecoveryPhrasePrompt && }
+
+ );
+};
+
+export const LeftPaneBanner = () => {
+ const dispatch = useDispatch();
+
+ const showRecoveryPhraseModal = () => {
+ dispatch(
+ recoveryPhraseModal({
+ onClickOk: () => {
+ dispatch(disableRecoveryPhrasePrompt());
+ },
+ })
+ );
+ };
+
+ const BannerInner = () => {
+ return (
+
+ {window.i18n('recoveryPhraseRevealMessage')}
+
+
+ );
+ };
const theme = useTheme();
return (
-
- {label && }
- {buttonIcon && (
-
-
-
- )}
-
+
+
+
+
+
+ {window.i18n('recoveryPhraseSecureTitle')} 90%
+
+
+
+
+
);
};
+
+const StyledProgressBarContainer = styled.div`
+ width: 100%;
+ height: 5px;
+ flex-direction: row;
+ background: ${p => p.theme.colors.sessionBorderColor};
+`;
+
+const StyledProgressBarInner = styled.div`
+ background: ${p => p.theme.colors.accent};
+ width: 90%;
+ transition: width 0.5s ease-in;
+ height: 100%;
+`;
+
+export const StyledBannerTitle = styled.div`
+ line-height: 1.3;
+ font-size: ${p => p.theme.common.fonts.md};
+ font-weight: bold;
+ margin: ${p => p.theme.common.margins.sm} ${p => p.theme.common.margins.sm} 0
+ ${p => p.theme.common.margins.sm};
+
+ span {
+ color: ${p => p.theme.colors.textAccent};
+ }
+`;
+
+export const StyledLeftPaneBanner = styled.div`
+ background: ${p => p.theme.colors.recoveryPhraseBannerBackground};
+ display: flex;
+ flex-direction: column;
+ border-bottom: ${p => p.theme.colors.sessionBorder};
+`;
+
+const StyledBannerInner = styled.div`
+ p {
+ margin: 0;
+ }
+
+ .left-pane-banner___phrase {
+ margin-top: ${props => props.theme.common.margins.md};
+ }
+
+ .session-button {
+ margin-top: ${props => props.theme.common.margins.sm};
+ }
+`;
diff --git a/ts/components/session/LeftPaneSettingSection.tsx b/ts/components/session/LeftPaneSettingSection.tsx
index be32bc1ad..7c8b79e6f 100644
--- a/ts/components/session/LeftPaneSettingSection.tsx
+++ b/ts/components/session/LeftPaneSettingSection.tsx
@@ -1,7 +1,6 @@
+import React from 'react';
import classNames from 'classnames';
-
import { SessionButton, SessionButtonColor, SessionButtonType } from './SessionButton';
-
import { SessionIcon, SessionIconSize, SessionIconType } from './icon';
import { SessionSettingCategory } from './settings/SessionSettings';
import { LeftPaneSectionHeader } from './LeftPaneSectionHeader';
@@ -10,7 +9,6 @@ import { showSettingsSection } from '../../state/ducks/section';
import { getFocusedSettingsSection } from '../../state/selectors/section';
import { getTheme } from '../../state/selectors/theme';
import { recoveryPhraseModal, updateDeleteAccountModal } from '../../state/ducks/modalDialog';
-import React from 'react';
const getCategories = () => {
return [
diff --git a/ts/components/session/SessionInboxView.tsx b/ts/components/session/SessionInboxView.tsx
index 6937a19e3..36187b644 100644
--- a/ts/components/session/SessionInboxView.tsx
+++ b/ts/components/session/SessionInboxView.tsx
@@ -53,6 +53,7 @@ export class SessionInboxView extends React.Component {
}
const persistor = persistStore(this.store);
+ window.persistStore = persistor;
return (
diff --git a/ts/components/session/conversation/SessionRecording.tsx b/ts/components/session/conversation/SessionRecording.tsx
index 0efc3161c..7b6cf12dd 100644
--- a/ts/components/session/conversation/SessionRecording.tsx
+++ b/ts/components/session/conversation/SessionRecording.tsx
@@ -3,11 +3,11 @@ import classNames from 'classnames';
import moment from 'moment';
import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon';
-import { SessionButton, SessionButtonColor, SessionButtonType } from '../SessionButton';
import { Constants } from '../../../session';
import { ToastUtils } from '../../../session/utils';
import autoBind from 'auto-bind';
import MicRecorder from 'mic-recorder-to-mp3';
+import styled from 'styled-components';
interface Props {
onExitVoiceNoteView: any;
@@ -33,6 +33,24 @@ function getTimestamp(asInt = false) {
return asInt ? Math.floor(timestamp) : timestamp;
}
+export interface StyledFlexWrapperProps {
+ flexDirection: 'row' | 'column';
+ marginHorizontal: string;
+}
+
+/**
+ * Generic wrapper for quickly passing in theme constant values.
+ */
+export const StyledFlexWrapper = styled.div`
+ display: flex;
+ flex-direction: ${(props: StyledFlexWrapperProps) => props.flexDirection};
+ align-items: center;
+
+ .session-button {
+ margin: ${(props: StyledFlexWrapperProps) => props.marginHorizontal};
+ }
+`;
+
class SessionRecordingInner extends React.Component {
private recorder: any;
private audioBlobMp3?: Blob;
@@ -77,19 +95,12 @@ class SessionRecordingInner extends React.Component {
// tslint:disable-next-line: cyclomatic-complexity
public render() {
- const {
- actionHover,
- isPlaying,
- isPaused,
- isRecording,
- startTimestamp,
- nowTimestamp,
- } = this.state;
-
- const actionStopRecording = actionHover && isRecording;
- const actionPlayAudio = !isRecording && !isPlaying;
+ const { isPlaying, isPaused, isRecording, startTimestamp, nowTimestamp } = this.state;
+
+ const hasRecordingAndPaused = !isRecording && !isPlaying;
+ const hasRecording = !!this.audioElement?.duration && this.audioElement?.duration > 0;
const actionPauseAudio = !isRecording && !isPaused && isPlaying;
- const actionDefault = !actionStopRecording && !actionPlayAudio && !actionPauseAudio;
+ const actionDefault = !isRecording && !hasRecordingAndPaused && !actionPauseAudio;
// if we are recording, we base the time recording on our state values
// if we are playing ( audioElement?.currentTime is !== 0, use that instead)
@@ -102,6 +113,14 @@ class SessionRecordingInner extends React.Component {
0;
const displayTimeString = moment.utc(displayTimeMs).format('m:ss');
+ const recordingDurationMs = this.audioElement?.duration
+ ? this.audioElement?.duration * 1000
+ : 1;
+
+ let remainingTimeString = '';
+ if (recordingDurationMs !== undefined) {
+ remainingTimeString = ` / ${moment.utc(recordingDurationMs).format('m:ss')}`;
+ }
const actionPauseFn = isPlaying ? this.pauseAudio : this.stopRecordingStream;
@@ -112,28 +131,37 @@ class SessionRecordingInner extends React.Component {
onMouseEnter={this.handleHoverActions}
onMouseLeave={this.handleUnhoverActions}
>
- {actionStopRecording && (
-
- )}
- {actionPauseAudio && (
-
- )}
- {actionPlayAudio && (
-
- )}
+
+ {isRecording && (
+
+ )}
+ {actionPauseAudio && (
+
+ )}
+ {hasRecordingAndPaused && (
+
+ )}
+ {hasRecording && (
+
+ )}
+
{actionDefault && (
{
)}
-
- {displayTimeString}
- {isRecording &&
}
-
+ {hasRecording && !isRecording ? (
+
+ {displayTimeString + remainingTimeString}
+
+ ) : null}
+
+ {isRecording ? (
+
+ {displayTimeString}
+
+
+ ) : null}
{!isRecording && (
-
+
{
/>
)}
-
-
- {isRecording ? (
-
- ) : (
-
- )}
-
);
}
@@ -271,6 +295,9 @@ class SessionRecordingInner extends React.Component {
this.props.onExitVoiceNoteView();
}
+ /**
+ * Sends the recorded voice message
+ */
private async onSendVoiceMessage() {
if (!this.audioBlobMp3 || !this.audioBlobMp3.size) {
window?.log?.info('Empty audio blob');
@@ -305,6 +332,9 @@ class SessionRecordingInner extends React.Component {
});
}
+ /**
+ * Stops recording audio, sets recording state to stopped.
+ */
private async stopRecordingStream() {
if (!this.recorder) {
return;
@@ -313,10 +343,39 @@ class SessionRecordingInner extends React.Component {
this.recorder = undefined;
this.audioBlobMp3 = blob;
+ this.updateAudioElementAndDuration();
+
// Stop recording
this.stopRecordingState();
}
+ /**
+ * Creates an audio element using the recorded audio blob.
+ * Updates the duration for displaying audio duration.
+ */
+ private updateAudioElementAndDuration() {
+ // init audio element
+ const audioURL = window.URL.createObjectURL(this.audioBlobMp3);
+ this.audioElement = new Audio(audioURL);
+
+ this.setState({
+ recordDuration: this.audioElement.duration,
+ });
+
+ this.audioElement.loop = false;
+ this.audioElement.onended = () => {
+ this.pauseAudio();
+ };
+
+ this.audioElement.oncanplaythrough = async () => {
+ const duration = this.state.recordDuration;
+
+ if (duration && this.audioElement && this.audioElement.currentTime < duration) {
+ await this.audioElement?.play();
+ }
+ };
+ }
+
private async onKeyDown(event: any) {
if (event.key === 'Escape') {
await this.onDeleteVoiceMessage();
diff --git a/ts/components/session/icon/Icons.tsx b/ts/components/session/icon/Icons.tsx
index a942fa1ab..4e0aa2c0e 100644
--- a/ts/components/session/icon/Icons.tsx
+++ b/ts/components/session/icon/Icons.tsx
@@ -12,6 +12,7 @@ export enum SessionIconType {
CirclePlus = 'circlePlus',
CircleElipses = 'circleElipses',
Contacts = 'contacts',
+ Delete = 'delete',
Ellipses = 'ellipses',
Emoji = 'emoji',
Error = 'error',
@@ -127,6 +128,12 @@ export const icons = {
viewBox: '4 4 7 7',
ratio: 1,
},
+ [SessionIconType.Delete]: {
+ path:
+ 'M11.17 37.16h83.48a8.4 8.4 0 012 .16 5.93 5.93 0 012.88 1.56 5.43 5.43 0 011.64 3.34 7.65 7.65 0 01-.06 1.44L94 117.31V117.72a7.06 7.06 0 01-.2.9v.06a5.89 5.89 0 01-5.47 4.07H17.32a6.17 6.17 0 01-1.25-.19 6.17 6.17 0 01-1.16-.48 6.18 6.18 0 01-3.08-4.88l-7-73.49a7.69 7.69 0 01-.06-1.66 5.37 5.37 0 011.63-3.29 6 6 0 013-1.58 8.94 8.94 0 011.79-.13zM5.65 8.8h31.47V6a2.44 2.44 0 010-.27 6 6 0 011.76-4A6 6 0 0143.09 0h19.67a6 6 0 015.7 6v2.8h32.39a4.7 4.7 0 014.31 4.43v10.36a2.59 2.59 0 01-2.59 2.59H2.59A2.59 2.59 0 010 23.62V13.53a1.56 1.56 0 010-.31 4.72 4.72 0 013.88-4.34 10.4 10.4 0 011.77-.08zm42.1 52.7a4.77 4.77 0 019.49 0v37a4.77 4.77 0 01-9.49 0v-37zm23.73-.2a4.58 4.58 0 015-4.06 4.47 4.47 0 014.51 4.46l-2 37a4.57 4.57 0 01-5 4.06 4.47 4.47 0 01-4.51-4.46l2-37zM25 61.7a4.46 4.46 0 014.5-4.46 4.58 4.58 0 015 4.06l2 37a4.47 4.47 0 01-4.51 4.46 4.57 4.57 0 01-5-4.06l-2-37z',
+ viewBox: '0 0 105.16 122.88',
+ ratio: 1,
+ },
[SessionIconType.DoubleCheckCircleFilled]: {
path:
'M7.91731278,0.313257194 C6.15053376,1.58392424 5,3.65760134 5,6 C5,6.343797 5.0247846,6.68180525 5.07266453,7.01233547 L5,7.085 L3.205,5.295 L2.5,6 L5,8.5 L5.33970233,8.16029767 C5.80439817,9.59399486 6.71914823,10.8250231 7.91731278,11.6867428 C7.31518343,11.8898758 6.67037399,12 6,12 C2.688,12 0,9.312 0,6 C0,2.688 2.688,0 6,0 C6.67037399,0 7.31518343,0.110124239 7.91731278,0.313257194 Z M12,0 C15.312,0 18,2.688 18,6 C18,9.312 15.312,12 12,12 C8.688,12 6,9.312 6,6 C6,2.688 8.688,0 12,0 Z M11,8.5 L15.5,4 L14.795,3.29 L11,7.085 L9.205,5.295 L8.5,6 L11,8.5 Z',
diff --git a/ts/receiver/attachments.ts b/ts/receiver/attachments.ts
index a9134a75e..a4b804fb0 100644
--- a/ts/receiver/attachments.ts
+++ b/ts/receiver/attachments.ts
@@ -171,12 +171,12 @@ async function processPreviews(message: MessageModel, convo: ConversationModel):
const image = message.isTrustedForAttachmentDownload()
? await AttachmentDownloads.addJob(item.image, {
- messageId: message.id,
- type: 'preview',
- index,
- isOpenGroupV2,
- openGroupV2Details,
- })
+ messageId: message.id,
+ type: 'preview',
+ index,
+ isOpenGroupV2,
+ openGroupV2Details,
+ })
: null;
return { ...item, image };
diff --git a/ts/state/ducks/SessionTheme.tsx b/ts/state/ducks/SessionTheme.tsx
index 1aba14216..587a9f5e3 100644
--- a/ts/state/ducks/SessionTheme.tsx
+++ b/ts/state/ducks/SessionTheme.tsx
@@ -10,8 +10,14 @@ const warning = '#e7b100';
const destructive = '#ff453a';
const accentLightTheme = '#00e97b';
const accentDarkTheme = '#00f782';
-const borderLightTheme = '#f1f1f1';
-const borderDarkTheme = '#ffffff0F';
+const borderLightThemeColor = '#f1f1f1';
+const borderDarkThemeColor = '#ffffff0F';
+const borderHighContrastLightTheme = '#afafaf';
+const borderHighContrastDarkTheme = '#484848';
+
+// const borderAvatarColor = '#00000059';
+// const borderLightTheme = '#f1f1f1';
+// const borderDarkTheme = '#ffffff0F';
const common = {
fonts: {
@@ -53,6 +59,7 @@ export const lightTheme: DefaultTheme = {
textColorSubtleNoOpacity: '#52514f',
textColorOpposite: white,
textHighlight: `${black}33`,
+ textAccent: '#00c769',
// inbox
inboxBackground: white,
// buttons
@@ -73,9 +80,12 @@ export const lightTheme: DefaultTheme = {
conversationItemHasUnread: '#fcfcfc',
conversationItemSelected: '#f0f0f0',
clickableHovered: '#dfdfdf',
- sessionBorder: `1px solid ${borderLightTheme}`,
+ sessionBorder: `1px solid ${borderLightThemeColor}`,
+ sessionBorderColor: borderLightThemeColor,
+ sessionBorderHighContrast: `1px solid ${borderHighContrastLightTheme}`,
sessionUnreadBorder: `4px solid ${accentLightTheme}`,
leftpaneOverlayBackground: white,
+ recoveryPhraseBannerBackground: white,
// scrollbars
scrollBarTrack: '#fcfcfc',
scrollBarThumb: '#474646',
@@ -109,6 +119,7 @@ export const darkTheme = {
textColorSubtleNoOpacity: '#7f7d7d',
textColorOpposite: black,
textHighlight: `${accentDarkTheme}99`,
+ textAccent: accentDarkTheme,
// inbox
inboxBackground: 'linear-gradient(180deg, #171717 0%, #121212 100%)',
// buttons
@@ -129,9 +140,12 @@ export const darkTheme = {
conversationItemHasUnread: '#2c2c2c',
conversationItemSelected: '#404040',
clickableHovered: '#414347',
- sessionBorder: `1px solid ${borderDarkTheme}`,
+ sessionBorder: `1px solid ${borderDarkThemeColor}`,
+ sessionBorderColor: borderDarkThemeColor,
+ sessionBorderHighContrast: `1px solid ${borderHighContrastDarkTheme}`,
sessionUnreadBorder: `4px solid ${accentDarkTheme}`,
leftpaneOverlayBackground: 'linear-gradient(180deg, #171717 0%, #121212 100%)',
+ recoveryPhraseBannerBackground: '#1f1f1f',
// scrollbars
scrollBarTrack: '#1b1b1b',
scrollBarThumb: '#474646',
diff --git a/ts/state/ducks/userConfig.tsx b/ts/state/ducks/userConfig.tsx
index 1c704e2b4..af64b2cb4 100644
--- a/ts/state/ducks/userConfig.tsx
+++ b/ts/state/ducks/userConfig.tsx
@@ -6,10 +6,12 @@ import { createSlice } from '@reduxjs/toolkit';
export interface UserConfigState {
audioAutoplay: boolean;
+ showRecoveryPhrasePrompt: boolean;
}
export const initialUserConfigState = {
audioAutoplay: false,
+ showRecoveryPhrasePrompt: true,
};
const userConfigSlice = createSlice({
@@ -19,9 +21,12 @@ const userConfigSlice = createSlice({
toggleAudioAutoplay: state => {
state.audioAutoplay = !state.audioAutoplay;
},
+ disableRecoveryPhrasePrompt: state => {
+ state.showRecoveryPhrasePrompt = false;
+ },
},
});
const { actions, reducer } = userConfigSlice;
-export const { toggleAudioAutoplay } = actions;
+export const { toggleAudioAutoplay, disableRecoveryPhrasePrompt } = actions;
export const userConfigReducer = reducer;
diff --git a/ts/state/selectors/userConfig.ts b/ts/state/selectors/userConfig.ts
index 084f8d9ac..9c8641cb2 100644
--- a/ts/state/selectors/userConfig.ts
+++ b/ts/state/selectors/userConfig.ts
@@ -8,3 +8,8 @@ export const getAudioAutoplay = createSelector(
getUserConfig,
(state: UserConfigState): boolean => state.audioAutoplay
);
+
+export const getShowRecoveryPhrasePrompt = createSelector(
+ getUserConfig,
+ (state: UserConfigState): boolean => state.showRecoveryPhrasePrompt
+);
diff --git a/ts/styled.d.ts b/ts/styled.d.ts
index 905375e34..953f901d3 100644
--- a/ts/styled.d.ts
+++ b/ts/styled.d.ts
@@ -39,6 +39,7 @@ declare module 'styled-components' {
textColorSubtleNoOpacity: string;
textColorOpposite: string;
textHighlight: string;
+ textAccent: string;
// inbox
inboxBackground: string;
// buttons
@@ -60,8 +61,11 @@ declare module 'styled-components' {
conversationItemSelected: string;
clickableHovered: string;
sessionBorder: string;
+ sessionBorderColor: string;
+ sessionBorderHighContrast: string;
sessionUnreadBorder: string;
leftpaneOverlayBackground: string;
+ recoveryPhraseBannerBackground: string;
// scrollbars
scrollBarTrack: string;
scrollBarThumb: string;
diff --git a/ts/window.d.ts b/ts/window.d.ts
index b540c3d1f..7b7e58120 100644
--- a/ts/window.d.ts
+++ b/ts/window.d.ts
@@ -52,6 +52,7 @@ declare global {
};
lokiSnodeAPI: LokiSnodeAPI;
onLogin: any;
+ persistStore?: Persistor;
resetDatabase: any;
restart: any;
getSeedNodeList: () => Array | undefined;