Merge pull request #1054 from loki-project/clearnet
Clearnet --> Master for v1.0.6 #2pull/1390/head v1.0.6
commit
85e5a067c9
@ -0,0 +1,88 @@
|
||||
/* global exports, require */
|
||||
/* eslint-disable strict */
|
||||
|
||||
const { Menu } = require('electron');
|
||||
const osLocale = require('os-locale');
|
||||
|
||||
exports.setup = (browserWindow, messages) => {
|
||||
const { session } = browserWindow.webContents;
|
||||
const userLocale = osLocale.sync().replace(/_/g, '-');
|
||||
const userLocales = [userLocale, userLocale.split('-')[0]];
|
||||
|
||||
const available = session.availableSpellCheckerLanguages;
|
||||
const languages = userLocales.filter(l => available.includes(l));
|
||||
console.log(`spellcheck: user locale: ${userLocale}`);
|
||||
console.log('spellcheck: available spellchecker languages: ', available);
|
||||
console.log('spellcheck: setting languages to: ', languages);
|
||||
session.setSpellCheckerLanguages(languages);
|
||||
|
||||
browserWindow.webContents.on('context-menu', (_event, params) => {
|
||||
const { editFlags } = params;
|
||||
const isMisspelled = Boolean(params.misspelledWord);
|
||||
const showMenu = params.isEditable || editFlags.canCopy;
|
||||
|
||||
// Popup editor menu
|
||||
if (showMenu) {
|
||||
const template = [];
|
||||
|
||||
if (isMisspelled) {
|
||||
if (params.dictionarySuggestions.length > 0) {
|
||||
template.push(
|
||||
...params.dictionarySuggestions.map(label => ({
|
||||
label,
|
||||
click: () => {
|
||||
browserWindow.webContents.replaceMisspelling(label);
|
||||
},
|
||||
}))
|
||||
);
|
||||
} else {
|
||||
template.push({
|
||||
label: messages.contextMenuNoSuggestions.message,
|
||||
enabled: false,
|
||||
});
|
||||
}
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
|
||||
if (params.isEditable) {
|
||||
if (editFlags.canUndo) {
|
||||
template.push({ label: messages.editMenuUndo.message, role: 'undo' });
|
||||
}
|
||||
// This is only ever `true` if undo was triggered via the context menu
|
||||
// (not ctrl/cmd+z)
|
||||
if (editFlags.canRedo) {
|
||||
template.push({ label: messages.editMenuRedo.message, role: 'redo' });
|
||||
}
|
||||
if (editFlags.canUndo || editFlags.canRedo) {
|
||||
template.push({ type: 'separator' });
|
||||
}
|
||||
if (editFlags.canCut) {
|
||||
template.push({ label: messages.editMenuCut.message, role: 'cut' });
|
||||
}
|
||||
}
|
||||
|
||||
if (editFlags.canPaste) {
|
||||
template.push({ label: messages.editMenuPaste.message, role: 'paste' });
|
||||
}
|
||||
|
||||
if (editFlags.canPaste) {
|
||||
template.push({
|
||||
label: messages.editMenuPasteAndMatchStyle.message,
|
||||
role: 'pasteAndMatchStyle',
|
||||
});
|
||||
}
|
||||
|
||||
// Only enable select all in editors because select all in non-editors
|
||||
// results in all the UI being selected
|
||||
if (editFlags.canSelectAll && params.isEditable) {
|
||||
template.push({
|
||||
label: messages.editMenuSelectAll.message,
|
||||
role: 'selectall',
|
||||
});
|
||||
}
|
||||
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
menu.popup(browserWindow);
|
||||
}
|
||||
});
|
||||
};
|
@ -0,0 +1,531 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta content='width=device-width, user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0' name='viewport'>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none';
|
||||
child-src 'self';
|
||||
connect-src 'self' https: wss:;
|
||||
font-src 'self';
|
||||
form-action 'self';
|
||||
frame-src 'none';
|
||||
img-src 'self' blob: data:;
|
||||
media-src 'self' blob:;
|
||||
object-src 'none';
|
||||
script-src 'self' 'unsafe-eval';
|
||||
style-src 'self' 'unsafe-inline';"
|
||||
>
|
||||
<title>Session</title>
|
||||
<link href='images/loki/loki_icon_128.png' rel='shortcut icon'>
|
||||
<link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
|
||||
|
||||
<!--
|
||||
When making changes to these templates, be sure to update test/index.html as well
|
||||
-->
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='app-loading-screen'>
|
||||
<div class='content'>
|
||||
<img src='images/session/full-logo.svg' class='session-full-logo' />
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='conversation-loading-screen'>
|
||||
<div class='content'>
|
||||
<img src='images/session/full-logo.svg' class='session-full-logo' />
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='two-column'>
|
||||
<div class='gutter'>
|
||||
<div class='network-status-container'></div>
|
||||
<div class='left-pane-placeholder'></div>
|
||||
</div>
|
||||
<div id='main-view'>
|
||||
<div class='conversation-stack'>
|
||||
<div class='conversation placeholder'>
|
||||
<div class='conversation-header'></div>
|
||||
<div class='container'>
|
||||
<div class='content'>
|
||||
<img src='images/session/full-logo.svg' class='session-full-logo' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='lightbox-container'></div>
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='scroll-down-button-view'>
|
||||
<button class='text module-scroll-down__button {{ buttonClass }}' alt='{{ moreBelow }}'>
|
||||
<div class='module-scroll-down__icon'></div>
|
||||
</button>
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='last-seen-indicator-view'>
|
||||
<div class='module-last-seen-indicator__bar'/>
|
||||
<div class='module-last-seen-indicator__text'>
|
||||
{{ unreadMessages }}
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='expired_alert'>
|
||||
<a target='_blank' href='https://getsession.org/'>
|
||||
<button class='upgrade'>{{ upgrade }}</button>
|
||||
</a>
|
||||
<span>{{ expiredWarning }}</span>
|
||||
<br clear="both">
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='banner'>
|
||||
<div class='body'>
|
||||
<span class='icon warning'></span>
|
||||
{{ message }}
|
||||
<span class='icon dismiss'></span>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='conversation'>
|
||||
<div class='conversation-content'>
|
||||
<div class='conversation-content-left'>
|
||||
<div class='conversation-header'></div>
|
||||
<div class='main panel'>
|
||||
<div class='discussion-container'>
|
||||
<div class='bar-container hide'>
|
||||
<div class='bar active progress-bar-striped progress-bar'></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='bottom-bar' id='footer'>
|
||||
<div class='emoji-panel-container'></div>
|
||||
<div class='member-list-container'></div>
|
||||
<div id='bulk-edit-view'></div>
|
||||
<div class='attachment-list'></div>
|
||||
<div class='compose'>
|
||||
<form class='send clearfix file-input'>
|
||||
<div class='flex'>
|
||||
<div id='choose-file' class='choose-file'>
|
||||
<button class='paperclip thumbnail' {{#disable-inputs}} disabled="disabled" {{/disable-inputs}}></button>
|
||||
<input type='file' class='file-input' multiple='multiple'>
|
||||
</div>
|
||||
<div class='capture-audio'>
|
||||
<button class='microphone' {{#disable-inputs}} disabled="disabled" {{/disable-inputs}}></button>
|
||||
</div>
|
||||
<div class='send-message-container'>
|
||||
<textarea maxlength='2000' class='send-message' placeholder='{{ send-message }}' rows='1' dir='auto' {{#disable-inputs}} disabled="disabled" {{/disable-inputs}}></textarea>
|
||||
</div>
|
||||
<button class='emoji' {{#disable-inputs}} disabled="disabled" {{/disable-inputs}}></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='conversation-content-right'>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='message-list'>
|
||||
<div class='messages'></div>
|
||||
<div class='typing-container'></div>
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='recorder'>
|
||||
<button class='finish'><span class='icon'></span></button>
|
||||
<span class='time'>0:00</span>
|
||||
<button class='close'><span class='icon'></span></button>
|
||||
</script>
|
||||
<script type='text/x-tmpl-mustache' id='password-dialog'>
|
||||
<div class="content">
|
||||
{{ #title }}
|
||||
<h4>{{ title }}</h4>
|
||||
{{ /title }}
|
||||
<input type='password' id='password' placeholder='Password' autofocus>
|
||||
<input type='password' id='password-confirmation' placeholder='Type in your password again' autofocus>
|
||||
<div class='error'></div>
|
||||
<div class='buttons'>
|
||||
<button class='cancel' tabindex='2'>{{ cancel }}</button>
|
||||
<button class='ok' tabindex='1'>{{ ok }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
<script type='text/x-tmpl-mustache' id='identicon-svg'>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'>
|
||||
<circle cx='50' cy='50' r='40' fill='{{ color }}' />
|
||||
<text text-anchor='middle' fill='white' font-family='sans-serif' font-size='24px' x='50' y='50' baseline-shift='-8px'>
|
||||
{{ content }}
|
||||
</text>
|
||||
</svg>
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='phone-number'>
|
||||
<div class='phone-input-form'>
|
||||
<div class='number-container'>
|
||||
<input type='tel' class='number' placeholder="Phone Number" />
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='file-size-modal'>
|
||||
{{ file-size-warning }}
|
||||
({{ limit }}{{ units }})
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='attachment-type-modal'>
|
||||
Sorry, your attachment has a type, {{type}}, that is not currently supported.
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='group-member-list'>
|
||||
<div class='container'>
|
||||
{{ #summary }} <div class='summary'>{{ summary }}</div>{{ /summary }}
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='key-verification'>
|
||||
<div class='container'>
|
||||
{{ ^hasTheirKey }}
|
||||
<div class='placeholder'>{{ theirKeyUnknown }}</div>
|
||||
{{ /hasTheirKey }}
|
||||
{{ #hasTheirKey }}
|
||||
<label> {{ yourSafetyNumberWith }} </label>
|
||||
<!--<div class='qr'></div>-->
|
||||
<div class='key'>
|
||||
{{ #chunks }} <span>{{ . }}</span> {{ /chunks }}
|
||||
</div>
|
||||
{{ /hasTheirKey }}
|
||||
{{ verifyHelp }}
|
||||
<p> {{> link_to_support }} </p>
|
||||
<div class='summary'>
|
||||
{{ #isVerified }}
|
||||
<span class='icon verified'></span>
|
||||
{{ /isVerified }}
|
||||
{{ ^isVerified }}
|
||||
<span class='icon shield'></span>
|
||||
{{ /isVerified }}
|
||||
{{ verifiedStatus }}
|
||||
</div>
|
||||
<div class='verify'>
|
||||
<button class='verify grey'>
|
||||
{{ verifyButton }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='clear-data'>
|
||||
{{#isStep1}}
|
||||
<div class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<span class='banner-icon alert-outline-red'></span>
|
||||
<div class='header'>{{ header }}</div>
|
||||
<div class='body-text-wide'>{{ body }}</div>
|
||||
</div>
|
||||
<div class='nav'>
|
||||
<div>
|
||||
<a class='button neutral cancel'>{{ cancelButton }}</a>
|
||||
<a class='button destructive delete-all-data'>{{ deleteButton }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/isStep1}}
|
||||
{{#isStep2}}
|
||||
<div id='step3' class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<span class='banner-icon delete'></span>
|
||||
<div class='header'>{{ deleting }}</div>
|
||||
</div>
|
||||
<div class='progress'>
|
||||
<div class='bar-container'>
|
||||
<div class='bar progress-bar progress-bar-striped active'></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/isStep2}}
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='networkStatus'>
|
||||
<div class='network-status-message'>
|
||||
<h3>{{ message }}</h3>
|
||||
<span>{{ instructions }}</span>
|
||||
</div>
|
||||
{{ #reconnectDurationAsSeconds }}
|
||||
<div class="network-status-message">
|
||||
{{ attemptingReconnectionMessage }}
|
||||
</div>
|
||||
{{/reconnectDurationAsSeconds }}
|
||||
{{ #action }}
|
||||
<div class="action">
|
||||
<button class='small blue {{ buttonClass }}'>{{ action }}</button>
|
||||
</div>
|
||||
{{/action }}
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='import-flow-template'>
|
||||
{{#isStep2}}
|
||||
<div id='step2' class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<span class='banner-icon folder-outline'></span>
|
||||
<div class='header'>{{ chooseHeader }}</div>
|
||||
<div class='body-text'>{{ choose }}</div>
|
||||
</div>
|
||||
<div class='nav'>
|
||||
<div>
|
||||
<a class='button choose'>{{ chooseButton }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/isStep2}}
|
||||
{{#isStep3}}
|
||||
<div id='step3' class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<span class='banner-icon import'></span>
|
||||
<div class='header'>{{ importingHeader }}</div>
|
||||
</div>
|
||||
<div class='progress'>
|
||||
<div class='bar-container'>
|
||||
<div class='bar progress-bar progress-bar-striped active'></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/isStep3}}
|
||||
{{#isStep4}}
|
||||
<div id='step4' class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<span class='banner-icon check-circle-outline'></span>
|
||||
<div class='header'>{{ completeHeader }}</div>
|
||||
</div>
|
||||
<div class='nav'>
|
||||
{{#restartButton}}
|
||||
<div>
|
||||
<a class='button restart'>{{ restartButton }}</a>
|
||||
</div>
|
||||
{{/restartButton}}
|
||||
{{#registerButton}}
|
||||
<div>
|
||||
<a class='button register'>{{ registerButton }}</a>
|
||||
</div>
|
||||
{{/registerButton}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/isStep4}}
|
||||
|
||||
{{#isError}}
|
||||
<div id='error' class='step'>
|
||||
<div class='inner error-dialog clearfix'>
|
||||
<div class='step-body'>
|
||||
<span class='banner-icon alert-outline'></span>
|
||||
<div class='header'>{{ errorHeader }}</div>
|
||||
<div class='body-text-wide'>
|
||||
{{ errorMessageFirst }}
|
||||
<p>{{ errorMessageSecond }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class='nav'>
|
||||
<div>
|
||||
<a class='button choose'>{{ chooseButton }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/isError}}
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='link-flow-template'>
|
||||
{{#isStep3}}
|
||||
<div id='step3' class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<div class='header'>{{ linkYourPhone }}</div>
|
||||
<div id="qr">
|
||||
<div class='container'>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='nav'>
|
||||
<div class='instructions'>
|
||||
<div class='android'>
|
||||
<div class='label'>
|
||||
<span class='os-icon android'></span>
|
||||
</div>
|
||||
<div class='body'>
|
||||
<div>→ {{ signalSettings }}</div>
|
||||
<div>→ {{ linkedDevices }}</div>
|
||||
<div>→ {{ androidFinalStep }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='apple'>
|
||||
<div class='label'>
|
||||
<span class='os-icon apple'></span>
|
||||
</div>
|
||||
<div class='body'>
|
||||
<div>→ {{ signalSettings }}</div>
|
||||
<div>→ {{ linkedDevices }}</div>
|
||||
<div>→ {{ appleFinalStep }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/isStep3}}
|
||||
{{#isStep4}}
|
||||
<form id='link-phone'>
|
||||
<div id='step4' class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<span class='banner-icon lead-pencil'></span>
|
||||
<div class='header'>{{ chooseName }}</div>
|
||||
<div>
|
||||
<input type='text' class='device-name' spellcheck='false' maxlength='50' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='nav'>
|
||||
<div>
|
||||
<a class='button finish'>{{ finishLinkingPhoneButton }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{{/isStep4}}
|
||||
{{#isStep5}}
|
||||
<div id='step5' class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<span class='banner-icon sync'></span>
|
||||
<div class='header'>{{ syncing }}</div>
|
||||
</div>
|
||||
<div class='progress'>
|
||||
<div class='bar-container'>
|
||||
<div class='bar progress-bar progress-bar-striped active'></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/isStep5}}
|
||||
|
||||
{{#isError}}
|
||||
<div id='error' class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<span class='banner-icon alert-outline'></span>
|
||||
<div class='header'>{{ errorHeader }}</div>
|
||||
<div class='body'>{{ errorMessage }}</div>
|
||||
</div>
|
||||
<div class='nav'>
|
||||
<a class='button try-again'>{{ errorButton }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/isError}}
|
||||
</script>
|
||||
|
||||
|
||||
<script type='text/javascript' src='js/components.js'></script>
|
||||
<script type='text/javascript' src='js/reliable_trigger.js'></script>
|
||||
<script type='text/javascript' src='js/database.js'></script>
|
||||
<script type='text/javascript' src='js/storage.js'></script>
|
||||
<script type='text/javascript' src='js/legacy_storage.js'></script>
|
||||
<script type='text/javascript' src='js/signal_protocol_store.js'></script>
|
||||
<script type='text/javascript' src='js/libtextsecure.js'></script>
|
||||
<script type='text/javascript' src='js/libloki.js'></script>
|
||||
|
||||
<script type='text/javascript' src='js/focus_listener.js'></script>
|
||||
<script type='text/javascript' src='js/notifications.js'></script>
|
||||
<script type='text/javascript' src='js/delivery_receipts.js'></script>
|
||||
<script type='text/javascript' src='js/read_receipts.js'></script>
|
||||
<script type='text/javascript' src='js/read_syncs.js'></script>
|
||||
<script type='text/javascript' src='js/libphonenumber-util.js'></script>
|
||||
<script type='text/javascript' src='js/models/messages.js'></script>
|
||||
<script type='text/javascript' src='js/models/conversations.js'></script>
|
||||
<script type='text/javascript' src='js/models/blockedNumbers.js'></script>
|
||||
<script type='text/javascript' src='js/expiring_messages.js'></script>
|
||||
|
||||
<script type='text/javascript' src='js/chromium.js'></script>
|
||||
<script type='text/javascript' src='js/registration.js'></script>
|
||||
<script type='text/javascript' src='js/expire.js'></script>
|
||||
<script type='text/javascript' src='js/conversation_controller.js'></script>
|
||||
<script type='text/javascript' src='js/blocked_number_controller.js'></script>
|
||||
<script type='text/javascript' src='js/message_controller.js'></script>
|
||||
|
||||
<script type='text/javascript' src='js/views/react_wrapper_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/whisper_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/last_seen_indicator_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/scroll_down_button_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/toast_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/session_toast_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/conversation_loading_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/session_toggle_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/session_modal_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/session_dropdown_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/session_confirm_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/file_input_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/list_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/contact_list_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/message_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/key_verification_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/message_list_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/member_list_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/bulk_edit_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/group_member_list_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/recorder_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/conversation_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/inbox_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/network_status_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/confirmation_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/nickname_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/password_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/seed_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/qr_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/connecting_to_server_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/beta_release_disclaimer_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/identicon_svg_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/install_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/banner_view.js'></script>
|
||||
<script type="text/javascript" src="js/views/phone-input-view.js"></script>
|
||||
<script type='text/javascript' src='js/views/session_registration_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/app_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/import_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/device_pairing_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/device_pairing_words_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/create_group_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/confirm_session_reset_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/edit_profile_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/invite_friends_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/moderators_add_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/moderators_remove_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/user_details_dialog_view.js'></script>
|
||||
|
||||
<script type='text/javascript' src='js/wall_clock_listener.js'></script>
|
||||
<script type='text/javascript' src='js/rotate_signed_prekey_listener.js'></script>
|
||||
<script type='text/javascript' src='js/keychange_listener.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class='app-loading-screen'>
|
||||
<div class='content'>
|
||||
<img src='images/session/full-logo.svg' class='session-full-logo' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type='text/javascript' src='js/background.js'></script>
|
||||
</body>
|
||||
</html>
|
@ -1,16 +0,0 @@
|
||||
{
|
||||
"storageProfile": "development1",
|
||||
"localServerPort": "8082",
|
||||
"seedNodeList": [
|
||||
{
|
||||
"ip": "public.loki.foundation",
|
||||
"port": "38157"
|
||||
},
|
||||
{
|
||||
"ip": "storage.testnetseed1.loki.network",
|
||||
"port": "38157"
|
||||
}
|
||||
],
|
||||
"openDevTools": true,
|
||||
"defaultPublicChatServer": "https://chat-dev.lokinet.org/"
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
{
|
||||
"storageProfile": "development2",
|
||||
"localServerPort": "8083",
|
||||
"seedNodeList": [
|
||||
{
|
||||
"ip": "public.loki.foundation",
|
||||
"port": "38157"
|
||||
},
|
||||
{
|
||||
"ip": "storage.testnetseed1.loki.network",
|
||||
"port": "38157"
|
||||
}
|
||||
],
|
||||
"openDevTools": true,
|
||||
"defaultPublicChatServer": "https://chat-dev.lokinet.org/"
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"storageProfile": "devprod1Profile",
|
||||
"localServerPort": "8082",
|
||||
"openDevTools": true,
|
||||
"updatesEnabled": false
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
{
|
||||
"storageProfile": "devprodProfile",
|
||||
"openDevTools": true,
|
||||
"updatesEnabled": false
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"openDevTools": true,
|
||||
"updatesEnabled": false
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"storageProfile": "staging",
|
||||
"openDevTools": true
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
{
|
||||
"storageProfile": "swarm-testing2",
|
||||
"seedNodeList": [
|
||||
{
|
||||
"ip": "localhost",
|
||||
"port": "22129"
|
||||
}
|
||||
],
|
||||
"openDevTools": true,
|
||||
"defaultPublicChatServer": "https://team-chat.lokinet.org/"
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"openDevTools": false,
|
||||
"updatesEnabled": false
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
/* eslint-disable func-names */
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
const common = require('./common');
|
||||
const { afterEach, beforeEach, describe, it } = require('mocha');
|
||||
const ConversationPage = require('./page-objects/conversation.page');
|
||||
|
||||
describe('Add friends', function() {
|
||||
let app;
|
||||
let app2;
|
||||
this.timeout(60000);
|
||||
this.slow(15000);
|
||||
|
||||
beforeEach(async () => {
|
||||
await common.killallElectron();
|
||||
await common.stopStubSnodeServer();
|
||||
|
||||
const app1Props = {
|
||||
mnemonic: common.TEST_MNEMONIC1,
|
||||
displayName: common.TEST_DISPLAY_NAME1,
|
||||
stubSnode: true,
|
||||
};
|
||||
|
||||
const app2Props = {
|
||||
mnemonic: common.TEST_MNEMONIC2,
|
||||
displayName: common.TEST_DISPLAY_NAME2,
|
||||
stubSnode: true,
|
||||
};
|
||||
|
||||
[app, app2] = await Promise.all([
|
||||
common.startAndStub(app1Props),
|
||||
common.startAndStub2(app2Props),
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await common.stopApp(app);
|
||||
await common.killallElectron();
|
||||
await common.stopStubSnodeServer();
|
||||
});
|
||||
|
||||
it('addFriends: can add a friend by sessionID', async () => {
|
||||
const textMessage = common.generateSendMessageText();
|
||||
|
||||
await app.client.element(ConversationPage.contactsButtonSection).click();
|
||||
await app.client.element(ConversationPage.addContactButton).click();
|
||||
await app.client.isExisting(ConversationPage.leftPaneOverlay).should
|
||||
.eventually.be.true;
|
||||
|
||||
await common.setValueWrapper(
|
||||
app,
|
||||
ConversationPage.sessionIDInput,
|
||||
common.TEST_PUBKEY2
|
||||
);
|
||||
await app.client
|
||||
.element(ConversationPage.sessionIDInput)
|
||||
.getValue()
|
||||
.should.eventually.equal(common.TEST_PUBKEY2);
|
||||
|
||||
await app.client.element(ConversationPage.nextButton).click();
|
||||
await app.client.waitForExist(
|
||||
ConversationPage.sendFriendRequestTextarea,
|
||||
1000
|
||||
);
|
||||
|
||||
// send a text message to that user (will be a friend request)
|
||||
await app.client
|
||||
.element(ConversationPage.sendFriendRequestTextarea)
|
||||
.setValue(textMessage);
|
||||
await app.client.keys('Enter');
|
||||
await app.client.waitForExist(
|
||||
ConversationPage.existingFriendRequestText(textMessage),
|
||||
1000
|
||||
);
|
||||
|
||||
// assure friend request message has been sent
|
||||
await common.timeout(3000);
|
||||
await app.client.isExisting(ConversationPage.retrySendButton).should
|
||||
.eventually.be.false;
|
||||
|
||||
// wait for left notification Friend Request count to go to 1 and click it
|
||||
await app2.client.waitForExist(
|
||||
ConversationPage.oneNotificationFriendRequestLeft,
|
||||
5000
|
||||
);
|
||||
await app2.client
|
||||
.element(ConversationPage.oneNotificationFriendRequestLeft)
|
||||
.click();
|
||||
// open the dropdown from the top friend request count
|
||||
await app2.client.isExisting(
|
||||
ConversationPage.oneNotificationFriendRequestTop
|
||||
).should.eventually.be.true;
|
||||
await app2.client
|
||||
.element(ConversationPage.oneNotificationFriendRequestTop)
|
||||
.click();
|
||||
|
||||
// we should have our app1 friend request here
|
||||
await app2.client.isExisting(
|
||||
ConversationPage.friendRequestFromUser(
|
||||
common.TEST_DISPLAY_NAME1,
|
||||
common.TEST_PUBKEY1
|
||||
)
|
||||
).should.eventually.be.true;
|
||||
await app2.client.isExisting(ConversationPage.acceptFriendRequestButton)
|
||||
.should.eventually.be.true;
|
||||
|
||||
// accept the friend request and validate that on both side the "accepted FR" message is shown
|
||||
await app2.client
|
||||
.element(ConversationPage.acceptFriendRequestButton)
|
||||
.click();
|
||||
await app2.client.waitForExist(
|
||||
ConversationPage.acceptedFriendRequestMessage,
|
||||
1000
|
||||
);
|
||||
await app.client.waitForExist(
|
||||
ConversationPage.acceptedFriendRequestMessage,
|
||||
5000
|
||||
);
|
||||
});
|
||||
});
|
@ -0,0 +1,129 @@
|
||||
/* eslint-disable func-names */
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
const common = require('./common');
|
||||
const { afterEach, beforeEach, describe, it } = require('mocha');
|
||||
const ConversationPage = require('./page-objects/conversation.page');
|
||||
|
||||
describe('Closed groups', function() {
|
||||
let app;
|
||||
let app2;
|
||||
this.timeout(60000);
|
||||
this.slow(15000);
|
||||
|
||||
beforeEach(async () => {
|
||||
await common.killallElectron();
|
||||
await common.stopStubSnodeServer();
|
||||
|
||||
[app, app2] = await common.startAppsAsFriends();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await common.stopApp(app);
|
||||
await common.killallElectron();
|
||||
await common.stopStubSnodeServer();
|
||||
});
|
||||
|
||||
it('closedGroup: can create a closed group with a friend and send/receive a message', async () => {
|
||||
await app.client.element(ConversationPage.globeButtonSection).click();
|
||||
await app.client.element(ConversationPage.createClosedGroupButton).click();
|
||||
|
||||
// fill the groupname
|
||||
await common.setValueWrapper(
|
||||
app,
|
||||
ConversationPage.closedGroupNameTextarea,
|
||||
common.VALID_CLOSED_GROUP_NAME1
|
||||
);
|
||||
await app.client
|
||||
.element(ConversationPage.closedGroupNameTextarea)
|
||||
.getValue()
|
||||
.should.eventually.equal(common.VALID_CLOSED_GROUP_NAME1);
|
||||
|
||||
await app.client
|
||||
.element(ConversationPage.createClosedGroupMemberItem)
|
||||
.isVisible();
|
||||
|
||||
// select the first friend as a member of the groups being created
|
||||
await app.client
|
||||
.element(ConversationPage.createClosedGroupMemberItem)
|
||||
.click();
|
||||
await app.client
|
||||
.element(ConversationPage.createClosedGroupMemberItemSelected)
|
||||
.isVisible();
|
||||
|
||||
// trigger the creation of the group
|
||||
await app.client
|
||||
.element(ConversationPage.validateCreationClosedGroupButton)
|
||||
.click();
|
||||
|
||||
await app.client.waitForExist(
|
||||
ConversationPage.sessionToastGroupCreatedSuccess,
|
||||
1000
|
||||
);
|
||||
await app.client.isExisting(
|
||||
ConversationPage.headerTitleGroupName(common.VALID_CLOSED_GROUP_NAME1)
|
||||
).should.eventually.be.true;
|
||||
await app.client
|
||||
.element(ConversationPage.headerTitleMembers(2))
|
||||
.isVisible();
|
||||
|
||||
// validate overlay is closed
|
||||
await app.client.isExisting(ConversationPage.leftPaneOverlay).should
|
||||
.eventually.be.false;
|
||||
|
||||
// move back to the conversation section
|
||||
await app.client
|
||||
.element(ConversationPage.conversationButtonSection)
|
||||
.click();
|
||||
|
||||
// validate open chat has been added
|
||||
await app.client.isExisting(
|
||||
ConversationPage.rowOpenGroupConversationName(
|
||||
common.VALID_CLOSED_GROUP_NAME1
|
||||
)
|
||||
).should.eventually.be.true;
|
||||
|
||||
// next check app2 has been invited and has the group in its conversations
|
||||
await app2.client.waitForExist(
|
||||
ConversationPage.rowOpenGroupConversationName(
|
||||
common.VALID_CLOSED_GROUP_NAME1
|
||||
),
|
||||
6000
|
||||
);
|
||||
// open the closed group conversation on app2
|
||||
await app2.client
|
||||
.element(ConversationPage.conversationButtonSection)
|
||||
.click();
|
||||
await common.timeout(500);
|
||||
await app2.client
|
||||
.element(
|
||||
ConversationPage.rowOpenGroupConversationName(
|
||||
common.VALID_CLOSED_GROUP_NAME1
|
||||
)
|
||||
)
|
||||
.click();
|
||||
|
||||
// send a message from app and validate it is received on app2
|
||||
const textMessage = common.generateSendMessageText();
|
||||
await app.client
|
||||
.element(ConversationPage.sendMessageTextarea)
|
||||
.setValue(textMessage);
|
||||
await app.client
|
||||
.element(ConversationPage.sendMessageTextarea)
|
||||
.getValue()
|
||||
.should.eventually.equal(textMessage);
|
||||
// send the message
|
||||
await app.client.keys('Enter');
|
||||
|
||||
// validate that the message has been added to the message list view
|
||||
await app.client.waitForExist(
|
||||
ConversationPage.existingSendMessageText(textMessage),
|
||||
2000
|
||||
);
|
||||
|
||||
// validate that the message has been added to the message list view
|
||||
await app2.client.waitForExist(
|
||||
ConversationPage.existingReceivedMessageText(textMessage),
|
||||
5000
|
||||
);
|
||||
});
|
||||
});
|
@ -0,0 +1,498 @@
|
||||
/* eslint-disable no-console */
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
/* eslint-disable prefer-destructuring */
|
||||
|
||||
const { Application } = require('spectron');
|
||||
const path = require('path');
|
||||
|
||||
const chai = require('chai');
|
||||
const chaiAsPromised = require('chai-as-promised');
|
||||
const RegistrationPage = require('./page-objects/registration.page');
|
||||
const ConversationPage = require('./page-objects/conversation.page');
|
||||
const { exec } = require('child_process');
|
||||
const url = require('url');
|
||||
const http = require('http');
|
||||
const fse = require('fs-extra');
|
||||
|
||||
chai.should();
|
||||
chai.use(chaiAsPromised);
|
||||
chai.config.includeStack = true;
|
||||
|
||||
const STUB_SNODE_SERVER_PORT = 3000;
|
||||
const ENABLE_LOG = false;
|
||||
|
||||
module.exports = {
|
||||
/* ************** USERS ****************** */
|
||||
TEST_MNEMONIC1:
|
||||
'faxed mechanic mocked agony unrest loincloth pencil eccentric boyfriend oasis speedy ribbon faxed',
|
||||
TEST_PUBKEY1:
|
||||
'0552b85a43fb992f6bdb122a5a379505a0b99a16f0628ab8840249e2a60e12a413',
|
||||
TEST_DISPLAY_NAME1: 'integration_tester_1',
|
||||
|
||||
TEST_MNEMONIC2:
|
||||
'guide inbound jerseys bays nouns basin sulking awkward stockpile ostrich ascend pylons ascend',
|
||||
TEST_PUBKEY2:
|
||||
'054e1ca8681082dbd9aad1cf6fc89a32254e15cba50c75b5a73ac10a0b96bcbd2a',
|
||||
TEST_DISPLAY_NAME2: 'integration_tester_2',
|
||||
|
||||
/* ************** OPEN GROUPS ****************** */
|
||||
VALID_GROUP_URL: 'https://chat.getsession.org',
|
||||
VALID_GROUP_URL2: 'https://chat-dev.lokinet.org',
|
||||
VALID_GROUP_NAME: 'Session Public Chat',
|
||||
VALID_GROUP_NAME2: 'Loki Dev Chat',
|
||||
|
||||
/* ************** CLOSED GROUPS ****************** */
|
||||
VALID_CLOSED_GROUP_NAME1: 'Closed Group 1',
|
||||
|
||||
USER_DATA_ROOT_FOLDER: '',
|
||||
|
||||
async timeout(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
},
|
||||
|
||||
// a wrapper to work around electron/spectron bug
|
||||
async setValueWrapper(app, selector, value) {
|
||||
await app.client.element(selector).click();
|
||||
// keys, setValue and addValue hang on certain platforms
|
||||
// could put a branch here to use one of those
|
||||
// if we know what platforms are good and which ones are broken
|
||||
await app.client.execute(
|
||||
(slctr, val) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
const iter = document.evaluate(
|
||||
slctr,
|
||||
// eslint-disable-next-line no-undef
|
||||
document,
|
||||
null,
|
||||
// eslint-disable-next-line no-undef
|
||||
XPathResult.UNORDERED_NODE_ITERATOR_TYPE,
|
||||
null
|
||||
);
|
||||
const elem = iter.iterateNext();
|
||||
if (elem) {
|
||||
elem.value = val;
|
||||
} else {
|
||||
console.error('Cant find', slctr, elem, iter);
|
||||
}
|
||||
},
|
||||
selector,
|
||||
value
|
||||
);
|
||||
// let session js detect the text change
|
||||
await app.client.element(selector).click();
|
||||
},
|
||||
|
||||
async startApp(environment = 'test-integration-session') {
|
||||
const env = environment.startsWith('test-integration')
|
||||
? 'test-integration'
|
||||
: environment;
|
||||
const instance = environment.replace('test-integration-', '');
|
||||
|
||||
const app1 = new Application({
|
||||
path: path.join(__dirname, '..', 'node_modules', '.bin', 'electron'),
|
||||
args: ['.'],
|
||||
env: {
|
||||
NODE_ENV: env,
|
||||
NODE_APP_INSTANCE: instance,
|
||||
USE_STUBBED_NETWORK: true,
|
||||
ELECTRON_ENABLE_LOGGING: true,
|
||||
ELECTRON_ENABLE_STACK_DUMPING: true,
|
||||
ELECTRON_DISABLE_SANDBOX: 1,
|
||||
},
|
||||
requireName: 'electronRequire',
|
||||
// chromeDriverLogPath: '../chromedriverlog.txt',
|
||||
chromeDriverArgs: [
|
||||
`remote-debugging-port=${Math.floor(
|
||||
Math.random() * (9999 - 9000) + 9000
|
||||
)}`,
|
||||
],
|
||||
});
|
||||
|
||||
chaiAsPromised.transferPromiseness = app1.transferPromiseness;
|
||||
|
||||
await app1.start();
|
||||
await app1.client.waitUntilWindowLoaded();
|
||||
|
||||
return app1;
|
||||
},
|
||||
|
||||
async startApp2() {
|
||||
const app2 = await this.startApp('test-integration-session-2');
|
||||
return app2;
|
||||
},
|
||||
|
||||
async stopApp(app1) {
|
||||
if (app1 && app1.isRunning()) {
|
||||
await app1.stop();
|
||||
}
|
||||
},
|
||||
|
||||
async killallElectron() {
|
||||
// rtharp - my 2nd client on MacOs needs: pkill -f "node_modules/.bin/electron"
|
||||
// node_modules/electron/dist/electron is node_modules/electron/dist/Electron.app on MacOS
|
||||
const killStr =
|
||||
process.platform === 'win32'
|
||||
? 'taskkill /im electron.exe /t /f'
|
||||
: 'pkill -f "node_modules/electron/dist/electron" | pkill -f "node_modules/.bin/electron"';
|
||||
return new Promise(resolve => {
|
||||
exec(killStr, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
resolve({ stdout, stderr });
|
||||
} else {
|
||||
resolve({ stdout, stderr });
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async rmFolder(folder) {
|
||||
await fse.remove(folder);
|
||||
},
|
||||
|
||||
async startAndAssureCleanedApp2() {
|
||||
const app2 = await this.startAndAssureCleanedApp(
|
||||
'test-integration-session-2'
|
||||
);
|
||||
return app2;
|
||||
},
|
||||
|
||||
async startAndAssureCleanedApp(env = 'test-integration-session') {
|
||||
const userData = path.join(this.USER_DATA_ROOT_FOLDER, `Session-${env}`);
|
||||
|
||||
await this.rmFolder(userData);
|
||||
|
||||
const app1 = await this.startApp(env);
|
||||
await app1.client.waitForExist(
|
||||
RegistrationPage.registrationTabSignIn,
|
||||
4000
|
||||
);
|
||||
|
||||
return app1;
|
||||
},
|
||||
|
||||
async startAndStub({
|
||||
mnemonic,
|
||||
displayName,
|
||||
stubSnode = false,
|
||||
stubOpenGroups = false,
|
||||
env = 'test-integration-session',
|
||||
}) {
|
||||
const app = await this.startAndAssureCleanedApp(env);
|
||||
|
||||
if (stubSnode) {
|
||||
await this.startStubSnodeServer();
|
||||
this.stubSnodeCalls(app);
|
||||
}
|
||||
|
||||
if (stubOpenGroups) {
|
||||
this.stubOpenGroupsCalls(app);
|
||||
}
|
||||
|
||||
if (mnemonic && displayName) {
|
||||
await this.restoreFromMnemonic(app, mnemonic, displayName);
|
||||
// not sure we need this - rtharp.
|
||||
await this.timeout(2000);
|
||||
}
|
||||
|
||||
return app;
|
||||
},
|
||||
|
||||
async startAndStub2(props) {
|
||||
const app2 = await this.startAndStub({
|
||||
env: 'test-integration-session-2',
|
||||
...props,
|
||||
});
|
||||
|
||||
return app2;
|
||||
},
|
||||
|
||||
async restoreFromMnemonic(app, mnemonic, displayName) {
|
||||
await app.client.element(RegistrationPage.registrationTabSignIn).click();
|
||||
await app.client.element(RegistrationPage.restoreFromSeedMode).click();
|
||||
await this.setValueWrapper(
|
||||
app,
|
||||
RegistrationPage.recoveryPhraseInput,
|
||||
mnemonic
|
||||
);
|
||||
|
||||
await this.setValueWrapper(
|
||||
app,
|
||||
RegistrationPage.displayNameInput,
|
||||
displayName
|
||||
);
|
||||
|
||||
await app.client.element(RegistrationPage.continueSessionButton).click();
|
||||
await app.client.waitForExist(
|
||||
RegistrationPage.conversationListContainer,
|
||||
4000
|
||||
);
|
||||
},
|
||||
|
||||
async startAppsAsFriends() {
|
||||
const app1Props = {
|
||||
mnemonic: this.TEST_MNEMONIC1,
|
||||
displayName: this.TEST_DISPLAY_NAME1,
|
||||
stubSnode: true,
|
||||
};
|
||||
|
||||
const app2Props = {
|
||||
mnemonic: this.TEST_MNEMONIC2,
|
||||
displayName: this.TEST_DISPLAY_NAME2,
|
||||
stubSnode: true,
|
||||
};
|
||||
|
||||
const [app1, app2] = await Promise.all([
|
||||
this.startAndStub(app1Props),
|
||||
this.startAndStub2(app2Props),
|
||||
]);
|
||||
|
||||
/** add each other as friends */
|
||||
const textMessage = this.generateSendMessageText();
|
||||
|
||||
await app1.client.element(ConversationPage.contactsButtonSection).click();
|
||||
await app1.client.element(ConversationPage.addContactButton).click();
|
||||
|
||||
await this.setValueWrapper(
|
||||
app1,
|
||||
ConversationPage.sessionIDInput,
|
||||
this.TEST_PUBKEY2
|
||||
);
|
||||
await app1.client.element(ConversationPage.nextButton).click();
|
||||
await app1.client.waitForExist(
|
||||
ConversationPage.sendFriendRequestTextarea,
|
||||
1000
|
||||
);
|
||||
|
||||
// send a text message to that user (will be a friend request)
|
||||
await this.setValueWrapper(
|
||||
app1,
|
||||
ConversationPage.sendFriendRequestTextarea,
|
||||
textMessage
|
||||
);
|
||||
await app1.client.keys('Enter');
|
||||
await app1.client.waitForExist(
|
||||
ConversationPage.existingFriendRequestText(textMessage),
|
||||
1000
|
||||
);
|
||||
|
||||
// wait for left notification Friend Request count to go to 1 and click it
|
||||
await app2.client.waitForExist(
|
||||
ConversationPage.oneNotificationFriendRequestLeft,
|
||||
5000
|
||||
);
|
||||
await app2.client
|
||||
.element(ConversationPage.oneNotificationFriendRequestLeft)
|
||||
.click();
|
||||
// open the dropdown from the top friend request count
|
||||
await app2.client.isExisting(
|
||||
ConversationPage.oneNotificationFriendRequestTop
|
||||
).should.eventually.be.true;
|
||||
await app2.client
|
||||
.element(ConversationPage.oneNotificationFriendRequestTop)
|
||||
.click();
|
||||
|
||||
// accept the friend request and validate that on both side the "accepted FR" message is shown
|
||||
await app2.client
|
||||
.element(ConversationPage.acceptFriendRequestButton)
|
||||
.click();
|
||||
await app2.client.waitForExist(
|
||||
ConversationPage.acceptedFriendRequestMessage,
|
||||
1000
|
||||
);
|
||||
await app1.client.waitForExist(
|
||||
ConversationPage.acceptedFriendRequestMessage,
|
||||
5000
|
||||
);
|
||||
|
||||
return [app1, app2];
|
||||
},
|
||||
|
||||
async linkApp2ToApp(app1, app2) {
|
||||
// app needs to be logged in as user1 and app2 needs to be logged out
|
||||
// start the pairing dialog for the first app
|
||||
await app1.client.element(ConversationPage.settingsButtonSection).click();
|
||||
await app1.client.element(ConversationPage.deviceSettingsRow).click();
|
||||
|
||||
await app1.client.isVisible(ConversationPage.noPairedDeviceMessage);
|
||||
// we should not find the linkDeviceButtonDisabled button (as DISABLED)
|
||||
await app1.client.isExisting(ConversationPage.linkDeviceButtonDisabled)
|
||||
.should.eventually.be.false;
|
||||
await app1.client.element(ConversationPage.linkDeviceButton).click();
|
||||
|
||||
// validate device pairing dialog is shown and has a qrcode
|
||||
await app1.client.isVisible(ConversationPage.devicePairingDialog);
|
||||
await app1.client.isVisible(ConversationPage.qrImageDiv);
|
||||
|
||||
// next trigger the link request from the app2 with the app1 pubkey
|
||||
await app2.client.element(RegistrationPage.registrationTabSignIn).click();
|
||||
await app2.client.element(RegistrationPage.linkDeviceMode).click();
|
||||
|
||||
await this.setValueWrapper(
|
||||
app2,
|
||||
RegistrationPage.textareaLinkDevicePubkey,
|
||||
this.TEST_PUBKEY1
|
||||
);
|
||||
await app2.client.element(RegistrationPage.linkDeviceTriggerButton).click();
|
||||
await app1.client.waitForExist(RegistrationPage.toastWrapper, 7000);
|
||||
let secretWordsapp1 = await app1.client
|
||||
.element(RegistrationPage.secretToastDescription)
|
||||
.getText();
|
||||
secretWordsapp1 = secretWordsapp1.split(': ')[1];
|
||||
|
||||
await app2.client.waitForExist(RegistrationPage.toastWrapper, 6000);
|
||||
await app2.client
|
||||
.element(RegistrationPage.secretToastDescription)
|
||||
.getText()
|
||||
.should.eventually.be.equal(secretWordsapp1);
|
||||
await app1.client.element(ConversationPage.allowPairingButton).click();
|
||||
await app1.client.element(ConversationPage.okButton).click();
|
||||
// validate device paired in settings list with correct secrets
|
||||
await app1.client.waitForExist(
|
||||
ConversationPage.devicePairedDescription(secretWordsapp1),
|
||||
2000
|
||||
);
|
||||
|
||||
await app1.client.isExisting(ConversationPage.unpairDeviceButton).should
|
||||
.eventually.be.true;
|
||||
await app1.client.isExisting(ConversationPage.linkDeviceButtonDisabled)
|
||||
.should.eventually.be.true;
|
||||
|
||||
// validate app2 (secondary device) is linked successfully
|
||||
await app2.client.waitForExist(
|
||||
RegistrationPage.conversationListContainer,
|
||||
4000
|
||||
);
|
||||
|
||||
// validate primary pubkey of app2 is the same that in app1
|
||||
await app2.webContents
|
||||
.executeJavaScript("window.storage.get('primaryDevicePubKey')")
|
||||
.should.eventually.be.equal(this.TEST_PUBKEY1);
|
||||
},
|
||||
|
||||
async triggerUnlinkApp2FromApp(app1, app2) {
|
||||
// check app2 is loggedin
|
||||
await app2.client.isExisting(RegistrationPage.conversationListContainer)
|
||||
.should.eventually.be.true;
|
||||
|
||||
await app1.client.element(ConversationPage.settingsButtonSection).click();
|
||||
await app1.client.element(ConversationPage.deviceSettingsRow).click();
|
||||
await app1.client.isExisting(ConversationPage.linkDeviceButtonDisabled)
|
||||
.should.eventually.be.true;
|
||||
// click the unlink button
|
||||
await app1.client.element(ConversationPage.unpairDeviceButton).click();
|
||||
await app1.client.element(ConversationPage.validateUnpairDevice).click();
|
||||
|
||||
await app1.client.waitForExist(
|
||||
ConversationPage.noPairedDeviceMessage,
|
||||
2000
|
||||
);
|
||||
await app1.client.element(ConversationPage.linkDeviceButton).isEnabled()
|
||||
.should.eventually.be.true;
|
||||
|
||||
// let time to app2 to catch the event and restart dropping its data
|
||||
await this.timeout(5000);
|
||||
|
||||
// check that the app restarted
|
||||
// (did not find a better way than checking the app no longer being accessible)
|
||||
let isApp2Joinable = true;
|
||||
try {
|
||||
await app2.client.isExisting(RegistrationPage.registrationTabSignIn)
|
||||
.should.eventually.be.true;
|
||||
} catch (err) {
|
||||
// if we get an error here, it means Spectron is lost.
|
||||
// this is a good thing because it means app2 restarted
|
||||
isApp2Joinable = false;
|
||||
}
|
||||
|
||||
if (isApp2Joinable) {
|
||||
throw new Error(
|
||||
'app2 is still joinable so it did not restart, so it did not unlink correctly'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
generateSendMessageText: () =>
|
||||
`Test message from integration tests ${Date.now()}`,
|
||||
|
||||
stubOpenGroupsCalls: app1 => {
|
||||
app1.webContents.executeJavaScript(
|
||||
'window.LokiAppDotNetServerAPI = window.StubAppDotNetAPI;'
|
||||
);
|
||||
},
|
||||
|
||||
stubSnodeCalls(app1) {
|
||||
app1.webContents.executeJavaScript(
|
||||
'window.LokiMessageAPI = window.StubMessageAPI;'
|
||||
);
|
||||
},
|
||||
|
||||
logsContainsString: async (app1, str) => {
|
||||
const logs = JSON.stringify(await app1.client.getRenderProcessLogs());
|
||||
return logs.includes(str);
|
||||
},
|
||||
|
||||
async startStubSnodeServer() {
|
||||
if (!this.stubSnode) {
|
||||
this.messages = {};
|
||||
this.stubSnode = http.createServer((request, response) => {
|
||||
const { query } = url.parse(request.url, true);
|
||||
const { pubkey, data, timestamp } = query;
|
||||
|
||||
if (pubkey) {
|
||||
if (request.method === 'POST') {
|
||||
if (ENABLE_LOG) {
|
||||
console.warn('POST', [data, timestamp]);
|
||||
}
|
||||
|
||||
let ori = this.messages[pubkey];
|
||||
if (!this.messages[pubkey]) {
|
||||
ori = [];
|
||||
}
|
||||
|
||||
this.messages[pubkey] = [...ori, { data, timestamp }];
|
||||
|
||||
response.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
response.end();
|
||||
} else {
|
||||
const retrievedMessages = { messages: this.messages[pubkey] };
|
||||
if (ENABLE_LOG) {
|
||||
console.warn('GET', pubkey, retrievedMessages);
|
||||
}
|
||||
if (this.messages[pubkey]) {
|
||||
response.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
response.write(JSON.stringify(retrievedMessages));
|
||||
this.messages[pubkey] = [];
|
||||
}
|
||||
response.end();
|
||||
}
|
||||
}
|
||||
response.end();
|
||||
});
|
||||
this.stubSnode.listen(STUB_SNODE_SERVER_PORT);
|
||||
} else {
|
||||
this.messages = {};
|
||||
}
|
||||
},
|
||||
|
||||
async stopStubSnodeServer() {
|
||||
if (this.stubSnode) {
|
||||
this.stubSnode.close();
|
||||
this.stubSnode = null;
|
||||
}
|
||||
},
|
||||
|
||||
// async killStubSnodeServer() {
|
||||
// return new Promise(resolve => {
|
||||
// exec(
|
||||
// `lsof -ti:${STUB_SNODE_SERVER_PORT} |xargs kill -9`,
|
||||
// (err, stdout, stderr) => {
|
||||
// if (err) {
|
||||
// resolve({ stdout, stderr });
|
||||
// } else {
|
||||
// resolve({ stdout, stderr });
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
// });
|
||||
// },
|
||||
};
|
@ -0,0 +1,23 @@
|
||||
/* eslint-disable no-console */
|
||||
/* eslint-disable more/no-then */
|
||||
/* eslint-disable global-require */
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
|
||||
const { before } = require('mocha');
|
||||
const common = require('./common');
|
||||
|
||||
require('./registration_test');
|
||||
require('./open_group_test');
|
||||
require('./add_friends_test');
|
||||
require('./link_device_test');
|
||||
require('./closed_group_test');
|
||||
|
||||
before(async () => {
|
||||
// start the app once before all tests to get the platform-dependent
|
||||
// path of user data and store it to common.USER_DATA_ROOT_FOLDER
|
||||
const app1 = await common.startApp();
|
||||
common.USER_DATA_ROOT_FOLDER = await app1.electron.remote.app.getPath(
|
||||
'appData'
|
||||
);
|
||||
await common.stopApp(app1);
|
||||
});
|
@ -0,0 +1,48 @@
|
||||
/* eslint-disable prefer-destructuring */
|
||||
/* eslint-disable more/no-then */
|
||||
/* eslint-disable func-names */
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
const common = require('./common');
|
||||
const { afterEach, beforeEach, describe, it } = require('mocha');
|
||||
|
||||
describe('Link Device', function() {
|
||||
let app;
|
||||
let app2;
|
||||
this.timeout(60000);
|
||||
this.slow(15000);
|
||||
|
||||
beforeEach(async () => {
|
||||
await common.killallElectron();
|
||||
await common.stopStubSnodeServer();
|
||||
|
||||
const app1Props = {
|
||||
mnemonic: common.TEST_MNEMONIC1,
|
||||
displayName: common.TEST_DISPLAY_NAME1,
|
||||
stubSnode: true,
|
||||
};
|
||||
|
||||
const app2Props = {
|
||||
stubSnode: true,
|
||||
};
|
||||
|
||||
[app, app2] = await Promise.all([
|
||||
common.startAndStub(app1Props),
|
||||
common.startAndStub2(app2Props),
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await common.killallElectron();
|
||||
await common.stopStubSnodeServer();
|
||||
});
|
||||
|
||||
it('linkDevice: link two desktop devices', async () => {
|
||||
await common.linkApp2ToApp(app, app2);
|
||||
});
|
||||
|
||||
it('linkDevice: unlink two devices', async () => {
|
||||
await common.linkApp2ToApp(app, app2);
|
||||
await common.timeout(1000);
|
||||
await common.triggerUnlinkApp2FromApp(app, app2);
|
||||
});
|
||||
});
|
@ -0,0 +1,151 @@
|
||||
/* eslint-disable func-names */
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
const common = require('./common');
|
||||
const { afterEach, beforeEach, describe, it } = require('mocha');
|
||||
const ConversationPage = require('./page-objects/conversation.page');
|
||||
|
||||
describe('Open groups', function() {
|
||||
let app;
|
||||
this.timeout(30000);
|
||||
this.slow(15000);
|
||||
|
||||
beforeEach(async () => {
|
||||
await common.killallElectron();
|
||||
const login = {
|
||||
mnemonic: common.TEST_MNEMONIC1,
|
||||
displayName: common.TEST_DISPLAY_NAME1,
|
||||
stubOpenGroups: true,
|
||||
};
|
||||
app = await common.startAndStub(login);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await common.killallElectron();
|
||||
});
|
||||
|
||||
// reduce code duplication to get the initial join
|
||||
async function joinOpenGroup(url, name) {
|
||||
await app.client.element(ConversationPage.globeButtonSection).click();
|
||||
await app.client.element(ConversationPage.joinOpenGroupButton).click();
|
||||
|
||||
await common.setValueWrapper(app, ConversationPage.openGroupInputUrl, url);
|
||||
await app.client
|
||||
.element(ConversationPage.openGroupInputUrl)
|
||||
.getValue()
|
||||
.should.eventually.equal(url);
|
||||
await app.client.element(ConversationPage.joinOpenGroupButton).click();
|
||||
|
||||
// validate session loader is shown
|
||||
await app.client.isExisting(ConversationPage.sessionLoader).should
|
||||
.eventually.be.true;
|
||||
// account for slow home internet connection delays...
|
||||
await app.client.waitForExist(
|
||||
ConversationPage.sessionToastJoinOpenGroupSuccess,
|
||||
60 * 1000
|
||||
);
|
||||
|
||||
// validate overlay is closed
|
||||
await app.client.isExisting(ConversationPage.leftPaneOverlay).should
|
||||
.eventually.be.false;
|
||||
|
||||
// validate open chat has been added
|
||||
await app.client.waitForExist(
|
||||
ConversationPage.rowOpenGroupConversationName(name),
|
||||
4000
|
||||
);
|
||||
}
|
||||
|
||||
it('openGroup: works with valid open group url', async () => {
|
||||
await joinOpenGroup(common.VALID_GROUP_URL, common.VALID_GROUP_NAME);
|
||||
});
|
||||
|
||||
it('openGroup: cannot join two times the same open group', async () => {
|
||||
await joinOpenGroup(common.VALID_GROUP_URL2, common.VALID_GROUP_NAME2);
|
||||
|
||||
// adding a second time the same open group
|
||||
await app.client.element(ConversationPage.globeButtonSection).click();
|
||||
await app.client.element(ConversationPage.joinOpenGroupButton).click();
|
||||
|
||||
await common.setValueWrapper(
|
||||
app,
|
||||
ConversationPage.openGroupInputUrl,
|
||||
common.VALID_GROUP_URL2
|
||||
);
|
||||
await app.client.element(ConversationPage.joinOpenGroupButton).click();
|
||||
// validate session loader is not shown
|
||||
await app.client.isExisting(ConversationPage.sessionLoader).should
|
||||
.eventually.be.false;
|
||||
|
||||
await app.client.waitForExist(
|
||||
ConversationPage.sessionToastJoinOpenGroupAlreadyExist,
|
||||
1 * 1000
|
||||
);
|
||||
|
||||
// validate overlay is still opened
|
||||
await app.client.isExisting(ConversationPage.leftPaneOverlay).should
|
||||
.eventually.be.true;
|
||||
});
|
||||
|
||||
it('openGroup: can send message to open group', async () => {
|
||||
// join dev-chat group
|
||||
await app.client.element(ConversationPage.globeButtonSection).click();
|
||||
await app.client.element(ConversationPage.joinOpenGroupButton).click();
|
||||
|
||||
await common.setValueWrapper(
|
||||
app,
|
||||
ConversationPage.openGroupInputUrl,
|
||||
common.VALID_GROUP_URL2
|
||||
);
|
||||
await app.client.element(ConversationPage.joinOpenGroupButton).click();
|
||||
|
||||
// wait for toast to appear
|
||||
await app.client.waitForExist(
|
||||
ConversationPage.sessionToastJoinOpenGroupSuccess,
|
||||
30 * 1000
|
||||
);
|
||||
await common.timeout(5 * 1000); // wait for toast to clear
|
||||
|
||||
await app.client.waitForExist(
|
||||
ConversationPage.rowOpenGroupConversationName(common.VALID_GROUP_NAME2),
|
||||
10 * 1000
|
||||
);
|
||||
// generate a message containing the current timestamp so we can find it in the list of messages
|
||||
const textMessage = common.generateSendMessageText();
|
||||
await app.client
|
||||
.element(ConversationPage.conversationButtonSection)
|
||||
.click();
|
||||
|
||||
await app.client.isExisting(
|
||||
ConversationPage.rowOpenGroupConversationName(common.VALID_GROUP_NAME2)
|
||||
);
|
||||
|
||||
await app.client
|
||||
.element(
|
||||
ConversationPage.rowOpenGroupConversationName(common.VALID_GROUP_NAME2)
|
||||
)
|
||||
.click();
|
||||
|
||||
await common.setValueWrapper(
|
||||
app,
|
||||
ConversationPage.sendMessageTextarea,
|
||||
textMessage
|
||||
);
|
||||
await app.client
|
||||
.element(ConversationPage.sendMessageTextarea)
|
||||
.getValue()
|
||||
.should.eventually.equal(textMessage);
|
||||
// allow some time to fetch some messages
|
||||
await common.timeout(3000);
|
||||
|
||||
// send the message
|
||||
await app.client.keys('Enter');
|
||||
await common.timeout(5000);
|
||||
// validate that the message has been added to the message list view
|
||||
await app.client.waitForExist(
|
||||
ConversationPage.existingSendMessageText(textMessage),
|
||||
3 * 1000
|
||||
);
|
||||
// we should validate that the message has been added effectively sent
|
||||
// (checking the check icon on the metadata part of the message?)
|
||||
});
|
||||
});
|
@ -0,0 +1,24 @@
|
||||
module.exports = {
|
||||
// generics
|
||||
objWithClassAndText: (obj, classname, text) =>
|
||||
`//${obj}[contains(string(), "${text}")][contains(@class, "${classname}")]`,
|
||||
|
||||
divRoleButtonWithText: text =>
|
||||
`//div[contains(string(), "${text}")][contains(@role, "button")]`,
|
||||
divRoleButtonWithTextDisabled: text =>
|
||||
`//div[contains(string(), "${text}")][contains(@role, "button")][contains(@class, "disabled")]`,
|
||||
divRoleButtonDangerWithText: text =>
|
||||
`${module.exports.divRoleButtonWithText(text)}[contains(@class, "danger")]`,
|
||||
inputWithPlaceholder: placeholder =>
|
||||
`//input[contains(@placeholder, "${placeholder}")]`,
|
||||
textAreaWithPlaceholder: placeholder =>
|
||||
`//textarea[contains(@placeholder, "${placeholder}")]`,
|
||||
byId: id => `//*[@id="${id}"]`,
|
||||
divWithClass: classname => `//div[contains(@class, "${classname}")]`,
|
||||
divWithClassAndText: (classname, text) =>
|
||||
module.exports.objWithClassAndText('div', classname, text),
|
||||
spanWithClassAndText: (classname, text) =>
|
||||
module.exports.objWithClassAndText('span', classname, text),
|
||||
toastWithText: text =>
|
||||
module.exports.divWithClassAndText('session-toast-wrapper', text),
|
||||
};
|
@ -0,0 +1,121 @@
|
||||
const commonPage = require('./common.page');
|
||||
|
||||
module.exports = {
|
||||
// conversation view
|
||||
sessionLoader: commonPage.divWithClass('session-loader'),
|
||||
leftPaneOverlay: commonPage.divWithClass('module-left-pane-overlay'),
|
||||
sendMessageTextarea: commonPage.textAreaWithPlaceholder('Type your message'),
|
||||
sendFriendRequestTextarea: commonPage.textAreaWithPlaceholder(
|
||||
'Send your first message'
|
||||
),
|
||||
existingSendMessageText: textMessage =>
|
||||
`//*[contains(@class, "module-message__text--outgoing")and .//span[contains(@class, "text-selectable")][contains(string(), '${textMessage}')]]`,
|
||||
existingFriendRequestText: textMessage =>
|
||||
`//*[contains(@class, "module-message-friend-request__container")and .//span[contains(@class, "text-selectable")][contains(string(), '${textMessage}')]]`,
|
||||
existingReceivedMessageText: textMessage =>
|
||||
`//*[contains(@class, "module-message__text--incoming")and .//span[contains(@class, "text-selectable")][contains(string(), '${textMessage}')]]`,
|
||||
|
||||
// conversations
|
||||
conversationButtonSection:
|
||||
'//*[contains(@class,"session-icon-button") and .//*[contains(@class, "chatBubble")]]',
|
||||
retrySendButton: commonPage.divWithClassAndText(
|
||||
'module-friend-request__buttonContainer--outgoing',
|
||||
'Retry Send'
|
||||
),
|
||||
headerTitleMembers: number =>
|
||||
commonPage.spanWithClassAndText(
|
||||
'module-conversation-header__title-text',
|
||||
`${number} members`
|
||||
),
|
||||
|
||||
// channels
|
||||
globeButtonSection:
|
||||
'//*[contains(@class,"session-icon-button") and .//*[contains(@class, "globe")]]',
|
||||
joinOpenGroupButton: commonPage.divRoleButtonWithText('Join Open Group'),
|
||||
openGroupInputUrl: commonPage.textAreaWithPlaceholder('chat.getsession.org'),
|
||||
sessionToastJoinOpenGroupSuccess: commonPage.toastWithText(
|
||||
'Successfully connected to new open group server'
|
||||
),
|
||||
sessionToastJoinOpenGroupAlreadyExist: commonPage.toastWithText(
|
||||
'You are already connected to this public channel'
|
||||
),
|
||||
rowOpenGroupConversationName: groupName =>
|
||||
commonPage.spanWithClassAndText(
|
||||
'module-conversation__user__profile-number',
|
||||
groupName
|
||||
),
|
||||
|
||||
// closed group
|
||||
createClosedGroupButton: commonPage.divRoleButtonWithText(
|
||||
'Create Closed Group'
|
||||
),
|
||||
closedGroupNameTextarea: commonPage.textAreaWithPlaceholder(
|
||||
'Enter a group name'
|
||||
),
|
||||
createClosedGroupMemberItem: commonPage.divWithClass('session-member-item'),
|
||||
createClosedGroupMemberItemSelected: commonPage.divWithClass(
|
||||
'session-member-item selected'
|
||||
),
|
||||
validateCreationClosedGroupButton: commonPage.divRoleButtonWithText(
|
||||
'Create Closed Group'
|
||||
),
|
||||
sessionToastGroupCreatedSuccess: commonPage.toastWithText(
|
||||
'Group created successfully'
|
||||
),
|
||||
headerTitleGroupName: groupname =>
|
||||
commonPage.spanWithClassAndText(
|
||||
'module-contact-name__profile-name',
|
||||
groupname
|
||||
),
|
||||
|
||||
// contacts
|
||||
contactsButtonSection:
|
||||
'//*[contains(@class,"session-icon-button") and .//*[contains(@class, "users")]]',
|
||||
addContactButton: commonPage.divRoleButtonWithText('Add Contact'),
|
||||
sessionIDInput: commonPage.textAreaWithPlaceholder('Enter a Session ID'),
|
||||
nextButton: commonPage.divRoleButtonWithText('Next'),
|
||||
oneNotificationFriendRequestLeft:
|
||||
'//*[contains(@class,"session-icon-button") and .//*[contains(@class, "users")] and .//*[contains(@class, "notification-count") and contains(string(), "1")] ]',
|
||||
oneNotificationFriendRequestTop:
|
||||
'//*[contains(@class,"notification-count hover") and contains(string(), "1")]',
|
||||
friendRequestFromUser: (displayName, pubkey) =>
|
||||
`//*[contains(@class,"module-left-pane__list-popup") and .//*[contains(@class, "module-conversation__user") and .//*[contains(string(), "${displayName}")] and .//*[contains(string(), "(...${pubkey.substring(
|
||||
60
|
||||
)})")]]]`,
|
||||
acceptFriendRequestButton:
|
||||
'//*[contains(@role, "button")][contains(@class, "session-button")][contains(string(), "Accept")]',
|
||||
acceptedFriendRequestMessage:
|
||||
'//*[contains(@class, "module-friend-request__title")][contains(string(), "Friend request accepted")]',
|
||||
|
||||
// settings
|
||||
settingsButtonSection:
|
||||
'//*[contains(@class,"session-icon-button") and .//*[contains(@class, "gear")]]',
|
||||
deviceSettingsRow:
|
||||
'//*[contains(@class, "left-pane-setting-category-list-item")][contains(string(), "Devices")]',
|
||||
|
||||
descriptionDeleteAccount: commonPage.spanWithClassAndText(
|
||||
'session-confirm-main-message',
|
||||
'Are you sure you want to delete your account?'
|
||||
),
|
||||
validateDeleteAccount: commonPage.divRoleButtonDangerWithText('OK'),
|
||||
|
||||
// device linking
|
||||
noPairedDeviceMessage:
|
||||
'//*[contains(@class, "session-settings-item__title")][contains(string(), "No linked devices")]',
|
||||
linkDeviceButton: commonPage.divRoleButtonWithText('Link New Device'),
|
||||
linkDeviceButtonDisabled: commonPage.divRoleButtonWithTextDisabled(
|
||||
'Link New Device'
|
||||
),
|
||||
devicePairingDialog: '//*[contains(@class,"device-pairing-dialog")]',
|
||||
qrImageDiv: commonPage.divWithClass('qr-image'),
|
||||
allowPairingButton: commonPage.divRoleButtonWithText('Allow Linking'),
|
||||
okButton: commonPage.divRoleButtonWithText('OK'),
|
||||
devicePairedDescription: secretWords =>
|
||||
commonPage.divWithClassAndText(
|
||||
'session-settings-item__description',
|
||||
secretWords
|
||||
),
|
||||
unpairDeviceButton: commonPage.divRoleButtonDangerWithText('Unlink Device'),
|
||||
deleteAccountButton: commonPage.divRoleButtonDangerWithText('Delete Account'),
|
||||
validateUnpairDevice: commonPage.divRoleButtonDangerWithText('Unlink'),
|
||||
};
|
@ -0,0 +1,143 @@
|
||||
/* eslint-disable prefer-arrow-callback */
|
||||
/* eslint-disable func-names */
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
|
||||
const common = require('./common');
|
||||
const { afterEach, beforeEach, describe, it } = require('mocha');
|
||||
const RegistrationPage = require('./page-objects/registration.page');
|
||||
const ConversationPage = require('./page-objects/conversation.page');
|
||||
|
||||
describe('Window Test and Login', function() {
|
||||
let app;
|
||||
this.timeout(20000);
|
||||
this.slow(15000);
|
||||
|
||||
beforeEach(async () => {
|
||||
await common.killallElectron();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await common.stopApp(app);
|
||||
await common.killallElectron();
|
||||
});
|
||||
|
||||
it('registration: opens one window', async () => {
|
||||
app = await common.startAndAssureCleanedApp();
|
||||
app.client.getWindowCount().should.eventually.be.equal(1);
|
||||
});
|
||||
|
||||
it('registration: window title is correct', async () => {
|
||||
app = await common.startAndAssureCleanedApp();
|
||||
|
||||
app.client
|
||||
.getTitle()
|
||||
.should.eventually.be.equal('Session - test-integration-session');
|
||||
});
|
||||
|
||||
it('registration: can restore from seed', async () => {
|
||||
app = await common.startAndAssureCleanedApp();
|
||||
|
||||
await app.client.element(RegistrationPage.registrationTabSignIn).click();
|
||||
await app.client.element(RegistrationPage.restoreFromSeedMode).click();
|
||||
await app.client
|
||||
.element(RegistrationPage.recoveryPhraseInput)
|
||||
.setValue(common.TEST_MNEMONIC1);
|
||||
await app.client
|
||||
.element(RegistrationPage.displayNameInput)
|
||||
.setValue(common.TEST_DISPLAY_NAME1);
|
||||
|
||||
// validate fields are filled
|
||||
await app.client
|
||||
.element(RegistrationPage.recoveryPhraseInput)
|
||||
.getValue()
|
||||
.should.eventually.equal(common.TEST_MNEMONIC1);
|
||||
await app.client
|
||||
.element(RegistrationPage.displayNameInput)
|
||||
.getValue()
|
||||
.should.eventually.equal(common.TEST_DISPLAY_NAME1);
|
||||
|
||||
// trigger login
|
||||
await app.client.element(RegistrationPage.continueSessionButton).click();
|
||||
await app.client.waitForExist(
|
||||
RegistrationPage.conversationListContainer,
|
||||
4000
|
||||
);
|
||||
|
||||
await common.timeout(2000);
|
||||
|
||||
await app.webContents
|
||||
.executeJavaScript("window.storage.get('primaryDevicePubKey')")
|
||||
.should.eventually.be.equal(common.TEST_PUBKEY1);
|
||||
});
|
||||
|
||||
it('registration: can create new account', async () => {
|
||||
app = await common.startAndAssureCleanedApp();
|
||||
await app.client.element(RegistrationPage.createSessionIDButton).click();
|
||||
// wait for the animation of generated pubkey to finish
|
||||
await common.timeout(2000);
|
||||
const pubkeyGenerated = await app.client
|
||||
.element(RegistrationPage.textareaGeneratedPubkey)
|
||||
.getValue();
|
||||
// validate generated pubkey
|
||||
pubkeyGenerated.should.have.lengthOf(66);
|
||||
pubkeyGenerated.substr(0, 2).should.be.equal('05');
|
||||
await app.client.element(RegistrationPage.continueButton).click();
|
||||
await app.client.isExisting(RegistrationPage.displayNameInput).should
|
||||
.eventually.be.true;
|
||||
await app.client
|
||||
.element(RegistrationPage.displayNameInput)
|
||||
.setValue(common.TEST_DISPLAY_NAME1);
|
||||
await app.client.element(RegistrationPage.getStartedButton).click();
|
||||
await app.client.waitForExist(
|
||||
ConversationPage.conversationButtonSection,
|
||||
5000
|
||||
);
|
||||
|
||||
await app.webContents
|
||||
.executeJavaScript("window.storage.get('primaryDevicePubKey')")
|
||||
.should.eventually.be.equal(pubkeyGenerated);
|
||||
});
|
||||
|
||||
it('registration: can delete account when logged in', async () => {
|
||||
// login as user1
|
||||
const login = {
|
||||
mnemonic: common.TEST_MNEMONIC1,
|
||||
displayName: common.TEST_DISPLAY_NAME1,
|
||||
stubOpenGroups: true,
|
||||
};
|
||||
app = await common.startAndStub(login);
|
||||
|
||||
await app.client.waitForExist(
|
||||
RegistrationPage.conversationListContainer,
|
||||
4000
|
||||
);
|
||||
|
||||
await app.webContents
|
||||
.executeJavaScript("window.storage.get('primaryDevicePubKey')")
|
||||
.should.eventually.be.equal(common.TEST_PUBKEY1);
|
||||
// delete account
|
||||
await app.client.element(ConversationPage.settingsButtonSection).click();
|
||||
await app.client.element(ConversationPage.deleteAccountButton).click();
|
||||
await app.client.isExisting(ConversationPage.descriptionDeleteAccount)
|
||||
.should.eventually.be.true;
|
||||
// click on the modal OK button to delete the account
|
||||
await app.client.element(ConversationPage.validateDeleteAccount).click();
|
||||
// wait for the app restart
|
||||
await common.timeout(2000);
|
||||
|
||||
// Spectron will loose the connection with the app during the app restart.
|
||||
// We have to restart the app without altering the logged in user or anything here, just to get a valid new ref to the app.
|
||||
await common.stopApp(app);
|
||||
app = await common.startApp();
|
||||
|
||||
// validate that on app start, the registration sign in is shown
|
||||
await app.client.waitForExist(RegistrationPage.registrationTabSignIn, 3000);
|
||||
// validate that no pubkey are set in storage
|
||||
await app.webContents
|
||||
.executeJavaScript("window.storage.get('primaryDevicePubKey')")
|
||||
.should.eventually.be.equal(null);
|
||||
// and that the conversation list is not shown
|
||||
await app.client.isExisting(RegistrationPage.conversationListContainer)
|
||||
.should.eventually.be.false;
|
||||
});
|
||||
});
|
@ -0,0 +1,166 @@
|
||||
/* global clearTimeout, Buffer, TextDecoder, process */
|
||||
|
||||
const OriginalAppDotNetApi = require('../../js/modules/loki_app_dot_net_api.js');
|
||||
|
||||
const sampleFeed =
|
||||
'<?xml version="1.0" encoding="windows-1252"?><rss version="2.0"><channel> <title>FeedForAll Sample Feed</title></channel></rss>';
|
||||
|
||||
const samplesGetMessages = {
|
||||
meta: { code: 200 },
|
||||
data: [
|
||||
{
|
||||
channel_id: 1,
|
||||
created_at: '2020-03-18T04:48:44.000Z',
|
||||
entities: {
|
||||
mentions: [],
|
||||
hashtags: [],
|
||||
links: [],
|
||||
},
|
||||
id: 3662,
|
||||
machine_only: false,
|
||||
num_replies: 0,
|
||||
source: {},
|
||||
thread_id: 3662,
|
||||
reply_to: null,
|
||||
text: 'hgt',
|
||||
html: '<span itemscope="https://app.net/schemas/Post">hgt</span>',
|
||||
annotations: [
|
||||
{
|
||||
type: 'network.loki.messenger.publicChat',
|
||||
value: {
|
||||
timestamp: 1584506921361,
|
||||
sig:
|
||||
'262ab113810564d7ff6474dea264e10e2143d91c004903d06d8d9fddb5b74b2c6245865544d5cf76ee16a3fca045bc028a48c51f8a290508a29b6013d014dc83',
|
||||
sigver: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
user: {
|
||||
id: 2448,
|
||||
username:
|
||||
'050cd79763303bcc251bd489a6f7da823a2b8555402b01a7959ebca550d048600f',
|
||||
created_at: '2020-03-18T02:42:05.000Z',
|
||||
canonical_url: null,
|
||||
type: null,
|
||||
timezone: null,
|
||||
locale: null,
|
||||
avatar_image: {
|
||||
url: null,
|
||||
width: null,
|
||||
height: null,
|
||||
is_default: false,
|
||||
},
|
||||
cover_image: {
|
||||
url: null,
|
||||
width: null,
|
||||
height: null,
|
||||
is_default: false,
|
||||
},
|
||||
counts: {
|
||||
following: 0,
|
||||
posts: 0,
|
||||
followers: 0,
|
||||
stars: 0,
|
||||
},
|
||||
name: 'asdf',
|
||||
annotations: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
class StubAppDotNetAPI extends OriginalAppDotNetApi {
|
||||
// make a request to the server
|
||||
async serverRequest(endpoint, options = {}) {
|
||||
const { method } = options;
|
||||
// console.warn('STUBBED ', method, ':', endpoint);
|
||||
|
||||
if (
|
||||
endpoint === 'loki/v1/rss/messenger' ||
|
||||
endpoint === 'loki/v1/rss/loki'
|
||||
) {
|
||||
return {
|
||||
statusCode: 200,
|
||||
response: {
|
||||
data: sampleFeed,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (endpoint === 'channels/1/messages') {
|
||||
if (!method) {
|
||||
return {
|
||||
statusCode: 200,
|
||||
response: samplesGetMessages,
|
||||
};
|
||||
}
|
||||
return {
|
||||
statusCode: 200,
|
||||
response: {
|
||||
data: [],
|
||||
meta: {
|
||||
max_id: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
endpoint === 'loki/v1/channel/1/deletes' ||
|
||||
endpoint === 'loki/v1/channel/1/moderators'
|
||||
) {
|
||||
return {
|
||||
statusCode: 200,
|
||||
response: {
|
||||
data: [],
|
||||
meta: {
|
||||
max_id: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (endpoint === 'channels/1') {
|
||||
let name = 'Unknown group';
|
||||
if (this.baseServerUrl.includes('/chat-dev.lokinet.org')) {
|
||||
name = 'Loki Dev Chat';
|
||||
} else if (this.baseServerUrl.includes('/chat.getsession.org')) {
|
||||
name = 'Session Public Chat';
|
||||
}
|
||||
return {
|
||||
statusCode: 200,
|
||||
response: {
|
||||
data: {
|
||||
annotations: [
|
||||
{
|
||||
type: 'net.patter-app.settings',
|
||||
value: {
|
||||
name,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
if (endpoint === 'token') {
|
||||
return {
|
||||
statusCode: 200,
|
||||
response: {
|
||||
data: {
|
||||
user: {
|
||||
name: 'unknown name',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
response: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StubAppDotNetAPI;
|
@ -0,0 +1,46 @@
|
||||
/* global clearTimeout, dcodeIO, Buffer, TextDecoder, process */
|
||||
const nodeFetch = require('node-fetch');
|
||||
|
||||
class StubMessageAPI {
|
||||
constructor(ourKey) {
|
||||
this.ourKey = ourKey;
|
||||
this.baseUrl = 'http://localhost:3000';
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
async sendMessage(pubKey, data, messageTimeStamp, ttl, options = {}) {
|
||||
// console.warn('STUBBED message api ', pubKey, ttl);
|
||||
const post = {
|
||||
method: 'POST',
|
||||
};
|
||||
|
||||
const data64 = dcodeIO.ByteBuffer.wrap(data).toString('base64');
|
||||
|
||||
await nodeFetch(
|
||||
`${
|
||||
this.baseUrl
|
||||
}/messages?pubkey=${pubKey}×tamp=${messageTimeStamp}&data=${encodeURIComponent(
|
||||
data64
|
||||
)}`,
|
||||
post
|
||||
);
|
||||
}
|
||||
|
||||
async startLongPolling(numConnections, stopPolling, callback) {
|
||||
const ourPubkey = this.ourKey;
|
||||
|
||||
const get = {
|
||||
method: 'GET',
|
||||
};
|
||||
const res = await nodeFetch(
|
||||
`${this.baseUrl}/messages?pubkey=${ourPubkey}`,
|
||||
get
|
||||
);
|
||||
const json = await res.json();
|
||||
// console.warn('STUBBED polling messages ', json.messages);
|
||||
|
||||
callback(json.messages || []);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StubMessageAPI;
|
@ -1,180 +0,0 @@
|
||||
/* global require, process, _ */
|
||||
|
||||
/* eslint-disable strict */
|
||||
|
||||
const electron = require('electron');
|
||||
|
||||
const osLocale = require('os-locale');
|
||||
const os = require('os');
|
||||
const semver = require('semver');
|
||||
const spellchecker = require('spellchecker');
|
||||
|
||||
const { remote, webFrame } = electron;
|
||||
|
||||
// `remote.require` since `Menu` is a main-process module.
|
||||
const buildEditorContextMenu = remote.require('electron-editor-context-menu');
|
||||
|
||||
const EN_VARIANT = /^en/;
|
||||
|
||||
// Prevent the spellchecker from showing contractions as errors.
|
||||
const ENGLISH_SKIP_WORDS = [
|
||||
'ain',
|
||||
'couldn',
|
||||
'didn',
|
||||
'doesn',
|
||||
'hadn',
|
||||
'hasn',
|
||||
'mightn',
|
||||
'mustn',
|
||||
'needn',
|
||||
'oughtn',
|
||||
'shan',
|
||||
'shouldn',
|
||||
'wasn',
|
||||
'weren',
|
||||
'wouldn',
|
||||
];
|
||||
|
||||
function setupLinux(locale) {
|
||||
if (process.env.HUNSPELL_DICTIONARIES || locale !== 'en_US') {
|
||||
// apt-get install hunspell-<locale> can be run for easy access
|
||||
// to other dictionaries
|
||||
const location = process.env.HUNSPELL_DICTIONARIES || '/usr/share/hunspell';
|
||||
|
||||
window.log.info(
|
||||
'Detected Linux. Setting up spell check with locale',
|
||||
locale,
|
||||
'and dictionary location',
|
||||
location
|
||||
);
|
||||
spellchecker.setDictionary(locale, location);
|
||||
} else {
|
||||
window.log.info(
|
||||
'Detected Linux. Using default en_US spell check dictionary'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function setupWin7AndEarlier(locale) {
|
||||
if (process.env.HUNSPELL_DICTIONARIES || locale !== 'en_US') {
|
||||
const location = process.env.HUNSPELL_DICTIONARIES;
|
||||
|
||||
window.log.info(
|
||||
'Detected Windows 7 or below. Setting up spell-check with locale',
|
||||
locale,
|
||||
'and dictionary location',
|
||||
location
|
||||
);
|
||||
spellchecker.setDictionary(locale, location);
|
||||
} else {
|
||||
window.log.info(
|
||||
'Detected Windows 7 or below. Using default en_US spell check dictionary'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// We load locale this way and not via app.getLocale() because this call returns
|
||||
// 'es_ES' and not just 'es.' And hunspell requires the fully-qualified locale.
|
||||
const locale = osLocale.sync().replace('-', '_');
|
||||
|
||||
// The LANG environment variable is how node spellchecker finds its default language:
|
||||
// https://github.com/atom/node-spellchecker/blob/59d2d5eee5785c4b34e9669cd5d987181d17c098/lib/spellchecker.js#L29
|
||||
if (!process.env.LANG) {
|
||||
process.env.LANG = locale;
|
||||
}
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
setupLinux(locale);
|
||||
} else if (process.platform === 'windows' && semver.lt(os.release(), '8.0.0')) {
|
||||
setupWin7AndEarlier(locale);
|
||||
} else {
|
||||
// OSX and Windows 8+ have OS-level spellcheck APIs
|
||||
window.log.info(
|
||||
'Using OS-level spell check API with locale',
|
||||
process.env.LANG
|
||||
);
|
||||
}
|
||||
|
||||
const simpleChecker = {
|
||||
spellCheck(text) {
|
||||
return !this.isMisspelled(text);
|
||||
},
|
||||
isMisspelled(text) {
|
||||
const misspelled = spellchecker.isMisspelled(text);
|
||||
|
||||
// The idea is to make this as fast as possible. For the many, many calls which
|
||||
// don't result in the red squiggly, we minimize the number of checks.
|
||||
if (!misspelled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only if we think we've found an error do we check the locale and skip list.
|
||||
if (locale.match(EN_VARIANT) && _.contains(ENGLISH_SKIP_WORDS, text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
getSuggestions(text) {
|
||||
return spellchecker.getCorrectionsForMisspelling(text);
|
||||
},
|
||||
add(text) {
|
||||
spellchecker.add(text);
|
||||
},
|
||||
};
|
||||
|
||||
const dummyChecker = {
|
||||
spellCheck() {
|
||||
return true;
|
||||
},
|
||||
isMisspelled() {
|
||||
return false;
|
||||
},
|
||||
getSuggestions() {
|
||||
return [];
|
||||
},
|
||||
add() {
|
||||
// nothing
|
||||
},
|
||||
};
|
||||
|
||||
window.spellChecker = simpleChecker;
|
||||
window.disableSpellCheck = () => {
|
||||
window.removeEventListener('contextmenu', spellCheckHandler);
|
||||
webFrame.setSpellCheckProvider('en-US', false, dummyChecker);
|
||||
};
|
||||
|
||||
window.enableSpellCheck = () => {
|
||||
webFrame.setSpellCheckProvider(
|
||||
'en-US',
|
||||
// Not sure what this parameter (`autoCorrectWord`) does: https://github.com/atom/electron/issues/4371
|
||||
// The documentation for `webFrame.setSpellCheckProvider` passes `true` so we do too.
|
||||
true,
|
||||
simpleChecker
|
||||
);
|
||||
window.addEventListener('contextmenu', spellCheckHandler);
|
||||
};
|
||||
|
||||
const spellCheckHandler = e => {
|
||||
// Only show the context menu in text editors.
|
||||
if (!e.target.closest('textarea, input, [contenteditable="true"]')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedText = window.getSelection().toString();
|
||||
const isMisspelled = selectedText && simpleChecker.isMisspelled(selectedText);
|
||||
const spellingSuggestions =
|
||||
isMisspelled && simpleChecker.getSuggestions(selectedText).slice(0, 5);
|
||||
const menu = buildEditorContextMenu({
|
||||
isMisspelled,
|
||||
spellingSuggestions,
|
||||
});
|
||||
|
||||
// The 'contextmenu' event is emitted after 'selectionchange' has fired
|
||||
// but possibly before the visible selection has changed. Try to wait
|
||||
// to show the menu until after that, otherwise the visible selection
|
||||
// will update after the menu dismisses and look weird.
|
||||
setTimeout(() => {
|
||||
menu.popup(remote.getCurrentWindow());
|
||||
}, 30);
|
||||
};
|
@ -1,490 +0,0 @@
|
||||
/* global
|
||||
Whisper,
|
||||
$,
|
||||
getAccountManager,
|
||||
textsecure,
|
||||
i18n,
|
||||
passwordUtil,
|
||||
_,
|
||||
setTimeout,
|
||||
displayNameRegex
|
||||
*/
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
const REGISTER_INDEX = 0;
|
||||
const PROFILE_INDEX = 1;
|
||||
let currentPageIndex = REGISTER_INDEX;
|
||||
|
||||
Whisper.StandaloneRegistrationView = Whisper.View.extend({
|
||||
templateName: 'standalone',
|
||||
className: 'full-screen-flow standalone-fullscreen',
|
||||
initialize() {
|
||||
this.accountManager = getAccountManager();
|
||||
// Clean status in case the app closed unexpectedly
|
||||
textsecure.storage.remove('secondaryDeviceStatus');
|
||||
|
||||
this.render();
|
||||
|
||||
const number = textsecure.storage.user.getNumber();
|
||||
if (number) {
|
||||
this.$('input.number').val(number);
|
||||
}
|
||||
this.phoneView = new Whisper.PhoneInputView({
|
||||
el: this.$('#phone-number-input'),
|
||||
});
|
||||
this.$('#error').hide();
|
||||
|
||||
this.$('.standalone-mnemonic').hide();
|
||||
this.$('.standalone-secondary-device').hide();
|
||||
|
||||
this.onGenerateMnemonic();
|
||||
|
||||
const options = window.mnemonic.get_languages().map(language => {
|
||||
const text = language
|
||||
// Split by whitespace or underscore
|
||||
.split(/[\s_]+/)
|
||||
// Capitalise each word
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
return `<option value="${language}">${text}</option>`;
|
||||
});
|
||||
this.$('#mnemonic-language').append(options);
|
||||
this.$('#mnemonic-language').val('english');
|
||||
this.$('#mnemonic-display-language').append(options);
|
||||
this.$('#mnemonic-display-language').val('english');
|
||||
|
||||
this.$passwordInput = this.$('#password');
|
||||
this.$passwordConfirmationInput = this.$('#password-confirmation');
|
||||
this.$passwordInputError = this.$('.password-inputs .error');
|
||||
|
||||
this.registrationParams = {};
|
||||
this.$pages = this.$('.page');
|
||||
this.pairingInterval = null;
|
||||
this.showRegisterPage();
|
||||
|
||||
this.onValidatePassword();
|
||||
|
||||
this.onSecondaryDeviceRegistered = this.onSecondaryDeviceRegistered.bind(
|
||||
this
|
||||
);
|
||||
|
||||
this.$('#display-name').get(0).oninput = () => {
|
||||
this.sanitiseNameInput();
|
||||
};
|
||||
|
||||
this.$('#display-name').get(0).onpaste = () => {
|
||||
// Sanitise data immediately after paste because it's easier
|
||||
setTimeout(() => {
|
||||
this.sanitiseNameInput();
|
||||
});
|
||||
};
|
||||
this.sanitiseNameInput();
|
||||
},
|
||||
events: {
|
||||
keyup: 'onKeyup',
|
||||
'validation input.number': 'onValidation',
|
||||
'click #request-voice': 'requestVoice',
|
||||
'click #request-sms': 'requestSMSVerification',
|
||||
'change #code': 'onChangeCode',
|
||||
'click #register': 'registerWithoutMnemonic',
|
||||
'click #register-mnemonic': 'registerWithMnemonic',
|
||||
'click #register-secondary-device': 'registerSecondaryDevice',
|
||||
'click #cancel-secondary-device': 'cancelSecondaryDevice',
|
||||
'click #back-button': 'onBack',
|
||||
'click #save-button': 'onSaveProfile',
|
||||
'change #mnemonic': 'onChangeMnemonic',
|
||||
'click #generate-mnemonic': 'onGenerateMnemonic',
|
||||
'change #mnemonic-display-language': 'onGenerateMnemonic',
|
||||
'click #copy-mnemonic': 'onCopyMnemonic',
|
||||
'click .section-toggle': 'toggleSection',
|
||||
'keyup #password': 'onValidatePassword',
|
||||
'keyup #password-confirmation': 'onValidatePassword',
|
||||
},
|
||||
sanitiseNameInput() {
|
||||
const oldVal = this.$('#display-name').val();
|
||||
const newVal = oldVal.replace(displayNameRegex, '');
|
||||
this.$('#display-name').val(newVal);
|
||||
if (_.isEmpty(newVal)) {
|
||||
this.$('#save-button').attr('disabled', 'disabled');
|
||||
return false;
|
||||
}
|
||||
this.$('#save-button').removeAttr('disabled');
|
||||
return true;
|
||||
},
|
||||
async showPage(pageIndex) {
|
||||
// eslint-disable-next-line func-names
|
||||
this.$pages.each(function(index) {
|
||||
if (index !== pageIndex) {
|
||||
$(this).hide();
|
||||
} else {
|
||||
$(this).show();
|
||||
currentPageIndex = pageIndex;
|
||||
}
|
||||
});
|
||||
},
|
||||
async showRegisterPage() {
|
||||
this.registrationParams = {};
|
||||
this.showPage(REGISTER_INDEX);
|
||||
},
|
||||
async showProfilePage(mnemonic, language) {
|
||||
this.registrationParams = {
|
||||
mnemonic,
|
||||
language,
|
||||
};
|
||||
this.$passwordInput.val('');
|
||||
this.$passwordConfirmationInput.val('');
|
||||
this.onValidatePassword();
|
||||
this.showPage(PROFILE_INDEX);
|
||||
this.$('#display-name').focus();
|
||||
},
|
||||
onKeyup(event) {
|
||||
if (
|
||||
currentPageIndex !== PROFILE_INDEX &&
|
||||
currentPageIndex !== REGISTER_INDEX
|
||||
) {
|
||||
// Only want enter/escape keys to work on profile page
|
||||
return;
|
||||
}
|
||||
|
||||
const validName = this.sanitiseNameInput();
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
if (event.target.id === 'mnemonic') {
|
||||
this.registerWithMnemonic();
|
||||
} else if (event.target.id === 'primary-pubkey') {
|
||||
this.registerSecondaryDevice();
|
||||
} else if (validName) {
|
||||
this.onSaveProfile();
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
case 'Esc':
|
||||
this.onBack();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
},
|
||||
async register(mnemonic, language) {
|
||||
// Make sure the password is valid
|
||||
if (this.validatePassword()) {
|
||||
window.pushToast({
|
||||
title: i18n('invalidPassword'),
|
||||
type: 'info',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const input = this.trim(this.$passwordInput.val());
|
||||
|
||||
// Ensure we clear the secondary device registration status
|
||||
textsecure.storage.remove('secondaryDeviceStatus');
|
||||
|
||||
try {
|
||||
await this.resetRegistration();
|
||||
|
||||
await window.setPassword(input);
|
||||
await this.accountManager.registerSingleDevice(
|
||||
mnemonic,
|
||||
language,
|
||||
this.trim(this.$('#display-name').val())
|
||||
);
|
||||
this.$el.trigger('openInbox');
|
||||
} catch (e) {
|
||||
if (typeof e === 'string') {
|
||||
window.pushToast({
|
||||
title: e,
|
||||
type: 'info',
|
||||
});
|
||||
}
|
||||
this.log(e);
|
||||
}
|
||||
},
|
||||
registerWithoutMnemonic() {
|
||||
const mnemonic = this.$('#mnemonic-display').text();
|
||||
const language = this.$('#mnemonic-display-language').val();
|
||||
this.showProfilePage(mnemonic, language);
|
||||
},
|
||||
async onSecondaryDeviceRegistered() {
|
||||
clearInterval(this.pairingInterval);
|
||||
// Ensure the left menu is updated
|
||||
Whisper.events.trigger('userChanged', { isSecondaryDevice: true });
|
||||
// will re-run the background initialisation
|
||||
Whisper.events.trigger('registration_done');
|
||||
this.$el.trigger('openInbox');
|
||||
},
|
||||
async resetRegistration() {
|
||||
await window.Signal.Data.removeAllIdentityKeys();
|
||||
await window.Signal.Data.removeAllPrivateConversations();
|
||||
Whisper.Registration.remove();
|
||||
// Do not remove all items since they are only set
|
||||
// at startup.
|
||||
textsecure.storage.remove('identityKey');
|
||||
textsecure.storage.remove('secondaryDeviceStatus');
|
||||
window.ConversationController.reset();
|
||||
await window.ConversationController.load();
|
||||
Whisper.RotateSignedPreKeyListener.stop(Whisper.events);
|
||||
},
|
||||
async cancelSecondaryDevice() {
|
||||
Whisper.events.off(
|
||||
'secondaryDeviceRegistration',
|
||||
this.onSecondaryDeviceRegistered
|
||||
);
|
||||
this.$('#register-secondary-device')
|
||||
.removeAttr('disabled')
|
||||
.text('Link');
|
||||
this.$('#cancel-secondary-device').hide();
|
||||
this.$('.standalone-secondary-device #pubkey').text('');
|
||||
await this.resetRegistration();
|
||||
},
|
||||
async registerSecondaryDevice() {
|
||||
if (textsecure.storage.get('secondaryDeviceStatus') === 'ongoing') {
|
||||
return;
|
||||
}
|
||||
await this.resetRegistration();
|
||||
textsecure.storage.put('secondaryDeviceStatus', 'ongoing');
|
||||
this.$('#register-secondary-device')
|
||||
.attr('disabled', 'disabled')
|
||||
.text('Sending...');
|
||||
this.$('#cancel-secondary-device').show();
|
||||
const mnemonic = this.$('#mnemonic-display').text();
|
||||
const language = this.$('#mnemonic-display-language').val();
|
||||
const primaryPubKey = this.$('#primary-pubkey').val();
|
||||
this.$('.standalone-secondary-device #error').hide();
|
||||
// Ensure only one listener
|
||||
Whisper.events.off(
|
||||
'secondaryDeviceRegistration',
|
||||
this.onSecondaryDeviceRegistered
|
||||
);
|
||||
Whisper.events.once(
|
||||
'secondaryDeviceRegistration',
|
||||
this.onSecondaryDeviceRegistered
|
||||
);
|
||||
const onError = async error => {
|
||||
this.$('.standalone-secondary-device #error')
|
||||
.text(error)
|
||||
.show();
|
||||
await this.resetRegistration();
|
||||
this.$('#register-secondary-device')
|
||||
.removeAttr('disabled')
|
||||
.text('Link');
|
||||
this.$('#cancel-secondary-device').hide();
|
||||
};
|
||||
const c = new Whisper.Conversation({
|
||||
id: primaryPubKey,
|
||||
type: 'private',
|
||||
});
|
||||
const validationError = c.validateNumber();
|
||||
if (validationError) {
|
||||
onError('Invalid public key');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.accountManager.registerSingleDevice(
|
||||
mnemonic,
|
||||
language,
|
||||
null
|
||||
);
|
||||
await this.accountManager.requestPairing(primaryPubKey);
|
||||
const pubkey = textsecure.storage.user.getNumber();
|
||||
const words = window.mnemonic.pubkey_to_secret_words(pubkey);
|
||||
|
||||
this.$('.standalone-secondary-device #pubkey').text(
|
||||
`Here is your secret:\n${words}`
|
||||
);
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
}
|
||||
},
|
||||
registerWithMnemonic() {
|
||||
const mnemonic = this.$('#mnemonic').val();
|
||||
const language = this.$('#mnemonic-language').val();
|
||||
try {
|
||||
window.mnemonic.mn_decode(mnemonic, language);
|
||||
} catch (error) {
|
||||
this.$('#mnemonic').addClass('error-input');
|
||||
this.$('#error').text(error);
|
||||
this.$('#error').show();
|
||||
return;
|
||||
}
|
||||
this.$('#error').hide();
|
||||
this.$('#mnemonic').removeClass('error-input');
|
||||
if (!mnemonic) {
|
||||
this.log('Please provide a mnemonic word list');
|
||||
} else {
|
||||
this.showProfilePage(mnemonic, language);
|
||||
}
|
||||
},
|
||||
onSaveProfile() {
|
||||
if (_.isEmpty(this.registrationParams)) {
|
||||
this.onBack();
|
||||
return;
|
||||
}
|
||||
|
||||
const { mnemonic, language } = this.registrationParams;
|
||||
this.register(mnemonic, language);
|
||||
},
|
||||
onBack() {
|
||||
this.showRegisterPage();
|
||||
},
|
||||
onChangeMnemonic() {
|
||||
this.$('#status').html('');
|
||||
},
|
||||
async onGenerateMnemonic() {
|
||||
const language = this.$('#mnemonic-display-language').val();
|
||||
const mnemonic = await this.accountManager.generateMnemonic(language);
|
||||
this.$('#mnemonic-display').text(mnemonic);
|
||||
},
|
||||
onCopyMnemonic() {
|
||||
window.clipboard.writeText(this.$('#mnemonic-display').text());
|
||||
window.pushToast({
|
||||
title: i18n('copiedMnemonic'),
|
||||
type: 'info',
|
||||
});
|
||||
},
|
||||
log(s) {
|
||||
window.log.info(s);
|
||||
this.$('#status').text(s);
|
||||
},
|
||||
displayError(error) {
|
||||
this.$('#error')
|
||||
.hide()
|
||||
.text(error)
|
||||
.addClass('in')
|
||||
.fadeIn();
|
||||
},
|
||||
onValidation() {
|
||||
if (this.$('#number-container').hasClass('valid')) {
|
||||
this.$('#request-sms, #request-voice').removeAttr('disabled');
|
||||
} else {
|
||||
this.$('#request-sms, #request-voice').prop('disabled', 'disabled');
|
||||
}
|
||||
},
|
||||
onChangeCode() {
|
||||
if (!this.validateCode()) {
|
||||
this.$('#code').addClass('invalid');
|
||||
} else {
|
||||
this.$('#code').removeClass('invalid');
|
||||
}
|
||||
},
|
||||
requestVoice() {
|
||||
window.removeSetupMenuItems();
|
||||
this.$('#error').hide();
|
||||
const number = this.phoneView.validateNumber();
|
||||
if (number) {
|
||||
this.accountManager
|
||||
.requestVoiceVerification(number)
|
||||
.catch(this.displayError.bind(this));
|
||||
this.$('#step2')
|
||||
.addClass('in')
|
||||
.fadeIn();
|
||||
} else {
|
||||
this.$('#number-container').addClass('invalid');
|
||||
}
|
||||
},
|
||||
requestSMSVerification() {
|
||||
window.removeSetupMenuItems();
|
||||
$('#error').hide();
|
||||
const number = this.phoneView.validateNumber();
|
||||
if (number) {
|
||||
this.accountManager
|
||||
.requestSMSVerification(number)
|
||||
.catch(this.displayError.bind(this));
|
||||
this.$('#step2')
|
||||
.addClass('in')
|
||||
.fadeIn();
|
||||
} else {
|
||||
this.$('#number-container').addClass('invalid');
|
||||
}
|
||||
},
|
||||
|
||||
toggleSection(e) {
|
||||
function focusInput() {
|
||||
const inputs = $(this).find('input');
|
||||
if ($(this).is(':visible')) {
|
||||
if (inputs[0]) {
|
||||
inputs[0].focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Expand or collapse this panel
|
||||
const $target = this.$(e.currentTarget);
|
||||
const $next = $target.next();
|
||||
|
||||
// Toggle section visibility
|
||||
$next.slideToggle('fast', focusInput);
|
||||
$target.toggleClass('section-toggle-visible');
|
||||
|
||||
// Hide the other sections
|
||||
this.$('.section-toggle')
|
||||
.not($target)
|
||||
.removeClass('section-toggle-visible');
|
||||
this.$('.section-content')
|
||||
.not($next)
|
||||
.slideUp('fast');
|
||||
},
|
||||
validatePassword() {
|
||||
const input = this.trim(this.$passwordInput.val());
|
||||
const confirmationInput = this.trim(
|
||||
this.$passwordConfirmationInput.val()
|
||||
);
|
||||
|
||||
// If user hasn't set a value then skip
|
||||
if (!input && !confirmationInput) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const error = passwordUtil.validatePassword(input, i18n);
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (input !== confirmationInput) {
|
||||
return "Password don't match";
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
onValidatePassword() {
|
||||
const passwordValidation = this.validatePassword();
|
||||
if (passwordValidation) {
|
||||
this.$passwordInput.addClass('error-input');
|
||||
this.$passwordConfirmationInput.addClass('error-input');
|
||||
|
||||
this.$passwordInput.removeClass('match-input');
|
||||
this.$passwordConfirmationInput.removeClass('match-input');
|
||||
|
||||
this.$passwordInputError.text(passwordValidation);
|
||||
this.$passwordInputError.show();
|
||||
} else {
|
||||
this.$passwordInput.removeClass('error-input');
|
||||
this.$passwordConfirmationInput.removeClass('error-input');
|
||||
|
||||
this.$passwordInputError.text('');
|
||||
this.$passwordInputError.hide();
|
||||
|
||||
// Show green box around inputs that match
|
||||
const input = this.trim(this.$passwordInput.val());
|
||||
const confirmationInput = this.trim(
|
||||
this.$passwordConfirmationInput.val()
|
||||
);
|
||||
if (input && input === confirmationInput) {
|
||||
this.$passwordInput.addClass('match-input');
|
||||
this.$passwordConfirmationInput.addClass('match-input');
|
||||
} else {
|
||||
this.$passwordInput.removeClass('match-input');
|
||||
this.$passwordConfirmationInput.removeClass('match-input');
|
||||
}
|
||||
}
|
||||
},
|
||||
trim(value) {
|
||||
return value ? value.trim() : value;
|
||||
},
|
||||
});
|
||||
})();
|
@ -1,141 +0,0 @@
|
||||
/* global libloki, Multibase, libsignal, StringView, dcodeIO */
|
||||
|
||||
'use strict';
|
||||
|
||||
async function generateSnodeKeysAndAddress() {
|
||||
// snode identitys is a ed25519 keypair
|
||||
const sodium = await window.getSodium();
|
||||
const ed25519KeyPair = sodium.crypto_sign_keypair();
|
||||
const keyPair = {
|
||||
pubKey: ed25519KeyPair.publicKey,
|
||||
privKey: ed25519KeyPair.privateKey,
|
||||
};
|
||||
// snode address is the pubkey in base32z
|
||||
let address = Multibase.encode(
|
||||
'base32z',
|
||||
Multibase.Buffer.from(keyPair.pubKey)
|
||||
).toString();
|
||||
// remove first letter, which is the encoding code
|
||||
address = address.substring(1);
|
||||
return { keyPair, address };
|
||||
}
|
||||
|
||||
describe('Snode Channel', () => {
|
||||
describe('snodeCipher singleton', () => {
|
||||
it('should be defined at libloki.crypto', () => {
|
||||
assert.isDefined(libloki.crypto.snodeCipher);
|
||||
assert.isTrue(
|
||||
libloki.crypto.snodeCipher instanceof libloki.crypto._LokiSnodeChannel
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#decodeSnodeAddressToPubKey', () => {
|
||||
it('should decode a base32z encoded .snode address', async () => {
|
||||
const { keyPair, address } = await generateSnodeKeysAndAddress();
|
||||
|
||||
const buffer = libloki.crypto._decodeSnodeAddressToPubKey(
|
||||
`http://${address}.snode`
|
||||
);
|
||||
|
||||
const expected = new Uint8Array(keyPair.pubKey);
|
||||
assert.strictEqual(expected.length, 32);
|
||||
assert.strictEqual(buffer.length, 32);
|
||||
for (let i = 0; i < buffer.length; i += 1) {
|
||||
assert.strictEqual(buffer[i], expected[i]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('#LokiSnodeChannel', () => {
|
||||
it('should generate an ephemeral key pair', () => {
|
||||
const channel = new libloki.crypto._LokiSnodeChannel();
|
||||
|
||||
assert.isDefined(channel._ephemeralKeyPair);
|
||||
assert.isTrue(channel._ephemeralKeyPair.privKey instanceof ArrayBuffer);
|
||||
assert.isTrue(channel._ephemeralKeyPair.pubKey instanceof ArrayBuffer);
|
||||
const pubKeyHex = StringView.arrayBufferToHex(
|
||||
channel._ephemeralKeyPair.pubKey
|
||||
);
|
||||
assert.strictEqual(channel.getChannelPublicKeyHex(), pubKeyHex);
|
||||
});
|
||||
|
||||
it('should cache something by snode address', async () => {
|
||||
const { address } = await generateSnodeKeysAndAddress();
|
||||
|
||||
const channel = new libloki.crypto._LokiSnodeChannel();
|
||||
// cache should be empty
|
||||
assert.strictEqual(Object.keys(channel._cache).length, 0);
|
||||
|
||||
// push to cache
|
||||
await channel._getSymmetricKey(address);
|
||||
|
||||
assert.strictEqual(Object.keys(channel._cache).length, 1);
|
||||
assert.strictEqual(Object.keys(channel._cache)[0], address);
|
||||
});
|
||||
|
||||
it('should encrypt data correctly', async () => {
|
||||
// message sent by Session
|
||||
const snode = await generateSnodeKeysAndAddress();
|
||||
const messageSent = 'I am Groot';
|
||||
const textEncoder = new TextEncoder();
|
||||
const data = textEncoder.encode(messageSent);
|
||||
|
||||
const channel = new libloki.crypto._LokiSnodeChannel();
|
||||
const encrypted = await channel.encrypt(snode.address, data);
|
||||
|
||||
assert.strictEqual(typeof encrypted, 'string');
|
||||
|
||||
// message received by storage server
|
||||
const senderPubKey = StringView.hexToArrayBuffer(
|
||||
channel.getChannelPublicKeyHex()
|
||||
);
|
||||
const sodium = await window.getSodium();
|
||||
const snodePrivKey = sodium.crypto_sign_ed25519_sk_to_curve25519(
|
||||
snode.keyPair.privKey
|
||||
).buffer;
|
||||
const symmetricKey = libsignal.Curve.calculateAgreement(
|
||||
senderPubKey,
|
||||
snodePrivKey
|
||||
);
|
||||
const encryptedArrayBuffer = dcodeIO.ByteBuffer.wrap(
|
||||
encrypted,
|
||||
'base64'
|
||||
).toArrayBuffer();
|
||||
const decrypted = await libloki.crypto.DHDecrypt(
|
||||
symmetricKey,
|
||||
encryptedArrayBuffer
|
||||
);
|
||||
const textDecoder = new TextDecoder();
|
||||
const messageReceived = textDecoder.decode(decrypted);
|
||||
assert.strictEqual(messageSent, messageReceived);
|
||||
});
|
||||
|
||||
it('should decrypt data correctly', async () => {
|
||||
const channel = new libloki.crypto._LokiSnodeChannel();
|
||||
// message sent by storage server
|
||||
const snode = await generateSnodeKeysAndAddress();
|
||||
const messageSent = 'You are Groot';
|
||||
const textEncoder = new TextEncoder();
|
||||
const data = textEncoder.encode(messageSent);
|
||||
const senderPubKey = StringView.hexToArrayBuffer(
|
||||
channel.getChannelPublicKeyHex()
|
||||
);
|
||||
const sodium = await window.getSodium();
|
||||
const snodePrivKey = sodium.crypto_sign_ed25519_sk_to_curve25519(
|
||||
snode.keyPair.privKey
|
||||
).buffer;
|
||||
const symmetricKey = libsignal.Curve.calculateAgreement(
|
||||
senderPubKey,
|
||||
snodePrivKey
|
||||
);
|
||||
const encrypted = await libloki.crypto.DHEncrypt(symmetricKey, data);
|
||||
const encryptedBase64 = dcodeIO.ByteBuffer.wrap(encrypted).toString(
|
||||
'base64'
|
||||
);
|
||||
// message received by Session
|
||||
const decrypted = await channel.decrypt(snode.address, encryptedBase64);
|
||||
assert.strictEqual(messageSent, decrypted);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,66 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const fs = require('fs');
|
||||
const _ = require('lodash');
|
||||
|
||||
const packageJson = require('./package.json');
|
||||
|
||||
const { version } = packageJson;
|
||||
const beta = /beta/;
|
||||
|
||||
// You might be wondering why this file is necessary. It comes down to our desire to allow
|
||||
// side-by-side installation of production and beta builds. Electron-Builder uses
|
||||
// top-level data from package.json for many things, like the executable name, the
|
||||
// debian package name, the install directory under /opt on linux, etc. We tried
|
||||
// adding the ${channel} macro to these values, but Electron-Builder didn't like that.
|
||||
|
||||
if (!beta.test(version)) {
|
||||
process.exit();
|
||||
}
|
||||
|
||||
console.log('prepare_beta_build: updating package.json');
|
||||
|
||||
// -------
|
||||
|
||||
const NAME_PATH = 'name';
|
||||
const PRODUCTION_NAME = 'loki-messenger-desktop';
|
||||
const BETA_NAME = 'loki-messenger-desktop-beta';
|
||||
|
||||
const PRODUCT_NAME_PATH = 'productName';
|
||||
const PRODUCTION_PRODUCT_NAME = 'Session';
|
||||
const BETA_PRODUCT_NAME = 'Session Beta';
|
||||
|
||||
const APP_ID_PATH = 'build.appId';
|
||||
const PRODUCTION_APP_ID = 'com.loki-project.messenger-desktop';
|
||||
const BETA_APP_ID = 'com.loki-project.messenger-desktop-beta';
|
||||
|
||||
const STARTUP_WM_CLASS_PATH = 'build.linux.desktop.StartupWMClass';
|
||||
const PRODUCTION_STARTUP_WM_CLASS = 'Session';
|
||||
const BETA_STARTUP_WM_CLASS = 'Session Beta';
|
||||
|
||||
// -------
|
||||
|
||||
function checkValue(object, objectPath, expected) {
|
||||
const actual = _.get(object, objectPath);
|
||||
if (actual !== expected) {
|
||||
throw new Error(`${objectPath} was ${actual}; expected ${expected}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ------
|
||||
|
||||
checkValue(packageJson, NAME_PATH, PRODUCTION_NAME);
|
||||
checkValue(packageJson, PRODUCT_NAME_PATH, PRODUCTION_PRODUCT_NAME);
|
||||
checkValue(packageJson, APP_ID_PATH, PRODUCTION_APP_ID);
|
||||
checkValue(packageJson, STARTUP_WM_CLASS_PATH, PRODUCTION_STARTUP_WM_CLASS);
|
||||
|
||||
// -------
|
||||
|
||||
_.set(packageJson, NAME_PATH, BETA_NAME);
|
||||
_.set(packageJson, PRODUCT_NAME_PATH, BETA_PRODUCT_NAME);
|
||||
_.set(packageJson, APP_ID_PATH, BETA_APP_ID);
|
||||
_.set(packageJson, STARTUP_WM_CLASS_PATH, BETA_STARTUP_WM_CLASS);
|
||||
|
||||
// -------
|
||||
|
||||
fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, ' '));
|
@ -1,69 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const fs = require('fs');
|
||||
const _ = require('lodash');
|
||||
|
||||
const packageJson = require('./package.json');
|
||||
const defaultConfig = require('./config/default.json');
|
||||
|
||||
function checkValue(object, objectPath, expected) {
|
||||
const actual = _.get(object, objectPath);
|
||||
if (actual !== expected) {
|
||||
throw new Error(`${objectPath} was ${actual}; expected ${expected}`);
|
||||
}
|
||||
}
|
||||
|
||||
// You might be wondering why this file is necessary. We have some very specific
|
||||
// requirements around our import-flavor builds. They need to look exactly the same as
|
||||
// normal builds, but they must immediately open into import mode. So they need a
|
||||
// slight config tweak, and then a change to the .app/.exe name (note: we do NOT want to
|
||||
// change where data is stored or anything, since that would make these builds
|
||||
// incompatible with the mainline builds) So we just change the artifact name.
|
||||
//
|
||||
// Another key thing to know about these builds is that we should not upload the
|
||||
// latest.yml (windows) and latest-mac.yml (mac) that go along with the executables.
|
||||
// This would interrupt the normal install flow for users installing from
|
||||
// signal.org/download. So any release script will need to upload these files manually
|
||||
// instead of relying on electron-builder, which will upload everything.
|
||||
|
||||
// -------
|
||||
|
||||
console.log('prepare_import_build: updating config/default.json');
|
||||
|
||||
const IMPORT_PATH = 'import';
|
||||
const IMPORT_START_VALUE = false;
|
||||
const IMPORT_END_VALUE = true;
|
||||
|
||||
checkValue(defaultConfig, IMPORT_PATH, IMPORT_START_VALUE);
|
||||
|
||||
_.set(defaultConfig, IMPORT_PATH, IMPORT_END_VALUE);
|
||||
|
||||
// -------
|
||||
|
||||
console.log('prepare_import_build: updating package.json');
|
||||
|
||||
const MAC_ASSET_PATH = 'build.mac.artifactName';
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
const MAC_ASSET_START_VALUE = '${name}-mac-${version}.${ext}';
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
const MAC_ASSET_END_VALUE = '${name}-mac-${version}-import.${ext}';
|
||||
|
||||
const WIN_ASSET_PATH = 'build.win.artifactName';
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
const WIN_ASSET_START_VALUE = '${name}-win-${version}.${ext}';
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
const WIN_ASSET_END_VALUE = '${name}-win-${version}-import.${ext}';
|
||||
|
||||
checkValue(packageJson, MAC_ASSET_PATH, MAC_ASSET_START_VALUE);
|
||||
checkValue(packageJson, WIN_ASSET_PATH, WIN_ASSET_START_VALUE);
|
||||
|
||||
_.set(packageJson, MAC_ASSET_PATH, MAC_ASSET_END_VALUE);
|
||||
_.set(packageJson, WIN_ASSET_PATH, WIN_ASSET_END_VALUE);
|
||||
|
||||
// ---
|
||||
|
||||
fs.writeFileSync(
|
||||
'./config/default.json',
|
||||
JSON.stringify(defaultConfig, null, ' ')
|
||||
);
|
||||
fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, ' '));
|
@ -1,184 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none';
|
||||
child-src 'self';
|
||||
connect-src 'self' https: wss:;
|
||||
font-src 'self';
|
||||
form-action 'self';
|
||||
frame-src 'none';
|
||||
img-src 'self' blob: data:;
|
||||
media-src 'self' blob:;
|
||||
object-src 'none';
|
||||
script-src 'self';
|
||||
style-src 'self' 'unsafe-inline';"
|
||||
>
|
||||
<link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
|
||||
<style>
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
<script type='text/x-tmpl-mustache' id='syncSettings'>
|
||||
<hr>
|
||||
<h3>{{ sync }}</h3>
|
||||
<div>
|
||||
<button class='grey sync'>{{ syncNow }}</button>
|
||||
<p>
|
||||
{{ syncExplanation }}
|
||||
<div class='synced_at'>
|
||||
{{ lastSynced }} {{ syncDate }} {{ syncTime }}
|
||||
</div>
|
||||
<div class='sync_failed'>{{ syncFailed }}</div>
|
||||
<div class='clearfix'></div>
|
||||
</p>
|
||||
</div>
|
||||
</script>
|
||||
<script type='text/x-tmpl-mustache' id='blockedUserSettings'>
|
||||
<h3>{{ blockedHeader }}</h3>
|
||||
<div class='blocked-user-settings'>
|
||||
<button class='grey unblock-button'>{{ unblockMessage }}</button>
|
||||
</div>
|
||||
</script>
|
||||
<script type='text/x-tmpl-mustache' id='settings'>
|
||||
<div class='content'>
|
||||
<a class='x close' alt='close settings' href='#'></a>
|
||||
<h2>{{ settings }}</h2>
|
||||
<div class='device-name-settings'>
|
||||
<b>{{ deviceNameLabel }}:</b> {{ deviceName }}
|
||||
</div>
|
||||
<hr>
|
||||
<div class='theme-settings'>
|
||||
<h3>{{ theme }}</h3>
|
||||
<div>
|
||||
<input type='radio' name='theme' id='theme-setting-light' value='light'>
|
||||
<label for='theme-setting-light'>{{ themeLight }}</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type='radio' name='theme' id='theme-setting-dark' value='dark'>
|
||||
<label for='theme-setting-dark'>{{ themeDark }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
{{ #isHideMenuBarSupported }}
|
||||
<div class='menu-bar-setting'>
|
||||
<input type='checkbox' name='hide-menu-bar' id='hide-menu-bar'/>
|
||||
<label for='hide-menu-bar'>{{ hideMenuBar }}</label>
|
||||
</div>
|
||||
{{ /isHideMenuBarSupported }}
|
||||
<hr>
|
||||
<div class='notification-settings'>
|
||||
<h3>{{ notifications }}</h3>
|
||||
<p>{{ notificationSettingsDialog }}</p>
|
||||
<div>
|
||||
<input type='radio' name='notifications' id='notification-setting-message' value='message'>
|
||||
<label for='notification-setting-message'>{{ nameAndMessage }} </label>
|
||||
</div>
|
||||
<div>
|
||||
<input type='radio' name='notifications' id='notification-setting-name' value='name'/>
|
||||
<label for='notification-setting-name'>{{ nameOnly }} </label>
|
||||
</div>
|
||||
<div>
|
||||
<input type='radio' name='notifications' id='notification-setting-count' value='count'/>
|
||||
<label for='notification-setting-count'>{{ noNameOrMessage }} </label>
|
||||
</div>
|
||||
<div>
|
||||
<input type='radio' name='notifications' id='notification-setting-off' value='off'/>
|
||||
<label for='notification-setting-off'>{{ disableNotifications }} </label>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
{{ #isAudioNotificationSupported }}
|
||||
<div class='audio-notification-setting'>
|
||||
<input type='checkbox' name='audio-notification' id='audio-notification'/>
|
||||
<label for='audio-notification'>{{ audioNotificationDescription }}</label>
|
||||
</div>
|
||||
{{ /isAudioNotificationSupported }}
|
||||
<hr>
|
||||
<h3>{{ generalHeader }}</h3>
|
||||
<div class='spell-check-setting'>
|
||||
<input type='checkbox' name='spell-check-setting' id='spell-check-setting' />
|
||||
<label for='spell-check-setting'>{{ spellCheckDescription }}</label>
|
||||
</div>
|
||||
<div class='link-preview-setting'>
|
||||
<input type='checkbox' name='link-preview-setting' id='link-preview-setting' />
|
||||
<label for='link-preview-setting'>{{ linkPreviewsSettingDescription }}</label>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div class='permissions-setting'>
|
||||
<h3>{{ permissions }}</h3>
|
||||
<div class='media-permissions'>
|
||||
<input type='checkbox' name='media-permissions' id='media-permissions' />
|
||||
<label for='media-permissions'>{{ mediaPermissionsDescription }}</label>
|
||||
</div>
|
||||
<div class='read-receipt-setting'>
|
||||
<input type='checkbox' name='read-receipt-setting' id='read-receipt-setting' />
|
||||
<label for='read-receipt-setting'>{{ readReceiptSettingDescription }}</label>
|
||||
</div>
|
||||
<div class='typing-indicators-setting'>
|
||||
<input type='checkbox' name='typing-indicators-setting' id='typing-indicators-setting' />
|
||||
<label for='typing-indicators-setting'>{{ typingIndicatorsSettingDescription }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class='sync-setting'></div>
|
||||
<hr>
|
||||
<div class='message-ttl-setting'>
|
||||
<h3>{{ messageTTL }}</h3>
|
||||
<div>{{ messageTTLSettingDescription }}</div>
|
||||
<div id='warning'>{{ messageTTLSettingWarning }}</div>
|
||||
<div class='inputs'>
|
||||
<input
|
||||
name='message-ttl-setting'
|
||||
id='message-ttl-setting'
|
||||
type='range'
|
||||
list='tickmarks'
|
||||
min='12'
|
||||
max='96'
|
||||
step='6'
|
||||
value='24'
|
||||
>
|
||||
<label for='message-ttl-setting'>24 Hours</label>
|
||||
<datalist id='tickmarks'>
|
||||
<option value='12'>
|
||||
<option value='18'>
|
||||
<option value='24'>
|
||||
<option value='30'>
|
||||
<option value='36'>
|
||||
<option value='42'>
|
||||
<option value='48'>
|
||||
<option value='54'>
|
||||
<option value='60'>
|
||||
<option value='66'>
|
||||
<option value='72'>
|
||||
<option value='78'>
|
||||
<option value='84'>
|
||||
<option value='90'>
|
||||
<option value='96'>
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class='clear-data-settings'>
|
||||
<h3>{{ clearDataHeader }}</h3>
|
||||
<div>
|
||||
<button class='grey destructive clear-data'>{{ clearDataButton }}</button>
|
||||
<p>{{ clearDataExplanation }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class='blocked-user-setting'>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type='text/javascript' src='js/components.js'></script>
|
||||
<script type='text/javascript' src='js/storage.js'></script>
|
||||
<script type='text/javascript' src='js/models/blockedNumbers.js'></script>
|
||||
<script type='text/javascript' src='js/blocked_number_controller.js'></script>
|
||||
<script type='text/javascript' src='js/views/whisper_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/list_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/blocked_number_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/settings_view.js'></script>
|
||||
<script type='text/javascript' src='js/settings_start.js'></script>
|
||||
</html>
|
@ -1,50 +0,0 @@
|
||||
.password {
|
||||
.content-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: $color-dark-05;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 3em;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 30em;
|
||||
}
|
||||
|
||||
.error {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.reset {
|
||||
font-size: 15px;
|
||||
margin-top: 1em;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
a {
|
||||
color: #78be20;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
color: $color-dark-05;
|
||||
background: $color-dark-85;
|
||||
|
||||
.step {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
.password {
|
||||
height: 100vh;
|
||||
|
||||
.clear-data,
|
||||
.password-prompt {
|
||||
&-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
background-color: $session-color-black;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
padding: 3 * $session-margin-lg;
|
||||
}
|
||||
|
||||
&-error-section {
|
||||
width: 100%;
|
||||
color: $session-color-white;
|
||||
margin: -$session-margin-sm 0px 2 * $session-margin-lg 0px;
|
||||
|
||||
.session-label {
|
||||
&.primary {
|
||||
background-color: rgba($session-color-primary, 0.3);
|
||||
}
|
||||
padding: $session-margin-xs $session-margin-sm;
|
||||
font-size: $session-font-xs;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&-container {
|
||||
font-family: 'SF Pro Text';
|
||||
color: $session-color-white;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 600px;
|
||||
min-width: 420px;
|
||||
padding: 3 * $session-margin-lg 2 * $session-margin-lg;
|
||||
box-sizing: border-box;
|
||||
background-color: $session-shade-4;
|
||||
border: 1px solid $session-shade-8;
|
||||
border-radius: 2px;
|
||||
|
||||
.warning-info-area,
|
||||
.password-info-area {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
h1 {
|
||||
color: $session-color-white;
|
||||
}
|
||||
svg {
|
||||
margin-right: $session-margin-lg;
|
||||
}
|
||||
}
|
||||
|
||||
p,
|
||||
input {
|
||||
margin: $session-margin-lg 0px;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
#password-prompt-input {
|
||||
width: 100%;
|
||||
color: #fff;
|
||||
background-color: #2e2e2e;
|
||||
margin-top: 2 * $session-margin-lg;
|
||||
padding: $session-margin-xs $session-margin-lg;
|
||||
outline: none;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
letter-spacing: 5px;
|
||||
font-family: 'SF Pro Text';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
describe('spellChecker', () => {
|
||||
it('should work', () => {
|
||||
assert(
|
||||
window.spellChecker.spellCheck('correct'),
|
||||
'Spellchecker returned false on a correct word.'
|
||||
);
|
||||
assert(
|
||||
!window.spellChecker.spellCheck('fhqwgads'),
|
||||
'Spellchecker returned true on a incorrect word.'
|
||||
);
|
||||
});
|
||||
});
|
@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export enum NotificationCountSize {
|
||||
// Size in px
|
||||
ON_ICON = 20,
|
||||
ON_HEADER = 24,
|
||||
}
|
||||
|
||||
interface Props {
|
||||
count?: number;
|
||||
size: number;
|
||||
onClick?: any;
|
||||
}
|
||||
|
||||
export class SessionNotificationCount extends React.Component<Props> {
|
||||
public static defaultProps = {
|
||||
size: NotificationCountSize.ON_ICON,
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { count, size, onClick } = this.props;
|
||||
|
||||
const hasHover = !!onClick;
|
||||
|
||||
const MAX_SINGLE_DIGIT = 9;
|
||||
const overflow = typeof count === 'number' && count > MAX_SINGLE_DIGIT;
|
||||
|
||||
const fontSizeVal = overflow ? size / 2 : size / 2 + 2;
|
||||
const fontSize = `${fontSizeVal}px`;
|
||||
|
||||
const bubbleStyle = {
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
};
|
||||
|
||||
const countStyle = {
|
||||
fontSize,
|
||||
marginTop: overflow ? `${size / 8}px` : '0px',
|
||||
marginLeft: overflow ? `-${size / 4}px` : '0px',
|
||||
};
|
||||
|
||||
const supStyle = {
|
||||
top: `-${size * (3 / 8)}px`,
|
||||
};
|
||||
|
||||
const countElement: JSX.Element = overflow ? (
|
||||
<>
|
||||
{MAX_SINGLE_DIGIT}
|
||||
<sup style={supStyle}>+</sup>
|
||||
</>
|
||||
) : (
|
||||
<>{count}</>
|
||||
);
|
||||
|
||||
const shouldRender = typeof count === 'number' && count > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
{shouldRender && (
|
||||
<div
|
||||
className={classNames('notification-count', hasHover && 'hover')}
|
||||
onClick={onClick}
|
||||
style={bubbleStyle}
|
||||
role="button"
|
||||
>
|
||||
<span style={countStyle}>{countElement}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue