Merge pull request #951 from loki-project/clearnet

Merge into Master; prep for v1.0.3
pull/1052/head v1.0.3
Mikunj Varsani 5 years ago committed by GitHub
commit f0bb328952
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -54,10 +54,12 @@ jobs:
- name: Build mac production binaries - name: Build mac production binaries
if: runner.os == 'macOS' if: runner.os == 'macOS'
run: $(yarn bin)/electron-builder --config.extraMetadata.environment=$SIGNAL_ENV --config.mac.bundleVersion=${{ github.ref }} --publish=never --config.directories.output=release run: |
source ./build/setup-mac-certificate.sh
$(yarn bin)/electron-builder --config.extraMetadata.environment=$SIGNAL_ENV --config.mac.bundleVersion=${{ github.ref }} --publish=never --config.directories.output=release
env: env:
CSC_LINK: ${{ secrets.MAC_CERTIFICATE }} MAC_CERTIFICATE: ${{ secrets.MAC_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }} MAC_CERTIFICATE_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }}
SIGNING_APPLE_ID: ${{ secrets.SIGNING_APPLE_ID }} SIGNING_APPLE_ID: ${{ secrets.SIGNING_APPLE_ID }}
SIGNING_APP_PASSWORD: ${{ secrets.SIGNING_APP_PASSWORD }} SIGNING_APP_PASSWORD: ${{ secrets.SIGNING_APP_PASSWORD }}
SIGNING_TEAM_ID: ${{ secrets.SIGNING_TEAM_ID }} SIGNING_TEAM_ID: ${{ secrets.SIGNING_TEAM_ID }}

@ -0,0 +1,67 @@
# This script will run tests anytime a pull request is added
name: Session Test
on:
pull_request:
branches:
- development
- clearnet
- github-actions
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [windows-2016, macos-latest, ubuntu-latest]
env:
SIGNAL_ENV: production
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- run: git config --global core.autocrlf false
- name: Checkout git repo
uses: actions/checkout@v1
- name: Install node
uses: actions/setup-node@v1
with:
node-version: 10.13.0
- name: Setup node for windows
if: runner.os == 'Windows'
run: |
npm install --global --production windows-build-tools@4.0.0
npm install --global node-gyp@latest
npm config set python python2.7
npm config set msvs_version 2015
- name: Install yarn
run: npm install yarn --no-save
- name: Install Dependencies
run: yarn install --frozen-lockfile
- name: Generate and concat files
run: yarn generate
- name: Lint Files
run: |
yarn format-full --list-different
yarn eslint
yarn tslint
- name: Make linux use en_US locale
if: runner.os == 'Linux'
run: |
sudo apt-get install -y hunspell-en-us
sudo locale-gen en_US.UTF-8
sudo dpkg-reconfigure locales
echo ::set-env name=DISPLAY:::9.0
echo ::set-env name=LANG::en_US.UTF-8
- name: Test
uses: GabrielBB/xvfb-action@v1.0
with:
run: yarn test

@ -51,10 +51,12 @@ jobs:
- name: Build mac production binaries - name: Build mac production binaries
if: runner.os == 'macOS' if: runner.os == 'macOS'
run: $(yarn bin)/electron-builder --config.extraMetadata.environment=$SIGNAL_ENV --config.mac.bundleVersion=${{ github.ref }} --publish=always run: |
source ./build/setup-mac-certificate.sh
$(yarn bin)/electron-builder --config.extraMetadata.environment=$SIGNAL_ENV --config.mac.bundleVersion=${{ github.ref }} --publish=always
env: env:
CSC_LINK: ${{ secrets.MAC_CERTIFICATE }} MAC_CERTIFICATE: ${{ secrets.MAC_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }} MAC_CERTIFICATE_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }}
SIGNING_APPLE_ID: ${{ secrets.SIGNING_APPLE_ID }} SIGNING_APPLE_ID: ${{ secrets.SIGNING_APPLE_ID }}
SIGNING_APP_PASSWORD: ${{ secrets.SIGNING_APP_PASSWORD }} SIGNING_APP_PASSWORD: ${{ secrets.SIGNING_APP_PASSWORD }}
SIGNING_TEAM_ID: ${{ secrets.SIGNING_TEAM_ID }} SIGNING_TEAM_ID: ${{ secrets.SIGNING_TEAM_ID }}

@ -1,61 +0,0 @@
# TODO: Figure out a way to use nvm in the linux build
linux:
image: node:10.13.0
tags:
- docker
script:
- whoami
- node -v
- yarn -v
- yarn install --frozen-lockfile
- export SIGNAL_ENV=production
- yarn generate
- $(yarn bin)/electron-builder --config.extraMetadata.environment=$SIGNAL_ENV --config.mac.bundleVersion='$CI_COMMIT_REF_SLUG' --publish=never --config.directories.output=release
cache:
paths:
- node_modules/
artifacts:
paths:
- release/
osx:
tags:
- osx
script:
- nvm install
- npm install --global yarn
- yarn install --frozen-lockfile
- export SIGNAL_ENV=production
- yarn generate
- $(yarn bin)/electron-builder --config.extraMetadata.environment=$SIGNAL_ENV --config.mac.bundleVersion='$CI_COMMIT_REF_SLUG' --publish=never --config.directories.output=release
cache:
paths:
- node_modules/
artifacts:
paths:
- release/
windows:
tags:
- windows-cmd
script:
# install
- set PATH=%PATH%;C:\Users\Administrator\AppData\Local\nvs\
- set SIGNAL_ENV=production
- set /p NVMRC_VER=<.nvmrc
- call nvs add %NVMRC_VER%
- call nvs use %NVMRC_VER%
- call "C:\\PROGRA~2\\MICROS~1\\2017\\BuildTools\\Common7\\Tools\\VsDevCmd.bat"
- call yarn install --frozen-lockfile
# build
- call yarn generate
- call node build\grunt.js
- call yarn prepare-beta-build
- call node_modules\.bin\electron-builder --config.extraMetadata.environment=%SIGNAL_ENV% --publish=never --config.directories.output=release
- call node build\grunt.js test-release:win
cache:
paths:
- node_modules/
artifacts:
paths:
- release/

@ -1,33 +0,0 @@
language: node_js
cache:
yarn: true
directories:
- node_modules
node_js:
- '10.13.0'
install:
- travis_wait 30 yarn install --frozen-lockfile --network-timeout 1000000
script:
- yarn generate
- yarn lint-windows
- yarn test
env:
global:
- SIGNAL_ENV: production
sudo: false
notifications:
email: false
matrix:
include:
- name: 'Linux'
os: linux
dist: trusty
before_install:
- sudo apt-get install -y libgtk2.0-0 libgtk-3-0 libgconf-2-4 libasound2 libxtst6 libxss1 libnss3 xvfb hunspell-en-us
before_script:
- Xvfb -ac -screen scrn 1280x2000x24 :9.0 &
- export DISPLAY=:9.0
- export LC_ALL=en_US
- name: 'OSX'
os: osx

