diff --git a/js/background.js b/js/background.js index 558c143c6..c58986015 100644 --- a/js/background.js +++ b/js/background.js @@ -450,7 +450,6 @@ }, window.CONSTANTS.NOTIFICATION_ENABLE_TIMEOUT_SECONDS * 1000); window.NewReceiver.queueAllCached(); - window.libsession.Utils.AttachmentDownloads.start({ logger: window.log, }); diff --git a/package.json b/package.json index a99ffbff1..2af38bea6 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "session-desktop", "productName": "Session", "description": "Private messaging from your desktop", - "version": "1.6.6", + "version": "1.6.7", "license": "GPL-3.0", "author": { "name": "Loki Project", diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx index 4b5e4cfb0..da5a16129 100644 --- a/ts/components/LeftPane.tsx +++ b/ts/components/LeftPane.tsx @@ -7,7 +7,6 @@ import { openConversationExternal } from '../state/ducks/conversations'; import { LeftPaneContactSection } from './session/LeftPaneContactSection'; import { LeftPaneSettingSection } from './session/LeftPaneSettingSection'; import { SessionTheme } from '../state/ducks/SessionTheme'; -import { SessionOffline } from './session/network/SessionOffline'; import { SessionExpiredWarning } from './session/network/SessionExpiredWarning'; import { getFocusedSection } from '../state/selectors/section'; import { useDispatch, useSelector } from 'react-redux'; @@ -44,7 +43,6 @@ const InnerLeftPaneMessageSection = (props: { isExpired: boolean }) => { return ( <> - {props.isExpired && } { return ( <> - dispatch(openConversationExternal(id, messageId)) diff --git a/ts/components/OnionStatusPathDialog.tsx b/ts/components/OnionStatusPathDialog.tsx index 72c386aed..a7099318e 100644 --- a/ts/components/OnionStatusPathDialog.tsx +++ b/ts/components/OnionStatusPathDialog.tsx @@ -20,11 +20,10 @@ import { onionPathModal } from '../state/ducks/modalDialog'; import { getFirstOnionPath, getFirstOnionPathLength, + getIsOnline, getOnionPathsCount, } from '../state/selectors/onions'; -// tslint:disable-next-line: no-submodule-imports -import useNetworkState from 'react-use/lib/useNetworkState'; import { SessionSpinner } from './session/SessionSpinner'; import { Flex } from './basic/Flex'; @@ -36,9 +35,10 @@ export type StatusLightType = { const OnionPathModalInner = () => { const onionPath = useSelector(getFirstOnionPath); + const isOnline = useSelector(getIsOnline); // including the device and destination in calculation const glowDuration = onionPath.length + 2; - if (!onionPath || onionPath.length === 0) { + if (!isOnline || !onionPath || onionPath.length === 0) { return ; } @@ -144,7 +144,7 @@ export const ActionPanelOnionStatusLight = (props: { const theme = useTheme(); const onionPathsCount = useSelector(getOnionPathsCount); const firstPathLength = useSelector(getFirstOnionPathLength); - const isOnline = useNetworkState().online; + const isOnline = useSelector(getIsOnline); // Set icon color based on result const red = theme.colors.destructive; @@ -164,6 +164,9 @@ export const ActionPanelOnionStatusLight = (props: { iconType={SessionIconType.Circle} iconColor={iconColor} onClick={handleClick} + glowDuration={10} + glowStartDelay={0} + noScale={true} isSelected={isSelected} theme={theme} /> diff --git a/ts/components/conversation/message/ClickToTrustSender.tsx b/ts/components/conversation/message/ClickToTrustSender.tsx index 43eb71736..257755185 100644 --- a/ts/components/conversation/message/ClickToTrustSender.tsx +++ b/ts/components/conversation/message/ClickToTrustSender.tsx @@ -58,6 +58,9 @@ export const ClickToTrustSender = (props: { messageId: string }) => { const downloadedAttachments = await Promise.all( msgAttachments.map(async (attachment: any, index: any) => { + if (attachment.path) { + return { ...attachment, pending: false }; + } return AttachmentDownloads.addJob(attachment, { messageId: message.id, type: 'attachment', diff --git a/ts/components/session/SessionConfirm.tsx b/ts/components/session/SessionConfirm.tsx index 27b40bb93..190c11632 100644 --- a/ts/components/session/SessionConfirm.tsx +++ b/ts/components/session/SessionConfirm.tsx @@ -17,7 +17,7 @@ export interface SessionConfirmDialogProps { title?: string; onOk?: any; onClose?: any; - onClickOk?: () => any; + onClickOk?: () => Promise | void; onClickClose?: () => any; okText?: string; cancelText?: string; diff --git a/ts/components/session/SessionToggle.tsx b/ts/components/session/SessionToggle.tsx index eb7ed2de4..378b18a46 100644 --- a/ts/components/session/SessionToggle.tsx +++ b/ts/components/session/SessionToggle.tsx @@ -1,84 +1,63 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import classNames from 'classnames'; import { updateConfirmModal } from '../../state/ducks/modalDialog'; +import { useDispatch } from 'react-redux'; -interface Props { +type Props = { active: boolean; - onClick: any; + onClick: () => void; confirmationDialogParams?: any | undefined; +}; - updateConfirmModal?: any; -} +export const SessionToggle = (props: Props) => { + const [active, setActive] = useState(false); -interface State { - active: boolean; -} - -export class SessionToggle extends React.PureComponent { - public static defaultProps = { - onClick: () => null, - }; - - constructor(props: any) { - super(props); - this.clickHandler = this.clickHandler.bind(this); - - const { active } = this.props; + const dispatch = useDispatch(); - this.state = { - active: active, - }; - } - - public render() { - return ( -
-
-
- ); - } + useEffect(() => { + setActive(props.active); + }, []); - private clickHandler(event: any) { + const clickHandler = (event: any) => { const stateManager = (e: any) => { - this.setState({ - active: !this.state.active, - }); - - if (this.props.onClick) { - e.stopPropagation(); - this.props.onClick(); - } + setActive(!active); + e.stopPropagation(); + props.onClick(); }; - if ( - this.props.confirmationDialogParams && - this.props.updateConfirmModal && - this.props.confirmationDialogParams.shouldShowConfirm() - ) { + if (props.confirmationDialogParams && props.confirmationDialogParams.shouldShowConfirm()) { // If item needs a confirmation dialog to turn ON, render it const closeConfirmModal = () => { - this.props.updateConfirmModal(null); + dispatch(updateConfirmModal(null)); }; - this.props.updateConfirmModal({ - onClickOk: () => { - stateManager(event); - closeConfirmModal(); - }, - onClickClose: () => { - this.props.updateConfirmModal(null); - }, - ...this.props.confirmationDialogParams, - updateConfirmModal, - }); + dispatch( + updateConfirmModal({ + onClickOk: () => { + stateManager(event); + closeConfirmModal(); + }, + onClickClose: () => { + updateConfirmModal(null); + }, + ...props.confirmationDialogParams, + updateConfirmModal, + }) + ); return; } stateManager(event); - } -} + }; + + return ( +
+
+
+ ); +}; diff --git a/ts/components/session/icon/SessionIcon.tsx b/ts/components/session/icon/SessionIcon.tsx index 1818cf674..8ecaadf9d 100644 --- a/ts/components/session/icon/SessionIcon.tsx +++ b/ts/components/session/icon/SessionIcon.tsx @@ -12,6 +12,7 @@ export type SessionIconProps = { glowDuration?: number; borderRadius?: number; glowStartDelay?: number; + noScale?: boolean; theme?: DefaultTheme; }; @@ -46,6 +47,7 @@ type StyledSvgProps = { borderRadius?: number; glowDuration?: number; glowStartDelay?: number; + noScale?: boolean; iconColor?: string; }; @@ -91,16 +93,22 @@ const animation = (props: { glowDuration?: number; glowStartDelay?: number; iconColor?: string; + noScale?: boolean; }) => { if (props.rotateDuration) { return css` ${rotate} ${props.rotateDuration}s infinite linear; `; - } else if ( - props.glowDuration !== undefined && - props.glowStartDelay !== undefined && - props.iconColor - ) { + } + if (props.noScale) { + return css``; + } + + if (props.glowDuration === 10) { + console.warn('scake', props); + } + + if (props.glowDuration !== undefined && props.glowStartDelay !== undefined && props.iconColor) { return css` ${glow( props.iconColor, @@ -108,9 +116,9 @@ const animation = (props: { props.glowStartDelay )} ${props.glowDuration}s ease infinite; `; - } else { - return; } + + return; }; //tslint:disable no-unnecessary-callback-wrapper @@ -119,6 +127,7 @@ const Svg = styled.svg` transform: ${props => `rotate(${props.iconRotation}deg)`}; animation: ${props => animation(props)}; border-radius: ${props => props.borderRadius}; + filter: ${props => (props.noScale ? `drop-shadow(0px 0px 4px ${props.iconColor})` : '')}; `; //tslint:enable no-unnecessary-callback-wrapper @@ -132,6 +141,7 @@ const SessionSvg = (props: { rotateDuration?: number; glowDuration?: number; glowStartDelay?: number; + noScale?: boolean; borderRadius?: number; theme: DefaultTheme; }) => { @@ -146,6 +156,7 @@ const SessionSvg = (props: { glowDuration: props.glowDuration, glowStartDelay: props.glowStartDelay, iconColor: props.iconColor, + noScale: props.noScale, }; return ( @@ -166,6 +177,7 @@ export const SessionIcon = (props: SessionIconProps) => { glowDuration, borderRadius, glowStartDelay, + noScale, } = props; let { iconSize, iconRotation } = props; iconSize = iconSize || SessionIconSize.Medium; @@ -189,6 +201,7 @@ export const SessionIcon = (props: SessionIconProps) => { rotateDuration={rotateDuration} glowDuration={glowDuration} glowStartDelay={glowStartDelay} + noScale={noScale} borderRadius={borderRadius} iconRotation={iconRotation} iconColor={iconColor} diff --git a/ts/components/session/icon/SessionIconButton.tsx b/ts/components/session/icon/SessionIconButton.tsx index fe4c410e8..495cdce2e 100644 --- a/ts/components/session/icon/SessionIconButton.tsx +++ b/ts/components/session/icon/SessionIconButton.tsx @@ -20,6 +20,9 @@ export const SessionIconButton = (props: SProps) => { isSelected, notificationCount, theme, + glowDuration, + glowStartDelay, + noScale, } = props; const clickHandler = (e: any) => { if (props.onClick) { @@ -42,6 +45,9 @@ export const SessionIconButton = (props: SProps) => { iconColor={iconColor} iconRotation={iconRotation} theme={themeToUSe} + glowDuration={glowDuration} + glowStartDelay={glowStartDelay} + noScale={noScale} /> {Boolean(notificationCount) && }
diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index f31dc6a52..dd59e04a6 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -21,6 +21,7 @@ import { showUpdateGroupNameByConvoId, unblockConvoById, } from '../../../interactions/conversationInteractions'; +import { SessionButtonColor } from '../SessionButton'; function showTimerOptions( isPublic: boolean, @@ -176,9 +177,9 @@ export function getDeleteContactMenuItem( ? window.i18n('leaveGroupConfirmation') : window.i18n('deleteContactConfirmation'), onClickClose, - onClickOk: () => { - void getConversationController().deleteContact(conversationId); - onClickClose(); + okTheme: SessionButtonColor.Danger, + onClickOk: async () => { + await getConversationController().deleteContact(conversationId); }, }) ); diff --git a/ts/components/session/network/SessionOffline.tsx b/ts/components/session/network/SessionOffline.tsx deleted file mode 100644 index 3dbb4a6b4..000000000 --- a/ts/components/session/network/SessionOffline.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; - -// tslint:disable-next-line: no-submodule-imports -import useNetworkState from 'react-use/lib/useNetworkState'; -import styled from 'styled-components'; - -type ContainerProps = { - show: boolean; -}; - -const OfflineContainer = styled.div` - background: ${props => props.theme.colors.accent}; - color: ${props => props.theme.colors.textColor}; - padding: ${props => (props.show ? props.theme.common.margins.sm : '0px')}; - margin: ${props => (props.show ? props.theme.common.margins.xs : '0px')}; - height: ${props => (props.show ? 'auto' : '0px')}; - overflow: hidden; - transition: ${props => props.theme.common.animations.defaultDuration}; -`; - -const OfflineTitle = styled.h3` - padding-top: 0px; - margin-top: 0px; -`; - -const OfflineMessage = styled.div``; - -export const SessionOffline = () => { - const isOnline = useNetworkState().online; - return ( - - {window.i18n('offline')} - {window.i18n('checkNetworkConnection')} - - ); -}; diff --git a/ts/components/session/settings/SessionSettingListItem.tsx b/ts/components/session/settings/SessionSettingListItem.tsx index f205fd487..fe04793e1 100644 --- a/ts/components/session/settings/SessionSettingListItem.tsx +++ b/ts/components/session/settings/SessionSettingListItem.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import classNames from 'classnames'; import Slider from 'rc-slider'; @@ -9,7 +9,7 @@ import { SessionSettingType } from './SessionSettings'; import { SessionRadioGroup } from '../SessionRadioGroup'; import { SessionConfirmDialogProps } from '../SessionConfirm'; -interface Props { +type Props = { title?: string; description?: string; type: SessionSettingType | undefined; @@ -19,112 +19,79 @@ interface Props { onSliderChange?: any; content: any; confirmationDialogParams?: SessionConfirmDialogProps; +}; - // for updating modal in redux - updateConfirmModal?: any; -} - -interface State { - sliderValue: number | null; -} +export const SessionSettingListItem = (props: Props) => { + const handleSlider = (valueToForward: any) => { + if (props.onSliderChange) { + props.onSliderChange(valueToForward); + } -export class SessionSettingListItem extends React.Component { - public static defaultProps = { - inline: true, + setSliderValue(valueToForward); }; - public constructor(props: Props) { - super(props); - this.state = { - sliderValue: null, - }; - - this.handleClick = this.handleClick.bind(this); - } - - public render(): JSX.Element { - const { title, description, type, value, content } = this.props; - const inline = - !!type && ![SessionSettingType.Options, SessionSettingType.Slider].includes(type); + const [sliderValue, setSliderValue] = useState(null); - const currentSliderValue = - type === SessionSettingType.Slider && (this.state.sliderValue || value); + const { title, description, type, value, content } = props; + const inline = !!type && ![SessionSettingType.Options, SessionSettingType.Slider].includes(type); - return ( -
-
-
{title}
+ const currentSliderValue = type === SessionSettingType.Slider && (sliderValue || value); - {description &&
{description}
} -
+ return ( +
+
+
{title}
-
- {type === SessionSettingType.Toggle && ( -
- -
- )} + {description &&
{description}
} +
- {type === SessionSettingType.Button && ( - + {type === SessionSettingType.Toggle && ( +
+ props.onClick?.()} + confirmationDialogParams={props.confirmationDialogParams} /> - )} - - {type === SessionSettingType.Options && ( - { - this.props.onClick(selectedRadioValue); - }} +
+ )} + + {type === SessionSettingType.Button && ( + props.onClick?.()} + /> + )} + + {type === SessionSettingType.Options && ( + { + props.onClick(selectedRadioValue); + }} + /> + )} + + {type === SessionSettingType.Slider && ( +
+ - )} - {type === SessionSettingType.Slider && ( -
- { - this.handleSlider(sliderValue); - }} - /> - -
-

{content.info(currentSliderValue)}

-
+
+

{content.info(currentSliderValue)}

- )} -
+
+ )}
- ); - } - - private handleClick() { - if (this.props.onClick) { - this.props.onClick(); - } - } - - private handleSlider(value: any) { - if (this.props.onSliderChange) { - this.props.onSliderChange(value); - } - - this.setState({ - sliderValue: value, - }); - } -} +
+ ); +}; diff --git a/ts/components/session/settings/SessionSettings.tsx b/ts/components/session/settings/SessionSettings.tsx index d03453754..abcff8bb7 100644 --- a/ts/components/session/settings/SessionSettings.tsx +++ b/ts/components/session/settings/SessionSettings.tsx @@ -40,7 +40,6 @@ export interface SettingsViewProps { // pass the conversation as props, so our render is called everytime they change. // we have to do this to make the list refresh on unblock() conversations?: ConversationLookupType; - updateConfirmModal?: any; } interface State { @@ -156,7 +155,6 @@ class SettingsViewInner extends React.Component { onSliderChange={sliderFn} content={content} confirmationDialogParams={setting.confirmationDialogParams} - updateConfirmModal={this.props.updateConfirmModal} /> )}
diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index ce9b35e1f..2aa01feef 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -36,6 +36,7 @@ import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsMana import { IMAGE_JPEG } from '../types/MIME'; import { FSv2 } from '../fileserver'; import { fromBase64ToArray, toHex } from '../session/utils/String'; +import { SessionButtonColor } from '../components/session/SessionButton'; export const getCompleteUrlForV2ConvoId = async (convoId: string) => { if (convoId.match(openGroupV2ConversationIdRegex)) { @@ -219,8 +220,8 @@ export function showLeaveGroupByConvoId(conversationId: string) { updateConfirmModal({ title, message, - onClickOk: () => { - void conversation.leaveClosedGroup(); + onClickOk: async () => { + await conversation.leaveClosedGroup(); onClickClose(); }, onClickClose, @@ -302,8 +303,8 @@ export function deleteMessagesByConvoIdWithConfirmation(conversationId: string) window?.inboxStore?.dispatch(updateConfirmModal(null)); }; - const onClickOk = () => { - void deleteMessagesByConvoIdNoConfirmation(conversationId); + const onClickOk = async () => { + await deleteMessagesByConvoIdNoConfirmation(conversationId); onClickClose(); }; @@ -312,6 +313,7 @@ export function deleteMessagesByConvoIdWithConfirmation(conversationId: string) title: window.i18n('deleteMessages'), message: window.i18n('deleteConversationConfirmation'), onClickOk, + okTheme: SessionButtonColor.Danger, onClickClose, }) ); diff --git a/ts/interactions/messageInteractions.ts b/ts/interactions/messageInteractions.ts index 8f7aabcaa..bfc21fbd0 100644 --- a/ts/interactions/messageInteractions.ts +++ b/ts/interactions/messageInteractions.ts @@ -173,7 +173,9 @@ const acceptOpenGroupInvitationV2 = (completeUrl: string, roomName?: string) => updateConfirmModal({ title: window.i18n('joinOpenGroupAfterInvitationConfirmationTitle', roomName), message: window.i18n('joinOpenGroupAfterInvitationConfirmationDesc', roomName), - onClickOk: () => joinOpenGroupV2WithUIEvents(completeUrl, true, false), + onClickOk: async () => { + await joinOpenGroupV2WithUIEvents(completeUrl, true, false); + }, onClickClose, }) diff --git a/ts/models/message.ts b/ts/models/message.ts index d86455acb..339058372 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -846,7 +846,7 @@ export class MessageModel extends Backbone.Model { const chatParams = { identifier: this.id, body, - timestamp: this.get('sent_at') || Date.now(), + timestamp: Date.now(), // force a new timestamp to handle user fixed his clock expireTimer: this.get('expireTimer'), attachments, preview, diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index 70096487a..cc951424f 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -318,7 +318,6 @@ async function handleRegularMessage( if (existingExpireTimer) { message.set({ expireTimer: existingExpireTimer }); - message.set({ expirationStartTimestamp: now }); } // Expire timer updates are now explicit. diff --git a/ts/session/onions/onionSend.ts b/ts/session/onions/onionSend.ts index 3af4cd57d..934a18731 100644 --- a/ts/session/onions/onionSend.ts +++ b/ts/session/onions/onionSend.ts @@ -179,7 +179,7 @@ export const sendViaOnion = async ( { retries: 9, // each path can fail 3 times before being dropped, we have 3 paths at most factor: 2, - minTimeout: 1000, + minTimeout: 100, maxTimeout: 4000, onFailedAttempt: e => { window?.log?.warn( diff --git a/ts/session/sending/LokiMessageApi.ts b/ts/session/sending/LokiMessageApi.ts index eb5d7863e..d2ecbb800 100644 --- a/ts/session/sending/LokiMessageApi.ts +++ b/ts/session/sending/LokiMessageApi.ts @@ -70,11 +70,11 @@ export async function sendMessage( ); throw e; } - if (!snode) { + if (!usedNodes || usedNodes.length === 0) { throw new window.textsecure.EmptySwarmError(pubKey, 'Ran out of swarm nodes to query'); - } else { - window?.log?.info( - `loki_message:::sendMessage - Successfully stored message to ${pubKey} via ${snode.ip}:${snode.port}` - ); } + + window?.log?.info( + `loki_message:::sendMessage - Successfully stored message to ${pubKey} via ${snode.ip}:${snode.port}` + ); } diff --git a/ts/session/snode_api/SNodeAPI.ts b/ts/session/snode_api/SNodeAPI.ts index 8df50f5db..911d4a8d3 100644 --- a/ts/session/snode_api/SNodeAPI.ts +++ b/ts/session/snode_api/SNodeAPI.ts @@ -24,11 +24,14 @@ import { toHex, } from '../utils/String'; import { Snode } from '../../data/data'; +import { updateIsOnline } from '../../state/ducks/onion'; // ONS name can have [a-zA-Z0-9_-] except that - is not allowed as start or end // do not define a regex but rather create it on the fly to avoid https://stackoverflow.com/questions/3891641/regex-test-only-works-every-other-time export const onsNameRegex = '^\\w([\\w-]*[\\w])?$'; +export const ERROR_CODE_NO_CONNECT = 'ENETUNREACH: No network connection.'; + const getSslAgentForSeedNode = (seedNodeHost: string, isSsl = false) => { let filePrefix = ''; let pubkey256 = ''; @@ -493,8 +496,8 @@ export async function storeOnNode(targetNode: Snode, params: SendParams): Promis e, `destination ${targetNode.ip}:${targetNode.port}` ); + throw e; } - return false; } /** */ @@ -527,9 +530,12 @@ export async function retrieveNextMessages( try { const json = JSON.parse(result.body); + window.inboxStore?.dispatch(updateIsOnline(true)); + return json.messages || []; } catch (e) { window?.log?.warn('exception while parsing json of nextMessage:', e); + window.inboxStore?.dispatch(updateIsOnline(true)); return []; } @@ -538,6 +544,11 @@ export async function retrieveNextMessages( 'Got an error while retrieving next messages. Not retrying as we trigger fetch often:', e ); + if (e.message === ERROR_CODE_NO_CONNECT) { + window.inboxStore?.dispatch(updateIsOnline(false)); + } else { + window.inboxStore?.dispatch(updateIsOnline(true)); + } return []; } } diff --git a/ts/session/snode_api/onions.ts b/ts/session/snode_api/onions.ts index f8c4b7124..a4e81ed25 100644 --- a/ts/session/snode_api/onions.ts +++ b/ts/session/snode_api/onions.ts @@ -13,6 +13,7 @@ import { hrefPnServerDev, hrefPnServerProd } from '../../pushnotification/PnServ let snodeFailureCount: Record = {}; import { Snode } from '../../data/data'; +import { ERROR_CODE_NO_CONNECT } from './SNodeAPI'; // tslint:disable-next-line: variable-name export const TEST_resetSnodeFailureCount = () => { @@ -37,6 +38,9 @@ export interface SnodeResponse { export const NEXT_NODE_NOT_FOUND_PREFIX = 'Next node not found: '; +export const CLOCK_OUT_OF_SYNC_MESSAGE_ERROR = + 'Your clock is out of sync with the network. Check your clock.'; + // Returns the actual ciphertext, symmetric key that will be used // for decryption, and an ephemeral_key to send to the next hop async function encryptForPubKey(pubKeyX25519hex: string, reqObj: any): Promise { @@ -195,9 +199,8 @@ async function buildOnionGuardNodePayload( function process406Error(statusCode: number) { if (statusCode === 406) { // clock out of sync - console.warn('clock out of sync todo'); // this will make the pRetry stop - throw new pRetry.AbortError('You clock is out of sync with the network. Check your clock.'); + throw new pRetry.AbortError(CLOCK_OUT_OF_SYNC_MESSAGE_ERROR); } } @@ -783,6 +786,7 @@ const sendOnionRequest = async ({ // we are talking to a snode... agent: snodeHttpsAgent, abortSignal, + timeout: 5000, }; const guardUrl = `https://${guardNode.ip}:${guardNode.port}/onion_req/v2`; @@ -859,7 +863,7 @@ export async function lokiOnionFetch( return onionFetchRetryable(targetNode, body, associatedWith); }, { - retries: 9, + retries: 4, factor: 1, minTimeout: 1000, maxTimeout: 2000, @@ -875,6 +879,14 @@ export async function lokiOnionFetch( } catch (e) { window?.log?.warn('onionFetchRetryable failed ', e); // console.warn('error to show to user'); + if (e?.errno === 'ENETUNREACH') { + // better handle the no connection state + throw new Error(ERROR_CODE_NO_CONNECT); + } + if (e?.message === CLOCK_OUT_OF_SYNC_MESSAGE_ERROR) { + window?.log?.warn('Its an clock out of sync error '); + throw new pRetry.AbortError(CLOCK_OUT_OF_SYNC_MESSAGE_ERROR); + } throw e; } } diff --git a/ts/session/snode_api/swarmPolling.ts b/ts/session/snode_api/swarmPolling.ts index 87239c427..40a45c194 100644 --- a/ts/session/snode_api/swarmPolling.ts +++ b/ts/session/snode_api/swarmPolling.ts @@ -51,7 +51,6 @@ export const getSwarmPollingInstance = () => { }; export class SwarmPolling { - private ourPubkey: PubKey | undefined; private groupPolling: Array<{ pubkey: PubKey; lastPolledTimestamp: number }>; private readonly lastHashes: { [key: string]: PubkeyToHash }; @@ -61,7 +60,6 @@ export class SwarmPolling { } public async start(waitForFirstPoll = false): Promise { - this.ourPubkey = UserUtils.getOurPubKeyFromCache(); this.loadGroupIds(); if (waitForFirstPoll) { await this.TEST_pollForAllKeys(); @@ -74,7 +72,6 @@ export class SwarmPolling { * Used fo testing only */ public TEST_reset() { - this.ourPubkey = undefined; this.groupPolling = []; } @@ -88,10 +85,6 @@ export class SwarmPolling { public removePubkey(pk: PubKey | string) { const pubkey = PubKey.cast(pk); window?.log?.info('Swarm removePubkey: removing pubkey from polling', pubkey.key); - - if (this.ourPubkey && PubKey.cast(pk).isEqual(this.ourPubkey)) { - this.ourPubkey = undefined; - } this.groupPolling = this.groupPolling.filter(group => !pubkey.isEqual(group.pubkey)); } @@ -132,9 +125,8 @@ export class SwarmPolling { */ public async TEST_pollForAllKeys() { // we always poll as often as possible for our pubkey - const directPromise = this.ourPubkey - ? this.TEST_pollOnceForKey(this.ourPubkey, false) - : Promise.resolve(); + const ourPubkey = UserUtils.getOurPubKeyFromCache(); + const directPromise = this.TEST_pollOnceForKey(ourPubkey, false); const now = Date.now(); const groupPromises = this.groupPolling.map(async group => { diff --git a/ts/state/ducks/onion.tsx b/ts/state/ducks/onion.tsx index 2c607adf3..4ebedebf2 100644 --- a/ts/state/ducks/onion.tsx +++ b/ts/state/ducks/onion.tsx @@ -3,10 +3,12 @@ import { Snode } from '../../data/data'; export type OnionState = { snodePaths: Array>; + isOnline: boolean; }; export const initialOnionPathState = { snodePaths: new Array>(), + isOnline: false, }; /** @@ -17,12 +19,15 @@ const onionSlice = createSlice({ initialState: initialOnionPathState, reducers: { updateOnionPaths(state: OnionState, action: PayloadAction>>) { - return { snodePaths: action.payload }; + return { ...state, snodePaths: action.payload }; + }, + updateIsOnline(state: OnionState, action: PayloadAction) { + return { ...state, isOnline: action.payload }; }, }, }); // destructures const { actions, reducer } = onionSlice; -export const { updateOnionPaths } = actions; +export const { updateOnionPaths, updateIsOnline } = actions; export const defaultOnionReducer = reducer; diff --git a/ts/state/selectors/onions.ts b/ts/state/selectors/onions.ts index 1e3d95a0d..897e8b3ff 100644 --- a/ts/state/selectors/onions.ts +++ b/ts/state/selectors/onions.ts @@ -21,3 +21,8 @@ export const getFirstOnionPathLength = createSelector( getFirstOnionPath, (state: Array): number => state.length || 0 ); + +export const getIsOnline = createSelector( + getOnionPaths, + (state: OnionState): boolean => state.isOnline +); diff --git a/ts/test/session/unit/onion/OnionErrors_test.ts b/ts/test/session/unit/onion/OnionErrors_test.ts index ef52c2909..44a9a538c 100644 --- a/ts/test/session/unit/onion/OnionErrors_test.ts +++ b/ts/test/session/unit/onion/OnionErrors_test.ts @@ -216,7 +216,7 @@ describe('OnionPathsErrors', () => { throw new Error('Error expected'); } catch (e) { expect(e.message).to.equal( - 'You clock is out of sync with the network. Check your clock.' + 'Your clock is out of sync with the network. Check your clock.' ); // this makes sure that this call would not be retried expect(e.name).to.equal('AbortError'); @@ -237,7 +237,7 @@ describe('OnionPathsErrors', () => { throw new Error('Error expected'); } catch (e) { expect(e.message).to.equal( - 'You clock is out of sync with the network. Check your clock.' + 'Your clock is out of sync with the network. Check your clock.' ); // this makes sure that this call would not be retried expect(e.name).to.equal('AbortError');