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
|
* things that are errors in the js-sdk config that the current
|
||||||
* code does not adhere to, turned down to warn
|
* 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"],
|
"valid-jsdoc": ["warn"],
|
||||||
"new-cap": ["warn"],
|
"new-cap": ["warn"],
|
||||||
"key-spacing": ["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)
|
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)
|
[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',
|
devtool: 'inline-source-map',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
webpackMiddleware: {
|
||||||
|
stats: {
|
||||||
|
// don't fill the console up with a mahoosive list of modules
|
||||||
|
chunks: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
browserNoActivityTimeout: 15000,
|
browserNoActivityTimeout: 15000,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "0.8.5",
|
"version": "0.8.6",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
|
@ -290,7 +290,7 @@ export function bodyToHtml(content, highlights, opts) {
|
||||||
}
|
}
|
||||||
|
|
||||||
EMOJI_REGEX.lastIndex = 0;
|
EMOJI_REGEX.lastIndex = 0;
|
||||||
let contentBodyTrimmed = content.body.trim();
|
let contentBodyTrimmed = content.body !== undefined ? content.body.trim() : '';
|
||||||
let match = EMOJI_REGEX.exec(contentBodyTrimmed);
|
let match = EMOJI_REGEX.exec(contentBodyTrimmed);
|
||||||
let emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length;
|
let emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length;
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ import UserActivity from './UserActivity';
|
||||||
import Presence from './Presence';
|
import Presence from './Presence';
|
||||||
import dis from './dispatcher';
|
import dis from './dispatcher';
|
||||||
import DMRoomMap from './utils/DMRoomMap';
|
import DMRoomMap from './utils/DMRoomMap';
|
||||||
|
import RtsClient from './RtsClient';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called at startup, to attempt to build a logged-in Matrix session. It tries
|
* 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
|
* Transitions to a logged-in state using the given credentials
|
||||||
* @param {MatrixClientCreds} credentials The credentials to use
|
* @param {MatrixClientCreds} credentials The credentials to use
|
||||||
|
@ -261,6 +267,19 @@ export function setLoggedIn(credentials) {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Error using local storage: can't persist session!", 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 {
|
} else {
|
||||||
console.warn("No local storage available: can't persist session!");
|
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 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
|
* a given message actually uses any markdown syntax or whether
|
||||||
* it's plain text.
|
* it's plain text.
|
||||||
*/
|
*/
|
||||||
export default class Markdown {
|
export default class Markdown {
|
||||||
constructor(input) {
|
constructor(input) {
|
||||||
this.input = 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() {
|
isPlainText() {
|
||||||
// we determine if the message requires markdown by
|
const walker = this.parsed.walker();
|
||||||
// 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;
|
|
||||||
|
|
||||||
function setNotPlain() {
|
let ev;
|
||||||
is_plain = false;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toHTML() {
|
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
|
// If there is only one top level node, just return the
|
||||||
// bare text: it's a single line of text and so should be
|
// bare text: it's a single line of text and so should be
|
||||||
// 'inline', rather than unnecessarily wrapped in its own
|
// 'inline', rather than unnecessarily wrapped in its own
|
||||||
// p tag. If, however, we have multiple nodes, each gets
|
// p tag. If, however, we have multiple nodes, each gets
|
||||||
// its own p tag to keep them as separate paragraphs.
|
// its own p tag to keep them as separate paragraphs.
|
||||||
var par = node;
|
if (is_multi_line(node)) {
|
||||||
while (par.parent) {
|
|
||||||
par = par.parent;
|
|
||||||
}
|
|
||||||
if (par.firstChild != par.lastChild) {
|
|
||||||
real_paragraph.call(this, node, entering);
|
real_paragraph.call(this, node, entering);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var parsed = this.parser.parse(this.input);
|
renderer.html_inline = html_if_tag_allowed;
|
||||||
var rendered = this.renderer.render(parsed);
|
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() {
|
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
|
// The default `out` function only sends the input through an XML
|
||||||
// escaping function, which causes messages to be entity encoded,
|
// escaping function, which causes messages to be entity encoded,
|
||||||
// which we don't want in this case.
|
// 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.
|
// The `lit` function adds a string literal to the output buffer.
|
||||||
this.lit(s);
|
this.lit(s);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.renderer.paragraph = function(node, entering) {
|
renderer.paragraph = function(node, entering) {
|
||||||
// If there is only one top level node, just return the
|
// as with toHTML, only append lines to paragraphs if there are
|
||||||
// bare text: it's a single line of text and so should be
|
// multiple paragraphs
|
||||||
// 'inline', rather than unnecessarily wrapped in its own
|
if (is_multi_line(node)) {
|
||||||
// p tag. If, however, we have multiple nodes, each gets
|
if (!entering && node.next) {
|
||||||
// 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) {
|
|
||||||
this.lit('\n\n');
|
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);
|
return renderer.render(this.parsed);
|
||||||
var rendered = this.renderer.render(parsed);
|
|
||||||
|
|
||||||
this.renderer.paragraph = real_paragraph;
|
|
||||||
|
|
||||||
return rendered;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,13 @@ const AsyncWrapper = React.createClass({
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this._unmounted = false;
|
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) => {
|
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) {
|
if (this._unmounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -177,7 +183,7 @@ class ModalManager {
|
||||||
|
|
||||||
var modal = this._modals[0];
|
var modal = this._modals[0];
|
||||||
var dialog = (
|
var dialog = (
|
||||||
<div className={"mx_Dialog_wrapper " + modal.className}>
|
<div className={"mx_Dialog_wrapper " + (modal.className ? modal.className : '') }>
|
||||||
<div className="mx_Dialog">
|
<div className="mx_Dialog">
|
||||||
{modal.elem}
|
{modal.elem}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
/** The types of page which can be shown by the LoggedInView */
|
/** The types of page which can be shown by the LoggedInView */
|
||||||
export default {
|
export default {
|
||||||
|
HomePage: "home_page",
|
||||||
RoomView: "room_view",
|
RoomView: "room_view",
|
||||||
UserSettings: "user_settings",
|
UserSettings: "user_settings",
|
||||||
CreateRoom: "create_room",
|
CreateRoom: "create_room",
|
||||||
|
|
|
@ -16,17 +16,35 @@ limitations under the License.
|
||||||
|
|
||||||
var MatrixClientPeg = require('./MatrixClientPeg');
|
var MatrixClientPeg = require('./MatrixClientPeg');
|
||||||
var dis = require('./dispatcher');
|
var dis = require('./dispatcher');
|
||||||
|
var sdk = require('./index');
|
||||||
|
var Modal = require('./Modal');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
resend: function(event) {
|
resend: function(event) {
|
||||||
MatrixClientPeg.get().resendEvent(
|
MatrixClientPeg.get().resendEvent(
|
||||||
event, MatrixClientPeg.get().getRoom(event.getRoomId())
|
event, MatrixClientPeg.get().getRoom(event.getRoomId())
|
||||||
).done(function() {
|
).done(function(res) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'message_sent',
|
action: 'message_sent',
|
||||||
event: event
|
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({
|
dis.dispatch({
|
||||||
action: 'message_send_failed',
|
action: 'message_send_failed',
|
||||||
event: event
|
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;
|
this.params.idSid = idSid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setReferrer(referrer) {
|
||||||
|
this.params.referrer = referrer;
|
||||||
|
}
|
||||||
|
|
||||||
setGuestAccessToken(token) {
|
setGuestAccessToken(token) {
|
||||||
this.guestAccessToken = token;
|
this.guestAccessToken = token;
|
||||||
}
|
}
|
||||||
|
|
|
@ -136,6 +136,11 @@ class EmailIdentityStage extends Stage {
|
||||||
"&session_id=" +
|
"&session_id=" +
|
||||||
encodeURIComponent(this.signupInstance.getServerData().session);
|
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;
|
var self = this;
|
||||||
return this.client.requestRegisterEmailToken(
|
return this.client.requestRegisterEmailToken(
|
||||||
this.signupInstance.email,
|
this.signupInstance.email,
|
||||||
|
|
|
@ -149,6 +149,23 @@ module.exports = {
|
||||||
return MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings);
|
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 {
|
isFeatureEnabled: function(feature: string): boolean {
|
||||||
// Disable labs for guests.
|
// Disable labs for guests.
|
||||||
if (MatrixClientPeg.get().isGuest()) return false;
|
if (MatrixClientPeg.get().isGuest()) return false;
|
||||||
|
|
|
@ -62,11 +62,11 @@ module.exports = React.createClass({
|
||||||
oldNode.style.visibility = c.props.style.visibility;
|
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);
|
//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;
|
self.children[c.key] = old;
|
||||||
} else {
|
} else {
|
||||||
// new element. If we have a startStyle, use that as the style and go through
|
// 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");
|
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -32,10 +48,11 @@ module.exports = {
|
||||||
return whoIsTyping;
|
return whoIsTyping;
|
||||||
},
|
},
|
||||||
|
|
||||||
whoIsTypingString: function(room, limit) {
|
whoIsTypingString: function(whoIsTyping, limit) {
|
||||||
const whoIsTyping = this.usersTypingApartFromMe(room);
|
let othersCount = 0;
|
||||||
const othersCount = limit === undefined ?
|
if (whoIsTyping.length > limit) {
|
||||||
0 : Math.max(whoIsTyping.length - limit, 0);
|
othersCount = whoIsTyping.length - limit + 1;
|
||||||
|
}
|
||||||
if (whoIsTyping.length == 0) {
|
if (whoIsTyping.length == 0) {
|
||||||
return '';
|
return '';
|
||||||
} else if (whoIsTyping.length == 1) {
|
} else if (whoIsTyping.length == 1) {
|
||||||
|
@ -46,7 +63,7 @@ module.exports = {
|
||||||
});
|
});
|
||||||
if (othersCount) {
|
if (othersCount) {
|
||||||
const other = ' other' + (othersCount > 1 ? 's' : '');
|
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';
|
othersCount + other + ' are typing';
|
||||||
} else {
|
} else {
|
||||||
const lastPerson = names.pop();
|
const lastPerson = names.pop();
|
||||||
|
|
|
@ -71,7 +71,7 @@ export default React.createClass({
|
||||||
return this.props.matrixClient.exportRoomKeys();
|
return this.props.matrixClient.exportRoomKeys();
|
||||||
}).then((k) => {
|
}).then((k) => {
|
||||||
return MegolmExportEncryption.encryptMegolmKeyFile(
|
return MegolmExportEncryption.encryptMegolmKeyFile(
|
||||||
JSON.stringify(k), passphrase
|
JSON.stringify(k), passphrase,
|
||||||
);
|
);
|
||||||
}).then((f) => {
|
}).then((f) => {
|
||||||
const blob = new Blob([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() {
|
render: function() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
|
|
||||||
|
|
||||||
const disableForm = (this.state.phase === PHASE_EXPORTING);
|
const disableForm = (this.state.phase === PHASE_EXPORTING);
|
||||||
|
|
||||||
|
@ -159,10 +164,9 @@ export default React.createClass({
|
||||||
<input className='mx_Dialog_primary' type='submit' value='Export'
|
<input className='mx_Dialog_primary' type='submit' value='Export'
|
||||||
disabled={disableForm}
|
disabled={disableForm}
|
||||||
/>
|
/>
|
||||||
<AccessibleButton element='button' onClick={this.props.onFinished}
|
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||||
disabled={disableForm}>
|
|
||||||
Cancel
|
Cancel
|
||||||
</AccessibleButton>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
|
@ -80,7 +80,7 @@ export default React.createClass({
|
||||||
|
|
||||||
return readFileAsArrayBuffer(file).then((arrayBuffer) => {
|
return readFileAsArrayBuffer(file).then((arrayBuffer) => {
|
||||||
return MegolmExportEncryption.decryptMegolmKeyFile(
|
return MegolmExportEncryption.decryptMegolmKeyFile(
|
||||||
arrayBuffer, passphrase
|
arrayBuffer, passphrase,
|
||||||
);
|
);
|
||||||
}).then((keys) => {
|
}).then((keys) => {
|
||||||
return this.props.matrixClient.importRoomKeys(JSON.parse(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() {
|
render: function() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
|
|
||||||
|
|
||||||
const disableForm = (this.state.phase !== PHASE_EDIT);
|
const disableForm = (this.state.phase !== PHASE_EDIT);
|
||||||
|
|
||||||
|
@ -158,10 +163,9 @@ export default React.createClass({
|
||||||
<input className='mx_Dialog_primary' type='submit' value='Import'
|
<input className='mx_Dialog_primary' type='submit' value='Import'
|
||||||
disabled={!this.state.enableSubmit || disableForm}
|
disabled={!this.state.enableSubmit || disableForm}
|
||||||
/>
|
/>
|
||||||
<AccessibleButton element='button' onClick={this.props.onFinished}
|
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||||
disabled={disableForm}>
|
|
||||||
Cancel
|
Cancel
|
||||||
</AccessibleButton>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</BaseDialog>
|
</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);
|
views$dialogs$SetDisplayNameDialog && (module.exports.components['views.dialogs.SetDisplayNameDialog'] = views$dialogs$SetDisplayNameDialog);
|
||||||
import views$dialogs$TextInputDialog from './components/views/dialogs/TextInputDialog';
|
import views$dialogs$TextInputDialog from './components/views/dialogs/TextInputDialog';
|
||||||
views$dialogs$TextInputDialog && (module.exports.components['views.dialogs.TextInputDialog'] = 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';
|
import views$elements$AccessibleButton from './components/views/elements/AccessibleButton';
|
||||||
views$elements$AccessibleButton && (module.exports.components['views.elements.AccessibleButton'] = views$elements$AccessibleButton);
|
views$elements$AccessibleButton && (module.exports.components['views.elements.AccessibleButton'] = views$elements$AccessibleButton);
|
||||||
import views$elements$AddressSelector from './components/views/elements/AddressSelector';
|
import views$elements$AddressSelector from './components/views/elements/AddressSelector';
|
||||||
|
|
|
@ -105,6 +105,7 @@ var FilePanel = React.createClass({
|
||||||
showUrlPreview = { false }
|
showUrlPreview = { false }
|
||||||
tileShape="file_grid"
|
tileShape="file_grid"
|
||||||
opacity={ this.props.opacity }
|
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,
|
onRoomCreated: React.PropTypes.func,
|
||||||
onUserSettingsClose: React.PropTypes.func,
|
onUserSettingsClose: React.PropTypes.func,
|
||||||
|
|
||||||
|
teamToken: React.PropTypes.string,
|
||||||
|
|
||||||
// and lots and lots of other stuff.
|
// and lots and lots of other stuff.
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -137,6 +139,7 @@ export default React.createClass({
|
||||||
var UserSettings = sdk.getComponent('structures.UserSettings');
|
var UserSettings = sdk.getComponent('structures.UserSettings');
|
||||||
var CreateRoom = sdk.getComponent('structures.CreateRoom');
|
var CreateRoom = sdk.getComponent('structures.CreateRoom');
|
||||||
var RoomDirectory = sdk.getComponent('structures.RoomDirectory');
|
var RoomDirectory = sdk.getComponent('structures.RoomDirectory');
|
||||||
|
var HomePage = sdk.getComponent('structures.HomePage');
|
||||||
var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
||||||
var GuestWarningBar = sdk.getComponent('globals.GuestWarningBar');
|
var GuestWarningBar = sdk.getComponent('globals.GuestWarningBar');
|
||||||
var NewVersionBar = sdk.getComponent('globals.NewVersionBar');
|
var NewVersionBar = sdk.getComponent('globals.NewVersionBar');
|
||||||
|
@ -171,6 +174,7 @@ export default React.createClass({
|
||||||
brand={this.props.config.brand}
|
brand={this.props.config.brand}
|
||||||
collapsedRhs={this.props.collapse_rhs}
|
collapsedRhs={this.props.collapse_rhs}
|
||||||
enableLabs={this.props.config.enableLabs}
|
enableLabs={this.props.config.enableLabs}
|
||||||
|
referralBaseUrl={this.props.config.referralBaseUrl}
|
||||||
/>;
|
/>;
|
||||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
|
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
|
||||||
break;
|
break;
|
||||||
|
@ -190,6 +194,16 @@ export default React.createClass({
|
||||||
/>;
|
/>;
|
||||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
|
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
|
||||||
break;
|
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:
|
case PageTypes.UserView:
|
||||||
page_element = null; // deliberately null for now
|
page_element = null; // deliberately null for now
|
||||||
right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.sideOpacity} />;
|
right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.sideOpacity} />;
|
||||||
|
@ -218,7 +232,12 @@ export default React.createClass({
|
||||||
<div className='mx_MatrixChat_wrapper'>
|
<div className='mx_MatrixChat_wrapper'>
|
||||||
{topBar}
|
{topBar}
|
||||||
<div className={bodyClasses}>
|
<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'>
|
<main className='mx_MatrixChat_middlePanel'>
|
||||||
{page_element}
|
{page_element}
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -190,6 +190,20 @@ module.exports = React.createClass({
|
||||||
if (this.props.config.sync_timeline_limit) {
|
if (this.props.config.sync_timeline_limit) {
|
||||||
MatrixClientPeg.opts.initialSyncLimit = 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() {
|
componentDidMount: function() {
|
||||||
|
@ -210,6 +224,12 @@ module.exports = React.createClass({
|
||||||
window.addEventListener('resize', this.handleResize);
|
window.addEventListener('resize', this.handleResize);
|
||||||
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
|
// the extra q() ensures that synchronous exceptions hit the same codepath as
|
||||||
// asynchronous ones.
|
// asynchronous ones.
|
||||||
q().then(() => {
|
q().then(() => {
|
||||||
|
@ -421,6 +441,10 @@ module.exports = React.createClass({
|
||||||
this._setPage(PageTypes.RoomDirectory);
|
this._setPage(PageTypes.RoomDirectory);
|
||||||
this.notifyNewScreen('directory');
|
this.notifyNewScreen('directory');
|
||||||
break;
|
break;
|
||||||
|
case 'view_home_page':
|
||||||
|
this._setPage(PageTypes.HomePage);
|
||||||
|
this.notifyNewScreen('home');
|
||||||
|
break;
|
||||||
case 'view_create_chat':
|
case 'view_create_chat':
|
||||||
this._createChat();
|
this._createChat();
|
||||||
break;
|
break;
|
||||||
|
@ -690,7 +714,11 @@ module.exports = React.createClass({
|
||||||
)[0].roomId;
|
)[0].roomId;
|
||||||
self.setState({ready: true, currentRoomId: firstRoom, page_type: PageTypes.RoomView});
|
self.setState({ready: true, currentRoomId: firstRoom, page_type: PageTypes.RoomView});
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
self.setState({ready: true, page_type: PageTypes.RoomView});
|
self.setState({ready: true, page_type: PageTypes.RoomView});
|
||||||
|
@ -710,7 +738,11 @@ module.exports = React.createClass({
|
||||||
} else {
|
} else {
|
||||||
// There is no information on presentedId
|
// There is no information on presentedId
|
||||||
// so point user to fallback like /directory
|
// 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'});
|
dis.dispatch({action: 'focus_composer'});
|
||||||
|
@ -774,6 +806,10 @@ module.exports = React.createClass({
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_user_settings',
|
action: 'view_user_settings',
|
||||||
});
|
});
|
||||||
|
} else if (screen == 'home') {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_home_page',
|
||||||
|
});
|
||||||
} else if (screen == 'directory') {
|
} else if (screen == 'directory') {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room_directory',
|
action: 'view_room_directory',
|
||||||
|
@ -1033,6 +1069,7 @@ module.exports = React.createClass({
|
||||||
onRoomIdResolved={this.onRoomIdResolved}
|
onRoomIdResolved={this.onRoomIdResolved}
|
||||||
onRoomCreated={this.onRoomCreated}
|
onRoomCreated={this.onRoomCreated}
|
||||||
onUserSettingsClose={this.onUserSettingsClose}
|
onUserSettingsClose={this.onUserSettingsClose}
|
||||||
|
teamToken={this._teamToken}
|
||||||
{...this.props}
|
{...this.props}
|
||||||
{...this.state}
|
{...this.state}
|
||||||
/>
|
/>
|
||||||
|
@ -1055,12 +1092,13 @@ module.exports = React.createClass({
|
||||||
sessionId={this.state.register_session_id}
|
sessionId={this.state.register_session_id}
|
||||||
idSid={this.state.register_id_sid}
|
idSid={this.state.register_id_sid}
|
||||||
email={this.props.startingFragmentQueryParams.email}
|
email={this.props.startingFragmentQueryParams.email}
|
||||||
|
referrer={this.props.startingFragmentQueryParams.referrer}
|
||||||
username={this.state.upgradeUsername}
|
username={this.state.upgradeUsername}
|
||||||
guestAccessToken={this.state.guestAccessToken}
|
guestAccessToken={this.state.guestAccessToken}
|
||||||
defaultHsUrl={this.getDefaultHsUrl()}
|
defaultHsUrl={this.getDefaultHsUrl()}
|
||||||
defaultIsUrl={this.getDefaultIsUrl()}
|
defaultIsUrl={this.getDefaultIsUrl()}
|
||||||
brand={this.props.config.brand}
|
brand={this.props.config.brand}
|
||||||
teamsConfig={this.props.config.teamsConfig}
|
teamServerConfig={this.props.config.teamServerConfig}
|
||||||
customHsUrl={this.getCurrentHsUrl()}
|
customHsUrl={this.getCurrentHsUrl()}
|
||||||
customIsUrl={this.getCurrentIsUrl()}
|
customIsUrl={this.getCurrentIsUrl()}
|
||||||
registrationUrl={this.props.registrationUrl}
|
registrationUrl={this.props.registrationUrl}
|
||||||
|
|
|
@ -48,6 +48,7 @@ var NotificationPanel = React.createClass({
|
||||||
showUrlPreview = { false }
|
showUrlPreview = { false }
|
||||||
opacity={ this.props.opacity }
|
opacity={ this.props.opacity }
|
||||||
tileShape="notif"
|
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
|
// the number of messages which have arrived since we've been scrolled up
|
||||||
numUnreadMessages: React.PropTypes.number,
|
numUnreadMessages: React.PropTypes.number,
|
||||||
|
|
||||||
// true if there are messages in the room which had errors on send
|
// string to display when there are messages in the room which had errors on send
|
||||||
hasUnsentMessages: React.PropTypes.bool,
|
unsentMessageError: React.PropTypes.string,
|
||||||
|
|
||||||
// this is true if we are fully scrolled-down, and are looking at
|
// this is true if we are fully scrolled-down, and are looking at
|
||||||
// the end of the live timeline.
|
// 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
|
// callback for when the status bar can be hidden from view, as it is
|
||||||
// not displaying anything
|
// not displaying anything
|
||||||
onHidden: React.PropTypes.func,
|
onHidden: React.PropTypes.func,
|
||||||
|
|
||||||
// callback for when the status bar is displaying something and should
|
// callback for when the status bar is displaying something and should
|
||||||
// be visible
|
// be visible
|
||||||
onVisible: React.PropTypes.func,
|
onVisible: React.PropTypes.func,
|
||||||
|
@ -81,17 +82,14 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
return {
|
return {
|
||||||
whoIsTypingLimit: 2,
|
whoIsTypingLimit: 3,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
syncState: MatrixClientPeg.get().getSyncState(),
|
syncState: MatrixClientPeg.get().getSyncState(),
|
||||||
whoisTypingString: WhoIsTyping.whoIsTypingString(
|
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
|
||||||
this.props.room,
|
|
||||||
this.props.whoIsTypingLimit
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -105,7 +103,7 @@ module.exports = React.createClass({
|
||||||
this.props.onResize();
|
this.props.onResize();
|
||||||
}
|
}
|
||||||
|
|
||||||
const size = this._getSize(this.state, this.props);
|
const size = this._getSize(this.props, this.state);
|
||||||
if (size > 0) {
|
if (size > 0) {
|
||||||
this.props.onVisible();
|
this.props.onVisible();
|
||||||
} else {
|
} else {
|
||||||
|
@ -113,7 +111,9 @@ module.exports = React.createClass({
|
||||||
clearTimeout(this.hideDebouncer);
|
clearTimeout(this.hideDebouncer);
|
||||||
}
|
}
|
||||||
this.hideDebouncer = setTimeout(() => {
|
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);
|
}, HIDE_DEBOUNCE_MS);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -138,26 +138,23 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onRoomMemberTyping: function(ev, member) {
|
onRoomMemberTyping: function(ev, member) {
|
||||||
this.setState({
|
this.setState({
|
||||||
whoisTypingString: WhoIsTyping.whoIsTypingString(
|
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
|
||||||
this.props.room,
|
|
||||||
this.props.whoIsTypingLimit
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// We don't need the actual height - just whether it is likely to have
|
// 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
|
// changed - so we use '0' to indicate normal size, and other values to
|
||||||
// indicate other sizes.
|
// indicate other sizes.
|
||||||
_getSize: function(state, props) {
|
_getSize: function(props, state) {
|
||||||
if (state.syncState === "ERROR" ||
|
if (state.syncState === "ERROR" ||
|
||||||
state.whoisTypingString ||
|
(state.usersTyping.length > 0) ||
|
||||||
props.numUnreadMessages ||
|
props.numUnreadMessages ||
|
||||||
!props.atEndOfLiveTimeline ||
|
!props.atEndOfLiveTimeline ||
|
||||||
props.hasActiveCall) {
|
props.hasActiveCall) {
|
||||||
return STATUS_BAR_EXPANDED;
|
return STATUS_BAR_EXPANDED;
|
||||||
} else if (props.tabCompleteEntries) {
|
} else if (props.tabCompleteEntries) {
|
||||||
return STATUS_BAR_HIDDEN;
|
return STATUS_BAR_HIDDEN;
|
||||||
} else if (props.hasUnsentMessages) {
|
} else if (props.unsentMessageError) {
|
||||||
return STATUS_BAR_EXPANDED_LARGE;
|
return STATUS_BAR_EXPANDED_LARGE;
|
||||||
}
|
}
|
||||||
return STATUS_BAR_HIDDEN;
|
return STATUS_BAR_HIDDEN;
|
||||||
|
@ -166,7 +163,8 @@ module.exports = React.createClass({
|
||||||
// determine if we need to call onResize
|
// determine if we need to call onResize
|
||||||
_checkForResize: function(prevProps, prevState) {
|
_checkForResize: function(prevProps, prevState) {
|
||||||
// figure out the old height and the new height of the status bar.
|
// 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.
|
// return suitable content for the image on the left of the status bar.
|
||||||
|
@ -217,10 +215,13 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderTypingIndicatorAvatars: function(limit) {
|
_renderTypingIndicatorAvatars: function(limit) {
|
||||||
let users = WhoIsTyping.usersTypingApartFromMe(this.props.room);
|
let users = this.state.usersTyping;
|
||||||
|
|
||||||
let othersCount = Math.max(users.length - limit, 0);
|
let othersCount = 0;
|
||||||
users = users.slice(0, limit);
|
if (users.length > limit) {
|
||||||
|
othersCount = users.length - limit + 1;
|
||||||
|
users = users.slice(0, limit - 1);
|
||||||
|
}
|
||||||
|
|
||||||
let avatars = users.map((u, index) => {
|
let avatars = users.map((u, index) => {
|
||||||
let showInitial = othersCount === 0 && index === users.length - 1;
|
let showInitial = othersCount === 0 && index === users.length - 1;
|
||||||
|
@ -238,7 +239,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
if (othersCount > 0) {
|
if (othersCount > 0) {
|
||||||
avatars.push(
|
avatars.push(
|
||||||
<span className="mx_RoomStatusBar_typingIndicatorRemaining">
|
<span className="mx_RoomStatusBar_typingIndicatorRemaining" key="others">
|
||||||
+{othersCount}
|
+{othersCount}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
@ -285,12 +286,12 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.hasUnsentMessages) {
|
if (this.props.unsentMessageError) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar">
|
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||||
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ "/>
|
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ "/>
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||||
Some of your messages have not been sent.
|
{ this.props.unsentMessageError }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||||
<a className="mx_RoomStatusBar_resend_link"
|
<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) {
|
if (typingString) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomStatusBar_typingBar">
|
<div className="mx_RoomStatusBar_typingBar">
|
||||||
|
@ -344,7 +348,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var content = this._getContent();
|
var content = this._getContent();
|
||||||
var indicator = this._getIndicator(this.state.whoisTypingString !== null);
|
var indicator = this._getIndicator(this.state.usersTyping.length > 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomStatusBar">
|
<div className="mx_RoomStatusBar">
|
||||||
|
|
|
@ -128,7 +128,7 @@ module.exports = React.createClass({
|
||||||
draggingFile: false,
|
draggingFile: false,
|
||||||
searching: false,
|
searching: false,
|
||||||
searchResults: null,
|
searchResults: null,
|
||||||
hasUnsentMessages: false,
|
unsentMessageError: '',
|
||||||
callState: null,
|
callState: null,
|
||||||
guestsCanJoin: false,
|
guestsCanJoin: false,
|
||||||
canPeek: false,
|
canPeek: false,
|
||||||
|
@ -182,7 +182,7 @@ module.exports = React.createClass({
|
||||||
room: room,
|
room: room,
|
||||||
roomId: result.room_id,
|
roomId: result.room_id,
|
||||||
roomLoading: !room,
|
roomLoading: !room,
|
||||||
hasUnsentMessages: this._hasUnsentMessages(room),
|
unsentMessageError: this._getUnsentMessageError(room),
|
||||||
}, this._onHaveRoom);
|
}, this._onHaveRoom);
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -196,7 +196,7 @@ module.exports = React.createClass({
|
||||||
roomId: this.props.roomAddress,
|
roomId: this.props.roomAddress,
|
||||||
room: room,
|
room: room,
|
||||||
roomLoading: !room,
|
roomLoading: !room,
|
||||||
hasUnsentMessages: this._hasUnsentMessages(room),
|
unsentMessageError: this._getUnsentMessageError(room),
|
||||||
}, this._onHaveRoom);
|
}, this._onHaveRoom);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -397,7 +397,7 @@ module.exports = React.createClass({
|
||||||
case 'message_sent':
|
case 'message_sent':
|
||||||
case 'message_send_cancelled':
|
case 'message_send_cancelled':
|
||||||
this.setState({
|
this.setState({
|
||||||
hasUnsentMessages: this._hasUnsentMessages(this.state.room)
|
unsentMessageError: this._getUnsentMessageError(this.state.room),
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'notifier_enabled':
|
case 'notifier_enabled':
|
||||||
|
@ -636,8 +636,15 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
}, 500),
|
}, 500),
|
||||||
|
|
||||||
_hasUnsentMessages: function(room) {
|
_getUnsentMessageError: function(room) {
|
||||||
return this._getUnsentMessages(room).length > 0;
|
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) {
|
_getUnsentMessages: function(room) {
|
||||||
|
@ -1332,12 +1339,14 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onStatusBarVisible: function() {
|
onStatusBarVisible: function() {
|
||||||
|
if (this.unmounted) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
statusBarVisible: true,
|
statusBarVisible: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onStatusBarHidden: function() {
|
onStatusBarHidden: function() {
|
||||||
|
if (this.unmounted) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
statusBarVisible: false,
|
statusBarVisible: false,
|
||||||
});
|
});
|
||||||
|
@ -1507,18 +1516,19 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
|
|
||||||
var statusBar;
|
var statusBar;
|
||||||
|
let isStatusAreaExpanded = true;
|
||||||
|
|
||||||
if (ContentMessages.getCurrentUploads().length > 0) {
|
if (ContentMessages.getCurrentUploads().length > 0) {
|
||||||
var UploadBar = sdk.getComponent('structures.UploadBar');
|
var UploadBar = sdk.getComponent('structures.UploadBar');
|
||||||
statusBar = <UploadBar room={this.state.room} />;
|
statusBar = <UploadBar room={this.state.room} />;
|
||||||
} else if (!this.state.searchResults) {
|
} else if (!this.state.searchResults) {
|
||||||
var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar');
|
var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar');
|
||||||
|
isStatusAreaExpanded = this.state.statusBarVisible;
|
||||||
statusBar = <RoomStatusBar
|
statusBar = <RoomStatusBar
|
||||||
room={this.state.room}
|
room={this.state.room}
|
||||||
tabComplete={this.tabComplete}
|
tabComplete={this.tabComplete}
|
||||||
numUnreadMessages={this.state.numUnreadMessages}
|
numUnreadMessages={this.state.numUnreadMessages}
|
||||||
hasUnsentMessages={this.state.hasUnsentMessages}
|
unsentMessageError={this.state.unsentMessageError}
|
||||||
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}
|
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}
|
||||||
hasActiveCall={inCall}
|
hasActiveCall={inCall}
|
||||||
onResendAllClick={this.onResendAllClick}
|
onResendAllClick={this.onResendAllClick}
|
||||||
|
@ -1527,7 +1537,7 @@ module.exports = React.createClass({
|
||||||
onResize={this.onChildResize}
|
onResize={this.onChildResize}
|
||||||
onVisible={this.onStatusBarVisible}
|
onVisible={this.onStatusBarVisible}
|
||||||
onHidden={this.onStatusBarHidden}
|
onHidden={this.onStatusBarHidden}
|
||||||
whoIsTypingLimit={2}
|
whoIsTypingLimit={3}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1683,7 +1693,7 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let statusBarAreaClass = "mx_RoomView_statusArea mx_fadable";
|
let statusBarAreaClass = "mx_RoomView_statusArea mx_fadable";
|
||||||
if (this.state.statusBarVisible) {
|
if (isStatusAreaExpanded) {
|
||||||
statusBarAreaClass += " mx_RoomView_statusArea_expanded";
|
statusBarAreaClass += " mx_RoomView_statusArea_expanded";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ var DEBUG_SCROLL = false;
|
||||||
|
|
||||||
// The amount of extra scroll distance to allow prior to unfilling.
|
// The amount of extra scroll distance to allow prior to unfilling.
|
||||||
// See _getExcessHeight.
|
// See _getExcessHeight.
|
||||||
const UNPAGINATION_PADDING = 1500;
|
const UNPAGINATION_PADDING = 3000;
|
||||||
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent
|
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent
|
||||||
// many scroll events causing many unfilling requests.
|
// many scroll events causing many unfilling requests.
|
||||||
const UNFILL_REQUEST_DEBOUNCE_MS = 200;
|
const UNFILL_REQUEST_DEBOUNCE_MS = 200;
|
||||||
|
@ -570,7 +570,7 @@ module.exports = React.createClass({
|
||||||
var boundingRect = node.getBoundingClientRect();
|
var boundingRect = node.getBoundingClientRect();
|
||||||
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
|
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+")");
|
pixelOffset + " (delta: "+scrollDelta+")");
|
||||||
|
|
||||||
if(scrollDelta != 0) {
|
if(scrollDelta != 0) {
|
||||||
|
@ -582,7 +582,7 @@ module.exports = React.createClass({
|
||||||
_saveScrollState: function() {
|
_saveScrollState: function() {
|
||||||
if (this.props.stickyBottom && this.isAtBottom()) {
|
if (this.props.stickyBottom && this.isAtBottom()) {
|
||||||
this.scrollState = { stuckAtBottom: true };
|
this.scrollState = { stuckAtBottom: true };
|
||||||
debuglog("Saved scroll state", this.scrollState);
|
debuglog("ScrollPanel: Saved scroll state", this.scrollState);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -601,12 +601,12 @@ module.exports = React.createClass({
|
||||||
trackedScrollToken: node.dataset.scrollToken,
|
trackedScrollToken: node.dataset.scrollToken,
|
||||||
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
|
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
|
||||||
};
|
};
|
||||||
debuglog("Saved scroll state", this.scrollState);
|
debuglog("ScrollPanel: saved scroll state", this.scrollState);
|
||||||
return;
|
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() {
|
_restoreSavedScrollState: function() {
|
||||||
|
@ -640,7 +640,7 @@ module.exports = React.createClass({
|
||||||
this._lastSetScroll = scrollNode.scrollTop;
|
this._lastSetScroll = scrollNode.scrollTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
debuglog("Set scrollTop:", scrollNode.scrollTop,
|
debuglog("ScrollPanel: set scrollTop:", scrollNode.scrollTop,
|
||||||
"requested:", scrollTop,
|
"requested:", scrollTop,
|
||||||
"_lastSetScroll:", this._lastSetScroll);
|
"_lastSetScroll:", this._lastSetScroll);
|
||||||
},
|
},
|
||||||
|
|
|
@ -96,6 +96,9 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
// shape property to be passed to EventTiles
|
// shape property to be passed to EventTiles
|
||||||
tileShape: React.PropTypes.string,
|
tileShape: React.PropTypes.string,
|
||||||
|
|
||||||
|
// placeholder text to use if the timeline is empty
|
||||||
|
empty: React.PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
statics: {
|
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
|
// give the messagepanel a stickybottom if we're at the end of the
|
||||||
// live timeline, so that the arrival of new events triggers a
|
// live timeline, so that the arrival of new events triggers a
|
||||||
// scroll.
|
// scroll.
|
||||||
|
|
|
@ -90,8 +90,8 @@ module.exports = React.createClass({displayName: 'UploadBar',
|
||||||
<div className="mx_UploadBar_uploadProgressOuter">
|
<div className="mx_UploadBar_uploadProgressOuter">
|
||||||
<div className="mx_UploadBar_uploadProgressInner" style={innerProgressStyle}></div>
|
<div className="mx_UploadBar_uploadProgressInner" style={innerProgressStyle}></div>
|
||||||
</div>
|
</div>
|
||||||
<img className="mx_UploadBar_uploadIcon" src="img/fileicon.png" width="17" height="22"/>
|
<img className="mx_UploadBar_uploadIcon mx_filterFlipColor" 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_uploadCancel mx_filterFlipColor" src="img/cancel.svg" width="18" height="18"
|
||||||
onClick={function() { ContentMessages.cancelUpload(upload.promise); }}
|
onClick={function() { ContentMessages.cancelUpload(upload.promise); }}
|
||||||
/>
|
/>
|
||||||
<div className="mx_UploadBar_uploadBytes">
|
<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.
|
// Enumerate the available themes, with a nice human text label.
|
||||||
// 'id' gives the key name in the im.vector.web.settings account data event
|
// 'id' gives the key name in the im.vector.web.settings account data event
|
||||||
// 'value' is the value for that key in the 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
|
// True to show the 'labs' section of experimental features
|
||||||
enableLabs: React.PropTypes.bool,
|
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
|
// true if RightPanel is collapsed
|
||||||
collapsedRhs: React.PropTypes.bool,
|
collapsedRhs: React.PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
@ -148,6 +163,8 @@ module.exports = React.createClass({
|
||||||
syncedSettings.theme = 'light';
|
syncedSettings.theme = 'light';
|
||||||
}
|
}
|
||||||
this._syncedSettings = syncedSettings;
|
this._syncedSettings = syncedSettings;
|
||||||
|
|
||||||
|
this._localSettings = UserSettingsStore.getLocalSettings();
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
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() {
|
_renderUserInterfaceSettings: function() {
|
||||||
var client = MatrixClientPeg.get();
|
var client = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
@ -514,21 +552,20 @@ module.exports = React.createClass({
|
||||||
const deviceId = client.deviceId;
|
const deviceId = client.deviceId;
|
||||||
const identityKey = client.getDeviceEd25519Key() || "<not supported>";
|
const identityKey = client.getDeviceEd25519Key() || "<not supported>";
|
||||||
|
|
||||||
let exportButton = null,
|
let importExportButtons = null;
|
||||||
importButton = null;
|
|
||||||
|
|
||||||
if (client.isCryptoEnabled) {
|
if (client.isCryptoEnabled) {
|
||||||
exportButton = (
|
importExportButtons = (
|
||||||
<AccessibleButton className="mx_UserSettings_button"
|
<div className="mx_UserSettings_importExportButtons">
|
||||||
onClick={this._onExportE2eKeysClicked}>
|
<AccessibleButton className="mx_UserSettings_button"
|
||||||
Export E2E room keys
|
onClick={this._onExportE2eKeysClicked}>
|
||||||
</AccessibleButton>
|
Export E2E room keys
|
||||||
);
|
</AccessibleButton>
|
||||||
importButton = (
|
<AccessibleButton className="mx_UserSettings_button"
|
||||||
<AccessibleButton className="mx_UserSettings_button"
|
onClick={this._onImportE2eKeysClicked}>
|
||||||
onClick={this._onImportE2eKeysClicked}>
|
Import E2E room keys
|
||||||
Import E2E room keys
|
</AccessibleButton>
|
||||||
</AccessibleButton>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
@ -539,13 +576,36 @@ module.exports = React.createClass({
|
||||||
<li><label>Device ID:</label> <span><code>{deviceId}</code></span></li>
|
<li><label>Device ID:</label> <span><code>{deviceId}</code></span></li>
|
||||||
<li><label>Device key:</label> <span><code><b>{identityKey}</b></code></span></li>
|
<li><label>Device key:</label> <span><code><b>{identityKey}</b></code></span></li>
|
||||||
</ul>
|
</ul>
|
||||||
{exportButton}
|
{ importExportButtons }
|
||||||
{importButton}
|
</div>
|
||||||
|
<div className="mx_UserSettings_section">
|
||||||
|
{ CRYPTO_SETTINGS_LABELS.map( this._renderLocalSetting ) }
|
||||||
</div>
|
</div>
|
||||||
</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() {
|
_renderDevicesPanel: function() {
|
||||||
var DevicesPanel = sdk.getComponent('settings.DevicesPanel');
|
var DevicesPanel = sdk.getComponent('settings.DevicesPanel');
|
||||||
return (
|
return (
|
||||||
|
@ -819,6 +879,8 @@ module.exports = React.createClass({
|
||||||
{accountJsx}
|
{accountJsx}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{this._renderReferral()}
|
||||||
|
|
||||||
{notification_area}
|
{notification_area}
|
||||||
|
|
||||||
{this._renderUserInterfaceSettings()}
|
{this._renderUserInterfaceSettings()}
|
||||||
|
@ -842,7 +904,7 @@ module.exports = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_advanced">
|
<div className="mx_UserSettings_advanced">
|
||||||
matrix-react-sdk version: {REACT_SDK_VERSION}<br/>
|
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/>
|
olm version: {olmVersionString}<br/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -25,6 +25,7 @@ var ServerConfig = require("../../views/login/ServerConfig");
|
||||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
var RegistrationForm = require("../../views/login/RegistrationForm");
|
var RegistrationForm = require("../../views/login/RegistrationForm");
|
||||||
var CaptchaForm = require("../../views/login/CaptchaForm");
|
var CaptchaForm = require("../../views/login/CaptchaForm");
|
||||||
|
var RtsClient = require("../../../RtsClient");
|
||||||
|
|
||||||
var MIN_PASSWORD_LENGTH = 6;
|
var MIN_PASSWORD_LENGTH = 6;
|
||||||
|
|
||||||
|
@ -47,23 +48,16 @@ module.exports = React.createClass({
|
||||||
defaultIsUrl: React.PropTypes.string,
|
defaultIsUrl: React.PropTypes.string,
|
||||||
brand: React.PropTypes.string,
|
brand: React.PropTypes.string,
|
||||||
email: React.PropTypes.string,
|
email: React.PropTypes.string,
|
||||||
|
referrer: React.PropTypes.string,
|
||||||
username: React.PropTypes.string,
|
username: React.PropTypes.string,
|
||||||
guestAccessToken: React.PropTypes.string,
|
guestAccessToken: React.PropTypes.string,
|
||||||
teamsConfig: React.PropTypes.shape({
|
teamServerConfig: React.PropTypes.shape({
|
||||||
// Email address to request new teams
|
// Email address to request new teams
|
||||||
supportEmail: React.PropTypes.string,
|
supportEmail: React.PropTypes.string.isRequired,
|
||||||
teams: React.PropTypes.arrayOf(React.PropTypes.shape({
|
// URL of the riot-team-server to get team configurations and track referrals
|
||||||
// The displayed name of the team
|
teamServerURL: React.PropTypes.string.isRequired,
|
||||||
"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,
|
|
||||||
}),
|
}),
|
||||||
|
teamSelected: React.PropTypes.object,
|
||||||
|
|
||||||
defaultDeviceDisplayName: React.PropTypes.string,
|
defaultDeviceDisplayName: React.PropTypes.string,
|
||||||
|
|
||||||
|
@ -75,6 +69,7 @@ module.exports = React.createClass({
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
busy: false,
|
busy: false,
|
||||||
|
teamServerBusy: false,
|
||||||
errorText: null,
|
errorText: null,
|
||||||
// We remember the values entered by the user because
|
// We remember the values entered by the user because
|
||||||
// the registration form will be unmounted during the
|
// the registration form will be unmounted during the
|
||||||
|
@ -90,6 +85,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
this._unmounted = false;
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
// attach this to the instance rather than this.state since it isn't UI
|
// attach this to the instance rather than this.state since it isn't UI
|
||||||
this.registerLogic = new Signup.Register(
|
this.registerLogic = new Signup.Register(
|
||||||
|
@ -102,11 +98,44 @@ module.exports = React.createClass({
|
||||||
this.registerLogic.setRegistrationUrl(this.props.registrationUrl);
|
this.registerLogic.setRegistrationUrl(this.props.registrationUrl);
|
||||||
this.registerLogic.setIdSid(this.props.idSid);
|
this.registerLogic.setIdSid(this.props.idSid);
|
||||||
this.registerLogic.setGuestAccessToken(this.props.guestAccessToken);
|
this.registerLogic.setGuestAccessToken(this.props.guestAccessToken);
|
||||||
|
if (this.props.referrer) {
|
||||||
|
this.registerLogic.setReferrer(this.props.referrer);
|
||||||
|
}
|
||||||
this.registerLogic.recheckState();
|
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() {
|
componentWillUnmount: function() {
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
|
this._unmounted = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
|
@ -184,24 +213,41 @@ module.exports = React.createClass({
|
||||||
accessToken: response.access_token
|
accessToken: response.access_token
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-join rooms
|
if (
|
||||||
if (self.props.teamsConfig && self.props.teamsConfig.teams) {
|
self._rtsClient &&
|
||||||
for (let i = 0; i < self.props.teamsConfig.teams.length; i++) {
|
self.props.referrer &&
|
||||||
let team = self.props.teamsConfig.teams[i];
|
self.state.teamSelected
|
||||||
if (self.state.formVals.email.endsWith(team.emailSuffix)) {
|
) {
|
||||||
console.log("User successfully registered with team " + team.name);
|
// 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) {
|
if (!team.rooms) {
|
||||||
break;
|
return;
|
||||||
}
|
}
|
||||||
|
// Auto-join rooms
|
||||||
team.rooms.forEach((room) => {
|
team.rooms.forEach((room) => {
|
||||||
if (room.autoJoin) {
|
if (room.auto_join && room.room_id) {
|
||||||
console.log("Auto-joining " + room.id);
|
console.log(`Auto-joining ${room.room_id}`);
|
||||||
MatrixClientPeg.get().joinRoom(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) {
|
if (self.props.brand) {
|
||||||
|
@ -273,7 +319,15 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onTeamSelected: function(teamSelected) {
|
||||||
|
if (!this._unmounted) {
|
||||||
|
this.setState({ teamSelected });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
_getRegisterContentJsx: function() {
|
_getRegisterContentJsx: function() {
|
||||||
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
|
||||||
var currStep = this.registerLogic.getStep();
|
var currStep = this.registerLogic.getStep();
|
||||||
var registerStep;
|
var registerStep;
|
||||||
switch (currStep) {
|
switch (currStep) {
|
||||||
|
@ -283,17 +337,23 @@ module.exports = React.createClass({
|
||||||
case "Register.STEP_m.login.dummy":
|
case "Register.STEP_m.login.dummy":
|
||||||
// NB. Our 'username' prop is specifically for upgrading
|
// NB. Our 'username' prop is specifically for upgrading
|
||||||
// a guest account
|
// a guest account
|
||||||
|
if (this.state.teamServerBusy) {
|
||||||
|
registerStep = <Spinner />;
|
||||||
|
break;
|
||||||
|
}
|
||||||
registerStep = (
|
registerStep = (
|
||||||
<RegistrationForm
|
<RegistrationForm
|
||||||
showEmail={true}
|
showEmail={true}
|
||||||
defaultUsername={this.state.formVals.username}
|
defaultUsername={this.state.formVals.username}
|
||||||
defaultEmail={this.state.formVals.email}
|
defaultEmail={this.state.formVals.email}
|
||||||
defaultPassword={this.state.formVals.password}
|
defaultPassword={this.state.formVals.password}
|
||||||
teamsConfig={this.props.teamsConfig}
|
teamsConfig={this.state.teamsConfig}
|
||||||
guestUsername={this.props.username}
|
guestUsername={this.props.username}
|
||||||
minPasswordLength={MIN_PASSWORD_LENGTH}
|
minPasswordLength={MIN_PASSWORD_LENGTH}
|
||||||
onError={this.onFormValidationFailed}
|
onError={this.onFormValidationFailed}
|
||||||
onRegisterClick={this.onFormSubmit} />
|
onRegisterClick={this.onFormSubmit}
|
||||||
|
onTeamSelected={this.onTeamSelected}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "Register.STEP_m.login.email.identity":
|
case "Register.STEP_m.login.email.identity":
|
||||||
|
@ -322,7 +382,6 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
var busySpinner;
|
var busySpinner;
|
||||||
if (this.state.busy) {
|
if (this.state.busy) {
|
||||||
var Spinner = sdk.getComponent("elements.Spinner");
|
|
||||||
busySpinner = (
|
busySpinner = (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
);
|
);
|
||||||
|
@ -367,7 +426,7 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className="mx_Login">
|
<div className="mx_Login">
|
||||||
<div className="mx_Login_box">
|
<div className="mx_Login_box">
|
||||||
<LoginHeader />
|
<LoginHeader icon={this.state.teamSelected ? this.state.teamSelected.icon : null}/>
|
||||||
{this._getRegisterContentJsx()}
|
{this._getRegisterContentJsx()}
|
||||||
<LoginFooter />
|
<LoginFooter />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -145,27 +145,48 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
if (imageUrl === this.state.defaultImageUrl) {
|
if (imageUrl === this.state.defaultImageUrl) {
|
||||||
const initialLetter = this._getInitialLetter(name);
|
const initialLetter = this._getInitialLetter(name);
|
||||||
return (
|
const textNode = (
|
||||||
<span className="mx_BaseAvatar" {...otherProps}>
|
<EmojiText className="mx_BaseAvatar_initial" aria-hidden="true"
|
||||||
<EmojiText className="mx_BaseAvatar_initial" aria-hidden="true"
|
style={{ fontSize: (width * 0.65) + "px",
|
||||||
style={{ fontSize: (width * 0.65) + "px",
|
width: width + "px",
|
||||||
width: width + "px",
|
lineHeight: height + "px" }}
|
||||||
lineHeight: height + "px" }}>{initialLetter}</EmojiText>
|
>
|
||||||
<img className="mx_BaseAvatar_image" src={imageUrl}
|
{initialLetter}
|
||||||
alt="" title={title} onError={this.onError}
|
</EmojiText>
|
||||||
width={width} height={height} />
|
|
||||||
</span>
|
|
||||||
);
|
);
|
||||||
|
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) {
|
if (onClick != null) {
|
||||||
return (
|
return (
|
||||||
<AccessibleButton className="mx_BaseAvatar" onClick={onClick}>
|
<AccessibleButton className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||||
<img className="mx_BaseAvatar_image" src={imageUrl}
|
element='img'
|
||||||
onError={this.onError}
|
src={imageUrl}
|
||||||
width={width} height={height}
|
onClick={onClick}
|
||||||
title={title} alt=""
|
onError={this.onError}
|
||||||
{...otherProps} />
|
width={width} height={height}
|
||||||
</AccessibleButton>
|
title={title} alt=""
|
||||||
|
{...otherProps} />
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -14,17 +14,18 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require("react");
|
import React from 'react';
|
||||||
var classNames = require('classnames');
|
import classNames from 'classnames';
|
||||||
var sdk = require("../../../index");
|
import sdk from '../../../index';
|
||||||
var Invite = require("../../../Invite");
|
import { getAddressType, inviteMultipleToRoom } from '../../../Invite';
|
||||||
var createRoom = require("../../../createRoom");
|
import createRoom from '../../../createRoom';
|
||||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
var DMRoomMap = require('../../../utils/DMRoomMap');
|
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||||
var rate_limited_func = require("../../../ratelimitedfunc");
|
import rate_limited_func from '../../../ratelimitedfunc';
|
||||||
var dis = require("../../../dispatcher");
|
import dis from '../../../dispatcher';
|
||||||
var Modal = require('../../../Modal');
|
import Modal from '../../../Modal';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
import q from 'q';
|
||||||
|
|
||||||
const TRUNCATE_QUERY_LIST = 40;
|
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
|
// If the query isn't a user we know about, but is a
|
||||||
// valid address, add an entry for that
|
// valid address, add an entry for that
|
||||||
if (queryList.length == 0) {
|
if (queryList.length == 0) {
|
||||||
const addrType = Invite.getAddressType(query);
|
const addrType = getAddressType(query);
|
||||||
if (addrType !== null) {
|
if (addrType !== null) {
|
||||||
queryList.push({
|
queryList[0] = {
|
||||||
addressType: addrType,
|
addressType: addrType,
|
||||||
address: query,
|
address: query,
|
||||||
isKnown: false,
|
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,
|
inviteList: inviteList,
|
||||||
queryList: [],
|
queryList: [],
|
||||||
});
|
});
|
||||||
|
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -229,6 +235,7 @@ module.exports = React.createClass({
|
||||||
inviteList: inviteList,
|
inviteList: inviteList,
|
||||||
queryList: [],
|
queryList: [],
|
||||||
});
|
});
|
||||||
|
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||||
},
|
},
|
||||||
|
|
||||||
_getDirectMessageRoom: function(addr) {
|
_getDirectMessageRoom: function(addr) {
|
||||||
|
@ -266,7 +273,7 @@ module.exports = React.createClass({
|
||||||
if (this.props.roomId) {
|
if (this.props.roomId) {
|
||||||
// Invite new user to a room
|
// Invite new user to a room
|
||||||
var self = this;
|
var self = this;
|
||||||
Invite.inviteMultipleToRoom(this.props.roomId, addrTexts)
|
inviteMultipleToRoom(this.props.roomId, addrTexts)
|
||||||
.then(function(addrs) {
|
.then(function(addrs) {
|
||||||
var room = MatrixClientPeg.get().getRoom(self.props.roomId);
|
var room = MatrixClientPeg.get().getRoom(self.props.roomId);
|
||||||
return self._showAnyInviteErrors(addrs, room);
|
return self._showAnyInviteErrors(addrs, room);
|
||||||
|
@ -300,7 +307,7 @@ module.exports = React.createClass({
|
||||||
var room;
|
var room;
|
||||||
createRoom().then(function(roomId) {
|
createRoom().then(function(roomId) {
|
||||||
room = MatrixClientPeg.get().getRoom(roomId);
|
room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
return Invite.inviteMultipleToRoom(roomId, addrTexts);
|
return inviteMultipleToRoom(roomId, addrTexts);
|
||||||
})
|
})
|
||||||
.then(function(addrs) {
|
.then(function(addrs) {
|
||||||
return self._showAnyInviteErrors(addrs, room);
|
return self._showAnyInviteErrors(addrs, room);
|
||||||
|
@ -380,7 +387,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_isDmChat: function(addrs) {
|
_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;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
|
@ -408,7 +415,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_addInputToList: function() {
|
_addInputToList: function() {
|
||||||
const addressText = this.refs.textinput.value.trim();
|
const addressText = this.refs.textinput.value.trim();
|
||||||
const addrType = Invite.getAddressType(addressText);
|
const addrType = getAddressType(addressText);
|
||||||
const addrObj = {
|
const addrObj = {
|
||||||
addressType: addrType,
|
addressType: addrType,
|
||||||
address: addressText,
|
address: addressText,
|
||||||
|
@ -432,9 +439,45 @@ module.exports = React.createClass({
|
||||||
inviteList: inviteList,
|
inviteList: inviteList,
|
||||||
queryList: [],
|
queryList: [],
|
||||||
});
|
});
|
||||||
|
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||||
return inviteList;
|
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() {
|
render: function() {
|
||||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
const AddressSelector = sdk.getComponent("elements.AddressSelector");
|
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 BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
|
||||||
|
const nameClasses = classNames({
|
||||||
|
"mx_AddressTile_name": true,
|
||||||
|
"mx_AddressTile_justified": this.props.justified,
|
||||||
|
});
|
||||||
|
|
||||||
let info;
|
let info;
|
||||||
let error = false;
|
let error = false;
|
||||||
if (address.addressType === "mx" && address.isKnown) {
|
if (address.addressType === "mx" && address.isKnown) {
|
||||||
const nameClasses = classNames({
|
|
||||||
"mx_AddressTile_name": true,
|
|
||||||
"mx_AddressTile_justified": this.props.justified,
|
|
||||||
});
|
|
||||||
|
|
||||||
const idClasses = classNames({
|
const idClasses = classNames({
|
||||||
"mx_AddressTile_id": true,
|
"mx_AddressTile_id": true,
|
||||||
"mx_AddressTile_justified": this.props.justified,
|
"mx_AddressTile_justified": this.props.justified,
|
||||||
|
@ -123,13 +123,21 @@ export default React.createClass({
|
||||||
<div className={unknownMxClasses}>{ this.props.address.address }</div>
|
<div className={unknownMxClasses}>{ this.props.address.address }</div>
|
||||||
);
|
);
|
||||||
} else if (address.addressType === "email") {
|
} else if (address.addressType === "email") {
|
||||||
var emailClasses = classNames({
|
const emailClasses = classNames({
|
||||||
"mx_AddressTile_email": true,
|
"mx_AddressTile_email": true,
|
||||||
"mx_AddressTile_justified": this.props.justified,
|
"mx_AddressTile_justified": this.props.justified,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let nameNode = null;
|
||||||
|
if (address.displayName) {
|
||||||
|
nameNode = <div className={nameClasses}>{ address.displayName }</div>
|
||||||
|
}
|
||||||
|
|
||||||
info = (
|
info = (
|
||||||
<div className={emailClasses}>{ address.address }</div>
|
<div className="mx_AddressTile_mx">
|
||||||
|
<div className={emailClasses}>{ address.address }</div>
|
||||||
|
{nameNode}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
error = true;
|
error = true;
|
||||||
|
|
|
@ -27,6 +27,28 @@ export default React.createClass({
|
||||||
device: React.PropTypes.object.isRequired,
|
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() {
|
onVerifyClick: function() {
|
||||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
Modal.createDialog(QuestionDialog, {
|
Modal.createDialog(QuestionDialog, {
|
||||||
|
@ -41,9 +63,9 @@ export default React.createClass({
|
||||||
</p>
|
</p>
|
||||||
<div className="mx_UserSettings_cryptoSection">
|
<div className="mx_UserSettings_cryptoSection">
|
||||||
<ul>
|
<ul>
|
||||||
<li><label>Device name:</label> <span>{ this.props.device.getDisplayName() }</span></li>
|
<li><label>Device name:</label> <span>{ this.state.device.getDisplayName() }</span></li>
|
||||||
<li><label>Device ID:</label> <span><code>{ this.props.device.deviceId}</code></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.props.device.getFingerprint() }</b></code></span></li>
|
<li><label>Device key:</label> <span><code><b>{ this.state.device.getFingerprint() }</b></code></span></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
|
@ -60,7 +82,7 @@ export default React.createClass({
|
||||||
onFinished: confirm=>{
|
onFinished: confirm=>{
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
MatrixClientPeg.get().setDeviceVerified(
|
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() {
|
onUnverifyClick: function() {
|
||||||
MatrixClientPeg.get().setDeviceVerified(
|
MatrixClientPeg.get().setDeviceVerified(
|
||||||
this.props.userId, this.props.device.deviceId, false
|
this.props.userId, this.state.device.deviceId, false
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
onBlacklistClick: function() {
|
onBlacklistClick: function() {
|
||||||
MatrixClientPeg.get().setDeviceBlocked(
|
MatrixClientPeg.get().setDeviceBlocked(
|
||||||
this.props.userId, this.props.device.deviceId, true
|
this.props.userId, this.state.device.deviceId, true
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
onUnblacklistClick: function() {
|
onUnblacklistClick: function() {
|
||||||
MatrixClientPeg.get().setDeviceBlocked(
|
MatrixClientPeg.get().setDeviceBlocked(
|
||||||
this.props.userId, this.props.device.deviceId, false
|
this.props.userId, this.state.device.deviceId, false
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var blacklistButton = null, verifyButton = null;
|
var blacklistButton = null, verifyButton = null;
|
||||||
|
|
||||||
if (this.props.device.isBlocked()) {
|
if (this.state.device.isBlocked()) {
|
||||||
blacklistButton = (
|
blacklistButton = (
|
||||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblacklist"
|
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblacklist"
|
||||||
onClick={this.onUnblacklistClick}>
|
onClick={this.onUnblacklistClick}>
|
||||||
|
@ -104,7 +126,7 @@ export default React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.device.isVerified()) {
|
if (this.state.device.isVerified()) {
|
||||||
verifyButton = (
|
verifyButton = (
|
||||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
|
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
|
||||||
onClick={this.onUnverifyClick}>
|
onClick={this.onUnverifyClick}>
|
||||||
|
|
|
@ -385,7 +385,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
userEvents[userId].push({
|
userEvents[userId].push({
|
||||||
mxEvent: e,
|
mxEvent: e,
|
||||||
displayName: e.target.name || userId,
|
displayName: (e.target ? e.target.name : null) || userId,
|
||||||
index: index,
|
index: index,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -44,8 +44,8 @@ module.exports = React.createClass({
|
||||||
teams: React.PropTypes.arrayOf(React.PropTypes.shape({
|
teams: React.PropTypes.arrayOf(React.PropTypes.shape({
|
||||||
// The displayed name of the team
|
// The displayed name of the team
|
||||||
"name": React.PropTypes.string,
|
"name": React.PropTypes.string,
|
||||||
// The suffix with which every team email address ends
|
// The domain of team email addresses
|
||||||
"emailSuffix": React.PropTypes.string,
|
"domain": React.PropTypes.string,
|
||||||
})).required,
|
})).required,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
@ -117,9 +117,6 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_doSubmit: function() {
|
_doSubmit: function() {
|
||||||
let email = this.refs.email.value.trim();
|
let email = this.refs.email.value.trim();
|
||||||
if (this.state.selectedTeam) {
|
|
||||||
email += "@" + this.state.selectedTeam.emailSuffix;
|
|
||||||
}
|
|
||||||
var promise = this.props.onRegisterClick({
|
var promise = this.props.onRegisterClick({
|
||||||
username: this.refs.username.value.trim() || this.props.guestUsername,
|
username: this.refs.username.value.trim() || this.props.guestUsername,
|
||||||
password: this.refs.password.value.trim(),
|
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
|
* Returns true if all fields were valid last time
|
||||||
* they were validated.
|
* they were validated.
|
||||||
|
@ -167,20 +145,36 @@ module.exports = React.createClass({
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_isUniEmail: function(email) {
|
||||||
|
return email.endsWith('.ac.uk') || email.endsWith('.edu') || email.endsWith('matrix.org');
|
||||||
|
},
|
||||||
|
|
||||||
validateField: function(field_id) {
|
validateField: function(field_id) {
|
||||||
var pwd1 = this.refs.password.value.trim();
|
var pwd1 = this.refs.password.value.trim();
|
||||||
var pwd2 = this.refs.passwordConfirm.value.trim();
|
var pwd2 = this.refs.passwordConfirm.value.trim();
|
||||||
|
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
case FIELD_EMAIL:
|
case FIELD_EMAIL:
|
||||||
let email = this.refs.email.value;
|
const email = this.refs.email.value;
|
||||||
if (this.props.teamsConfig) {
|
if (this.props.teamsConfig && this._isUniEmail(email)) {
|
||||||
let team = this.state.selectedTeam;
|
const matchingTeam = this.props.teamsConfig.teams.find(
|
||||||
if (team) {
|
(team) => {
|
||||||
email = email + "@" + team.emailSuffix;
|
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");
|
this.markFieldValid(field_id, valid, "RegistrationForm.ERR_EMAIL_INVALID");
|
||||||
break;
|
break;
|
||||||
case FIELD_USERNAME:
|
case FIELD_USERNAME:
|
||||||
|
@ -260,61 +254,35 @@ module.exports = React.createClass({
|
||||||
return cls;
|
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() {
|
render: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
var emailSection, teamSection, teamAdditionSupport, registerButton;
|
var emailSection, belowEmailSection, registerButton;
|
||||||
if (this.props.showEmail) {
|
if (this.props.showEmail) {
|
||||||
let emailSuffix = this._renderEmailInputSuffix();
|
|
||||||
emailSection = (
|
emailSection = (
|
||||||
<div>
|
<input type="text" ref="email"
|
||||||
<input type="text" ref="email"
|
autoFocus={true} placeholder="Email address (optional)"
|
||||||
autoFocus={true} placeholder="Email address (optional)"
|
defaultValue={this.props.defaultEmail}
|
||||||
defaultValue={this.props.defaultEmail}
|
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
|
||||||
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
|
onBlur={function() {self.validateField(FIELD_EMAIL);}}
|
||||||
onBlur={function() {self.validateField(FIELD_EMAIL);}}
|
value={self.state.email}/>
|
||||||
value={self.state.email}/>
|
|
||||||
{emailSuffix ? <input className="mx_Login_field" value={emailSuffix} disabled/> : null }
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
if (this.props.teamsConfig) {
|
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) {
|
if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) {
|
||||||
teamAdditionSupport = (
|
belowEmailSection = (
|
||||||
<span>
|
<p className="mx_Login_support">
|
||||||
If your team is not listed, email
|
Sorry, but your university is not registered with us just yet.
|
||||||
|
Email us on
|
||||||
<a href={"mailto:" + this.props.teamsConfig.supportEmail}>
|
<a href={"mailto:" + this.props.teamsConfig.supportEmail}>
|
||||||
{this.props.teamsConfig.supportEmail}
|
{this.props.teamsConfig.supportEmail}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<form onSubmit={this.onSubmit}>
|
<form onSubmit={this.onSubmit}>
|
||||||
{teamSection}
|
|
||||||
{teamAdditionSupport}
|
|
||||||
<br />
|
|
||||||
{emailSection}
|
{emailSection}
|
||||||
<br />
|
{belowEmailSection}
|
||||||
<input type="text" ref="username"
|
<input type="text" ref="username"
|
||||||
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername}
|
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername}
|
||||||
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
|
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
|
||||||
|
|
|
@ -40,6 +40,7 @@ import * as HtmlUtils from '../../../HtmlUtils';
|
||||||
import Autocomplete from './Autocomplete';
|
import Autocomplete from './Autocomplete';
|
||||||
import {Completion} from "../../../autocomplete/Autocompleter";
|
import {Completion} from "../../../autocomplete/Autocompleter";
|
||||||
import Markdown from '../../../Markdown';
|
import Markdown from '../../../Markdown';
|
||||||
|
import {onSendMessageFailed} from './MessageComposerInputOld';
|
||||||
|
|
||||||
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
|
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 = sendTextFn.call(this.client, this.props.room.roomId, contentText);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessagePromise.then(() => {
|
sendMessagePromise.done((res) => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'message_sent',
|
action: 'message_sent',
|
||||||
});
|
});
|
||||||
}, () => {
|
}, (e) => onSendMessageFailed(e, this.props.room));
|
||||||
dis.dispatch({
|
|
||||||
action: 'message_send_failed',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
editorState: this.createEditorState(),
|
editorState: this.createEditorState(),
|
||||||
|
|
|
@ -29,10 +29,31 @@ var TYPING_USER_TIMEOUT = 10000;
|
||||||
var TYPING_SERVER_TIMEOUT = 30000;
|
var TYPING_SERVER_TIMEOUT = 30000;
|
||||||
var MARKDOWN_ENABLED = true;
|
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
|
* The textInput part of the MessageComposer
|
||||||
*/
|
*/
|
||||||
module.exports = React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'MessageComposerInput',
|
displayName: 'MessageComposerInput',
|
||||||
|
|
||||||
statics: {
|
statics: {
|
||||||
|
@ -331,21 +352,18 @@ module.exports = React.createClass({
|
||||||
MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, htmlText);
|
MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, htmlText);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const contentText = mdown.toPlaintext();
|
if (mdown) contentText = mdown.toPlaintext();
|
||||||
sendMessagePromise = isEmote ?
|
sendMessagePromise = isEmote ?
|
||||||
MatrixClientPeg.get().sendEmoteMessage(this.props.room.roomId, contentText) :
|
MatrixClientPeg.get().sendEmoteMessage(this.props.room.roomId, contentText) :
|
||||||
MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText);
|
MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessagePromise.done(function() {
|
sendMessagePromise.done(function(res) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'message_sent'
|
action: 'message_sent'
|
||||||
});
|
});
|
||||||
}, function() {
|
}, (e) => onSendMessageFailed(e, this.props.room));
|
||||||
dis.dispatch({
|
|
||||||
action: 'message_send_failed'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this.refs.textarea.value = '';
|
this.refs.textarea.value = '';
|
||||||
this.resizeInput();
|
this.resizeInput();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
|
@ -170,15 +170,15 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
let title;
|
let title;
|
||||||
if (this.props.timestamp) {
|
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);
|
let ts = new Date(this.props.timestamp);
|
||||||
if (this.props.showFullTimestamp) {
|
if (this.props.showFullTimestamp) {
|
||||||
// "15/12/2016, 7:05:45 PM (@alice:matrix.org)"
|
// "15/12/2016, 7:05:45 PM (@alice:matrix.org)"
|
||||||
title = ts.toLocaleString() + suffix;
|
title = prefix + ts.toLocaleString();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// "7:05:45 PM (@alice:matrix.org)"
|
// "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"
|
width={14} height={14} resizeMethod="crop"
|
||||||
style={style}
|
style={style}
|
||||||
title={title}
|
title={title}
|
||||||
|
onClick={this.props.onClick}
|
||||||
/>
|
/>
|
||||||
</Velociraptor>
|
</Velociraptor>
|
||||||
);
|
);
|
||||||
/* onClick={this.props.onClick} */
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -301,8 +301,8 @@ module.exports = React.createClass({
|
||||||
var rightPanel_buttons;
|
var rightPanel_buttons;
|
||||||
if (this.props.collapsedRhs) {
|
if (this.props.collapsedRhs) {
|
||||||
rightPanel_buttons =
|
rightPanel_buttons =
|
||||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title="<">
|
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title="Show panel">
|
||||||
<TintableSvg src="img/minimise.svg" width="10" height="16"/>
|
<TintableSvg src="img/maximise.svg" width="10" height="16"/>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -146,7 +146,7 @@ module.exports = React.createClass({
|
||||||
<div>
|
<div>
|
||||||
<div className="mx_RoomPreviewBar_join_text">
|
<div className="mx_RoomPreviewBar_join_text">
|
||||||
You are trying to access { name }.<br/>
|
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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -24,6 +24,8 @@ var ObjectUtils = require("../../../ObjectUtils");
|
||||||
var dis = require("../../../dispatcher");
|
var dis = require("../../../dispatcher");
|
||||||
var ScalarAuthClient = require("../../../ScalarAuthClient");
|
var ScalarAuthClient = require("../../../ScalarAuthClient");
|
||||||
var ScalarMessaging = require('../../../ScalarMessaging');
|
var ScalarMessaging = require('../../../ScalarMessaging');
|
||||||
|
var UserSettingsStore = require('../../../UserSettingsStore');
|
||||||
|
|
||||||
|
|
||||||
// parse a string as an integer; if the input is undefined, or cannot be parsed
|
// parse a string as an integer; if the input is undefined, or cannot be parsed
|
||||||
// as an integer, return a default.
|
// as an integer, return a default.
|
||||||
|
@ -228,11 +230,13 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
// encryption
|
// encryption
|
||||||
p = this.saveEncryption();
|
p = this.saveEnableEncryption();
|
||||||
if (!q.isFulfilled(p)) {
|
if (!q.isFulfilled(p)) {
|
||||||
promises.push(p);
|
promises.push(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.saveBlacklistUnverifiedDevicesPerRoom();
|
||||||
|
|
||||||
console.log("Performing %s operations: %s", promises.length, JSON.stringify(promises));
|
console.log("Performing %s operations: %s", promises.length, JSON.stringify(promises));
|
||||||
return promises;
|
return promises;
|
||||||
},
|
},
|
||||||
|
@ -252,7 +256,7 @@ module.exports = React.createClass({
|
||||||
return this.refs.url_preview_settings.saveSettings();
|
return this.refs.url_preview_settings.saveSettings();
|
||||||
},
|
},
|
||||||
|
|
||||||
saveEncryption: function() {
|
saveEnableEncryption: function() {
|
||||||
if (!this.refs.encrypt) { return q(); }
|
if (!this.refs.encrypt) { return q(); }
|
||||||
|
|
||||||
var encrypt = this.refs.encrypt.checked;
|
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) {
|
_hasDiff: function(strA, strB) {
|
||||||
// treat undefined as an empty string because other components may blindly
|
// treat undefined as an empty string because other components may blindly
|
||||||
// call setName("") when there has been no diff made to the name!
|
// 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 cli = MatrixClientPeg.get();
|
||||||
var roomState = this.props.room.currentState;
|
var roomState = this.props.room.currentState;
|
||||||
var isEncrypted = cli.isRoomEncrypted(this.props.room.roomId);
|
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 &&
|
if (!isEncrypted &&
|
||||||
roomState.mayClientSendStateEvent("m.room.encryption", cli)) {
|
roomState.mayClientSendStateEvent("m.room.encryption", cli)) {
|
||||||
return (
|
return (
|
||||||
<label>
|
<div>
|
||||||
<input type="checkbox" ref="encrypt" onClick={ this.onEnableEncryptionClick }/>
|
<label>
|
||||||
<img className="mx_RoomSettings_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12" />
|
<input type="checkbox" ref="encrypt" onClick={ this.onEnableEncryptionClick }/>
|
||||||
Enable encryption (warning: cannot be disabled again!)
|
<img className="mx_RoomSettings_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12" />
|
||||||
</label>
|
Enable encryption (warning: cannot be disabled again!)
|
||||||
|
</label>
|
||||||
|
{ settings }
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return (
|
return (
|
||||||
<label>
|
<div>
|
||||||
{ isEncrypted
|
<label>
|
||||||
? <img className="mx_RoomSettings_e2eIcon" src="img/e2e-verified.svg" width="10" height="12" />
|
{ isEncrypted
|
||||||
: <img className="mx_RoomSettings_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12" />
|
? <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>
|
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);
|
const ciphertextLength = body.length-(1+16+16+4+32);
|
||||||
if (body.length < 0) {
|
if (ciphertextLength < 0) {
|
||||||
throw new Error('Invalid file: too short');
|
throw new Error('Invalid file: too short');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,19 +102,19 @@ export function decryptMegolmKeyFile(data, password) {
|
||||||
*/
|
*/
|
||||||
export function encryptMegolmKeyFile(data, password, options) {
|
export function encryptMegolmKeyFile(data, password, options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
const kdf_rounds = options.kdf_rounds || 100000;
|
const kdf_rounds = options.kdf_rounds || 500000;
|
||||||
|
|
||||||
const salt = new Uint8Array(16);
|
const salt = new Uint8Array(16);
|
||||||
window.crypto.getRandomValues(salt);
|
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);
|
const iv = new Uint8Array(16);
|
||||||
window.crypto.getRandomValues(iv);
|
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) => {
|
return deriveKeys(salt, kdf_rounds, password).then((keys) => {
|
||||||
const [aes_key, hmac_key] = 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]
|
* @return {Promise<[CryptoKey, CryptoKey]>} promise for [aes key, hmac key]
|
||||||
*/
|
*/
|
||||||
function deriveKeys(salt, iterations, password) {
|
function deriveKeys(salt, iterations, password) {
|
||||||
|
const start = new Date();
|
||||||
return subtleCrypto.importKey(
|
return subtleCrypto.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
new TextEncoder().encode(password),
|
new TextEncoder().encode(password),
|
||||||
|
@ -182,6 +183,9 @@ function deriveKeys(salt, iterations, password) {
|
||||||
512
|
512
|
||||||
);
|
);
|
||||||
}).then((keybits) => {
|
}).then((keybits) => {
|
||||||
|
const now = new Date();
|
||||||
|
console.log("E2e import/export: deriveKeys took " + (now - start) + "ms");
|
||||||
|
|
||||||
const aes_key = keybits.slice(0, 32);
|
const aes_key = keybits.slice(0, 32);
|
||||||
const hmac_key = keybits.slice(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) {
|
it('resolves a room alias to a room id', function (done) {
|
||||||
peg.get().getRoomIdForAlias.returns(q({room_id: "!randomcharacters:aser.ver"}));
|
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);
|
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) {
|
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 */
|
/* returns a promise which will resolve when the fill happens */
|
||||||
awaitFill: function(dir) {
|
awaitFill: function(dir) {
|
||||||
|
console.log("ScrollPanel Tester: awaiting " + dir + " fill");
|
||||||
var defer = q.defer();
|
var defer = q.defer();
|
||||||
this._fillDefers[dir] = defer;
|
this._fillDefers[dir] = defer;
|
||||||
return defer.promise;
|
return defer.promise;
|
||||||
|
@ -80,7 +81,7 @@ var Tester = React.createClass({
|
||||||
|
|
||||||
_onScroll: function(ev) {
|
_onScroll: function(ev) {
|
||||||
var st = ev.target.scrollTop;
|
var st = ev.target.scrollTop;
|
||||||
console.log("Scroll event; scrollTop: " + st);
|
console.log("ScrollPanel Tester: scroll event; scrollTop: " + st);
|
||||||
this.lastScrollEvent = st;
|
this.lastScrollEvent = st;
|
||||||
|
|
||||||
var d = this._scrollDefer;
|
var d = this._scrollDefer;
|
||||||
|
@ -159,10 +160,29 @@ describe('ScrollPanel', function() {
|
||||||
scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass(
|
scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass(
|
||||||
tester, "gm-scroll-view");
|
tester, "gm-scroll-view");
|
||||||
|
|
||||||
// wait for a browser tick to let the initial paginates complete
|
// we need to make sure we don't call done() until q has finished
|
||||||
setTimeout(function() {
|
// running the completion handlers from the fill requests. We can't
|
||||||
done();
|
// just use .done(), because that will end up ahead of those handlers
|
||||||
}, 0);
|
// 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() {
|
afterEach(function() {
|
||||||
|
|
|
@ -99,7 +99,11 @@ describe('TimelinePanel', function() {
|
||||||
// the document so that we can interact with it properly.
|
// the document so that we can interact with it properly.
|
||||||
parentDiv = document.createElement('div');
|
parentDiv = document.createElement('div');
|
||||||
parentDiv.style.width = '800px';
|
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';
|
parentDiv.style.overflow = 'hidden';
|
||||||
document.body.appendChild(parentDiv);
|
document.body.appendChild(parentDiv);
|
||||||
});
|
});
|
||||||
|
@ -235,7 +239,7 @@ describe('TimelinePanel', function() {
|
||||||
expect(client.paginateEventTimeline.callCount).toEqual(0);
|
expect(client.paginateEventTimeline.callCount).toEqual(0);
|
||||||
done();
|
done();
|
||||||
}, 0);
|
}, 0);
|
||||||
}, 0);
|
}, 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should let you scroll down to the bottom after you've scrolled up", function(done) {
|
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) {
|
export function beforeEach(context) {
|
||||||
var desc = context.currentTest.fullTitle();
|
var desc = context.currentTest.fullTitle();
|
||||||
|
|
||||||
console.log();
|
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(desc);
|
||||||
console.log(new Array(1 + desc.length).join("="));
|
console.log(new Array(1 + desc.length).join("="));
|
||||||
};
|
};
|
||||||
|
|
|
@ -75,6 +75,16 @@ describe('MegolmExportEncryption', function() {
|
||||||
.toThrow('Trailer line not found');
|
.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) {
|
it('should decrypt a range of inputs', function(done) {
|
||||||
function next(i) {
|
function next(i) {
|
||||||
if (i >= TEST_VECTORS.length) {
|
if (i >= TEST_VECTORS.length) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue