diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 121de6fb5..813778600 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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", diff --git a/images/session/brand.svg b/images/session/brand.svg index beb9de0e1..9dec9e8ea 100644 --- a/images/session/brand.svg +++ b/images/session/brand.svg @@ -1,31 +1,2 @@ -image/svg+xml \ No newline at end of file + \ No newline at end of file diff --git a/images/session/session_chat_icon.png b/images/session/session_chat_icon.png deleted file mode 100644 index cb4072089..000000000 Binary files a/images/session/session_chat_icon.png and /dev/null differ diff --git a/images/session/session_icon.svg b/images/session/session_icon.svg new file mode 100644 index 000000000..60cfeb751 --- /dev/null +++ b/images/session/session_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/session/session_icon_1024.png b/images/session/session_icon_1024.png index 5fe3a21d3..39228f4c5 100644 Binary files a/images/session/session_icon_1024.png and b/images/session/session_icon_1024.png differ diff --git a/images/session/session_icon_128.png b/images/session/session_icon_128.png deleted file mode 100644 index e3a410547..000000000 Binary files a/images/session/session_icon_128.png and /dev/null differ diff --git a/images/session/session_icon_256.png b/images/session/session_icon_256.png deleted file mode 100644 index 027e60116..000000000 Binary files a/images/session/session_icon_256.png and /dev/null differ diff --git a/images/session/session_icon_64.png b/images/session/session_icon_64.png deleted file mode 100644 index fedcd0d8d..000000000 Binary files a/images/session/session_icon_64.png and /dev/null differ diff --git a/js/background.js b/js/background.js index fc18b5435..558c143c6 100644 --- a/js/background.js +++ b/js/background.js @@ -450,11 +450,7 @@ }, 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, }); diff --git a/package.json b/package.json index 72a9eb215..a99ffbff1 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/session-file-server b/session-file-server deleted file mode 160000 index 5173163fe..000000000 --- a/session-file-server +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5173163fe18ac575676020e2f8621cf7a2956df3 diff --git a/stylesheets/_session.scss b/stylesheets/_session.scss index 4fbf56b6e..6a3f36333 100644 --- a/stylesheets/_session.scss +++ b/stylesheets/_session.scss @@ -470,6 +470,7 @@ label { } &__close > div { float: left; + padding: $session-margin-xs; margin: 0px; } &__icons > div { @@ -1203,6 +1204,7 @@ input { flex-direction: column; margin: $session-margin-sm; align-items: flex-start; + position: relative; .onion__node { display: flex; @@ -1210,14 +1212,41 @@ input { align-items: center; margin: $session-margin-xs; - * { - margin: $session-margin-sm; + &:nth-child(2) { + margin-top: 0; + } + + &:nth-last-child(2) { + margin-bottom: 0; } .session-icon-button { margin: $session-margin-sm !important; } } + + .onion__node-list-lights { + position: relative; + } + .onion__node__country { + margin: $session-margin-sm; + } + + .onion__growing-icon { + flex-grow: 1; + display: flex; + align-items: center; + } + + .onion__vertical-line { + background: $onionPathLineColor; + position: absolute; + height: calc(100% - 2 * 15px); + margin: 15px calc(100% / 2 - 1px); + + width: 1px; + // z-index: -1; + } } .session-nickname-wrapper { diff --git a/stylesheets/themes.scss b/stylesheets/themes.scss index bd346b42b..97cd50a3f 100644 --- a/stylesheets/themes.scss +++ b/stylesheets/themes.scss @@ -11,6 +11,8 @@ $borderLightTheme: #f1f1f1; // search for references on ts TODO: make this expos $borderDarkTheme: rgba($white, 0.06); $inputBackgroundColor: #8e8e93; +$onionPathLineColor: rgba(#7a7a7a, 0.6); + $borderAvatarColor: unquote( '#00000059' ); // search for references on ts TODO: make this exposed on ts diff --git a/ts/components/OnionStatusPathDialog.tsx b/ts/components/OnionStatusPathDialog.tsx index 93fb7f0f5..72c386aed 100644 --- a/ts/components/OnionStatusPathDialog.tsx +++ b/ts/components/OnionStatusPathDialog.tsx @@ -8,7 +8,6 @@ import Electron from 'electron'; const { shell } = Electron; import { useDispatch, useSelector } from 'react-redux'; -import { StateType } from '../state/reducer'; import { SessionIcon, SessionIconButton, SessionIconSize, SessionIconType } from './session/icon'; import { SessionWrapperModal } from './session/SessionWrapperModal'; @@ -27,6 +26,7 @@ import { // tslint:disable-next-line: no-submodule-imports import useNetworkState from 'react-use/lib/useNetworkState'; import { SessionSpinner } from './session/SessionSpinner'; +import { Flex } from './basic/Flex'; export type StatusLightType = { glowStartDelay: number; @@ -56,53 +56,57 @@ const OnionPathModalInner = () => { <>

