Merge branch 'develop' into kegan/indexeddb
This commit is contained in:
commit
f628ee2ef0
50 changed files with 1288 additions and 368 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
|
@ -53,7 +53,13 @@ module.exports = {
|
|||
* things that are errors in the js-sdk config that the current
|
||||
* code does not adhere to, turned down to warn
|
||||
*/
|
||||
"max-len": ["warn"],
|
||||
"max-len": ["warn", {
|
||||
// apparently people believe the length limit shouldn't apply
|
||||
// to JSX.
|
||||
ignorePattern: '^\\s*<',
|
||||
ignoreComments: true,
|
||||
code: 90,
|
||||
}],
|
||||
"valid-jsdoc": ["warn"],
|
||||
"new-cap": ["warn"],
|
||||
"key-spacing": ["warn"],
|
||||
|
|
138
CHANGELOG.md
138
CHANGELOG.md
|
@ -1,3 +1,141 @@
|
|||
Changes in [0.8.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.6) (2017-02-04)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.6-rc.3...v0.8.6)
|
||||
|
||||
* Update to matrix-js-sdk 0.7.5 (no changes from 0.7.5-rc.3)
|
||||
|
||||
Changes in [0.8.6-rc.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.6-rc.3) (2017-02-03)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.6-rc.2...v0.8.6-rc.3)
|
||||
|
||||
* Update to matrix-js-sdk 0.7.5-rc.3
|
||||
* Fix deviceverifybuttons
|
||||
[5fd7410](https://github.com/matrix-org/matrix-react-sdk/commit/827b5a6811ac6b9d1f9a3002a94f9f6ac3f1d49c)
|
||||
|
||||
|
||||
Changes in [0.8.6-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.6-rc.2) (2017-02-03)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.6-rc.1...v0.8.6-rc.2)
|
||||
|
||||
* Update to new matrix-js-sdk to get support for new device change notifications interface
|
||||
|
||||
|
||||
Changes in [0.8.6-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.6-rc.1) (2017-02-03)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.5...v0.8.6-rc.1)
|
||||
|
||||
* Fix timeline & notifs panel spuriously being empty
|
||||
[\#675](https://github.com/matrix-org/matrix-react-sdk/pull/675)
|
||||
* UI for blacklisting unverified devices per-room & globally
|
||||
[\#636](https://github.com/matrix-org/matrix-react-sdk/pull/636)
|
||||
* Show better error message in statusbar after UnkDevDialog
|
||||
[\#674](https://github.com/matrix-org/matrix-react-sdk/pull/674)
|
||||
* Make default avatars clickable
|
||||
[\#673](https://github.com/matrix-org/matrix-react-sdk/pull/673)
|
||||
* Fix one read receipt randomly not appearing
|
||||
[\#672](https://github.com/matrix-org/matrix-react-sdk/pull/672)
|
||||
* very barebones support for warning users when rooms contain unknown devices
|
||||
[\#635](https://github.com/matrix-org/matrix-react-sdk/pull/635)
|
||||
* Fix expanding/unexapnding read receipts
|
||||
[\#671](https://github.com/matrix-org/matrix-react-sdk/pull/671)
|
||||
* show placeholder when timeline empty
|
||||
[\#670](https://github.com/matrix-org/matrix-react-sdk/pull/670)
|
||||
* Make read receipt's titles more explanatory
|
||||
[\#669](https://github.com/matrix-org/matrix-react-sdk/pull/669)
|
||||
* Fix spurious HTML tags being passed through literally
|
||||
[\#667](https://github.com/matrix-org/matrix-react-sdk/pull/667)
|
||||
* Reinstate max-len lint configs
|
||||
[\#665](https://github.com/matrix-org/matrix-react-sdk/pull/665)
|
||||
* Throw errors on !==200 status codes from RTS
|
||||
[\#662](https://github.com/matrix-org/matrix-react-sdk/pull/662)
|
||||
* Exempt lines which look like pure JSX from the maxlen line
|
||||
[\#664](https://github.com/matrix-org/matrix-react-sdk/pull/664)
|
||||
* Make tests pass on Chrome again
|
||||
[\#663](https://github.com/matrix-org/matrix-react-sdk/pull/663)
|
||||
* Add referral section to user settings
|
||||
[\#661](https://github.com/matrix-org/matrix-react-sdk/pull/661)
|
||||
* Two megolm export fixes:
|
||||
[\#660](https://github.com/matrix-org/matrix-react-sdk/pull/660)
|
||||
* GET /teams from RTS instead of config.json
|
||||
[\#658](https://github.com/matrix-org/matrix-react-sdk/pull/658)
|
||||
* Guard onStatusBarVisible/Hidden with this.unmounted
|
||||
[\#656](https://github.com/matrix-org/matrix-react-sdk/pull/656)
|
||||
* Fix cancel button on e2e import/export dialogs
|
||||
[\#654](https://github.com/matrix-org/matrix-react-sdk/pull/654)
|
||||
* Look up email addresses in ChatInviteDialog
|
||||
[\#653](https://github.com/matrix-org/matrix-react-sdk/pull/653)
|
||||
* Move BugReportDialog to riot-web
|
||||
[\#652](https://github.com/matrix-org/matrix-react-sdk/pull/652)
|
||||
* Fix dark theme styling of roomheader cancel button
|
||||
[\#651](https://github.com/matrix-org/matrix-react-sdk/pull/651)
|
||||
* Allow modals to stack up
|
||||
[\#649](https://github.com/matrix-org/matrix-react-sdk/pull/649)
|
||||
* Add bug report UI
|
||||
[\#642](https://github.com/matrix-org/matrix-react-sdk/pull/642)
|
||||
* Better feedback in invite dialog
|
||||
[\#625](https://github.com/matrix-org/matrix-react-sdk/pull/625)
|
||||
* Import and export for Megolm session data
|
||||
[\#647](https://github.com/matrix-org/matrix-react-sdk/pull/647)
|
||||
* Overhaul MELS to deal with causality, kicks, etc.
|
||||
[\#613](https://github.com/matrix-org/matrix-react-sdk/pull/613)
|
||||
* Re-add dispatcher as alt-up/down uses it
|
||||
[\#650](https://github.com/matrix-org/matrix-react-sdk/pull/650)
|
||||
* Create a common BaseDialog
|
||||
[\#645](https://github.com/matrix-org/matrix-react-sdk/pull/645)
|
||||
* Fix SetDisplayNameDialog
|
||||
[\#648](https://github.com/matrix-org/matrix-react-sdk/pull/648)
|
||||
* Sync typing indication with avatar typing indication
|
||||
[\#643](https://github.com/matrix-org/matrix-react-sdk/pull/643)
|
||||
* Warn users of E2E key loss when changing/resetting passwords or logging out
|
||||
[\#646](https://github.com/matrix-org/matrix-react-sdk/pull/646)
|
||||
* Better user interface for screen readers and keyboard navigation
|
||||
[\#616](https://github.com/matrix-org/matrix-react-sdk/pull/616)
|
||||
* Reduce log spam: Revert a16aeeef2a0f16efedf7e6616cdf3c2c8752a077
|
||||
[\#644](https://github.com/matrix-org/matrix-react-sdk/pull/644)
|
||||
* Expand timeline in situations when _getIndicator not null
|
||||
[\#641](https://github.com/matrix-org/matrix-react-sdk/pull/641)
|
||||
* Correctly get the path of the js-sdk .eslintrc.js
|
||||
[\#640](https://github.com/matrix-org/matrix-react-sdk/pull/640)
|
||||
* Add 'searching known users' to the user picker
|
||||
[\#621](https://github.com/matrix-org/matrix-react-sdk/pull/621)
|
||||
* Add mocha env for tests in eslint config
|
||||
[\#639](https://github.com/matrix-org/matrix-react-sdk/pull/639)
|
||||
* Fix typing avatars displaying "me"
|
||||
[\#637](https://github.com/matrix-org/matrix-react-sdk/pull/637)
|
||||
* Fix device verification from e2e info
|
||||
[\#638](https://github.com/matrix-org/matrix-react-sdk/pull/638)
|
||||
* Make user search do a bit better on word boundary
|
||||
[\#623](https://github.com/matrix-org/matrix-react-sdk/pull/623)
|
||||
* Use an eslint config based on the js-sdk
|
||||
[\#634](https://github.com/matrix-org/matrix-react-sdk/pull/634)
|
||||
* Fix error display in account deactivate dialog
|
||||
[\#633](https://github.com/matrix-org/matrix-react-sdk/pull/633)
|
||||
* Configure travis to test riot-web after building
|
||||
[\#629](https://github.com/matrix-org/matrix-react-sdk/pull/629)
|
||||
* Sanitize ChatInviteDialog
|
||||
[\#626](https://github.com/matrix-org/matrix-react-sdk/pull/626)
|
||||
* (hopefully) fix theming on Chrome
|
||||
[\#630](https://github.com/matrix-org/matrix-react-sdk/pull/630)
|
||||
* Megolm session import and export
|
||||
[\#617](https://github.com/matrix-org/matrix-react-sdk/pull/617)
|
||||
* Allow Modal to be used with async-loaded components
|
||||
[\#618](https://github.com/matrix-org/matrix-react-sdk/pull/618)
|
||||
* Fix escaping markdown by rendering plaintext
|
||||
[\#622](https://github.com/matrix-org/matrix-react-sdk/pull/622)
|
||||
* Implement auto-join rooms on registration
|
||||
[\#628](https://github.com/matrix-org/matrix-react-sdk/pull/628)
|
||||
* Matthew/fix theme npe
|
||||
[\#627](https://github.com/matrix-org/matrix-react-sdk/pull/627)
|
||||
* Implement theming via alternate stylesheets
|
||||
[\#624](https://github.com/matrix-org/matrix-react-sdk/pull/624)
|
||||
* Replace marked with commonmark
|
||||
[\#575](https://github.com/matrix-org/matrix-react-sdk/pull/575)
|
||||
* Fix vector-im/riot-web#2833 : Fail nicely when people try to register
|
||||
numeric user IDs
|
||||
[\#619](https://github.com/matrix-org/matrix-react-sdk/pull/619)
|
||||
* Show the error dialog when requests to PUT power levels fail
|
||||
[\#614](https://github.com/matrix-org/matrix-react-sdk/pull/614)
|
||||
|
||||
Changes in [0.8.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.5) (2017-01-16)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.5-rc.1...v0.8.5)
|
||||
|
|
|
@ -165,6 +165,14 @@ module.exports = function (config) {
|
|||
},
|
||||
devtool: 'inline-source-map',
|
||||
},
|
||||
|
||||
webpackMiddleware: {
|
||||
stats: {
|
||||
// don't fill the console up with a mahoosive list of modules
|
||||
chunks: false,
|
||||
},
|
||||
},
|
||||
|
||||
browserNoActivityTimeout: 15000,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "0.8.5",
|
||||
"version": "0.8.6",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
|
|
@ -290,7 +290,7 @@ export function bodyToHtml(content, highlights, opts) {
|
|||
}
|
||||
|
||||
EMOJI_REGEX.lastIndex = 0;
|
||||
let contentBodyTrimmed = content.body.trim();
|
||||
let contentBodyTrimmed = content.body !== undefined ? content.body.trim() : '';
|
||||
let match = EMOJI_REGEX.exec(contentBodyTrimmed);
|
||||
let emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length;
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import UserActivity from './UserActivity';
|
|||
import Presence from './Presence';
|
||||
import dis from './dispatcher';
|
||||
import DMRoomMap from './utils/DMRoomMap';
|
||||
import RtsClient from './RtsClient';
|
||||
|
||||
/**
|
||||
* Called at startup, to attempt to build a logged-in Matrix session. It tries
|
||||
|
@ -229,6 +230,11 @@ function _restoreFromLocalStorage() {
|
|||
}
|
||||
}
|
||||
|
||||
let rtsClient = null;
|
||||
export function initRtsClient(url) {
|
||||
rtsClient = new RtsClient(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transitions to a logged-in state using the given credentials
|
||||
* @param {MatrixClientCreds} credentials The credentials to use
|
||||
|
@ -261,6 +267,19 @@ export function setLoggedIn(credentials) {
|
|||
} catch (e) {
|
||||
console.warn("Error using local storage: can't persist session!", e);
|
||||
}
|
||||
|
||||
if (rtsClient) {
|
||||
rtsClient.login(credentials.userId).then((body) => {
|
||||
if (body.team_token) {
|
||||
localStorage.setItem("mx_team_token", body.team_token);
|
||||
}
|
||||
}, (err) =>{
|
||||
console.error(
|
||||
"Failed to get team token on login, not persisting to localStorage",
|
||||
err
|
||||
);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.warn("No local storage available: can't persist session!");
|
||||
}
|
||||
|
|
155
src/Markdown.js
155
src/Markdown.js
|
@ -15,110 +15,143 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import commonmark from 'commonmark';
|
||||
import escape from 'lodash/escape';
|
||||
|
||||
const ALLOWED_HTML_TAGS = ['del'];
|
||||
|
||||
// These types of node are definitely text
|
||||
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
|
||||
|
||||
function is_allowed_html_tag(node) {
|
||||
// Regex won't work for tags with attrs, but we only
|
||||
// allow <del> anyway.
|
||||
const matches = /^<\/?(.*)>$/.exec(node.literal);
|
||||
if (matches && matches.length == 2) {
|
||||
const tag = matches[1];
|
||||
return ALLOWED_HTML_TAGS.indexOf(tag) > -1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function html_if_tag_allowed(node) {
|
||||
if (is_allowed_html_tag(node)) {
|
||||
this.lit(node.literal);
|
||||
return;
|
||||
} else {
|
||||
this.lit(escape(node.literal));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns true if the parse output containing the node
|
||||
* comprises multiple block level elements (ie. lines),
|
||||
* or false if it is only a single line.
|
||||
*/
|
||||
function is_multi_line(node) {
|
||||
var par = node;
|
||||
while (par.parent) {
|
||||
par = par.parent;
|
||||
}
|
||||
return par.firstChild != par.lastChild;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class that wraps marked, adding the ability to see whether
|
||||
* Class that wraps commonmark, adding the ability to see whether
|
||||
* a given message actually uses any markdown syntax or whether
|
||||
* it's plain text.
|
||||
*/
|
||||
export default class Markdown {
|
||||
constructor(input) {
|
||||
this.input = input;
|
||||
this.parser = new commonmark.Parser();
|
||||
this.renderer = new commonmark.HtmlRenderer({safe: false});
|
||||
|
||||
const parser = new commonmark.Parser();
|
||||
this.parsed = parser.parse(this.input);
|
||||
}
|
||||
|
||||
isPlainText() {
|
||||
// we determine if the message requires markdown by
|
||||
// running the parser on the tokens with a dummy
|
||||
// rendered and seeing if any of the renderer's
|
||||
// functions are called other than those noted below.
|
||||
// In case you were wondering, no we can't just examine
|
||||
// the tokens because the tokens we have are only the
|
||||
// output of the *first* tokenizer: any line-based
|
||||
// markdown is processed by marked within Parser by
|
||||
// the 'inline lexer'...
|
||||
let is_plain = true;
|
||||
const walker = this.parsed.walker();
|
||||
|
||||
function setNotPlain() {
|
||||
is_plain = false;
|
||||
let ev;
|
||||
while ( (ev = walker.next()) ) {
|
||||
const node = ev.node;
|
||||
if (TEXT_NODES.indexOf(node.type) > -1) {
|
||||
// definitely text
|
||||
continue;
|
||||
} else if (node.type == 'html_inline' || node.type == 'html_block') {
|
||||
// if it's an allowed html tag, we need to render it and therefore
|
||||
// we will need to use HTML. If it's not allowed, it's not HTML since
|
||||
// we'll just be treating it as text.
|
||||
if (is_allowed_html_tag(node)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const dummy_renderer = new commonmark.HtmlRenderer();
|
||||
for (const k of Object.keys(commonmark.HtmlRenderer.prototype)) {
|
||||
dummy_renderer[k] = setNotPlain;
|
||||
}
|
||||
// text and paragraph are just text
|
||||
dummy_renderer.text = function(t) { return t; };
|
||||
dummy_renderer.softbreak = function(t) { return t; };
|
||||
dummy_renderer.paragraph = function(t) { return t; };
|
||||
|
||||
const dummy_parser = new commonmark.Parser();
|
||||
dummy_renderer.render(dummy_parser.parse(this.input));
|
||||
|
||||
return is_plain;
|
||||
return true;
|
||||
}
|
||||
|
||||
toHTML() {
|
||||
const real_paragraph = this.renderer.paragraph;
|
||||
const renderer = new commonmark.HtmlRenderer({safe: false});
|
||||
const real_paragraph = renderer.paragraph;
|
||||
|
||||
this.renderer.paragraph = function(node, entering) {
|
||||
renderer.paragraph = function(node, entering) {
|
||||
// If there is only one top level node, just return the
|
||||
// bare text: it's a single line of text and so should be
|
||||
// 'inline', rather than unnecessarily wrapped in its own
|
||||
// p tag. If, however, we have multiple nodes, each gets
|
||||
// its own p tag to keep them as separate paragraphs.
|
||||
var par = node;
|
||||
while (par.parent) {
|
||||
par = par.parent;
|
||||
}
|
||||
if (par.firstChild != par.lastChild) {
|
||||
if (is_multi_line(node)) {
|
||||
real_paragraph.call(this, node, entering);
|
||||
}
|
||||
};
|
||||
|
||||
var parsed = this.parser.parse(this.input);
|
||||
var rendered = this.renderer.render(parsed);
|
||||
renderer.html_inline = html_if_tag_allowed;
|
||||
renderer.html_block = function(node) {
|
||||
// as with `paragraph`, we only insert line breaks
|
||||
// if there are multiple lines in the markdown.
|
||||
const isMultiLine = is_multi_line(node);
|
||||
|
||||
this.renderer.paragraph = real_paragraph;
|
||||
if (isMultiLine) this.cr();
|
||||
html_if_tag_allowed.call(this, node);
|
||||
if (isMultiLine) this.cr();
|
||||
}
|
||||
|
||||
return rendered;
|
||||
return renderer.render(this.parsed);
|
||||
}
|
||||
|
||||
/*
|
||||
* Render the markdown message to plain text. That is, essentially
|
||||
* just remove any backslashes escaping what would otherwise be
|
||||
* markdown syntax
|
||||
* (to fix https://github.com/vector-im/riot-web/issues/2870)
|
||||
*/
|
||||
toPlaintext() {
|
||||
const real_paragraph = this.renderer.paragraph;
|
||||
const renderer = new commonmark.HtmlRenderer({safe: false});
|
||||
const real_paragraph = renderer.paragraph;
|
||||
|
||||
// The default `out` function only sends the input through an XML
|
||||
// escaping function, which causes messages to be entity encoded,
|
||||
// which we don't want in this case.
|
||||
this.renderer.out = function(s) {
|
||||
renderer.out = function(s) {
|
||||
// The `lit` function adds a string literal to the output buffer.
|
||||
this.lit(s);
|
||||
};
|
||||
|
||||
this.renderer.paragraph = function(node, entering) {
|
||||
// If there is only one top level node, just return the
|
||||
// bare text: it's a single line of text and so should be
|
||||
// 'inline', rather than unnecessarily wrapped in its own
|
||||
// p tag. If, however, we have multiple nodes, each gets
|
||||
// its own p tag to keep them as separate paragraphs.
|
||||
var par = node;
|
||||
while (par.parent) {
|
||||
node = par;
|
||||
par = par.parent;
|
||||
}
|
||||
if (node != par.lastChild) {
|
||||
if (!entering) {
|
||||
renderer.paragraph = function(node, entering) {
|
||||
// as with toHTML, only append lines to paragraphs if there are
|
||||
// multiple paragraphs
|
||||
if (is_multi_line(node)) {
|
||||
if (!entering && node.next) {
|
||||
this.lit('\n\n');
|
||||
}
|
||||
}
|
||||
};
|
||||
renderer.html_block = function(node) {
|
||||
this.lit(node.literal);
|
||||
if (is_multi_line(node) && node.next) this.lit('\n\n');
|
||||
}
|
||||
|
||||
var parsed = this.parser.parse(this.input);
|
||||
var rendered = this.renderer.render(parsed);
|
||||
|
||||
this.renderer.paragraph = real_paragraph;
|
||||
|
||||
return rendered;
|
||||
return renderer.render(this.parsed);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,13 @@ const AsyncWrapper = React.createClass({
|
|||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log('Starting load of AsyncWrapper for modal');
|
||||
this.props.loader((e) => {
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log('AsyncWrapper load completed with '+e.displayName);
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
|
@ -177,7 +183,7 @@ class ModalManager {
|
|||
|
||||
var modal = this._modals[0];
|
||||
var dialog = (
|
||||
<div className={"mx_Dialog_wrapper " + modal.className}>
|
||||
<div className={"mx_Dialog_wrapper " + (modal.className ? modal.className : '') }>
|
||||
<div className="mx_Dialog">
|
||||
{modal.elem}
|
||||
</div>
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
|
||||
/** The types of page which can be shown by the LoggedInView */
|
||||
export default {
|
||||
HomePage: "home_page",
|
||||
RoomView: "room_view",
|
||||
UserSettings: "user_settings",
|
||||
CreateRoom: "create_room",
|
||||
|
|
|
@ -16,17 +16,35 @@ limitations under the License.
|
|||
|
||||
var MatrixClientPeg = require('./MatrixClientPeg');
|
||||
var dis = require('./dispatcher');
|
||||
var sdk = require('./index');
|
||||
var Modal = require('./Modal');
|
||||
|
||||
module.exports = {
|
||||
resend: function(event) {
|
||||
MatrixClientPeg.get().resendEvent(
|
||||
event, MatrixClientPeg.get().getRoom(event.getRoomId())
|
||||
).done(function() {
|
||||
).done(function(res) {
|
||||
dis.dispatch({
|
||||
action: 'message_sent',
|
||||
event: event
|
||||
});
|
||||
}, function() {
|
||||
}, function(err) {
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log('Resend got send failure: ' + err.name + '('+err+')');
|
||||
if (err.name === "UnknownDeviceError") {
|
||||
var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog");
|
||||
Modal.createDialog(UnknownDeviceDialog, {
|
||||
devices: err.devices,
|
||||
room: MatrixClientPeg.get().getRoom(event.getRoomId()),
|
||||
onFinished: (r) => {
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log('UnknownDeviceDialog closed with '+r);
|
||||
},
|
||||
}, "mx_Dialog_unknownDevice");
|
||||
}
|
||||
|
||||
dis.dispatch({
|
||||
action: 'message_send_failed',
|
||||
event: event
|
||||
|
|
97
src/RtsClient.js
Normal file
97
src/RtsClient.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
import 'whatwg-fetch';
|
||||
|
||||
function checkStatus(response) {
|
||||
if (!response.ok) {
|
||||
return response.text().then((text) => {
|
||||
throw new Error(text);
|
||||
});
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
function parseJson(response) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function encodeQueryParams(params) {
|
||||
return '?' + Object.keys(params).map((k) => {
|
||||
return k + '=' + encodeURIComponent(params[k]);
|
||||
}).join('&');
|
||||
}
|
||||
|
||||
const request = (url, opts) => {
|
||||
if (opts && opts.qs) {
|
||||
url += encodeQueryParams(opts.qs);
|
||||
delete opts.qs;
|
||||
}
|
||||
if (opts && opts.body) {
|
||||
if (!opts.headers) {
|
||||
opts.headers = {};
|
||||
}
|
||||
opts.body = JSON.stringify(opts.body);
|
||||
opts.headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
return fetch(url, opts)
|
||||
.then(checkStatus)
|
||||
.then(parseJson);
|
||||
};
|
||||
|
||||
|
||||
export default class RtsClient {
|
||||
constructor(url) {
|
||||
this._url = url;
|
||||
}
|
||||
|
||||
getTeamsConfig() {
|
||||
return request(this._url + '/teams');
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a referral with the Riot Team Server. This should be called once a referred
|
||||
* user has been successfully registered.
|
||||
* @param {string} referrer the user ID of one who referred the user to Riot.
|
||||
* @param {string} userId the user ID of the user being referred.
|
||||
* @param {string} userEmail the email address linked to `userId`.
|
||||
* @returns {Promise} a promise that resolves to { team_token: 'sometoken' } upon
|
||||
* success.
|
||||
*/
|
||||
trackReferral(referrer, userId, userEmail) {
|
||||
return request(this._url + '/register',
|
||||
{
|
||||
body: {
|
||||
referrer: referrer,
|
||||
user_id: userId,
|
||||
user_email: userEmail,
|
||||
},
|
||||
method: 'POST',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getTeam(teamToken) {
|
||||
return request(this._url + '/teamConfiguration',
|
||||
{
|
||||
qs: {
|
||||
team_token: teamToken,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal to the RTS that a login has occurred and that a user requires their team's
|
||||
* token.
|
||||
* @param {string} userId the user ID of the user who is a member of a team.
|
||||
* @returns {Promise} a promise that resolves to { team_token: 'sometoken' } upon
|
||||
* success.
|
||||
*/
|
||||
login(userId) {
|
||||
return request(this._url + '/login',
|
||||
{
|
||||
qs: {
|
||||
user_id: userId,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -91,6 +91,10 @@ class Register extends Signup {
|
|||
this.params.idSid = idSid;
|
||||
}
|
||||
|
||||
setReferrer(referrer) {
|
||||
this.params.referrer = referrer;
|
||||
}
|
||||
|
||||
setGuestAccessToken(token) {
|
||||
this.guestAccessToken = token;
|
||||
}
|
||||
|
|
|
@ -136,6 +136,11 @@ class EmailIdentityStage extends Stage {
|
|||
"&session_id=" +
|
||||
encodeURIComponent(this.signupInstance.getServerData().session);
|
||||
|
||||
// Add the user ID of the referring user, if set
|
||||
if (this.signupInstance.params.referrer) {
|
||||
nextLink += "&referrer=" + encodeURIComponent(this.signupInstance.params.referrer);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
return this.client.requestRegisterEmailToken(
|
||||
this.signupInstance.email,
|
||||
|
|
|
@ -149,6 +149,23 @@ module.exports = {
|
|||
return MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings);
|
||||
},
|
||||
|
||||
getLocalSettings: function() {
|
||||
var localSettingsString = localStorage.getItem('mx_local_settings') || '{}';
|
||||
return JSON.parse(localSettingsString);
|
||||
},
|
||||
|
||||
getLocalSetting: function(type, defaultValue = null) {
|
||||
var settings = this.getLocalSettings();
|
||||
return settings.hasOwnProperty(type) ? settings[type] : null;
|
||||
},
|
||||
|
||||
setLocalSetting: function(type, value) {
|
||||
var settings = this.getLocalSettings();
|
||||
settings[type] = value;
|
||||
// FIXME: handle errors
|
||||
localStorage.setItem('mx_local_settings', JSON.stringify(settings));
|
||||
},
|
||||
|
||||
isFeatureEnabled: function(feature: string): boolean {
|
||||
// Disable labs for guests.
|
||||
if (MatrixClientPeg.get().isGuest()) return false;
|
||||
|
|
|
@ -62,11 +62,11 @@ module.exports = React.createClass({
|
|||
oldNode.style.visibility = c.props.style.visibility;
|
||||
}
|
||||
});
|
||||
if (oldNode.style.visibility == 'hidden' && c.props.style.visibility == 'visible') {
|
||||
oldNode.style.visibility = c.props.style.visibility;
|
||||
}
|
||||
//console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
|
||||
}
|
||||
if (oldNode.style.visibility == 'hidden' && c.props.style.visibility == 'visible') {
|
||||
oldNode.style.visibility = c.props.style.visibility;
|
||||
}
|
||||
self.children[c.key] = old;
|
||||
} else {
|
||||
// new element. If we have a startStyle, use that as the style and go through
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||
|
||||
module.exports = {
|
||||
|
@ -32,10 +48,11 @@ module.exports = {
|
|||
return whoIsTyping;
|
||||
},
|
||||
|
||||
whoIsTypingString: function(room, limit) {
|
||||
const whoIsTyping = this.usersTypingApartFromMe(room);
|
||||
const othersCount = limit === undefined ?
|
||||
0 : Math.max(whoIsTyping.length - limit, 0);
|
||||
whoIsTypingString: function(whoIsTyping, limit) {
|
||||
let othersCount = 0;
|
||||
if (whoIsTyping.length > limit) {
|
||||
othersCount = whoIsTyping.length - limit + 1;
|
||||
}
|
||||
if (whoIsTyping.length == 0) {
|
||||
return '';
|
||||
} else if (whoIsTyping.length == 1) {
|
||||
|
@ -46,7 +63,7 @@ module.exports = {
|
|||
});
|
||||
if (othersCount) {
|
||||
const other = ' other' + (othersCount > 1 ? 's' : '');
|
||||
return names.slice(0, limit).join(', ') + ' and ' +
|
||||
return names.slice(0, limit - 1).join(', ') + ' and ' +
|
||||
othersCount + other + ' are typing';
|
||||
} else {
|
||||
const lastPerson = names.pop();
|
||||
|
|
|
@ -71,7 +71,7 @@ export default React.createClass({
|
|||
return this.props.matrixClient.exportRoomKeys();
|
||||
}).then((k) => {
|
||||
return MegolmExportEncryption.encryptMegolmKeyFile(
|
||||
JSON.stringify(k), passphrase
|
||||
JSON.stringify(k), passphrase,
|
||||
);
|
||||
}).then((f) => {
|
||||
const blob = new Blob([f], {
|
||||
|
@ -95,9 +95,14 @@ export default React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onCancelClick: function(ev) {
|
||||
ev.preventDefault();
|
||||
this.props.onFinished(false);
|
||||
return false;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
|
||||
|
||||
const disableForm = (this.state.phase === PHASE_EXPORTING);
|
||||
|
||||
|
@ -159,10 +164,9 @@ export default React.createClass({
|
|||
<input className='mx_Dialog_primary' type='submit' value='Export'
|
||||
disabled={disableForm}
|
||||
/>
|
||||
<AccessibleButton element='button' onClick={this.props.onFinished}
|
||||
disabled={disableForm}>
|
||||
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||
Cancel
|
||||
</AccessibleButton>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -80,7 +80,7 @@ export default React.createClass({
|
|||
|
||||
return readFileAsArrayBuffer(file).then((arrayBuffer) => {
|
||||
return MegolmExportEncryption.decryptMegolmKeyFile(
|
||||
arrayBuffer, passphrase
|
||||
arrayBuffer, passphrase,
|
||||
);
|
||||
}).then((keys) => {
|
||||
return this.props.matrixClient.importRoomKeys(JSON.parse(keys));
|
||||
|
@ -98,9 +98,14 @@ export default React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onCancelClick: function(ev) {
|
||||
ev.preventDefault();
|
||||
this.props.onFinished(false);
|
||||
return false;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
|
||||
|
||||
const disableForm = (this.state.phase !== PHASE_EDIT);
|
||||
|
||||
|
@ -158,10 +163,9 @@ export default React.createClass({
|
|||
<input className='mx_Dialog_primary' type='submit' value='Import'
|
||||
disabled={!this.state.enableSubmit || disableForm}
|
||||
/>
|
||||
<AccessibleButton element='button' onClick={this.props.onFinished}
|
||||
disabled={disableForm}>
|
||||
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||
Cancel
|
||||
</AccessibleButton>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -89,6 +89,8 @@ import views$dialogs$SetDisplayNameDialog from './components/views/dialogs/SetDi
|
|||
views$dialogs$SetDisplayNameDialog && (module.exports.components['views.dialogs.SetDisplayNameDialog'] = views$dialogs$SetDisplayNameDialog);
|
||||
import views$dialogs$TextInputDialog from './components/views/dialogs/TextInputDialog';
|
||||
views$dialogs$TextInputDialog && (module.exports.components['views.dialogs.TextInputDialog'] = views$dialogs$TextInputDialog);
|
||||
import views$dialogs$UnknownDeviceDialog from './components/views/dialogs/UnknownDeviceDialog';
|
||||
views$dialogs$UnknownDeviceDialog && (module.exports.components['views.dialogs.UnknownDeviceDialog'] = views$dialogs$UnknownDeviceDialog);
|
||||
import views$elements$AccessibleButton from './components/views/elements/AccessibleButton';
|
||||
views$elements$AccessibleButton && (module.exports.components['views.elements.AccessibleButton'] = views$elements$AccessibleButton);
|
||||
import views$elements$AddressSelector from './components/views/elements/AddressSelector';
|
||||
|
|
|
@ -105,6 +105,7 @@ var FilePanel = React.createClass({
|
|||
showUrlPreview = { false }
|
||||
tileShape="file_grid"
|
||||
opacity={ this.props.opacity }
|
||||
empty="There are no visible files in this room"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -42,6 +42,8 @@ export default React.createClass({
|
|||
onRoomCreated: React.PropTypes.func,
|
||||
onUserSettingsClose: React.PropTypes.func,
|
||||
|
||||
teamToken: React.PropTypes.string,
|
||||
|
||||
// and lots and lots of other stuff.
|
||||
},
|
||||
|
||||
|
@ -137,6 +139,7 @@ export default React.createClass({
|
|||
var UserSettings = sdk.getComponent('structures.UserSettings');
|
||||
var CreateRoom = sdk.getComponent('structures.CreateRoom');
|
||||
var RoomDirectory = sdk.getComponent('structures.RoomDirectory');
|
||||
var HomePage = sdk.getComponent('structures.HomePage');
|
||||
var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
||||
var GuestWarningBar = sdk.getComponent('globals.GuestWarningBar');
|
||||
var NewVersionBar = sdk.getComponent('globals.NewVersionBar');
|
||||
|
@ -171,6 +174,7 @@ export default React.createClass({
|
|||
brand={this.props.config.brand}
|
||||
collapsedRhs={this.props.collapse_rhs}
|
||||
enableLabs={this.props.config.enableLabs}
|
||||
referralBaseUrl={this.props.config.referralBaseUrl}
|
||||
/>;
|
||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
|
||||
break;
|
||||
|
@ -190,6 +194,16 @@ export default React.createClass({
|
|||
/>;
|
||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
|
||||
break;
|
||||
|
||||
case PageTypes.HomePage:
|
||||
page_element = <HomePage
|
||||
collapsedRhs={this.props.collapse_rhs}
|
||||
teamServerUrl={this.props.config.teamServerConfig.teamServerURL}
|
||||
teamToken={this.props.teamToken}
|
||||
/>
|
||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>
|
||||
break;
|
||||
|
||||
case PageTypes.UserView:
|
||||
page_element = null; // deliberately null for now
|
||||
right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.sideOpacity} />;
|
||||
|
@ -218,7 +232,12 @@ export default React.createClass({
|
|||
<div className='mx_MatrixChat_wrapper'>
|
||||
{topBar}
|
||||
<div className={bodyClasses}>
|
||||
<LeftPanel selectedRoom={this.props.currentRoomId} collapsed={this.props.collapse_lhs || false} opacity={this.props.sideOpacity}/>
|
||||
<LeftPanel
|
||||
selectedRoom={this.props.currentRoomId}
|
||||
collapsed={this.props.collapse_lhs || false}
|
||||
opacity={this.props.sideOpacity}
|
||||
teamToken={this.props.teamToken}
|
||||
/>
|
||||
<main className='mx_MatrixChat_middlePanel'>
|
||||
{page_element}
|
||||
</main>
|
||||
|
|
|
@ -190,6 +190,20 @@ module.exports = React.createClass({
|
|||
if (this.props.config.sync_timeline_limit) {
|
||||
MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit;
|
||||
}
|
||||
|
||||
// Persist the team token across refreshes using sessionStorage. A new window or
|
||||
// tab will not persist sessionStorage, but refreshes will.
|
||||
if (this.props.startingFragmentQueryParams.team_token) {
|
||||
window.sessionStorage.setItem(
|
||||
'mx_team_token',
|
||||
this.props.startingFragmentQueryParams.team_token,
|
||||
);
|
||||
}
|
||||
|
||||
// Use the locally-stored team token first, then as a fall-back, check to see if
|
||||
// a referral link was used, which will contain a query parameter `team_token`.
|
||||
this._teamToken = window.localStorage.getItem('mx_team_token') ||
|
||||
window.sessionStorage.getItem('mx_team_token');
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -210,6 +224,12 @@ module.exports = React.createClass({
|
|||
window.addEventListener('resize', this.handleResize);
|
||||
this.handleResize();
|
||||
|
||||
if (this.props.config.teamServerConfig &&
|
||||
this.props.config.teamServerConfig.teamServerURL
|
||||
) {
|
||||
Lifecycle.initRtsClient(this.props.config.teamServerConfig.teamServerURL);
|
||||
}
|
||||
|
||||
// the extra q() ensures that synchronous exceptions hit the same codepath as
|
||||
// asynchronous ones.
|
||||
q().then(() => {
|
||||
|
@ -421,6 +441,10 @@ module.exports = React.createClass({
|
|||
this._setPage(PageTypes.RoomDirectory);
|
||||
this.notifyNewScreen('directory');
|
||||
break;
|
||||
case 'view_home_page':
|
||||
this._setPage(PageTypes.HomePage);
|
||||
this.notifyNewScreen('home');
|
||||
break;
|
||||
case 'view_create_chat':
|
||||
this._createChat();
|
||||
break;
|
||||
|
@ -690,7 +714,11 @@ module.exports = React.createClass({
|
|||
)[0].roomId;
|
||||
self.setState({ready: true, currentRoomId: firstRoom, page_type: PageTypes.RoomView});
|
||||
} else {
|
||||
self.setState({ready: true, page_type: PageTypes.RoomDirectory});
|
||||
if (self._teamToken) {
|
||||
self.setState({ready: true, page_type: PageTypes.HomePage});
|
||||
} else {
|
||||
self.setState({ready: true, page_type: PageTypes.RoomDirectory});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.setState({ready: true, page_type: PageTypes.RoomView});
|
||||
|
@ -710,7 +738,11 @@ module.exports = React.createClass({
|
|||
} else {
|
||||
// There is no information on presentedId
|
||||
// so point user to fallback like /directory
|
||||
self.notifyNewScreen('directory');
|
||||
if (self._teamToken) {
|
||||
self.notifyNewScreen('home');
|
||||
} else {
|
||||
self.notifyNewScreen('directory');
|
||||
}
|
||||
}
|
||||
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
|
@ -774,6 +806,10 @@ module.exports = React.createClass({
|
|||
dis.dispatch({
|
||||
action: 'view_user_settings',
|
||||
});
|
||||
} else if (screen == 'home') {
|
||||
dis.dispatch({
|
||||
action: 'view_home_page',
|
||||
});
|
||||
} else if (screen == 'directory') {
|
||||
dis.dispatch({
|
||||
action: 'view_room_directory',
|
||||
|
@ -1033,6 +1069,7 @@ module.exports = React.createClass({
|
|||
onRoomIdResolved={this.onRoomIdResolved}
|
||||
onRoomCreated={this.onRoomCreated}
|
||||
onUserSettingsClose={this.onUserSettingsClose}
|
||||
teamToken={this._teamToken}
|
||||
{...this.props}
|
||||
{...this.state}
|
||||
/>
|
||||
|
@ -1055,12 +1092,13 @@ module.exports = React.createClass({
|
|||
sessionId={this.state.register_session_id}
|
||||
idSid={this.state.register_id_sid}
|
||||
email={this.props.startingFragmentQueryParams.email}
|
||||
referrer={this.props.startingFragmentQueryParams.referrer}
|
||||
username={this.state.upgradeUsername}
|
||||
guestAccessToken={this.state.guestAccessToken}
|
||||
defaultHsUrl={this.getDefaultHsUrl()}
|
||||
defaultIsUrl={this.getDefaultIsUrl()}
|
||||
brand={this.props.config.brand}
|
||||
teamsConfig={this.props.config.teamsConfig}
|
||||
teamServerConfig={this.props.config.teamServerConfig}
|
||||
customHsUrl={this.getCurrentHsUrl()}
|
||||
customIsUrl={this.getCurrentIsUrl()}
|
||||
registrationUrl={this.props.registrationUrl}
|
||||
|
|
|
@ -48,6 +48,7 @@ var NotificationPanel = React.createClass({
|
|||
showUrlPreview = { false }
|
||||
opacity={ this.props.opacity }
|
||||
tileShape="notif"
|
||||
empty="You have no visible notifications"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -39,8 +39,8 @@ module.exports = React.createClass({
|
|||
// the number of messages which have arrived since we've been scrolled up
|
||||
numUnreadMessages: React.PropTypes.number,
|
||||
|
||||
// true if there are messages in the room which had errors on send
|
||||
hasUnsentMessages: React.PropTypes.bool,
|
||||
// string to display when there are messages in the room which had errors on send
|
||||
unsentMessageError: React.PropTypes.string,
|
||||
|
||||
// this is true if we are fully scrolled-down, and are looking at
|
||||
// the end of the live timeline.
|
||||
|
@ -74,6 +74,7 @@ module.exports = React.createClass({
|
|||
// callback for when the status bar can be hidden from view, as it is
|
||||
// not displaying anything
|
||||
onHidden: React.PropTypes.func,
|
||||
|
||||
// callback for when the status bar is displaying something and should
|
||||
// be visible
|
||||
onVisible: React.PropTypes.func,
|
||||
|
@ -81,17 +82,14 @@ module.exports = React.createClass({
|
|||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
whoIsTypingLimit: 2,
|
||||
whoIsTypingLimit: 3,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
syncState: MatrixClientPeg.get().getSyncState(),
|
||||
whoisTypingString: WhoIsTyping.whoIsTypingString(
|
||||
this.props.room,
|
||||
this.props.whoIsTypingLimit
|
||||
),
|
||||
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -105,7 +103,7 @@ module.exports = React.createClass({
|
|||
this.props.onResize();
|
||||
}
|
||||
|
||||
const size = this._getSize(this.state, this.props);
|
||||
const size = this._getSize(this.props, this.state);
|
||||
if (size > 0) {
|
||||
this.props.onVisible();
|
||||
} else {
|
||||
|
@ -113,7 +111,9 @@ module.exports = React.createClass({
|
|||
clearTimeout(this.hideDebouncer);
|
||||
}
|
||||
this.hideDebouncer = setTimeout(() => {
|
||||
this.props.onHidden();
|
||||
// temporarily stop hiding the statusbar as per
|
||||
// https://github.com/vector-im/riot-web/issues/1991#issuecomment-276953915
|
||||
// this.props.onHidden();
|
||||
}, HIDE_DEBOUNCE_MS);
|
||||
}
|
||||
},
|
||||
|
@ -138,26 +138,23 @@ module.exports = React.createClass({
|
|||
|
||||
onRoomMemberTyping: function(ev, member) {
|
||||
this.setState({
|
||||
whoisTypingString: WhoIsTyping.whoIsTypingString(
|
||||
this.props.room,
|
||||
this.props.whoIsTypingLimit
|
||||
),
|
||||
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
|
||||
});
|
||||
},
|
||||
|
||||
// We don't need the actual height - just whether it is likely to have
|
||||
// changed - so we use '0' to indicate normal size, and other values to
|
||||
// indicate other sizes.
|
||||
_getSize: function(state, props) {
|
||||
_getSize: function(props, state) {
|
||||
if (state.syncState === "ERROR" ||
|
||||
state.whoisTypingString ||
|
||||
(state.usersTyping.length > 0) ||
|
||||
props.numUnreadMessages ||
|
||||
!props.atEndOfLiveTimeline ||
|
||||
props.hasActiveCall) {
|
||||
return STATUS_BAR_EXPANDED;
|
||||
} else if (props.tabCompleteEntries) {
|
||||
return STATUS_BAR_HIDDEN;
|
||||
} else if (props.hasUnsentMessages) {
|
||||
} else if (props.unsentMessageError) {
|
||||
return STATUS_BAR_EXPANDED_LARGE;
|
||||
}
|
||||
return STATUS_BAR_HIDDEN;
|
||||
|
@ -166,7 +163,8 @@ module.exports = React.createClass({
|
|||
// determine if we need to call onResize
|
||||
_checkForResize: function(prevProps, prevState) {
|
||||
// figure out the old height and the new height of the status bar.
|
||||
return this._getSize(prevProps, prevState) !== this._getSize(this.props, this.state);
|
||||
return this._getSize(prevProps, prevState)
|
||||
!== this._getSize(this.props, this.state);
|
||||
},
|
||||
|
||||
// return suitable content for the image on the left of the status bar.
|
||||
|
@ -217,10 +215,13 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_renderTypingIndicatorAvatars: function(limit) {
|
||||
let users = WhoIsTyping.usersTypingApartFromMe(this.props.room);
|
||||
let users = this.state.usersTyping;
|
||||
|
||||
let othersCount = Math.max(users.length - limit, 0);
|
||||
users = users.slice(0, limit);
|
||||
let othersCount = 0;
|
||||
if (users.length > limit) {
|
||||
othersCount = users.length - limit + 1;
|
||||
users = users.slice(0, limit - 1);
|
||||
}
|
||||
|
||||
let avatars = users.map((u, index) => {
|
||||
let showInitial = othersCount === 0 && index === users.length - 1;
|
||||
|
@ -238,7 +239,7 @@ module.exports = React.createClass({
|
|||
|
||||
if (othersCount > 0) {
|
||||
avatars.push(
|
||||
<span className="mx_RoomStatusBar_typingIndicatorRemaining">
|
||||
<span className="mx_RoomStatusBar_typingIndicatorRemaining" key="others">
|
||||
+{othersCount}
|
||||
</span>
|
||||
);
|
||||
|
@ -285,12 +286,12 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
if (this.props.hasUnsentMessages) {
|
||||
if (this.props.unsentMessageError) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ "/>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
Some of your messages have not been sent.
|
||||
{ this.props.unsentMessageError }
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||
<a className="mx_RoomStatusBar_resend_link"
|
||||
|
@ -321,7 +322,10 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
var typingString = this.state.whoisTypingString;
|
||||
const typingString = WhoIsTyping.whoIsTypingString(
|
||||
this.state.usersTyping,
|
||||
this.props.whoIsTypingLimit
|
||||
);
|
||||
if (typingString) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_typingBar">
|
||||
|
@ -344,7 +348,7 @@ module.exports = React.createClass({
|
|||
|
||||
render: function() {
|
||||
var content = this._getContent();
|
||||
var indicator = this._getIndicator(this.state.whoisTypingString !== null);
|
||||
var indicator = this._getIndicator(this.state.usersTyping.length > 0);
|
||||
|
||||
return (
|
||||
<div className="mx_RoomStatusBar">
|
||||
|
|
|
@ -128,7 +128,7 @@ module.exports = React.createClass({
|
|||
draggingFile: false,
|
||||
searching: false,
|
||||
searchResults: null,
|
||||
hasUnsentMessages: false,
|
||||
unsentMessageError: '',
|
||||
callState: null,
|
||||
guestsCanJoin: false,
|
||||
canPeek: false,
|
||||
|
@ -182,7 +182,7 @@ module.exports = React.createClass({
|
|||
room: room,
|
||||
roomId: result.room_id,
|
||||
roomLoading: !room,
|
||||
hasUnsentMessages: this._hasUnsentMessages(room),
|
||||
unsentMessageError: this._getUnsentMessageError(room),
|
||||
}, this._onHaveRoom);
|
||||
}, (err) => {
|
||||
this.setState({
|
||||
|
@ -196,7 +196,7 @@ module.exports = React.createClass({
|
|||
roomId: this.props.roomAddress,
|
||||
room: room,
|
||||
roomLoading: !room,
|
||||
hasUnsentMessages: this._hasUnsentMessages(room),
|
||||
unsentMessageError: this._getUnsentMessageError(room),
|
||||
}, this._onHaveRoom);
|
||||
}
|
||||
},
|
||||
|
@ -397,7 +397,7 @@ module.exports = React.createClass({
|
|||
case 'message_sent':
|
||||
case 'message_send_cancelled':
|
||||
this.setState({
|
||||
hasUnsentMessages: this._hasUnsentMessages(this.state.room)
|
||||
unsentMessageError: this._getUnsentMessageError(this.state.room),
|
||||
});
|
||||
break;
|
||||
case 'notifier_enabled':
|
||||
|
@ -636,8 +636,15 @@ module.exports = React.createClass({
|
|||
}
|
||||
}, 500),
|
||||
|
||||
_hasUnsentMessages: function(room) {
|
||||
return this._getUnsentMessages(room).length > 0;
|
||||
_getUnsentMessageError: function(room) {
|
||||
const unsentMessages = this._getUnsentMessages(room);
|
||||
if (!unsentMessages.length) return "";
|
||||
for (const event of unsentMessages) {
|
||||
if (!event.error || event.error.name !== "UnknownDeviceError") {
|
||||
return "Some of your messages have not been sent.";
|
||||
}
|
||||
}
|
||||
return "Message not sent due to unknown devices being present";
|
||||
},
|
||||
|
||||
_getUnsentMessages: function(room) {
|
||||
|
@ -1332,12 +1339,14 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onStatusBarVisible: function() {
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
statusBarVisible: true,
|
||||
});
|
||||
},
|
||||
|
||||
onStatusBarHidden: function() {
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
statusBarVisible: false,
|
||||
});
|
||||
|
@ -1507,18 +1516,19 @@ module.exports = React.createClass({
|
|||
});
|
||||
|
||||
var statusBar;
|
||||
let isStatusAreaExpanded = true;
|
||||
|
||||
if (ContentMessages.getCurrentUploads().length > 0) {
|
||||
var UploadBar = sdk.getComponent('structures.UploadBar');
|
||||
statusBar = <UploadBar room={this.state.room} />;
|
||||
} else if (!this.state.searchResults) {
|
||||
var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar');
|
||||
|
||||
isStatusAreaExpanded = this.state.statusBarVisible;
|
||||
statusBar = <RoomStatusBar
|
||||
room={this.state.room}
|
||||
tabComplete={this.tabComplete}
|
||||
numUnreadMessages={this.state.numUnreadMessages}
|
||||
hasUnsentMessages={this.state.hasUnsentMessages}
|
||||
unsentMessageError={this.state.unsentMessageError}
|
||||
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}
|
||||
hasActiveCall={inCall}
|
||||
onResendAllClick={this.onResendAllClick}
|
||||
|
@ -1527,7 +1537,7 @@ module.exports = React.createClass({
|
|||
onResize={this.onChildResize}
|
||||
onVisible={this.onStatusBarVisible}
|
||||
onHidden={this.onStatusBarHidden}
|
||||
whoIsTypingLimit={2}
|
||||
whoIsTypingLimit={3}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
@ -1683,7 +1693,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
let statusBarAreaClass = "mx_RoomView_statusArea mx_fadable";
|
||||
if (this.state.statusBarVisible) {
|
||||
if (isStatusAreaExpanded) {
|
||||
statusBarAreaClass += " mx_RoomView_statusArea_expanded";
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ var DEBUG_SCROLL = false;
|
|||
|
||||
// The amount of extra scroll distance to allow prior to unfilling.
|
||||
// See _getExcessHeight.
|
||||
const UNPAGINATION_PADDING = 1500;
|
||||
const UNPAGINATION_PADDING = 3000;
|
||||
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent
|
||||
// many scroll events causing many unfilling requests.
|
||||
const UNFILL_REQUEST_DEBOUNCE_MS = 200;
|
||||
|
@ -570,7 +570,7 @@ module.exports = React.createClass({
|
|||
var boundingRect = node.getBoundingClientRect();
|
||||
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
|
||||
|
||||
debuglog("Scrolling to token '" + node.dataset.scrollToken + "'+" +
|
||||
debuglog("ScrollPanel: scrolling to token '" + node.dataset.scrollToken + "'+" +
|
||||
pixelOffset + " (delta: "+scrollDelta+")");
|
||||
|
||||
if(scrollDelta != 0) {
|
||||
|
@ -582,7 +582,7 @@ module.exports = React.createClass({
|
|||
_saveScrollState: function() {
|
||||
if (this.props.stickyBottom && this.isAtBottom()) {
|
||||
this.scrollState = { stuckAtBottom: true };
|
||||
debuglog("Saved scroll state", this.scrollState);
|
||||
debuglog("ScrollPanel: Saved scroll state", this.scrollState);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -601,12 +601,12 @@ module.exports = React.createClass({
|
|||
trackedScrollToken: node.dataset.scrollToken,
|
||||
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
|
||||
};
|
||||
debuglog("Saved scroll state", this.scrollState);
|
||||
debuglog("ScrollPanel: saved scroll state", this.scrollState);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
debuglog("Unable to save scroll state: found no children in the viewport");
|
||||
debuglog("ScrollPanel: unable to save scroll state: found no children in the viewport");
|
||||
},
|
||||
|
||||
_restoreSavedScrollState: function() {
|
||||
|
@ -640,7 +640,7 @@ module.exports = React.createClass({
|
|||
this._lastSetScroll = scrollNode.scrollTop;
|
||||
}
|
||||
|
||||
debuglog("Set scrollTop:", scrollNode.scrollTop,
|
||||
debuglog("ScrollPanel: set scrollTop:", scrollNode.scrollTop,
|
||||
"requested:", scrollTop,
|
||||
"_lastSetScroll:", this._lastSetScroll);
|
||||
},
|
||||
|
|
|
@ -96,6 +96,9 @@ var TimelinePanel = React.createClass({
|
|||
|
||||
// shape property to be passed to EventTiles
|
||||
tileShape: React.PropTypes.string,
|
||||
|
||||
// placeholder text to use if the timeline is empty
|
||||
empty: React.PropTypes.string,
|
||||
},
|
||||
|
||||
statics: {
|
||||
|
@ -990,6 +993,14 @@ var TimelinePanel = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
if (this.state.events.length == 0 && !this.state.canBackPaginate && this.props.empty) {
|
||||
return (
|
||||
<div className={ this.props.className + " mx_RoomView_messageListWrapper" }>
|
||||
<div className="mx_RoomView_empty">{ this.props.empty }</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// give the messagepanel a stickybottom if we're at the end of the
|
||||
// live timeline, so that the arrival of new events triggers a
|
||||
// scroll.
|
||||
|
|
|
@ -90,8 +90,8 @@ module.exports = React.createClass({displayName: 'UploadBar',
|
|||
<div className="mx_UploadBar_uploadProgressOuter">
|
||||
<div className="mx_UploadBar_uploadProgressInner" style={innerProgressStyle}></div>
|
||||
</div>
|
||||
<img className="mx_UploadBar_uploadIcon" src="img/fileicon.png" width="17" height="22"/>
|
||||
<img className="mx_UploadBar_uploadCancel" src="img/cancel.svg" width="18" height="18"
|
||||
<img className="mx_UploadBar_uploadIcon mx_filterFlipColor" src="img/fileicon.png" width="17" height="22"/>
|
||||
<img className="mx_UploadBar_uploadCancel mx_filterFlipColor" src="img/cancel.svg" width="18" height="18"
|
||||
onClick={function() { ContentMessages.cancelUpload(upload.promise); }}
|
||||
/>
|
||||
<div className="mx_UploadBar_uploadBytes">
|
||||
|
|
|
@ -59,6 +59,18 @@ const SETTINGS_LABELS = [
|
|||
*/
|
||||
];
|
||||
|
||||
const CRYPTO_SETTINGS_LABELS = [
|
||||
{
|
||||
id: 'blacklistUnverifiedDevices',
|
||||
label: 'Never send encrypted messages to unverified devices from this device',
|
||||
},
|
||||
// XXX: this is here for documentation; the actual setting is managed via RoomSettings
|
||||
// {
|
||||
// id: 'blacklistUnverifiedDevicesPerRoom'
|
||||
// label: 'Never send encrypted messages to unverified devices in this room',
|
||||
// }
|
||||
];
|
||||
|
||||
// Enumerate the available themes, with a nice human text label.
|
||||
// 'id' gives the key name in the im.vector.web.settings account data event
|
||||
// 'value' is the value for that key in the event
|
||||
|
@ -92,6 +104,9 @@ module.exports = React.createClass({
|
|||
// True to show the 'labs' section of experimental features
|
||||
enableLabs: React.PropTypes.bool,
|
||||
|
||||
// The base URL to use in the referral link. Defaults to window.location.origin.
|
||||
referralBaseUrl: React.PropTypes.string,
|
||||
|
||||
// true if RightPanel is collapsed
|
||||
collapsedRhs: React.PropTypes.bool,
|
||||
},
|
||||
|
@ -148,6 +163,8 @@ module.exports = React.createClass({
|
|||
syncedSettings.theme = 'light';
|
||||
}
|
||||
this._syncedSettings = syncedSettings;
|
||||
|
||||
this._localSettings = UserSettingsStore.getLocalSettings();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -444,6 +461,27 @@ module.exports = React.createClass({
|
|||
);
|
||||
},
|
||||
|
||||
_renderReferral: function() {
|
||||
const teamToken = window.localStorage.getItem('mx_team_token');
|
||||
if (!teamToken) {
|
||||
return null;
|
||||
}
|
||||
if (typeof teamToken !== 'string') {
|
||||
console.warn('Team token not a string');
|
||||
return null;
|
||||
}
|
||||
const href = (this.props.referralBaseUrl || window.location.origin) +
|
||||
`/#/register?referrer=${this._me}&team_token=${teamToken}`;
|
||||
return (
|
||||
<div>
|
||||
<h3>Referral</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
Refer a friend to Riot: <a href={href}>{href}</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_renderUserInterfaceSettings: function() {
|
||||
var client = MatrixClientPeg.get();
|
||||
|
||||
|
@ -514,21 +552,20 @@ module.exports = React.createClass({
|
|||
const deviceId = client.deviceId;
|
||||
const identityKey = client.getDeviceEd25519Key() || "<not supported>";
|
||||
|
||||
let exportButton = null,
|
||||
importButton = null;
|
||||
let importExportButtons = null;
|
||||
|
||||
if (client.isCryptoEnabled) {
|
||||
exportButton = (
|
||||
<AccessibleButton className="mx_UserSettings_button"
|
||||
onClick={this._onExportE2eKeysClicked}>
|
||||
Export E2E room keys
|
||||
</AccessibleButton>
|
||||
);
|
||||
importButton = (
|
||||
<AccessibleButton className="mx_UserSettings_button"
|
||||
onClick={this._onImportE2eKeysClicked}>
|
||||
Import E2E room keys
|
||||
</AccessibleButton>
|
||||
importExportButtons = (
|
||||
<div className="mx_UserSettings_importExportButtons">
|
||||
<AccessibleButton className="mx_UserSettings_button"
|
||||
onClick={this._onExportE2eKeysClicked}>
|
||||
Export E2E room keys
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_UserSettings_button"
|
||||
onClick={this._onImportE2eKeysClicked}>
|
||||
Import E2E room keys
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
|
@ -539,13 +576,36 @@ module.exports = React.createClass({
|
|||
<li><label>Device ID:</label> <span><code>{deviceId}</code></span></li>
|
||||
<li><label>Device key:</label> <span><code><b>{identityKey}</b></code></span></li>
|
||||
</ul>
|
||||
{exportButton}
|
||||
{importButton}
|
||||
{ importExportButtons }
|
||||
</div>
|
||||
<div className="mx_UserSettings_section">
|
||||
{ CRYPTO_SETTINGS_LABELS.map( this._renderLocalSetting ) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_renderLocalSetting: function(setting) {
|
||||
const client = MatrixClientPeg.get();
|
||||
return <div className="mx_UserSettings_toggle" key={ setting.id }>
|
||||
<input id={ setting.id }
|
||||
type="checkbox"
|
||||
defaultChecked={ this._localSettings[setting.id] }
|
||||
onChange={
|
||||
e => {
|
||||
UserSettingsStore.setLocalSetting(setting.id, e.target.checked)
|
||||
if (setting.id === 'blacklistUnverifiedDevices') { // XXX: this is a bit ugly
|
||||
client.setGlobalBlacklistUnverifiedDevices(e.target.checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
<label htmlFor={ setting.id }>
|
||||
{ setting.label }
|
||||
</label>
|
||||
</div>;
|
||||
},
|
||||
|
||||
_renderDevicesPanel: function() {
|
||||
var DevicesPanel = sdk.getComponent('settings.DevicesPanel');
|
||||
return (
|
||||
|
@ -819,6 +879,8 @@ module.exports = React.createClass({
|
|||
{accountJsx}
|
||||
</div>
|
||||
|
||||
{this._renderReferral()}
|
||||
|
||||
{notification_area}
|
||||
|
||||
{this._renderUserInterfaceSettings()}
|
||||
|
@ -842,7 +904,7 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
<div className="mx_UserSettings_advanced">
|
||||
matrix-react-sdk version: {REACT_SDK_VERSION}<br/>
|
||||
vector-web version: {this.state.vectorVersion !== null ? this.state.vectorVersion : 'unknown'}<br/>
|
||||
riot-web version: {this.state.vectorVersion !== null ? this.state.vectorVersion : 'unknown'}<br/>
|
||||
olm version: {olmVersionString}<br/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -25,6 +25,7 @@ var ServerConfig = require("../../views/login/ServerConfig");
|
|||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var RegistrationForm = require("../../views/login/RegistrationForm");
|
||||
var CaptchaForm = require("../../views/login/CaptchaForm");
|
||||
var RtsClient = require("../../../RtsClient");
|
||||
|
||||
var MIN_PASSWORD_LENGTH = 6;
|
||||
|
||||
|
@ -47,23 +48,16 @@ module.exports = React.createClass({
|
|||
defaultIsUrl: React.PropTypes.string,
|
||||
brand: React.PropTypes.string,
|
||||
email: React.PropTypes.string,
|
||||
referrer: React.PropTypes.string,
|
||||
username: React.PropTypes.string,
|
||||
guestAccessToken: React.PropTypes.string,
|
||||
teamsConfig: React.PropTypes.shape({
|
||||
teamServerConfig: React.PropTypes.shape({
|
||||
// Email address to request new teams
|
||||
supportEmail: React.PropTypes.string,
|
||||
teams: React.PropTypes.arrayOf(React.PropTypes.shape({
|
||||
// The displayed name of the team
|
||||
"name": React.PropTypes.string,
|
||||
// The suffix with which every team email address ends
|
||||
"emailSuffix": React.PropTypes.string,
|
||||
// The rooms to use during auto-join
|
||||
"rooms": React.PropTypes.arrayOf(React.PropTypes.shape({
|
||||
"id": React.PropTypes.string,
|
||||
"autoJoin": React.PropTypes.bool,
|
||||
})),
|
||||
})).required,
|
||||
supportEmail: React.PropTypes.string.isRequired,
|
||||
// URL of the riot-team-server to get team configurations and track referrals
|
||||
teamServerURL: React.PropTypes.string.isRequired,
|
||||
}),
|
||||
teamSelected: React.PropTypes.object,
|
||||
|
||||
defaultDeviceDisplayName: React.PropTypes.string,
|
||||
|
||||
|
@ -75,6 +69,7 @@ module.exports = React.createClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
busy: false,
|
||||
teamServerBusy: false,
|
||||
errorText: null,
|
||||
// We remember the values entered by the user because
|
||||
// the registration form will be unmounted during the
|
||||
|
@ -90,6 +85,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
// attach this to the instance rather than this.state since it isn't UI
|
||||
this.registerLogic = new Signup.Register(
|
||||
|
@ -102,11 +98,44 @@ module.exports = React.createClass({
|
|||
this.registerLogic.setRegistrationUrl(this.props.registrationUrl);
|
||||
this.registerLogic.setIdSid(this.props.idSid);
|
||||
this.registerLogic.setGuestAccessToken(this.props.guestAccessToken);
|
||||
if (this.props.referrer) {
|
||||
this.registerLogic.setReferrer(this.props.referrer);
|
||||
}
|
||||
this.registerLogic.recheckState();
|
||||
|
||||
if (
|
||||
this.props.teamServerConfig &&
|
||||
this.props.teamServerConfig.teamServerURL &&
|
||||
!this._rtsClient
|
||||
) {
|
||||
this._rtsClient = new RtsClient(this.props.teamServerConfig.teamServerURL);
|
||||
|
||||
this.setState({
|
||||
teamServerBusy: true,
|
||||
});
|
||||
// GET team configurations including domains, names and icons
|
||||
this._rtsClient.getTeamsConfig().then((data) => {
|
||||
const teamsConfig = {
|
||||
teams: data,
|
||||
supportEmail: this.props.teamServerConfig.supportEmail,
|
||||
};
|
||||
console.log('Setting teams config to ', teamsConfig);
|
||||
this.setState({
|
||||
teamsConfig: teamsConfig,
|
||||
teamServerBusy: false,
|
||||
});
|
||||
}, (err) => {
|
||||
console.error('Error retrieving config for teams', err);
|
||||
this.setState({
|
||||
teamServerBusy: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
this._unmounted = true;
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -184,24 +213,41 @@ module.exports = React.createClass({
|
|||
accessToken: response.access_token
|
||||
});
|
||||
|
||||
// Auto-join rooms
|
||||
if (self.props.teamsConfig && self.props.teamsConfig.teams) {
|
||||
for (let i = 0; i < self.props.teamsConfig.teams.length; i++) {
|
||||
let team = self.props.teamsConfig.teams[i];
|
||||
if (self.state.formVals.email.endsWith(team.emailSuffix)) {
|
||||
console.log("User successfully registered with team " + team.name);
|
||||
if (
|
||||
self._rtsClient &&
|
||||
self.props.referrer &&
|
||||
self.state.teamSelected
|
||||
) {
|
||||
// Track referral, get team_token in order to retrieve team config
|
||||
self._rtsClient.trackReferral(
|
||||
self.props.referrer,
|
||||
response.user_id,
|
||||
self.state.formVals.email
|
||||
).then((data) => {
|
||||
const teamToken = data.team_token;
|
||||
// Store for use /w welcome pages
|
||||
window.localStorage.setItem('mx_team_token', teamToken);
|
||||
|
||||
self._rtsClient.getTeam(teamToken).then((team) => {
|
||||
console.log(
|
||||
`User successfully registered with team ${team.name}`
|
||||
);
|
||||
if (!team.rooms) {
|
||||
break;
|
||||
return;
|
||||
}
|
||||
// Auto-join rooms
|
||||
team.rooms.forEach((room) => {
|
||||
if (room.autoJoin) {
|
||||
console.log("Auto-joining " + room.id);
|
||||
MatrixClientPeg.get().joinRoom(room.id);
|
||||
if (room.auto_join && room.room_id) {
|
||||
console.log(`Auto-joining ${room.room_id}`);
|
||||
MatrixClientPeg.get().joinRoom(room.room_id);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, (err) => {
|
||||
console.error('Error getting team config', err);
|
||||
});
|
||||
}, (err) => {
|
||||
console.error('Error tracking referral', err);
|
||||
});
|
||||
}
|
||||
|
||||
if (self.props.brand) {
|
||||
|
@ -273,7 +319,15 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onTeamSelected: function(teamSelected) {
|
||||
if (!this._unmounted) {
|
||||
this.setState({ teamSelected });
|
||||
}
|
||||
},
|
||||
|
||||
_getRegisterContentJsx: function() {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
||||
var currStep = this.registerLogic.getStep();
|
||||
var registerStep;
|
||||
switch (currStep) {
|
||||
|
@ -283,17 +337,23 @@ module.exports = React.createClass({
|
|||
case "Register.STEP_m.login.dummy":
|
||||
// NB. Our 'username' prop is specifically for upgrading
|
||||
// a guest account
|
||||
if (this.state.teamServerBusy) {
|
||||
registerStep = <Spinner />;
|
||||
break;
|
||||
}
|
||||
registerStep = (
|
||||
<RegistrationForm
|
||||
showEmail={true}
|
||||
defaultUsername={this.state.formVals.username}
|
||||
defaultEmail={this.state.formVals.email}
|
||||
defaultPassword={this.state.formVals.password}
|
||||
teamsConfig={this.props.teamsConfig}
|
||||
teamsConfig={this.state.teamsConfig}
|
||||
guestUsername={this.props.username}
|
||||
minPasswordLength={MIN_PASSWORD_LENGTH}
|
||||
onError={this.onFormValidationFailed}
|
||||
onRegisterClick={this.onFormSubmit} />
|
||||
onRegisterClick={this.onFormSubmit}
|
||||
onTeamSelected={this.onTeamSelected}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "Register.STEP_m.login.email.identity":
|
||||
|
@ -322,7 +382,6 @@ module.exports = React.createClass({
|
|||
}
|
||||
var busySpinner;
|
||||
if (this.state.busy) {
|
||||
var Spinner = sdk.getComponent("elements.Spinner");
|
||||
busySpinner = (
|
||||
<Spinner />
|
||||
);
|
||||
|
@ -367,7 +426,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_Login">
|
||||
<div className="mx_Login_box">
|
||||
<LoginHeader />
|
||||
<LoginHeader icon={this.state.teamSelected ? this.state.teamSelected.icon : null}/>
|
||||
{this._getRegisterContentJsx()}
|
||||
<LoginFooter />
|
||||
</div>
|
||||
|
|
|
@ -145,27 +145,48 @@ module.exports = React.createClass({
|
|||
|
||||
if (imageUrl === this.state.defaultImageUrl) {
|
||||
const initialLetter = this._getInitialLetter(name);
|
||||
return (
|
||||
<span className="mx_BaseAvatar" {...otherProps}>
|
||||
<EmojiText className="mx_BaseAvatar_initial" aria-hidden="true"
|
||||
style={{ fontSize: (width * 0.65) + "px",
|
||||
width: width + "px",
|
||||
lineHeight: height + "px" }}>{initialLetter}</EmojiText>
|
||||
<img className="mx_BaseAvatar_image" src={imageUrl}
|
||||
alt="" title={title} onError={this.onError}
|
||||
width={width} height={height} />
|
||||
</span>
|
||||
const textNode = (
|
||||
<EmojiText className="mx_BaseAvatar_initial" aria-hidden="true"
|
||||
style={{ fontSize: (width * 0.65) + "px",
|
||||
width: width + "px",
|
||||
lineHeight: height + "px" }}
|
||||
>
|
||||
{initialLetter}
|
||||
</EmojiText>
|
||||
);
|
||||
const imgNode = (
|
||||
<img className="mx_BaseAvatar_image" src={imageUrl}
|
||||
alt="" title={title} onError={this.onError}
|
||||
width={width} height={height} />
|
||||
);
|
||||
if (onClick != null) {
|
||||
return (
|
||||
<AccessibleButton element='span' className="mx_BaseAvatar"
|
||||
onClick={onClick} {...otherProps}
|
||||
>
|
||||
{textNode}
|
||||
{imgNode}
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span className="mx_BaseAvatar" {...otherProps}>
|
||||
{textNode}
|
||||
{imgNode}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (onClick != null) {
|
||||
return (
|
||||
<AccessibleButton className="mx_BaseAvatar" onClick={onClick}>
|
||||
<img className="mx_BaseAvatar_image" src={imageUrl}
|
||||
onError={this.onError}
|
||||
width={width} height={height}
|
||||
title={title} alt=""
|
||||
{...otherProps} />
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
element='img'
|
||||
src={imageUrl}
|
||||
onClick={onClick}
|
||||
onError={this.onError}
|
||||
width={width} height={height}
|
||||
title={title} alt=""
|
||||
{...otherProps} />
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
|
|
|
@ -14,17 +14,18 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
var React = require("react");
|
||||
var classNames = require('classnames');
|
||||
var sdk = require("../../../index");
|
||||
var Invite = require("../../../Invite");
|
||||
var createRoom = require("../../../createRoom");
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var DMRoomMap = require('../../../utils/DMRoomMap');
|
||||
var rate_limited_func = require("../../../ratelimitedfunc");
|
||||
var dis = require("../../../dispatcher");
|
||||
var Modal = require('../../../Modal');
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import sdk from '../../../index';
|
||||
import { getAddressType, inviteMultipleToRoom } from '../../../Invite';
|
||||
import createRoom from '../../../createRoom';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import rate_limited_func from '../../../ratelimitedfunc';
|
||||
import dis from '../../../dispatcher';
|
||||
import Modal from '../../../Modal';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import q from 'q';
|
||||
|
||||
const TRUNCATE_QUERY_LIST = 40;
|
||||
|
||||
|
@ -186,13 +187,17 @@ module.exports = React.createClass({
|
|||
// If the query isn't a user we know about, but is a
|
||||
// valid address, add an entry for that
|
||||
if (queryList.length == 0) {
|
||||
const addrType = Invite.getAddressType(query);
|
||||
const addrType = getAddressType(query);
|
||||
if (addrType !== null) {
|
||||
queryList.push({
|
||||
queryList[0] = {
|
||||
addressType: addrType,
|
||||
address: query,
|
||||
isKnown: false,
|
||||
});
|
||||
};
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
if (addrType == 'email') {
|
||||
this._lookupThreepid(addrType, query).done();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -212,6 +217,7 @@ module.exports = React.createClass({
|
|||
inviteList: inviteList,
|
||||
queryList: [],
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -229,6 +235,7 @@ module.exports = React.createClass({
|
|||
inviteList: inviteList,
|
||||
queryList: [],
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
},
|
||||
|
||||
_getDirectMessageRoom: function(addr) {
|
||||
|
@ -266,7 +273,7 @@ module.exports = React.createClass({
|
|||
if (this.props.roomId) {
|
||||
// Invite new user to a room
|
||||
var self = this;
|
||||
Invite.inviteMultipleToRoom(this.props.roomId, addrTexts)
|
||||
inviteMultipleToRoom(this.props.roomId, addrTexts)
|
||||
.then(function(addrs) {
|
||||
var room = MatrixClientPeg.get().getRoom(self.props.roomId);
|
||||
return self._showAnyInviteErrors(addrs, room);
|
||||
|
@ -300,7 +307,7 @@ module.exports = React.createClass({
|
|||
var room;
|
||||
createRoom().then(function(roomId) {
|
||||
room = MatrixClientPeg.get().getRoom(roomId);
|
||||
return Invite.inviteMultipleToRoom(roomId, addrTexts);
|
||||
return inviteMultipleToRoom(roomId, addrTexts);
|
||||
})
|
||||
.then(function(addrs) {
|
||||
return self._showAnyInviteErrors(addrs, room);
|
||||
|
@ -380,7 +387,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_isDmChat: function(addrs) {
|
||||
if (addrs.length === 1 && Invite.getAddressType(addrs[0]) === "mx" && !this.props.roomId) {
|
||||
if (addrs.length === 1 && getAddressType(addrs[0]) === "mx" && !this.props.roomId) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
@ -408,7 +415,7 @@ module.exports = React.createClass({
|
|||
|
||||
_addInputToList: function() {
|
||||
const addressText = this.refs.textinput.value.trim();
|
||||
const addrType = Invite.getAddressType(addressText);
|
||||
const addrType = getAddressType(addressText);
|
||||
const addrObj = {
|
||||
addressType: addrType,
|
||||
address: addressText,
|
||||
|
@ -432,9 +439,45 @@ module.exports = React.createClass({
|
|||
inviteList: inviteList,
|
||||
queryList: [],
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
return inviteList;
|
||||
},
|
||||
|
||||
_lookupThreepid: function(medium, address) {
|
||||
let cancelled = false;
|
||||
// Note that we can't safely remove this after we're done
|
||||
// because we don't know that it's the same one, so we just
|
||||
// leave it: it's replacing the old one each time so it's
|
||||
// not like they leak.
|
||||
this._cancelThreepidLookup = function() {
|
||||
cancelled = true;
|
||||
}
|
||||
|
||||
// wait a bit to let the user finish typing
|
||||
return q.delay(500).then(() => {
|
||||
if (cancelled) return null;
|
||||
return MatrixClientPeg.get().lookupThreePid(medium, address);
|
||||
}).then((res) => {
|
||||
if (res === null || !res.mxid) return null;
|
||||
if (cancelled) return null;
|
||||
|
||||
return MatrixClientPeg.get().getProfileInfo(res.mxid);
|
||||
}).then((res) => {
|
||||
if (res === null) return null;
|
||||
if (cancelled) return null;
|
||||
this.setState({
|
||||
queryList: [{
|
||||
// an InviteAddressType
|
||||
addressType: medium,
|
||||
address: address,
|
||||
displayName: res.displayname,
|
||||
avatarMxc: res.avatar_url,
|
||||
isKnown: true,
|
||||
}]
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const AddressSelector = sdk.getComponent("elements.AddressSelector");
|
||||
|
|
178
src/components/views/dialogs/UnknownDeviceDialog.js
Normal file
178
src/components/views/dialogs/UnknownDeviceDialog.js
Normal file
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
|
||||
function DeviceListEntry(props) {
|
||||
const {userId, device} = props;
|
||||
|
||||
const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
|
||||
|
||||
return (
|
||||
<li>
|
||||
<DeviceVerifyButtons device={ device } userId={ userId } />
|
||||
{ device.deviceId }
|
||||
<br/>
|
||||
{ device.getDisplayName() }
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
DeviceListEntry.propTypes = {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
|
||||
// deviceinfo
|
||||
device: React.PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
||||
function UserUnknownDeviceList(props) {
|
||||
const {userId, userDevices} = props;
|
||||
|
||||
const deviceListEntries = Object.keys(userDevices).map((deviceId) =>
|
||||
<DeviceListEntry key={ deviceId } userId={ userId }
|
||||
device={ userDevices[deviceId] } />,
|
||||
);
|
||||
|
||||
return (
|
||||
<ul className="mx_UnknownDeviceDialog_deviceList">
|
||||
{deviceListEntries}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
UserUnknownDeviceList.propTypes = {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
|
||||
// map from deviceid -> deviceinfo
|
||||
userDevices: React.PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
||||
function UnknownDeviceList(props) {
|
||||
const {devices} = props;
|
||||
|
||||
const userListEntries = Object.keys(devices).map((userId) =>
|
||||
<li key={ userId }>
|
||||
<p>{ userId }:</p>
|
||||
<UserUnknownDeviceList userId={ userId } userDevices={ devices[userId] } />
|
||||
</li>,
|
||||
);
|
||||
|
||||
return <ul>{userListEntries}</ul>;
|
||||
}
|
||||
|
||||
UnknownDeviceList.propTypes = {
|
||||
// map from userid -> deviceid -> deviceinfo
|
||||
devices: React.PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'UnknownEventDialog',
|
||||
|
||||
propTypes: {
|
||||
room: React.PropTypes.object.isRequired,
|
||||
|
||||
// map from userid -> deviceid -> deviceinfo
|
||||
devices: React.PropTypes.object.isRequired,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
// Given we've now shown the user the unknown device, it is no longer
|
||||
// unknown to them. Therefore mark it as 'known'.
|
||||
Object.keys(this.props.devices).forEach((userId) => {
|
||||
Object.keys(this.props.devices[userId]).map((deviceId) => {
|
||||
MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
|
||||
});
|
||||
});
|
||||
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log('Opening UnknownDeviceDialog');
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const client = MatrixClientPeg.get();
|
||||
const blacklistUnverified = client.getGlobalBlacklistUnverifiedDevices() ||
|
||||
this.props.room.getBlacklistUnverifiedDevices();
|
||||
|
||||
let warning;
|
||||
if (blacklistUnverified) {
|
||||
warning = (
|
||||
<h4>
|
||||
You are currently blacklisting unverified devices; to send
|
||||
messages to these devices you must verify them.
|
||||
</h4>
|
||||
);
|
||||
} else {
|
||||
warning = (
|
||||
<div>
|
||||
<p>
|
||||
This means there is no guarantee that the devices
|
||||
belong to the users they claim to.
|
||||
</p>
|
||||
<p>
|
||||
We recommend you go through the verification process
|
||||
for each device before continuing, but you can resend
|
||||
the message without verifying if you prefer.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog className='mx_UnknownDeviceDialog'
|
||||
onFinished={() => {
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log("UnknownDeviceDialog closed by escape");
|
||||
this.props.onFinished();
|
||||
}}
|
||||
title='Room contains unknown devices'
|
||||
>
|
||||
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
|
||||
<h4>
|
||||
This room contains unknown devices which have not been
|
||||
verified.
|
||||
</h4>
|
||||
{ warning }
|
||||
Unknown devices:
|
||||
|
||||
<UnknownDeviceList devices={this.props.devices} />
|
||||
</GeminiScrollbar>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" autoFocus={ true }
|
||||
onClick={() => {
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log("UnknownDeviceDialog closed by OK");
|
||||
this.props.onFinished();
|
||||
}}>
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
// XXX: do we want to give the user the option to enable blacklistUnverifiedDevices for this room (or globally) at this point?
|
||||
// It feels like confused users will likely turn it on and then disappear in a cloud of UISIs...
|
||||
},
|
||||
});
|
|
@ -94,14 +94,14 @@ export default React.createClass({
|
|||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
const nameClasses = classNames({
|
||||
"mx_AddressTile_name": true,
|
||||
"mx_AddressTile_justified": this.props.justified,
|
||||
});
|
||||
|
||||
let info;
|
||||
let error = false;
|
||||
if (address.addressType === "mx" && address.isKnown) {
|
||||
const nameClasses = classNames({
|
||||
"mx_AddressTile_name": true,
|
||||
"mx_AddressTile_justified": this.props.justified,
|
||||
});
|
||||
|
||||
const idClasses = classNames({
|
||||
"mx_AddressTile_id": true,
|
||||
"mx_AddressTile_justified": this.props.justified,
|
||||
|
@ -123,13 +123,21 @@ export default React.createClass({
|
|||
<div className={unknownMxClasses}>{ this.props.address.address }</div>
|
||||
);
|
||||
} else if (address.addressType === "email") {
|
||||
var emailClasses = classNames({
|
||||
const emailClasses = classNames({
|
||||
"mx_AddressTile_email": true,
|
||||
"mx_AddressTile_justified": this.props.justified,
|
||||
});
|
||||
|
||||
let nameNode = null;
|
||||
if (address.displayName) {
|
||||
nameNode = <div className={nameClasses}>{ address.displayName }</div>
|
||||
}
|
||||
|
||||
info = (
|
||||
<div className={emailClasses}>{ address.address }</div>
|
||||
<div className="mx_AddressTile_mx">
|
||||
<div className={emailClasses}>{ address.address }</div>
|
||||
{nameNode}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
error = true;
|
||||
|
|
|
@ -27,6 +27,28 @@ export default React.createClass({
|
|||
device: React.PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
device: this.props.device
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
},
|
||||
|
||||
onDeviceVerificationChanged: function(userId, deviceId, deviceInfo) {
|
||||
if (userId === this.props.userId && deviceId === this.props.device.deviceId) {
|
||||
this.setState({ device: deviceInfo });
|
||||
}
|
||||
},
|
||||
|
||||
onVerifyClick: function() {
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
|
@ -41,9 +63,9 @@ export default React.createClass({
|
|||
</p>
|
||||
<div className="mx_UserSettings_cryptoSection">
|
||||
<ul>
|
||||
<li><label>Device name:</label> <span>{ this.props.device.getDisplayName() }</span></li>
|
||||
<li><label>Device ID:</label> <span><code>{ this.props.device.deviceId}</code></span></li>
|
||||
<li><label>Device key:</label> <span><code><b>{ this.props.device.getFingerprint() }</b></code></span></li>
|
||||
<li><label>Device name:</label> <span>{ this.state.device.getDisplayName() }</span></li>
|
||||
<li><label>Device ID:</label> <span><code>{ this.state.device.deviceId}</code></span></li>
|
||||
<li><label>Device key:</label> <span><code><b>{ this.state.device.getFingerprint() }</b></code></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
|
@ -60,7 +82,7 @@ export default React.createClass({
|
|||
onFinished: confirm=>{
|
||||
if (confirm) {
|
||||
MatrixClientPeg.get().setDeviceVerified(
|
||||
this.props.userId, this.props.device.deviceId, true
|
||||
this.props.userId, this.state.device.deviceId, true
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -69,26 +91,26 @@ export default React.createClass({
|
|||
|
||||
onUnverifyClick: function() {
|
||||
MatrixClientPeg.get().setDeviceVerified(
|
||||
this.props.userId, this.props.device.deviceId, false
|
||||
this.props.userId, this.state.device.deviceId, false
|
||||
);
|
||||
},
|
||||
|
||||
onBlacklistClick: function() {
|
||||
MatrixClientPeg.get().setDeviceBlocked(
|
||||
this.props.userId, this.props.device.deviceId, true
|
||||
this.props.userId, this.state.device.deviceId, true
|
||||
);
|
||||
},
|
||||
|
||||
onUnblacklistClick: function() {
|
||||
MatrixClientPeg.get().setDeviceBlocked(
|
||||
this.props.userId, this.props.device.deviceId, false
|
||||
this.props.userId, this.state.device.deviceId, false
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var blacklistButton = null, verifyButton = null;
|
||||
|
||||
if (this.props.device.isBlocked()) {
|
||||
if (this.state.device.isBlocked()) {
|
||||
blacklistButton = (
|
||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblacklist"
|
||||
onClick={this.onUnblacklistClick}>
|
||||
|
@ -104,7 +126,7 @@ export default React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
if (this.props.device.isVerified()) {
|
||||
if (this.state.device.isVerified()) {
|
||||
verifyButton = (
|
||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
|
||||
onClick={this.onUnverifyClick}>
|
||||
|
|
|
@ -385,7 +385,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
userEvents[userId].push({
|
||||
mxEvent: e,
|
||||
displayName: e.target.name || userId,
|
||||
displayName: (e.target ? e.target.name : null) || userId,
|
||||
index: index,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -44,8 +44,8 @@ module.exports = React.createClass({
|
|||
teams: React.PropTypes.arrayOf(React.PropTypes.shape({
|
||||
// The displayed name of the team
|
||||
"name": React.PropTypes.string,
|
||||
// The suffix with which every team email address ends
|
||||
"emailSuffix": React.PropTypes.string,
|
||||
// The domain of team email addresses
|
||||
"domain": React.PropTypes.string,
|
||||
})).required,
|
||||
}),
|
||||
|
||||
|
@ -117,9 +117,6 @@ module.exports = React.createClass({
|
|||
|
||||
_doSubmit: function() {
|
||||
let email = this.refs.email.value.trim();
|
||||
if (this.state.selectedTeam) {
|
||||
email += "@" + this.state.selectedTeam.emailSuffix;
|
||||
}
|
||||
var promise = this.props.onRegisterClick({
|
||||
username: this.refs.username.value.trim() || this.props.guestUsername,
|
||||
password: this.refs.password.value.trim(),
|
||||
|
@ -134,25 +131,6 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onSelectTeam: function(teamIndex) {
|
||||
let team = this._getSelectedTeam(teamIndex);
|
||||
if (team) {
|
||||
this.refs.email.value = this.refs.email.value.split("@")[0];
|
||||
}
|
||||
this.setState({
|
||||
selectedTeam: team,
|
||||
showSupportEmail: teamIndex === "other",
|
||||
});
|
||||
},
|
||||
|
||||
_getSelectedTeam: function(teamIndex) {
|
||||
if (this.props.teamsConfig &&
|
||||
this.props.teamsConfig.teams[teamIndex]) {
|
||||
return this.props.teamsConfig.teams[teamIndex];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if all fields were valid last time
|
||||
* they were validated.
|
||||
|
@ -167,20 +145,36 @@ module.exports = React.createClass({
|
|||
return true;
|
||||
},
|
||||
|
||||
_isUniEmail: function(email) {
|
||||
return email.endsWith('.ac.uk') || email.endsWith('.edu') || email.endsWith('matrix.org');
|
||||
},
|
||||
|
||||
validateField: function(field_id) {
|
||||
var pwd1 = this.refs.password.value.trim();
|
||||
var pwd2 = this.refs.passwordConfirm.value.trim();
|
||||
|
||||
switch (field_id) {
|
||||
case FIELD_EMAIL:
|
||||
let email = this.refs.email.value;
|
||||
if (this.props.teamsConfig) {
|
||||
let team = this.state.selectedTeam;
|
||||
if (team) {
|
||||
email = email + "@" + team.emailSuffix;
|
||||
}
|
||||
const email = this.refs.email.value;
|
||||
if (this.props.teamsConfig && this._isUniEmail(email)) {
|
||||
const matchingTeam = this.props.teamsConfig.teams.find(
|
||||
(team) => {
|
||||
return email.split('@').pop() === team.domain;
|
||||
}
|
||||
) || null;
|
||||
this.setState({
|
||||
selectedTeam: matchingTeam,
|
||||
showSupportEmail: !matchingTeam,
|
||||
});
|
||||
this.props.onTeamSelected(matchingTeam);
|
||||
} else {
|
||||
this.props.onTeamSelected(null);
|
||||
this.setState({
|
||||
selectedTeam: null,
|
||||
showSupportEmail: false,
|
||||
});
|
||||
}
|
||||
let valid = email === '' || Email.looksValid(email);
|
||||
const valid = email === '' || Email.looksValid(email);
|
||||
this.markFieldValid(field_id, valid, "RegistrationForm.ERR_EMAIL_INVALID");
|
||||
break;
|
||||
case FIELD_USERNAME:
|
||||
|
@ -260,61 +254,35 @@ module.exports = React.createClass({
|
|||
return cls;
|
||||
},
|
||||
|
||||
_renderEmailInputSuffix: function() {
|
||||
let suffix = null;
|
||||
if (!this.state.selectedTeam) {
|
||||
return suffix;
|
||||
}
|
||||
let team = this.state.selectedTeam;
|
||||
if (team) {
|
||||
suffix = "@" + team.emailSuffix;
|
||||
}
|
||||
return suffix;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
var emailSection, teamSection, teamAdditionSupport, registerButton;
|
||||
var emailSection, belowEmailSection, registerButton;
|
||||
if (this.props.showEmail) {
|
||||
let emailSuffix = this._renderEmailInputSuffix();
|
||||
emailSection = (
|
||||
<div>
|
||||
<input type="text" ref="email"
|
||||
autoFocus={true} placeholder="Email address (optional)"
|
||||
defaultValue={this.props.defaultEmail}
|
||||
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
|
||||
onBlur={function() {self.validateField(FIELD_EMAIL);}}
|
||||
value={self.state.email}/>
|
||||
{emailSuffix ? <input className="mx_Login_field" value={emailSuffix} disabled/> : null }
|
||||
</div>
|
||||
<input type="text" ref="email"
|
||||
autoFocus={true} placeholder="Email address (optional)"
|
||||
defaultValue={this.props.defaultEmail}
|
||||
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
|
||||
onBlur={function() {self.validateField(FIELD_EMAIL);}}
|
||||
value={self.state.email}/>
|
||||
);
|
||||
if (this.props.teamsConfig) {
|
||||
teamSection = (
|
||||
<select
|
||||
defaultValue="-1"
|
||||
className="mx_Login_field"
|
||||
onBlur={function() {self.validateField(FIELD_EMAIL);}}
|
||||
onChange={function(ev) {self.onSelectTeam(ev.target.value);}}
|
||||
>
|
||||
<option key="-1" value="-1">No team</option>
|
||||
{this.props.teamsConfig.teams.map((t, index) => {
|
||||
return (
|
||||
<option key={index} value={index}>
|
||||
{t.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
<option key="-2" value="other">Other</option>
|
||||
</select>
|
||||
);
|
||||
if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) {
|
||||
teamAdditionSupport = (
|
||||
<span>
|
||||
If your team is not listed, email
|
||||
belowEmailSection = (
|
||||
<p className="mx_Login_support">
|
||||
Sorry, but your university is not registered with us just yet.
|
||||
Email us on
|
||||
<a href={"mailto:" + this.props.teamsConfig.supportEmail}>
|
||||
{this.props.teamsConfig.supportEmail}
|
||||
</a>
|
||||
</span>
|
||||
</a>
|
||||
to get your university signed up. Or continue to register with Riot to enjoy our open source platform.
|
||||
</p>
|
||||
);
|
||||
} else if (this.state.selectedTeam) {
|
||||
belowEmailSection = (
|
||||
<p className="mx_Login_support">
|
||||
You are registering with {this.state.selectedTeam.name}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -333,11 +301,8 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
{teamSection}
|
||||
{teamAdditionSupport}
|
||||
<br />
|
||||
{emailSection}
|
||||
<br />
|
||||
{belowEmailSection}
|
||||
<input type="text" ref="username"
|
||||
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername}
|
||||
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
|
||||
|
|
|
@ -40,6 +40,7 @@ import * as HtmlUtils from '../../../HtmlUtils';
|
|||
import Autocomplete from './Autocomplete';
|
||||
import {Completion} from "../../../autocomplete/Autocompleter";
|
||||
import Markdown from '../../../Markdown';
|
||||
import {onSendMessageFailed} from './MessageComposerInputOld';
|
||||
|
||||
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
|
||||
|
||||
|
@ -553,15 +554,11 @@ export default class MessageComposerInput extends React.Component {
|
|||
sendMessagePromise = sendTextFn.call(this.client, this.props.room.roomId, contentText);
|
||||
}
|
||||
|
||||
sendMessagePromise.then(() => {
|
||||
sendMessagePromise.done((res) => {
|
||||
dis.dispatch({
|
||||
action: 'message_sent',
|
||||
});
|
||||
}, () => {
|
||||
dis.dispatch({
|
||||
action: 'message_send_failed',
|
||||
});
|
||||
});
|
||||
}, (e) => onSendMessageFailed(e, this.props.room));
|
||||
|
||||
this.setState({
|
||||
editorState: this.createEditorState(),
|
||||
|
|
|
@ -29,10 +29,31 @@ var TYPING_USER_TIMEOUT = 10000;
|
|||
var TYPING_SERVER_TIMEOUT = 30000;
|
||||
var MARKDOWN_ENABLED = true;
|
||||
|
||||
export function onSendMessageFailed(err, room) {
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log('MessageComposer got send failure: ' + err.name + '('+err+')');
|
||||
if (err.name === "UnknownDeviceError") {
|
||||
const UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog");
|
||||
Modal.createDialog(UnknownDeviceDialog, {
|
||||
devices: err.devices,
|
||||
room: room,
|
||||
onFinished: (r) => {
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log('UnknownDeviceDialog closed with '+r);
|
||||
},
|
||||
}, "mx_Dialog_unknownDevice");
|
||||
}
|
||||
dis.dispatch({
|
||||
action: 'message_send_failed',
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* The textInput part of the MessageComposer
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
export default React.createClass({
|
||||
displayName: 'MessageComposerInput',
|
||||
|
||||
statics: {
|
||||
|
@ -331,21 +352,18 @@ module.exports = React.createClass({
|
|||
MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, htmlText);
|
||||
}
|
||||
else {
|
||||
const contentText = mdown.toPlaintext();
|
||||
if (mdown) contentText = mdown.toPlaintext();
|
||||
sendMessagePromise = isEmote ?
|
||||
MatrixClientPeg.get().sendEmoteMessage(this.props.room.roomId, contentText) :
|
||||
MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText);
|
||||
}
|
||||
|
||||
sendMessagePromise.done(function() {
|
||||
sendMessagePromise.done(function(res) {
|
||||
dis.dispatch({
|
||||
action: 'message_sent'
|
||||
});
|
||||
}, function() {
|
||||
dis.dispatch({
|
||||
action: 'message_send_failed'
|
||||
});
|
||||
});
|
||||
}, (e) => onSendMessageFailed(e, this.props.room));
|
||||
|
||||
this.refs.textarea.value = '';
|
||||
this.resizeInput();
|
||||
ev.preventDefault();
|
||||
|
|
|
@ -170,15 +170,15 @@ module.exports = React.createClass({
|
|||
|
||||
let title;
|
||||
if (this.props.timestamp) {
|
||||
let suffix = " (" + this.props.member.userId + ")";
|
||||
const prefix = "Seen by " + this.props.member.userId + " at ";
|
||||
let ts = new Date(this.props.timestamp);
|
||||
if (this.props.showFullTimestamp) {
|
||||
// "15/12/2016, 7:05:45 PM (@alice:matrix.org)"
|
||||
title = ts.toLocaleString() + suffix;
|
||||
title = prefix + ts.toLocaleString();
|
||||
}
|
||||
else {
|
||||
// "7:05:45 PM (@alice:matrix.org)"
|
||||
title = ts.toLocaleTimeString() + suffix;
|
||||
title = prefix + ts.toLocaleTimeString();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -192,9 +192,9 @@ module.exports = React.createClass({
|
|||
width={14} height={14} resizeMethod="crop"
|
||||
style={style}
|
||||
title={title}
|
||||
onClick={this.props.onClick}
|
||||
/>
|
||||
</Velociraptor>
|
||||
);
|
||||
/* onClick={this.props.onClick} */
|
||||
},
|
||||
});
|
||||
|
|
|
@ -301,8 +301,8 @@ module.exports = React.createClass({
|
|||
var rightPanel_buttons;
|
||||
if (this.props.collapsedRhs) {
|
||||
rightPanel_buttons =
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title="<">
|
||||
<TintableSvg src="img/minimise.svg" width="10" height="16"/>
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title="Show panel">
|
||||
<TintableSvg src="img/maximise.svg" width="10" height="16"/>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
|
|
|
@ -146,7 +146,7 @@ module.exports = React.createClass({
|
|||
<div>
|
||||
<div className="mx_RoomPreviewBar_join_text">
|
||||
You are trying to access { name }.<br/>
|
||||
Would you like to <a onClick={ this.props.onJoinClick }>join</a> in order to participate in the discussion?
|
||||
<a onClick={ this.props.onJoinClick }><b>Click here</b></a> to join the discussion!
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -24,6 +24,8 @@ var ObjectUtils = require("../../../ObjectUtils");
|
|||
var dis = require("../../../dispatcher");
|
||||
var ScalarAuthClient = require("../../../ScalarAuthClient");
|
||||
var ScalarMessaging = require('../../../ScalarMessaging');
|
||||
var UserSettingsStore = require('../../../UserSettingsStore');
|
||||
|
||||
|
||||
// parse a string as an integer; if the input is undefined, or cannot be parsed
|
||||
// as an integer, return a default.
|
||||
|
@ -228,11 +230,13 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
// encryption
|
||||
p = this.saveEncryption();
|
||||
p = this.saveEnableEncryption();
|
||||
if (!q.isFulfilled(p)) {
|
||||
promises.push(p);
|
||||
}
|
||||
|
||||
this.saveBlacklistUnverifiedDevicesPerRoom();
|
||||
|
||||
console.log("Performing %s operations: %s", promises.length, JSON.stringify(promises));
|
||||
return promises;
|
||||
},
|
||||
|
@ -252,7 +256,7 @@ module.exports = React.createClass({
|
|||
return this.refs.url_preview_settings.saveSettings();
|
||||
},
|
||||
|
||||
saveEncryption: function() {
|
||||
saveEnableEncryption: function() {
|
||||
if (!this.refs.encrypt) { return q(); }
|
||||
|
||||
var encrypt = this.refs.encrypt.checked;
|
||||
|
@ -265,6 +269,29 @@ module.exports = React.createClass({
|
|||
);
|
||||
},
|
||||
|
||||
saveBlacklistUnverifiedDevicesPerRoom: function() {
|
||||
if (!this.refs.blacklistUnverified) return;
|
||||
if (this._isRoomBlacklistUnverified() !== this.refs.blacklistUnverified.checked) {
|
||||
this._setRoomBlacklistUnverified(this.refs.blacklistUnverified.checked);
|
||||
}
|
||||
},
|
||||
|
||||
_isRoomBlacklistUnverified: function() {
|
||||
var blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom;
|
||||
if (blacklistUnverifiedDevicesPerRoom) {
|
||||
return blacklistUnverifiedDevicesPerRoom[this.props.room.roomId];
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
_setRoomBlacklistUnverified: function(value) {
|
||||
var blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom || {};
|
||||
blacklistUnverifiedDevicesPerRoom[this.props.room.roomId] = value;
|
||||
UserSettingsStore.setLocalSetting('blacklistUnverifiedDevicesPerRoom', blacklistUnverifiedDevicesPerRoom);
|
||||
|
||||
this.props.room.setBlacklistUnverifiedDevices(value);
|
||||
},
|
||||
|
||||
_hasDiff: function(strA, strB) {
|
||||
// treat undefined as an empty string because other components may blindly
|
||||
// call setName("") when there has been no diff made to the name!
|
||||
|
@ -477,26 +504,42 @@ module.exports = React.createClass({
|
|||
var cli = MatrixClientPeg.get();
|
||||
var roomState = this.props.room.currentState;
|
||||
var isEncrypted = cli.isRoomEncrypted(this.props.room.roomId);
|
||||
var isGlobalBlacklistUnverified = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevices;
|
||||
var isRoomBlacklistUnverified = this._isRoomBlacklistUnverified();
|
||||
|
||||
var settings =
|
||||
<label>
|
||||
<input type="checkbox" ref="blacklistUnverified"
|
||||
defaultChecked={ isGlobalBlacklistUnverified || isRoomBlacklistUnverified }
|
||||
disabled={ isGlobalBlacklistUnverified || (this.refs.encrypt && !this.refs.encrypt.checked) }/>
|
||||
Never send encrypted messages to unverified devices in this room from this device.
|
||||
</label>;
|
||||
|
||||
if (!isEncrypted &&
|
||||
roomState.mayClientSendStateEvent("m.room.encryption", cli)) {
|
||||
return (
|
||||
<label>
|
||||
<input type="checkbox" ref="encrypt" onClick={ this.onEnableEncryptionClick }/>
|
||||
<img className="mx_RoomSettings_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12" />
|
||||
Enable encryption (warning: cannot be disabled again!)
|
||||
</label>
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox" ref="encrypt" onClick={ this.onEnableEncryptionClick }/>
|
||||
<img className="mx_RoomSettings_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12" />
|
||||
Enable encryption (warning: cannot be disabled again!)
|
||||
</label>
|
||||
{ settings }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<label>
|
||||
{ isEncrypted
|
||||
? <img className="mx_RoomSettings_e2eIcon" src="img/e2e-verified.svg" width="10" height="12" />
|
||||
: <img className="mx_RoomSettings_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12" />
|
||||
}
|
||||
Encryption is { isEncrypted ? "" : "not " } enabled in this room.
|
||||
</label>
|
||||
<div>
|
||||
<label>
|
||||
{ isEncrypted
|
||||
? <img className="mx_RoomSettings_e2eIcon" src="img/e2e-verified.svg" width="10" height="12" />
|
||||
: <img className="mx_RoomSettings_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12" />
|
||||
}
|
||||
Encryption is { isEncrypted ? "" : "not " } enabled in this room.
|
||||
</label>
|
||||
{ settings }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -50,7 +50,7 @@ export function decryptMegolmKeyFile(data, password) {
|
|||
}
|
||||
|
||||
const ciphertextLength = body.length-(1+16+16+4+32);
|
||||
if (body.length < 0) {
|
||||
if (ciphertextLength < 0) {
|
||||
throw new Error('Invalid file: too short');
|
||||
}
|
||||
|
||||
|
@ -102,19 +102,19 @@ export function decryptMegolmKeyFile(data, password) {
|
|||
*/
|
||||
export function encryptMegolmKeyFile(data, password, options) {
|
||||
options = options || {};
|
||||
const kdf_rounds = options.kdf_rounds || 100000;
|
||||
const kdf_rounds = options.kdf_rounds || 500000;
|
||||
|
||||
const salt = new Uint8Array(16);
|
||||
window.crypto.getRandomValues(salt);
|
||||
|
||||
// clear bit 63 of the salt to stop us hitting the 64-bit counter boundary
|
||||
// (which would mean we wouldn't be able to decrypt on Android). The loss
|
||||
// of a single bit of salt is a price we have to pay.
|
||||
salt[9] &= 0x7f;
|
||||
|
||||
const iv = new Uint8Array(16);
|
||||
window.crypto.getRandomValues(iv);
|
||||
|
||||
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
|
||||
// (which would mean we wouldn't be able to decrypt on Android). The loss
|
||||
// of a single bit of iv is a price we have to pay.
|
||||
iv[9] &= 0x7f;
|
||||
|
||||
return deriveKeys(salt, kdf_rounds, password).then((keys) => {
|
||||
const [aes_key, hmac_key] = keys;
|
||||
|
||||
|
@ -164,6 +164,7 @@ export function encryptMegolmKeyFile(data, password, options) {
|
|||
* @return {Promise<[CryptoKey, CryptoKey]>} promise for [aes key, hmac key]
|
||||
*/
|
||||
function deriveKeys(salt, iterations, password) {
|
||||
const start = new Date();
|
||||
return subtleCrypto.importKey(
|
||||
'raw',
|
||||
new TextEncoder().encode(password),
|
||||
|
@ -182,6 +183,9 @@ function deriveKeys(salt, iterations, password) {
|
|||
512
|
||||
);
|
||||
}).then((keybits) => {
|
||||
const now = new Date();
|
||||
console.log("E2e import/export: deriveKeys took " + (now - start) + "ms");
|
||||
|
||||
const aes_key = keybits.slice(0, 32);
|
||||
const hmac_key = keybits.slice(32);
|
||||
|
||||
|
|
|
@ -42,17 +42,12 @@ describe('RoomView', function () {
|
|||
it('resolves a room alias to a room id', function (done) {
|
||||
peg.get().getRoomIdForAlias.returns(q({room_id: "!randomcharacters:aser.ver"}));
|
||||
|
||||
var onRoomIdResolved = sinon.spy();
|
||||
function onRoomIdResolved(room_id) {
|
||||
expect(room_id).toEqual("!randomcharacters:aser.ver");
|
||||
done();
|
||||
}
|
||||
|
||||
ReactDOM.render(<RoomView roomAddress="#alias:ser.ver" onRoomIdResolved={onRoomIdResolved} />, parentDiv);
|
||||
|
||||
process.nextTick(function() {
|
||||
// These expect()s don't read very well and don't give very good failure
|
||||
// messages, but expect's toHaveBeenCalled only takes an expect spy object,
|
||||
// not a sinon spy object.
|
||||
expect(onRoomIdResolved.called).toExist();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('joins by alias if given an alias', function (done) {
|
||||
|
|
|
@ -73,6 +73,7 @@ var Tester = React.createClass({
|
|||
|
||||
/* returns a promise which will resolve when the fill happens */
|
||||
awaitFill: function(dir) {
|
||||
console.log("ScrollPanel Tester: awaiting " + dir + " fill");
|
||||
var defer = q.defer();
|
||||
this._fillDefers[dir] = defer;
|
||||
return defer.promise;
|
||||
|
@ -80,7 +81,7 @@ var Tester = React.createClass({
|
|||
|
||||
_onScroll: function(ev) {
|
||||
var st = ev.target.scrollTop;
|
||||
console.log("Scroll event; scrollTop: " + st);
|
||||
console.log("ScrollPanel Tester: scroll event; scrollTop: " + st);
|
||||
this.lastScrollEvent = st;
|
||||
|
||||
var d = this._scrollDefer;
|
||||
|
@ -159,10 +160,29 @@ describe('ScrollPanel', function() {
|
|||
scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass(
|
||||
tester, "gm-scroll-view");
|
||||
|
||||
// wait for a browser tick to let the initial paginates complete
|
||||
setTimeout(function() {
|
||||
done();
|
||||
}, 0);
|
||||
// we need to make sure we don't call done() until q has finished
|
||||
// running the completion handlers from the fill requests. We can't
|
||||
// just use .done(), because that will end up ahead of those handlers
|
||||
// in the queue. We can't use window.setTimeout(0), because that also might
|
||||
// run ahead of those handlers.
|
||||
const sp = tester.scrollPanel();
|
||||
let retriesRemaining = 1;
|
||||
const awaitReady = function() {
|
||||
return q().then(() => {
|
||||
if (sp._pendingFillRequests.b === false &&
|
||||
sp._pendingFillRequests.f === false
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (retriesRemaining == 0) {
|
||||
throw new Error("fillRequests did not complete");
|
||||
}
|
||||
retriesRemaining--;
|
||||
return awaitReady();
|
||||
});
|
||||
};
|
||||
awaitReady().done(done);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
|
|
|
@ -99,7 +99,11 @@ describe('TimelinePanel', function() {
|
|||
// the document so that we can interact with it properly.
|
||||
parentDiv = document.createElement('div');
|
||||
parentDiv.style.width = '800px';
|
||||
parentDiv.style.height = '600px';
|
||||
|
||||
// This has to be slightly carefully chosen. We expect to have to do
|
||||
// exactly one pagination to fill it.
|
||||
parentDiv.style.height = '500px';
|
||||
|
||||
parentDiv.style.overflow = 'hidden';
|
||||
document.body.appendChild(parentDiv);
|
||||
});
|
||||
|
@ -235,7 +239,7 @@ describe('TimelinePanel', function() {
|
|||
expect(client.paginateEventTimeline.callCount).toEqual(0);
|
||||
done();
|
||||
}, 0);
|
||||
}, 0);
|
||||
}, 10);
|
||||
});
|
||||
|
||||
it("should let you scroll down to the bottom after you've scrolled up", function(done) {
|
||||
|
|
|
@ -14,7 +14,15 @@ var MatrixEvent = jssdk.MatrixEvent;
|
|||
*/
|
||||
export function beforeEach(context) {
|
||||
var desc = context.currentTest.fullTitle();
|
||||
|
||||
console.log();
|
||||
|
||||
// this puts a mark in the chrome devtools timeline, which can help
|
||||
// figure out what's been going on.
|
||||
if (console.timeStamp) {
|
||||
console.timeStamp(desc);
|
||||
}
|
||||
|
||||
console.log(desc);
|
||||
console.log(new Array(1 + desc.length).join("="));
|
||||
};
|
||||
|
|
|
@ -75,6 +75,16 @@ describe('MegolmExportEncryption', function() {
|
|||
.toThrow('Trailer line not found');
|
||||
});
|
||||
|
||||
it('should handle a too-short body', function() {
|
||||
const input=stringToArray(`-----BEGIN MEGOLM SESSION DATA-----
|
||||
AXNhbHRzYWx0c2FsdHNhbHSIiIiIiIiIiIiIiIiIiIiIAAAACmIRUW2OjZ3L2l6j9h0lHlV3M2dx
|
||||
cissyYBxjsfsAn
|
||||
-----END MEGOLM SESSION DATA-----
|
||||
`);
|
||||
expect(()=>{MegolmExportEncryption.decryptMegolmKeyFile(input, '')})
|
||||
.toThrow('Invalid file: too short');
|
||||
});
|
||||
|
||||
it('should decrypt a range of inputs', function(done) {
|
||||
function next(i) {
|
||||
if (i >= TEST_VECTORS.length) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue