From 15aa6b5ef969922abee7227f15d19b687d4f92f5 Mon Sep 17 00:00:00 2001
From: Audric Ackermann <audric@loki.network>
Date: Mon, 28 Jun 2021 13:31:21 +1000
Subject: [PATCH 1/7] add loading for leaving opengroup dialog

---
 ts/components/session/SessionConfirm.tsx      |   2 +-
 ts/components/session/SessionToggle.tsx       | 105 +++++-------
 ts/components/session/menu/Menu.tsx           |   7 +-
 .../settings/SessionSettingListItem.tsx       | 161 +++++++-----------
 .../session/settings/SessionSettings.tsx      |   2 -
 ts/interactions/conversationInteractions.ts   |  10 +-
 ts/interactions/messageInteractions.ts        |   4 +-
 7 files changed, 120 insertions(+), 171 deletions(-)

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> | 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<Props, State> {
-  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 (
-      <div
-        className={classNames('session-toggle', this.state.active ? 'active' : '')}
-        role="button"
-        onClick={this.clickHandler}
-      >
-        <div className="knob" />
-      </div>
-    );
-  }
+  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 (
+    <div
+      className={classNames('session-toggle', active ? 'active' : '')}
+      role="button"
+      onClick={clickHandler}
+    >
+      <div className="knob" />
+    </div>
+  );
+};
diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx
index 39b741a86..e7c0bc1b5 100644
--- a/ts/components/session/menu/Menu.tsx
+++ b/ts/components/session/menu/Menu.tsx
@@ -25,6 +25,7 @@ import {
   showUpdateGroupNameByConvoId,
   unblockConvoById,
 } from '../../../interactions/conversationInteractions';
+import { SessionButtonColor } from '../SessionButton';
 
 function showTimerOptions(
   isPublic: boolean,
@@ -162,9 +163,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/settings/SessionSettingListItem.tsx b/ts/components/session/settings/SessionSettingListItem.tsx
index f205fd487..66c18d4fa 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 = (value: any) => {
+    if (props.onSliderChange) {
+      props.onSliderChange(value);
+    }
 
-export class SessionSettingListItem extends React.Component<Props, State> {
-  public static defaultProps = {
-    inline: true,
+    setSliderValue(value);
   };
 
-  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 (
-      <div className={classNames('session-settings-item', inline && 'inline')}>
-        <div className="session-settings-item__info">
-          <div className="session-settings-item__title">{title}</div>
+  const currentSliderValue = type === SessionSettingType.Slider && (sliderValue || value);
 
-          {description && <div className="session-settings-item__description">{description}</div>}
-        </div>
+  return (
+    <div className={classNames('session-settings-item', inline && 'inline')}>
+      <div className="session-settings-item__info">
+        <div className="session-settings-item__title">{title}</div>
 
-        <div className="session-settings-item__content">
-          {type === SessionSettingType.Toggle && (
-            <div className="session-settings-item__selection">
-              <SessionToggle
-                active={Boolean(value)}
-                onClick={this.handleClick}
-                confirmationDialogParams={this.props.confirmationDialogParams}
-                updateConfirmModal={this.props.updateConfirmModal}
-              />
-            </div>
-          )}
+        {description && <div className="session-settings-item__description">{description}</div>}
+      </div>
 
-          {type === SessionSettingType.Button && (
-            <SessionButton
-              text={content.buttonText}
-              buttonColor={content.buttonColor}
-              onClick={this.handleClick}
+      <div className="session-settings-item__content">
+        {type === SessionSettingType.Toggle && (
+          <div className="session-settings-item__selection">
+            <SessionToggle
+              active={Boolean(value)}
+              onClick={() => props.onClick?.()}
+              confirmationDialogParams={props.confirmationDialogParams}
             />
-          )}
-
-          {type === SessionSettingType.Options && (
-            <SessionRadioGroup
-              initialItem={content.options.initalItem}
-              group={content.options.group}
-              items={content.options.items}
-              onClick={(selectedRadioValue: string) => {
-                this.props.onClick(selectedRadioValue);
-              }}
+          </div>
+        )}
+
+        {type === SessionSettingType.Button && (
+          <SessionButton
+            text={content.buttonText}
+            buttonColor={content.buttonColor}
+            onClick={() => props.onClick?.()}
+          />
+        )}
+
+        {type === SessionSettingType.Options && (
+          <SessionRadioGroup
+            initialItem={content.options.initalItem}
+            group={content.options.group}
+            items={content.options.items}
+            onClick={(selectedRadioValue: string) => {
+              props.onClick(selectedRadioValue);
+            }}
+          />
+        )}
+
+        {type === SessionSettingType.Slider && (
+          <div className="slider-wrapper">
+            <Slider
+              dots={true}
+              step={content.step}
+              min={content.min}
+              max={content.max}
+              defaultValue={currentSliderValue}
+              onAfterChange={handleSlider}
             />
-          )}
 
-          {type === SessionSettingType.Slider && (
-            <div className="slider-wrapper">
-              <Slider
-                dots={true}
-                step={content.step}
-                min={content.min}
-                max={content.max}
-                defaultValue={currentSliderValue}
-                onAfterChange={sliderValue => {
-                  this.handleSlider(sliderValue);
-                }}
-              />
-
-              <div className="slider-info">
-                <p>{content.info(currentSliderValue)}</p>
-              </div>
+            <div className="slider-info">
+              <p>{content.info(currentSliderValue)}</p>
             </div>
-          )}
-        </div>
+          </div>
+        )}
       </div>
-    );
-  }
-
-  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,
-    });
-  }
-}
+    </div>
+  );
+};
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<SettingsViewProps, State> {
                     onSliderChange={sliderFn}
                     content={content}
                     confirmationDialogParams={setting.confirmationDialogParams}
-                    updateConfirmModal={this.props.updateConfirmModal}
                   />
                 )}
               </div>
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 8199e427a..50b6d9268 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,
     })

From 5e55c2cfabfbc486e3c3f3102253b439600e117a Mon Sep 17 00:00:00 2001
From: Audric Ackermann <audric@loki.network>
Date: Mon, 28 Jun 2021 15:09:14 +1000
Subject: [PATCH 2/7] fix lint

---
 ts/components/session/settings/SessionSettingListItem.tsx | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/ts/components/session/settings/SessionSettingListItem.tsx b/ts/components/session/settings/SessionSettingListItem.tsx
index 66c18d4fa..fe04793e1 100644
--- a/ts/components/session/settings/SessionSettingListItem.tsx
+++ b/ts/components/session/settings/SessionSettingListItem.tsx
@@ -22,12 +22,12 @@ type Props = {
 };
 
 export const SessionSettingListItem = (props: Props) => {
-  const handleSlider = (value: any) => {
+  const handleSlider = (valueToForward: any) => {
     if (props.onSliderChange) {
-      props.onSliderChange(value);
+      props.onSliderChange(valueToForward);
     }
 
-    setSliderValue(value);
+    setSliderValue(valueToForward);
   };
 
   const [sliderValue, setSliderValue] = useState(null);

From e85f69a144e5e0dc1296382380334f02c70b2b09 Mon Sep 17 00:00:00 2001
From: Audric Ackermann <audric@loki.network>
Date: Tue, 29 Jun 2021 13:48:43 +1000
Subject: [PATCH 3/7] use our retrieve status as isOnline status

---
 js/background.js                              |  1 -
 ts/components/LeftPane.tsx                    |  3 --
 ts/components/OnionStatusPathDialog.tsx       |  8 ++---
 .../session/network/SessionOffline.tsx        | 36 -------------------
 ts/session/onions/onionSend.ts                |  2 +-
 ts/session/snode_api/SNodeAPI.ts              | 11 ++++++
 ts/session/snode_api/onions.ts                |  8 ++++-
 ts/session/snode_api/swarmPolling.ts          | 12 ++-----
 ts/state/ducks/onion.tsx                      |  9 +++--
 ts/state/selectors/onions.ts                  |  5 +++
 10 files changed, 37 insertions(+), 58 deletions(-)
 delete mode 100644 ts/components/session/network/SessionOffline.tsx

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/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 (
     <>
-      <SessionOffline />
       {props.isExpired && <SessionExpiredWarning />}
       <LeftPaneMessageSection
         theme={theme}
@@ -74,7 +72,6 @@ const InnerLeftPaneContactSection = () => {
 
   return (
     <>
-      <SessionOffline />
       <LeftPaneContactSection
         openConversationExternal={(id, messageId) =>
           dispatch(openConversationExternal(id, messageId))
diff --git a/ts/components/OnionStatusPathDialog.tsx b/ts/components/OnionStatusPathDialog.tsx
index 72c386aed..1fef5ad03 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 <SessionSpinner loading={true} />;
   }
 
@@ -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;
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<ContainerProps>`
-  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 (
-    <OfflineContainer show={!isOnline}>
-      <OfflineTitle>{window.i18n('offline')}</OfflineTitle>
-      <OfflineMessage>{window.i18n('checkNetworkConnection')}</OfflineMessage>
-    </OfflineContainer>
-  );
-};
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/snode_api/SNodeAPI.ts b/ts/session/snode_api/SNodeAPI.ts
index 8df50f5db..bc25631d0 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 = '';
@@ -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..1c508fefd 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<string, number> = {};
 
 import { Snode } from '../../data/data';
+import { ERROR_CODE_NO_CONNECT } from './SNodeAPI';
 
 // tslint:disable-next-line: variable-name
 export const TEST_resetSnodeFailureCount = () => {
@@ -783,6 +784,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 +861,7 @@ export async function lokiOnionFetch(
         return onionFetchRetryable(targetNode, body, associatedWith);
       },
       {
-        retries: 9,
+        retries: 4,
         factor: 1,
         minTimeout: 1000,
         maxTimeout: 2000,
@@ -875,6 +877,10 @@ 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);
+    }
     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<void> {
-    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<Array<Snode>>;
+  isOnline: boolean;
 };
 
 export const initialOnionPathState = {
   snodePaths: new Array<Array<Snode>>(),
+  isOnline: false,
 };
 
 /**
@@ -17,12 +19,15 @@ const onionSlice = createSlice({
   initialState: initialOnionPathState,
   reducers: {
     updateOnionPaths(state: OnionState, action: PayloadAction<Array<Array<Snode>>>) {
-      return { snodePaths: action.payload };
+      return { ...state, snodePaths: action.payload };
+    },
+    updateIsOnline(state: OnionState, action: PayloadAction<boolean>) {
+      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<Snode>): number => state.length || 0
 );
+
+export const getIsOnline = createSelector(
+  getOnionPaths,
+  (state: OnionState): boolean => state.isOnline
+);

From 504a9afc0aa61a09242cd117c0ae62d7a9f37a49 Mon Sep 17 00:00:00 2001
From: Audric Ackermann <audric@loki.network>
Date: Tue, 29 Jun 2021 14:55:59 +1000
Subject: [PATCH 4/7] fix up handling of clock out of sync

---
 ts/models/message.ts                 |  2 +-
 ts/session/sending/LokiMessageApi.ts | 10 +++++-----
 ts/session/snode_api/SNodeAPI.ts     |  2 +-
 ts/session/snode_api/onions.ts       | 10 ++++++++--
 4 files changed, 15 insertions(+), 9 deletions(-)

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<MessageAttributes> {
       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/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 bc25631d0..911d4a8d3 100644
--- a/ts/session/snode_api/SNodeAPI.ts
+++ b/ts/session/snode_api/SNodeAPI.ts
@@ -496,8 +496,8 @@ export async function storeOnNode(targetNode: Snode, params: SendParams): Promis
       e,
       `destination ${targetNode.ip}:${targetNode.port}`
     );
+    throw e;
   }
-  return false;
 }
 
 /** */
diff --git a/ts/session/snode_api/onions.ts b/ts/session/snode_api/onions.ts
index 1c508fefd..f73c99acf 100644
--- a/ts/session/snode_api/onions.ts
+++ b/ts/session/snode_api/onions.ts
@@ -38,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 =
+  'You 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<DestinationContext> {
@@ -196,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);
   }
 }
 
@@ -881,6 +883,10 @@ export async function lokiOnionFetch(
       // 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;
   }
 }

From 447f862ace9986d7bf9f66960ef9c567dd102a30 Mon Sep 17 00:00:00 2001
From: Audric Ackermann <audric@loki.network>
Date: Tue, 29 Jun 2021 15:20:21 +1000
Subject: [PATCH 5/7] add some static glowing to the actionpanel light

---
 ts/components/OnionStatusPathDialog.tsx       |  3 +++
 ts/components/session/icon/SessionIcon.tsx    | 27 ++++++++++++++-----
 .../session/icon/SessionIconButton.tsx        |  6 +++++
 ts/session/snode_api/onions.ts                |  2 +-
 .../session/unit/onion/OnionErrors_test.ts    |  4 +--
 5 files changed, 32 insertions(+), 10 deletions(-)

diff --git a/ts/components/OnionStatusPathDialog.tsx b/ts/components/OnionStatusPathDialog.tsx
index 1fef5ad03..a7099318e 100644
--- a/ts/components/OnionStatusPathDialog.tsx
+++ b/ts/components/OnionStatusPathDialog.tsx
@@ -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/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<StyledSvgProps>`
   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) && <SessionNotificationCount count={notificationCount} />}
     </div>
diff --git a/ts/session/snode_api/onions.ts b/ts/session/snode_api/onions.ts
index f73c99acf..a4e81ed25 100644
--- a/ts/session/snode_api/onions.ts
+++ b/ts/session/snode_api/onions.ts
@@ -39,7 +39,7 @@ export interface SnodeResponse {
 export const NEXT_NODE_NOT_FOUND_PREFIX = 'Next node not found: ';
 
 export const CLOCK_OUT_OF_SYNC_MESSAGE_ERROR =
-  'You clock is out of sync with the network. Check your clock';
+  '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
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');

From 52293d6787a051c705e0763da06469270fc91c3b Mon Sep 17 00:00:00 2001
From: Audric Ackermann <audric@loki.network>
Date: Tue, 29 Jun 2021 16:31:57 +1000
Subject: [PATCH 6/7] fix trust do not trigger redownload of already dl medias

---
 package.json                                              | 2 +-
 ts/components/conversation/message/ClickToTrustSender.tsx | 3 +++
 2 files changed, 4 insertions(+), 1 deletion(-)

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

From 9cb69cf8fdd8c553ee1ca32779206231a2209c2d Mon Sep 17 00:00:00 2001
From: audric <audric@loki.network>
Date: Wed, 30 Jun 2021 15:02:22 +1000
Subject: [PATCH 7/7] do not start expire timer before seing message

---
 ts/receiver/queuedJob.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts
index e5fe07924..04c0c719f 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.