diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000..fabbe10497 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +src/vector/modernizr.js +src/component-index.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000000..c181384fd5 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ["./node_modules/matrix-react-sdk/.eslintrc.js"], +} diff --git a/.gitignore b/.gitignore index 2e34b6c1c1..c28df64c65 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ /node_modules /packages/ /webapp +/.npmrc .DS_Store npm-debug.log electron/dist +electron/pub diff --git a/.travis.yml b/.travis.yml index 1fbc4dccb7..af738bb429 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,4 +3,5 @@ node_js: - 6 # node v6, to match jenkins install: - npm install + - (cd node_modules/matrix-js-sdk && npm install) - (cd node_modules/matrix-react-sdk && npm run build) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12ebad994d..ee745baa1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,168 @@ -hanges in [0.8.4](https://github.com/vector-im/vector-web/releases/tag/v0.8.4) (2016-11-04) +Changes in [0.9.6](https://github.com/vector-im/riot-web/releases/tag/v0.9.6) (2017-01-16) +========================================================================================== +[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.6-rc.1...v0.9.6) + + * Update to matrix-js-sdk 0.9.6 for video calling fix + +Changes in [0.9.6-rc.1](https://github.com/vector-im/riot-web/releases/tag/v0.9.6-rc.1) (2017-01-13) +==================================================================================================== +[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.5...v0.9.6-rc.1) + + * Build the js-sdk in the CI script + [\#2920](https://github.com/vector-im/riot-web/pull/2920) + * Hopefully fix Windows shortcuts + [\#2917](https://github.com/vector-im/riot-web/pull/2917) + * Update README now the js-sdk has a transpile step + [\#2921](https://github.com/vector-im/riot-web/pull/2921) + * Use the role for 'toggle dev tools' + [\#2915](https://github.com/vector-im/riot-web/pull/2915) + * Enable screen sharing easter-egg in desktop app + [\#2909](https://github.com/vector-im/riot-web/pull/2909) + * make electron send email validation URLs with a nextlink of riot.im + [\#2808](https://github.com/vector-im/riot-web/pull/2808) + * add Debian Stretch install steps to readme + [\#2809](https://github.com/vector-im/riot-web/pull/2809) + * Update desktop build instructions fixes #2792 + [\#2793](https://github.com/vector-im/riot-web/pull/2793) + * CSS for the delete threepid button + [\#2784](https://github.com/vector-im/riot-web/pull/2784) + +Changes in [0.9.5](https://github.com/vector-im/riot-web/releases/tag/v0.9.5) (2016-12-24) +========================================================================================== +[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.4...v0.9.5) + + * make electron send email validation URLs with a nextlink of riot.im rather than file:/// + * add gnu-tar to debian electron build deps + * fix win32 shortcut in start menu + +Changes in [0.9.4](https://github.com/vector-im/riot-web/releases/tag/v0.9.4) (2016-12-22) +========================================================================================== +[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.3...v0.9.4) + + * Update to libolm 2.1.0. This should help resolve a problem with browser + sessions being logged out ([\#2726](https://github.com/vector-im/riot-web/issues/2726)). + +Changes in [0.9.3](https://github.com/vector-im/riot-web/releases/tag/v0.9.3) (2016-12-22) +========================================================================================== +[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.2...v0.9.3) + + * (from matrix-react-sdk) Fix regression where the date separator would be displayed + at the wrong time of day. + * README.md: fix GFMD for nativefier + [\#2755](https://github.com/vector-im/riot-web/pull/2755) + +Changes in [0.9.2](https://github.com/vector-im/riot-web/releases/tag/v0.9.2) (2016-12-16) +========================================================================================== +[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.1...v0.9.2) + + * Remove the client side filtering from the room dir + [\#2750](https://github.com/vector-im/riot-web/pull/2750) + * Configure olm memory size + [\#2745](https://github.com/vector-im/riot-web/pull/2745) + * Support room dir 3rd party network filtering + [\#2747](https://github.com/vector-im/riot-web/pull/2747) + +Changes in [0.9.1](https://github.com/vector-im/riot-web/releases/tag/v0.9.1) (2016-12-09) +========================================================================================== +[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.1-rc.2...v0.9.1) + + * Update README to say how to build the desktop app + [\#2732](https://github.com/vector-im/riot-web/pull/2732) + * Add signing ID in release_config.yaml + [\#2731](https://github.com/vector-im/riot-web/pull/2731) + * Makeover! + [\#2722](https://github.com/vector-im/riot-web/pull/2722) + * Fix broken tests + [\#2730](https://github.com/vector-im/riot-web/pull/2730) + * Make the 'loading' tests work in isolation + [\#2727](https://github.com/vector-im/riot-web/pull/2727) + * Use a PNG for the icon on non-Windows + [\#2708](https://github.com/vector-im/riot-web/pull/2708) + * Add missing brackets to call to toUpperCase + [\#2703](https://github.com/vector-im/riot-web/pull/2703) + +Changes in [0.9.1-rc.2](https://github.com/vector-im/riot-web/releases/tag/v0.9.1-rc.2) (2016-12-06) +==================================================================================================== +[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.1-rc.1...v0.9.1-rc.2) + + * Fix clicking on notifications + [\#2700](https://github.com/vector-im/riot-web/pull/2700) + * Desktop app: Only show window when ready + [\#2697](https://github.com/vector-im/riot-web/pull/2697) + +Changes in [0.9.1-rc.1](https://github.com/vector-im/riot-web/releases/tag/v0.9.1-rc.1) (2016-12-05) +==================================================================================================== +[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.0...v0.9.1-rc.1) + + * Final bits to prepare electron distribtion: + [\#2653](https://github.com/vector-im/riot-web/pull/2653) + * Update name & repo to reflect renamed repository + [\#2692](https://github.com/vector-im/riot-web/pull/2692) + * Document cross_origin_renderer_url + [\#2680](https://github.com/vector-im/riot-web/pull/2680) + * Add css for the iframes for e2e attachments + [\#2659](https://github.com/vector-im/riot-web/pull/2659) + * Fix config location in some more places + [\#2670](https://github.com/vector-im/riot-web/pull/2670) + * CSS updates for s/block/blacklist for e2e + [\#2662](https://github.com/vector-im/riot-web/pull/2662) + * Update to electron 1.4.8 + [\#2660](https://github.com/vector-im/riot-web/pull/2660) + * Add electron config + [\#2644](https://github.com/vector-im/riot-web/pull/2644) + * Move getDefaultDeviceName into the Platforms + [\#2643](https://github.com/vector-im/riot-web/pull/2643) + * Add Freenode & Mozilla domains + [\#2641](https://github.com/vector-im/riot-web/pull/2641) + * Include config.sample.json in dist tarball + [\#2614](https://github.com/vector-im/riot-web/pull/2614) + +Changes in [0.9.0](https://github.com/vector-im/vector-web/releases/tag/v0.9.0) (2016-11-19) +============================================================================================ +[Full Changelog](https://github.com/vector-im/vector-web/compare/v0.8.4...v0.9.0) + + * Add a cachebuster to /version + [\#2596](https://github.com/vector-im/vector-web/pull/2596) + * Add a 'View decrypted source' button + [\#2587](https://github.com/vector-im/vector-web/pull/2587) + * Fix changelog dialog to read new version format + [\#2577](https://github.com/vector-im/vector-web/pull/2577) + * Build all of the vector dir in the build process + [\#2558](https://github.com/vector-im/vector-web/pull/2558) + * Support for get_app_version + [\#2553](https://github.com/vector-im/vector-web/pull/2553) + * Add CSS for mlist truncation + [\#2565](https://github.com/vector-im/vector-web/pull/2565) + * Add menu option for `external_url` if present + [\#2560](https://github.com/vector-im/vector-web/pull/2560) + * Make auto-update configureable + [\#2555](https://github.com/vector-im/vector-web/pull/2555) + * Missed files electron windows fixes + [\#2556](https://github.com/vector-im/vector-web/pull/2556) + * Add some CSS for scalar error popup + [\#2554](https://github.com/vector-im/vector-web/pull/2554) + * Catch unhandled errors in the electron process + [\#2552](https://github.com/vector-im/vector-web/pull/2552) + * Slight grab-bag of fixes for electron on Windows + [\#2551](https://github.com/vector-im/vector-web/pull/2551) + * Electron app (take 3) + [\#2535](https://github.com/vector-im/vector-web/pull/2535) + * Use webpack-dev-server instead of http-server + [\#2542](https://github.com/vector-im/vector-web/pull/2542) + * Better support no-config when loading from file + [\#2541](https://github.com/vector-im/vector-web/pull/2541) + * Fix loading with no config from HTTP + [\#2540](https://github.com/vector-im/vector-web/pull/2540) + * Move 'new version' support into Platform + [\#2532](https://github.com/vector-im/vector-web/pull/2532) + * Add Notification support to the Web Platform + [\#2533](https://github.com/vector-im/vector-web/pull/2533) + * Use the defaults if given a blank config file + [\#2534](https://github.com/vector-im/vector-web/pull/2534) + * Implement Platforms + [\#2531](https://github.com/vector-im/vector-web/pull/2531) + +Changes in [0.8.4](https://github.com/vector-im/vector-web/releases/tag/v0.8.4) (2016-11-04) ============================================================================================ [Full Changelog](https://github.com/vector-im/vector-web/compare/v0.8.4-rc.2...v0.8.4) diff --git a/README.md b/README.md index 9d1fb48c0a..fd6acfd778 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ https://riot.im/develop for those who like living dangerously. To host your own copy of Riot, the quickest bet is to use a pre-built released version of Riot: -1. Download the latest version from https://github.com/vector-im/vector-web/releases +1. Download the latest version from https://github.com/vector-im/riot-web/releases 1. Untar the tarball on your web server 1. Move (or symlink) the vector-x.x.x directory to an appropriate name 1. If desired, copy `config.sample.json` to `config.json` and edit it @@ -25,6 +25,14 @@ Note that Chrome does not allow microphone or webcam access for sites served over http (except localhost), so for working VoIP you will need to serve Riot over https. +### Installation Steps for Debian Stretch +1. Add the repository to your sources.list using either of the following two options: + - Directly to sources.list: `echo "deb https://riot.im/packages/debian/ stretch main" | sudo tee -a /etc/apt/sources.list` + - As a separate entry in sources.list.d: `echo "deb https://riot.im/packages/debian/ stretch main" | sudo tee /etc/apt/sources.list.d/riot.list` +2. Add the gpg signing key for the riot repository: `curl -s https://riot.im/packages/debian/repo-key.asc | sudo apt-key add -` +3. Update your package lists: `sudo apt-get update` +4. Install Riot: `sudo apt-get install riot-web` + Important Security Note ======================= @@ -36,7 +44,7 @@ access to Riot (or other apps) due to sharing the same domain. We have put some coarse mitigations into place to try to protect against this situation, but it's still not good practice to do it in the first place. See -https://github.com/vector-im/vector-web/issues/1977 for more details. +https://github.com/vector-im/riot-web/issues/1977 for more details. Building From Source ==================== @@ -45,13 +53,17 @@ Riot is a modular webapp built with modern ES6 and requires a npm build system to build. 1. Install or update `node.js` so that your `npm` is at least at version `2.0.0` -1. Clone the repo: `git clone https://github.com/vector-im/vector-web.git` -1. Switch to the vector-web directory: `cd vector-web` +1. Clone the repo: `git clone https://github.com/vector-im/riot-web.git` +1. Switch to the riot-web directory: `cd riot-web` 1. Install the prerequisites: `npm install` 1. If you are using the `develop` branch of vector-web, you will probably need - to rebuild one of the dependencies, due to - https://github.com/npm/npm/issues/3055: `(cd node_modules/matrix-react-sdk - && npm install)` + to rebuild some of the dependencies, due to + https://github.com/npm/npm/issues/3055: + + ``` + (cd node_modules/matrix-js-sdk && npm install) + (cd node_modules/matrix-react-sdk && npm install) + ``` 1. Configure the app by copying `config.sample.json` to `config.json` and modifying it (see below for details) 1. `npm run dist` to build a tarball to deploy. Untaring this file will give @@ -59,16 +71,16 @@ to build. web server. Note that `npm run dist` is not supported on Windows, so Windows users can run `npm -run build`, which will build all the necessary files into the `vector` -directory. The version of Vector will not appear in Settings without -using the dist script. You can then mount the vector directory on your +run build`, which will build all the necessary files into the `webapp` +directory. The version of Riot will not appear in Settings without +using the dist script. You can then mount the `webapp` directory on your webserver to actually serve up the app, which is entirely static content. config.json =========== -You can configure the app by copying `vector/config.sample.json` to -`vector/config.json` and customising it: +You can configure the app by copying `config.sample.json` to +`config.json` and customising it: 1. `default_hs_url` is the default home server url. 1. `default_is_url` is the default identity server url (this is the server used @@ -81,55 +93,73 @@ You can configure the app by copying `vector/config.sample.json` to and https://vector.im. In future identity servers will be decentralised. 1. `integrations_ui_url`: URL to the web interface for the integrations server. 1. `integrations_rest_url`: URL to the REST interface for the integrations server. -1. `roomDirectory`: config for the public room directory. This section encodes behaviour - on the room directory screen for filtering the list by server / network type and joining - third party networks. This config section will disappear once APIs are available to - get this information for home servers. This section is optional. +1. `roomDirectory`: config for the public room directory. This section is optional. 1. `roomDirectory.servers`: List of other Home Servers' directories to include in the drop down list. Optional. -1. `roomDirectory.serverConfig`: Config for each server in `roomDirectory.servers`. Optional. -1. `roomDirectory.serverConfig..networks`: List of networks (named - in `roomDirectory.networks`) to include for this server. Optional. -1. `roomDirectory.networks`: config for each network type. Optional. -1. `roomDirectory..name`: Human-readable name for the network. Required. -1. `roomDirectory..protocol`: Protocol as given by the server in - `/_matrix/client/unstable/thirdparty/protocols` response. Required to be able to join - this type of third party network. -1. `roomDirectory..domain`: Domain as given by the server in - `/_matrix/client/unstable/thirdparty/protocols` response, if present. Required to be - able to join this type of third party network, if present in `thirdparty/protocols`. -1. `roomDirectory..portalRoomPattern`: Regular expression matching aliases - for portal rooms to locations on this network. Required. -1. `roomDirectory..icon`: URL to an icon to be displayed for this network. Required. -1. `roomDirectory..example`: Textual example of a location on this network, - eg. '#channel' for an IRC network. Optional. -1. `roomDirectory..nativePattern`: Regular expression that matches a - valid location on this network. This is used as a hint to the user to indicate - when a valid location has been entered so it's not necessary for this to be - exactly correct. Optional. +1. `update_base_url` (electron app only): HTTPS URL to a web server to download + updates from. This should be the path to the directory containing `macos` + and `win32` (for update packages, not installer packages). +1. `cross_origin_renderer_url`: URL to a static HTML page hosting code to help display + encrypted file attachments. This MUST be hosted on a completely separate domain to + anything else since it is used to isolate the privileges of file attachments to this + domain. Default: `usercontent.riot.im`. This needs to contain v1.html from + https://github.com/matrix-org/usercontent/blob/master/v1.html Running as a Desktop app ======================== -In future we'll do an official distribution of Riot as an desktop app. Meanwhile, -there are a few options: +Riot can also be run as a desktop app, wrapped in electron. You can download a +pre-built version from https://riot.im/desktop.html or, if you prefer, +built it yourself. -@asdf:matrix.org points out that you can use nativefier and it just works(tm): +To run as a desktop app: + +1. Follow the instructions in 'Building From Source' above +2. Install electron and run it: + + ``` + npm install electron + node_modules/.bin/electron . + ``` + +To build packages, use electron-builder. This is configured to output: + * dmg + zip for macOS + * exe + nupkg for Windows + * deb for Linux +But this can be customised by editing the `build` section of package.json +as per https://github.com/electron-userland/electron-builder/wiki/Options + +See https://github.com/electron-userland/electron-builder/wiki/Multi-Platform-Build +for dependencies required for building packages for various platforms. + +The only platform that can build packages for all three platforms is macOS: +``` +brew install wine --without-x11 +brew install mono +brew install gnu-tar +npm install +npm run build:electron +``` + +For other packages, use electron-builder manually. For example, to build a package +for 64 bit Linux: + + 1. Follow the instructions in 'Building From Source' above + 2. `node_modules/.bin/build -l --x64` + +All electron packages go into `electron/dist/` + +Many thanks to @aviraldg for the initial work on the electron integration. + +Other options for running as a desktop app: + * https://github.com/krisak/vector-electron-desktop + * @asdf:matrix.org points out that you can use nativefier and it just works(tm) ``` sudo npm install nativefier -g nativefier https://riot.im/app/ ``` -krisa has a dedicated electron project at -https://github.com/krisak/vector-electron-desktop (although you should swap out -the 'vector' folder for the latest vector tarball you want to run. Get a -tarball from https://github.com/vector-im/vector-web/releases or build your own -- see Building From Source above). - -There's also a (much) older electron distribution at https://github.com/stevenhammerton/vector-desktop - - Development =========== @@ -149,13 +179,13 @@ the `component-index.js` for the app (used in future for skinning) development on Riot forcing `matrix-react-sdk` to move fast at the expense of maintaining a clear abstraction between the two.** Hacking on Riot inevitably means hacking equally on `matrix-react-sdk`, and there are bits of -`matrix-react-sdk` behaviour incorrectly residing in the `vector-web` project +`matrix-react-sdk` behaviour incorrectly residing in the `riot-web` project (e.g. matrix-react-sdk specific CSS), and a bunch of Riot specific behaviour in the `matrix-react-sdk` (grep for `vector` / `riot`). This separation problem will be solved asap once development on Riot (and thus matrix-react-sdk) has stabilised. Until then, the two projects should basically be considered as a single unit. In particular, `matrix-react-sdk` issues are currently filed -against `vector-web` in github. +against `riot-web` in github. Please note that Riot is intended to run correctly without access to the public internet. So please don't depend on resources (JS libs, CSS, images, fonts) @@ -190,8 +220,8 @@ Then similarly with `matrix-react-sdk`: Finally, build and start Riot itself: -1. `git clone git@github.com:vector-im/vector-web.git` -1. `cd vector-web` +1. `git clone git@github.com:vector-im/riot-web.git` +1. `cd riot-web` 1. `git checkout develop` 1. `npm install` 1. `rm -r node_modules/matrix-js-sdk; ln -s ../../matrix-js-sdk node_modules/` @@ -215,10 +245,10 @@ Finally, build and start Riot itself: disables caching, so do NOT use it in production. 1. Open http://127.0.0.1:8080/ in your browser to see your newly built Riot. -When you make changes to `matrix-react-sdk`, you will need to run `npm run -build` in the relevant directory. You can do this automatically by instead -running `npm start` in the directory, to start a development builder which -will watch for changes to the files and rebuild automatically. +When you make changes to `matrix-react-sdk` or `matrix-js-sdk`, you will need +to run `npm run build` in the relevant directory. You can do this automatically +by instead running `npm start` in the directory, to start a development builder +which will watch for changes to the files and rebuild automatically. If you add or remove any components from the Riot skin, you will need to rebuild the skin's index by running, `npm run reskindex`. diff --git a/config.sample.json b/config.sample.json index 49303769af..a65646ac77 100644 --- a/config.sample.json +++ b/config.sample.json @@ -4,64 +4,11 @@ "brand": "Riot", "integrations_ui_url": "https://scalar.vector.im/", "integrations_rest_url": "https://scalar.vector.im/api", + "bug_report_endpoint_url": "https://vector.im/bugs", "enableLabs": true, "roomDirectory": { "servers": [ "matrix.org" - ], - "serverConfig": { - "matrix.org": { - "networks": [ - "_matrix", - "gitter", - "irc:freenode", - "irc:mozilla", - "irc:snoonet", - "irc:oftc" - ] - } - }, - "networks": { - "gitter": { - "protocol": "gitter", - "portalRoomPattern": "#gitter_.*:matrix.org", - "name": "Gitter", - "icon": "//gitter.im/favicon.ico", - "example": "org/community", - "nativePattern": "[^\\s]+/[^\\s]+$" - }, - "irc:freenode": { - "portalRoomPattern": "#freenode_.*:matrix.org", - "name": "Freenode", - "icon": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX", - "example": "#channel", - "nativePattern": "^#[^\\s]+$" - }, - "irc:mozilla": { - "portalRoomPattern": "#mozilla_.*:matrix.org", - "name": "Mozilla", - "icon": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX", - "example": "#channel", - "nativePattern": "^#[^\\s]+$" - }, - "irc:snoonet": { - "protocol": "irc", - "domain": "ipv6-irc.snoonet.org", - "portalRoomPattern": "#_snoonet_.*:matrix.org", - "name": "Snoonet", - "icon": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX", - "example": "#channel", - "nativePattern": "^#[^\\s]+$" - }, - "irc:oftc": { - "protocol": "irc", - "domain": "irc.oftc.net", - "portalRoomPattern": "#_oftc_.*:matrix.org", - "name": "OFTC", - "icon": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX", - "example": "#channel", - "nativePattern": "^#[^\\s]+$" - } - } + ] } } diff --git a/docs/theming.md b/docs/theming.md new file mode 100644 index 0000000000..c6373e52b6 --- /dev/null +++ b/docs/theming.md @@ -0,0 +1,25 @@ +Theming Riot +============ + +Themes are a very basic way of providing simple alternative look & feels to the +riot-web app via CSS & custom imagery. + +They are *NOT* co be confused with 'skins', which describe apps which sit on top +of matrix-react-sdk - e.g. in theory Riot itself is a react-sdk skin. +As of Jan 2017, skins are not fully supported; riot is the only available skin. + +To define a theme for Riot: + + 1. Pick a name, e.g. `teal`. at time of writing we have `light` and `dark`. + 2. Fork `src/skins/vector/css/themes/dark.scss` to be teal.scss + 3. Fork `src/skins/vector/css/themes/_base.scss` to be _teal.scss + 4. Override variables in _teal.scss as desired. You may wish to delete ones + which don't differ from _base.scss, to make it clear which are being + overridden. If every single colour is being changed (as per _dark.scss) + then you might as well keep them all. + 5. Add the theme to the list of entrypoints in webpack.config.js + 6. Add the theme to the list of themes in matrix-react-sdk's UserSettings.js + 7. Sit back and admire your handywork. + +In future, the assets for a theme will probably be gathered together into a +single directory tree. diff --git a/electron/build/icon.icns b/electron/build/icon.icns index 55c03d96cd..d74e97e738 100644 Binary files a/electron/build/icon.icns and b/electron/build/icon.icns differ diff --git a/electron/build/icon.ico b/electron/build/icon.ico index 8b681ffba3..8f8ff94eea 100644 Binary files a/electron/build/icon.ico and b/electron/build/icon.ico differ diff --git a/electron/build/icons/128x128.png b/electron/build/icons/128x128.png new file mode 100644 index 0000000000..54149b1ae2 Binary files /dev/null and b/electron/build/icons/128x128.png differ diff --git a/electron/build/icons/16x16.png b/electron/build/icons/16x16.png new file mode 100644 index 0000000000..def4ec5eee Binary files /dev/null and b/electron/build/icons/16x16.png differ diff --git a/electron/build/icons/24x24.png b/electron/build/icons/24x24.png new file mode 100644 index 0000000000..6fe144ea13 Binary files /dev/null and b/electron/build/icons/24x24.png differ diff --git a/electron/build/icons/256x256.png b/electron/build/icons/256x256.png new file mode 100644 index 0000000000..563e6d5edc Binary files /dev/null and b/electron/build/icons/256x256.png differ diff --git a/electron/build/icons/48x48.png b/electron/build/icons/48x48.png new file mode 100644 index 0000000000..9cd225d402 Binary files /dev/null and b/electron/build/icons/48x48.png differ diff --git a/electron/build/icons/512x512.png b/electron/build/icons/512x512.png new file mode 100644 index 0000000000..328a723c97 Binary files /dev/null and b/electron/build/icons/512x512.png differ diff --git a/electron/build/icons/64x64.png b/electron/build/icons/64x64.png new file mode 100644 index 0000000000..20a7a6cff3 Binary files /dev/null and b/electron/build/icons/64x64.png differ diff --git a/electron/build/icons/96x96.png b/electron/build/icons/96x96.png new file mode 100644 index 0000000000..eb48c2c91e Binary files /dev/null and b/electron/build/icons/96x96.png differ diff --git a/electron/img/riot.png b/electron/img/riot.png new file mode 100644 index 0000000000..fe13aa99c3 Binary files /dev/null and b/electron/img/riot.png differ diff --git a/electron/riot.im/README b/electron/riot.im/README new file mode 100644 index 0000000000..09c218740c --- /dev/null +++ b/electron/riot.im/README @@ -0,0 +1,4 @@ +This directory contains the config file for the official riot.im distribution +of Riot Desktop. You probably do not want to build with this config unless +you're building the official riot.im distribution, or you'll find your builds +will replace themselves with the riot.im build. diff --git a/electron/riot.im/config.json b/electron/riot.im/config.json new file mode 100644 index 0000000000..e129e5bfb5 --- /dev/null +++ b/electron/riot.im/config.json @@ -0,0 +1,72 @@ +{ + "update_base_url": "https://riot.im/download/desktop/update/", + "default_hs_url": "https://matrix.org", + "default_is_url": "https://vector.im", + "brand": "Riot", + "integrations_ui_url": "https://scalar.vector.im/", + "integrations_rest_url": "https://scalar.vector.im/api", + "enableLabs": true, + "roomDirectory": { + "servers": [ + "matrix.org" + ], + "serverConfig": { + "matrix.org": { + "networks": [ + "_matrix", + "gitter", + "irc:freenode", + "irc:mozilla", + "irc:snoonet", + "irc:oftc" + ] + } + }, + "networks": { + "gitter": { + "protocol": "gitter", + "portalRoomPattern": "#gitter_.*:matrix.org", + "name": "Gitter", + "icon": "https://gitter.im/favicon.ico", + "example": "org/community", + "nativePattern": "[^\\s]+/[^\\s]+$" + }, + "irc:freenode": { + "protocol": "irc", + "domain": "chat.freenode.net", + "portalRoomPattern": "#freenode_.*:matrix.org", + "name": "Freenode", + "icon": "https://matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX", + "example": "#channel", + "nativePattern": "^#[^\\s]+$" + }, + "irc:mozilla": { + "protocol": "irc", + "domain": "chat.freenode.net", + "portalRoomPattern": "#mozilla_.*:matrix.org", + "name": "Mozilla", + "icon": "https://matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX", + "example": "#channel", + "nativePattern": "^#[^\\s]+$" + }, + "irc:snoonet": { + "protocol": "irc", + "domain": "ipv6-irc.snoonet.org", + "portalRoomPattern": "#_snoonet_.*:matrix.org", + "name": "Snoonet", + "icon": "https://matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX", + "example": "#channel", + "nativePattern": "^#[^\\s]+$" + }, + "irc:oftc": { + "protocol": "irc", + "domain": "irc.oftc.net", + "portalRoomPattern": "#_oftc_.*:matrix.org", + "name": "OFTC", + "icon": "https://matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX", + "example": "#channel", + "nativePattern": "^#[^\\s]+$" + } + } + } +} diff --git a/electron/src/electron-main.js b/electron/src/electron-main.js index 18758eee90..33b44ce9d1 100644 --- a/electron/src/electron-main.js +++ b/electron/src/electron-main.js @@ -26,11 +26,13 @@ if (check_squirrel_hooks()) return; const electron = require('electron'); const url = require('url'); +const tray = require('./tray'); + const VectorMenu = require('./vectormenu'); let vectorConfig = {}; try { - vectorConfig = require('../../vector/config.json'); + vectorConfig = require('../../webapp/config.json'); } catch (e) { // it would be nice to check the error code here and bail if the config // is unparseable, but we get MODULE_NOT_FOUND in the case of a missing @@ -101,9 +103,9 @@ function pollForUpdates() { } } -function startAutoUpdate(update_url) { - if (update_url.slice(-1) !== '/') { - update_url = update_url + '/'; +function startAutoUpdate(update_base_url) { + if (update_base_url.slice(-1) !== '/') { + update_base_url = update_base_url + '/'; } try { // For reasons best known to Squirrel, the way it checks for updates @@ -112,9 +114,18 @@ function startAutoUpdate(update_url) { // 204 No Content. On windows it takes a base path and looks for // files under that path. if (process.platform == 'darwin') { - electron.autoUpdater.setFeedURL(update_url); + // include the current version in the URL we hit. Electron doesn't add + // it anywhere (apart from the User-Agent) so it's up to us. We could + // (and previously did) just use the User-Agent, but this doesn't + // rely on NSURLConnection setting the User-Agent to what we expect, + // and also acts as a convenient cache-buster to ensure that when the + // app updates it always gets a fresh value to avoid update-looping. + electron.autoUpdater.setFeedURL( + update_base_url + + 'macos/?localVersion=' + encodeURIComponent(electron.app.getVersion()) + ); } else if (process.platform == 'win32') { - electron.autoUpdater.setFeedURL(update_url + 'win32/'); + electron.autoUpdater.setFeedURL(update_base_url + 'win32/' + process.arch + '/'); } else { // Squirrel / electron only supports auto-update on these two platforms. // I'm not even going to try to guess which feed style they'd use if they @@ -148,26 +159,56 @@ process.on('uncaughtException', function (error) { electron.ipcMain.on('install_update', installUpdate); +electron.app.commandLine.appendSwitch('--enable-usermedia-screen-capturing'); + +const shouldQuit = electron.app.makeSingleInstance((commandLine, workingDirectory) => { + // Someone tried to run a second instance, we should focus our window. + if (mainWindow) { + if (!mainWindow.isVisible()) mainWindow.show(); + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.focus(); + } +}); + +if (shouldQuit) { + electron.app.quit() +} + electron.app.on('ready', () => { - if (vectorConfig.update_url) { - console.log("Starting auto update with URL: " + vectorConfig.update_url); - startAutoUpdate(vectorConfig.update_url); + if (vectorConfig.update_base_url) { + console.log("Starting auto update with base URL: " + vectorConfig.update_base_url); + startAutoUpdate(vectorConfig.update_base_url); } else { - console.log("No update_url is defined: auto update is disabled"); + console.log("No update_base_url is defined: auto update is disabled"); } + const icon_path = `${__dirname}/../img/riot.` + ( + process.platform == 'win32' ? 'ico' : 'png' + ); + mainWindow = new electron.BrowserWindow({ - icon: `${__dirname}/../img/riot.ico`, + icon: icon_path, width: 1024, height: 768, + show: false, + autoHideMenuBar: true, }); mainWindow.loadURL(`file://${__dirname}/../../webapp/index.html`); electron.Menu.setApplicationMenu(VectorMenu); + // Create trayIcon icon + tray.create(mainWindow, { + icon_path: icon_path, + brand: vectorConfig.brand || 'Riot' + }); + + mainWindow.once('ready-to-show', () => { + mainWindow.show(); + }); mainWindow.on('closed', () => { mainWindow = null; }); mainWindow.on('close', (e) => { - if (process.platform == 'darwin' && !appQuitting) { + if (!appQuitting && (tray.hasTray() || process.platform == 'darwin')) { // On Mac, closing the window just hides it // (this is generally how single-window Mac apps // behave, eg. Mail.app) @@ -198,3 +239,9 @@ electron.app.on('activate', () => { electron.app.on('before-quit', () => { appQuitting = true; }); + +// Set the App User Model ID to match what the squirrel +// installer uses for the shortcut icon. +// This makes notifications work on windows 8.1 (and is +// a noop on other platforms). +electron.app.setAppUserModelId('com.squirrel.riot-web.Riot'); diff --git a/electron/src/squirrelhooks.js b/electron/src/squirrelhooks.js index 10fb8d9ec5..15ed670f0c 100644 --- a/electron/src/squirrelhooks.js +++ b/electron/src/squirrelhooks.js @@ -1,9 +1,30 @@ +/* +Copyright 2017 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + const path = require('path'); const spawn = require('child_process').spawn; const app = require('electron').app; function run_update_exe(args, done) { + // Invokes Squirrel's Update.exe which will do things for us like create shortcuts + // Note that there's an Update.exe in the app-x.x.x directory and one in the parent + // directory: we need to run the one in the parent directory, because it discovers + // information about the app by inspecting the directory it's run from. const updateExe = path.resolve(path.dirname(process.execPath), '..', 'Update.exe'); + console.log('Spawning `%s` with args `%s`', updateExe, args); spawn(updateExe, args, { detached: true }).on('close', done); diff --git a/electron/src/tray.js b/electron/src/tray.js new file mode 100644 index 0000000000..2ccdf40ccc --- /dev/null +++ b/electron/src/tray.js @@ -0,0 +1,67 @@ +/* +Copyright 2017 Karl Glatz +Copyright 2017 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const path = require('path'); +const electron = require('electron'); + +const app = electron.app; +const Tray = electron.Tray; +const MenuItem = electron.MenuItem; + +let trayIcon = null; + +exports.hasTray = function hasTray() { + return (trayIcon !== null); +} + +exports.create = function (win, config) { + // no trays on darwin + if (process.platform === 'darwin' || trayIcon) { + return; + } + + const toggleWin = function () { + if (win.isVisible() && !win.isMinimized()) { + win.hide(); + } else { + if (win.isMinimized()) win.restore(); + if (!win.isVisible()) win.show(); + win.focus(); + } + }; + + const contextMenu = electron.Menu.buildFromTemplate([ + { + label: 'Show/Hide ' + config.brand, + click: toggleWin + }, + { + type: 'separator' + }, + { + label: 'Quit', + click: function () { + app.quit(); + } + } + ]); + + trayIcon = new Tray(config.icon_path); + trayIcon.setToolTip(config.brand); + trayIcon.setContextMenu(contextMenu); + trayIcon.on('click', toggleWin); +}; diff --git a/electron/src/vectormenu.js b/electron/src/vectormenu.js index f4d55c15a7..70ed3ac33c 100644 --- a/electron/src/vectormenu.js +++ b/electron/src/vectormenu.js @@ -72,11 +72,7 @@ const template = [ role: 'togglefullscreen' }, { - label: 'Toggle Developer Tools', - accelerator: process.platform == 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I', - click: function(item, focusedWindow) { - if (focusedWindow) focusedWindow.toggleDevTools(); - } + role: 'toggledevtools' } ] }, diff --git a/karma.conf.js b/karma.conf.js index 2474216916..901832c78c 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -29,12 +29,22 @@ module.exports = function (config) { files: [ 'node_modules/babel-polyfill/browser.js', testFile, - {pattern: 'vector/img/*', watched: false, included: false, served: true, nocache: false}, + + // make the images available via our httpd. They will be avaliable + // below http://localhost:[PORT]/base/. See also `proxies` which + // defines alternative URLs for them. + // + // This isn't required by any of the tests, but it stops karma + // logging warnings when it serves a 404 for them. + { + pattern: 'src/skins/vector/img/*', + watched: false, included: false, served: true, nocache: false, + }, ], - // redirect img links to the karma server proxies: { - "/img/": "/base/vector/img/", + // redirect img links to the karma server. See above. + "/img/": "/base/src/skins/vector/img/", }, // preprocess matching files before serving them to the browser @@ -86,6 +96,12 @@ module.exports = function (config) { webpack: { module: { + preLoaders: [ + // use the source-map-loader for javascript. This means + // that we have a better chance of seeing line numbers from + // the pre-babeled source. + { test: /\.js$/, loader: "source-map-loader" }, + ], loaders: [ { test: /\.json$/, loader: "json" }, { diff --git a/package.json b/package.json index 173b9e1d18..915056e636 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { - "name": "vector-web", + "name": "riot-web", "productName": "Riot", "main": "electron/src/electron-main.js", - "version": "0.8.4", + "version": "0.9.6", "description": "A feature-rich client for Matrix.org", "author": "Vector Creations Ltd.", "repository": { "type": "git", - "url": "https://github.com/vector-im/vector-web" + "url": "https://github.com/vector-im/riot-web" }, "license": "Apache-2.0", "files": [ @@ -27,27 +27,23 @@ "matrix-react-parent": "matrix-react-sdk", "scripts": { "reskindex": "reskindex -h src/header", - "build:res": "cpx \"{src/skins/vector/fonts,src/skins/vector/img}/**\" webapp/ && cpx \"{res/media,res/vector-icons}/**\" webapp/", - "build:config": "cpx config.json webapp/", - "build:emojione": "cpx \"node_modules/emojione/assets/svg/*\" webapp/emojione/svg/", + "build:res": "node scripts/copy-res.js", "build:modernizr": "modernizr -c .modernizr.json -d src/vector/modernizr.js", - "build:css": "mkdirp build && catw \"src/skins/vector/css/**/*.css\" -o build/components.css --no-watch", "build:compile": "babel --source-maps -d lib src", "build:bundle": "NODE_ENV=production webpack -p --progress", "build:bundle:dev": "webpack --optimize-occurence-order --progress", - "build:electron": "build -lwm", - "build": "node scripts/babelcheck.js && npm run build:res && npm run build:config && npm run build:emojione && npm run build:css && npm run build:bundle", - "build:dev": "node scripts/babelcheck.js && npm run build:res && npm run build:config && npm run build:emojione && npm run build:css && npm run build:bundle:dev", + "build:electron": "npm run clean && npm run build && build -wml --ia32 --x64", + "build": "node scripts/babelcheck.js && npm run build:res && npm run build:bundle", + "build:dev": "node scripts/babelcheck.js && npm run build:res && npm run build:bundle:dev", "dist": "scripts/package.sh", - "start:res": "parallelshell \"cpx -w \\\"{src/skins/vector/fonts,src/skins/vector/img}/**\\\" webapp/\" \"cpx -w \\\"{res/media,res/vector-icons}/**\\\" webapp/\"", - "start:config": "cpx -w config.json webapp/", - "start:emojione": "cpx \"node_modules/emojione/assets/svg/*\" webapp/emojione/svg/ -w", - "start:js": "webpack-dev-server -w --progress", + "start:res": "node scripts/copy-res.js -w", + "start:js": "webpack-dev-server --output-filename=bundles/_dev_/[name].js --output-chunk-file=bundles/_dev_/[name].js -w --progress", "start:js:prod": "NODE_ENV=production webpack-dev-server -w --progress", - "start:skins:css": "mkdirp build && catw \"src/skins/vector/css/**/*.css\" -o build/components.css", - "start": "node scripts/babelcheck.js && parallelshell \"npm run start:emojione\" \"npm run start:res\" \"npm run start:config\" \"npm run start:js\" \"npm run start:skins:css\"", - "start:prod": "parallelshell \"npm run start:emojione\" \"npm run start:js:prod\" \"npm run start:skins:css\"", - "clean": "rimraf build lib webapp", + "start": "node scripts/babelcheck.js && parallelshell \"npm run start:res\" \"npm run start:js\"", + "start:prod": "parallelshell \"npm run start:res\" \"npm run start:js:prod\"", + "lint": "eslint src/", + "lintall": "eslint src/ test/", + "clean": "rimraf lib webapp electron/dist", "prepublish": "npm run build:compile", "test": "karma start --single-run=true --autoWatch=false --browsers PhantomJS --colors=false", "test:multi": "karma start" @@ -70,16 +66,17 @@ "matrix-react-sdk": "matrix-org/matrix-react-sdk#develop", "modernizr": "^3.1.0", "q": "^1.4.1", - "react": "^15.2.1", + "react": "^15.4.0", "react-dnd": "^2.1.4", "react-dnd-html5-backend": "^2.1.2", - "react-dom": "^15.2.1", + "react-dom": "^15.4.0", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", "sanitize-html": "^1.11.1", "ua-parser-js": "^0.7.10", "url": "^0.11.0" }, "devDependencies": { + "autoprefixer": "^6.6.0", "babel-cli": "^6.5.2", "babel-core": "^6.14.0", "babel-eslint": "^6.1.0", @@ -94,11 +91,16 @@ "babel-preset-es2017": "^6.16.0", "babel-preset-react": "^6.16.0", "babel-preset-stage-2": "^6.17.0", - "catw": "^1.0.1", + "chokidar": "^1.6.1", "cpx": "^1.3.2", "css-raw-loader": "^0.1.1", - "electron-builder": "^7.23.2", + "electron-builder": "^11.2.4", + "electron-builder-squirrel-windows": "^11.2.1", "emojione": "^2.2.3", + "eslint": "^3.14.0", + "eslint-config-google": "^0.7.1", + "eslint-plugin-flowtype": "^2.30.0", + "eslint-plugin-react": "^6.9.0", "expect": "^1.16.0", "fs-extra": "^0.30.0", "html-webpack-plugin": "^2.24.0", @@ -111,37 +113,52 @@ "karma-phantomjs-launcher": "^1.0.0", "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^1.7.0", + "minimist": "^1.2.0", "mkdirp": "^0.5.1", "mocha": "^2.4.5", "parallelshell": "^1.2.0", "phantomjs-prebuilt": "^2.1.7", - "react-addons-perf": "^15.0", - "react-addons-test-utils": "^15.0.1", + "postcss-extend": "^1.0.5", + "postcss-import": "^9.0.0", + "postcss-loader": "^1.2.2", + "postcss-mixins": "^5.4.1", + "postcss-nested": "^1.0.0", + "postcss-scss": "^0.4.0", + "postcss-simple-vars": "^3.0.0", + "postcss-strip-inline-comments": "^0.1.5", + "react-addons-perf": "^15.4.0", + "react-addons-test-utils": "^15.4.0", "rimraf": "^2.4.3", "source-map-loader": "^0.1.5", "webpack": "^1.12.14", "webpack-dev-server": "^1.16.2" }, "optionalDependencies": { - "olm": "https://matrix.org/packages/npm/olm/olm-2.0.0.tgz" + "olm": "https://matrix.org/packages/npm/olm/olm-2.1.0.tgz" }, "build": { "appId": "im.riot.app", "category": "Network", - "electronVersion": "1.4.2", + "electronVersion": "1.4.14", "//asar=false": "https://github.com/electron-userland/electron-builder/issues/675", "asar": false, "dereference": true, "//files": "We bundle everything, so we only need to include webapp/", "files": [ - "!**/*", "electron/src/**", "electron/img/**", "webapp/**", "package.json" ], - "squirrelWindows": { - "iconUrl": "https://riot.im/favicon.ico" + "linux": { + "target": "deb", + "maintainer": "support@riot.im", + "desktop": { + "StartupWMClass": "riot-web" + } + }, + "win": { + "target": "squirrel" } }, "directories": { diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000000..5305d9ed9e --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,13 @@ +module.exports = { + plugins: [ + require("postcss-import")(), + require("autoprefixer")(), + require("postcss-simple-vars")(), + require("postcss-extend")(), + require("postcss-nested")(), + require("postcss-mixins")(), + require("postcss-strip-inline-comments")(), + ], + "parser": "postcss-scss", + "local-plugins": true, +}; diff --git a/release_config.yaml b/release_config.yaml new file mode 100644 index 0000000000..2be2a0def5 --- /dev/null +++ b/release_config.yaml @@ -0,0 +1 @@ +signing_id: packages@riot.im diff --git a/res/vector-icons/android-chrome-144x144.png b/res/vector-icons/android-chrome-144x144.png index 2a83f52169..30c8cf18dc 100644 Binary files a/res/vector-icons/android-chrome-144x144.png and b/res/vector-icons/android-chrome-144x144.png differ diff --git a/res/vector-icons/android-chrome-192x192.png b/res/vector-icons/android-chrome-192x192.png index 5794bdb0e4..560e20ad8c 100644 Binary files a/res/vector-icons/android-chrome-192x192.png and b/res/vector-icons/android-chrome-192x192.png differ diff --git a/res/vector-icons/android-chrome-36x36.png b/res/vector-icons/android-chrome-36x36.png index 17b6fe83b6..ee41772f4d 100644 Binary files a/res/vector-icons/android-chrome-36x36.png and b/res/vector-icons/android-chrome-36x36.png differ diff --git a/res/vector-icons/android-chrome-48x48.png b/res/vector-icons/android-chrome-48x48.png index 7435fbd844..9cd225d402 100644 Binary files a/res/vector-icons/android-chrome-48x48.png and b/res/vector-icons/android-chrome-48x48.png differ diff --git a/res/vector-icons/android-chrome-72x72.png b/res/vector-icons/android-chrome-72x72.png index 9fdbbefb64..d9de0c5715 100644 Binary files a/res/vector-icons/android-chrome-72x72.png and b/res/vector-icons/android-chrome-72x72.png differ diff --git a/res/vector-icons/android-chrome-96x96.png b/res/vector-icons/android-chrome-96x96.png index b9d4326942..eb48c2c91e 100644 Binary files a/res/vector-icons/android-chrome-96x96.png and b/res/vector-icons/android-chrome-96x96.png differ diff --git a/res/vector-icons/apple-touch-icon-114x114.png b/res/vector-icons/apple-touch-icon-114x114.png index 0f0ba555bc..e07fa43752 100644 Binary files a/res/vector-icons/apple-touch-icon-114x114.png and b/res/vector-icons/apple-touch-icon-114x114.png differ diff --git a/res/vector-icons/apple-touch-icon-120x120.png b/res/vector-icons/apple-touch-icon-120x120.png index bd52479553..8265091318 100644 Binary files a/res/vector-icons/apple-touch-icon-120x120.png and b/res/vector-icons/apple-touch-icon-120x120.png differ diff --git a/res/vector-icons/apple-touch-icon-144x144.png b/res/vector-icons/apple-touch-icon-144x144.png index 2a83f52169..30c8cf18dc 100644 Binary files a/res/vector-icons/apple-touch-icon-144x144.png and b/res/vector-icons/apple-touch-icon-144x144.png differ diff --git a/res/vector-icons/apple-touch-icon-152x152.png b/res/vector-icons/apple-touch-icon-152x152.png index 04ed2d47ac..5bc63546c8 100644 Binary files a/res/vector-icons/apple-touch-icon-152x152.png and b/res/vector-icons/apple-touch-icon-152x152.png differ diff --git a/res/vector-icons/apple-touch-icon-180x180.png b/res/vector-icons/apple-touch-icon-180x180.png index fe13aa99c3..85e9f8ca74 100644 Binary files a/res/vector-icons/apple-touch-icon-180x180.png and b/res/vector-icons/apple-touch-icon-180x180.png differ diff --git a/res/vector-icons/apple-touch-icon-57x57.png b/res/vector-icons/apple-touch-icon-57x57.png index 3d7055c52d..253c3db70a 100644 Binary files a/res/vector-icons/apple-touch-icon-57x57.png and b/res/vector-icons/apple-touch-icon-57x57.png differ diff --git a/res/vector-icons/apple-touch-icon-60x60.png b/res/vector-icons/apple-touch-icon-60x60.png index 0dc6bcb534..192a34675b 100644 Binary files a/res/vector-icons/apple-touch-icon-60x60.png and b/res/vector-icons/apple-touch-icon-60x60.png differ diff --git a/res/vector-icons/apple-touch-icon-72x72.png b/res/vector-icons/apple-touch-icon-72x72.png index 9fdbbefb64..d9de0c5715 100644 Binary files a/res/vector-icons/apple-touch-icon-72x72.png and b/res/vector-icons/apple-touch-icon-72x72.png differ diff --git a/res/vector-icons/apple-touch-icon-76x76.png b/res/vector-icons/apple-touch-icon-76x76.png index 5724b6b80b..b94716457b 100644 Binary files a/res/vector-icons/apple-touch-icon-76x76.png and b/res/vector-icons/apple-touch-icon-76x76.png differ diff --git a/res/vector-icons/apple-touch-icon-precomposed.png b/res/vector-icons/apple-touch-icon-precomposed.png index fe13aa99c3..85e9f8ca74 100644 Binary files a/res/vector-icons/apple-touch-icon-precomposed.png and b/res/vector-icons/apple-touch-icon-precomposed.png differ diff --git a/res/vector-icons/apple-touch-icon.png b/res/vector-icons/apple-touch-icon.png index fe13aa99c3..85e9f8ca74 100644 Binary files a/res/vector-icons/apple-touch-icon.png and b/res/vector-icons/apple-touch-icon.png differ diff --git a/res/vector-icons/favicon-16x16.png b/res/vector-icons/favicon-16x16.png index cc7e01e3da..def4ec5eee 100644 Binary files a/res/vector-icons/favicon-16x16.png and b/res/vector-icons/favicon-16x16.png differ diff --git a/res/vector-icons/favicon-32x32.png b/res/vector-icons/favicon-32x32.png index a0089a4ea9..c999923ac1 100644 Binary files a/res/vector-icons/favicon-32x32.png and b/res/vector-icons/favicon-32x32.png differ diff --git a/res/vector-icons/favicon-96x96.png b/res/vector-icons/favicon-96x96.png index b9d4326942..eb48c2c91e 100644 Binary files a/res/vector-icons/favicon-96x96.png and b/res/vector-icons/favicon-96x96.png differ diff --git a/res/vector-icons/favicon.ico b/res/vector-icons/favicon.ico index 1b40e1747a..8f8ff94eea 100644 Binary files a/res/vector-icons/favicon.ico and b/res/vector-icons/favicon.ico differ diff --git a/res/vector-icons/mstile-144x144.png b/res/vector-icons/mstile-144x144.png index 4e13047234..30c8cf18dc 100644 Binary files a/res/vector-icons/mstile-144x144.png and b/res/vector-icons/mstile-144x144.png differ diff --git a/res/vector-icons/mstile-150x150.png b/res/vector-icons/mstile-150x150.png index 7222a40cd7..5b8fca8a48 100644 Binary files a/res/vector-icons/mstile-150x150.png and b/res/vector-icons/mstile-150x150.png differ diff --git a/res/vector-icons/mstile-310x150.png b/res/vector-icons/mstile-310x150.png index af5ec1b3d9..d809f00a05 100644 Binary files a/res/vector-icons/mstile-310x150.png and b/res/vector-icons/mstile-310x150.png differ diff --git a/res/vector-icons/mstile-310x310.png b/res/vector-icons/mstile-310x310.png index f5fecd426c..ec62f8ee9d 100644 Binary files a/res/vector-icons/mstile-310x310.png and b/res/vector-icons/mstile-310x310.png differ diff --git a/res/vector-icons/mstile-70x70.png b/res/vector-icons/mstile-70x70.png index d45b51fb33..820a909e21 100644 Binary files a/res/vector-icons/mstile-70x70.png and b/res/vector-icons/mstile-70x70.png differ diff --git a/scripts/copy-res.js b/scripts/copy-res.js new file mode 100755 index 0000000000..826d9a96b3 --- /dev/null +++ b/scripts/copy-res.js @@ -0,0 +1,81 @@ +#!/usr/bin/env node + +// copies the resources into the webapp directory. +// + +// cpx includes globbed parts of the filename in the destination, but excludes +// common parents. Hence, "res/{a,b}/**": the output will be "dest/a/..." and +// "dest/b/...". +const COPY_LIST = [ + ["res/{media,vector-icons}/**", "webapp"], + ["src/skins/vector/{fonts,img}/**", "webapp"], + ["node_modules/emojione/assets/svg/*", "webapp/emojione/svg/"], + ["./config.json", "webapp", {directwatch: 1}], +]; + +const parseArgs = require('minimist'); +const Cpx = require('cpx'); +const chokidar = require('chokidar'); + +const argv = parseArgs( + process.argv.slice(2), {} +); + +var watch = argv.w; +var verbose = argv.v; + +function errCheck(err) { + if (err) { + console.error(err.message); + process.exit(1); + } +} + +function next(i, err) { + errCheck(err); + + if (i >= COPY_LIST.length) { + return; + } + + const ent = COPY_LIST[i]; + const source = ent[0]; + const dest = ent[1]; + const opts = ent[2] || {}; + + const cpx = new Cpx.Cpx(source, dest); + + if (verbose) { + cpx.on("copy", (event) => { + console.log(`Copied: ${event.srcPath} --> ${event.dstPath}`); + }); + cpx.on("remove", (event) => { + console.log(`Removed: ${event.path}`); + }); + } + + const cb = (err) => {next(i+1, err)}; + + if (watch) { + if (opts.directwatch) { + // cpx -w creates a watcher for the parent of any files specified, + // which in the case of config.json is '.', which inevitably takes + // ages to crawl. So we create our own watcher on the files + // instead. + const copy = () => {cpx.copy(errCheck)}; + chokidar.watch(source) + .on('add', copy) + .on('change', copy) + .on('ready', cb) + .on('error', errCheck); + } else { + cpx.on('watch-ready', cb); + cpx.on("watch-error", cb); + cpx.watch(); + } + } else { + cpx.copy(cb); + } +} + +next(0); diff --git a/scripts/deploy.py b/scripts/deploy.py new file mode 100755 index 0000000000..c96b46e81f --- /dev/null +++ b/scripts/deploy.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python +# +# download and unpack a riot-web tarball. +# +# Allows `bundles` to be extracted to a common directory, and a link to +# config.json to be added. + +from __future__ import print_function + +import argparse +import os +import os.path +import subprocess +import sys +import tarfile + +try: + # python3 + from urllib.request import urlretrieve +except ImportError: + # python2 + from urllib import urlretrieve + +class DeployException(Exception): + pass + +def create_relative_symlink(linkname, target): + relpath = os.path.relpath(target, os.path.dirname(linkname)) + print ("Symlink %s -> %s" % (linkname, relpath)) + os.symlink(relpath, linkname) + + +def move_bundles(source, dest): + """Move the contents of the 'bundles' directory to a common dir + + We check that we will not be overwriting anything before we proceed. + + Args: + source (str): path to 'bundles' within the extracted tarball + dest (str): target common directory + """ + + if not os.path.isdir(dest): + os.mkdir(dest) + + # build a map from source to destination, checking for non-existence as we go. + renames = {} + for f in os.listdir(source): + dst = os.path.join(dest, f) + if os.path.exists(dst): + raise DeployException( + "Not deploying. The bundle includes '%s' which we have previously deployed." + % f + ) + renames[os.path.join(source, f)] = dst + + for (src, dst) in renames.iteritems(): + print ("Move %s -> %s" % (src, dst)) + os.rename(src, dst) + +class Deployer: + def __init__(self): + self.packages_path = "." + self.bundles_path = None + self.should_clean = False + self.config_location = None + self.verify_signature = True + + def deploy(self, tarball, extract_path): + """Download a tarball if necessary, and unpack it + + Returns: + (str) the path to the unpacked deployment + """ + print("Deploying %s to %s" % (tarball, extract_path)) + + name_str = os.path.basename(tarball).replace(".tar.gz", "") + extracted_dir = os.path.join(extract_path, name_str) + if os.path.exists(extracted_dir): + raise DeployException('Cannot unpack %s: %s already exists' % ( + tarball, extracted_dir)) + + downloaded = False + if tarball.startswith("http://") or tarball.startswith("https://"): + tarball = self.download_and_verify(tarball) + print("Downloaded file: %s" % tarball) + downloaded = True + + try: + with tarfile.open(tarball) as tar: + tar.extractall(extract_path) + finally: + if self.should_clean and downloaded: + os.remove(tarball) + + print ("Extracted into: %s" % extracted_dir) + + if self.config_location: + create_relative_symlink( + target=self.config_location, + linkname=os.path.join(extracted_dir, 'config.json') + ) + + if self.bundles_path: + extracted_bundles = os.path.join(extracted_dir, 'bundles') + move_bundles(source=extracted_bundles, dest=self.bundles_path) + + # replace the (hopefully now empty) extracted_bundles dir with a + # symlink to the common dir. + os.rmdir(extracted_bundles) + create_relative_symlink( + target=self.bundles_path, + linkname=extracted_bundles, + ) + return extracted_dir + + def download_and_verify(self, url): + tarball = self.download_file(url) + + if self.verify_signature: + sigfile = self.download_file(url + ".asc") + subprocess.check_call(["gpg", "--verify", sigfile, tarball]) + + return tarball + + def download_file(self, url): + if not os.path.isdir(self.packages_path): + os.mkdir(self.packages_path) + local_filename = os.path.join(self.packages_path, + url.split('/')[-1]) + sys.stdout.write("Downloading %s -> %s..." % (url, local_filename)) + sys.stdout.flush() + urlretrieve(url, local_filename) + print ("Done") + return local_filename + +if __name__ == "__main__": + parser = argparse.ArgumentParser("Deploy a Riot build on a web server.") + parser.add_argument( + "-p", "--packages-dir", default="./packages", help=( + "The directory to download the tarball into. (Default: '%(default)s')" + ) + ) + parser.add_argument( + "-e", "--extract-path", default="./deploys", help=( + "The location to extract .tar.gz files to. (Default: '%(default)s')" + ) + ) + parser.add_argument( + "-b", "--bundles-dir", nargs='?', default="./bundles", help=( + "A directory to move the contents of the 'bundles' directory to. A \ + symlink to the bundles directory will also be written inside the \ + extracted tarball. Example: './bundles'. \ + (Default: '%(default)s')" + ) + ) + parser.add_argument( + "-c", "--clean", action="store_true", default=False, help=( + "Remove .tar.gz files after they have been downloaded and extracted. \ + (Default: %(default)s)" + ) + ) + parser.add_argument( + "--config", nargs='?', default='./config.json', help=( + "Write a symlink at config.json in the extracted tarball to this \ + location. (Default: '%(default)s')" + ) + ) + parser.add_argument( + "tarball", help=( + "filename of tarball, or URL to download." + ), + ) + + args = parser.parse_args() + + deployer = Deployer() + deployer.packages_path = args.packages_dir + deployer.bundles_path = args.bundles_dir + deployer.should_clean = args.clean + deployer.config_location = args.config + + deployer.deploy(args.tarball, args.extract_path) diff --git a/scripts/electron-package.sh b/scripts/electron-package.sh new file mode 100755 index 0000000000..87e353f746 --- /dev/null +++ b/scripts/electron-package.sh @@ -0,0 +1,130 @@ +#!/bin/bash + +set -e + +usage() { + echo "Usage: $0 -v -c [-n]" + echo + echo "version: commit-ish to check out and build" + echo "config file: a path to a json config file to" + echo "ship with the build. In addition, update_base_url:" + echo "from this file is used to set up auto-update." + echo "-n: build with no config file." + echo + echo "Values may also be passed as environment variables" +} + +conffile= +version= +skipcfg=0 +while getopts "c:v:n" opt; do + case $opt in + c) + conffile=$OPTARG + ;; + v) + version=$OPTARG + ;; + n) + skipcfg=1 + ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + usage + exit + ;; + esac +done + +if [ -z "$version" ]; then + echo "No version supplied" + usage + exit +fi + +if [ -z "$conffile" ] && [ "$skipcfg" = 0 ]; then + echo "No config file given. Use -c to supply a config file or" + echo "-n to build with no config file (and no auto update)." + exit +fi + +if [ -n "$conffile" ]; then + update_base_url=`jq -r .update_base_url $conffile` + + if [ -z "$update_base_url" ]; then + echo "No update URL supplied. Use update_base_url: null if you really" + echo "want a build with no auto-update." + usage + exit + fi + # Make sure the base URL ends in a slash if it doesn't already + update_base_url=`echo $update_base_url | sed -e 's#\([^\/]\)$#\1\/#'` +fi + +if [ ! -f package.json ]; then + echo "No package.json found. This script must be run from" + echo "the vector-web directory." + exit +fi + +echo "Building $version using Update base URL $update_base_url" + +projdir=`pwd` +builddir=`mktemp -d 2>/dev/null || mktemp -d -t 'buildtmp'` +pushd "$builddir" + +git clone "$projdir" . +git checkout "$version" + +# Figure out what version we're building +vername=`jq -r .version package.json` + +if [ -n "$conffile" ]; then + popd + cp "$conffile" "$builddir/" + pushd "$builddir" +fi + +npm install +npm run build:electron + +popd + +distdir="$builddir/electron/dist" +pubdir="$projdir/electron/pub" +rm -r "$pubdir" || true +mkdir -p "$pubdir" + +# Install packages: what the user downloads the first time, +# (DMGs for mac, exe installer for windows) +mkdir -p "$pubdir/install/macos" +cp $distdir/mac/*.dmg "$pubdir/install/macos/" + +mkdir -p "$pubdir/install/win32/ia32/" +cp $distdir/win-ia32/*.exe "$pubdir/install/win32/ia32/" + +mkdir -p "$pubdir/install/win32/x64/" +cp $distdir/win/*.exe "$pubdir/install/win32/x64/" + +# Packages for auto-update +mkdir -p "$pubdir/update/macos" +cp $distdir/mac/*.zip "$pubdir/update/macos/" +echo "$vername" > "$pubdir/update/macos/latest" + +mkdir -p "$pubdir/update/win32/ia32/" +cp $distdir/win-ia32/*.nupkg "$pubdir/update/win32/ia32/" +cp $distdir/win-ia32/RELEASES "$pubdir/update/win32/ia32/" + +mkdir -p "$pubdir/update/win32/x64/" +cp $distdir/win/*.nupkg "$pubdir/update/win32/x64/" +cp $distdir/win/RELEASES "$pubdir/update/win32/x64/" + +# Move the debs to the main project dir's dist folder +rm -r "$projdir/electron/dist" || true +mkdir -p "$projdir/electron/dist" +cp $distdir/*.deb "$projdir/electron/dist/" + +rm -rf "$builddir" + +echo "Riot Desktop is ready to go in $pubdir: this directory can be hosted on your web server." +echo "deb archives are in electron/dist/ - these should be added into your debian repository" diff --git a/scripts/jenkins.sh b/scripts/jenkins.sh index be8d8deebe..5ccc199164 100755 --- a/scripts/jenkins.sh +++ b/scripts/jenkins.sh @@ -19,12 +19,16 @@ tar -C olm -xz < olm/olm-*.tgz rm -r node_modules/olm cp -r olm/package node_modules/olm -# we may be using a dev branch of react-sdk, in which case we need to build it +# we may be using dev branches of js-sdk and react-sdk, in which case we need to build them +(cd node_modules/matrix-js-sdk && npm run build) (cd node_modules/matrix-react-sdk && npm run build) # run the mocha tests npm run test +# run eslint +npm run lintall -- -f checkstyle -o eslint.xml || true + rm dist/vector-*.tar.gz || true # rm previous artifacts without failing if it doesn't exist # node_modules deps from 'npm install' don't have a .git dir so can't diff --git a/scripts/make-icons.sh b/scripts/make-icons.sh new file mode 100755 index 0000000000..c77064abdd --- /dev/null +++ b/scripts/make-icons.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +if [ $# != 1 ] +then + echo "Usage: $0 " + exit +fi + +set -e +set -x + +tmpdir=`mktemp -d 2>/dev/null || mktemp -d -t 'icontmp'` + +for i in 1024 512 310 256 192 180 152 150 144 128 120 114 96 76 72 70 64 60 57 48 36 32 24 16 +do + #convert -background none -density 1000 -resize $i -extent $i -gravity center "$1" "$tmpdir/$i.png" + + # Above is the imagemagick command to render an svg to png. Unfortunately, its support for SVGs + # with CSS isn't very good (with rsvg and even moreso the built in renderer) so we use cairosvg. + # This can be installed with: + # pip install cairosvg==1.0.22 # Version 2 doesn't support python 2 + # pip install tinycss + # pip install cssselect # These are necessary for CSS support + # You'll also need xmlstarlet from your favourite package manager + # + # Cairosvg doesn't suport rendering at a specific size (https://github.com/Kozea/CairoSVG/issues/83#issuecomment-215720176) + # so we have to 'resize the svg' first (add width and height attributes to the svg element) to make it render at the + # size we need. + # XXX: This will break if the svg already has width and height attributes + cp "$1" "$tmpdir/tmp.svg" + xmlstarlet ed -N x="http://www.w3.org/2000/svg" --insert "/x:svg" --type attr -n width -v $i "$tmpdir/tmp.svg" > "$tmpdir/tmp2.svg" + xmlstarlet ed -N x="http://www.w3.org/2000/svg" --insert "/x:svg" --type attr -n height -v $i "$tmpdir/tmp2.svg" > "$tmpdir/tmp3.svg" + cairosvg -f png -o "$tmpdir/$i.png" "$tmpdir/tmp3.svg" + rm "$tmpdir/tmp.svg" "$tmpdir/tmp2.svg" "$tmpdir/tmp3.svg" +done + +# one more for the non-square mstile +cp "$1" "$tmpdir/tmp.svg" +xmlstarlet ed -N x="http://www.w3.org/2000/svg" --insert "/x:svg" --type attr -n width -v 310 "$tmpdir/tmp.svg" > "$tmpdir/tmp2.svg" +xmlstarlet ed -N x="http://www.w3.org/2000/svg" --insert "/x:svg" --type attr -n height -v 150 "$tmpdir/tmp2.svg" > "$tmpdir/tmp3.svg" +cairosvg -f png -o "$tmpdir/310x150.png" "$tmpdir/tmp3.svg" +rm "$tmpdir/tmp.svg" "$tmpdir/tmp2.svg" "$tmpdir/tmp3.svg" + +mkdir "$tmpdir/Riot.iconset" +cp "$tmpdir/16.png" "$tmpdir/Riot.iconset/icon_16x16.png" +cp "$tmpdir/32.png" "$tmpdir/Riot.iconset/icon_16x16@2x.png" +cp "$tmpdir/32.png" "$tmpdir/Riot.iconset/icon_32x32.png" +cp "$tmpdir/64.png" "$tmpdir/Riot.iconset/icon_32x32@2x.png" +cp "$tmpdir/128.png" "$tmpdir/Riot.iconset/icon_128x128.png" +cp "$tmpdir/256.png" "$tmpdir/Riot.iconset/icon_128x128@2x.png" +cp "$tmpdir/256.png" "$tmpdir/Riot.iconset/icon_256x256.png" +cp "$tmpdir/512.png" "$tmpdir/Riot.iconset/icon_256x256@2x.png" +cp "$tmpdir/512.png" "$tmpdir/Riot.iconset/icon_512x512.png" +cp "$tmpdir/1024.png" "$tmpdir/Riot.iconset/icon_512x512@2x.png" +iconutil -c icns -o electron/build/icon.icns "$tmpdir/Riot.iconset" + +cp "$tmpdir/36.png" "res/vector-icons/android-chrome-36x36.png" +cp "$tmpdir/48.png" "res/vector-icons/android-chrome-48x48.png" +cp "$tmpdir/72.png" "res/vector-icons/android-chrome-72x72.png" +cp "$tmpdir/96.png" "res/vector-icons/android-chrome-96x96.png" +cp "$tmpdir/144.png" "res/vector-icons/android-chrome-144x144.png" +cp "$tmpdir/192.png" "res/vector-icons/android-chrome-192x192.png" +cp "$tmpdir/180.png" "res/vector-icons/apple-touch-icon.png" +cp "$tmpdir/180.png" "res/vector-icons/apple-touch-icon-precomposed.png" +cp "$tmpdir/57.png" "res/vector-icons/apple-touch-icon-57x57.png" +cp "$tmpdir/60.png" "res/vector-icons/apple-touch-icon-60x60.png" +cp "$tmpdir/72.png" "res/vector-icons/apple-touch-icon-72x72.png" +cp "$tmpdir/76.png" "res/vector-icons/apple-touch-icon-76x76.png" +cp "$tmpdir/114.png" "res/vector-icons/apple-touch-icon-114x114.png" +cp "$tmpdir/120.png" "res/vector-icons/apple-touch-icon-120x120.png" +cp "$tmpdir/144.png" "res/vector-icons/apple-touch-icon-144x144.png" +cp "$tmpdir/152.png" "res/vector-icons/apple-touch-icon-152x152.png" +cp "$tmpdir/180.png" "res/vector-icons/apple-touch-icon-180x180.png" +cp "$tmpdir/16.png" "res/vector-icons/favicon-16x16.png" +cp "$tmpdir/32.png" "res/vector-icons/favicon-32x32.png" +cp "$tmpdir/96.png" "res/vector-icons/favicon-96x96.png" +cp "$tmpdir/70.png" "res/vector-icons/mstile-70x70.png" +cp "$tmpdir/144.png" "res/vector-icons/mstile-144x144.png" +cp "$tmpdir/150.png" "res/vector-icons/mstile-150x150.png" +cp "$tmpdir/310.png" "res/vector-icons/mstile-310x310.png" +cp "$tmpdir/310x150.png" "res/vector-icons/mstile-310x150.png" + +convert "$tmpdir/16.png" "$tmpdir/32.png" "$tmpdir/64.png" "$tmpdir/128.png" "$tmpdir/256.png" "res/vector-icons/favicon.ico" + +cp "res/vector-icons/favicon.ico" "electron/build/icon.ico" + +# https://github.com/electron-userland/electron-builder/blob/3f97b86993d4ea5172e562b182230a194de0f621/src/targets/LinuxTargetHelper.ts#L127 +for i in 24 96 16 48 64 128 256 512 +do + cp "$tmpdir/$i.png" "electron/build/icons/${i}x${i}.png" +done + +rm -r "$tmpdir" diff --git a/scripts/package.sh b/scripts/package.sh index 05227e1bb5..5c1fdd5e36 100755 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -15,6 +15,11 @@ fi npm run clean npm run build$dev + +# include the sample config in the tarball. Arguably this should be done by +# `npm run build`, but it's just too painful. +cp config.sample.json webapp/ + mkdir -p dist cp -r webapp vector-$version echo $version > vector-$version/version diff --git a/scripts/redeploy.py b/scripts/redeploy.py index 6e6e64aa15..598f6c5265 100755 --- a/scripts/redeploy.py +++ b/scripts/redeploy.py @@ -1,26 +1,30 @@ #!/usr/bin/env python +# +# auto-deploy script for https://riot.im/develop +# +# Listens for HTTP hits. When it gets one, downloads the artifact from jenkins +# and deploys it as the new version. +# +# Requires the following python packages: +# +# - requests +# - flask +# from __future__ import print_function import json, requests, tarfile, argparse, os, errno +import time from urlparse import urljoin + from flask import Flask, jsonify, request, abort + +from deploy import Deployer, DeployException + app = Flask(__name__) -arg_jenkins_url, arg_extract_path, arg_should_clean, arg_symlink, arg_config_location = ( - None, None, None, None, None -) - -def download_file(url): - local_filename = url.split('/')[-1] - r = requests.get(url, stream=True) - with open(local_filename, 'wb') as f: - for chunk in r.iter_content(chunk_size=1024): - if chunk: # filter out keep-alive new chunks - f.write(chunk) - return local_filename - -def untar_to(tarball, dest): - with tarfile.open(tarball) as tar: - tar.extractall(dest) +arg_jenkins_url = None +deployer = None +arg_extract_path = None +arg_symlink = None def create_symlink(source, linkname): try: @@ -57,6 +61,9 @@ def on_receive_jenkins_poke(): abort(400, "Missing or bad build number") return + return fetch_jenkins_build(job_name, build_num) + +def fetch_jenkins_build(job_name, build_num): artifact_url = urljoin( arg_jenkins_url, "job/%s/%s/api/json" % (job_name, build_num) ) @@ -106,28 +113,45 @@ def on_receive_jenkins_poke(): arg_jenkins_url, "job/%s/%s/artifact/%s" % (job_name, build_num, tar_gz_path) ) - print("Retrieving .tar.gz file: %s" % tar_gz_url) - filename = download_file(tar_gz_url) - print("Downloaded file: %s" % filename) - name_str = filename.replace(".tar.gz", "") - untar_to(filename, arg_extract_path) - - extracted_dir = os.path.join(arg_extract_path, name_str) - - if arg_should_clean: - os.remove(filename) + # we extract into a directory based on the build number. This avoids the + # problem of multiple builds building the same git version and thus having + # the same tarball name. That would lead to two potential problems: + # (a) sometimes jenkins serves corrupted artifacts; we would replace + # a good deploy with a bad one + # (b) we'll be overwriting the live deployment, which means people might + # see half-written files. + build_dir = os.path.join(arg_extract_path, "%s-#%s" % (job_name, build_num)) + try: + extracted_dir = deploy_tarball(tar_gz_url, build_dir) + except DeployException as e: + abort(400, e.message) create_symlink(source=extracted_dir, linkname=arg_symlink) - if arg_config_location: - create_symlink(source=arg_config_location, linkname=os.path.join(extracted_dir, 'config.json')) - return jsonify({}) +def deploy_tarball(tar_gz_url, build_dir): + """Download a tarball from jenkins and unpack it + + Returns: + (str) the path to the unpacked deployment + """ + if os.path.exists(build_dir): + raise DeployException( + "Not deploying. We have previously deployed this build." + ) + os.mkdir(build_dir) + + # we rely on the fact that flask only serves one request at a time to + # ensure that we do not overwrite a tarball from a concurrent request. + + return deployer.deploy(tar_gz_url, build_dir) + + if __name__ == "__main__": parser = argparse.ArgumentParser("Runs a Vector redeployment server.") parser.add_argument( - "-j", "--jenkins", dest="jenkins", default="http://matrix.org/jenkins/", help=( + "-j", "--jenkins", dest="jenkins", default="https://matrix.org/jenkins/", help=( "The base URL of the Jenkins web server. This will be hit to get the\ built artifacts (the .gz file) for redeploying." ) @@ -142,6 +166,13 @@ if __name__ == "__main__": "The location to extract .tar.gz files to." ) ) + parser.add_argument( + "-b", "--bundles-dir", dest="bundles_dir", help=( + "A directory to move the contents of the 'bundles' directory to. A \ + symlink to the bundles directory will also be written inside the \ + extracted tarball. Example: './bundles'." + ) + ) parser.add_argument( "-c", "--clean", dest="clean", action="store_true", default=False, help=( "Remove .tar.gz files after they have been downloaded and extracted." @@ -160,18 +191,47 @@ if __name__ == "__main__": To this location." ) ) + parser.add_argument( + "--test", dest="tarball_uri", help=( + "Don't start an HTTP listener. Instead download a build from Jenkins \ + immediately." + ), + ) + args = parser.parse_args() if args.jenkins.endswith("/"): # important for urljoin arg_jenkins_url = args.jenkins else: arg_jenkins_url = args.jenkins + "/" arg_extract_path = args.extract - arg_should_clean = args.clean arg_symlink = args.symlink - arg_config_location = args.config - print( - "Listening on port %s. Extracting to %s%s. Symlinking to %s. Jenkins URL: %s. Config location: %s" % - (args.port, arg_extract_path, - " (clean after)" if arg_should_clean else "", arg_symlink, arg_jenkins_url, arg_config_location) - ) - app.run(host="0.0.0.0", port=args.port, debug=True) + + if not os.path.isdir(arg_extract_path): + os.mkdir(arg_extract_path) + + deployer = Deployer() + deployer.bundles_path = args.bundles_dir + deployer.should_clean = args.clean + deployer.config_location = args.config + + # we don't pgp-sign jenkins artifacts; instead we rely on HTTPS access to + # the jenkins server (and the jenkins server not being compromised and/or + # github not serving it compromised source). If that's not good enough for + # you, don't use riot.im/develop. + deployer.verify_signature = False + + if args.tarball_uri is not None: + build_dir = os.path.join(arg_extract_path, "test-%i" % (time.time())) + deploy_tarball(args.tarball_uri, build_dir) + else: + print( + "Listening on port %s. Extracting to %s%s. Symlinking to %s. Jenkins URL: %s. Config location: %s" % + (args.port, + arg_extract_path, + " (clean after)" if deployer.should_clean else "", + arg_symlink, + arg_jenkins_url, + deployer.config_location, + ) + ) + app.run(host="0.0.0.0", port=args.port, debug=True) diff --git a/src/component-index.js b/src/component-index.js index 068ab0610d..4e6eefb6fb 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -27,62 +27,64 @@ limitations under the License. module.exports.components = require('matrix-react-sdk/lib/component-index').components; import structures$BottomLeftMenu from './components/structures/BottomLeftMenu'; -module.exports.components['structures.BottomLeftMenu'] = structures$BottomLeftMenu; +structures$BottomLeftMenu && (module.exports.components['structures.BottomLeftMenu'] = structures$BottomLeftMenu); import structures$CompatibilityPage from './components/structures/CompatibilityPage'; -module.exports.components['structures.CompatibilityPage'] = structures$CompatibilityPage; +structures$CompatibilityPage && (module.exports.components['structures.CompatibilityPage'] = structures$CompatibilityPage); import structures$LeftPanel from './components/structures/LeftPanel'; -module.exports.components['structures.LeftPanel'] = structures$LeftPanel; +structures$LeftPanel && (module.exports.components['structures.LeftPanel'] = structures$LeftPanel); import structures$RightPanel from './components/structures/RightPanel'; -module.exports.components['structures.RightPanel'] = structures$RightPanel; +structures$RightPanel && (module.exports.components['structures.RightPanel'] = structures$RightPanel); import structures$HomePage from './components/structures/HomePage'; -module.exports.components['structures.HomePage'] = structures$HomePage; +structures$HomePage && (module.exports.components['structures.HomePage'] = structures$HomePage); import structures$RoomDirectory from './components/structures/RoomDirectory'; -module.exports.components['structures.RoomDirectory'] = structures$RoomDirectory; +structures$RoomDirectory && (module.exports.components['structures.RoomDirectory'] = structures$RoomDirectory); import structures$RoomSubList from './components/structures/RoomSubList'; -module.exports.components['structures.RoomSubList'] = structures$RoomSubList; +structures$RoomSubList && (module.exports.components['structures.RoomSubList'] = structures$RoomSubList); import structures$SearchBox from './components/structures/SearchBox'; -module.exports.components['structures.SearchBox'] = structures$SearchBox; +structures$SearchBox && (module.exports.components['structures.SearchBox'] = structures$SearchBox); import structures$ViewSource from './components/structures/ViewSource'; -module.exports.components['structures.ViewSource'] = structures$ViewSource; +structures$ViewSource && (module.exports.components['structures.ViewSource'] = structures$ViewSource); import views$context_menus$MessageContextMenu from './components/views/context_menus/MessageContextMenu'; -module.exports.components['views.context_menus.MessageContextMenu'] = views$context_menus$MessageContextMenu; +views$context_menus$MessageContextMenu && (module.exports.components['views.context_menus.MessageContextMenu'] = views$context_menus$MessageContextMenu); import views$context_menus$NotificationStateContextMenu from './components/views/context_menus/NotificationStateContextMenu'; -module.exports.components['views.context_menus.NotificationStateContextMenu'] = views$context_menus$NotificationStateContextMenu; +views$context_menus$NotificationStateContextMenu && (module.exports.components['views.context_menus.NotificationStateContextMenu'] = views$context_menus$NotificationStateContextMenu); import views$context_menus$RoomTagContextMenu from './components/views/context_menus/RoomTagContextMenu'; -module.exports.components['views.context_menus.RoomTagContextMenu'] = views$context_menus$RoomTagContextMenu; +views$context_menus$RoomTagContextMenu && (module.exports.components['views.context_menus.RoomTagContextMenu'] = views$context_menus$RoomTagContextMenu); +import views$dialogs$BugReportDialog from './components/views/dialogs/BugReportDialog'; +views$dialogs$BugReportDialog && (module.exports.components['views.dialogs.BugReportDialog'] = views$dialogs$BugReportDialog); import views$dialogs$ChangelogDialog from './components/views/dialogs/ChangelogDialog'; -module.exports.components['views.dialogs.ChangelogDialog'] = views$dialogs$ChangelogDialog; +views$dialogs$ChangelogDialog && (module.exports.components['views.dialogs.ChangelogDialog'] = views$dialogs$ChangelogDialog); import views$directory$NetworkDropdown from './components/views/directory/NetworkDropdown'; -module.exports.components['views.directory.NetworkDropdown'] = views$directory$NetworkDropdown; +views$directory$NetworkDropdown && (module.exports.components['views.directory.NetworkDropdown'] = views$directory$NetworkDropdown); import views$elements$ImageView from './components/views/elements/ImageView'; -module.exports.components['views.elements.ImageView'] = views$elements$ImageView; +views$elements$ImageView && (module.exports.components['views.elements.ImageView'] = views$elements$ImageView); import views$elements$Spinner from './components/views/elements/Spinner'; -module.exports.components['views.elements.Spinner'] = views$elements$Spinner; +views$elements$Spinner && (module.exports.components['views.elements.Spinner'] = views$elements$Spinner); import views$globals$GuestWarningBar from './components/views/globals/GuestWarningBar'; -module.exports.components['views.globals.GuestWarningBar'] = views$globals$GuestWarningBar; +views$globals$GuestWarningBar && (module.exports.components['views.globals.GuestWarningBar'] = views$globals$GuestWarningBar); import views$globals$MatrixToolbar from './components/views/globals/MatrixToolbar'; -module.exports.components['views.globals.MatrixToolbar'] = views$globals$MatrixToolbar; +views$globals$MatrixToolbar && (module.exports.components['views.globals.MatrixToolbar'] = views$globals$MatrixToolbar); import views$globals$NewVersionBar from './components/views/globals/NewVersionBar'; -module.exports.components['views.globals.NewVersionBar'] = views$globals$NewVersionBar; +views$globals$NewVersionBar && (module.exports.components['views.globals.NewVersionBar'] = views$globals$NewVersionBar); import views$login$VectorCustomServerDialog from './components/views/login/VectorCustomServerDialog'; -module.exports.components['views.login.VectorCustomServerDialog'] = views$login$VectorCustomServerDialog; +views$login$VectorCustomServerDialog && (module.exports.components['views.login.VectorCustomServerDialog'] = views$login$VectorCustomServerDialog); import views$login$VectorLoginFooter from './components/views/login/VectorLoginFooter'; -module.exports.components['views.login.VectorLoginFooter'] = views$login$VectorLoginFooter; +views$login$VectorLoginFooter && (module.exports.components['views.login.VectorLoginFooter'] = views$login$VectorLoginFooter); import views$login$VectorLoginHeader from './components/views/login/VectorLoginHeader'; -module.exports.components['views.login.VectorLoginHeader'] = views$login$VectorLoginHeader; +views$login$VectorLoginHeader && (module.exports.components['views.login.VectorLoginHeader'] = views$login$VectorLoginHeader); import views$messages$DateSeparator from './components/views/messages/DateSeparator'; -module.exports.components['views.messages.DateSeparator'] = views$messages$DateSeparator; +views$messages$DateSeparator && (module.exports.components['views.messages.DateSeparator'] = views$messages$DateSeparator); import views$messages$MessageTimestamp from './components/views/messages/MessageTimestamp'; -module.exports.components['views.messages.MessageTimestamp'] = views$messages$MessageTimestamp; +views$messages$MessageTimestamp && (module.exports.components['views.messages.MessageTimestamp'] = views$messages$MessageTimestamp); import views$rooms$DNDRoomTile from './components/views/rooms/DNDRoomTile'; -module.exports.components['views.rooms.DNDRoomTile'] = views$rooms$DNDRoomTile; +views$rooms$DNDRoomTile && (module.exports.components['views.rooms.DNDRoomTile'] = views$rooms$DNDRoomTile); import views$rooms$RoomDropTarget from './components/views/rooms/RoomDropTarget'; -module.exports.components['views.rooms.RoomDropTarget'] = views$rooms$RoomDropTarget; +views$rooms$RoomDropTarget && (module.exports.components['views.rooms.RoomDropTarget'] = views$rooms$RoomDropTarget); import views$rooms$RoomTooltip from './components/views/rooms/RoomTooltip'; -module.exports.components['views.rooms.RoomTooltip'] = views$rooms$RoomTooltip; +views$rooms$RoomTooltip && (module.exports.components['views.rooms.RoomTooltip'] = views$rooms$RoomTooltip); import views$rooms$SearchBar from './components/views/rooms/SearchBar'; -module.exports.components['views.rooms.SearchBar'] = views$rooms$SearchBar; +views$rooms$SearchBar && (module.exports.components['views.rooms.SearchBar'] = views$rooms$SearchBar); import views$settings$IntegrationsManager from './components/views/settings/IntegrationsManager'; -module.exports.components['views.settings.IntegrationsManager'] = views$settings$IntegrationsManager; +views$settings$IntegrationsManager && (module.exports.components['views.settings.IntegrationsManager'] = views$settings$IntegrationsManager); import views$settings$Notifications from './components/views/settings/Notifications'; -module.exports.components['views.settings.Notifications'] = views$settings$Notifications; +views$settings$Notifications && (module.exports.components['views.settings.Notifications'] = views$settings$Notifications); diff --git a/src/components/structures/CompatibilityPage.js b/src/components/structures/CompatibilityPage.js index dd139e6563..bae33803aa 100644 --- a/src/components/structures/CompatibilityPage.js +++ b/src/components/structures/CompatibilityPage.js @@ -45,10 +45,8 @@ module.exports = React.createClass({ available or experimental in your current browser.

