Merge pull request #951 from loki-project/clearnet

Merge into Master; prep for v1.0.3
This commit is contained in:
Mikunj Varsani 2020-03-05 16:04:28 +11:00 committed by GitHub
commit f0bb328952
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
129 changed files with 2065 additions and 2470 deletions

View file

@ -54,10 +54,12 @@ jobs:
- name: Build mac production binaries
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:
CSC_LINK: ${{ secrets.MAC_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }}
MAC_CERTIFICATE: ${{ secrets.MAC_CERTIFICATE }}
MAC_CERTIFICATE_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }}
SIGNING_APPLE_ID: ${{ secrets.SIGNING_APPLE_ID }}
SIGNING_APP_PASSWORD: ${{ secrets.SIGNING_APP_PASSWORD }}
SIGNING_TEAM_ID: ${{ secrets.SIGNING_TEAM_ID }}

67
.github/workflows/pull-request.yml vendored Normal file
View file

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

View file

@ -51,10 +51,12 @@ jobs:
- name: Build mac production binaries
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:
CSC_LINK: ${{ secrets.MAC_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }}
MAC_CERTIFICATE: ${{ secrets.MAC_CERTIFICATE }}
MAC_CERTIFICATE_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }}
SIGNING_APPLE_ID: ${{ secrets.SIGNING_APPLE_ID }}
SIGNING_APP_PASSWORD: ${{ secrets.SIGNING_APP_PASSWORD }}
SIGNING_TEAM_ID: ${{ secrets.SIGNING_TEAM_ID }}

View file

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

View file

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

View file