{window.i18n('onionPathIndicatorDescription')}

- {nodes.map((snode: Snode | any, index: number) => { - return ( - - ); - })} + +
+
+ + + {nodes.map((_snode: Snode | any, index: number) => { + return ( + + ); + })} + +
+ + {nodes.map((snode: Snode | any, index: number) => { + let labelText = snode.label + ? snode.label + : countryLookup.byIso(ip2country(snode.ip))?.country; + if (!labelText) { + labelText = window.i18n('unknownCountry'); + } + return labelText ?
{labelText}
: null; + })} +
+
); }; export type OnionNodeStatusLightType = { - snode: Snode; - label?: string; glowStartDelay: number; glowDuration: number; }; /** - * Component containing a coloured status light and an adjacent country label. + * Component containing a coloured status light. */ export const OnionNodeStatusLight = (props: OnionNodeStatusLightType): JSX.Element => { - const { snode, label, glowStartDelay, glowDuration } = props; + const { glowStartDelay, glowDuration } = props; const theme = useTheme(); - let labelText = label ? label : countryLookup.byIso(ip2country(snode.ip))?.country; - if (!labelText) { - labelText = window.i18n('unknownCountry'); - } return ( -
- - {labelText ? ( - <> -
{labelText}
- - ) : null} -
+ ); }; @@ -114,15 +118,17 @@ export const ModalStatusLight = (props: StatusLightType) => { const theme = useSelector(getTheme); return ( - +
+ +
); }; @@ -154,7 +160,7 @@ export const ActionPanelOnionStatusLight = (props: { return ( { 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(); diff --git a/ts/components/conversation/ModeratorsRemoveDialog.tsx b/ts/components/conversation/ModeratorsRemoveDialog.tsx index b3c1eb7fb..30d6d10d3 100644 --- a/ts/components/conversation/ModeratorsRemoveDialog.tsx +++ b/ts/components/conversation/ModeratorsRemoveDialog.tsx @@ -205,7 +205,7 @@ export class RemoveModeratorsDialog extends React.Component { if (!res) { window?.log?.warn('failed to remove moderators:', res); - ToastUtils.pushUserNeedsToHaveJoined(); + ToastUtils.pushFailedToRemoveFromModerator(); } else { window?.log?.info(`${removedMods} removed from moderators...`); ToastUtils.pushUserRemovedFromModerators(); diff --git a/ts/components/conversation/Quote.tsx b/ts/components/conversation/Quote.tsx index 539c63f53..ac1904f4c 100644 --- a/ts/components/conversation/Quote.tsx +++ b/ts/components/conversation/Quote.tsx @@ -239,14 +239,7 @@ export const QuoteText = (props: any) => { }; export const QuoteAuthor = (props: any) => { - const { - authorProfileName, - authorPhoneNumber, - authorName, - isFromMe, - isIncoming, - isPublic, - } = props; + const { authorProfileName, authorPhoneNumber, authorName, isFromMe, isIncoming } = props; return (
{ name={authorName} profileName={authorProfileName} compact={true} - shouldShowPubkey={Boolean(isPublic)} + shouldShowPubkey={false} // never show the pubkey for quoted messages author /> )}
diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index 5b3d34982..6c32f113d 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -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(); }; /** diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx index 086c345fd..22df902de 100644 --- a/ts/components/session/conversation/SessionCompositionBox.tsx +++ b/ts/components/session/conversation/SessionCompositionBox.tsx @@ -802,12 +802,6 @@ export class SessionCompositionBox extends React.Component { 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; diff --git a/ts/components/session/icon/Icons.tsx b/ts/components/session/icon/Icons.tsx index e0c9e7daa..39a7ff3d8 100644 --- a/ts/components/session/icon/Icons.tsx +++ b/ts/components/session/icon/Icons.tsx @@ -103,8 +103,12 @@ export const icons = { ratio: 1, }, [SessionIconType.Circle]: { - path: 'M 100, 100m -75, 0a 75,75 0 1,0 150,0a 75,75 0 1,0 -150,0', - viewBox: '0 0 200 200', + path: '\ + M 0, 50\ + a 50,50 0 1,1 100,0\ + a 50,50 0 1,1 -100,0\ + ', + viewBox: '0 0 100 100', ratio: 1, }, [SessionIconType.CircleCheck]: { diff --git a/ts/data/data.ts b/ts/data/data.ts index cb2826eea..2613be494 100644 --- a/ts/data/data.ts +++ b/ts/data/data.ts @@ -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'; diff --git a/ts/interactions/messageInteractions.ts b/ts/interactions/messageInteractions.ts index 8199e427a..8f7aabcaa 100644 --- a/ts/interactions/messageInteractions.ts +++ b/ts/interactions/messageInteractions.ts @@ -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(); diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index aa179de25..55b6544c8 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -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', diff --git a/ts/session/constants.ts b/ts/session/constants.ts index eebeb4120..df97e93d2 100644 --- a/ts/session/constants.ts +++ b/ts/session/constants.ts @@ -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:', diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index fdf7faed1..5d249c1b4 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -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([]); diff --git a/ts/session/messages/MessageController.ts b/ts/session/messages/MessageController.ts index aaa7d37a1..c3e276abe 100644 --- a/ts/session/messages/MessageController.ts +++ b/ts/session/messages/MessageController.ts @@ -69,7 +69,6 @@ export class MessageController { }); } - // tslint:disable-next-line: function-name public get(identifier: string) { return this.messageLookup.get(identifier); } diff --git a/ts/session/snode_api/swarmPolling.ts b/ts/session/snode_api/swarmPolling.ts index ea2512e45..87239c427 100644 --- a/ts/session/snode_api/swarmPolling.ts +++ b/ts/session/snode_api/swarmPolling.ts @@ -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,133 @@ export const getSwarmPollingInstance = () => { }; export class SwarmPolling { - private pubkeys: Array; - private groupPubkeys: Array; + private ourPubkey: PubKey | undefined; + 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 { + this.ourPubkey = UserUtils.getOurPubKeyFromCache(); this.loadGroupIds(); - void this.pollForAllKeys(); + if (waitForFirstPoll) { + await this.TEST_pollForAllKeys(); + } else { + void this.TEST_pollForAllKeys(); + } + } + + /** + * Used fo testing only + */ + public TEST_reset() { + this.ourPubkey = undefined; + this.groupPolling = []; } public addGroupId(pubkey: PubKey) { - if (this.groupPubkeys.findIndex(m => m.key === pubkey.key) === -1) { + if (this.groupPolling.findIndex(m => m.pubkey.key === pubkey.key) === -1) { window?.log?.info('Swarm addGroupId: adding pubkey to polling', pubkey.key); - this.groupPubkeys.push(pubkey); + this.groupPolling.push({ pubkey, lastPolledTimestamp: 0 }); } } - public addPubkey(pk: PubKey | string) { + public removePubkey(pk: PubKey | string) { const pubkey = PubKey.cast(pk); - if (this.pubkeys.findIndex(m => m.key === pubkey.key) === -1) { - this.pubkeys.push(pubkey); + 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)); } - public removePubkey(pk: PubKey | string) { - const pubkey = PubKey.cast(pk); - window?.log?.info('Swarm removePubkey: removing pubkey from polling', pubkey.key); + /** + * 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 directPromise = this.ourPubkey + ? this.TEST_pollOnceForKey(this.ourPubkey, false) + : Promise.resolve(); + + 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 +208,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 +231,7 @@ export class SwarmPolling { // Fetches messages for `pubkey` from `node` potentially updating // the lash hash record - protected async pollNodeForKey(node: Snode, pubkey: PubKey): Promise> { + private async pollNodeForKey(node: Snode, pubkey: PubKey): Promise> { const edkey = node.pubkey_ed25519; const pkStr = pubkey.key; @@ -188,24 +286,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, diff --git a/ts/session/types/PubKey.ts b/ts/session/types/PubKey.ts index 94ef6f686..63adcd9d9 100644 --- a/ts/session/types/PubKey.ts +++ b/ts/session/types/PubKey.ts @@ -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 diff --git a/ts/session/utils/AttachmentsDownload.ts b/ts/session/utils/AttachmentsDownload.ts index 6ae23e6f8..9df66264e 100644 --- a/ts/session/utils/AttachmentsDownload.ts +++ b/ts/session/utils/AttachmentsDownload.ts @@ -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); diff --git a/ts/session/utils/Toast.tsx b/ts/session/utils/Toast.tsx index ba562a35b..c90fbb5bf 100644 --- a/ts/session/utils/Toast.tsx +++ b/ts/session/utils/Toast.tsx @@ -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') - ); -} diff --git a/ts/state/ducks/defaultRooms.tsx b/ts/state/ducks/defaultRooms.tsx index 40dd860e8..ab08894ad 100644 --- a/ts/state/ducks/defaultRooms.tsx +++ b/ts/state/ducks/defaultRooms.tsx @@ -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) { diff --git a/ts/test/session/unit/swarm_polling/SwarmPolling_test.ts b/ts/test/session/unit/swarm_polling/SwarmPolling_test.ts new file mode 100644 index 000000000..5ff3eca58 --- /dev/null +++ b/ts/test/session/unit/swarm_polling/SwarmPolling_test.ts @@ -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; + + 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]); + }); + }); + }); +}); diff --git a/ts/test/session/unit/utils/Password.ts b/ts/test/session/unit/utils/Password_test.ts similarity index 99% rename from ts/test/session/unit/utils/Password.ts rename to ts/test/session/unit/utils/Password_test.ts index a5cca5a39..b1747b0d2 100644 --- a/ts/test/session/unit/utils/Password.ts +++ b/ts/test/session/unit/utils/Password_test.ts @@ -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 => { diff --git a/ts/test/test-utils/utils/pubkey.ts b/ts/test/test-utils/utils/pubkey.ts index 04f17bfc6..b73ac6d43 100644 --- a/ts/test/test-utils/utils/pubkey.ts +++ b/ts/test/test-utils/utils/pubkey.ts @@ -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)); diff --git a/ts/util/blockedNumberController.ts b/ts/util/blockedNumberController.ts index abdfc4b06..d16399d5d 100644 --- a/ts/util/blockedNumberController.ts +++ b/ts/util/blockedNumberController.ts @@ -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'; diff --git a/ts/util/passwordUtils.ts b/ts/util/passwordUtils.ts index d5f5d6f30..d6c12d1e0 100644 --- a/ts/util/passwordUtils.ts +++ b/ts/util/passwordUtils.ts @@ -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; } diff --git a/tslint.json b/tslint.json index 4f804d015..79c9acaf1 100644 --- a/tslint.json +++ b/tslint.json @@ -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]+$" } ],