mirror of
https://github.com/oxen-io/session-desktop.git
synced 2023-12-14 02:12:57 +01:00
Merge branch 'clearnet' into simplify-onboarding
This commit is contained in:
commit
a1194fa7bb
4
.github/workflows/build-binaries.yml
vendored
4
.github/workflows/build-binaries.yml
vendored
|
@ -76,7 +76,9 @@ jobs:
|
|||
|
||||
- name: Build linux production binaries
|
||||
if: runner.os == 'Linux'
|
||||
run: yarn build-release
|
||||
run: |
|
||||
sudo apt-get install -y rpm
|
||||
yarn build-release
|
||||
|
||||
- name: Remove unpacked files
|
||||
run: |
|
||||
|
|
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
@ -73,4 +73,6 @@ jobs:
|
|||
|
||||
- name: Build linux production binaries
|
||||
if: runner.os == 'Linux'
|
||||
run: yarn build-release-publish
|
||||
run: |
|
||||
sudo apt-get install -y rpm
|
||||
yarn build-release-publish
|
||||
|
|
|
@ -30,12 +30,8 @@ js/WebAudioRecorderMp3.js
|
|||
libtextsecure/libsignal-protocol.js
|
||||
js/util_worker.js
|
||||
libtextsecure/test/blanket_mocha.js
|
||||
test/blanket_mocha.js
|
||||
mnemonic_languages/**
|
||||
|
||||
# Test fixtures
|
||||
test/fixtures.js
|
||||
|
||||
# Managed by package manager (`bower` and `yarn`/`npm`):
|
||||
/bower.json
|
||||
/package.json
|
||||
|
|
|
@ -36,9 +36,9 @@ Then you need `git`, if you don't have that yet: https://git-scm.com/
|
|||
### Windows
|
||||
|
||||
1. **Windows 7 only:**
|
||||
* Install Microsoft .NET Framework 4.5.1:
|
||||
- Install Microsoft .NET Framework 4.5.1:
|
||||
https://www.microsoft.com/en-us/download/details.aspx?id=40773
|
||||
* Install Windows SDK version 8.1: https://developer.microsoft.com/en-us/windows/downloads/sdk-archive
|
||||
- Install Windows SDK version 8.1: https://developer.microsoft.com/en-us/windows/downloads/sdk-archive
|
||||
1. Install _Windows Build Tools_: Open the [Command Prompt (`cmd.exe`) as Administrator](<https://technet.microsoft.com/en-us/library/cc947813(v=ws.10).aspx>)
|
||||
and run: `npm install --vs2015 --global --production --add-python-to-path windows-build-tools`
|
||||
|
||||
|
@ -136,16 +136,6 @@ Please write tests! Our testing framework is
|
|||
|
||||
The easiest way to run all tests at once is `yarn test`.
|
||||
|
||||
You can browse tests from the command line with `grunt unit-tests` or in an
|
||||
interactive session with `NODE_ENV=test yarn run start`. The `libtextsecure` tests are run
|
||||
similarly: `grunt lib-unit-tests` and `NODE_ENV=test-lib yarn run start`. You can tweak
|
||||
the appropriate `test.html` for both of these runs to get code coverage numbers via
|
||||
`blanket.js` (it's shown at the bottom of the web page when the run is complete).
|
||||
|
||||
To run Node.js tests, you can run `yarn test-server` from the command line. You can get
|
||||
code coverage numbers for this kind of run via `yarn test-server-coverage`, then display
|
||||
the report with `yarn open-coverage`.
|
||||
|
||||
## Pull requests
|
||||
|
||||
So you wanna make a pull request? Please observe the following guidelines.
|
||||
|
@ -156,29 +146,29 @@ So you wanna make a pull request? Please observe the following guidelines.
|
|||
[Transifex](https://www.transifex.com/projects/p/signal-desktop).
|
||||
-->
|
||||
|
||||
* First, make sure that your `yarn ready` run passes - it's very similar to what our
|
||||
- First, make sure that your `yarn ready` run passes - it's very similar to what our
|
||||
Continuous Integration servers do to test the app.
|
||||
* Never use plain strings right in the source code - pull them from `messages.json`!
|
||||
- Never use plain strings right in the source code - pull them from `messages.json`!
|
||||
You **only** need to modify the default locale
|
||||
[`_locales/en/messages.json`](_locales/en/messages.json).
|
||||
<!-- TODO:
|
||||
Other locales are generated automatically based on that file and then periodically
|
||||
uploaded to Transifex for translation. -->
|
||||
* [Rebase](https://nathanleclaire.com/blog/2014/09/14/dont-be-scared-of-git-rebase/) your
|
||||
- [Rebase](https://nathanleclaire.com/blog/2014/09/14/dont-be-scared-of-git-rebase/) your
|
||||
changes on the latest `development` branch, resolving any conflicts.
|
||||
This ensures that your changes will merge cleanly when you open your PR.
|
||||
* Be sure to add and run tests!
|
||||
* Make sure the diff between our master and your branch contains only the
|
||||
- Be sure to add and run tests!
|
||||
- Make sure the diff between our master and your branch contains only the
|
||||
minimal set of changes needed to implement your feature or bugfix. This will
|
||||
make it easier for the person reviewing your code to approve the changes.
|
||||
Please do not submit a PR with commented out code or unfinished features.
|
||||
* Avoid meaningless or too-granular commits. If your branch contains commits like
|
||||
- Avoid meaningless or too-granular commits. If your branch contains commits like
|
||||
the lines of "Oops, reverted this change" or "Just experimenting, will
|
||||
delete this later", please [squash or rebase those changes away](https://robots.thoughtbot.com/git-interactive-rebase-squash-amend-rewriting-history).
|
||||
* Don't have too few commits. If you have a complicated or long lived feature
|
||||
- Don't have too few commits. If you have a complicated or long lived feature
|
||||
branch, it may make sense to break the changes up into logical atomic chunks
|
||||
to aid in the review process.
|
||||
* Provide a well written and nicely formatted commit message. See [this
|
||||
- Provide a well written and nicely formatted commit message. See [this
|
||||
link](http://chris.beams.io/posts/git-commit/)
|
||||
for some tips on formatting. As far as content, try to include in your
|
||||
summary
|
||||
|
|
210
Gruntfile.js
210
Gruntfile.js
|
@ -1,12 +1,6 @@
|
|||
const path = require('path');
|
||||
const packageJson = require('./package.json');
|
||||
const importOnce = require('node-sass-import-once');
|
||||
const rimraf = require('rimraf');
|
||||
const mkdirp = require('mkdirp');
|
||||
const spectron = require('spectron');
|
||||
const asar = require('asar');
|
||||
const fs = require('fs');
|
||||
const assert = require('assert');
|
||||
const sass = require('node-sass');
|
||||
|
||||
/* eslint-disable more/no-then, no-console */
|
||||
|
@ -53,10 +47,6 @@ module.exports = grunt => {
|
|||
src: libtextsecurecomponents,
|
||||
dest: 'libtextsecure/components.js',
|
||||
},
|
||||
test: {
|
||||
src: ['node_modules/mocha/mocha.js', 'node_modules/chai/chai.js', 'test/_test.js'],
|
||||
dest: 'test/test.js',
|
||||
},
|
||||
libtextsecure: {
|
||||
options: {
|
||||
banner: ';(function() {\n',
|
||||
|
@ -130,27 +120,6 @@ module.exports = grunt => {
|
|||
cmd: 'yarn build-protobuf',
|
||||
},
|
||||
},
|
||||
'test-release': {
|
||||
osx: {
|
||||
archive: `mac/${packageJson.productName}.app/Contents/Resources/app.asar`,
|
||||
appUpdateYML: `mac/${packageJson.productName}.app/Contents/Resources/app-update.yml`,
|
||||
exe: `mac/${packageJson.productName}.app/Contents/MacOS/${packageJson.productName}`,
|
||||
},
|
||||
mas: {
|
||||
archive: 'mas/Signal.app/Contents/Resources/app.asar',
|
||||
appUpdateYML: 'mac/Signal.app/Contents/Resources/app-update.yml',
|
||||
exe: `mas/${packageJson.productName}.app/Contents/MacOS/${packageJson.productName}`,
|
||||
},
|
||||
linux: {
|
||||
archive: 'linux-unpacked/resources/app.asar',
|
||||
exe: `linux-unpacked/${packageJson.name}`,
|
||||
},
|
||||
win: {
|
||||
archive: 'win-unpacked/resources/app.asar',
|
||||
appUpdateYML: 'win-unpacked/resources/app-update.yml',
|
||||
exe: `win-unpacked/${packageJson.productName}.exe`,
|
||||
},
|
||||
},
|
||||
gitinfo: {}, // to be populated by grunt gitinfo
|
||||
});
|
||||
|
||||
|
@ -197,186 +166,7 @@ module.exports = grunt => {
|
|||
mkdirp.sync('release');
|
||||
});
|
||||
|
||||
function runTests(environment, cb) {
|
||||
let failure;
|
||||
const { Application } = spectron;
|
||||
const electronBinary = process.platform === 'win32' ? 'electron.cmd' : 'electron';
|
||||
const app = new Application({
|
||||
path: path.join(__dirname, 'node_modules', '.bin', electronBinary),
|
||||
args: [path.join(__dirname, 'main.js')],
|
||||
env: {
|
||||
NODE_ENV: environment,
|
||||
},
|
||||
requireName: 'unused',
|
||||
chromeDriverArgs: [
|
||||
`remote-debugging-port=${Math.floor(Math.random() * (9999 - 9000) + 9000)}`,
|
||||
],
|
||||
});
|
||||
|
||||
function getMochaResults() {
|
||||
// eslint-disable-next-line no-undef
|
||||
return window.mochaResults;
|
||||
}
|
||||
|
||||
app
|
||||
.start()
|
||||
.then(() =>
|
||||
app.client.waitUntil(
|
||||
() => app.client.execute(getMochaResults).then(data => Boolean(data.value)),
|
||||
25000,
|
||||
'Expected to find window.mochaResults set!'
|
||||
)
|
||||
)
|
||||
.then(() => app.client.execute(getMochaResults))
|
||||
.then(data => {
|
||||
const results = data.value;
|
||||
if (results.failures > 0) {
|
||||
console.error(results.reports);
|
||||
failure = () => grunt.fail.fatal(`Found ${results.failures} failing unit tests.`);
|
||||
return app.client.log('browser');
|
||||
}
|
||||
grunt.log.ok(`${results.passes} tests passed.`);
|
||||
return null;
|
||||
})
|
||||
.then(logs => {
|
||||
if (logs) {
|
||||
console.error();
|
||||
console.error('Because tests failed, printing browser logs:');
|
||||
console.error(logs);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
failure = () => grunt.fail.fatal(`Something went wrong: ${error.message} ${error.stack}`);
|
||||
})
|
||||
.then(() => {
|
||||
// We need to use the failure variable and this early stop to clean up before
|
||||
// shutting down. Grunt's fail methods are the only way to set the return value,
|
||||
// but they shut the process down immediately!
|
||||
if (failure) {
|
||||
console.log();
|
||||
console.log('Main process logs:');
|
||||
return app.client.getMainProcessLogs().then(logs => {
|
||||
logs.forEach(log => {
|
||||
console.log(log);
|
||||
});
|
||||
try {
|
||||
return app.stop();
|
||||
} catch (err) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
try {
|
||||
return app.stop();
|
||||
} catch (err) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
if (failure) {
|
||||
failure();
|
||||
}
|
||||
cb();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Second-level error:', error.message, error.stack);
|
||||
if (failure) {
|
||||
failure();
|
||||
}
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
grunt.registerTask('unit-tests', 'Run unit tests w/Electron', function thisNeeded() {
|
||||
const environment = grunt.option('env') || 'test';
|
||||
const done = this.async();
|
||||
|
||||
runTests(environment, done);
|
||||
});
|
||||
|
||||
grunt.registerMultiTask('test-release', 'Test packaged releases', function thisNeeded() {
|
||||
const dir = grunt.option('dir') || 'release';
|
||||
const environment = grunt.option('env') || 'production';
|
||||
const config = this.data;
|
||||
const archive = [dir, config.archive].join('/');
|
||||
const files = [
|
||||
'config/default.json',
|
||||
`config/${environment}.json`,
|
||||
`config/local-${environment}.json`,
|
||||
];
|
||||
|
||||
console.log(this.target, archive);
|
||||
const releaseFiles = files.concat(config.files || []);
|
||||
releaseFiles.forEach(fileName => {
|
||||
console.log(fileName);
|
||||
try {
|
||||
asar.statFile(archive, fileName);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw new Error(`Missing file ${fileName}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (config.appUpdateYML) {
|
||||
const appUpdateYML = [dir, config.appUpdateYML].join('/');
|
||||
if (fs.existsSync(appUpdateYML)) {
|
||||
console.log('auto update ok');
|
||||
} else {
|
||||
throw new Error(`Missing auto update config ${appUpdateYML}`);
|
||||
}
|
||||
}
|
||||
|
||||
const done = this.async();
|
||||
// A simple test to verify a visible window is opened with a title
|
||||
const { Application } = spectron;
|
||||
|
||||
const app = new Application({
|
||||
path: [dir, config.exe].join('/'),
|
||||
requireName: 'unused',
|
||||
chromeDriverArgs: [
|
||||
`remote-debugging-port=${Math.floor(Math.random() * (9999 - 9000) + 9000)}`,
|
||||
],
|
||||
});
|
||||
|
||||
app
|
||||
.start()
|
||||
.then(() => app.client.getWindowCount())
|
||||
.then(count => {
|
||||
assert.equal(count, 1);
|
||||
console.log('window opened');
|
||||
})
|
||||
.then(() =>
|
||||
// Get the window's title
|
||||
app.client.getTitle()
|
||||
)
|
||||
.then(title => {
|
||||
// TODO: restore once fixed on win
|
||||
if (this.target !== 'win') {
|
||||
// Verify the window's title
|
||||
assert.equal(title, packageJson.productName);
|
||||
console.log('title ok');
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
assert(app.chromeDriver.logLines.indexOf(`NODE_ENV ${environment}`) > -1);
|
||||
console.log('environment ok');
|
||||
})
|
||||
.then(
|
||||
() =>
|
||||
// Successfully completed test
|
||||
app.stop(),
|
||||
error =>
|
||||
// Test failed!
|
||||
app.stop().then(() => {
|
||||
grunt.fail.fatal(`Test failed: ${error.message} ${error.stack}`);
|
||||
})
|
||||
)
|
||||
.then(done);
|
||||
});
|
||||
|
||||
grunt.registerTask('dev', ['default', 'watch']);
|
||||
grunt.registerTask('test', ['unit-tests']);
|
||||
grunt.registerTask('date', ['gitinfo', 'getExpireTime']);
|
||||
grunt.registerTask('default', [
|
||||
'exec:build-protobuf',
|
||||
|
|
|
@ -150,6 +150,8 @@
|
|||
"savedTheFile": "Media saved by $name$",
|
||||
"linkPreviewsTitle": "Send Link Previews",
|
||||
"linkPreviewDescription": "Previews are supported for most urls",
|
||||
"startInTrayTitle": "Start in Tray",
|
||||
"startInTrayDescription": "Start Session as a minified app ",
|
||||
"linkPreviewsConfirmMessage": "You will not have full metadata protection when sending or receiving link previews.",
|
||||
"mediaPermissionsTitle": "Microphone and Camera",
|
||||
"mediaPermissionsDescription": "Allow access to camera and microphone",
|
||||
|
|
|
@ -224,8 +224,7 @@ function openAndMigrateDatabase(filePath, key) {
|
|||
keyDatabase(db, key);
|
||||
switchToWAL(db);
|
||||
migrateSchemaVersion(db);
|
||||
// Because foreign key support is not enabled by default! // actually, Session does not care
|
||||
// db.pragma('foreign_keys = ON');
|
||||
db.pragma('secure_delete = ON');
|
||||
|
||||
return db;
|
||||
} catch (error) {
|
||||
|
|
|
@ -33,10 +33,6 @@ function initialize() {
|
|||
} catch (error) {
|
||||
const errorForDisplay = error && error.stack ? error.stack : error;
|
||||
console.log(`sql channel error with call ${callName}: ${errorForDisplay}`);
|
||||
// FIXME this line cause the test-integration to fail and we probably don't need it during test
|
||||
if (!process.env.NODE_ENV.includes('test-integration')) {
|
||||
event.sender.send(`${SQL_CHANNEL_KEY}-done`, jobId, errorForDisplay);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
const path = require('path');
|
||||
|
||||
const fs = require('fs');
|
||||
const { app, Menu, Tray } = require('electron');
|
||||
|
||||
let trayContextMenu = null;
|
||||
let tray = null;
|
||||
|
||||
function createTrayIcon(getMainWindow, messages) {
|
||||
const iconNoNewMessages = path.join(__dirname, '..', 'images', 'session', `session_icon.png`);
|
||||
// keep the duplicated part to allow for search and find
|
||||
const iconFile = process.platform === 'darwin' ? 'session_icon_16.png' : 'session_icon_32.png';
|
||||
const iconNoNewMessages = path.join(__dirname, '..', 'images', 'session', iconFile);
|
||||
tray = new Tray(iconNoNewMessages);
|
||||
|
||||
tray.forceOnTop = mainWindow => {
|
||||
|
@ -70,16 +71,6 @@ function createTrayIcon(getMainWindow, messages) {
|
|||
tray.setContextMenu(trayContextMenu);
|
||||
};
|
||||
|
||||
tray.updateIcon = () => {
|
||||
const image = iconNoNewMessages;
|
||||
|
||||
if (!fs.existsSync(image)) {
|
||||
console.log('tray.updateIcon: Image for tray update does not exist!');
|
||||
return;
|
||||
}
|
||||
tray.setImage(image);
|
||||
};
|
||||
|
||||
tray.on('click', tray.showWindow);
|
||||
|
||||
tray.setToolTip(messages.sessionMessenger);
|
||||
|
|
|
@ -5,15 +5,12 @@
|
|||
"contentProxyUrl": "",
|
||||
"seedNodeList": [
|
||||
{
|
||||
"ip_url": "https://116.203.53.213:4433/",
|
||||
"url": "https://storage.seed1.loki.network:4433/"
|
||||
},
|
||||
{
|
||||
"ip_url": "https://212.199.114.66:4433/",
|
||||
"url": "https://storage.seed3.loki.network:4433/"
|
||||
},
|
||||
{
|
||||
"ip_url": "https://144.76.164.202:4433/",
|
||||
"url": "https://public.loki.foundation:4433/"
|
||||
}
|
||||
],
|
||||
|
@ -22,7 +19,5 @@
|
|||
"buildExpiration": 0,
|
||||
"commitHash": "",
|
||||
"import": false,
|
||||
"serverTrustRoot": "BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx",
|
||||
"defaultPublicChatServer": "https://chat.getsession.org",
|
||||
"defaultFileServer": "https://file.getsession.org"
|
||||
"serverTrustRoot": "BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx"
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
{
|
||||
"seedNodeList": [
|
||||
{
|
||||
"url": "http://public.loki.foundation:38157/",
|
||||
"ip_url": "http://144.76.164.202:38157/"
|
||||
"url": "http://public.loki.foundation:38157/"
|
||||
}
|
||||
],
|
||||
"openDevTools": true
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
{
|
||||
"seedNodeList": [
|
||||
{
|
||||
"ip_url": "http://127.0.0.1:22129/",
|
||||
"url": "http://localhost:22129/"
|
||||
}
|
||||
],
|
||||
"openDevTools": true,
|
||||
"defaultFileServer": "https://file-dev.getsession.org"
|
||||
"openDevTools": true
|
||||
}
|
||||
|
|
BIN
images/session/session_icon_16.png
Normal file
BIN
images/session/session_icon_16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
BIN
images/session/session_icon_32.png
Normal file
BIN
images/session/session_icon_32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
|
@ -63,7 +63,7 @@
|
|||
// of preload.js processing
|
||||
window.setImmediate = window.nodeSetImmediate;
|
||||
window.globalOnlineStatus = true; // default to true as we don't get an event on app start
|
||||
|
||||
window.getGlobalOnlineStatus = () => window.globalOnlineStatus;
|
||||
const { Views } = window.Signal;
|
||||
|
||||
// Implicitly used in `indexeddb-backbonejs-adapter`:
|
||||
|
@ -334,7 +334,7 @@
|
|||
// if not undefined, we take the opposite
|
||||
const newValue = currentValue !== undefined ? !currentValue : false;
|
||||
window.Events.setSpellCheck(newValue);
|
||||
window.libsession.Utils.ToastUtils.pushSpellCheckDirty();
|
||||
window.libsession.Utils.ToastUtils.pushRestartNeeded();
|
||||
};
|
||||
|
||||
window.toggleMediaPermissions = async () => {
|
||||
|
@ -409,9 +409,6 @@
|
|||
|
||||
// Clear timer, since we're only called when the timer is expired
|
||||
disconnectTimer = null;
|
||||
|
||||
// FIXME audric stop polling opengroupv2 and swarm nodes
|
||||
|
||||
window.libsession.Utils.AttachmentDownloads.stop();
|
||||
}
|
||||
|
||||
|
|
99
main.js
99
main.js
|
@ -46,8 +46,6 @@ function getMainWindow() {
|
|||
|
||||
// Tray icon and related objects
|
||||
let tray = null;
|
||||
const startInTray = process.argv.some(arg => arg === '--start-in-tray');
|
||||
const usingTrayIcon = startInTray || process.argv.some(arg => arg === '--use-tray-icon');
|
||||
|
||||
const config = require('./app/config');
|
||||
|
||||
|
@ -171,7 +169,6 @@ function prepareURL(pathSegments, moreKeys) {
|
|||
contentProxyUrl: config.contentProxyUrl,
|
||||
serverTrustRoot: config.get('serverTrustRoot'),
|
||||
appStartInitialSpellcheckSetting,
|
||||
defaultFileServer: config.get('defaultFileServer'),
|
||||
...moreKeys,
|
||||
},
|
||||
});
|
||||
|
@ -231,13 +228,19 @@ function isVisible(window, bounds) {
|
|||
);
|
||||
}
|
||||
|
||||
function getStartInTray() {
|
||||
const startInTray =
|
||||
process.argv.some(arg => arg === '--start-in-tray') || userConfig.get('startInTray');
|
||||
const usingTrayIcon = startInTray || process.argv.some(arg => arg === '--use-tray-icon');
|
||||
return { usingTrayIcon, startInTray };
|
||||
}
|
||||
async function createWindow() {
|
||||
const { screen } = electron;
|
||||
const { minWidth, minHeight, width, height } = getWindowSize();
|
||||
|
||||
const windowOptions = Object.assign(
|
||||
{
|
||||
show: !startInTray, // allow to start minimised in tray
|
||||
show: true,
|
||||
width,
|
||||
height,
|
||||
minWidth,
|
||||
|
@ -246,6 +249,7 @@ async function createWindow() {
|
|||
backgroundColor: '#fff',
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
enableRemoteModule: true,
|
||||
nodeIntegrationInWorker: false,
|
||||
contextIsolation: false,
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
|
@ -340,13 +344,7 @@ async function createWindow() {
|
|||
}
|
||||
});
|
||||
|
||||
if (config.environment === 'test') {
|
||||
mainWindow.loadURL(prepareURL([__dirname, 'test', 'index.html']));
|
||||
} else if (config.environment.includes('test-integration')) {
|
||||
mainWindow.loadURL(prepareURL([__dirname, 'background_test.html']));
|
||||
} else {
|
||||
mainWindow.loadURL(prepareURL([__dirname, 'background.html']));
|
||||
}
|
||||
mainWindow.loadURL(prepareURL([__dirname, 'background.html']));
|
||||
|
||||
if (config.get('openDevTools')) {
|
||||
// Open the DevTools.
|
||||
|
@ -364,11 +362,7 @@ async function createWindow() {
|
|||
shouldQuit: windowState.shouldQuit(),
|
||||
});
|
||||
// If the application is terminating, just do the default
|
||||
if (
|
||||
config.environment === 'test' ||
|
||||
config.environment.includes('test-integration') ||
|
||||
(mainWindow.readyForShutdown && windowState.shouldQuit())
|
||||
) {
|
||||
if (mainWindow.readyForShutdown && windowState.shouldQuit()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -378,7 +372,10 @@ async function createWindow() {
|
|||
|
||||
// On Mac, or on other platforms when the tray icon is in use, the window
|
||||
// should be only hidden, not closed, when the user clicks the close button
|
||||
if (!windowState.shouldQuit() && (usingTrayIcon || process.platform === 'darwin')) {
|
||||
if (
|
||||
!windowState.shouldQuit() &&
|
||||
(getStartInTray().usingTrayIcon || process.platform === 'darwin')
|
||||
) {
|
||||
// toggle the visibility of the show/hide tray icon menu entries
|
||||
if (tray) {
|
||||
tray.updateContextMenu();
|
||||
|
@ -485,7 +482,7 @@ function showPasswordWindow() {
|
|||
|
||||
passwordWindow.on('close', e => {
|
||||
// If the application is terminating, just do the default
|
||||
if (config.environment === 'test' || windowState.shouldQuit()) {
|
||||
if (windowState.shouldQuit()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -495,7 +492,10 @@ function showPasswordWindow() {
|
|||
|
||||
// On Mac, or on other platforms when the tray icon is in use, the window
|
||||
// should be only hidden, not closed, when the user clicks the close button
|
||||
if (!windowState.shouldQuit() && (usingTrayIcon || process.platform === 'darwin')) {
|
||||
if (
|
||||
!windowState.shouldQuit() &&
|
||||
(getStartInTray().usingTrayIcon || process.platform === 'darwin')
|
||||
) {
|
||||
// toggle the visibility of the show/hide tray icon menu entries
|
||||
if (tray) {
|
||||
tray.updateContextMenu();
|
||||
|
@ -610,14 +610,12 @@ app.on('ready', async () => {
|
|||
const userDataPath = await getRealPath(app.getPath('userData'));
|
||||
const installPath = await getRealPath(app.getAppPath());
|
||||
|
||||
if (process.env.NODE_ENV !== 'test' && !process.env.NODE_ENV.includes('test-integration')) {
|
||||
installFileHandler({
|
||||
protocol: electronProtocol,
|
||||
userDataPath,
|
||||
installPath,
|
||||
isWindows: process.platform === 'win32',
|
||||
});
|
||||
}
|
||||
installFileHandler({
|
||||
protocol: electronProtocol,
|
||||
userDataPath,
|
||||
installPath,
|
||||
isWindows: process.platform === 'win32',
|
||||
});
|
||||
|
||||
installWebHandler({
|
||||
protocol: electronProtocol,
|
||||
|
@ -630,7 +628,7 @@ app.on('ready', async () => {
|
|||
logger.info('app ready');
|
||||
logger.info(`starting version ${packageJson.version}`);
|
||||
if (!locale) {
|
||||
const appLocale = process.env.NODE_ENV === 'test' ? 'en' : app.getLocale();
|
||||
const appLocale = app.getLocale() || 'en';
|
||||
locale = loadLocale({ appLocale, logger });
|
||||
}
|
||||
|
||||
|
@ -700,7 +698,7 @@ async function showMainWindow(sqlKey, passwordAttempt = false) {
|
|||
|
||||
await createWindow();
|
||||
|
||||
if (usingTrayIcon) {
|
||||
if (getStartInTray().usingTrayIcon) {
|
||||
tray = createTrayIcon(getMainWindow, locale.messages);
|
||||
}
|
||||
|
||||
|
@ -768,6 +766,10 @@ app.on('before-quit', () => {
|
|||
shouldQuit: windowState.shouldQuit(),
|
||||
});
|
||||
|
||||
if (tray) {
|
||||
tray.destroy();
|
||||
}
|
||||
|
||||
windowState.markShouldQuit();
|
||||
});
|
||||
|
||||
|
@ -775,11 +777,7 @@ app.on('before-quit', () => {
|
|||
app.on('window-all-closed', () => {
|
||||
// On OS X it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (
|
||||
process.platform !== 'darwin' ||
|
||||
config.environment === 'test' ||
|
||||
config.environment.includes('test-integration')
|
||||
) {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
@ -866,12 +864,6 @@ ipc.on('close-about', () => {
|
|||
}
|
||||
});
|
||||
|
||||
ipc.on('update-tray-icon', (event, unreadCount) => {
|
||||
if (tray) {
|
||||
tray.updateIcon(unreadCount);
|
||||
}
|
||||
});
|
||||
|
||||
// Password screen related IPC calls
|
||||
ipc.on('password-window-login', async (event, passPhrase) => {
|
||||
const sendResponse = e => event.sender.send('password-window-login-response', e);
|
||||
|
@ -885,6 +877,33 @@ ipc.on('password-window-login', async (event, passPhrase) => {
|
|||
sendResponse(localisedError || 'Invalid password');
|
||||
}
|
||||
});
|
||||
ipc.on('start-in-tray-on-start', async (event, newValue) => {
|
||||
try {
|
||||
userConfig.set('startInTray', newValue);
|
||||
if (newValue) {
|
||||
if (!tray) {
|
||||
tray = createTrayIcon(getMainWindow, locale.messages);
|
||||
}
|
||||
} else {
|
||||
// destroy is not working for a lot of desktop env. So for simplicity, we don't destroy it here but just
|
||||
// show a toast to explain to the user that he needs to restart
|
||||
// tray.destroy();
|
||||
// tray = null;
|
||||
}
|
||||
event.sender.send('start-in-tray-on-start-response', null);
|
||||
} catch (e) {
|
||||
event.sender.send('start-in-tray-on-start-response', e);
|
||||
}
|
||||
});
|
||||
|
||||
ipc.on('get-start-in-tray', async (event, newValue) => {
|
||||
try {
|
||||
const val = userConfig.get('startInTray', newValue);
|
||||
event.sender.send('get-start-in-tray-response', val);
|
||||
} catch (e) {
|
||||
event.sender.send('get-start-in-tray-response', false);
|
||||
}
|
||||
});
|
||||
|
||||
ipc.on('set-password', async (event, passPhrase, oldPhrase) => {
|
||||
const sendResponse = e => event.sender.send('set-password-response', e);
|
||||
|
|
21
package.json
21
package.json
|
@ -18,7 +18,6 @@
|
|||
"start": "cross-env NODE_APP_INSTANCE=$MULTI electron .",
|
||||
"start-prod": "cross-env NODE_ENV=production NODE_APP_INSTANCE=devprod$MULTI electron .",
|
||||
"start-prod2": "cross-env NODE_ENV=production NODE_APP_INSTANCE=devprod2 electron .",
|
||||
"start-swarm-test": "cross-env NODE_ENV=swarm-testing NODE_APP_INSTANCE=$MULTI electron .",
|
||||
"grunt": "grunt",
|
||||
"grunt:dev": "yarn clean-transpile; yarn grunt dev --force",
|
||||
"generate": "yarn grunt --force",
|
||||
|
@ -26,20 +25,17 @@
|
|||
"build-release-non-linux": "cross-env SIGNAL_ENV=production electron-builder --config.extraMetadata.environment=production --publish=never --config.directories.output=release",
|
||||
"build-release:win32": "yarn build-release-non-linux",
|
||||
"build-release:macos": "yarn build-release-non-linux",
|
||||
"build-release:linux": "yarn build-release-non-linux && yarn sedtoAppImage && yarn build-release-non-linux && yarn sedtoDeb",
|
||||
"build-release:linux": "yarn sedtoDeb; yarn build-release-non-linux && yarn sedtoAppImage && yarn build-release-non-linux && yarn sedtoDeb",
|
||||
"build-release-publish": "run-script-os",
|
||||
"build-release-publish-non-linux": "$(yarn bin)/electron-builder --config.extraMetadata.environment=$SIGNAL_ENV --publish=always",
|
||||
"build-release-publish:win32": "yarn build-release-publish-non-linux",
|
||||
"build-release-publish:macos": "yarn build-release-publish-non-linux",
|
||||
"build-release-publish:linux": "yarn build-release-publish-non-linux && yarn sedtoAppImage && yarn build-release-publish-non-linux && yarn sedtoDeb",
|
||||
"build-release-publish:linux": "yarn sedtoDeb; yarn build-release-publish-non-linux && yarn sedtoAppImage && yarn build-release-publish-non-linux && yarn sedtoDeb",
|
||||
"build-module-protobuf": "pbjs --target static-module --wrap commonjs --out ts/protobuf/compiled.js protos/*.proto && pbts --out ts/protobuf/compiled.d.ts ts/protobuf/compiled.js --force-long",
|
||||
"clean-module-protobuf": "rimraf ts/protobuf/compiled.d.ts ts/protobuf/compiled.js",
|
||||
"build-protobuf": "yarn build-module-protobuf",
|
||||
"clean-protobuf": "yarn clean-module-protobuf",
|
||||
"test": "yarn test-node && yarn test-electron",
|
||||
"test-view": "NODE_ENV=test yarn run start",
|
||||
"test-lib-view": "NODE_ENV=test-lib yarn run start",
|
||||
"test-electron": "yarn grunt test",
|
||||
"test": "yarn test-node",
|
||||
"test-node": "mocha --recursive --exit --timeout 10000 test/app test/modules \"./ts/test/**/*_test.js\" ",
|
||||
"eslint-full": "eslint .",
|
||||
"lint-full": "yarn format-full && yarn lint-files-full",
|
||||
|
@ -51,8 +47,8 @@
|
|||
"clean-transpile": "rimraf 'ts/**/*.js ts/*.js' 'ts/*.js.map' 'ts/**/*.js.map' && rimraf tsconfig.tsbuildinfo;",
|
||||
"ready": "yarn clean-transpile; yarn grunt && yarn lint-full && yarn test",
|
||||
"build:webpack:sql-worker": "cross-env NODE_ENV=production webpack -c webpack-sql-worker.config.ts",
|
||||
"sedtoAppImage": "sed -i 's/\"target\": \"deb\"/\"target\": \"AppImage\"/g' package.json",
|
||||
"sedtoDeb": "sed -i 's/\"target\": \"AppImage\"/\"target\": \"deb\"/g' package.json"
|
||||
"sedtoAppImage": "sed -i 's/\"target\": \\[\"deb\", \"rpm\", \"freebsd\"\\]/\"target\": \"AppImage\"/g' package.json",
|
||||
"sedtoDeb": "sed -i 's/\"target\": \"AppImage\"/\"target\": \\[\"deb\", \"rpm\", \"freebsd\"\\]/g' package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^1.4.0",
|
||||
|
@ -186,7 +182,7 @@
|
|||
"chai-bytes": "^0.1.2",
|
||||
"css-loader": "^3.6.0",
|
||||
"dashdash": "1.14.1",
|
||||
"electron": "8.2.0",
|
||||
"electron": "^13.1.9",
|
||||
"electron-builder": "22.8.0",
|
||||
"electron-notarize": "^0.2.0",
|
||||
"eslint": "4.14.0",
|
||||
|
@ -213,7 +209,6 @@
|
|||
"qs": "6.5.1",
|
||||
"run-script-os": "^1.1.6",
|
||||
"sinon": "9.0.2",
|
||||
"spectron": "^10.0.0",
|
||||
"ts-loader": "4.1.0",
|
||||
"ts-mock-imports": "^1.3.0",
|
||||
"tslint": "5.19.0",
|
||||
|
@ -275,17 +270,15 @@
|
|||
"StartupWMClass": "Session"
|
||||
},
|
||||
"asarUnpack": "node_modules/spellchecker/vendor/hunspell_dictionaries",
|
||||
"target": "deb",
|
||||
"target": ["deb", "rpm", "freebsd"],
|
||||
"icon": "build/icon.icns"
|
||||
},
|
||||
"asarUnpack": [
|
||||
"ts/sql/mainWorker.bundle.js",
|
||||
"node_modules/better-sqlite3/build/Release/better_sqlite3.node"
|
||||
],
|
||||
"deb": {
|
||||
"depends": [
|
||||
"libnotify4",
|
||||
"libappindicator1",
|
||||
"libxtst6",
|
||||
"libnss3",
|
||||
"libasound2",
|
||||
|
|
59
preload.js
59
preload.js
|
@ -46,11 +46,6 @@ window.getServerTrustRoot = () => config.serverTrustRoot;
|
|||
window.JobQueue = JobQueue;
|
||||
window.isBehindProxy = () => Boolean(config.proxyUrl);
|
||||
|
||||
window.getStoragePubKey = key => (window.isDev() ? key.substring(2) : key);
|
||||
|
||||
window.getDefaultFileServer = () => config.defaultFileServer;
|
||||
window.initialisedAPI = false;
|
||||
|
||||
window.lokiFeatureFlags = {
|
||||
useOnionRequests: true,
|
||||
useFileOnionRequests: true,
|
||||
|
@ -59,12 +54,6 @@ window.lokiFeatureFlags = {
|
|||
enablePinConversations: true,
|
||||
};
|
||||
|
||||
if (typeof process.env.NODE_ENV === 'string' && process.env.NODE_ENV.includes('test-integration')) {
|
||||
window.electronRequire = require;
|
||||
// during test-integration, file server is started on localhost
|
||||
window.getDefaultFileServer = () => 'http://127.0.0.1:7070';
|
||||
}
|
||||
|
||||
window.isBeforeVersion = (toCheck, baseVersion) => {
|
||||
try {
|
||||
return semver.lt(toCheck, baseVersion);
|
||||
|
@ -123,6 +112,23 @@ window.setPassword = (passPhrase, oldPhrase) =>
|
|||
ipc.send('set-password', passPhrase, oldPhrase);
|
||||
});
|
||||
|
||||
window.setStartInTray = startInTray =>
|
||||
new Promise((resolve, reject) => {
|
||||
ipc.once('start-in-tray-on-start-response', (_event, error) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
ipc.send('start-in-tray-on-start', startInTray);
|
||||
});
|
||||
|
||||
window.getStartInTray = () =>
|
||||
new Promise(resolve => {
|
||||
ipc.once('get-start-in-tray-response', (event, value) => resolve(value));
|
||||
ipc.send('get-start-in-tray');
|
||||
});
|
||||
|
||||
window.libsession = require('./ts/session');
|
||||
|
||||
window.getConversationController = window.libsession.Conversations.getConversationController;
|
||||
|
@ -162,8 +168,6 @@ ipc.on('mediaPermissionsChanged', () => {
|
|||
window.closeAbout = () => ipc.send('close-about');
|
||||
window.readyForUpdates = () => ipc.send('ready-for-updates');
|
||||
|
||||
window.updateTrayIcon = unreadCount => ipc.send('update-tray-icon', unreadCount);
|
||||
|
||||
ipc.on('get-theme-setting', () => {
|
||||
const theme = window.Events.getThemeSetting();
|
||||
ipc.send('get-success-theme-setting', theme);
|
||||
|
@ -343,35 +347,6 @@ Promise.prototype.ignore = function() {
|
|||
this.then(() => {});
|
||||
};
|
||||
|
||||
if (
|
||||
config.environment.includes('test') &&
|
||||
!config.environment.includes('swarm-testing') &&
|
||||
!config.environment.includes('test-integration')
|
||||
) {
|
||||
const isWindows = process.platform === 'win32';
|
||||
/* eslint-disable global-require, import/no-extraneous-dependencies */
|
||||
window.test = {
|
||||
glob: require('glob'),
|
||||
fse: require('fs-extra'),
|
||||
tmp: require('tmp'),
|
||||
path: require('path'),
|
||||
basePath: __dirname,
|
||||
attachmentsPath: window.Signal.Migrations.attachmentsPath,
|
||||
isWindows,
|
||||
};
|
||||
/* eslint-enable global-require, import/no-extraneous-dependencies */
|
||||
window.lokiFeatureFlags = {};
|
||||
}
|
||||
if (config.environment.includes('test-integration')) {
|
||||
window.lokiFeatureFlags = {
|
||||
useOnionRequests: false,
|
||||
useFileOnionRequests: false,
|
||||
};
|
||||
/* eslint-disable global-require, import/no-extraneous-dependencies */
|
||||
window.sinon = require('sinon');
|
||||
/* eslint-enable global-require, import/no-extraneous-dependencies */
|
||||
}
|
||||
|
||||
// Blocking
|
||||
|
||||
const { BlockedNumberController } = require('./ts/util/blockedNumberController');
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
// For reference: https://github.com/airbnb/javascript
|
||||
|
||||
module.exports = {
|
||||
env: {
|
||||
mocha: true,
|
||||
browser: true,
|
||||
},
|
||||
|
||||
globals: {
|
||||
assert: true,
|
||||
assertEqualArrayBuffers: true,
|
||||
clearDatabase: true,
|
||||
dcodeIO: true,
|
||||
getString: true,
|
||||
hexToArrayBuffer: true,
|
||||
PROTO_ROOT: true,
|
||||
stringToArrayBuffer: true,
|
||||
},
|
||||
|
||||
parserOptions: {
|
||||
sourceType: 'script',
|
||||
},
|
||||
|
||||
rules: {
|
||||
// We still get the value of this rule, it just allows for dev deps
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
{
|
||||
devDependencies: true,
|
||||
},
|
||||
],
|
||||
|
||||
// We want to keep each test structured the same, even if its contents are tiny
|
||||
'arrow-body-style': 'off',
|
||||
|
||||
strict: 'off',
|
||||
'more/no-then': 'off',
|
||||
},
|
||||
};
|
|
@ -1,89 +0,0 @@
|
|||
/* global chai, Whisper, _, Backbone */
|
||||
|
||||
mocha
|
||||
.setup('bdd')
|
||||
.fullTrace()
|
||||
.timeout(10000);
|
||||
window.assert = chai.assert;
|
||||
window.PROTO_ROOT = '../protos';
|
||||
|
||||
const OriginalReporter = mocha._reporter;
|
||||
|
||||
const SauceReporter = function Constructor(runner) {
|
||||
const failedTests = [];
|
||||
|
||||
runner.on('end', () => {
|
||||
window.mochaResults = runner.stats;
|
||||
window.mochaResults.reports = failedTests;
|
||||
});
|
||||
|
||||
runner.on('fail', (test, err) => {
|
||||
const flattenTitles = item => {
|
||||
const titles = [];
|
||||
while (item.parent.title) {
|
||||
titles.push(item.parent.title);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
item = item.parent;
|
||||
}
|
||||
return titles.reverse();
|
||||
};
|
||||
failedTests.push({
|
||||
name: test.title,
|
||||
result: false,
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
titles: flattenTitles(test),
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new OriginalReporter(runner);
|
||||
};
|
||||
|
||||
SauceReporter.prototype = OriginalReporter.prototype;
|
||||
|
||||
mocha.reporter(SauceReporter);
|
||||
|
||||
// Override the database id.
|
||||
window.Whisper = window.Whisper || {};
|
||||
window.Whisper.Database = window.Whisper.Database || {};
|
||||
Whisper.Database.id = 'test';
|
||||
|
||||
/*
|
||||
* global helpers for tests
|
||||
*/
|
||||
window.assertEqualArrayBuffers = (ab1, ab2) => {
|
||||
assert.deepEqual(new Uint8Array(ab1), new Uint8Array(ab2));
|
||||
};
|
||||
|
||||
window.hexToArrayBuffer = str => {
|
||||
const ret = new ArrayBuffer(str.length / 2);
|
||||
const array = new Uint8Array(ret);
|
||||
for (let i = 0; i < str.length / 2; i += 1) {
|
||||
array[i] = parseInt(str.substr(i * 2, 2), 16);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
function deleteIndexedDB() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const idbReq = indexedDB.deleteDatabase('test');
|
||||
idbReq.onsuccess = resolve;
|
||||
idbReq.error = reject;
|
||||
});
|
||||
}
|
||||
|
||||
/* Delete the database before running any tests */
|
||||
before(async () => {
|
||||
await deleteIndexedDB();
|
||||
await window.Signal.Data.removeAll();
|
||||
await window.storage.fetch();
|
||||
});
|
||||
|
||||
window.clearDatabase = async () => {
|
||||
await window.Signal.Data.removeAll();
|
||||
await window.storage.fetch();
|
||||
};
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
window.Whisper.events = _.clone(Backbone.Events);
|
|
@ -1,12 +0,0 @@
|
|||
// For reference: https://github.com/airbnb/javascript
|
||||
|
||||
module.exports = {
|
||||
env: {
|
||||
mocha: true,
|
||||
browser: false,
|
||||
},
|
||||
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
},
|
||||
};
|
|
@ -1,170 +0,0 @@
|
|||
[
|
||||
{
|
||||
"label": "Session",
|
||||
"submenu": [
|
||||
{
|
||||
"label": "About",
|
||||
"click": null
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Hide",
|
||||
"role": "hide"
|
||||
},
|
||||
{
|
||||
"label": "Hide Others",
|
||||
"role": "hideothers"
|
||||
},
|
||||
{
|
||||
"label": "Show All",
|
||||
"role": "unhide"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Quit Session",
|
||||
"role": "quit"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "&File"
|
||||
},
|
||||
{
|
||||
"label": "&Edit",
|
||||
"submenu": [
|
||||
{
|
||||
"label": "Undo",
|
||||
"role": "undo"
|
||||
},
|
||||
{
|
||||
"label": "Redo",
|
||||
"role": "redo"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Cut",
|
||||
"role": "cut"
|
||||
},
|
||||
{
|
||||
"label": "Copy",
|
||||
"role": "copy"
|
||||
},
|
||||
{
|
||||
"label": "Paste",
|
||||
"role": "paste"
|
||||
},
|
||||
{
|
||||
"label": "Paste and Match Style",
|
||||
"role": "pasteandmatchstyle"
|
||||
},
|
||||
{
|
||||
"label": "Delete",
|
||||
"role": "delete"
|
||||
},
|
||||
{
|
||||
"label": "Select all",
|
||||
"role": "selectall"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "&View",
|
||||
"submenu": [
|
||||
{
|
||||
"label": "Actual Size",
|
||||
"role": "resetzoom"
|
||||
},
|
||||
{
|
||||
"accelerator": "Command+=",
|
||||
"label": "Zoom In",
|
||||
"role": "zoomin"
|
||||
},
|
||||
{
|
||||
"label": "Zoom Out",
|
||||
"role": "zoomout"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Toggle Full Screen",
|
||||
"role": "togglefullscreen"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Debug Log",
|
||||
"click": null
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Toggle Developer Tools",
|
||||
"role": "toggledevtools"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "&Window",
|
||||
"role": "window",
|
||||
"submenu": [
|
||||
{
|
||||
"label": "Close Window",
|
||||
"accelerator": "CmdOrCtrl+W",
|
||||
"role": "close"
|
||||
},
|
||||
{
|
||||
"label": "Minimize",
|
||||
"accelerator": "CmdOrCtrl+M",
|
||||
"role": "minimize"
|
||||
},
|
||||
{
|
||||
"label": "Zoom",
|
||||
"role": "zoom"
|
||||
},
|
||||
{
|
||||
"label": "Show",
|
||||
"click": null
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Bring All to Front",
|
||||
"role": "front"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "&Help",
|
||||
"role": "help",
|
||||
"submenu": [
|
||||
{
|
||||
"label": "Go to Release Notes",
|
||||
"click": null
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Go to Support Page",
|
||||
"click": null
|
||||
},
|
||||
{
|
||||
"label": "Report an Issue",
|
||||
"click": null
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -1,167 +0,0 @@
|
|||
[
|
||||
{
|
||||
"label": "Session",
|
||||
"submenu": [
|
||||
{
|
||||
"label": "About",
|
||||
"click": null
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Hide",
|
||||
"role": "hide"
|
||||
},
|
||||
{
|
||||
"label": "Hide Others",
|
||||
"role": "hideothers"
|
||||
},
|
||||
{
|
||||
"label": "Show All",
|
||||
"role": "unhide"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Quit Session",
|
||||
"role": "quit"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "&Edit",
|
||||
"submenu": [
|
||||
{
|
||||
"label": "Undo",
|
||||
"role": "undo"
|
||||
},
|
||||
{
|
||||
"label": "Redo",
|
||||
"role": "redo"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Cut",
|
||||
"role": "cut"
|
||||
},
|
||||
{
|
||||
"label": "Copy",
|
||||
"role": "copy"
|
||||
},
|
||||
{
|
||||
"label": "Paste",
|
||||
"role": "paste"
|
||||
},
|
||||
{
|
||||
"label": "Paste and Match Style",
|
||||
"role": "pasteandmatchstyle"
|
||||
},
|
||||
{
|
||||
"label": "Delete",
|
||||
"role": "delete"
|
||||
},
|
||||
{
|
||||
"label": "Select all",
|
||||
"role": "selectall"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "&View",
|
||||
"submenu": [
|
||||
{
|
||||
"label": "Actual Size",
|
||||
"role": "resetzoom"
|
||||
},
|
||||
{
|
||||
"accelerator": "Command+=",
|
||||
"label": "Zoom In",
|
||||
"role": "zoomin"
|
||||
},
|
||||
{
|
||||
"label": "Zoom Out",
|
||||
"role": "zoomout"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Toggle Full Screen",
|
||||
"role": "togglefullscreen"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Debug Log",
|
||||
"click": null
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Toggle Developer Tools",
|
||||
"role": "toggledevtools"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "&Window",
|
||||
"role": "window",
|
||||
"submenu": [
|
||||
{
|
||||
"label": "Close Window",
|
||||
"accelerator": "CmdOrCtrl+W",
|
||||
"role": "close"
|
||||
},
|
||||
{
|
||||
"label": "Minimize",
|
||||
"accelerator": "CmdOrCtrl+M",
|
||||
"role": "minimize"
|
||||
},
|
||||
{
|
||||
"label": "Zoom",
|
||||
"role": "zoom"
|
||||
},
|
||||
{
|
||||
"label": "Show",
|
||||
"click": null
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Bring All to Front",
|
||||
"role": "front"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "&Help",
|
||||
"role": "help",
|
||||
"submenu": [
|
||||
{
|
||||
"label": "Go to Release Notes",
|
||||
"click": null
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Go to Support Page",
|
||||
"click": null
|
||||
},
|
||||
{
|
||||
"label": "Report an Issue",
|
||||
"click": null
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -1,134 +0,0 @@
|
|||
[
|
||||
{
|
||||
"label": "&File",
|
||||
"submenu": [
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Quit Session",
|
||||
"role": "quit"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "&Edit",
|
||||
"submenu": [
|
||||
{
|
||||
"label": "Undo",
|
||||
"role": "undo"
|
||||
},
|
||||
{
|
||||
"label": "Redo",
|
||||
"role": "redo"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Cut",
|
||||
"role": "cut"
|
||||
},
|
||||
{
|
||||
"label": "Copy",
|
||||
"role": "copy"
|
||||
},
|
||||
{
|
||||
"label": "Paste",
|
||||
"role": "paste"
|
||||
},
|
||||
{
|
||||
"label": "Paste and Match Style",
|
||||
"role": "pasteandmatchstyle"
|
||||
},
|
||||
{
|
||||
"label": "Delete",
|
||||
"role": "delete"
|
||||
},
|
||||
{
|
||||
"label": "Select all",
|
||||
"role": "selectall"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "&View",
|
||||
"submenu": [
|
||||
{
|
||||
"label": "Actual Size",
|
||||
"role": "resetzoom"
|
||||
},
|
||||
{
|
||||
"accelerator": "Control+Plus",
|
||||
"label": "Zoom In",
|
||||
"role": "zoomin"
|
||||
},
|
||||
{
|
||||
"label": "Zoom Out",
|
||||
"role": "zoomout"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Toggle Full Screen",
|
||||
"role": "togglefullscreen"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Debug Log",
|
||||
"click": null
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Toggle Developer Tools",
|
||||
"role": "toggledevtools"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "&Window",
|
||||
"role": "window",
|
||||
"submenu": [
|
||||
{
|
||||
"label": "Minimize",
|
||||
"role": "minimize"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "&Help",
|
||||
"role": "help",
|
||||
"submenu": [
|
||||
{
|
||||
"label": "Go to Release Notes",
|
||||
"click": null
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Go to Support Page",
|
||||
"click": null
|
||||
},
|
||||
{
|
||||
"label": "Report an Issue",
|
||||
"click": null
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "About",
|
||||
"click": null
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -1,131 +0,0 @@
|
|||
[
|
||||
{
|
||||
"label": "&File",
|
||||
"submenu": [
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Quit Session",
|
||||
"role": "quit"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "&Edit",
|
||||
"submenu": [
|
||||
{
|
||||
"label": "Undo",
|
||||
"role": "undo"
|
||||
},
|
||||
{
|
||||
"label": "Redo",
|
||||
"role": "redo"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Cut",
|
||||
"role": "cut"
|
||||
},
|
||||
{
|
||||
"label": "Copy",
|
||||
"role": "copy"
|
||||
},
|
||||
{
|
||||
"label": "Paste",
|
||||
"role": "paste"
|
||||
},
|
||||
{
|
||||
"label": "Paste and Match Style",
|
||||
"role": "pasteandmatchstyle"
|
||||
},
|
||||
{
|
||||
"label": "Delete",
|
||||
"role": "delete"
|
||||
},
|
||||
{
|
||||
"label": "Select all",
|
||||
"role": "selectall"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "&View",
|
||||
"submenu": [
|
||||
{
|
||||
"label": "Actual Size",
|
||||
"role": "resetzoom"
|
||||
},
|
||||
{
|
||||
"accelerator": "Control+Plus",
|
||||
"label": "Zoom In",
|
||||
"role": "zoomin"
|
||||
},
|
||||
{
|
||||
"label": "Zoom Out",
|
||||
"role": "zoomout"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Toggle Full Screen",
|
||||
"role": "togglefullscreen"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Debug Log",
|
||||
"click": null
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Toggle Developer Tools",
|
||||
"role": "toggledevtools"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "&Window",
|
||||
"role": "window",
|
||||
"submenu": [
|
||||
{
|
||||
"label": "Minimize",
|
||||
"role": "minimize"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "&Help",
|
||||
"role": "help",
|
||||
"submenu": [
|
||||
{
|
||||
"label": "Go to Release Notes",
|
||||
"click": null
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "Go to Support Page",
|
||||
"click": null
|
||||
},
|
||||
{
|
||||
"label": "Report an Issue",
|
||||
"click": null
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"label": "About",
|
||||
"click": null
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -1,287 +0,0 @@
|
|||
// NOTE: Temporarily allow `then` until we convert the entire file to `async` / `await`:
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const tmp = require('tmp');
|
||||
const { expect } = require('chai');
|
||||
|
||||
const {
|
||||
eliminateOutOfDateFiles,
|
||||
eliminateOldEntries,
|
||||
isLineAfterDate,
|
||||
fetchLog,
|
||||
fetch,
|
||||
} = require('../../app/logging');
|
||||
|
||||
describe('app/logging', () => {
|
||||
let basePath;
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = tmp.dirSync({
|
||||
unsafeCleanup: true,
|
||||
});
|
||||
basePath = tmpDir.name;
|
||||
});
|
||||
|
||||
afterEach(done => {
|
||||
// we need the unsafe option to recursively remove the directory
|
||||
try {
|
||||
tmpDir.removeCallback(done);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('removeCallback failed with ', e);
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
describe('#isLineAfterDate', () => {
|
||||
it('returns false if falsy', () => {
|
||||
const actual = isLineAfterDate('', new Date());
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
it('returns false if invalid JSON', () => {
|
||||
const actual = isLineAfterDate('{{}', new Date());
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
it('returns false if date is invalid', () => {
|
||||
const line = JSON.stringify({ time: '2018-01-04T19:17:05.014Z' });
|
||||
const actual = isLineAfterDate(line, new Date('try6'));
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
it('returns false if log time is invalid', () => {
|
||||
const line = JSON.stringify({ time: 'try7' });
|
||||
const date = new Date('2018-01-04T19:17:00.000Z');
|
||||
const actual = isLineAfterDate(line, date);
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
it('returns false if date before provided date', () => {
|
||||
const line = JSON.stringify({ time: '2018-01-04T19:17:00.000Z' });
|
||||
const date = new Date('2018-01-04T19:17:05.014Z');
|
||||
const actual = isLineAfterDate(line, date);
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
it('returns true if date is after provided date', () => {
|
||||
const line = JSON.stringify({ time: '2018-01-04T19:17:05.014Z' });
|
||||
const date = new Date('2018-01-04T19:17:00.000Z');
|
||||
const actual = isLineAfterDate(line, date);
|
||||
expect(actual).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#eliminateOutOfDateFiles', () => {
|
||||
it('deletes an empty file', () => {
|
||||
const date = new Date();
|
||||
const log = '\n';
|
||||
const target = path.join(basePath, 'log.log');
|
||||
fs.writeFileSync(target, log);
|
||||
|
||||
return eliminateOutOfDateFiles(basePath, date).then(() => {
|
||||
expect(fs.existsSync(target)).to.equal(false);
|
||||
});
|
||||
});
|
||||
it('deletes a file with invalid JSON lines', () => {
|
||||
const date = new Date();
|
||||
const log = '{{}\n';
|
||||
const target = path.join(basePath, 'log.log');
|
||||
fs.writeFileSync(target, log);
|
||||
|
||||
return eliminateOutOfDateFiles(basePath, date).then(() => {
|
||||
expect(fs.existsSync(target)).to.equal(false);
|
||||
});
|
||||
});
|
||||
it('deletes a file with all dates before provided date', () => {
|
||||
const date = new Date('2018-01-04T19:17:05.014Z');
|
||||
const contents = [
|
||||
JSON.stringify({ time: '2018-01-04T19:17:00.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
|
||||
].join('\n');
|
||||
const target = path.join(basePath, 'log.log');
|
||||
fs.writeFileSync(target, contents);
|
||||
|
||||
return eliminateOutOfDateFiles(basePath, date).then(() => {
|
||||
expect(fs.existsSync(target)).to.equal(false);
|
||||
});
|
||||
});
|
||||
it('keeps a file with first line date before provided date', () => {
|
||||
const date = new Date('2018-01-04T19:16:00.000Z');
|
||||
const contents = [
|
||||
JSON.stringify({ time: '2018-01-04T19:17:00.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
|
||||
].join('\n');
|
||||
const target = path.join(basePath, 'log.log');
|
||||
fs.writeFileSync(target, contents);
|
||||
|
||||
return eliminateOutOfDateFiles(basePath, date).then(() => {
|
||||
expect(fs.existsSync(target)).to.equal(true);
|
||||
});
|
||||
});
|
||||
it('keeps a file with last line date before provided date', () => {
|
||||
const date = new Date('2018-01-04T19:17:01.000Z');
|
||||
const contents = [
|
||||
JSON.stringify({ time: '2018-01-04T19:17:00.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
|
||||
].join('\n');
|
||||
const target = path.join(basePath, 'log.log');
|
||||
fs.writeFileSync(target, contents);
|
||||
|
||||
return eliminateOutOfDateFiles(basePath, date).then(() => {
|
||||
expect(fs.existsSync(target)).to.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#eliminateOldEntries', () => {
|
||||
it('eliminates all non-parsing entries', () => {
|
||||
const date = new Date('2018-01-04T19:17:01.000Z');
|
||||
const contents = [
|
||||
'random line',
|
||||
JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
|
||||
].join('\n');
|
||||
const expected = [
|
||||
JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
|
||||
].join('\n');
|
||||
|
||||
const target = path.join(basePath, 'log.log');
|
||||
const files = [
|
||||
{
|
||||
path: target,
|
||||
},
|
||||
];
|
||||
|
||||
fs.writeFileSync(target, contents);
|
||||
|
||||
return eliminateOldEntries(files, date).then(() => {
|
||||
expect(fs.readFileSync(target, 'utf8')).to.equal(`${expected}\n`);
|
||||
});
|
||||
});
|
||||
it('preserves all lines if before target date', () => {
|
||||
const date = new Date('2018-01-04T19:17:03.000Z');
|
||||
const contents = [
|
||||
'random line',
|
||||
JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
|
||||
].join('\n');
|
||||
const expected = [JSON.stringify({ time: '2018-01-04T19:17:03.014Z' })].join('\n');
|
||||
|
||||
const target = path.join(basePath, 'log.log');
|
||||
const files = [
|
||||
{
|
||||
path: target,
|
||||
},
|
||||
];
|
||||
|
||||
fs.writeFileSync(target, contents);
|
||||
|
||||
return eliminateOldEntries(files, date).then(() => {
|
||||
expect(fs.readFileSync(target, 'utf8')).to.equal(`${expected}\n`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#fetchLog', () => {
|
||||
it('returns error if file does not exist', () => {
|
||||
const target = 'random_file';
|
||||
return fetchLog(target).then(
|
||||
() => {
|
||||
throw new Error('Expected an error!');
|
||||
},
|
||||
error => {
|
||||
expect(error)
|
||||
.to.have.property('message')
|
||||
.that.match(/random_file/);
|
||||
}
|
||||
);
|
||||
});
|
||||
it('returns empty array if file has no valid JSON lines', () => {
|
||||
const contents = 'line 1\nline2\n';
|
||||
const expected = [];
|
||||
const target = path.join(basePath, 'test.log');
|
||||
|
||||
fs.writeFileSync(target, contents);
|
||||
|
||||
return fetchLog(target).then(result => {
|
||||
expect(result).to.deep.equal(expected);
|
||||
});
|
||||
});
|
||||
it('returns just three fields in each returned line', () => {
|
||||
const contents = [
|
||||
JSON.stringify({
|
||||
one: 1,
|
||||
two: 2,
|
||||
level: 1,
|
||||
time: 2,
|
||||
msg: 3,
|
||||
}),
|
||||
JSON.stringify({
|
||||
one: 1,
|
||||
two: 2,
|
||||
level: 2,
|
||||
time: 3,
|
||||
msg: 4,
|
||||
}),
|
||||
'',
|
||||
].join('\n');
|
||||
const expected = [
|
||||
{
|
||||
level: 1,
|
||||
time: 2,
|
||||
msg: 3,
|
||||
},
|
||||
{
|
||||
level: 2,
|
||||
time: 3,
|
||||
msg: 4,
|
||||
},
|
||||
];
|
||||
|
||||
const target = path.join(basePath, 'test.log');
|
||||
|
||||
fs.writeFileSync(target, contents);
|
||||
|
||||
return fetchLog(target).then(result => {
|
||||
expect(result).to.deep.equal(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#fetch', () => {
|
||||
it('returns single entry if no files', () => {
|
||||
return fetch(basePath).then(results => {
|
||||
expect(results).to.have.length(1);
|
||||
expect(results[0].msg).to.match(/Loaded this list/);
|
||||
});
|
||||
});
|
||||
it('returns sorted entries from all files', () => {
|
||||
const first = [JSON.stringify({ msg: 2, time: '2018-01-04T19:17:05.014Z' }), ''].join('\n');
|
||||
const second = [
|
||||
JSON.stringify({ msg: 1, time: '2018-01-04T19:17:00.014Z' }),
|
||||
JSON.stringify({ msg: 3, time: '2018-01-04T19:18:00.014Z' }),
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
fs.writeFileSync(path.join(basePath, 'first.log'), first);
|
||||
fs.writeFileSync(path.join(basePath, 'second.log'), second);
|
||||
|
||||
return fetch(basePath).then(results => {
|
||||
expect(results).to.have.length(4);
|
||||
expect(results[0].msg).to.equal(1);
|
||||
expect(results[1].msg).to.equal(2);
|
||||
expect(results[2].msg).to.equal(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,73 +0,0 @@
|
|||
const { assert } = require('chai');
|
||||
|
||||
const SignalMenu = require('../../app/menu');
|
||||
const { load: loadLocale } = require('../../app/locale');
|
||||
|
||||
const PLATFORMS = [
|
||||
{
|
||||
label: 'macOS',
|
||||
platform: 'darwin',
|
||||
fixtures: {
|
||||
default: './fixtures/menu-mac-os',
|
||||
setup: './fixtures/menu-mac-os-setup',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Windows',
|
||||
platform: 'win32',
|
||||
fixtures: {
|
||||
default: './fixtures/menu-windows-linux',
|
||||
setup: './fixtures/menu-windows-linux-setup',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Linux',
|
||||
platform: 'linux',
|
||||
fixtures: {
|
||||
default: './fixtures/menu-windows-linux',
|
||||
setup: './fixtures/menu-windows-linux-setup',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const INCLUDE_SETUP_OPTIONS = [false, true];
|
||||
|
||||
describe('SignalMenu', () => {
|
||||
describe('createTemplate', () => {
|
||||
PLATFORMS.forEach(({ label, platform, fixtures }) => {
|
||||
context(label, () => {
|
||||
INCLUDE_SETUP_OPTIONS.forEach(includeSetup => {
|
||||
const prefix = includeSetup ? 'with' : 'without';
|
||||
context(`${prefix} setup options`, () => {
|
||||
it('should return correct template', () => {
|
||||
const logger = {
|
||||
error(message) {
|
||||
throw new Error(message);
|
||||
},
|
||||
};
|
||||
const options = {
|
||||
openNewBugForm: null,
|
||||
openReleaseNotes: null,
|
||||
openSupportPage: null,
|
||||
platform,
|
||||
includeSetup,
|
||||
showAbout: null,
|
||||
showDebugLog: null,
|
||||
showSettings: null,
|
||||
showWindow: null,
|
||||
};
|
||||
const appLocale = 'en';
|
||||
const { messages } = loadLocale({ appLocale, logger });
|
||||
|
||||
const actual = SignalMenu.createTemplate(options, messages);
|
||||
const fixturePath = includeSetup ? fixtures.setup : fixtures.default;
|
||||
// eslint-disable-next-line global-require, import/no-dynamic-require
|
||||
const fixture = require(fixturePath);
|
||||
assert.deepEqual(actual, fixture);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,85 +0,0 @@
|
|||
const { expect } = require('chai');
|
||||
|
||||
const { _urlToPath } = require('../../app/protocol_filter');
|
||||
|
||||
describe('Protocol Filter', () => {
|
||||
describe('_urlToPath', () => {
|
||||
it('returns proper file path for unix style file URI with hash', () => {
|
||||
const path = 'file:///Users/someone/Development/signal/electron/background.html#first-page';
|
||||
const expected = '/Users/someone/Development/signal/electron/background.html';
|
||||
|
||||
const actual = _urlToPath(path);
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
|
||||
it('returns proper file path for unix style file URI with querystring', () => {
|
||||
const path =
|
||||
'file:///Users/someone/Development/signal/electron/background.html?name=Signal&locale=en&version=2.4.0';
|
||||
const expected = '/Users/someone/Development/signal/electron/background.html';
|
||||
|
||||
const actual = _urlToPath(path);
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
|
||||
it('returns proper file path for unix style file URI with hash and querystring', () => {
|
||||
const path =
|
||||
'file:///Users/someone/Development/signal/electron/background.html#somewhere?name=Signal';
|
||||
const expected = '/Users/someone/Development/signal/electron/background.html';
|
||||
|
||||
const actual = _urlToPath(path);
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
|
||||
it('returns proper file path for file URI on windows', () => {
|
||||
const path =
|
||||
'file:///C:/Users/Someone/dev/desktop/background.html?name=Signal&locale=en&version=2.4.0';
|
||||
const expected = 'C:/Users/Someone/dev/desktop/background.html';
|
||||
const isWindows = true;
|
||||
|
||||
const actual = _urlToPath(path, { isWindows });
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
|
||||
it('translates from URL format to filesystem format', () => {
|
||||
const path = 'file:///Users/someone/Development%20Files/signal/electron/background.html';
|
||||
const expected = '/Users/someone/Development Files/signal/electron/background.html';
|
||||
|
||||
const actual = _urlToPath(path);
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
|
||||
it('translates from URL format to filesystem format', () => {
|
||||
const path = 'file:///Users/someone/Development%20Files/signal/electron/background.html';
|
||||
const expected = '/Users/someone/Development Files/signal/electron/background.html';
|
||||
|
||||
const actual = _urlToPath(path);
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
|
||||
// this seems to be the only way to get a relative path through Electron
|
||||
it('handles SMB share path', () => {
|
||||
const path = 'file://relative/path';
|
||||
const expected = 'relative/path';
|
||||
|
||||
const actual = _urlToPath(path);
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
|
||||
it('handles SMB share path on windows', () => {
|
||||
const path = 'file://relative/path';
|
||||
const expected = 'elative/path';
|
||||
const isWindows = true;
|
||||
|
||||
const actual = _urlToPath(path, { isWindows });
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
|
||||
it('hands back a path with .. in it', () => {
|
||||
const path = 'file://../../..';
|
||||
const expected = '../../..';
|
||||
|
||||
const actual = _urlToPath(path);
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
});
|
||||
});
|
File diff suppressed because one or more lines are too long
|
@ -1,112 +0,0 @@
|
|||
/* global Signal, textsecure, libsignal */
|
||||
|
||||
'use strict';
|
||||
|
||||
describe('Crypto', () => {
|
||||
describe('symmetric encryption', () => {
|
||||
it('roundtrips', async () => {
|
||||
const message = 'this is my message';
|
||||
const plaintext = dcodeIO.ByteBuffer.wrap(message, 'binary').toArrayBuffer();
|
||||
const key = textsecure.crypto.getRandomBytes(32);
|
||||
|
||||
const encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext);
|
||||
const decrypted = await Signal.Crypto.decryptSymmetric(key, encrypted);
|
||||
|
||||
const equal = Signal.Crypto.constantTimeEqual(plaintext, decrypted);
|
||||
if (!equal) {
|
||||
throw new Error('The output and input did not match!');
|
||||
}
|
||||
});
|
||||
|
||||
it('roundtrip fails if nonce is modified', async () => {
|
||||
const message = 'this is my message';
|
||||
const plaintext = dcodeIO.ByteBuffer.wrap(message, 'binary').toArrayBuffer();
|
||||
const key = textsecure.crypto.getRandomBytes(32);
|
||||
|
||||
const encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext);
|
||||
const uintArray = new Uint8Array(encrypted);
|
||||
uintArray[2] += 2;
|
||||
|
||||
try {
|
||||
await Signal.Crypto.decryptSymmetric(key, uintArray.buffer);
|
||||
} catch (error) {
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
'decryptSymmetric: Failed to decrypt; MAC verification failed'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('Expected error to be thrown');
|
||||
});
|
||||
|
||||
it('roundtrip fails if mac is modified', async () => {
|
||||
const message = 'this is my message';
|
||||
const plaintext = dcodeIO.ByteBuffer.wrap(message, 'binary').toArrayBuffer();
|
||||
const key = textsecure.crypto.getRandomBytes(32);
|
||||
|
||||
const encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext);
|
||||
const uintArray = new Uint8Array(encrypted);
|
||||
uintArray[uintArray.length - 3] += 2;
|
||||
|
||||
try {
|
||||
await Signal.Crypto.decryptSymmetric(key, uintArray.buffer);
|
||||
} catch (error) {
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
'decryptSymmetric: Failed to decrypt; MAC verification failed'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('Expected error to be thrown');
|
||||
});
|
||||
|
||||
it('roundtrip fails if encrypted contents are modified', async () => {
|
||||
const message = 'this is my message';
|
||||
const plaintext = dcodeIO.ByteBuffer.wrap(message, 'binary').toArrayBuffer();
|
||||
const key = textsecure.crypto.getRandomBytes(32);
|
||||
|
||||
const encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext);
|
||||
const uintArray = new Uint8Array(encrypted);
|
||||
uintArray[35] += 9;
|
||||
|
||||
try {
|
||||
await Signal.Crypto.decryptSymmetric(key, uintArray.buffer);
|
||||
} catch (error) {
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
'decryptSymmetric: Failed to decrypt; MAC verification failed'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('Expected error to be thrown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('attachment encryption', () => {
|
||||
it('roundtrips', async () => {
|
||||
const staticKeyPair = await libsignal.KeyHelper.generateIdentityKeyPair();
|
||||
const message = 'this is my message';
|
||||
const plaintext = Signal.Crypto.bytesFromString(message);
|
||||
const path = 'fa/facdf99c22945b1c9393345599a276f4b36ad7ccdc8c2467f5441b742c2d11fa';
|
||||
|
||||
const encrypted = await Signal.Crypto.encryptAttachment(
|
||||
staticKeyPair.pubKey.slice(1),
|
||||
path,
|
||||
plaintext
|
||||
);
|
||||
const decrypted = await Signal.Crypto.decryptAttachment(
|
||||
staticKeyPair.privKey,
|
||||
path,
|
||||
encrypted
|
||||
);
|
||||
|
||||
const equal = Signal.Crypto.constantTimeEqual(plaintext, decrypted);
|
||||
if (!equal) {
|
||||
throw new Error('The output and input did not match!');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,36 +0,0 @@
|
|||
/* global Whisper */
|
||||
|
||||
'use strict';
|
||||
|
||||
describe('Database', () => {
|
||||
describe('handleDOMException', () => {
|
||||
it('handles null, still calls reject', () => {
|
||||
let called = 0;
|
||||
const reject = () => {
|
||||
called += 1;
|
||||
};
|
||||
const error = null;
|
||||
const prefix = 'something';
|
||||
|
||||
Whisper.Database.handleDOMException(prefix, error, reject);
|
||||
|
||||
assert.strictEqual(called, 1);
|
||||
});
|
||||
|
||||
it('handles object code and message', () => {
|
||||
let called = 0;
|
||||
const reject = () => {
|
||||
called += 1;
|
||||
};
|
||||
const error = {
|
||||
code: 4,
|
||||
message: 'some cryptic error',
|
||||
};
|
||||
const prefix = 'something';
|
||||
|
||||
Whisper.Database.handleDOMException(prefix, error, reject);
|
||||
|
||||
assert.strictEqual(called, 1);
|
||||
});
|
||||
});
|
||||
});
|
2719
test/fixtures.js
2719
test/fixtures.js
File diff suppressed because it is too large
Load diff
|
@ -1,34 +0,0 @@
|
|||
/* global $, textsecure, Whisper */
|
||||
|
||||
'use strict';
|
||||
|
||||
describe('Fixtures', () => {
|
||||
before(async () => {
|
||||
// NetworkStatusView checks this method every five seconds while showing
|
||||
|
||||
await clearDatabase();
|
||||
await textsecure.storage.user.setNumberAndDeviceId(
|
||||
'05123456789abcdef05123456789abcdef05123456789abcdef05123456789abcd',
|
||||
2,
|
||||
'testDevice'
|
||||
);
|
||||
|
||||
await window
|
||||
.getConversationController()
|
||||
.getOrCreateAndWait(window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache(), 'private');
|
||||
});
|
||||
|
||||
it('renders', async () => {
|
||||
await Whisper.Fixtures().saveAll();
|
||||
|
||||
window.getConversationController().reset();
|
||||
await window.getConversationController().load();
|
||||
|
||||
let view = new Whisper.InboxView({ window });
|
||||
view.$el.prependTo($('#render-light-theme'));
|
||||
|
||||
view = new Whisper.InboxView({ window });
|
||||
view.$el.removeClass('light-theme').addClass('dark-theme');
|
||||
view.$el.prependTo($('#render-dark-theme'));
|
||||
});
|
||||
});
|
|
@ -1,27 +0,0 @@
|
|||
/* global i18n */
|
||||
|
||||
describe('i18n', () => {
|
||||
describe('i18n', () => {
|
||||
it('returns empty string for unknown string', () => {
|
||||
assert.strictEqual(i18n('random'), '');
|
||||
});
|
||||
it('returns message for given string', () => {
|
||||
assert.equal(i18n('reportIssue'), 'Report an issue');
|
||||
});
|
||||
it('returns message with single substitution', () => {
|
||||
const actual = i18n('attemptingReconnection', 5);
|
||||
assert.equal(actual, 'Attempting reconnect in 5 seconds');
|
||||
});
|
||||
it('returns message with multiple substitutions', () => {
|
||||
const actual = i18n('theyChangedTheTimer', ['Someone', '5 minutes']);
|
||||
assert.equal(actual, 'Someone set the disappearing message timer to 5 minutes');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLocale', () => {
|
||||
it('returns a string with length two or greater', () => {
|
||||
const locale = i18n.getLocale();
|
||||
assert.isAtLeast(locale.trim().length, 2);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,63 +0,0 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>TextSecure test runner</title>
|
||||
<link rel="stylesheet" href="../node_modules/mocha/mocha.css" />
|
||||
<link rel="stylesheet" href="../stylesheets/manifest.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="mocha">
|
||||
</div>
|
||||
<div id="tests">
|
||||
</div>
|
||||
<div id="render-light-theme" class="index" style="width: 800; height: 500; margin:10px; border: solid 1px black;">
|
||||
</div>
|
||||
<div id="render-dark-theme" class="index" style="width: 800; height: 500; margin:10px; border: solid 1px black;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script type="text/javascript" src="../js/components.js"></script>
|
||||
<script type="text/javascript" src="test.js"></script>
|
||||
|
||||
<script type="text/javascript" src="../js/registration.js" data-cover></script>
|
||||
<script type="text/javascript" src="../js/chromium.js" data-cover></script>
|
||||
<script type="text/javascript" src="../js/database.js" data-cover></script>
|
||||
<script type="text/javascript" src="../js/storage.js" data-cover></script>
|
||||
<script type="text/javascript" src="../js/libtextsecure.js" data-cover></script>
|
||||
|
||||
<script type="text/javascript" src="../js/expiring_messages.js" data-cover></script>
|
||||
<script type="text/javascript" src="../js/notifications.js" data-cover></script>
|
||||
<script type="text/javascript" src="../js/focus_listener.js"></script>
|
||||
|
||||
<script type="text/javascript" src="../js/chromium.js" data-cover></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/session_inbox_view.js'></script>
|
||||
<script type="text/javascript" src="../js/views/identicon_svg_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>
|
||||
|
||||
<!-- DIALOGS-->
|
||||
<script type="text/javascript" src="views/whisper_view_test.js"></script>
|
||||
|
||||
<script type="text/javascript" src="models/conversations_test.js"></script>
|
||||
<script type="text/javascript" src="models/messages_test.js"></script>
|
||||
|
||||
<script type="text/javascript" src="database_test.js"></script>
|
||||
<script type="text/javascript" src="i18n_test.js"></script>
|
||||
|
||||
<script type="text/javascript" src="fixtures.js"></script>
|
||||
<script type="text/javascript" src="fixtures_test.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
mocha.run();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,51 +0,0 @@
|
|||
Manual test script
|
||||
|
||||
Some things are very difficult to test programmatically. Also, if you don't have adequate test coverage, a good first step is a comprehensive manual test script! https://blog.scottnonnenberg.com/web-application-test-strategy/
|
||||
|
||||
Conversation view:
|
||||
Last seen indicator:
|
||||
(dismissed three ways: 1. sending a message 2. switching away from conversation and back again 3. clicking scroll down button when last seen indicator is off-screen above)
|
||||
|
||||
- Switch away from Signal app, but keep it visible
|
||||
- Receive messages to conversation out of focus, and the last seen indicator should move up the screen with each new message. When the number of new messages can no longer fit on the screen, the last seen indicator should stay at the top of the screen, and new messages will appear below. The scroll down button will turn blue to indicate new messages out of view.
|
||||
- Switch back to Signal app, and the last seen indicator and scroll down button should stay where they are.
|
||||
- Click the scroll down button to go to the bottom of the window, and the button should disappear.
|
||||
- Send a message, then scroll up. The last seen indicator should be gone.
|
||||
|
||||
- Switch to a different conversation, then receive messages on original conversation
|
||||
- Switch back to original conversation, and the last seen indicator should be visible
|
||||
- Switch away from conversation and back. The last seen indicator should be gone.
|
||||
|
||||
- Switch to a different conversation, then receive a lot of messages on original conversation
|
||||
- Switch back to original conversation, and the last seen indicator should be visible, along with the scroll down button.
|
||||
- Click the scroll down button to be taken to the newest message in the conversation
|
||||
|
||||
- Scroll up on a conversation then switch to another application, keeping the Signal application visible. Receive new messages on that conversation. Switch back to application. The scroll down button should be blue, and the conversation scroll location should stay where it was. There should be a last seen indicator visible above the new messages.
|
||||
|
||||
- Scroll to bottom of a conversation, then switch to another application, keeping Signal application visible. Receive new messages on that conversation. As new messages come in, the last seen indicator should march up the screen. Before it reaches the top, switch back to the application. This will mark those messages as read. Switch away from the application again, and receive new messages. The last seen indicator will scroll off the top of the screen as more and more new messages come in.
|
||||
|
||||
- ADVANCED: Set up an automated script (or friend) to send you repeated messages. You should see the right number of unread upon entry of the conversation, along with with the last seen indicator. While the conversation is focused, new messages should increment the last seen indicator until it is offscreen above. Click the scroll down button to eliminate the last seen indicator, then scroll up. New messages received while scrolled up should not scroll the conversation, but will add a new last seen indicator and scroll down button.
|
||||
|
||||
- ADVANCED: Set fetch limit to a low number, like 3 (in models/messages.js, fetchConversation function). Load the application, and don't select the conversation. Receive more than four new messages in that conversation. Select the conversation. The last seen indicator should reflect the total number of new messages and all of them should be visible.
|
||||
|
||||
Marking messages as unread:
|
||||
- Switch to a different conversation, then receive lots of messages on original conversation, more than would fill the screen
|
||||
- Note the count before clicking into the conversation. Count the number of visible messages and ensure that the conversation's unread count is decremented by the right amount.
|
||||
- Slowly scroll down so that one more message is visible. The conversation unread count should go down by one.
|
||||
- Click the scroll down button. All messages should be marked read - even if you skipped a couple screens to get to the bottom.
|
||||
|
||||
Scrolling:
|
||||
- If scrolled to bottom of a conversation, should stay there when a new message comes in
|
||||
- If scrolled to the middle of a conversation, should stay there when a new message comes in
|
||||
- When you've scrolled up an entire screen's worth, a scroll down button in the bottom right should appear.
|
||||
|
||||
Scroll-down button:
|
||||
- Clicking it takes you to the bottom of the conversation, makes the button disappear
|
||||
- If a new message comes in while it is already showing, it turns blue
|
||||
- If a new message comes in while not at the bottom of the conversation (but button is not already showing), it should appear, already blue.
|
||||
- If you've scrolled up higher than the last seen indicator, then clicking the scroll down button should take you to the last seen indicator. Once there, clicking the button will take you to the bottom of the conversation, at which point the button will disappear.
|
||||
|
||||
Electron window locations
|
||||
- Load app, move and resize window, close app. Start app. Window should be in the same place, with the same size.
|
||||
- (OSX) Load app, full-screen window, close app. Start app. Window should be full screen.
|
||||
- (Windows) Load app, maximize window, close app. Start app. Window should be maximized.
|
|
@ -1,129 +0,0 @@
|
|||
// /* global textsecure, Whisper */
|
||||
|
||||
// 'use strict';
|
||||
// FIXME audric enable back those test
|
||||
describe('ConversationCollection', () => {
|
||||
// textsecure.messaging = true;
|
||||
// before(clearDatabase);
|
||||
// after(clearDatabase);
|
||||
// it('should be ordered newest to oldest', () => {
|
||||
// const conversations = new window.models.Conversation.ConversationCollection();
|
||||
// // Timestamps
|
||||
// const today = new Date();
|
||||
// const tomorrow = new Date();
|
||||
// tomorrow.setDate(today.getDate() + 1);
|
||||
// // Add convos
|
||||
// conversations.add({ active_at: today });
|
||||
// conversations.add({ active_at: tomorrow });
|
||||
// const { models } = conversations;
|
||||
// const firstTimestamp = models[0].get('active_at').getTime();
|
||||
// const secondTimestamp = models[1].get('active_at').getTime();
|
||||
// // Compare timestamps
|
||||
// assert(firstTimestamp > secondTimestamp);
|
||||
// });
|
||||
// });
|
||||
// describe('Conversation', () => {
|
||||
// const attributes = {
|
||||
// type: 'private',
|
||||
// id: '051d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab',
|
||||
// };
|
||||
// before(async () => {
|
||||
// const convo = new window.models.Conversation.ConversationCollection().add(attributes);
|
||||
// await window.Signal.Data.saveConversation(convo.attributes, {
|
||||
// Conversation: window.models.Conversation.ConversationModel,
|
||||
// });
|
||||
// // const message = convo.messageCollection.add({
|
||||
// // body: 'hello world',
|
||||
// // conversationId: convo.id,
|
||||
// // type: 'outgoing',
|
||||
// // sent_at: Date.now(),
|
||||
// // received_at: Date.now(),
|
||||
// // });
|
||||
// // await message.commit(false);
|
||||
// });
|
||||
// after(clearDatabase);
|
||||
// it('contains its own messages', async () => {
|
||||
// const convo = new window.models.Conversation.ConversationCollection().add({
|
||||
// id: '051d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab',
|
||||
// });
|
||||
// await convo.fetchMessages();
|
||||
// assert.notEqual(convo.messageCollection.length, 0);
|
||||
// });
|
||||
// it('contains only its own messages', async () => {
|
||||
// const convo = new window.models.Conversation.ConversationCollection().add({
|
||||
// id: '052d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab',
|
||||
// });
|
||||
// await convo.fetchMessages();
|
||||
// assert.strictEqual(convo.messageCollection.length, 0);
|
||||
// });
|
||||
// it('adds conversation to message collection upon leaving group', async () => {
|
||||
// const convo = new window.models.Conversation.ConversationCollection().add({
|
||||
// type: 'group',
|
||||
// id: '052d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab',
|
||||
// });
|
||||
// await convo.leaveClosedGroup();
|
||||
// assert.notEqual(convo.messageCollection.length, 0);
|
||||
// });
|
||||
// it('has a title', () => {
|
||||
// const convos = new window.models.Conversation.ConversationCollection();
|
||||
// let convo = convos.add(attributes);
|
||||
// assert.equal(
|
||||
// convo.getTitle(),
|
||||
// '051d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab'
|
||||
// );
|
||||
// convo = convos.add({ type: '' });
|
||||
// assert.equal(convo.getTitle(), 'Unknown group');
|
||||
// convo = convos.add({ name: 'name' });
|
||||
// assert.equal(convo.getTitle(), 'name');
|
||||
// });
|
||||
// it('returns the number', () => {
|
||||
// const convos = new window.models.Conversation.ConversationCollection();
|
||||
// let convo = convos.add(attributes);
|
||||
// assert.equal(
|
||||
// convo.getNumber(),
|
||||
// '051d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab'
|
||||
// );
|
||||
// convo = convos.add({ type: '' });
|
||||
// assert.equal(convo.getNumber(), '');
|
||||
// });
|
||||
// describe('when set to private', () => {
|
||||
// it('correctly validates hex numbers', () => {
|
||||
// const regularId = new window.models.Conversation.ConversationModel({
|
||||
// type: 'private',
|
||||
// id:
|
||||
// '051d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab',
|
||||
// });
|
||||
// const invalidId = new window.models.Conversation.ConversationModel({
|
||||
// type: 'private',
|
||||
// id:
|
||||
// 'j71d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab',
|
||||
// });
|
||||
// assert.ok(regularId.isValid());
|
||||
// assert.notOk(invalidId.isValid());
|
||||
// });
|
||||
// it('correctly validates length', () => {
|
||||
// const regularId33 = new window.models.Conversation.ConversationModel({
|
||||
// type: 'private',
|
||||
// id:
|
||||
// '051d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab',
|
||||
// });
|
||||
// const regularId32 = new window.models.Conversation.ConversationModel({
|
||||
// type: 'private',
|
||||
// id: '1d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab',
|
||||
// });
|
||||
// const shortId = new window.models.Conversation.ConversationModel({
|
||||
// type: 'private',
|
||||
// id: '771d11d',
|
||||
// });
|
||||
// const longId = new window.models.Conversation.ConversationModel({
|
||||
// type: 'private',
|
||||
// id:
|
||||
// '771d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94abaa',
|
||||
// });
|
||||
// assert.ok(regularId33.isValid());
|
||||
// assert.ok(regularId32.isValid());
|
||||
// assert.notOk(shortId.isValid());
|
||||
// assert.notOk(longId.isValid());
|
||||
// });
|
||||
// });
|
||||
});
|
|
@ -1,154 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const attributes = {
|
||||
type: 'outgoing',
|
||||
body: 'hi',
|
||||
conversationId: 'foo',
|
||||
attachments: [],
|
||||
received_at: new Date().getTime(),
|
||||
};
|
||||
|
||||
const source = '+14155555555';
|
||||
|
||||
describe('MessageCollection', () => {
|
||||
before(async () => {
|
||||
await clearDatabase();
|
||||
window.getConversationController().reset();
|
||||
window.textsecure.storage.user.getNumber = () =>
|
||||
'051111111111111111111111111111111111111111111111111111111111111111';
|
||||
await window.getConversationController().load();
|
||||
});
|
||||
after(() => {
|
||||
return clearDatabase();
|
||||
});
|
||||
|
||||
it('gets outgoing contact', () => {
|
||||
const messages = new window.models.Message.MessageCollection();
|
||||
const message = messages.add(attributes);
|
||||
message.getContact();
|
||||
});
|
||||
|
||||
it('gets incoming contact', () => {
|
||||
const messages = new window.models.Message.MessageCollection();
|
||||
const message = messages.add({
|
||||
type: 'incoming',
|
||||
source,
|
||||
conversationId: 'conversationId',
|
||||
});
|
||||
message.getContact();
|
||||
});
|
||||
|
||||
it('should be ordered oldest to newest', () => {
|
||||
const messages = new window.models.Message.MessageCollection();
|
||||
// Timestamps
|
||||
const today = new Date();
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(today.getDate() + 1);
|
||||
|
||||
// Add threads
|
||||
messages.add({ received_at: today, conversationId: 'conversationId' });
|
||||
messages.add({ received_at: tomorrow, conversationId: 'conversationId' });
|
||||
|
||||
const { models } = messages;
|
||||
const firstTimestamp = models[0].get('received_at').getTime();
|
||||
const secondTimestamp = models[1].get('received_at').getTime();
|
||||
|
||||
// Compare timestamps
|
||||
assert(firstTimestamp < secondTimestamp);
|
||||
});
|
||||
|
||||
// it('checks if is incoming message', () => {
|
||||
// const messages = new window.models.Message.MessageCollection();
|
||||
// let message = messages.add(attributes);
|
||||
// assert.notOk(message.isIncoming());
|
||||
// message = messages.add({
|
||||
// type: 'incoming',
|
||||
// conversationId: 'conversationId',
|
||||
// });
|
||||
// assert.ok(message.isIncoming());
|
||||
// });
|
||||
|
||||
// it('checks if is outgoing message', () => {
|
||||
// const messages = new window.models.Message.MessageCollection();
|
||||
// let message = messages.add(attributes);
|
||||
// assert.ok(message.isOutgoing());
|
||||
// message = messages.add({
|
||||
// type: 'incoming',
|
||||
// conversationId: 'conversationId',
|
||||
// });
|
||||
// assert.notOk(message.isOutgoing());
|
||||
// });
|
||||
|
||||
it('checks if is group update', () => {
|
||||
const messages = new window.models.Message.MessageCollection();
|
||||
let message = messages.add(attributes);
|
||||
assert.notOk(message.isGroupUpdate());
|
||||
|
||||
message = messages.add({
|
||||
group_update: true,
|
||||
conversationId: 'conversationId',
|
||||
});
|
||||
assert.ok(message.isGroupUpdate());
|
||||
});
|
||||
|
||||
it('returns an accurate description', () => {
|
||||
const messages = new window.models.Message.MessageCollection();
|
||||
let message = messages.add(attributes);
|
||||
|
||||
assert.equal(
|
||||
message.getDescription(),
|
||||
'hi',
|
||||
'If no group updates or end session flags, return message body.'
|
||||
);
|
||||
|
||||
message = messages.add({
|
||||
group_update: { left: 'Alice' },
|
||||
conversationId: 'conversationId',
|
||||
});
|
||||
assert.equal(
|
||||
message.getDescription(),
|
||||
'Alice has left the group.',
|
||||
'Notes one person leaving the group.'
|
||||
);
|
||||
|
||||
message = messages.add({
|
||||
group_update: { name: 'blerg' },
|
||||
conversationId: 'conversationId',
|
||||
});
|
||||
assert.equal(
|
||||
message.getDescription(),
|
||||
"Group name is now 'blerg'.",
|
||||
'Returns a single notice if only group_updates.name changes.'
|
||||
);
|
||||
|
||||
message = messages.add({
|
||||
group_update: { joined: ['Bob'] },
|
||||
conversationId: 'conversationId',
|
||||
});
|
||||
assert.equal(
|
||||
message.getDescription(),
|
||||
'Bob joined the group.',
|
||||
'Returns a single notice if only group_updates.joined changes.'
|
||||
);
|
||||
|
||||
message = messages.add({
|
||||
group_update: { joined: ['Bob', 'Alice', 'Eve'] },
|
||||
conversationId: 'conversationId',
|
||||
});
|
||||
assert.equal(
|
||||
message.getDescription(),
|
||||
'Bob, Alice, Eve joined the group.',
|
||||
'Notes when >1 person joins the group.'
|
||||
);
|
||||
|
||||
message = messages.add({
|
||||
group_update: { joined: ['Bob'], name: 'blerg' },
|
||||
conversationId: 'conversationId',
|
||||
});
|
||||
assert.equal(
|
||||
message.getDescription(),
|
||||
"Group name is now 'blerg'. Bob joined the group.",
|
||||
'Notes when there are multiple changes to group_updates properties.'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,30 +0,0 @@
|
|||
// For reference: https://github.com/airbnb/javascript
|
||||
|
||||
module.exports = {
|
||||
env: {
|
||||
mocha: true,
|
||||
browser: true,
|
||||
},
|
||||
|
||||
globals: {
|
||||
check: true,
|
||||
gen: true,
|
||||
},
|
||||
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
},
|
||||
|
||||
rules: {
|
||||
// We still get the value of this rule, it just allows for dev deps
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
{
|
||||
devDependencies: true,
|
||||
},
|
||||
],
|
||||
|
||||
// We want to keep each test structured the same, even if its contents are tiny
|
||||
'arrow-body-style': 'off',
|
||||
},
|
||||
};
|
|
@ -1,161 +0,0 @@
|
|||
const path = require('path');
|
||||
|
||||
const { assert } = require('chai');
|
||||
|
||||
const Privacy = require('../../js/modules/privacy');
|
||||
|
||||
const APP_ROOT_PATH = path.join(__dirname, '..', '..', '..');
|
||||
|
||||
describe('Privacy', () => {
|
||||
describe('redactSessionID', () => {
|
||||
it('should redact all session IDs', () => {
|
||||
const text =
|
||||
'This is a log line with a session ID 0531032fc7415b7cc1b7516480ad121d391eddce3cfb2cee27dd5b215609c32827 and another one 05766049a70e725ad02f7fe61b10e461380a4d7433f98096b3cacbf0362d5cab62';
|
||||
|
||||
const actual = Privacy.redactSessionID(text);
|
||||
const expected = 'This is a log line with a session ID [REDACTED] and another one [REDACTED]';
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
|
||||
it('should not redact non session IDS', () => {
|
||||
const text =
|
||||
'This is a log line with a non-session ID sadsad0531032fc7415b7cc1b7516480ad121d391eddce3cfb2cee27dd5b215609c32827888 and another one 766049a70e725ad02f7fe61b10e461380a4d7433f98096b3cacbf0362d5cab6234';
|
||||
|
||||
const actual = Privacy.redactSessionID(text);
|
||||
assert.equal(actual, text);
|
||||
});
|
||||
});
|
||||
|
||||
describe('redactGroupIds', () => {
|
||||
it('should redact all group IDs', () => {
|
||||
const text = 'This is a log line with two group IDs: group(123456789) and group(abcdefghij)';
|
||||
|
||||
const actual = Privacy.redactGroupIds(text);
|
||||
const expected =
|
||||
'This is a log line with two group IDs: group([REDACTED]789) and group([REDACTED]hij)';
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
|
||||
it('should remove newlines from redacted group IDs', () => {
|
||||
const text =
|
||||
'This is a log line with two group IDs: group(12345678\n9)\nand group(abc\ndefghij)';
|
||||
|
||||
const actual = Privacy.redactGroupIds(text);
|
||||
const expected =
|
||||
'This is a log line with two group IDs: group([REDACTED]789)\n' +
|
||||
'and group([REDACTED]hij)';
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('redactAll', () => {
|
||||
it('should redact all sensitive information', () => {
|
||||
const encodedAppRootPath = APP_ROOT_PATH.replace(/ /g, '%20');
|
||||
const text =
|
||||
'This is a log line with sensitive information:\n' +
|
||||
`path1 ${APP_ROOT_PATH}/main.js\n` +
|
||||
'phone1 0531032fc7415b7cc1b7516480ad121d391eddce3cfb2cee27dd5b215609c32827 ipsum\n' +
|
||||
'group 31032fc7415b7cc1b7516480ad121d391eddce3cfb2cee27dd5b215609c32827 eeee\n' +
|
||||
'group1 group(123456789) doloret\n' +
|
||||
`path2 file:///${encodedAppRootPath}/js/background.js.` +
|
||||
'phone2 0531033dc7415b7cc1b7516480ad121d391eddce3cfb2cee27dd5b215609c32827 lorem\n' +
|
||||
'group2 group(abcdefghij) doloret\n' +
|
||||
'url1 https://you-have-to-hide.me aaa\n' +
|
||||
'url1 http://you-have-to-hide.me bbb\n' +
|
||||
'url1 127.0.0.1:22021 ccc\n';
|
||||
|
||||
const actual = Privacy.redactAll(text);
|
||||
const expected =
|
||||
'This is a log line with sensitive information:\n' +
|
||||
'path1 [REDACTED]/main.js\n' +
|
||||
'phone1 [REDACTED] ipsum\n' +
|
||||
'group [REDACTED] eeee\n' +
|
||||
'group1 group([REDACTED]789) doloret\n' +
|
||||
'path2 file:///[REDACTED]/js/background.js.' +
|
||||
'phone2 [REDACTED] lorem\n' +
|
||||
'group2 group([REDACTED]hij) doloret\n' +
|
||||
'url1 [REDACTED] aaa\n' +
|
||||
'url1 [REDACTED] bbb\n' +
|
||||
'url1 [REDACTED]:22021 ccc\n';
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_redactPath', () => {
|
||||
it('should redact file paths', () => {
|
||||
const testPath = '/Users/meow/Library/Application Support/Signal Beta';
|
||||
const text =
|
||||
'This is a log line with sensitive information:\n' +
|
||||
`path1 ${testPath}/main.js\n` +
|
||||
'phone1 0531032fc7415b7cc1b7516480ad121d391eddce3cfb2cee27dd5b215609c32827 ipsum\n';
|
||||
|
||||
const actual = Privacy._redactPath(testPath)(text);
|
||||
const expected =
|
||||
'This is a log line with sensitive information:\n' +
|
||||
'path1 [REDACTED]/main.js\n' +
|
||||
'phone1 0531032fc7415b7cc1b7516480ad121d391eddce3cfb2cee27dd5b215609c32827 ipsum\n';
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
|
||||
it('should redact URL-encoded paths', () => {
|
||||
const testPath = '/Users/meow/Library/Application Support/Signal Beta';
|
||||
const encodedTestPath = encodeURI(testPath);
|
||||
const text =
|
||||
'This is a log line with sensitive information:\n' +
|
||||
`path1 ${testPath}/main.js\n` +
|
||||
'phone1 0531032fc7415b7cc1b7516480ad121d391eddce3cfb2cee27dd5b215609c32827 ipsum\n' +
|
||||
'group1 group(123456789) doloret\n' +
|
||||
`path2 file:///${encodedTestPath}/js/background.js.`;
|
||||
|
||||
const actual = Privacy._redactPath(testPath)(text);
|
||||
const expected =
|
||||
'This is a log line with sensitive information:\n' +
|
||||
'path1 [REDACTED]/main.js\n' +
|
||||
'phone1 0531032fc7415b7cc1b7516480ad121d391eddce3cfb2cee27dd5b215609c32827 ipsum\n' +
|
||||
'group1 group(123456789) doloret\n' +
|
||||
'path2 file:///[REDACTED]/js/background.js.';
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
|
||||
it('should redact stack traces with both forward and backslashes', () => {
|
||||
const testPath = 'C:/Users/Meow/AppData/Local/Programs/loki-messenger-beta';
|
||||
const modifiedTestPath = 'C:\\Users\\Meow\\AppData\\Local\\Programs\\loki-messenger-beta';
|
||||
const text =
|
||||
'This is a log line with sensitive information:\n' +
|
||||
`path1 ${testPath}\\main.js\n` +
|
||||
'phone1 +12223334455 ipsum\n' +
|
||||
'group1 group(123456789) doloret\n' +
|
||||
`path2 ${modifiedTestPath}\\js\\background.js.`;
|
||||
|
||||
const actual = Privacy._redactPath(testPath)(text);
|
||||
const expected =
|
||||
'This is a log line with sensitive information:\n' +
|
||||
'path1 [REDACTED]\\main.js\n' +
|
||||
'phone1 +12223334455 ipsum\n' +
|
||||
'group1 group(123456789) doloret\n' +
|
||||
'path2 [REDACTED]\\js\\background.js.';
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
|
||||
it('should redact stack traces with escaped backslashes', () => {
|
||||
const testPath = 'C:\\Users\\Meow\\AppData\\Local\\Programs\\loki-messenger-beta';
|
||||
const modifiedTestPath =
|
||||
'C:\\\\Users\\\\Meow\\\\AppData\\\\Local\\\\Programs\\\\loki-messenger-beta';
|
||||
const text =
|
||||
'This is a log line with sensitive information:\n' +
|
||||
`path1 ${testPath}\\main.js\n` +
|
||||
'phone1 +12223334455 ipsum\n' +
|
||||
'group1 group(123456789) doloret\n' +
|
||||
`path2 ${modifiedTestPath}\\js\\background.js.`;
|
||||
|
||||
const actual = Privacy._redactPath(testPath)(text);
|
||||
const expected =
|
||||
'This is a log line with sensitive information:\n' +
|
||||
'path1 [REDACTED]\\main.js\n' +
|
||||
'phone1 +12223334455 ipsum\n' +
|
||||
'group1 group(123456789) doloret\n' +
|
||||
'path2 [REDACTED]\\js\\background.js.';
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,226 +0,0 @@
|
|||
require('mocha-testcheck').install();
|
||||
|
||||
const { assert } = require('chai');
|
||||
|
||||
const Attachment = require('../../../js/modules/types/attachment');
|
||||
const { stringToArrayBuffer } = require('../../../js/modules/string_to_array_buffer');
|
||||
|
||||
describe('Attachment', () => {
|
||||
describe('replaceUnicodeOrderOverrides', () => {
|
||||
it('should sanitize left-to-right order override character', async () => {
|
||||
const input = {
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'test\u202Dfig.exe',
|
||||
size: 1111,
|
||||
};
|
||||
const expected = {
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'test\uFFFDfig.exe',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const actual = await Attachment.replaceUnicodeOrderOverrides(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should sanitize right-to-left order override character', async () => {
|
||||
const input = {
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'test\u202Efig.exe',
|
||||
size: 1111,
|
||||
};
|
||||
const expected = {
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'test\uFFFDfig.exe',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const actual = await Attachment.replaceUnicodeOrderOverrides(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should sanitize multiple override characters', async () => {
|
||||
const input = {
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'test\u202e\u202dlol\u202efig.exe',
|
||||
size: 1111,
|
||||
};
|
||||
const expected = {
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'test\uFFFD\uFFFDlol\uFFFDfig.exe',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const actual = await Attachment.replaceUnicodeOrderOverrides(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
const hasNoUnicodeOrderOverrides = value =>
|
||||
!value.includes('\u202D') && !value.includes('\u202E');
|
||||
|
||||
check.it(
|
||||
'should ignore non-order-override characters',
|
||||
gen.string.suchThat(hasNoUnicodeOrderOverrides),
|
||||
fileName => {
|
||||
const input = {
|
||||
contentType: 'image/jpeg',
|
||||
fileName,
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const actual = Attachment._replaceUnicodeOrderOverridesSync(input);
|
||||
assert.deepEqual(actual, input);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('replaceUnicodeV2', () => {
|
||||
it('should remove all bad characters', async () => {
|
||||
const input = {
|
||||
size: 1111,
|
||||
fileName:
|
||||
'file\u202A\u202B\u202C\u202D\u202E\u2066\u2067\u2068\u2069\u200E\u200F\u061C.jpeg',
|
||||
};
|
||||
const expected = {
|
||||
fileName:
|
||||
'file\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD.jpeg',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const actual = await Attachment.replaceUnicodeV2(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should should leave normal filename alone', async () => {
|
||||
const input = {
|
||||
fileName: 'normal.jpeg',
|
||||
size: 1111,
|
||||
};
|
||||
const expected = {
|
||||
fileName: 'normal.jpeg',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const actual = await Attachment.replaceUnicodeV2(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should handle missing fileName', async () => {
|
||||
const input = {
|
||||
size: 1111,
|
||||
};
|
||||
const expected = {
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const actual = await Attachment.replaceUnicodeV2(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeSchemaVersion', () => {
|
||||
it('should remove existing schema version', () => {
|
||||
const input = {
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'foo.jpg',
|
||||
size: 1111,
|
||||
schemaVersion: 1,
|
||||
};
|
||||
|
||||
const expected = {
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'foo.jpg',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const actual = Attachment.removeSchemaVersion({
|
||||
attachment: input,
|
||||
logger: {
|
||||
error: () => null,
|
||||
},
|
||||
});
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('migrateDataToFileSystem', () => {
|
||||
it('should write data to disk and store relative path to it', async () => {
|
||||
const input = {
|
||||
contentType: 'image/jpeg',
|
||||
data: stringToArrayBuffer('Above us only sky'),
|
||||
fileName: 'foo.jpg',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const expected = {
|
||||
contentType: 'image/jpeg',
|
||||
path: 'abc/abcdefgh123456789',
|
||||
fileName: 'foo.jpg',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const expectedAttachmentData = stringToArrayBuffer('Above us only sky');
|
||||
const writeNewAttachmentData = async attachmentData => {
|
||||
assert.deepEqual(attachmentData, expectedAttachmentData);
|
||||
return 'abc/abcdefgh123456789';
|
||||
};
|
||||
|
||||
const actual = await Attachment.migrateDataToFileSystem(input, {
|
||||
writeNewAttachmentData,
|
||||
logger: {
|
||||
warn: () => null,
|
||||
},
|
||||
});
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should skip over (invalid) attachments without data', async () => {
|
||||
const input = {
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'foo.jpg',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const expected = {
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'foo.jpg',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const writeNewAttachmentData = async () => 'abc/abcdefgh123456789';
|
||||
|
||||
const actual = await Attachment.migrateDataToFileSystem(input, {
|
||||
writeNewAttachmentData,
|
||||
logger: {
|
||||
warn: () => null,
|
||||
},
|
||||
});
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should throw error if data is not valid', async () => {
|
||||
const input = {
|
||||
contentType: 'image/jpeg',
|
||||
data: 42,
|
||||
fileName: 'foo.jpg',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const writeNewAttachmentData = async () => 'abc/abcdefgh123456789';
|
||||
|
||||
try {
|
||||
await Attachment.migrateDataToFileSystem(input, {
|
||||
writeNewAttachmentData,
|
||||
logger: {
|
||||
warn: () => null,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
assert.strictEqual(error.message, 'Expected 42 to be an array buffer got: number');
|
||||
return;
|
||||
}
|
||||
|
||||
assert.fail('Unreachable');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,37 +0,0 @@
|
|||
const Path = require('path');
|
||||
|
||||
const { assert } = require('chai');
|
||||
|
||||
const Errors = require('../../../js/modules/types/errors');
|
||||
|
||||
const APP_ROOT_PATH = Path.join(__dirname, '..', '..', '..');
|
||||
|
||||
describe('Errors', () => {
|
||||
describe('toLogFormat', () => {
|
||||
it('should return error stack trace if present', () => {
|
||||
const error = new Error('boom');
|
||||
assert.typeOf(error, 'Error');
|
||||
|
||||
const formattedError = Errors.toLogFormat(error);
|
||||
assert.include(formattedError, 'errors_test.js');
|
||||
assert.include(formattedError, APP_ROOT_PATH, 'Formatted stack has app path');
|
||||
});
|
||||
|
||||
it('should return error string representation if stack is missing', () => {
|
||||
const error = new Error('boom');
|
||||
error.stack = null;
|
||||
assert.typeOf(error, 'Error');
|
||||
assert.isNull(error.stack);
|
||||
|
||||
const formattedError = Errors.toLogFormat(error);
|
||||
assert.strictEqual(formattedError, 'Error: boom');
|
||||
});
|
||||
|
||||
[0, false, null, undefined].forEach(value => {
|
||||
it(`should return \`${value}\` argument`, () => {
|
||||
const formattedNonError = Errors.toLogFormat(value);
|
||||
assert.strictEqual(formattedNonError, value);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,581 +0,0 @@
|
|||
const { assert } = require('chai');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const Message = require('../../../js/modules/types/message');
|
||||
const { SignalService } = require('../../../ts/protobuf');
|
||||
const { stringToArrayBuffer } = require('../../../js/modules/string_to_array_buffer');
|
||||
|
||||
describe('Message', () => {
|
||||
const logger = {
|
||||
warn: () => null,
|
||||
error: () => null,
|
||||
};
|
||||
|
||||
describe('createAttachmentDataWriter', () => {
|
||||
it('should ignore messages that didn’t go through attachment migration', async () => {
|
||||
const input = {
|
||||
body: 'Imagine there is no heaven…',
|
||||
schemaVersion: 2,
|
||||
};
|
||||
const expected = {
|
||||
body: 'Imagine there is no heaven…',
|
||||
schemaVersion: 2,
|
||||
};
|
||||
const writeExistingAttachmentData = () => {};
|
||||
|
||||
const actual = await Message.createAttachmentDataWriter({
|
||||
writeExistingAttachmentData,
|
||||
logger,
|
||||
})(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should ignore messages without attachments', async () => {
|
||||
const input = {
|
||||
body: 'Imagine there is no heaven…',
|
||||
schemaVersion: 4,
|
||||
attachments: [],
|
||||
};
|
||||
const expected = {
|
||||
body: 'Imagine there is no heaven…',
|
||||
schemaVersion: 4,
|
||||
attachments: [],
|
||||
};
|
||||
const writeExistingAttachmentData = () => {};
|
||||
|
||||
const actual = await Message.createAttachmentDataWriter({
|
||||
writeExistingAttachmentData,
|
||||
logger,
|
||||
})(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should write attachments to file system on original path', async () => {
|
||||
const input = {
|
||||
body: 'Imagine there is no heaven…',
|
||||
schemaVersion: 4,
|
||||
attachments: [
|
||||
{
|
||||
path: 'ab/abcdefghi',
|
||||
data: stringToArrayBuffer('It’s easy if you try'),
|
||||
},
|
||||
],
|
||||
};
|
||||
const expected = {
|
||||
body: 'Imagine there is no heaven…',
|
||||
schemaVersion: 4,
|
||||
attachments: [
|
||||
{
|
||||
path: 'ab/abcdefghi',
|
||||
},
|
||||
],
|
||||
preview: [],
|
||||
};
|
||||
|
||||
const writeExistingAttachmentData = attachment => {
|
||||
assert.equal(attachment.path, 'ab/abcdefghi');
|
||||
assert.deepEqual(attachment.data, stringToArrayBuffer('It’s easy if you try'));
|
||||
};
|
||||
|
||||
const actual = await Message.createAttachmentDataWriter({
|
||||
writeExistingAttachmentData,
|
||||
logger,
|
||||
})(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should process quote attachment thumbnails', async () => {
|
||||
const input = {
|
||||
body: 'Imagine there is no heaven…',
|
||||
schemaVersion: 4,
|
||||
attachments: [],
|
||||
quote: {
|
||||
attachments: [
|
||||
{
|
||||
thumbnail: {
|
||||
path: 'ab/abcdefghi',
|
||||
data: stringToArrayBuffer('It’s easy if you try'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const expected = {
|
||||
body: 'Imagine there is no heaven…',
|
||||
schemaVersion: 4,
|
||||
attachments: [],
|
||||
quote: {
|
||||
attachments: [
|
||||
{
|
||||
thumbnail: {
|
||||
path: 'ab/abcdefghi',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
preview: [],
|
||||
};
|
||||
|
||||
const writeExistingAttachmentData = attachment => {
|
||||
assert.equal(attachment.path, 'ab/abcdefghi');
|
||||
assert.deepEqual(attachment.data, stringToArrayBuffer('It’s easy if you try'));
|
||||
};
|
||||
|
||||
const actual = await Message.createAttachmentDataWriter({
|
||||
writeExistingAttachmentData,
|
||||
logger,
|
||||
})(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initializeSchemaVersion', () => {
|
||||
it('should ignore messages with previously inherited schema', () => {
|
||||
const input = {
|
||||
body: 'Imagine there is no heaven…',
|
||||
schemaVersion: 2,
|
||||
};
|
||||
const expected = {
|
||||
body: 'Imagine there is no heaven…',
|
||||
schemaVersion: 2,
|
||||
};
|
||||
|
||||
const actual = Message.initializeSchemaVersion({
|
||||
message: input,
|
||||
logger,
|
||||
});
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
context('for message without attachments', () => {
|
||||
it('should initialize schema version to zero', () => {
|
||||
const input = {
|
||||
body: 'Imagine there is no heaven…',
|
||||
attachments: [],
|
||||
};
|
||||
const expected = {
|
||||
body: 'Imagine there is no heaven…',
|
||||
attachments: [],
|
||||
schemaVersion: 0,
|
||||
};
|
||||
|
||||
const actual = Message.initializeSchemaVersion({
|
||||
message: input,
|
||||
logger,
|
||||
});
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
|
||||
context('for message with attachments', () => {
|
||||
it('should inherit existing attachment schema version', () => {
|
||||
const input = {
|
||||
body: 'Imagine there is no heaven…',
|
||||
attachments: [
|
||||
{
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'lennon.jpg',
|
||||
schemaVersion: 7,
|
||||
},
|
||||
],
|
||||
};
|
||||
const expected = {
|
||||
body: 'Imagine there is no heaven…',
|
||||
attachments: [
|
||||
{
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'lennon.jpg',
|
||||
},
|
||||
],
|
||||
schemaVersion: 7,
|
||||
};
|
||||
|
||||
const actual = Message.initializeSchemaVersion({
|
||||
message: input,
|
||||
logger,
|
||||
});
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('upgradeSchema', () => {
|
||||
it('should upgrade an unversioned message to the latest version', async () => {
|
||||
const input = {
|
||||
attachments: [
|
||||
{
|
||||
contentType: 'audio/aac',
|
||||
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
|
||||
data: stringToArrayBuffer('It’s easy if you try'),
|
||||
fileName: 'test\u202Dfig.exe',
|
||||
size: 1111,
|
||||
},
|
||||
],
|
||||
schemaVersion: 0,
|
||||
};
|
||||
const expected = {
|
||||
attachments: [
|
||||
{
|
||||
contentType: 'audio/aac',
|
||||
flags: 1,
|
||||
path: 'abc/abcdefg',
|
||||
fileName: 'test\uFFFDfig.exe',
|
||||
size: 1111,
|
||||
},
|
||||
],
|
||||
hasAttachments: 1,
|
||||
hasVisualMediaAttachments: undefined,
|
||||
hasFileAttachments: undefined,
|
||||
schemaVersion: Message.CURRENT_SCHEMA_VERSION,
|
||||
};
|
||||
|
||||
const expectedAttachmentData = stringToArrayBuffer('It’s easy if you try');
|
||||
const context = {
|
||||
writeNewAttachmentData: async attachmentData => {
|
||||
assert.deepEqual(attachmentData, expectedAttachmentData);
|
||||
return 'abc/abcdefg';
|
||||
},
|
||||
getAbsoluteAttachmentPath: () => 'some/path/on/disk',
|
||||
makeObjectUrl: () => 'blob://FAKE',
|
||||
revokeObjectUrl: () => null,
|
||||
getImageDimensions: () => ({ height: 10, width: 15 }),
|
||||
makeImageThumbnail: () => new Blob(),
|
||||
makeVideoScreenshot: () => new Blob(),
|
||||
logger: {
|
||||
warn: () => null,
|
||||
error: () => null,
|
||||
},
|
||||
};
|
||||
const actual = await Message.upgradeSchema(input, context);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
context('with multiple upgrade steps', () => {
|
||||
it('should return last valid message when any upgrade step fails', async () => {
|
||||
const input = {
|
||||
attachments: [
|
||||
{
|
||||
contentType: 'application/json',
|
||||
data: null,
|
||||
fileName: 'test\u202Dfig.exe',
|
||||
size: 1111,
|
||||
},
|
||||
],
|
||||
schemaVersion: 0,
|
||||
};
|
||||
const expected = {
|
||||
attachments: [
|
||||
{
|
||||
contentType: 'application/json',
|
||||
data: null,
|
||||
fileName: 'test\u202Dfig.exe',
|
||||
size: 1111,
|
||||
},
|
||||
],
|
||||
hasUpgradedToVersion1: true,
|
||||
schemaVersion: 1,
|
||||
};
|
||||
|
||||
const v1 = async message => Object.assign({}, message, { hasUpgradedToVersion1: true });
|
||||
const v2 = async () => {
|
||||
throw new Error('boom');
|
||||
};
|
||||
const v3 = async message => Object.assign({}, message, { hasUpgradedToVersion3: true });
|
||||
|
||||
const toVersion1 = Message._withSchemaVersion({
|
||||
schemaVersion: 1,
|
||||
upgrade: v1,
|
||||
});
|
||||
const toVersion2 = Message._withSchemaVersion({
|
||||
schemaVersion: 2,
|
||||
upgrade: v2,
|
||||
});
|
||||
const toVersion3 = Message._withSchemaVersion({
|
||||
schemaVersion: 3,
|
||||
upgrade: v3,
|
||||
});
|
||||
|
||||
const context = { logger };
|
||||
const upgradeSchema = async message =>
|
||||
toVersion3(await toVersion2(await toVersion1(message, context), context), context);
|
||||
|
||||
const actual = await upgradeSchema(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should skip out-of-order upgrade steps', async () => {
|
||||
const input = {
|
||||
attachments: [
|
||||
{
|
||||
contentType: 'application/json',
|
||||
data: null,
|
||||
fileName: 'test\u202Dfig.exe',
|
||||
size: 1111,
|
||||
},
|
||||
],
|
||||
schemaVersion: 0,
|
||||
};
|
||||
const expected = {
|
||||
attachments: [
|
||||
{
|
||||
contentType: 'application/json',
|
||||
data: null,
|
||||
fileName: 'test\u202Dfig.exe',
|
||||
size: 1111,
|
||||
},
|
||||
],
|
||||
schemaVersion: 2,
|
||||
hasUpgradedToVersion1: true,
|
||||
hasUpgradedToVersion2: true,
|
||||
};
|
||||
|
||||
const v1 = async attachment =>
|
||||
Object.assign({}, attachment, { hasUpgradedToVersion1: true });
|
||||
const v2 = async attachment =>
|
||||
Object.assign({}, attachment, { hasUpgradedToVersion2: true });
|
||||
const v3 = async attachment =>
|
||||
Object.assign({}, attachment, { hasUpgradedToVersion3: true });
|
||||
|
||||
const toVersion1 = Message._withSchemaVersion({
|
||||
schemaVersion: 1,
|
||||
upgrade: v1,
|
||||
});
|
||||
const toVersion2 = Message._withSchemaVersion({
|
||||
schemaVersion: 2,
|
||||
upgrade: v2,
|
||||
});
|
||||
const toVersion3 = Message._withSchemaVersion({
|
||||
schemaVersion: 3,
|
||||
upgrade: v3,
|
||||
});
|
||||
|
||||
const context = { logger };
|
||||
// NOTE: We upgrade to 3 before 2, i.e. the pipeline should abort:
|
||||
const upgradeSchema = async attachment =>
|
||||
toVersion2(await toVersion3(await toVersion1(attachment, context), context), context);
|
||||
|
||||
const actual = await upgradeSchema(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_withSchemaVersion', () => {
|
||||
it('should require a version number', () => {
|
||||
const toVersionX = () => {};
|
||||
assert.throws(
|
||||
() => Message._withSchemaVersion({ schemaVersion: toVersionX, upgrade: 2 }),
|
||||
'_withSchemaVersion: schemaVersion is invalid'
|
||||
);
|
||||
});
|
||||
|
||||
it('should require an upgrade function', () => {
|
||||
assert.throws(
|
||||
() => Message._withSchemaVersion({ schemaVersion: 2, upgrade: 3 }),
|
||||
'_withSchemaVersion: upgrade must be a function'
|
||||
);
|
||||
});
|
||||
|
||||
it('should skip upgrading if message has already been upgraded', async () => {
|
||||
const upgrade = async message => Object.assign({}, message, { foo: true });
|
||||
const upgradeWithVersion = Message._withSchemaVersion({
|
||||
schemaVersion: 3,
|
||||
upgrade,
|
||||
});
|
||||
|
||||
const input = {
|
||||
id: 'guid-guid-guid-guid',
|
||||
schemaVersion: 4,
|
||||
};
|
||||
const expected = {
|
||||
id: 'guid-guid-guid-guid',
|
||||
schemaVersion: 4,
|
||||
};
|
||||
const actual = await upgradeWithVersion(input, { logger });
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should return original message if upgrade function throws', async () => {
|
||||
const upgrade = async () => {
|
||||
throw new Error('boom!');
|
||||
};
|
||||
const upgradeWithVersion = Message._withSchemaVersion({
|
||||
schemaVersion: 3,
|
||||
upgrade,
|
||||
});
|
||||
|
||||
const input = {
|
||||
id: 'guid-guid-guid-guid',
|
||||
schemaVersion: 0,
|
||||
};
|
||||
const expected = {
|
||||
id: 'guid-guid-guid-guid',
|
||||
schemaVersion: 0,
|
||||
};
|
||||
const actual = await upgradeWithVersion(input, { logger });
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should return original message if upgrade function returns null', async () => {
|
||||
const upgrade = async () => null;
|
||||
const upgradeWithVersion = Message._withSchemaVersion({
|
||||
schemaVersion: 3,
|
||||
upgrade,
|
||||
});
|
||||
|
||||
const input = {
|
||||
id: 'guid-guid-guid-guid',
|
||||
schemaVersion: 0,
|
||||
};
|
||||
const expected = {
|
||||
id: 'guid-guid-guid-guid',
|
||||
schemaVersion: 0,
|
||||
};
|
||||
const actual = await upgradeWithVersion(input, { logger });
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_mapQuotedAttachments', () => {
|
||||
it('handles message with no quote', async () => {
|
||||
const upgradeAttachment = sinon.stub().throws(new Error("Shouldn't be called"));
|
||||
const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment);
|
||||
|
||||
const message = {
|
||||
body: 'hey there!',
|
||||
};
|
||||
const result = await upgradeVersion(message);
|
||||
assert.deepEqual(result, message);
|
||||
});
|
||||
|
||||
it('handles quote with no attachments', async () => {
|
||||
const upgradeAttachment = sinon.stub().throws(new Error("Shouldn't be called"));
|
||||
const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment);
|
||||
|
||||
const message = {
|
||||
body: 'hey there!',
|
||||
quote: {
|
||||
text: 'hey!',
|
||||
},
|
||||
};
|
||||
const expected = {
|
||||
body: 'hey there!',
|
||||
quote: {
|
||||
text: 'hey!',
|
||||
attachments: [],
|
||||
},
|
||||
};
|
||||
const result = await upgradeVersion(message, { logger });
|
||||
assert.deepEqual(result, expected);
|
||||
});
|
||||
|
||||
it('handles zero attachments', async () => {
|
||||
const upgradeAttachment = sinon.stub().throws(new Error("Shouldn't be called"));
|
||||
const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment);
|
||||
|
||||
const message = {
|
||||
body: 'hey there!',
|
||||
quote: {
|
||||
text: 'hey!',
|
||||
attachments: [],
|
||||
},
|
||||
};
|
||||
const result = await upgradeVersion(message, { logger });
|
||||
assert.deepEqual(result, message);
|
||||
});
|
||||
|
||||
it('handles attachments with no thumbnail', async () => {
|
||||
const upgradeAttachment = sinon.stub().throws(new Error("Shouldn't be called"));
|
||||
const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment);
|
||||
|
||||
const message = {
|
||||
body: 'hey there!',
|
||||
quote: {
|
||||
text: 'hey!',
|
||||
attachments: [
|
||||
{
|
||||
fileName: 'manifesto.txt',
|
||||
contentType: 'text/plain',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const result = await upgradeVersion(message, { logger });
|
||||
assert.deepEqual(result, message);
|
||||
});
|
||||
|
||||
it('does not eliminate thumbnails with missing data field', async () => {
|
||||
const upgradeAttachment = sinon.stub().returns({ fileName: 'processed!' });
|
||||
const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment);
|
||||
|
||||
const message = {
|
||||
body: 'hey there!',
|
||||
quote: {
|
||||
text: 'hey!',
|
||||
attachments: [
|
||||
{
|
||||
fileName: 'cat.gif',
|
||||
contentType: 'image/gif',
|
||||
thumbnail: {
|
||||
fileName: 'not yet downloaded!',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const expected = {
|
||||
body: 'hey there!',
|
||||
quote: {
|
||||
text: 'hey!',
|
||||
attachments: [
|
||||
{
|
||||
contentType: 'image/gif',
|
||||
fileName: 'cat.gif',
|
||||
thumbnail: {
|
||||
fileName: 'processed!',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const result = await upgradeVersion(message, { logger });
|
||||
assert.deepEqual(result, expected);
|
||||
});
|
||||
|
||||
it('calls provided async function for each quoted attachment', async () => {
|
||||
const upgradeAttachment = sinon.stub().resolves({
|
||||
path: '/new/path/on/disk',
|
||||
});
|
||||
const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment);
|
||||
|
||||
const message = {
|
||||
body: 'hey there!',
|
||||
quote: {
|
||||
text: 'hey!',
|
||||
attachments: [
|
||||
{
|
||||
thumbnail: {
|
||||
data: 'data is here',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const expected = {
|
||||
body: 'hey there!',
|
||||
quote: {
|
||||
text: 'hey!',
|
||||
attachments: [
|
||||
{
|
||||
thumbnail: {
|
||||
path: '/new/path/on/disk',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const result = await upgradeVersion(message, { logger });
|
||||
assert.deepEqual(result, expected);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,28 +0,0 @@
|
|||
const { assert } = require('chai');
|
||||
|
||||
const MIME = require('../../../ts/types/MIME');
|
||||
|
||||
describe('MIME', () => {
|
||||
describe('isJPEG', () => {
|
||||
it('should return true for `image/jpeg`', () => {
|
||||
assert.isTrue(MIME.isJPEG('image/jpeg'));
|
||||
});
|
||||
|
||||
[
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'image/jpg', // invalid MIME type: https://stackoverflow.com/a/37266399/125305
|
||||
'image/gif',
|
||||
'image/tiff',
|
||||
'application/json',
|
||||
0,
|
||||
false,
|
||||
null,
|
||||
undefined,
|
||||
].forEach(value => {
|
||||
it(`should return false for \`${value}\``, () => {
|
||||
assert.isFalse(MIME.isJPEG(value));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,20 +0,0 @@
|
|||
require('mocha-testcheck').install();
|
||||
const { assert } = require('chai');
|
||||
|
||||
const SchemaVersion = require('../../../js/modules/types/schema_version');
|
||||
|
||||
describe('SchemaVersion', () => {
|
||||
describe('isValid', () => {
|
||||
check.it('should return true for positive integers', gen.posInt, input => {
|
||||
assert.isTrue(SchemaVersion.isValid(input));
|
||||
});
|
||||
|
||||
check.it(
|
||||
'should return false for any other value',
|
||||
gen.primitive.suchThat(value => typeof value !== 'number' || value < 0),
|
||||
input => {
|
||||
assert.isFalse(SchemaVersion.isValid(input));
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,38 +0,0 @@
|
|||
/* global Whisper */
|
||||
|
||||
describe('Whisper.View', () => {
|
||||
it('renders a template with render_attributes', () => {
|
||||
const ViewClass = Whisper.View.extend({
|
||||
template: '<div>{{ variable }}</div>',
|
||||
render_attributes: {
|
||||
variable: 'value',
|
||||
},
|
||||
});
|
||||
|
||||
const view = new ViewClass();
|
||||
view.render();
|
||||
assert.strictEqual(view.$el.html(), '<div>value</div>');
|
||||
});
|
||||
it('renders a template with no render_attributes', () => {
|
||||
const ViewClass = Whisper.View.extend({
|
||||
template: '<div>static text</div>',
|
||||
});
|
||||
|
||||
const view = new ViewClass();
|
||||
view.render();
|
||||
assert.strictEqual(view.$el.html(), '<div>static text</div>');
|
||||
});
|
||||
it('renders a template function with render_attributes function', () => {
|
||||
const ViewClass = Whisper.View.extend({
|
||||
template() {
|
||||
return '<div>{{ variable }}</div>';
|
||||
},
|
||||
render_attributes() {
|
||||
return { variable: 'value' };
|
||||
},
|
||||
});
|
||||
const view = new ViewClass();
|
||||
view.render();
|
||||
assert.strictEqual(view.$el.html(), '<div>value</div>');
|
||||
});
|
||||
});
|
|
@ -7,7 +7,7 @@ import { AttachmentType } from '../types/Attachment';
|
|||
|
||||
import { SessionInput } from './session/SessionInput';
|
||||
import { SessionButton, SessionButtonColor, SessionButtonType } from './session/SessionButton';
|
||||
import { darkTheme, lightTheme } from '../state/ducks/SessionTheme';
|
||||
import { darkTheme } from '../state/ducks/SessionTheme';
|
||||
import autoBind from 'auto-bind';
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -38,8 +38,6 @@ export class ContactListItem extends React.Component<Props> {
|
|||
</span>
|
||||
) : null;
|
||||
|
||||
const showNumber = isMe || name;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// tslint:disable:react-a11y-anchors
|
||||
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
|
||||
import is from '@sindresorhus/is';
|
||||
|
||||
|
@ -136,7 +136,7 @@ interface IconButtonProps {
|
|||
}
|
||||
|
||||
const IconButton = ({ onClick, type, theme }: IconButtonProps) => {
|
||||
const clickHandler = (event: React.MouseEvent<HTMLAnchorElement>): void => {
|
||||
const clickHandler = (_event: React.MouseEvent<HTMLAnchorElement>): void => {
|
||||
if (!onClick) {
|
||||
return;
|
||||
}
|
||||
|
@ -196,6 +196,7 @@ const Icon = ({
|
|||
/>
|
||||
);
|
||||
|
||||
// tslint:disable-next-line: max-func-body-length
|
||||
export const LightboxObject = ({
|
||||
objectURL,
|
||||
contentType,
|
||||
|
@ -216,37 +217,12 @@ export const LightboxObject = ({
|
|||
return false;
|
||||
}, []);
|
||||
|
||||
const playVideo = () => {
|
||||
if (!videoRef) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { current } = videoRef;
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (current.paused) {
|
||||
void current.play();
|
||||
} else {
|
||||
current.pause();
|
||||
}
|
||||
};
|
||||
|
||||
const pauseVideo = () => {
|
||||
if (!videoRef) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { current } = videoRef;
|
||||
if (current) {
|
||||
current.pause();
|
||||
}
|
||||
};
|
||||
|
||||
// auto play video on showing a video attachment
|
||||
useUnmount(() => {
|
||||
pauseVideo();
|
||||
if (!videoRef?.current) {
|
||||
return;
|
||||
}
|
||||
videoRef.current.pause.pause();
|
||||
});
|
||||
|
||||
if (isImageTypeSupported) {
|
||||
|
@ -263,13 +239,15 @@ export const LightboxObject = ({
|
|||
const isVideoTypeSupported = GoogleChrome.isVideoTypeSupported(contentType);
|
||||
if (isVideoTypeSupported) {
|
||||
if (urlToLoad) {
|
||||
playVideo();
|
||||
if (videoRef?.current.paused) {
|
||||
void videoRef.current.play();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<video
|
||||
role="button"
|
||||
ref={videoRef}
|
||||
onClick={playVideo}
|
||||
controls={true}
|
||||
style={styles.object as any}
|
||||
key={urlToLoad}
|
||||
|
|
|
@ -1,116 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Avatar, AvatarSize } from './Avatar';
|
||||
import { ContactName } from './conversation/ContactName';
|
||||
|
||||
import { FindAndFormatContactType, PropsForSearchResults } from '../state/ducks/conversations';
|
||||
|
||||
type PropsHousekeeping = {
|
||||
isSelected?: boolean;
|
||||
};
|
||||
|
||||
type Props = PropsForSearchResults & PropsHousekeeping;
|
||||
|
||||
const FromName = (props: { from: FindAndFormatContactType; to: FindAndFormatContactType }) => {
|
||||
const { from, to } = props;
|
||||
|
||||
if (from.isMe && to.isMe) {
|
||||
return (
|
||||
<span className="module-message-search-result__header__name">
|
||||
{window.i18n('noteToSelf')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (from.isMe) {
|
||||
return <span className="module-message-search-result__header__name">{window.i18n('you')}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
// tslint:disable: use-simple-attributes
|
||||
<ContactName
|
||||
phoneNumber={from.phoneNumber}
|
||||
name={from.name || ''}
|
||||
profileName={from.profileName || ''}
|
||||
module="module-message-search-result__header__name"
|
||||
shouldShowPubkey={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const From = (props: { from: FindAndFormatContactType; to: FindAndFormatContactType }) => {
|
||||
const { to, from } = props;
|
||||
const fromName = <FromName from={from} to={to} />;
|
||||
|
||||
if (!to.isMe) {
|
||||
return (
|
||||
<div className="module-message-search-result__header__from">
|
||||
{fromName} {window.i18n('to')}{' '}
|
||||
<span className="module-mesages-search-result__header__group">
|
||||
<ContactName
|
||||
phoneNumber={to.phoneNumber}
|
||||
name={to.name || ''}
|
||||
profileName={to.profileName || ''}
|
||||
shouldShowPubkey={false}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="module-message-search-result__header__from">{fromName}</div>;
|
||||
};
|
||||
|
||||
const AvatarItem = (props: { from: FindAndFormatContactType }) => {
|
||||
const { from } = props;
|
||||
const userName = from.profileName || from.phoneNumber;
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
avatarPath={from.avatarPath || ''}
|
||||
name={userName}
|
||||
size={AvatarSize.S}
|
||||
pubkey={from.phoneNumber}
|
||||
/>
|
||||
);
|
||||
};
|
||||
// export const MessageSearchResult = (props: Props) => {
|
||||
// const { from, id: messageId, isSelected, conversationId, receivedAt, snippet, to } = props;
|
||||
|
||||
// const dispatch = useDispatch();
|
||||
|
||||
// if (!from || !to) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// return (
|
||||
// <div
|
||||
// role="button"
|
||||
// onClick={() => {
|
||||
// dispatch(
|
||||
// openConversationExternal({
|
||||
// id: conversationId,
|
||||
// messageId,
|
||||
// firstUnreadIdOnOpen: undefined,
|
||||
// })
|
||||
// );
|
||||
// }}
|
||||
// className={classNames(
|
||||
// 'module-message-search-result',
|
||||
// isSelected ? 'module-message-search-result--is-selected' : null
|
||||
// )}
|
||||
// >
|
||||
// <AvatarItem from={from} />
|
||||
// <div className="module-message-search-result__text">
|
||||
// <div className="module-message-search-result__header">
|
||||
// <From from={from} to={to} />
|
||||
// <div className="module-message-search-result__header__timestamp">
|
||||
// <Timestamp timestamp={receivedAt} />
|
||||
// </div>
|
||||
// </div>
|
||||
// <div className="module-message-search-result__body">
|
||||
// <MessageBodyHighlight text={snippet || ''} />
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// };
|
|
@ -4,7 +4,6 @@ import {
|
|||
ConversationListItemProps,
|
||||
MemoConversationListItemWithDetails,
|
||||
} from './ConversationListItem';
|
||||
// import { MessageSearchResult } from './MessageSearchResult';
|
||||
|
||||
export type SearchResultsProps = {
|
||||
contacts: Array<ConversationListItemProps>;
|
||||
|
@ -26,7 +25,7 @@ const ContactsItem = (props: { header: string; items: Array<ConversationListItem
|
|||
};
|
||||
|
||||
export const SearchResults = (props: SearchResultsProps) => {
|
||||
const { conversations, contacts, hideMessagesHeader, messages, searchTerm } = props;
|
||||
const { conversations, contacts, messages, searchTerm } = props;
|
||||
|
||||
const haveConversations = conversations && conversations.length;
|
||||
const haveContacts = contacts && contacts.length;
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
import React, { useEffect } from 'react';
|
||||
|
||||
import { RenderTextCallbackType } from '../../types/Util';
|
||||
import classNames from 'classnames';
|
||||
import { PubKey } from '../../session/types';
|
||||
import { ConversationModel } from '../../models/conversation';
|
||||
import { UserUtils } from '../../session/utils';
|
||||
import { getConversationController } from '../../session/conversations';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
|
||||
interface MentionProps {
|
||||
key: string;
|
||||
|
@ -48,7 +45,7 @@ export class AddMentions extends React.Component<Props> {
|
|||
const results: Array<any> = [];
|
||||
const FIND_MENTIONS = new RegExp(`@${PubKey.regexForPubkeys}`, 'g');
|
||||
|
||||
// We have to do this, because renderNonNewLine is not required in our Props object,
|
||||
// We have to do this, because renderOther is not required in our Props object,
|
||||
// but it is always provided via defaultProps.
|
||||
if (!renderOther) {
|
||||
return;
|
||||
|
|
|
@ -16,8 +16,6 @@ export class AddNewLines extends React.Component<Props> {
|
|||
|
||||
public render() {
|
||||
const { text, renderNonNewLine, convoId } = this.props;
|
||||
const results: Array<any> = [];
|
||||
const FIND_NEWLINES = /\n/g;
|
||||
|
||||
// We have to do this, because renderNonNewLine is not required in our Props object,
|
||||
// but it is always provided via defaultProps.
|
||||
|
@ -25,31 +23,6 @@ export class AddNewLines extends React.Component<Props> {
|
|||
return;
|
||||
}
|
||||
|
||||
let match = FIND_NEWLINES.exec(text);
|
||||
let last = 0;
|
||||
let count = 1;
|
||||
|
||||
if (!match) {
|
||||
return renderNonNewLine({ text, key: 0, convoId });
|
||||
}
|
||||
|
||||
while (match) {
|
||||
if (last < match.index) {
|
||||
const textWithNoNewline = text.slice(last, match.index);
|
||||
results.push(renderNonNewLine({ text: textWithNoNewline, key: count++, convoId }));
|
||||
}
|
||||
|
||||
results.push(<br key={count++} />);
|
||||
|
||||
// @ts-ignore
|
||||
last = FIND_NEWLINES.lastIndex;
|
||||
match = FIND_NEWLINES.exec(text);
|
||||
}
|
||||
|
||||
if (last < text.length) {
|
||||
results.push(renderNonNewLine({ text: text.slice(last), key: count++, convoId }));
|
||||
}
|
||||
|
||||
return results;
|
||||
return renderNonNewLine({ text, key: 0, convoId });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,8 +7,8 @@ import { SessionIconButton, SessionIconSize, SessionIconType } from '../session/
|
|||
import { SessionButton, SessionButtonColor, SessionButtonType } from '../session/SessionButton';
|
||||
import { ConversationAvatar } from '../session/usingClosedConversationDetails';
|
||||
import { MemoConversationHeaderMenu } from '../session/menu/ConversationHeaderMenu';
|
||||
import { contextMenu, theme } from 'react-contexify';
|
||||
import styled, { ThemeProvider, useTheme } from 'styled-components';
|
||||
import { contextMenu } from 'react-contexify';
|
||||
import styled, { useTheme } from 'styled-components';
|
||||
import { ConversationNotificationSettingType } from '../../models/conversation';
|
||||
import {
|
||||
getConversationHeaderProps,
|
||||
|
@ -29,7 +29,6 @@ import {
|
|||
openRightPanel,
|
||||
resetSelectedMessageIds,
|
||||
} from '../../state/ducks/conversations';
|
||||
import { getTheme } from '../../state/selectors/theme';
|
||||
|
||||
export interface TimerOption {
|
||||
name: string;
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import React from 'react';
|
||||
import { compact, flatten } from 'lodash';
|
||||
import { flatten } from 'lodash';
|
||||
|
||||
import { Intl } from '../Intl';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import {
|
||||
PropsForGroupUpdate,
|
||||
PropsForGroupUpdateAdd,
|
||||
|
|
|
@ -6,6 +6,7 @@ import { RenderTextCallbackType } from '../../types/Util';
|
|||
import { isLinkSneaky } from '../../../js/modules/link_previews';
|
||||
import { updateConfirmModal } from '../../state/ducks/modalDialog';
|
||||
import { shell } from 'electron';
|
||||
import { MessageInteraction } from '../../interactions';
|
||||
|
||||
const linkify = LinkifyIt();
|
||||
|
||||
|
@ -85,10 +86,15 @@ export class Linkify extends React.Component<Props> {
|
|||
title: window.i18n('linkVisitWarningTitle'),
|
||||
message: window.i18n('linkVisitWarningMessage', url),
|
||||
okText: window.i18n('open'),
|
||||
cancelText: window.i18n('copy'),
|
||||
onClickOk: openLink,
|
||||
onClickClose: () => {
|
||||
window.inboxStore?.dispatch(updateConfirmModal(null));
|
||||
},
|
||||
|
||||
onClickCancel: () => {
|
||||
MessageInteraction.copyBodyToClipboard(url);
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -600,6 +600,7 @@ class MessageInner extends React.PureComponent<Props, State> {
|
|||
selectedMessages,
|
||||
receivedAt,
|
||||
isUnread,
|
||||
text,
|
||||
} = this.props;
|
||||
const { expired, expiring } = this.state;
|
||||
|
||||
|
@ -628,6 +629,10 @@ class MessageInner extends React.PureComponent<Props, State> {
|
|||
|
||||
const isIncoming = direction === 'incoming';
|
||||
|
||||
const hasText = Boolean(text);
|
||||
|
||||
const bgShouldBeTransparent = isShowingImage && !hasText;
|
||||
|
||||
return (
|
||||
<ReadableMessage
|
||||
messageId={messageId}
|
||||
|
@ -653,7 +658,7 @@ class MessageInner extends React.PureComponent<Props, State> {
|
|||
className={classNames(
|
||||
'module-message__container',
|
||||
`module-message__container--${direction}`,
|
||||
isShowingImage
|
||||
bgShouldBeTransparent
|
||||
? `module-message__container--${direction}--transparent`
|
||||
: `module-message__container--${direction}--opaque`
|
||||
)}
|
||||
|
|
|
@ -79,7 +79,18 @@ export class MessageBody extends React.Component<Props> {
|
|||
};
|
||||
|
||||
public renderJsxSelectable(jsx: JSX.Element): JSX.Element {
|
||||
return <span className="text-selectable">{jsx}</span>;
|
||||
return (
|
||||
<span
|
||||
className="text-selectable"
|
||||
onDragStart={(e: any) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
{jsx}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import { AttachmentTypeWithPath } from '../../types/Attachment';
|
||||
import _ from 'lodash';
|
||||
import { animation, Item, Menu } from 'react-contexify';
|
||||
|
||||
import { MessageInteraction } from '../../interactions';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// tslint:disable:react-this-binding-issue
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import * as MIME from '../../../ts/types/MIME';
|
||||
|
@ -309,16 +309,8 @@ export const QuoteReferenceWarning = (props: any) => {
|
|||
};
|
||||
|
||||
export const Quote = (props: QuotePropsWithListener) => {
|
||||
const [_imageBroken, setImageBroken] = useState(false);
|
||||
|
||||
const handleImageErrorBound = null;
|
||||
|
||||
const handleImageError = () => {
|
||||
// tslint:disable-next-line no-console
|
||||
console.log('Message: Image failed to load; failing over to placeholder');
|
||||
setImageBroken(true);
|
||||
};
|
||||
|
||||
const { isIncoming, onClick, referencedMessageNotFound, withContentAbove } = props;
|
||||
|
||||
if (!validateQuote(props)) {
|
||||
|
|
|
@ -2,11 +2,8 @@ import _, { noop } from 'lodash';
|
|||
import React, { useCallback } from 'react';
|
||||
import { InView } from 'react-intersection-observer';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
// tslint:disable-next-line: no-submodule-imports
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import { getMessageById } from '../../data/data';
|
||||
import { useAppIsFocused } from '../../hooks/useAppFocused';
|
||||
import { MessageModelType } from '../../models/messageType';
|
||||
import { Constants } from '../../session';
|
||||
import { getConversationController } from '../../session/conversations';
|
||||
import {
|
||||
|
|
|
@ -43,7 +43,7 @@ const TimestampContainerNotListItem = styled(props => <OpacityMetadataComponent
|
|||
`;
|
||||
|
||||
export const Timestamp = (props: Props) => {
|
||||
const [lastUpdated, setLastUpdated] = useState(Date.now());
|
||||
const [_lastUpdated, setLastUpdated] = useState(Date.now());
|
||||
// this is kind of a hack, but we use lastUpdated just to trigger a refresh.
|
||||
// formatRelativeTime() will print the correct moment.
|
||||
const update = () => {
|
||||
|
@ -54,8 +54,7 @@ export const Timestamp = (props: Props) => {
|
|||
|
||||
useInterval(update, UPDATE_FREQUENCY);
|
||||
|
||||
const { module, timestamp, withImageNoCaption, extended } = props;
|
||||
const moduleName = module || 'module-timestamp';
|
||||
const { timestamp, withImageNoCaption, extended } = props;
|
||||
|
||||
if (timestamp === null || timestamp === undefined) {
|
||||
return null;
|
||||
|
|
|
@ -1,40 +1,29 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface Props {
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export class TypingAnimation extends React.Component<Props> {
|
||||
public render() {
|
||||
const { color } = this.props;
|
||||
|
||||
return (
|
||||
<div className="module-typing-animation" title={window.i18n('typingAlt')}>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-typing-animation__dot',
|
||||
'module-typing-animation__dot--first',
|
||||
color ? `module-typing-animation__dot--${color}` : null
|
||||
)}
|
||||
/>
|
||||
<div className="module-typing-animation__spacer" />
|
||||
<div
|
||||
className={classNames(
|
||||
'module-typing-animation__dot',
|
||||
'module-typing-animation__dot--second',
|
||||
color ? `module-typing-animation__dot--${color}` : null
|
||||
)}
|
||||
/>
|
||||
<div className="module-typing-animation__spacer" />
|
||||
<div
|
||||
className={classNames(
|
||||
'module-typing-animation__dot',
|
||||
'module-typing-animation__dot--third',
|
||||
color ? `module-typing-animation__dot--${color}` : null
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export const TypingAnimation = () => {
|
||||
return (
|
||||
<div className="module-typing-animation" title={window.i18n('typingAlt')}>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-typing-animation__dot',
|
||||
'module-typing-animation__dot--first'
|
||||
)}
|
||||
/>
|
||||
<div className="module-typing-animation__spacer" />
|
||||
<div
|
||||
className={classNames(
|
||||
'module-typing-animation__dot',
|
||||
'module-typing-animation__dot--second'
|
||||
)}
|
||||
/>
|
||||
<div className="module-typing-animation__spacer" />
|
||||
<div
|
||||
className={classNames(
|
||||
'module-typing-animation__dot',
|
||||
'module-typing-animation__dot--third'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -29,6 +29,10 @@ export const TypingBubble = (props: TypingBubbleProps) => {
|
|||
return <></>;
|
||||
}
|
||||
|
||||
if (!props.isTyping) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TypingBubbleContainer {...props}>
|
||||
<TypingAnimation />
|
||||
|
|
|
@ -4,8 +4,6 @@ import { DocumentListItem } from './DocumentListItem';
|
|||
import { MediaGridItem } from './MediaGridItem';
|
||||
import { MediaItemType } from '../../LightboxGallery';
|
||||
import { missingCaseError } from '../../../util/missingCaseError';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getSelectedConversationKey } from '../../../state/selectors/conversations';
|
||||
|
||||
type Props = {
|
||||
type: 'media' | 'documents';
|
||||
|
|
|
@ -4,9 +4,6 @@ import classNames from 'classnames';
|
|||
import moment from 'moment';
|
||||
// tslint:disable-next-line:match-default-export-name
|
||||
import formatFileSize from 'filesize';
|
||||
import { getDecryptedMediaUrl } from '../../../session/crypto/DecryptedAttachmentsManager';
|
||||
import { sendDataExtractionNotification } from '../../../session/messages/outgoing/controlMessage/DataExtractionNotificationMessage';
|
||||
import { AttachmentTypeWithPath, save } from '../../../types/Attachment';
|
||||
import { MediaItemType } from '../../LightboxGallery';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getSelectedConversationKey } from '../../../state/selectors/conversations';
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type Props = {
|
||||
date: string;
|
||||
direction: string;
|
||||
withImageNoCaption: boolean;
|
||||
};
|
||||
|
||||
export const MetadataDate = (props: Props) => {
|
||||
return <></>;
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import styled, { DefaultTheme, useTheme } from 'styled-components';
|
||||
import styled, { useTheme } from 'styled-components';
|
||||
import { MessageDeliveryStatus } from '../../../models/messageType';
|
||||
import { SessionIcon, SessionIconSize, SessionIconType } from '../../session/icon';
|
||||
import { OpacityMetadataComponent } from './MessageMetadata';
|
||||
|
|
|
@ -16,7 +16,7 @@ export const AdminLeaveClosedGroupDialog = (props: Props) => {
|
|||
const warningAsAdmin = `${window.i18n('leaveGroupConfirmationAdmin')}`;
|
||||
const okText = window.i18n('leaveAndRemoveForEveryone');
|
||||
const cancelText = window.i18n('cancel');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [_isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const onClickOK = async () => {
|
||||
setIsLoading(true);
|
||||
|
|
|
@ -2,7 +2,6 @@ import React, { useState } from 'react';
|
|||
|
||||
import { SessionButton, SessionButtonColor } from '../session/SessionButton';
|
||||
import { ContactType, SessionMemberListItem } from '../session/SessionMemberListItem';
|
||||
import { useTheme } from 'styled-components';
|
||||
import { getConversationController } from '../../session/conversations';
|
||||
import { ToastUtils, UserUtils } from '../../session/utils';
|
||||
import { initiateGroupUpdate } from '../../session/group';
|
||||
|
@ -22,7 +21,6 @@ type Props = {
|
|||
const InviteContactsDialogInner = (props: Props) => {
|
||||
const { conversationId } = props;
|
||||
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const convo = getConversationController().get(conversationId);
|
||||
|
|
|
@ -2,7 +2,6 @@ import React, { useState } from 'react';
|
|||
import { SessionButton, SessionButtonColor, SessionButtonType } from '../session/SessionButton';
|
||||
import { PubKey } from '../../session/types';
|
||||
import { ToastUtils } from '../../session/utils';
|
||||
import { useTheme } from 'styled-components';
|
||||
import { SessionSpinner } from '../session/SessionSpinner';
|
||||
import { Flex } from '../basic/Flex';
|
||||
import { ApiV2 } from '../../opengroup/opengroupV2';
|
||||
|
@ -10,7 +9,6 @@ import { SessionWrapperModal } from '../session/SessionWrapperModal';
|
|||
import { getConversationController } from '../../session/conversations';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateAddModeratorsModal } from '../../state/ducks/modalDialog';
|
||||
import _ from 'lodash';
|
||||
|
||||
type Props = {
|
||||
conversationId: string;
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import React from 'react';
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import Electron from 'electron';
|
||||
const { shell } = Electron;
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ export interface SessionConfirmDialogProps {
|
|||
onClose?: any;
|
||||
onClickOk?: () => Promise<void> | void;
|
||||
onClickClose?: () => any;
|
||||
onClickCancel?: () => any;
|
||||
okText?: string;
|
||||
cancelText?: string;
|
||||
hideCancel?: boolean;
|
||||
|
@ -40,6 +41,7 @@ const SessionConfirmInner = (props: SessionConfirmDialogProps) => {
|
|||
sessionIcon,
|
||||
iconSize,
|
||||
shouldShowConfirm,
|
||||
onClickCancel,
|
||||
} = props;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
@ -75,6 +77,10 @@ const SessionConfirmInner = (props: SessionConfirmDialogProps) => {
|
|||
* Performs specified on close action then removes the modal.
|
||||
*/
|
||||
const onClickCancelHandler = () => {
|
||||
if (onClickCancel) {
|
||||
onClickCancel();
|
||||
}
|
||||
|
||||
if (onClickClose) {
|
||||
onClickClose();
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { SessionButton, SessionButtonColor, SessionButtonType } from '../session/SessionButton';
|
||||
import { SessionButton, SessionButtonColor } from '../session/SessionButton';
|
||||
import { ContactType, SessionMemberListItem } from '../session/SessionMemberListItem';
|
||||
import { ToastUtils, UserUtils } from '../../session/utils';
|
||||
import autoBind from 'auto-bind';
|
||||
|
@ -10,7 +10,7 @@ import { getConversationController } from '../../session/conversations';
|
|||
import _ from 'lodash';
|
||||
import { SpacerLG, SpacerMD, Text } from '../basic/Text';
|
||||
import { SessionWrapperModal } from '../session/SessionWrapperModal';
|
||||
import { ConversationModel, ConversationTypeEnum } from '../../models/conversation';
|
||||
import { ConversationModel } from '../../models/conversation';
|
||||
import { updateGroupMembersModal } from '../../state/ducks/modalDialog';
|
||||
import { ClosedGroup } from '../../session';
|
||||
|
||||
|
@ -130,9 +130,7 @@ export class UpdateGroupMembersDialog extends React.Component<Props, State> {
|
|||
'error-message',
|
||||
this.state.errorDisplayed ? 'error-shown' : 'error-faded'
|
||||
);
|
||||
const existingZombies = this.convo.get('zombies') || [];
|
||||
|
||||
const hasZombies = Boolean(existingZombies.length);
|
||||
const okText = window.i18n('ok');
|
||||
const cancelText = window.i18n('cancel');
|
||||
const titleText = window.i18n('updateGroupDialogTitle', this.convo.getName());
|
||||
|
@ -286,7 +284,7 @@ export class UpdateGroupMembersDialog extends React.Component<Props, State> {
|
|||
});
|
||||
}
|
||||
|
||||
private onZombieClicked(selected: ContactType) {
|
||||
private onZombieClicked(_selected: ContactType) {
|
||||
if (!this.state.isAdmin) {
|
||||
ToastUtils.pushOnlyAdminCanRemove();
|
||||
return;
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import React, { Dispatch, useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { SessionIconButton, SessionIconSize, SessionIconType } from './icon';
|
||||
import { Avatar, AvatarSize } from '../Avatar';
|
||||
import { darkTheme, lightTheme } from '../../state/ducks/SessionTheme';
|
||||
import { SessionToastContainer } from './SessionToastContainer';
|
||||
import { getConversationController } from '../../session/conversations';
|
||||
import { UserUtils } from '../../session/utils';
|
||||
import { syncConfigurationIfNeeded } from '../../session/utils/syncUtils';
|
||||
|
||||
import {
|
||||
createOrUpdateItem,
|
||||
generateAttachmentKeyIfEmpty,
|
||||
getAllOpenGroupV1Conversations,
|
||||
getItemById,
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
} from '../../state/ducks/conversations';
|
||||
import { SearchResults, SearchResultsProps } from '../SearchResults';
|
||||
import { SessionSearchInput } from './SessionSearchInput';
|
||||
import _, { debounce } from 'lodash';
|
||||
import { cleanSearchTerm } from '../../util/cleanSearchTerm';
|
||||
import { RowRendererParamsType } from '../LeftPane';
|
||||
import { SessionClosableOverlay, SessionClosableOverlayType } from './SessionClosableOverlay';
|
||||
|
@ -29,7 +28,7 @@ import autoBind from 'auto-bind';
|
|||
import { onsNameRegex } from '../../session/snode_api/SNodeAPI';
|
||||
import { SNodeAPI } from '../../session/snode_api';
|
||||
import { clearSearch, search, updateSearchTerm } from '../../state/ducks/search';
|
||||
import { getFirstUnreadMessageIdInConversation } from '../../data/data';
|
||||
import _ from 'lodash';
|
||||
|
||||
export interface Props {
|
||||
searchTerm: string;
|
||||
|
@ -70,7 +69,7 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
|
|||
};
|
||||
|
||||
autoBind(this);
|
||||
this.debouncedSearch = debounce(this.search.bind(this), 20);
|
||||
this.debouncedSearch = _.debounce(this.search.bind(this), 20);
|
||||
}
|
||||
|
||||
public renderRow = ({ index, key, style }: RowRendererParamsType): JSX.Element => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { SessionIcon, SessionIconSize, SessionIconType } from './icon';
|
||||
import { DefaultTheme, useTheme } from 'styled-components';
|
||||
import { useTheme } from 'styled-components';
|
||||
import { SessionButton } from './SessionButton';
|
||||
|
||||
const Tab = ({
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
import React, { useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { SessionButton, SessionButtonColor, SessionButtonType } from './SessionButton';
|
||||
|
||||
import { SessionIcon, SessionIconSize, SessionIconType } from './icon';
|
||||
import { SessionSettingCategory } from './settings/SessionSettings';
|
||||
import { DefaultTheme } from 'styled-components';
|
||||
import { LeftPaneSectionHeader } from './LeftPaneSectionHeader';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { showSettingsSection } from '../../state/ducks/section';
|
||||
import { getFocusedSettingsSection } from '../../state/selectors/section';
|
||||
import { getTheme } from '../../state/selectors/theme';
|
||||
import { recoveryPhraseModal, updateDeleteAccountModal } from '../../state/ducks/modalDialog';
|
||||
import React from 'react';
|
||||
|
||||
const getCategories = () => {
|
||||
return [
|
||||
|
|
|
@ -7,7 +7,6 @@ import { ContactType, SessionMemberListItem } from './SessionMemberListItem';
|
|||
import { ReduxConversationType } from '../../state/ducks/conversations';
|
||||
import { SessionButton, SessionButtonColor, SessionButtonType } from './SessionButton';
|
||||
import { SessionSpinner } from './SessionSpinner';
|
||||
import { DefaultTheme } from 'styled-components';
|
||||
import { ConversationTypeEnum } from '../../models/conversation';
|
||||
import { SessionJoinableRooms } from './SessionJoinableDefaultRooms';
|
||||
import { SpacerLG, SpacerMD } from '../basic/Text';
|
||||
|
|
|
@ -25,7 +25,7 @@ import { SessionMainPanel } from '../SessionMainPanel';
|
|||
// tslint:disable-next-line: no-submodule-imports
|
||||
import { PersistGate } from 'redux-persist/integration/react';
|
||||
import { persistStore } from 'redux-persist';
|
||||
import { TimerOptionsArray, TimerOptionsState } from '../../state/ducks/timerOptions';
|
||||
import { TimerOptionsArray } from '../../state/ducks/timerOptions';
|
||||
|
||||
// Workaround: A react component's required properties are filtering up through connect()
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||
|
|
|
@ -1,117 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Constants } from '../../session';
|
||||
|
||||
interface Props {
|
||||
// Value ranges from 0 to 100
|
||||
value: number;
|
||||
// Optional. Load with initial value and have
|
||||
// it shoot to new value immediately
|
||||
prevValue?: number;
|
||||
sendStatus: -1 | 0 | 1 | 2;
|
||||
visible: boolean;
|
||||
showOnComplete: boolean;
|
||||
resetProgress: any;
|
||||
}
|
||||
|
||||
interface State {
|
||||
show: boolean;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export class SessionProgress extends React.PureComponent<Props, State> {
|
||||
public static defaultProps = {
|
||||
showOnComplete: true,
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
const { visible } = this.props;
|
||||
|
||||
this.state = {
|
||||
show: true,
|
||||
visible,
|
||||
};
|
||||
|
||||
this.onComplete = this.onComplete.bind(this);
|
||||
}
|
||||
|
||||
public componentWillReceiveProps() {
|
||||
// Reset show for each reset
|
||||
this.setState({ show: true });
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { value, prevValue, sendStatus } = this.props;
|
||||
const { show } = this.state;
|
||||
|
||||
// Duration will be the decimal (in seconds) of
|
||||
// the percentage differnce, else 0.25s;
|
||||
// Minimum shift duration of 0.25s;
|
||||
|
||||
// 1. Width depends on progress.
|
||||
// 2. Transition duration scales with the
|
||||
// distance it needs to travel
|
||||
|
||||
const successColor = Constants.UI.COLORS.GREEN;
|
||||
const failureColor = Constants.UI.COLORS.DANGER_ALT;
|
||||
const backgroundColor = sendStatus === -1 ? failureColor : successColor;
|
||||
|
||||
const shiftDurationMs = this.getShiftDuration(this.props.value, prevValue) * 1000;
|
||||
const showDurationMs = 500;
|
||||
const showOffsetMs = shiftDurationMs + 500;
|
||||
|
||||
const willComplete = value >= 100;
|
||||
if (willComplete && !show) {
|
||||
setTimeout(this.onComplete, shiftDurationMs);
|
||||
}
|
||||
|
||||
const style = {
|
||||
'background-color': backgroundColor,
|
||||
transform: `translateX(-${100 - value}%)`,
|
||||
'transition-property': 'transform',
|
||||
// 'transition-property': 'transform, opacity',
|
||||
'transition-duration': `${shiftDurationMs}ms`,
|
||||
// 'transition-duration': `${shiftDurationMs}ms, ${showDurationMs}ms`,
|
||||
'transition-delay': '0ms',
|
||||
// 'transition-delay': `0ms, ${showOffsetMs}ms`,
|
||||
'transition-timing-funtion': 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
//'transition-timing-funtion':'cubic-bezier(0.25, 0.46, 0.45, 0.94), linear',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="session-progress">
|
||||
{show && (
|
||||
<div className="session-progress__progress" style={style}>
|
||||
 
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public onComplete() {
|
||||
if (!this.state.show) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ show: false }, () => {
|
||||
setTimeout(() => this.props.resetProgress(), 2000);
|
||||
});
|
||||
}
|
||||
|
||||
private getShiftDuration(value: number, prevValue?: number) {
|
||||
// Generates a shift duration which is based upon the distance requred to travel.
|
||||
// Follows the curve of y = (1-c)*sqrt(x) + c
|
||||
// Input values are between 0 and 100.
|
||||
// Max time = 1.0s.
|
||||
|
||||
const minTime = 0.25;
|
||||
if (!prevValue) {
|
||||
return minTime;
|
||||
}
|
||||
|
||||
const distance = Math.abs(value - prevValue) / 100;
|
||||
return (1 - minTime) * Math.sqrt(distance) + minTime;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import { animation, contextMenu, Item, Menu } from 'react-contexify';
|
||||
import { DefaultTheme } from 'styled-components';
|
||||
import { SessionIconButton, SessionIconSize, SessionIconType } from './icon';
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -4,7 +4,6 @@ import { UserSearchResults } from '../UserSearchResults';
|
|||
import { SessionSearchInput } from './SessionSearchInput';
|
||||
|
||||
import { SearchResultsProps } from '../SearchResults';
|
||||
import { DefaultTheme } from 'styled-components';
|
||||
import autoBind from 'auto-bind';
|
||||
|
||||
export interface Props {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import _, { debounce, update } from 'lodash';
|
||||
import _, { debounce } from 'lodash';
|
||||
|
||||
import { Attachment, AttachmentType } from '../../../types/Attachment';
|
||||
import * as MIME from '../../../types/MIME';
|
||||
|
@ -551,6 +551,11 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
// we try to match the first link found in the current message
|
||||
const links = window.Signal.LinkPreviews.findLinks(this.props.draft, undefined);
|
||||
if (!links || links.length === 0 || ignoredLink === links[0]) {
|
||||
if (this.state.stagedLinkPreview) {
|
||||
this.setState({
|
||||
stagedLinkPreview: undefined,
|
||||
});
|
||||
}
|
||||
return <></>;
|
||||
}
|
||||
const firstLink = links[0];
|
||||
|
@ -769,7 +774,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
private async onKeyUp(event: any) {
|
||||
private async onKeyUp() {
|
||||
const { draft } = this.props;
|
||||
// Called whenever the user changes the message composition field. But only
|
||||
// fires if there's content in the message field after the change.
|
||||
|
|
|
@ -14,7 +14,7 @@ import styled, { DefaultTheme } from 'styled-components';
|
|||
import { SessionMessagesListContainer } from './SessionMessagesListContainer';
|
||||
import { LightboxGallery, MediaItemType } from '../../LightboxGallery';
|
||||
|
||||
import { AttachmentType, AttachmentTypeWithPath, save } from '../../../types/Attachment';
|
||||
import { AttachmentType, AttachmentTypeWithPath } from '../../../types/Attachment';
|
||||
import { ToastUtils, UserUtils } from '../../../session/utils';
|
||||
import * as MIME from '../../../types/MIME';
|
||||
import { SessionFileDropzone } from './SessionFileDropzone';
|
||||
|
@ -112,7 +112,7 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
// ~~~~~~~~~~~~~~~~ LIFECYCLES ~~~~~~~~~~~~~~~~
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
public componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
public componentDidUpdate(prevProps: Props, _prevState: State) {
|
||||
const {
|
||||
selectedConversationKey: newConversationKey,
|
||||
selectedConversation: newConversation,
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { SessionScrollButton } from '../SessionScrollButton';
|
||||
import _ from 'lodash';
|
||||
import { contextMenu } from 'react-contexify';
|
||||
import {
|
||||
quotedMessageToAnimate,
|
||||
|
|
|
@ -6,7 +6,6 @@ import { SessionDropdown } from '../SessionDropdown';
|
|||
import { MediaGallery } from '../../conversation/media-gallery/MediaGallery';
|
||||
import _ from 'lodash';
|
||||
import { Constants } from '../../../session';
|
||||
import { ConversationAvatar } from '../usingClosedConversationDetails';
|
||||
import { AttachmentTypeWithPath } from '../../../types/Attachment';
|
||||
import { useTheme } from 'styled-components';
|
||||
import {
|
||||
|
@ -120,7 +119,6 @@ const HeaderItem = () => {
|
|||
}
|
||||
const {
|
||||
avatarPath,
|
||||
isPublic,
|
||||
id,
|
||||
isGroup,
|
||||
isKickedFromGroup,
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import React from 'react';
|
||||
import { icons, SessionIconSize, SessionIconType } from '../icon';
|
||||
import styled, { css, DefaultTheme, keyframes, useTheme } from 'styled-components';
|
||||
import _ from 'lodash';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getTheme } from '../../../state/selectors/theme';
|
||||
import { lightTheme } from '../../../state/ducks/SessionTheme';
|
||||
|
||||
export type SessionIconProps = {
|
||||
|
|
|
@ -3,6 +3,7 @@ import classNames from 'classnames';
|
|||
import { SessionIcon, SessionIconProps } from '../icon';
|
||||
import { SessionNotificationCount } from '../SessionNotificationCount';
|
||||
import { DefaultTheme, useTheme } from 'styled-components';
|
||||
import _ from 'lodash';
|
||||
|
||||
interface SProps extends SessionIconProps {
|
||||
onClick?: any;
|
||||
|
@ -12,7 +13,7 @@ interface SProps extends SessionIconProps {
|
|||
isHidden?: boolean;
|
||||
}
|
||||
|
||||
export const SessionIconButton = (props: SProps) => {
|
||||
const SessionIconButtonInner = (props: SProps) => {
|
||||
const {
|
||||
iconType,
|
||||
iconSize,
|
||||
|
@ -56,3 +57,5 @@ export const SessionIconButton = (props: SProps) => {
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SessionIconButton = React.memo(SessionIconButtonInner, _.isEqual);
|
||||
|
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
|||
|
||||
import { getNumberOfPinnedConversations } from '../../../state/selectors/conversations';
|
||||
import { getFocusedSection } from '../../../state/selectors/section';
|
||||
import { TimerOption } from '../../conversation/ConversationHeader';
|
||||
import { Item, Submenu } from 'react-contexify';
|
||||
import { ConversationNotificationSettingType } from '../../../models/conversation';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
@ -324,7 +323,7 @@ export function getDisappearingMenuItem(
|
|||
Boolean(isBlocked)
|
||||
)
|
||||
) {
|
||||
const isRtlMode = isRtlBody();
|
||||
// const isRtlMode = isRtlBody();
|
||||
|
||||
return (
|
||||
// Remove the && false to make context menu work with RTL support
|
||||
|
|
|
@ -66,7 +66,7 @@ export const SessionSettingListItem = (props: Props) => {
|
|||
|
||||
{type === SessionSettingType.Options && (
|
||||
<SessionRadioGroup
|
||||
initialItem={content.options.initalItem}
|
||||
initialItem={content.options.initialItem}
|
||||
group={content.options.group}
|
||||
items={content.options.items}
|
||||
onClick={(selectedRadioValue: string) => {
|
||||
|
|
|
@ -22,6 +22,7 @@ import { toggleAudioAutoplay } from '../../../state/ducks/userConfig';
|
|||
import { sessionPassword } from '../../../state/ducks/modalDialog';
|
||||
import { PasswordAction } from '../../dialog/SessionPasswordDialog';
|
||||
import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon';
|
||||
import { ToastUtils } from '../../../session/utils';
|
||||
|
||||
export enum SessionSettingCategory {
|
||||
Appearance = 'appearance',
|
||||
|
@ -368,6 +369,33 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
|
|||
okTheme: SessionButtonColor.Danger,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: 'start-in-tray-setting',
|
||||
title: window.i18n('startInTrayTitle'),
|
||||
description: window.i18n('startInTrayDescription'),
|
||||
hidden: false,
|
||||
type: SessionSettingType.Toggle,
|
||||
category: SessionSettingCategory.Appearance,
|
||||
setFn: async () => {
|
||||
try {
|
||||
const newValue = !(await window.getStartInTray());
|
||||
|
||||
// make sure to write it here too, as this is the value used on the UI to mark the toggle as true/false
|
||||
window.setSettingValue('start-in-tray-setting', newValue);
|
||||
await window.setStartInTray(newValue);
|
||||
if (!newValue) {
|
||||
ToastUtils.pushRestartNeeded();
|
||||
}
|
||||
} catch (e) {
|
||||
window.log.warn('start in tray change error:', e);
|
||||
}
|
||||
},
|
||||
content: undefined,
|
||||
comparisonValue: undefined,
|
||||
onClick: undefined,
|
||||
confirmationDialogParams: undefined,
|
||||
},
|
||||
{
|
||||
id: 'audio-message-autoplay-setting',
|
||||
title: window.i18n('audioMessageAutoplayTitle'),
|
||||
|
@ -385,6 +413,7 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
|
|||
onClick: undefined,
|
||||
confirmationDialogParams: undefined,
|
||||
},
|
||||
|
||||
{
|
||||
id: 'notification-setting',
|
||||
title: window.i18n('notificationSettingsDialog'),
|
||||
|
@ -400,7 +429,7 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> {
|
|||
content: {
|
||||
options: {
|
||||
group: 'notification-setting',
|
||||
initalItem: window.getSettingValue('notification-setting') || 'message',
|
||||
initialItem: window.getSettingValue('notification-setting') || 'message',
|
||||
items: [
|
||||
{
|
||||
label: window.i18n('nameAndMessage'),
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import { SettingsViewProps } from './SessionSettings';
|
||||
import { DefaultTheme, withTheme } from 'styled-components';
|
||||
|
||||
interface Props extends SettingsViewProps {
|
||||
// tslint:disable-next-line: react-unused-props-and-state
|
||||
|
|
|
@ -171,7 +171,7 @@ export function init() {
|
|||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on(`${SQL_CHANNEL_KEY}-done`, (event, jobId, errorForDisplay, result) => {
|
||||
ipcRenderer.on(`${SQL_CHANNEL_KEY}-done`, (_event, jobId, errorForDisplay, result) => {
|
||||
const job = _getJob(jobId);
|
||||
if (!job) {
|
||||
throw new Error(
|
||||
|
@ -892,7 +892,7 @@ export async function removeOtherData(): Promise<void> {
|
|||
async function callChannel(name: string): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer.send(name);
|
||||
ipcRenderer.once(`${name}-done`, (event, error) => {
|
||||
ipcRenderer.once(`${name}-done`, (_event, error) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
|
|
|
@ -36,12 +36,11 @@ import {
|
|||
conversationReset,
|
||||
quoteMessage,
|
||||
resetSelectedMessageIds,
|
||||
SortedMessageModelProps,
|
||||
} from '../state/ducks/conversations';
|
||||
import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsManager';
|
||||
import { IMAGE_JPEG } from '../types/MIME';
|
||||
import { FSv2 } from '../fileserver';
|
||||
import { fromBase64ToArray, fromHexToArray, toHex } from '../session/utils/String';
|
||||
import { fromHexToArray, toHex } from '../session/utils/String';
|
||||
import { SessionButtonColor } from '../components/session/SessionButton';
|
||||
import { perfEnd, perfStart } from '../session/utils/Performance';
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import _ from 'lodash';
|
||||
import { getV2OpenGroupRoom } from '../data/opengroups';
|
||||
import { ConversationModel } from '../models/conversation';
|
||||
import { ApiV2 } from '../opengroup/opengroupV2';
|
||||
import { joinOpenGroupV2WithUIEvents } from '../opengroup/opengroupV2/JoinOpenGroupV2';
|
||||
import { isOpenGroupV2, openGroupV2CompleteURLRegex } from '../opengroup/utils/OpenGroupUtils';
|
||||
|
@ -130,13 +129,6 @@ export function copyBodyToClipboard(body?: string | null) {
|
|||
ToastUtils.pushCopiedToClipBoard();
|
||||
}
|
||||
|
||||
export function copyPubKey(sender: string) {
|
||||
// this.getSource return out pubkey if this is an outgoing message, or the sender pubkey
|
||||
window.clipboard.writeText();
|
||||
|
||||
ToastUtils.pushCopiedToClipBoard();
|
||||
}
|
||||
|
||||
export async function removeSenderFromModerator(sender: string, convoId: string) {
|
||||
try {
|
||||
const pubKeyToRemove = PubKey.cast(sender);
|
||||
|
|
|
@ -44,6 +44,7 @@ import { getOpenGroupV2FromConversationId } from '../opengroup/utils/OpenGroupUt
|
|||
import { createTaskWithTimeout } from '../session/utils/TaskWithTimeout';
|
||||
import { perfEnd, perfStart } from '../session/utils/Performance';
|
||||
import { ReplyingToMessageProps } from '../components/session/conversation/SessionCompositionBox';
|
||||
import { ed25519Str } from '../session/onions/onionPath';
|
||||
|
||||
export enum ConversationTypeEnum {
|
||||
GROUP = 'group',
|
||||
|
@ -231,7 +232,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
|
|||
return `opengroup(${this.id})`;
|
||||
}
|
||||
|
||||
return `group(${this.id})`;
|
||||
return `group(${ed25519Str(this.id)})`;
|
||||
}
|
||||
|
||||
public isMe() {
|
||||
|
@ -389,7 +390,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
|
|||
}
|
||||
}
|
||||
|
||||
public async onExpired(message: MessageModel) {
|
||||
public async onExpired(_message: MessageModel) {
|
||||
await this.updateLastMessage();
|
||||
|
||||
// removeMessage();
|
||||
|
@ -997,10 +998,10 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
|
|||
hasErrors: Boolean(errors && errors.length),
|
||||
});
|
||||
}
|
||||
|
||||
const oldUnreadNowReadAttrs = oldUnreadNowRead.map(m => m.attributes);
|
||||
|
||||
await saveMessages(oldUnreadNowReadAttrs);
|
||||
if (oldUnreadNowReadAttrs?.length) {
|
||||
await saveMessages(oldUnreadNowReadAttrs);
|
||||
}
|
||||
const allProps: Array<MessageModelProps> = [];
|
||||
|
||||
for (const nowRead of oldUnreadNowRead) {
|
||||
|
@ -1206,8 +1207,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
|
|||
* @param profileKey MUST be a hex string
|
||||
*/
|
||||
public async setProfileKey(profileKey?: Uint8Array, autoCommit = true) {
|
||||
const re = /[0-9A-Fa-f]*/g;
|
||||
|
||||
if (!profileKey) {
|
||||
return;
|
||||
}
|
||||
|
@ -1496,7 +1495,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
|
|||
}
|
||||
}
|
||||
|
||||
public async clearContactTypingTimer(sender: string) {
|
||||
public async clearContactTypingTimer(_sender: string) {
|
||||
if (!!this.typingTimer) {
|
||||
global.clearTimeout(this.typingTimer);
|
||||
this.typingTimer = null;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import Backbone from 'backbone';
|
||||
// tslint:disable-next-line: match-default-export-name
|
||||
import filesize from 'filesize';
|
||||
import _, { noop } from 'lodash';
|
||||
import { SignalService } from '../../ts/protobuf';
|
||||
import { getMessageQueue, Utils } from '../../ts/session';
|
||||
import { getConversationController } from '../../ts/session/conversations';
|
||||
|
@ -22,7 +21,6 @@ import autoBind from 'auto-bind';
|
|||
import { saveMessage } from '../../ts/data/data';
|
||||
import { ConversationModel, ConversationTypeEnum } from './conversation';
|
||||
import {
|
||||
actions as conversationActions,
|
||||
FindAndFormatContactType,
|
||||
LastMessageStatusType,
|
||||
MessageModelProps,
|
||||
|
@ -44,7 +42,6 @@ import {
|
|||
import { VisibleMessage } from '../session/messages/outgoing/visibleMessage/VisibleMessage';
|
||||
import { buildSyncMessage } from '../session/utils/syncUtils';
|
||||
import { isOpenGroupV2 } from '../opengroup/utils/OpenGroupUtils';
|
||||
import { MessageInteraction } from '../interactions';
|
||||
import {
|
||||
uploadAttachmentsV2,
|
||||
uploadLinkPreviewsV2,
|
||||
|
@ -56,6 +53,7 @@ import { getMessageController } from '../session/messages';
|
|||
import { isUsFromCache } from '../session/utils/User';
|
||||
import { perfEnd, perfStart } from '../session/utils/Performance';
|
||||
import { AttachmentTypeWithPath } from '../types/Attachment';
|
||||
import _ from 'lodash';
|
||||
|
||||
export class MessageModel extends Backbone.Model<MessageAttributes> {
|
||||
constructor(attributes: MessageAttributesOptionals & { skipTimerInit?: boolean }) {
|
||||
|
@ -651,7 +649,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
|
|||
});
|
||||
}
|
||||
|
||||
public getPropsForQuote(options: any = {}) {
|
||||
public getPropsForQuote(_options: any = {}) {
|
||||
const quote = this.get('quote');
|
||||
|
||||
if (!quote) {
|
||||
|
@ -787,11 +785,6 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
|
|||
return toRet;
|
||||
}
|
||||
|
||||
public copyPubKey() {
|
||||
// this.getSource return out pubkey if this is an outgoing message, or the sender pubkey
|
||||
MessageInteraction.copyPubKey(this.getSource());
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads attachments, previews and quotes.
|
||||
*
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import {
|
||||
getV2OpenGroupRoomByRoomId,
|
||||
OpenGroupV2Room,
|
||||
saveV2OpenGroupRoom,
|
||||
} from '../../data/opengroups';
|
||||
import { getV2OpenGroupRoomByRoomId, saveV2OpenGroupRoom } from '../../data/opengroups';
|
||||
import { allowOnlyOneAtATime } from '../../session/utils/Promise';
|
||||
import { fromBase64ToArrayBuffer, toHex } from '../../session/utils/String';
|
||||
import { toHex } from '../../session/utils/String';
|
||||
import { getIdentityKeyPair, getOurPubKeyStrFromCache } from '../../session/utils/User';
|
||||
import { OpenGroupRequestCommonType, OpenGroupV2Request } from './ApiUtil';
|
||||
import { sendApiV2Request } from './OpenGroupAPIV2';
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import _ from 'lodash';
|
||||
import { FileServerV2Request } from '../../fileserver/FileServerApiV2';
|
||||
import { getSodium } from '../../session/crypto';
|
||||
import { PubKey } from '../../session/types';
|
||||
import { allowOnlyOneAtATime, sleepFor } from '../../session/utils/Promise';
|
||||
import { fromBase64ToArrayBuffer, fromHex, fromHexToArray } from '../../session/utils/String';
|
||||
import { allowOnlyOneAtATime } from '../../session/utils/Promise';
|
||||
import { updateDefaultRooms, updateDefaultRoomsInProgress } from '../../state/ducks/defaultRooms';
|
||||
import { getCompleteUrlFromRoom } from '../utils/OpenGroupUtils';
|
||||
import { parseOpenGroupV2 } from './JoinOpenGroupV2';
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue