diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 176161e2c..7324c04d2 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -578,5 +578,6 @@ "resolution": "Resolution", "duration": "Duration", "notApplicable": "N/A", - "unknownError": "Unknown Error" + "unknownError": "Unknown Error", + "displayNameErrorNew": "We were unable to load your display name. Please enter a new display name to continue." } diff --git a/package.json b/package.json index 2f2a73da1..7c6849700 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "session-desktop", "productName": "Session", "description": "Private messaging from your desktop", - "version": "1.12.1", + "version": "1.12.2", "license": "GPL-3.0", "author": { "name": "Oxen Labs", diff --git a/preload.js b/preload.js index 13712388d..5805bcb49 100644 --- a/preload.js +++ b/preload.js @@ -29,7 +29,7 @@ window.getNodeVersion = () => configAny.node_version; window.sessionFeatureFlags = { useOnionRequests: true, - useTestNet: isTestNet(), + useTestNet: isTestNet() || isTestIntegration(), integrationTestEnv: isTestIntegration(), useClosedGroupV3: false, debug: { diff --git a/stylesheets/_session_constants.scss b/stylesheets/_session_constants.scss index 87f9d4ca4..02b7323be 100644 --- a/stylesheets/_session_constants.scss +++ b/stylesheets/_session_constants.scss @@ -153,7 +153,6 @@ $session-margin-md: 15px; $session-margin-lg: 20px; // Animations -$session-transition-duration: 0.25s; @keyframes fadein { from { diff --git a/stylesheets/_session_slider.scss b/stylesheets/_session_slider.scss index 81feb400c..db3b56efa 100644 --- a/stylesheets/_session_slider.scss +++ b/stylesheets/_session_slider.scss @@ -172,13 +172,13 @@ } .rc-slider-tooltip-zoom-down-enter, .rc-slider-tooltip-zoom-down-appear { - animation-duration: 0.3s; + animation-duration: var(--default-duration); animation-fill-mode: both; display: block !important; animation-play-state: paused; } .rc-slider-tooltip-zoom-down-leave { - animation-duration: 0.3s; + animation-duration: var(--default-duration); animation-fill-mode: both; display: block !important; animation-play-state: paused; diff --git a/ts/components/SessionFocusTrap.tsx b/ts/components/SessionFocusTrap.tsx new file mode 100644 index 000000000..3c33c2f27 --- /dev/null +++ b/ts/components/SessionFocusTrap.tsx @@ -0,0 +1,21 @@ +import FocusTrap from 'focus-trap-react'; +import React, { useState } from 'react'; +import { useMount } from 'react-use'; + +/** + * Focus trap which activates on mount. + */ +export function SessionFocusTrap(props: { children: React.ReactNode }) { + const [active, setActive] = useState(false); + + // Activate the trap on mount so we **should** have a button to tab through. focus-trap-react will throw if we don't have a button when the trap becomes active. + // We might still have an issue for dialogs which have buttons added with a useEffect, or asynchronously but have no buttons on mount. + // If that happens we will need to force those dialogs to have at least one button so focus-trap-react does not throw. + useMount(() => setActive(true)); + + return ( + + {props.children} + + ); +} diff --git a/ts/components/SessionWrapperModal.tsx b/ts/components/SessionWrapperModal.tsx index 42753784e..8e8f37ac0 100644 --- a/ts/components/SessionWrapperModal.tsx +++ b/ts/components/SessionWrapperModal.tsx @@ -1,11 +1,11 @@ import classNames from 'classnames'; -import FocusTrap from 'focus-trap-react'; import React, { useRef } from 'react'; import useKey from 'react-use/lib/useKey'; import { SessionIconButton } from './icon'; import { SessionButton, SessionButtonColor, SessionButtonType } from './basic/SessionButton'; +import { SessionFocusTrap } from './SessionFocusTrap'; export type SessionWrapperModalType = { title?: string; @@ -17,7 +17,7 @@ export type SessionWrapperModalType = { cancelText?: string; showExitIcon?: boolean; headerIconButtons?: Array; - children: any; + children: React.ReactNode; headerReverse?: boolean; additionalClassName?: string; }; @@ -64,7 +64,7 @@ export const SessionWrapperModal = (props: SessionWrapperModalType) => { }; return ( - +
{
-
+ ); }; diff --git a/ts/components/calling/CallButtons.tsx b/ts/components/calling/CallButtons.tsx index 20aca8886..74ca159b9 100644 --- a/ts/components/calling/CallButtons.tsx +++ b/ts/components/calling/CallButtons.tsx @@ -355,7 +355,7 @@ const StyledCallWindowControls = styled.div<{ isFullScreen: boolean; makeVisible margin-right: auto; left: 0; right: 0; - transition: all 0.25s ease-in-out; + transition: all var(--default-duration) ease-in-out; display: flex; justify-content: center; diff --git a/ts/components/conversation/composition/UserMentions.tsx b/ts/components/conversation/composition/UserMentions.tsx index e5c7aeef5..e82ed30d3 100644 --- a/ts/components/conversation/composition/UserMentions.tsx +++ b/ts/components/conversation/composition/UserMentions.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { SuggestionDataItem } from 'react-mentions'; -import { MemberListItem } from '../../MemberListItem'; import { HTMLDirection } from '../../../util/i18n'; +import { MemberListItem } from '../../MemberListItem'; const listRTLStyle = { position: 'absolute', bottom: '0px', right: '100%' }; @@ -21,7 +21,7 @@ export const styleForCompositionBoxSuggestions = (dir: HTMLDirection = 'ltr') => paddingBottom: '5px', backgroundColor: 'var(--suggestions-background-color)', color: 'var(--suggestions-text-color)', - transition: '0.25s', + transition: 'var(--default-duration)', '&focused': { backgroundColor: 'var(--suggestions-background-hover-color)', diff --git a/ts/components/conversation/header/ConversationHeaderSelectionOverlay.tsx b/ts/components/conversation/header/ConversationHeaderSelectionOverlay.tsx index 663081eae..ff3040688 100644 --- a/ts/components/conversation/header/ConversationHeaderSelectionOverlay.tsx +++ b/ts/components/conversation/header/ConversationHeaderSelectionOverlay.tsx @@ -1,5 +1,5 @@ -import FocusTrap from 'focus-trap-react'; import React from 'react'; + import { useDispatch, useSelector } from 'react-redux'; import useKey from 'react-use/lib/useKey'; @@ -17,6 +17,7 @@ import { SessionButtonType, } from '../../basic/SessionButton'; import { SessionIconButton } from '../../icon'; +import { SessionFocusTrap } from '../../SessionFocusTrap'; export const SelectionOverlay = () => { const selectedMessageIds = useSelector(getSelectedMessageIds); @@ -69,7 +70,7 @@ export const SelectionOverlay = () => { const classNameAndId = 'message-selection-overlay'; return ( - +
@@ -93,6 +94,6 @@ export const SelectionOverlay = () => { />
-
+ ); }; diff --git a/ts/components/conversation/right-panel/RightPanel.tsx b/ts/components/conversation/right-panel/RightPanel.tsx index d3b28dc10..8dc6c1b3f 100644 --- a/ts/components/conversation/right-panel/RightPanel.tsx +++ b/ts/components/conversation/right-panel/RightPanel.tsx @@ -13,7 +13,7 @@ export const StyledRightPanelContainer = styled.div` height: var(--right-panel-height); right: 0vw; - transition: transform 0.3s ease-in-out; + transition: transform var(--default-duration) ease-in-out; transform: translateX(100%); will-change: transform; width: var(--right-panel-width); @@ -25,7 +25,7 @@ export const StyledRightPanelContainer = styled.div` &.show { transform: none; - transition: transform 0.3s ease-in-out; + transition: transform var(--default-duration) ease-in-out; z-index: 3; visibility: visible; } diff --git a/ts/components/dialog/SessionSeedModal.tsx b/ts/components/dialog/SessionSeedModal.tsx index c2927301c..63fb672a9 100644 --- a/ts/components/dialog/SessionSeedModal.tsx +++ b/ts/components/dialog/SessionSeedModal.tsx @@ -62,6 +62,7 @@ const Password = (props: PasswordProps) => { @@ -77,12 +78,14 @@ const Password = (props: PasswordProps) => { text={i18n('done')} buttonType={SessionButtonType.Simple} onClick={confirmPassword} + dataTestId="session-confirm-ok-button" /> diff --git a/ts/components/leftpane/LeftPaneSectionHeader.tsx b/ts/components/leftpane/LeftPaneSectionHeader.tsx index 78a807474..198da2ff4 100644 --- a/ts/components/leftpane/LeftPaneSectionHeader.tsx +++ b/ts/components/leftpane/LeftPaneSectionHeader.tsx @@ -28,7 +28,7 @@ const StyledProgressBarContainer = styled.div` const StyledProgressBarInner = styled.div` background: var(--primary-color); width: 90%; - transition: width 0.5s ease-in; + transition: width var(--default-duration) ease-in; height: 100%; `; diff --git a/ts/components/leftpane/MessageRequestsBanner.tsx b/ts/components/leftpane/MessageRequestsBanner.tsx index ccfae4bd8..8f06487f5 100644 --- a/ts/components/leftpane/MessageRequestsBanner.tsx +++ b/ts/components/leftpane/MessageRequestsBanner.tsx @@ -4,10 +4,10 @@ import { createPortal } from 'react-dom'; import { useSelector } from 'react-redux'; import styled from 'styled-components'; import { getUnreadConversationRequests } from '../../state/selectors/conversations'; +import { isSearching } from '../../state/selectors/search'; import { getHideMessageRequestBanner } from '../../state/selectors/userConfig'; import { SessionIcon, SessionIconSize, SessionIconType } from '../icon'; import { MessageRequestBannerContextMenu } from '../menu/MessageRequestBannerContextMenu'; -import { isSearching } from '../../state/selectors/search'; const StyledMessageRequestBanner = styled.div` height: 64px; @@ -137,6 +137,6 @@ export const MessageRequestsBanner = (props: { handleOnClick: () => any }) => { ); }; -const Portal = ({ children }: { children: any }) => { +const Portal = ({ children }: { children: React.ReactNode }) => { return createPortal(children, document.querySelector('.inbox.index') as Element); }; diff --git a/ts/components/leftpane/conversation-list-item/ConversationListItem.tsx b/ts/components/leftpane/conversation-list-item/ConversationListItem.tsx index d1ce3e5f7..a15090ceb 100644 --- a/ts/components/leftpane/conversation-list-item/ConversationListItem.tsx +++ b/ts/components/leftpane/conversation-list-item/ConversationListItem.tsx @@ -34,7 +34,7 @@ type PropsHousekeeping = { type Props = { conversationId: string } & PropsHousekeeping; -const Portal = ({ children }: { children: any }) => { +const Portal = ({ children }: { children: React.ReactNode }) => { return createPortal(children, document.querySelector('.inbox.index') as Element); }; diff --git a/ts/components/leftpane/overlay/choose-action/OverlayChooseAction.tsx b/ts/components/leftpane/overlay/choose-action/OverlayChooseAction.tsx index 210e512a2..d162a5fa7 100644 --- a/ts/components/leftpane/overlay/choose-action/OverlayChooseAction.tsx +++ b/ts/components/leftpane/overlay/choose-action/OverlayChooseAction.tsx @@ -14,7 +14,7 @@ const StyledActionRow = styled.button` display: flex; align-items: center; border-bottom: 1px var(--border-color) solid; - transition-duration: 0.25s; + transition-duration: var(--default-duration); width: 100%; &:first-child { diff --git a/ts/components/registration/RegistrationStages.tsx b/ts/components/registration/RegistrationStages.tsx index fe3e196d6..fe29943f4 100644 --- a/ts/components/registration/RegistrationStages.tsx +++ b/ts/components/registration/RegistrationStages.tsx @@ -1,12 +1,11 @@ import React, { createContext, useEffect, useState } from 'react'; -import { SignUpMode, SignUpTab } from './SignUpTab'; -import { SignInMode, SignInTab } from './SignInTab'; import { Data } from '../../data/data'; +import { SettingsKey } from '../../data/settings-key'; import { getSwarmPollingInstance } from '../../session/apis/snode_api'; import { getConversationController } from '../../session/conversations'; import { mnDecode } from '../../session/crypto/mnemonic'; import { PromiseUtils, StringUtils, ToastUtils } from '../../session/utils'; -import { TaskTimedOutError } from '../../session/utils/Promise'; +import { fromHex } from '../../session/utils/String'; import { trigger } from '../../shims/events'; import { generateMnemonic, @@ -14,9 +13,9 @@ import { sessionGenerateKeyPair, signInByLinkingDevice, } from '../../util/accountManager'; -import { fromHex } from '../../session/utils/String'; -import { setSignInByLinking, setSignWithRecoveryPhrase, Storage } from '../../util/storage'; -import { SettingsKey } from '../../data/settings-key'; +import { Storage, setSignInByLinking, setSignWithRecoveryPhrase } from '../../util/storage'; +import { SignInMode, SignInTab } from './SignInTab'; +import { SignUpMode, SignUpTab } from './SignUpTab'; export async function resetRegistration() { await Data.removeAll(); @@ -104,7 +103,10 @@ export async function signInWithRecovery(signInDetails: { * This is will try to sign in with the user recovery phrase. * If no ConfigurationMessage is received in 60seconds, the loading will be canceled. */ -export async function signInWithLinking(signInDetails: { userRecoveryPhrase: string }) { +export async function signInWithLinking( + signInDetails: { userRecoveryPhrase: string }, + setSignInMode: (phase: SignInMode) => void +) { const { userRecoveryPhrase } = signInDetails; window?.log?.info('LINKING DEVICE'); @@ -136,18 +138,14 @@ export async function signInWithLinking(signInDetails: { userRecoveryPhrase: str trigger('openInbox'); } catch (e) { await resetRegistration(); - if (e instanceof TaskTimedOutError) { - ToastUtils.pushToastError( - 'registrationError', - 'Could not find your display name. Please Sign In by Restoring Your Account instead.' - ); - } else { - ToastUtils.pushToastError( - 'registrationError', - `Error: ${e.message || 'Something went wrong'}` - ); - } - window?.log?.warn('exception during registration:', e); + ToastUtils.pushToastError('registrationError', window.i18n('displayNameErrorNew')); + window?.log?.error( + '[signInWithLinking] Error during sign in by linking lets try and sign in by recovery phrase', + e.message || e + ); + getSwarmPollingInstance().stop(e); + await setSignWithRecoveryPhrase(false); + setSignInMode(SignInMode.UsingRecoveryPhrase); } } diff --git a/ts/components/registration/SignInTab.tsx b/ts/components/registration/SignInTab.tsx index 088d4bc36..7392a137e 100644 --- a/ts/components/registration/SignInTab.tsx +++ b/ts/components/registration/SignInTab.tsx @@ -123,7 +123,7 @@ export const SignInTab = () => { // from the configuration message will be used. const showDisplayNameField = isRecovery; - // Display name is required only on isRecoveryMode + // Display name is required only on isRecoveryMode or if linking a device fails const displayNameOK = (isRecovery && !displayNameError && !!displayName) || isLinking; // Seed is mandatory no matter which mode @@ -139,9 +139,12 @@ export const SignInTab = () => { }); } else if (isLinking) { setIsLoading(true); - await signInWithLinking({ - userRecoveryPhrase: recoveryPhrase, - }); + await signInWithLinking( + { + userRecoveryPhrase: recoveryPhrase, + }, + setSignInMode + ); setIsLoading(false); } }; diff --git a/ts/mains/main_node.ts b/ts/mains/main_node.ts index 2abe40d6d..46de7cbde 100644 --- a/ts/mains/main_node.ts +++ b/ts/mains/main_node.ts @@ -428,7 +428,7 @@ async function createWindow() { }, 5000); } - if (isDevProd()) { + if (isDevProd() && !isTestIntegration()) { // Open the DevTools. mainWindow.webContents.openDevTools({ mode: 'bottom', diff --git a/ts/session/apis/snode_api/swarmPolling.ts b/ts/session/apis/snode_api/swarmPolling.ts index c43fce899..e8ddcc092 100644 --- a/ts/session/apis/snode_api/swarmPolling.ts +++ b/ts/session/apis/snode_api/swarmPolling.ts @@ -57,6 +57,8 @@ export function extractWebSocketContent( } let instance: SwarmPolling | undefined; +const timeouts: Array = []; + export const getSwarmPollingInstance = () => { if (!instance) { instance = new SwarmPolling(); @@ -84,9 +86,11 @@ export class SwarmPolling { if (waitForFirstPoll) { await this.pollForAllKeys(); } else { - setTimeout(() => { - void this.pollForAllKeys(); - }, 4000); + timeouts.push( + setTimeout(() => { + void this.pollForAllKeys(); + }, 4000) + ); } } @@ -98,6 +102,17 @@ export class SwarmPolling { this.hasStarted = false; } + // TODO[epic=ses-50] this is a temporary solution until onboarding is merged + public stop(e: Error) { + window.log.error(`[swarmPolling] stopped polling due to error: ${e.message || e}`); + + for (let i = 0; i < timeouts.length; i++) { + clearTimeout(timeouts[i]); + window.log.debug(`[swarmPolling] cleared timeout ${timeouts[i]} `); + } + this.resetSwarmPolling(); + } + public forcePolledTimestamp(pubkey: PubKey, lastPoll: number) { this.groupPolling = this.groupPolling.map(group => { if (PubKey.isEqual(pubkey, group.pubkey)) { @@ -163,7 +178,7 @@ export class SwarmPolling { if (!window.getGlobalOnlineStatus()) { window?.log?.error('pollForAllKeys: offline'); // Very important to set up a new polling call so we do retry at some point - setTimeout(this.pollForAllKeys.bind(this), SWARM_POLLING_TIMEOUT.ACTIVE); + timeouts.push(setTimeout(this.pollForAllKeys.bind(this), SWARM_POLLING_TIMEOUT.ACTIVE)); return; } // we always poll as often as possible for our pubkey @@ -200,7 +215,7 @@ export class SwarmPolling { window?.log?.warn('pollForAllKeys exception: ', e); throw e; } finally { - setTimeout(this.pollForAllKeys.bind(this), SWARM_POLLING_TIMEOUT.ACTIVE); + timeouts.push(setTimeout(this.pollForAllKeys.bind(this), SWARM_POLLING_TIMEOUT.ACTIVE)); } } diff --git a/ts/themes/SessionTheme.tsx b/ts/themes/SessionTheme.tsx index 3337df271..01231796b 100644 --- a/ts/themes/SessionTheme.tsx +++ b/ts/themes/SessionTheme.tsx @@ -1,10 +1,10 @@ import { ipcRenderer } from 'electron'; import React from 'react'; import { createGlobalStyle } from 'styled-components'; -import { switchThemeTo } from './switchTheme'; -import { classicDark } from './classicDark'; import { getOppositeTheme, isThemeMismatched } from '../util/theme'; -import { declareCSSVariables, THEME_GLOBALS } from './globals'; +import { classicDark } from './classicDark'; +import { THEME_GLOBALS, declareCSSVariables } from './globals'; +import { switchThemeTo } from './switchTheme'; // Defaults to Classic Dark theme const SessionGlobalStyles = createGlobalStyle` @@ -14,7 +14,7 @@ const SessionGlobalStyles = createGlobalStyle` }; `; -export const SessionTheme = ({ children }: { children: any }) => ( +export const SessionTheme = ({ children }: { children: React.ReactNode }) => ( <> {children} diff --git a/ts/themes/globals.tsx b/ts/themes/globals.tsx index fbd4d4948..d0743586c 100644 --- a/ts/themes/globals.tsx +++ b/ts/themes/globals.tsx @@ -1,3 +1,4 @@ +import { isTestIntegration } from '../shared/env_vars'; import { hexColorToRGB } from '../util/hexColorToRGB'; import { COLORS } from './constants/colors'; @@ -126,7 +127,7 @@ export const THEME_GLOBALS: ThemeGlobals = { '--composition-container-height': '60px', '--search-input-height': '34px', - '--default-duration': '0.25s', + '--default-duration': isTestIntegration() ? '0s' : '0.25s', '--green-color': COLORS.PRIMARY.GREEN, '--blue-color': COLORS.PRIMARY.BLUE, diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts index 3b31283ed..14aa1aba9 100644 --- a/ts/types/LocalizerKeys.ts +++ b/ts/types/LocalizerKeys.ts @@ -162,6 +162,7 @@ export type LocalizerKeys = | 'disappears' | 'displayName' | 'displayNameEmpty' + | 'displayNameErrorNew' | 'displayNameTooLong' | 'document' | 'documents'