Merge pull request #1747 from oxen-io/clearnet

Session 1.6.7
pull/1998/head
Audric Ackermann 4 years ago committed by GitHub
commit cde8a0b3d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -263,9 +263,9 @@
"leaveGroupConfirmationAdmin": "As you are the admin of this group, if you leave it it will be removed for every current members. Are you sure you want to leave this group?",
"cannotRemoveCreatorFromGroup": "Cannot remove this user",
"cannotRemoveCreatorFromGroupDesc": "You cannot remove this user as they are the creator of the group.",
"userNeedsToHaveJoined": "User needs to have joined",
"userNeedsToHaveJoinedDesc": "An error happened. The user needs to have already joined the server for this ADD to work.",
"noContactsForGroup": "You don't have any contacts yet",
"failedToAddAsModerator": "Failed to add user as moderator",
"failedToRemoveFromModerator": "Failed to remove user from the moderator list",
"copyMessage": "Copy message text",
"selectMessage": "Select message",
"editGroup": "Edit group",
@ -381,8 +381,6 @@
"noBlockedContacts": "No blocked contacts",
"userAddedToModerators": "User added to moderator list",
"userRemovedFromModerators": "User removed from moderator list",
"errorHappenedWhileRemovingModerator": "An error happened",
"errorHappenedWhileRemovingModeratorDesc": "An error happened while removing this user from the moderator list.",
"orJoinOneOfThese": "Or join one of these...",
"helpUsTranslateSession": "Help us Translate Session",
"translation": "Translation",

