Merge branch 'develop' of github.com:vector-im/riot-web into set_default_federate_by_settings

This commit is contained in:
Michael Telatynski 2017-08-02 12:24:15 +01:00
commit bf09c14e44
No known key found for this signature in database
GPG key ID: 0435A1D4BBD34D64
68 changed files with 1426 additions and 787 deletions

View file

@ -1,4 +1,4 @@
{
"presets": ["react", "es2015", "es2016"],
"plugins": ["transform-class-properties", "transform-object-rest-spread", "transform-async-to-generator", "transform-runtime", "add-module-exports"]
"plugins": ["transform-class-properties", "transform-object-rest-spread", "transform-async-to-bluebird", "transform-runtime", "add-module-exports"]
}

View file

@ -1,3 +1,10 @@
# we need trusty for the chrome addon
dist: trusty
# we don't need sudo, so can run in a container, which makes startup much
# quicker.
sudo: false
language: node_js
node_js:
# make sure we work with a range of node versions.
@ -5,8 +12,9 @@ node_js:
# - 4.x is still in LTS (until April 2018), but some of our deps (notably
# extract-zip) don't work with it
# - 5.x has been EOLed for nearly a year.
# - 6.x is the current 'LTS' version
# - 7.x is the current 'current' version (until October 2017)
# - 6.x is the active 'LTS' version
# - 7.x is no longer supported
# - 8.x is the current 'current' version (until October 2017)
#
# see: https://github.com/nodejs/LTS/
#
@ -16,6 +24,8 @@ node_js:
- 6.3
- 6
- 7
addons:
chrome: stable
install:
# clone the deps with depth 1: we know we will only ever need that one
# commit.

View file

