Merge branch 'develop' into luke/feature-css-msg-colors

This commit is contained in:
Luke Barnard 2017-02-09 13:14:15 +00:00 committed by GitHub
commit ae03244e6e
154 changed files with 4801 additions and 1256 deletions

BIN
.DS_Store vendored

Binary file not shown.

1
.eslintignore Normal file
View file

@ -0,0 +1 @@
src/component-index.js

117
.eslintrc
View file

@ -1,117 +0,0 @@
{
"parser": "babel-eslint",
"plugins": [
"react",
"flowtype"
],
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
"impliedStrict": true
}
},
"env": {
"browser": true,
"amd": true,
"es6": true,
"node": true,
"mocha": true
},
"extends": ["eslint:recommended", "plugin:react/recommended"],
"rules": {
"no-undef": ["warn"],
"global-strict": ["off"],
"no-extra-semi": ["warn"],
"no-underscore-dangle": ["off"],
"no-console": ["off"],
"no-unused-vars": ["off"],
"no-trailing-spaces": ["warn", {
"skipBlankLines": true
}],
"no-unreachable": ["warn"],
"no-spaced-func": ["warn"],
"no-new-func": ["error"],
"no-new-wrappers": ["error"],
"no-invalid-regexp": ["error"],
"no-extra-bind": ["error"],
"no-magic-numbers": ["error", {
"ignore": [-1, 0, 1], // usually used in array/string indexing
"ignoreArrayIndexes": true,
"enforceConst": true,
"detectObjects": true
}],
"consistent-return": ["error"],
"valid-jsdoc": ["error"],
"no-use-before-define": ["error"],
"camelcase": ["warn"],
"array-callback-return": ["error"],
"dot-location": ["warn", "property"],
"guard-for-in": ["error"],
"no-useless-call": ["warn"],
"no-useless-escape": ["warn"],
"no-useless-concat": ["warn"],
"brace-style": ["warn", "1tbs"],
"comma-style": ["warn", "last"],
"space-before-function-paren": ["warn", "never"],
"space-before-blocks": ["warn", "always"],
"keyword-spacing": ["warn", {
"before": true,
"after": true
}],
// dangling commas required, but only for multiline objects/arrays
"comma-dangle": ["warn", "always-multiline"],
// always === instead of ==, unless dealing with null/undefined
"eqeqeq": ["error", "smart"],
// always use curly braces, even with single statements
"curly": ["error", "all"],
// phasing out var in favour of let/const is a good idea
"no-var": ["warn"],
// always require semicolons
"semi": ["error", "always"],
// prefer rest and spread over the Old Ways
"prefer-spread": ["warn"],
"prefer-rest-params": ["warn"],
/** react **/
// bind or arrow function in props causes performance issues
"react/jsx-no-bind": ["error", {
"ignoreRefs": true
}],
"react/jsx-key": ["error"],
"react/prefer-stateless-function": ["warn"],
/** flowtype **/
"flowtype/require-parameter-type": [
1,
{
"excludeArrowFunctions": true
}
],
"flowtype/define-flow-type": 1,
"flowtype/require-return-type": [
1,
"always",
{
"annotateUndefined": "never",
"excludeArrowFunctions": true
}
],
"flowtype/space-after-type-colon": [
1,
"always"
],
"flowtype/space-before-type-colon": [
1,
"never"
]
},
"settings": {
"flowtype": {
"onlyFilesWithFlowAnnotation": true
}
}
}

77
.eslintrc.js Normal file
View file

@ -0,0 +1,77 @@
const path = require('path');
// get the path of the js-sdk so we can extend the config
// eslint supports loading extended configs by module,
// but only if they come from a module that starts with eslint-config-
// So we load the filename directly (and it could be in node_modules/
// or or ../node_modules/ etc)
const matrixJsSdkPath = path.dirname(require.resolve('matrix-js-sdk'));
module.exports = {
parser: "babel-eslint",
extends: [matrixJsSdkPath + "/.eslintrc.js"],
plugins: [
"react",
"flowtype",
],
env: {
es6: true,
},
parserOptions: {
ecmaFeatures: {
jsx: true,
}
},
rules: {
/** react **/
// This just uses the react plugin to help eslint known when
// variables have been used in JSX
"react/jsx-uses-vars": "error",
// bind or arrow function in props causes performance issues
"react/jsx-no-bind": ["error", {
"ignoreRefs": true,
}],
"react/jsx-key": ["error"],
/** flowtype **/
"flowtype/require-parameter-type": ["warn", {
"excludeArrowFunctions": true,
}],
"flowtype/define-flow-type": "warn",
"flowtype/require-return-type": ["warn",
"always",
{
"annotateUndefined": "never",
"excludeArrowFunctions": true,
}
],
"flowtype/space-after-type-colon": ["warn", "always"],
"flowtype/space-before-type-colon": ["warn", "never"],
/*
* things that are errors in the js-sdk config that the current
* code does not adhere to, turned down to warn
*/
"max-len": ["warn", {
// apparently people believe the length limit shouldn't apply
// to JSX.
ignorePattern: '^\\s*<',
ignoreComments: true,
code: 90,
}],
"valid-jsdoc": ["warn"],
"new-cap": ["warn"],
"key-spacing": ["warn"],
"arrow-parens": ["warn"],
"prefer-const": ["warn"],
// crashes currently: https://github.com/eslint/eslint/issues/6274
"generator-star-spacing": "off",
},
settings: {
flowtype: {
onlyFilesWithFlowAnnotation: true
},
},
};

25
.travis-test-riot.sh Executable file
View file

@ -0,0 +1,25 @@
#!/bin/bash
#
# script which is run by the travis build (after `npm run test`).
#
# clones riot-web develop and runs the tests against our version of react-sdk.
set -ev
RIOT_WEB_DIR=riot-web
REACT_SDK_DIR=`pwd`
git clone --depth=1 --branch develop https://github.com/vector-im/riot-web.git \
"$RIOT_WEB_DIR"
cd "$RIOT_WEB_DIR"
mkdir node_modules
npm install
(cd node_modules/matrix-js-sdk && npm install)
rm -r node_modules/matrix-react-sdk
ln -s "$REACT_SDK_DIR" node_modules/matrix-react-sdk
npm run test

View file

@ -1,3 +1,9 @@
language: node_js
node_js:
- node # Latest stable version of nodejs.
install:
- npm install
- (cd node_modules/matrix-js-sdk && npm install)
script:
- npm run test
- ./.travis-test-riot.sh

View file