@ -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.
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
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
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
desired Node.js version. [nvm for windows](https://github.com/coreybutler/nvm-windows) is
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:
```
git clone https://github.com/loki-project/loki-messenger.git
cd loki-messenger
git clone https://github.com/loki-project/session-desktop.git
cd session-desktop
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 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)
directory from `%appData%/Loki-Messenger` to `%appData%/Loki-Messenger-aliceProfile`.
directory from `%appData%/Session` to `%appData%/Session-aliceProfile`.
# Making changes

24
RELEASING.md Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -227,7 +227,7 @@
},
"loadDataDescription": {
"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":
"Introduction to the process of importing messages and contacts from disk"
},
@ -246,7 +246,7 @@
},
"importErrorFirst": {
"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"
},
"importErrorSecond": {
@ -403,13 +403,13 @@
},
"changedSinceVerifiedMultiple": {
"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":
"Shown on confirmation dialog when user attempts to send a message"
},
"changedSinceVerified": {
"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":
"Shown on confirmation dialog when user attempts to send a message",
"placeholders": {
@ -421,7 +421,7 @@
},
"changedRightAfterVerify": {
"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":
"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": {
@ -433,13 +433,13 @@
},
"changedRecentlyMultiple": {
"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":
"Shown on confirmation dialog when user attempts to send a message"
},
"changedRecently": {
"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":
"Shown on confirmation dialog when user attempts to send a message",
"placeholders": {
@ -451,7 +451,7 @@
},
"identityKeyErrorOnSend": {
"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":
"Shown when user clicks on a failed recipient in the message detail view after an identity key change",
"placeholders": {
@ -545,7 +545,7 @@
},
"identityChanged": {
"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": {
"message": "Error handling incoming message"
@ -610,12 +610,12 @@
"loadingPreview": {
"message": "Loading Preview...",
"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": {
"message": "Draft thumbnail link preview for $domain$",
"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": {
"path": {
"content": "$1",
@ -626,7 +626,7 @@
"previewThumbnail": {
"message": "Thumbnail link preview for $domain$",
"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": {
"path": {
"content": "$1",
@ -726,7 +726,7 @@
"signalDesktopPreferences": {
"message": "Session Preferences",
"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": {
"message": "About Session",
@ -809,7 +809,7 @@
"sendMessageToContact": {
"message": "Send Message",
"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": {
"message": "home",
@ -935,13 +935,13 @@
},
"cannotUpdateDetail": {
"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":
"Shown if a general error happened while trying to install update package"
},
"readOnlyVolume": {
"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":
"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!",
"description": "Warning for the time to live setting"
},
"zoomFactorSettingTitle": {
"message": "Zoom Factor",
"description": "Title of the Zoom Factor setting"
},
"notificationSettingsDialog": {
"message": "When messages arrive, display notifications that reveal...",
"description": "Explain the purpose of the notification settings"
@ -1946,7 +1950,7 @@
},
"unlinkedWarning": {
"message":
"Relink Signal Desktop to your mobile device to continue messaging."
"Relink Session Desktop to your mobile device to continue messaging."
},
"unlinked": {
"message": "Unlinked"
@ -1955,20 +1959,29 @@
"message": "Relink"
},
"autoUpdateNewVersionTitle": {
"message": "Signal update available"
"message": "Session update available"
},
"autoUpdateNewVersionMessage": {
"message": "There is a new version of Signal available."
"message": "There is a new version of Session available."
},
"autoUpdateNewVersionInstructions": {
"message": "Press Restart Signal to apply the updates."
"message": "Press Restart Session to apply the updates."
},
"autoUpdateRestartButtonLabel": {
"message": "Restart Signal"
"message": "Restart Session"
},
"autoUpdateLaterButtonLabel": {
"message": "Later"
},
"autoUpdateDownloadButtonLabel": {
"message": "Download"
},
"autoUpdateDownloadedMessage": {
"message": "The new update has been downloaded."
},
"autoUpdateDownloadInstructions": {
"message": "Would you like to download the update?"
},
"leftTheGroup": {
"message": "$name$ left the group",
"description":
@ -2204,9 +2217,15 @@
"message": "Edit 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": {
"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": {
"message": "Creating a Closed Group",
@ -2597,6 +2616,9 @@
"message": "Enter other devices Session ID here"
},
"continueYourSession": {
"message": "Continue Your Session"
},
"linkDevice": {
"message": "Link Device"
},
"restoreSessionID": {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Signal frissítés elérhető",
"message": "Session frissítés elérhető",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "A Signal új verziója érhető el.",
"message": "A Session új verziója érhető el.",
"description": ""
},
"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": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Signal újraindítása",
"message": "Session újraindítása",
"description": ""
},
"autoUpdateLaterButtonLabel": {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Atualização do Signal disponível",
"message": "Atualização do Session disponível",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Uma nova versão do Signal está disponível.",
"message": "Uma nova versão do Session está disponível.",
"description": ""
},
"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": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Reiniciar Signal",
"message": "Reiniciar Session",
"description": ""
},
"autoUpdateLaterButtonLabel": {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Ka gati përditësim të Signal-it",
"message": "Ka gati përditësim të Session-it",
"description": ""
},
"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": ""
},
"autoUpdateNewVersionInstructions": {
"message": "Shtypni Rinise Signal-in që të zbatohen përditësimet.",
"message": "Shtypni Rinise Session-in që të zbatohen përditësimet.",
"description": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Riniseni Signal-in",
"message": "Riniseni Session-in",
"description": ""
},
"autoUpdateLaterButtonLabel": {

View file

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

View file

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

View file

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

View file

@ -1460,19 +1460,19 @@
"description": ""
},
"autoUpdateNewVersionTitle": {
"message": "Signal güncellemesi mevcut",
"message": "Session güncellemesi mevcut",
"description": ""
},
"autoUpdateNewVersionMessage": {
"message": "Signal'ın yeni bir sürümü mevcut.",
"message": "Session'ın yeni bir sürümü mevcut.",
"description": ""
},
"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": ""
},
"autoUpdateRestartButtonLabel": {
"message": "Signal'i Yeniden Başlat",
"message": "Session'i Yeniden Başlat",
"description": ""
},
"autoUpdateLaterButtonLabel": {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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]/;
async function setupSQLCipher(instance, { key }) {
// 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
const value = deriveKey ? `'${key}'` : `"x'${key}'"`;
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) {
@ -1071,6 +1083,13 @@ async function initialize({ configDir, key, messages, passwordAttempt }) {
db = promisified;
// 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();
} catch (error) {
if (passwordAttempt) {

View file

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

View file

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

View file

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

15
build/setup-mac-certificate.sh Executable file
View file

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

View file

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

View file

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

View file

@ -41,7 +41,7 @@
</div>
<p>
<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 }}
</a>
</p>

View file

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

View file

@ -286,6 +286,9 @@
}
first = false;
// Update zoom
window.updateZoomFactor();
const currentPoWDifficulty = storage.get('PoWDifficulty', null);
if (!currentPoWDifficulty) {
storage.put('PoWDifficulty', window.getDefaultPoWDifficulty());
@ -398,17 +401,13 @@
await storage.put('version', currentVersion);
if (newVersion) {
if (
lastVersion &&
window.isBeforeVersion(lastVersion, 'v1.15.0-beta.5')
) {
await window.Signal.Logs.deleteAll();
window.restart();
}
window.log.info(
`New version detected: ${currentVersion}; previous: ${lastVersion}`
);
await window.Signal.Data.cleanupOrphanedAttachments();
await window.Signal.Logs.deleteAll();
}
if (isIndexedDBPresent) {
@ -431,10 +430,6 @@
Views.Initialization.setMessage(window.i18n('optimizingApplication'));
if (newVersion) {
await window.Signal.Data.cleanupOrphanedAttachments();
}
Views.Initialization.setMessage(window.i18n('loading'));
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 ev = new Event('message');
@ -729,6 +724,44 @@
if (convo.isPublic()) {
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)) {
// queue update from server
// and let that set the conversation
@ -741,7 +774,11 @@
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 recipients = _.union(convo.get('members'), members);
@ -750,7 +787,7 @@
convo.updateGroup({
groupId,
groupName,
avatar,
avatar: nullAvatar,
recipients,
members,
options,
@ -787,6 +824,7 @@
'group'
);
convo.updateGroupAdmins([primaryDeviceKey]);
convo.updateGroup(ev.groupDetails);
// Group conversations are automatically 'friends'
@ -795,8 +833,6 @@
window.friends.friendRequestStatusEnum.friends
);
convo.updateGroupAdmins([primaryDeviceKey]);
appView.openConversation(groupId, {});
};
@ -994,7 +1030,9 @@
let friendList = contacts;
if (friendList !== undefined) {
friendList = friendList.filter(
friend => friend.type === 'direct' && !friend.isMe
friend =>
(friend.type === 'direct' && !friend.isMe) ||
(friend.type === 'group' && !friend.isPublic && !friend.isRss)
);
}
return friendList;
@ -1372,6 +1410,8 @@
await window.lokiFileServerAPI.updateOurDeviceMapping();
// TODO: we should ensure the message was sent and retry automatically if not
await libloki.api.sendUnpairingMessageToSecondary(pubKey);
// Remove all traces of the device
ConversationController.deleteContact(pubKey);
Whisper.events.trigger('refreshLinkedDeviceList');
});
}
@ -1470,6 +1510,9 @@
};
Whisper.Notifications.disable(); // avoid notification flood until empty
setTimeout(() => {
Whisper.Notifications.enable();
}, window.CONSTANTS.NOTIFICATION_ENABLE_TIMEOUT_SECONDS * 1000);
if (Whisper.Registration.ongoingSecondaryDeviceRegistration()) {
const ourKey = textsecure.storage.user.getNumber();
@ -1642,6 +1685,11 @@
// 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.
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) {
const { count } = ev;

View file

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

View file

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

View file

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

View file

@ -5,6 +5,7 @@ const nodeFetch = require('node-fetch');
const { URL, URLSearchParams } = require('url');
const FormData = require('form-data');
const https = require('https');
const path = require('path');
// Can't be less than 1200 if we have unauth'd requests
const PUBLICCHAT_MSG_POLL_EVERY = 1.5 * 1000; // 1.5s
@ -229,7 +230,7 @@ class LokiAppDotNetServerAPI {
window.storage.get('primaryDevicePubKey') ||
textsecure.storage.user.getNumber();
const profileConvo = ConversationController.get(ourNumber);
const profile = profileConvo.getLokiProfile();
const profile = profileConvo && profileConvo.getLokiProfile();
const profileName = profile && profile.displayName;
// if doesn't match, write it to the network
if (tokenRes.response.data.user.name !== profileName) {
@ -627,6 +628,12 @@ class LokiAppDotNetServerAPI {
url
);
}
if (mode === '_sendToProxy') {
// if we can detect, certain types of failures, we can retry...
if (e.code === 'ECONNRESET') {
// retry with counter?
}
}
return {
err: e,
};
@ -877,6 +884,7 @@ class LokiAppDotNetServerAPI {
};
}
// for avatar
async uploadData(data) {
const endpoint = 'files';
const options = {
@ -901,6 +909,7 @@ class LokiAppDotNetServerAPI {
};
}
// for files
putAttachment(attachmentBin) {
const formData = new FormData();
const buffer = Buffer.from(attachmentBin);
@ -1246,7 +1255,50 @@ class LokiPublicChannelAPI {
this.conversation.setGroupName(note.value.name);
}
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?
// who are the moderators?
@ -1256,6 +1308,15 @@ class LokiPublicChannelAPI {
if (data.counts && Number.isInteger(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
@ -1372,7 +1433,7 @@ class LokiPublicChannelAPI {
}
if (quote) {
// TODO: Enable quote attachments again using proper ADN style
// Disable quote attachments
quote.attachments = [];
}
@ -1476,6 +1537,14 @@ class LokiPublicChannelAPI {
});
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;
}
@ -1663,18 +1732,31 @@ class LokiPublicChannelAPI {
// filter out invalid messages
pendingMessages = pendingMessages.filter(messageData => !!messageData);
// separate messages coming from primary and secondary devices
const [primaryMessages, slaveMessages] = _.partition(
let [primaryMessages, slaveMessages] = _.partition(
pendingMessages,
message => !(message.source in slavePrimaryMap)
);
// process primary devices' message directly
primaryMessages.forEach(message =>
this.chatAPI.emit('publicMessage', {
message,
})
);
pendingMessages = []; // allow memory to be freed
// get minimum ID for primaryMessages and slaveMessages
const firstPrimaryId = _.min(primaryMessages.map(msg => msg.serverId));
const firstSlaveId = _.min(slaveMessages.map(msg => msg.serverId));
if (firstPrimaryId < firstSlaveId) {
// early send
// split off count from pendingMessages
let sendNow = [];
[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
const verifiedDeviceResults = await this.serverAPI.getUsers(
@ -1731,11 +1813,25 @@ class LokiPublicChannelAPI {
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', {
message: messageData,
message,
});
});
/* eslint-enable no-param-reassign */
// if we received one of our own messages

View file

@ -217,7 +217,12 @@ class LokiMessageAPI {
}
return true;
} 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) {
const { newSwarm } = e;
await lokiSnodeAPI.updateSwarmNodes(params.pubKey, newSwarm);
@ -272,6 +277,8 @@ class LokiMessageAPI {
try {
// TODO: Revert back to using snode address instead of IP
let messages = await this.retrieveNextMessages(nodeData.ip, nodeData);
// this only tracks retrieval failures
// won't include parsing failures...
successiveFailures = 0;
if (messages.length) {
const lastMessage = _.last(messages);
@ -288,7 +295,12 @@ class LokiMessageAPI {
// Execute callback even with empty array to signal online status
callback(messages);
} 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) {
const { newSwarm } = e;
await lokiSnodeAPI.updateSwarmNodes(this.ourKey, newSwarm);
@ -312,9 +324,24 @@ class LokiMessageAPI {
}
}
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);
}
}
// 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) {
@ -342,12 +369,31 @@ class LokiMessageAPI {
}
async startLongPolling(numConnections, stopPolling, callback) {
log.info('startLongPolling for', numConnections, 'connections');
this.ourSwarmNodes = {};
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) {
await lokiSnodeAPI.refreshSwarmNodesForPubKey(this.ourKey);
nodes = await lokiSnodeAPI.getSwarmNodesForPubKey(this.ourKey);
log.warn(
'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) {
const lastHash = await window.Signal.Data.getLastHashBySnode(
nodes[i].address
@ -364,9 +410,13 @@ class LokiMessageAPI {
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)
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...
}
}

View file

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

View file

@ -4,6 +4,8 @@
const is = require('@sindresorhus/is');
const { lokiRpc } = require('./loki_rpc');
const RANDOM_SNODES_TO_USE = 3;
class LokiSnodeAPI {
constructor({ serverUrl, localUrl }) {
if (!is.string(serverUrl)) {
@ -18,6 +20,7 @@ class LokiSnodeAPI {
async getRandomSnodeAddress() {
/* resolve random snode */
if (this.randomSnodePool.length === 0) {
// allow exceptions to pass through upwards
await this.initialiseRandomPool();
}
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 = {
limit: 20,
active_only: true,
@ -43,8 +49,9 @@ class LokiSnodeAPI {
Math.floor(Math.random() * seedNodes.length),
1
)[0];
let snodes = [];
try {
const result = await lokiRpc(
const response = await lokiRpc(
`http://${seedNode.ip}`,
seedNode.port,
'get_n_service_nodes',
@ -53,7 +60,7 @@ class LokiSnodeAPI {
'/json_rpc' // Seed request endpoint
);
// 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'
);
this.randomSnodePool = snodes.map(snode => ({
@ -64,12 +71,23 @@ class LokiSnodeAPI {
}));
} catch (e) {
log.warn('initialiseRandomPool error', e.code, e.message);
if (seedNodes.length === 0) {
throw new window.textsecure.SeedNodeError(
'Failed to contact seed node'
);
if (consecutiveErrors < 3) {
// retry after a possible delay
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 conversation = ConversationController.get(pubKey);
await conversation.updateSwarmNodes(filteredNodes);
return filteredNodes;
} catch (e) {
throw new window.textsecure.ReplayableError({
message: 'Could not get conversation',
@ -120,7 +139,8 @@ class LokiSnodeAPI {
async refreshSwarmNodesForPubKey(pubKey) {
const newNodes = await this.getFreshSwarmNodes(pubKey);
this.updateSwarmNodes(pubKey, newNodes);
const filteredNodes = this.updateSwarmNodes(pubKey, newNodes);
return filteredNodes;
}
async getFreshSwarmNodes(pubKey) {
@ -130,6 +150,7 @@ class LokiSnodeAPI {
try {
newSwarmNodes = await this.getSwarmNodes(pubKey);
} catch (e) {
log.error('getFreshSwarmNodes error', e.code, e.message);
// TODO: Handle these errors sensibly
newSwarmNodes = [];
}
@ -141,9 +162,7 @@ class LokiSnodeAPI {
return newSwarmNodes;
}
async getSwarmNodes(pubKey) {
// TODO: Hit multiple random nodes and merge lists?
const snode = await this.getRandomSnodeAddress();
async getSnodesForPubkey(snode, pubKey) {
try {
const result = await lokiRpc(
`https://${snode.ip}`,
@ -158,7 +177,7 @@ class LokiSnodeAPI {
);
if (!result) {
log.warn(
`getSwarmNodes lokiRpc on ${snode.ip}:${
`getSnodesForPubkey lokiRpc on ${snode.ip}:${
snode.port
} returned falsish value`,
result
@ -168,11 +187,39 @@ class LokiSnodeAPI {
const snodes = result.snodes.filter(tSnode => tSnode.ip !== '0.0.0.0');
return snodes;
} 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);
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;

View file

@ -34,8 +34,6 @@
Whisper.Notifications = new (Backbone.Collection.extend({
initialize() {
this.isEnabled = false;
this.on('add', this.update);
this.on('remove', this.onRemove);
this.lastNotification = null;
@ -45,7 +43,11 @@
// 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.
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() {
if (this.lastNotification) {

View file

@ -252,6 +252,10 @@
window.Whisper.events.trigger('inviteFriends', this.model);
},
onUpdateGroupName: () => {
window.Whisper.events.trigger('updateGroupName', this.model);
},
onAddModerators: () => {
window.Whisper.events.trigger('addModerators', this.model);
},
@ -287,6 +291,9 @@
isAdmin: this.model.get('groupAdmins').includes(ourPK),
isRss: this.model.isRss(),
memberCount: members.length,
amMod: this.model.isModerator(
window.storage.get('primaryDevicePubKey')
),
timerOptions: Whisper.ExpirationTimerOptions.map(item => ({
name: item.getName(),
@ -1189,6 +1196,13 @@
const el = this.$(`#${message.id}`);
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;
// We're fully below the viewport, continue searching up.

View file

@ -54,36 +54,17 @@
this.conversation = groupConvo;
this.titleText = i18n('updateGroupDialogTitle');
this.okText = i18n('ok');
this.cancelText = i18n('cancel');
this.close = this.close.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.isPublic = groupConvo.isPublic();
this.groupId = groupConvo.id;
this.members = groupConvo.get('members') || [];
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 =>
(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
if (this.isPublic) {
// fix the title
@ -95,9 +76,6 @@
this.isAdmin = groupConvo.isModerator(
window.storage.get('primaryDevicePubKey')
);
// zero out friendList for now
this.friendsAndMembers = [];
this.existingMembers = [];
}
this.$el.focus();
@ -109,24 +87,23 @@
Component: window.Signal.Components.UpdateGroupNameDialog,
props: {
titleText: this.titleText,
groupName: this.groupName,
okText: this.okText,
isPublic: this.isPublic,
cancelText: this.cancelText,
existingMembers: this.existingMembers,
groupName: this.groupName,
okText: i18n('ok'),
cancelText: i18n('cancel'),
isAdmin: this.isAdmin,
onClose: this.close,
i18n,
onSubmit: this.onSubmit,
onClose: this.close,
avatarPath: this.avatarPath,
},
});
this.$el.append(this.dialogView.el);
return this;
},
onSubmit(newGroupName, members) {
const groupId = this.conversation.get('id');
window.doUpdateGroup(groupId, newGroupName, members);
onSubmit(groupName, avatar) {
window.doUpdateGroup(this.groupId, groupName, this.members, avatar);
},
close() {
this.remove();
@ -136,40 +113,15 @@
Whisper.UpdateGroupMembersDialogView = Whisper.View.extend({
className: 'loki-dialog modal',
initialize(groupConvo) {
const ourPK = textsecure.storage.user.getNumber();
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.onSubmit = this.onSubmit.bind(this);
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) {
// fix the title
this.titleText = `${i18n('updatePublicGroupDialogTitle')}: ${
this.groupName
}`;
@ -181,6 +133,26 @@
// zero out friendList for now
this.friendsAndMembers = [];
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();
@ -201,6 +173,7 @@
isAdmin: this.isAdmin,
onClose: this.close,
onSubmit: this.onSubmit,
groupId: this.groupId,
},
});
@ -210,9 +183,13 @@
onSubmit(groupName, newMembers) {
const ourPK = textsecure.storage.user.getNumber();
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() {
this.remove();

View file

@ -74,7 +74,10 @@
newMembers.length + existingMembers.length >
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({
title: msg,

View file

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

View file

@ -1,4 +1,4 @@
/* global window, textsecure, Whisper, dcodeIO, StringView, ConversationController */
/* global window, textsecure, dcodeIO, StringView, ConversationController */
// eslint-disable-next-line func-names
(function() {
@ -109,8 +109,16 @@
}
async function createContactSyncProtoMessage(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(
conversations.map(async conversation => {
sessionContacts.map(async conversation => {
const profile = conversation.getLokiProfile();
const number = conversation.getNumber();
const name = profile
@ -151,6 +159,63 @@
});
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) {
const pairingAuthorisation = createPairingAuthorisationProtoMessage(
authorisation
@ -179,13 +244,6 @@
profile,
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;
}
// Send
@ -221,5 +279,7 @@
createPairingAuthorisationProtoMessage,
sendUnpairingMessageToSecondary,
createContactSyncProtoMessage,
createGroupSyncProtoMessage,
createOpenGroupsSyncProtoMessage,
};
})();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -664,31 +664,90 @@ MessageSender.prototype = {
return Promise.resolve();
},
async sendContactSyncMessage(contactConversation) {
if (!contactConversation.isPrivate()) {
return Promise.resolve();
}
async sendContactSyncMessage(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 allOurDevices = (await libloki.storage.getAllDevicePubKeysForPrimaryPubKey(
primaryDeviceKey
))
// Don't send to ourselves
.filter(pubKey => pubKey !== textsecure.storage.user.getNumber());
if (
allOurDevices.includes(contactConversation.id) ||
!primaryDeviceKey ||
allOurDevices.length === 0
) {
// If we havn't got a primaryDeviceKey then we are in the middle of pairing
if (!primaryDeviceKey) {
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');
if (!primaryDeviceKey) {
return Promise.resolve();
}
// We need to sync across 1 group at a time
// This is because we could hit the storage server limit with one group
const syncPromises = conversations
.map(c => libloki.api.createGroupSyncProtoMessage([c]))
.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);
},
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();
}
const syncMessage = await libloki.api.createContactSyncProtoMessage([
contactConversation,
]);
const contentMessage = new textsecure.protobuf.Content();
contentMessage.syncMessage = syncMessage;
contentMessage.syncMessage = openGroupsSyncMessage;
const silent = true;
return this.sendIndividualProto(
@ -1107,7 +1166,7 @@ MessageSender.prototype = {
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();
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) {
const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
@ -1251,6 +1318,10 @@ textsecure.MessageSender = function MessageSenderWrapper(username, password) {
sender
);
this.sendContactSyncMessage = sender.sendContactSyncMessage.bind(sender);
this.sendGroupSyncMessage = sender.sendGroupSyncMessage.bind(sender);
this.sendOpenGroupsSyncMessage = sender.sendOpenGroupsSyncMessage.bind(
sender
);
this.sendRequestConfigurationSyncMessage = sender.sendRequestConfigurationSyncMessage.bind(
sender
);
@ -1263,6 +1334,7 @@ textsecure.MessageSender = function MessageSenderWrapper(username, password) {
this.addNumberToGroup = sender.addNumberToGroup.bind(sender);
this.setGroupName = sender.setGroupName.bind(sender);
this.setGroupAvatar = sender.setGroupAvatar.bind(sender);
this.requestGroupInfo = sender.requestGroupInfo.bind(sender);
this.leaveGroup = sender.leaveGroup.bind(sender);
this.sendSyncMessage = sender.sendSyncMessage.bind(sender);
this.getProfile = sender.getProfile.bind(sender);

View file

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

47
main.js
View file

@ -65,9 +65,7 @@ const appInstance = config.util.getEnv('NODE_APP_INSTANCE') || 0;
const attachments = require('./app/attachments');
const attachmentChannel = require('./app/attachment_channel');
// TODO: Enable when needed
// const updater = require('./ts/updater/index');
const updater = null;
const updater = require('./ts/updater/index');
const createTrayIcon = require('./app/tray_icon');
const ephemeralConfig = require('./app/ephemeral_config');
@ -227,6 +225,7 @@ function createWindow() {
minWidth: MIN_WIDTH,
minHeight: MIN_HEIGHT,
autoHideMenuBar: false,
backgroundColor: '#fff',
webPreferences: {
nodeIntegration: false,
nodeIntegrationInWorker: false,
@ -286,7 +285,7 @@ function createWindow() {
// Disable system main menu
mainWindow.setMenu(null);
electronLocalshortcut.register(mainWindow, 'f5', () => {
electronLocalshortcut.register(mainWindow, 'F5', () => {
mainWindow.reload();
});
electronLocalshortcut.register(mainWindow, 'CommandOrControl+R', () => {
@ -409,38 +408,56 @@ ipc.on('show-window', () => {
showWindow();
});
let updatesStarted = false;
ipc.on('ready-for-updates', async () => {
if (updatesStarted || !updater) {
let isReadyForUpdates = false;
async function readyForUpdates() {
if (isReadyForUpdates) {
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 {
await updater.start(getMainWindow, locale.messages, logger);
} catch (error) {
logger.error(
const log = logger || console;
log.error(
'Error starting update checks:',
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() {
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() {
shell.openExternal(
'https://github.com/loki-project/loki-messenger/issues/new'
'https://github.com/loki-project/session-desktop/issues/new'
);
}
function openSupportPage() {
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();
// Check updates
readyForUpdates();
}
function setupMenu(options) {
@ -1025,6 +1045,7 @@ ipc.on('password-window-login', async (event, passPhrase) => {
const passwordAttempt = true;
await showMainWindow(passPhrase, passwordAttempt);
sendResponse();
if (passwordWindow) {
passwordWindow.close();
passwordWindow = null;

View file

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

View file

@ -2,6 +2,8 @@
/* global window: false */
const path = require('path');
const electron = require('electron');
const { webFrame } = electron;
const semver = require('semver');
const { deferredToPromise } = require('./js/modules/deferred_to_promise');
@ -70,6 +72,7 @@ window.CONSTANTS = {
MAX_MESSAGE_BODY_LENGTH: 64 * 1024,
// Limited due to the proof-of-work requirement
SMALL_GROUP_SIZE_LIMIT: 10,
NOTIFICATION_ENABLE_TIMEOUT_SECONDS: 10, // number of seconds to turn on notifications after reconnect/start of app
};
window.versionInfo = {
@ -84,6 +87,19 @@ window.wrapDeferred = deferredToPromise;
const ipc = electron.ipcRenderer;
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);
// Set the password for the database
@ -217,6 +233,10 @@ window.getSettingValue = (settingID, comparisonValue = null) => {
window.setSettingValue = (settingID, value) => {
window.storage.put(settingID, value);
if (settingID === 'zoom-factor-setting') {
window.updateZoomFactor();
}
};
installGetter('device-name', 'getDeviceName');
@ -474,23 +494,6 @@ contextMenu({
// /tmp mounted as noexec on Linux.
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.pubkeyPattern = /@[a-fA-F0-9]{64,66}\b/g;
@ -508,3 +511,26 @@ Promise.prototype.ignore = function() {
// eslint-disable-next-line more/no-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: () => [],
};
}

View file

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

View file

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

View file

@ -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 {
display: flex;
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 {
display: flex;
justify-content: center;

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
<html>
<head>
<meta charset='utf-8'>
<meta charset="utf-8">
<title>TextSecure test runner</title>
<link rel="stylesheet" href="../node_modules/mocha/mocha.css" />
<link rel="stylesheet" href="../stylesheets/manifest.css" />
@ -11,90 +11,90 @@
</div>
<div id="tests">
</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 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>
<script type='text/x-tmpl-mustache' id='app-loading-screen'>
<div class='content'>
<img src='images/session/full-logo.svg' class='session-full-logo' />
<script type="text/x-tmpl-mustache" id="app-loading-screen">
<div class="content">
<img src="images/session/full-logo.svg" class="session-full-logo" />
</div>
</script>
<script type='text/x-tmpl-mustache' id='conversation-loading-screen'>
<div class='content'>
<img src='images/session/full-logo.svg' class='session-full-logo' />
<script type="text/x-tmpl-mustache" id="conversation-loading-screen">
<div class="content">
<img src="images/session/full-logo.svg" class="session-full-logo" />
</div>
</script>
<script type='text/x-tmpl-mustache' id='two-column'>
<div class='gutter'>
<div class='network-status-container'></div>
<div class='left-pane-placeholder'></div>
<script type="text/x-tmpl-mustache" id="two-column">
<div class="gutter">
<div class="network-status-container"></div>
<div class="left-pane-placeholder"></div>
</div>
<div class='conversation-stack'>
<div class='conversation placeholder'>
<div class='conversation-header'></div>
<div class='container'>
<div class='content'>
<img src='images/session/full-logo.svg' class='session-full-logo' />
<div class="conversation-stack">
<div class="conversation placeholder">
<div class="conversation-header"></div>
<div class="container">
<div class="content">
<img src="images/session/full-logo.svg" class="session-full-logo" />
</div>
</div>
</div>
</div>
<div class='lightbox-container'></div>
<div class="lightbox-container"></div>
</script>
<script type='text/x-tmpl-mustache' id='scroll-down-button-view'>
<button class='text module-scroll-down__button {{ buttonClass }}' alt='{{ moreBelow }}'>
<div class='module-scroll-down__icon'></div>
<script type="text/x-tmpl-mustache" id="scroll-down-button-view">
<button class="text module-scroll-down__button {{ buttonClass }}" alt="{{ moreBelow }}">
<div class="module-scroll-down__icon"></div>
</button>
</script>
<script type='text/x-tmpl-mustache' id='last-seen-indicator-view'>
<div class='module-last-seen-indicator__bar'/>
<div class='module-last-seen-indicator__text'>
<script type="text/x-tmpl-mustache" id="last-seen-indicator-view">
<div class="module-last-seen-indicator__bar"/>
<div class="module-last-seen-indicator__text">
{{ unreadMessages }}
</div>
</script>
<script type='text/x-tmpl-mustache' id='banner'>
<div class='body'>
<span class='icon warning'></span>
<script type="text/x-tmpl-mustache" id="banner">
<div class="body">
<span class="icon warning"></span>
{{ message }}
<span class='icon dismiss'></span>
<span class="icon dismiss"></span>
</div>
</script>
<script type='text/x-tmpl-mustache' id='toast'>
<script type="text/x-tmpl-mustache" id="toast">
{{ toastMessage }}
</script>
<script type='text/x-tmpl-mustache' id='conversation'>
<div class='conversation-header'></div>
<div class='main panel'>
<div class='discussion-container'>
<div class='bar-container hide'>
<div class='bar active progress-bar-striped progress-bar'></div>
<script type="text/x-tmpl-mustache" id="conversation">
<div class="conversation-header"></div>
<div class="main panel">
<div class="discussion-container">
<div class="bar-container hide">
<div class="bar active progress-bar-striped progress-bar"></div>
</div>
</div>
<div class='bottom-bar' id='footer'>
<div class='emoji-panel-container'></div>
<div class='attachment-list'></div>
<div class='compose'>
<form class='send clearfix file-input'>
<div class='flex'>
<button class='emoji'></button>
<textarea class='send-message' placeholder='{{ send-message }}' rows='1' dir='auto'></textarea>
<div class='capture-audio'>
<button class='microphone'></button>
<div class="bottom-bar" id="footer">
<div class="emoji-panel-container"></div>
<div class="attachment-list"></div>
<div class="compose">
<form class="send clearfix file-input">
<div class="flex">
<button class="emoji"></button>
<textarea class="send-message" placeholder="{{ send-message }}" rows="1" dir="auto"></textarea>
<div class="capture-audio">
<button class="microphone"></button>
</div>
<div class='choose-file'>
<button class='paperclip thumbnail'></button>
<input type='file' class='file-input' multiple='multiple'>
<div class="choose-file">
<button class="paperclip thumbnail"></button>
<input type="file" class="file-input" multiple="multiple">
</div>
</div>
</form>
@ -103,39 +103,39 @@
</div>
</script>
<script type='text/x-tmpl-mustache' id='message-list'>
<div class='messages'></div>
<div class='typing-container'></div>
<script type="text/x-tmpl-mustache" id="message-list">
<div class="messages"></div>
<div class="typing-container"></div>
</script>
<script type='text/x-tmpl-mustache' id='recorder'>
<button class='finish'><span class='icon'></span></button>
<span class='time'>0:00</span>
<button class='close'><span class='icon'></span></button>
<script type="text/x-tmpl-mustache" id="recorder">
<button class="finish"><span class="icon"></span></button>
<span class="time">0:00</span>
<button class="close"><span class="icon"></span></button>
</script>
<script type='text/x-tmpl-mustache' id='nickname-dialog'>
<div class='content'>
<div class='message'>{{ message }}</div>
<input type='text' name='name' class='name' placeholder='Type a name' value='{{ name }}'>
<div class='buttons'>
<button class='clear' tabindex='3'>{{ clear }}</button>
<button class='cancel' tabindex='2'>{{ cancel }}</button>
<button class='ok' tabindex='1'>{{ ok }}</button>
</div>
</div>
</script>
<script type='text/x-tmpl-mustache' id='confirmation-dialog'>
<script type="text/x-tmpl-mustache" id="nickname-dialog">
<div class="content">
<div class='message'>{{ message }}</div>
<div class='buttons'>
{{ #showCancel }}
<button class='cancel' tabindex='2'>{{ cancel }}</button>
{{ /showCancel }}
<button class='ok' tabindex='1'>{{ ok }}</button>
<div class="message">{{ message }}</div>
<input type="text" name="name" class="name" placeholder="Type a name" value="{{ name }}">
<div class="buttons">
<button class="clear" tabindex="3">{{ clear }}</button>
<button class="cancel" tabindex="2">{{ cancel }}</button>
<button class="ok" tabindex="1">{{ ok }}</button>
</div>
</div>
</script>
<script type='text/x-tmpl-mustache' id='beta-disclaimer-dialog'>
<script type="text/x-tmpl-mustache" id="confirmation-dialog">
<div class="content">
<div class="message">{{ message }}</div>
<div class="buttons">
{{ #showCancel }}
<button class="cancel" tabindex="2">{{ cancel }}</button>
{{ /showCancel }}
<button class="ok" tabindex="1">{{ ok }}</button>
</div>
</div>
</script>
<script type="text/x-tmpl-mustache" id="beta-disclaimer-dialog">
<div class="content">
<div class="betaDisclaimerView" style="display: none;">
<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>.
</p>
<button class='ok' tabindex='1'>{{ ok }}</button>
<button class="ok" tabindex="1">{{ ok }}</button>
</div>
</div>
</script>
<script type='text/x-tmpl-mustache' id='identicon-svg'>
<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'>
<circle cx='50' cy='50' r='40' fill='{{ color }}' />
<text text-anchor='middle' fill='white' font-family='sans-serif' font-size='24px' x='50' y='50' baseline-shift='-8px'>
<script type="text/x-tmpl-mustache" id="identicon-svg">
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<circle cx="50" cy="50" r="40" fill="{{ color }}" />
<text text-anchor="middle" fill="white" font-family="sans-serif" font-size="24px" x="50" y="50" baseline-shift="-8px">
{{ content }}
</text>
</svg>
</script>
<script type='text/x-tmpl-mustache' id='phone-number'>
<div class='phone-input-form'>
<div class='number-container'>
<input type='tel' class='number' placeholder="Phone Number" />
<script type="text/x-tmpl-mustache" id="phone-number">
<div class="phone-input-form">
<div class="number-container">
<input type="tel" class="number" placeholder="Phone Number" />
</div>
</div>
</script>
<script type='text/x-tmpl-mustache' id='file-size-modal'>
<script type="text/x-tmpl-mustache" id="file-size-modal">
{{ file-size-warning }}
({{ limit }}{{ units }})
</script>
<script type='text/x-tmpl-mustache' id='attachment-type-modal'>
<script type="text/x-tmpl-mustache" id="attachment-type-modal">
Sorry, your attachment has a type, {{type}}, that is not currently supported.
</script>
<script type='text/x-tmpl-mustache' id='group-member-list'>
<div class='container'>
{{ #summary }} <div class='summary'>{{ summary }}</div>{{ /summary }}
<script type="text/x-tmpl-mustache" id="group-member-list">
<div class="container">
{{ #summary }} <div class="summary">{{ summary }}</div>{{ /summary }}
</div>
</script>
<script type='text/x-tmpl-mustache' id='key-verification'>
<div class='container'>
<script type="text/x-tmpl-mustache" id="key-verification">
<div class="container">
{{ ^hasTheirKey }}
<div class='placeholder'>{{ theirKeyUnknown }}</div>
<div class="placeholder">{{ theirKeyUnknown }}</div>
{{ /hasTheirKey }}
{{ #hasTheirKey }}
<label> {{ yourSafetyNumberWith }} </label>
<!--<div class='qr'></div>-->
<div class='key'>
<!--<div class="qr"></div>-->
<div class="key">
{{ #chunks }} <span>{{ . }}</span> {{ /chunks }}
</div>
{{ /hasTheirKey }}
{{ verifyHelp }}
<p> {{> link_to_support }} </p>
<div class='summary'>
<div class="summary">
{{ #isVerified }}
<span class='icon verified'></span>
<span class="icon verified"></span>
{{ /isVerified }}
{{ ^isVerified }}
<span class='icon shield'></span>
<span class="icon shield"></span>
{{ /isVerified }}
{{ verifiedStatus }}
</div>
<div class='verify'>
<button class='verify grey'>
<div class="verify">
<button class="verify grey">
{{ verifyButton }}
</button>
</div>
</div>
</script>
<script type='text/x-tmpl-mustache' id='clear-data'>
<script type="text/x-tmpl-mustache" id="clear-data">
{{#isStep1}}
<div class='step'>
<div class='inner'>
<div class='step-body'>
<span class='banner-icon alert-outline-red'></span>
<div class='header'>{{ header }}</div>
<div class='body-text-wide'>{{ body }}</div>
<div class="step">
<div class="inner">
<div class="step-body">
<span class="banner-icon alert-outline-red"></span>
<div class="header">{{ header }}</div>
<div class="body-text-wide">{{ body }}</div>
</div>
<div class='nav'>
<div class="nav">
<div>
<a class='button neutral cancel'>{{ cancelButton }}</a>
<a class='button destructive delete-all-data'>{{ deleteButton }}</a>
<a class="button neutral cancel">{{ cancelButton }}</a>
<a class="button destructive delete-all-data">{{ deleteButton }}</a>
</div>
</div>
</div>
</div>
{{/isStep1}}
{{#isStep2}}
<div id='step3' class='step'>
<div class='inner'>
<div class='step-body'>
<span class='banner-icon delete'></span>
<div class='header'>{{ deleting }}</div>
<div id="step3" class="step">
<div class="inner">
<div class="step-body">
<span class="banner-icon delete"></span>
<div class="header">{{ deleting }}</div>
</div>
<div class='progress'>
<div class='bar-container'>
<div class='bar progress-bar progress-bar-striped active'></div>
<div class="progress">
<div class="bar-container">
<div class="bar progress-bar progress-bar-striped active"></div>
</div>
</div>
</div>
@ -269,8 +269,8 @@
{{/isStep2}}
</script>
<script type='text/x-tmpl-mustache' id='networkStatus'>
<div class='network-status-message'>
<script type="text/x-tmpl-mustache" id="networkStatus">
<div class="network-status-message">
<h3>{{ message }}</h3>
<span>{{ instructions }}</span>
</div>
@ -281,60 +281,60 @@
{{/reconnectDurationAsSeconds }}
{{ #action }}
<div class="action">
<button class='small blue {{ buttonClass }}'>{{ action }}</button>
<button class="small blue {{ buttonClass }}">{{ action }}</button>
</div>
{{/action }}
</script>
<script type='text/x-tmpl-mustache' id='import-flow-template'>
<script type="text/x-tmpl-mustache" id="import-flow-template">
{{#isStep2}}
<div id='step2' class='step'>
<div class='inner'>
<div class='step-body'>
<span class='banner-icon folder-outline'></span>
<div class='header'>{{ chooseHeader }}</div>
<div class='body-text'>{{ choose }}</div>
<div id="step2" class="step">
<div class="inner">
<div class="step-body">
<span class="banner-icon folder-outline"></span>
<div class="header">{{ chooseHeader }}</div>
<div class="body-text">{{ choose }}</div>
</div>
<div class='nav'>
<div class="nav">
<div>
<a class='button choose'>{{ chooseButton }}</a>
<a class="button choose">{{ chooseButton }}</a>
</div>
</div>
</div>
</div>
{{/isStep2}}
{{#isStep3}}
<div id='step3' class='step'>
<div class='inner'>
<div class='step-body'>
<span class='banner-icon import'></span>
<div class='header'>{{ importingHeader }}</div>
<div id="step3" class="step">
<div class="inner">
<div class="step-body">
<span class="banner-icon import"></span>
<div class="header">{{ importingHeader }}</div>
</div>
<div class='progress'>
<div class='bar-container'>
<div class='bar progress-bar progress-bar-striped active'></div>
<div class="progress">
<div class="bar-container">
<div class="bar progress-bar progress-bar-striped active"></div>
</div>
</div>
</div>
</div>
{{/isStep3}}
{{#isStep4}}
<div id='step4' class='step'>
<div class='inner'>
<div class='step-body'>
<span class='banner-icon check-circle-outline'></span>
<div class='header'>{{ completeHeader }}</div>
<div id="step4" class="step">
<div class="inner">
<div class="step-body">
<span class="banner-icon check-circle-outline"></span>
<div class="header">{{ completeHeader }}</div>
</div>
<div class='nav'>
<div class="nav">
{{#restartButton}}
<div>
<a class='button restart'>{{ restartButton }}</a>
<a class="button restart">{{ restartButton }}</a>
</div>
{{/restartButton}}
{{#registerButton}}
<div>
<a class='button register'>{{ registerButton }}</a>
<a class="button register">{{ registerButton }}</a>
</div>
{{/registerButton}}
</div>
@ -343,19 +343,19 @@
{{/isStep4}}
{{#isError}}
<div id='error' class='step'>
<div class='inner error-dialog clearfix'>
<div class='step-body'>
<span class='banner-icon alert-outline'></span>
<div class='header'>{{ errorHeader }}</div>
<div class='body-text-wide'>
<div id="error" class="step">
<div class="inner error-dialog clearfix">
<div class="step-body">
<span class="banner-icon alert-outline"></span>
<div class="header">{{ errorHeader }}</div>
<div class="body-text-wide">
{{ errorMessageFirst }}
<p>{{ errorMessageSecond }}</p>
</div>
</div>
<div class='nav'>
<div class="nav">
<div>
<a class='button choose'>{{ chooseButton }}</a>
<a class="button choose">{{ chooseButton }}</a>
</div>
</div>
</div>
@ -363,37 +363,37 @@
{{/isError}}
</script>
<script type='text/x-tmpl-mustache' id='link-flow-template'>
<script type="text/x-tmpl-mustache" id="link-flow-template">
{{#isStep3}}
<div id='step3' class='step'>
<div class='inner'>
<div class='step-body'>
<div class='header'>{{ linkYourPhone }}</div>
<div id="step3" class="step">
<div class="inner">
<div class="step-body">
<div class="header">{{ linkYourPhone }}</div>
<div id="qr">
<div class='container'>
<span class='dot'></span>
<span class='dot'></span>
<span class='dot'></span>
<div class="container">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
</div>
</div>
<div class='nav'>
<div class='instructions'>
<div class='android'>
<div class='label'>
<span class='os-icon android'></span>
<div class="nav">
<div class="instructions">
<div class="android">
<div class="label">
<span class="os-icon android"></span>
</div>
<div class='body'>
<div class="body">
<div>→ {{ signalSettings }}</div>
<div>→ {{ linkedDevices }}</div>
<div>→ {{ androidFinalStep }}</div>
</div>
</div>
<div class='apple'>
<div class='label'>
<span class='os-icon apple'></span>
<div class="apple">
<div class="label">
<span class="os-icon apple"></span>
</div>
<div class='body'>
<div class="body">
<div>→ {{ signalSettings }}</div>
<div>→ {{ linkedDevices }}</div>
<div>→ {{ appleFinalStep }}</div>
@ -405,20 +405,20 @@
</div>
{{/isStep3}}
{{#isStep4}}
<form id='link-phone'>
<div id='step4' class='step'>
<div class='inner'>
<div class='step-body'>
<span class='banner-icon lead-pencil'></span>
<div class='header'>{{ chooseName }}</div>
<form id="link-phone">
<div id="step4" class="step">
<div class="inner">
<div class="step-body">
<span class="banner-icon lead-pencil"></span>
<div class="header">{{ chooseName }}</div>
<div>
<input type='text' class='device-name' spellcheck='false' maxlength='50' />
<input type="text" class="device-name" spellcheck="false" maxlength="50" />
</div>
</div>
<div class='nav'>
<div class="nav">
<div>
<a class='button finish'>{{ finishLinkingPhoneButton }}</a>
<a class="button finish">{{ finishLinkingPhoneButton }}</a>
</div>
</div>
</div>
@ -426,15 +426,15 @@
</form>
{{/isStep4}}
{{#isStep5}}
<div id='step5' class='step'>
<div class='inner'>
<div class='step-body'>
<span class='banner-icon sync'></span>
<div class='header'>{{ syncing }}</div>
<div id="step5" class="step">
<div class="inner">
<div class="step-body">
<span class="banner-icon sync"></span>
<div class="header">{{ syncing }}</div>
</div>
<div class='progress'>
<div class='bar-container'>
<div class='bar progress-bar progress-bar-striped active'></div>
<div class="progress">
<div class="bar-container">
<div class="bar progress-bar progress-bar-striped active"></div>
</div>
</div>
</div>
@ -442,44 +442,44 @@
{{/isStep5}}
{{#isError}}
<div id='error' class='step'>
<div class='inner'>
<div class='step-body'>
<span class='banner-icon alert-outline'></span>
<div class='header'>{{ errorHeader }}</div>
<div class='body'>{{ errorMessage }}</div>
<div id="error" class="step">
<div class="inner">
<div class="step-body">
<span class="banner-icon alert-outline"></span>
<div class="header">{{ errorHeader }}</div>
<div class="body">{{ errorMessage }}</div>
</div>
<div class='nav'>
<a class='button try-again'>{{ errorButton }}</a>
<div class="nav">
<a class="button try-again">{{ errorButton }}</a>
</div>
</div>
</div>
{{/isError}}
</script>
<script type='text/x-tmpl-mustache' id='standalone'>
<div class='step'>
<div class='inner'>
<div class='step-body'>
<img class='banner-image' src='images/icon_128.png' />
<div class='header'>Create your Signal Account</div>
<div id='phone-number-input'>
<div class='phone-input-form'>
<div id='number-container' class='number-container'>
<input type='tel' class='number' placeholder='Phone Number' />
<script type="text/x-tmpl-mustache" id="standalone">
<div class="step">
<div class="inner">
<div class="step-body">
<img class="banner-image" src="images/icon_128.png" />
<div class="header">Create your Signal Account</div>
<div id="phone-number-input">
<div class="phone-input-form">
<div id="number-container" class="number-container">
<input type="tel" class="number" placeholder="Phone Number" />
</div>
</div>
</div>
<div class='clearfix'>
<a class='button' id='request-sms'>Send SMS</a>
<a class='link' id='request-voice' tabindex=-1>Call</a>
<div class="clearfix">
<a class="button" id="request-sms">Send SMS</a>
<a class="link" id="request-voice" tabindex=-1>Call</a>
</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'>
<div id='error' class='collapse'></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">
<div id="error" class="collapse"></div>
<div id=status></div>
</div>
<div class='nav'>
<a class='button' id='verifyCode' data-loading-text='Please wait...'>Register</a>
<div class="nav">
<a class="button" id="verifyCode" data-loading-text="Please wait...">Register</a>
</div>
</div>
</div>
@ -491,7 +491,7 @@
<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='../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/chromium.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/blockedNumbers.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/keychange_listener.js" data-cover></script>
<script type='text/javascript' src='../js/expiring_messages.js' data-cover></script>
<script type='text/javascript' src='../js/notifications.js' data-cover></script>
<script type='text/javascript' src='../js/focus_listener.js'></script>
<script type="text/javascript" src="../js/expiring_messages.js" data-cover></script>
<script type="text/javascript" src="../js/notifications.js" data-cover></script>
<script type="text/javascript" src="../js/focus_listener.js"></script>
<script type="text/javascript" src="../js/chromium.js" data-cover></script>
<script type='text/javascript' src='../js/views/react_wrapper_view.js'></script>
<script type='text/javascript' src='../js/views/whisper_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/debug_log_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/toast_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/file_input_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/list_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/contact_list_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/timestamp_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/message_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/key_verification_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/message_list_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/member_list_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/bulk_edit_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/group_member_list_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/recorder_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/conversation_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/hint_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/inbox_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/network_status_view.js'></script>
<script type='text/javascript' src='../js/views/confirmation_dialog_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/nickname_dialog_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/password_dialog_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/seed_dialog_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/qr_dialog_view.js'></script>
<script type='text/javascript' src='../js/views/add_server_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/last_seen_indicator_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/scroll_down_button_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/banner_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/conversation_loading_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/invite_friends_dialog_view.js'></script>
<script type='text/javascript' src='../js/views/beta_release_disclaimer_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"></script>
<script type="text/javascript" src="../js/views/last_seen_indicator_view.js"></script>
<script type="text/javascript" src="../js/views/scroll_down_button_view.js"></script>
<script type="text/javascript" src="../js/views/toast_view.js"></script>
<script type="text/javascript" src="../js/views/session_toast_view.js"></script>
<script type="text/javascript" src="../js/views/conversation_loading_view.js"></script>
<script type="text/javascript" src="../js/views/session_toggle_view.js"></script>
<script type="text/javascript" src="../js/views/session_modal_view.js"></script>
<script type="text/javascript" src="../js/views/session_dropdown_view.js"></script>
<script type="text/javascript" src="../js/views/session_confirm_view.js"></script>
<script type="text/javascript" src="../js/views/file_input_view.js"></script>
<script type="text/javascript" src="../js/views/list_view.js"></script>
<script type="text/javascript" src="../js/views/contact_list_view.js"></script>
<script type="text/javascript" src="../js/views/message_view.js"></script>
<script type="text/javascript" src="../js/views/key_verification_view.js"></script>
<script type="text/javascript" src="../js/views/message_list_view.js"></script>
<script type="text/javascript" src="../js/views/member_list_view.js"></script>
<script type="text/javascript" src="../js/views/bulk_edit_view.js"></script>
<script type="text/javascript" src="../js/views/group_member_list_view.js"></script>
<script type="text/javascript" src="../js/views/recorder_view.js"></script>
<script type="text/javascript" src="../js/views/conversation_view.js"></script>
<script type="text/javascript" src="../js/views/inbox_view.js"></script>
<script type="text/javascript" src="../js/views/network_status_view.js"></script>
<script type="text/javascript" src="../js/views/confirmation_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/nickname_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/password_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/seed_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/qr_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/connecting_to_server_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/beta_release_disclaimer_view.js"></script>
<script type="text/javascript" src="../js/views/identicon_svg_view.js"></script>
<script type="text/javascript" src="../js/views/install_view.js"></script>
<script type="text/javascript" src="../js/views/banner_view.js"></script>
<script type="text/javascript" src="../js/views/phone-input-view.js"></script>
<script type="text/javascript" src="../js/views/session_registration_view.js"></script>
<script type="text/javascript" src="../js/views/app_view.js"></script>
<script type="text/javascript" src="../js/views/import_view.js"></script>
<script type="text/javascript" src="../js/views/device_pairing_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/device_pairing_words_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/create_group_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/confirm_session_reset_view.js"></script>
<script type="text/javascript" src="../js/views/edit_profile_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/invite_friends_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/moderators_add_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/moderators_remove_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/user_details_dialog_view.js"></script>
<!-- <script type="text/javascript" src="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/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/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/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="blocked_number_controller_test.js"></script>

View file

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

View file

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

View file

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

View file

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

View file

@ -75,6 +75,10 @@ export class SearchResults extends React.Component<Props> {
))}
</div>
) : null}
{haveFriends
? this.renderContacts(i18n('friendsHeader'), friends, true)
: null}
{haveMessages ? (
<div className="module-search-results__messages">
{hideMessagesHeader ? null : (
@ -95,4 +99,26 @@ export class SearchResults extends React.Component<Props> {
</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