@ -1,31 +1,2 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 404.08533 448.40668"
height="448.40668"
width="404.08533"
xml:space="preserve"
id="svg2"
version="1.1"><metadata
id="metadata8"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs6"><clipPath
id="clipPath18"
clipPathUnits="userSpaceOnUse"><path
id="path16"
d="M 0,336.305 H 303.064 V 0 H 0 Z" /></clipPath></defs><g
transform="matrix(1.3333333,0,0,-1.3333333,0,448.40667)"
id="g10"><g
id="g12"><g
clip-path="url(#clipPath18)"
id="g14"><g
transform="translate(216.4556,21.0229)"
id="g20"><path
id="path22"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 0,0 h -147.251 c -25.182,0 -46.881,19.311 -48.123,44.461 -1.328,26.885 20.162,49.182 46.765,49.182 h 84.804 c 5.189,0 9.395,4.206 9.395,9.394 v 69.229 L 29.025,126.032 C 51.087,113.807 64.975,90.781 65.565,65.646 66.415,29.551 36.104,0 0,0 m -158.872,168.227 c -22.062,12.225 -35.95,35.251 -36.541,60.386 -0.849,36.095 29.462,65.646 65.565,65.646 H 17.403 c 25.182,0 46.881,-19.311 48.124,-44.462 1.328,-26.884 -20.162,-49.181 -46.764,-49.181 0,0 -60.987,-0.01 -84.808,-0.014 -5.186,-0.001 -9.374,-4.206 -9.376,-9.392 l -0.016,-69.217 z M 39.213,144.42 -24.265,179.593 h 43.028 c 37.408,0 67.845,30.434 67.845,67.843 0,37.408 -30.437,67.846 -67.845,67.846 h -150.224 c -46.864,0 -84.995,-38.131 -84.995,-84.995 0,-33.425 18.162,-64.248 47.396,-80.449 l 63.477,-35.172 h -43.026 c -37.41,0 -67.847,-30.435 -67.847,-67.843 0,-37.408 30.437,-67.846 67.847,-67.846 H 1.613 c 46.866,0 84.995,38.13 84.995,84.995 0,33.425 -18.162,64.248 -47.395,80.448" /></g></g></g></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 404.085 448.407"><defs><clipPath id="a" clipPathUnits="userSpaceOnUse"><path d="M0 336.305h303.064V0H0z"/></clipPath></defs><g clip-path="url(#a)" transform="matrix(1.33333 0 0 -1.33333 0 448.407)"><path d="M216.456 21.023H69.205c-25.182 0-46.881 19.31-48.123 44.46-1.328 26.886 20.162 49.183 46.765 49.183h84.804a9.394 9.394 0 019.395 9.394v69.229l83.435-46.234c22.062-12.225 35.95-35.251 36.54-60.386.85-36.095-29.461-65.646-65.565-65.646M57.584 189.25c-22.062 12.225-35.95 35.25-36.541 60.386-.85 36.095 29.462 65.646 65.565 65.646h147.25c25.183 0 46.882-19.311 48.125-44.462 1.328-26.884-20.162-49.181-46.764-49.181l-84.808-.014c-5.186-.001-9.374-4.206-9.376-9.392l-.016-69.217zm198.085-23.807l-63.478 35.173h43.028c37.408 0 67.845 30.434 67.845 67.843 0 37.408-30.437 67.846-67.845 67.846H84.995C38.13 336.305 0 298.174 0 251.31c0-33.425 18.162-64.248 47.396-80.45l63.477-35.171H67.847C30.437 135.689 0 105.254 0 67.846S30.437 0 67.847 0h150.222c46.866 0 84.995 38.13 84.995 84.995 0 33.425-18.162 64.248-47.395 80.448" fill="#fff"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><circle cx="512" cy="512.3" r="511.3" fill="#333132"/><path d="M431 574.8c-.8-7.4-6.7-8.2-10.8-10.6-13.6-7.9-27.5-15.4-41.3-23l-22.5-12.3c-8.5-4.7-17.1-9.2-25.6-14.1-10.5-6-21-11.9-31.1-18.6-18.9-12.5-33.8-29.1-46.3-48.1-8.3-12.6-14.8-26.1-19.2-40.4-6.7-21.7-10.8-44.1-7.8-66.8 1.8-14 4.6-28 9.7-41.6 7.8-20.8 19.3-38.8 34.2-54.8 9.8-10.6 21.2-19.1 33.4-26.8 14.7-9.3 30.7-15.4 47.4-19 13.8-3 28.1-4.3 42.2-4.4 89.9-.4 179.7-.3 269.6 0 12.6 0 25.5 1 37.7 4.1 24.3 6.2 45.7 18.2 63 37 11.2 12.2 20.4 25.8 25.8 41.2 7.3 20.7 12.3 42.1 6.7 64.4-2.1 8.5-2.7 17.5-6.1 25.4-4.7 10.9-10.8 21.2-17.2 31.2-8.7 13.5-20.5 24.3-34.4 32.2-10.1 5.7-21 10.2-32 14.3-18.1 6.7-37.2 5-56.1 5.2-17.2.2-34.5 0-51.7.1-1.7 0-3.4 1.2-5.1 1.9 1.3 1.8 2.1 4.3 3.9 5.3 13.5 7.8 27.2 15.4 40.8 22.9 11 6 22.3 11.7 33.2 17.9 15.2 8.5 30.2 17.4 45.3 26.1 19.3 11.1 34.8 26.4 47.8 44.3 9.7 13.3 17.2 27.9 23 43.5 6.1 16.6 9.2 33.8 10.4 51.3.6 9.1-.7 18.5-1.9 27.6-1.2 9.1-2.7 18.4-5.6 27.1-3.3 10.2-7.4 20.2-12.4 29.6-8.4 15.7-19.6 29.4-32.8 41.4-12.7 11.5-26.8 20.6-42.4 27.6-22.9 10.3-46.9 14.4-71.6 14.5-89.7.3-179.4.2-269.1-.1-12.6 0-25.5-1-37.7-3.9-24.5-5.7-45.8-18-63.3-36.4-11.6-12.3-20.2-26.5-26.6-41.9-2.7-6.4-4.1-13.5-5.4-20.4-1.5-8.1-2.8-16.3-3.1-24.5-.6-15.7 2.8-30.9 8.2-45.4 8.2-22 21.7-40.6 40.2-55.2 10-7.9 21.3-13.7 33.1-18.8 16.6-7.2 34-8.1 51.4-8.5 21.9-.5 43.9-.1 65.9-.1 1.9-.1 3.9-.3 6.2-.4zm96.3-342.4c0 .1 0 .1 0 0-48.3.1-96.6-.6-144.9.5-13.5.3-27.4 3.9-40.1 8.7-14.9 5.6-28.1 14.6-39.9 25.8-20.2 19-32.2 42.2-37.2 68.9-3.6 19-1.4 38.1 4.1 56.5 4.1 13.7 10.5 26.4 18.5 38.4 14.8 22.2 35.7 36.7 58.4 49.2 11 6.1 22.2 11.9 33.2 18 13.5 7.5 26.9 15.1 40.4 22.6 13.1 7.3 26.2 14.5 39.2 21.7 9.7 5.3 19.4 10.7 29.1 16.1 2.9 1.6 4.1.2 4.5-2.4.3-2 .3-4 .3-6.1v-58.8c0-19.9.1-39.9 0-59.8 0-6.6 1.7-12.8 7.6-16.1 3.5-2 8.2-2.8 12.4-2.8 50.3-.2 100.7-.2 151-.1 19.8 0 38.3-4.4 55.1-15.1 23.1-14.8 36.3-36.3 40.6-62.9 3.4-20.8-1-40.9-12.4-58.5-17.8-27.5-43.6-43-76.5-43.6-47.8-.8-95.6-.2-143.4-.2zm-30.6 559.7c45.1 0 90.2-.2 135.3.1 18.9.1 36.6-3.9 53.9-11.1 18.4-7.7 33.6-19.8 46.3-34.9 9.1-10.8 16.2-22.9 20.8-36.5 4.2-12.4 7.4-24.7 7.3-37.9-.1-10.3.2-20.5-3.4-30.5-2.6-7.2-3.4-15.2-6.4-22.1-3.9-8.9-8.9-17.3-14-25.5-12.9-20.8-31.9-34.7-52.8-46.4-10.6-5.9-21.2-11.6-31.8-17.5-10.3-5.7-20.4-11.7-30.7-17.4-11.2-6.1-22.5-11.9-33.7-18-16.6-9.1-33.1-18.4-49.8-27.5-4.9-2.7-6.1-1.9-6.4 3.9-.1 2-.1 4.1-.1 6.1v114.5c0 14.8-5.6 20.4-20.4 20.4-47.6.1-95.3-.1-142.9.2-10.5.1-21.1 1.4-31.6 2.8-16.5 2.2-30.5 9.9-42.8 21-17 15.5-27 34.7-29.4 57.5-1.1 10.9-.4 21.7 2.9 32.5 3.7 12.3 9.2 23.4 17.5 33 19.2 22.1 43.4 33.3 72.7 33.3 46.6.1 93 0 139.5 0z" fill="#00f782"/></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

@ -340,11 +340,6 @@
window.libsession.Utils.ToastUtils.pushSpellCheckDirty();
};
window.toggleLinkPreview = () => {
const newValue = !window.getSettingValue('link-preview-setting');
window.setSettingValue('link-preview-setting', newValue);
};
window.toggleMediaPermissions = () => {
const value = window.getMediaPermissions();
window.setMediaPermissions(!value);
@ -450,11 +445,6 @@
}, window.CONSTANTS.NOTIFICATION_ENABLE_TIMEOUT_SECONDS * 1000);
window.NewReceiver.queueAllCached();
window
.getSwarmPollingInstance()
.addPubkey(window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache());
window.getSwarmPollingInstance().start();
window.libsession.Utils.AttachmentDownloads.start({
logger: window.log,
});