- Please install Chrome or - Firefox for the best experience. - Safari and - Opera work too. + Please install Chrome or Firefox for + the best experience. Safari and Opera work too.

With your current browser, the look and feel of the application may diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index e369a26ec2..5fb0324c40 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -31,6 +31,8 @@ var linkifyMatrix = require('matrix-react-sdk/lib/linkify-matrix'); var sanitizeHtml = require('sanitize-html'); var q = require('q'); +import {instanceForInstanceId, protocolNameForInstanceId} from '../../utils/DirectoryUtils'; + linkifyMatrix(linkify); module.exports = React.createClass({ @@ -42,9 +44,7 @@ module.exports = React.createClass({ getDefaultProps: function() { return { - config: { - networks: [], - }, + config: {}, } }, @@ -52,36 +52,26 @@ module.exports = React.createClass({ return { publicRooms: [], loading: true, - network: null, + protocolsLoading: true, + instanceId: null, + includeAll: false, roomServer: null, filterString: null, } }, componentWillMount: function() { - // precompile Regexps - this.portalRoomPatterns = {}; - this.nativePatterns = {}; - if (this.props.config.networks) { - for (const network of Object.keys(this.props.config.networks)) { - const network_info = this.props.config.networks[network]; - if (network_info.portalRoomPattern) { - this.portalRoomPatterns[network] = new RegExp(network_info.portalRoomPattern); - } - if (network_info.nativePattern) { - this.nativePatterns[network] = new RegExp(network_info.nativePattern); - } - } - } - this.nextBatch = null; this.filterTimeout = null; this.scrollPanel = null; this.protocols = null; + this.setState({protocolsLoading: true}); MatrixClientPeg.get().getThirdpartyProtocols().done((response) => { this.protocols = response; + this.setState({protocolsLoading: false}); }, (err) => { + this.setState({protocolsLoading: false}); if (MatrixClientPeg.get().isGuest()) { // Guests currently aren't allowed to use this API, so // ignore this as otherwise this error is literally the @@ -131,6 +121,11 @@ module.exports = React.createClass({ if (my_server != MatrixClientPeg.getHomeServerName()) { opts.server = my_server; } + if (this.state.instanceId) { + opts.third_party_instance_id = this.state.instanceId; + } else if (this.state.includeAll) { + opts.include_all_networks = true; + } if (this.nextBatch) opts.since = this.nextBatch; if (my_filter_string) opts.filter = { generic_search_term: my_filter_string } ; return MatrixClientPeg.get().publicRooms(opts).then((data) => { @@ -231,7 +226,7 @@ module.exports = React.createClass({ } }, - onOptionChange: function(server, network) { + onOptionChange: function(server, instanceId, includeAll) { // clear next batch so we don't try to load more rooms this.nextBatch = null; this.setState({ @@ -240,7 +235,8 @@ module.exports = React.createClass({ // to clear the list anyway. publicRooms: [], roomServer: server, - network: network, + instanceId: instanceId, + includeAll: includeAll, }, this.refreshRoomList); // We also refresh the room list each time even though this // filtering is client-side. It hopefully won't be client side @@ -271,7 +267,7 @@ module.exports = React.createClass({ this.filterTimeout = setTimeout(() => { this.filterTimeout = null; this.refreshRoomList(); - }, 300); + }, 700); }, onFilterClear: function() { @@ -286,14 +282,19 @@ module.exports = React.createClass({ }, onJoinClick: function(alias) { - // If we're on the 'Matrix' network (or all networks), - // just show that rooms alias - if (this.state.network == null || this.state.network == '_matrix') { + // If we don't have a particular instance id selected, just show that rooms alias + if (!this.state.instanceId) { + // If the user specified an alias without a domain, add on whichever server is selected + // in the dropdown + if (alias.indexOf(':') == -1) { + alias = alias + ':' + this.state.roomServer; + } this.showRoomAlias(alias); } else { - // This is a 3rd party protocol. Let's see if we - // can join it - const fields = this._getFieldsForThirdPartyLocation(alias, this.state.network); + // This is a 3rd party protocol. Let's see if we can join it + const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId); + const instance = instanceForInstanceId(this.protocols, this.state.instanceId); + const fields = protocolName ? this._getFieldsForThirdPartyLocation(alias, this.protocols[protocolName], instance) : null; if (!fields) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { @@ -302,8 +303,7 @@ module.exports = React.createClass({ }); return; } - const protocol = this._protocolForThirdPartyNetwork(this.state.network); - MatrixClientPeg.get().getThirdpartyLocation(protocol, fields).done((resp) => { + MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).done((resp) => { if (resp.length > 0 && resp[0].alias) { this.showRoomAlias(resp[0].alias); } else { @@ -372,13 +372,7 @@ module.exports = React.createClass({ if (!this.state.publicRooms) return []; - var rooms = this.state.publicRooms.filter((a) => { - if (this.state.network) { - if (!this._isRoomInNetwork(a, this.state.roomServer, this.state.network)) return false; - } - - return true; - }); + var rooms = this.state.publicRooms; var rows = []; var self = this; var guestRead, guestJoin, perms; @@ -440,119 +434,46 @@ module.exports = React.createClass({ this.scrollPanel = element; }, - /** - * Terrible temporary function that guess what network a public room - * entry is in, until synapse is able to tell us - */ - _isRoomInNetwork: function(room, server, network) { - // We carve rooms into two categories here. 'portal' rooms are - // rooms created by a user joining a bridge 'portal' alias to - // participate in that room or a foreign network. A room is a - // portal room if it has exactly one alias and that alias matches - // a pattern defined in the config. Its network is the key - // of the pattern that it matches. - // All other rooms are considered 'native matrix' rooms, and - // go into the special '_matrix' network. - - let roomNetwork = '_matrix'; - if (room.aliases && room.aliases.length == 1) { - if (this.props.config.serverConfig && this.props.config.serverConfig[server] && this.props.config.serverConfig[server].networks) { - for (const n of this.props.config.serverConfig[server].networks) { - const pat = this.portalRoomPatterns[n]; - if (pat && pat.test(room.aliases[0])) { - roomNetwork = n; - } - } - } - } - return roomNetwork == network; - }, - - _stringLooksLikeId: function(s, network) { + _stringLooksLikeId: function(s, field_type) { let pat = /^#[^\s]+:[^\s]/; - if ( - network && network != '_matrix' && - this.nativePatterns[network] - ) { - pat = this.nativePatterns[network]; + if (field_type && field_type.regexp) { + pat = new RegExp(field_type.regexp); } return pat.test(s); }, - _protocolForThirdPartyNetwork: function(network) { - if ( - this.props.config.networks && - this.props.config.networks[network] && - this.props.config.networks[network].protocol - ) { - return this.props.config.networks[network].protocol; - } - }, - - _getFieldsForThirdPartyLocation: function(user_input, network) { - if (!this.props.config.networks || !this.props.config.networks[network]) return null; - - const network_info = this.props.config.networks[network]; - if (!network_info.protocol) return null; - - if (!this.protocols) return null; - - let matched_instance; - // Try to find which instance in the 'protocols' response - // matches this network. We look for a matching protocol - // and the existence of a 'domain' field and if present, - // its value. - if ( - this.protocols[network_info.protocol] && - this.protocols[network_info.protocol].instances && - this.protocols[network_info.protocol].instances.length == 1 - ) { - const the_instance = this.protocols[network_info.protocol].instances[0]; - // If there's only one instance in this protocol, use it - // as long as it has no domain (which we assume to mean it's - // there is only one possible instance). - if ( - ( - the_instance.fields.domain === undefined && - network_info.domain === undefined - ) || - ( - the_instance.fields.domain !== undefined && - the_instance.fields.domain == network_info.domain - ) - ) { - matched_instance = the_instance; - } - } else if (network_info.domain) { - // otherwise, we look for one with a matching domain. - for (const this_instance of this.protocols[network_info.protocol].instances) { - if (this_instance.fields.domain == network_info.domain) { - matched_instance = this_instance; - } - } - } - - if (matched_instance === undefined) return null; - - // now make an object with the fields specified by that protocol. We + _getFieldsForThirdPartyLocation: function(userInput, protocol, instance) { + // make an object with the fields specified by that protocol. We // require that the values of all but the last field come from the // instance. The last is the user input. - const required_fields = this.protocols[network_info.protocol].location_fields; + const requiredFields = protocol.location_fields; + if (!requiredFields) return null; const fields = {}; - for (let i = 0; i < required_fields.length - 1; ++i) { - const this_field = required_fields[i]; - if (matched_instance.fields[this_field] === undefined) return null; - fields[this_field] = matched_instance.fields[this_field]; + for (let i = 0; i < requiredFields.length - 1; ++i) { + const thisField = requiredFields[i]; + if (instance.fields[thisField] === undefined) return null; + fields[thisField] = instance.fields[thisField]; } - fields[required_fields[required_fields.length - 1]] = user_input; + fields[requiredFields[requiredFields.length - 1]] = userInput; return fields; }, render: function() { + const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader'); + const Loader = sdk.getComponent("elements.Spinner"); + + if (this.state.protocolsLoading) { + return ( +

+ + +
+ ); + } + let content; if (this.state.loading) { - const Loader = sdk.getComponent("elements.Spinner"); content =
; @@ -583,26 +504,35 @@ module.exports = React.createClass({ ; } - let placeholder = 'Search for a room'; - if (this.state.network === null || this.state.network === '_matrix') { - placeholder = '#example:' + this.state.roomServer; - } else if ( - this.props.config.networks && - this.props.config.networks[this.state.network] && - this.props.config.networks[this.state.network].example && - this._getFieldsForThirdPartyLocation(this.state.filterString, this.state.network) + const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId); + let instance_expected_field_type; + if ( + protocolName && + this.protocols && + this.protocols[protocolName] && + this.protocols[protocolName].location_fields.length > 0 && + this.protocols[protocolName].field_types ) { - placeholder = this.props.config.networks[this.state.network].example; + const last_field = this.protocols[protocolName].location_fields.slice(-1)[0]; + instance_expected_field_type = this.protocols[protocolName].field_types[last_field]; } - let showJoinButton = this._stringLooksLikeId(this.state.filterString, this.state.network); - if (this.state.network && this.state.network != '_matrix') { - if (this._getFieldsForThirdPartyLocation(this.state.filterString, this.state.network) === null) { + + let placeholder = 'Search for a room'; + if (!this.state.instanceId) { + placeholder = '#example:' + this.state.roomServer; + } else if (instance_expected_field_type) { + placeholder = instance_expected_field_type.placeholder; + } + + let showJoinButton = this._stringLooksLikeId(this.state.filterString, instance_expected_field_type); + if (protocolName) { + const instance = instanceForInstanceId(this.protocols, this.state.instanceId); + if (this._getFieldsForThirdPartyLocation(this.state.filterString, this.protocols[protocolName], instance) === null) { showJoinButton = false; } } - const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader'); const NetworkDropdown = sdk.getComponent('directory.NetworkDropdown'); const DirectorySearchBox = sdk.getComponent('elements.DirectorySearchBox'); return ( @@ -615,7 +545,7 @@ module.exports = React.createClass({ onChange={this.onFilterChange} onClear={this.onFilterClear} onJoinClick={this.onJoinClick} placeholder={placeholder} showJoinButton={showJoinButton} /> - + {content} diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js index 1dbc5319c1..a0e198cecc 100644 --- a/src/components/structures/ViewSource.js +++ b/src/components/structures/ViewSource.js @@ -22,7 +22,8 @@ module.exports = React.createClass({ displayName: 'ViewSource', propTypes: { - onFinished: React.PropTypes.func.isRequired + content: React.PropTypes.object.isRequired, + onFinished: React.PropTypes.func.isRequired, }, componentDidMount: function() { @@ -45,10 +46,9 @@ module.exports = React.createClass({ return (
-                    {JSON.stringify(this.props.mxEvent.event, null, 2)}
+                    {JSON.stringify(this.props.content, null, 2)}
                 
); } }); - diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 7786b9bd5d..e3640f775c 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -47,7 +47,16 @@ module.exports = React.createClass({ onViewSourceClick: function() { var ViewSource = sdk.getComponent('structures.ViewSource'); Modal.createDialog(ViewSource, { - mxEvent: this.props.mxEvent + content: this.props.mxEvent.event, + }, 'mx_Dialog_viewsource'); + if (this.props.onFinished) this.props.onFinished(); + }, + + onViewClearSourceClick: function() { + const ViewSource = sdk.getComponent('structures.ViewSource'); + Modal.createDialog(ViewSource, { + // FIXME: _clearEvent is private + content: this.props.mxEvent._clearEvent, }, 'mx_Dialog_viewsource'); if (this.props.onFinished) this.props.onFinished(); }, @@ -97,6 +106,7 @@ module.exports = React.createClass({ var eventStatus = this.props.mxEvent.status; var resendButton; var viewSourceButton; + var viewClearSourceButton; var redactButton; var cancelButton; var permalinkButton; @@ -133,6 +143,14 @@ module.exports = React.createClass({ ); + if (this.props.mxEvent.getType() !== this.props.mxEvent.getWireType()) { + viewClearSourceButton = ( +
+ View Decrypted Source +
+ ); + } + if (this.props.eventTileOps) { if (this.props.eventTileOps.isWidgetHidden()) { unhidePreviewButton = ( @@ -174,6 +192,7 @@ module.exports = React.createClass({ {redactButton} {cancelButton} {viewSourceButton} + {viewClearSourceButton} {unhidePreviewButton} {permalinkButton} {UserSettingsStore.isFeatureEnabled('rich_text_editor') ? quoteButton : null} diff --git a/src/components/views/context_menus/NotificationStateContextMenu.js b/src/components/views/context_menus/NotificationStateContextMenu.js index 243275db27..d4b40d1732 100644 --- a/src/components/views/context_menus/NotificationStateContextMenu.js +++ b/src/components/views/context_menus/NotificationStateContextMenu.js @@ -120,22 +120,22 @@ module.exports = React.createClass({
- + All messages (loud)
- + All messages
- + Mentions only
- + Mute
diff --git a/src/components/views/dialogs/BugReportDialog.js b/src/components/views/dialogs/BugReportDialog.js new file mode 100644 index 0000000000..dcc0850ef7 --- /dev/null +++ b/src/components/views/dialogs/BugReportDialog.js @@ -0,0 +1,127 @@ +/* +Copyright 2017 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import sdk from 'matrix-react-sdk'; +import rageshake from '../../../vector/rageshake'; + +export default class BugReportDialog extends React.Component { + constructor(props, context) { + super(props, context); + this.state = { + sendLogs: true, + busy: false, + err: null, + text: "", + }; + this._onSubmit = this._onSubmit.bind(this); + this._onCancel = this._onCancel.bind(this); + this._onTextChange = this._onTextChange.bind(this); + this._onSendLogsChange = this._onSendLogsChange.bind(this); + } + + _onCancel(ev) { + this.props.onFinished(false); + } + + _onSubmit(ev) { + const sendLogs = this.state.sendLogs; + const userText = this.state.text; + if (!sendLogs && userText.trim().length === 0) { + this.setState({ + err: "Please describe the bug and/or send logs.", + }); + return; + } + this.setState({ busy: true, err: null }); + rageshake.sendBugReport(userText, sendLogs).then(() => { + this.setState({ busy: false }); + this.props.onFinished(false); + }, (err) => { + this.setState({ busy: false, err: `Failed: ${err.message}` }); + }); + } + + _onTextChange(ev) { + this.setState({ text: ev.target.value }); + } + + _onSendLogsChange(ev) { + this.setState({ sendLogs: ev.target.checked }); + } + + render() { + const Loader = sdk.getComponent("elements.Spinner"); + + let error = null; + if (this.state.err) { + error =
+ {this.state.err} +
; + } + + const okLabel = this.state.busy ? : 'Send'; + + let cancelButton = null; + if (!this.state.busy) { + cancelButton = ; + } + + return ( +
+
+ Report a bug +
+
+

Please describe the bug. What did you do? + What did you expect to happen? + What actually happened?

+