@ -1,3 +1,168 @@
Changes in [0.8.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.6) (2017-02-04)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.6-rc.3...v0.8.6)
* Update to matrix-js-sdk 0.7.5 (no changes from 0.7.5-rc.3)
Changes in [0.8.6-rc.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.6-rc.3) (2017-02-03)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.6-rc.2...v0.8.6-rc.3)
* Update to matrix-js-sdk 0.7.5-rc.3
* Fix deviceverifybuttons
[5fd7410](https://github.com/matrix-org/matrix-react-sdk/commit/827b5a6811ac6b9d1f9a3002a94f9f6ac3f1d49c)
Changes in [0.8.6-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.6-rc.2) (2017-02-03)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.6-rc.1...v0.8.6-rc.2)
* Update to new matrix-js-sdk to get support for new device change notifications interface
Changes in [0.8.6-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.6-rc.1) (2017-02-03)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.5...v0.8.6-rc.1)
* Fix timeline & notifs panel spuriously being empty
[\#675](https://github.com/matrix-org/matrix-react-sdk/pull/675)
* UI for blacklisting unverified devices per-room & globally
[\#636](https://github.com/matrix-org/matrix-react-sdk/pull/636)
* Show better error message in statusbar after UnkDevDialog
[\#674](https://github.com/matrix-org/matrix-react-sdk/pull/674)
* Make default avatars clickable
[\#673](https://github.com/matrix-org/matrix-react-sdk/pull/673)
* Fix one read receipt randomly not appearing
[\#672](https://github.com/matrix-org/matrix-react-sdk/pull/672)
* very barebones support for warning users when rooms contain unknown devices
[\#635](https://github.com/matrix-org/matrix-react-sdk/pull/635)
* Fix expanding/unexapnding read receipts
[\#671](https://github.com/matrix-org/matrix-react-sdk/pull/671)
* show placeholder when timeline empty
[\#670](https://github.com/matrix-org/matrix-react-sdk/pull/670)
* Make read receipt's titles more explanatory
[\#669](https://github.com/matrix-org/matrix-react-sdk/pull/669)
* Fix spurious HTML tags being passed through literally
[\#667](https://github.com/matrix-org/matrix-react-sdk/pull/667)
* Reinstate max-len lint configs
[\#665](https://github.com/matrix-org/matrix-react-sdk/pull/665)
* Throw errors on !==200 status codes from RTS
[\#662](https://github.com/matrix-org/matrix-react-sdk/pull/662)
* Exempt lines which look like pure JSX from the maxlen line
[\#664](https://github.com/matrix-org/matrix-react-sdk/pull/664)
* Make tests pass on Chrome again
[\#663](https://github.com/matrix-org/matrix-react-sdk/pull/663)
* Add referral section to user settings
[\#661](https://github.com/matrix-org/matrix-react-sdk/pull/661)
* Two megolm export fixes:
[\#660](https://github.com/matrix-org/matrix-react-sdk/pull/660)
* GET /teams from RTS instead of config.json
[\#658](https://github.com/matrix-org/matrix-react-sdk/pull/658)
* Guard onStatusBarVisible/Hidden with this.unmounted
[\#656](https://github.com/matrix-org/matrix-react-sdk/pull/656)
* Fix cancel button on e2e import/export dialogs
[\#654](https://github.com/matrix-org/matrix-react-sdk/pull/654)
* Look up email addresses in ChatInviteDialog
[\#653](https://github.com/matrix-org/matrix-react-sdk/pull/653)
* Move BugReportDialog to riot-web
[\#652](https://github.com/matrix-org/matrix-react-sdk/pull/652)
* Fix dark theme styling of roomheader cancel button
[\#651](https://github.com/matrix-org/matrix-react-sdk/pull/651)
* Allow modals to stack up
[\#649](https://github.com/matrix-org/matrix-react-sdk/pull/649)
* Add bug report UI
[\#642](https://github.com/matrix-org/matrix-react-sdk/pull/642)
* Better feedback in invite dialog
[\#625](https://github.com/matrix-org/matrix-react-sdk/pull/625)
* Import and export for Megolm session data
[\#647](https://github.com/matrix-org/matrix-react-sdk/pull/647)
* Overhaul MELS to deal with causality, kicks, etc.
[\#613](https://github.com/matrix-org/matrix-react-sdk/pull/613)
* Re-add dispatcher as alt-up/down uses it
[\#650](https://github.com/matrix-org/matrix-react-sdk/pull/650)
* Create a common BaseDialog
[\#645](https://github.com/matrix-org/matrix-react-sdk/pull/645)
* Fix SetDisplayNameDialog
[\#648](https://github.com/matrix-org/matrix-react-sdk/pull/648)
* Sync typing indication with avatar typing indication
[\#643](https://github.com/matrix-org/matrix-react-sdk/pull/643)
* Warn users of E2E key loss when changing/resetting passwords or logging out
[\#646](https://github.com/matrix-org/matrix-react-sdk/pull/646)
* Better user interface for screen readers and keyboard navigation
[\#616](https://github.com/matrix-org/matrix-react-sdk/pull/616)
* Reduce log spam: Revert a16aeeef2a0f16efedf7e6616cdf3c2c8752a077
[\#644](https://github.com/matrix-org/matrix-react-sdk/pull/644)
* Expand timeline in situations when _getIndicator not null
[\#641](https://github.com/matrix-org/matrix-react-sdk/pull/641)
* Correctly get the path of the js-sdk .eslintrc.js
[\#640](https://github.com/matrix-org/matrix-react-sdk/pull/640)
* Add 'searching known users' to the user picker
[\#621](https://github.com/matrix-org/matrix-react-sdk/pull/621)
* Add mocha env for tests in eslint config
[\#639](https://github.com/matrix-org/matrix-react-sdk/pull/639)
* Fix typing avatars displaying "me"
[\#637](https://github.com/matrix-org/matrix-react-sdk/pull/637)
* Fix device verification from e2e info
[\#638](https://github.com/matrix-org/matrix-react-sdk/pull/638)
* Make user search do a bit better on word boundary
[\#623](https://github.com/matrix-org/matrix-react-sdk/pull/623)
* Use an eslint config based on the js-sdk
[\#634](https://github.com/matrix-org/matrix-react-sdk/pull/634)
* Fix error display in account deactivate dialog
[\#633](https://github.com/matrix-org/matrix-react-sdk/pull/633)
* Configure travis to test riot-web after building
[\#629](https://github.com/matrix-org/matrix-react-sdk/pull/629)
* Sanitize ChatInviteDialog
[\#626](https://github.com/matrix-org/matrix-react-sdk/pull/626)
* (hopefully) fix theming on Chrome
[\#630](https://github.com/matrix-org/matrix-react-sdk/pull/630)
* Megolm session import and export
[\#617](https://github.com/matrix-org/matrix-react-sdk/pull/617)
* Allow Modal to be used with async-loaded components
[\#618](https://github.com/matrix-org/matrix-react-sdk/pull/618)
* Fix escaping markdown by rendering plaintext
[\#622](https://github.com/matrix-org/matrix-react-sdk/pull/622)
* Implement auto-join rooms on registration
[\#628](https://github.com/matrix-org/matrix-react-sdk/pull/628)
* Matthew/fix theme npe
[\#627](https://github.com/matrix-org/matrix-react-sdk/pull/627)
* Implement theming via alternate stylesheets
[\#624](https://github.com/matrix-org/matrix-react-sdk/pull/624)
* Replace marked with commonmark
[\#575](https://github.com/matrix-org/matrix-react-sdk/pull/575)
* Fix vector-im/riot-web#2833 : Fail nicely when people try to register
numeric user IDs
[\#619](https://github.com/matrix-org/matrix-react-sdk/pull/619)
* Show the error dialog when requests to PUT power levels fail
[\#614](https://github.com/matrix-org/matrix-react-sdk/pull/614)
Changes in [0.8.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.5) (2017-01-16)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.5-rc.1...v0.8.5)
* Pull in newer matrix-js-sdk for video calling fix
Changes in [0.8.5-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.5-rc.1) (2017-01-13)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.4...v0.8.5-rc.1)
* Build the js-sdk in the CI script
[\#612](https://github.com/matrix-org/matrix-react-sdk/pull/612)
* Fix redacted member events being visible
[\#609](https://github.com/matrix-org/matrix-react-sdk/pull/609)
* Use `getStateKey` instead of `getSender`
[\#611](https://github.com/matrix-org/matrix-react-sdk/pull/611)
* Move screen sharing error check into platform
[\#608](https://github.com/matrix-org/matrix-react-sdk/pull/608)
* Fix 'create account' link in 'forgot password'
[\#606](https://github.com/matrix-org/matrix-react-sdk/pull/606)
* Let electron users complete captchas in a web browser
[\#601](https://github.com/matrix-org/matrix-react-sdk/pull/601)
* Add support for deleting threepids
[\#597](https://github.com/matrix-org/matrix-react-sdk/pull/597)
* Display msisdn threepids as 'Phone'
[\#598](https://github.com/matrix-org/matrix-react-sdk/pull/598)
Changes in [0.8.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.4) (2016-12-24)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.3...v0.8.4)

View file

@ -12,11 +12,14 @@ set -x
# install the other dependencies
npm install
# we may be using a dev branch of js-sdk in which case we need to build it
(cd node_modules/matrix-js-sdk && npm install)
# run the mocha tests
npm run test
# run eslint
npm run lint -- -f checkstyle -o eslint.xml || true
npm run lintall -- -f checkstyle -o eslint.xml || true
# delete the old tarball, if it exists
rm -f matrix-react-sdk-*.tgz

View file

@ -165,6 +165,14 @@ module.exports = function (config) {
},
devtool: 'inline-source-map',
},
webpackMiddleware: {
stats: {
// don't fill the console up with a mahoosive list of modules
chunks: false,
},
},
browserNoActivityTimeout: 15000,
});
};

View file

@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
"version": "0.8.4",
"version": "0.8.6",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@ -10,6 +10,7 @@
"license": "Apache-2.0",
"main": "lib/index.js",
"files": [
".eslintrc.js",
"CHANGELOG.md",
"CONTRIBUTING.rst",
"LICENSE",
@ -46,10 +47,12 @@
"browser-encrypt-attachment": "^0.3.0",
"browser-request": "^0.3.3",
"classnames": "^2.1.2",
"commonmark": "^0.27.0",
"draft-js": "^0.8.1",
"draft-js-export-html": "^0.5.0",
"draft-js-export-markdown": "^0.2.0",
"emojione": "2.2.3",
"file-saver": "^1.3.3",
"filesize": "^3.1.2",
"flux": "^2.0.3",
"fuse.js": "^2.2.0",
@ -58,7 +61,6 @@
"isomorphic-fetch": "^2.2.1",
"linkifyjs": "^2.1.3",
"lodash": "^4.13.1",
"marked": "^0.3.5",
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
"optimist": "^0.6.1",
"q": "^1.4.1",
@ -67,13 +69,14 @@
"react-dom": "^15.4.0",
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
"sanitize-html": "^1.11.1",
"text-encoding-utf-8": "^1.0.1",
"velocity-vector": "vector-im/velocity#059e3b2",
"whatwg-fetch": "^1.0.0"
},
"devDependencies": {
"babel-cli": "^6.5.2",
"babel-core": "^6.14.0",
"babel-eslint": "^6.1.0",
"babel-eslint": "^6.1.2",
"babel-loader": "^6.2.5",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-transform-async-to-generator": "^6.16.0",
@ -85,9 +88,10 @@
"babel-preset-es2016": "^6.11.3",
"babel-preset-es2017": "^6.14.0",
"babel-preset-react": "^6.11.1",
"eslint": "^2.13.1",
"eslint-plugin-flowtype": "^2.17.0",
"eslint-plugin-react": "^6.2.1",
"eslint": "^3.13.1",
"eslint-config-google": "^0.7.1",
"eslint-plugin-flowtype": "^2.30.0",
"eslint-plugin-react": "^6.9.0",
"expect": "^1.16.0",
"json-loader": "^0.5.3",
"karma": "^0.13.22",

View file

@ -21,7 +21,7 @@ var MatrixClientPeg = require("./MatrixClientPeg");
* optionally, the identity servers.
*
* This involves getting an email token from the identity server to "prove" that
* the client owns the given email address, which is then passed to the
* the client owns the given email address, which is then passed to the
* add threepid API on the homeserver.
*/
class AddThreepid {

View file

@ -49,12 +49,12 @@ module.exports = {
},
defaultAvatarUrlForString: function(s) {
var images = [ '76cfa6', '50e2c2', 'f4c371' ];
var images = ['76cfa6', '50e2c2', 'f4c371'];
var total = 0;
for (var i = 0; i < s.length; ++i) {
total += s.charCodeAt(i);
}
return 'img/' + images[total % images.length] + '.png';
}
}
};

View file

@ -41,7 +41,7 @@ export default class BasePlatform {
* Returns true if the platform supports displaying
* notifications, otherwise false.
*/
supportsNotifications() : boolean {
supportsNotifications(): boolean {
return false;
}
@ -49,7 +49,7 @@ export default class BasePlatform {
* Returns true if the application currently has permission
* to display notifications. Otherwise false.
*/
maySendNotifications() : boolean {
maySendNotifications(): boolean {
return false;
}
@ -60,7 +60,7 @@ export default class BasePlatform {
* that is 'granted' if the user allowed the request or
* 'denied' otherwise.
*/
requestNotificationPermission() : Promise<string> {
requestNotificationPermission(): Promise<string> {
}
displayNotification(title: string, msg: string, avatarUrl: string, room: Object) {

View file

@ -159,10 +159,10 @@ function _setCallState(call, roomId, status) {
calls[roomId] = call;
if (status === "ringing") {
play("ringAudio")
play("ringAudio");
}
else if (call && call.call_state === "ringing") {
pause("ringAudio")
pause("ringAudio");
}
if (call) {

View file

@ -256,7 +256,7 @@ function uploadFile(matrixClient, roomId, file) {
});
});
} else {
const basePromise = matrixClient.uploadContent(file);
const basePromise = matrixClient.uploadContent(file);
const promise1 = basePromise.then(function(url) {
// If the attachment isn't encrypted then include the URL directly.
return {"url": url};

View file

@ -48,5 +48,5 @@ module.exports = {
//return pad(date.getHours()) + ':' + pad(date.getMinutes());
return ('00' + date.getHours()).slice(-2) + ':' + ('00' + date.getMinutes()).slice(-2);
}
}
};

View file

@ -136,6 +136,6 @@ module.exports = {
fromUsers: function(users, showInviteButton, inviteFn) {
return users.map(function(u) {
return new UserEntity(u, showInviteButton, inviteFn);
})
});
}
};

View file

@ -96,16 +96,16 @@ var sanitizeHtmlParams = {
],
allowedAttributes: {
// custom ones first:
font: [ 'color' , 'style' ], // custom to matrix
a: [ 'href', 'name', 'target', 'rel' ], // remote target: custom to matrix
font: ['color', 'style'], // custom to matrix
a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix
// We don't currently allow img itself by default, but this
// would make sense if we did
img: [ 'src' ],
img: ['src'],
},
// Lots of these won't come up by default because we don't allow them
selfClosing: [ 'img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta' ],
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
// URL schemes we permit
allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ],
allowedSchemes: ['http', 'https', 'ftp', 'mailto'],
// DO NOT USE. sanitize-html allows all URL starting with '//'
// so this will always allow links to whatever scheme the
@ -320,7 +320,7 @@ export function bodyToHtml(content, highlights, opts) {
}
EMOJI_REGEX.lastIndex = 0;
let contentBodyTrimmed = content.body.trim();
let contentBodyTrimmed = content.body !== undefined ? content.body.trim() : '';
let match = EMOJI_REGEX.exec(contentBodyTrimmed);
let emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length;

View file

@ -53,5 +53,5 @@ module.exports = {
return Math.floor(heightMulti * fullHeight);
}
},
}
};

View file

@ -19,9 +19,12 @@ import MultiInviter from './utils/MultiInviter';
const emailRegex = /^\S+@\S+\.\S+$/;
// We allow localhost for mxids to avoid confusion
const mxidRegex = /^@\S+:(?:\S+\.\S+|localhost)$/
export function getAddressType(inputText) {
const isEmailAddress = /^\S+@\S+\.\S+$/.test(inputText);
const isMatrixId = inputText[0] === '@' && inputText.indexOf(":") > 0;
const isEmailAddress = emailRegex.test(inputText);
const isMatrixId = mxidRegex.test(inputText);
// sanity check the input for user IDs
if (isEmailAddress) {
@ -55,29 +58,7 @@ export function inviteToRoom(roomId, addr) {
* @returns Promise
*/
export function inviteMultipleToRoom(roomId, addrs) {
this.inviter = new MultiInviter(roomId);
return this.inviter.invite(addrs);
const inviter = new MultiInviter(roomId);
return inviter.invite(addrs);
}
/**
* Checks is the supplied address is valid
*
* @param {addr} The mx userId or email address to check
* @returns true, false, or null for unsure
*/
export function isValidAddress(addr) {
// Check if the addr is a valid type
var addrType = this.getAddressType(addr);
if (addrType === "mx") {
let user = MatrixClientPeg.get().getUser(addr);
if (user) {
return true;
} else {
return null;
}
} else if (addrType === "email") {
return true;
} else {
return false;
}
}

View file

@ -20,6 +20,7 @@ module.exports = {
TAB: 9,
ENTER: 13,
SHIFT: 16,
ESCAPE: 27,
PAGE_UP: 33,
PAGE_DOWN: 34,
END: 35,

View file

@ -18,11 +18,12 @@ import q from 'q';
import Matrix from 'matrix-js-sdk';
import MatrixClientPeg from './MatrixClientPeg';
import Notifier from './Notifier'
import Notifier from './Notifier';
import UserActivity from './UserActivity';
import Presence from './Presence';
import dis from './dispatcher';
import DMRoomMap from './utils/DMRoomMap';
import RtsClient from './RtsClient';
/**
* Called at startup, to attempt to build a logged-in Matrix session. It tries
@ -140,7 +141,7 @@ function _loginWithToken(queryParams, defaultDeviceDisplayName) {
homeserverUrl: queryParams.homeserver,
identityServerUrl: queryParams.identityServer,
guest: false,
})
});
}, (err) => {
console.error("Failed to log in with login token: " + err + " " +
err.data);
@ -229,6 +230,11 @@ function _restoreFromLocalStorage() {
}
}
let rtsClient = null;
export function initRtsClient(url) {
rtsClient = new RtsClient(url);
}
/**
* Transitions to a logged-in state using the given credentials
* @param {MatrixClientCreds} credentials The credentials to use
@ -261,6 +267,19 @@ export function setLoggedIn(credentials) {
} catch (e) {
console.warn("Error using local storage: can't persist session!", e);
}
if (rtsClient) {
rtsClient.login(credentials.userId).then((body) => {
if (body.team_token) {
localStorage.setItem("mx_team_token", body.team_token);
}
}, (err) =>{
console.error(
"Failed to get team token on login, not persisting to localStorage",
err
);
});
}
} else {
console.warn("No local storage available: can't persist session!");
}

View file

@ -14,115 +14,144 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import marked from 'marked';
import commonmark from 'commonmark';
import escape from 'lodash/escape';
// marked only applies the default options on the high
// level marked() interface, so we do it here.
const marked_options = Object.assign({}, marked.defaults, {
gfm: true,
tables: true,
breaks: true,
pedantic: false,
sanitize: true,
smartLists: true,
smartypants: false,
xhtml: true, // return self closing tags (ie. <br /> not <br>)
});
const ALLOWED_HTML_TAGS = ['del'];
// These types of node are definitely text
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
function is_allowed_html_tag(node) {
// Regex won't work for tags with attrs, but we only
// allow <del> anyway.
const matches = /^<\/?(.*)>$/.exec(node.literal);
if (matches && matches.length == 2) {
const tag = matches[1];
return ALLOWED_HTML_TAGS.indexOf(tag) > -1;
}
return false;
}
function html_if_tag_allowed(node) {
if (is_allowed_html_tag(node)) {
this.lit(node.literal);
return;
} else {
this.lit(escape(node.literal));
}
}
/*
* Returns true if the parse output containing the node
* comprises multiple block level elements (ie. lines),
* or false if it is only a single line.
*/
function is_multi_line(node) {
var par = node;
while (par.parent) {
par = par.parent;
}
return par.firstChild != par.lastChild;
}
/**
* Class that wraps marked, adding the ability to see whether
* Class that wraps commonmark, adding the ability to see whether
* a given message actually uses any markdown syntax or whether
* it's plain text.
*/
export default class Markdown {
constructor(input) {
const lexer = new marked.Lexer(marked_options);
this.tokens = lexer.lex(input);
}
this.input = input;
_copyTokens() {
// copy tokens (the parser modifies its input arg)
const tokens_copy = this.tokens.slice();
// it also has a 'links' property, because this is javascript
// and why wouldn't you have an array that also has properties?
return Object.assign(tokens_copy, this.tokens);
const parser = new commonmark.Parser();
this.parsed = parser.parse(this.input);
}
isPlainText() {
// we determine if the message requires markdown by
// running the parser on the tokens with a dummy
// rendered and seeing if any of the renderer's
// functions are called other than those noted below.
// In case you were wondering, no we can't just examine
// the tokens because the tokens we have are only the
// output of the *first* tokenizer: any line-based
// markdown is processed by marked within Parser by
// the 'inline lexer'...
let is_plain = true;
const walker = this.parsed.walker();
function setNotPlain() {
is_plain = false;
}
const dummy_renderer = {};
for (const k of Object.keys(marked.Renderer.prototype)) {
dummy_renderer[k] = setNotPlain;
}
// text and paragraph are just text
dummy_renderer.text = function(t){return t;}
dummy_renderer.paragraph = function(t){return t;}
// ignore links where text is just the url:
// this ignores plain URLs that markdown has
// detected whilst preserving markdown syntax links
dummy_renderer.link = function(href, title, text) {
if (text != href) {
is_plain = false;
let ev;
while ( (ev = walker.next()) ) {
const node = ev.node;
if (TEXT_NODES.indexOf(node.type) > -1) {
// definitely text
continue;
} else if (node.type == 'html_inline' || node.type == 'html_block') {
// if it's an allowed html tag, we need to render it and therefore
// we will need to use HTML. If it's not allowed, it's not HTML since
// we'll just be treating it as text.
if (is_allowed_html_tag(node)) {
return false;
}
} else {
return false;
}
}
const dummy_options = Object.assign({}, marked_options, {
renderer: dummy_renderer,
});
const dummy_parser = new marked.Parser(dummy_options);
dummy_parser.parse(this._copyTokens());
return is_plain;
return true;
}
toHTML() {
const real_renderer = new marked.Renderer();
real_renderer.link = function(href, title, text) {
// prevent marked from turning plain URLs
// into links, because its algorithm is fairly
// poor. Let's send plain URLs rather than
// badly linkified ones (the linkifier Vector
// uses on message display is way better, eg.
// handles URLs with closing parens at the end).
if (text == href) {
return href;
}
return marked.Renderer.prototype.link.apply(this, arguments);
}
const renderer = new commonmark.HtmlRenderer({safe: false});
const real_paragraph = renderer.paragraph;
real_renderer.paragraph = (text) => {
// The tokens at the top level are the 'blocks', so if we
// have more than one, there are multiple 'paragraphs'.
// If there is only one top level token, just return the
renderer.paragraph = function(node, entering) {
// If there is only one top level node, just return the
// bare text: it's a single line of text and so should be
// 'inline', rather than necessarily wrapped in its own
// p tag. If, however, we have multiple tokens, each gets
// 'inline', rather than unnecessarily wrapped in its own
// p tag. If, however, we have multiple nodes, each gets
// its own p tag to keep them as separate paragraphs.
if (this.tokens.length == 1) {
return text;
if (is_multi_line(node)) {
real_paragraph.call(this, node, entering);
}
return '<p>' + text + '</p>';
};
renderer.html_inline = html_if_tag_allowed;
renderer.html_block = function(node) {
// as with `paragraph`, we only insert line breaks
// if there are multiple lines in the markdown.
const isMultiLine = is_multi_line(node);
if (isMultiLine) this.cr();
html_if_tag_allowed.call(this, node);
if (isMultiLine) this.cr();
}
const real_options = Object.assign({}, marked_options, {
renderer: real_renderer,
});
const real_parser = new marked.Parser(real_options);
return real_parser.parse(this._copyTokens());
return renderer.render(this.parsed);
}
/*
* Render the markdown message to plain text. That is, essentially
* just remove any backslashes escaping what would otherwise be
* markdown syntax
* (to fix https://github.com/vector-im/riot-web/issues/2870)
*/
toPlaintext() {
const renderer = new commonmark.HtmlRenderer({safe: false});
const real_paragraph = renderer.paragraph;
// The default `out` function only sends the input through an XML
// escaping function, which causes messages to be entity encoded,
// which we don't want in this case.
renderer.out = function(s) {
// The `lit` function adds a string literal to the output buffer.
this.lit(s);
};
renderer.paragraph = function(node, entering) {
// as with toHTML, only append lines to paragraphs if there are
// multiple paragraphs
if (is_multi_line(node)) {
if (!entering && node.next) {
this.lit('\n\n');
}
}
};
renderer.html_block = function(node) {
this.lit(node.literal);
if (is_multi_line(node) && node.next) this.lit('\n\n');
}
return renderer.render(this.parsed);
}
}

View file

@ -19,44 +19,180 @@ limitations under the License.
var React = require('react');
var ReactDOM = require('react-dom');
import sdk from './index';
module.exports = {
DialogContainerId: "mx_Dialog_Container",
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
getOrCreateContainer: function() {
var container = document.getElementById(this.DialogContainerId);
/**
* Wrap an asynchronous loader function with a react component which shows a
* spinner until the real component loads.
*/
const AsyncWrapper = React.createClass({
propTypes: {
/** A function which takes a 'callback' argument which it will call
* with the real component once it loads.
*/
loader: React.PropTypes.func.isRequired,
},
getInitialState: function() {
return {
component: null,
};
},
componentWillMount: function() {
this._unmounted = false;
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148
console.log('Starting load of AsyncWrapper for modal');
this.props.loader((e) => {
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148
console.log('AsyncWrapper load completed with '+e.displayName);
if (this._unmounted) {
return;
}
this.setState({component: e});
});
},
componentWillUnmount: function() {
this._unmounted = true;
},
render: function() {
const {loader, ...otherProps} = this.props;
if (this.state.component) {
const Component = this.state.component;
return <Component {...otherProps} />;
} else {
// show a spinner until the component is loaded.
const Spinner = sdk.getComponent("elements.Spinner");
return <Spinner />;
}
},
});
class ModalManager {
constructor() {
this._counter = 0;
/** list of the modals we have stacked up, with the most recent at [0] */
this._modals = [
/* {
elem: React component for this dialog
onFinished: caller-supplied onFinished callback
className: CSS class for the dialog wrapper div
} */
];
this.closeAll = this.closeAll.bind(this);
}
getOrCreateContainer() {
let container = document.getElementById(DIALOG_CONTAINER_ID);
if (!container) {
container = document.createElement("div");
container.id = this.DialogContainerId;
container.id = DIALOG_CONTAINER_ID;
document.body.appendChild(container);
}
return container;
},
}
createDialog: function (Element, props, className) {
createDialog(Element, props, className) {
return this.createDialogAsync((cb) => {cb(Element);}, props, className);
}
/**
* Open a modal view.
*
* This can be used to display a react component which is loaded as an asynchronous
* webpack component. To do this, set 'loader' as:
*
* (cb) => {
* require(['<module>'], cb);
* }
*
* @param {Function} loader a function which takes a 'callback' argument,
* which it should call with a React component which will be displayed as
* the modal view.
*
* @param {Object} props properties to pass to the displayed
* component. (We will also pass an 'onFinished' property.)
*
* @param {String} className CSS class to apply to the modal wrapper
*/
createDialogAsync(loader, props, className) {
var self = this;
const modal = {};
// never call this via modal.close() from onFinished() otherwise it will loop
// never call this from onFinished() otherwise it will loop
//
// nb explicit function() rather than arrow function, to get `arguments`
var closeDialog = function() {
if (props && props.onFinished) props.onFinished.apply(null, arguments);
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
var i = self._modals.indexOf(modal);
if (i >= 0) {
self._modals.splice(i, 1);
}
self._reRender();
};
// don't attempt to reuse the same AsyncWrapper for different dialogs,
// otherwise we'll get confused.
const modalCount = this._counter++;
// FIXME: If a dialog uses getDefaultProps it clobbers the onFinished
// property set here so you can't close the dialog from a button click!
modal.elem = (
<AsyncWrapper key={modalCount} loader={loader} {...props}
onFinished={closeDialog}/>
);
modal.onFinished = props ? props.onFinished : null;
modal.className = className;
this._modals.unshift(modal);
this._reRender();
return {close: closeDialog};
}
closeAll() {
const modals = this._modals;
this._modals = [];
for (let i = 0; i < modals.length; i++) {
const m = modals[i];
if (m.onFinished) {
m.onFinished(false);
}
}
this._reRender();
}
_reRender() {
if (this._modals.length == 0) {
ReactDOM.unmountComponentAtNode(this.getOrCreateContainer());
return;
}
var modal = this._modals[0];
var dialog = (
<div className={"mx_Dialog_wrapper " + className}>
<div className={"mx_Dialog_wrapper " + (modal.className ? modal.className : '') }>
<div className="mx_Dialog">
<Element {...props} onFinished={closeDialog}/>
{modal.elem}
</div>
<div className="mx_Dialog_background" onClick={ closeDialog.bind(this, false) }></div>
<div className="mx_Dialog_background" onClick={ this.closeAll }></div>
</div>
);
ReactDOM.render(dialog, this.getOrCreateContainer());
}
}
return {close: closeDialog};
},
};
export default new ModalManager();

View file

@ -53,7 +53,7 @@ var Notifier = {
if (!msg) return;
var title;
if (!ev.sender || room.name == ev.sender.name) {
if (!ev.sender || room.name == ev.sender.name) {
title = room.name;
// notificationMessageForEvent includes sender,
// but we already have the sender here
@ -88,7 +88,7 @@ var Notifier = {
if (e) {
e.load();
e.play();
};
}
},
start: function() {

View file

@ -64,7 +64,7 @@ module.exports.getKeyValueArrayDiffs = function(before, after) {
} else if (itemDelta[item] === -1) {
results.push({ place: "del", key: muxedKey, val: item });
} else {
// itemDelta of 0 means it was unchanged between before/after
// itemDelta of 0 means it was unchanged between before/after
}
});
break;

View file

@ -16,6 +16,7 @@ limitations under the License.
/** The types of page which can be shown by the LoggedInView */
export default {
HomePage: "home_page",
RoomView: "room_view",
UserSettings: "user_settings",
CreateRoom: "create_room",

View file

@ -111,7 +111,7 @@ class Presence {
this.timer = setTimeout(function() {
self._onUnavailableTimerFire();
}, UNAVAILABLE_TIME_MS);
}
}
}
module.exports = new Presence();

View file

@ -16,17 +16,35 @@ limitations under the License.
var MatrixClientPeg = require('./MatrixClientPeg');
var dis = require('./dispatcher');
var sdk = require('./index');
var Modal = require('./Modal');
module.exports = {
resend: function(event) {
MatrixClientPeg.get().resendEvent(
event, MatrixClientPeg.get().getRoom(event.getRoomId())
).done(function() {
).done(function(res) {
dis.dispatch({
action: 'message_sent',
event: event
});
}, function() {
}, function(err) {
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148
console.log('Resend got send failure: ' + err.name + '('+err+')');
if (err.name === "UnknownDeviceError") {
var UnknownDeviceDialog = sdk.getComponent("dialogs.UnknownDeviceDialog");
Modal.createDialog(UnknownDeviceDialog, {
devices: err.devices,
room: MatrixClientPeg.get().getRoom(event.getRoomId()),
onFinished: (r) => {
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148
console.log('UnknownDeviceDialog closed with '+r);
},
}, "mx_Dialog_unknownDevice");
}
dis.dispatch({
action: 'message_send_failed',
event: event

View file

@ -12,7 +12,7 @@ import {
SelectionState,
Entity,
} from 'draft-js';
import * as sdk from './index';
import * as sdk from './index';
import * as emojione from 'emojione';
import {stateToHTML} from 'draft-js-export-html';
import {SelectionRange} from "./autocomplete/Autocompleter";
@ -109,7 +109,7 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator {
return <span className="mx_UserPill">{avatar}{props.children}</span>;
}
};
let roomDecorator = {
strategy: (contentBlock, callback) => {
findWithRegex(ROOM_REGEX, contentBlock, callback);

View file

@ -26,7 +26,7 @@ function tsOfNewestEvent(room) {
}
function mostRecentActivityFirst(roomList) {
return roomList.sort(function(a,b) {
return roomList.sort(function(a, b) {
return tsOfNewestEvent(b) - tsOfNewestEvent(a);
});
}

View file

@ -146,7 +146,7 @@ function isRuleForRoom(roomId, rule) {
}
const cond = rule.conditions[0];
if (
cond.kind == 'event_match' &&
cond.kind == 'event_match' &&
cond.key == 'room_id' &&
cond.pattern == roomId
) {

View file

@ -37,7 +37,7 @@ export function getOnlyOtherMember(room, me) {
if (joinedMembers.length === 2) {
return joinedMembers.filter(function(m) {
return m.userId !== me.userId
return m.userId !== me.userId;
})[0];
}

97
src/RtsClient.js Normal file
View 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,
},
}
);
}
}

View file

@ -371,7 +371,7 @@ const onMessage = function(event) {
}, (err) => {
console.error(err);
sendError(event, "Failed to lookup current room.");
})
});
};
module.exports = {

View file

@ -19,6 +19,8 @@ var DEFAULTS = {
integrations_ui_url: "https://scalar.vector.im/",
// Base URL to the REST interface of the integrations server
integrations_rest_url: "https://scalar.vector.im/api",
// Where to send bug reports. If not specified, bugs cannot be sent.
bug_report_endpoint_url: null,
};
class SdkConfig {

View file

@ -91,6 +91,10 @@ class Register extends Signup {
this.params.idSid = idSid;
}
setReferrer(referrer) {
this.params.referrer = referrer;
}
setGuestAccessToken(token) {
this.guestAccessToken = token;
}
@ -203,7 +207,17 @@ class Register extends Signup {
} else if (error.errcode == 'M_INVALID_USERNAME') {
throw new Error("User names may only contain alphanumeric characters, underscores or dots!");
} else if (error.httpStatus >= 400 && error.httpStatus < 500) {
throw new Error(`Registration failed! (${error.httpStatus})`);
let msg = null;
if (error.message) {
msg = error.message;
} else if (error.errcode) {
msg = error.errcode;
}
if (msg) {
throw new Error(`Registration failed! (${error.httpStatus}) - ${msg}`);
} else {
throw new Error(`Registration failed! (${error.httpStatus}) - That's all we know.`);
}
} else if (error.httpStatus >= 500 && error.httpStatus < 600) {
throw new Error(
`Server error during registration! (${error.httpStatus})`

View file

@ -136,6 +136,11 @@ class EmailIdentityStage extends Stage {
"&session_id=" +
encodeURIComponent(this.signupInstance.getServerData().session);
// Add the user ID of the referring user, if set
if (this.signupInstance.params.referrer) {
nextLink += "&referrer=" + encodeURIComponent(this.signupInstance.params.referrer);
}
var self = this;
return this.client.requestRegisterEmailToken(
this.signupInstance.email,

View file

@ -41,7 +41,7 @@ class Command {
}
getUsage() {
return "Usage: " + this.getCommandWithArgs()
return "Usage: " + this.getCommandWithArgs();
}
}
@ -84,7 +84,7 @@ var commands = {
var matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/);
if (matches) {
Tinter.tint(matches[1], matches[4]);
var colorScheme = {}
var colorScheme = {};
colorScheme.primary_color = matches[1];
if (matches[4]) {
colorScheme.secondary_color = matches[4];
@ -288,7 +288,7 @@ var commands = {
// helpful aliases
var aliases = {
j: "join"
}
};
module.exports = {
/**
@ -331,9 +331,9 @@ module.exports = {
// Return all the commands plus /me and /markdown which aren't handled like normal commands
var cmds = Object.keys(commands).sort().map(function(cmdKey) {
return commands[cmdKey];
})
cmds.push(new Command("me", "<action>", function(){}));
cmds.push(new Command("markdown", "<on|off>", function(){}));
});
cmds.push(new Command("me", "<action>", function() {}));
cmds.push(new Command("markdown", "<on|off>", function() {}));
return cmds;
}

View file

@ -112,7 +112,7 @@ class TabComplete {
return;
}
// ES6 destructuring; ignore first element (the complete match)
var [ , boundaryGroup, partialGroup] = res;
var [, boundaryGroup, partialGroup] = res;
if (partialGroup.length === 0 && passive) {
return;
@ -254,7 +254,7 @@ class TabComplete {
if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
// tab key has been pressed at this point
this.handleTabPress(false, ev.shiftKey)
this.handleTabPress(false, ev.shiftKey);
// prevent the default TAB operation (typically focus shifting)
ev.preventDefault();
@ -386,6 +386,6 @@ class TabComplete {
this.memberTabOrder[ev.getSender()] = this.memberOrderSeq++;
}
}
};
}
module.exports = TabComplete;

View file

@ -13,7 +13,6 @@ 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 React = require("react");
var sdk = require("./index");
class Entry {
@ -90,7 +89,7 @@ CommandEntry.fromCommands = function(commandArray) {
return commandArray.map(function(cmd) {
return new CommandEntry(cmd.getCommand(), cmd.getCommandWithArgs());
});
}
};
class MemberEntry extends Entry {
constructor(member) {
@ -119,7 +118,7 @@ MemberEntry.fromMemberList = function(members) {
return members.map(function(m) {
return new MemberEntry(m);
});
}
};
module.exports.Entry = Entry;
module.exports.MemberEntry = MemberEntry;

View file

@ -75,7 +75,6 @@ function textForMemberEvent(ev) {
return targetName + " joined the room.";
}
}
return '';
case 'leave':
if (ev.getSender() === ev.getStateKey()) {
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
@ -203,4 +202,4 @@ module.exports = {
if (!hdlr) return "";
return hdlr(ev);
}
}
};

View file

@ -14,9 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var dis = require("./dispatcher");
var sdk = require("./index");
// FIXME: these vars should be bundled up and attached to
// module.exports otherwise this will break when included by both
// react-sdk and apps layered on top.
@ -42,6 +39,7 @@ var keyHex = [
"#76CFA6", // Vector Green
"#EAF5F0", // Vector Light Green
"#D3EFE1", // BottomLeftMenu overlay (20% Vector Green overlaid on Vector Light Green)
"#FFFFFF", // white highlights of the SVGs (for switching to dark theme)
];
// cache of our replacement colours
@ -50,6 +48,7 @@ var colors = [
keyHex[0],
keyHex[1],
keyHex[2],
keyHex[3],
];
var cssFixups = [
@ -150,7 +149,7 @@ function hexToRgb(color) {
function rgbToHex(rgb) {
var val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
return '#' + (0x1000000 + val).toString(16).slice(1)
return '#' + (0x1000000 + val).toString(16).slice(1);
}
// List of functions to call when the tint changes.
@ -185,7 +184,7 @@ module.exports = {
}
if (!secondaryColor) {
var x = 0.16; // average weighting factor calculated from vector green & light green
const x = 0.16; // average weighting factor calculated from vector green & light green
var rgb = hexToRgb(primaryColor);
rgb[0] = x * rgb[0] + (1 - x) * 255;
rgb[1] = x * rgb[1] + (1 - x) * 255;
@ -194,7 +193,7 @@ module.exports = {
}
if (!tertiaryColor) {
var x = 0.19;
const x = 0.19;
var rgb1 = hexToRgb(primaryColor);
var rgb2 = hexToRgb(secondaryColor);
rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0];
@ -210,7 +209,9 @@ module.exports = {
return;
}
colors = [primaryColor, secondaryColor, tertiaryColor];
colors[0] = primaryColor;
colors[1] = secondaryColor;
colors[2] = tertiaryColor;
if (DEBUG) console.log("Tinter.tint");
@ -224,6 +225,19 @@ module.exports = {
});
},
tintSvgWhite: function(whiteColor) {
if (!whiteColor) {
whiteColor = colors[3];
}
if (colors[3] === whiteColor) {
return;
}
colors[3] = whiteColor;
tintables.forEach(function(tintable) {
tintable();
});
},
// XXX: we could just move this all into TintableSvg, but as it's so similar
// to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg)
// keeping it here for now.

View file

@ -149,6 +149,23 @@ module.exports = {
return MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings);
},
getLocalSettings: function() {
var localSettingsString = localStorage.getItem('mx_local_settings') || '{}';
return JSON.parse(localSettingsString);
},
getLocalSetting: function(type, defaultValue = null) {
var settings = this.getLocalSettings();
return settings.hasOwnProperty(type) ? settings[type] : null;
},
setLocalSetting: function(type, value) {
var settings = this.getLocalSettings();
settings[type] = value;
// FIXME: handle errors
localStorage.setItem('mx_local_settings', JSON.stringify(settings));
},
isFeatureEnabled: function(feature: string): boolean {
// Disable labs for guests.
if (MatrixClientPeg.get().isGuest()) return false;

View file

@ -62,11 +62,11 @@ module.exports = React.createClass({
oldNode.style.visibility = c.props.style.visibility;
}
});
if (oldNode.style.visibility == 'hidden' && c.props.style.visibility == 'visible') {
oldNode.style.visibility = c.props.style.visibility;
}
//console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
}
if (oldNode.style.visibility == 'hidden' && c.props.style.visibility == 'visible') {
oldNode.style.visibility = c.props.style.visibility;
}
self.children[c.key] = old;
} else {
// new element. If we have a startStyle, use that as the style and go through
@ -76,7 +76,7 @@ module.exports = React.createClass({
var startStyles = self.props.startStyles;
if (startStyles.length > 0) {
var startStyle = startStyles[0]
var startStyle = startStyles[0];
newProps.style = startStyle;
// console.log("mounted@startstyle0: "+JSON.stringify(startStyle));
}
@ -105,7 +105,7 @@ module.exports = React.createClass({
) {
var startStyles = this.props.startStyles;
var transitionOpts = this.props.enterTransitionOpts;
var domNode = ReactDom.findDOMNode(node);
const domNode = ReactDom.findDOMNode(node);
// start from startStyle 1: 0 is the one we gave it
// to start with, so now we animate 1 etc.
for (var i = 1; i < startStyles.length; ++i) {
@ -145,7 +145,7 @@ module.exports = React.createClass({
// and the FAQ entry, "Preventing memory leaks when
// creating/destroying large numbers of elements"
// (https://github.com/julianshapiro/velocity/issues/47)
var domNode = ReactDom.findDOMNode(this.nodes[k]);
const domNode = ReactDom.findDOMNode(this.nodes[k]);
Velocity.Utilities.removeData(domNode);
}
this.nodes[k] = node;

View file

@ -6,10 +6,12 @@ function bounce( p ) {
var pow2,
bounce = 4;
while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {}
while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {
// just sets pow2
}
return 1 / Math.pow( 4, 3 - bounce ) - 7.5625 * Math.pow( ( pow2 * 3 - 2 ) / 22 - p, 2 );
}
Velocity.Easings.easeOutBounce = function(p) {
return 1 - bounce(1 - p);
}
};

View file

@ -1,3 +1,19 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var MatrixClientPeg = require("./MatrixClientPeg");
module.exports = {
@ -32,18 +48,26 @@ module.exports = {
return whoIsTyping;
},
whoIsTypingString: function(room) {
var whoIsTyping = this.usersTypingApartFromMe(room);
whoIsTypingString: function(whoIsTyping, limit) {
let othersCount = 0;
if (whoIsTyping.length > limit) {
othersCount = whoIsTyping.length - limit + 1;
}
if (whoIsTyping.length == 0) {
return null;
return '';
} else if (whoIsTyping.length == 1) {
return whoIsTyping[0].name + ' is typing';
}
const names = whoIsTyping.map(function(m) {
return m.name;
});
if (othersCount) {
const other = ' other' + (othersCount > 1 ? 's' : '');
return names.slice(0, limit - 1).join(', ') + ' and ' +
othersCount + other + ' are typing';
} else {
var names = whoIsTyping.map(function(m) {
return m.name;
});
var lastPerson = names.shift();
const lastPerson = names.pop();
return names.join(', ') + ' and ' + lastPerson + ' are typing';
}
}
}
};

View file

@ -0,0 +1,175 @@
/*
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 FileSaver from 'file-saver';
import React from 'react';
import * as Matrix from 'matrix-js-sdk';
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
import sdk from '../../../index';
const PHASE_EDIT = 1;
const PHASE_EXPORTING = 2;
export default React.createClass({
displayName: 'ExportE2eKeysDialog',
propTypes: {
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
onFinished: React.PropTypes.func.isRequired,
},
getInitialState: function() {
return {
phase: PHASE_EDIT,
errStr: null,
};
},
componentWillMount: function() {
this._unmounted = false;
},
componentWillUnmount: function() {
this._unmounted = true;
},
_onPassphraseFormSubmit: function(ev) {
ev.preventDefault();
const passphrase = this.refs.passphrase1.value;
if (passphrase !== this.refs.passphrase2.value) {
this.setState({errStr: 'Passphrases must match'});
return false;
}
if (!passphrase) {
this.setState({errStr: 'Passphrase must not be empty'});
return false;
}
this._startExport(passphrase);
return false;
},
_startExport: function(passphrase) {
// extra Promise.resolve() to turn synchronous exceptions into
// asynchronous ones.
Promise.resolve().then(() => {
return this.props.matrixClient.exportRoomKeys();
}).then((k) => {
return MegolmExportEncryption.encryptMegolmKeyFile(
JSON.stringify(k), passphrase,
);
}).then((f) => {
const blob = new Blob([f], {
type: 'text/plain;charset=us-ascii',
});
FileSaver.saveAs(blob, 'riot-keys.txt');
this.props.onFinished(true);
}).catch((e) => {
if (this._unmounted) {
return;
}
this.setState({
errStr: e.message,
phase: PHASE_EDIT,
});
});
this.setState({
errStr: null,
phase: PHASE_EXPORTING,
});
},
_onCancelClick: function(ev) {
ev.preventDefault();
this.props.onFinished(false);
return false;
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const disableForm = (this.state.phase === PHASE_EXPORTING);
return (
<BaseDialog className='mx_exportE2eKeysDialog'
onFinished={this.props.onFinished}
title="Export room keys"
>
<form onSubmit={this._onPassphraseFormSubmit}>
<div className="mx_Dialog_content">
<p>
This process allows you to export the keys for messages
you have received in encrypted rooms to a local file. You
will then be able to import the file into another Matrix
client in the future, so that client will also be able to
decrypt these messages.
</p>
<p>
The exported file will allow anyone who can read it to decrypt
any encrypted messages that you can see, so you should be
careful to keep it secure. To help with this, you should enter
a passphrase below, which will be used to encrypt the exported
data. It will only be possible to import the data by using the
same passphrase.
</p>
<div className='error'>
{this.state.errStr}
</div>
<div className='mx_E2eKeysDialog_inputTable'>
<div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='passphrase1'>
Enter passphrase
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input ref='passphrase1' id='passphrase1'
autoFocus={true} size='64' type='password'
disabled={disableForm}
/>
</div>
</div>
<div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='passphrase2'>
Confirm passphrase
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input ref='passphrase2' id='passphrase2'
size='64' type='password'
disabled={disableForm}
/>
</div>
</div>
</div>
</div>
<div className='mx_Dialog_buttons'>
<input className='mx_Dialog_primary' type='submit' value='Export'
disabled={disableForm}
/>
<button onClick={this._onCancelClick} disabled={disableForm}>
Cancel
</button>
</div>
</form>
</BaseDialog>
);
},
});

View file

@ -0,0 +1,174 @@
/*
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 * as Matrix from 'matrix-js-sdk';
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
import sdk from '../../../index';
function readFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
resolve(e.target.result);
};
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
}
const PHASE_EDIT = 1;
const PHASE_IMPORTING = 2;
export default React.createClass({
displayName: 'ImportE2eKeysDialog',
propTypes: {
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
onFinished: React.PropTypes.func.isRequired,
},
getInitialState: function() {
return {
enableSubmit: false,
phase: PHASE_EDIT,
errStr: null,
};
},
componentWillMount: function() {
this._unmounted = false;
},
componentWillUnmount: function() {
this._unmounted = true;
},
_onFormChange: function(ev) {
const files = this.refs.file.files || [];
this.setState({
enableSubmit: (this.refs.passphrase.value !== "" && files.length > 0),
});
},
_onFormSubmit: function(ev) {
ev.preventDefault();
this._startImport(this.refs.file.files[0], this.refs.passphrase.value);
return false;
},
_startImport: function(file, passphrase) {
this.setState({
errStr: null,
phase: PHASE_IMPORTING,
});
return readFileAsArrayBuffer(file).then((arrayBuffer) => {
return MegolmExportEncryption.decryptMegolmKeyFile(
arrayBuffer, passphrase,
);
}).then((keys) => {
return this.props.matrixClient.importRoomKeys(JSON.parse(keys));
}).then(() => {
// TODO: it would probably be nice to give some feedback about what we've imported here.
this.props.onFinished(true);
}).catch((e) => {
if (this._unmounted) {
return;
}
this.setState({
errStr: e.message,
phase: PHASE_EDIT,
});
});
},
_onCancelClick: function(ev) {
ev.preventDefault();
this.props.onFinished(false);
return false;
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const disableForm = (this.state.phase !== PHASE_EDIT);
return (
<BaseDialog className='mx_importE2eKeysDialog'
onFinished={this.props.onFinished}
title="Import room keys"
>
<form onSubmit={this._onFormSubmit}>
<div className="mx_Dialog_content">
<p>
This process allows you to import encryption keys
that you had previously exported from another Matrix
client. You will then be able to decrypt any
messages that the other client could decrypt.
</p>
<p>
The export file will be protected with a passphrase.
You should enter the passphrase here, to decrypt the
file.
</p>
<div className='error'>
{this.state.errStr}
</div>
<div className='mx_E2eKeysDialog_inputTable'>
<div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='importFile'>
File to import
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input ref='file' id='importFile' type='file'
autoFocus={true}
onChange={this._onFormChange}
disabled={disableForm} />
</div>
</div>
<div className='mx_E2eKeysDialog_inputRow'>
<div className='mx_E2eKeysDialog_inputLabel'>
<label htmlFor='passphrase'>
Enter passphrase
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input ref='passphrase' id='passphrase'
size='64' type='password'
onChange={this._onFormChange}
disabled={disableForm}/>
</div>
</div>
</div>
</div>
<div className='mx_Dialog_buttons'>
<input className='mx_Dialog_primary' type='submit' value='Import'
disabled={!this.state.enableSubmit || disableForm}
/>
<button onClick={this._onCancelClick} disabled={disableForm}>
Cancel
</button>
</div>
</form>
</BaseDialog>
);
},
});

View file

@ -26,7 +26,7 @@ export default class AutocompleteProvider {
}
commandRegex.lastIndex = 0;
let match;
while ((match = commandRegex.exec(query)) != null) {
let matchStart = match.index,

View file

@ -83,7 +83,7 @@ export default class CommandProvider extends AutocompleteProvider {
static getInstance(): CommandProvider {
if (instance == null)
instance = new CommandProvider();
{instance = new CommandProvider();}
return instance;
}

View file

@ -13,7 +13,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
constructor() {
super(DDG_REGEX);
}
static getQueryUri(query: String) {
return `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}`
+ `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`;

View file

@ -44,7 +44,7 @@ export default class EmojiProvider extends AutocompleteProvider {
static getInstance() {
if (instance == null)
instance = new EmojiProvider();
{instance = new EmojiProvider();}
return instance;
}

View file

@ -52,12 +52,12 @@ export default class RoomProvider extends AutocompleteProvider {
getName() {
return '💬 Rooms';
}
static getInstance() {
if (instance == null) {
instance = new RoomProvider();
}
return instance;
}

View file

@ -71,18 +71,16 @@ import views$create_room$Presets from './components/views/create_room/Presets';
views$create_room$Presets && (module.exports.components['views.create_room.Presets'] = views$create_room$Presets);
import views$create_room$RoomAlias from './components/views/create_room/RoomAlias';
views$create_room$RoomAlias && (module.exports.components['views.create_room.RoomAlias'] = views$create_room$RoomAlias);
import views$dialogs$BaseDialog from './components/views/dialogs/BaseDialog';
views$dialogs$BaseDialog && (module.exports.components['views.dialogs.BaseDialog'] = views$dialogs$BaseDialog);
import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInviteDialog';
views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = views$dialogs$ChatInviteDialog);
import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog';
views$dialogs$DeactivateAccountDialog && (module.exports.components['views.dialogs.DeactivateAccountDialog'] = views$dialogs$DeactivateAccountDialog);
import views$dialogs$EncryptedEventDialog from './components/views/dialogs/EncryptedEventDialog';
views$dialogs$EncryptedEventDialog && (module.exports.components['views.dialogs.EncryptedEventDialog'] = views$dialogs$EncryptedEventDialog);
import views$dialogs$ErrorDialog from './components/views/dialogs/ErrorDialog';
views$dialogs$ErrorDialog && (module.exports.components['views.dialogs.ErrorDialog'] = views$dialogs$ErrorDialog);
import views$dialogs$InteractiveAuthDialog from './components/views/dialogs/InteractiveAuthDialog';
views$dialogs$InteractiveAuthDialog && (module.exports.components['views.dialogs.InteractiveAuthDialog'] = views$dialogs$InteractiveAuthDialog);
import views$dialogs$LogoutPrompt from './components/views/dialogs/LogoutPrompt';
views$dialogs$LogoutPrompt && (module.exports.components['views.dialogs.LogoutPrompt'] = views$dialogs$LogoutPrompt);
import views$dialogs$NeedToRegisterDialog from './components/views/dialogs/NeedToRegisterDialog';
views$dialogs$NeedToRegisterDialog && (module.exports.components['views.dialogs.NeedToRegisterDialog'] = views$dialogs$NeedToRegisterDialog);
import views$dialogs$QuestionDialog from './components/views/dialogs/QuestionDialog';
@ -91,6 +89,10 @@ import views$dialogs$SetDisplayNameDialog from './components/views/dialogs/SetDi
views$dialogs$SetDisplayNameDialog && (module.exports.components['views.dialogs.SetDisplayNameDialog'] = views$dialogs$SetDisplayNameDialog);
import views$dialogs$TextInputDialog from './components/views/dialogs/TextInputDialog';
views$dialogs$TextInputDialog && (module.exports.components['views.dialogs.TextInputDialog'] = views$dialogs$TextInputDialog);
import views$dialogs$UnknownDeviceDialog from './components/views/dialogs/UnknownDeviceDialog';
views$dialogs$UnknownDeviceDialog && (module.exports.components['views.dialogs.UnknownDeviceDialog'] = views$dialogs$UnknownDeviceDialog);
import views$elements$AccessibleButton from './components/views/elements/AccessibleButton';
views$elements$AccessibleButton && (module.exports.components['views.elements.AccessibleButton'] = views$elements$AccessibleButton);
import views$elements$AddressSelector from './components/views/elements/AddressSelector';
views$elements$AddressSelector && (module.exports.components['views.elements.AddressSelector'] = views$elements$AddressSelector);
import views$elements$AddressTile from './components/views/elements/AddressTile';

View file

@ -47,7 +47,7 @@ module.exports = {
return container;
},
createMenu: function (Element, props) {
createMenu: function(Element, props) {
var self = this;
var closeMenu = function() {
@ -67,7 +67,7 @@ module.exports = {
chevronOffset.top = props.chevronOffset;
}
// To overide the deafult chevron colour, if it's been set
// To override the default chevron colour, if it's been set
var chevronCSS = "";
if (props.menuColour) {
chevronCSS = `
@ -78,15 +78,15 @@ module.exports = {
.mx_ContextualMenu_chevron_right:after {
border-left-color: ${props.menuColour};
}
`
`;
}
var chevron = null;
if (props.left) {
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_left"></div>
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_left"></div>;
position.left = props.left;
} else {
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_right"></div>
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_right"></div>;
position.right = props.right;
}

View file

@ -118,7 +118,7 @@ module.exports = React.createClass({
var self = this;
deferred.then(function (resp) {
deferred.then(function(resp) {
self.setState({
phase: self.phases.CREATED,
});
@ -210,7 +210,7 @@ module.exports = React.createClass({
onAliasChanged: function(alias) {
this.setState({
alias: alias
})
});
},
onEncryptChanged: function(ev) {

View file

@ -35,7 +35,7 @@ var FilePanel = React.createClass({
getInitialState: function() {
return {
timelineSet: null,
}
};
},
componentWillMount: function() {
@ -105,6 +105,7 @@ var FilePanel = React.createClass({
showUrlPreview = { false }
tileShape="file_grid"
opacity={ this.props.opacity }
empty="There are no visible files in this room"
/>
);
}

View file

@ -21,6 +21,7 @@ import KeyCode from '../../KeyCode';
import Notifier from '../../Notifier';
import PageTypes from '../../PageTypes';
import sdk from '../../index';
import dis from '../../dispatcher';
/**
* This is what our MatrixChat shows when we are logged in. The precise view is
@ -41,6 +42,8 @@ export default React.createClass({
onRoomCreated: React.PropTypes.func,
onUserSettingsClose: React.PropTypes.func,
teamToken: React.PropTypes.string,
// and lots and lots of other stuff.
},
@ -136,6 +139,7 @@ export default React.createClass({
var UserSettings = sdk.getComponent('structures.UserSettings');
var CreateRoom = sdk.getComponent('structures.CreateRoom');
var RoomDirectory = sdk.getComponent('structures.RoomDirectory');
var HomePage = sdk.getComponent('structures.HomePage');
var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
var GuestWarningBar = sdk.getComponent('globals.GuestWarningBar');
var NewVersionBar = sdk.getComponent('globals.NewVersionBar');
@ -160,8 +164,8 @@ export default React.createClass({
collapsedRhs={this.props.collapse_rhs}
ConferenceHandler={this.props.ConferenceHandler}
scrollStateMap={this._scrollStateMap}
/>
if (!this.props.collapse_rhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.sideOpacity} />
/>;
if (!this.props.collapse_rhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.sideOpacity} />;
break;
case PageTypes.UserSettings:
@ -170,28 +174,39 @@ export default React.createClass({
brand={this.props.config.brand}
collapsedRhs={this.props.collapse_rhs}
enableLabs={this.props.config.enableLabs}
/>
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>
referralBaseUrl={this.props.config.referralBaseUrl}
/>;
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
break;
case PageTypes.CreateRoom:
page_element = <CreateRoom
onRoomCreated={this.props.onRoomCreated}
collapsedRhs={this.props.collapse_rhs}
/>
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;
case PageTypes.RoomDirectory:
page_element = <RoomDirectory
collapsedRhs={this.props.collapse_rhs}
config={this.props.config.roomDirectory}
/>;
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
break;
case PageTypes.HomePage:
page_element = <HomePage
collapsedRhs={this.props.collapse_rhs}
teamServerUrl={this.props.config.teamServerConfig.teamServerURL}
teamToken={this.props.teamToken}
/>
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>
break;
case PageTypes.UserView:
page_element = null; // deliberately null for now
right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.sideOpacity} />
right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.sideOpacity} />;
break;
}

View file

@ -77,7 +77,7 @@ module.exports = React.createClass({
getChildContext: function() {
return {
appConfig: this.props.config,
}
};
},
getInitialState: function() {
@ -190,6 +190,11 @@ module.exports = React.createClass({
if (this.props.config.sync_timeline_limit) {
MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit;
}
// 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') ||
this.props.startingFragmentQueryParams.team_token;
},
componentDidMount: function() {
@ -210,6 +215,12 @@ module.exports = React.createClass({
window.addEventListener('resize', this.handleResize);
this.handleResize();
if (this.props.config.teamServerConfig &&
this.props.config.teamServerConfig.teamServerURL
) {
Lifecycle.initRtsClient(this.props.config.teamServerConfig.teamServerURL);
}
// the extra q() ensures that synchronous exceptions hit the same codepath as
// asynchronous ones.
q().then(() => {
@ -421,6 +432,10 @@ module.exports = React.createClass({
this._setPage(PageTypes.RoomDirectory);
this.notifyNewScreen('directory');
break;
case 'view_home_page':
this._setPage(PageTypes.HomePage);
this.notifyNewScreen('home');
break;
case 'view_create_chat':
this._createChat();
break;
@ -456,6 +471,9 @@ module.exports = React.createClass({
middleOpacity: payload.middleOpacity,
});
break;
case 'set_theme':
this._onSetTheme(payload.value);
break;
case 'on_logged_in':
this._onLoggedIn();
break;
@ -586,6 +604,50 @@ module.exports = React.createClass({
this.setState({loading: false});
},
/**
* Called whenever someone changes the theme
*/
_onSetTheme: function(theme) {
if (!theme) {
theme = 'light';
}
// look for the stylesheet elements.
// styleElements is a map from style name to HTMLLinkElement.
var styleElements = Object.create(null);
var i, a;
for (i = 0; (a = document.getElementsByTagName("link")[i]); i++) {
var href = a.getAttribute("href");
// shouldn't we be using the 'title' tag rather than the href?
var match = href.match(/^bundles\/.*\/theme-(.*)\.css$/);
if (match) {
styleElements[match[1]] = a;
}
}
if (!(theme in styleElements)) {
throw new Error("Unknown theme " + theme);
}
// disable all of them first, then enable the one we want. Chrome only
// bothers to do an update on a true->false transition, so this ensures
// that we get exactly one update, at the right time.
Object.values(styleElements).forEach((a) => {
a.disabled = true;
});
styleElements[theme].disabled = false;
if (theme === 'dark') {
// abuse the tinter to change all the SVG's #fff to #2d2d2d
// XXX: obviously this shouldn't be hardcoded here.
Tinter.tintSvgWhite('#2d2d2d');
}
else {
Tinter.tintSvgWhite('#ffffff');
}
},
/**
* Called when a new logged in session has started
*/
@ -643,7 +705,11 @@ module.exports = React.createClass({
)[0].roomId;
self.setState({ready: true, currentRoomId: firstRoom, page_type: PageTypes.RoomView});
} else {
self.setState({ready: true, page_type: PageTypes.RoomDirectory});
if (self._teamToken) {
self.setState({ready: true, page_type: PageTypes.HomePage});
} else {
self.setState({ready: true, page_type: PageTypes.RoomDirectory});
}
}
} else {
self.setState({ready: true, page_type: PageTypes.RoomView});
@ -663,7 +729,11 @@ module.exports = React.createClass({
} else {
// There is no information on presentedId
// so point user to fallback like /directory
self.notifyNewScreen('directory');
if (self._teamToken) {
self.notifyNewScreen('home');
} else {
self.notifyNewScreen('directory');
}
}
dis.dispatch({action: 'focus_composer'});
@ -687,6 +757,16 @@ module.exports = React.createClass({
action: 'logout'
});
});
cli.on("accountData", function(ev) {
if (ev.getType() === 'im.vector.web.settings') {
if (ev.getContent() && ev.getContent().theme) {
dis.dispatch({
action: 'set_theme',
value: ev.getContent().theme,
});
}
}
});
},
onFocus: function(ev) {
@ -717,6 +797,10 @@ module.exports = React.createClass({
dis.dispatch({
action: 'view_user_settings',
});
} else if (screen == 'home') {
dis.dispatch({
action: 'view_home_page',
});
} else if (screen == 'directory') {
dis.dispatch({
action: 'view_room_directory',
@ -976,10 +1060,11 @@ module.exports = React.createClass({
onRoomIdResolved={this.onRoomIdResolved}
onRoomCreated={this.onRoomCreated}
onUserSettingsClose={this.onUserSettingsClose}
teamToken={this._teamToken}
{...this.props}
{...this.state}
/>
)
);
} else if (this.state.logged_in) {
// we think we are logged in, but are still waiting for the /sync to complete
var Spinner = sdk.getComponent('elements.Spinner');
@ -998,11 +1083,13 @@ module.exports = React.createClass({
sessionId={this.state.register_session_id}
idSid={this.state.register_id_sid}
email={this.props.startingFragmentQueryParams.email}
referrer={this.props.startingFragmentQueryParams.referrer}
username={this.state.upgradeUsername}
guestAccessToken={this.state.guestAccessToken}
defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()}
brand={this.props.config.brand}
teamServerConfig={this.props.config.teamServerConfig}
customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()}
registrationUrl={this.props.registrationUrl}

View file

@ -19,7 +19,7 @@ var ReactDOM = require("react-dom");
var dis = require("../../dispatcher");
var sdk = require('../../index');
var MatrixClientPeg = require('../../MatrixClientPeg')
var MatrixClientPeg = require('../../MatrixClientPeg');
const MILLIS_IN_DAY = 86400000;
@ -281,8 +281,7 @@ module.exports = React.createClass({
var isMembershipChange = (e) =>
e.getType() === 'm.room.member'
&& ['join', 'leave'].indexOf(e.getContent().membership) !== -1
&& (!e.getPrevContent() || e.getContent().membership !== e.getPrevContent().membership);
&& (!e.getPrevContent() || e.getContent().membership !== e.getPrevContent().membership);
for (i = 0; i < this.props.events.length; i++) {
var mxEv = this.props.events[i];
@ -295,8 +294,8 @@ module.exports = React.createClass({
var last = (i == lastShownEventIndex);
// Wrap consecutive member events in a ListSummary
if (isMembershipChange(mxEv)) {
// Wrap consecutive member events in a ListSummary, ignore if redacted
if (isMembershipChange(mxEv) && EventTile.haveTileForEvent(mxEv)) {
let ts1 = mxEv.getTs();
// Ensure that the key of the MemberEventListSummary does not change with new
// member events. This will prevent it from being re-created unnecessarily, and
@ -317,6 +316,11 @@ module.exports = React.createClass({
for (;i + 1 < this.props.events.length; i++) {
let collapsedMxEv = this.props.events[i + 1];
// Ignore redacted member events
if (!EventTile.haveTileForEvent(collapsedMxEv)) {
continue;
}
if (!isMembershipChange(collapsedMxEv) ||
this._wantsDateSeparator(this.props.events[i], collapsedMxEv.getDate())) {
break;
@ -335,7 +339,7 @@ module.exports = React.createClass({
prevEvent = e;
return ret;
}
).reduce((a,b) => a.concat(b));
).reduce((a, b) => a.concat(b));
if (eventTiles.length === 0) {
eventTiles = null;

View file

@ -48,6 +48,7 @@ var NotificationPanel = React.createClass({
showUrlPreview = { false }
opacity={ this.props.opacity }
tileShape="notif"
empty="You have no visible notifications"
/>
);
}

View file

@ -19,6 +19,12 @@ var sdk = require('../../index');
var dis = require("../../dispatcher");
var WhoIsTyping = require("../../WhoIsTyping");
var MatrixClientPeg = require("../../MatrixClientPeg");
const MemberAvatar = require("../views/avatars/MemberAvatar");
const HIDE_DEBOUNCE_MS = 10000;
const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1;
const STATUS_BAR_EXPANDED_LARGE = 2;
module.exports = React.createClass({
displayName: 'RoomStatusBar',
@ -33,8 +39,8 @@ module.exports = React.createClass({
// the number of messages which have arrived since we've been scrolled up
numUnreadMessages: React.PropTypes.number,
// true if there are messages in the room which had errors on send
hasUnsentMessages: React.PropTypes.bool,
// string to display when there are messages in the room which had errors on send
unsentMessageError: React.PropTypes.string,
// this is true if we are fully scrolled-down, and are looking at
// the end of the live timeline.
@ -45,6 +51,10 @@ module.exports = React.createClass({
// more interesting)
hasActiveCall: React.PropTypes.bool,
// Number of names to display in typing indication. E.g. set to 3, will
// result in "X, Y, Z and 100 others are typing."
whoIsTypingLimit: React.PropTypes.number,
// callback for when the user clicks on the 'resend all' button in the
// 'unsent messages' bar
onResendAllClick: React.PropTypes.func,
@ -60,12 +70,26 @@ module.exports = React.createClass({
// status bar. This is used to trigger a re-layout in the parent
// component.
onResize: React.PropTypes.func,
// callback for when the status bar can be hidden from view, as it is
// not displaying anything
onHidden: React.PropTypes.func,
// callback for when the status bar is displaying something and should
// be visible
onVisible: React.PropTypes.func,
},
getDefaultProps: function() {
return {
whoIsTypingLimit: 3,
};
},
getInitialState: function() {
return {
syncState: MatrixClientPeg.get().getSyncState(),
whoisTypingString: WhoIsTyping.whoIsTypingString(this.props.room),
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
};
},
@ -78,6 +102,20 @@ module.exports = React.createClass({
if(this.props.onResize && this._checkForResize(prevProps, prevState)) {
this.props.onResize();
}
const size = this._getSize(this.props, this.state);
if (size > 0) {
this.props.onVisible();
} else {
if (this.hideDebouncer) {
clearTimeout(this.hideDebouncer);
}
this.hideDebouncer = setTimeout(() => {
// temporarily stop hiding the statusbar as per
// https://github.com/vector-im/riot-web/issues/1991#issuecomment-276953915
// this.props.onHidden();
}, HIDE_DEBOUNCE_MS);
}
},
componentWillUnmount: function() {
@ -100,39 +138,33 @@ module.exports = React.createClass({
onRoomMemberTyping: function(ev, member) {
this.setState({
whoisTypingString: WhoIsTyping.whoIsTypingString(this.props.room),
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
});
},
// We don't need the actual height - just whether it is likely to have
// changed - so we use '0' to indicate normal size, and other values to
// indicate other sizes.
_getSize: function(props, state) {
if (state.syncState === "ERROR" ||
(state.usersTyping.length > 0) ||
props.numUnreadMessages ||
!props.atEndOfLiveTimeline ||
props.hasActiveCall) {
return STATUS_BAR_EXPANDED;
} else if (props.tabCompleteEntries) {
return STATUS_BAR_HIDDEN;
} else if (props.unsentMessageError) {
return STATUS_BAR_EXPANDED_LARGE;
}
return STATUS_BAR_HIDDEN;
},
// determine if we need to call onResize
_checkForResize: function(prevProps, prevState) {
// figure out the old height and the new height of the status bar. We
// don't need the actual height - just whether it is likely to have
// changed - so we use '0' to indicate normal size, and other values to
// indicate other sizes.
var oldSize, newSize;
if (prevState.syncState === "ERROR") {
oldSize = 1;
} else if (prevProps.tabCompleteEntries) {
oldSize = 0;
} else if (prevProps.hasUnsentMessages) {
oldSize = 2;
} else {
oldSize = 0;
}
if (this.state.syncState === "ERROR") {
newSize = 1;
} else if (this.props.tabCompleteEntries) {
newSize = 0;
} else if (this.props.hasUnsentMessages) {
newSize = 2;
} else {
newSize = 0;
}
return newSize != oldSize;
// 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 suitable content for the image on the left of the status bar.
@ -173,10 +205,8 @@ module.exports = React.createClass({
if (wantPlaceholder) {
return (
<div className="mx_RoomStatusBar_placeholderIndicator">
<span>.</span>
<span>.</span>
<span>.</span>
<div className="mx_RoomStatusBar_typingIndicatorAvatars">
{this._renderTypingIndicatorAvatars(this.props.whoIsTypingLimit)}
</div>
);
}
@ -184,6 +214,39 @@ module.exports = React.createClass({
return null;
},
_renderTypingIndicatorAvatars: function(limit) {
let users = this.state.usersTyping;
let othersCount = 0;
if (users.length > limit) {
othersCount = users.length - limit + 1;
users = users.slice(0, limit - 1);
}
let avatars = users.map((u, index) => {
let showInitial = othersCount === 0 && index === users.length - 1;
return (
<MemberAvatar
key={u.userId}
member={u}
width={24}
height={24}
resizeMethod="crop"
defaultToInitialLetter={showInitial}
/>
);
});
if (othersCount > 0) {
avatars.push(
<span className="mx_RoomStatusBar_typingIndicatorRemaining" key="others">
+{othersCount}
</span>
);
}
return avatars;
},
// return suitable content for the main (text) part of the status bar.
_getContent: function() {
@ -223,12 +286,12 @@ module.exports = React.createClass({
);
}
if (this.props.hasUnsentMessages) {
if (this.props.unsentMessageError) {
return (
<div className="mx_RoomStatusBar_connectionLostBar">
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ "/>
<div className="mx_RoomStatusBar_connectionLostBar_title">
Some of your messages have not been sent.
{ this.props.unsentMessageError }
</div>
<div className="mx_RoomStatusBar_connectionLostBar_desc">
<a className="mx_RoomStatusBar_resend_link"
@ -259,7 +322,10 @@ module.exports = React.createClass({
);
}
var typingString = this.state.whoisTypingString;
const typingString = WhoIsTyping.whoIsTypingString(
this.state.usersTyping,
this.props.whoIsTypingLimit
);
if (typingString) {
return (
<div className="mx_RoomStatusBar_typingBar">
@ -282,7 +348,7 @@ module.exports = React.createClass({
render: function() {
var content = this._getContent();
var indicator = this._getIndicator(this.state.whoisTypingString !== null);
var indicator = this._getIndicator(this.state.usersTyping.length > 0);
return (
<div className="mx_RoomStatusBar">

View file

@ -48,7 +48,7 @@ if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
var debuglog = console.log.bind(console);
} else {
var debuglog = function () {};
var debuglog = function() {};
}
module.exports = React.createClass({
@ -128,7 +128,7 @@ module.exports = React.createClass({
draggingFile: false,
searching: false,
searchResults: null,
hasUnsentMessages: false,
unsentMessageError: '',
callState: null,
guestsCanJoin: false,
canPeek: false,
@ -146,7 +146,9 @@ module.exports = React.createClass({
showTopUnreadMessagesBar: false,
auxPanelMaxHeight: undefined,
}
statusBarVisible: false,
};
},
componentWillMount: function() {
@ -180,7 +182,7 @@ module.exports = React.createClass({
room: room,
roomId: result.room_id,
roomLoading: !room,
hasUnsentMessages: this._hasUnsentMessages(room),
unsentMessageError: this._getUnsentMessageError(room),
}, this._onHaveRoom);
}, (err) => {
this.setState({
@ -194,7 +196,7 @@ module.exports = React.createClass({
roomId: this.props.roomAddress,
room: room,
roomLoading: !room,
hasUnsentMessages: this._hasUnsentMessages(room),
unsentMessageError: this._getUnsentMessageError(room),
}, this._onHaveRoom);
}
},
@ -395,7 +397,7 @@ module.exports = React.createClass({
case 'message_sent':
case 'message_send_cancelled':
this.setState({
hasUnsentMessages: this._hasUnsentMessages(this.state.room)
unsentMessageError: this._getUnsentMessageError(this.state.room),
});
break;
case 'notifier_enabled':
@ -634,8 +636,15 @@ module.exports = React.createClass({
}
}, 500),
_hasUnsentMessages: function(room) {
return this._getUnsentMessages(room).length > 0;
_getUnsentMessageError: function(room) {
const unsentMessages = this._getUnsentMessages(room);
if (!unsentMessages.length) return "";
for (const event of unsentMessages) {
if (!event.error || event.error.name !== "UnknownDeviceError") {
return "Some of your messages have not been sent.";
}
}
return "Message not sent due to unknown devices being present";
},
_getUnsentMessages: function(room) {
@ -674,8 +683,9 @@ module.exports = React.createClass({
},
onSearchResultsFillRequest: function(backwards) {
if (!backwards)
if (!backwards) {
return q(false);
}
if (this.state.searchResults.next_batch) {
debuglog("requesting more search results");
@ -719,15 +729,11 @@ module.exports = React.createClass({
if (!result.displayname) {
var SetDisplayNameDialog = sdk.getComponent('views.dialogs.SetDisplayNameDialog');
var dialog_defer = q.defer();
var dialog_ref;
Modal.createDialog(SetDisplayNameDialog, {
currentDisplayName: result.displayname,
ref: (r) => {
dialog_ref = r;
},
onFinished: (submitted) => {
onFinished: (submitted, newDisplayName) => {
if (submitted) {
cli.setDisplayName(dialog_ref.getValue()).done(() => {
cli.setDisplayName(newDisplayName).done(() => {
dialog_defer.resolve();
});
}
@ -758,7 +764,7 @@ module.exports = React.createClass({
}).then(() => {
var sign_url = this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : undefined;
return MatrixClientPeg.get().joinRoom(this.props.roomAddress,
{ inviteSignUrl: sign_url } )
{ inviteSignUrl: sign_url } );
}).then(function(resp) {
var roomId = resp.roomId;
@ -962,7 +968,7 @@ module.exports = React.createClass({
// For overlapping highlights,
// favour longer (more specific) terms first
highlights = highlights.sort(function(a, b) {
return b.length - a.length });
return b.length - a.length; });
self.setState({
searchHighlights: highlights,
@ -1025,7 +1031,7 @@ module.exports = React.createClass({
if (scrollPanel) {
scrollPanel.checkScroll();
}
}
};
var lastRoomId;
@ -1090,7 +1096,7 @@ module.exports = React.createClass({
}
this.refs.room_settings.save().then((results) => {
var fails = results.filter(function(result) { return result.state !== "fulfilled" });
var fails = results.filter(function(result) { return result.state !== "fulfilled"; });
console.log("Settings saved with %s errors", fails.length);
if (fails.length) {
fails.forEach(function(result) {
@ -1099,7 +1105,7 @@ module.exports = React.createClass({
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Failed to save settings",
description: fails.map(function(result) { return result.reason }).join("\n"),
description: fails.map(function(result) { return result.reason; }).join("\n"),
});
// still editing room settings
}
@ -1183,7 +1189,7 @@ module.exports = React.createClass({
this.setState({ searching: true });
},
onCancelSearchClick: function () {
onCancelSearchClick: function() {
this.setState({
searching: false,
searchResults: null,
@ -1208,8 +1214,9 @@ module.exports = React.createClass({
// decide whether or not the top 'unread messages' bar should be shown
_updateTopUnreadMessagesBar: function() {
if (!this.refs.messagePanel)
if (!this.refs.messagePanel) {
return;
}
var pos = this.refs.messagePanel.getReadMarkerPosition();
@ -1331,6 +1338,20 @@ module.exports = React.createClass({
// no longer anything to do here
},
onStatusBarVisible: function() {
if (this.unmounted) return;
this.setState({
statusBarVisible: true,
});
},
onStatusBarHidden: function() {
if (this.unmounted) return;
this.setState({
statusBarVisible: false,
});
},
showSettings: function(show) {
// XXX: this is a bit naughty; we should be doing this via props
if (show) {
@ -1495,25 +1516,29 @@ module.exports = React.createClass({
});
var statusBar;
let isStatusAreaExpanded = true;
if (ContentMessages.getCurrentUploads().length > 0) {
var UploadBar = sdk.getComponent('structures.UploadBar');
statusBar = <UploadBar room={this.state.room} />
statusBar = <UploadBar room={this.state.room} />;
} else if (!this.state.searchResults) {
var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar');
isStatusAreaExpanded = this.state.statusBarVisible;
statusBar = <RoomStatusBar
room={this.state.room}
tabComplete={this.tabComplete}
numUnreadMessages={this.state.numUnreadMessages}
hasUnsentMessages={this.state.hasUnsentMessages}
unsentMessageError={this.state.unsentMessageError}
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}
hasActiveCall={inCall}
onResendAllClick={this.onResendAllClick}
onCancelAllClick={this.onCancelAllClick}
onScrollToBottomClick={this.jumpToLiveTimeline}
onResize={this.onChildResize}
/>
onVisible={this.onStatusBarVisible}
onHidden={this.onStatusBarHidden}
whoIsTypingLimit={3}
/>;
}
var aux = null;
@ -1569,7 +1594,7 @@ module.exports = React.createClass({
messageComposer =
<MessageComposer
room={this.state.room} onResize={this.onChildResize} uploadFile={this.uploadFile}
callState={this.state.callState} tabComplete={this.tabComplete} opacity={ this.props.opacity }/>
callState={this.state.callState} tabComplete={this.tabComplete} opacity={ this.props.opacity }/>;
}
// TODO: Why aren't we storing the term/scope/count in this format
@ -1597,14 +1622,14 @@ module.exports = React.createClass({
<img src={call.isLocalVideoMuted() ? "img/video-unmute.svg" : "img/video-mute.svg"}
alt={call.isLocalVideoMuted() ? "Click to unmute video" : "Click to mute video"}
width="31" height="27"/>
</div>
</div>;
}
voiceMuteButton =
<div className="mx_RoomView_voipButton" onClick={this.onMuteAudioClick}>
<img src={call.isMicrophoneMuted() ? "img/voice-unmute.svg" : "img/voice-mute.svg"}
alt={call.isMicrophoneMuted() ? "Click to unmute audio" : "Click to mute audio"}
width="21" height="26"/>
</div>
</div>;
// wrap the existing status bar into a 'callStatusBar' which adds more knobs.
statusBar =
@ -1614,7 +1639,7 @@ module.exports = React.createClass({
{ zoomButton }
{ statusBar }
<TintableSvg className="mx_RoomView_voipChevron" src="img/voip-chevron.svg" width="22" height="17"/>
</div>
</div>;
}
// if we have search results, we keep the messagepanel (so that it preserves its
@ -1667,6 +1692,10 @@ module.exports = React.createClass({
</div>
);
}
let statusBarAreaClass = "mx_RoomView_statusArea mx_fadable";
if (isStatusAreaExpanded) {
statusBarAreaClass += " mx_RoomView_statusArea_expanded";
}
return (
<div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") } ref="roomView">
@ -1689,7 +1718,7 @@ module.exports = React.createClass({
{ topUnreadMessagesBar }
{ messagePanel }
{ searchResultsPanel }
<div className="mx_RoomView_statusArea mx_fadable" style={{ opacity: this.props.opacity }}>
<div className={statusBarAreaClass} style={{opacity: this.props.opacity}}>
<div className="mx_RoomView_statusAreaBox">
<div className="mx_RoomView_statusAreaBox_line"></div>
{ statusBar }

View file

@ -25,7 +25,7 @@ var DEBUG_SCROLL = false;
// The amount of extra scroll distance to allow prior to unfilling.
// See _getExcessHeight.
const UNPAGINATION_PADDING = 1500;
const UNPAGINATION_PADDING = 3000;
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent
// many scroll events causing many unfilling requests.
const UNFILL_REQUEST_DEBOUNCE_MS = 200;
@ -34,7 +34,7 @@ if (DEBUG_SCROLL) {
// using bind means that we get to keep useful line numbers in the console
var debuglog = console.log.bind(console);
} else {
var debuglog = function () {};
var debuglog = function() {};
}
/* This component implements an intelligent scrolling list.
@ -570,7 +570,7 @@ module.exports = React.createClass({
var boundingRect = node.getBoundingClientRect();
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
debuglog("Scrolling to token '" + node.dataset.scrollToken + "'+" +
debuglog("ScrollPanel: scrolling to token '" + node.dataset.scrollToken + "'+" +
pixelOffset + " (delta: "+scrollDelta+")");
if(scrollDelta != 0) {
@ -582,7 +582,7 @@ module.exports = React.createClass({
_saveScrollState: function() {
if (this.props.stickyBottom && this.isAtBottom()) {
this.scrollState = { stuckAtBottom: true };
debuglog("Saved scroll state", this.scrollState);
debuglog("ScrollPanel: Saved scroll state", this.scrollState);
return;
}
@ -600,13 +600,13 @@ module.exports = React.createClass({
stuckAtBottom: false,
trackedScrollToken: node.dataset.scrollToken,
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
}
debuglog("Saved scroll state", this.scrollState);
};
debuglog("ScrollPanel: saved scroll state", this.scrollState);
return;
}
}
debuglog("Unable to save scroll state: found no children in the viewport");
debuglog("ScrollPanel: unable to save scroll state: found no children in the viewport");
},
_restoreSavedScrollState: function() {
@ -640,7 +640,7 @@ module.exports = React.createClass({
this._lastSetScroll = scrollNode.scrollTop;
}
debuglog("Set scrollTop:", scrollNode.scrollTop,
debuglog("ScrollPanel: set scrollTop:", scrollNode.scrollTop,
"requested:", scrollTop,
"_lastSetScroll:", this._lastSetScroll);
},

View file

@ -38,7 +38,7 @@ if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
var debuglog = console.log.bind(console);
} else {
var debuglog = function () {};
var debuglog = function() {};
}
/*
@ -96,6 +96,9 @@ var TimelinePanel = React.createClass({
// shape property to be passed to EventTiles
tileShape: React.PropTypes.string,
// placeholder text to use if the timeline is empty
empty: React.PropTypes.string,
},
statics: {
@ -322,7 +325,7 @@ var TimelinePanel = React.createClass({
});
},
onMessageListScroll: function () {
onMessageListScroll: function() {
if (this.props.onScroll) {
this.props.onScroll();
}
@ -387,7 +390,7 @@ var TimelinePanel = React.createClass({
// if we're at the end of the live timeline, append the pending events
if (this.props.timelineSet.room && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
events.push(... this.props.timelineSet.room.getPendingEvents());
events.push(...this.props.timelineSet.room.getPendingEvents());
}
var updatedState = {events: events};
@ -564,8 +567,9 @@ var TimelinePanel = React.createClass({
// first find where the current RM is
for (var i = 0; i < events.length; i++) {
if (events[i].getId() == this.state.readMarkerEventId)
if (events[i].getId() == this.state.readMarkerEventId) {
break;
}
}
if (i >= events.length) {
return;
@ -644,7 +648,7 @@ var TimelinePanel = React.createClass({
var tl = this.props.timelineSet.getTimelineForEvent(rmId);
var rmTs;
if (tl) {
var event = tl.getEvents().find((e) => { return e.getId() == rmId });
var event = tl.getEvents().find((e) => { return e.getId() == rmId; });
if (event) {
rmTs = event.getTs();
}
@ -821,7 +825,7 @@ var TimelinePanel = React.createClass({
description: message,
onFinished: onFinished,
});
}
};
var prom = this._timelineWindow.load(eventId, INITIAL_SIZE);
@ -843,7 +847,7 @@ var TimelinePanel = React.createClass({
timelineLoading: true,
});
prom = prom.then(onLoaded, onError)
prom = prom.then(onLoaded, onError);
}
prom.done();
@ -868,7 +872,7 @@ var TimelinePanel = React.createClass({
// if we're at the end of the live timeline, append the pending events
if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
events.push(... this.props.timelineSet.getPendingEvents());
events.push(...this.props.timelineSet.getPendingEvents());
}
return events;
@ -930,8 +934,9 @@ var TimelinePanel = React.createClass({
_getCurrentReadReceipt: function(ignoreSynthesized) {
var client = MatrixClientPeg.get();
// the client can be null on logout
if (client == null)
if (client == null) {
return null;
}
var myUserId = client.credentials.userId;
return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized);
@ -988,6 +993,14 @@ var TimelinePanel = React.createClass({
);
}
if (this.state.events.length == 0 && !this.state.canBackPaginate && this.props.empty) {
return (
<div className={ this.props.className + " mx_RoomView_messageListWrapper" }>
<div className="mx_RoomView_empty">{ this.props.empty }</div>
</div>
);
}
// give the messagepanel a stickybottom if we're at the end of the
// live timeline, so that the arrival of new events triggers a
// scroll.

View file

@ -57,7 +57,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
// }];
if (uploads.length == 0) {
return <div />
return <div />;
}
var upload;
@ -68,7 +68,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
}
}
if (!upload) {
return <div />
return <div />;
}
var innerProgressStyle = {
@ -76,7 +76,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
};
var uploadedSize = filesize(upload.loaded);
var totalSize = filesize(upload.total);
if (uploadedSize.replace(/^.* /,'') === totalSize.replace(/^.* /,'')) {
if (uploadedSize.replace(/^.* /, '') === totalSize.replace(/^.* /, '')) {
uploadedSize = uploadedSize.replace(/ .*/, '');
}
@ -90,8 +90,8 @@ module.exports = React.createClass({displayName: 'UploadBar',
<div className="mx_UploadBar_uploadProgressOuter">
<div className="mx_UploadBar_uploadProgressInner" style={innerProgressStyle}></div>
</div>
<img className="mx_UploadBar_uploadIcon" src="img/fileicon.png" width="17" height="22"/>
<img className="mx_UploadBar_uploadCancel" src="img/cancel.svg" width="18" height="18"
<img className="mx_UploadBar_uploadIcon mx_filterFlipColor" src="img/fileicon.png" width="17" height="22"/>
<img className="mx_UploadBar_uploadCancel mx_filterFlipColor" src="img/cancel.svg" width="18" height="18"
onClick={function() { ContentMessages.cancelUpload(upload.promise); }}
/>
<div className="mx_UploadBar_uploadBytes">

View file

@ -26,12 +26,73 @@ var UserSettingsStore = require('../../UserSettingsStore');
var GeminiScrollbar = require('react-gemini-scrollbar');
var Email = require('../../email');
var AddThreepid = require('../../AddThreepid');
var SdkConfig = require('../../SdkConfig');
import AccessibleButton from '../views/elements/AccessibleButton';
// if this looks like a release, use the 'version' from package.json; else use
// the git sha.
const REACT_SDK_VERSION =
'dist' in package_json ? package_json.version : package_json.gitHead || "<local>";
// Enumerate some simple 'flip a bit' UI settings (if any).
// 'id' gives the key name in the im.vector.web.settings account data event
// 'label' is how we describe it in the UI.
const SETTINGS_LABELS = [
/*
{
id: 'alwaysShowTimestamps',
label: 'Always show message timestamps',
},
{
id: 'showTwelveHourTimestamps',
label: 'Show timestamps in 12 hour format (e.g. 2:30pm)',
},
{
id: 'useCompactLayout',
label: 'Use compact timeline layout',
},
{
id: 'useFixedWidthFont',
label: 'Use fixed width font',
},
*/
];
const CRYPTO_SETTINGS_LABELS = [
{
id: 'blacklistUnverifiedDevices',
label: 'Never send encrypted messages to unverified devices from this device',
},
// XXX: this is here for documentation; the actual setting is managed via RoomSettings
// {
// id: 'blacklistUnverifiedDevicesPerRoom'
// label: 'Never send encrypted messages to unverified devices in this room',
// }
];
// Enumerate the available themes, with a nice human text label.
// 'id' gives the key name in the im.vector.web.settings account data event
// 'value' is the value for that key in the event
// 'label' is how we describe it in the UI.
//
// XXX: Ideally we would have a theme manifest or something and they'd be nicely
// packaged up in a single directory, and/or located at the application layer.
// But for now for expedience we just hardcode them here.
const THEMES = [
{
id: 'theme',
label: 'Light theme',
value: 'light',
},
{
id: 'theme',
label: 'Dark theme',
value: 'dark',
}
];
module.exports = React.createClass({
displayName: 'UserSettings',
@ -43,6 +104,9 @@ module.exports = React.createClass({
// True to show the 'labs' section of experimental features
enableLabs: React.PropTypes.bool,
// The base URL to use in the referral link. Defaults to window.location.origin.
referralBaseUrl: React.PropTypes.string,
// true if RightPanel is collapsed
collapsedRhs: React.PropTypes.bool,
},
@ -93,6 +157,14 @@ module.exports = React.createClass({
middleOpacity: 0.3,
});
this._refreshFromServer();
var syncedSettings = UserSettingsStore.getSyncedSettings();
if (!syncedSettings.theme) {
syncedSettings.theme = 'light';
}
this._syncedSettings = syncedSettings;
this._localSettings = UserSettingsStore.getLocalSettings();
},
componentDidMount: function() {
@ -175,8 +247,26 @@ module.exports = React.createClass({
},
onLogoutClicked: function(ev) {
var LogoutPrompt = sdk.getComponent('dialogs.LogoutPrompt');
this.logoutModal = Modal.createDialog(LogoutPrompt);
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: "Sign out?",
description:
<div>
For security, logging out will delete any end-to-end encryption keys from this browser,
making previous encrypted chat history unreadable if you log back in.
In future this <a href="https://github.com/vector-im/riot-web/issues/2108">will be improved</a>,
but for now be warned.
</div>,
button: "Sign out",
onFinished: (confirmed) => {
if (confirmed) {
dis.dispatch({action: 'logout'});
if (this.props.onFinished) {
this.props.onFinished();
}
}
},
});
},
onPasswordChangeError: function(err) {
@ -293,8 +383,8 @@ module.exports = React.createClass({
this.setState({email_add_pending: false});
if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
var message = "Unable to verify email address. "
message += "Please check your email and click on the link it contains. Once this is done, click continue."
var message = "Unable to verify email address. ";
message += "Please check your email and click on the link it contains. Once this is done, click continue.";
Modal.createDialog(QuestionDialog, {
title: "Verification Pending",
description: message,
@ -316,6 +406,14 @@ module.exports = React.createClass({
Modal.createDialog(DeactivateAccountDialog, {});
},
_onBugReportClicked: function() {
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
if (!BugReportDialog) {
return;
}
Modal.createDialog(BugReportDialog, {});
},
_onInviteStateChange: function(event, member, oldMembership) {
if (member.userId === this._me && oldMembership === "invite") {
this.forceUpdate();
@ -339,81 +437,175 @@ module.exports = React.createClass({
}).done();
},
_onExportE2eKeysClicked: function() {
Modal.createDialogAsync(
(cb) => {
require.ensure(['../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
cb(require('../../async-components/views/dialogs/ExportE2eKeysDialog'));
}, "e2e-export");
}, {
matrixClient: MatrixClientPeg.get(),
}
);
},
_onImportE2eKeysClicked: function() {
Modal.createDialogAsync(
(cb) => {
require.ensure(['../../async-components/views/dialogs/ImportE2eKeysDialog'], () => {
cb(require('../../async-components/views/dialogs/ImportE2eKeysDialog'));
}, "e2e-export");
}, {
matrixClient: MatrixClientPeg.get(),
}
);
},
_renderReferral: function() {
const teamToken = window.localStorage.getItem('mx_team_token');
if (!teamToken) {
return null;
}
if (typeof teamToken !== 'string') {
console.warn('Team token not a string');
return null;
}
const href = (this.props.referralBaseUrl || window.location.origin) +
`/#/register?referrer=${this._me}&team_token=${teamToken}`;
return (
<div>
<h3>Referral</h3>
<div className="mx_UserSettings_section">
Refer a friend to Riot: <a href={href}>{href}</a>
</div>
</div>
);
},
_renderUserInterfaceSettings: function() {
var client = MatrixClientPeg.get();
var settingsLabels = [
/*
{
id: 'alwaysShowTimestamps',
label: 'Always show message timestamps',
},
{
id: 'showTwelveHourTimestamps',
label: 'Show timestamps in 12 hour format (e.g. 2:30pm)',
},
{
id: 'useCompactLayout',
label: 'Use compact timeline layout',
},
{
id: 'useFixedWidthFont',
label: 'Use fixed width font',
},
*/
];
var syncedSettings = UserSettingsStore.getSyncedSettings();
return (
<div>
<h3>User Interface</h3>
<div className="mx_UserSettings_section">
<div className="mx_UserSettings_toggle">
<input id="urlPreviewsDisabled"
type="checkbox"
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
onChange={ e => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) }
/>
<label htmlFor="urlPreviewsDisabled">
Disable inline URL previews by default
</label>
</div>
{ this._renderUrlPreviewSelector() }
{ SETTINGS_LABELS.map( this._renderSyncedSetting ) }
{ THEMES.map( this._renderThemeSelector ) }
</div>
{ settingsLabels.forEach( setting => {
<div className="mx_UserSettings_toggle">
<input id={ setting.id }
type="checkbox"
defaultChecked={ syncedSettings[setting.id] }
onChange={ e => UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) }
/>
<label htmlFor={ setting.id }>
{ settings.label }
</label>
</div>
})}
</div>
);
},
_renderUrlPreviewSelector: function() {
return <div className="mx_UserSettings_toggle">
<input id="urlPreviewsDisabled"
type="checkbox"
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
onChange={ e => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) }
/>
<label htmlFor="urlPreviewsDisabled">
Disable inline URL previews by default
</label>
</div>;
},
_renderSyncedSetting: function(setting) {
return <div className="mx_UserSettings_toggle" key={ setting.id }>
<input id={ setting.id }
type="checkbox"
defaultChecked={ this._syncedSettings[setting.id] }
onChange={ e => UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) }
/>
<label htmlFor={ setting.id }>
{ setting.label }
</label>
</div>;
},
_renderThemeSelector: function(setting) {
return <div className="mx_UserSettings_toggle" key={ setting.id + "_" + setting.value }>
<input id={ setting.id + "_" + setting.value }
type="radio"
name={ setting.id }
value={ setting.value }
defaultChecked={ this._syncedSettings[setting.id] === setting.value }
onChange={ e => {
if (e.target.checked) {
UserSettingsStore.setSyncedSetting(setting.id, setting.value);
}
dis.dispatch({
action: 'set_theme',
value: setting.value,
});
}
}
/>
<label htmlFor={ setting.id + "_" + setting.value }>
{ setting.label }
</label>
</div>;
},
_renderCryptoInfo: function() {
const client = MatrixClientPeg.get();
const deviceId = client.deviceId;
const identityKey = client.getDeviceEd25519Key() || "<not supported>";
let importExportButtons = null;
if (client.isCryptoEnabled) {
importExportButtons = (
<div className="mx_UserSettings_importExportButtons">
<AccessibleButton className="mx_UserSettings_button"
onClick={this._onExportE2eKeysClicked}>
Export E2E room keys
</AccessibleButton>
<AccessibleButton className="mx_UserSettings_button"
onClick={this._onImportE2eKeysClicked}>
Import E2E room keys
</AccessibleButton>
</div>
);
}
return (
<div>
<h3>Cryptography</h3>
<div className="mx_UserSettings_section mx_UserSettings_cryptoSection">
<ul>
<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 ID:</label> <span><code>{deviceId}</code></span></li>
<li><label>Device key:</label> <span><code><b>{identityKey}</b></code></span></li>
</ul>
{ importExportButtons }
</div>
<div className="mx_UserSettings_section">
{ CRYPTO_SETTINGS_LABELS.map( this._renderLocalSetting ) }
</div>
</div>
);
},
_renderLocalSetting: function(setting) {
const client = MatrixClientPeg.get();
return <div className="mx_UserSettings_toggle" key={ setting.id }>
<input id={ setting.id }
type="checkbox"
defaultChecked={ this._localSettings[setting.id] }
onChange={
e => {
UserSettingsStore.setLocalSetting(setting.id, e.target.checked)
if (setting.id === 'blacklistUnverifiedDevices') { // XXX: this is a bit ugly
client.setGlobalBlacklistUnverifiedDevices(e.target.checked);
}
}
}
/>
<label htmlFor={ setting.id }>
{ setting.label }
</label>
</div>;
},
_renderDevicesPanel: function() {
var DevicesPanel = sdk.getComponent('settings.DevicesPanel');
return (
@ -424,7 +616,24 @@ module.exports = React.createClass({
);
},
_renderLabs: function () {
_renderBugReport: function() {
if (!SdkConfig.get().bug_report_endpoint_url) {
return <div />
}
return (
<div>
<h3>Bug Report</h3>
<div className="mx_UserSettings_section">
<p>Found a bug?</p>
<button className="mx_UserSettings_button danger"
onClick={this._onBugReportClicked}>Report it
</button>
</div>
</div>
);
},
_renderLabs: function() {
// default to enabled if undefined
if (this.props.enableLabs === false) return null;
@ -460,7 +669,7 @@ module.exports = React.createClass({
{features}
</div>
</div>
)
);
},
_renderDeactivateAccount: function() {
@ -470,9 +679,9 @@ module.exports = React.createClass({
return <div>
<h3>Deactivate Account</h3>
<div className="mx_UserSettings_section">
<button className="mx_UserSettings_button danger"
<AccessibleButton className="mx_UserSettings_button danger"
onClick={this._onDeactivateAccountClicked}>Deactivate my account
</button>
</AccessibleButton>
</div>
</div>;
},
@ -492,10 +701,10 @@ module.exports = React.createClass({
// bind() the invited rooms so any new invites that may come in as this button is clicked
// don't inadvertently get rejected as well.
reject = (
<button className="mx_UserSettings_button danger"
<AccessibleButton className="mx_UserSettings_button danger"
onClick={this._onRejectAllInvitesClicked.bind(this, invitedRooms)}>
Reject all {invitedRooms.length} invites
</button>
</AccessibleButton>
);
}
@ -544,10 +753,10 @@ module.exports = React.createClass({
<label htmlFor={id}>{this.nameForMedium(val.medium)}</label>
</div>
<div className="mx_UserSettings_profileInputCell">
<input key={val.address} id={id} value={val.address} disabled />
<input type="text" key={val.address} id={id} value={val.address} disabled />
</div>
<div className="mx_UserSettings_threepidButton">
<img src="img/icon_context_delete.svg" width="14" height="14" alt="Remove" onClick={this.onRemoveThreepidClicked.bind(this, val)} />
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
<img src="img/cancel-small.svg" width="14" height="14" alt="Remove" onClick={this.onRemoveThreepidClicked.bind(this, val)} />
</div>
</div>
);
@ -569,7 +778,7 @@ module.exports = React.createClass({
blurToCancel={ false }
onValueChanged={ this.onAddThreepidClicked } />
</div>
<div className="mx_UserSettings_threepidButton">
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
<img src="img/plus.svg" width="14" height="14" alt="Add" onClick={ this.onAddThreepidClicked.bind(this, undefined, true) }/>
</div>
</div>
@ -650,7 +859,7 @@ module.exports = React.createClass({
</div>
<div className="mx_UserSettings_avatarPicker_edit">
<label htmlFor="avatarInput" ref="file_label">
<img src="img/camera.svg"
<img src="img/camera.svg" className="mx_filterFlipColor"
alt="Upload avatar" title="Upload avatar"
width="17" height="15" />
</label>
@ -663,13 +872,15 @@ module.exports = React.createClass({
<div className="mx_UserSettings_section">
<div className="mx_UserSettings_logout mx_UserSettings_button" onClick={this.onLogoutClicked}>
<AccessibleButton className="mx_UserSettings_logout mx_UserSettings_button" onClick={this.onLogoutClicked}>
Sign out
</div>
</AccessibleButton>
{accountJsx}
</div>
{this._renderReferral()}
{notification_area}
{this._renderUserInterfaceSettings()}
@ -677,6 +888,7 @@ module.exports = React.createClass({
{this._renderDevicesPanel()}
{this._renderCryptoInfo()}
{this._renderBulkOptions()}
{this._renderBugReport()}
<h3>Advanced</h3>
@ -692,7 +904,7 @@ module.exports = React.createClass({
</div>
<div className="mx_UserSettings_advanced">
matrix-react-sdk version: {REACT_SDK_VERSION}<br/>
vector-web version: {this.state.vectorVersion !== null ? this.state.vectorVersion : 'unknown'}<br/>
riot-web version: {this.state.vectorVersion !== null ? this.state.vectorVersion : 'unknown'}<br/>
olm version: {olmVersionString}<br/>
</div>
</div>

View file

@ -58,7 +58,7 @@ module.exports = React.createClass({
this.setState({
progress: null
});
})
});
},
onVerify: function(ev) {
@ -71,7 +71,7 @@ module.exports = React.createClass({
this.setState({ progress: "complete" });
}, (err) => {
this.showErrorDialog(err.message);
})
});
},
onSubmitForm: function(ev) {
@ -87,10 +87,26 @@ module.exports = React.createClass({
this.showErrorDialog("New passwords must match each other.");
}
else {
this.submitPasswordReset(
this.state.enteredHomeserverUrl, this.state.enteredIdentityServerUrl,
this.state.email, this.state.password
);
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: "Warning",
description:
<div>
Resetting password will currently reset any end-to-end encryption keys on all devices,
making encrypted chat history unreadable.
In future this <a href="https://github.com/vector-im/riot-web/issues/2671">may be improved</a>,
but for now be warned.
</div>,
button: "Continue",
onFinished: (confirmed) => {
if (confirmed) {
this.submitPasswordReset(
this.state.enteredHomeserverUrl, this.state.enteredIdentityServerUrl,
this.state.email, this.state.password
);
}
},
});
}
},
@ -129,7 +145,7 @@ module.exports = React.createClass({
var resetPasswordJsx;
if (this.state.progress === "sending_email") {
resetPasswordJsx = <Spinner />
resetPasswordJsx = <Spinner />;
}
else if (this.state.progress === "sent_email") {
resetPasswordJsx = (

View file

@ -173,7 +173,7 @@ module.exports = React.createClass({
},
_getCurrentFlowStep: function() {
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null;
},
_setStateFromError: function(err, isLoginAttempt) {
@ -195,7 +195,7 @@ module.exports = React.createClass({
}
let errorText = "Error: Problem communicating with the given homeserver " +
(errCode ? "(" + errCode + ")" : "")
(errCode ? "(" + errCode + ")" : "");
if (err.cors === 'rejected') {
if (window.location.protocol === 'https:' &&
@ -258,7 +258,7 @@ module.exports = React.createClass({
loginAsGuestJsx =
<a className="mx_Login_create" onClick={this._onLoginAsGuestClick} href="#">
Login as guest
</a>
</a>;
}
var returnToAppJsx;
@ -266,7 +266,7 @@ module.exports = React.createClass({
returnToAppJsx =
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
Return to app
</a>
</a>;
}
return (

View file

@ -25,6 +25,7 @@ var ServerConfig = require("../../views/login/ServerConfig");
var MatrixClientPeg = require("../../../MatrixClientPeg");
var RegistrationForm = require("../../views/login/RegistrationForm");
var CaptchaForm = require("../../views/login/CaptchaForm");
var RtsClient = require("../../../RtsClient");
var MIN_PASSWORD_LENGTH = 6;
@ -47,8 +48,16 @@ module.exports = React.createClass({
defaultIsUrl: React.PropTypes.string,
brand: React.PropTypes.string,
email: React.PropTypes.string,
referrer: React.PropTypes.string,
username: React.PropTypes.string,
guestAccessToken: React.PropTypes.string,
teamServerConfig: React.PropTypes.shape({
// Email address to request new teams
supportEmail: React.PropTypes.string.isRequired,
// URL of the riot-team-server to get team configurations and track referrals
teamServerURL: React.PropTypes.string.isRequired,
}),
teamSelected: React.PropTypes.object,
defaultDeviceDisplayName: React.PropTypes.string,
@ -60,6 +69,7 @@ module.exports = React.createClass({
getInitialState: function() {
return {
busy: false,
teamServerBusy: false,
errorText: null,
// We remember the values entered by the user because
// the registration form will be unmounted during the
@ -75,6 +85,7 @@ module.exports = React.createClass({
},
componentWillMount: function() {
this._unmounted = false;
this.dispatcherRef = dis.register(this.onAction);
// attach this to the instance rather than this.state since it isn't UI
this.registerLogic = new Signup.Register(
@ -87,11 +98,44 @@ module.exports = React.createClass({
this.registerLogic.setRegistrationUrl(this.props.registrationUrl);
this.registerLogic.setIdSid(this.props.idSid);
this.registerLogic.setGuestAccessToken(this.props.guestAccessToken);
if (this.props.referrer) {
this.registerLogic.setReferrer(this.props.referrer);
}
this.registerLogic.recheckState();
if (
this.props.teamServerConfig &&
this.props.teamServerConfig.teamServerURL &&
!this._rtsClient
) {
this._rtsClient = new RtsClient(this.props.teamServerConfig.teamServerURL);
this.setState({
teamServerBusy: true,
});
// GET team configurations including domains, names and icons
this._rtsClient.getTeamsConfig().then((data) => {
const teamsConfig = {
teams: data,
supportEmail: this.props.teamServerConfig.supportEmail,
};
console.log('Setting teams config to ', teamsConfig);
this.setState({
teamsConfig: teamsConfig,
teamServerBusy: false,
});
}, (err) => {
console.error('Error retrieving config for teams', err);
this.setState({
teamServerBusy: false,
});
});
}
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
this._unmounted = true;
},
componentDidMount: function() {
@ -169,6 +213,43 @@ module.exports = React.createClass({
accessToken: response.access_token
});
if (
self._rtsClient &&
self.props.referrer &&
self.state.teamSelected
) {
// Track referral, get team_token in order to retrieve team config
self._rtsClient.trackReferral(
self.props.referrer,
response.user_id,
self.state.formVals.email
).then((data) => {
const teamToken = data.team_token;
// Store for use /w welcome pages
window.localStorage.setItem('mx_team_token', teamToken);
self._rtsClient.getTeam(teamToken).then((team) => {
console.log(
`User successfully registered with team ${team.name}`
);
if (!team.rooms) {
return;
}
// Auto-join rooms
team.rooms.forEach((room) => {
if (room.auto_join && room.room_id) {
console.log(`Auto-joining ${room.room_id}`);
MatrixClientPeg.get().joinRoom(room.room_id);
}
});
}, (err) => {
console.error('Error getting team config', err);
});
}, (err) => {
console.error('Error tracking referral', err);
});
}
if (self.props.brand) {
MatrixClientPeg.get().getPushers().done((resp)=>{
var pushers = resp.pushers;
@ -238,7 +319,15 @@ module.exports = React.createClass({
});
},
onTeamSelected: function(teamSelected) {
if (!this._unmounted) {
this.setState({ teamSelected });
}
},
_getRegisterContentJsx: function() {
const Spinner = sdk.getComponent("elements.Spinner");
var currStep = this.registerLogic.getStep();
var registerStep;
switch (currStep) {
@ -248,16 +337,23 @@ module.exports = React.createClass({
case "Register.STEP_m.login.dummy":
// NB. Our 'username' prop is specifically for upgrading
// a guest account
if (this.state.teamServerBusy) {
registerStep = <Spinner />;
break;
}
registerStep = (
<RegistrationForm
showEmail={true}
defaultUsername={this.state.formVals.username}
defaultEmail={this.state.formVals.email}
defaultPassword={this.state.formVals.password}
teamsConfig={this.state.teamsConfig}
guestUsername={this.props.username}
minPasswordLength={MIN_PASSWORD_LENGTH}
onError={this.onFormValidationFailed}
onRegisterClick={this.onFormSubmit} />
onRegisterClick={this.onFormSubmit}
onTeamSelected={this.onTeamSelected}
/>
);
break;
case "Register.STEP_m.login.email.identity":
@ -286,7 +382,6 @@ module.exports = React.createClass({
}
var busySpinner;
if (this.state.busy) {
var Spinner = sdk.getComponent("elements.Spinner");
busySpinner = (
<Spinner />
);
@ -297,7 +392,7 @@ module.exports = React.createClass({
returnToAppJsx =
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
Return to app
</a>
</a>;
}
return (
@ -331,7 +426,7 @@ module.exports = React.createClass({
return (
<div className="mx_Login">
<div className="mx_Login_box">
<LoginHeader />
<LoginHeader icon={this.state.teamSelected ? this.state.teamSelected.icon : null}/>
{this._getRegisterContentJsx()}
<LoginFooter />
</div>

View file

@ -19,6 +19,7 @@ limitations under the License.
var React = require('react');
var AvatarLogic = require("../../../Avatar");
import sdk from '../../../index';
import AccessibleButton from '../elements/AccessibleButton';
module.exports = React.createClass({
displayName: 'BaseAvatar',
@ -41,7 +42,7 @@ module.exports = React.createClass({
height: 40,
resizeMethod: 'crop',
defaultToInitialLetter: true
}
};
},
getInitialState: function() {
@ -138,30 +139,63 @@ module.exports = React.createClass({
const {
name, idName, title, url, urls, width, height, resizeMethod,
defaultToInitialLetter,
defaultToInitialLetter, onClick,
...otherProps
} = this.props;
if (imageUrl === this.state.defaultImageUrl) {
const initialLetter = this._getInitialLetter(name);
const textNode = (
<EmojiText className="mx_BaseAvatar_initial" aria-hidden="true"
style={{ fontSize: (width * 0.65) + "px",
width: width + "px",
lineHeight: height + "px" }}
>
{initialLetter}
</EmojiText>
);
const imgNode = (
<img className="mx_BaseAvatar_image" src={imageUrl}
alt="" title={title} onError={this.onError}
width={width} height={height} />
);
if (onClick != null) {
return (
<AccessibleButton element='span' className="mx_BaseAvatar"
onClick={onClick} {...otherProps}
>
{textNode}
{imgNode}
</AccessibleButton>
);
} else {
return (
<span className="mx_BaseAvatar" {...otherProps}>
{textNode}
{imgNode}
</span>
);
}
}
if (onClick != null) {
return (
<span className="mx_BaseAvatar" {...otherProps}>
<EmojiText className="mx_BaseAvatar_initial" aria-hidden="true"
style={{ fontSize: (width * 0.65) + "px",
width: width + "px",
lineHeight: height + "px" }}>{initialLetter}</EmojiText>
<img className="mx_BaseAvatar_image" src={imageUrl}
alt="" title={title} onError={this.onError}
width={width} height={height} />
</span>
<AccessibleButton className="mx_BaseAvatar mx_BaseAvatar_image"
element='img'
src={imageUrl}
onClick={onClick}
onError={this.onError}
width={width} height={height}
title={title} alt=""
{...otherProps} />
);
} else {
return (
<img className="mx_BaseAvatar mx_BaseAvatar_image" src={imageUrl}
onError={this.onError}
width={width} height={height}
title={title} alt=""
{...otherProps} />
);
}
return (
<img className="mx_BaseAvatar mx_BaseAvatar_image" src={imageUrl}
onError={this.onError}
width={width} height={height}
title={title} alt=""
{...otherProps} />
);
}
});

View file

@ -42,7 +42,7 @@ module.exports = React.createClass({
height: 40,
resizeMethod: 'crop',
viewUserOnClick: false,
}
};
},
getInitialState: function() {
@ -64,7 +64,7 @@ module.exports = React.createClass({
props.width,
props.height,
props.resizeMethod)
}
};
},
render: function() {
@ -78,7 +78,7 @@ module.exports = React.createClass({
action: 'view_user',
member: this.props.member,
});
}
};
}
return (

View file

@ -39,7 +39,7 @@ module.exports = React.createClass({
height: 36,
resizeMethod: 'crop',
oobData: {},
}
};
},
getInitialState: function() {
@ -51,7 +51,7 @@ module.exports = React.createClass({
componentWillReceiveProps: function(newProps) {
this.setState({
urls: this.getImageUrls(newProps)
})
});
},
getImageUrls: function(props) {

View file

@ -40,7 +40,7 @@ module.exports = React.createClass({
},
onValueChanged: function(ev) {
this.props.onChange(ev.target.value)
this.props.onChange(ev.target.value);
},
render: function() {

View file

@ -0,0 +1,72 @@
/*
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 * as KeyCode from '../../../KeyCode';
/**
* Basic container for modal dialogs.
*
* Includes a div for the title, and a keypress handler which cancels the
* dialog on escape.
*/
export default React.createClass({
displayName: 'BaseDialog',
propTypes: {
// onFinished callback to call when Escape is pressed
onFinished: React.PropTypes.func.isRequired,
// callback to call when Enter is pressed
onEnterPressed: React.PropTypes.func,
// CSS class to apply to dialog div
className: React.PropTypes.string,
// Title for the dialog.
// (could probably actually be something more complicated than a string if desired)
title: React.PropTypes.string.isRequired,
// children should be the content of the dialog
children: React.PropTypes.node,
},
_onKeyDown: function(e) {
if (e.keyCode === KeyCode.ESCAPE) {
e.stopPropagation();
e.preventDefault();
this.props.onFinished();
} else if (e.keyCode === KeyCode.ENTER) {
if (this.props.onEnterPressed) {
e.stopPropagation();
e.preventDefault();
this.props.onEnterPressed(e);
}
}
},
render: function() {
return (
<div onKeyDown={this._onKeyDown} className={this.props.className}>
<div className='mx_Dialog_title'>
{ this.props.title }
</div>
{ this.props.children }
</div>
);
},
});

View file

@ -14,19 +14,30 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require("react");
var classNames = require('classnames');
var sdk = require("../../../index");
var Invite = require("../../../Invite");
var createRoom = require("../../../createRoom");
var MatrixClientPeg = require("../../../MatrixClientPeg");
var DMRoomMap = require('../../../utils/DMRoomMap');
var rate_limited_func = require("../../../ratelimitedfunc");
var dis = require("../../../dispatcher");
var Modal = require('../../../Modal');
import React from 'react';
import classNames from 'classnames';
import sdk from '../../../index';
import { getAddressType, inviteMultipleToRoom } from '../../../Invite';
import createRoom from '../../../createRoom';
import MatrixClientPeg from '../../../MatrixClientPeg';
import DMRoomMap from '../../../utils/DMRoomMap';
import rate_limited_func from '../../../ratelimitedfunc';
import dis from '../../../dispatcher';
import Modal from '../../../Modal';
import AccessibleButton from '../elements/AccessibleButton';
import q from 'q';
const TRUNCATE_QUERY_LIST = 40;
/*
* Escapes a string so it can be used in a RegExp
* Basically just replaces: \ ^ $ * + ? . ( ) | { } [ ]
* From http://stackoverflow.com/a/6969486
*/
function escapeRegExp(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
}
module.exports = React.createClass({
displayName: "ChatInviteDialog",
propTypes: {
@ -48,7 +59,7 @@ module.exports = React.createClass({
title: "Start a chat",
description: "Who would you like to communicate with?",
value: "",
placeholder: "User ID, Name or email",
placeholder: "Email, name or matrix ID",
button: "Start Chat",
focus: true
};
@ -57,7 +68,14 @@ module.exports = React.createClass({
getInitialState: function() {
return {
error: false,
// List of AddressTile.InviteAddressType objects represeting
// the list of addresses we're going to invite
inviteList: [],
// List of AddressTile.InviteAddressType objects represeting
// the set of autocompletion results for the current search
// query.
queryList: [],
};
},
@ -71,15 +89,12 @@ module.exports = React.createClass({
},
onButtonClick: function() {
var inviteList = this.state.inviteList.slice();
let inviteList = this.state.inviteList.slice();
// Check the text input field to see if user has an unconverted address
// If there is and it's valid add it to the local inviteList
var check = Invite.isValidAddress(this.refs.textinput.value);
if (check === true || check === null) {
inviteList.push(this.refs.textinput.value);
} else if (this.refs.textinput.value.length > 0) {
this.setState({ error: true });
return;
if (this.refs.textinput.value !== '') {
inviteList = this._addInputToList();
if (inviteList === null) return;
}
if (inviteList.length > 0) {
@ -119,15 +134,15 @@ module.exports = React.createClass({
} else if (e.keyCode === 38) { // up arrow
e.stopPropagation();
e.preventDefault();
this.addressSelector.onKeyUp();
this.addressSelector.moveSelectionUp();
} else if (e.keyCode === 40) { // down arrow
e.stopPropagation();
e.preventDefault();
this.addressSelector.onKeyDown();
} else if (this.state.queryList.length > 0 && (e.keyCode === 188, e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
this.addressSelector.moveSelectionDown();
} else if (this.state.queryList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
e.stopPropagation();
e.preventDefault();
this.addressSelector.onKeySelect();
this.addressSelector.chooseSelection();
} else if (this.refs.textinput.value.length === 0 && this.state.inviteList.length && e.keyCode === 8) { // backspace
e.stopPropagation();
e.preventDefault();
@ -135,33 +150,56 @@ module.exports = React.createClass({
} else if (e.keyCode === 13) { // enter
e.stopPropagation();
e.preventDefault();
this.onButtonClick();
if (this.refs.textinput.value == '') {
// if there's nothing in the input box, submit the form
this.onButtonClick();
} else {
this._addInputToList();
}
} else if (e.keyCode === 188 || e.keyCode === 9) { // comma or tab
e.stopPropagation();
e.preventDefault();
var check = Invite.isValidAddress(this.refs.textinput.value);
if (check === true || check === null) {
var inviteList = this.state.inviteList.slice();
inviteList.push(this.refs.textinput.value.trim());
this.setState({
inviteList: inviteList,
queryList: [],
});
} else {
this.setState({ error: true });
}
this._addInputToList();
}
},
onQueryChanged: function(ev) {
var query = ev.target.value;
var queryList = [];
const query = ev.target.value;
let queryList = [];
// Only do search if there is something to search
if (query.length > 0) {
if (query.length > 0 && query != '@') {
// filter the known users list
queryList = this._userList.filter((user) => {
return this._matches(query, user);
}).map((user) => {
// Return objects, structure of which is defined
// by InviteAddressType
return {
addressType: 'mx',
address: user.userId,
displayName: user.displayName,
avatarMxc: user.avatarUrl,
isKnown: true,
}
});
// If the query isn't a user we know about, but is a
// valid address, add an entry for that
if (queryList.length == 0) {
const addrType = getAddressType(query);
if (addrType !== null) {
queryList[0] = {
addressType: addrType,
address: query,
isKnown: false,
};
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
if (addrType == 'email') {
this._lookupThreepid(addrType, query).done();
}
}
}
}
this.setState({
@ -179,7 +217,8 @@ module.exports = React.createClass({
inviteList: inviteList,
queryList: [],
});
}
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
};
},
onClick: function(index) {
@ -191,11 +230,12 @@ module.exports = React.createClass({
onSelected: function(index) {
var inviteList = this.state.inviteList.slice();
inviteList.push(this.state.queryList[index].userId);
inviteList.push(this.state.queryList[index]);
this.setState({
inviteList: inviteList,
queryList: [],
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
},
_getDirectMessageRoom: function(addr) {
@ -226,10 +266,14 @@ module.exports = React.createClass({
return;
}
const addrTexts = addrs.map((addr) => {
return addr.address;
});
if (this.props.roomId) {
// Invite new user to a room
var self = this;
Invite.inviteMultipleToRoom(this.props.roomId, addrs)
inviteMultipleToRoom(this.props.roomId, addrTexts)
.then(function(addrs) {
var room = MatrixClientPeg.get().getRoom(self.props.roomId);
return self._showAnyInviteErrors(addrs, room);
@ -244,9 +288,9 @@ module.exports = React.createClass({
return null;
})
.done();
} else if (this._isDmChat(addrs)) {
} else if (this._isDmChat(addrTexts)) {
// Start the DM chat
createRoom({dmUserId: addrs[0]})
createRoom({dmUserId: addrTexts[0]})
.catch(function(err) {
console.error(err.stack);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -263,7 +307,7 @@ module.exports = React.createClass({
var room;
createRoom().then(function(roomId) {
room = MatrixClientPeg.get().getRoom(roomId);
return Invite.inviteMultipleToRoom(roomId, addrs);
return inviteMultipleToRoom(roomId, addrTexts);
})
.then(function(addrs) {
return self._showAnyInviteErrors(addrs, room);
@ -281,7 +325,7 @@ module.exports = React.createClass({
}
// Close - this will happen before the above, as that is async
this.props.onFinished(true, addrs);
this.props.onFinished(true, addrTexts);
},
_updateUserList: new rate_limited_func(function() {
@ -315,19 +359,27 @@ module.exports = React.createClass({
return true;
}
// split spaces in name and try matching constituent parts
var parts = name.split(" ");
for (var i = 0; i < parts.length; i++) {
if (parts[i].indexOf(query) === 0) {
return true;
}
// Try to find the query following a "word boundary", except that
// this does avoids using \b because it only considers letters from
// the roman alphabet to be word characters.
// Instead, we look for the query following either:
// * The start of the string
// * Whitespace, or
// * A fixed number of punctuation characters
const expr = new RegExp("(?:^|[\\s\\(\)'\",\.-_@\?;:{}\\[\\]\\#~`\\*\\&\\$])" + escapeRegExp(query));
if (expr.test(name)) {
return true;
}
return false;
},
_isOnInviteList: function(uid) {
for (let i = 0; i < this.state.inviteList.length; i++) {
if (this.state.inviteList[i].toLowerCase() === uid) {
if (
this.state.inviteList[i].addressType == 'mx' &&
this.state.inviteList[i].address.toLowerCase() === uid
) {
return true;
}
}
@ -335,7 +387,7 @@ module.exports = React.createClass({
},
_isDmChat: function(addrs) {
if (addrs.length === 1 && Invite.getAddressType(addrs[0]) === "mx" && !this.props.roomId) {
if (addrs.length === 1 && getAddressType(addrs[0]) === "mx" && !this.props.roomId) {
return true;
} else {
return false;
@ -361,9 +413,74 @@ module.exports = React.createClass({
return addrs;
},
_addInputToList: function() {
const addressText = this.refs.textinput.value.trim();
const addrType = getAddressType(addressText);
const addrObj = {
addressType: addrType,
address: addressText,
isKnown: false,
};
if (addrType == null) {
this.setState({ error: true });
return null;
} else if (addrType == 'mx') {
const user = MatrixClientPeg.get().getUser(addrObj.address);
if (user) {
addrObj.displayName = user.displayName;
addrObj.avatarMxc = user.avatarUrl;
addrObj.isKnown = true;
}
}
const inviteList = this.state.inviteList.slice();
inviteList.push(addrObj);
this.setState({
inviteList: inviteList,
queryList: [],
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
return inviteList;
},
_lookupThreepid: function(medium, address) {
let cancelled = false;
// Note that we can't safely remove this after we're done
// because we don't know that it's the same one, so we just
// leave it: it's replacing the old one each time so it's
// not like they leak.
this._cancelThreepidLookup = function() {
cancelled = true;
}
// wait a bit to let the user finish typing
return q.delay(500).then(() => {
if (cancelled) return null;
return MatrixClientPeg.get().lookupThreePid(medium, address);
}).then((res) => {
if (res === null || !res.mxid) return null;
if (cancelled) return null;
return MatrixClientPeg.get().getProfileInfo(res.mxid);
}).then((res) => {
if (res === null) return null;
if (cancelled) return null;
this.setState({
queryList: [{
// an InviteAddressType
addressType: medium,
address: address,
displayName: res.displayname,
avatarMxc: res.avatar_url,
isKnown: true,
}]
});
});
},
render: function() {
var TintableSvg = sdk.getComponent("elements.TintableSvg");
var AddressSelector = sdk.getComponent("elements.AddressSelector");
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const AddressSelector = sdk.getComponent("elements.AddressSelector");
this.scrollElement = null;
var query = [];
@ -394,13 +511,18 @@ module.exports = React.createClass({
var error;
var addressSelector;
if (this.state.error) {
error = <div className="mx_ChatInviteDialog_error">You have entered an invalid contact. Try using their Matrix ID or email address.</div>
error = <div className="mx_ChatInviteDialog_error">You have entered an invalid contact. Try using their Matrix ID or email address.</div>;
} else {
const addressSelectorHeader = <div className="mx_ChatInviteDialog_addressSelectHeader">
Searching known users
</div>;
addressSelector = (
<AddressSelector ref={(ref) => {this.addressSelector = ref}}
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
addressList={ this.state.queryList }
onSelected={ this.onSelected }
truncateAt={ TRUNCATE_QUERY_LIST } />
truncateAt={ TRUNCATE_QUERY_LIST }
header={ addressSelectorHeader }
/>
);
}
@ -409,9 +531,10 @@ module.exports = React.createClass({
<div className="mx_Dialog_title">
{this.props.title}
</div>
<div className="mx_ChatInviteDialog_cancel" onClick={this.onCancel} >
<AccessibleButton className="mx_ChatInviteDialog_cancel"
onClick={this.onCancel} >
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
</div>
</AccessibleButton>
<div className="mx_ChatInviteDialog_label">
<label htmlFor="textinput">{ this.props.description }</label>
</div>

View file

@ -80,8 +80,8 @@ export default class DeactivateAccountDialog extends React.Component {
let error = null;
if (this.state.errStr) {
error = <div className="error">
{this.state.err_str}
</div>
{this.state.errStr}
</div>;
passwordBoxClass = 'error';
}
@ -92,7 +92,7 @@ export default class DeactivateAccountDialog extends React.Component {
if (!this.state.busy) {
cancelButton = <button onClick={this._onCancel} autoFocus={true}>
Cancel
</button>
</button>;
}
return (

View file

@ -25,9 +25,10 @@ limitations under the License.
* });
*/
var React = require("react");
import React from 'react';
import sdk from '../../../index';
module.exports = React.createClass({
export default React.createClass({
displayName: 'ErrorDialog',
propTypes: {
title: React.PropTypes.string,
@ -49,20 +50,11 @@ module.exports = React.createClass({
};
},
onKeyDown: function(e) {
if (e.keyCode === 27) { // escape
e.stopPropagation();
e.preventDefault();
this.props.onFinished(false);
}
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<div className="mx_ErrorDialog" onKeyDown={ this.onKeyDown }>
<div className="mx_Dialog_title">
{this.props.title}
</div>
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
title={this.props.title}>
<div className="mx_Dialog_content">
{this.props.description}
</div>
@ -71,7 +63,7 @@ module.exports = React.createClass({
{this.props.button}
</button>
</div>
</div>
</BaseDialog>
);
}
},
});

View file

@ -111,20 +111,9 @@ export default React.createClass({
});
},
_onKeyDown: function(e) {
if (e.keyCode === 27) { // escape
e.stopPropagation();
e.preventDefault();
if (!this.state.busy) {
this._onCancel();
}
}
else if (e.keyCode === 13) { // enter
e.stopPropagation();
e.preventDefault();
if (this.state.submitButtonEnabled && !this.state.busy) {
this._onSubmit();
}
_onEnterPressed: function(e) {
if (this.state.submitButtonEnabled && !this.state.busy) {
this._onSubmit();
}
},
@ -171,6 +160,7 @@ export default React.createClass({
render: function() {
const Loader = sdk.getComponent("elements.Spinner");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
let error = null;
if (this.state.errorText) {
@ -200,10 +190,11 @@ export default React.createClass({
);
return (
<div className="mx_InteractiveAuthDialog" onKeyDown={this._onKeyDown}>
<div className="mx_Dialog_title">
{this.props.title}
</div>
<BaseDialog className="mx_InteractiveAuthDialog"
onEnterPressed={this._onEnterPressed}
onFinished={this.props.onFinished}
title={this.props.title}
>
<div className="mx_Dialog_content">
<p>This operation requires additional authentication.</p>
{this._renderCurrentStage()}
@ -213,7 +204,7 @@ export default React.createClass({
{submitButton}
{cancelButton}
</div>
</div>
</BaseDialog>
);
},
});

View file

@ -1,61 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require('react');
var dis = require("../../../dispatcher");
module.exports = React.createClass({
displayName: 'LogoutPrompt',
propTypes: {
onFinished: React.PropTypes.func,
},
logOut: function() {
dis.dispatch({action: 'logout'});
if (this.props.onFinished) {
this.props.onFinished();
}
},
cancelPrompt: function() {
if (this.props.onFinished) {
this.props.onFinished();
}
},
onKeyDown: function(e) {
if (e.keyCode === 27) { // escape
e.stopPropagation();
e.preventDefault();
this.cancelPrompt();
}
},
render: function() {
return (
<div>
<div className="mx_Dialog_content">
Sign out?
</div>
<div className="mx_Dialog_buttons" onKeyDown={ this.onKeyDown }>
<button className="mx_Dialog_primary" autoFocus onClick={this.logOut}>Sign Out</button>
<button onClick={this.cancelPrompt}>Cancel</button>
</div>
</div>
);
}
});

View file

@ -23,8 +23,9 @@ limitations under the License.
* });
*/
var React = require("react");
var dis = require("../../../dispatcher");
import React from 'react';
import dis from '../../../dispatcher';
import sdk from '../../../index';
module.exports = React.createClass({
displayName: 'NeedToRegisterDialog',
@ -54,11 +55,12 @@ module.exports = React.createClass({
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<div className="mx_NeedToRegisterDialog">
<div className="mx_Dialog_title">
{this.props.title}
</div>
<BaseDialog className="mx_NeedToRegisterDialog"
onFinished={this.props.onFinished}
title={this.props.title}
>
<div className="mx_Dialog_content">
{this.props.description}
</div>
@ -70,7 +72,7 @@ module.exports = React.createClass({
Register
</button>
</div>
</div>
</BaseDialog>
);
}
},
});

View file

@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require("react");
import React from 'react';
import sdk from '../../../index';
module.exports = React.createClass({
export default React.createClass({
displayName: 'QuestionDialog',
propTypes: {
title: React.PropTypes.string,
@ -46,25 +47,13 @@ module.exports = React.createClass({
this.props.onFinished(false);
},
onKeyDown: function(e) {
if (e.keyCode === 27) { // escape
e.stopPropagation();
e.preventDefault();
this.props.onFinished(false);
}
else if (e.keyCode === 13) { // enter
e.stopPropagation();
e.preventDefault();
this.props.onFinished(true);
}
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<div className="mx_QuestionDialog" onKeyDown={ this.onKeyDown }>
<div className="mx_Dialog_title">
{this.props.title}
</div>
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
onEnterPressed={ this.onOk }
title={this.props.title}
>
<div className="mx_Dialog_content">
{this.props.description}
</div>
@ -77,7 +66,7 @@ module.exports = React.createClass({
Cancel
</button>
</div>
</div>
</BaseDialog>
);
}
},
});

View file

@ -14,11 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require("react");
var sdk = require("../../../index.js");
var MatrixClientPeg = require("../../../MatrixClientPeg");
import React from 'react';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
module.exports = React.createClass({
/**
* Prompt the user to set a display name.
*
* On success, `onFinished(true, newDisplayName)` is called.
*/
export default React.createClass({
displayName: 'SetDisplayNameDialog',
propTypes: {
onFinished: React.PropTypes.func.isRequired,
@ -42,10 +47,6 @@ module.exports = React.createClass({
this.refs.input_value.select();
},
getValue: function() {
return this.state.value;
},
onValueChange: function(ev) {
this.setState({
value: ev.target.value
@ -54,16 +55,17 @@ module.exports = React.createClass({
onFormSubmit: function(ev) {
ev.preventDefault();
this.props.onFinished(true);
this.props.onFinished(true, this.state.value);
return false;
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<div className="mx_SetDisplayNameDialog">
<div className="mx_Dialog_title">
Set a Display Name
</div>
<BaseDialog className="mx_SetDisplayNameDialog"
onFinished={this.props.onFinished}
title="Set a Display Name"
>
<div className="mx_Dialog_content">
Your display name is how you'll appear to others when you speak in rooms.<br/>
What would you like it to be?
@ -79,7 +81,7 @@ module.exports = React.createClass({
<input className="mx_Dialog_primary" type="submit" value="Set" />
</div>
</form>
</div>
</BaseDialog>
);
}
},
});

View file

@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require("react");
import React from 'react';
import sdk from '../../../index';
module.exports = React.createClass({
export default React.createClass({
displayName: 'TextInputDialog',
propTypes: {
title: React.PropTypes.string,
@ -27,7 +28,7 @@ module.exports = React.createClass({
value: React.PropTypes.string,
button: React.PropTypes.string,
focus: React.PropTypes.bool,
onFinished: React.PropTypes.func.isRequired
onFinished: React.PropTypes.func.isRequired,
},
getDefaultProps: function() {
@ -36,7 +37,7 @@ module.exports = React.createClass({
value: "",
description: "",
button: "OK",
focus: true
focus: true,
};
},
@ -55,25 +56,13 @@ module.exports = React.createClass({
this.props.onFinished(false);
},
onKeyDown: function(e) {
if (e.keyCode === 27) { // escape
e.stopPropagation();
e.preventDefault();
this.props.onFinished(false);
}
else if (e.keyCode === 13) { // enter
e.stopPropagation();
e.preventDefault();
this.props.onFinished(true, this.refs.textinput.value);
}
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<div className="mx_TextInputDialog">
<div className="mx_Dialog_title">
{this.props.title}
</div>
<BaseDialog className="mx_TextInputDialog" onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
title={this.props.title}
>
<div className="mx_Dialog_content">
<div className="mx_TextInputDialog_label">
<label htmlFor="textinput"> {this.props.description} </label>
@ -90,7 +79,7 @@ module.exports = React.createClass({
{this.props.button}
</button>
</div>
</div>
</BaseDialog>
);
}
},
});

View 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...
},
});

View file

@ -0,0 +1,54 @@
/*
Copyright 2016 Jani Mustonen
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';
/**
* AccessibleButton is a generic wrapper for any element that should be treated
* as a button. Identifies the element as a button, setting proper tab
* indexing and keyboard activation behavior.
*
* @param {Object} props react element properties
* @returns {Object} rendered react
*/
export default function AccessibleButton(props) {
const {element, onClick, children, ...restProps} = props;
restProps.onClick = onClick;
restProps.onKeyDown = function(e) {
if (e.keyCode == 13 || e.keyCode == 32) return onClick();
};
restProps.tabIndex = restProps.tabIndex || "0";
restProps.role = "button";
return React.createElement(element, restProps, children);
}
/**
* children: React's magic prop. Represents all children given to the element.
* element: (optional) The base element type. "div" by default.
* onClick: (required) Event handler for button activation. Should be
* implemented exactly like a normal onClick handler.
*/
AccessibleButton.propTypes = {
children: React.PropTypes.node,
element: React.PropTypes.string,
onClick: React.PropTypes.func.isRequired,
};
AccessibleButton.defaultProps = {
element: 'div',
};
AccessibleButton.displayName = "AccessibleButton";

View file

@ -16,18 +16,24 @@ limitations under the License.
'use strict';
var React = require("react");
var sdk = require("../../../index");
var classNames = require('classnames');
import React from 'react';
import sdk from '../../../index';
import classNames from 'classnames';
import { InviteAddressType } from './AddressTile';
module.exports = React.createClass({
export default React.createClass({
displayName: 'AddressSelector',
propTypes: {
onSelected: React.PropTypes.func.isRequired,
addressList: React.PropTypes.array.isRequired,
// List of the addresses to display
addressList: React.PropTypes.arrayOf(InviteAddressType).isRequired,
truncateAt: React.PropTypes.number.isRequired,
selected: React.PropTypes.number,
// Element to put as a header on top of the list
header: React.PropTypes.node,
},
getInitialState: function() {
@ -55,7 +61,7 @@ module.exports = React.createClass({
}
},
onKeyUp: function() {
moveSelectionUp: function() {
if (this.state.selected > 0) {
this.setState({
selected: this.state.selected - 1,
@ -64,7 +70,7 @@ module.exports = React.createClass({
}
},
onKeyDown: function() {
moveSelectionDown: function() {
if (this.state.selected < this._maxSelected(this.props.addressList)) {
this.setState({
selected: this.state.selected + 1,
@ -73,25 +79,19 @@ module.exports = React.createClass({
}
},
onKeySelect: function() {
chooseSelection: function() {
this.selectAddress(this.state.selected);
},
onClick: function(index) {
var self = this;
return function() {
self.selectAddress(index);
};
this.selectAddress(index);
},
onMouseEnter: function(index) {
var self = this;
return function() {
self.setState({
selected: index,
hover: true,
});
};
this.setState({
selected: index,
hover: true,
});
},
onMouseLeave: function() {
@ -124,8 +124,8 @@ module.exports = React.createClass({
// Saving the addressListElement so we can use it to work out, in the componentDidUpdate
// method, how far to scroll when using the arrow keys
addressList.push(
<div className={classes} onClick={this.onClick(i)} onMouseEnter={this.onMouseEnter(i)} onMouseLeave={this.onMouseLeave} key={i} ref={(ref) => { this.addressListElement = ref; }} >
<AddressTile address={this.props.addressList[i].userId} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />
<div className={classes} onClick={this.onClick.bind(this, i)} onMouseEnter={this.onMouseEnter.bind(this, i)} onMouseLeave={this.onMouseLeave} key={i} ref={(ref) => { this.addressListElement = ref; }} >
<AddressTile address={this.props.addressList[i]} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />
</div>
);
}
@ -135,7 +135,7 @@ module.exports = React.createClass({
_maxSelected: function(list) {
var listSize = list.length === 0 ? 0 : list.length - 1;
var maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize
var maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize;
return maxSelected;
},
@ -146,7 +146,8 @@ module.exports = React.createClass({
});
return (
<div className={classes} ref={(ref) => {this.scrollElement = ref}}>
<div className={classes} ref={(ref) => {this.scrollElement = ref;}}>
{ this.props.header }
{ this.createAddressListTiles() }
</div>
);

View file

@ -23,16 +23,33 @@ var Invite = require("../../../Invite");
var MatrixClientPeg = require("../../../MatrixClientPeg");
var Avatar = require('../../../Avatar');
module.exports = React.createClass({
// React PropType definition for an object describing
// an address that can be invited to a room (which
// could be a third party identifier or a matrix ID)
// along with some additional information about the
// address / target.
export const InviteAddressType = React.PropTypes.shape({
addressType: React.PropTypes.oneOf([
'mx', 'email'
]).isRequired,
address: React.PropTypes.string.isRequired,
displayName: React.PropTypes.string,
avatarMxc: React.PropTypes.string,
// true if the address is known to be a valid address (eg. is a real
// user we've seen) or false otherwise (eg. is just an address the
// user has entered)
isKnown: React.PropTypes.bool,
});
export default React.createClass({
displayName: 'AddressTile',
propTypes: {
address: React.PropTypes.string.isRequired,
address: InviteAddressType.isRequired,
canDismiss: React.PropTypes.bool,
onDismissed: React.PropTypes.func,
justified: React.PropTypes.bool,
networkName: React.PropTypes.string,
networkUrl: React.PropTypes.string,
},
getDefaultProps: function() {
@ -40,37 +57,30 @@ module.exports = React.createClass({
canDismiss: false,
onDismissed: function() {}, // NOP
justified: false,
networkName: "",
networkUrl: "",
};
},
render: function() {
var userId, name, imgUrl, email;
var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
var TintableSvg = sdk.getComponent("elements.TintableSvg");
const address = this.props.address;
const name = address.displayName || address.address;
// Check if the addr is a valid type
var addrType = Invite.getAddressType(this.props.address);
if (addrType === "mx") {
let user = MatrixClientPeg.get().getUser(this.props.address);
if (user) {
userId = user.userId;
name = user.rawDisplayName || userId;
imgUrl = Avatar.avatarUrlForUser(user, 25, 25, "crop");
} else {
name=this.props.address;
imgUrl = "img/icon-mx-user.svg";
}
} else if (addrType === "email") {
email = this.props.address;
name="email";
imgUrl = "img/icon-email-user.svg";
} else {
name="Unknown";
imgUrl = "img/avatar-error.svg";
let imgUrl;
if (address.avatarMxc) {
imgUrl = MatrixClientPeg.get().mxcUrlToHttp(
address.avatarMxc, 25, 25, 'crop'
);
}
if (address.addressType === "mx") {
if (!imgUrl) imgUrl = 'img/icon-mx-user.svg';
} else if (address.addressType === 'email') {
if (!imgUrl) imgUrl = 'img/icon-email-user.svg';
} else {
if (!imgUrl) imgUrl = "img/avatar-error.svg";
}
// Removing networks for now as they're not really supported
/*
var network;
if (this.props.networkUrl !== "") {
network = (
@ -79,16 +89,20 @@ module.exports = React.createClass({
</div>
);
}
*/
var info;
var error = false;
if (addrType === "mx" && userId) {
var nameClasses = classNames({
"mx_AddressTile_name": true,
"mx_AddressTile_justified": this.props.justified,
});
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const TintableSvg = sdk.getComponent("elements.TintableSvg");
var idClasses = classNames({
const nameClasses = classNames({
"mx_AddressTile_name": true,
"mx_AddressTile_justified": this.props.justified,
});
let info;
let error = false;
if (address.addressType === "mx" && address.isKnown) {
const idClasses = classNames({
"mx_AddressTile_id": true,
"mx_AddressTile_justified": this.props.justified,
});
@ -96,26 +110,34 @@ module.exports = React.createClass({
info = (
<div className="mx_AddressTile_mx">
<div className={nameClasses}>{ name }</div>
<div className={idClasses}>{ userId }</div>
<div className={idClasses}>{ address.address }</div>
</div>
);
} else if (addrType === "mx") {
var unknownMxClasses = classNames({
} else if (address.addressType === "mx") {
const unknownMxClasses = classNames({
"mx_AddressTile_unknownMx": true,
"mx_AddressTile_justified": this.props.justified,
});
info = (
<div className={unknownMxClasses}>{ this.props.address }</div>
<div className={unknownMxClasses}>{ this.props.address.address }</div>
);
} else if (email) {
var emailClasses = classNames({
} else if (address.addressType === "email") {
const emailClasses = classNames({
"mx_AddressTile_email": true,
"mx_AddressTile_justified": this.props.justified,
});
let nameNode = null;
if (address.displayName) {
nameNode = <div className={nameClasses}>{ address.displayName }</div>
}
info = (
<div className={emailClasses}>{ email }</div>
<div className="mx_AddressTile_mx">
<div className={emailClasses}>{ address.address }</div>
{nameNode}
</div>
);
} else {
error = true;
@ -129,12 +151,12 @@ module.exports = React.createClass({
);
}
var classes = classNames({
const classes = classNames({
"mx_AddressTile": true,
"mx_AddressTile_error": error,
});
var dismiss;
let dismiss;
if (this.props.canDismiss) {
dismiss = (
<div className="mx_AddressTile_dismiss" onClick={this.props.onDismissed} >
@ -145,7 +167,6 @@ module.exports = React.createClass({
return (
<div className={classes}>
{ network }
<div className="mx_AddressTile_avatar">
<BaseAvatar width={25} height={25} name={name} title={name} url={imgUrl} />
</div>

View file

@ -27,6 +27,28 @@ export default React.createClass({
device: React.PropTypes.object.isRequired,
},
getInitialState: function() {
return {
device: this.props.device
};
},
componentWillMount: function() {
const cli = MatrixClientPeg.get();
cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
},
componentWillUnmount: function() {
const cli = MatrixClientPeg.get();
cli.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
},
onDeviceVerificationChanged: function(userId, deviceId, deviceInfo) {
if (userId === this.props.userId && deviceId === this.props.device.deviceId) {
this.setState({ device: deviceInfo });
}
},
onVerifyClick: function() {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
@ -41,9 +63,9 @@ export default React.createClass({
</p>
<div className="mx_UserSettings_cryptoSection">
<ul>
<li><label>Device name:</label> <span>{ this.props.device.getDisplayName() }</span></li>
<li><label>Device ID:</label> <span><code>{ this.props.device.deviceId}</code></span></li>
<li><label>Device key:</label> <span><code><b>{ this.props.device.getFingerprint() }</b></code></span></li>
<li><label>Device name:</label> <span>{ this.state.device.getDisplayName() }</span></li>
<li><label>Device ID:</label> <span><code>{ this.state.device.deviceId}</code></span></li>
<li><label>Device key:</label> <span><code><b>{ this.state.device.getFingerprint() }</b></code></span></li>
</ul>
</div>
<p>
@ -60,7 +82,7 @@ export default React.createClass({
onFinished: confirm=>{
if (confirm) {
MatrixClientPeg.get().setDeviceVerified(
this.props.userId, this.props.device.deviceId, true
this.props.userId, this.state.device.deviceId, true
);
}
},
@ -69,26 +91,26 @@ export default React.createClass({
onUnverifyClick: function() {
MatrixClientPeg.get().setDeviceVerified(
this.props.userId, this.props.device.deviceId, false
this.props.userId, this.state.device.deviceId, false
);
},
onBlacklistClick: function() {
MatrixClientPeg.get().setDeviceBlocked(
this.props.userId, this.props.device.deviceId, true
this.props.userId, this.state.device.deviceId, true
);
},
onUnblacklistClick: function() {
MatrixClientPeg.get().setDeviceBlocked(
this.props.userId, this.props.device.deviceId, false
this.props.userId, this.state.device.deviceId, false
);
},
render: function() {
var blacklistButton = null, verifyButton = null;
if (this.props.device.isBlocked()) {
if (this.state.device.isBlocked()) {
blacklistButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblacklist"
onClick={this.onUnblacklistClick}>
@ -104,7 +126,7 @@ export default React.createClass({
);
}
if (this.props.device.isVerified()) {
if (this.state.device.isVerified()) {
verifyButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
onClick={this.onUnverifyClick}>

View file

@ -57,7 +57,7 @@ module.exports = React.createClass({
getInitialState: function() {
return {
phase: this.Phases.Display,
}
};
},
componentWillReceiveProps: function(nextProps) {
@ -164,7 +164,7 @@ module.exports = React.createClass({
this.setState({
phase: this.Phases.Edit,
})
});
},
onFocus: function(ev) {
@ -197,9 +197,9 @@ module.exports = React.createClass({
sel.removeAllRanges();
if (this.props.blurToCancel)
this.cancelEdit();
{this.cancelEdit();}
else
this.onFinish(ev);
{this.onFinish(ev);}
this.showPlaceholder(!this.value);
},

View file

@ -24,7 +24,7 @@ module.exports = React.createClass({
events: React.PropTypes.array.isRequired,
// An array of EventTiles to render when expanded
children: React.PropTypes.array.isRequired,
// The maximum number of names to show in either the join or leave summaries
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
summaryLength: React.PropTypes.number,
// The maximum number of avatars to display in the summary
avatarsMaxLength: React.PropTypes.number,
@ -40,110 +40,12 @@ module.exports = React.createClass({
getDefaultProps: function() {
return {
summaryLength: 3,
summaryLength: 1,
threshold: 3,
avatarsMaxLength: 5,
};
},
_toggleSummary: function() {
this.setState({
expanded: !this.state.expanded,
});
},
_getEventSenderName: function(ev) {
if (!ev) {
return 'undefined';
}
return ev.sender.name || ev.event.content.displayname || ev.getSender();
},
_renderNameList: function(events) {
if (events.length === 0) {
return null;
}
let originalNumber = events.length;
events = events.slice(0, this.props.summaryLength);
let lastEvent = events.pop();
let names = events.map((ev) => {
return this._getEventSenderName(ev);
}).join(', ');
let lastName = this._getEventSenderName(lastEvent);
if (names.length === 0) {
// special-case for a single event
return lastName;
}
let remaining = originalNumber - this.props.summaryLength;
if (remaining > 0) {
// name1, name2, name3, and 100 others
return names + ', ' + lastName + ', and ' + remaining + ' others';
} else {
// name1, name2 and name3
return names + ' and ' + lastName;
}
},
_renderSummary: function(joinEvents, leaveEvents) {
let joiners = this._renderNameList(joinEvents);
let leavers = this._renderNameList(leaveEvents);
let joinSummary = null;
if (joiners) {
joinSummary = (
<span>
{joiners} joined the room
</span>
);
}
let leaveSummary = null;
if (leavers) {
leaveSummary = (
<span>
{leavers} left the room
</span>
);
}
// The joinEvents and leaveEvents are representative of the net movement
// per-user, and so it is possible that the total net movement is nil,
// whilst there are some events in the expanded list. If the total net
// movement is nil, then neither joinSummary nor leaveSummary will be
// truthy, so return null.
if (!joinSummary && !leaveSummary) {
return null;
}
return (
<span>
{joinSummary}{joinSummary && leaveSummary?'; ':''}
{leaveSummary}.&nbsp;
</span>
);
},
_renderAvatars: function(events) {
let avatars = events.slice(0, this.props.avatarsMaxLength).map((e) => {
return (
<MemberAvatar
key={e.getId()}
member={e.sender}
width={14}
height={14}
/>
);
});
return (
<span>
{avatars}
</span>
);
},
shouldComponentUpdate: function(nextProps, nextState) {
// Update if
// - The number of summarised events has changed
@ -157,10 +59,296 @@ module.exports = React.createClass({
);
},
_toggleSummary: function() {
this.setState({
expanded: !this.state.expanded,
});
},
/**
* Render the JSX for users aggregated by their transition sequences (`eventAggregates`) where
* the sequences are ordered by `orderedTransitionSequences`.
* @param {object[]} eventAggregates a map of transition sequence to array of user display names
* or user IDs.
* @param {string[]} orderedTransitionSequences an array which is some ordering of
* `Object.keys(eventAggregates)`.
* @returns {ReactElement} a single <span> containing the textual summary of the aggregated
* events that occurred.
*/
_renderSummary: function(eventAggregates, orderedTransitionSequences) {
const summaries = orderedTransitionSequences.map((transitions) => {
const userNames = eventAggregates[transitions];
const nameList = this._renderNameList(userNames);
const plural = userNames.length > 1;
const splitTransitions = transitions.split(',');
// Some neighbouring transitions are common, so canonicalise some into "pair"
// transitions
const canonicalTransitions = this._getCanonicalTransitions(splitTransitions);
// Transform into consecutive repetitions of the same transition (like 5
// consecutive 'joined_and_left's)
const coalescedTransitions = this._coalesceRepeatedTransitions(
canonicalTransitions
);
const descs = coalescedTransitions.map((t) => {
return this._getDescriptionForTransition(
t.transitionType, plural, t.repeats
);
});
const desc = this._renderCommaSeparatedList(descs);
return nameList + " " + desc;
});
if (!summaries) {
return null;
}
return (
<span>
{summaries.join(", ")}
</span>
);
},
/**
* @param {string[]} users an array of user display names or user IDs.
* @returns {string} a comma-separated list that ends with "and [n] others" if there are
* more items in `users` than `this.props.summaryLength`, which is the number of names
* included before "and [n] others".
*/
_renderNameList: function(users) {
return this._renderCommaSeparatedList(users, this.props.summaryLength);
},
/**
* Canonicalise an array of transitions such that some pairs of transitions become
* single transitions. For example an input ['joined','left'] would result in an output
* ['joined_and_left'].
* @param {string[]} transitions an array of transitions.
* @returns {string[]} an array of transitions.
*/
_getCanonicalTransitions: function(transitions) {
const modMap = {
'joined': {
'after': 'left',
'newTransition': 'joined_and_left',
},
'left': {
'after': 'joined',
'newTransition': 'left_and_joined',
},
// $currentTransition : {
// 'after' : $nextTransition,
// 'newTransition' : 'new_transition_type',
// },
};
const res = [];
for (let i = 0; i < transitions.length; i++) {
const t = transitions[i];
const t2 = transitions[i + 1];
let transition = t;
if (i < transitions.length - 1 && modMap[t] && modMap[t].after === t2) {
transition = modMap[t].newTransition;
i++;
}
res.push(transition);
}
return res;
},
/**
* Transform an array of transitions into an array of transitions and how many times
* they are repeated consecutively.
*
* An array of 123 "joined_and_left" transitions, would result in:
* ```
* [{
* transitionType: "joined_and_left"
* repeats: 123
* }]
* ```
* @param {string[]} transitions the array of transitions to transform.
* @returns {object[]} an array of coalesced transitions.
*/
_coalesceRepeatedTransitions: function(transitions) {
const res = [];
for (let i = 0; i < transitions.length; i++) {
if (res.length > 0 && res[res.length - 1].transitionType === transitions[i]) {
res[res.length - 1].repeats += 1;
} else {
res.push({
transitionType: transitions[i],
repeats: 1,
});
}
}
return res;
},
/**
* For a certain transition, t, describe what happened to the users that
* underwent the transition.
* @param {string} t the transition type.
* @param {boolean} plural whether there were multiple users undergoing the same
* transition.
* @param {number} repeats the number of times the transition was repeated in a row.
* @returns {string} the written English equivalent of the transition.
*/
_getDescriptionForTransition(t, plural, repeats) {
const beConjugated = plural ? "were" : "was";
const invitation = "their invitation" + (plural || (repeats > 1) ? "s" : "");
let res = null;
const map = {
"joined": "joined",
"left": "left",
"joined_and_left": "joined and left",
"left_and_joined": "left and rejoined",
"invite_reject": "rejected " + invitation,
"invite_withdrawal": "had " + invitation + " withdrawn",
"invited": beConjugated + " invited",
"banned": beConjugated + " banned",
"unbanned": beConjugated + " unbanned",
"kicked": beConjugated + " kicked",
};
if (Object.keys(map).includes(t)) {
res = map[t] + (repeats > 1 ? " " + repeats + " times" : "" );
}
return res;
},
/**
* Constructs a written English string representing `items`, with an optional limit on
* the number of items included in the result. If specified and if the length of
*`items` is greater than the limit, the string "and n others" will be appended onto
* the result.
* If `items` is empty, returns the empty string. If there is only one item, return
* it.
* @param {string[]} items the items to construct a string from.
* @param {number?} itemLimit the number by which to limit the list.
* @returns {string} a string constructed by joining `items` with a comma between each
* item, but with the last item appended as " and [lastItem]".
*/
_renderCommaSeparatedList(items, itemLimit) {
const remaining = itemLimit === undefined ? 0 : Math.max(
items.length - itemLimit, 0
);
if (items.length === 0) {
return "";
} else if (items.length === 1) {
return items[0];
} else if (remaining) {
items = items.slice(0, itemLimit);
const other = " other" + (remaining > 1 ? "s" : "");
return items.join(', ') + ' and ' + remaining + other;
} else {
const lastItem = items.pop();
return items.join(', ') + ' and ' + lastItem;
}
},
_renderAvatars: function(roomMembers) {
const avatars = roomMembers.slice(0, this.props.avatarsMaxLength).map((m) => {
return (
<MemberAvatar key={m.userId} member={m} width={14} height={14} />
);
});
return (
<span>
{avatars}
</span>
);
},
_getTransitionSequence: function(events) {
return events.map(this._getTransition);
},
/**
* Label a given membership event, `e`, where `getContent().membership` has
* changed for each transition allowed by the Matrix protocol. This attempts to
* label the membership changes that occur in `../../../TextForEvent.js`.
* @param {MatrixEvent} e the membership change event to label.
* @returns {string?} the transition type given to this event. This defaults to `null`
* if a transition is not recognised.
*/
_getTransition: function(e) {
switch (e.mxEvent.getContent().membership) {
case 'invite': return 'invited';
case 'ban': return 'banned';
case 'join': return 'joined';
case 'leave':
if (e.mxEvent.getSender() === e.mxEvent.getStateKey()) {
switch (e.mxEvent.getPrevContent().membership) {
case 'invite': return 'invite_reject';
default: return 'left';
}
}
switch (e.mxEvent.getPrevContent().membership) {
case 'invite': return 'invite_withdrawal';
case 'ban': return 'unbanned';
case 'join': return 'kicked';
default: return 'left';
}
default: return null;
}
},
_getAggregate: function(userEvents) {
// A map of aggregate type to arrays of display names. Each aggregate type
// is a comma-delimited string of transitions, e.g. "joined,left,kicked".
// The array of display names is the array of users who went through that
// sequence during eventsToRender.
const aggregate = {
// $aggregateType : []:string
};
// A map of aggregate types to the indices that order them (the index of
// the first event for a given transition sequence)
const aggregateIndices = {
// $aggregateType : int
};
const users = Object.keys(userEvents);
users.forEach(
(userId) => {
const firstEvent = userEvents[userId][0];
const displayName = firstEvent.displayName;
const seq = this._getTransitionSequence(userEvents[userId]);
if (!aggregate[seq]) {
aggregate[seq] = [];
aggregateIndices[seq] = -1;
}
aggregate[seq].push(displayName);
if (aggregateIndices[seq] === -1 ||
firstEvent.index < aggregateIndices[seq]) {
aggregateIndices[seq] = firstEvent.index;
}
}
);
return {
names: aggregate,
indices: aggregateIndices,
};
},
render: function() {
let eventsToRender = this.props.events;
let fewEvents = eventsToRender.length < this.props.threshold;
let expanded = this.state.expanded || fewEvents;
const eventsToRender = this.props.events;
const fewEvents = eventsToRender.length < this.props.threshold;
const expanded = this.state.expanded || fewEvents;
let expandedEvents = null;
if (expanded) {
@ -175,70 +363,56 @@ module.exports = React.createClass({
);
}
// Map user IDs to the first and last member events in eventsToRender for each user
let userEvents = {
// $userId : {first : e0, last : e1}
// Map user IDs to an array of objects:
const userEvents = {
// $userId : [{
// // The original event
// mxEvent: e,
// // The display name of the user (if not, then user ID)
// displayName: e.target.name || userId,
// // The original index of the event in this.props.events
// index: index,
// }]
};
eventsToRender.forEach((e) => {
const userId = e.getSender();
const avatarMembers = [];
eventsToRender.forEach((e, index) => {
const userId = e.getStateKey();
// Initialise a user's events
if (!userEvents[userId]) {
userEvents[userId] = {first: null, last: null};
userEvents[userId] = [];
avatarMembers.push(e.target);
}
if (!userEvents[userId].first) {
userEvents[userId].first = e;
}
userEvents[userId].last = e;
userEvents[userId].push({
mxEvent: e,
displayName: (e.target ? e.target.name : null) || userId,
index: index,
});
});
// Populate the join/leave event arrays with events that represent what happened
// overall to a user's membership. If no events are added to either array for a
// particular user, they will be considered a user that "joined and left".
let joinEvents = [];
let leaveEvents = [];
let joinedAndLeft = 0;
let senders = Object.keys(userEvents);
senders.forEach(
(userId) => {
let firstEvent = userEvents[userId].first;
let lastEvent = userEvents[userId].last;
const aggregate = this._getAggregate(userEvents);
// Membership BEFORE eventsToRender
let previousMembership = firstEvent.getPrevContent().membership || "leave";
// If the last membership event differs from previousMembership, use that.
if (previousMembership !== lastEvent.getContent().membership) {
if (lastEvent.event.content.membership === 'join') {
joinEvents.push(lastEvent);
} else if (lastEvent.event.content.membership === 'leave') {
leaveEvents.push(lastEvent);
}
} else {
// Increment the number of users whose membership change was nil overall
joinedAndLeft++;
}
}
// Sort types by order of lowest event index within sequence
const orderedTransitionSequences = Object.keys(aggregate.names).sort(
(seq1, seq2) => aggregate.indices[seq1] > aggregate.indices[seq2]
);
let avatars = this._renderAvatars(joinEvents.concat(leaveEvents));
let summary = this._renderSummary(joinEvents, leaveEvents);
let toggleButton = (
const avatars = this._renderAvatars(avatarMembers);
const summary = this._renderSummary(aggregate.names, orderedTransitionSequences);
const toggleButton = (
<a className="mx_MemberEventListSummary_toggle" onClick={this._toggleSummary}>
{expanded ? 'collapse' : 'expand'}
</a>
);
let plural = (joinEvents.length + leaveEvents.length > 0) ? 'others' : 'users';
let noun = (joinedAndLeft === 1 ? 'user' : plural);
let summaryContainer = (
const summaryContainer = (
<div className="mx_EventTile_line">
<div className="mx_EventTile_info">
<span className="mx_MemberEventListSummary_avatars">
{avatars}
</span>
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
{summary}{joinedAndLeft ? joinedAndLeft + ' ' + noun + ' joined and left' : ''}
{summary}
</span>&nbsp;
{toggleButton}
</div>

View file

@ -73,7 +73,7 @@ module.exports = React.createClass({
getValue: function() {
var value;
if (this.refs.select) {
value = reverseRoles[ this.refs.select.value ];
value = reverseRoles[this.refs.select.value];
if (this.refs.custom) {
if (value === undefined) value = parseInt( this.refs.custom.value );
}
@ -86,10 +86,10 @@ module.exports = React.createClass({
if (this.state.custom) {
var input;
if (this.props.disabled) {
input = <span>{ this.props.value }</span>
input = <span>{ this.props.value }</span>;
}
else {
input = <input ref="custom" type="text" size="3" defaultValue={ this.props.value } onBlur={ this.onCustomBlur } onKeyDown={ this.onCustomKeyDown }/>
input = <input ref="custom" type="text" size="3" defaultValue={ this.props.value } onBlur={ this.onCustomBlur } onKeyDown={ this.onCustomKeyDown }/>;
}
customPicker = <span> of { input }</span>;
}
@ -115,7 +115,7 @@ module.exports = React.createClass({
<option value="Moderator">Moderator (50)</option>
<option value="Admin">Admin (100)</option>
<option value="Custom">Custom level</option>
</select>
</select>;
}
return (

View file

@ -35,4 +35,4 @@ module.exports = React.createClass({
<div className="mx_ProgressBar"><div className="mx_ProgressBar_fill" style={progressStyle}></div></div>
);
}
});
});

View file

@ -69,6 +69,7 @@ var TintableSvg = React.createClass({
width={ this.props.width }
height={ this.props.height }
onLoad={ this.onLoad }
tabIndex="-1"
/>
);
}

View file

@ -55,7 +55,7 @@ module.exports = React.createClass({
overflowJsx = this.props.createOverflowElement(
overflowCount, childCount
);
// cut out the overflow elements
childArray.splice(childCount - overflowCount, overflowCount);
childsJsx = childArray; // use what is left

View file

@ -56,7 +56,7 @@ module.exports = React.createClass({
<div>
<ul className="mx_UserSelector_UserIdList" ref="list">
{this.props.selected_users.map(function(user_id, i) {
return <li key={user_id}>{user_id} - <span onClick={function() {self.removeUser(user_id);}}>X</span></li>
return <li key={user_id}>{user_id} - <span onClick={function() {self.removeUser(user_id);}}>X</span></li>;
})}
</ul>
<input type="text" ref="user_id_input" defaultValue="" className="mx_UserSelector_userIdInput" placeholder="ex. @bob:example.com"/>

View file

@ -52,7 +52,7 @@ module.exports = React.createClass({
this._onCaptchaLoaded();
} else {
console.log("Loading recaptcha script...");
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded()};
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();};
var protocol = global.location.protocol;
if (protocol === "file:") {
var warning = document.createElement('div');
@ -101,7 +101,7 @@ module.exports = React.createClass({
} catch (e) {
this.setState({
errorText: e.toString(),
})
});
}
},

View file

@ -70,7 +70,7 @@ export const PasswordAuthEntry = React.createClass({
});
},
_onPasswordFieldChange: function (ev) {
_onPasswordFieldChange: function(ev) {
// enable the submit button iff the password is non-empty
this.props.setSubmitButtonEnabled(Boolean(ev.target.value));
},
@ -209,4 +209,4 @@ export function getEntryComponentForLoginType(loginType) {
}
}
return FallbackAuthEntry;
};
}

Some files were not shown because too many files have changed in this diff Show more