@ -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",
@ -40,7 +40,7 @@
"format-full": "prettier --list-different --write \"*.{css,js,json,scss,ts,tsx}\" \"./**/*.{css,js,json,scss,ts,tsx}\"",
"transpile": "tsc --incremental",
"transpile:watch": "tsc -w",
"clean-transpile": "rimraf ts/**/*.js ts/*.js ts/*.js.map ts/**/*.js.map && rimraf tsconfig.tsbuildinfo;",
"clean-transpile": "rimraf 'ts/**/*.js ts/*.js' 'ts/*.js.map' 'ts/**/*.js.map' && rimraf tsconfig.tsbuildinfo;",
"ready": "yarn clean-transpile; yarn grunt && yarn lint-full && yarn test"
},
"dependencies": {

@ -1 +0,0 @@
Subproject commit 5173163fe18ac575676020e2f8621cf7a2956df3

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

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

@ -47,7 +47,7 @@ export const AddModeratorsDialog = (props: Props) => {
if (!isAdded) {
window?.log?.warn('failed to add moderators:', isAdded);
ToastUtils.pushUserNeedsToHaveJoined();
ToastUtils.pushFailedToAddAsModerator();
} else {
window?.log?.info(`${pubkey.key} added as moderator...`);
ToastUtils.pushUserAddedToModerators();

@ -205,7 +205,7 @@ export class RemoveModeratorsDialog extends React.Component<Props, State> {
if (!res) {
window?.log?.warn('failed to remove moderators:', res);
ToastUtils.pushUserNeedsToHaveJoined();
ToastUtils.pushFailedToRemoveFromModerator();
} else {
window?.log?.info(`${removedMods} removed from moderators...`);
ToastUtils.pushUserRemovedFromModerators();

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

@ -245,9 +245,7 @@ const doAppStartUp = () => {
debounce(triggerAvatarReUploadIfNeeded, 200);
// TODO: Investigate the case where we reconnect
const ourKey = UserUtils.getOurPubKeyStrFromCache();
getSwarmPollingInstance().addPubkey(ourKey);
getSwarmPollingInstance().start();
void getSwarmPollingInstance().start();
};
/**

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

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

@ -37,6 +37,11 @@ import { getMentionsInput } from '../../../state/selectors/mentionsInput';
import { updateConfirmModal } from '../../../state/ducks/modalDialog';
import { SessionButtonColor } from '../SessionButton';
import { SessionConfirmDialogProps } from '../SessionConfirm';
import {
createOrUpdateItem,
getItemById,
hasLinkPreviewPopupBeenDisplayed,
} from '../../../data/data';
export interface ReplyingToMessageProps {
convoId: string;
@ -218,7 +223,7 @@ export class SessionCompositionBox extends React.Component<Props, State> {
imgBlob = item.getAsFile();
break;
case 'text':
this.showLinkSharingConfirmationModalDialog(e);
void this.showLinkSharingConfirmationModalDialog(e);
break;
default:
}
@ -237,18 +242,24 @@ export class SessionCompositionBox extends React.Component<Props, State> {
* Check if what is pasted is a URL and prompt confirmation for a setting change
* @param e paste event
*/
private showLinkSharingConfirmationModalDialog(e: any) {
private async showLinkSharingConfirmationModalDialog(e: any) {
const pastedText = e.clipboardData.getData('text');
if (this.isURL(pastedText)) {
const alreadyDisplayedPopup =
(await getItemById(hasLinkPreviewPopupBeenDisplayed))?.value || false;
window.inboxStore?.dispatch(
updateConfirmModal({
shouldShowConfirm: () => !window.getSettingValue('link-preview-setting'),
shouldShowConfirm: () =>
!window.getSettingValue('link-preview-setting') && !alreadyDisplayedPopup,
title: window.i18n('linkPreviewsTitle'),
message: window.i18n('linkPreviewsConfirmMessage'),
okTheme: SessionButtonColor.Danger,
onClickOk: () => {
window.setSettingValue('link-preview-setting', true);
},
onClickClose: async () => {
await createOrUpdateItem({ id: hasLinkPreviewPopupBeenDisplayed, value: true });
},
})
);
}
@ -802,12 +813,6 @@ export class SessionCompositionBox extends React.Component<Props, State> {
const { isBlocked, isPrivate, left, isKickedFromGroup } = this.props;
// deny sending of message if our app version is expired
if (window.extension.expiredStatus() === true) {
ToastUtils.pushToastError('expiredWarning', window.i18n('expiredWarning'));
return;
}
if (isBlocked && isPrivate) {
ToastUtils.pushUnblockToSend();
return;

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

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

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

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

@ -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<Props, State> {
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 (
<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>
);
};

@ -9,7 +9,11 @@ import { StateType } from '../../../state/reducer';
import { getConversationController } from '../../../session/conversations';
import { getConversationLookup } from '../../../state/selectors/conversations';
import { connect, useSelector } from 'react-redux';
import { getPasswordHash } from '../../../../ts/data/data';
import {
createOrUpdateItem,
getPasswordHash,
hasLinkPreviewPopupBeenDisplayed,
} from '../../../../ts/data/data';
import { SpacerLG, SpacerXS } from '../../basic/Text';
import { shell } from 'electron';
import { SessionConfirmDialogProps } from '../SessionConfirm';
@ -40,7 +44,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 +159,6 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
onSliderChange={sliderFn}
content={content}
confirmationDialogParams={setting.confirmationDialogParams}
updateConfirmModal={this.props.updateConfirmModal}
/>
)}
</div>
@ -341,7 +343,13 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
hidden: false,
type: SessionSettingType.Toggle,
category: SessionSettingCategory.Appearance,
setFn: window.toggleLinkPreview,
setFn: async () => {
const newValue = !window.getSettingValue('link-preview-setting');
window.setSettingValue('link-preview-setting', newValue);
if (!newValue) {
await createOrUpdateItem({ id: hasLinkPreviewPopupBeenDisplayed, value: false });
}
},
content: undefined,
comparisonValue: undefined,
onClick: undefined,

@ -1,7 +1,7 @@
import Electron from 'electron';
const { ipcRenderer } = Electron;
// tslint:disable: function-name no-require-imports no-var-requires one-variable-per-declaration no-void-expression
// tslint:disable: no-require-imports no-var-requires one-variable-per-declaration no-void-expression
import _ from 'lodash';
import { ConversationCollection, ConversationModel } from '../models/conversation';
@ -64,6 +64,7 @@ export type ServerToken = {
export const hasSyncedInitialConfigurationItem = 'hasSyncedInitialConfigurationItem';
export const lastAvatarUploadTimestamp = 'lastAvatarUploadTimestamp';
export const hasLinkPreviewPopupBeenDisplayed = 'hasLinkPreviewPopupBeenDisplayed';
const channelsToMake = {
shutdown,

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

@ -134,7 +134,7 @@ export async function removeSenderFromModerator(sender: string, convoId: string)
if (!res) {
window?.log?.warn('failed to remove moderator:', res);
ToastUtils.pushErrorHappenedWhileRemovingModerator();
ToastUtils.pushFailedToRemoveFromModerator();
} else {
window?.log?.info(`${pubKeyToRemove.key} removed from moderators...`);
ToastUtils.pushUserRemovedFromModerators();
@ -154,7 +154,7 @@ export async function addSenderAsModerator(sender: string, convoId: string) {
if (!res) {
window?.log?.warn('failed to add moderator:', res);
ToastUtils.pushUserNeedsToHaveJoined();
ToastUtils.pushFailedToAddAsModerator();
} else {
window?.log?.info(`${pubKeyToAdd.key} added to moderators...`);
ToastUtils.pushUserAddedToModerators();
@ -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,
})

@ -44,6 +44,7 @@ import { NotificationForConvoOption } from '../components/conversation/Conversat
import { useDispatch } from 'react-redux';
import { updateConfirmModal } from '../state/ducks/modalDialog';
import { createTaskWithTimeout } from '../session/utils/TaskWithTimeout';
import { DURATION, SWARM_POLLING_TIMEOUT } from '../session/constants';
export enum ConversationTypeEnum {
GROUP = 'group',

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

@ -318,7 +318,6 @@ async function handleRegularMessage(
if (existingExpireTimer) {
message.set({ expireTimer: existingExpireTimer });
message.set({ expirationStartTimestamp: now });
}
// Expire timer updates are now explicit.

@ -17,6 +17,12 @@ export const TTL_DEFAULT = {
TTL_MAX: 14 * DURATION.DAYS,
};
export const SWARM_POLLING_TIMEOUT = {
ACTIVE: DURATION.SECONDS * 5,
MEDIUM_ACTIVE: DURATION.SECONDS * 60,
INACTIVE: DURATION.MINUTES * 60,
};
export const PROTOCOLS = {
// tslint:disable-next-line: no-http-string
HTTP: 'http:',

@ -116,7 +116,7 @@ export class ConversationController {
conversation.initialPromise = create();
conversation.initialPromise.then(async () => {
if (window.inboxStore) {
if (window?.inboxStore) {
window.inboxStore?.dispatch(
conversationActions.conversationAdded(conversation.id, conversation.getProps())
);
@ -242,7 +242,7 @@ export class ConversationController {
window.log.info(`deleteContact !isPrivate, convo removed from DB: ${id}`);
this.conversations.remove(conversation);
if (window.inboxStore) {
if (window?.inboxStore) {
window.inboxStore?.dispatch(conversationActions.conversationRemoved(conversation.id));
window.inboxStore?.dispatch(
conversationActions.conversationChanged(conversation.id, conversation.getProps())
@ -310,7 +310,7 @@ export class ConversationController {
public reset() {
this._initialPromise = Promise.resolve();
this._initialFetchComplete = false;
if (window.inboxStore) {
if (window?.inboxStore) {
window.inboxStore?.dispatch(conversationActions.removeAllConversations());
}
this.conversations.reset([]);

@ -69,7 +69,6 @@ export class MessageController {
});
}
// tslint:disable-next-line: function-name
public get(identifier: string) {
return this.messageLookup.get(identifier);
}

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

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

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

@ -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 = () => {
@ -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<DestinationContext> {
@ -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;
}
}

@ -1,5 +1,5 @@
import { PubKey } from '../types';
import { getSwarmFor } from './snodePool';
import * as snodePool from './snodePool';
import { retrieveNextMessages } from './SNodeAPI';
import { SignalService } from '../../protobuf';
import * as Receiver from '../../receiver/receiver';
@ -12,9 +12,10 @@ import {
updateLastHash,
} from '../../../ts/data/data';
import { StringUtils } from '../../session/utils';
import { getConversationController } from '../conversations';
import { StringUtils, UserUtils } from '../../session/utils';
import { ConversationModel } from '../../models/conversation';
import { DURATION, SWARM_POLLING_TIMEOUT } from '../constants';
import { getConversationController } from '../conversations';
type PubkeyToHash = { [key: string]: string };
@ -50,49 +51,125 @@ export const getSwarmPollingInstance = () => {
};
export class SwarmPolling {
private pubkeys: Array<PubKey>;
private groupPubkeys: Array<PubKey>;
private groupPolling: Array<{ pubkey: PubKey; lastPolledTimestamp: number }>;
private readonly lastHashes: { [key: string]: PubkeyToHash };
constructor() {
this.pubkeys = [];
this.groupPubkeys = [];
this.groupPolling = [];
this.lastHashes = {};
}
public start(): void {
public async start(waitForFirstPoll = false): Promise<void> {
this.loadGroupIds();
void this.pollForAllKeys();
if (waitForFirstPoll) {
await this.TEST_pollForAllKeys();
} else {
void this.TEST_pollForAllKeys();
}
}
public addGroupId(pubkey: PubKey) {
if (this.groupPubkeys.findIndex(m => m.key === pubkey.key) === -1) {
window?.log?.info('Swarm addGroupId: adding pubkey to polling', pubkey.key);
this.groupPubkeys.push(pubkey);
}
/**
* Used fo testing only
*/
public TEST_reset() {
this.groupPolling = [];
}
public addPubkey(pk: PubKey | string) {
const pubkey = PubKey.cast(pk);
if (this.pubkeys.findIndex(m => m.key === pubkey.key) === -1) {
this.pubkeys.push(pubkey);
public addGroupId(pubkey: PubKey) {
if (this.groupPolling.findIndex(m => m.pubkey.key === pubkey.key) === -1) {
window?.log?.info('Swarm addGroupId: adding pubkey to polling', pubkey.key);
this.groupPolling.push({ pubkey, lastPolledTimestamp: 0 });
}
}
public removePubkey(pk: PubKey | string) {
const pubkey = PubKey.cast(pk);
window?.log?.info('Swarm removePubkey: removing pubkey from polling', pubkey.key);
this.groupPolling = this.groupPolling.filter(group => !pubkey.isEqual(group.pubkey));
}
/**
* Only public for testing
* As of today, we pull closed group pubkeys as follow:
* if activeAt is not set, poll only once per hour
* if activeAt is less than an hour old, poll every 5 seconds or so
* if activeAt is less than a day old, poll every minutes only.
* If activeAt is more than a day old, poll only once per hour
*/
public TEST_getPollingTimeout(convoId: PubKey) {
const convo = getConversationController().get(convoId.key);
if (!convo) {
return SWARM_POLLING_TIMEOUT.INACTIVE;
}
const activeAt = convo.get('active_at');
if (!activeAt) {
return SWARM_POLLING_TIMEOUT.INACTIVE;
}
const currentTimestamp = Date.now();
this.pubkeys = this.pubkeys.filter(key => !pubkey.isEqual(key));
this.groupPubkeys = this.groupPubkeys.filter(key => !pubkey.isEqual(key));
// consider that this is an active group if activeAt is less than an hour old
if (currentTimestamp - activeAt <= DURATION.HOURS * 1) {
return SWARM_POLLING_TIMEOUT.ACTIVE;
}
if (currentTimestamp - activeAt <= DURATION.DAYS * 1) {
return SWARM_POLLING_TIMEOUT.MEDIUM_ACTIVE;
}
return SWARM_POLLING_TIMEOUT.INACTIVE;
}
protected async pollOnceForKey(pubkey: PubKey, isGroup: boolean) {
/**
* Only public for testing
*/
public async TEST_pollForAllKeys() {
// we always poll as often as possible for our pubkey
const ourPubkey = UserUtils.getOurPubKeyFromCache();
const directPromise = this.TEST_pollOnceForKey(ourPubkey, false);
const now = Date.now();
const groupPromises = this.groupPolling.map(async group => {
const convoPollingTimeout = this.TEST_getPollingTimeout(group.pubkey);
const diff = now - group.lastPolledTimestamp;
const loggingId =
getConversationController()
.get(group.pubkey.key)
?.idForLogging() || group.pubkey.key;
if (diff >= convoPollingTimeout) {
(window?.log?.info || console.warn)(
`Polling for ${loggingId}; timeout: ${convoPollingTimeout} ; diff: ${diff}`
);
return this.TEST_pollOnceForKey(group.pubkey, true);
}
(window?.log?.info || console.warn)(
`Not polling for ${loggingId}; timeout: ${convoPollingTimeout} ; diff: ${diff}`
);
return Promise.resolve();
});
try {
await Promise.all(_.concat(directPromise, groupPromises));
} catch (e) {
(window?.log?.info || console.warn)('pollForAllKeys swallowing exception: ', e);
throw e;
} finally {
setTimeout(this.TEST_pollForAllKeys.bind(this), SWARM_POLLING_TIMEOUT.ACTIVE);
}
}
/**
* Only exposed as public for testing
*/
public async TEST_pollOnceForKey(pubkey: PubKey, isGroup: boolean) {
// NOTE: sometimes pubkey is string, sometimes it is object, so
// accept both until this is fixed:
const pkStr = pubkey.key;
const snodes = await getSwarmFor(pkStr);
const snodes = await snodePool.getSwarmFor(pkStr);
// Select nodes for which we already have lastHashes
const alreadyPolled = snodes.filter((n: Snode) => this.lastHashes[n.pubkey_ed25519]);
@ -123,6 +200,19 @@ export class SwarmPolling {
// Merge results into one list of unique messages
const messages = _.uniqBy(_.flatten(results), (x: any) => x.hash);
if (isGroup) {
// update the last fetched timestamp
this.groupPolling = this.groupPolling.map(group => {
if (PubKey.isEqual(pubkey, group.pubkey)) {
return {
...group,
lastPolledTimestamp: Date.now(),
};
}
return group;
});
}
const newMessages = await this.handleSeenMessages(messages);
newMessages.forEach((m: Message) => {
@ -133,7 +223,7 @@ export class SwarmPolling {
// Fetches messages for `pubkey` from `node` potentially updating
// the lash hash record
protected async pollNodeForKey(node: Snode, pubkey: PubKey): Promise<Array<any>> {
private async pollNodeForKey(node: Snode, pubkey: PubKey): Promise<Array<any>> {
const edkey = node.pubkey_ed25519;
const pkStr = pubkey.key;
@ -188,24 +278,6 @@ export class SwarmPolling {
return newMessages;
}
private async pollForAllKeys() {
const directPromises = this.pubkeys.map(async pk => {
return this.pollOnceForKey(pk, false);
});
const groupPromises = this.groupPubkeys.map(async pk => {
return this.pollOnceForKey(pk, true);
});
try {
await Promise.all(_.concat(directPromises, groupPromises));
} catch (e) {
window?.log?.warn('pollForAllKeys swallowing exception: ', e);
throw e;
} finally {
setTimeout(this.pollForAllKeys.bind(this), 2000);
}
}
private async updateLastHash(
edkey: string,
pubkey: PubKey,

@ -152,6 +152,10 @@ export class PubKey {
return key.replace(PubKey.PREFIX_GROUP_TEXTSECURE, '');
}
public static isEqual(comparator1: PubKey | string, comparator2: PubKey | string) {
return PubKey.cast(comparator1).isEqual(comparator2);
}
public isEqual(comparator: PubKey | string) {
return comparator instanceof PubKey
? this.key === comparator.key

@ -90,7 +90,6 @@ export async function addJob(attachment: any, job: any = {}) {
};
}
// tslint:disable: function-name
async function _tick() {
await _maybeStartJob();
timeout = setTimeout(_tick, TICK_INTERVAL);

@ -191,12 +191,12 @@ export function pushOnlyAdminCanRemove() {
);
}
export function pushUserNeedsToHaveJoined() {
pushToastWarning(
'userNeedsToHaveJoined',
window.i18n('userNeedsToHaveJoined'),
window.i18n('userNeedsToHaveJoinedDesc')
);
export function pushFailedToAddAsModerator() {
pushToastWarning('failedToAddAsModerator', window.i18n('failedToAddAsModerator'));
}
export function pushFailedToRemoveFromModerator() {
pushToastWarning('failedToRemoveFromModerator', window.i18n('failedToRemoveFromModerator'));
}
export function pushUserAddedToModerators() {
@ -210,11 +210,3 @@ export function pushUserRemovedFromModerators() {
export function pushInvalidPubKey() {
pushToastSuccess('invalidPubKey', window.i18n('invalidPubkeyFormat'));
}
export function pushErrorHappenedWhileRemovingModerator() {
pushToastError(
'errorHappenedWhileRemovingModerator',
window.i18n('errorHappenedWhileRemovingModerator'),
window.i18n('errorHappenedWhileRemovingModeratorDesc')
);
}

@ -33,7 +33,6 @@ const defaultRoomsSlice = createSlice({
},
updateDefaultRoomsInProgress(state, action) {
const inProgress = action.payload as boolean;
window?.log?.info('fetching default rooms inProgress?', action.payload);
return { ...state, inProgress };
},
updateDefaultBase64RoomData(state, action: PayloadAction<Base64Update>) {

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

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

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

@ -0,0 +1,307 @@
// tslint:disable: no-implicit-dependencies max-func-body-length no-unused-expression
import chai from 'chai';
import Sinon, * as sinon from 'sinon';
import _, { noop } from 'lodash';
import { describe } from 'mocha';
import chaiAsPromised from 'chai-as-promised';
import { TestUtils } from '../../../test-utils';
import { UserUtils } from '../../../../session/utils';
import { getConversationController } from '../../../../session/conversations';
import * as Data from '../../../../../ts/data/data';
import { getSwarmPollingInstance, SnodePool } from '../../../../session/snode_api';
import { SwarmPolling } from '../../../../session/snode_api/swarmPolling';
import { SWARM_POLLING_TIMEOUT } from '../../../../session/constants';
import {
ConversationCollection,
ConversationModel,
ConversationTypeEnum,
} from '../../../../models/conversation';
import { PubKey } from '../../../../session/types';
// tslint:disable: chai-vague-errors
chai.use(chaiAsPromised as any);
chai.should();
const { expect } = chai;
// tslint:disable-next-line: max-func-body-length
describe('SwarmPolling', () => {
// Initialize new stubbed cache
const sandbox = sinon.createSandbox();
const ourPubkey = TestUtils.generateFakePubKey();
const ourNumber = ourPubkey.key;
let pollOnceForKeySpy: Sinon.SinonSpy<any>;
let swarmPolling: SwarmPolling;
let clock: Sinon.SinonFakeTimers;
beforeEach(async () => {
// Utils Stubs
sandbox.stub(UserUtils, 'getOurPubKeyStrFromCache').returns(ourNumber);
sandbox.stub(Data, 'getAllConversations').resolves(new ConversationCollection());
sandbox.stub(Data, 'getItemById').resolves();
sandbox.stub(Data, 'saveConversation').resolves();
sandbox.stub(Data, 'getSwarmNodesForPubkey').resolves();
sandbox.stub(SnodePool, 'getSwarmFor').resolves([]);
TestUtils.stubWindow('profileImages', { removeImagesNotInArray: noop, hasImage: noop });
TestUtils.stubWindow('inboxStore', undefined);
const convoController = getConversationController();
await convoController.load();
getConversationController().getOrCreate(ourPubkey.key, ConversationTypeEnum.PRIVATE);
swarmPolling = getSwarmPollingInstance();
swarmPolling.TEST_reset();
pollOnceForKeySpy = sandbox.spy(swarmPolling, 'TEST_pollOnceForKey');
clock = sinon.useFakeTimers(Date.now());
});
afterEach(() => {
TestUtils.restoreStubs();
sandbox.restore();
getConversationController().reset();
clock.restore();
});
describe('getPollingTimeout', () => {
it('returns INACTIVE for non existing convo', () => {
const fakeConvo = TestUtils.generateFakePubKey();
expect(swarmPolling.TEST_getPollingTimeout(fakeConvo)).to.eq(SWARM_POLLING_TIMEOUT.INACTIVE);
});
it('returns ACTIVE for convo with less than an hour old activeAt', () => {
const convo = getConversationController().getOrCreate(
TestUtils.generateFakePubKeyStr(),
ConversationTypeEnum.GROUP
);
convo.set('active_at', Date.now() - 3555);
expect(swarmPolling.TEST_getPollingTimeout(PubKey.cast(convo.id))).to.eq(
SWARM_POLLING_TIMEOUT.ACTIVE
);
});
it('returns INACTIVE for convo with undefined activeAt', () => {
const convo = getConversationController().getOrCreate(
TestUtils.generateFakePubKeyStr(),
ConversationTypeEnum.GROUP
);
convo.set('active_at', undefined);
expect(swarmPolling.TEST_getPollingTimeout(PubKey.cast(convo.id))).to.eq(
SWARM_POLLING_TIMEOUT.INACTIVE
);
});
it('returns MEDIUM_ACTIVE for convo with activeAt of less than a day', () => {
const convo = getConversationController().getOrCreate(
TestUtils.generateFakePubKeyStr(),
ConversationTypeEnum.GROUP
);
convo.set('active_at', Date.now() - 1000 * 3600 * 23);
expect(swarmPolling.TEST_getPollingTimeout(PubKey.cast(convo.id))).to.eq(
SWARM_POLLING_TIMEOUT.MEDIUM_ACTIVE
);
});
it('returns INACTIVE for convo with activeAt of more than a day', () => {
const convo = getConversationController().getOrCreate(
TestUtils.generateFakePubKeyStr(),
ConversationTypeEnum.GROUP
);
convo.set('active_at', Date.now() - 1000 * 3600 * 25);
expect(swarmPolling.TEST_getPollingTimeout(PubKey.cast(convo.id))).to.eq(
SWARM_POLLING_TIMEOUT.INACTIVE
);
});
});
describe('pollForAllKeys', () => {
it('does run for our pubkey even if activeAt is really old ', async () => {
const convo = getConversationController().getOrCreate(
ourNumber,
ConversationTypeEnum.PRIVATE
);
convo.set('active_at', Date.now() - 1000 * 3600 * 25);
await swarmPolling.start(true);
expect(pollOnceForKeySpy.callCount).to.eq(1);
expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false]);
});
it('does run for our pubkey even if activeAt is recent ', async () => {
const convo = getConversationController().getOrCreate(
ourNumber,
ConversationTypeEnum.PRIVATE
);
convo.set('active_at', Date.now());
await swarmPolling.start(true);
expect(pollOnceForKeySpy.callCount).to.eq(1);
expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false]);
});
it('does run for group pubkey on start no matter the recent timestamp ', async () => {
const convo = getConversationController().getOrCreate(
TestUtils.generateFakePubKeyStr(),
ConversationTypeEnum.GROUP
);
convo.set('active_at', Date.now());
const groupConvoPubkey = PubKey.cast(convo.id);
swarmPolling.addGroupId(groupConvoPubkey);
await swarmPolling.start(true);
// our pubkey will be polled for, hence the 2
expect(pollOnceForKeySpy.callCount).to.eq(2);
expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false]);
expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true]);
});
it('does run for group pubkey on start no matter the old timestamp ', async () => {
const convo = getConversationController().getOrCreate(
TestUtils.generateFakePubKeyStr(),
ConversationTypeEnum.GROUP
);
convo.set('active_at', 1);
const groupConvoPubkey = PubKey.cast(convo.id);
swarmPolling.addGroupId(groupConvoPubkey);
await swarmPolling.start(true);
// our pubkey will be polled for, hence the 2
expect(pollOnceForKeySpy.callCount).to.eq(2);
expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false]);
expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true]);
});
it('does run for group pubkey on start but not another time if activeAt is old ', async () => {
const convo = getConversationController().getOrCreate(
TestUtils.generateFakePubKeyStr(),
ConversationTypeEnum.GROUP
);
convo.set('active_at', 1);
const groupConvoPubkey = PubKey.cast(convo.id);
swarmPolling.addGroupId(groupConvoPubkey);
await swarmPolling.start(true);
await swarmPolling.TEST_pollForAllKeys();
expect(pollOnceForKeySpy.callCount).to.eq(3);
expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true]);
expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false]);
});
it('does run twice if activeAt less than one hour ', async () => {
const convo = getConversationController().getOrCreate(
TestUtils.generateFakePubKeyStr(),
ConversationTypeEnum.GROUP
);
convo.set('active_at', Date.now());
const groupConvoPubkey = PubKey.cast(convo.id);
swarmPolling.addGroupId(groupConvoPubkey);
await swarmPolling.start(true);
clock.tick(6000);
// no need to do that as the tick will trigger a call in all cases after 5 secs
// await swarmPolling.TEST_pollForAllKeys();
expect(pollOnceForKeySpy.callCount).to.eq(4);
expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true]);
expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false]);
expect(pollOnceForKeySpy.lastCall.args).to.deep.eq([groupConvoPubkey, true]);
});
it('does run once only if activeAt is more than one hour', async () => {
const convo = getConversationController().getOrCreate(
TestUtils.generateFakePubKeyStr(),
ConversationTypeEnum.GROUP
);
convo.set('active_at', Date.now());
const groupConvoPubkey = PubKey.cast(convo.id);
swarmPolling.addGroupId(groupConvoPubkey);
await swarmPolling.start(true);
// more than hour old, we should not tick after just 5 seconds
convo.set('active_at', Date.now() - 3605 * 1000);
clock.tick(6000);
expect(pollOnceForKeySpy.callCount).to.eq(3);
expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true]);
expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false]);
});
it('does run once if activeAt is more than 1 days old ', async () => {
const convo = getConversationController().getOrCreate(
TestUtils.generateFakePubKeyStr(),
ConversationTypeEnum.GROUP
);
convo.set('active_at', Date.now());
const groupConvoPubkey = PubKey.cast(convo.id);
swarmPolling.addGroupId(groupConvoPubkey);
await swarmPolling.start(true);
// more than hour old, we should not tick after just 5 seconds
convo.set('active_at', Date.now() - 25 * 3600 * 1000);
clock.tick(6 * 1000); // active
expect(pollOnceForKeySpy.callCount).to.eq(3);
expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true]);
expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([ourPubkey, false]);
});
describe('multiple runs', () => {
let convo: ConversationModel;
let groupConvoPubkey: PubKey;
beforeEach(async () => {
convo = getConversationController().getOrCreate(
TestUtils.generateFakePubKeyStr(),
ConversationTypeEnum.GROUP
);
convo.set('active_at', Date.now());
groupConvoPubkey = PubKey.cast(convo.id);
swarmPolling.addGroupId(groupConvoPubkey);
await swarmPolling.start(true);
});
it('does run twice if activeAt is more than 1 hour old and we tick more than one minute ', async () => {
pollOnceForKeySpy.resetHistory();
// more than hour old but less than a day, we should tick after just 60 seconds
convo.set('active_at', Date.now() - 3605 * 1000);
clock.tick(61 * 1000); // medium_active
await swarmPolling.TEST_pollForAllKeys();
expect(pollOnceForKeySpy.callCount).to.eq(3);
// first two calls are our pubkey
expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false]);
expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([ourPubkey, false]);
expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([groupConvoPubkey, true]);
});
it('does run twice if activeAt is more than 1 day old and we tick more than one hour ', async () => {
pollOnceForKeySpy.resetHistory();
convo.set('active_at', Date.now() - 25 * 3600 * 1000);
clock.tick(3700 * 1000); // inactive
await swarmPolling.TEST_pollForAllKeys();
expect(pollOnceForKeySpy.callCount).to.eq(3);
// first two calls are our pubkey
expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false]);
expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([ourPubkey, false]);
expect(pollOnceForKeySpy.thirdCall.args).to.deep.eq([groupConvoPubkey, true]);
});
});
});
});

@ -40,6 +40,7 @@ describe('Password Util', () => {
'TiJf@lk^jsO^z8MUn%)[Sd~UPQ)ci9CGS@jb<^',
'$u&%{r]apg#G@3dQdCkB_p8)gxhNFr=K&yfM_M8O&2Z.vQyvx',
'bf^OMnYku*iX;{Piw_0zvz',
'@@@@/???\\4545',
'#'.repeat(50),
];
valid.forEach(pass => {

@ -11,6 +11,15 @@ export function generateFakePubKey(): PubKey {
return new PubKey(pubkeyString);
}
export function generateFakePubKeyStr(): string {
// Generates a mock pubkey for testing
const numBytes = PubKey.PUBKEY_LEN / 2 - 1;
const hexBuffer = crypto.randomBytes(numBytes).toString('hex');
const pubkeyString = `05${hexBuffer}`;
return pubkeyString;
}
export function generateFakeECKeyPair(): ECKeyPair {
const pubkey = generateFakePubKey().toArray();
const privKey = new Uint8Array(crypto.randomBytes(64));

@ -1,4 +1,4 @@
import { createOrUpdateItem, getItemById } from '../../ts/data/data';
import { createOrUpdateItem, getItemById } from '../data/data';
import { PubKey } from '../session/types';
import { UserUtils } from '../session/utils';

@ -33,7 +33,7 @@ export const validatePassword = (phrase: string) => {
}
// Restrict characters to letters, numbers and symbols
const characterRegex = /^[a-zA-Z0-9-!()._`~@#$%^&*+=[\]{}|<>,;: ]+$/;
const characterRegex = /^[a-zA-Z0-9-!?/\\()._`~@#$%^&*+=[\]{}|<>,;: ]+$/;
if (!characterRegex.test(trimmed)) {
return window?.i18n ? window?.i18n('passwordCharacterError') : ERRORS.CHARACTER;
}

1
ts/window.d.ts vendored

@ -59,7 +59,6 @@ declare global {
showResetSessionIdDialog: any;
storage: any;
textsecure: LibTextsecure;
toggleLinkPreview: any;
toggleMediaPermissions: any;
toggleMenuBar: any;
toggleSpellCheck: any;

@ -65,10 +65,10 @@
"function-name": [
true,
{
"function-regex": "^[a-z][\\w\\d]+$",
"method-regex": "^[a-z][\\w\\d]+$",
"private-method-regex": "^[a-z][\\w\\d]+$",
"protected-method-regex": "^[a-z][\\w\\d]+$",
"function-regex": "^(TEST_)?(_)?[a-z][\\w\\d]+$",
"method-regex": "^(TEST_)?(_)?[a-z][\\w\\d]+$",
"private-method-regex": "^(TEST_)?(_)?[a-z][\\w\\d]+$",
"protected-method-regex": "^(TEST_)?(_)?[a-z][\\w\\d]+$",
"static-method-regex": "^[a-zA-Z][\\w\\d]+$"
}
],

Loading…
Cancel
Save