@ -11,7 +11,7 @@ for it or creating a new one yourself. You can use also that issue as a place to
your intentions and get feedback from the users most likely to appreciate your changes. your intentions and get feedback from the users most likely to appreciate your changes.
You're most likely to have your pull request accepted easily if it addresses bugs already You're most likely to have your pull request accepted easily if it addresses bugs already
in the [Next Steps project](https://github.com/loki-project/loki-messenger/projects/1), in the [Next Steps project](https://github.com/loki-project/session-desktop/projects/1),
especially if they are near the top of the Backlog column. Those are what we'll be looking especially if they are near the top of the Backlog column. Those are what we'll be looking
at next, so it would be great if you helped us out! at next, so it would be great if you helped us out!
@ -22,7 +22,7 @@ ounce of prevention, as they say!](https://www.goodreads.com/quotes/247269-an-ou
## Developer Setup ## Developer Setup
First, you'll need [Node.js](https://nodejs.org/) which matches our current version. First, you'll need [Node.js](https://nodejs.org/) which matches our current version.
You can check [`.nvmrc` in the `development` branch](https://github.com/loki-project/loki-messenger/blob/development/.nvmrc) to see what the current version is. If you have [nvm](https://github.com/creationix/nvm) You can check [`.nvmrc` in the `development` branch](https://github.com/loki-project/session-desktop/blob/development/.nvmrc) to see what the current version is. If you have [nvm](https://github.com/creationix/nvm)
you can just run `nvm use` in the project directory and it will switch to the project's you can just run `nvm use` in the project directory and it will switch to the project's
desired Node.js version. [nvm for windows](https://github.com/coreybutler/nvm-windows) is desired Node.js version. [nvm for windows](https://github.com/coreybutler/nvm-windows) is
still useful, but it doesn't support `.nvmrc` files. still useful, but it doesn't support `.nvmrc` files.
@ -56,8 +56,8 @@ Then you need `git`, if you don't have that yet: https://git-scm.com/
Now, run these commands in your preferred terminal in a good directory for development: Now, run these commands in your preferred terminal in a good directory for development:
``` ```
git clone https://github.com/loki-project/loki-messenger.git git clone https://github.com/loki-project/session-desktop.git
cd loki-messenger cd session-desktop
npm install --global yarn # (only if you dont already have `yarn`) npm install --global yarn # (only if you dont already have `yarn`)
yarn install --frozen-lockfile # Install and build dependencies (this will take a while) yarn install --frozen-lockfile # Install and build dependencies (this will take a while)
yarn grunt # Generate final JS and CSS assets yarn grunt # Generate final JS and CSS assets
@ -115,7 +115,7 @@ NODE_APP_INSTANCE=alice yarn run start
``` ```
This changes the [userData](https://electron.atom.io/docs/all/#appgetpathname) This changes the [userData](https://electron.atom.io/docs/all/#appgetpathname)
directory from `%appData%/Loki-Messenger` to `%appData%/Loki-Messenger-aliceProfile`. directory from `%appData%/Session` to `%appData%/Session-aliceProfile`.
# Making changes # Making changes

@ -0,0 +1,24 @@
# Releasing
Creating a new Session Desktop release is very simple.
1. Bump up the version in `package.json`.
2. Merge all changes required into the `master` branch.
* This will trigger github actions to start building a draft release
3. After github actions has finished building. Go to Release page in the repository.
4. Click on the draft release and change the tag target to `master`.
5. Add in release notes.
6. Generate gpg signatures.
7. Click publish release.
## Notes
Artifacts attached in the release shouldn't be deleted! These include the yml files (latest, latest-mac, latest-linux). These are all necessary to get auto updating to work correctly.
### Mac
Mac currently uses 2 formats `dmg` and `zip`.
We need the `zip` format for auto updating to work correctly.
We also need the `dmg` because on MacOS Catalina, there is a system bug where extracting the artifact `zip` using the default _Archive Utility_ will make it so the extracted application is invalid and it will fail to open. A work around for this is to extract the `zip` using an alternate program such as _The Unarchiver_.
Once this bug is fixed we can go back to using the `zip` format by itself.

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Signal update available", "message": "Session update available",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "There is a new version of Signal available.", "message": "There is a new version of Session available.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Press Restart Signal to apply the updates.", "message": "Press Restart Session to apply the updates.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Restart Signal", "message": "Restart Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1468,11 +1468,11 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Натиснете Рестарт на Signal за да валидирате промените.", "message": "Натиснете Рестарт на Session за да валидирате промените.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Рестарт на Signal", "message": "Рестарт на Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Disponible una actualització del Signal", "message": "Disponible una actualització del Session",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Hi ha disponible una versió nova del Signal.", "message": "Hi ha disponible una versió nova del Session.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Premeu Reinicia el Signal per a aplicar les actualitzacions.", "message": "Premeu Reinicia el Session per a aplicar les actualitzacions.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Reinicia el Signal", "message": "Reinicia el Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Dostupná aktualizace Signal", "message": "Dostupná aktualizace Session",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Je k dispozici nová verze aplikace Signal.", "message": "Je k dispozici nová verze aplikace Session.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Stiskněte na Restartovat Signal pro aplikování změn", "message": "Stiskněte na Restartovat Session pro aplikování změn",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Restartovat Signal", "message": "Restartovat Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Signalopdatering tilgængelig", "message": "Sessionopdatering tilgængelig",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Der er en ny version af Signal tilgængelig.", "message": "Der er en ny version af Session tilgængelig.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Genstart Signal for at anvende opdateringerne.", "message": "Genstart Session for at anvende opdateringerne.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Genstart Signal", "message": "Genstart Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Aktualisierung für Signal verfügbar", "message": "Aktualisierung für Session verfügbar",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Eine neue Version von Signal ist verfügbar.", "message": "Eine neue Version von Session ist verfügbar.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Zum Aktualisieren klicke auf »Signal neu starten«.", "message": "Zum Aktualisieren klicke auf »Session neu starten«.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Signal neu starten", "message": "Session neu starten",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,11 +1460,11 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Διαθέσιμη ενημέρωση του Signal", "message": "Διαθέσιμη ενημέρωση του Session",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Μια νέα έκδοση του Signal είναι διαθέσιμη.", "message": "Μια νέα έκδοση του Session είναι διαθέσιμη.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
@ -1472,7 +1472,7 @@
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Επανεκκίνηση του Signal", "message": "Επανεκκίνηση του Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -227,7 +227,7 @@
}, },
"loadDataDescription": { "loadDataDescription": {
"message": "message":
"You've just gone through the export process, and your contacts and messages are waiting patiently on your computer. Select the folder that contains your saved Signal data.", "You've just gone through the export process, and your contacts and messages are waiting patiently on your computer. Select the folder that contains your saved Session data.",
"description": "description":
"Introduction to the process of importing messages and contacts from disk" "Introduction to the process of importing messages and contacts from disk"
}, },
@ -246,7 +246,7 @@
}, },
"importErrorFirst": { "importErrorFirst": {
"message": "message":
"Make sure you have chosen the correct directory that contains your saved Signal data. Its name should begin with 'Signal Export.' You can also save a new copy of your data from the Chrome App.", "Make sure you have chosen the correct directory that contains your saved Session data. Its name should begin with 'Session Export.' You can also save a new copy of your data from the Chrome App.",
"description": "Message shown if the import went wrong; first paragraph" "description": "Message shown if the import went wrong; first paragraph"
}, },
"importErrorSecond": { "importErrorSecond": {
@ -403,13 +403,13 @@
}, },
"changedSinceVerifiedMultiple": { "changedSinceVerifiedMultiple": {
"message": "message":
"Your safety numbers with multiple group members have changed since you last verified. This could mean that someone is trying to intercept your communication or that they have simply reinstalled Signal.", "Your safety numbers with multiple group members have changed since you last verified. This could mean that someone is trying to intercept your communication or that they have simply reinstalled Session.",
"description": "description":
"Shown on confirmation dialog when user attempts to send a message" "Shown on confirmation dialog when user attempts to send a message"
}, },
"changedSinceVerified": { "changedSinceVerified": {
"message": "message":
"Your safety number with $name$ has changed since you last verified. This could mean that someone is trying to intercept your communication or that $name$ has simply reinstalled Signal.", "Your safety number with $name$ has changed since you last verified. This could mean that someone is trying to intercept your communication or that $name$ has simply reinstalled Session.",
"description": "description":
"Shown on confirmation dialog when user attempts to send a message", "Shown on confirmation dialog when user attempts to send a message",
"placeholders": { "placeholders": {
@ -421,7 +421,7 @@
}, },
"changedRightAfterVerify": { "changedRightAfterVerify": {
"message": "message":
"The safety number you are trying to verify has changed. Please review your new safety number with $name$. Remember, this change could mean that someone is trying to intercept your communication or that $name$ has simply reinstalled Signal.", "The safety number you are trying to verify has changed. Please review your new safety number with $name$. Remember, this change could mean that someone is trying to intercept your communication or that $name$ has simply reinstalled Session.",
"description": "description":
"Shown on the safety number screen when the user has selected to verify/unverify a contact's safety number, and we immediately discover a safety number change", "Shown on the safety number screen when the user has selected to verify/unverify a contact's safety number, and we immediately discover a safety number change",
"placeholders": { "placeholders": {
@ -433,13 +433,13 @@
}, },
"changedRecentlyMultiple": { "changedRecentlyMultiple": {
"message": "message":
"Your safety numbers with multiple group members have changed recently. This could mean that someone is trying to intercept your communication or that they have simply reinstalled Signal.", "Your safety numbers with multiple group members have changed recently. This could mean that someone is trying to intercept your communication or that they have simply reinstalled Session.",
"description": "description":
"Shown on confirmation dialog when user attempts to send a message" "Shown on confirmation dialog when user attempts to send a message"
}, },
"changedRecently": { "changedRecently": {
"message": "message":
"Your safety number with $name$ has changed recently. This could mean that someone is trying to intercept your communication or that $name$ has simply reinstalled Signal.", "Your safety number with $name$ has changed recently. This could mean that someone is trying to intercept your communication or that $name$ has simply reinstalled Session.",
"description": "description":
"Shown on confirmation dialog when user attempts to send a message", "Shown on confirmation dialog when user attempts to send a message",
"placeholders": { "placeholders": {
@ -451,7 +451,7 @@
}, },
"identityKeyErrorOnSend": { "identityKeyErrorOnSend": {
"message": "message":
"Your safety number with $name$ has changed. This could either mean that someone is trying to intercept your communication or that $name$ has simply reinstalled Signal. You may wish to verify your saftey number with this contact.", "Your safety number with $name$ has changed. This could either mean that someone is trying to intercept your communication or that $name$ has simply reinstalled Session. You may wish to verify your saftey number with this contact.",
"description": "description":
"Shown when user clicks on a failed recipient in the message detail view after an identity key change", "Shown when user clicks on a failed recipient in the message detail view after an identity key change",
"placeholders": { "placeholders": {
@ -545,7 +545,7 @@
}, },
"identityChanged": { "identityChanged": {
"message": "message":
"Your safety number with this contact has changed. This could either mean that someone is trying to intercept your communication, or this contact simply reinstalled Signal. You may wish to verify the new safety number below." "Your safety number with this contact has changed. This could either mean that someone is trying to intercept your communication, or this contact simply reinstalled Session. You may wish to verify the new safety number below."
}, },
"incomingError": { "incomingError": {
"message": "Error handling incoming message" "message": "Error handling incoming message"
@ -610,12 +610,12 @@
"loadingPreview": { "loadingPreview": {
"message": "Loading Preview...", "message": "Loading Preview...",
"description": "description":
"Shown while Signal Desktop is fetching metadata for a url in composition area" "Shown while Session Desktop is fetching metadata for a url in composition area"
}, },
"stagedPreviewThumbnail": { "stagedPreviewThumbnail": {
"message": "Draft thumbnail link preview for $domain$", "message": "Draft thumbnail link preview for $domain$",
"description": "description":
"Shown while Signal Desktop is fetching metadata for a url in composition area", "Shown while Session Desktop is fetching metadata for a url in composition area",
"placeholders": { "placeholders": {
"path": { "path": {
"content": "$1", "content": "$1",
@ -626,7 +626,7 @@
"previewThumbnail": { "previewThumbnail": {
"message": "Thumbnail link preview for $domain$", "message": "Thumbnail link preview for $domain$",
"description": "description":
"Shown while Signal Desktop is fetching metadata for a url in composition area", "Shown while Session Desktop is fetching metadata for a url in composition area",
"placeholders": { "placeholders": {
"path": { "path": {
"content": "$1", "content": "$1",
@ -726,7 +726,7 @@
"signalDesktopPreferences": { "signalDesktopPreferences": {
"message": "Session Preferences", "message": "Session Preferences",
"description": "description":
"Title of the window that pops up with Signal Desktop preferences in it" "Title of the window that pops up with Session Desktop preferences in it"
}, },
"aboutSignalDesktop": { "aboutSignalDesktop": {
"message": "About Session", "message": "About Session",
@ -809,7 +809,7 @@
"sendMessageToContact": { "sendMessageToContact": {
"message": "Send Message", "message": "Send Message",
"description": "description":
"Shown when you are sent a contact and that contact has a signal account" "Shown when you are sent a contact and that contact has a session"
}, },
"home": { "home": {
"message": "home", "message": "home",
@ -935,13 +935,13 @@
}, },
"cannotUpdateDetail": { "cannotUpdateDetail": {
"message": "message":
"Signal Desktop failed to update, but there is a new version available. Please go to https://getsession.org/ and install the new version manually, then either contact support or file a bug about this problem.", "Session Desktop failed to update, but there is a new version available. Please go to https://getsession.org/ and install the new version manually, then either contact support or file a bug about this problem.",
"description": "description":
"Shown if a general error happened while trying to install update package" "Shown if a general error happened while trying to install update package"
}, },
"readOnlyVolume": { "readOnlyVolume": {
"message": "message":
"Signal Desktop is likely in a macOS quarantine, and will not be able to auto-update. Please try moving Signal.app to /Applications with Finder.", "Session Desktop is likely in a macOS quarantine, and will not be able to auto-update. Please try moving Session.app to /Applications with Finder.",
"description": "description":
"Shown on MacOS if running on a read-only volume and we cannot update" "Shown on MacOS if running on a read-only volume and we cannot update"
}, },
@ -1474,6 +1474,10 @@
"Warning! Lowering the TTL could result in messages being lost if the recipient doesn't collect them in time!", "Warning! Lowering the TTL could result in messages being lost if the recipient doesn't collect them in time!",
"description": "Warning for the time to live setting" "description": "Warning for the time to live setting"
}, },
"zoomFactorSettingTitle": {
"message": "Zoom Factor",
"description": "Title of the Zoom Factor setting"
},
"notificationSettingsDialog": { "notificationSettingsDialog": {
"message": "When messages arrive, display notifications that reveal...", "message": "When messages arrive, display notifications that reveal...",
"description": "Explain the purpose of the notification settings" "description": "Explain the purpose of the notification settings"
@ -1946,7 +1950,7 @@
}, },
"unlinkedWarning": { "unlinkedWarning": {
"message": "message":
"Relink Signal Desktop to your mobile device to continue messaging." "Relink Session Desktop to your mobile device to continue messaging."
}, },
"unlinked": { "unlinked": {
"message": "Unlinked" "message": "Unlinked"
@ -1955,20 +1959,29 @@
"message": "Relink" "message": "Relink"
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Signal update available" "message": "Session update available"
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "There is a new version of Signal available." "message": "There is a new version of Session available."
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Press Restart Signal to apply the updates." "message": "Press Restart Session to apply the updates."
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Restart Signal" "message": "Restart Session"
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {
"message": "Later" "message": "Later"
}, },
"autoUpdateDownloadButtonLabel": {
"message": "Download"
},
"autoUpdateDownloadedMessage": {
"message": "The new update has been downloaded."
},
"autoUpdateDownloadInstructions": {
"message": "Would you like to download the update?"
},
"leftTheGroup": { "leftTheGroup": {
"message": "$name$ left the group", "message": "$name$ left the group",
"description": "description":
@ -2204,9 +2217,15 @@
"message": "Edit Profile", "message": "Edit Profile",
"description": "Button action that the user can click to edit their profile" "description": "Button action that the user can click to edit their profile"
}, },
"editGroupNameOrPicture": {
"message": "Edit group name or picture",
"description":
"Button action that the user can click to edit a group name (open)"
},
"editGroupName": { "editGroupName": {
"message": "Edit group name", "message": "Edit group name",
"description": "Button action that the user can click to edit a group name" "description":
"Button action that the user can click to edit a group name (closed)"
}, },
"createGroupDialogTitle": { "createGroupDialogTitle": {
"message": "Creating a Closed Group", "message": "Creating a Closed Group",
@ -2597,6 +2616,9 @@
"message": "Enter other devices Session ID here" "message": "Enter other devices Session ID here"
}, },
"continueYourSession": { "continueYourSession": {
"message": "Continue Your Session"
},
"linkDevice": {
"message": "Link Device" "message": "Link Device"
}, },
"restoreSessionID": { "restoreSessionID": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Ĝisdatiĝo de Signal disponeblas", "message": "Ĝisdatiĝo de Session disponeblas",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Nova versio de Signal disponeblas.", "message": "Nova versio de Session disponeblas.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Premu „Restartigi Signal-on“ por ĝisdatigi.", "message": "Premu „Restartigi Session-on“ por ĝisdatigi.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Restartigi Signal-on", "message": "Restartigi Session-on",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Actualización de Signal Desktop disponible", "message": "Actualización de Session Desktop disponible",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Hay una nueva versión de Signal Desktop disponible.", "message": "Hay una nueva versión de Session Desktop disponible.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Pulsa en 'Reiniciar Signal' para aplicar cambios.", "message": "Pulsa en 'Reiniciar Session' para aplicar cambios.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Reiniciar Signal", "message": "Reiniciar Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1324,19 +1324,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Actualización de Signal disponible", "message": "Actualización de Session disponible",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Hay una nueva versión de Signal disponible.", "message": "Hay una nueva versión de Session disponible.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Press Restart Signal to apply the updates.", "message": "Press Restart Session to apply the updates.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Restart Signal", "message": "Restart Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Signali uuendus on saadaval", "message": "Session uuendus on saadaval",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Signalist on saadaval uus versioon.", "message": "Session on saadaval uus versioon.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Uuenduste paigaldamiseks vajuta \"Taaskäivita Signal\".", "message": "Uuenduste paigaldamiseks vajuta \"Taaskäivita Session\".",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Taaskäivita Signal", "message": "Taaskäivita Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "به‌روزرسانی Signal در دسترس است", "message": "به‌روزرسانی Session در دسترس است",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "نسخه جدیدی از Signal در دسترس است.", "message": "نسخه جدیدی از Session در دسترس است.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "برای اعمال آپدیت ها Signal را ری استارت کنید.", "message": "برای اعمال آپدیت ها Session را ری استارت کنید.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "راه اندازی مجدد Signal", "message": "راه اندازی مجدد Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Signal päivitys saatavilla", "message": "Session päivitys saatavilla",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Uusi versio Signalista on saatavilla.", "message": "Uusi versio Session on saatavilla.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Paina Käynnistä Signal uudelleen asentaaksesi päivitykset.", "message": "Paina Käynnistä Session uudelleen asentaaksesi päivitykset.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Käynnistä Signal uudelleen", "message": "Käynnistä Session uudelleen",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Une mise à jour de Signal est proposée", "message": "Une mise à jour de Session est proposée",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Une nouvelle version de Signal est proposée.", "message": "Une nouvelle version de Session est proposée.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Appuyez sur « Redémarrer Signal » pour appliquer les mises à jour.", "message": "Appuyez sur « Redémarrer Session » pour appliquer les mises à jour.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Redémarrer Signal", "message": "Redémarrer Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "עדכון Signal זמין", "message": "עדכון Session זמין",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "יש גרסה חדשה של Signal זמינה.", "message": "יש גרסה חדשה של Session זמינה.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "לחץ על הפעל מחדש את Signal כדי להחיל את העדכונים.", "message": "לחץ על הפעל מחדש את Session כדי להחיל את העדכונים.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "הפעל מחדש את Signal", "message": "הפעל מחדש את Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Signal update available", "message": "Session update available",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "There is a new version of Signal available.", "message": "There is a new version of Session available.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Press Restart Signal to apply the updates.", "message": "Press Restart Session to apply the updates.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Restart Signal", "message": "Restart Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Dostupna nadogradnja za Signal", "message": "Dostupna nadogradnja za Session",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Dostupna je nova inačica Signala.", "message": "Dostupna je nova inačica Session.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Press Restart Signal to apply the updates.", "message": "Press Restart Session to apply the updates.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Restart Signal", "message": "Restart Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Signal frissítés elérhető", "message": "Session frissítés elérhető",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "A Signal új verziója érhető el.", "message": "A Session új verziója érhető el.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Kattints a Signal újraindítására a frissítések alkalmazásához! ", "message": "Kattints a Session újraindítására a frissítések alkalmazásához! ",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Signal újraindítása", "message": "Session újraindítása",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Tersedia Signal versi terbaru", "message": "Tersedia Session versi terbaru",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Tersedia versi terbaru Signal.", "message": "Tersedia versi terbaru Session.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Tekan memulai awal Signal untuk mendapatkan versi terbaru.", "message": "Tekan memulai awal Session untuk mendapatkan versi terbaru.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Mulai ulang Signal", "message": "Mulai ulang Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Aggiornamento Signal disponibile", "message": "Aggiornamento Session disponibile",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "È disponibile una nuova versione di Signal.", "message": "È disponibile una nuova versione di Session.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Premi \"Riavvia Signal\" per applicare gli aggiornamenti.", "message": "Premi \"Riavvia Session\" per applicare gli aggiornamenti.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Riavvia Signal", "message": "Riavvia Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Signalのアップデートがあります", "message": "Sessionのアップデートがあります",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "新しく生まれ変わったSignalがあります", "message": "新しく生まれ変わったSessionがあります",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "アップデートを適用するにはSignalを再起動してください。", "message": "アップデートを適用するにはSessionを再起動してください。",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Signalを再起動", "message": "Sessionを再起動",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "មានបច្ចុប្បន្នភាព Signal", "message": "មានបច្ចុប្បន្នភាព Session",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "មានSignalជំនាន់ថ្មី", "message": "មានSessionជំនាន់ថ្មី",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "ចុច បើក Signalឡើងវិញ ដើម្បីដំណើការបច្ចុប្បន្នភាព។", "message": "ចុច បើក Sessionឡើងវិញ ដើម្បីដំណើការបច្ចុប្បន្នភាព។",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "បើកSignal ឡើងវិញ", "message": "បើកSession ឡើងវិញ",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Signal update available", "message": "Session update available",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "There is a new version of Signal available.", "message": "There is a new version of Session available.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Press Restart Signal to apply the updates.", "message": "Press Restart Session to apply the updates.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Restart Signal", "message": "Restart Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Signal update available", "message": "Session update available",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "There is a new version of Signal available.", "message": "There is a new version of Session available.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Press Restart Signal to apply the updates.", "message": "Press Restart Session to apply the updates.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Restart Signal", "message": "Restart Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Yra prieinamas Signal atnaujinimas", "message": "Yra prieinamas Session atnaujinimas",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Yra prieinama nauja Signal versija.", "message": "Yra prieinama nauja Session versija.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Norėdami pritaikyti atnaujinimus, paspauskite \"Paleisti Signal iš naujo\".", "message": "Norėdami pritaikyti atnaujinimus, paspauskite \"Paleisti Session iš naujo\".",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Paleisti Signal iš naujo", "message": "Paleisti Session iš naujo",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Signal update available", "message": "Session update available",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "There is a new version of Signal available.", "message": "There is a new version of Session available.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Press Restart Signal to apply the updates.", "message": "Press Restart Session to apply the updates.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Restart Signal", "message": "Restart Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Signal oppdatering tilgjengelig", "message": "Session oppdatering tilgjengelig",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "En ny versjon av Signal er tilgjengelig", "message": "En ny versjon av Session er tilgjengelig",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Trykk Restart Signal for å fullføre oppgraderingen.", "message": "Trykk Restart Session for å fullføre oppgraderingen.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Start Signal På Nytt", "message": "Start Session På Nytt",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Update voor Signal beschikbaar", "message": "Update voor Session beschikbaar",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Er is een nieuwe versie van Signal beschikbaar.", "message": "Er is een nieuwe versie van Session beschikbaar.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Klik op Signal herstarten om de updates toe te passen.", "message": "Klik op Session herstarten om de updates toe te passen.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Signal herstarten", "message": "Session herstarten",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Signal-oppdatering tilgjengeleg", "message": "Session-oppdatering tilgjengeleg",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Ei ny utgåve av Signal er tilgjengeleg", "message": "Ei ny utgåve av Session er tilgjengeleg",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Trykk «Start Signal på nytt» for å fullføra oppgraderinga.", "message": "Trykk «Start Session på nytt» for å fullføra oppgraderinga.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Start Signal på nytt", "message": "Start Session på nytt",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Signal oppdatering tilgjengelig", "message": "Session oppdatering tilgjengelig",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "En ny versjon av Signal er tilgjengelig", "message": "En ny versjon av Session er tilgjengelig",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Trykk Restart Signal for å fullføre oppgraderingen.", "message": "Trykk Restart Session for å fullføre oppgraderingen.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Start Signal På Nytt", "message": "Start Session På Nytt",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,11 +1460,11 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Dostępna aktualizacja aplikacji Signal", "message": "Dostępna aktualizacja aplikacji Session",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Dostępna nowa wersja Signal", "message": "Dostępna nowa wersja Session",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Atualização do Signal disponível", "message": "Atualização do Session disponível",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Uma nova versão do Signal está disponível.", "message": "Uma nova versão do Session está disponível.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Por favor, toque em 'reiniciar Signal' para aplicar as atualizações.", "message": "Por favor, toque em 'reiniciar Session' para aplicar as atualizações.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Reiniciar Signal", "message": "Reiniciar Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Existe uma actualização disponível para o Signal", "message": "Existe uma actualização disponível para o Session",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Está disponível uma nova versão do Signal.", "message": "Está disponível uma nova versão do Session.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Pressione 'Reiniciar o Signal' para aplicar as atualizações.", "message": "Pressione 'Reiniciar o Session' para aplicar as atualizações.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Reiniciar o Signal", "message": "Reiniciar o Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Este disponibilă o actualizare de Signal ", "message": "Este disponibilă o actualizare de Session ",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Este disponibilă o nouă versiune de Signal.", "message": "Este disponibilă o nouă versiune de Session.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Apasă pe Repornire Signal pentru a aplica actualizările.", "message": "Apasă pe Repornire Session pentru a aplica actualizările.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Repornește Signal", "message": "Repornește Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Доступно обновление Signal", "message": "Доступно обновление Session",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Доступна новая версия Signal", "message": "Доступна новая версия Session",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Для применения обновлений перезапустите Signal.", "message": "Для применения обновлений перезапустите Session.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Перезапустите Signal", "message": "Перезапустите Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Dostupná aktualizácia pre Signal", "message": "Dostupná aktualizácia pre Session",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Je k dispozícii nová verzia Signal.", "message": "Je k dispozícii nová verzia Session.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Reštartujte Signal pre dokončenie aktualizácie.", "message": "Reštartujte Session pre dokončenie aktualizácie.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Reštartovať Signal", "message": "Reštartovať Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Na voljo je posodobitev aplikacije Signal", "message": "Na voljo je posodobitev aplikacije Session",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Na voljo je nova različica aplikacije Signal.", "message": "Na voljo je nova različica aplikacije Session.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Za uveljavitev nadgradenj pritisnite tipko Ponovno zaženi Signal", "message": "Za uveljavitev nadgradenj pritisnite tipko Ponovno zaženi Session",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Ponovno zaženi Signal", "message": "Ponovno zaženi Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Ka gati përditësim të Signal-it", "message": "Ka gati përditësim të Session-it",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Ka të gatshëm një version të ri të Signal-it", "message": "Ka të gatshëm një version të ri të Session-it",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Shtypni Rinise Signal-in që të zbatohen përditësimet.", "message": "Shtypni Rinise Session-in që të zbatohen përditësimet.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Riniseni Signal-in", "message": "Riniseni Session-in",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Нова верзија Signal-а је доступна", "message": "Нова верзија Session-а је доступна",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "There is a new version of Signal available.", "message": "There is a new version of Session available.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Press Restart Signal to apply the updates.", "message": "Press Restart Session to apply the updates.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Restart Signal", "message": "Restart Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Uppdatering för Signal tillgänglig", "message": "Uppdatering för Session tillgänglig",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Det finns en ny version av Signal tillgänglig.", "message": "Det finns en ny version av Session tillgänglig.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Vänligen starta om Signal för att uppdatera", "message": "Vänligen starta om Session för att uppdatera",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Starta om Signal", "message": "Starta om Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "มีการอัพเดทสำหรับ Signal", "message": "มีการอัพเดทสำหรับ Session",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "มี Signal รุ่นใหม่แล้ว", "message": "มี Session รุ่นใหม่แล้ว",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "กด เริ่มต้น Signal ใหม่เพื่อเริ่มใช้การอัพเดต", "message": "กด เริ่มต้น Session ใหม่เพื่อเริ่มใช้การอัพเดต",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "เริ่มต้น Signal ใหม่", "message": "เริ่มต้น Session ใหม่",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Signal güncellemesi mevcut", "message": "Session güncellemesi mevcut",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Signal'ın yeni bir sürümü mevcut.", "message": "Session'ın yeni bir sürümü mevcut.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Güncellemeleri uygulamak için 'Signal'i Yeniden Başlat'a basınız.", "message": "Güncellemeleri uygulamak için 'Session'i Yeniden Başlat'a basınız.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Signal'i Yeniden Başlat", "message": "Session'i Yeniden Başlat",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Доступне оновлення Signal", "message": "Доступне оновлення Session",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "Нова версія Signal доступна.", "message": "Нова версія Session доступна.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Press Restart Signal to apply the updates.", "message": "Press Restart Session to apply the updates.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Restart Signal", "message": "Restart Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Signal update available", "message": "Session update available",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "There is a new version of Signal available.", "message": "There is a new version of Session available.",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Press Restart Signal to apply the updates.", "message": "Press Restart Session to apply the updates.",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "Restart Signal", "message": "Restart Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Signal 有可用更新", "message": "Session 有可用更新",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "有新版的 Signal 可用。", "message": "有新版的 Session 可用。",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "点击“重启 Signal”来安装更新。", "message": "点击“重启 Session",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "重启 Signal", "message": "重启 Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -1460,19 +1460,19 @@
"description": "" "description": ""
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Signal 可用的更新", "message": "Session 可用的更新",
"description": "" "description": ""
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "這是新版本的 Signal。", "message": "這是新版本的 Session",
"description": "" "description": ""
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "點選重啟 Signal 來套用更新。", "message": "點選重啟 Session 來套用更新。",
"description": "" "description": ""
}, },
"autoUpdateRestartButtonLabel": { "autoUpdateRestartButtonLabel": {
"message": "重啟 Signal", "message": "重啟 Session",
"description": "" "description": ""
}, },
"autoUpdateLaterButtonLabel": { "autoUpdateLaterButtonLabel": {

@ -10,6 +10,9 @@ let quitText = 'Quit';
let copyErrorAndQuitText = 'Copy error and quit'; let copyErrorAndQuitText = 'Copy error and quit';
function handleError(prefix, error) { function handleError(prefix, error) {
if (console._error) {
console._error(`${prefix}:`, Errors.toLogFormat(error));
}
console.error(`${prefix}:`, Errors.toLogFormat(error)); console.error(`${prefix}:`, Errors.toLogFormat(error));
if (app.isReady()) { if (app.isReady()) {

@ -231,6 +231,15 @@ async function getSQLCipherVersion(instance) {
} }
} }
async function getSQLIntegrityCheck(instance) {
const row = await instance.get('PRAGMA cipher_integrity_check;');
if (row) {
return row.cipher_integrity_check;
}
return null;
}
const HEX_KEY = /[^0-9A-Fa-f]/; const HEX_KEY = /[^0-9A-Fa-f]/;
async function setupSQLCipher(instance, { key }) { async function setupSQLCipher(instance, { key }) {
// If the key isn't hex then we need to derive a hex key from it // If the key isn't hex then we need to derive a hex key from it
@ -239,6 +248,9 @@ async function setupSQLCipher(instance, { key }) {
// https://www.zetetic.net/sqlcipher/sqlcipher-api/#key // https://www.zetetic.net/sqlcipher/sqlcipher-api/#key
const value = deriveKey ? `'${key}'` : `"x'${key}'"`; const value = deriveKey ? `'${key}'` : `"x'${key}'"`;
await instance.run(`PRAGMA key = ${value};`); await instance.run(`PRAGMA key = ${value};`);
// https://www.zetetic.net/blog/2018/11/30/sqlcipher-400-release/#compatability-sqlcipher-4-0-0
await instance.run('PRAGMA cipher_migrate;');
} }
async function setSQLPassword(password) { async function setSQLPassword(password) {
@ -1071,6 +1083,13 @@ async function initialize({ configDir, key, messages, passwordAttempt }) {
db = promisified; db = promisified;
// test database // test database
const result = await getSQLIntegrityCheck(db);
if (result) {
console.log('Database integrity check failed:', result);
throw new Error(`Integrity check failed: ${result}`);
}
await getMessageCount(); await getMessageCount();
} catch (error) { } catch (error) {
if (passwordAttempt) { if (passwordAttempt) {

@ -1,24 +0,0 @@
platform:
- x64
cache:
- '%LOCALAPPDATA%\electron\Cache'
- node_modules -> package.json
install:
- systeminfo | findstr /C:"OS"
- set PATH=C:\Ruby23-x64\bin;%PATH%
- ps: Install-Product node 10.13.0 x64
- yarn install --frozen-lockfile
build_script:
- node build\grunt.js
- yarn generate
- yarn lint-windows
- yarn test-node
test_script:
- node build\grunt.js test
environment:
SIGNAL_ENV: production

@ -1,52 +0,0 @@
#!/bin/bash
# Setup - creates the local repo which will be mirrored up to S3, then back-fill it. Your
# future deploys will eliminate all old versions without these backfill steps:
# aptly repo create signal-desktop
# aptly mirror create -ignore-signatures backfill-mirror https://updates.signal.org/desktop/apt xenial
# aptly mirror update -ignore-signatures backfill-mirror
# aptly repo import backfill-mirror signal-desktop signal-desktop signal-desktop-beta
# aptly repo show -with-packages signal-desktop
#
# First run on a machine - uncomment the first set of 'aptly publish snapshot' commands,
# comment the other two. Sets up the two publish channels, one local, one to S3.
#
# Testing - comment out the lines with s3:$ENDPOINT to publish only locally. To eliminate
# effects of testing, remove package from repo, then move back to old snapshot:
# aptly repo remove signal-desktop signal-desktop_1.0.35_amd64
# aptly publish switch -gpg-key=57F6FB06 xenial signal-desktop_v1.0.34
#
# Pruning package set - we generally want 2-3 versions of each stream available,
# production and beta. You can remove old packages like this:
# aptly repo show -with-packages signal-desktop
# aptly repo remove signal-desktop signal-desktop_1.0.34_amd64
#
# Release:
# NAME=signal-desktop(-beta) VERSION=X.X.X ./aptly.sh
echo "Releasing $NAME build version $VERSION"
REPO=signal-desktop
CURRENT=xenial
# PREVIOUS=xenial
ENDPOINT=signal-desktop-apt # Matches endpoint name in .aptly.conf
SNAPSHOT=signal-desktop_v$VERSION
GPG_KEYID=57F6FB06
aptly repo add $REPO release/$NAME\_$VERSION\_*.deb
aptly snapshot create $SNAPSHOT from repo $REPO
# run these only on first release to a given repo from a given machine. the first set is
# for local testing, the second set is to set up the production server.
# https://www.aptly.info/doc/aptly/publish/snapshot/
# aptly publish snapshot -gpg-key=$GPG_KEYID -distribution=$CURRENT $SNAPSHOT
# aptly publish snapshot -gpg-key=$GPG_KEYID -distribution=$PREVIOUS $SNAPSHOT
# aptly publish snapshot -gpg-key=$GPG_KEYID -distribution=$CURRENT -config=.aptly.conf $SNAPSHOT s3:$ENDPOINT:
# aptly publish snapshot -gpg-key=$GPG_KEYID -distribution=$PREVIOUS -config=.aptly.conf $SNAPSHOT s3:$ENDPOINT:
# these update already-published repos, run every time after that
# https://www.aptly.info/doc/aptly/publish/switch/
aptly publish switch -gpg-key=$GPG_KEYID $CURRENT $SNAPSHOT
# aptly publish switch -gpg-key=$GPG_KEYID $PREVIOUS $SNAPSHOT
aptly publish switch -gpg-key=$GPG_KEYID -config=.aptly.conf $CURRENT s3:$ENDPOINT: $SNAPSHOT
# aptly publish switch -gpg-key=$GPG_KEYID -config=.aptly.conf $PREVIOUS s3:$ENDPOINT: $SNAPSHOT

@ -1,7 +1,7 @@
{ {
"name": "loki-messenger", "name": "session-desktop",
"version": "0.0.0", "version": "0.0.0",
"homepage": "https://github.com/loki-project/loki-messenger", "homepage": "https://github.com/loki-project/session-desktop",
"license": "GPLV3", "license": "GPLV3",
"private": true, "private": true,
"dependencies": { "dependencies": {

@ -0,0 +1,15 @@
#!/usr/bin/env bash
if [ -z "$MAC_CERTIFICATE" ]; then
echo "MAC_CERTIFICATE not set. Ignoring."
else
export CSC_LINK="$MAC_CERTIFICATE"
echo "MAC_CERTIFICATE found."
fi
if [ -z "$MAC_CERTIFICATE_PASSWORD" ]; then
echo "MAC_CERTIFICATE_PASSWORD not set. Ignoring."
else
export CSC_KEY_PASSWORD="$MAC_CERTIFICATE_PASSWORD"
echo "MAC_CERTIFICATE_PASSWORD found."
fi

@ -23,10 +23,6 @@
"port": "38157" "port": "38157"
} }
], ],
"disableAutoUpdate": true,
"updatesUrl": "TODO",
"updatesPublicKey":
"fd7dd3de7149dc0a127909fee7de0f7620ddd0de061b37a2c303e37de802a401",
"updatesEnabled": false, "updatesEnabled": false,
"openDevTools": false, "openDevTools": false,
"buildExpiration": 0, "buildExpiration": 0,

@ -1 +1,3 @@
{} {
"updatesEnabled": true
}

@ -41,7 +41,7 @@
</div> </div>
<p> <p>
<a class='report-link' target='_blank' <a class='report-link' target='_blank'
href='https://github.com/loki-project/loki-messenger/issues/new/'> href='https://github.com/loki-project/session-desktop/issues/new/'>
{{ reportIssue }} {{ reportIssue }}
</a> </a>
</p> </p>

@ -1,5 +1,3 @@
provider: s3 owner: <yourGHName>
region: us-east-1 repo: <yourGHRepoName>
bucket: your-test-bucket.signal.org provider: github
path: desktop
acl: public-read

@ -286,6 +286,9 @@
} }
first = false; first = false;
// Update zoom
window.updateZoomFactor();
const currentPoWDifficulty = storage.get('PoWDifficulty', null); const currentPoWDifficulty = storage.get('PoWDifficulty', null);
if (!currentPoWDifficulty) { if (!currentPoWDifficulty) {
storage.put('PoWDifficulty', window.getDefaultPoWDifficulty()); storage.put('PoWDifficulty', window.getDefaultPoWDifficulty());
@ -398,17 +401,13 @@
await storage.put('version', currentVersion); await storage.put('version', currentVersion);
if (newVersion) { if (newVersion) {
if (
lastVersion &&
window.isBeforeVersion(lastVersion, 'v1.15.0-beta.5')
) {
await window.Signal.Logs.deleteAll();
window.restart();
}
window.log.info( window.log.info(
`New version detected: ${currentVersion}; previous: ${lastVersion}` `New version detected: ${currentVersion}; previous: ${lastVersion}`
); );
await window.Signal.Data.cleanupOrphanedAttachments();
await window.Signal.Logs.deleteAll();
} }
if (isIndexedDBPresent) { if (isIndexedDBPresent) {
@ -431,10 +430,6 @@
Views.Initialization.setMessage(window.i18n('optimizingApplication')); Views.Initialization.setMessage(window.i18n('optimizingApplication'));
if (newVersion) {
await window.Signal.Data.cleanupOrphanedAttachments();
}
Views.Initialization.setMessage(window.i18n('loading')); Views.Initialization.setMessage(window.i18n('loading'));
idleDetector = new IdleDetector(); idleDetector = new IdleDetector();
@ -702,7 +697,7 @@
} }
}); });
window.doUpdateGroup = async (groupId, groupName, members) => { window.doUpdateGroup = async (groupId, groupName, members, avatar) => {
const ourKey = textsecure.storage.user.getNumber(); const ourKey = textsecure.storage.user.getNumber();
const ev = new Event('message'); const ev = new Event('message');
@ -729,6 +724,44 @@
if (convo.isPublic()) { if (convo.isPublic()) {
const API = await convo.getPublicSendData(); const API = await convo.getPublicSendData();
if (avatar) {
// I hate duplicating this...
const readFile = attachment =>
new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = e => {
const data = e.target.result;
resolve({
...attachment,
data,
size: data.byteLength,
});
};
fileReader.onerror = reject;
fileReader.onabort = reject;
fileReader.readAsArrayBuffer(attachment.file);
});
const attachment = await readFile({ file: avatar });
// const tempUrl = window.URL.createObjectURL(avatar);
// Get file onto public chat server
const fileObj = await API.serverAPI.putAttachment(attachment.data);
if (fileObj === null) {
// problem
window.warn('File upload failed');
return;
}
// lets not allow ANY URLs, lets force it to be local to public chat server
const relativeFileUrl = fileObj.url.replace(
API.serverAPI.baseServerUrl,
''
);
// write it to the channel
await API.setChannelAvatar(relativeFileUrl);
}
if (await API.setChannelName(groupName)) { if (await API.setChannelName(groupName)) {
// queue update from server // queue update from server
// and let that set the conversation // and let that set the conversation
@ -741,7 +774,11 @@
return; return;
} }
const avatar = ''; const nullAvatar = '';
if (avatar) {
// would get to download this file on each client in the group
// and reference the local file
}
const options = {}; const options = {};
const recipients = _.union(convo.get('members'), members); const recipients = _.union(convo.get('members'), members);
@ -750,7 +787,7 @@
convo.updateGroup({ convo.updateGroup({
groupId, groupId,
groupName, groupName,
avatar, avatar: nullAvatar,
recipients, recipients,
members, members,
options, options,
@ -787,6 +824,7 @@
'group' 'group'
); );
convo.updateGroupAdmins([primaryDeviceKey]);
convo.updateGroup(ev.groupDetails); convo.updateGroup(ev.groupDetails);
// Group conversations are automatically 'friends' // Group conversations are automatically 'friends'
@ -795,8 +833,6 @@
window.friends.friendRequestStatusEnum.friends window.friends.friendRequestStatusEnum.friends
); );
convo.updateGroupAdmins([primaryDeviceKey]);
appView.openConversation(groupId, {}); appView.openConversation(groupId, {});
}; };
@ -994,7 +1030,9 @@
let friendList = contacts; let friendList = contacts;
if (friendList !== undefined) { if (friendList !== undefined) {
friendList = friendList.filter( friendList = friendList.filter(
friend => friend.type === 'direct' && !friend.isMe friend =>
(friend.type === 'direct' && !friend.isMe) ||
(friend.type === 'group' && !friend.isPublic && !friend.isRss)
); );
} }
return friendList; return friendList;
@ -1372,6 +1410,8 @@
await window.lokiFileServerAPI.updateOurDeviceMapping(); await window.lokiFileServerAPI.updateOurDeviceMapping();
// TODO: we should ensure the message was sent and retry automatically if not // TODO: we should ensure the message was sent and retry automatically if not
await libloki.api.sendUnpairingMessageToSecondary(pubKey); await libloki.api.sendUnpairingMessageToSecondary(pubKey);
// Remove all traces of the device
ConversationController.deleteContact(pubKey);
Whisper.events.trigger('refreshLinkedDeviceList'); Whisper.events.trigger('refreshLinkedDeviceList');
}); });
} }
@ -1470,6 +1510,9 @@
}; };
Whisper.Notifications.disable(); // avoid notification flood until empty Whisper.Notifications.disable(); // avoid notification flood until empty
setTimeout(() => {
Whisper.Notifications.enable();
}, window.CONSTANTS.NOTIFICATION_ENABLE_TIMEOUT_SECONDS * 1000);
if (Whisper.Registration.ongoingSecondaryDeviceRegistration()) { if (Whisper.Registration.ongoingSecondaryDeviceRegistration()) {
const ourKey = textsecure.storage.user.getNumber(); const ourKey = textsecure.storage.user.getNumber();
@ -1642,6 +1685,11 @@
// very fast, and it looks like a network blip. But we need to suppress // very fast, and it looks like a network blip. But we need to suppress
// notifications in these scenarios too. So we listen for 'reconnect' events. // notifications in these scenarios too. So we listen for 'reconnect' events.
Whisper.Notifications.disable(); Whisper.Notifications.disable();
// Enable back notifications once most messages have been fetched
setTimeout(() => {
Whisper.Notifications.enable();
}, window.CONSTANTS.NOTIFICATION_ENABLE_TIMEOUT_SECONDS * 1000);
} }
function onProgress(ev) { function onProgress(ev) {
const { count } = ev; const { count } = ev;

@ -935,7 +935,7 @@
if (newStatus === FriendRequestStatusEnum.friends) { if (newStatus === FriendRequestStatusEnum.friends) {
if (!blockSync) { if (!blockSync) {
// Sync contact // Sync contact
this.wrapSend(textsecure.messaging.sendContactSyncMessage(this)); this.wrapSend(textsecure.messaging.sendContactSyncMessage([this]));
} }
// Only enable sending profileKey after becoming friends // Only enable sending profileKey after becoming friends
this.set({ profileSharing: true }); this.set({ profileSharing: true });
@ -2232,6 +2232,7 @@
this.get('name'), this.get('name'),
this.get('avatar'), this.get('avatar'),
this.get('members'), this.get('members'),
this.get('groupAdmins'),
groupUpdate.recipients, groupUpdate.recipients,
options options
) )
@ -2239,6 +2240,21 @@
); );
}, },
sendGroupInfo(recipients) {
if (this.isClosedGroup()) {
const options = this.getSendOptions();
textsecure.messaging.updateGroup(
this.id,
this.get('name'),
this.get('avatar'),
this.get('members'),
this.get('groupAdmins'),
recipients,
options
);
}
},
async leaveGroup() { async leaveGroup() {
const now = Date.now(); const now = Date.now();
if (this.get('type') === 'group') { if (this.get('type') === 'group') {
@ -2323,6 +2339,7 @@
const ourNumber = textsecure.storage.user.getNumber(); const ourNumber = textsecure.storage.user.getNumber();
return !stillUnread.some( return !stillUnread.some(
m => m =>
m.propsForMessage &&
m.propsForMessage.text && m.propsForMessage.text &&
m.propsForMessage.text.indexOf(`@${ourNumber}`) !== -1 m.propsForMessage.text.indexOf(`@${ourNumber}`) !== -1
); );

@ -1422,7 +1422,7 @@
if (this.get('type') !== 'friend-request') { if (this.get('type') !== 'friend-request') {
const c = this.getConversation(); const c = this.getConversation();
// Don't bother sending sync messages to public chats // Don't bother sending sync messages to public chats
if (!c.isPublic()) { if (c && !c.isPublic()) {
this.sendSyncMessage(); this.sendSyncMessage();
} }
} }
@ -1929,78 +1929,90 @@
} }
} }
if ( if (initialMessage.group) {
initialMessage.group && if (
initialMessage.group.members && initialMessage.group.type === GROUP_TYPES.REQUEST_INFO &&
initialMessage.group.type === GROUP_TYPES.UPDATE !newGroup
) { ) {
if (newGroup) { conversation.sendGroupInfo([source]);
conversation.updateGroupAdmins(initialMessage.group.admins); return null;
} else if (
conversation.setFriendRequestStatus( initialMessage.group.members &&
window.friends.friendRequestStatusEnum.friends initialMessage.group.type === GROUP_TYPES.UPDATE
); ) {
} if (newGroup) {
conversation.updateGroupAdmins(initialMessage.group.admins);
const fromAdmin = conversation
.get('groupAdmins')
.includes(primarySource);
if (!fromAdmin) {
// Make sure the message is not removing members / renaming the group
const nameChanged =
conversation.get('name') !== initialMessage.group.name;
if (nameChanged) { conversation.setFriendRequestStatus(
window.log.warn( window.friends.friendRequestStatusEnum.friends
'Non-admin attempts to change the name of the group'
); );
} } else {
const fromAdmin = conversation
.get('groupAdmins')
.includes(primarySource);
if (!fromAdmin) {
// Make sure the message is not removing members / renaming the group
const nameChanged =
conversation.get('name') !== initialMessage.group.name;
if (nameChanged) {
window.log.warn(
'Non-admin attempts to change the name of the group'
);
}
const membersMissing = const membersMissing =
_.difference( _.difference(
conversation.get('members'), conversation.get('members'),
initialMessage.group.members initialMessage.group.members
).length > 0; ).length > 0;
if (membersMissing) { if (membersMissing) {
window.log.warn('Non-admin attempts to remove group members'); window.log.warn('Non-admin attempts to remove group members');
} }
const messageAllowed = !nameChanged && !membersMissing; const messageAllowed = !nameChanged && !membersMissing;
if (!messageAllowed) { if (!messageAllowed) {
confirm(); confirm();
return null; return null;
}
}
} }
} // For every member, see if we need to establish a session:
// For every member, see if we need to establish a session: initialMessage.group.members.forEach(memberPubKey => {
initialMessage.group.members.forEach(memberPubKey => { const haveSession = _.some(
const haveSession = _.some( textsecure.storage.protocol.sessions,
textsecure.storage.protocol.sessions, s => s.number === memberPubKey
s => s.number === memberPubKey );
);
const ourPubKey = textsecure.storage.user.getNumber(); const ourPubKey = textsecure.storage.user.getNumber();
if (!haveSession && memberPubKey !== ourPubKey) { if (!haveSession && memberPubKey !== ourPubKey) {
ConversationController.getOrCreateAndWait( ConversationController.getOrCreateAndWait(
memberPubKey,
'private'
).then(() => {
textsecure.messaging.sendMessageToNumber(
memberPubKey, memberPubKey,
'(If you see this message, you must be using an out-of-date client)', 'private'
[], ).then(() => {
undefined, textsecure.messaging.sendMessageToNumber(
[], memberPubKey,
Date.now(), '(If you see this message, you must be using an out-of-date client)',
undefined, [],
undefined, undefined,
{ messageType: 'friend-request', sessionRequest: true } [],
); Date.now(),
}); undefined,
} undefined,
}); { messageType: 'friend-request', sessionRequest: true }
);
});
}
});
} else if (newGroup) {
// We have an unknown group, we should request info from the sender
textsecure.messaging.requestGroupInfo(conversationId, [
primarySource,
]);
}
} }
const isSessionRequest = const isSessionRequest =

@ -57,7 +57,7 @@ exports.upload = async content => {
form.append('Content-Type', contentType); form.append('Content-Type', contentType);
form.append('file', contentBuffer, { form.append('file', contentBuffer, {
contentType, contentType,
filename: `loki-messenger-debug-log-${VERSION}.txt`, filename: `session-desktop-debug-log-${VERSION}.txt`,
}); });
// WORKAROUND: See comment on `submitFormData`: // WORKAROUND: See comment on `submitFormData`:

@ -5,6 +5,7 @@ const nodeFetch = require('node-fetch');
const { URL, URLSearchParams } = require('url'); const { URL, URLSearchParams } = require('url');
const FormData = require('form-data'); const FormData = require('form-data');
const https = require('https'); const https = require('https');
const path = require('path');
// Can't be less than 1200 if we have unauth'd requests // Can't be less than 1200 if we have unauth'd requests
const PUBLICCHAT_MSG_POLL_EVERY = 1.5 * 1000; // 1.5s const PUBLICCHAT_MSG_POLL_EVERY = 1.5 * 1000; // 1.5s
@ -229,7 +230,7 @@ class LokiAppDotNetServerAPI {
window.storage.get('primaryDevicePubKey') || window.storage.get('primaryDevicePubKey') ||
textsecure.storage.user.getNumber(); textsecure.storage.user.getNumber();
const profileConvo = ConversationController.get(ourNumber); const profileConvo = ConversationController.get(ourNumber);
const profile = profileConvo.getLokiProfile(); const profile = profileConvo && profileConvo.getLokiProfile();
const profileName = profile && profile.displayName; const profileName = profile && profile.displayName;
// if doesn't match, write it to the network // if doesn't match, write it to the network
if (tokenRes.response.data.user.name !== profileName) { if (tokenRes.response.data.user.name !== profileName) {
@ -627,6 +628,12 @@ class LokiAppDotNetServerAPI {
url url
); );
} }
if (mode === '_sendToProxy') {
// if we can detect, certain types of failures, we can retry...
if (e.code === 'ECONNRESET') {
// retry with counter?
}
}
return { return {
err: e, err: e,
}; };
@ -877,6 +884,7 @@ class LokiAppDotNetServerAPI {
}; };
} }
// for avatar
async uploadData(data) { async uploadData(data) {
const endpoint = 'files'; const endpoint = 'files';
const options = { const options = {
@ -901,6 +909,7 @@ class LokiAppDotNetServerAPI {
}; };
} }
// for files
putAttachment(attachmentBin) { putAttachment(attachmentBin) {
const formData = new FormData(); const formData = new FormData();
const buffer = Buffer.from(attachmentBin); const buffer = Buffer.from(attachmentBin);
@ -1246,7 +1255,50 @@ class LokiPublicChannelAPI {
this.conversation.setGroupName(note.value.name); this.conversation.setGroupName(note.value.name);
} }
if (note.value && note.value.avatar) { if (note.value && note.value.avatar) {
this.conversation.setProfileAvatar(note.value.avatar); if (note.value.avatar.match(/^images\//)) {
// local file avatar
const resolvedAvatar = path.normalize(note.value.avatar);
const base = path.normalize('images/');
const re = new RegExp(`^${base}`);
// do we at least ends up inside images/ somewhere?
if (re.test(resolvedAvatar)) {
this.conversation.set('avatar', resolvedAvatar);
}
} else {
// relative URL avatar
const avatarAbsUrl = this.serverAPI.baseServerUrl + note.value.avatar;
const {
writeNewAttachmentData,
deleteAttachmentData,
} = window.Signal.Migrations;
// do we already have this image? no, then
// download a copy and save it
const imageData = await nodeFetch(avatarAbsUrl);
// eslint-disable-next-line no-inner-declarations
function toArrayBuffer(buf) {
const ab = new ArrayBuffer(buf.length);
const view = new Uint8Array(ab);
// eslint-disable-next-line no-plusplus
for (let i = 0; i < buf.length; i++) {
view[i] = buf[i];
}
return ab;
}
// eslint-enable-next-line no-inner-declarations
const buffer = await imageData.buffer();
const newAttributes = await window.Signal.Types.Conversation.maybeUpdateAvatar(
this.conversation.attributes,
toArrayBuffer(buffer),
{
writeNewAttachmentData,
deleteAttachmentData,
}
);
// update group
this.conversation.set('avatar', newAttributes.avatar);
}
} }
// is it mutable? // is it mutable?
// who are the moderators? // who are the moderators?
@ -1256,6 +1308,15 @@ class LokiPublicChannelAPI {
if (data.counts && Number.isInteger(data.counts.subscribers)) { if (data.counts && Number.isInteger(data.counts.subscribers)) {
this.conversation.setSubscriberCount(data.counts.subscribers); this.conversation.setSubscriberCount(data.counts.subscribers);
} }
await window.Signal.Data.updateConversation(
this.conversation.id,
this.conversation.attributes,
{
Conversation: Whisper.Conversation,
}
);
await this.pollForChannelOnce();
} }
// get moderation actions // get moderation actions
@ -1372,7 +1433,7 @@ class LokiPublicChannelAPI {
} }
if (quote) { if (quote) {
// TODO: Enable quote attachments again using proper ADN style // Disable quote attachments
quote.attachments = []; quote.attachments = [];
} }
@ -1476,6 +1537,14 @@ class LokiPublicChannelAPI {
}); });
if (res.err || !res.response) { if (res.err || !res.response) {
log.error(
'Could not get messages from',
this.serverAPI.baseServerUrl,
this.baseChannelUrl
);
if (res.err) {
log.error('pollOnceForMessages receive error', res.err);
}
return; return;
} }
@ -1663,18 +1732,31 @@ class LokiPublicChannelAPI {
// filter out invalid messages // filter out invalid messages
pendingMessages = pendingMessages.filter(messageData => !!messageData); pendingMessages = pendingMessages.filter(messageData => !!messageData);
// separate messages coming from primary and secondary devices // separate messages coming from primary and secondary devices
const [primaryMessages, slaveMessages] = _.partition( let [primaryMessages, slaveMessages] = _.partition(
pendingMessages, pendingMessages,
message => !(message.source in slavePrimaryMap) message => !(message.source in slavePrimaryMap)
); );
// process primary devices' message directly // get minimum ID for primaryMessages and slaveMessages
primaryMessages.forEach(message => const firstPrimaryId = _.min(primaryMessages.map(msg => msg.serverId));
this.chatAPI.emit('publicMessage', { const firstSlaveId = _.min(slaveMessages.map(msg => msg.serverId));
message, if (firstPrimaryId < firstSlaveId) {
}) // early send
); // split off count from pendingMessages
let sendNow = [];
pendingMessages = []; // allow memory to be freed [sendNow, pendingMessages] = _.partition(
pendingMessages,
message => message.serverId < firstSlaveId
);
sendNow.forEach(message => {
// send them out now
log.info('emitting primary message', message.serverId);
this.chatAPI.emit('publicMessage', {
message,
});
});
sendNow = false;
}
primaryMessages = false; // free memory
// get actual chat server data (mainly the name rn) of primary device // get actual chat server data (mainly the name rn) of primary device
const verifiedDeviceResults = await this.serverAPI.getUsers( const verifiedDeviceResults = await this.serverAPI.getUsers(
@ -1731,11 +1813,25 @@ class LokiPublicChannelAPI {
messageData.message.profileKey = profileKey; messageData.message.profileKey = profileKey;
} }
} }
/* eslint-enable no-param-reassign */ });
slaveMessages = false; // free memory
// process all messages in the order received
pendingMessages.forEach(message => {
// if slave device
if (message.source in slavePrimaryMap) {
// prevent our own device sent messages from coming back in
if (message.source === ourNumberDevice) {
// we originally sent these
return;
}
}
log.info('emitting pending message', message.serverId);
this.chatAPI.emit('publicMessage', { this.chatAPI.emit('publicMessage', {
message: messageData, message,
}); });
}); });
/* eslint-enable no-param-reassign */ /* eslint-enable no-param-reassign */
// if we received one of our own messages // if we received one of our own messages

@ -217,7 +217,12 @@ class LokiMessageAPI {
} }
return true; return true;
} catch (e) { } catch (e) {
log.warn('Loki send message:', e); log.warn(
'Loki send message error:',
e.code,
e.message,
`from ${address}`
);
if (e instanceof textsecure.WrongSwarmError) { if (e instanceof textsecure.WrongSwarmError) {
const { newSwarm } = e; const { newSwarm } = e;
await lokiSnodeAPI.updateSwarmNodes(params.pubKey, newSwarm); await lokiSnodeAPI.updateSwarmNodes(params.pubKey, newSwarm);
@ -272,6 +277,8 @@ class LokiMessageAPI {
try { try {
// TODO: Revert back to using snode address instead of IP // TODO: Revert back to using snode address instead of IP
let messages = await this.retrieveNextMessages(nodeData.ip, nodeData); let messages = await this.retrieveNextMessages(nodeData.ip, nodeData);
// this only tracks retrieval failures
// won't include parsing failures...
successiveFailures = 0; successiveFailures = 0;
if (messages.length) { if (messages.length) {
const lastMessage = _.last(messages); const lastMessage = _.last(messages);
@ -288,7 +295,12 @@ class LokiMessageAPI {
// Execute callback even with empty array to signal online status // Execute callback even with empty array to signal online status
callback(messages); callback(messages);
} catch (e) { } catch (e) {
log.warn('Loki retrieve messages:', e.code, e.message); log.warn(
'Loki retrieve messages error:',
e.code,
e.message,
`on ${nodeData.ip}:${nodeData.port}`
);
if (e instanceof textsecure.WrongSwarmError) { if (e instanceof textsecure.WrongSwarmError) {
const { newSwarm } = e; const { newSwarm } = e;
await lokiSnodeAPI.updateSwarmNodes(this.ourKey, newSwarm); await lokiSnodeAPI.updateSwarmNodes(this.ourKey, newSwarm);
@ -312,9 +324,24 @@ class LokiMessageAPI {
} }
} }
if (successiveFailures >= MAX_ACCEPTABLE_FAILURES) { if (successiveFailures >= MAX_ACCEPTABLE_FAILURES) {
log.warn(
`removing ${nodeData.ip}:${
nodeData.port
} from our swarm pool. We have ${
Object.keys(this.ourSwarmNodes).length
} usable swarm nodes left`
);
await lokiSnodeAPI.unreachableNode(this.ourKey, address); await lokiSnodeAPI.unreachableNode(this.ourKey, address);
} }
} }
// if not stopPollingResult
if (_.isEmpty(this.ourSwarmNodes)) {
log.error(
'We no longer have any swarm nodes available to try in pool, closing retrieve connection'
);
return false;
}
return true;
} }
async retrieveNextMessages(nodeUrl, nodeData) { async retrieveNextMessages(nodeUrl, nodeData) {
@ -342,12 +369,31 @@ class LokiMessageAPI {
} }
async startLongPolling(numConnections, stopPolling, callback) { async startLongPolling(numConnections, stopPolling, callback) {
log.info('startLongPolling for', numConnections, 'connections');
this.ourSwarmNodes = {}; this.ourSwarmNodes = {};
let nodes = await lokiSnodeAPI.getSwarmNodesForPubKey(this.ourKey); let nodes = await lokiSnodeAPI.getSwarmNodesForPubKey(this.ourKey);
log.info('swarmNodes', nodes.length, 'for', this.ourKey);
Object.keys(nodes).forEach(j => {
const node = nodes[j];
log.info(`${j} ${node.ip}:${node.port}`);
});
if (nodes.length < numConnections) { if (nodes.length < numConnections) {
await lokiSnodeAPI.refreshSwarmNodesForPubKey(this.ourKey); log.warn(
nodes = await lokiSnodeAPI.getSwarmNodesForPubKey(this.ourKey); 'Not enough SwarmNodes for our pubkey in local database, getting current list from blockchain'
);
nodes = await lokiSnodeAPI.refreshSwarmNodesForPubKey(this.ourKey);
if (nodes.length < numConnections) {
log.error(
'Could not get enough SwarmNodes for our pubkey from blockchain'
);
}
} }
log.info(
`There are currently ${
nodes.length
} swarmNodes for pubKey in our local database`
);
for (let i = 0; i < nodes.length; i += 1) { for (let i = 0; i < nodes.length; i += 1) {
const lastHash = await window.Signal.Data.getLastHashBySnode( const lastHash = await window.Signal.Data.getLastHashBySnode(
nodes[i].address nodes[i].address
@ -364,9 +410,13 @@ class LokiMessageAPI {
promises.push(this.openRetrieveConnection(stopPolling, callback)); promises.push(this.openRetrieveConnection(stopPolling, callback));
} }
// blocks until all snodes in our swarms have been removed from the list // blocks until numConnections snodes in our swarms have been removed from the list
// less than numConnections being active is fine, only need to restart if none per Niels 20/02/13
// or if there is network issues (ENOUTFOUND due to lokinet) // or if there is network issues (ENOUTFOUND due to lokinet)
await Promise.all(promises); await Promise.all(promises);
log.error('All our long poll swarm connections have been removed');
// should we just call ourself again?
// no, our caller already handles this...
} }
} }

@ -29,10 +29,10 @@ const decryptResponse = async (response, address) => {
return {}; return {};
}; };
// TODO: Don't allow arbitrary URLs, only snodes and loki servers
const sendToProxy = async (options = {}, targetNode) => { const sendToProxy = async (options = {}, targetNode) => {
const randSnode = await lokiSnodeAPI.getRandomSnodeAddress(); const randSnode = await lokiSnodeAPI.getRandomSnodeAddress();
// Don't allow arbitrary URLs, only snodes and loki servers
const url = `https://${randSnode.ip}:${randSnode.port}/proxy`; const url = `https://${randSnode.ip}:${randSnode.port}/proxy`;
const snPubkeyHex = StringView.hexToArrayBuffer(targetNode.pubkey_x25519); const snPubkeyHex = StringView.hexToArrayBuffer(targetNode.pubkey_x25519);
@ -67,20 +67,59 @@ const sendToProxy = async (options = {}, targetNode) => {
const response = await nodeFetch(url, firstHopOptions); const response = await nodeFetch(url, firstHopOptions);
process.env.NODE_TLS_REJECT_UNAUTHORIZED = 1; process.env.NODE_TLS_REJECT_UNAUTHORIZED = 1;
const ciphertext = await response.text(); // detect SNode is not ready (not in swarm; not done syncing)
if (response.status === 503) {
const ciphertext = await response.text();
log.error(
`lokiRpc sendToProxy snode ${randSnode.ip}:${randSnode.port} error`,
ciphertext
);
// mark as bad for this round (should give it some time and improve success rates)
lokiSnodeAPI.markRandomNodeUnreachable(randSnode);
// retry for a new working snode
return sendToProxy(options, targetNode);
}
const ciphertextBuffer = dcodeIO.ByteBuffer.wrap( // FIXME: handle nodeFetch errors/exceptions...
ciphertext, if (response.status !== 200) {
'base64' // let us know we need to create handlers for new unhandled codes
).toArrayBuffer(); log.warn('lokiRpc sendToProxy fetch non-200 statusCode', response.status);
}
const plaintextBuffer = await window.libloki.crypto.DHDecrypt( const ciphertext = await response.text();
symmetricKey, if (!ciphertext) {
ciphertextBuffer // avoid base64 decode failure
); log.warn('Server did not return any data for', options);
}
const textDecoder = new TextDecoder(); let plaintext;
const plaintext = textDecoder.decode(plaintextBuffer); let ciphertextBuffer;
try {
ciphertextBuffer = dcodeIO.ByteBuffer.wrap(
ciphertext,
'base64'
).toArrayBuffer();
const plaintextBuffer = await window.libloki.crypto.DHDecrypt(
symmetricKey,
ciphertextBuffer
);
const textDecoder = new TextDecoder();
plaintext = textDecoder.decode(plaintextBuffer);
} catch (e) {
log.error(
'lokiRpc sendToProxy decode error',
e.code,
e.message,
`from ${randSnode.ip}:${randSnode.port} ciphertext:`,
ciphertext
);
if (ciphertextBuffer) {
log.error('ciphertextBuffer', ciphertextBuffer);
}
return false;
}
try { try {
const jsonRes = JSON.parse(plaintext); const jsonRes = JSON.parse(plaintext);
@ -90,10 +129,10 @@ const sendToProxy = async (options = {}, targetNode) => {
return JSON.parse(jsonRes.body); return JSON.parse(jsonRes.body);
} catch (e) { } catch (e) {
log.error( log.error(
'lokiRpc sendToProxy error', 'lokiRpc sendToProxy parse error',
e.code, e.code,
e.message, e.message,
'json', `from ${randSnode.ip}:${randSnode.port} json:`,
jsonRes.body jsonRes.body
); );
} }
@ -102,10 +141,10 @@ const sendToProxy = async (options = {}, targetNode) => {
return jsonRes; return jsonRes;
} catch (e) { } catch (e) {
log.error( log.error(
'lokiRpc sendToProxy error', 'lokiRpc sendToProxy parse error',
e.code, e.code,
e.message, e.message,
'json', `from ${randSnode.ip}:${randSnode.port} json:`,
plaintext plaintext
); );
} }
@ -150,7 +189,7 @@ const lokiFetch = async (url, options = {}, targetNode = null) => {
try { try {
if (window.lokiFeatureFlags.useSnodeProxy && targetNode) { if (window.lokiFeatureFlags.useSnodeProxy && targetNode) {
const result = await sendToProxy(fetchOptions, targetNode); const result = await sendToProxy(fetchOptions, targetNode);
return result.json(); return result ? result.json() : false;
} }
if (url.match(/https:\/\//)) { if (url.match(/https:\/\//)) {

@ -4,6 +4,8 @@
const is = require('@sindresorhus/is'); const is = require('@sindresorhus/is');
const { lokiRpc } = require('./loki_rpc'); const { lokiRpc } = require('./loki_rpc');
const RANDOM_SNODES_TO_USE = 3;
class LokiSnodeAPI { class LokiSnodeAPI {
constructor({ serverUrl, localUrl }) { constructor({ serverUrl, localUrl }) {
if (!is.string(serverUrl)) { if (!is.string(serverUrl)) {
@ -18,6 +20,7 @@ class LokiSnodeAPI {
async getRandomSnodeAddress() { async getRandomSnodeAddress() {
/* resolve random snode */ /* resolve random snode */
if (this.randomSnodePool.length === 0) { if (this.randomSnodePool.length === 0) {
// allow exceptions to pass through upwards
await this.initialiseRandomPool(); await this.initialiseRandomPool();
} }
if (this.randomSnodePool.length === 0) { if (this.randomSnodePool.length === 0) {
@ -28,7 +31,10 @@ class LokiSnodeAPI {
]; ];
} }
async initialiseRandomPool(seedNodes = [...window.seedNodeList]) { async initialiseRandomPool(
seedNodes = [...window.seedNodeList],
consecutiveErrors = 0
) {
const params = { const params = {
limit: 20, limit: 20,
active_only: true, active_only: true,
@ -43,8 +49,9 @@ class LokiSnodeAPI {
Math.floor(Math.random() * seedNodes.length), Math.floor(Math.random() * seedNodes.length),
1 1
)[0]; )[0];
let snodes = [];
try { try {
const result = await lokiRpc( const response = await lokiRpc(
`http://${seedNode.ip}`, `http://${seedNode.ip}`,
seedNode.port, seedNode.port,
'get_n_service_nodes', 'get_n_service_nodes',
@ -53,7 +60,7 @@ class LokiSnodeAPI {
'/json_rpc' // Seed request endpoint '/json_rpc' // Seed request endpoint
); );
// Filter 0.0.0.0 nodes which haven't submitted uptime proofs // Filter 0.0.0.0 nodes which haven't submitted uptime proofs
const snodes = result.result.service_node_states.filter( snodes = response.result.service_node_states.filter(
snode => snode.public_ip !== '0.0.0.0' snode => snode.public_ip !== '0.0.0.0'
); );
this.randomSnodePool = snodes.map(snode => ({ this.randomSnodePool = snodes.map(snode => ({
@ -64,12 +71,23 @@ class LokiSnodeAPI {
})); }));
} catch (e) { } catch (e) {
log.warn('initialiseRandomPool error', e.code, e.message); log.warn('initialiseRandomPool error', e.code, e.message);
if (seedNodes.length === 0) { if (consecutiveErrors < 3) {
throw new window.textsecure.SeedNodeError( // retry after a possible delay
'Failed to contact seed node' setTimeout(() => {
); log.info(
'Retrying initialising random snode pool, try #',
consecutiveErrors
);
this.initialiseRandomPool(seedNodes, consecutiveErrors + 1);
}, consecutiveErrors * consecutiveErrors * 5000);
} else {
log.error('Giving up trying to contact seed node');
if (snodes.length === 0) {
throw new window.textsecure.SeedNodeError(
'Failed to contact seed node'
);
}
} }
this.initialiseRandomPool(seedNodes);
} }
} }
@ -111,6 +129,7 @@ class LokiSnodeAPI {
const filteredNodes = newNodes.filter(snode => snode.ip !== '0.0.0.0'); const filteredNodes = newNodes.filter(snode => snode.ip !== '0.0.0.0');
const conversation = ConversationController.get(pubKey); const conversation = ConversationController.get(pubKey);
await conversation.updateSwarmNodes(filteredNodes); await conversation.updateSwarmNodes(filteredNodes);
return filteredNodes;
} catch (e) { } catch (e) {
throw new window.textsecure.ReplayableError({ throw new window.textsecure.ReplayableError({
message: 'Could not get conversation', message: 'Could not get conversation',
@ -120,7 +139,8 @@ class LokiSnodeAPI {
async refreshSwarmNodesForPubKey(pubKey) { async refreshSwarmNodesForPubKey(pubKey) {
const newNodes = await this.getFreshSwarmNodes(pubKey); const newNodes = await this.getFreshSwarmNodes(pubKey);
this.updateSwarmNodes(pubKey, newNodes); const filteredNodes = this.updateSwarmNodes(pubKey, newNodes);
return filteredNodes;
} }
async getFreshSwarmNodes(pubKey) { async getFreshSwarmNodes(pubKey) {
@ -130,6 +150,7 @@ class LokiSnodeAPI {
try { try {
newSwarmNodes = await this.getSwarmNodes(pubKey); newSwarmNodes = await this.getSwarmNodes(pubKey);
} catch (e) { } catch (e) {
log.error('getFreshSwarmNodes error', e.code, e.message);
// TODO: Handle these errors sensibly // TODO: Handle these errors sensibly
newSwarmNodes = []; newSwarmNodes = [];
} }
@ -141,9 +162,7 @@ class LokiSnodeAPI {
return newSwarmNodes; return newSwarmNodes;
} }
async getSwarmNodes(pubKey) { async getSnodesForPubkey(snode, pubKey) {
// TODO: Hit multiple random nodes and merge lists?
const snode = await this.getRandomSnodeAddress();
try { try {
const result = await lokiRpc( const result = await lokiRpc(
`https://${snode.ip}`, `https://${snode.ip}`,
@ -158,7 +177,7 @@ class LokiSnodeAPI {
); );
if (!result) { if (!result) {
log.warn( log.warn(
`getSwarmNodes lokiRpc on ${snode.ip}:${ `getSnodesForPubkey lokiRpc on ${snode.ip}:${
snode.port snode.port
} returned falsish value`, } returned falsish value`,
result result
@ -168,11 +187,39 @@ class LokiSnodeAPI {
const snodes = result.snodes.filter(tSnode => tSnode.ip !== '0.0.0.0'); const snodes = result.snodes.filter(tSnode => tSnode.ip !== '0.0.0.0');
return snodes; return snodes;
} catch (e) { } catch (e) {
log.error('getSwarmNodes error', e.code, e.message); log.error(
'getSnodesForPubkey error',
e.code,
e.message,
`for ${snode.ip}:${snode.port}`
);
this.markRandomNodeUnreachable(snode); this.markRandomNodeUnreachable(snode);
return this.getSwarmNodes(pubKey); return [];
} }
} }
async getSwarmNodes(pubKey) {
const snodes = [];
const questions = [...Array(RANDOM_SNODES_TO_USE).keys()];
await Promise.all(
questions.map(async () => {
// allow exceptions to pass through upwards
const rSnode = await this.getRandomSnodeAddress();
const resList = await this.getSnodesForPubkey(rSnode, pubKey);
// should we only activate entries that are in all results?
resList.map(item => {
const hasItem = snodes.some(
hItem => item.ip === hItem.ip && item.port === hItem.port
);
if (!hasItem) {
snodes.push(item);
}
return true;
});
})
);
return snodes;
}
} }
module.exports = LokiSnodeAPI; module.exports = LokiSnodeAPI;

@ -34,8 +34,6 @@
Whisper.Notifications = new (Backbone.Collection.extend({ Whisper.Notifications = new (Backbone.Collection.extend({
initialize() { initialize() {
this.isEnabled = false; this.isEnabled = false;
this.on('add', this.update);
this.on('remove', this.onRemove);
this.lastNotification = null; this.lastNotification = null;
@ -45,7 +43,11 @@
// and batches up the quick successive update() calls we get from an incoming // and batches up the quick successive update() calls we get from an incoming
// read sync, which might have a number of messages referenced inside of it. // read sync, which might have a number of messages referenced inside of it.
this.fastUpdate = this.update; this.fastUpdate = this.update;
this.update = _.debounce(this.update, 1000); this.update = _.debounce(this.update, 2000);
// make those calls use the debounced function
this.on('add', this.update);
this.on('remove', this.onRemove);
}, },
update() { update() {
if (this.lastNotification) { if (this.lastNotification) {

@ -252,6 +252,10 @@
window.Whisper.events.trigger('inviteFriends', this.model); window.Whisper.events.trigger('inviteFriends', this.model);
}, },
onUpdateGroupName: () => {
window.Whisper.events.trigger('updateGroupName', this.model);
},
onAddModerators: () => { onAddModerators: () => {
window.Whisper.events.trigger('addModerators', this.model); window.Whisper.events.trigger('addModerators', this.model);
}, },
@ -287,6 +291,9 @@
isAdmin: this.model.get('groupAdmins').includes(ourPK), isAdmin: this.model.get('groupAdmins').includes(ourPK),
isRss: this.model.isRss(), isRss: this.model.isRss(),
memberCount: members.length, memberCount: members.length,
amMod: this.model.isModerator(
window.storage.get('primaryDevicePubKey')
),
timerOptions: Whisper.ExpirationTimerOptions.map(item => ({ timerOptions: Whisper.ExpirationTimerOptions.map(item => ({
name: item.getName(), name: item.getName(),
@ -1189,6 +1196,13 @@
const el = this.$(`#${message.id}`); const el = this.$(`#${message.id}`);
const position = el.position(); const position = el.position();
// This message is likely not loaded yet in the DOM
if (!position) {
// should this be break?
// eslint-disable-next-line no-continue
continue;
}
const { top } = position; const { top } = position;
// We're fully below the viewport, continue searching up. // We're fully below the viewport, continue searching up.

@ -54,36 +54,17 @@
this.conversation = groupConvo; this.conversation = groupConvo;
this.titleText = i18n('updateGroupDialogTitle'); this.titleText = i18n('updateGroupDialogTitle');
this.okText = i18n('ok');
this.cancelText = i18n('cancel');
this.close = this.close.bind(this); this.close = this.close.bind(this);
this.onSubmit = this.onSubmit.bind(this); this.onSubmit = this.onSubmit.bind(this);
this.isPublic = groupConvo.isPublic(); this.isPublic = groupConvo.isPublic();
this.groupId = groupConvo.id;
this.members = groupConvo.get('members') || [];
this.avatarPath = groupConvo.getAvatarPath();
const ourPK = textsecure.storage.user.getNumber(); const ourPK = textsecure.storage.user.getNumber();
this.isAdmin = groupConvo.get('groupAdmins').includes(ourPK); this.isAdmin = groupConvo.get('groupAdmins').includes(ourPK);
const convos = window.getConversations().models.filter(d => !!d);
let existingMembers = groupConvo.get('members') || [];
// Show a contact if they are our friend or if they are a member
const friendsAndMembers = convos.filter(
d =>
(d.isFriend() || existingMembers.includes(d.id)) &&
d.isPrivate() &&
!d.isMe()
);
this.friendsAndMembers = _.uniq(friendsAndMembers, true, d => d.id);
// at least make sure it's an array
if (!Array.isArray(existingMembers)) {
existingMembers = [];
}
this.existingMembers = existingMembers;
// public chat settings overrides // public chat settings overrides
if (this.isPublic) { if (this.isPublic) {
// fix the title // fix the title
@ -95,9 +76,6 @@
this.isAdmin = groupConvo.isModerator( this.isAdmin = groupConvo.isModerator(
window.storage.get('primaryDevicePubKey') window.storage.get('primaryDevicePubKey')
); );
// zero out friendList for now
this.friendsAndMembers = [];
this.existingMembers = [];
} }
this.$el.focus(); this.$el.focus();
@ -109,24 +87,23 @@
Component: window.Signal.Components.UpdateGroupNameDialog, Component: window.Signal.Components.UpdateGroupNameDialog,
props: { props: {
titleText: this.titleText, titleText: this.titleText,
groupName: this.groupName,
okText: this.okText,
isPublic: this.isPublic, isPublic: this.isPublic,
cancelText: this.cancelText, groupName: this.groupName,
existingMembers: this.existingMembers, okText: i18n('ok'),
cancelText: i18n('cancel'),
isAdmin: this.isAdmin, isAdmin: this.isAdmin,
onClose: this.close, i18n,
onSubmit: this.onSubmit, onSubmit: this.onSubmit,
onClose: this.close,
avatarPath: this.avatarPath,
}, },
}); });
this.$el.append(this.dialogView.el); this.$el.append(this.dialogView.el);
return this; return this;
}, },
onSubmit(newGroupName, members) { onSubmit(groupName, avatar) {
const groupId = this.conversation.get('id'); window.doUpdateGroup(this.groupId, groupName, this.members, avatar);
window.doUpdateGroup(groupId, newGroupName, members);
}, },
close() { close() {
this.remove(); this.remove();
@ -136,40 +113,15 @@
Whisper.UpdateGroupMembersDialogView = Whisper.View.extend({ Whisper.UpdateGroupMembersDialogView = Whisper.View.extend({
className: 'loki-dialog modal', className: 'loki-dialog modal',
initialize(groupConvo) { initialize(groupConvo) {
const ourPK = textsecure.storage.user.getNumber();
this.groupName = groupConvo.get('name'); this.groupName = groupConvo.get('name');
this.conversation = groupConvo;
this.titleText = i18n('updateGroupDialogTitle');
this.okText = i18n('ok');
this.cancelText = i18n('cancel');
this.close = this.close.bind(this); this.close = this.close.bind(this);
this.onSubmit = this.onSubmit.bind(this); this.onSubmit = this.onSubmit.bind(this);
this.isPublic = groupConvo.isPublic(); this.isPublic = groupConvo.isPublic();
this.groupId = groupConvo.id;
this.avatarPath = groupConvo.getAvatarPath();
const ourPK = textsecure.storage.user.getNumber();
this.isAdmin = groupConvo.get('groupAdmins').includes(ourPK);
const convos = window.getConversations().models.filter(d => !!d);
let existingMembers = groupConvo.get('members') || [];
// Show a contact if they are our friend or if they are a member
const friendsAndMembers = convos.filter(
d => existingMembers.includes(d.id) && d.isPrivate() && !d.isMe()
);
this.friendsAndMembers = _.uniq(friendsAndMembers, true, d => d.id);
// at least make sure it's an array
if (!Array.isArray(existingMembers)) {
existingMembers = [];
}
this.existingMembers = existingMembers;
// public chat settings overrides
if (this.isPublic) { if (this.isPublic) {
// fix the title
this.titleText = `${i18n('updatePublicGroupDialogTitle')}: ${ this.titleText = `${i18n('updatePublicGroupDialogTitle')}: ${
this.groupName this.groupName
}`; }`;
@ -181,6 +133,26 @@
// zero out friendList for now // zero out friendList for now
this.friendsAndMembers = []; this.friendsAndMembers = [];
this.existingMembers = []; this.existingMembers = [];
} else {
this.titleText = i18n('updateGroupDialogTitle');
this.isAdmin = groupConvo.get('groupAdmins').includes(ourPK);
const convos = window.getConversations().models.filter(d => !!d);
this.existingMembers = groupConvo.get('members') || [];
// Show a contact if they are our friend or if they are a member
this.friendsAndMembers = convos.filter(
d => this.existingMembers.includes(d.id) && d.isPrivate() && !d.isMe()
);
this.friendsAndMembers = _.uniq(
this.friendsAndMembers,
true,
d => d.id
);
// at least make sure it's an array
if (!Array.isArray(this.existingMembers)) {
this.existingMembers = [];
}
} }
this.$el.focus(); this.$el.focus();
@ -201,6 +173,7 @@
isAdmin: this.isAdmin, isAdmin: this.isAdmin,
onClose: this.close, onClose: this.close,
onSubmit: this.onSubmit, onSubmit: this.onSubmit,
groupId: this.groupId,
}, },
}); });
@ -210,9 +183,13 @@
onSubmit(groupName, newMembers) { onSubmit(groupName, newMembers) {
const ourPK = textsecure.storage.user.getNumber(); const ourPK = textsecure.storage.user.getNumber();
const allMembers = window.Lodash.concat(newMembers, [ourPK]); const allMembers = window.Lodash.concat(newMembers, [ourPK]);
const groupId = this.conversation.get('id');
window.doUpdateGroup(groupId, groupName, allMembers); window.doUpdateGroup(
this.groupId,
groupName,
allMembers,
this.avatarPath
);
}, },
close() { close() {
this.remove(); this.remove();

@ -74,7 +74,10 @@
newMembers.length + existingMembers.length > newMembers.length + existingMembers.length >
window.CONSTANTS.SMALL_GROUP_SIZE_LIMIT window.CONSTANTS.SMALL_GROUP_SIZE_LIMIT
) { ) {
const msg = window.i18n('maxGroupMembersError', window.CONSTANTS.SMALL_GROUP_SIZE_LIMIT); const msg = window.i18n(
'maxGroupMembersError',
window.CONSTANTS.SMALL_GROUP_SIZE_LIMIT
);
window.pushToast({ window.pushToast({
title: msg, title: msg,

@ -24,6 +24,15 @@
}; };
}, },
registerEvents() {
this.unregisterEvents();
document.addEventListener('keyup', this.props.onClickClose, false);
},
unregisterEvents() {
document.removeEventListener('keyup', this.props.onClickClose, false);
},
render() { render() {
this.$('.session-confirm-wrapper').remove(); this.$('.session-confirm-wrapper').remove();
@ -32,25 +41,29 @@
Component: window.Signal.Components.SessionConfirm, Component: window.Signal.Components.SessionConfirm,
props: this.props, props: this.props,
}); });
this.registerEvents();
this.$el.prepend(this.confirmView.el); this.$el.prepend(this.confirmView.el);
}, },
ok() { ok() {
this.$('.session-confirm-wrapper').remove(); this.$('.session-confirm-wrapper').remove();
this.unregisterEvents();
if (this.props.resolve) { if (this.props.resolve) {
this.props.resolve(); this.props.resolve();
} }
}, },
cancel() { cancel() {
this.$('.session-confirm-wrapper').remove(); this.$('.session-confirm-wrapper').remove();
this.unregisterEvents();
if (this.props.reject) { if (this.props.reject) {
this.props.reject(); this.props.reject();
} }
}, },
onKeyup(event) { onKeyup(event) {
if (event.key === 'Escape' || event.key === 'Esc') { if (event.key === 'Escape' || event.key === 'Esc') {
this.cancel(); this.unregisterEvents();
this.props.onClickClose();
} }
}, },
}); });

@ -1,4 +1,4 @@
/* global window, textsecure, Whisper, dcodeIO, StringView, ConversationController */ /* global window, textsecure, dcodeIO, StringView, ConversationController */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function() { (function() {
@ -109,8 +109,16 @@
} }
async function createContactSyncProtoMessage(conversations) { async function createContactSyncProtoMessage(conversations) {
// Extract required contacts information out of conversations // Extract required contacts information out of conversations
const sessionContacts = conversations.filter(
c => c.isPrivate() && !c.isSecondaryDevice()
);
if (sessionContacts.length === 0) {
return null;
}
const rawContacts = await Promise.all( const rawContacts = await Promise.all(
conversations.map(async conversation => { sessionContacts.map(async conversation => {
const profile = conversation.getLokiProfile(); const profile = conversation.getLokiProfile();
const number = conversation.getNumber(); const number = conversation.getNumber();
const name = profile const name = profile
@ -151,6 +159,63 @@
}); });
return syncMessage; return syncMessage;
} }
function createGroupSyncProtoMessage(conversations) {
// We only want to sync across closed groups that we haven't left
const sessionGroups = conversations.filter(
c => c.isClosedGroup() && !c.get('left') && c.isFriend()
);
if (sessionGroups.length === 0) {
return null;
}
const rawGroups = sessionGroups.map(conversation => ({
id: window.Signal.Crypto.bytesFromString(conversation.id),
name: conversation.get('name'),
members: conversation.get('members') || [],
blocked: conversation.isBlocked(),
expireTimer: conversation.get('expireTimer'),
admins: conversation.get('groupAdmins') || [],
}));
// Convert raw groups to an array of buffers
const groupDetails = rawGroups
.map(x => new textsecure.protobuf.GroupDetails(x))
.map(x => x.encode());
// Serialise array of byteBuffers into 1 byteBuffer
const byteBuffer = serialiseByteBuffers(groupDetails);
const data = new Uint8Array(byteBuffer.toArrayBuffer());
const groups = new textsecure.protobuf.SyncMessage.Groups({
data,
});
const syncMessage = new textsecure.protobuf.SyncMessage({
groups,
});
return syncMessage;
}
function createOpenGroupsSyncProtoMessage(conversations) {
// We only want to sync across open groups that we haven't left
const sessionOpenGroups = conversations.filter(
c => c.isPublic() && !c.isRss() && !c.get('left')
);
if (sessionOpenGroups.length === 0) {
return null;
}
const openGroups = sessionOpenGroups.map(
conversation =>
new textsecure.protobuf.SyncMessage.OpenGroupDetails({
url: conversation.id.split('@').pop(),
channelId: conversation.get('channelId'),
})
);
const syncMessage = new textsecure.protobuf.SyncMessage({
openGroups,
});
return syncMessage;
}
async function sendPairingAuthorisation(authorisation, recipientPubKey) { async function sendPairingAuthorisation(authorisation, recipientPubKey) {
const pairingAuthorisation = createPairingAuthorisationProtoMessage( const pairingAuthorisation = createPairingAuthorisationProtoMessage(
authorisation authorisation
@ -179,13 +244,6 @@
profile, profile,
profileKey, profileKey,
}); });
// Attach contact list
const conversations = await window.Signal.Data.getConversationsWithFriendStatus(
window.friends.friendRequestStatusEnum.friends,
{ ConversationCollection: Whisper.ConversationCollection }
);
const syncMessage = await createContactSyncProtoMessage(conversations);
content.syncMessage = syncMessage;
content.dataMessage = dataMessage; content.dataMessage = dataMessage;
} }
// Send // Send
@ -221,5 +279,7 @@
createPairingAuthorisationProtoMessage, createPairingAuthorisationProtoMessage,
sendUnpairingMessageToSecondary, sendUnpairingMessageToSecondary,
createContactSyncProtoMessage, createContactSyncProtoMessage,
createGroupSyncProtoMessage,
createOpenGroupsSyncProtoMessage,
}; };
})(); })();

@ -131,7 +131,8 @@
if (deviceMapping.isPrimary === '0') { if (deviceMapping.isPrimary === '0') {
const { primaryDevicePubKey } = const { primaryDevicePubKey } =
authorisations.find( authorisations.find(
authorisation => authorisation.secondaryDevicePubKey === pubKey authorisation =>
authorisation && authorisation.secondaryDevicePubKey === pubKey
) || {}; ) || {};
if (primaryDevicePubKey) { if (primaryDevicePubKey) {
// do NOT call getprimaryDeviceMapping recursively // do NOT call getprimaryDeviceMapping recursively

@ -1,6 +1,9 @@
/* global window, mocha, chai, assert, Whisper */ /* global window, mocha, chai, assert, Whisper */
mocha.setup('bdd'); mocha
.setup('bdd')
.fullTrace()
.timeout(10000);
window.assert = chai.assert; window.assert = chai.assert;
window.PROTO_ROOT = '../../protos'; window.PROTO_ROOT = '../../protos';

@ -634,6 +634,11 @@
blockSync: true, blockSync: true,
} }
); );
// Send sync messages
const conversations = window.getConversations().models;
textsecure.messaging.sendContactSyncMessage(conversations);
textsecure.messaging.sendGroupSyncMessage(conversations);
textsecure.messaging.sendOpenGroupsSyncMessage(conversations);
}, },
validatePubKeyHex(pubKey) { validatePubKeyHex(pubKey) {
const c = new Whisper.Conversation({ const c = new Whisper.Conversation({

@ -100,6 +100,7 @@
); );
} catch (e) { } catch (e) {
// we'll try again anyway // we'll try again anyway
window.log.error('http-resource pollServer error', e.code, e.message);
} }
if (this.calledStop) { if (this.calledStop) {
@ -109,6 +110,10 @@
connected = false; connected = false;
// Exhausted all our snodes urls, trying again later from scratch // Exhausted all our snodes urls, trying again later from scratch
setTimeout(() => { setTimeout(() => {
window.log.info(
`Exhausted all our snodes urls, trying again in ${EXHAUSTED_SNODES_RETRY_DELAY /
1000}s from scratch`
);
this.pollServer(); this.pollServer();
}, EXHAUSTED_SNODES_RETRY_DELAY); }, EXHAUSTED_SNODES_RETRY_DELAY);
}; };

@ -24,6 +24,8 @@
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
/* eslint-disable no-unreachable */ /* eslint-disable no-unreachable */
let openGroupBound = false;
function MessageReceiver(username, password, signalingKey, options = {}) { function MessageReceiver(username, password, signalingKey, options = {}) {
this.count = 0; this.count = 0;
@ -51,11 +53,18 @@ function MessageReceiver(username, password, signalingKey, options = {}) {
// only do this once to prevent duplicates // only do this once to prevent duplicates
if (lokiPublicChatAPI) { if (lokiPublicChatAPI) {
// bind events window.log.info('Binding open group events handler', openGroupBound);
lokiPublicChatAPI.on( if (!openGroupBound) {
'publicMessage', // clear any previous binding
this.handleUnencryptedMessage.bind(this) lokiPublicChatAPI.removeAllListeners('publicMessage');
); // we only need one MR in the system handling these
// bind events
lokiPublicChatAPI.on(
'publicMessage',
this.handleUnencryptedMessage.bind(this)
);
openGroupBound = true;
}
} else { } else {
window.log.error('Can not handle open group data, API is not available'); window.log.error('Can not handle open group data, API is not available');
} }
@ -1469,18 +1478,24 @@ MessageReceiver.prototype.extend({
this.removeFromCache(envelope); this.removeFromCache(envelope);
}, },
async handleSyncMessage(envelope, syncMessage) { async handleSyncMessage(envelope, syncMessage) {
// We should only accept sync messages from our devices
const ourNumber = textsecure.storage.user.getNumber(); const ourNumber = textsecure.storage.user.getNumber();
// NOTE: Maybe we should be caching this list? const ourPrimaryNumber = window.storage.get('primaryDevicePubKey');
const ourDevices = await libloki.storage.getAllDevicePubKeysForPrimaryPubKey( const ourOtherDevices = await libloki.storage.getAllDevicePubKeysForPrimaryPubKey(
window.storage.get('primaryDevicePubKey') window.storage.get('primaryDevicePubKey')
); );
const validSyncSender = const ourDevices = new Set([
ourDevices && ourDevices.some(devicePubKey => devicePubKey === ourNumber); ourNumber,
ourPrimaryNumber,
...ourOtherDevices,
]);
const validSyncSender = ourDevices.has(envelope.source);
if (!validSyncSender) { if (!validSyncSender) {
throw new Error( throw new Error(
"Received sync message from a device we aren't paired with" "Received sync message from a device we aren't paired with"
); );
} }
if (syncMessage.sent) { if (syncMessage.sent) {
const sentMessage = syncMessage.sent; const sentMessage = syncMessage.sent;
const to = sentMessage.message.group const to = sentMessage.message.group
@ -1499,6 +1514,8 @@ MessageReceiver.prototype.extend({
return this.handleContacts(envelope, syncMessage.contacts); return this.handleContacts(envelope, syncMessage.contacts);
} else if (syncMessage.groups) { } else if (syncMessage.groups) {
return this.handleGroups(envelope, syncMessage.groups); return this.handleGroups(envelope, syncMessage.groups);
} else if (syncMessage.openGroups) {
return this.handleOpenGroups(envelope, syncMessage.openGroups);
} else if (syncMessage.blocked) { } else if (syncMessage.blocked) {
return this.handleBlocked(envelope, syncMessage.blocked); return this.handleBlocked(envelope, syncMessage.blocked);
} else if (syncMessage.request) { } else if (syncMessage.request) {
@ -1574,11 +1591,10 @@ MessageReceiver.prototype.extend({
}, },
handleGroups(envelope, groups) { handleGroups(envelope, groups) {
window.log.info('group sync'); window.log.info('group sync');
const { blob } = groups;
// Note: we do not return here because we don't want to block the next message on // Note: we do not return here because we don't want to block the next message on
// this attachment download and a lot of processing of that attachment. // this attachment download and a lot of processing of that attachment.
this.handleAttachment(blob).then(attachmentPointer => { this.handleAttachment(groups).then(attachmentPointer => {
const groupBuffer = new GroupBuffer(attachmentPointer.data); const groupBuffer = new GroupBuffer(attachmentPointer.data);
let groupDetails = groupBuffer.next(); let groupDetails = groupBuffer.next();
const promises = []; const promises = [];
@ -1601,6 +1617,12 @@ MessageReceiver.prototype.extend({
}); });
}); });
}, },
handleOpenGroups(envelope, openGroups) {
openGroups.forEach(({ url, channelId }) => {
window.attemptConnection(url, channelId);
});
return this.removeFromCache(envelope);
},
handleBlocked(envelope, blocked) { handleBlocked(envelope, blocked) {
window.log.info('Setting these numbers as blocked:', blocked.numbers); window.log.info('Setting these numbers as blocked:', blocked.numbers);
textsecure.storage.put('blocked', blocked.numbers); textsecure.storage.put('blocked', blocked.numbers);
@ -1786,6 +1808,10 @@ MessageReceiver.prototype.extend({
decrypted.group.members = []; decrypted.group.members = [];
decrypted.group.avatar = null; decrypted.group.avatar = null;
break; break;
case textsecure.protobuf.GroupContext.Type.REQUEST_INFO:
decrypted.body = null;
decrypted.attachments = [];
break;
default: default:
this.removeFromCache(envelope); this.removeFromCache(envelope);
throw new Error('Unknown group message type'); throw new Error('Unknown group message type');

@ -664,31 +664,90 @@ MessageSender.prototype = {
return Promise.resolve(); return Promise.resolve();
}, },
async sendContactSyncMessage(contactConversation) { async sendContactSyncMessage(conversations) {
if (!contactConversation.isPrivate()) { // If we havn't got a primaryDeviceKey then we are in the middle of pairing
// primaryDevicePubKey is set to our own number if we are the master device
const primaryDeviceKey = window.storage.get('primaryDevicePubKey');
if (!primaryDeviceKey) {
return Promise.resolve(); return Promise.resolve();
} }
// We need to sync across 3 contacts at a time
// This is to avoid hitting storage server limit
const chunked = _.chunk(conversations, 3);
const syncMessages = await Promise.all(
chunked.map(c => libloki.api.createContactSyncProtoMessage(c))
);
const syncPromises = syncMessages
.filter(message => message != null)
.map(syncMessage => {
const contentMessage = new textsecure.protobuf.Content();
contentMessage.syncMessage = syncMessage;
const silent = true;
return this.sendIndividualProto(
primaryDeviceKey,
contentMessage,
Date.now(),
silent,
{} // options
);
});
return Promise.all(syncPromises);
},
sendGroupSyncMessage(conversations) {
// If we havn't got a primaryDeviceKey then we are in the middle of pairing
// primaryDevicePubKey is set to our own number if we are the master device
const primaryDeviceKey = window.storage.get('primaryDevicePubKey'); const primaryDeviceKey = window.storage.get('primaryDevicePubKey');
const allOurDevices = (await libloki.storage.getAllDevicePubKeysForPrimaryPubKey( if (!primaryDeviceKey) {
primaryDeviceKey return Promise.resolve();
)) }
// Don't send to ourselves
.filter(pubKey => pubKey !== textsecure.storage.user.getNumber()); // We need to sync across 1 group at a time
if ( // This is because we could hit the storage server limit with one group
allOurDevices.includes(contactConversation.id) || const syncPromises = conversations
!primaryDeviceKey || .map(c => libloki.api.createGroupSyncProtoMessage([c]))
allOurDevices.length === 0 .filter(message => message != null)
) { .map(syncMessage => {
// If we havn't got a primaryDeviceKey then we are in the middle of pairing const contentMessage = new textsecure.protobuf.Content();
contentMessage.syncMessage = syncMessage;
const silent = true;
return this.sendIndividualProto(
primaryDeviceKey,
contentMessage,
Date.now(),
silent,
{} // options
);
});
return Promise.all(syncPromises);
},
sendOpenGroupsSyncMessage(conversations) {
// If we havn't got a primaryDeviceKey then we are in the middle of pairing
// primaryDevicePubKey is set to our own number if we are the master device
const primaryDeviceKey = window.storage.get('primaryDevicePubKey');
if (!primaryDeviceKey) {
return Promise.resolve();
}
// Send the whole list of open groups in a single message
const openGroupsSyncMessage = libloki.api.createOpenGroupsSyncProtoMessage(
conversations
);
if (!openGroupsSyncMessage) {
window.log.info('No open groups to sync');
return Promise.resolve(); return Promise.resolve();
} }
const syncMessage = await libloki.api.createContactSyncProtoMessage([
contactConversation,
]);
const contentMessage = new textsecure.protobuf.Content(); const contentMessage = new textsecure.protobuf.Content();
contentMessage.syncMessage = syncMessage; contentMessage.syncMessage = openGroupsSyncMessage;
const silent = true; const silent = true;
return this.sendIndividualProto( return this.sendIndividualProto(
@ -1107,7 +1166,7 @@ MessageSender.prototype = {
return this.sendMessage(attrs, options); return this.sendMessage(attrs, options);
}, },
updateGroup(groupId, name, avatar, members, recipients, options) { updateGroup(groupId, name, avatar, members, admins, recipients, options) {
const proto = new textsecure.protobuf.DataMessage(); const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext(); proto.group = new textsecure.protobuf.GroupContext();
@ -1164,6 +1223,14 @@ MessageSender.prototype = {
}); });
}, },
requestGroupInfo(groupId, groupNumbers, options) {
const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
proto.group.id = stringToArrayBuffer(groupId);
proto.group.type = textsecure.protobuf.GroupContext.Type.REQUEST_INFO;
return this.sendGroupProto(groupNumbers, proto, Date.now(), options);
},
leaveGroup(groupId, groupNumbers, options) { leaveGroup(groupId, groupNumbers, options) {
const proto = new textsecure.protobuf.DataMessage(); const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext(); proto.group = new textsecure.protobuf.GroupContext();
@ -1251,6 +1318,10 @@ textsecure.MessageSender = function MessageSenderWrapper(username, password) {
sender sender
); );
this.sendContactSyncMessage = sender.sendContactSyncMessage.bind(sender); this.sendContactSyncMessage = sender.sendContactSyncMessage.bind(sender);
this.sendGroupSyncMessage = sender.sendGroupSyncMessage.bind(sender);
this.sendOpenGroupsSyncMessage = sender.sendOpenGroupsSyncMessage.bind(
sender
);
this.sendRequestConfigurationSyncMessage = sender.sendRequestConfigurationSyncMessage.bind( this.sendRequestConfigurationSyncMessage = sender.sendRequestConfigurationSyncMessage.bind(
sender sender
); );
@ -1263,6 +1334,7 @@ textsecure.MessageSender = function MessageSenderWrapper(username, password) {
this.addNumberToGroup = sender.addNumberToGroup.bind(sender); this.addNumberToGroup = sender.addNumberToGroup.bind(sender);
this.setGroupName = sender.setGroupName.bind(sender); this.setGroupName = sender.setGroupName.bind(sender);
this.setGroupAvatar = sender.setGroupAvatar.bind(sender); this.setGroupAvatar = sender.setGroupAvatar.bind(sender);
this.requestGroupInfo = sender.requestGroupInfo.bind(sender);
this.leaveGroup = sender.leaveGroup.bind(sender); this.leaveGroup = sender.leaveGroup.bind(sender);
this.sendSyncMessage = sender.sendSyncMessage.bind(sender); this.sendSyncMessage = sender.sendSyncMessage.bind(sender);
this.getProfile = sender.getProfile.bind(sender); this.getProfile = sender.getProfile.bind(sender);

@ -1,6 +1,9 @@
/* global mocha, chai, assert */ /* global mocha, chai, assert */
mocha.setup('bdd'); mocha
.setup('bdd')
.fullTrace()
.timeout(10000);
window.assert = chai.assert; window.assert = chai.assert;
window.PROTO_ROOT = '../../protos'; window.PROTO_ROOT = '../../protos';

@ -65,9 +65,7 @@ const appInstance = config.util.getEnv('NODE_APP_INSTANCE') || 0;
const attachments = require('./app/attachments'); const attachments = require('./app/attachments');
const attachmentChannel = require('./app/attachment_channel'); const attachmentChannel = require('./app/attachment_channel');
// TODO: Enable when needed const updater = require('./ts/updater/index');
// const updater = require('./ts/updater/index');
const updater = null;
const createTrayIcon = require('./app/tray_icon'); const createTrayIcon = require('./app/tray_icon');
const ephemeralConfig = require('./app/ephemeral_config'); const ephemeralConfig = require('./app/ephemeral_config');
@ -227,6 +225,7 @@ function createWindow() {
minWidth: MIN_WIDTH, minWidth: MIN_WIDTH,
minHeight: MIN_HEIGHT, minHeight: MIN_HEIGHT,
autoHideMenuBar: false, autoHideMenuBar: false,
backgroundColor: '#fff',
webPreferences: { webPreferences: {
nodeIntegration: false, nodeIntegration: false,
nodeIntegrationInWorker: false, nodeIntegrationInWorker: false,
@ -286,7 +285,7 @@ function createWindow() {
// Disable system main menu // Disable system main menu
mainWindow.setMenu(null); mainWindow.setMenu(null);
electronLocalshortcut.register(mainWindow, 'f5', () => { electronLocalshortcut.register(mainWindow, 'F5', () => {
mainWindow.reload(); mainWindow.reload();
}); });
electronLocalshortcut.register(mainWindow, 'CommandOrControl+R', () => { electronLocalshortcut.register(mainWindow, 'CommandOrControl+R', () => {
@ -409,38 +408,56 @@ ipc.on('show-window', () => {
showWindow(); showWindow();
}); });
let updatesStarted = false; let isReadyForUpdates = false;
ipc.on('ready-for-updates', async () => { async function readyForUpdates() {
if (updatesStarted || !updater) { if (isReadyForUpdates) {
return; return;
} }
updatesStarted = true;
isReadyForUpdates = true;
// disable for now
/*
// First, install requested sticker pack
const incomingUrl = getIncomingUrl(process.argv);
if (incomingUrl) {
handleSgnlLink(incomingUrl);
}
*/
// Second, start checking for app updates
try { try {
await updater.start(getMainWindow, locale.messages, logger); await updater.start(getMainWindow, locale.messages, logger);
} catch (error) { } catch (error) {
logger.error( const log = logger || console;
log.error(
'Error starting update checks:', 'Error starting update checks:',
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
} }
}); }
ipc.once('ready-for-updates', readyForUpdates);
// Forcefully call readyForUpdates after 10 minutes.
// This ensures we start the updater.
const TEN_MINUTES = 10 * 60 * 1000;
setTimeout(readyForUpdates, TEN_MINUTES);
function openReleaseNotes() { function openReleaseNotes() {
shell.openExternal( shell.openExternal(
`https://github.com/loki-project/loki-messenger/releases/tag/v${app.getVersion()}` `https://github.com/loki-project/session-desktop/releases/tag/v${app.getVersion()}`
); );
} }
function openNewBugForm() { function openNewBugForm() {
shell.openExternal( shell.openExternal(
'https://github.com/loki-project/loki-messenger/issues/new' 'https://github.com/loki-project/session-desktop/issues/new'
); );
} }
function openSupportPage() { function openSupportPage() {
shell.openExternal( shell.openExternal(
'https://loki-project.github.io/loki-docs/LokiServices/Messenger/' 'https://docs.loki.network/LokiServices/Messenger/Session/'
); );
} }
@ -841,6 +858,9 @@ async function showMainWindow(sqlKey, passwordAttempt = false) {
} }
setupMenu(); setupMenu();
// Check updates
readyForUpdates();
} }
function setupMenu(options) { function setupMenu(options) {
@ -1025,6 +1045,7 @@ ipc.on('password-window-login', async (event, passPhrase) => {
const passwordAttempt = true; const passwordAttempt = true;
await showMainWindow(passPhrase, passwordAttempt); await showMainWindow(passPhrase, passwordAttempt);
sendResponse(); sendResponse();
if (passwordWindow) { if (passwordWindow) {
passwordWindow.close(); passwordWindow.close();
passwordWindow = null; passwordWindow = null;

@ -2,13 +2,16 @@
"name": "session-messenger-desktop", "name": "session-messenger-desktop",
"productName": "Session", "productName": "Session",
"description": "Private messaging from your desktop", "description": "Private messaging from your desktop",
"repository": "https://github.com/loki-project/loki-messenger.git", "version": "1.0.3",
"version": "1.0.2",
"license": "GPL-3.0", "license": "GPL-3.0",
"author": { "author": {
"name": "Loki Project", "name": "Loki Project",
"email": "team@loki.network" "email": "team@loki.network"
}, },
"repository": {
"type": "git",
"url": "https://github.com/loki-project/session-desktop.git"
},
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
"postinstall": "electron-builder install-app-deps && rimraf node_modules/dtrace-provider", "postinstall": "electron-builder install-app-deps && rimraf node_modules/dtrace-provider",
@ -25,7 +28,6 @@
"build": "electron-builder --config.extraMetadata.environment=$SIGNAL_ENV", "build": "electron-builder --config.extraMetadata.environment=$SIGNAL_ENV",
"build-release": "cross-env SIGNAL_ENV=production npm run build -- --config.directories.output=release", "build-release": "cross-env SIGNAL_ENV=production npm run build -- --config.directories.output=release",
"make:linux:x64:appimage": "electron-builder build --linux appimage --x64", "make:linux:x64:appimage": "electron-builder build --linux appimage --x64",
"sign-release": "node ts/updater/generateSignature.js",
"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", "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",
"clean-module-protobuf": "rm -f ts/protobuf/compiled.d.ts ts/protobuf/compiled.js", "clean-module-protobuf": "rm -f ts/protobuf/compiled.d.ts ts/protobuf/compiled.js",
"build-protobuf": "yarn build-module-protobuf", "build-protobuf": "yarn build-module-protobuf",
@ -42,6 +44,7 @@
"test-node-coverage": "nyc --reporter=lcov --reporter=text mocha --recursive test/app test/modules ts/test libloki/test/node", "test-node-coverage": "nyc --reporter=lcov --reporter=text mocha --recursive test/app test/modules ts/test libloki/test/node",
"test-node-coverage-html": "nyc --reporter=lcov --reporter=html mocha --recursive test/a/* */pp test/modules ts/test libloki/test/node", "test-node-coverage-html": "nyc --reporter=lcov --reporter=html mocha --recursive test/a/* */pp test/modules ts/test libloki/test/node",
"eslint": "eslint --cache .", "eslint": "eslint --cache .",
"eslint-fix": "eslint --fix .",
"eslint-full": "eslint .", "eslint-full": "eslint .",
"lint": "yarn format --list-different && yarn lint-windows", "lint": "yarn format --list-different && yarn lint-windows",
"lint-full": "yarn format-full --list-different; yarn lint-windows-full", "lint-full": "yarn format-full --list-different; yarn lint-windows-full",
@ -60,7 +63,7 @@
"ready": "yarn clean-transpile && yarn grunt && yarn lint-full && yarn test-node && yarn test-electron && yarn lint-deps" "ready": "yarn clean-transpile && yarn grunt && yarn lint-full && yarn test-node && yarn test-electron && yarn lint-deps"
}, },
"dependencies": { "dependencies": {
"@journeyapps/sqlcipher": "https://github.com/scottnonnenberg-signal/node-sqlcipher.git#2e28733b61640556b0272a3bfc78b0357daf71e6", "@journeyapps/sqlcipher": "https://github.com/scottnonnenberg-signal/node-sqlcipher.git#b10f232fac62ba7f8775c9e086bb5558fe7d948b",
"@sindresorhus/is": "0.8.0", "@sindresorhus/is": "0.8.0",
"@types/dompurify": "^2.0.0", "@types/dompurify": "^2.0.0",
"@types/rc-slider": "^8.6.5", "@types/rc-slider": "^8.6.5",
@ -77,8 +80,9 @@
"dompurify": "^2.0.7", "dompurify": "^2.0.7",
"electron-context-menu": "^0.15.0", "electron-context-menu": "^0.15.0",
"electron-editor-context-menu": "1.1.1", "electron-editor-context-menu": "1.1.1",
"electron-is-dev": "0.3.0", "electron-is-dev": "^1.1.0",
"electron-localshortcut": "^3.2.1", "electron-localshortcut": "^3.2.1",
"electron-updater": "^4.2.2",
"emoji-datasource": "4.0.0", "emoji-datasource": "4.0.0",
"emoji-datasource-apple": "4.0.0", "emoji-datasource-apple": "4.0.0",
"emoji-js": "3.4.0", "emoji-js": "3.4.0",
@ -124,7 +128,7 @@
"reselect": "4.0.0", "reselect": "4.0.0",
"rimraf": "2.6.2", "rimraf": "2.6.2",
"semver": "5.4.1", "semver": "5.4.1",
"spellchecker": "3.5.1", "spellchecker": "3.7.0",
"tar": "4.4.8", "tar": "4.4.8",
"testcheck": "1.0.0-rc.2", "testcheck": "1.0.0-rc.2",
"tmp": "0.0.33", "tmp": "0.0.33",
@ -138,6 +142,7 @@
"@types/classnames": "2.2.3", "@types/classnames": "2.2.3",
"@types/color": "^3.0.0", "@types/color": "^3.0.0",
"@types/config": "0.0.34", "@types/config": "0.0.34",
"@types/electron-is-dev": "^1.1.1",
"@types/filesize": "3.6.0", "@types/filesize": "3.6.0",
"@types/fs-extra": "5.0.5", "@types/fs-extra": "5.0.5",
"@types/google-libphonenumber": "7.4.14", "@types/google-libphonenumber": "7.4.14",
@ -207,12 +212,13 @@
"build": { "build": {
"appId": "com.loki-project.messenger-desktop", "appId": "com.loki-project.messenger-desktop",
"afterSign": "build/notarize.js", "afterSign": "build/notarize.js",
"artifactName": "${name}-${os}-${version}.${ext}", "artifactName": "${name}-${os}-${arch}-${version}.${ext}",
"mac": { "mac": {
"category": "public.app-category.social-networking", "category": "public.app-category.social-networking",
"icon": "build/icons/mac/icon.icns", "icon": "build/icons/mac/icon.icns",
"target": [ "target": [
"dmg" "dmg",
"zip"
], ],
"bundleVersion": "1", "bundleVersion": "1",
"hardenedRuntime": true, "hardenedRuntime": true,
@ -226,6 +232,7 @@
"win": { "win": {
"asarUnpack": "node_modules/spellchecker/vendor/hunspell_dictionaries", "asarUnpack": "node_modules/spellchecker/vendor/hunspell_dictionaries",
"publisherName": "Loki Project", "publisherName": "Loki Project",
"verifyUpdateCodeSignature": false,
"icon": "build/icons/win/icon.ico", "icon": "build/icons/win/icon.ico",
"target": [ "target": [
"nsis" "nsis"
@ -315,7 +322,10 @@
"node_modules/socks/build/client/*.js", "node_modules/socks/build/client/*.js",
"node_modules/smart-buffer/build/*.js", "node_modules/smart-buffer/build/*.js",
"!node_modules/@journeyapps/sqlcipher/deps/*", "!node_modules/@journeyapps/sqlcipher/deps/*",
"!build/*.js" "!node_modules/@journeyapps/sqlcipher/build/*",
"!node_modules/@journeyapps/sqlcipher/lib/binding/node-*",
"!build/*.js",
"!dev-app-update.yml"
] ]
} }
} }

@ -2,6 +2,8 @@
/* global window: false */ /* global window: false */
const path = require('path'); const path = require('path');
const electron = require('electron'); const electron = require('electron');
const { webFrame } = electron;
const semver = require('semver'); const semver = require('semver');
const { deferredToPromise } = require('./js/modules/deferred_to_promise'); const { deferredToPromise } = require('./js/modules/deferred_to_promise');
@ -70,6 +72,7 @@ window.CONSTANTS = {
MAX_MESSAGE_BODY_LENGTH: 64 * 1024, MAX_MESSAGE_BODY_LENGTH: 64 * 1024,
// Limited due to the proof-of-work requirement // Limited due to the proof-of-work requirement
SMALL_GROUP_SIZE_LIMIT: 10, SMALL_GROUP_SIZE_LIMIT: 10,
NOTIFICATION_ENABLE_TIMEOUT_SECONDS: 10, // number of seconds to turn on notifications after reconnect/start of app
}; };
window.versionInfo = { window.versionInfo = {
@ -84,6 +87,19 @@ window.wrapDeferred = deferredToPromise;
const ipc = electron.ipcRenderer; const ipc = electron.ipcRenderer;
const localeMessages = ipc.sendSync('locale-data'); const localeMessages = ipc.sendSync('locale-data');
window.updateZoomFactor = () => {
const zoomFactor = window.getSettingValue('zoom-factor-setting') || 100;
window.setZoomFactor(zoomFactor / 100);
};
window.setZoomFactor = number => {
webFrame.setZoomFactor(number);
};
window.getZoomFactor = () => {
webFrame.getZoomFactor();
};
window.setBadgeCount = count => ipc.send('set-badge-count', count); window.setBadgeCount = count => ipc.send('set-badge-count', count);
// Set the password for the database // Set the password for the database
@ -217,6 +233,10 @@ window.getSettingValue = (settingID, comparisonValue = null) => {
window.setSettingValue = (settingID, value) => { window.setSettingValue = (settingID, value) => {
window.storage.put(settingID, value); window.storage.put(settingID, value);
if (settingID === 'zoom-factor-setting') {
window.updateZoomFactor();
}
}; };
installGetter('device-name', 'getDeviceName'); installGetter('device-name', 'getDeviceName');
@ -474,23 +494,6 @@ contextMenu({
// /tmp mounted as noexec on Linux. // /tmp mounted as noexec on Linux.
require('./js/spell_check'); require('./js/spell_check');
if (config.environment === 'test') {
const isTravis = 'TRAVIS' in process.env && 'CI' in process.env;
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,
isTravis,
isWindows,
};
/* eslint-enable global-require, import/no-extraneous-dependencies */
}
window.shortenPubkey = pubkey => `(...${pubkey.substring(pubkey.length - 6)})`; window.shortenPubkey = pubkey => `(...${pubkey.substring(pubkey.length - 6)})`;
window.pubkeyPattern = /@[a-fA-F0-9]{64,66}\b/g; window.pubkeyPattern = /@[a-fA-F0-9]{64,66}\b/g;
@ -508,3 +511,26 @@ Promise.prototype.ignore = function() {
// eslint-disable-next-line more/no-then // eslint-disable-next-line more/no-then
this.then(() => {}); this.then(() => {});
}; };
if (config.environment.includes('test')) {
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 = {};
window.lokiSnodeAPI = {
refreshSwarmNodesForPubKey: () => [],
getFreshSwarmNodes: () => [],
updateSwarmNodes: () => {},
updateLastHash: () => {},
getSwarmNodesForPubKey: () => [],
};
}

@ -282,6 +282,7 @@ message SyncMessage {
message Groups { message Groups {
optional AttachmentPointer blob = 1; optional AttachmentPointer blob = 1;
optional bytes data = 101;
} }
message Blocked { message Blocked {
@ -313,15 +314,21 @@ message SyncMessage {
optional bool linkPreviews = 4; optional bool linkPreviews = 4;
} }
optional Sent sent = 1; message OpenGroupDetails {
optional Contacts contacts = 2; optional string url = 1;
optional Groups groups = 3; optional uint32 channelId = 2;
optional Request request = 4; }
repeated Read read = 5;
optional Blocked blocked = 6; optional Sent sent = 1;
optional Verified verified = 7; optional Contacts contacts = 2;
optional Configuration configuration = 9; optional Groups groups = 3;
optional bytes padding = 8; optional Request request = 4;
repeated Read read = 5;
optional Blocked blocked = 6;
optional Verified verified = 7;
optional Configuration configuration = 9;
optional bytes padding = 8;
repeated OpenGroupDetails openGroups = 100;
} }
message AttachmentPointer { message AttachmentPointer {
@ -390,4 +397,5 @@ message GroupDetails {
optional uint32 expireTimer = 6; optional uint32 expireTimer = 6;
optional string color = 7; optional string color = 7;
optional bool blocked = 8; optional bool blocked = 8;
repeated string admins = 9;
} }

@ -11,6 +11,7 @@
} }
.edit-profile-dialog, .edit-profile-dialog,
.create-group-dialog,
.user-details-dialog { .user-details-dialog {
.content { .content {
max-width: 100% !important; max-width: 100% !important;

@ -1020,29 +1020,6 @@ label {
} }
} }
.image-upload-section {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
cursor: pointer;
width: 80px;
height: 80px;
border-radius: 100%;
background-color: rgba($session-color-black, 0.72);
box-shadow: 0px 0px 3px 0.5px rgba(0, 0, 0, 0.75);
opacity: 0;
transition: $session-transition-duration;
&:after {
content: 'Edit';
}
&:hover {
opacity: 1;
}
}
.session-id-section { .session-id-section {
display: flex; display: flex;
align-items: center; align-items: center;
@ -1099,6 +1076,29 @@ label {
} }
} }
.image-upload-section {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
cursor: pointer;
width: 80px;
height: 80px;
border-radius: 100%;
background-color: rgba($session-color-black, 0.72);
box-shadow: 0px 0px 3px 0.5px rgba(0, 0, 0, 0.75);
opacity: 0;
transition: $session-transition-duration;
&:after {
content: 'Edit';
}
&:hover {
opacity: 1;
}
}
.qr-image { .qr-image {
display: flex; display: flex;
justify-content: center; justify-content: center;

@ -215,7 +215,6 @@ $session-compose-margin: 20px;
&__list { &__list {
height: -webkit-fill-available; height: -webkit-fill-available;
border-top: 1px solid rgba(255, 255, 255, 0.05);
&-popup { &-popup {
width: -webkit-fill-available; width: -webkit-fill-available;
@ -642,14 +641,20 @@ $session-compose-margin: 20px;
.panel-text-divider { .panel-text-divider {
width: 100%; width: 100%;
text-align: center; text-align: center;
border-bottom: 1px solid $session-color-dark-grey; display: flex;
line-height: 0.1em;
margin: 50px 0 50px; margin: 50px 0 50px;
.panel-text-divider-line {
border-bottom: 1px solid $session-color-dark-grey;
line-height: 0.1em;
flex-grow: 1;
height: 1px;
align-self: center;
}
span { span {
padding: 5px 10px; padding: 5px 10px;
border-radius: 50px; border-radius: 50px;
background-color: $session-background;
color: $session-color-light-grey; color: $session-color-light-grey;
border: 1px solid $session-color-dark-grey; border: 1px solid $session-color-dark-grey;
font-family: 'SF Pro Text'; font-family: 'SF Pro Text';

@ -1,6 +1,9 @@
/* global chai, Whisper, _, Backbone */ /* global chai, Whisper, _, Backbone */
mocha.setup('bdd'); mocha
.setup('bdd')
.fullTrace()
.timeout(10000);
window.assert = chai.assert; window.assert = chai.assert;
window.PROTO_ROOT = '../protos'; window.PROTO_ROOT = '../protos';

@ -239,20 +239,12 @@ describe('Backup', () => {
it('exports then imports to produce the same data we started with', async function thisNeeded() { it('exports then imports to produce the same data we started with', async function thisNeeded() {
this.timeout(6000); this.timeout(6000);
const { const { attachmentsPath, fse, glob, path, tmp, isWindows } = window.test;
attachmentsPath,
fse, // Skip this test on windows
glob,
path,
tmp,
isTravis,
isWindows,
} = window.test;
// Skip this test on travis windows
// because it always fails due to lstat permission error. // because it always fails due to lstat permission error.
// Don't know how to fix it so this is a temp work around. // Don't know how to fix it so this is a temp work around.
if (isTravis && isWindows) { if (isWindows) {
console.log( console.log(
'Skipping exports then imports to produce the same data we started' 'Skipping exports then imports to produce the same data we started'
); );

@ -1,7 +1,7 @@
<html> <html>
<head> <head>
<meta charset='utf-8'> <meta charset="utf-8">
<title>TextSecure test runner</title> <title>TextSecure test runner</title>
<link rel="stylesheet" href="../node_modules/mocha/mocha.css" /> <link rel="stylesheet" href="../node_modules/mocha/mocha.css" />
<link rel="stylesheet" href="../stylesheets/manifest.css" /> <link rel="stylesheet" href="../stylesheets/manifest.css" />
@ -11,90 +11,90 @@
</div> </div>
<div id="tests"> <div id="tests">
</div> </div>
<div id="render-light-theme" class='index' style="width: 800; height: 500; margin:10px; border: solid 1px black;"> <div id="render-light-theme" class="index" style="width: 800; height: 500; margin:10px; border: solid 1px black;">
</div> </div>
<div id="render-dark-theme" class='index' style="width: 800; height: 500; margin:10px; border: solid 1px black;"> <div id="render-dark-theme" class="index" style="width: 800; height: 500; margin:10px; border: solid 1px black;">
</div> </div>
</div> </div>
<script type='text/x-tmpl-mustache' id='app-loading-screen'> <script type="text/x-tmpl-mustache" id="app-loading-screen">
<div class='content'> <div class="content">
<img src='images/session/full-logo.svg' class='session-full-logo' /> <img src="images/session/full-logo.svg" class="session-full-logo" />
</div> </div>
</script> </script>
<script type='text/x-tmpl-mustache' id='conversation-loading-screen'> <script type="text/x-tmpl-mustache" id="conversation-loading-screen">
<div class='content'> <div class="content">
<img src='images/session/full-logo.svg' class='session-full-logo' /> <img src="images/session/full-logo.svg" class="session-full-logo" />
</div> </div>
</script> </script>
<script type='text/x-tmpl-mustache' id='two-column'> <script type="text/x-tmpl-mustache" id="two-column">
<div class='gutter'> <div class="gutter">
<div class='network-status-container'></div> <div class="network-status-container"></div>
<div class='left-pane-placeholder'></div> <div class="left-pane-placeholder"></div>
</div> </div>
<div class='conversation-stack'> <div class="conversation-stack">
<div class='conversation placeholder'> <div class="conversation placeholder">
<div class='conversation-header'></div> <div class="conversation-header"></div>
<div class='container'> <div class="container">
<div class='content'> <div class="content">
<img src='images/session/full-logo.svg' class='session-full-logo' /> <img src="images/session/full-logo.svg" class="session-full-logo" />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class='lightbox-container'></div> <div class="lightbox-container"></div>
</script> </script>
<script type='text/x-tmpl-mustache' id='scroll-down-button-view'> <script type="text/x-tmpl-mustache" id="scroll-down-button-view">
<button class='text module-scroll-down__button {{ buttonClass }}' alt='{{ moreBelow }}'> <button class="text module-scroll-down__button {{ buttonClass }}" alt="{{ moreBelow }}">
<div class='module-scroll-down__icon'></div> <div class="module-scroll-down__icon"></div>
</button> </button>
</script> </script>
<script type='text/x-tmpl-mustache' id='last-seen-indicator-view'> <script type="text/x-tmpl-mustache" id="last-seen-indicator-view">
<div class='module-last-seen-indicator__bar'/> <div class="module-last-seen-indicator__bar"/>
<div class='module-last-seen-indicator__text'> <div class="module-last-seen-indicator__text">
{{ unreadMessages }} {{ unreadMessages }}
</div> </div>
</script> </script>
<script type='text/x-tmpl-mustache' id='banner'> <script type="text/x-tmpl-mustache" id="banner">
<div class='body'> <div class="body">
<span class='icon warning'></span> <span class="icon warning"></span>
{{ message }} {{ message }}
<span class='icon dismiss'></span> <span class="icon dismiss"></span>
</div> </div>
</script> </script>
<script type='text/x-tmpl-mustache' id='toast'> <script type="text/x-tmpl-mustache" id="toast">
{{ toastMessage }} {{ toastMessage }}
</script> </script>
<script type='text/x-tmpl-mustache' id='conversation'> <script type="text/x-tmpl-mustache" id="conversation">
<div class='conversation-header'></div> <div class="conversation-header"></div>
<div class='main panel'> <div class="main panel">
<div class='discussion-container'> <div class="discussion-container">
<div class='bar-container hide'> <div class="bar-container hide">
<div class='bar active progress-bar-striped progress-bar'></div> <div class="bar active progress-bar-striped progress-bar"></div>
</div> </div>
</div> </div>
<div class='bottom-bar' id='footer'> <div class="bottom-bar" id="footer">
<div class='emoji-panel-container'></div> <div class="emoji-panel-container"></div>
<div class='attachment-list'></div> <div class="attachment-list"></div>
<div class='compose'> <div class="compose">
<form class='send clearfix file-input'> <form class="send clearfix file-input">
<div class='flex'> <div class="flex">
<button class='emoji'></button> <button class="emoji"></button>
<textarea class='send-message' placeholder='{{ send-message }}' rows='1' dir='auto'></textarea> <textarea class="send-message" placeholder="{{ send-message }}" rows="1" dir="auto"></textarea>
<div class='capture-audio'> <div class="capture-audio">
<button class='microphone'></button> <button class="microphone"></button>
</div> </div>
<div class='choose-file'> <div class="choose-file">
<button class='paperclip thumbnail'></button> <button class="paperclip thumbnail"></button>
<input type='file' class='file-input' multiple='multiple'> <input type="file" class="file-input" multiple="multiple">
</div> </div>
</div> </div>
</form> </form>
@ -103,39 +103,39 @@
</div> </div>
</script> </script>
<script type='text/x-tmpl-mustache' id='message-list'> <script type="text/x-tmpl-mustache" id="message-list">
<div class='messages'></div> <div class="messages"></div>
<div class='typing-container'></div> <div class="typing-container"></div>
</script> </script>
<script type='text/x-tmpl-mustache' id='recorder'> <script type="text/x-tmpl-mustache" id="recorder">
<button class='finish'><span class='icon'></span></button> <button class="finish"><span class="icon"></span></button>
<span class='time'>0:00</span> <span class="time">0:00</span>
<button class='close'><span class='icon'></span></button> <button class="close"><span class="icon"></span></button>
</script> </script>
<script type='text/x-tmpl-mustache' id='nickname-dialog'> <script type="text/x-tmpl-mustache" id="nickname-dialog">
<div class='content'> <div class="content">
<div class='message'>{{ message }}</div> <div class="message">{{ message }}</div>
<input type='text' name='name' class='name' placeholder='Type a name' value='{{ name }}'> <input type="text" name="name" class="name" placeholder="Type a name" value="{{ name }}">
<div class='buttons'> <div class="buttons">
<button class='clear' tabindex='3'>{{ clear }}</button> <button class="clear" tabindex="3">{{ clear }}</button>
<button class='cancel' tabindex='2'>{{ cancel }}</button> <button class="cancel" tabindex="2">{{ cancel }}</button>
<button class='ok' tabindex='1'>{{ ok }}</button> <button class="ok" tabindex="1">{{ ok }}</button>
</div> </div>
</div> </div>
</script> </script>
<script type='text/x-tmpl-mustache' id='confirmation-dialog'> <script type="text/x-tmpl-mustache" id="confirmation-dialog">
<div class="content"> <div class="content">
<div class='message'>{{ message }}</div> <div class="message">{{ message }}</div>
<div class='buttons'> <div class="buttons">
{{ #showCancel }} {{ #showCancel }}
<button class='cancel' tabindex='2'>{{ cancel }}</button> <button class="cancel" tabindex="2">{{ cancel }}</button>
{{ /showCancel }} {{ /showCancel }}
<button class='ok' tabindex='1'>{{ ok }}</button> <button class="ok" tabindex="1">{{ ok }}</button>
</div> </div>
</div> </div>
</script> </script>
<script type='text/x-tmpl-mustache' id='beta-disclaimer-dialog'> <script type="text/x-tmpl-mustache" id="beta-disclaimer-dialog">
<div class="content"> <div class="content">
<div class="betaDisclaimerView" style="display: none;"> <div class="betaDisclaimerView" style="display: none;">
<h2> <h2>
@ -167,101 +167,101 @@
As a beta, this software is still experimental. When things aren't working for you, or you feel confused by the app, please let us know by filing an issue on <a href="https://github.com/loki-project/loki-messenger">Github</a> or making suggestions on <a href="https://discordapp.com/invite/67GXfD6">Discord</a>. As a beta, this software is still experimental. When things aren't working for you, or you feel confused by the app, please let us know by filing an issue on <a href="https://github.com/loki-project/loki-messenger">Github</a> or making suggestions on <a href="https://discordapp.com/invite/67GXfD6">Discord</a>.
</p> </p>
<button class='ok' tabindex='1'>{{ ok }}</button> <button class="ok" tabindex="1">{{ ok }}</button>
</div> </div>
</div> </div>
</script> </script>
<script type='text/x-tmpl-mustache' id='identicon-svg'> <script type="text/x-tmpl-mustache" id="identicon-svg">
<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'> <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<circle cx='50' cy='50' r='40' fill='{{ color }}' /> <circle cx="50" cy="50" r="40" fill="{{ color }}" />
<text text-anchor='middle' fill='white' font-family='sans-serif' font-size='24px' x='50' y='50' baseline-shift='-8px'> <text text-anchor="middle" fill="white" font-family="sans-serif" font-size="24px" x="50" y="50" baseline-shift="-8px">
{{ content }} {{ content }}
</text> </text>
</svg> </svg>
</script> </script>
<script type='text/x-tmpl-mustache' id='phone-number'> <script type="text/x-tmpl-mustache" id="phone-number">
<div class='phone-input-form'> <div class="phone-input-form">
<div class='number-container'> <div class="number-container">
<input type='tel' class='number' placeholder="Phone Number" /> <input type="tel" class="number" placeholder="Phone Number" />
</div> </div>
</div> </div>
</script> </script>
<script type='text/x-tmpl-mustache' id='file-size-modal'> <script type="text/x-tmpl-mustache" id="file-size-modal">
{{ file-size-warning }} {{ file-size-warning }}
({{ limit }}{{ units }}) ({{ limit }}{{ units }})
</script> </script>
<script type='text/x-tmpl-mustache' id='attachment-type-modal'> <script type="text/x-tmpl-mustache" id="attachment-type-modal">
Sorry, your attachment has a type, {{type}}, that is not currently supported. Sorry, your attachment has a type, {{type}}, that is not currently supported.
</script> </script>
<script type='text/x-tmpl-mustache' id='group-member-list'> <script type="text/x-tmpl-mustache" id="group-member-list">
<div class='container'> <div class="container">
{{ #summary }} <div class='summary'>{{ summary }}</div>{{ /summary }} {{ #summary }} <div class="summary">{{ summary }}</div>{{ /summary }}
</div> </div>
</script> </script>
<script type='text/x-tmpl-mustache' id='key-verification'> <script type="text/x-tmpl-mustache" id="key-verification">
<div class='container'> <div class="container">
{{ ^hasTheirKey }} {{ ^hasTheirKey }}
<div class='placeholder'>{{ theirKeyUnknown }}</div> <div class="placeholder">{{ theirKeyUnknown }}</div>
{{ /hasTheirKey }} {{ /hasTheirKey }}
{{ #hasTheirKey }} {{ #hasTheirKey }}
<label> {{ yourSafetyNumberWith }} </label> <label> {{ yourSafetyNumberWith }} </label>
<!--<div class='qr'></div>--> <!--<div class="qr"></div>-->
<div class='key'> <div class="key">
{{ #chunks }} <span>{{ . }}</span> {{ /chunks }} {{ #chunks }} <span>{{ . }}</span> {{ /chunks }}
</div> </div>
{{ /hasTheirKey }} {{ /hasTheirKey }}
{{ verifyHelp }} {{ verifyHelp }}
<p> {{> link_to_support }} </p> <p> {{> link_to_support }} </p>
<div class='summary'> <div class="summary">
{{ #isVerified }} {{ #isVerified }}
<span class='icon verified'></span> <span class="icon verified"></span>
{{ /isVerified }} {{ /isVerified }}
{{ ^isVerified }} {{ ^isVerified }}
<span class='icon shield'></span> <span class="icon shield"></span>
{{ /isVerified }} {{ /isVerified }}
{{ verifiedStatus }} {{ verifiedStatus }}
</div> </div>
<div class='verify'> <div class="verify">
<button class='verify grey'> <button class="verify grey">
{{ verifyButton }} {{ verifyButton }}
</button> </button>
</div> </div>
</div> </div>
</script> </script>
<script type='text/x-tmpl-mustache' id='clear-data'> <script type="text/x-tmpl-mustache" id="clear-data">
{{#isStep1}} {{#isStep1}}
<div class='step'> <div class="step">
<div class='inner'> <div class="inner">
<div class='step-body'> <div class="step-body">
<span class='banner-icon alert-outline-red'></span> <span class="banner-icon alert-outline-red"></span>
<div class='header'>{{ header }}</div> <div class="header">{{ header }}</div>
<div class='body-text-wide'>{{ body }}</div> <div class="body-text-wide">{{ body }}</div>
</div> </div>
<div class='nav'> <div class="nav">
<div> <div>
<a class='button neutral cancel'>{{ cancelButton }}</a> <a class="button neutral cancel">{{ cancelButton }}</a>
<a class='button destructive delete-all-data'>{{ deleteButton }}</a> <a class="button destructive delete-all-data">{{ deleteButton }}</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{{/isStep1}} {{/isStep1}}
{{#isStep2}} {{#isStep2}}
<div id='step3' class='step'> <div id="step3" class="step">
<div class='inner'> <div class="inner">
<div class='step-body'> <div class="step-body">
<span class='banner-icon delete'></span> <span class="banner-icon delete"></span>
<div class='header'>{{ deleting }}</div> <div class="header">{{ deleting }}</div>
</div> </div>
<div class='progress'> <div class="progress">
<div class='bar-container'> <div class="bar-container">
<div class='bar progress-bar progress-bar-striped active'></div> <div class="bar progress-bar progress-bar-striped active"></div>
</div> </div>
</div> </div>
</div> </div>
@ -269,8 +269,8 @@
{{/isStep2}} {{/isStep2}}
</script> </script>
<script type='text/x-tmpl-mustache' id='networkStatus'> <script type="text/x-tmpl-mustache" id="networkStatus">
<div class='network-status-message'> <div class="network-status-message">
<h3>{{ message }}</h3> <h3>{{ message }}</h3>
<span>{{ instructions }}</span> <span>{{ instructions }}</span>
</div> </div>
@ -281,60 +281,60 @@
{{/reconnectDurationAsSeconds }} {{/reconnectDurationAsSeconds }}
{{ #action }} {{ #action }}
<div class="action"> <div class="action">
<button class='small blue {{ buttonClass }}'>{{ action }}</button> <button class="small blue {{ buttonClass }}">{{ action }}</button>
</div> </div>
{{/action }} {{/action }}
</script> </script>
<script type='text/x-tmpl-mustache' id='import-flow-template'>
<script type="text/x-tmpl-mustache" id="import-flow-template">
{{#isStep2}} {{#isStep2}}
<div id='step2' class='step'> <div id="step2" class="step">
<div class='inner'> <div class="inner">
<div class='step-body'> <div class="step-body">
<span class='banner-icon folder-outline'></span> <span class="banner-icon folder-outline"></span>
<div class='header'>{{ chooseHeader }}</div> <div class="header">{{ chooseHeader }}</div>
<div class='body-text'>{{ choose }}</div> <div class="body-text">{{ choose }}</div>
</div> </div>
<div class='nav'> <div class="nav">
<div> <div>
<a class='button choose'>{{ chooseButton }}</a> <a class="button choose">{{ chooseButton }}</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{{/isStep2}} {{/isStep2}}
{{#isStep3}} {{#isStep3}}
<div id='step3' class='step'> <div id="step3" class="step">
<div class='inner'> <div class="inner">
<div class='step-body'> <div class="step-body">
<span class='banner-icon import'></span> <span class="banner-icon import"></span>
<div class='header'>{{ importingHeader }}</div> <div class="header">{{ importingHeader }}</div>
</div> </div>
<div class='progress'> <div class="progress">
<div class='bar-container'> <div class="bar-container">
<div class='bar progress-bar progress-bar-striped active'></div> <div class="bar progress-bar progress-bar-striped active"></div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{{/isStep3}} {{/isStep3}}
{{#isStep4}} {{#isStep4}}
<div id='step4' class='step'> <div id="step4" class="step">
<div class='inner'> <div class="inner">
<div class='step-body'> <div class="step-body">
<span class='banner-icon check-circle-outline'></span> <span class="banner-icon check-circle-outline"></span>
<div class='header'>{{ completeHeader }}</div> <div class="header">{{ completeHeader }}</div>
</div> </div>
<div class='nav'> <div class="nav">
{{#restartButton}} {{#restartButton}}
<div> <div>
<a class='button restart'>{{ restartButton }}</a> <a class="button restart">{{ restartButton }}</a>
</div> </div>
{{/restartButton}} {{/restartButton}}
{{#registerButton}} {{#registerButton}}
<div> <div>
<a class='button register'>{{ registerButton }}</a> <a class="button register">{{ registerButton }}</a>
</div> </div>
{{/registerButton}} {{/registerButton}}
</div> </div>
@ -343,19 +343,19 @@
{{/isStep4}} {{/isStep4}}
{{#isError}} {{#isError}}
<div id='error' class='step'> <div id="error" class="step">
<div class='inner error-dialog clearfix'> <div class="inner error-dialog clearfix">
<div class='step-body'> <div class="step-body">
<span class='banner-icon alert-outline'></span> <span class="banner-icon alert-outline"></span>
<div class='header'>{{ errorHeader }}</div> <div class="header">{{ errorHeader }}</div>
<div class='body-text-wide'> <div class="body-text-wide">
{{ errorMessageFirst }} {{ errorMessageFirst }}
<p>{{ errorMessageSecond }}</p> <p>{{ errorMessageSecond }}</p>
</div> </div>
</div> </div>
<div class='nav'> <div class="nav">
<div> <div>
<a class='button choose'>{{ chooseButton }}</a> <a class="button choose">{{ chooseButton }}</a>
</div> </div>
</div> </div>
</div> </div>
@ -363,37 +363,37 @@
{{/isError}} {{/isError}}
</script> </script>
<script type='text/x-tmpl-mustache' id='link-flow-template'> <script type="text/x-tmpl-mustache" id="link-flow-template">
{{#isStep3}} {{#isStep3}}
<div id='step3' class='step'> <div id="step3" class="step">
<div class='inner'> <div class="inner">
<div class='step-body'> <div class="step-body">
<div class='header'>{{ linkYourPhone }}</div> <div class="header">{{ linkYourPhone }}</div>
<div id="qr"> <div id="qr">
<div class='container'> <div class="container">
<span class='dot'></span> <span class="dot"></span>
<span class='dot'></span> <span class="dot"></span>
<span class='dot'></span> <span class="dot"></span>
</div> </div>
</div> </div>
</div> </div>
<div class='nav'> <div class="nav">
<div class='instructions'> <div class="instructions">
<div class='android'> <div class="android">
<div class='label'> <div class="label">
<span class='os-icon android'></span> <span class="os-icon android"></span>
</div> </div>
<div class='body'> <div class="body">
<div>→ {{ signalSettings }}</div> <div>→ {{ signalSettings }}</div>
<div>→ {{ linkedDevices }}</div> <div>→ {{ linkedDevices }}</div>
<div>→ {{ androidFinalStep }}</div> <div>→ {{ androidFinalStep }}</div>
</div> </div>
</div> </div>
<div class='apple'> <div class="apple">
<div class='label'> <div class="label">
<span class='os-icon apple'></span> <span class="os-icon apple"></span>
</div> </div>
<div class='body'> <div class="body">
<div>→ {{ signalSettings }}</div> <div>→ {{ signalSettings }}</div>
<div>→ {{ linkedDevices }}</div> <div>→ {{ linkedDevices }}</div>
<div>→ {{ appleFinalStep }}</div> <div>→ {{ appleFinalStep }}</div>
@ -405,20 +405,20 @@
</div> </div>
{{/isStep3}} {{/isStep3}}
{{#isStep4}} {{#isStep4}}
<form id='link-phone'> <form id="link-phone">
<div id='step4' class='step'> <div id="step4" class="step">
<div class='inner'> <div class="inner">
<div class='step-body'> <div class="step-body">
<span class='banner-icon lead-pencil'></span> <span class="banner-icon lead-pencil"></span>
<div class='header'>{{ chooseName }}</div> <div class="header">{{ chooseName }}</div>
<div> <div>
<input type='text' class='device-name' spellcheck='false' maxlength='50' /> <input type="text" class="device-name" spellcheck="false" maxlength="50" />
</div> </div>
</div> </div>
<div class='nav'> <div class="nav">
<div> <div>
<a class='button finish'>{{ finishLinkingPhoneButton }}</a> <a class="button finish">{{ finishLinkingPhoneButton }}</a>
</div> </div>
</div> </div>
</div> </div>
@ -426,15 +426,15 @@
</form> </form>
{{/isStep4}} {{/isStep4}}
{{#isStep5}} {{#isStep5}}
<div id='step5' class='step'> <div id="step5" class="step">
<div class='inner'> <div class="inner">
<div class='step-body'> <div class="step-body">
<span class='banner-icon sync'></span> <span class="banner-icon sync"></span>
<div class='header'>{{ syncing }}</div> <div class="header">{{ syncing }}</div>
</div> </div>
<div class='progress'> <div class="progress">
<div class='bar-container'> <div class="bar-container">
<div class='bar progress-bar progress-bar-striped active'></div> <div class="bar progress-bar progress-bar-striped active"></div>
</div> </div>
</div> </div>
</div> </div>
@ -442,44 +442,44 @@
{{/isStep5}} {{/isStep5}}
{{#isError}} {{#isError}}
<div id='error' class='step'> <div id="error" class="step">
<div class='inner'> <div class="inner">
<div class='step-body'> <div class="step-body">
<span class='banner-icon alert-outline'></span> <span class="banner-icon alert-outline"></span>
<div class='header'>{{ errorHeader }}</div> <div class="header">{{ errorHeader }}</div>
<div class='body'>{{ errorMessage }}</div> <div class="body">{{ errorMessage }}</div>
</div> </div>
<div class='nav'> <div class="nav">
<a class='button try-again'>{{ errorButton }}</a> <a class="button try-again">{{ errorButton }}</a>
</div> </div>
</div> </div>
</div> </div>
{{/isError}} {{/isError}}
</script> </script>
<script type='text/x-tmpl-mustache' id='standalone'> <script type="text/x-tmpl-mustache" id="standalone">
<div class='step'> <div class="step">
<div class='inner'> <div class="inner">
<div class='step-body'> <div class="step-body">
<img class='banner-image' src='images/icon_128.png' /> <img class="banner-image" src="images/icon_128.png" />
<div class='header'>Create your Signal Account</div> <div class="header">Create your Signal Account</div>
<div id='phone-number-input'> <div id="phone-number-input">
<div class='phone-input-form'> <div class="phone-input-form">
<div id='number-container' class='number-container'> <div id="number-container" class="number-container">
<input type='tel' class='number' placeholder='Phone Number' /> <input type="tel" class="number" placeholder="Phone Number" />
</div> </div>
</div> </div>
</div> </div>
<div class='clearfix'> <div class="clearfix">
<a class='button' id='request-sms'>Send SMS</a> <a class="button" id="request-sms">Send SMS</a>
<a class='link' id='request-voice' tabindex=-1>Call</a> <a class="link" id="request-voice" tabindex=-1>Call</a>
</div> </div>
<input class='form-control' type='text' pattern='\s*[0-9]{3}-?[0-9]{3}\s*' title='Enter your 6-digit verification code. If you did not receive a code, click Call or Send SMS to request a new one' id='code' placeholder='Verification Code' autocomplete='off'> <input class="form-control" type="text" pattern="\s*[0-9]{3}-?[0-9]{3}\s*" title="Enter your 6-digit verification code. If you did not receive a code, click Call or Send SMS to request a new one" id="code" placeholder="Verification Code" autocomplete="off">
<div id='error' class='collapse'></div> <div id="error" class="collapse"></div>
<div id=status></div> <div id=status></div>
</div> </div>
<div class='nav'> <div class="nav">
<a class='button' id='verifyCode' data-loading-text='Please wait...'>Register</a> <a class="button" id="verifyCode" data-loading-text="Please wait...">Register</a>
</div> </div>
</div> </div>
</div> </div>
@ -491,7 +491,7 @@
<script type="text/javascript" src="../js/reliable_trigger.js" data-cover></script> <script type="text/javascript" src="../js/reliable_trigger.js" data-cover></script>
<script type="text/javascript" src="test.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/registration.js" data-cover></script>
<script type="text/javascript" src="../js/expire.js" data-cover></script> <script type="text/javascript" src="../js/expire.js" data-cover></script>
<script type="text/javascript" src="../js/chromium.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/database.js" data-cover></script>
@ -505,50 +505,62 @@
<script type="text/javascript" src="../js/models/conversations.js" data-cover></script> <script type="text/javascript" src="../js/models/conversations.js" data-cover></script>
<script type="text/javascript" src="../js/models/blockedNumbers.js" data-cover></script> <script type="text/javascript" src="../js/models/blockedNumbers.js" data-cover></script>
<script type="text/javascript" src="../js/conversation_controller.js" data-cover></script> <script type="text/javascript" src="../js/conversation_controller.js" data-cover></script>
<script type='text/javascript' src='../js/blocked_number_controller.js'></script> <script type="text/javascript" src="../js/blocked_number_controller.js"></script>
<script type="text/javascript" src="../js/message_controller.js" data-cover></script> <script type="text/javascript" src="../js/message_controller.js" data-cover></script>
<script type="text/javascript" src="../js/keychange_listener.js" data-cover></script> <script type="text/javascript" src="../js/keychange_listener.js" data-cover></script>
<script type='text/javascript' src='../js/expiring_messages.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/notifications.js" data-cover></script>
<script type='text/javascript' src='../js/focus_listener.js'></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/chromium.js" data-cover></script>
<script type='text/javascript' src='../js/views/react_wrapper_view.js'></script> <script type="text/javascript" src="../js/views/react_wrapper_view.js"></script>
<script type='text/javascript' src='../js/views/whisper_view.js' data-cover></script> <script type="text/javascript" src="../js/views/whisper_view.js"></script>
<script type='text/javascript' src='../js/views/debug_log_view.js' data-cover></script> <script type="text/javascript" src="../js/views/last_seen_indicator_view.js"></script>
<script type='text/javascript' src='../js/views/toast_view.js' data-cover></script> <script type="text/javascript" src="../js/views/scroll_down_button_view.js"></script>
<script type='text/javascript' src='../js/views/file_input_view.js' data-cover></script> <script type="text/javascript" src="../js/views/toast_view.js"></script>
<script type='text/javascript' src='../js/views/list_view.js' data-cover></script> <script type="text/javascript" src="../js/views/session_toast_view.js"></script>
<script type='text/javascript' src='../js/views/contact_list_view.js' data-cover></script> <script type="text/javascript" src="../js/views/conversation_loading_view.js"></script>
<script type='text/javascript' src='../js/views/timestamp_view.js' data-cover></script> <script type="text/javascript" src="../js/views/session_toggle_view.js"></script>
<script type='text/javascript' src='../js/views/message_view.js' data-cover></script> <script type="text/javascript" src="../js/views/session_modal_view.js"></script>
<script type='text/javascript' src='../js/views/key_verification_view.js' data-cover></script> <script type="text/javascript" src="../js/views/session_dropdown_view.js"></script>
<script type='text/javascript' src='../js/views/message_list_view.js' data-cover></script> <script type="text/javascript" src="../js/views/session_confirm_view.js"></script>
<script type='text/javascript' src='../js/views/member_list_view.js' data-cover></script> <script type="text/javascript" src="../js/views/file_input_view.js"></script>
<script type='text/javascript' src='../js/views/bulk_edit_view.js' data-cover></script> <script type="text/javascript" src="../js/views/list_view.js"></script>
<script type='text/javascript' src='../js/views/group_member_list_view.js' data-cover></script> <script type="text/javascript" src="../js/views/contact_list_view.js"></script>
<script type='text/javascript' src='../js/views/recorder_view.js' data-cover></script> <script type="text/javascript" src="../js/views/message_view.js"></script>
<script type='text/javascript' src='../js/views/conversation_view.js' data-cover></script> <script type="text/javascript" src="../js/views/key_verification_view.js"></script>
<script type='text/javascript' src='../js/views/hint_view.js' data-cover></script> <script type="text/javascript" src="../js/views/message_list_view.js"></script>
<script type='text/javascript' src='../js/views/inbox_view.js' data-cover></script> <script type="text/javascript" src="../js/views/member_list_view.js"></script>
<script type='text/javascript' src='../js/views/network_status_view.js'></script> <script type="text/javascript" src="../js/views/bulk_edit_view.js"></script>
<script type='text/javascript' src='../js/views/confirmation_dialog_view.js' data-cover></script> <script type="text/javascript" src="../js/views/group_member_list_view.js"></script>
<script type='text/javascript' src='../js/views/nickname_dialog_view.js' data-cover></script> <script type="text/javascript" src="../js/views/recorder_view.js"></script>
<script type='text/javascript' src='../js/views/password_dialog_view.js' data-cover></script> <script type="text/javascript" src="../js/views/conversation_view.js"></script>
<script type='text/javascript' src='../js/views/seed_dialog_view.js' data-cover></script> <script type="text/javascript" src="../js/views/inbox_view.js"></script>
<script type='text/javascript' src='../js/views/qr_dialog_view.js'></script> <script type="text/javascript" src="../js/views/network_status_view.js"></script>
<script type='text/javascript' src='../js/views/add_server_dialog_view.js'></script> <script type="text/javascript" src="../js/views/confirmation_dialog_view.js"></script>
<script type='text/javascript' src='../js/views/identicon_svg_view.js' data-cover></script> <script type="text/javascript" src="../js/views/nickname_dialog_view.js"></script>
<script type='text/javascript' src='../js/views/last_seen_indicator_view.js' data-cover></script> <script type="text/javascript" src="../js/views/password_dialog_view.js"></script>
<script type='text/javascript' src='../js/views/scroll_down_button_view.js' data-cover></script> <script type="text/javascript" src="../js/views/seed_dialog_view.js"></script>
<script type='text/javascript' src='../js/views/banner_view.js' data-cover></script> <script type="text/javascript" src="../js/views/qr_dialog_view.js"></script>
<script type='text/javascript' src='../js/views/conversation_loading_view.js'></script> <script type="text/javascript" src="../js/views/connecting_to_server_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/beta_release_disclaimer_view.js"></script>
<script type='text/javascript' src='../js/views/create_group_dialog_view.js'></script> <script type="text/javascript" src="../js/views/identicon_svg_view.js"></script>
<script type='text/javascript' src='../js/views/confirm_session_reset_view.js'></script> <script type="text/javascript" src="../js/views/install_view.js"></script>
<script type='text/javascript' src='../js/views/invite_friends_dialog_view.js'></script> <script type="text/javascript" src="../js/views/banner_view.js"></script>
<script type='text/javascript' src='../js/views/beta_release_disclaimer_view.js'></script> <script type="text/javascript" src="../js/views/phone-input-view.js"></script>
<script type="text/javascript" src="../js/views/session_registration_view.js"></script>
<script type="text/javascript" src="../js/views/app_view.js"></script>
<script type="text/javascript" src="../js/views/import_view.js"></script>
<script type="text/javascript" src="../js/views/device_pairing_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/device_pairing_words_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/create_group_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/confirm_session_reset_view.js"></script>
<script type="text/javascript" src="../js/views/edit_profile_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/invite_friends_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/moderators_add_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/moderators_remove_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/user_details_dialog_view.js"></script>
<!-- <script type="text/javascript" src="metadata/SecretSessionCipher_test.js"></script> --> <!-- <script type="text/javascript" src="metadata/SecretSessionCipher_test.js"></script> -->
@ -559,11 +571,10 @@
<script type="text/javascript" src="views/inbox_view_test.js"></script> <script type="text/javascript" src="views/inbox_view_test.js"></script>
<script type="text/javascript" src="views/network_status_view_test.js"></script> <script type="text/javascript" src="views/network_status_view_test.js"></script>
<script type="text/javascript" src="views/last_seen_indicator_view_test.js"></script> <script type="text/javascript" src="views/last_seen_indicator_view_test.js"></script>
<script type='text/javascript' src='views/scroll_down_button_view_test.js'></script> <script type="text/javascript" src="views/scroll_down_button_view_test.js"></script>
<script type="text/javascript" src="models/conversations_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="models/messages_test.js"></script>
<script type="text/javascript" src="models/profile_test.js"></script>
<script type="text/javascript" src="libphonenumber_util_test.js"></script> <script type="text/javascript" src="libphonenumber_util_test.js"></script>
<script type="text/javascript" src="blocked_number_controller_test.js"></script> <script type="text/javascript" src="blocked_number_controller_test.js"></script>

@ -1,6 +1,12 @@
describe('spellChecker', () => { describe('spellChecker', () => {
it('should work', () => { it('should work', () => {
assert(window.spellChecker.spellCheck('correct')); assert(
assert(!window.spellChecker.spellCheck('fhqwgads')); window.spellChecker.spellCheck('correct'),
'Spellchecker returned false on a correct word.'
);
assert(
!window.spellChecker.spellCheck('fhqwgads'),
'Spellchecker returned true on a incorrect word.'
);
}); });
}); });

@ -1,13 +0,0 @@
#!/usr/bin/env bash
set -e
if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
export DISPLAY=:99.0
sh -e /etc/init.d/xvfb start
sleep 3
fi
yarn test-electron
NODE_ENV=production yarn grunt test-release:$TRAVIS_OS_NAME

@ -21,7 +21,7 @@ export class ConfirmDialog extends React.Component<Props> {
return ( return (
<SessionModal <SessionModal
title={this.props.titleText} title={this.props.titleText}
onClose={() => null} onClose={this.props.onClose}
onOk={() => null} onOk={() => null}
> >
<div className="spacer-md" /> <div className="spacer-md" />

@ -16,6 +16,7 @@ import {
SessionIconType, SessionIconType,
} from './session/icon'; } from './session/icon';
import { SessionModal } from './session/SessionModal'; import { SessionModal } from './session/SessionModal';
import { PillDivider } from './session/PillDivider';
declare global { declare global {
interface Window { interface Window {
@ -107,9 +108,7 @@ export class EditProfileDialog extends React.Component<Props, State> {
{viewEdit && this.renderEditView()} {viewEdit && this.renderEditView()}
<div className="session-id-section"> <div className="session-id-section">
<div className="panel-text-divider"> <PillDivider text={window.i18n('yourSessionID')} />
<span>{window.i18n('yourSessionID')}</span>
</div>
<p <p
className={classNames( className={classNames(
'text-selectable', 'text-selectable',

@ -75,6 +75,10 @@ export class SearchResults extends React.Component<Props> {
))} ))}
</div> </div>
) : null} ) : null}
{haveFriends
? this.renderContacts(i18n('friendsHeader'), friends, true)
: null}
{haveMessages ? ( {haveMessages ? (
<div className="module-search-results__messages"> <div className="module-search-results__messages">
{hideMessagesHeader ? null : ( {hideMessagesHeader ? null : (
@ -95,4 +99,26 @@ export class SearchResults extends React.Component<Props> {
</div> </div>
); );
} }
private renderContacts(
header: string,
items: Array<ConversationListItemPropsType>,
friends?: boolean
) {
const { i18n, openConversation } = this.props;
return (
<div className="module-search-results__contacts">
<div className="module-search-results__contacts-header">{header}</div>
{items.map(contact => (
<ConversationListItem
key={contact.phoneNumber}
isFriend={friends}
{...contact}
onClick={openConversation}
i18n={i18n}
/>
))}
</div>
);
}
} }

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save