@ -5,7 +5,7 @@ Changes in [0.11.4](https://github.com/vector-im/riot-web/releases/tag/v0.11.4)
* Update matrix-js-sdk and react-sdk to fix a regression where the
background indexedb worker was disabled, failures to open indexeddb
causing the app to fail to start, a race when starting that could break
switching to rooms, and the inability to to invite user with mixed case
switching to rooms, and the inability to invite users with mixed case
usernames.
Changes in [0.11.3](https://github.com/vector-im/riot-web/releases/tag/v0.11.3) (2017-06-20)

View file

@ -81,7 +81,7 @@ to build.
npm run build
```
However, we recommend setting up a proper development environment (see "Setting
up a development environment" below) if you want to run your own copy of the
up a dev environment" below) if you want to run your own copy of the
`develop` branch, as it makes it much easier to keep these dependencies
up-to-date. Or just use https://riot.im/develop - the continuous integration
release of the develop branch.
@ -253,7 +253,6 @@ Finally, build and start Riot itself:
1. `rm -r node_modules/matrix-react-sdk; ln -s ../../matrix-react-sdk node_modules/`
1. `npm start`
1. Wait a few seconds for the initial build to finish; you should see something like:
```
Hash: b0af76309dd56d7275c8
Version: webpack 1.12.14
@ -282,19 +281,34 @@ If any of these steps error with, `file table overflow`, you are probably on a m
which has a very low limit on max open files. Run `ulimit -Sn 1024` and try again.
You'll need to do this in each new terminal you open before building Riot.
How to add a new translation?
=============================
Running the tests
-----------------
There are a number of application-level tests in the `tests` directory; these
are designed to run in a browser instance under the control of
[karma](https://karma-runner.github.io). To run them:
* Make sure you have Chrome installed (a recent version, like 59)
* Make sure you have `matrix-js-sdk` and `matrix-react-sdk` installed and
built, as above
* `npm run test`
The above will run the tests under Chrome in a `headless` mode.
You can also tell karma to run the tests in a loop (every time the source
changes), in an instance of Chrome on your desktop, with `npm run
test-multi`. This also gives you the option of running the tests in 'debug'
mode, which is useful for stepping through the tests in the developer tools.
Translations
============
To add a new translation, head to the [translating doc](docs/translating.md).
For a developer guide, see the [translating dev doc](docs/translating-dev.md).
[<img src="https://translate.riot.im/widgets/riot-web/-/multi-auto.svg" alt="translationsstatus" width="340">](https://translate.riot.im/engage/riot-web/?utm_source=widget)
Head to the [translating doc](docs/translating.md)
Adding Strings to the translations (Developer Guide)
====================================================
Head to the [translating dev doc](docs/translating-dev.md)
Triaging issues
===============

View file

@ -29,6 +29,7 @@ const AutoLaunch = require('auto-launch');
const tray = require('./tray');
const vectorMenu = require('./vectormenu');
const webContentsHandler = require('./webcontents-handler');
const updater = require('./updater');
const windowStateKeeper = require('electron-window-state');
@ -46,69 +47,9 @@ try {
// Continue with the defaults (ie. an empty config)
}
const UPDATE_POLL_INTERVAL_MS = 60 * 60 * 1000;
const INITIAL_UPDATE_DELAY_MS = 30 * 1000;
let mainWindow = null;
let appQuitting = false;
global.appQuitting = false;
function installUpdate() {
// for some reason, quitAndInstall does not fire the
// before-quit event, so we need to set the flag here.
appQuitting = true;
electron.autoUpdater.quitAndInstall();
}
function pollForUpdates() {
try {
electron.autoUpdater.checkForUpdates();
} catch (e) {
console.log('Couldn\'t check for update', e);
}
}
function startAutoUpdate(updateBaseUrl) {
if (updateBaseUrl.slice(-1) !== '/') {
updateBaseUrl = updateBaseUrl + '/';
}
try {
// For reasons best known to Squirrel, the way it checks for updates
// is completely different between macOS and windows. On macOS, it
// hits a URL that either gives it a 200 with some json or
// 204 No Content. On windows it takes a base path and looks for
// files under that path.
if (process.platform === 'darwin') {
// 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(
`${updateBaseUrl}macos/?localVersion=${encodeURIComponent(electron.app.getVersion())}`);
} else if (process.platform === 'win32') {
electron.autoUpdater.setFeedURL(`${updateBaseUrl}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
// implemented it on Linux, or if it would be different again.
console.log('Auto update not supported on this platform');
}
// We check for updates ourselves rather than using 'updater' because we need to
// do it in the main process (and we don't really need to check every 10 minutes:
// every hour should be just fine for a desktop app)
// However, we still let the main window listen for the update events.
// We also wait a short time before checking for updates the first time because
// of squirrel on windows and it taking a small amount of time to release a
// lock file.
setTimeout(pollForUpdates, INITIAL_UPDATE_DELAY_MS);
setInterval(pollForUpdates, UPDATE_POLL_INTERVAL_MS);
} catch (err) {
// will fail if running in debug mode
console.log('Couldn\'t enable update checking', err);
}
}
// handle uncaught errors otherwise it displays
// stack traces in popup dialogs, which is terrible (which
@ -120,8 +61,6 @@ process.on('uncaughtException', function(error) {
console.log('Unhandled exception', error);
});
electron.ipcMain.on('install_update', installUpdate);
let focusHandlerAttached = false;
electron.ipcMain.on('setBadgeCount', function(ev, count) {
electron.app.setBadgeCount(count);
@ -233,7 +172,7 @@ electron.app.on('ready', () => {
if (vectorConfig.update_base_url) {
console.log(`Starting auto update with base URL: ${vectorConfig.update_base_url}`);
startAutoUpdate(vectorConfig.update_base_url);
updater.start(vectorConfig.update_base_url);
} else {
console.log('No update_base_url is defined: auto update is disabled');
}
@ -246,7 +185,7 @@ electron.app.on('ready', () => {
defaultHeight: 768,
});
mainWindow = new electron.BrowserWindow({
mainWindow = global.mainWindow = new electron.BrowserWindow({
icon: iconPath,
show: false,
autoHideMenuBar: true,
@ -264,7 +203,7 @@ electron.app.on('ready', () => {
mainWindow.hide();
// Create trayIcon icon
tray.create(mainWindow, {
tray.create({
icon_path: iconPath,
brand: vectorConfig.brand || 'Riot',
});
@ -276,10 +215,10 @@ electron.app.on('ready', () => {
}
mainWindow.on('closed', () => {
mainWindow = null;
mainWindow = global.mainWindow = null;
});
mainWindow.on('close', (e) => {
if (!appQuitting && (tray.hasTray() || process.platform === 'darwin')) {
if (!global.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)
@ -302,7 +241,10 @@ electron.app.on('activate', () => {
});
electron.app.on('before-quit', () => {
appQuitting = true;
global.appQuitting = true;
if (mainWindow) {
mainWindow.webContents.send('before-quit');
}
});
// Set the App User Model ID to match what the squirrel

View file

@ -26,17 +26,17 @@ exports.hasTray = function hasTray() {
return (trayIcon !== null);
};
exports.create = function(win, config) {
exports.create = function(config) {
// no trays on darwin
if (process.platform === 'darwin' || trayIcon) return;
const toggleWin = function() {
if (win.isVisible() && !win.isMinimized()) {
win.hide();
if (global.mainWindow.isVisible() && !global.mainWindow.isMinimized()) {
global.mainWindow.hide();
} else {
if (win.isMinimized()) win.restore();
if (!win.isVisible()) win.show();
win.focus();
if (global.mainWindow.isMinimized()) global.mainWindow.restore();
if (!global.mainWindow.isVisible()) global.mainWindow.show();
global.mainWindow.focus();
}
};
@ -54,41 +54,46 @@ exports.create = function(win, config) {
},
]);
trayIcon = new Tray(config.icon_path);
const defaultIcon = nativeImage.createFromPath(config.icon_path);
trayIcon = new Tray(defaultIcon);
trayIcon.setToolTip(config.brand);
trayIcon.setContextMenu(contextMenu);
trayIcon.on('click', toggleWin);
let lastFavicon = null;
win.webContents.on('page-favicon-updated', async function(ev, favicons) {
let newFavicon = config.icon_path;
if (favicons && favicons.length > 0 && favicons[0].startsWith('data:')) {
newFavicon = favicons[0];
global.mainWindow.webContents.on('page-favicon-updated', async function(ev, favicons) {
if (!favicons || favicons.length <= 0 || !favicons[0].startsWith('data:')) {
if (lastFavicon !== null) {
win.setIcon(defaultIcon);
trayIcon.setImage(defaultIcon);
lastFavicon = null;
}
return;
}
// No need to change, shortcut
if (newFavicon === lastFavicon) return;
lastFavicon = newFavicon;
if (favicons[0] === lastFavicon) return;
lastFavicon = favicons[0];
// if its not default we have to construct into nativeImage
if (newFavicon !== config.icon_path) {
newFavicon = nativeImage.createFromDataURL(favicons[0]);
let newFavicon = nativeImage.createFromDataURL(favicons[0]);
if (process.platform === 'win32') {
try {
const icoPath = path.join(app.getPath('temp'), 'win32_riot_icon.ico')
const icoBuf = await pngToIco(newFavicon.toPNG());
fs.writeFileSync(icoPath, icoBuf);
newFavicon = icoPath;
} catch (e) {console.error(e);}
// Windows likes ico's too much.
if (process.platform === 'win32') {
try {
const icoPath = path.join(app.getPath('temp'), 'win32_riot_icon.ico');
fs.writeFileSync(icoPath, await pngToIco(newFavicon.toPNG()));
newFavicon = nativeImage.createFromPath(icoPath);
} catch (e) {
console.error("Failed to make win32 ico", e);
}
}
trayIcon.setImage(newFavicon);
win.setIcon(newFavicon);
global.mainWindow.setIcon(newFavicon);
});
win.webContents.on('page-title-updated', function(ev, title) {
global.mainWindow.webContents.on('page-title-updated', function(ev, title) {
trayIcon.setToolTip(title);
});
};

View file

@ -0,0 +1,84 @@
const { app, autoUpdater, ipcMain } = require('electron');
const UPDATE_POLL_INTERVAL_MS = 60 * 60 * 1000;
const INITIAL_UPDATE_DELAY_MS = 30 * 1000;
function installUpdate() {
// for some reason, quitAndInstall does not fire the
// before-quit event, so we need to set the flag here.
global.appQuitting = true;
autoUpdater.quitAndInstall();
}
function pollForUpdates() {
try {
autoUpdater.checkForUpdates();
} catch (e) {
console.log('Couldn\'t check for update', e);
}
}
module.exports = {};
module.exports.start = function startAutoUpdate(updateBaseUrl) {
if (updateBaseUrl.slice(-1) !== '/') {
updateBaseUrl = updateBaseUrl + '/';
}
try {
let url;
// For reasons best known to Squirrel, the way it checks for updates
// is completely different between macOS and windows. On macOS, it
// hits a URL that either gives it a 200 with some json or
// 204 No Content. On windows it takes a base path and looks for
// files under that path.
if (process.platform === 'darwin') {
// 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.
url = `${updateBaseUrl}macos/?localVersion=${encodeURIComponent(app.getVersion())}`;
} else if (process.platform === 'win32') {
url = `${updateBaseUrl}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
// implemented it on Linux, or if it would be different again.
console.log('Auto update not supported on this platform');
}
if (url) {
autoUpdater.setFeedURL(url);
// We check for updates ourselves rather than using 'updater' because we need to
// do it in the main process (and we don't really need to check every 10 minutes:
// every hour should be just fine for a desktop app)
// However, we still let the main window listen for the update events.
// We also wait a short time before checking for updates the first time because
// of squirrel on windows and it taking a small amount of time to release a
// lock file.
setTimeout(pollForUpdates, INITIAL_UPDATE_DELAY_MS);
setInterval(pollForUpdates, UPDATE_POLL_INTERVAL_MS);
}
} catch (err) {
// will fail if running in debug mode
console.log('Couldn\'t enable update checking', err);
}
}
ipcMain.on('install_update', installUpdate);
ipcMain.on('check_updates', pollForUpdates);
function ipcChannelSendUpdateStatus(status) {
if (global.mainWindow) {
global.mainWindow.webContents.send('check_updates', status);
}
}
autoUpdater.on('update-available', function() {
ipcChannelSendUpdateStatus(true);
}).on('update-not-available', function() {
ipcChannelSendUpdateStatus(false);
}).on('error', function(error) {
ipcChannelSendUpdateStatus(error.message);
});

View file

@ -113,8 +113,23 @@ module.exports = function (config) {
browsers: [
'Chrome',
//'PhantomJS',
//'ChromeHeadless'
],
customLaunchers: {
'ChromeHeadless': {
base: 'Chrome',
flags: [
// '--no-sandbox',
// See https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md
'--headless',
'--disable-gpu',
// Without a remote debugging port, Google Chrome exits immediately.
'--remote-debugging-port=9222',
],
}
},
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
// singleRun: false,

View file

@ -48,12 +48,13 @@
"lintall": "eslint src/ test/",
"clean": "rimraf lib webapp electron_app/dist",
"prepublish": "npm run build:compile",
"test": "karma start --single-run=true --autoWatch=false --browsers PhantomJS --colors=false",
"test": "karma start --single-run=true --autoWatch=false --browsers ChromeHeadless",
"test-multi": "karma start"
},
"dependencies": {
"babel-polyfill": "^6.5.0",
"babel-runtime": "^6.11.6",
"bluebird": "^3.5.0",
"browser-request": "^0.3.3",
"classnames": "^2.1.2",
"draft-js": "^0.8.1",
@ -69,11 +70,10 @@
"matrix-react-sdk": "0.9.7",
"modernizr": "^3.1.0",
"pako": "^1.0.5",
"q": "^1.4.1",
"react": "^15.4.0",
"react": "^15.6.0",
"react-dnd": "^2.1.4",
"react-dnd-html5-backend": "^2.1.2",
"react-dom": "^15.4.0",
"react-dom": "^15.6.0",
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
"sanitize-html": "^1.11.1",
"text-encoding-utf-8": "^1.0.1",
@ -88,7 +88,7 @@
"babel-eslint": "^6.1.0",
"babel-loader": "^6.2.5",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-transform-async-to-generator": "^6.16.0",
"babel-plugin-transform-async-to-bluebird": "^1.1.1",
"babel-plugin-transform-class-properties": "^6.16.0",
"babel-plugin-transform-object-rest-spread": "^6.16.0",
"babel-plugin-transform-runtime": "^6.15.0",
@ -119,13 +119,13 @@
"karma-cli": "^0.1.2",
"karma-junit-reporter": "^0.4.1",
"karma-mocha": "^0.2.2",
"karma-phantomjs-launcher": "^1.0.0",
"karma-webpack": "^1.7.0",
"matrix-mock-request": "^1.2.0",
"matrix-react-test-utils": "^0.2.0",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"mocha": "^2.4.5",
"parallelshell": "^1.2.0",
"phantomjs-prebuilt": "^2.1.7",
"postcss-extend": "^1.0.5",
"postcss-import": "^9.0.0",
"postcss-loader": "^1.2.2",
@ -135,7 +135,7 @@
"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",
"react-addons-test-utils": "^15.6.0",
"rimraf": "^2.4.3",
"source-map-loader": "^0.1.5",
"webpack": "^1.12.14",

View file

@ -63,7 +63,8 @@ class Deployer:
self.packages_path = "."
self.bundles_path = None
self.should_clean = False
self.config_location = None
# filename -> symlink path e.g 'config.localhost.json' => '../localhost/config.json'
self.config_locations = {}
self.verify_signature = True
def deploy(self, tarball, extract_path):
@ -95,11 +96,12 @@ class Deployer:
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.config_locations:
for config_filename, config_loc in self.config_locations.iteritems():
create_relative_symlink(
target=config_loc,
linkname=os.path.join(extracted_dir, config_filename)
)
if self.bundles_path:
extracted_bundles = os.path.join(extracted_dir, 'bundles')
@ -178,6 +180,8 @@ if __name__ == "__main__":
deployer.packages_path = args.packages_dir
deployer.bundles_path = args.bundles_dir
deployer.should_clean = args.clean
deployer.config_location = args.config
deployer.config_locations = {
"config.json": args.config,
}
deployer.deploy(args.tarball, args.extract_path)

View file

@ -13,6 +13,7 @@
from __future__ import print_function
import json, requests, tarfile, argparse, os, errno
import time
import traceback
from urlparse import urljoin
from flask import Flask, jsonify, request, abort
@ -124,6 +125,7 @@ def fetch_jenkins_build(job_name, build_num):
try:
extracted_dir = deploy_tarball(tar_gz_url, build_dir)
except DeployException as e:
traceback.print_exc()
abort(400, e.message)
create_symlink(source=extracted_dir, linkname=arg_symlink)
@ -185,10 +187,16 @@ if __name__ == "__main__":
to the /vector directory INSIDE the tarball."
)
)
def _raise(ex):
raise ex
# --config config.json=../../config.json --config config.localhost.json=./localhost.json
parser.add_argument(
"--config", dest="config", help=(
"Write a symlink to config.json in the extracted tarball. \
To this location."
"--config", action="append", dest="configs",
type=lambda kv: kv.split("=", 1) if "=" in kv else _raise(Exception("Missing =")), help=(
"A list of configs to symlink into the extracted tarball. \
For example, --config config.json=../config.json config2.json=../test/config.json"
)
)
parser.add_argument(
@ -212,7 +220,8 @@ if __name__ == "__main__":
deployer = Deployer()
deployer.bundles_path = args.bundles_dir
deployer.should_clean = args.clean
deployer.config_location = args.config
deployer.config_locations = dict(args.configs) if args.configs else {}
# 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
@ -225,13 +234,13 @@ if __name__ == "__main__":
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" %
"Listening on port %s. Extracting to %s%s. Symlinking to %s. Jenkins URL: %s. Config locations: %s" %
(args.port,
arg_extract_path,
" (clean after)" if deployer.should_clean else "",
arg_symlink,
arg_jenkins_url,
deployer.config_location,
deployer.config_locations,
)
)
app.run(host="0.0.0.0", port=args.port, debug=True)

View file

@ -16,7 +16,7 @@ limitations under the License.
"use strict";
var q = require("q");
import Promise from 'bluebird';
var Matrix = require("matrix-js-sdk");
var Room = Matrix.Room;
var CallHandler = require('matrix-react-sdk/lib/CallHandler');
@ -53,11 +53,11 @@ ConferenceCall.prototype._joinConferenceUser = function() {
// Make sure the conference user is in the group chat room
var groupRoom = this.client.getRoom(this.groupRoomId);
if (!groupRoom) {
return q.reject("Bad group room ID");
return Promise.reject("Bad group room ID");
}
var member = groupRoom.getMember(this.confUserId);
if (member && member.membership === "join") {
return q();
return Promise.resolve();
}
return this.client.invite(this.groupRoomId, this.confUserId);
};
@ -75,7 +75,7 @@ ConferenceCall.prototype._getConferenceUserRoom = function() {
}
}
if (confRoom) {
return q(confRoom);
return Promise.resolve(confRoom);
}
return this.client.createRoom({
preset: "private_chat",

View file

@ -52,6 +52,8 @@ module.exports = React.createClass({
},
componentWillMount: function() {
this._unmounted = false;
if (this.props.teamToken && this.props.teamServerUrl) {
this.setState({
iframeSrc: `${this.props.teamServerUrl}/static/${this.props.teamToken}/home.html`
@ -67,9 +69,14 @@ module.exports = React.createClass({
request(
{ method: "GET", url: src },
(err, response, body) => {
if (this._unmounted) {
return;
}
if (err || response.status < 200 || response.status >= 300) {
console.log(err);
this.setState({ page: "Couldn't load home page" });
console.warn(`Error loading home page: ${err}`);
this.setState({ page: _t("Couldn't load home page") });
return;
}
body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1)=>this.translate(g1));
@ -79,6 +86,10 @@ module.exports = React.createClass({
}
},
componentWillUnmount: function() {
this._unmounted = true;
},
render: function() {
if (this.state.iframeSrc) {
return (

View file

@ -28,7 +28,7 @@ var linkify = require('linkifyjs');
var linkifyString = require('linkifyjs/string');
var linkifyMatrix = require('matrix-react-sdk/lib/linkify-matrix');
var sanitizeHtml = require('sanitize-html');
var q = require('q');
import Promise from 'bluebird';
import { _t } from 'matrix-react-sdk/lib/languageHandler';
@ -73,6 +73,7 @@ module.exports = React.createClass({
this.protocols = response;
this.setState({protocolsLoading: false});
}, (err) => {
console.warn(`error loading thirdparty protocols: ${err}`);
this.setState({protocolsLoading: false});
if (MatrixClientPeg.get().isGuest()) {
// Guests currently aren't allowed to use this API, so
@ -116,7 +117,7 @@ module.exports = React.createClass({
},
getMoreRooms: function() {
if (!MatrixClientPeg.get()) return q();
if (!MatrixClientPeg.get()) return Promise.resolve();
const my_filter_string = this.state.filterString;
const my_server = this.state.roomServer;
@ -265,7 +266,7 @@ module.exports = React.createClass({
},
onFillRequest: function(backwards) {
if (backwards || !this.nextBatch) return q(false);
if (backwards || !this.nextBatch) return Promise.resolve(false);
return this.getMoreRooms();
},

View file

@ -30,6 +30,7 @@ var RoomNotifs = require('matrix-react-sdk/lib/RoomNotifs');
var FormattingUtils = require('matrix-react-sdk/lib/utils/FormattingUtils');
var AccessibleButton = require('matrix-react-sdk/lib/components/views/elements/AccessibleButton');
import Modal from 'matrix-react-sdk/lib/Modal';
import KeyCode from 'matrix-react-sdk/lib/KeyCode';
// turn this on for drop & drag console debugging galore
var debug = false;
@ -151,10 +152,11 @@ var RoomSubList = React.createClass({
}
},
onRoomTileClick(roomId) {
onRoomTileClick(roomId, ev) {
dis.dispatch({
action: 'view_room',
room_id: roomId,
clear_search: (ev && (ev.keyCode == KeyCode.ENTER || ev.keyCode == KeyCode.SPACE)),
});
},
@ -509,7 +511,7 @@ var RoomSubList = React.createClass({
if (list[i].tags[self.props.tagName] && list[i].tags[self.props.tagName].order === undefined) {
MatrixClientPeg.get().setRoomTag(list[i].roomId, self.props.tagName, {order: (order + 1.0) / 2.0}).finally(function() {
// Do any final stuff here
}).fail(function(err) {
}).catch(function(err) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to add tag " + self.props.tagName + " to room" + err);
Modal.createDialog(ErrorDialog, {

View file

@ -16,12 +16,13 @@ limitations under the License.
'use strict';
var React = require('react');
import React from 'react';
import { _t } from 'matrix-react-sdk/lib/languageHandler';
var sdk = require('matrix-react-sdk')
var dis = require('matrix-react-sdk/lib/dispatcher');
var rate_limited_func = require('matrix-react-sdk/lib/ratelimitedfunc');
var AccessibleButton = require('matrix-react-sdk/lib/components/views/elements/AccessibleButton');
import KeyCode from 'matrix-react-sdk/lib/KeyCode';
import sdk from 'matrix-react-sdk';
import dis from 'matrix-react-sdk/lib/dispatcher';
import rate_limited_func from 'matrix-react-sdk/lib/ratelimitedfunc';
import AccessibleButton from 'matrix-react-sdk/lib/components/views/elements/AccessibleButton';
module.exports = React.createClass({
displayName: 'SearchBox',
@ -46,18 +47,19 @@ module.exports = React.createClass({
},
onAction: function(payload) {
// Disabling this as I find it really really annoying, and was used to the
// previous behaviour - see https://github.com/vector-im/riot-web/issues/3348
/*
switch (payload.action) {
// Clear up the text field when a room is selected.
case 'view_room':
if (this.refs.search) {
if (this.refs.search && payload.clear_search) {
this._clearSearch();
}
break;
case 'focus_room_filter':
if (this.refs.search) {
this.refs.search.focus();
this.refs.search.select();
}
break;
}
*/
},
onChange: function() {
@ -86,6 +88,15 @@ module.exports = React.createClass({
}
},
_onKeyDown: function(ev) {
switch (ev.keyCode) {
case KeyCode.ESCAPE:
this._clearSearch();
dis.dispatch({action: 'focus_composer'});
break;
}
},
_clearSearch: function() {
this.refs.search.value = "";
this.onChange();
@ -135,6 +146,7 @@ module.exports = React.createClass({
className="mx_SearchBox_search"
value={ this.state.searchTerm }
onChange={ this.onChange }
onKeyDown={ this._onKeyDown }
placeholder={ _t('Filter room names') }
/>
];

View file

@ -67,7 +67,7 @@ module.exports = React.createClass({
onResendClick: function() {
Resend.resend(this.props.mxEvent);
if (this.props.onFinished) this.props.onFinished();
this.closeMenu();
},
onViewSourceClick: function() {
@ -75,7 +75,7 @@ module.exports = React.createClass({
Modal.createDialog(ViewSource, {
content: this.props.mxEvent.event,
}, 'mx_Dialog_viewsource');
if (this.props.onFinished) this.props.onFinished();
this.closeMenu();
},
onViewClearSourceClick: function() {
@ -84,7 +84,7 @@ module.exports = React.createClass({
// FIXME: _clearEvent is private
content: this.props.mxEvent._clearEvent,
}, 'mx_Dialog_viewsource');
if (this.props.onFinished) this.props.onFinished();
this.closeMenu();
},
onRedactClick: function() {
@ -106,12 +106,12 @@ module.exports = React.createClass({
}).done();
},
}, 'mx_Dialog_confirmredact');
if (this.props.onFinished) this.props.onFinished();
this.closeMenu();
},
onCancelSendClick: function() {
Resend.removeFromQueue(this.props.mxEvent);
if (this.props.onFinished) this.props.onFinished();
this.closeMenu();
},
onForwardClick: function() {
@ -130,7 +130,7 @@ module.exports = React.createClass({
if (this.props.eventTileOps) {
this.props.eventTileOps.unhideWidget();
}
if (this.props.onFinished) this.props.onFinished();
this.closeMenu();
},
onQuoteClick: function() {
@ -139,6 +139,7 @@ module.exports = React.createClass({
action: 'quote',
event: this.props.mxEvent,
});
this.closeMenu();
},
render: function() {
@ -247,7 +248,7 @@ module.exports = React.createClass({
{viewClearSourceButton}
{unhidePreviewButton}
{permalinkButton}
{UserSettingsStore.isFeatureEnabled('rich_text_editor') ? quoteButton : null}
{quoteButton}
{externalURLButton}
</div>
);

View file

@ -17,7 +17,7 @@ limitations under the License.
'use strict';
import q from 'q';
import Promise from 'bluebird';
import React from 'react';
import classNames from 'classnames';
import sdk from 'matrix-react-sdk';
@ -61,14 +61,14 @@ module.exports = React.createClass({
const roomId = this.props.room.roomId;
var cli = MatrixClientPeg.get();
if (!cli.isGuest()) {
q.delay(500).then(function() {
Promise.delay(500).then(function() {
if (tagNameOff !== null && tagNameOff !== undefined) {
cli.deleteRoomTag(roomId, tagNameOff).finally(function() {
// Close the context menu
if (self.props.onFinished) {
self.props.onFinished();
};
}).fail(function(err) {
}).catch(function(err) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: _t('Failed to remove tag %(tagName)s from room', {tagName: tagNameOff}),
@ -85,7 +85,7 @@ module.exports = React.createClass({
if (self.props.onFinished) {
self.props.onFinished();
};
}).fail(function(err) {
}).catch(function(err) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: _t('Failed to remove tag %(tagName)s from room', {tagName: tagNameOn}),
@ -212,7 +212,7 @@ module.exports = React.createClass({
RoomNotifs.setRoomNotifsState(this.props.room.roomId, newState).done(() => {
// delay slightly so that the user can see their state change
// before closing the menu
return q.delay(500).then(() => {
return Promise.delay(500).then(() => {
if (this._unmounted) return;
// Close the context menu
if (this.props.onFinished) {

View file

@ -158,7 +158,7 @@ module.exports = React.createClass({
var eventRedact;
if(showEventMeta) {
eventRedact = (<div className="mx_ImageView_button" onClick={this.onRedactClick}>
{ _t('Redact') }
{ _t('Remove') }
</div>);
}

View file

@ -35,7 +35,7 @@ module.exports = React.createClass({
render: function() {
return (
<div className="mx_MatrixToolbar">
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="/!\"/>
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="Warning"/>
<div className="mx_MatrixToolbar_content">
{ _t('You are not receiving desktop notifications') } <a className="mx_MatrixToolbar_link" onClick={ this.onClick }> { _t('Enable them now') }</a>
</div>

View file

@ -96,7 +96,7 @@ export default React.createClass({
}
return (
<div className="mx_MatrixToolbar">
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="/!\"/>
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="Warning"/>
<div className="mx_MatrixToolbar_content">
{_t("A new version of Riot is available.")}
</div>

View file

@ -20,6 +20,7 @@ import React from 'react';
import sdk from 'matrix-react-sdk';
import Modal from 'matrix-react-sdk/lib/Modal';
import dis from 'matrix-react-sdk/lib/dispatcher';
import { _t, _tJsx } from 'matrix-react-sdk/lib/languageHandler';
export default React.createClass({
onUpdateClicked: function() {
@ -33,12 +34,11 @@ export default React.createClass({
dis.dispatch({
action: 'password_changed',
});
}
},
});
},
render: function() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const toolbarClasses = "mx_MatrixToolbar mx_MatrixToolbar_clickable";
return (
<div className={toolbarClasses} onClick={this.onUpdateClicked}>
@ -49,12 +49,16 @@ export default React.createClass({
alt="Warning"
/>
<div className="mx_MatrixToolbar_content">
To return to your account in future you need to <u>set a password</u>
{ _tJsx(
"To return to your account in future you need to <u>set a password</u>",
/<u>(.*?)<\/u>/,
(sub) => { return <u>{ sub }</u>; },
) }
</div>
<button className="mx_MatrixToolbar_action">
Set Password
{ _t("Set Password") }
</button>
</div>
);
}
},
});

View file

@ -0,0 +1,85 @@
/*
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
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.
*/
'use strict';
import React from 'react';
import { _t } from 'matrix-react-sdk/lib/languageHandler';
import PlatformPeg from 'matrix-react-sdk/lib/PlatformPeg';
import {updateCheckStatusEnum} from '../../../vector/platform/VectorBasePlatform';
import AccessibleButton from 'matrix-react-sdk/lib/components/views/elements/AccessibleButton';
const doneStatuses = [
updateCheckStatusEnum.ERROR,
updateCheckStatusEnum.NOTAVAILABLE,
];
export default React.createClass({
propTypes: {
status: React.PropTypes.oneOf(Object.values(updateCheckStatusEnum)).isRequired,
// Currently for error detail but will be usable for download progress
// once that is a thing that squirrel passes through electron.
detail: React.PropTypes.string,
},
getDefaultProps: function() {
return {
detail: '',
}
},
getStatusText: function() {
switch(this.props.status) {
case updateCheckStatusEnum.ERROR:
return _t('Error encountered (%(errorDetail)s).', { errorDetail: this.props.detail });
case updateCheckStatusEnum.CHECKING:
return _t('Checking for an update...');
case updateCheckStatusEnum.NOTAVAILABLE:
return _t('No update available.');
case updateCheckStatusEnum.DOWNLOADING:
return _t('Downloading update...');
}
}
,
hideToolbar: function() {
PlatformPeg.get().stopUpdateCheck();
},
render: function() {
const message = this.getStatusText();
const warning = _t('Warning');
let image;
if (doneStatuses.includes(this.props.status)) {
image = <img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt={warning}/>;
} else {
image = <img className="mx_MatrixToolbar_warning" src="img/spinner.gif" width="24" height="23" alt={message}/>;
}
return (
<div className="mx_MatrixToolbar">
{image}
<div className="mx_MatrixToolbar_content">
{message}
</div>
<AccessibleButton className="mx_MatrixToolbar_close" onClick={this.hideToolbar}>
<img src="img/cancel.svg" width="18" height="18" />
</AccessibleButton>
</div>
);
}
});

View file

@ -111,7 +111,7 @@ var roomTileSource = {
//component.state.set({ spinner: component.state.spinner ? component.state.spinner++ : 1 });
MatrixClientPeg.get().deleteRoomTag(item.room.roomId, prevTag).finally(function() {
//component.state.set({ spinner: component.state.spinner-- });
}).fail(function(err) {
}).catch(function(err) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to remove tag " + prevTag + " from room: " + err);
Modal.createDialog(ErrorDialog, {
@ -130,10 +130,7 @@ var roomTileSource = {
if (newTag && newTag !== 'im.vector.fake.direct' &&
(item.targetList !== item.originalList || newOrder)
) {
//component.state.set({ spinner: component.state.spinner ? component.state.spinner++ : 1 });
MatrixClientPeg.get().setRoomTag(item.room.roomId, newTag, newOrder).finally(function() {
//component.state.set({ spinner: component.state.spinner-- });
}).fail(function(err) {
MatrixClientPeg.get().setRoomTag(item.room.roomId, newTag, newOrder).catch(function(err) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to add tag " + newTag + " to room: " + err);
Modal.createDialog(ErrorDialog, {

View file

@ -17,7 +17,7 @@ limitations under the License.
'use strict';
var React = require('react');
import { _t, _tJsx } from 'matrix-react-sdk/lib/languageHandler';
var q = require("q");
import Promise from 'bluebird';
var sdk = require('matrix-react-sdk');
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
var UserSettingsStore = require('matrix-react-sdk/lib/UserSettingsStore');
@ -236,7 +236,7 @@ module.exports = React.createClass({
}
}
q.all(deferreds).done(function() {
Promise.all(deferreds).done(function() {
self._refreshFromServer();
}, function(error) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -306,7 +306,7 @@ module.exports = React.createClass({
}
}
q.all(deferreds).done(function(resps) {
Promise.all(deferreds).done(function(resps) {
self._refreshFromServer();
}, function(error) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -361,7 +361,7 @@ module.exports = React.createClass({
}
// Then, add the new ones
q.all(removeDeferreds).done(function(resps) {
Promise.all(removeDeferreds).done(function(resps) {
var deferreds = [];
var pushRuleVectorStateKind = self.state.vectorContentRules.vectorState;
@ -399,7 +399,7 @@ module.exports = React.createClass({
}
}
q.all(deferreds).done(function(resps) {
Promise.all(deferreds).done(function(resps) {
self._refreshFromServer();
}, onError);
}, onError);
@ -431,7 +431,9 @@ module.exports = React.createClass({
'global', kind, LEGACY_RULES[rule.rule_id], portLegacyActions(rule.actions)
).then( function() {
return cli.deletePushRule('global', kind, rule.rule_id);
})
}).catch( (e) => {
console.warn(`Error when porting legacy rule: ${e}`);
});
}(kind, rule));
}
}
@ -440,7 +442,7 @@ module.exports = React.createClass({
if (needsUpdate.length > 0) {
// If some of the rules need to be ported then wait for the porting
// to happen and then fetch the rules again.
return q.allSettled(needsUpdate).then( function() {
return Promise.all(needsUpdate).then( function() {
return cli.getPushRules();
});
} else {
@ -594,7 +596,7 @@ module.exports = React.createClass({
self.setState({pushers: resp.pushers});
});
q.all([pushRulesPromise, pushersPromise]).then(function() {
Promise.all([pushRulesPromise, pushersPromise]).then(function() {
self.setState({
phase: self.phases.DISPLAY
});

View file

@ -207,5 +207,7 @@
"General discussion about Matrix and Riot": "Allgemeine Diskussion über Matrix und Riot",
"(HTTP status %(httpStatus)s)": "(HTTP-Status %(httpStatus)s)",
"You have successfully set a password and an email address!": "Du hast erfolgreich ein Passwort und eine E-Mail-Adresse gesetzt!",
"Remember, you can always set an email address in user settings if you change your mind.": "Denk daran, dass du in den Benutzereinstellungen jederzeit eine E-Mail-Adresse setzen kannst."
"Remember, you can always set an email address in user settings if you change your mind.": "Denk daran, dass du in den Benutzereinstellungen jederzeit eine E-Mail-Adresse setzen kannst.",
"To return to your account in future you need to <u>set a password</u>": "Um in Zukunft zu deinem Konto zurückkehren zu können, musst du <u>ein Passwort setzen</u>",
"Set Password": "Setze ein Passwort"
}

View file

@ -198,5 +198,7 @@
"Failed to set Direct Message status of room": "Δεν ήταν δυνατός ο ορισμός της κατάστασης Direct Message του δωματίου",
"Support for those using, running and writing other bridges": "Υποστήριξη για τους χρήστες που χρησιμοποιούν ή αναπτύσσουν εφαρμογές ενσωμάτωσης για το Matrix",
"You have successfully set a password and an email address!": "Ο κωδικός πρόσβασης και η διεύθυνση ηλεκτρονικής αλληλογραφίας ορίστηκαν επιτυχώς!",
"Remember, you can always set an email address in user settings if you change your mind.": "Να θυμάστε ότι μπορείτε πάντα να ορίσετε μια διεύθυνση ηλεκτρονικής αλληλογραφίας στις ρυθμίσεις χρήστη αν αλλάξετε γνώμη."
"Remember, you can always set an email address in user settings if you change your mind.": "Να θυμάστε ότι μπορείτε πάντα να ορίσετε μια διεύθυνση ηλεκτρονικής αλληλογραφίας στις ρυθμίσεις χρήστη αν αλλάξετε γνώμη.",
"To return to your account in future you need to <u>set a password</u>": "Για να επιστρέψετε στον λογαριασμό σας μελλοντικα πρέπει να ορίσετε έναν <u>κωδικό πρόσβασης</u>",
"Set Password": "Ορισμός κωδικού πρόσβασης"
}

View file

@ -98,7 +98,6 @@
"Please Register": "Please Register",
"powered by Matrix": "powered by Matrix",
"Quote": "Quote",
"Redact": "Redact",
"Reject": "Reject",
"Remove %(name)s from the directory?": "Remove %(name)s from the directory?",
"Remove": "Remove",
@ -160,6 +159,11 @@
"Today": "Today",
"Yesterday": "Yesterday",
"OK": "OK",
"Warning": "Warning",
"Checking for an update...": "Checking for an update...",
"Error encountered (%(errorDetail)s).": "Error encountered (%(errorDetail)s).",
"No update available.": "No update available.",
"Downloading update...": "Downloading update...",
"You need to be using HTTPS to place a screen-sharing call.": "You need to be using HTTPS to place a screen-sharing call.",
"Welcome page": "Welcome page",
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!",
@ -198,5 +202,8 @@
"Please set a password!": "Please set a password!",
"This will allow you to return to your account after signing out, and sign in on other devices.": "This will allow you to return to your account after signing out, and sign in on other devices.",
"You have successfully set a password and an email address!": "You have successfully set a password and an email address!",
"Remember, you can always set an email address in user settings if you change your mind.": "Remember, you can always set an email address in user settings if you change your mind."
"Remember, you can always set an email address in user settings if you change your mind.": "Remember, you can always set an email address in user settings if you change your mind.",
"To return to your account in future you need to <u>set a password</u>": "To return to your account in future you need to <u>set a password</u>",
"Set Password": "Set Password",
"Couldn't load home page": "Couldn't load home page"
}

View file

@ -96,7 +96,6 @@
"Please Register": "Please Register",
"powered by Matrix": "powered by Matrix",
"Quote": "Quote",
"Redact": "Redact",
"Reject": "Reject",
"Remove %(name)s from the directory?": "Remove %(name)s from the directory?",
"Remove": "Remove",

View file

@ -198,5 +198,11 @@
"Lots of rooms already exist in Matrix, linked to existing networks (Slack, IRC, Gitter etc) or independent. Check out the directory!": "Muchas salas ya están disponibles en Matrix, enlazadas a redes existentes (Slack, IRC, Gitter, etc) o independientes. ¡Revisa el directorio!",
"You can now return to your account after signing out, and sign in on other devices.": "Ahora puedes regresar a tu cuenta después de cerrar tu sesión, e iniciar sesión en otros dispositivos.",
"Please set a password!": "¡Por favor establece una contraseña!",
"This will allow you to return to your account after signing out, and sign in on other devices.": "Esto le permitirá regresar a su cuenta después de cerrar sesión, así como iniciar sesión en otros dispositivos."
"This will allow you to return to your account after signing out, and sign in on other devices.": "Esto le permitirá regresar a su cuenta después de cerrar sesión, así como iniciar sesión en otros dispositivos.",
"Warning": "Advertencia",
"Checking for an update...": "Comprobando actualizaciones...",
"No update available.": "No hay actualizaciones disponibles.",
"Downloading update...": "Descargando actualizaciones...",
"To return to your account in future you need to <u>set a password</u>": "Para regresar a su cuenta en el futuro Ud. debe <u>establecer una contraseña</u>",
"Set Password": "Establezca la contraseña"
}

View file

@ -130,7 +130,7 @@
"Messages containing <span>keywords</span>": "Az üzenet <span>kulcsszavakat</span> tartalmaz",
"%(appName)s via %(browserName)s on %(osName)s": "%(appName)s alkalmazás %(browserName)s böngészőn %(osName)s rendszeren",
"A new version of Riot is available.": "Új verzió érhető el a Riot-ból.",
"All Rooms": "Minden szoba",
"All Rooms": "Minden szobában",
"Cancel": "Mégse",
"Changelog": "Változások",
"Collecting app version information": "Alkalmazás verzió információk összegyűjtése",
@ -148,7 +148,7 @@
"Search…": "Keresés…",
"Send": "Küld",
"Send logs": "Naplók elküldése",
"This Room": "Ez a szoba",
"This Room": "Ebben a szobában",
"Unavailable": "Elérhetetlen",
"Unknown device": "Ismeretlen eszköz",
"Update": "Frissítés",
@ -198,5 +198,7 @@
"This will allow you to return to your account after signing out, and sign in on other devices.": "Ezzel visszatérhetsz kijelentkezés után a fiókodhoz és más eszközökkel is be tudsz jelentkezni.",
"(HTTP status %(httpStatus)s)": "(HTTP állapot %(httpStatus)s)",
"You have successfully set a password and an email address!": "Sikeresen beállítottad a jelszavad és e-mail címed!",
"Remember, you can always set an email address in user settings if you change your mind.": "Ha meggondolod magad, bármikor beállíthatod az e-mail címed a felhasználói beállításoknál."
"Remember, you can always set an email address in user settings if you change your mind.": "Ha meggondolod magad, bármikor beállíthatod az e-mail címed a felhasználói beállításoknál.",
"To return to your account in future you need to <u>set a password</u>": "A fiókba való visszalépéshez <u>jelszót</u> kell beállítanod",
"Set Password": "Jelszó beállítása"
}

View file

@ -25,7 +25,7 @@
"Directory": "목록",
"Dismiss": "없애기",
"Download this file": "이 파일 받기",
"Enable desktop notifications": "데스크탑에서 알림 받기",
"Enable desktop notifications": "컴퓨터에서 알림 받기",
"Enable email notifications": "이메일로 알림 받기",
"Enable notifications for this account": "이 계정의 알림 받기",
"Error": "오류",
@ -62,7 +62,7 @@
"Remove from Directory": "목록에서 지우기",
"Report a bug": "오류 보고하기",
"Resend": "다시 보내기",
"Riot Desktop on %(platformName)s": "%(platformName)s에서 라이엇 데스크탑",
"Riot Desktop on %(platformName)s": "%(platformName)s에서 라이엇 컴퓨터판",
"Riot is not supported on mobile web. Install the app?": "라이엇은 모바일 사이트를 지원하지 않아요. 앱을 설치하시겠어요?",
"Room directory": "방 목록",
"Room not found": "방을 찾지 못했어요",
@ -87,7 +87,7 @@
"Waiting for response from server": "서버에서 응답을 기다리는 중",
"You cannot delete this image. (%(code)s)": "이 사진을 지우실 수 없어요. (%(code)s)",
"You cannot delete this message. (%(code)s)": "이 메시지를 지우실 수 없어요. (%(code)s)",
"You are not receiving desktop notifications": "데스크탑 알림을 받지 않고 있어요",
"You are not receiving desktop notifications": "컴퓨터 알림을 받지 않고 있어요",
"Sunday": "일요일",
"Monday": "월요일",
"Tuesday": "화요일",
@ -171,7 +171,7 @@
"Get started with some tips from Riot Bot!": "라이엇 봇에게 조언을 받고 시작하세요!",
"General discussion about Matrix and Riot": "매트릭스와 라이엇에 대한 일반 논의",
"Discussion of all things Matrix!": "매트릭스의 모든 것에 대한 토론!",
"Riot/Web &amp; Desktop chat": "라이엇/웹 &amp; 데스크탑 대화",
"Riot/Web &amp; Desktop chat": "라이엇/웹 &amp; 컴퓨터 이야기",
"Riot/iOS &amp; matrix-ios-sdk chat": "라이엇/IOS &amp; matrix-ios-sdk 대화",
"Riot/Android &amp; matrix-android-sdk chat": "매트릭스/안드로이드 &amp; matrix-ios-sdk 대화",
"Matrix technical discussions": "매트릭스 기술 논의",
@ -198,5 +198,7 @@
"You have successfully set a password and an email address!": "비밀번호와 이메일 주소를 설정했어요!",
"Remember, you can always set an email address in user settings if you change your mind.": "잊지마세요, 마음이 바뀌면 언제라도 사용자 설정에서 이메일 주소를 바꾸실 수 있다는 걸요.",
"You are Rioting as a guest. <a>Register</a> or <a>sign in</a> to access more rooms and features!": "손님으로 라이엇에 들어오셨네요. <a>계정을 등록하거나</a> <a>로그인하시고</a> 더 많은 방과 기능을 즐기세요!",
"You might have configured them in a client other than Riot. You cannot tune them in Riot but they still apply": "라이엇이 아닌 다른 클라이언트에서 구성하셨을 수도 있어요. 라이엇에서 조정할 수는 없지만 여전히 적용되있을 거에요"
"You might have configured them in a client other than Riot. You cannot tune them in Riot but they still apply": "라이엇이 아닌 다른 클라이언트에서 구성하셨을 수도 있어요. 라이엇에서 조정할 수는 없지만 여전히 적용되있을 거에요",
"To return to your account in future you need to <u>set a password</u>": "나중에 계정으로 돌아가려면 <u>비밀번호 설정</u>을 해야만 해요",
"Set Password": "비밀번호 설정"
}

View file

@ -198,5 +198,7 @@
"Please set a password!": "Proszę, ustaw hasło!",
"This will allow you to return to your account after signing out, and sign in on other devices.": "To pozwoli Ci powrócić do Twojego konta po wylogowaniu i ponownym zalogowaniu się na innych urządzeniach.",
"You have successfully set a password and an email address!": "Z powodzeniem ustawiono hasło i adres e-mail dla Twojego konta!",
"Remember, you can always set an email address in user settings if you change your mind.": "Pamiętaj, że zawsze możesz zmienić swój e-mail lub hasło w panelu ustawień użytkownika."
"Remember, you can always set an email address in user settings if you change your mind.": "Pamiętaj, że zawsze możesz zmienić swój e-mail lub hasło w panelu ustawień użytkownika.",
"To return to your account in future you need to <u>set a password</u>": "Aby wrócić do swojego konta w przyszłości musisz <u> ustawić hasło </u>",
"Set Password": "Ustaw hasło"
}

View file

@ -199,5 +199,9 @@
"Please set a password!": "Por favor, defina uma senha!",
"This will allow you to return to your account after signing out, and sign in on other devices.": "Isso permitirá que você possa retornar à sua conta após fazer logout, e também fazer login em outros dispositivos.",
"(HTTP status %(httpStatus)s)": "(Status HTTP %(httpStatus)s)",
"Decentralised, encrypted chat &amp; collaboration powered by [matrix]": "Chat descentralizado, criptografado e colaborativo impulsionado por [matrix]"
"Decentralised, encrypted chat &amp; collaboration powered by [matrix]": "Chat descentralizado, criptografado e colaborativo impulsionado por [matrix]",
"You have successfully set a password and an email address!": "Você definiu uma senha e um endereço de e-mail com sucesso!",
"Remember, you can always set an email address in user settings if you change your mind.": "Lembre-se: você pode sempre definir um endereço de e-mail nas configurações de usuário, se mudar de ideia.",
"To return to your account in future you need to <u>set a password</u>": "Para poder, futuramente, retornar à sua conta, você precisa <u>definir uma senha</u>",
"Set Password": "Definir senha"
}

View file

@ -205,5 +205,7 @@
"Co-ordination for Riot/Web translators": "Координирование для переводчиков Riot / Web",
"This will allow you to return to your account after signing out, and sign in on other devices.": "Это позволит Вам вернуться в свою учетную запись после выхода, и войти в систему на других устройствах.",
"You have successfully set a password and an email address!": "Пароль и адрес электронной почты успешно сохранены!",
"Remember, you can always set an email address in user settings if you change your mind.": "Помните, Вы всегда можете указать адрес электронной почты в пользовательских настройках, если передумаете."
"Remember, you can always set an email address in user settings if you change your mind.": "Помните, Вы всегда можете указать адрес электронной почты в пользовательских настройках, если передумаете.",
"Set Password": "Задать пароль",
"To return to your account in future you need to <u>set a password</u>": "Чтобы в будущем вернутся к вашей учётной записи, вы должны <u>задать пароль</u>"
}

View file

@ -171,7 +171,7 @@
"Riot/Web &amp; Desktop chat": "แชทเกี่ยวกับ Riot บนเว็บและเดสก์ทอป",
"Riot/iOS &amp; matrix-ios-sdk chat": "แชทเกี่ยวกับ Riot บน iOS และ matrix-ios-sdk",
"Riot/Android &amp; matrix-android-sdk chat": "แชทเกี่ยวกับ Riot บน Android และ matrix-android-sdk",
"Matrix technical discussions": "พูดคุยเรื่อง Matrix ทางเทคนิค",
"Matrix technical discussions": "พูดคุยเรื่อง Matrix เชิงเทคนิค",
"Running Matrix services": "การติดตั้งบริการ Matrix",
"Community-run support for Synapse": "ฝ่ายสนับสนุน Synapse โดยชุมชนผู้ใช้",
"Admin support for Dendrite": "ฝ่ายสนับสนุน Dendrite จากผู้ดูแล",
@ -182,7 +182,7 @@
"Implementing VR services with Matrix": "การอิมพลีเมนต์บริการ VR ด้วย Matrix",
"Implementing VoIP services with Matrix": "การอิมพลีเมนต์บริการ VoIP ด้วย Matrix",
"Support for those using, running and writing other bridges": "ฝ่ายสนับสนุนสำหรับผู้ใช้หรือพัฒนาตัวเชื่อมอื่น ๆ",
"Contributing code to Matrix and Riot": "สมทบโค๊ดกับ Matrix และ Riot",
"Contributing code to Matrix and Riot": "สมทบโค๊ดให้ Matrix และ Riot",
"Dev chat for the Riot/Web dev team": "แชทสำหรับทีมพัฒนา Riot บนเว็บ",
"Dev chat for the Dendrite dev team": "แชทสำหรับทีมพัฒนา Dendrite",
"Co-ordination for Riot/Web translators": "แชทสำหรับประสานงานการแปล Riot บนเว็บ",
@ -199,5 +199,12 @@
"General discussion about Matrix and Riot": "พูดคุยเรื่องทั่วไป ทั้ง Matrix และ Riot",
"(HTTP status %(httpStatus)s)": "(สถานะ HTTP %(httpStatus)s)",
"Remember, you can always set an email address in user settings if you change your mind.": "อย่าลืม คุณสามารถตั้งที่อยู่อีเมลในการตั้งค่าผู้ใช้ได้ทุกเมื่อหากคุณเปลี่ยนใจ",
"You have successfully set a password and an email address!": "ตั้งรหัสผ่านและที่อยู่อีเมลสำเร็จแล้ว!"
"You have successfully set a password and an email address!": "ตั้งรหัสผ่านและที่อยู่อีเมลสำเร็จแล้ว!",
"Warning": "คำเตือน",
"Checking for an update...": "กำลังตรวจหาอัปเดต...",
"Error encountered (%(errorDetail)s).": "เกิดข้อผิดพลาด (%(errorDetail)s)",
"No update available.": "ไม่มีอัปเดตที่ใหม่กว่า",
"Downloading update...": "กำลังดาวน์โหลดอัปเดต...",
"To return to your account in future you need to <u>set a password</u>": "คุณต้อง<u>ตั้งรหัสผ่าน</u>เพื่อจะกลับมาที่บัญชีนี้ในอนาคต",
"Set Password": "ตั้งรหัสผ่าน"
}

View file

@ -18,7 +18,7 @@
"Collapse panel": "Katlanır panel",
"Collecting app version information": "Uygulama sürümü bilgileri toplanıyor",
"Collecting logs": "Kayıtlar toplanıyor",
"Create new room": "Yeni oda oluştur",
"Create new room": "Yeni Oda Oluştur",
"Couldn't find a matching Matrix room": "Eşleşen bir Matrix odası bulunamadı",
"Custom Server Options": "Özel Sunucu Seçenekleri",
"customServer_text": "Farklı bir Ana Sunucu URL'si belirleyerek başka bir Matrix sunucusunda oturum açmak için Özel Sunucu Seçeneklerini kullanabilirsiniz. <br/> Bu , Riot'u mevcut Matrix hesabı ile farklı bir Ana Sunucuda kullanmanıza olanak tanır.<br/> <br/> Ayrıca Özel Kimlik Sunucu'da ayarlayabilirsiniz ama kullanıcıları e-posta adresleriyle veya kendi e-posta adresinizle davet edemezsiniz.",
@ -27,9 +27,9 @@
"Describe your problem here.": "Probleminizi burada açıklayın.",
"Direct Chat": "Doğrudan Sohbet",
"Directory": "Dizin",
"Dismiss": "Reddet",
"Dismiss": "Uzaklaştır",
"Download this file": "Bu dosyayı indir",
"Drop here %(toAction)s": "Burayı terket %(toAction)s",
"Drop here %(toAction)s": "%(toAction)s'ı buraya bırak",
"Enable audible notifications in web client": "Web istemcisinde sesli bildirimleri etkinleştir",
"Enable desktop notifications": "Masaüstü bildirimlerini etkinleştir",
"Enable email notifications": "E-posta bildirimlerini etkinleştir",
@ -43,11 +43,11 @@
"Failed to": "Başaramadı",
"Failed to add tag %(tagName)s to room": "%(tagName)s etiketi odaya eklenemedi",
"Failed to change settings": "Ayarlar değiştirilemedi",
"Failed to forget room %(errCode)s": "Odayı unutma başarısız oldu %(errCode)s",
"Failed to forget room %(errCode)s": "Oda unutulması başarısız oldu %(errCode)s",
"Failed to update keywords": "Anahtar kelimeler güncellenemedi",
"Failed to get protocol list from Home Server": "Ana Sunucu'dan protokol listesi alınamadı",
"Failed to get public room list": "Genel odalar listesi alınamadı",
"Failed to join the room": "Odaya girilemedi",
"Failed to join the room": "Odaya girme başarısız oldu",
"Failed to remove tag %(tagName)s from room": "Odadan %(tagName)s etiketi kaldırılamadı",
"Failed to send report: ": "Rapor gönderilemedi: ",
"Failed to set direct chat tag": "Direkt sohbet etiketi ayarlanamadı",
@ -90,7 +90,7 @@
"Notify me for anything else": "Başka herhangi bir şey için bana bildirim yap",
"Off": "Kapalı",
"On": "Açık",
"Operation failed": "Operasyon başarısız",
"Operation failed": "Operasyon başarısız oldu",
"Permalink": "Kalıcı Bağlantı(permalink)",
"Please describe the bug. What did you do? What did you expect to happen? What actually happened?": "Lütfen hatayı tanımlayın. Ne yaptınız ? Ne gerçekleşmesini beklediniz ? Ne gerçekleşti ?",
"Please describe the bug and/or send logs.": "Lütfen hatayı tanımlayın ve/veya kayıtları gönderin.",
@ -110,7 +110,7 @@
"Riot does not know how to join a room on this network": "Riot bu ağdaki bir odaya nasıl gireceğini bilmiyor",
"Riot is not supported on mobile web. Install the app?": "Riot mobil web'de desteklenmiyor . Uygulamayı yükle ?",
"Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot geçerli tarayıcınızda mevcut olmayan veya denemelik olan birçok gelişmiş tarayıcı özelliği kullanıyor.",
"Room directory": "Oda dizini",
"Room directory": "Oda Rehberi",
"Room not found": "Oda bulunamadı",
"Search": "Ara",
"Search…": "Arama…",
@ -120,7 +120,7 @@
"Settings": "Ayarlar",
"Source URL": "Kaynak URL",
"Sorry, your browser is <b>not</b> able to run Riot.": "Üzgünüz , tarayıcınız Riot'u <b> çalıştıramıyor </b>.",
"Start chat": "Sohbet başlat",
"Start chat": "Sohbet Başlat",
"The Home Server may be too old to support third party networks": "Ana Sunucu 3. parti ağları desteklemek için çok eski olabilir",
"There are advanced notifications which are not shown here": "Burada gösterilmeyen gelişmiş bildirimler var",
"The server may be unavailable or overloaded": "Sunucu kullanılamıyor veya aşırı yüklenmiş olabilir",
@ -133,7 +133,7 @@
"Unavailable": "Kullanım dışı",
"Unhide Preview": "Önizlemeyi Göster",
"Unknown device": "Bilinmeyen aygıt",
"unknown error code": "Bilinmeyen hata kodu",
"unknown error code": "bilinmeyen hata kodu",
"Unnamed room": "İsimsiz oda",
"Update": "Güncelleştirme",
"Uploaded on %(date)s by %(user)s": "%(user)s tarafında %(date)s e yüklendi",
@ -191,10 +191,10 @@
"Dev chat for the Dendrite dev team": "Dendrite Geliştirici Takımı için Geliştirici sohbeti",
"Co-ordination for Riot/Web translators": "Riot/Web çevirmenleri için koordinasyon",
"Lots of rooms already exist in Matrix, linked to existing networks (Slack, IRC, Gitter etc) or independent. Check out the directory!": "Matrix'te varolan ağlara (Slack , IRC , Gitter vb.) bağlı ya da bağımsız bir çok oda var . Dizini kontrol edin!",
"Failed to change password. Is your password correct?": "Şifre değiştirme başarısız oldu . Şifreniz doğru mu ?",
"Failed to change password. Is your password correct?": "Şifreniz değiştirilemedi . Şifreniz doğru mu ?",
"You have successfully set a password!": "Başarıyla bir şifre ayarladınız!",
"You can now return to your account after signing out, and sign in on other devices.": "Şimdi oturumunuzu iptal ettikten sonra başka cihazda oturum açarak hesabınıza dönebilirsiniz.",
"Continue": "Devam",
"Continue": "Devam Et",
"Please set a password!": "Lütfen bir şifre ayarlayın !",
"This will allow you to return to your account after signing out, and sign in on other devices.": "Bu oturumunuzu kapattıktan sonra hesabınıza dönmenizi ve diğer cihazlarda oturum açmanızı sağlar.",
"You have successfully set a password and an email address!": "Başarıyla bir şifre ve e-posta adresi ayarladın !",

View file

@ -194,5 +194,11 @@
"No rooms to show": "Кімнати відсутні",
"Noisy": "Шумний",
"Unavailable": "Нема в наявності",
"Unhide Preview": "Відкрити попередній перегляд"
"Unhide Preview": "Відкрити попередній перегляд",
"Failed to set Direct Message status of room": "Не вдалось встановити статус прямого спілкування в кімнаті",
"Messages containing my display name": "Повідомлення, вміщає моє ім'я",
"Running Matrix services": "Запуск служби Matrix",
"Set Password": "Задати пароль",
"Notifications on the following keywords follow rules which cant be displayed here:": "Сповіщення з наступних ключових слів дотримуються правил, що не можуть бути показані тут:",
"To return to your account in future you need to <u>set a password</u>": "Щоб мати змогу використовувати вашу обліковку у майбутньому, <u>зазначте пароль</u>"
}

View file

@ -306,3 +306,24 @@ textarea {
.changelog_text {
font-family: 'Open Sans', Arial, Helvetica, Sans-Serif;
}
.mx_Beta {
color: red;
margin-right: 10px;
position: relative;
top: -3px;
background-color: white;
padding: 0 4px;
border-radius: 3px;
border: 1px solid darkred;
cursor: help;
transition-duration: 200ms;
font-size: smaller;
filter: opacity(0.5);
}
.mx_Beta:hover {
color: white;
border: 1px solid gray;
background-color: darkred;
}

View file

@ -3,8 +3,10 @@
@import "./matrix-react-sdk/structures/_ContextualMenu.scss";
@import "./matrix-react-sdk/structures/_CreateRoom.scss";
@import "./matrix-react-sdk/structures/_FilePanel.scss";
@import "./matrix-react-sdk/structures/_GroupView.scss";
@import "./matrix-react-sdk/structures/_LoginBox.scss";
@import "./matrix-react-sdk/structures/_MatrixChat.scss";
@import "./matrix-react-sdk/structures/_MyGroups.scss";
@import "./matrix-react-sdk/structures/_NotificationPanel.scss";
@import "./matrix-react-sdk/structures/_RoomStatusBar.scss";
@import "./matrix-react-sdk/structures/_RoomView.scss";
@ -17,6 +19,7 @@
@import "./matrix-react-sdk/views/dialogs/_ChatCreateOrReuseChatDialog.scss";
@import "./matrix-react-sdk/views/dialogs/_ChatInviteDialog.scss";
@import "./matrix-react-sdk/views/dialogs/_ConfirmUserActionDialog.scss";
@import "./matrix-react-sdk/views/dialogs/_CreateGroupDialog.scss";
@import "./matrix-react-sdk/views/dialogs/_EncryptedEventDialog.scss";
@import "./matrix-react-sdk/views/dialogs/_SetMxIdDialog.scss";
@import "./matrix-react-sdk/views/dialogs/_UnknownDeviceDialog.scss";
@ -38,6 +41,7 @@
@import "./matrix-react-sdk/views/messages/_RoomAvatarEvent.scss";
@import "./matrix-react-sdk/views/messages/_TextualEvent.scss";
@import "./matrix-react-sdk/views/messages/_UnknownBody.scss";
@import "./matrix-react-sdk/views/rooms/_AppsDrawer.scss";
@import "./matrix-react-sdk/views/rooms/_Autocomplete.scss";
@import "./matrix-react-sdk/views/rooms/_EntityTile.scss";
@import "./matrix-react-sdk/views/rooms/_EventTile.scss";
@ -53,7 +57,6 @@
@import "./matrix-react-sdk/views/rooms/_RoomSettings.scss";
@import "./matrix-react-sdk/views/rooms/_RoomTile.scss";
@import "./matrix-react-sdk/views/rooms/_SearchableEntityList.scss";
@import "./matrix-react-sdk/views/rooms/_TabCompleteBar.scss";
@import "./matrix-react-sdk/views/rooms/_TopUnreadMessagesBar.scss";
@import "./matrix-react-sdk/views/settings/_DevicesPanel.scss";
@import "./matrix-react-sdk/views/settings/_IntegrationsManager.scss";

View file

@ -0,0 +1,157 @@
/*
Copyright 2017 Vector Creations 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.
*/
.mx_GroupView {
max-width: 960px;
width: 100%;
}
.mx_GroupView_error {
margin: auto;
}
.mx_GroupView_header {
max-width: 960px;
margin: auto;
height: 70px;
align-items: center;
display: flex;
margin-bottom: 20px;
}
.mx_GroupView_header_view {
border-bottom: 1px solid #e5e5e5;
}
.mx_GroupView_header_avatar, .mx_GroupView_header_info {
display: table-cell;
vertical-align: middle;
}
.mx_GroupHeader_button {
margin-left: 12px;
cursor: pointer;
}
.mx_GroupHeader_button object {
// prevents clicks from being swallowed by svg in 'object' tag
pointer-events: none;
}
.mx_GroupView_avatarPicker {
position: relative;
}
.mx_GroupView_avatarPicker_edit {
position: absolute;
top: 50px;
left: 15px;
}
.mx_GroupView_avatarPicker .mx_Spinner {
width: 48px;
height: 48px ! important;
}
.mx_GroupView_header_leftCol {
flex: 1;
}
.mx_GroupView_saveButton, .mx_GroupView_cancelButton {
display: table-cell;
}
.mx_GroupView_header_groupid {
font-weight: normal;
font-size: initial;
padding-left: 10px;
}
.mx_GroupView_header_name {
vertical-align: middle;
width: 100%;
height: 31px;
overflow: hidden;
color: $primary-fg-color;
font-weight: bold;
font-size: 22px;
padding-left: 19px;
padding-right: 16px;
/* why isn't text-overflow working? */
text-overflow: ellipsis;
border-bottom: 1px solid transparent;
}
.mx_GroupView_header_name input, .mx_GroupView_header_shortDesc input {
width: 400px;
}
.mx_GroupView_header_shortDesc {
vertical-align: bottom;
float: left;
max-height: 42px;
color: $settings-grey-fg-color;
font-weight: 300;
font-size: 13px;
padding-left: 19px;
margin-right: 16px;
overflow: hidden;
text-overflow: ellipsis;
border-bottom: 1px solid transparent;
}
.mx_GroupView_avatarPicker_label {
cursor: pointer;
}
.mx_GroupView_cancelButton {
padding-left: 8px;
}
.mx_GroupView_cancelButton img {
position: relative;
top: 5px;
}
.mx_GroupView_featuredThings {
margin-top: 20px;
}
.mx_GroupView_featuredThings_header {
font-weight: bold;
font-size: 120%;
margin-bottom: 20px;
}
.mx_GroupView_featuredThings_category {
font-weight: bold;
font-size: 110%;
margin-top: 10px;
}
.mx_GroupView_featuredThing {
cursor: pointer;
display: table-cell;
}
.mx_GroupView_uploadInput {
display: none;
}
.mx_GroupView_editLongDesc {
width: 100%;
height: 150px;
}

View file

@ -0,0 +1,44 @@
/*
Copyright 2017 Vector Creations 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.
*/
.mx_MyGroups_joinCreateBox {
display: table;
margin-bottom: 30px;
}
.mx_MyGroups_createBox, .mx_MyGroups_joinBox {
display: table-cell;
width: 40%;
}
.mx_MyGroups_joinCreateHeader {
font-weight: bold;
margin-bottom: 10px;
}
.mx_MyGroups_joinCreateButton {
float: left;
margin: 10px;
}
.mx_MyGroups_joinCreateButton object {
/* Otherwise the SVG object absorbs clicks and the button doesn't work */
pointer-events: none;
}
.mx_MyGroups_content {
clear: left;
}

View file

@ -140,11 +140,6 @@ limitations under the License.
cursor: pointer;
}
.mx_RoomStatusBar_tabCompleteBar {
padding-top: 10px;
color: $primary-fg-color;
}
.mx_RoomStatusBar_typingBar {
height: 50px;
line-height: 50px;
@ -155,26 +150,6 @@ limitations under the License.
display: block;
}
.mx_RoomStatusBar_tabCompleteWrapper {
display: flex;
height: 26px;
}
.mx_RoomStatusBar_tabCompleteWrapper .mx_TabCompleteBar {
flex: 1 1 auto;
}
.mx_RoomStatusBar_tabCompleteEol {
flex: 0 0 auto;
color: $accent-color;
}
.mx_RoomStatusBar_tabCompleteEol object {
vertical-align: middle;
margin-right: 8px;
margin-top: -2px;
}
.mx_MatrixChat_useCompactLayout {
.mx_RoomStatusBar {
min-height: 40px;

View file

@ -102,6 +102,13 @@ limitations under the License.
padding-right: 1em;
}
.mx_UserSettings_passwordWarning {
/* To move the "Sign out" button out of the way */
clear: both;
color: $warning-color;
margin-bottom: 5px;
}
.mx_UserSettings_importExportButtons {
padding-top: 10px;
padding-left: 40px;

View file

@ -1,5 +1,5 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -14,17 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import PlatformPeg from 'matrix-react-sdk/lib/PlatformPeg';
.mx_CreateGroupDialog_inputRow {
margin-top: 10px;
margin-bottom: 10px;
}
var POKE_RATE_MS = 10 * 60 * 1000; // 10 min
.mx_CreateGroupDialog_label {
text-align: left;
padding-bottom: 12px;
}
module.exports = {
start: function() {
module.exports.poll();
setInterval(module.exports.poll, POKE_RATE_MS);
},
.mx_CreateGroupDialog_input {
font-size: 15px;
border-radius: 3px;
border: 1px solid $input-border-color;
padding: 9px;
color: $primary-fg-color;
background-color: $primary-bg-color;
}
poll: function() {
PlatformPeg.get().pollForUpdate();
}
};

View file

@ -2,19 +2,42 @@
// naming scheme; it's completely unclear where or how they're being used
// --Matthew
.mx_UserPill {
color: white;
background-color: $accent-color;
padding: 2px 8px;
.mx_UserPill,
.mx_RoomPill {
border-radius: 16px;
display: inline-block;
height: 20px;
line-height: 20px;
}
/* More specific to override `.markdown-body a` color */
.mx_EventTile_content .markdown-body a.mx_UserPill,
.mx_UserPill {
color: $primary-fg-color;
background-color: $other-user-pill-bg-color;
padding-right: 5px;
}
.mx_EventTile_highlight .mx_EventTile_content .markdown-body a.mx_UserPill_me {
color: $accent-fg-color;
background-color: $mention-user-pill-bg-color;
padding-right: 5px;
}
/* More specific to override `.markdown-body a` color */
.mx_EventTile_content .markdown-body a.mx_RoomPill,
.mx_RoomPill {
background-color: white;
color: $accent-color;
border: 1px solid $accent-color;
padding: 2px 8px;
border-radius: 16px;
color: $accent-fg-color;
background-color: $rte-room-pill-color;
padding-right: 5px;
}
.mx_UserPill .mx_BaseAvatar,
.mx_RoomPill .mx_BaseAvatar {
position: relative;
top: 2px;
margin-left: 2px;
margin-right: 2px;
}
.mx_Markdown_BOLD {

View file

@ -14,6 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_MEmoteBody {
white-space: pre-wrap;
}
.mx_MEmoteBody_sender {
cursor: pointer;
}

View file

@ -0,0 +1,228 @@
/*
Copyright 2015, 2016 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.
*/
.mx_AppsDrawer {
}
.mx_AppsContainer {
display: flex;
flex-direction: row;
align-items: center;
}
.mx_AddWidget_button {
order: 2;
cursor: pointer;
padding-right: 12px;
padding: 0;
margin: 0 0 5px 0;
color: $accent-color;
font-size: 12px;
}
.mx_SetAppURLDialog_input {
border-radius: 3px;
border: 1px solid $input-border-color;
padding: 9px;
color: $primary-hairline-color;
background-color: $primary-bg-color;
font-size: 15px;
}
.mx_AppTile {
width: 50%;
margin: 0 5px 2px 0;
border: 1px solid $primary-hairline-color;
border-radius: 2px;
// height: 350px;
// display: inline-block;
}
.mx_AppTileFullWidth {
width: 100%;
margin: 0;
padding: 0;
border: 1px solid $primary-hairline-color;
border-radius: 2px;
// height: 350px;
// display: inline-block;
}
.mx_AppTileMenuBar {
// height: 15px;
margin: 0;
padding: 2px 10px;
// background-color: $e2e-verified-color;
border-bottom: 1px solid $primary-hairline-color;
font-size: 10px;
}
.mx_AppTileMenuBarWidgets {
float: right;
}
.mx_AppTileMenuBarWidget {
// pointer-events: none;
cursor: pointer;
width: 10px;
height: 10px;
padding: 1px;
transition-duration: 500ms;
border: 1px solid transparent;
}
.mx_AppTileMenuBarWidgetDelete {
filter: none;
}
.mx_AppTileMenuBarWidget:hover {
border: 1px solid $primary-hairline-color;
border-radius: 2px;
}
.mx_AppTileBody{
height: 350px;
overflow: hidden;
}
.mx_AppTileBody iframe {
width: 100%;
height: 350px;
overflow: hidden;
border: none;
padding: 0;
margin: 0;
display: block;
}
.mx_CloseAppWidget {
}
.mx_AppTileMenuBarWidgetPadding {
margin-right: 5px;
}
.mx_AppIconTile {
background-color: $lightbox-bg-color;
border: 1px solid rgba(0, 0, 0, 0);
width: 200px;
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
transition: 0.3s;
border-radius: 3px;
margin: 5px;
display: inline-block;
}
.mx_AppIconTile.mx_AppIconTile_active {
color: $accent-color;
border-color: $accent-color;
}
.mx_AppIconTile:hover {
border: 1px solid $accent-color;
box-shadow: 0 0 10px 5px rgba(200,200,200,0.5);
}
.mx_AppIconTile_content {
padding: 2px 16px;
height: 60px;
overflow: hidden;
}
.mx_AppIconTile_content h4 {
margin-top: 5px;
margin-bottom: 2px;
}
.mx_AppIconTile_content p {
margin-top: 0;
margin-bottom: 5px;
font-size: smaller;
}
.mx_AppIconTile_image {
padding: 10px;
width: 75%;
max-width:100px;
max-height:100px;
width: auto;
height: auto;
}
.mx_AppIconTile_imageContainer {
text-align: center;
width: 100%;
background-color: white;
border-radius: 3px 3px 0 0;
height: 155px;
display: flex;
justify-content: center;
align-items: center;
}
form.mx_Custom_Widget_Form div {
margin-top: 10px;
margin-bottom: 10px;
}
.mx_AppPermissionWarning {
text-align: center;
padding: 30px 0;
background-color: $primary-bg-color;
}
.mx_AppPermissionWarningImage {
margin: 10px 0;
}
.mx_AppPermissionWarningImage img {
width: 100px;
}
.mx_AppPermissionWarningText {
max-width: 400px;
margin: 10px auto 10px auto;
color: $primary-fg-color;
}
.mx_AppPermissionWarningTextLabel {
font-weight: bold;
display: block;
}
.mx_AppPermissionWarningTextURL {
color: $accent-color;
}
.mx_AppPermissionButton {
padding: 5px;
border-radius: 5px;
color: $warning-color;
background-color: $primary-bg-color;
}
.mx_AppPermissionButton:hover {
background-color: $primary-fg-color;
cursor: pointer;
}
.mx_AppLoading {
min-height: 305px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-weight: bold;
}

View file

@ -38,6 +38,7 @@
.mx_Autocomplete_Completion_pill {
border-radius: 17px;
height: 34px;
padding: 0px 5px;
display: flex;
user-select: none;
cursor: pointer;
@ -45,10 +46,22 @@
color: $primary-fg-color;
}
.mx_Autocomplete_Completion_pill * {
.mx_Autocomplete_Completion_pill > * {
margin: 0 3px;
}
.mx_Autocomplete_Completion_container_truncate {
.mx_Autocomplete_Completion_title,
.mx_Autocomplete_Completion_subtitle,
.mx_Autocomplete_Completion_description {
/* Ellipsis for long names/subtitles/descriptions*/
max-width: 150px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
/* container for pill-style completions */
.mx_Autocomplete_Completion_container_pill {
margin: 12px;

View file

@ -128,6 +128,12 @@ limitations under the License.
color: $event-sending-color;
}
.mx_EventTile_sending .mx_UserPill,
.mx_EventTile_sending .mx_RoomPill,
.mx_EventTile_sending .mx_emojione {
opacity: 0.5;
}
.mx_EventTile_notSent {
color: $event-notsent-color;
}
@ -316,7 +322,7 @@ limitations under the License.
font-family: inherit ! important;
white-space: normal ! important;
line-height: inherit ! important;
color: inherit;
color: inherit; // inherit the colour from the dark or light theme by default (but not for code blocks)
font-size: 14px;
}
@ -330,8 +336,11 @@ limitations under the License.
}
.mx_EventTile_content .markdown-body code {
// deliberate constants as we're behind an invert filter
background-color: #f8f8f8;
color: #333;
}
.mx_EventTile_copyButton {
position: absolute;
display: inline-block;
@ -343,14 +352,17 @@ limitations under the License.
height: 19px;
background-image: url($copy-button-url);
}
.mx_EventTile_body pre {
position: relative;
border: 1px solid transparent;
}
.mx_EventTile:hover .mx_EventTile_body pre
{
border: 1px solid $primary-hairline-color;
border: 1px solid #e5e5e5; // deliberate constant as we're behind an invert filter
}
.mx_EventTile_body pre:hover .mx_EventTile_copyButton
{
visibility: visible;
@ -364,6 +376,7 @@ limitations under the License.
.mx_EventTile_content .markdown-body h6
{
font-family: inherit ! important;
color: inherit;
}

View file

@ -78,6 +78,16 @@ limitations under the License.
margin-right: 6px;
}
@keyframes visualbell
{
from { background-color: #faa }
to { background-color: $primary-bg-color }
}
.mx_MessageComposer_input_error {
animation: 0.2s visualbell;
}
.mx_MessageComposer_input_empty .public-DraftEditorPlaceholder-root {
display: none;
}
@ -85,6 +95,15 @@ limitations under the License.
.mx_MessageComposer_input .DraftEditor-root {
width: 100%;
flex: 1;
word-break: break-word;
}
.mx_MessageComposer_input .DraftEditor-root .DraftEditor-editorContainer {
/* Ensure mx_UserPill and mx_RoomPill (see _RichText) are not obscured from the top */
padding-top: 2px;
}
.mx_MessageComposer_input .public-DraftEditor-content {
max-height: 120px;
overflow: auto;
}
@ -96,6 +115,12 @@ limitations under the License.
border-left: 4px solid $blockquote-bar-color;
}
.mx_MessageComposer_input pre.public-DraftStyleDefault-pre pre {
background-color: $rte-code-bg-color;
border-radius: 3px;
padding: 10px;
}
.mx_MessageComposer_input textarea {
display: block;
width: 100%;
@ -128,7 +153,8 @@ limitations under the License.
.mx_MessageComposer_upload,
.mx_MessageComposer_hangup,
.mx_MessageComposer_voicecall,
.mx_MessageComposer_videocall {
.mx_MessageComposer_videocall,
.mx_MessageComposer_apps {
/*display: table-cell;*/
/*vertical-align: middle;*/
/*padding-left: 10px;*/
@ -140,7 +166,8 @@ limitations under the License.
.mx_MessageComposer_upload object,
.mx_MessageComposer_hangup object,
.mx_MessageComposer_voicecall object,
.mx_MessageComposer_videocall object {
.mx_MessageComposer_videocall object,
.mx_MessageComposer_apps object {
pointer-events: none;
}
@ -181,11 +208,6 @@ limitations under the License.
cursor: pointer;
}
.mx_MessageComposer_format_button_disabled {
cursor: not-allowed;
opacity: 0.5;
}
.mx_MessageComposer_formatbar_cancel {
margin-right: 22px;
}

View file

@ -1,56 +0,0 @@
/*
Copyright 2015, 2016 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.
*/
.mx_TabCompleteBar {
overflow: hidden;
}
.mx_TabCompleteBar_item {
display: inline-block;
margin-right: 15px;
margin-bottom: 2px;
cursor: pointer;
}
.mx_TabCompleteBar_command {
margin-right: 8px;
background-color: $accent-color;
padding-left: 8px;
padding-right: 8px;
padding-top: 2px;
padding-bottom: 2px;
margin-bottom: 6px;
border-radius: 30px;
position: relative;
top: 1px;
}
.mx_TabCompleteBar_command .mx_TabCompleteBar_text {
opacity: 1.0;
vertical-align: initial;
color: $accent-fg-color;
}
.mx_TabCompleteBar_item img {
margin-right: 8px;
vertical-align: middle;
}
.mx_TabCompleteBar_text {
color: $primary-fg-color;
vertical-align: middle;
opacity: 0.5;
}

View file

@ -19,6 +19,8 @@ $focus-brightness: 125%;
// red warning colour
$warning-color: #ff0064;
$mention-user-pill-bg-color: #ff0064;
$other-user-pill-bg-color: rgba(0, 0, 0, 0.1);
$preview-bar-bg-color: #f7f7f7;
@ -78,6 +80,7 @@ $voip-accept-color: #80f480;
$rte-bg-color: #e9e9e9;
$rte-code-bg-color: rgba(0, 0, 0, 0.04);
$rte-room-pill-color: #aaa;
// ********************

View file

@ -20,6 +20,8 @@ $focus-brightness: 200%;
// red warning colour
$warning-color: #ff0064;
$other-user-pill-bg-color: rgba(255, 255, 255, 0.1);
$preview-bar-bg-color: #333;
// left-panel style muted accent color

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.4.2 (15857) - http://www.bohemiancoding.com/sketch -->
<title>Slice 1</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<path d="M9.74464309,-3.02908503 L8.14106175,-3.02908503 L8.14106175,8.19448443 L-3.03028759,8.19448443 L-3.03028759,9.7978515 L8.14106175,9.7978515 L8.14106175,20.9685098 L9.74464309,20.9685098 L9.74464309,9.7978515 L20.9697124,9.7978515 L20.9697124,8.19448443 L9.74464309,8.19448443 L9.74464309,-3.02908503" id="Fill-108" opacity="0.9" fill="#ff0064" sketch:type="MSShapeGroup" transform="translate(8.969712, 8.969712) rotate(-315.000000) translate(-8.969712, -8.969712) "></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
<g>
<rect x="178.846" y="92.087" transform="matrix(-0.7071 -0.7071 0.7071 -0.7071 224.3476 631.1498)" width="128.085" height="354.049"/>
<path d="M471.723,88.393l-48.115-48.114c-11.723-11.724-31.558-10.896-44.304,1.85l-45.202,45.203l90.569,90.568l45.202-45.202
C482.616,119.952,483.445,100.116,471.723,88.393z"/>
<polygon points="64.021,363.252 32,480 148.737,447.979 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 876 B

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="35px"
height="35px" viewBox="0 0 35 35" enable-background="new 0 0 35 35" xml:space="preserve">
<g id="Layer_1">
<path id="Oval-109-Copy" fill="#76CFA6" enable-background="new " d="M17.5,35C27.165,35,35,27.165,35,17.5S27.165,0,17.5,0
S0,7.835,0,17.5S7.835,35,17.5,35z"/>
<g id="Icon">
<g>
<path fill="none" stroke="#FFFFFF" d="M7.5,12.5h5v-5h-5V12.5z M15,27.5h5v-5h-5V27.5z M7.5,27.5h5v-5h-5V27.5z M7.5,20h5v-5h-5
V20z M15,20h5v-5h-5V20z M22.5,7.5v5h5v-5H22.5z M15,12.5h5v-5h-5V12.5z M22.5,20h5v-5h-5V20z M22.5,27.5h5v-5h-5V27.5z"/>
</g>
</g>
</g>
<g id="Layer_2">
<g id="Icon_1_" opacity="0.15">
<g>
<path fill="none" stroke="#76CFA6" d="M7.5,12.5h5v-5h-5V12.5z M15,27.5h5v-5h-5V27.5z M7.5,27.5h5v-5h-5V27.5z M7.5,20h5v-5h-5
V20z M15,20h5v-5h-5V20z M22.5,7.5v5h5v-5H22.5z M15,12.5h5v-5h-5V12.5z M22.5,20h5v-5h-5V20z M22.5,27.5h5v-5h-5V27.5z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="35px" height="35px" viewBox="0 0 35 35" enable-background="new 0 0 35 35" xml:space="preserve">
<path id="Oval-109-Copy" opacity="0.15" fill="#76CFA6" enable-background="new " d="M17.5,35C27.165,35,35,27.165,35,17.5
S27.165,0,17.5,0S0,7.835,0,17.5S7.835,35,17.5,35z"/>
<g id="Icon">
<g>
<path fill="none" stroke="#76CFA6" d="M7.5,12.5h5v-5h-5V12.5z M15,27.5h5v-5h-5V27.5z M7.5,27.5h5v-5h-5V27.5z M7.5,20h5v-5h-5
V20z M15,20h5v-5h-5V20z M22.5,7.5v5h5v-5H22.5z M15,12.5h5v-5h-5V12.5z M22.5,20h5v-5h-5V20z M22.5,27.5h5v-5h-5V27.5z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 941 B

View file

@ -45,6 +45,12 @@ rageshake.init().then(() => {
console.error("Failed to initialise rageshake: " + err);
});
window.addEventListener('beforeunload', (e) => {
console.log('riot-web closing');
// try to flush the logs to indexeddb
rageshake.flush();
});
// add React and ReactPerf to the global namespace, to make them easier to
// access via the console
@ -59,8 +65,7 @@ var sdk = require("matrix-react-sdk");
const PlatformPeg = require("matrix-react-sdk/lib/PlatformPeg");
sdk.loadSkin(require('../component-index'));
var VectorConferenceHandler = require('../VectorConferenceHandler');
var UpdateChecker = require("./updater");
var q = require('q');
import Promise from 'bluebird';
var request = require('browser-request');
import * as UserSettingsStore from 'matrix-react-sdk/lib/UserSettingsStore';
import * as languageHandler from 'matrix-react-sdk/lib/languageHandler';
@ -182,11 +187,11 @@ var makeRegistrationUrl = function(params) {
window.addEventListener('hashchange', onHashChange);
function getConfig() {
let deferred = q.defer();
function getConfig(configJsonFilename) {
let deferred = Promise.defer();
request(
{ method: "GET", url: "config.json" },
{ method: "GET", url: configJsonFilename },
(err, response, body) => {
if (err || response.status < 200 || response.status >= 300) {
// Lack of a config isn't an error, we should
@ -216,18 +221,16 @@ function getConfig() {
return deferred.promise;
}
function onLoadCompleted() {
function onTokenLoginCompleted() {
// if we did a token login, we're now left with the token, hs and is
// url as query params in the url; a little nasty but let's redirect to
// clear them.
if (window.location.search) {
var parsedUrl = url.parse(window.location.href);
parsedUrl.search = "";
var formatted = url.format(parsedUrl);
console.log("Redirecting to " + formatted + " to drop loginToken " +
"from queryparams");
window.location.href = formatted;
}
var parsedUrl = url.parse(window.location.href);
parsedUrl.search = "";
var formatted = url.format(parsedUrl);
console.log("Redirecting to " + formatted + " to drop loginToken " +
"from queryparams");
window.location.href = formatted;
}
async function loadApp() {
@ -258,10 +261,20 @@ async function loadApp() {
}
}
// Load the config file. First try to load up a domain-specific config of the
// form "config.$domain.json" and if that fails, fall back to config.json.
let configJson;
let configError;
try {
configJson = await getConfig();
try {
configJson = await getConfig(`config.${document.domain}.json`);
// 404s succeed with an empty json config, so check that there are keys
if (Object.keys(configJson).length === 0) {
throw new Error(); // throw to enter the catch
}
} catch (e) {
configJson = await getConfig("config.json");
}
} catch (e) {
configError = e;
}
@ -277,7 +290,9 @@ async function loadApp() {
Unable to load config file: please refresh the page to try again.
</div>, document.getElementById('matrixchat'));
} else if (validBrowser) {
UpdateChecker.start();
const platform = PlatformPeg.get();
platform.startUpdater();
const MatrixChat = sdk.getComponent('structures.MatrixChat');
window.matrixChat = ReactDOM.render(
<MatrixChat
@ -288,9 +303,9 @@ async function loadApp() {
realQueryParams={params}
startingFragmentQueryParams={fragparts.params}
enableGuest={true}
onLoadCompleted={onLoadCompleted}
onTokenLoginCompleted={onTokenLoginCompleted}
initialScreenAfterLogin={getScreenFromLocation(window.location)}
defaultDeviceDisplayName={PlatformPeg.get().getDefaultDeviceDisplayName()}
defaultDeviceDisplayName={platform.getDefaultDeviceDisplayName()}
/>,
document.getElementById('matrixchat')
);

View file

@ -17,14 +17,21 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import VectorBasePlatform from './VectorBasePlatform';
import VectorBasePlatform, {updateCheckStatusEnum} from './VectorBasePlatform';
import dis from 'matrix-react-sdk/lib/dispatcher';
import { _t } from 'matrix-react-sdk/lib/languageHandler';
import q from 'q';
import electron, {remote, ipcRenderer} from 'electron';
import Promise from 'bluebird';
import {remote, ipcRenderer} from 'electron';
import rageshake from '../rageshake';
remote.autoUpdater.on('update-downloaded', onUpdateDownloaded);
// try to flush the rageshake logs to indexeddb before quit.
ipcRenderer.on('before-quit', function () {
console.log('riot-desktop closing');
rageshake.flush();
});
function onUpdateDownloaded(ev: Event, releaseNotes: string, ver: string, date: Date, updateURL: string) {
dis.dispatch({
action: 'new_version',
@ -62,10 +69,42 @@ function _onAction(payload: Object) {
}
}
function getUpdateCheckStatus(status) {
if (status === true) {
return { status: updateCheckStatusEnum.DOWNLOADING };
} else if (status === false) {
return { status: updateCheckStatusEnum.NOTAVAILABLE };
} else {
return {
status: updateCheckStatusEnum.ERROR,
detail: status,
};
}
}
export default class ElectronPlatform extends VectorBasePlatform {
constructor() {
super();
dis.register(_onAction);
this.updatable = Boolean(remote.autoUpdater.getFeedURL());
/*
IPC Call `check_updates` returns:
true if there is an update available
false if there is not
or the error if one is encountered
*/
ipcRenderer.on('check_updates', (event, status) => {
if (!this.showUpdateCheck) return;
dis.dispatch({
action: 'check_updates',
value: getUpdateCheckStatus(status),
});
this.showUpdateCheck = false;
});
this.startUpdateCheck = this.startUpdateCheck.bind(this);
this.stopUpdateCheck = this.stopUpdateCheck.bind(this);
}
getHumanReadableName(): string {
@ -134,20 +173,21 @@ export default class ElectronPlatform extends VectorBasePlatform {
}
getAppVersion(): Promise<string> {
return q(remote.app.getVersion());
return Promise.resolve(remote.app.getVersion());
}
pollForUpdate() {
// In electron we control the update process ourselves, since
// it needs to run in the main process, so we just run the timer
// loop in the main electron process instead.
startUpdateCheck() {
if (this.showUpdateCheck) return;
super.startUpdateCheck();
ipcRenderer.send('check_updates');
}
installUpdate() {
// IPC to the main process to install the update, since quitAndInstall
// doesn't fire the before-quit event so the main process needs to know
// it should exit.
electron.ipcRenderer.send('install_update');
ipcRenderer.send('install_update');
}
getDefaultDeviceDisplayName(): string {
@ -161,7 +201,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
isElectron(): boolean { return true; }
requestNotificationPermission(): Promise<string> {
return q('granted');
return Promise.resolve('granted');
}
reload() {

View file

@ -19,9 +19,18 @@ limitations under the License.
import BasePlatform from 'matrix-react-sdk/lib/BasePlatform';
import { _t } from 'matrix-react-sdk/lib/languageHandler';
import dis from 'matrix-react-sdk/lib/dispatcher';
import Favico from 'favico.js';
export const updateCheckStatusEnum = {
CHECKING: 'CHECKING',
ERROR: 'ERROR',
NOTAVAILABLE: 'NOTAVAILABLE',
DOWNLOADING: 'DOWNLOADING',
READY: 'READY',
};
/**
* Vector-specific extensions to the BasePlatform template
*/
@ -34,7 +43,12 @@ export default class VectorBasePlatform extends BasePlatform {
// and we set the state each time, even if the value hasn't changed,
// so we'd need to fix that if enabling the animation.
this.favicon = new Favico({animation: 'none'});
this.showUpdateCheck = false;
this._updateFavicon();
this.updatable = true;
this.startUpdateCheck = this.startUpdateCheck.bind(this);
this.stopUpdateCheck = this.stopUpdateCheck.bind(this);
}
getHumanReadableName(): string {
@ -75,12 +89,32 @@ export default class VectorBasePlatform extends BasePlatform {
}
/**
* Check for the availability of an update to the version of the
* app that's currently running.
* If an update is available, this function should dispatch the
* 'new_version' action.
* Begin update polling, if applicable
*/
pollForUpdate() {
startUpdater() {
}
/**
* Whether we can call checkForUpdate on this platform build
*/
canSelfUpdate(): boolean {
return this.updatable;
}
startUpdateCheck() {
this.showUpdateCheck = true;
dis.dispatch({
action: 'check_updates',
value: { status: updateCheckStatusEnum.CHECKING },
});
}
stopUpdateCheck() {
this.showUpdateCheck = false;
dis.dispatch({
action: 'check_updates',
value: false,
})
}
/**

View file

@ -17,19 +17,24 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import VectorBasePlatform from './VectorBasePlatform';
import VectorBasePlatform, {updateCheckStatusEnum} from './VectorBasePlatform';
import request from 'browser-request';
import dis from 'matrix-react-sdk/lib/dispatcher.js';
import { _t } from 'matrix-react-sdk/lib/languageHandler';
import q from 'q';
import Promise from 'bluebird';
import url from 'url';
import UAParser from 'ua-parser-js';
var POKE_RATE_MS = 10 * 60 * 1000; // 10 min
export default class WebPlatform extends VectorBasePlatform {
constructor() {
super();
this.runningVersion = null;
this.startUpdateCheck = this.startUpdateCheck.bind(this);
this.stopUpdateCheck = this.stopUpdateCheck.bind(this);
}
getHumanReadableName(): string {
@ -63,7 +68,7 @@ export default class WebPlatform extends VectorBasePlatform {
// annoyingly, the latest spec says this returns a
// promise, but this is only supported in Chrome 46
// and Firefox 47, so adapt the callback API.
const defer = q.defer();
const defer = Promise.defer();
global.Notification.requestPermission((result) => {
defer.resolve(result);
});
@ -98,7 +103,7 @@ export default class WebPlatform extends VectorBasePlatform {
}
_getVersion(): Promise<string> {
const deferred = q.defer();
const deferred = Promise.defer();
// We add a cachebuster to the request to make sure that we know about
// the most recent version on the origin server. That might not
@ -127,13 +132,18 @@ export default class WebPlatform extends VectorBasePlatform {
getAppVersion(): Promise<string> {
if (this.runningVersion !== null) {
return q(this.runningVersion);
return Promise.resolve(this.runningVersion);
}
return this._getVersion();
}
startUpdater() {
this.pollForUpdate();
setInterval(this.pollForUpdate.bind(this), POKE_RATE_MS);
}
pollForUpdate() {
this._getVersion().done((ver) => {
return this._getVersion().then((ver) => {
if (this.runningVersion === null) {
this.runningVersion = ver;
} else if (this.runningVersion !== ver) {
@ -142,9 +152,29 @@ export default class WebPlatform extends VectorBasePlatform {
currentVersion: this.runningVersion,
newVersion: ver,
});
// Return to skip a MatrixChat state update
return;
}
return { status: updateCheckStatusEnum.NOTAVAILABLE };
}, (err) => {
console.error("Failed to poll for update", err);
return {
status: updateCheckStatusEnum.ERROR,
detail: err.message || err.status ? err.status.toString() : 'Unknown Error',
};
});
}
startUpdateCheck() {
if (this.showUpdateCheck) return;
super.startUpdateCheck();
this.pollForUpdate().then((updateState) => {
if (!this.showUpdateCheck) return;
if (!updateState) return;
dis.dispatch({
action: 'check_updates',
value: updateState,
});
});
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import q from "q";
import Promise from 'bluebird';
// This module contains all the code needed to log the console, persist it to
// disk and submit bug reports. Rationale is as follows:
@ -116,7 +116,7 @@ class IndexedDBLogStore {
*/
connect() {
let req = this.indexedDB.open("logs");
return q.Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
req.onsuccess = (event) => {
this.db = event.target.result;
// Periodically flush logs to local storage / indexeddb
@ -193,7 +193,7 @@ class IndexedDBLogStore {
}
// there is no flush promise or there was but it has finished, so do
// a brand new one, destroying the chain which may have been built up.
this.flushPromise = q.Promise((resolve, reject) => {
this.flushPromise = new Promise((resolve, reject) => {
if (!this.db) {
// not connected yet or user rejected access for us to r/w to
// the db.
@ -277,7 +277,7 @@ class IndexedDBLogStore {
}
function deleteLogs(id) {
return q.Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
const txn = db.transaction(
["logs", "logslastmod"], "readwrite"
);
@ -375,7 +375,7 @@ class IndexedDBLogStore {
*/
function selectQuery(store, keyRange, resultMapper) {
const query = store.openCursor(keyRange);
return q.Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
let results = [];
query.onerror = (event) => {
reject(new Error("Query failed: " + event.target.errorCode));
@ -410,8 +410,16 @@ module.exports = {
}
logger = new ConsoleLogger();
logger.monkeyPatch(window.console);
if (window.indexedDB) {
store = new IndexedDBLogStore(window.indexedDB, logger);
// just *accessing* indexedDB throws an exception in firefox with
// indexeddb disabled.
let indexedDB;
try {
indexedDB = window.indexedDB;
} catch(e) {}
if (indexedDB) {
store = new IndexedDBLogStore(indexedDB, logger);
initPromise = store.connect();
return initPromise;
}
@ -419,6 +427,13 @@ module.exports = {
return initPromise;
},
flush: function() {
if (!store) {
return;
}
store.flush();
},
/**
* Clean up old logs.
* @return Promise Resolves if cleaned logs.

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import pako from 'pako';
import q from "q";
import Promise from 'bluebird';
import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg';
import PlatformPeg from 'matrix-react-sdk/lib/PlatformPeg';
@ -100,7 +100,7 @@ export default async function sendBugReport(bugReportEndpoint, opts) {
}
function _submitReport(endpoint, body, progressCallback) {
const deferred = q.defer();
const deferred = Promise.defer();
const req = new XMLHttpRequest();
req.open("POST", endpoint);

View file

@ -33,10 +33,10 @@ var React = require('react');
var ReactDOM = require('react-dom');
var ReactTestUtils = require('react-addons-test-utils');
var expect = require('expect');
var q = require('q');
import Promise from 'bluebird';
var test_utils = require('../test-utils');
var MockHttpBackend = require('../mock-request');
var MockHttpBackend = require('matrix-mock-request');
var HS_URL='http://localhost';
var IS_URL='http://localhost';
@ -100,29 +100,19 @@ describe('joining a room', function () {
// wait for /sync to happen. This may take some time, as the client
// has to initialise indexeddb.
console.log("waiting for /sync");
let syncDone = false;
httpBackend.when('GET', '/sync')
.check((r) => {syncDone = true;})
.respond(200, {});
function awaitSync(attempts) {
if (syncDone) {
return q();
}
if (!attempts) {
throw new Error("Gave up waiting for /sync")
}
return httpBackend.flush().then(() => awaitSync(attempts-1));
}
return awaitSync(10).then(() => {
return httpBackend.flushAllExpected({
timeout: 1000,
}).then(() => {
// wait for the directory requests
httpBackend.when('POST', '/publicRooms').respond(200, {chunk: []});
httpBackend.when('GET', '/thirdparty/protocols').respond(200, {});
return q.all([
httpBackend.flush('/thirdparty/protocols'),
httpBackend.flush('/publicRooms'),
]);
return httpBackend.flushAllExpected();
}).then(() => {
console.log(`${Date.now()} App made requests for directory view; switching to a room.`);
var roomDir = ReactTestUtils.findRenderedComponentWithType(
matrixChat, RoomDirectory);
@ -139,19 +129,17 @@ describe('joining a room', function () {
httpBackend.when('GET', '/rooms/'+encodeURIComponent(ROOM_ID)+"/initialSync")
.respond(401, {errcode: 'M_GUEST_ACCESS_FORBIDDEN'});
return q.all([
httpBackend.flush('/directory/room/'+encodeURIComponent(ROOM_ALIAS), 1, 200),
httpBackend.flush('/rooms/'+encodeURIComponent(ROOM_ID)+"/initialSync", 1, 200),
]);
return httpBackend.flushAllExpected();
}).then(() => {
httpBackend.verifyNoOutstandingExpectation();
console.log(`${Date.now()} App made room preview request`);
return q.delay(1);
}).then(() => {
// we should now have a roomview, with a preview bar
// we should now have a roomview
roomView = ReactTestUtils.findRenderedComponentWithType(
matrixChat, RoomView);
// the preview bar may take a tick to be displayed
return Promise.delay(1);
}).then(() => {
const previewBar = ReactTestUtils.findRenderedComponentWithType(
roomView, RoomPreviewBar);
@ -164,14 +152,14 @@ describe('joining a room', function () {
.respond(200, {room_id: ROOM_ID});
}).then(() => {
// wait for the join request to be made
return q.delay(1);
return Promise.delay(1);
}).then(() => {
// and again, because the state update has to go to the store and
// then one dispatch within the store, then to the view
// XXX: This is *super flaky*: a better way would be to declare
// that we expect a certain state transition to happen, then wait
// for that transition to occur.
return q.delay(1);
return Promise.delay(1);
}).then(() => {
// the roomview should now be loading
expect(roomView.state.room).toBe(null);
@ -186,7 +174,7 @@ describe('joining a room', function () {
}).then(() => {
httpBackend.verifyNoOutstandingExpectation();
return q.delay(1);
return Promise.delay(1);
}).then(() => {
// We've joined, expect this to false
expect(roomView.state.joining).toBe(false);

View file

@ -22,16 +22,19 @@ import React from 'react';
import ReactDOM from 'react-dom';
import ReactTestUtils from 'react-addons-test-utils';
import expect from 'expect';
import q from 'q';
import Promise from 'bluebird';
import MatrixReactTestUtils from 'matrix-react-test-utils';
import jssdk from 'matrix-js-sdk';
import sdk from 'matrix-react-sdk';
import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg';
import * as languageHandler from 'matrix-react-sdk/lib/languageHandler';
import {VIEWS} from 'matrix-react-sdk/lib/components/structures/MatrixChat';
import dis from 'matrix-react-sdk/lib/dispatcher';
import * as test_utils from '../test-utils';
import MockHttpBackend from '../mock-request';
import MockHttpBackend from 'matrix-mock-request';
import {parseQs, parseQsFromFragment} from '../../src/vector/url_utils';
var DEFAULT_HS_URL='http://my_server';
@ -47,8 +50,8 @@ describe('loading:', function () {
// the mounted MatrixChat
let matrixChat;
// a promise which resolves when the MatrixChat calls onLoadCompleted
let loadCompletePromise;
// a promise which resolves when the MatrixChat calls onTokenLoginCompleted
let tokenLoginCompletePromise;
beforeEach(function() {
test_utils.beforeEach(this);
@ -68,7 +71,8 @@ describe('loading:', function () {
});
});
afterEach(async function() {
afterEach(async function () {
console.log(`${Date.now()}: loading: afterEach`);
if (parentDiv) {
ReactDOM.unmountComponentAtNode(parentDiv);
parentDiv.remove();
@ -78,9 +82,15 @@ describe('loading:', function () {
// unmounting should have cleared the MatrixClientPeg
expect(MatrixClientPeg.get()).toBe(null);
// chrome seems to take *ages* to delete the indexeddbs.
this.timeout(10000);
// clear the indexeddbs so we can start from a clean slate next time.
await test_utils.deleteIndexedDB('matrix-js-sdk:crypto');
await test_utils.deleteIndexedDB('matrix-js-sdk:riot-web-sync');
await Promise.all([
test_utils.deleteIndexedDB('matrix-js-sdk:crypto'),
test_utils.deleteIndexedDB('matrix-js-sdk:riot-web-sync'),
]);
console.log(`${Date.now()}: loading: afterEach complete`);
});
/* simulate the load process done by index.js
@ -99,8 +109,8 @@ describe('loading:', function () {
toString: function() { return this.search + this.hash; },
};
let loadCompleteDefer = q.defer();
loadCompletePromise = loadCompleteDefer.promise;
let tokenLoginCompleteDefer = Promise.defer();
tokenLoginCompletePromise = tokenLoginCompleteDefer.promise;
function onNewScreen(screen) {
console.log(Date.now() + " newscreen "+screen);
@ -135,7 +145,7 @@ describe('loading:', function () {
realQueryParams={params}
startingFragmentQueryParams={fragParts.params}
enableGuest={true}
onLoadCompleted={loadCompleteDefer.resolve}
onTokenLoginCompleted={() => tokenLoginCompleteDefer.resolve()}
initialScreenAfterLogin={getScreenFromLocation(windowLocation)}
makeRegistrationUrl={() => {throw new Error('Not implemented');}}
/>, parentDiv
@ -153,8 +163,8 @@ describe('loading:', function () {
.check((r) => {syncRequest = r;})
.respond(200, response);
console.log("waiting for /sync");
for (let attempts = 10; attempts > 0; attempts--) {
console.log(Date.now() + " waiting for /sync");
if (syncRequest) {
return syncRequest;
}
@ -167,7 +177,7 @@ describe('loading:', function () {
it('gives a login panel by default', function (done) {
loadApp();
q.delay(1).then(() => {
Promise.delay(1).then(() => {
// at this point, we're trying to do a guest registration;
// we expect a spinner
assertAtLoadingSpinner(matrixChat);
@ -179,12 +189,9 @@ describe('loading:', function () {
return httpBackend.flush();
}).then(() => {
// Wait for another trip around the event loop for the UI to update
return q.delay(1);
return awaitLoginComponent(matrixChat);
}).then(() => {
// we expect a single <Login> component following session load
ReactTestUtils.findRenderedComponentWithType(
matrixChat, sdk.getComponent('structures.login.Login'));
expect(windowLocation.hash).toEqual("");
expect(windowLocation.hash).toEqual("#/login");
}).done(done, done);
});
@ -193,7 +200,7 @@ describe('loading:', function () {
uriFragment: "#/room/!room:id",
});
q.delay(1).then(() => {
Promise.delay(1).then(() => {
// at this point, we're trying to do a guest registration;
// we expect a spinner
assertAtLoadingSpinner(matrixChat);
@ -205,7 +212,7 @@ describe('loading:', function () {
return httpBackend.flush();
}).then(() => {
// Wait for another trip around the event loop for the UI to update
return q.delay(1);
return Promise.delay(10);
}).then(() => {
return completeLogin(matrixChat);
}).then(() => {
@ -228,7 +235,7 @@ describe('loading:', function () {
uriFragment: "#/login",
});
return q.delay(100).then(() => {
return awaitLoginComponent(matrixChat).then(() => {
// we expect a single <Login> component
ReactTestUtils.findRenderedComponentWithType(
matrixChat, sdk.getComponent('structures.login.Login'));
@ -335,7 +342,7 @@ describe('loading:', function () {
},
});
return q.delay(1).then(() => {
return Promise.delay(1).then(() => {
// we expect a loading spinner while we log into the RTS
assertAtLoadingSpinner(matrixChat);
@ -360,6 +367,9 @@ describe('loading:', function () {
loadApp({
uriFragment: "#/login",
});
// give the UI a chance to display
return awaitLoginComponent(matrixChat);
});
it('shows a login view', function() {
@ -396,7 +406,7 @@ describe('loading:', function () {
it('shows a home page by default', function (done) {
loadApp();
q.delay(1).then(() => {
Promise.delay(1).then(() => {
// at this point, we're trying to do a guest registration;
// we expect a spinner
assertAtLoadingSpinner(matrixChat);
@ -429,7 +439,7 @@ describe('loading:', function () {
loadApp();
q.delay(1).then(() => {
Promise.delay(1).then(() => {
// at this point, we're trying to do a guest registration;
// we expect a spinner
assertAtLoadingSpinner(matrixChat);
@ -464,7 +474,7 @@ describe('loading:', function () {
loadApp({
uriFragment: "#/room/!room:id"
});
q.delay(1).then(() => {
Promise.delay(1).then(() => {
// at this point, we're trying to do a guest registration;
// we expect a spinner
assertAtLoadingSpinner(matrixChat);
@ -489,6 +499,76 @@ describe('loading:', function () {
expect(windowLocation.hash).toEqual("#/room/!room:id");
}).done(done, done);
});
describe('Login as user', function() {
beforeEach(function() {
// first we have to load the homepage
loadApp();
httpBackend.when('POST', '/register').check(function(req) {
expect(req.queryParams.kind).toEqual('guest');
}).respond(200, {
user_id: "@guest:localhost",
access_token: "secret_token",
});
return httpBackend.flush().then(() => {
return awaitSyncingSpinner(matrixChat);
}).then(() => {
// we got a sync spinner - let the sync complete
return expectAndAwaitSync();
}).then(() => {
// once the sync completes, we should have a home page
ReactTestUtils.findRenderedComponentWithType(
matrixChat, sdk.getComponent('structures.HomePage'));
// we simulate a click on the 'login' button by firing off
// the relevant dispatch.
//
// XXX: is it an anti-pattern to access the react-sdk's
// dispatcher in this way? Is it better to find the login
// button and simulate a click? (we might have to arrange
// for it to be shown - it's not always, due to the
// collapsing left panel
dis.dispatch({ action: 'start_login' });
return awaitLoginComponent(matrixChat);
});
});
it('should give us a login page', function() {
expect(windowLocation.hash).toEqual("#/login");
// we expect a single <Login> component
ReactTestUtils.findRenderedComponentWithType(
matrixChat, sdk.getComponent('structures.login.Login')
);
});
it('should allow us to return to the app', function() {
const login = ReactTestUtils.findRenderedComponentWithType(
matrixChat, sdk.getComponent('structures.login.Login')
);
const linkText = 'Return to app';
const returnToApp = ReactTestUtils.scryRenderedDOMComponentsWithTag(
login, 'a').find((e) => e.innerText === linkText);
if (!returnToApp) {
throw new Error(`Couldn't find '${linkText}' link`);
}
ReactTestUtils.Simulate.click(returnToApp);
return Promise.delay(1).then(() => {
// we should be straight back into the home page
ReactTestUtils.findRenderedComponentWithType(
matrixChat, sdk.getComponent('structures.HomePage'));
});
});
});
});
describe('Token login:', function() {
@ -497,7 +577,7 @@ describe('loading:', function () {
queryString: "?loginToken=secretToken&homeserver=https%3A%2F%2Fhomeserver&identityServer=https%3A%2F%2Fidserver",
});
q.delay(1).then(() => {
Promise.delay(1).then(() => {
// we expect a spinner while we're logging in
assertAtLoadingSpinner(matrixChat);
@ -513,12 +593,12 @@ describe('loading:', function () {
return httpBackend.flush();
}).then(() => {
// at this point, MatrixChat should fire onLoadCompleted, which
// at this point, MatrixChat should fire onTokenLoginCompleted, which
// makes index.js reload the app. We're not going to attempt to
// simulate the reload - just check that things are left in the
// right state for the reloaded app.
return loadCompletePromise;
return tokenLoginCompletePromise;
}).then(() => {
// check that the localstorage has been set up in such a way that
// the reloaded app can pick up where we leave off.
@ -530,7 +610,6 @@ describe('loading:', function () {
});
});
// check that we have a Login component, send a 'user:pass' login,
// and await the HTTP requests.
function completeLogin(matrixChat) {
@ -539,7 +618,6 @@ describe('loading:', function () {
matrixChat, sdk.getComponent('structures.login.Login'));
httpBackend.when('POST', '/login').check(function(req) {
console.log(req);
expect(req.data.type).toEqual('m.login.password');
expect(req.data.identifier.type).toEqual('m.id.user');
expect(req.data.identifier.user).toEqual('user');
@ -553,7 +631,7 @@ describe('loading:', function () {
return httpBackend.flush().then(() => {
// Wait for another trip around the event loop for the UI to update
return q.delay(1);
return Promise.delay(1);
}).then(() => {
// we expect a spinner
ReactTestUtils.findRenderedComponentWithType(
@ -589,7 +667,8 @@ function awaitSyncingSpinner(matrixChat, retryLimit, retryCount) {
retryCount = 0;
}
if (matrixChat.state.loading || matrixChat.state.loggingIn) {
if (matrixChat.state.view === VIEWS.LOADING ||
matrixChat.state.view === VIEWS.LOGGING_IN) {
console.log(Date.now() + " Awaiting sync spinner: still loading.");
if (retryCount >= retryLimit) {
throw new Error("MatrixChat still not loaded after " +
@ -597,7 +676,7 @@ function awaitSyncingSpinner(matrixChat, retryLimit, retryCount) {
}
// loading can take quite a long time, because we delete the
// indexedDB store.
return q.delay(5).then(() => {
return Promise.delay(5).then(() => {
return awaitSyncingSpinner(matrixChat, retryLimit, retryCount + 1);
});
}
@ -606,7 +685,7 @@ function awaitSyncingSpinner(matrixChat, retryLimit, retryCount) {
// state looks good, check the rendered output
assertAtSyncingSpinner(matrixChat);
return q();
return Promise.resolve();
}
function assertAtSyncingSpinner(matrixChat) {
@ -628,14 +707,13 @@ function awaitRoomView(matrixChat, retryLimit, retryCount) {
retryCount = 0;
}
if (matrixChat.state.loading ||
!(matrixChat.state.loggedIn && matrixChat.state.ready)) {
if (matrixChat.state.view !== VIEWS.LOGGED_IN || !matrixChat.state.ready) {
console.log(Date.now() + " Awaiting room view: not ready yet.");
if (retryCount >= retryLimit) {
throw new Error("MatrixChat still not ready after " +
retryCount + " tries");
}
return q.delay(0).then(() => {
return Promise.delay(0).then(() => {
return awaitRoomView(matrixChat, retryLimit, retryCount + 1);
});
}
@ -645,5 +723,11 @@ function awaitRoomView(matrixChat, retryLimit, retryCount) {
// state looks good, check the rendered output
ReactTestUtils.findRenderedComponentWithType(
matrixChat, sdk.getComponent('structures.RoomView'));
return q();
return Promise.resolve();
}
function awaitLoginComponent(matrixChat, attempts) {
return MatrixReactTestUtils.waitForRenderedComponentWithType(
matrixChat, sdk.getComponent('structures.login.Login'), attempts,
);
}

View file

@ -1,336 +0,0 @@
"use strict";
const q = require("q");
import expect from 'expect';
/**
* Construct a mock HTTP backend, heavily inspired by Angular.js.
* @constructor
*/
function HttpBackend() {
this.requests = [];
this.expectedRequests = [];
const self = this;
// the request function dependency that the SDK needs.
this.requestFn = function(opts, callback) {
const req = new Request(opts, callback);
console.log(`${Date.now()} HTTP backend received request: ${req}`);
self.requests.push(req);
const abort = function() {
const idx = self.requests.indexOf(req);
if (idx >= 0) {
console.log("Aborting HTTP request: %s %s", opts.method,
opts.uri);
self.requests.splice(idx, 1);
req.callback("aborted");
}
};
return {
abort: abort,
};
};
// very simplistic mapping from the whatwg fetch interface onto the request
// interface, so we can use the same mock backend for both.
this.fetchFn = function(input, init) {
init = init || {};
const requestOpts = {
uri: input,
method: init.method || 'GET',
body: init.body,
};
return new Promise((resolve, reject) => {
function callback(err, response, body) {
if (err) {
reject(err);
}
resolve({
ok: response.statusCode >= 200 && response.statusCode < 300,
json: () => body,
});
};
const req = new Request(requestOpts, callback);
console.log(`HTTP backend received request: ${req}`);
self.requests.push(req);
});
};
}
HttpBackend.prototype = {
/**
* Respond to all of the requests (flush the queue).
* @param {string} path The path to flush (optional) default: all.
* @param {integer} numToFlush The number of things to flush (optional), default: all.
* @param {integer=} waitTime The time (in ms) to wait for a request to happen.
* default: 100
*
* @return {Promise} resolves when there is nothing left to flush, with the
* number of requests flushed
*/
flush: function(path, numToFlush, waitTime) {
const defer = q.defer();
const self = this;
let flushed = 0;
if (waitTime === undefined) {
waitTime = 100;
}
function log(msg) {
console.log(`${Date.now()} flush[${path || ''}]: ${msg}`);
}
log("HTTP backend flushing... (path=" + path
+ " numToFlush=" + numToFlush
+ " waitTime=" + waitTime
+ ")",
);
const endTime = waitTime + Date.now();
const tryFlush = function() {
// if there's more real requests and more expected requests, flush 'em.
log(` trying to flush => reqs=[${self.requests}] ` +
`expected=[${self.expectedRequests}]`,
);
if (self._takeFromQueue(path)) {
// try again on the next tick.
flushed += 1;
if (numToFlush && flushed === numToFlush) {
log(`Flushed assigned amount: ${numToFlush}`);
defer.resolve(flushed);
} else {
log(` flushed. Trying for more.`);
setTimeout(tryFlush, 0);
}
} else if (flushed === 0 && Date.now() < endTime) {
// we may not have made the request yet, wait a generous amount of
// time before giving up.
log(` nothing to flush yet; waiting for requests.`);
setTimeout(tryFlush, 5);
} else {
if (flushed === 0) {
log("nothing to flush; giving up");
} else {
log(`no more flushes after flushing ${flushed} requests`);
}
defer.resolve(flushed);
}
};
setTimeout(tryFlush, 0);
return defer.promise;
},
/**
* Attempts to resolve requests/expected requests.
* @param {string} path The path to flush (optional) default: all.
* @return {boolean} true if something was resolved.
*/
_takeFromQueue: function(path) {
let req = null;
let i;
let j;
let matchingReq = null;
let expectedReq = null;
let testResponse = null;
for (i = 0; i < this.requests.length; i++) {
req = this.requests[i];
for (j = 0; j < this.expectedRequests.length; j++) {
expectedReq = this.expectedRequests[j];
if (path && path !== expectedReq.path) {
continue;
}
if (expectedReq.method === req.method &&
req.path.indexOf(expectedReq.path) !== -1) {
if (!expectedReq.data || (JSON.stringify(expectedReq.data) ===
JSON.stringify(req.data))) {
matchingReq = expectedReq;
this.expectedRequests.splice(j, 1);
break;
}
}
}
if (matchingReq) {
// remove from request queue
this.requests.splice(i, 1);
i--;
for (j = 0; j < matchingReq.checks.length; j++) {
matchingReq.checks[j](req);
}
testResponse = matchingReq.response;
console.log(`${Date.now()} responding to ${matchingReq.path}`);
let body = testResponse.body;
if (Object.prototype.toString.call(body) == "[object Function]") {
body = body(req.path, req.data);
}
req.callback(
testResponse.err, testResponse.response, body,
);
matchingReq = null;
}
}
if (testResponse) { // flushed something
return true;
}
return false;
},
/**
* Makes sure that the SDK hasn't sent any more requests to the backend.
*/
verifyNoOutstandingRequests: function() {
const firstOutstandingReq = this.requests[0] || {};
expect(this.requests.length).toEqual(0,
"Expected no more HTTP requests but received request to " +
firstOutstandingReq.path,
);
},
/**
* Makes sure that the test doesn't have any unresolved requests.
*/
verifyNoOutstandingExpectation: function() {
const firstOutstandingExpectation = this.expectedRequests[0] || {};
expect(this.expectedRequests.length).toEqual(0,
"Expected to see HTTP request for " + firstOutstandingExpectation.path,
);
},
/**
* Create an expected request.
* @param {string} method The HTTP method
* @param {string} path The path (which can be partial)
* @param {Object} data The expected data.
* @return {Request} An expected request.
*/
when: function(method, path, data) {
const pendingReq = new ExpectedRequest(method, path, data);
this.expectedRequests.push(pendingReq);
return pendingReq;
},
};
/**
* Represents the expectation of a request.
*
* <p>Includes the conditions to be matched against, the checks to be made,
* and the response to be returned.
*
* @constructor
* @param {string} method
* @param {string} path
* @param {object?} data
*/
function ExpectedRequest(method, path, data) {
this.method = method;
this.path = path;
this.data = data;
this.response = null;
this.checks = [];
}
ExpectedRequest.prototype = {
toString: function() {
return this.method + " " + this.path
},
/**
* Execute a check when this request has been satisfied.
* @param {Function} fn The function to execute.
* @return {Request} for chaining calls.
*/
check: function(fn) {
this.checks.push(fn);
return this;
},
/**
* Respond with the given data when this request is satisfied.
* @param {Number} code The HTTP status code.
* @param {Object|Function} data The HTTP JSON body. If this is a function,
* it will be invoked when the JSON body is required (which should be returned).
*/
respond: function(code, data) {
this.response = {
response: {
statusCode: code,
headers: {},
},
body: data,
err: null,
};
},
/**
* Fail with an Error when this request is satisfied.
* @param {Number} code The HTTP status code.
* @param {Error} err The error to throw (e.g. Network Error)
*/
fail: function(code, err) {
this.response = {
response: {
statusCode: code,
headers: {},
},
body: null,
err: err,
};
},
};
/**
* Represents a request made by the app.
*
* @constructor
* @param {object} opts opts passed to request()
* @param {function} callback
*/
function Request(opts, callback) {
this.opts = opts;
this.callback = callback;
Object.defineProperty(this, 'method', {
get: function() {
return opts.method;
},
});
Object.defineProperty(this, 'path', {
get: function() {
return opts.uri;
},
});
Object.defineProperty(this, 'data', {
get: function() {
return opts.body;
},
});
Object.defineProperty(this, 'queryParams', {
get: function() {
return opts.qs;
},
});
Object.defineProperty(this, 'headers', {
get: function() {
return opts.headers || {};
},
});
}
Request.prototype = {
toString: function() {
return this.method + " " + this.path;
},
};
/**
* The HttpBackend class.
*/
module.exports = HttpBackend;

View file

@ -1,6 +1,6 @@
"use strict";
var q = require('q');
import Promise from 'bluebird';
/**
* Perform common actions before each test case, e.g. printing the test case
@ -28,28 +28,33 @@ export function browserSupportsWebRTC() {
}
export function deleteIndexedDB(dbName) {
return new q.Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
if (!window.indexedDB) {
resolve();
return;
}
console.log(`Removing indexeddb instance: ${dbName}`);
const startTime = Date.now();
console.log(`${startTime}: Removing indexeddb instance: ${dbName}`);
const req = window.indexedDB.deleteDatabase(dbName);
req.onblocked = () => {
console.log(`can't yet delete indexeddb because it is open elsewhere`);
console.log(`${Date.now()}: can't yet delete indexeddb ${dbName} because it is open elsewhere`);
};
req.onerror = (ev) => {
reject(new Error(
"unable to delete indexeddb: " + ev.target.error,
`${Date.now()}: unable to delete indexeddb ${dbName}: ${ev.target.error}`,
));
};
req.onsuccess = () => {
console.log(`Removed indexeddb instance: ${dbName}`);
const now = Date.now();
console.log(`${now}: Removed indexeddb instance: ${dbName} in ${now-startTime} ms`);
resolve();
};
}).catch((e) => {
console.error(`${Date.now()}: Error removing indexeddb instance ${dbName}: ${e}`);
throw e;
});
}