Merge branch 'master' of https://github.com/matrix-org/matrix-react-sdk into rxl881/apps

This commit is contained in:
Richard Lewis 2017-06-12 14:50:25 +01:00
commit f9f924bbd6
163 changed files with 11304 additions and 2243 deletions

View file

@ -32,6 +32,8 @@ export default function AccessibleButton(props) {
};
restProps.tabIndex = restProps.tabIndex || "0";
restProps.role = "button";
restProps.className = (restProps.className ? restProps.className + " " : "") +
"mx_AccessibleButton";
return React.createElement(element, restProps, children);
}

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -138,7 +139,7 @@ export default React.createClass({
onClick={this.onClick.bind(this, i)}
onMouseEnter={this.onMouseEnter.bind(this, i)}
onMouseLeave={this.onMouseLeave}
key={this.props.addressList[i].userId}
key={this.props.addressList[i].addressType + "/" + this.props.addressList[i].address}
ref={(ref) => { this.addressListElement = ref; }}
>
<AddressTile address={this.props.addressList[i]} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />

View file

@ -16,12 +16,13 @@ limitations under the License.
'use strict';
var React = require('react');
var classNames = require('classnames');
var sdk = require("../../../index");
var Invite = require("../../../Invite");
var MatrixClientPeg = require("../../../MatrixClientPeg");
var Avatar = require('../../../Avatar');
import React from 'react';
import classNames from 'classnames';
import sdk from "../../../index";
import Invite from "../../../Invite";
import MatrixClientPeg from "../../../MatrixClientPeg";
import Avatar from '../../../Avatar';
import { _t } from '../../../languageHandler';
// React PropType definition for an object describing
// an address that can be invited to a room (which
@ -142,7 +143,7 @@ export default React.createClass({
});
info = (
<div className={unknownClasses}>Unknown Address</div>
<div className={unknownClasses}>{_t("Unknown Address")}</div>
);
}

View file

@ -18,6 +18,7 @@ import React from 'react';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
export default React.createClass({
displayName: 'DeviceVerifyButtons',
@ -50,42 +51,10 @@ export default React.createClass({
},
onVerifyClick: function() {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: "Verify device",
description: (
<div>
<p>
To verify that this device can be trusted, please contact its
owner using some other means (e.g. in person or a phone call)
and ask them whether the key they see in their User Settings
for this device matches the key below:
</p>
<div className="mx_UserSettings_cryptoSection">
<ul>
<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>
If it matches, press the verify button below.
If it doesnt, then someone else is intercepting this device
and you probably want to press the blacklist button instead.
</p>
<p>
In future this verification process will be more sophisticated.
</p>
</div>
),
button: "I verify that the keys match",
onFinished: confirm=>{
if (confirm) {
MatrixClientPeg.get().setDeviceVerified(
this.props.userId, this.state.device.deviceId, true
);
}
},
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
Modal.createDialog(DeviceVerifyDialog, {
userId: this.props.userId,
device: this.state.device,
});
},
@ -114,14 +83,14 @@ export default React.createClass({
blacklistButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblacklist"
onClick={this.onUnblacklistClick}>
Unblacklist
{_t("Unblacklist")}
</button>
);
} else {
blacklistButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_blacklist"
onClick={this.onBlacklistClick}>
Blacklist
{_t("Blacklist")}
</button>
);
}
@ -130,14 +99,14 @@ export default React.createClass({
verifyButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
onClick={this.onUnverifyClick}>
Unverify
{_t("Unverify")}
</button>
);
} else {
verifyButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify"
onClick={this.onVerifyClick}>
Verify...
{_t("Verify...")}
</button>
);
}

View file

@ -93,7 +93,7 @@ export default class DirectorySearchBox extends React.Component {
className="mx_DirectorySearchBox_input"
ref={this._collectInput}
onChange={this._onChange} onKeyUp={this._onKeyUp}
placeholder={this.props.placeholder}
placeholder={this.props.placeholder} autoFocus
/>
{join_button}
<span className="mx_DirectorySearchBox_clear_wrapper">

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from 'react';
import classnames from 'classnames';
import AccessibleButton from './AccessibleButton';
import { _t } from '../../../languageHandler';
class MenuOption extends React.Component {
constructor(props) {
@ -114,8 +115,11 @@ export default class Dropdown extends React.Component {
}
componentWillReceiveProps(nextProps) {
if (!nextProps.children || nextProps.children.length === 0) {
return;
}
this._reindexChildren(nextProps.children);
const firstChild = React.Children.toArray(nextProps.children)[0];
const firstChild = nextProps.children[0];
this.setState({
highlightedOption: firstChild ? firstChild.key : null,
});
@ -149,10 +153,12 @@ export default class Dropdown extends React.Component {
}
_onInputClick(ev) {
this.setState({
expanded: !this.state.expanded,
});
ev.preventDefault();
if (!this.state.expanded) {
this.setState({
expanded: true,
});
ev.preventDefault();
}
}
_onMenuOptionClick(dropdownKey) {
@ -248,13 +254,10 @@ export default class Dropdown extends React.Component {
</MenuOption>
);
});
if (!this.state.searchQuery) {
options.push(
<div key="_searchprompt" className="mx_Dropdown_searchPrompt">
Type to search...
</div>
);
if (options.length === 0) {
return [<div key="0" className="mx_Dropdown_option">
{_t("No results")}
</div>];
}
return options;
}
@ -267,16 +270,20 @@ export default class Dropdown extends React.Component {
let menu;
if (this.state.expanded) {
currentValue = <input type="text" className="mx_Dropdown_option"
ref={this._collectInputTextBox} onKeyPress={this._onInputKeyPress}
onKeyUp={this._onInputKeyUp}
onChange={this._onInputChange}
value={this.state.searchQuery}
/>;
if (this.props.searchEnabled) {
currentValue = <input type="text" className="mx_Dropdown_option"
ref={this._collectInputTextBox} onKeyPress={this._onInputKeyPress}
onKeyUp={this._onInputKeyUp}
onChange={this._onInputChange}
value={this.state.searchQuery}
/>;
}
menu = <div className="mx_Dropdown_menu" style={menuStyle}>
{this._getMenuOptions()}
</div>;
} else {
}
if (!currentValue) {
const selectedChild = this.props.getShortOption ?
this.props.getShortOption(this.props.value) :
this.childrenByKey[this.props.value];
@ -313,6 +320,7 @@ Dropdown.propTypes = {
onOptionChange: React.PropTypes.func.isRequired,
// Called when the value of the search field changes
onSearchChange: React.PropTypes.func,
searchEnabled: React.PropTypes.bool,
// Function that, given the key of an option, returns
// a node representing that option to be displayed in the
// box itself as the currently-selected option (ie. as

View file

@ -0,0 +1,121 @@
/*
Copyright 2017 Marcel Radzio (MTRNord)
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 UserSettingsStore from '../../../UserSettingsStore';
import { _t } from '../../../languageHandler';
import * as languageHandler from '../../../languageHandler';
function languageMatchesSearchQuery(query, language) {
if (language.label.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
if (language.value.toUpperCase() == query.toUpperCase()) return true;
return false;
}
export default class LanguageDropdown extends React.Component {
constructor(props) {
super(props);
this._onSearchChange = this._onSearchChange.bind(this);
this.state = {
searchQuery: '',
langs: null,
}
}
componentWillMount() {
languageHandler.getAllLanguagesFromJson().then((langs) => {
langs.sort(function(a, b){
if(a.label < b.label) return -1;
if(a.label > b.label) return 1;
return 0;
});
this.setState({langs});
}).catch(() => {
this.setState({langs: ['en']});
}).done();
if (!this.props.value) {
// If no value is given, we start with the first
// country selected, but our parent component
// doesn't know this, therefore we do this.
const _localSettings = UserSettingsStore.getLocalSettings();
if (_localSettings.hasOwnProperty('language')) {
this.props.onOptionChange(_localSettings.language);
}else {
const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser());
this.props.onOptionChange(language);
}
}
}
_onSearchChange(search) {
this.setState({
searchQuery: search,
});
}
render() {
if (this.state.langs === null) {
const Spinner = sdk.getComponent('elements.Spinner');
return <Spinner />;
}
const Dropdown = sdk.getComponent('elements.Dropdown');
let displayedLanguages;
if (this.state.searchQuery) {
displayedLanguages = this.state.langs.filter((lang) => {
return languageMatchesSearchQuery(this.state.searchQuery, lang);
});
} else {
displayedLanguages = this.state.langs;
}
const options = displayedLanguages.map((language) => {
return <div key={language.value}>
{language.label}
</div>;
});
// default value here too, otherwise we need to handle null / undefined
// values between mounting and the initial value propgating
let value = null;
const _localSettings = UserSettingsStore.getLocalSettings();
if (_localSettings.hasOwnProperty('language')) {
value = this.props.value || _localSettings.language;
} else {
const language = navigator.language || navigator.userLanguage;
value = this.props.value || language;
}
return <Dropdown className={this.props.className}
onOptionChange={this.props.onOptionChange} onSearchChange={this._onSearchChange}
searchEnabled={true} value={value}
>
{options}
</Dropdown>
}
}
LanguageDropdown.propTypes = {
className: React.PropTypes.string,
onOptionChange: React.PropTypes.func.isRequired,
value: React.PropTypes.string,
};

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import React from 'react';
const MemberAvatar = require('../avatars/MemberAvatar.js');
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'MemberEventListSummary',
@ -203,28 +204,146 @@ module.exports = React.createClass({
* @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.
* @returns {string} the written Human Readable equivalent of the transition.
*/
_getDescriptionForTransition(t, plural, repeats) {
const beConjugated = plural ? "were" : "was";
const invitation = "their invitation" + (plural || (repeats > 1) ? "s" : "");
// The empty interpolations 'severalUsers' and 'oneUser'
// are there only to show translators to non-English languages
// that the verb is conjugated to plural or singular Subject.
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",
};
switch(t) {
case "joined":
if (repeats > 1) {
res = (plural)
? _t("%(severalUsers)sjoined %(repeats)s times", { severalUsers: "", repeats: repeats })
: _t("%(oneUser)sjoined %(repeats)s times", { oneUser: "", repeats: repeats });
} else {
res = (plural)
? _t("%(severalUsers)sjoined", { severalUsers: "" })
: _t("%(oneUser)sjoined", { oneUser: "" });
}
if (Object.keys(map).includes(t)) {
res = map[t] + (repeats > 1 ? " " + repeats + " times" : "" );
break;
case "left":
if (repeats > 1) {
res = (plural)
? _t("%(severalUsers)sleft %(repeats)s times", { severalUsers: "", repeats: repeats })
: _t("%(oneUser)sleft %(repeats)s times", { oneUser: "", repeats: repeats });
} else {
res = (plural)
? _t("%(severalUsers)sleft", { severalUsers: "" })
: _t("%(oneUser)sleft", { oneUser: "" });
} break;
case "joined_and_left":
if (repeats > 1) {
res = (plural)
? _t("%(severalUsers)sjoined and left %(repeats)s times", { severalUsers: "", repeats: repeats })
: _t("%(oneUser)sjoined and left %(repeats)s times", { oneUser: "", repeats: repeats });
} else {
res = (plural)
? _t("%(severalUsers)sjoined and left", { severalUsers: "" })
: _t("%(oneUser)sjoined and left", { oneUser: "" });
}
break;
case "left_and_joined":
if (repeats > 1) {
res = (plural)
? _t("%(severalUsers)sleft and rejoined %(repeats)s times", { severalUsers: "", repeats: repeats })
: _t("%(oneUser)sleft and rejoined %(repeats)s times", { oneUser: "", repeats: repeats });
} else {
res = (plural)
? _t("%(severalUsers)sleft and rejoined", { severalUsers: "" })
: _t("%(oneUser)sleft and rejoined", { oneUser: "" });
} break;
break;
case "invite_reject":
if (repeats > 1) {
res = (plural)
? _t("%(severalUsers)srejected their invitations %(repeats)s times", { severalUsers: "", repeats: repeats })
: _t("%(oneUser)srejected their invitation %(repeats)s times", { oneUser: "", repeats: repeats });
} else {
res = (plural)
? _t("%(severalUsers)srejected their invitations", { severalUsers: "" })
: _t("%(oneUser)srejected their invitation", { oneUser: "" });
}
break;
case "invite_withdrawal":
if (repeats > 1) {
res = (plural)
? _t("%(severalUsers)shad their invitations withdrawn %(repeats)s times", { severalUsers: "", repeats: repeats })
: _t("%(oneUser)shad their invitation withdrawn %(repeats)s times", { oneUser: "", repeats: repeats });
} else {
res = (plural)
? _t("%(severalUsers)shad their invitations withdrawn", { severalUsers: "" })
: _t("%(oneUser)shad their invitation withdrawn", { oneUser: "" });
}
break;
case "invited":
if (repeats > 1) {
res = (plural)
? _t("were invited %(repeats)s times", { repeats: repeats })
: _t("was invited %(repeats)s times", { repeats: repeats });
} else {
res = (plural)
? _t("were invited")
: _t("was invited");
}
break;
case "banned":
if (repeats > 1) {
res = (plural)
? _t("were banned %(repeats)s times", { repeats: repeats })
: _t("was banned %(repeats)s times", { repeats: repeats });
} else {
res = (plural)
? _t("were banned")
: _t("was banned");
}
break;
case "unbanned":
if (repeats > 1) {
res = (plural)
? _t("were unbanned %(repeats)s times", { repeats: repeats })
: _t("was unbanned %(repeats)s times", { repeats: repeats });
} else {
res = (plural)
? _t("were unbanned")
: _t("was unbanned");
}
break;
case "kicked":
if (repeats > 1) {
res = (plural)
? _t("were kicked %(repeats)s times", { repeats: repeats })
: _t("was kicked %(repeats)s times", { repeats: repeats });
} else {
res = (plural)
? _t("were kicked")
: _t("was kicked");
}
break;
case "changed_name":
if (repeats > 1) {
res = (plural)
? _t("%(severalUsers)schanged their name %(repeats)s times", { severalUsers: "", repeats: repeats })
: _t("%(oneUser)schanged their name %(repeats)s times", { oneUser: "", repeats: repeats });
} else {
res = (plural)
? _t("%(severalUsers)schanged their name", { severalUsers: "" })
: _t("%(oneUser)schanged their name", { oneUser: "" });
}
break;
case "changed_avatar":
if (repeats > 1) {
res = (plural)
? _t("%(severalUsers)schanged their avatar %(repeats)s times", { severalUsers: "", repeats: repeats })
: _t("%(oneUser)schanged their avatar %(repeats)s times", { oneUser: "", repeats: repeats });
} else {
res = (plural)
? _t("%(severalUsers)schanged their avatar", { severalUsers: "" })
: _t("%(oneUser)schanged their avatar", { oneUser: "" });
}
break;
}
return res;
@ -252,11 +371,12 @@ module.exports = React.createClass({
return items[0];
} else if (remaining) {
items = items.slice(0, itemLimit);
const other = " other" + (remaining > 1 ? "s" : "");
return items.join(', ') + ' and ' + remaining + other;
return (remaining > 1)
? _t("%(items)s and %(remaining)s others", { items: items.join(', '), remaining: remaining } )
: _t("%(items)s and one other", { items: items.join(', ') });
} else {
const lastItem = items.pop();
return items.join(', ') + ' and ' + lastItem;
return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem });
}
},
@ -267,7 +387,7 @@ module.exports = React.createClass({
);
});
return (
<span className="mx_MemberEventListSummary_avatars">
<span className="mx_MemberEventListSummary_avatars" onClick={ this._toggleSummary }>
{avatars}
</span>
);
@ -289,7 +409,24 @@ module.exports = React.createClass({
switch (e.mxEvent.getContent().membership) {
case 'invite': return 'invited';
case 'ban': return 'banned';
case 'join': return 'joined';
case 'join':
if (e.mxEvent.getPrevContent().membership === 'join') {
if (e.mxEvent.getContent().displayname !==
e.mxEvent.getPrevContent().displayname)
{
return 'changed_name';
}
else if (e.mxEvent.getContent().avatar_url !==
e.mxEvent.getPrevContent().avatar_url)
{
return 'changed_avatar';
}
// console.log("MELS ignoring duplicate membership join event");
return null;
}
else {
return 'joined';
}
case 'leave':
if (e.mxEvent.getSender() === e.mxEvent.getStateKey()) {
switch (e.mxEvent.getPrevContent().membership) {
@ -350,6 +487,7 @@ module.exports = React.createClass({
render: function() {
const eventsToRender = this.props.events;
const eventIds = eventsToRender.map(e => e.getId()).join(',');
const fewEvents = eventsToRender.length < this.props.threshold;
const expanded = this.state.expanded || fewEvents;
@ -360,7 +498,7 @@ module.exports = React.createClass({
if (fewEvents) {
return (
<div className="mx_MemberEventListSummary">
<div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
{expandedEvents}
</div>
);
@ -418,7 +556,7 @@ module.exports = React.createClass({
);
return (
<div className="mx_MemberEventListSummary">
<div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
{toggleButton}
{summaryContainer}
{expanded ? <div className="mx_MemberEventListSummary_line">&nbsp;</div> : null}

View file

@ -16,18 +16,12 @@ limitations under the License.
'use strict';
var React = require('react');
var roles = {
0: 'User',
50: 'Moderator',
100: 'Admin',
};
import React from 'react';
import * as Roles from '../../../Roles';
import { _t } from '../../../languageHandler';
var LEVEL_ROLE_MAP = {};
var reverseRoles = {};
Object.keys(roles).forEach(function(key) {
reverseRoles[roles[key]] = key;
});
module.exports = React.createClass({
displayName: 'PowerSelector',
@ -49,9 +43,16 @@ module.exports = React.createClass({
getInitialState: function() {
return {
custom: (roles[this.props.value] === undefined),
custom: (LEVEL_ROLE_MAP[this.props.value] === undefined),
};
},
componentWillMount: function() {
LEVEL_ROLE_MAP = Roles.levelRoleMap();
Object.keys(LEVEL_ROLE_MAP).forEach(function(key) {
reverseRoles[LEVEL_ROLE_MAP[key]] = key;
});
},
onSelectChange: function(event) {
this.setState({ custom: event.target.value === "Custom" });
@ -99,22 +100,34 @@ module.exports = React.createClass({
selectValue = "Custom";
}
else {
selectValue = roles[this.props.value] || "Custom";
selectValue = LEVEL_ROLE_MAP[this.props.value] || "Custom";
}
var select;
if (this.props.disabled) {
select = <span>{ selectValue }</span>;
}
else {
// Each level must have a definition in LEVEL_ROLE_MAP
const levels = [0, 50, 100];
let options = levels.map((level) => {
return {
value: LEVEL_ROLE_MAP[level],
// Give a userDefault (users_default in the power event) of 0 but
// because level !== undefined, this should never be used.
text: Roles.textualPowerLevel(level, 0),
}
});
options.push({ value: "Custom", text: _t("Custom level") });
options = options.map((op) => {
return <option value={op.value} key={op.value}>{op.text}</option>;
});
select =
<select ref="select"
value={ this.props.controlled ? selectValue : undefined }
defaultValue={ !this.props.controlled ? selectValue : undefined }
onChange={ this.onSelectChange }>
<option value="User">User (0)</option>
<option value="Moderator">Moderator (50)</option>
<option value="Admin">Admin (100)</option>
<option value="Custom">Custom level</option>
{ options }
</select>;
}

View file

@ -16,7 +16,8 @@ limitations under the License.
'use strict';
var React = require('react');
import React from 'react';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'UserSelector',
@ -59,9 +60,9 @@ module.exports = React.createClass({
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"/>
<input type="text" ref="user_id_input" defaultValue="" className="mx_UserSelector_userIdInput" placeholder={_t("ex. @bob:example.com")}/>
<button onClick={this.onAddUserId} className="mx_UserSelector_AddUserId">
Add User
{_t("Add User")}
</button>
</div>
);