Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into set_default_federate_by_settings

This commit is contained in:
Michael Telatynski 2017-08-02 12:15:00 +01:00
commit fd454b476a
No known key found for this signature in database
GPG key ID: 0435A1D4BBD34D64
268 changed files with 27696 additions and 5893 deletions

View file

@ -32,6 +32,7 @@ module.exports = React.createClass({
urls: React.PropTypes.array, // [highest_priority, ... , lowest_priority]
width: React.PropTypes.number,
height: React.PropTypes.number,
// XXX resizeMethod not actually used.
resizeMethod: React.PropTypes.string,
defaultToInitialLetter: React.PropTypes.bool // true to add default url
},

View file

@ -0,0 +1,66 @@
/*
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 PropTypes from 'prop-types';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
export default React.createClass({
displayName: 'GroupAvatar',
propTypes: {
groupId: PropTypes.string,
groupAvatarUrl: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
resizeMethod: PropTypes.string,
},
getDefaultProps: function() {
return {
width: 36,
height: 36,
resizeMethod: 'crop',
};
},
getGroupAvatarUrl: function() {
return MatrixClientPeg.get().mxcUrlToHttp(
this.props.groupAvatarUrl,
this.props.width,
this.props.height,
this.props.resizeMethod,
);
},
render: function() {
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
// extract the props we use from props so we can pass any others through
// should consider adding this as a global rule in js-sdk?
/*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
const {groupId, groupAvatarUrl, ...otherProps} = this.props;
return (
<BaseAvatar
name={this.props.groupId[1]}
idName={this.props.groupId}
url={this.getGroupAvatarUrl()}
{...otherProps}
/>
);
},
});

View file

@ -59,7 +59,9 @@ module.exports = React.createClass({
ContentRepo.getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(),
props.oobData.avatarUrl,
props.width, props.height, props.resizeMethod
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod
), // highest priority
this.getRoomAvatarUrl(props),
this.getOneToOneAvatar(props),
@ -70,17 +72,19 @@ module.exports = React.createClass({
},
getRoomAvatarUrl: function(props) {
if (!this.props.room) return null;
if (!props.room) return null;
return props.room.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
props.width, props.height, props.resizeMethod,
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false
);
},
getOneToOneAvatar: function(props) {
if (!this.props.room) return null;
if (!props.room) return null;
var mlist = props.room.currentState.members;
var userIds = [];
@ -103,14 +107,18 @@ module.exports = React.createClass({
}
return theOtherGuy.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
props.width, props.height, props.resizeMethod,
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false
);
} else if (userIds.length == 1) {
return mlist[userIds[0]].getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
props.width, props.height, props.resizeMethod,
false
Math.floor(props.width * window.devicePixelRatio),
Math.floor(props.height * window.devicePixelRatio),
props.resizeMethod,
false
);
} else {
return null;
@ -118,9 +126,16 @@ module.exports = React.createClass({
},
getFallbackAvatar: function(props) {
if (!this.props.room) return null;
let roomId = null;
if (props.oobData && props.oobData.roomId) {
roomId = this.props.oobData.roomId;
} else if (props.room) {
roomId = props.room.roomId;
} else {
return null;
}
return Avatar.defaultAvatarUrlForString(props.room.roomId);
return Avatar.defaultAvatarUrlForString(roomId);
},
render: function() {

View file

@ -16,8 +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: 'CreateRoomButton',
propTypes: {
@ -36,7 +36,7 @@ module.exports = React.createClass({
render: function() {
return (
<button className="mx_CreateRoomButton" onClick={this.onClick}>Create Room</button>
<button className="mx_CreateRoomButton" onClick={this.onClick}>{_t("Create Room")}</button>
);
}
});

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
var React = require('react');
import { _t } from '../../../languageHandler';
var Presets = {
PrivateChat: "private_chat",
@ -46,9 +47,9 @@ module.exports = React.createClass({
render: function() {
return (
<select className="mx_Presets" onChange={this.onValueChanged} value={this.props.preset}>
<option value={this.Presets.PrivateChat}>Private Chat</option>
<option value={this.Presets.PublicChat}>Public Chat</option>
<option value={this.Presets.Custom}>Custom</option>
<option value={this.Presets.PrivateChat}>{_t("Private Chat")}</option>
<option value={this.Presets.PublicChat}>{_t("Public Chat")}</option>
<option value={this.Presets.Custom}>{_t("Custom")}</option>
</select>
);
}

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
var React = require('react');
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'RoomAlias',
@ -94,7 +95,7 @@ module.exports = React.createClass({
render: function() {
return (
<input type="text" className="mx_RoomAlias" placeholder="Alias (optional)"
<input type="text" className="mx_RoomAlias" placeholder={_t("Alias (optional)")}
onChange={this.onValueChanged} onFocus={this.onFocus} onBlur={this.onBlur}
value={this.props.alias}/>
);

View file

@ -67,7 +67,7 @@ export default React.createClass({
render: function() {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
return (
<div onKeyDown={this._onKeyDown} className={this.props.className}>
<AccessibleButton onClick={this._onCancelClick}

View file

@ -16,36 +16,30 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import DMRoomMap from '../../../utils/DMRoomMap';
import AccessibleButton from '../elements/AccessibleButton';
import Unread from '../../../Unread';
import classNames from 'classnames';
import createRoom from '../../../createRoom';
export default class ChatCreateOrReuseDialog extends React.Component {
constructor(props) {
super(props);
this.onNewDMClick = this.onNewDMClick.bind(this);
this.onRoomTileClick = this.onRoomTileClick.bind(this);
this.state = {
tiles: [],
profile: {
displayName: null,
avatarUrl: null,
},
profileError: null,
};
}
onNewDMClick() {
createRoom({dmUserId: this.props.userId});
this.props.onFinished(true);
}
onRoomTileClick(roomId) {
dis.dispatch({
action: 'view_room',
room_id: roomId,
});
this.props.onFinished(true);
}
render() {
componentWillMount() {
const client = MatrixClientPeg.get();
const dmRoomMap = new DMRoomMap(client);
@ -70,40 +64,123 @@ export default class ChatCreateOrReuseDialog extends React.Component {
highlight={highlight}
isInvite={me.membership == "invite"}
onClick={this.onRoomTileClick}
/>
/>,
);
}
}
const labelClasses = classNames({
mx_MemberInfo_createRoom_label: true,
mx_RoomTile_name: true,
this.setState({
tiles: tiles,
});
const startNewChat = <AccessibleButton
className="mx_MemberInfo_createRoom"
onClick={this.onNewDMClick}
>
<div className="mx_RoomTile_avatar">
<img src="img/create-big.svg" width="26" height="26" />
</div>
<div className={labelClasses}><i>Start new chat</i></div>
</AccessibleButton>;
if (tiles.length === 0) {
this.setState({
busyProfile: true,
});
MatrixClientPeg.get().getProfileInfo(this.props.userId).done((resp) => {
const profile = {
displayName: resp.displayname,
avatarUrl: null,
};
if (resp.avatar_url) {
profile.avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(
resp.avatar_url, 48, 48, "crop",
);
}
this.setState({
busyProfile: false,
profile: profile,
});
}, (err) => {
console.error(
'Unable to get profile for user ' + this.props.userId + ':',
err,
);
this.setState({
busyProfile: false,
profileError: err,
});
});
}
}
onRoomTileClick(roomId) {
this.props.onExistingRoomSelected(roomId);
}
render() {
let title = '';
let content = null;
if (this.state.tiles.length > 0) {
// Show the existing rooms with a "+" to add a new dm
title = _t('Create a new chat or reuse an existing one');
const labelClasses = classNames({
mx_MemberInfo_createRoom_label: true,
mx_RoomTile_name: true,
});
const startNewChat = <AccessibleButton
className="mx_MemberInfo_createRoom"
onClick={this.props.onNewDMClick}
>
<div className="mx_RoomTile_avatar">
<img src="img/create-big.svg" width="26" height="26" />
</div>
<div className={labelClasses}><i>{ _t("Start new chat") }</i></div>
</AccessibleButton>;
content = <div className="mx_Dialog_content">
{ _t('You already have existing direct chats with this user:') }
<div className="mx_ChatCreateOrReuseDialog_tiles">
{ this.state.tiles }
{ startNewChat }
</div>
</div>;
} else {
// Show the avatar, name and a button to confirm that a new chat is requested
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const Spinner = sdk.getComponent('elements.Spinner');
title = _t('Start chatting');
let profile = null;
if (this.state.busyProfile) {
profile = <Spinner />;
} else if (this.state.profileError) {
profile = <div className="error">
Unable to load profile information for { this.props.userId }
</div>;
} else {
profile = <div className="mx_ChatCreateOrReuseDialog_profile">
<BaseAvatar
name={this.state.profile.displayName || this.props.userId}
url={this.state.profile.avatarUrl}
width={48} height={48}
/>
<div className="mx_ChatCreateOrReuseDialog_profile_name">
{this.state.profile.displayName || this.props.userId}
</div>
</div>;
}
content = <div>
<div className="mx_Dialog_content">
<p>
{ _t('Click on the button below to start chatting!') }
</p>
{ profile }
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.props.onNewDMClick}>
{ _t('Start Chatting') }
</button>
</div>
</div>;
}
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className='mx_ChatCreateOrReuseDialog'
onFinished={() => {
this.props.onFinished(false)
}}
title='Create a new chat or reuse an existing one'
onFinished={ this.props.onFinished.bind(false) }
title={title}
>
<div className="mx_Dialog_content">
You already have existing direct chats with this user:
<div className="mx_ChatCreateOrReuseDialog_tiles">
{tiles}
{startNewChat}
</div>
</div>
{ content }
</BaseDialog>
);
}
@ -111,5 +188,8 @@ export default class ChatCreateOrReuseDialog extends React.Component {
ChatCreateOrReuseDialog.propTyps = {
userId: React.PropTypes.string.isRequired,
// Called when clicking outside of the dialog
onFinished: React.PropTypes.func.isRequired,
onNewDMClick: React.PropTypes.func.isRequired,
onExistingRoomSelected: React.PropTypes.func.isRequired,
};

View file

@ -15,25 +15,24 @@ limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import { _t } from '../../../languageHandler';
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';
import Fuse from 'fuse.js';
import Promise from 'bluebird';
import dis from '../../../dispatcher';
const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
module.exports = React.createClass({
displayName: "ChatInviteDialog",
propTypes: {
title: React.PropTypes.string,
title: React.PropTypes.string.isRequired,
description: React.PropTypes.oneOfType([
React.PropTypes.element,
React.PropTypes.string,
@ -43,17 +42,13 @@ module.exports = React.createClass({
roomId: React.PropTypes.string,
button: React.PropTypes.string,
focus: React.PropTypes.bool,
onFinished: React.PropTypes.func.isRequired
onFinished: React.PropTypes.func.isRequired,
},
getDefaultProps: function() {
return {
title: "Start a chat",
description: "Who would you like to communicate with?",
value: "",
placeholder: "Email, name or matrix ID",
button: "Start Chat",
focus: true
focus: true,
};
},
@ -61,12 +56,20 @@ module.exports = React.createClass({
return {
error: false,
// List of AddressTile.InviteAddressType objects represeting
// List of AddressTile.InviteAddressType objects representing
// 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
// Whether a search is ongoing
busy: false,
// An error message generated during the user directory search
searchError: null,
// Whether the server supports the user_directory API
serverSupportsUserDirectory: true,
// The query being searched for
query: "",
// List of AddressTile.InviteAddressType objects representing
// the set of auto-completion results for the current search
// query.
queryList: [],
};
@ -77,20 +80,6 @@ module.exports = React.createClass({
// Set the cursor at the end of the text input
this.refs.textinput.value = this.props.value;
}
// Create a Fuse instance for fuzzy searching this._userList
this._fuse = new Fuse(
// Use an empty list at first that will later be populated
// (see this._updateUserList)
[],
{
shouldSort: true,
location: 0, // The index of the query in the test string
distance: 5, // The distance away from location the query can be
// 0.0 = exact match, 1.0 = match anything
threshold: 0.3,
}
);
this._updateUserList();
},
onButtonClick: function() {
@ -112,17 +101,28 @@ module.exports = React.createClass({
// A Direct Message room already exists for this user, so select a
// room from a list that is similar to the one in MemberInfo panel
const ChatCreateOrReuseDialog = sdk.getComponent(
"views.dialogs.ChatCreateOrReuseDialog"
"views.dialogs.ChatCreateOrReuseDialog",
);
Modal.createDialog(ChatCreateOrReuseDialog, {
const close = Modal.createDialog(ChatCreateOrReuseDialog, {
userId: userId,
onFinished: (success) => {
if (success) {
this.props.onFinished(true, inviteList[0]);
}
// else show this ChatInviteDialog again
}
});
this.props.onFinished(success);
},
onNewDMClick: () => {
dis.dispatch({
action: 'start_chat',
user_id: userId,
});
close(true);
},
onExistingRoomSelected: (roomId) => {
dis.dispatch({
action: 'view_room',
room_id: roomId,
});
close(true);
},
}).close;
} else {
this._startChat(inviteList);
}
@ -148,15 +148,15 @@ module.exports = React.createClass({
} else if (e.keyCode === 38) { // up arrow
e.stopPropagation();
e.preventDefault();
this.addressSelector.moveSelectionUp();
if (this.addressSelector) this.addressSelector.moveSelectionUp();
} else if (e.keyCode === 40) { // down arrow
e.stopPropagation();
e.preventDefault();
this.addressSelector.moveSelectionDown();
if (this.addressSelector) 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.chooseSelection();
if (this.addressSelector) this.addressSelector.chooseSelection();
} else if (this.refs.textinput.value.length === 0 && this.state.inviteList.length && e.keyCode === 8) { // backspace
e.stopPropagation();
e.preventDefault();
@ -179,71 +179,36 @@ module.exports = React.createClass({
onQueryChanged: function(ev) {
const query = ev.target.value;
let queryList = [];
if (query.length < 2) {
return;
}
if (this.queryChangedDebouncer) {
clearTimeout(this.queryChangedDebouncer);
}
this.queryChangedDebouncer = setTimeout(() => {
// Only do search if there is something to search
if (query.length > 0 && query != '@') {
// Weighted keys prefer to match userIds when first char is @
this._fuse.options.keys = [{
name: 'displayName',
weight: query[0] === '@' ? 0.1 : 0.9,
},{
name: 'userId',
weight: query[0] === '@' ? 0.9 : 0.1,
}];
queryList = this._fuse.search(query).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 is a valid address, add an entry for that
// This is important, otherwise there's no way to invite
// a perfectly valid address if there are close matches.
const addrType = getAddressType(query);
if (addrType !== null) {
queryList.unshift({
addressType: addrType,
address: query,
isKnown: false,
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
if (addrType == 'email') {
this._lookupThreepid(addrType, query).done();
}
// Only do search if there is something to search
if (query.length > 0 && query != '@' && query.length >= 2) {
this.queryChangedDebouncer = setTimeout(() => {
if (this.state.serverSupportsUserDirectory) {
this._doUserDirectorySearch(query);
} else {
this._doLocalSearch(query);
}
}
}, QUERY_USER_DIRECTORY_DEBOUNCE_MS);
} else {
this.setState({
queryList: queryList,
error: false,
}, () => {
this.addressSelector.moveSelectionTop();
queryList: [],
query: "",
searchError: null,
});
}, 200);
}
},
onDismissed: function(index) {
var self = this;
return function() {
return () => {
var inviteList = self.state.inviteList.slice();
inviteList.splice(index, 1);
self.setState({
inviteList: inviteList,
queryList: [],
query: "",
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
};
@ -262,10 +227,109 @@ module.exports = React.createClass({
this.setState({
inviteList: inviteList,
queryList: [],
query: "",
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
},
_doUserDirectorySearch: function(query) {
this.setState({
busy: true,
query,
searchError: null,
});
MatrixClientPeg.get().searchUserDirectory({
term: query,
}).then((resp) => {
// The query might have changed since we sent the request, so ignore
// responses for anything other than the latest query.
if (this.state.query !== query) {
return;
}
this._processResults(resp.results, query);
}).catch((err) => {
console.error('Error whilst searching user directory: ', err);
this.setState({
searchError: err.errcode ? err.message : _t('Something went wrong!'),
});
if (err.errcode === 'M_UNRECOGNIZED') {
this.setState({
serverSupportsUserDirectory: false,
});
// Do a local search immediately
this._doLocalSearch(query);
}
}).done(() => {
this.setState({
busy: false,
});
});
},
_doLocalSearch: function(query) {
this.setState({
query,
searchError: null,
});
const queryLowercase = query.toLowerCase();
const results = [];
MatrixClientPeg.get().getUsers().forEach((user) => {
if (user.userId.toLowerCase().indexOf(queryLowercase) === -1 &&
user.displayName.toLowerCase().indexOf(queryLowercase) === -1
) {
return;
}
// Put results in the format of the new API
results.push({
user_id: user.userId,
display_name: user.displayName,
avatar_url: user.avatarUrl,
});
});
this._processResults(results, query);
},
_processResults: function(results, query) {
const queryList = [];
results.forEach((user) => {
if (user.user_id === MatrixClientPeg.get().credentials.userId) {
return;
}
// Return objects, structure of which is defined
// by InviteAddressType
queryList.push({
addressType: 'mx',
address: user.user_id,
displayName: user.display_name,
avatarMxc: user.avatar_url,
isKnown: true,
});
});
// If the query is a valid address, add an entry for that
// This is important, otherwise there's no way to invite
// a perfectly valid address if there are close matches.
const addrType = getAddressType(query);
if (addrType !== null) {
queryList.unshift({
addressType: addrType,
address: query,
isKnown: false,
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
if (addrType == 'email') {
this._lookupThreepid(addrType, query).done();
}
}
this.setState({
queryList,
error: false,
}, () => {
if (this.addressSelector) this.addressSelector.moveSelectionTop();
});
},
_getDirectMessageRooms: function(addr) {
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
const dmRooms = dmRoomMap.getDMRoomsForUserId(addr);
@ -284,11 +348,7 @@ module.exports = React.createClass({
_startChat: function(addrs) {
if (MatrixClientPeg.get().isGuest()) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "Guest users can't invite users. Please register."
});
dis.dispatch({action: 'view_set_mxid'});
return;
}
@ -308,8 +368,8 @@ module.exports = React.createClass({
console.error(err.stack);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Error",
description: "Failed to invite",
title: _t("Failed to invite"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
return null;
})
@ -321,8 +381,8 @@ module.exports = React.createClass({
console.error(err.stack);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Error",
description: "Failed to invite user",
title: _t("Failed to invite user"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
return null;
})
@ -342,8 +402,8 @@ module.exports = React.createClass({
console.error(err.stack);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Error",
description: "Failed to invite",
title: _t("Failed to invite"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
return null;
})
@ -354,18 +414,6 @@ module.exports = React.createClass({
this.props.onFinished(true, addrTexts);
},
_updateUserList: new rate_limited_func(function() {
// Get all the users
this._userList = MatrixClientPeg.get().getUsers();
// Remove current user
const meIx = this._userList.findIndex((u) => {
return u.userId === MatrixClientPeg.get().credentials.userId;
});
this._userList.splice(meIx, 1);
this._fuse.set(this._userList);
}, 500),
_isOnInviteList: function(uid) {
for (let i = 0; i < this.state.inviteList.length; i++) {
if (
@ -401,7 +449,7 @@ module.exports = React.createClass({
if (errorList.length > 0) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Failed to invite the following users to the " + room.name + " room:",
title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}),
description: errorList.join(", "),
});
}
@ -433,6 +481,7 @@ module.exports = React.createClass({
this.setState({
inviteList: inviteList,
queryList: [],
query: "",
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
return inviteList;
@ -449,7 +498,7 @@ module.exports = React.createClass({
}
// wait a bit to let the user finish typing
return q.delay(500).then(() => {
return Promise.delay(500).then(() => {
if (cancelled) return null;
return MatrixClientPeg.get().lookupThreePid(medium, address);
}).then((res) => {
@ -468,7 +517,7 @@ module.exports = React.createClass({
displayName: res.displayname,
avatarMxc: res.avatar_url,
isKnown: true,
}]
}],
});
});
},
@ -500,23 +549,27 @@ module.exports = React.createClass({
placeholder={this.props.placeholder}
defaultValue={this.props.value}
autoFocus={this.props.focus}>
</textarea>
</textarea>,
);
var error;
var addressSelector;
let error;
let 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">{_t("You have entered an invalid contact. Try using their Matrix ID or email address.")}</div>;
} else if (this.state.searchError) {
error = <div className="mx_ChatInviteDialog_error">{this.state.searchError}</div>;
} else if (
this.state.query.length > 0 &&
this.state.queryList.length === 0 &&
!this.state.busy
) {
error = <div className="mx_ChatInviteDialog_error">{_t("No results")}</div>;
} else {
const addressSelectorHeader = <div className="mx_ChatInviteDialog_addressSelectHeader">
Searching known users
</div>;
addressSelector = (
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
addressList={ this.state.queryList }
onSelected={ this.onSelected }
truncateAt={ TRUNCATE_QUERY_LIST }
header={ addressSelectorHeader }
/>
);
}

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
import classnames from 'classnames';
import { _t } from '../../../languageHandler';
/*
* A dialog for confirming a redaction.
@ -42,7 +43,7 @@ export default React.createClass({
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const title = "Confirm Redaction";
const title = _t("Confirm Removal");
const confirmButtonClass = classnames({
'mx_Dialog_primary': true,
@ -55,16 +56,16 @@ export default React.createClass({
title={title}
>
<div className="mx_Dialog_content">
Are you sure you wish to redact (delete) this event?
Note that if you redact a room name or topic change, it could undo the change.
{_t("Are you sure you wish to remove (delete) this event? " +
"Note that if you delete a room name or topic change, it could undo the change.")}
</div>
<div className="mx_Dialog_buttons">
<button className={confirmButtonClass} onClick={this.onOk}>
Redact
{_t("Remove")}
</button>
<button onClick={this.onCancel}>
Cancel
{_t("Cancel")}
</button>
</div>
</BaseDialog>

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import classnames from 'classnames';
/*
@ -69,7 +70,7 @@ export default React.createClass({
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
const title = this.props.action + " this person?";
const title = _t("%(actionVerb)s this person?", { actionVerb: this.props.action});
const confirmButtonClass = classnames({
'mx_Dialog_primary': true,
'danger': this.props.danger,
@ -82,7 +83,7 @@ export default React.createClass({
<form onSubmit={this.onOk}>
<input className="mx_ConfirmUserActionDialog_reasonField"
ref={this._collectReasonField}
placeholder="Reason"
placeholder={ _t("Reason") }
autoFocus={true}
/>
</form>
@ -111,7 +112,7 @@ export default React.createClass({
</button>
<button onClick={this.onCancel}>
Cancel
{ _t("Cancel") }
</button>
</div>
</BaseDialog>

View file

@ -0,0 +1,199 @@
/*
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 PropTypes from 'prop-types';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
// We match fairly liberally and leave it up to the server to reject if
// there are invalid characters etc.
const GROUP_REGEX = /^\+(.*?):(.*)$/;
export default React.createClass({
displayName: 'CreateGroupDialog',
propTypes: {
onFinished: PropTypes.func.isRequired,
},
getInitialState: function() {
return {
groupName: '',
groupId: '',
groupError: null,
creating: false,
createError: null,
};
},
_onGroupNameChange: function(e) {
this.setState({
groupName: e.target.value,
});
},
_onGroupIdChange: function(e) {
this.setState({
groupId: e.target.value,
});
},
_onGroupIdBlur: function(e) {
this._checkGroupId();
},
_checkGroupId: function(e) {
const parsedGroupId = this._parseGroupId(this.state.groupId);
let error = null;
if (parsedGroupId === null) {
error = _t(
"Group IDs must be of the form +localpart:%(domain)s",
{domain: MatrixClientPeg.get().getDomain()},
);
} else {
const domain = parsedGroupId[1];
if (domain !== MatrixClientPeg.get().getDomain()) {
error = _t(
"It is currently only possible to create groups on your own home server: "+
"use a group ID ending with %(domain)s",
{domain: MatrixClientPeg.get().getDomain()},
);
}
}
this.setState({
groupIdError: error,
});
return error;
},
_onFormSubmit: function(e) {
e.preventDefault();
if (this._checkGroupId()) return;
const parsedGroupId = this._parseGroupId(this.state.groupId);
const profile = {};
if (this.state.groupName !== '') {
profile.name = this.state.groupName;
}
this.setState({creating: true});
MatrixClientPeg.get().createGroup({
localpart: parsedGroupId[0],
profile: profile,
}).then((result) => {
dis.dispatch({
action: 'view_group',
group_id: result.group_id,
});
this.props.onFinished(true);
}).catch((e) => {
this.setState({createError: e});
}).finally(() => {
this.setState({creating: false});
}).done();
},
_onCancel: function() {
this.props.onFinished(false);
},
/**
* Parse a string that may be a group ID
* If the string is a valid group ID, return a list of [localpart, domain],
* otherwise return null.
*
* @param {string} groupId The ID of the group
* @return {string[]} array of localpart, domain
*/
_parseGroupId: function(groupId) {
const matches = GROUP_REGEX.exec(this.state.groupId);
if (!matches || matches.length < 3) {
return null;
}
return [matches[1], matches[2]];
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent('elements.Spinner');
if (this.state.creating) {
return <Spinner />;
}
let createErrorNode;
if (this.state.createError) {
// XXX: We should catch errcodes and give sensible i18ned messages for them,
// rather than displaying what the server gives us, but synapse doesn't give
// any yet.
createErrorNode = <div className="error">
<div>{_t('Room creation failed')}</div>
<div>{this.state.createError.message}</div>
</div>;
}
return (
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
onEnterPressed={this._onFormSubmit}
title={_t('Create Group')}
>
<form onSubmit={this._onFormSubmit}>
<div className="mx_Dialog_content">
<div className="mx_CreateGroupDialog_inputRow">
<div className="mx_CreateGroupDialog_label">
<label htmlFor="groupname">{_t('Group Name')}</label>
</div>
<div>
<input id="groupname" className="mx_CreateGroupDialog_input"
autoFocus={true} size="64"
placeholder={_t('Example')}
onChange={this._onGroupNameChange}
value={this.state.groupName}
/>
</div>
</div>
<div className="mx_CreateGroupDialog_inputRow">
<div className="mx_CreateGroupDialog_label">
<label htmlFor="groupid">{_t('Group ID')}</label>
</div>
<div>
<input id="groupid" className="mx_CreateGroupDialog_input"
size="64"
placeholder={_t('+example:%(domain)s', {domain: MatrixClientPeg.get().getDomain()})}
onChange={this._onGroupIdChange}
onBlur={this._onGroupIdBlur}
value={this.state.groupId}
/>
</div>
</div>
<div className="error">
{this.state.groupIdError}
</div>
{createErrorNode}
</div>
<div className="mx_Dialog_buttons">
<button onClick={this._onCancel}>
{ _t("Cancel") }
</button>
<input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
</div>
</form>
</BaseDialog>
);
},
});

View file

@ -20,6 +20,7 @@ import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import * as Lifecycle from '../../../Lifecycle';
import Velocity from 'velocity-vector';
import { _t } from '../../../languageHandler';
export default class DeactivateAccountDialog extends React.Component {
constructor(props, context) {
@ -56,10 +57,10 @@ export default class DeactivateAccountDialog extends React.Component {
Lifecycle.onLoggedOut();
this.props.onFinished(false);
}, (err) => {
let errStr = 'Unknown error';
let errStr = _t('Unknown error');
// https://matrix.org/jira/browse/SYN-744
if (err.httpStatus == 401 || err.httpStatus == 403) {
errStr = 'Incorrect password';
errStr = _t('Incorrect password');
Velocity(this._passwordField, "callout.shake", 300);
}
this.setState({
@ -85,29 +86,29 @@ export default class DeactivateAccountDialog extends React.Component {
passwordBoxClass = 'error';
}
const okLabel = this.state.busy ? <Loader /> : 'Deactivate Account';
const okLabel = this.state.busy ? <Loader /> : _t('Deactivate Account');
const okEnabled = this.state.confirmButtonEnabled && !this.state.busy;
let cancelButton = null;
if (!this.state.busy) {
cancelButton = <button onClick={this._onCancel} autoFocus={true}>
Cancel
{_t("Cancel")}
</button>;
}
return (
<div className="mx_DeactivateAccountDialog">
<div className="mx_Dialog_title danger">
Deactivate Account
{_t("Deactivate Account")}
</div>
<div className="mx_Dialog_content">
<p>This will make your account permanently unusable. You will not be able to re-register the same user ID.</p>
<p>{_t("This will make your account permanently unusable. You will not be able to re-register the same user ID.")}</p>
<p>This action is irreversible.</p>
<p>{_t("This action is irreversible.")}</p>
<p>To continue, please enter your password.</p>
<p>{_t("To continue, please enter your password.")}</p>
<p>Password:</p>
<p>{_t("Password")}:</p>
<input
type="password"
onChange={this._onPasswordFieldChange}

View file

@ -0,0 +1,77 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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 MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import { _t } from '../../../languageHandler';
export default function DeviceVerifyDialog(props) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const key = FormattingUtils.formatCryptoKey(props.device.getFingerprint());
const body = (
<div>
<p>
{_t("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>{_t("Device name")}:</label> <span>{ props.device.getDisplayName() }</span></li>
<li><label>{_t("Device ID")}:</label> <span><code>{ props.device.deviceId}</code></span></li>
<li><label>{_t("Device key")}:</label> <span><code><b>{ key }</b></code></span></li>
</ul>
</div>
<p>
{_t("If it matches, press the verify button below. " +
"If it doesn't, then someone else is intercepting this device " +
"and you probably want to press the blacklist button instead.")}
</p>
<p>
{_t("In future this verification process will be more sophisticated.")}
</p>
</div>
);
function onFinished(confirm) {
if (confirm) {
MatrixClientPeg.get().setDeviceVerified(
props.userId, props.device.deviceId, true,
);
}
props.onFinished(confirm);
}
return (
<QuestionDialog
title={_t("Verify device")}
description={body}
button={_t("I verify that the keys match")}
onFinished={onFinished}
/>
);
}
DeviceVerifyDialog.propTypes = {
userId: React.PropTypes.string.isRequired,
device: React.PropTypes.object.isRequired,
onFinished: React.PropTypes.func.isRequired,
};

View file

@ -27,6 +27,7 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default React.createClass({
displayName: 'ErrorDialog',
@ -43,24 +44,30 @@ export default React.createClass({
getDefaultProps: function() {
return {
title: "Error",
description: "An error has occurred.",
button: "OK",
focus: true,
title: null,
description: null,
button: null,
};
},
componentDidMount: function() {
if (this.props.focus) {
this.refs.button.focus();
}
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
title={this.props.title}>
title={this.props.title || _t('Error')}>
<div className="mx_Dialog_content">
{this.props.description}
{this.props.description || _t('An error has occurred.')}
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={this.props.focus}>
{this.props.button}
<button ref="button" className="mx_Dialog_primary" onClick={this.props.onFinished}>
{this.props.button || _t('OK')}
</button>
</div>
</BaseDialog>

View file

@ -15,11 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Matrix from 'matrix-js-sdk';
import React from 'react';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
@ -46,12 +45,6 @@ export default React.createClass({
title: React.PropTypes.string,
},
getDefaultProps: function() {
return {
title: "Authentication",
};
},
getInitialState: function() {
return {
authError: null,
@ -85,7 +78,7 @@ export default React.createClass({
<AccessibleButton onClick={this._onDismissClick}
className="mx_UserSettings_button"
>
Dismiss
{_t("Dismiss")}
</AccessibleButton>
</div>
);
@ -105,7 +98,7 @@ export default React.createClass({
return (
<BaseDialog className="mx_InteractiveAuthDialog"
onFinished={this.props.onFinished}
title={this.state.authError ? 'Error' : this.props.title}
title={this.state.authError ? 'Error' : (this.props.title || _t('Authentication'))}
>
{content}
</BaseDialog>

View file

@ -0,0 +1,172 @@
/*
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 Modal from '../../../Modal';
import React from 'react';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
/**
* Dialog which asks the user whether they want to share their keys with
* an unverified device.
*
* onFinished is called with `true` if the key should be shared, `false` if it
* should not, and `undefined` if the dialog is cancelled. (In other words:
* truthy: do the key share. falsy: don't share the keys).
*/
export default React.createClass({
propTypes: {
matrixClient: React.PropTypes.object.isRequired,
userId: React.PropTypes.string.isRequired,
deviceId: React.PropTypes.string.isRequired,
onFinished: React.PropTypes.func.isRequired,
},
getInitialState: function() {
return {
deviceInfo: null,
wasNewDevice: false,
};
},
componentDidMount: function() {
this._unmounted = false;
const userId = this.props.userId;
const deviceId = this.props.deviceId;
// give the client a chance to refresh the device list
this.props.matrixClient.downloadKeys([userId], false).then((r) => {
if (this._unmounted) { return; }
const deviceInfo = r[userId][deviceId];
if(!deviceInfo) {
console.warn(`No details found for device ${userId}:${deviceId}`);
this.props.onFinished(false);
return;
}
const wasNewDevice = !deviceInfo.isKnown();
this.setState({
deviceInfo: deviceInfo,
wasNewDevice: wasNewDevice,
});
// if the device was new before, it's not any more.
if (wasNewDevice) {
this.props.matrixClient.setDeviceKnown(
userId,
deviceId,
true,
);
}
}).done();
},
componentWillUnmount: function() {
this._unmounted = true;
},
_onVerifyClicked: function() {
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
console.log("KeyShareDialog: Starting verify dialog");
Modal.createDialog(DeviceVerifyDialog, {
userId: this.props.userId,
device: this.state.deviceInfo,
onFinished: (verified) => {
if (verified) {
// can automatically share the keys now.
this.props.onFinished(true);
}
},
});
},
_onShareClicked: function() {
console.log("KeyShareDialog: User clicked 'share'");
this.props.onFinished(true);
},
_onIgnoreClicked: function() {
console.log("KeyShareDialog: User clicked 'ignore'");
this.props.onFinished(false);
},
_renderContent: function() {
const displayName = this.state.deviceInfo.getDisplayName() ||
this.state.deviceInfo.deviceId;
let text;
if (this.state.wasNewDevice) {
text = "You added a new device '%(displayName)s', which is"
+ " requesting encryption keys.";
} else {
text = "Your unverified device '%(displayName)s' is requesting"
+ " encryption keys.";
}
text = _t(text, {displayName: displayName});
return (
<div>
<p>{text}</p>
<div className="mx_Dialog_buttons">
<button onClick={this._onVerifyClicked}>
{_t('Start verification')}
</button>
<button onClick={this._onShareClicked}>
{_t('Share without verifying')}
</button>
<button onClick={this._onIgnoreClicked}>
{_t('Ignore request')}
</button>
</div>
</div>
);
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent('views.elements.Spinner');
let content;
if (this.state.deviceInfo) {
content = this._renderContent();
} else {
content = (
<div>
<p>{_t('Loading device info...')}</p>
<Spinner />
</div>
);
}
return (
<BaseDialog className='mx_KeyShareRequestDialog'
onFinished={this.props.onFinished}
title={_t('Encryption key request')}
>
{content}
</BaseDialog>
);
},
});

View file

@ -1,78 +0,0 @@
/*
Copyright 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.
*/
/*
* Usage:
* Modal.createDialog(NeedToRegisterDialog, {
* title: "some text", (default: "Registration required")
* description: "some more text",
* onFinished: someFunction,
* });
*/
import React from 'react';
import dis from '../../../dispatcher';
import sdk from '../../../index';
module.exports = React.createClass({
displayName: 'NeedToRegisterDialog',
propTypes: {
title: React.PropTypes.string,
description: React.PropTypes.oneOfType([
React.PropTypes.element,
React.PropTypes.string,
]),
onFinished: React.PropTypes.func.isRequired,
},
getDefaultProps: function() {
return {
title: "Registration required",
description: "A registered account is required for this action",
};
},
onRegisterClicked: function() {
dis.dispatch({
action: "start_upgrade_registration",
});
if (this.props.onFinished) {
this.props.onFinished();
}
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className="mx_NeedToRegisterDialog"
onFinished={this.props.onFinished}
title={this.props.title}
>
<div className="mx_Dialog_content">
{this.props.description}
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={true}>
Cancel
</button>
<button onClick={this.onRegisterClicked}>
Register
</button>
</div>
</BaseDialog>
);
},
});

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default React.createClass({
displayName: 'QuestionDialog',
@ -33,7 +34,6 @@ export default React.createClass({
title: "",
description: "",
extraButtons: null,
button: "OK",
focus: true,
hasCancelButton: true,
};
@ -51,7 +51,7 @@ export default React.createClass({
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const cancelButton = this.props.hasCancelButton ? (
<button onClick={this.onCancel}>
Cancel
{_t("Cancel")}
</button>
) : null;
return (
@ -64,7 +64,7 @@ export default React.createClass({
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}>
{this.props.button}
{this.props.button || _t('OK')}
</button>
{this.props.extraButtons}
{cancelButton}

View file

@ -18,6 +18,7 @@ import React from 'react';
import sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
import { _t, _tJsx } from '../../../languageHandler';
export default React.createClass({
@ -43,29 +44,32 @@ export default React.createClass({
if (SdkConfig.get().bug_report_endpoint_url) {
bugreport = (
<p>Otherwise, <a onClick={this._sendBugReport} href='#'>
click here</a> to send a bug report.
<p>
{_tJsx(
"Otherwise, <a>click here</a> to send a bug report.",
/<a>(.*?)<\/a>/, (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{sub}</a>,
)}
</p>
);
}
return (
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
title='Unable to restore session'>
title={_t('Unable to restore session')}>
<div className="mx_Dialog_content">
<p>We encountered an error trying to restore your previous session. If
you continue, you will need to log in again, and encrypted chat
history will be unreadable.</p>
<p>{_t("We encountered an error trying to restore your previous session. If " +
"you continue, you will need to log in again, and encrypted chat " +
"history will be unreadable.")}</p>
<p>If you have previously used a more recent version of Riot, your session
may be incompatible with this version. Close this window and return
to the more recent version.</p>
<p>{_t("If you have previously used a more recent version of Riot, your session " +
"may be incompatible with this version. Close this window and return " +
"to the more recent version.")}</p>
{bugreport}
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this._continueClicked}>
Continue anyway
{_t("Continue anyway")}
</button>
</div>
</BaseDialog>

View file

@ -1,87 +0,0 @@
/*
Copyright 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.
*/
import React from 'react';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
/**
* 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,
currentDisplayName: React.PropTypes.string,
},
getInitialState: function() {
if (this.props.currentDisplayName) {
return { value: this.props.currentDisplayName };
}
if (MatrixClientPeg.get().isGuest()) {
return { value : "Guest " + MatrixClientPeg.get().getUserIdLocalpart() };
}
else {
return { value : MatrixClientPeg.get().getUserIdLocalpart() };
}
},
componentDidMount: function() {
this.refs.input_value.select();
},
onValueChange: function(ev) {
this.setState({
value: ev.target.value
});
},
onFormSubmit: function(ev) {
ev.preventDefault();
this.props.onFinished(true, this.state.value);
return false;
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<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?
</div>
<form onSubmit={this.onFormSubmit}>
<div className="mx_Dialog_content">
<input type="text" ref="input_value" value={this.state.value}
autoFocus={true} onChange={this.onValueChange} size="30"
className="mx_SetDisplayNameDialog_input"
/>
</div>
<div className="mx_Dialog_buttons">
<input className="mx_Dialog_primary" type="submit" value="Set" />
</div>
</form>
</BaseDialog>
);
},
});

View file

@ -0,0 +1,164 @@
/*
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 Email from '../../../email';
import AddThreepid from '../../../AddThreepid';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
/**
* Prompt the user to set an email address.
*
* On success, `onFinished(true)` is called.
*/
export default React.createClass({
displayName: 'SetEmailDialog',
propTypes: {
onFinished: React.PropTypes.func.isRequired,
},
getInitialState: function() {
return {
emailAddress: null,
emailBusy: false,
};
},
componentDidMount: function() {
},
onEmailAddressChanged: function(value) {
this.setState({
emailAddress: value,
});
},
onSubmit: function() {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const emailAddress = this.state.emailAddress;
if (!Email.looksValid(emailAddress)) {
Modal.createDialog(ErrorDialog, {
title: _t("Invalid Email Address"),
description: _t("This doesn't appear to be a valid email address"),
});
return;
}
this._addThreepid = new AddThreepid();
// we always bind emails when registering, so let's do the
// same here.
this._addThreepid.addEmailAddress(emailAddress, true).done(() => {
Modal.createDialog(QuestionDialog, {
title: _t("Verification Pending"),
description: _t(
"Please check your email and click on the link it contains. Once this " +
"is done, click continue.",
),
button: _t('Continue'),
onFinished: this.onEmailDialogFinished,
});
}, (err) => {
this.setState({emailBusy: false});
console.error("Unable to add email address " + emailAddress + " " + err);
Modal.createDialog(ErrorDialog, {
title: _t("Unable to add email address"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
});
this.setState({emailBusy: true});
},
onCancelled: function() {
this.props.onFinished(false);
},
onEmailDialogFinished: function(ok) {
if (ok) {
this.verifyEmailAddress();
} else {
this.setState({emailBusy: false});
}
},
verifyEmailAddress: function() {
this._addThreepid.checkEmailLinkClicked().done(() => {
this.props.onFinished(true);
}, (err) => {
this.setState({emailBusy: false});
if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const message = _t("Unable to verify email address.") + " " +
_t("Please check your email and click on the link it contains. Once this is done, click continue.");
Modal.createDialog(QuestionDialog, {
title: _t("Verification Pending"),
description: message,
button: _t('Continue'),
onFinished: this.onEmailDialogFinished,
});
} else {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to verify email address: " + err);
Modal.createDialog(ErrorDialog, {
title: _t("Unable to verify email address."),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
}
});
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent('elements.Spinner');
const EditableText = sdk.getComponent('elements.EditableText');
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
className="mx_SetEmailDialog_email_input"
placeholder={ _t("Email address") }
placeholderClassName="mx_SetEmailDialog_email_input_placeholder"
blurToCancel={ false }
onValueChanged={ this.onEmailAddressChanged } />;
return (
<BaseDialog className="mx_SetEmailDialog"
onFinished={this.onCancelled}
title={this.props.title}
>
<div className="mx_Dialog_content">
<p>
{ _t('This will allow you to reset your password and receive notifications.') }
</p>
{ emailInput }
</div>
<div className="mx_Dialog_buttons">
<input className="mx_Dialog_primary"
type="submit"
value={_t("Continue")}
onClick={this.onSubmit}
/>
<input
type="submit"
value={_t("Skip")}
onClick={this.onCancelled}
/>
</div>
</BaseDialog>
);
},
});

View file

@ -0,0 +1,294 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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 Promise from 'bluebird';
import React from 'react';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import classnames from 'classnames';
import KeyCode from '../../../KeyCode';
import { _t, _tJsx } from '../../../languageHandler';
// The amount of time to wait for further changes to the input username before
// sending a request to the server
const USERNAME_CHECK_DEBOUNCE_MS = 250;
/**
* Prompt the user to set a display name.
*
* On success, `onFinished(true, newDisplayName)` is called.
*/
export default React.createClass({
displayName: 'SetMxIdDialog',
propTypes: {
onFinished: React.PropTypes.func.isRequired,
// Called when the user requests to register with a different homeserver
onDifferentServerClicked: React.PropTypes.func.isRequired,
// Called if the user wants to switch to login instead
onLoginClick: React.PropTypes.func.isRequired,
},
getInitialState: function() {
return {
// The entered username
username: '',
// Indicate ongoing work on the username
usernameBusy: false,
// Indicate error with username
usernameError: '',
// Assume the homeserver supports username checking until "M_UNRECOGNIZED"
usernameCheckSupport: true,
// Whether the auth UI is currently being used
doingUIAuth: false,
// Indicate error with auth
authError: '',
};
},
componentDidMount: function() {
this.refs.input_value.select();
this._matrixClient = MatrixClientPeg.get();
},
onValueChange: function(ev) {
this.setState({
username: ev.target.value,
usernameBusy: true,
usernameError: '',
}, () => {
if (!this.state.username || !this.state.usernameCheckSupport) {
this.setState({
usernameBusy: false,
});
return;
}
// Debounce the username check to limit number of requests sent
if (this._usernameCheckTimeout) {
clearTimeout(this._usernameCheckTimeout);
}
this._usernameCheckTimeout = setTimeout(() => {
this._doUsernameCheck().finally(() => {
this.setState({
usernameBusy: false,
});
});
}, USERNAME_CHECK_DEBOUNCE_MS);
});
},
onKeyUp: function(ev) {
if (ev.keyCode === KeyCode.ENTER) {
this.onSubmit();
}
},
onSubmit: function(ev) {
this.setState({
doingUIAuth: true,
});
},
_doUsernameCheck: function() {
// Check if username is available
return this._matrixClient.isUsernameAvailable(this.state.username).then(
(isAvailable) => {
if (isAvailable) {
this.setState({usernameError: ''});
}
},
(err) => {
// Indicate whether the homeserver supports username checking
const newState = {
usernameCheckSupport: err.errcode !== "M_UNRECOGNIZED",
};
console.error('Error whilst checking username availability: ', err);
switch (err.errcode) {
case "M_USER_IN_USE":
newState.usernameError = _t('Username not available');
break;
case "M_INVALID_USERNAME":
newState.usernameError = _t(
'Username invalid: %(errMessage)s',
{ errMessage: err.message},
);
break;
case "M_UNRECOGNIZED":
// This homeserver doesn't support username checking, assume it's
// fine and rely on the error appearing in registration step.
newState.usernameError = '';
break;
case undefined:
newState.usernameError = _t('Something went wrong!');
break;
default:
newState.usernameError = _t(
'An error occurred: %(error_string)s',
{ error_string: err.message },
);
break;
}
this.setState(newState);
},
);
},
_generatePassword: function() {
return Math.random().toString(36).slice(2);
},
_makeRegisterRequest: function(auth) {
// Not upgrading - changing mxids
const guestAccessToken = null;
if (!this._generatedPassword) {
this._generatedPassword = this._generatePassword();
}
return this._matrixClient.register(
this.state.username,
this._generatedPassword,
undefined, // session id: included in the auth dict already
auth,
{},
guestAccessToken,
);
},
_onUIAuthFinished: function(success, response) {
this.setState({
doingUIAuth: false,
});
if (!success) {
this.setState({ authError: response.message });
return;
}
// XXX Implement RTS /register here
const teamToken = null;
this.props.onFinished(true, {
userId: response.user_id,
deviceId: response.device_id,
homeserverUrl: this._matrixClient.getHomeserverUrl(),
identityServerUrl: this._matrixClient.getIdentityServerUrl(),
accessToken: response.access_token,
password: this._generatedPassword,
teamToken: teamToken,
});
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
const Spinner = sdk.getComponent('elements.Spinner');
let auth;
if (this.state.doingUIAuth) {
auth = <InteractiveAuth
matrixClient={this._matrixClient}
makeRequest={this._makeRegisterRequest}
onAuthFinished={this._onUIAuthFinished}
inputs={{}}
poll={true}
/>;
}
const inputClasses = classnames({
"mx_SetMxIdDialog_input": true,
"error": Boolean(this.state.usernameError),
});
let usernameIndicator = null;
let usernameBusyIndicator = null;
if (this.state.usernameBusy) {
usernameBusyIndicator = <Spinner w="24" h="24"/>;
} else {
const usernameAvailable = this.state.username &&
this.state.usernameCheckSupport && !this.state.usernameError;
const usernameIndicatorClasses = classnames({
"error": Boolean(this.state.usernameError),
"success": usernameAvailable,
});
usernameIndicator = <div className={usernameIndicatorClasses}>
{ usernameAvailable ? _t('Username available') : this.state.usernameError }
</div>;
}
let authErrorIndicator = null;
if (this.state.authError) {
authErrorIndicator = <div className="error">
{ this.state.authError }
</div>;
}
const canContinue = this.state.username &&
!this.state.usernameError &&
!this.state.usernameBusy;
return (
<BaseDialog className="mx_SetMxIdDialog"
onFinished={this.props.onFinished}
title="To get started, please pick a username!"
>
<div className="mx_Dialog_content">
<div className="mx_SetMxIdDialog_input_group">
<input type="text" ref="input_value" value={this.state.username}
autoFocus={true}
onChange={this.onValueChange}
onKeyUp={this.onKeyUp}
size="30"
className={inputClasses}
/>
{ usernameBusyIndicator }
</div>
{ usernameIndicator }
<p>
{ _tJsx(
'This will be your account name on the <span></span> ' +
'homeserver, or you can pick a <a>different server</a>.',
[
/<span><\/span>/,
/<a>(.*?)<\/a>/,
],
[
(sub) => <span>{this.props.homeserverUrl}</span>,
(sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{sub}</a>,
],
)}
</p>
<p>
{ _tJsx(
'If you already have a Matrix account you can <a>log in</a> instead.',
/<a>(.*?)<\/a>/,
[(sub) => <a href="#" onClick={this.props.onLoginClick}>{sub}</a>],
)}
</p>
{ auth }
{ authErrorIndicator }
</div>
<div className="mx_Dialog_buttons">
<input className="mx_Dialog_primary"
type="submit"
value={_t("Continue")}
onClick={this.onSubmit}
disabled={!canContinue}
/>
</div>
</BaseDialog>
);
},
});

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default React.createClass({
displayName: 'TextInputDialog',
@ -36,7 +37,6 @@ export default React.createClass({
title: "",
value: "",
description: "",
button: "OK",
focus: true,
};
},
@ -73,7 +73,7 @@ export default React.createClass({
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onCancel}>
Cancel
{ _t("Cancel") }
</button>
<button className="mx_Dialog_primary" onClick={this.onOk}>
{this.props.button}

View file

@ -16,10 +16,10 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import MatrixClientPeg from '../../../MatrixClientPeg';
import GeminiScrollbar from 'react-gemini-scrollbar';
import Resend from '../../../Resend';
import { _t } from '../../../languageHandler';
function DeviceListEntry(props) {
const {userId, device} = props;
@ -120,17 +120,17 @@ export default React.createClass({
if (blacklistUnverified) {
warning = (
<h4>
You are currently blacklisting unverified devices; to send
messages to these devices you must verify them.
{_t("You are currently blacklisting unverified devices; to send " +
"messages to these devices you must verify them.")}
</h4>
);
} else {
warning = (
<div>
<p>
We recommend you go through the verification process
for each device to confirm they belong to their legitimate owner,
but you can resend the message without verifying if you prefer.
{_t("We recommend you go through the verification process " +
"for each device to confirm they belong to their legitimate owner, " +
"but you can resend the message without verifying if you prefer.")}
</p>
</div>
);
@ -145,14 +145,14 @@ export default React.createClass({
console.log("UnknownDeviceDialog closed by escape");
this.props.onFinished();
}}
title='Room contains unknown devices'
title={_t('Room contains unknown devices')}
>
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
<h4>
This room contains devices that you haven't seen before.
{_t('"%(RoomName)s" contains devices that you haven\'t seen before.', {RoomName: this.props.room.name})}
</h4>
{ warning }
Unknown devices:
{_t("Unknown devices")}:
<UnknownDeviceList devices={this.props.devices} />
</GeminiScrollbar>
@ -162,7 +162,7 @@ export default React.createClass({
this.props.onFinished();
Resend.resendUnsentEvents(this.props.room);
}}>
Send anyway
{_t("Send anyway")}
</button>
<button className="mx_Dialog_primary" autoFocus={ true }
onClick={() => {

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

@ -0,0 +1,84 @@
/*
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 PropTypes from 'prop-types';
import AccessibleButton from './AccessibleButton';
import dis from '../../../dispatcher';
import sdk from '../../../index';
export default React.createClass({
displayName: 'RoleButton',
propTypes: {
size: PropTypes.string,
tooltip: PropTypes.bool,
action: PropTypes.string.isRequired,
mouseOverAction: PropTypes.string,
label: PropTypes.string.isRequired,
iconPath: PropTypes.string.isRequired,
},
getDefaultProps: function() {
return {
size: "25",
tooltip: false,
};
},
getInitialState: function() {
return {
showTooltip: false,
};
},
_onClick: function(ev) {
ev.stopPropagation();
dis.dispatch({action: this.props.action});
},
_onMouseEnter: function() {
if (this.props.tooltip) this.setState({showTooltip: true});
if (this.props.mouseOverAction) {
dis.dispatch({action: this.props.mouseOverAction});
}
},
_onMouseLeave: function() {
this.setState({showTooltip: false});
},
render: function() {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
let tooltip;
if (this.state.showTooltip) {
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
tooltip = <RoomTooltip className="mx_RoleButton_tooltip" label={this.props.label} />;
}
return (
<AccessibleButton className="mx_RoleButton"
onClick={this._onClick}
onMouseEnter={this._onMouseEnter}
onMouseLeave={this._onMouseLeave}
>
<TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />
{tooltip}
</AccessibleButton>
);
}
});

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,11 @@ 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 MatrixClientPeg from "../../../MatrixClientPeg";
import { _t } from '../../../languageHandler';
// React PropType definition for an object describing
// an address that can be invited to a room (which
@ -142,7 +141,7 @@ export default React.createClass({
});
info = (
<div className={unknownClasses}>Unknown Address</div>
<div className={unknownClasses}>{_t("Unknown Address")}</div>
);
}

View file

@ -0,0 +1,74 @@
import React from 'react';
import PropTypes from 'prop-types';
import url from 'url';
import { _t } from '../../../languageHandler';
export default class AppPermission extends React.Component {
constructor(props) {
super(props);
const curlBase = this.getCurlBase();
this.state = { curlBase: curlBase};
}
// Return string representation of content URL without query parameters
getCurlBase() {
const wurl = url.parse(this.props.url);
let curl;
let curlString;
const searchParams = new URLSearchParams(wurl.search);
if(this.isScalarWurl(wurl) && searchParams && searchParams.get('url')) {
curl = url.parse(searchParams.get('url'));
if(curl) {
curl.search = curl.query = "";
curlString = curl.format();
}
}
if (!curl && wurl) {
wurl.search = wurl.query = "";
curlString = wurl.format();
}
return curlString;
}
isScalarWurl(wurl) {
if(wurl && wurl.hostname && (
wurl.hostname === 'scalar.vector.im' ||
wurl.hostname === 'scalar-staging.riot.im' ||
wurl.hostname === 'demo.riot.im' ||
wurl.hostname === 'localhost'
)) {
return true;
}
return false;
}
render() {
return (
<div className='mx_AppPermissionWarning'>
<div className='mx_AppPermissionWarningImage'>
<img src='img/warning.svg' alt={_t('Warning!')}/>
</div>
<div className='mx_AppPermissionWarningText'>
<span className='mx_AppPermissionWarningTextLabel'>Do you want to load widget from URL:</span> <span className='mx_AppPermissionWarningTextURL'>{this.state.curlBase}</span>
</div>
<input
className='mx_AppPermissionButton'
type='button'
value={_t('Allow')}
onClick={this.props.onPermissionGranted}
/>
</div>
);
}
}
AppPermission.propTypes = {
url: PropTypes.string.isRequired,
onPermissionGranted: PropTypes.func.isRequired,
};
AppPermission.defaultProps = {
onPermissionGranted: function() {},
};

View file

@ -0,0 +1,261 @@
/*
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.
*/
'use strict';
import url from 'url';
import React from 'react';
import MatrixClientPeg from '../../../MatrixClientPeg';
import ScalarAuthClient from '../../../ScalarAuthClient';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import AppPermission from './AppPermission';
import MessageSpinner from './MessageSpinner';
import WidgetUtils from '../../../WidgetUtils';
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
const betaHelpMsg = 'This feature is currently experimental and is intended for beta testing only';
export default React.createClass({
displayName: 'AppTile',
propTypes: {
id: React.PropTypes.string.isRequired,
url: React.PropTypes.string.isRequired,
name: React.PropTypes.string.isRequired,
room: React.PropTypes.object.isRequired,
type: React.PropTypes.string.isRequired,
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
fullWidth: React.PropTypes.bool,
},
getDefaultProps: function() {
return {
url: "",
};
},
getInitialState: function() {
const widgetPermissionId = [this.props.room.roomId, encodeURIComponent(this.props.url)].join('_');
const hasPermissionToLoad = localStorage.getItem(widgetPermissionId);
return {
loading: false,
widgetUrl: this.props.url,
widgetPermissionId: widgetPermissionId,
hasPermissionToLoad: Boolean(hasPermissionToLoad === 'true'),
error: null,
deleting: false,
};
},
// Returns true if props.url is a scalar URL, typically https://scalar.vector.im/api
isScalarUrl: function() {
const scalarUrl = SdkConfig.get().integrations_rest_url;
return scalarUrl && this.props.url.startsWith(scalarUrl);
},
componentWillMount: function() {
if (!this.isScalarUrl()) {
return;
}
// Fetch the token before loading the iframe as we need to mangle the URL
this.setState({
loading: true,
});
this._scalarClient = new ScalarAuthClient();
this._scalarClient.getScalarToken().done((token) => {
// Append scalar_token as a query param
this._scalarClient.scalarToken = token;
const u = url.parse(this.props.url);
if (!u.search) {
u.search = "?scalar_token=" + encodeURIComponent(token);
} else {
u.search += "&scalar_token=" + encodeURIComponent(token);
}
this.setState({
error: null,
widgetUrl: u.format(),
loading: false,
});
}, (err) => {
this.setState({
error: err.message,
loading: false,
});
});
},
_canUserModify: function() {
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
},
_onEditClick: function(e) {
console.log("Edit widget ID ", this.props.id);
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
const src = this._scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId, 'type_' + this.props.type);
Modal.createDialog(IntegrationsManager, {
src: src,
}, "mx_IntegrationsManager");
},
/* If user has permission to modify widgets, delete the widget, otherwise revoke access for the widget to load in the user's browser
*/
_onDeleteClick: function() {
if (this._canUserModify()) {
console.log("Delete widget %s", this.props.id);
this.setState({deleting: true});
MatrixClientPeg.get().sendStateEvent(
this.props.room.roomId,
'im.vector.modular.widgets',
{}, // empty content
this.props.id,
).then(() => {
console.log('Deleted widget');
}, (e) => {
console.error('Failed to delete widget', e);
this.setState({deleting: false});
});
} else {
console.log("Revoke widget permissions - %s", this.props.id);
this._revokeWidgetPermission();
}
},
// Widget labels to render, depending upon user permissions
// These strings are translated at the point that they are inserted in to the DOM, in the render method
_deleteWidgetLabel() {
if (this._canUserModify()) {
return 'Delete widget';
}
return 'Revoke widget access';
},
/* TODO -- Store permission in account data so that it is persisted across multiple devices */
_grantWidgetPermission() {
console.warn('Granting permission to load widget - ', this.state.widgetUrl);
localStorage.setItem(this.state.widgetPermissionId, true);
this.setState({hasPermissionToLoad: true});
},
_revokeWidgetPermission() {
console.warn('Revoking permission to load widget - ', this.state.widgetUrl);
localStorage.removeItem(this.state.widgetPermissionId);
this.setState({hasPermissionToLoad: false});
},
formatAppTileName: function() {
let appTileName = "No name";
if(this.props.name && this.props.name.trim()) {
appTileName = this.props.name.trim();
appTileName = appTileName[0].toUpperCase() + appTileName.slice(1).toLowerCase();
}
return appTileName;
},
render: function() {
let appTileBody;
// Don't render widget if it is in the process of being deleted
if (this.state.deleting) {
return <div></div>;
}
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
// because that would allow the iframe to prgramatically remove the sandbox attribute, but
// this would only be for content hosted on the same origin as the riot client: anything
// hosted on the same origin as the client will get the same access as if you clicked
// a link to it.
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
"allow-same-origin allow-scripts allow-presentation";
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
let safeWidgetUrl = '';
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
safeWidgetUrl = url.format(parsedWidgetUrl);
}
if (this.state.loading) {
appTileBody = (
<div className='mx_AppTileBody mx_AppLoading'>
<MessageSpinner msg='Loading...'/>
</div>
);
} else if (this.state.hasPermissionToLoad == true) {
appTileBody = (
<div className="mx_AppTileBody">
<iframe
ref="appFrame"
src={safeWidgetUrl}
allowFullScreen="true"
sandbox={sandboxFlags}
></iframe>
</div>
);
} else {
appTileBody = (
<div className="mx_AppTileBody">
<AppPermission
url={this.state.widgetUrl}
onPermissionGranted={this._grantWidgetPermission}
/>
</div>
);
}
// editing is done in scalar
const showEditButton = Boolean(this._scalarClient && this._canUserModify());
const deleteWidgetLabel = this._deleteWidgetLabel();
let deleteIcon = 'img/cancel.svg';
let deleteClasses = 'mx_filterFlipColor mx_AppTileMenuBarWidget';
if(this._canUserModify()) {
deleteIcon = 'img/cancel-red.svg';
deleteClasses += ' mx_AppTileMenuBarWidgetDelete';
}
return (
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
<div className="mx_AppTileMenuBar">
{this.formatAppTileName()}
<span className="mx_AppTileMenuBarWidgets">
<span className="mx_Beta" alt={betaHelpMsg} title={betaHelpMsg}>&#946;</span>
{/* Edit widget */}
{showEditButton && <img
src="img/edit.svg"
className="mx_filterFlipColor mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
width="8" height="8"
alt={_t('Edit')}
title={_t('Edit')}
onClick={this._onEditClick}
/>}
{/* Delete widget */}
<img src={deleteIcon}
className={deleteClasses}
width="8" height="8"
alt={_t(deleteWidgetLabel)}
title={_t(deleteWidgetLabel)}
onClick={this._onDeleteClick}
/>
</span>
</div>
{appTileBody}
</div>
);
},
});

View file

@ -0,0 +1,40 @@
/*
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 PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const CreateRoomButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton');
return (
<ActionButton action="view_create_room"
mouseOverAction={props.callout ? "callout_create_room" : null}
label={ _t("Create new room") }
iconPath="img/icons-create-room.svg"
size={props.size}
tooltip={props.tooltip}
/>
);
};
CreateRoomButton.propTypes = {
size: PropTypes.string,
tooltip: PropTypes.bool,
};
export default CreateRoomButton;

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

@ -16,7 +16,7 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
import q from 'q';
import Promise from 'bluebird';
/**
* A component which wraps an EditableText, with a spinner while updates take
@ -148,5 +148,5 @@ EditableTextContainer.defaultProps = {
initialValue: "",
placeholder: "",
blurToSubmit: false,
onSubmit: function(v) {return q(); },
onSubmit: function(v) {return Promise.resolve(); },
};

View file

@ -0,0 +1,39 @@
/*
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 PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const HomeButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton');
return (
<ActionButton action="view_home_page"
label={ _t("Home") }
iconPath="img/icons-home.svg"
size={props.size}
tooltip={props.tooltip}
/>
);
};
HomeButton.propTypes = {
size: PropTypes.string,
tooltip: PropTypes.bool,
};
export default HomeButton;

View file

@ -0,0 +1,120 @@
/*
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 * 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

@ -14,7 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import sdk from '../../../index';
const MemberAvatar = require('../avatars/MemberAvatar.js');
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'MemberEventListSummary',
@ -110,9 +112,13 @@ module.exports = React.createClass({
return null;
}
const EmojiText = sdk.getComponent('elements.EmojiText');
return (
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
{summaries.join(", ")}
<EmojiText>
{summaries.join(", ")}
</EmojiText>
</span>
);
},
@ -203,28 +209,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",
};
if (Object.keys(map).includes(t)) {
res = map[t] + (repeats > 1 ? " " + repeats + " times" : "" );
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: "" });
}
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;
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 +376,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 +392,7 @@ module.exports = React.createClass({
);
});
return (
<span className="mx_MemberEventListSummary_avatars">
<span className="mx_MemberEventListSummary_avatars" onClick={ this._toggleSummary }>
{avatars}
</span>
);
@ -289,7 +414,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 +492,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 +503,7 @@ module.exports = React.createClass({
if (fewEvents) {
return (
<div className="mx_MemberEventListSummary">
<div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
{expandedEvents}
</div>
);
@ -418,7 +561,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

@ -0,0 +1,34 @@
/*
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';
module.exports = React.createClass({
displayName: 'MessageSpinner',
render: function() {
const w = this.props.w || 32;
const h = this.props.h || 32;
const imgClass = this.props.imgClassName || "";
const msg = this.props.msg || "Loading...";
return (
<div className="mx_Spinner">
<div className="mx_Spinner_Msg">{msg}</div>&nbsp;
<img src="img/spinner.gif" width={w} height={h} className={imgClass}/>
</div>
);
},
});

View file

@ -0,0 +1,195 @@
/*
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 classNames from 'classnames';
import { Room, RoomMember } from 'matrix-js-sdk';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { MATRIXTO_URL_PATTERN } from '../../../linkify-matrix';
import { getDisplayAliasForRoom } from '../../../Rooms';
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
// For URLs of matrix.to links in the timeline which have been reformatted by
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room)\/(([\#\!\@\+])[^\/]*)$/;
const Pill = React.createClass({
statics: {
isPillUrl: (url) => {
return !!REGEX_MATRIXTO.exec(url);
},
isMessagePillUrl: (url) => {
return !!REGEX_LOCAL_MATRIXTO.exec(url);
},
TYPE_USER_MENTION: 'TYPE_USER_MENTION',
TYPE_ROOM_MENTION: 'TYPE_ROOM_MENTION',
},
props: {
// The URL to pillify (no validation is done, see isPillUrl and isMessagePillUrl)
url: PropTypes.string,
// Whether the pill is in a message
inMessage: PropTypes.bool,
// The room in which this pill is being rendered
room: PropTypes.instanceOf(Room),
},
getInitialState() {
return {
// ID/alias of the room/user
resourceId: null,
// Type of pill
pillType: null,
// The member related to the user pill
member: null,
// The room related to the room pill
room: null,
};
},
componentWillReceiveProps(nextProps) {
let regex = REGEX_MATRIXTO;
if (nextProps.inMessage) {
regex = REGEX_LOCAL_MATRIXTO;
}
// Default to the empty array if no match for simplicity
// resource and prefix will be undefined instead of throwing
const matrixToMatch = regex.exec(nextProps.url) || [];
const resourceId = matrixToMatch[1]; // The room/user ID
const prefix = matrixToMatch[2]; // The first character of prefix
const pillType = {
'@': Pill.TYPE_USER_MENTION,
'#': Pill.TYPE_ROOM_MENTION,
'!': Pill.TYPE_ROOM_MENTION,
}[prefix];
let member;
let room;
switch (pillType) {
case Pill.TYPE_USER_MENTION: {
const localMember = nextProps.room.getMember(resourceId);
member = localMember;
if (!localMember) {
member = new RoomMember(null, resourceId);
this.doProfileLookup(resourceId, member);
}
}
break;
case Pill.TYPE_ROOM_MENTION: {
const localRoom = resourceId[0] === '#' ?
MatrixClientPeg.get().getRooms().find((r) => {
return r.getAliases().includes(resourceId);
}) : MatrixClientPeg.get().getRoom(resourceId);
room = localRoom;
if (!localRoom) {
// TODO: This would require a new API to resolve a room alias to
// a room avatar and name.
// this.doRoomProfileLookup(resourceId, member);
}
}
break;
}
this.setState({resourceId, pillType, member, room});
},
componentWillMount() {
this._unmounted = false;
this.componentWillReceiveProps(this.props);
},
componentWillUnmount() {
this._unmounted = true;
},
doProfileLookup: function(userId, member) {
MatrixClientPeg.get().getProfileInfo(userId).then((resp) => {
if (this._unmounted) {
return;
}
member.name = resp.displayname;
member.rawDisplayName = resp.displayname;
member.events.member = {
getContent: () => {
return {avatar_url: resp.avatar_url};
},
};
this.setState({member});
}).catch((err) => {
console.error('Could not retrieve profile data for ' + userId + ':', err);
});
},
render: function() {
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
const resource = this.state.resourceId;
let avatar = null;
let linkText = resource;
let pillClass;
let userId;
switch (this.state.pillType) {
case Pill.TYPE_USER_MENTION: {
// If this user is not a member of this room, default to the empty member
const member = this.state.member;
if (member) {
userId = member.userId;
linkText = member.rawDisplayName.replace(' (IRC)', ''); // FIXME when groups are done
avatar = <MemberAvatar member={member} width={16} height={16}/>;
pillClass = 'mx_UserPill';
}
}
break;
case Pill.TYPE_ROOM_MENTION: {
const room = this.state.room;
if (room) {
linkText = (room ? getDisplayAliasForRoom(room) : null) || resource;
avatar = <RoomAvatar room={room} width={16} height={16}/>;
pillClass = 'mx_RoomPill';
}
}
break;
}
const classes = classNames(pillClass, {
"mx_UserPill_me": userId === MatrixClientPeg.get().credentials.userId,
});
if (this.state.pillType) {
return this.props.inMessage ?
<a className={classes} href={this.props.url} title={resource} data-offset-key={this.props.offsetKey}>
{avatar}
{linkText}
</a> :
<span className={classes} title={resource} data-offset-key={this.props.offsetKey}>
{avatar}
{linkText}
</span>;
} else {
// Deliberately render nothing if the URL isn't recognised
return null;
}
},
});
export default Pill;

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

@ -0,0 +1,40 @@
/*
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 PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const RoomDirectoryButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton');
return (
<ActionButton action="view_room_directory"
mouseOverAction={props.callout ? "callout_room_directory" : null}
label={ _t("Room directory") }
iconPath="img/icons-directory.svg"
size={props.size}
tooltip={props.tooltip}
/>
);
};
RoomDirectoryButton.propTypes = {
size: PropTypes.string,
tooltip: PropTypes.bool,
};
export default RoomDirectoryButton;

View file

@ -0,0 +1,39 @@
/*
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 PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const SettingsButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton');
return (
<ActionButton action="view_user_settings"
label={ _t("Settings") }
iconPath="img/icons-settings.svg"
size={props.size}
tooltip={props.tooltip}
/>
);
};
SettingsButton.propTypes = {
size: PropTypes.string,
tooltip: PropTypes.bool,
};
export default SettingsButton;

View file

@ -0,0 +1,40 @@
/*
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 PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const StartChatButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton');
return (
<ActionButton action="view_create_chat"
mouseOverAction={props.callout ? "callout_start_chat" : null}
label={ _t("Start chat") }
iconPath="img/icons-people.svg"
size={props.size}
tooltip={props.tooltip}
/>
);
};
StartChatButton.propTypes = {
size: PropTypes.string,
tooltip: PropTypes.bool,
};
export default StartChatButton;

View file

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require('react');
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'TruncatedList',
@ -33,7 +34,7 @@ module.exports = React.createClass({
truncateAt: 2,
createOverflowElement: function(overflowCount, totalCount) {
return (
<div>And {overflowCount} more...</div>
<div>{_t("And %(count)s more...", {count: overflowCount})}</div>
);
}
};

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>
);

View file

@ -16,7 +16,9 @@ limitations under the License.
'use strict';
var React = require('react');
import React from 'react';
import { _t } from '../../../languageHandler';
var DIV_ID = 'mx_recaptcha';
/**
@ -44,6 +46,10 @@ module.exports = React.createClass({
};
},
componentWillMount: function() {
this._captchaWidgetId = null;
},
componentDidMount: function() {
// Just putting a script tag into the returned jsx doesn't work, annoyingly,
// so we do this instead.
@ -73,6 +79,10 @@ module.exports = React.createClass({
}
},
componentWillUnmount: function() {
this._resetRecaptcha();
},
_renderRecaptcha: function(divId) {
if (!global.grecaptcha) {
console.error("grecaptcha not loaded!");
@ -88,12 +98,18 @@ module.exports = React.createClass({
}
console.log("Rendering to %s", divId);
global.grecaptcha.render(divId, {
this._captchaWidgetId = global.grecaptcha.render(divId, {
sitekey: publicKey,
callback: this.props.onCaptchaResponse,
});
},
_resetRecaptcha: function() {
if (this._captchaWidgetId !== null) {
global.grecaptcha.reset(this._captchaWidgetId);
}
},
_onCaptchaLoaded: function() {
console.log("Loaded recaptcha script.");
try {
@ -117,7 +133,7 @@ module.exports = React.createClass({
return (
<div ref="recaptchaContainer">
This Home Server would like to make sure you are not a robot
{_t("This Home Server would like to make sure you are not a robot")}
<br/>
<div id={DIV_ID}></div>
{error}

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: 'CasLogin',
@ -28,7 +29,7 @@ module.exports = React.createClass({
render: function() {
return (
<div>
<button onClick={this.props.onSubmit}>Sign in with CAS</button>
<button onClick={this.props.onSubmit}>{_t("Sign in with CAS")}</button>
</div>
);
}

View file

@ -19,7 +19,6 @@ import React from 'react';
import sdk from '../../../index';
import { COUNTRIES } from '../../../phonenumber';
import { charactersToImageNode } from '../../../HtmlUtils';
const COUNTRIES_BY_ISO2 = new Object(null);
for (const c of COUNTRIES) {
@ -27,22 +26,27 @@ for (const c of COUNTRIES) {
}
function countryMatchesSearchQuery(query, country) {
// Remove '+' if present (when searching for a prefix)
if (query[0] === '+') {
query = query.slice(1);
}
if (country.name.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
if (country.iso2 == query.toUpperCase()) return true;
if (country.prefix == query) return true;
if (country.prefix.indexOf(query) !== -1) return true;
return false;
}
const MAX_DISPLAYED_ROWS = 2;
export default class CountryDropdown extends React.Component {
constructor(props) {
super(props);
this._onSearchChange = this._onSearchChange.bind(this);
this._onOptionChange = this._onOptionChange.bind(this);
this._getShortOption = this._getShortOption.bind(this);
this.state = {
searchQuery: '',
}
};
}
componentWillMount() {
@ -50,7 +54,7 @@ export default class CountryDropdown extends React.Component {
// If no value is given, we start with the first
// country selected, but our parent component
// doesn't know this, therefore we do this.
this.props.onOptionChange(COUNTRIES[0].iso2);
this.props.onOptionChange(COUNTRIES[0]);
}
}
@ -60,14 +64,26 @@ export default class CountryDropdown extends React.Component {
});
}
_onOptionChange(iso2) {
this.props.onOptionChange(COUNTRIES_BY_ISO2[iso2]);
}
_flagImgForIso2(iso2) {
// Unicode Regional Indicator Symbol letter 'A'
const RIS_A = 0x1F1E6;
const ASCII_A = 65;
return charactersToImageNode(iso2,
RIS_A + (iso2.charCodeAt(0) - ASCII_A),
RIS_A + (iso2.charCodeAt(1) - ASCII_A),
);
return <img src={`flags/${iso2}.png`}/>;
}
_getShortOption(iso2) {
if (!this.props.isSmall) {
return undefined;
}
let countryPrefix;
if (this.props.showPrefix) {
countryPrefix = '+' + COUNTRIES_BY_ISO2[iso2].prefix;
}
return <span>
{ this._flagImgForIso2(iso2) }
{ countryPrefix }
</span>;
}
render() {
@ -93,14 +109,10 @@ export default class CountryDropdown extends React.Component {
displayedCountries = COUNTRIES;
}
if (displayedCountries.length > MAX_DISPLAYED_ROWS) {
displayedCountries = displayedCountries.slice(0, MAX_DISPLAYED_ROWS);
}
const options = displayedCountries.map((country) => {
return <div key={country.iso2}>
{this._flagImgForIso2(country.iso2)}
{country.name}
{country.name} <span>(+{country.prefix})</span>
</div>;
});
@ -108,18 +120,21 @@ export default class CountryDropdown extends React.Component {
// values between mounting and the initial value propgating
const value = this.props.value || COUNTRIES[0].iso2;
return <Dropdown className={this.props.className}
onOptionChange={this.props.onOptionChange} onSearchChange={this._onSearchChange}
menuWidth={298} getShortOption={this._flagImgForIso2}
value={value}
return <Dropdown className={this.props.className + " left_aligned"}
onOptionChange={this._onOptionChange} onSearchChange={this._onSearchChange}
menuWidth={298} getShortOption={this._getShortOption}
value={value} searchEnabled={true}
>
{options}
</Dropdown>
</Dropdown>;
}
}
CountryDropdown.propTypes = {
className: React.PropTypes.string,
isSmall: React.PropTypes.bool,
// if isSmall, show +44 in the selected value
showPrefix: React.PropTypes.bool,
onOptionChange: React.PropTypes.func.isRequired,
value: React.PropTypes.string,
};

View file

@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require("react");
import React from 'react';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'CustomServerDialog',
@ -23,24 +24,24 @@ module.exports = React.createClass({
return (
<div className="mx_ErrorDialog">
<div className="mx_Dialog_title">
Custom Server Options
{_t("Custom Server Options")}
</div>
<div className="mx_Dialog_content">
<span>
You can use the custom server options to sign into other Matrix
servers by specifying a different Home server URL.
{_t("You can use the custom server options to sign into other Matrix " +
"servers by specifying a different Home server URL.")}
<br/>
This allows you to use this app with an existing Matrix account on
a different home server.
{_t("This allows you to use this app with an existing Matrix account on " +
"a different home server.")}
<br/>
<br/>
You can also set a custom identity server but this will typically prevent
interaction with users based on email address.
{_t("You can also set a custom identity server but this will typically prevent " +
"interaction with users based on email address.")}
</span>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.props.onFinished} autoFocus={true}>
Dismiss
{_t("Dismiss")}
</button>
</div>
</div>

View file

@ -20,6 +20,7 @@ import url from 'url';
import classnames from 'classnames';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
/* This file contains a collection of components which are used by the
* InteractiveAuth to prompt the user to enter the information needed
@ -128,8 +129,8 @@ export const PasswordAuthEntry = React.createClass({
return (
<div>
<p>To continue, please enter your password.</p>
<p>Password:</p>
<p>{_t("To continue, please enter your password.")}</p>
<p>{_t("Password:")}</p>
<form onSubmit={this._onSubmit}>
<input
ref="passwordField"
@ -255,8 +256,8 @@ export const EmailIdentityAuthEntry = React.createClass({
} else {
return (
<div>
<p>An email has been sent to <i>{this.props.inputs.emailAddress}</i></p>
<p>Please check your email to continue registration.</p>
<p>{_t("An email has been sent to")} <i>{this.props.inputs.emailAddress}</i></p>
<p>{_t("Please check your email to continue registration.")}</p>
</div>
);
}
@ -348,7 +349,7 @@ export const MsisdnAuthEntry = React.createClass({
});
} else {
this.setState({
errorText: "Token incorrect",
errorText: _t("Token incorrect"),
});
}
}).catch((e) => {
@ -369,8 +370,8 @@ export const MsisdnAuthEntry = React.createClass({
});
return (
<div>
<p>A text message has been sent to +<i>{this._msisdn}</i></p>
<p>Please enter the code it contains:</p>
<p>{_t("A text message has been sent to")} +<i>{this._msisdn}</i></p>
<p>{_t("Please enter the code it contains:")}</p>
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
<form onSubmit={this._onFormSubmit}>
<input type="text"
@ -379,7 +380,7 @@ export const MsisdnAuthEntry = React.createClass({
onChange={this._onTokenChange}
/>
<br />
<input type="submit" value="Submit"
<input type="submit" value={_t("Submit")}
className={submitClasses}
disabled={!enableSubmit}
/>
@ -439,7 +440,7 @@ export const FallbackAuthEntry = React.createClass({
render: function() {
return (
<div>
<a onClick={this._onShowFallbackClick}>Start authentication</a>
<a onClick={this._onShowFallbackClick}>{_t("Start authentication")}</a>
<div className="error">
{this.props.errorText}
</div>

View file

@ -16,7 +16,8 @@ limitations under the License.
'use strict';
var React = require('react');
import { _t } from '../../../languageHandler';
import React from 'react';
module.exports = React.createClass({
displayName: 'LoginFooter',
@ -24,8 +25,8 @@ module.exports = React.createClass({
render: function() {
return (
<div className="mx_Login_links">
<a href="https://matrix.org">powered by Matrix</a>
<a href="https://matrix.org">{_t("powered by Matrix")}</a>
</div>
);
}
},
});

View file

@ -16,100 +16,163 @@ limitations under the License.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {field_input_incorrect} from '../../../UiEffects';
/**
* A pure UI component which displays a username/password form.
*/
module.exports = React.createClass({displayName: 'PasswordLogin',
propTypes: {
onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
onForgotPasswordClick: React.PropTypes.func, // fn()
initialUsername: React.PropTypes.string,
initialPhoneCountry: React.PropTypes.string,
initialPhoneNumber: React.PropTypes.string,
initialPassword: React.PropTypes.string,
onUsernameChanged: React.PropTypes.func,
onPhoneCountryChanged: React.PropTypes.func,
onPhoneNumberChanged: React.PropTypes.func,
onPasswordChanged: React.PropTypes.func,
loginIncorrect: React.PropTypes.bool,
},
class PasswordLogin extends React.Component {
static defaultProps = {
onUsernameChanged: function() {},
onPasswordChanged: function() {},
onPhoneCountryChanged: function() {},
onPhoneNumberChanged: function() {},
initialUsername: "",
initialPhoneCountry: "",
initialPhoneNumber: "",
initialPassword: "",
loginIncorrect: false,
hsDomain: "",
}
getDefaultProps: function() {
return {
onUsernameChanged: function() {},
onPasswordChanged: function() {},
onPhoneCountryChanged: function() {},
onPhoneNumberChanged: function() {},
initialUsername: "",
initialPhoneCountry: "",
initialPhoneNumber: "",
initialPassword: "",
loginIncorrect: false,
};
},
getInitialState: function() {
return {
constructor(props) {
super(props);
this.state = {
username: this.props.initialUsername,
password: this.props.initialPassword,
phoneCountry: this.props.initialPhoneCountry,
phoneNumber: this.props.initialPhoneNumber,
loginType: PasswordLogin.LOGIN_FIELD_MXID,
};
},
componentWillMount: function() {
this.onSubmitForm = this.onSubmitForm.bind(this);
this.onUsernameChanged = this.onUsernameChanged.bind(this);
this.onLoginTypeChange = this.onLoginTypeChange.bind(this);
this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this);
this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this);
this.onPasswordChanged = this.onPasswordChanged.bind(this);
}
componentWillMount() {
this._passwordField = null;
},
}
componentWillReceiveProps: function(nextProps) {
componentWillReceiveProps(nextProps) {
if (!this.props.loginIncorrect && nextProps.loginIncorrect) {
field_input_incorrect(this._passwordField);
}
},
}
onSubmitForm: function(ev) {
onSubmitForm(ev) {
ev.preventDefault();
if (this.state.loginType === PasswordLogin.LOGIN_FIELD_PHONE) {
this.props.onSubmit(
'', // XXX: Synapse breaks if you send null here:
this.state.phoneCountry,
this.state.phoneNumber,
this.state.password,
);
return;
}
this.props.onSubmit(
this.state.username,
this.state.phoneCountry,
this.state.phoneNumber,
null,
null,
this.state.password,
);
},
}
onUsernameChanged: function(ev) {
onUsernameChanged(ev) {
this.setState({username: ev.target.value});
this.props.onUsernameChanged(ev.target.value);
},
}
onPhoneCountryChanged: function(country) {
this.setState({phoneCountry: country});
this.props.onPhoneCountryChanged(country);
},
onLoginTypeChange(loginType) {
this.setState({
loginType: loginType,
username: "" // Reset because email and username use the same state
});
}
onPhoneNumberChanged: function(ev) {
onPhoneCountryChanged(country) {
this.setState({
phoneCountry: country.iso2,
phonePrefix: country.prefix,
});
this.props.onPhoneCountryChanged(country.iso2);
}
onPhoneNumberChanged(ev) {
this.setState({phoneNumber: ev.target.value});
this.props.onPhoneNumberChanged(ev.target.value);
},
}
onPasswordChanged: function(ev) {
onPasswordChanged(ev) {
this.setState({password: ev.target.value});
this.props.onPasswordChanged(ev.target.value);
},
}
render: function() {
renderLoginField(loginType) {
switch(loginType) {
case PasswordLogin.LOGIN_FIELD_EMAIL:
return <input
className="mx_Login_field mx_Login_email"
key="email_input"
type="text"
name="username" // make it a little easier for browser's remember-password
onChange={this.onUsernameChanged}
placeholder="joe@example.com"
value={this.state.username}
autoFocus
/>;
case PasswordLogin.LOGIN_FIELD_MXID:
return <input
className="mx_Login_field mx_Login_username"
key="username_input"
type="text"
name="username" // make it a little easier for browser's remember-password
onChange={this.onUsernameChanged}
placeholder={_t('User name')}
value={this.state.username}
autoFocus
/>;
case PasswordLogin.LOGIN_FIELD_PHONE:
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
return <div className="mx_Login_phoneSection">
<CountryDropdown
className="mx_Login_phoneCountry mx_Login_field_prefix"
ref="phone_country"
onOptionChange={this.onPhoneCountryChanged}
value={this.state.phoneCountry}
isSmall={true}
showPrefix={true}
/>
<input
className="mx_Login_phoneNumberField mx_Login_field mx_Login_field_has_prefix"
ref="phoneNumber"
key="phone_input"
type="text"
name="phoneNumber"
onChange={this.onPhoneNumberChanged}
placeholder={_t("Mobile phone number")}
value={this.state.phoneNumber}
autoFocus
/>
</div>;
}
}
render() {
var forgotPasswordJsx;
if (this.props.onForgotPasswordClick) {
forgotPasswordJsx = (
<a className="mx_Login_forgot" onClick={this.props.onForgotPasswordClick} href="#">
Forgot your password?
{ _t('Forgot your password?') }
</a>
);
}
@ -119,38 +182,54 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
error: this.props.loginIncorrect,
});
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
const Dropdown = sdk.getComponent('elements.Dropdown');
const loginField = this.renderLoginField(this.state.loginType);
return (
<div>
<form onSubmit={this.onSubmitForm}>
<input className="mx_Login_field mx_Login_username" type="text"
name="username" // make it a little easier for browser's remember-password
value={this.state.username} onChange={this.onUsernameChanged}
placeholder="Email or user name" autoFocus />
or
<div className="mx_Login_phoneSection">
<CountryDropdown ref="phone_country" onOptionChange={this.onPhoneCountryChanged}
className="mx_Login_phoneCountry"
value={this.state.phoneCountry}
/>
<input type="text" ref="phoneNumber"
onChange={this.onPhoneNumberChanged}
placeholder="Mobile phone number"
className="mx_Login_phoneNumberField mx_Login_field"
value={this.state.phoneNumber}
name="phoneNumber"
/>
<div className="mx_Login_type_container">
<label className="mx_Login_type_label">{ _t('Sign in with') }</label>
<Dropdown
className="mx_Login_type_dropdown"
value={this.state.loginType}
onOptionChange={this.onLoginTypeChange}>
<span key={PasswordLogin.LOGIN_FIELD_MXID}>{ _t('my Matrix ID') }</span>
<span key={PasswordLogin.LOGIN_FIELD_EMAIL}>{ _t('Email address') }</span>
<span key={PasswordLogin.LOGIN_FIELD_PHONE}>{ _t('Phone') }</span>
</Dropdown>
</div>
<br />
{loginField}
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
name="password"
value={this.state.password} onChange={this.onPasswordChanged}
placeholder="Password" />
placeholder={ _t('Password') } />
<br />
{forgotPasswordJsx}
<input className="mx_Login_submit" type="submit" value="Sign in" />
<input className="mx_Login_submit" type="submit" value={ _t('Sign in') } />
</form>
</div>
);
}
});
}
PasswordLogin.LOGIN_FIELD_EMAIL = "login_field_email";
PasswordLogin.LOGIN_FIELD_MXID = "login_field_mxid";
PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone";
PasswordLogin.propTypes = {
onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
onForgotPasswordClick: React.PropTypes.func, // fn()
initialUsername: React.PropTypes.string,
initialPhoneCountry: React.PropTypes.string,
initialPhoneNumber: React.PropTypes.string,
initialPassword: React.PropTypes.string,
onUsernameChanged: React.PropTypes.func,
onPhoneCountryChanged: React.PropTypes.func,
onPhoneNumberChanged: React.PropTypes.func,
onPasswordChanged: React.PropTypes.func,
loginIncorrect: React.PropTypes.bool,
};
module.exports = PasswordLogin;

View file

@ -21,6 +21,7 @@ import sdk from '../../../index';
import Email from '../../../email';
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
const FIELD_EMAIL = 'field_email';
const FIELD_PHONE_COUNTRY = 'field_phone_country';
@ -53,11 +54,6 @@ module.exports = React.createClass({
})).required,
}),
// A username that will be used if no username is entered.
// Specifying this param will also warn the user that entering
// a different username will cause a fresh account to be generated.
guestUsername: React.PropTypes.string,
minPasswordLength: React.PropTypes.number,
onError: React.PropTypes.func,
onRegisterClick: React.PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
@ -100,30 +96,29 @@ module.exports = React.createClass({
if (this.refs.email.value == '') {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: "Warning",
title: _t("Warning!"),
description:
<div>
If you don't specify an email address, you won't be able to reset your password.<br/>
Are you sure?
{_t("If you don't specify an email address, you won't be able to reset your password. " +
"Are you sure?")}
</div>,
button: "Continue",
button: _t("Continue"),
onFinished: function(confirmed) {
if (confirmed) {
self._doSubmit();
self._doSubmit(ev);
}
},
});
}
else {
self._doSubmit();
} else {
self._doSubmit(ev);
}
}
},
_doSubmit: function() {
_doSubmit: function(ev) {
let email = this.refs.email.value.trim();
var promise = this.props.onRegisterClick({
username: this.refs.username.value.trim() || this.props.guestUsername,
username: this.refs.username.value.trim(),
password: this.refs.password.value.trim(),
email: email,
phoneCountry: this.state.phoneCountry,
@ -191,7 +186,7 @@ module.exports = React.createClass({
break;
case FIELD_USERNAME:
// XXX: SPEC-1
var username = this.refs.username.value.trim() || this.props.guestUsername;
var username = this.refs.username.value.trim();
if (encodeURIComponent(username) != username) {
this.markFieldValid(
field_id,
@ -270,7 +265,8 @@ module.exports = React.createClass({
_onPhoneCountryChange(newVal) {
this.setState({
phoneCountry: newVal,
phoneCountry: newVal.iso2,
phonePrefix: newVal.prefix,
});
},
@ -280,7 +276,7 @@ module.exports = React.createClass({
const emailSection = (
<div>
<input type="text" ref="email"
autoFocus={true} placeholder="Email address (optional)"
autoFocus={true} placeholder={_t("Email address (optional)")}
defaultValue={this.props.defaultEmail}
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_EMAIL);}}
@ -303,7 +299,7 @@ module.exports = React.createClass({
} else if (this.state.selectedTeam) {
belowEmailSection = (
<p className="mx_Login_support">
You are registering with {this.state.selectedTeam.name}
{_t("You are registering with %(SelectedTeamName)s", {SelectedTeamName: this.state.selectedTeam.name})}
</p>
);
}
@ -313,14 +309,19 @@ module.exports = React.createClass({
const phoneSection = (
<div className="mx_Login_phoneSection">
<CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange}
className="mx_Login_phoneCountry"
className="mx_Login_phoneCountry mx_Login_field_prefix"
value={this.state.phoneCountry}
isSmall={true}
showPrefix={true}
/>
<input type="text" ref="phoneNumber"
placeholder="Mobile phone number (optional)"
placeholder={_t("Mobile phone number (optional)")}
defaultValue={this.props.defaultPhoneNumber}
className={this._classForField(
FIELD_PHONE_NUMBER, 'mx_Login_phoneNumberField', 'mx_Login_field'
FIELD_PHONE_NUMBER,
'mx_Login_phoneNumberField',
'mx_Login_field',
'mx_Login_field_has_prefix'
)}
onBlur={function() {self.validateField(FIELD_PHONE_NUMBER);}}
value={self.state.phoneNumber}
@ -329,13 +330,10 @@ module.exports = React.createClass({
);
const registerButton = (
<input className="mx_Login_submit" type="submit" value="Register" />
<input className="mx_Login_submit" type="submit" value={_t("Register")} />
);
let placeholderUserName = "User name";
if (this.props.guestUsername) {
placeholderUserName += " (default: " + this.props.guestUsername + ")";
}
let placeholderUserName = _t("User name");
return (
<div>
@ -348,16 +346,13 @@ module.exports = React.createClass({
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_USERNAME);}} />
<br />
{ this.props.guestUsername ?
<div className="mx_Login_fieldLabel">Setting a user name will create a fresh account</div> : null
}
<input type="password" ref="password"
className={this._classForField(FIELD_PASSWORD, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_PASSWORD);}}
placeholder="Password" defaultValue={this.props.defaultPassword} />
placeholder={_t("Password")} defaultValue={this.props.defaultPassword} />
<br />
<input type="password" ref="passwordConfirm"
placeholder="Confirm password"
placeholder={_t("Confirm password")}
className={this._classForField(FIELD_PASSWORD_CONFIRM, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_PASSWORD_CONFIRM);}}
defaultValue={this.props.defaultPassword} />

View file

@ -19,6 +19,7 @@ limitations under the License.
var React = require('react');
var Modal = require('../../../Modal');
var sdk = require('../../../index');
import { _t } from '../../../languageHandler';
/**
* A pure UI component which displays the HS and IS to use.
@ -27,8 +28,7 @@ module.exports = React.createClass({
displayName: 'ServerConfig',
propTypes: {
onHsUrlChanged: React.PropTypes.func,
onIsUrlChanged: React.PropTypes.func,
onServerConfigChange: React.PropTypes.func,
// default URLs are defined in config.json (or the hardcoded defaults)
// they are used if the user has not overridden them with a custom URL.
@ -50,8 +50,7 @@ module.exports = React.createClass({
getDefaultProps: function() {
return {
onHsUrlChanged: function() {},
onIsUrlChanged: function() {},
onServerConfigChange: function() {},
customHsUrl: "",
customIsUrl: "",
withToggleButton: false,
@ -75,7 +74,10 @@ module.exports = React.createClass({
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() {
var hsUrl = this.state.hs_url.trim().replace(/\/$/, "");
if (hsUrl === "") hsUrl = this.props.defaultHsUrl;
this.props.onHsUrlChanged(hsUrl);
this.props.onServerConfigChange({
hsUrl : this.state.hs_url,
isUrl : this.state.is_url,
});
});
});
},
@ -85,7 +87,10 @@ module.exports = React.createClass({
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() {
var isUrl = this.state.is_url.trim().replace(/\/$/, "");
if (isUrl === "") isUrl = this.props.defaultIsUrl;
this.props.onIsUrlChanged(isUrl);
this.props.onServerConfigChange({
hsUrl : this.state.hs_url,
isUrl : this.state.is_url,
});
});
});
},
@ -102,12 +107,16 @@ module.exports = React.createClass({
configVisible: visible
});
if (!visible) {
this.props.onHsUrlChanged(this.props.defaultHsUrl);
this.props.onIsUrlChanged(this.props.defaultIsUrl);
this.props.onServerConfigChange({
hsUrl : this.props.defaultHsUrl,
isUrl : this.props.defaultIsUrl,
});
}
else {
this.props.onHsUrlChanged(this.state.hs_url);
this.props.onIsUrlChanged(this.state.is_url);
this.props.onServerConfigChange({
hsUrl : this.state.hs_url,
isUrl : this.state.is_url,
});
}
},
@ -123,19 +132,19 @@ module.exports = React.createClass({
var toggleButton;
if (this.props.withToggleButton) {
toggleButton = (
<div style={{ textAlign: 'center' }}>
<div className="mx_ServerConfig_selector">
<input className="mx_Login_radio" id="basic" name="configVisible" type="radio"
checked={!this.state.configVisible}
onChange={this.onServerConfigVisibleChange.bind(this, false)} />
<label className="mx_Login_label" htmlFor="basic">
Default server
{_t("Default server")}
</label>
&nbsp;&nbsp;
<input className="mx_Login_radio" id="advanced" name="configVisible" type="radio"
checked={this.state.configVisible}
onChange={this.onServerConfigVisibleChange.bind(this, true)} />
<label className="mx_Login_label" htmlFor="advanced">
Custom server
{_t("Custom server")}
</label>
</div>
);
@ -147,7 +156,7 @@ module.exports = React.createClass({
<div style={serverConfigStyle}>
<div className="mx_ServerConfig">
<label className="mx_Login_label mx_ServerConfig_hslabel" htmlFor="hsurl">
Home server URL
{_t("Home server URL")}
</label>
<input className="mx_Login_field" id="hsurl" type="text"
placeholder={this.props.defaultHsUrl}
@ -155,7 +164,7 @@ module.exports = React.createClass({
value={this.state.hs_url}
onChange={this.onHomeserverChanged} />
<label className="mx_Login_label mx_ServerConfig_islabel" htmlFor="isurl">
Identity server URL
{_t("Identity server URL")}
</label>
<input className="mx_Login_field" id="isurl" type="text"
placeholder={this.props.defaultIsUrl}
@ -163,7 +172,7 @@ module.exports = React.createClass({
value={this.state.is_url}
onChange={this.onIdentityServerChanged} />
<a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}>
What does this mean?
{_t("What does this mean?")}
</a>
</div>
</div>

View file

@ -20,8 +20,8 @@ import React from 'react';
import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
import { _t } from '../../../languageHandler';
export default class MAudioBody extends React.Component {
constructor(props) {
@ -77,7 +77,7 @@ export default class MAudioBody extends React.Component {
return (
<span className="mx_MAudioBody" ref="body">
<img src="img/warning.svg" width="16" height="16"/>
Error decrypting audio
{_t("Error decrypting audio")}
</span>
);
}

View file

@ -20,10 +20,10 @@ import React from 'react';
import filesize from 'filesize';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {decryptFile} from '../../../utils/DecryptFile';
import Tinter from '../../../Tinter';
import request from 'browser-request';
import q from 'q';
import Modal from '../../../Modal';
@ -202,7 +202,7 @@ module.exports = React.createClass({
* @return {string} the human readable link text for the attachment.
*/
presentableTextForFile: function(content) {
var linkText = 'Attachment';
var linkText = _t("Attachment");
if (content.body && content.body.length > 0) {
// The content body should be the name of the file including a
// file extension.
@ -261,7 +261,7 @@ module.exports = React.createClass({
const content = this.props.mxEvent.getContent();
const text = this.presentableTextForFile(content);
const isEncrypted = content.file !== undefined;
const fileName = content.body && content.body.length > 0 ? content.body : "Attachment";
const fileName = content.body && content.body.length > 0 ? content.body : _t("Attachment");
const contentUrl = this._getContentUrl();
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -283,7 +283,8 @@ module.exports = React.createClass({
}).catch((err) => {
console.warn("Unable to decrypt attachment: ", err);
Modal.createDialog(ErrorDialog, {
description: "Error decrypting attachment"
title: _t("Error"),
description: _t("Error decrypting attachment"),
});
}).finally(() => {
decrypting = false;
@ -295,7 +296,7 @@ module.exports = React.createClass({
<span className="mx_MFileBody" ref="body">
<div className="mx_MImageBody_download">
<a href="javascript:void(0)" onClick={decrypt}>
Decrypt {text}
{ _t("Decrypt %(text)s", { text: text }) }
</a>
</div>
</span>
@ -314,7 +315,7 @@ module.exports = React.createClass({
// We can't provide a Content-Disposition header like we would for HTTP.
download: fileName,
target: "_blank",
textContent: "Download " + text,
textContent: _t("Download %(text)s", { text: text }),
}, "*");
};
@ -346,7 +347,7 @@ module.exports = React.createClass({
return (
<span className="mx_MFileBody">
<div className="mx_MImageBody_download">
<a className="mx_ImageBody_downloadLink" href={contentUrl} target="_blank">
<a className="mx_ImageBody_downloadLink" href={contentUrl} download={fileName} target="_blank">
{ fileName }
</a>
<div className="mx_MImageBody_size">
@ -360,9 +361,9 @@ module.exports = React.createClass({
return (
<span className="mx_MFileBody">
<div className="mx_MImageBody_download">
<a href={contentUrl} target="_blank" rel="noopener">
<a href={contentUrl} download={fileName} target="_blank" rel="noopener">
<img src={tintedDownloadImageURL} width="12" height="14" ref="downloadImage"/>
Download {text}
{ _t("Download %(text)s", { text: text }) }
</a>
</div>
</span>
@ -371,7 +372,7 @@ module.exports = React.createClass({
} else {
var extra = text ? (': ' + text) : '';
return <span className="mx_MFileBody">
Invalid file{extra}
{ _t("Invalid file%(extra)s", { extra: extra }) }
</span>;
}
},

View file

@ -24,8 +24,9 @@ import Modal from '../../../Modal';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
import q from 'q';
import Promise from 'bluebird';
import UserSettingsStore from '../../../UserSettingsStore';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'MImageBody',
@ -56,6 +57,7 @@ module.exports = React.createClass({
const ImageView = sdk.getComponent("elements.ImageView");
const params = {
src: httpUrl,
name: content.body && content.body.length > 0 ? content.body : _t('Attachment'),
mxEvent: this.props.mxEvent,
};
@ -121,7 +123,7 @@ module.exports = React.createClass({
this.fixupHeight();
const content = this.props.mxEvent.getContent();
if (content.file !== undefined && this.state.decryptedUrl === null) {
let thumbnailPromise = q(null);
let thumbnailPromise = Promise.resolve(null);
if (content.info.thumbnail_file) {
thumbnailPromise = decryptFile(
content.info.thumbnail_file,
@ -190,7 +192,7 @@ module.exports = React.createClass({
return (
<span className="mx_MImageBody" ref="body">
<img src="img/warning.svg" width="16" height="16"/>
Error decrypting image
{_t("Error decrypting image")}
</span>
);
}
@ -237,13 +239,13 @@ module.exports = React.createClass({
} else if (content.body) {
return (
<span className="mx_MImageBody">
Image '{content.body}' cannot be displayed.
{_t("Image '%(Body)s' cannot be displayed.", {Body: content.body})}
</span>
);
} else {
return (
<span className="mx_MImageBody">
This image cannot be displayed.
{_t("This image cannot be displayed.")}
</span>
);
}

View file

@ -19,11 +19,10 @@ limitations under the License.
import React from 'react';
import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Model from '../../../Modal';
import sdk from '../../../index';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
import q from 'q';
import Promise from 'bluebird';
import UserSettingsStore from '../../../UserSettingsStore';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'MVideoBody',
@ -80,7 +79,7 @@ module.exports = React.createClass({
const content = this.props.mxEvent.getContent();
if (content.file !== undefined) {
return this.state.decryptedThumbnailUrl;
} else if (content.info.thumbnail_url) {
} else if (content.info && content.info.thumbnail_url) {
return MatrixClientPeg.get().mxcUrlToHttp(content.info.thumbnail_url);
} else {
return null;
@ -90,7 +89,7 @@ module.exports = React.createClass({
componentDidMount: function() {
const content = this.props.mxEvent.getContent();
if (content.file !== undefined && this.state.decryptedUrl === null) {
var thumbnailPromise = q(null);
var thumbnailPromise = Promise.resolve(null);
if (content.info.thumbnail_file) {
thumbnailPromise = decryptFile(
content.info.thumbnail_file
@ -128,7 +127,7 @@ module.exports = React.createClass({
return (
<span className="mx_MVideoBody" ref="body">
<img src="img/warning.svg" width="16" height="16"/>
Error decrypting video
{_t("Error decrypting video")}
</span>
);
}

View file

@ -0,0 +1,92 @@
/*
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 MatrixClientPeg from '../../../MatrixClientPeg';
import { ContentRepo } from 'matrix-js-sdk';
import { _t, _tJsx } from '../../../languageHandler';
import sdk from '../../../index';
import Modal from '../../../Modal';
import AccessibleButton from '../elements/AccessibleButton';
module.exports = React.createClass({
displayName: 'RoomAvatarEvent',
propTypes: {
/* the MatrixEvent to show */
mxEvent: React.PropTypes.object.isRequired,
},
onAvatarClick: function(name) {
var httpUrl = MatrixClientPeg.get().mxcUrlToHttp(this.props.mxEvent.getContent().url);
var ImageView = sdk.getComponent("elements.ImageView");
var params = {
src: httpUrl,
name: name,
};
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
},
render: function() {
var ev = this.props.mxEvent;
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
var room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
var name = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', {
senderDisplayName: senderDisplayName,
roomName: room ? room.name : '',
});
if (!ev.getContent().url || ev.getContent().url.trim().length === 0) {
return (
<div className="mx_TextualEvent">
{ _t('%(senderDisplayName)s removed the room avatar.', {senderDisplayName: senderDisplayName}) }
</div>
);
}
var url = ContentRepo.getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(),
ev.getContent().url,
Math.ceil(14 * window.devicePixelRatio),
Math.ceil(14 * window.devicePixelRatio),
'crop'
);
// it sucks that _tJsx doesn't support normal _t substitutions :((
return (
<div className="mx_RoomAvatarEvent">
{ _tJsx('$senderDisplayName changed the room avatar to <img/>',
[
/\$senderDisplayName/,
/<img\/>/,
],
[
(sub) => senderDisplayName,
(sub) =>
<AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar"
onClick={ this.onAvatarClick.bind(this, name) }>
<BaseAvatar width={14} height={14} url={ url }
name={ name } />
</AccessibleButton>,
]
)
}
</div>
);
},
});

View file

@ -30,7 +30,7 @@ export default function SenderProfile(props) {
}
return (
<EmojiText className="mx_SenderProfile"
<EmojiText className="mx_SenderProfile" dir="auto"
onClick={props.onClick}>{`${name || ''} ${props.aux || ''}`}</EmojiText>
);
}

View file

@ -28,6 +28,11 @@ import ScalarAuthClient from '../../../ScalarAuthClient';
import Modal from '../../../Modal';
import SdkConfig from '../../../SdkConfig';
import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler';
import UserSettingsStore from "../../../UserSettingsStore";
import MatrixClientPeg from '../../../MatrixClientPeg';
import {RoomMember} from 'matrix-js-sdk';
import classNames from 'classnames';
linkifyMatrix(linkify);
@ -62,9 +67,26 @@ module.exports = React.createClass({
};
},
copyToClipboard: function(text) {
const textArea = document.createElement("textarea");
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
try {
const successful = document.execCommand('copy');
} catch (err) {
console.log('Unable to copy');
}
document.body.removeChild(textArea);
},
componentDidMount: function() {
this._unmounted = false;
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
// are still sent as plaintext URLs. If these are ever pillified in the composer,
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
this.pillifyLinks(this.refs.content.children);
linkifyElement(this.refs.content, linkifyMatrix.options);
this.calculateUrlPreview();
@ -76,10 +98,29 @@ module.exports = React.createClass({
setTimeout(() => {
if (this._unmounted) return;
for (let i = 0; i < blocks.length; i++) {
highlight.highlightBlock(blocks[i]);
if (UserSettingsStore.getSyncedSetting("enableSyntaxHighlightLanguageDetection", false)) {
highlight.highlightBlock(blocks[i])
} else {
// Only syntax highlight if there's a class starting with language-
let classes = blocks[i].className.split(/\s+/).filter(function (cl) {
return cl.startsWith('language-');
});
if (classes.length != 0) {
highlight.highlightBlock(blocks[i]);
}
}
}
}, 10);
}
// add event handlers to the 'copy code' buttons
const buttons = ReactDOM.findDOMNode(this).getElementsByClassName("mx_EventTile_copyButton");
for (let i = 0; i < buttons.length; i++) {
buttons[i].onclick = (e) => {
const copyCode = buttons[i].parentNode.getElementsByTagName("code")[0];
this.copyToClipboard(copyCode.textContent);
};
}
}
},
@ -109,9 +150,15 @@ module.exports = React.createClass({
if (this.props.showUrlPreview && !this.state.links.length) {
var links = this.findLinks(this.refs.content.children);
if (links.length) {
this.setState({ links: links.map((link)=>{
return link.getAttribute("href");
})});
// de-dup the links (but preserve ordering)
const seen = new Set();
links = links.filter((link) => {
if (seen.has(link)) return false;
seen.add(link);
return true;
});
this.setState({ links: links });
// lazy-load the hidden state of the preview widget from localstorage
if (global.localStorage) {
@ -122,17 +169,42 @@ module.exports = React.createClass({
}
},
pillifyLinks: function(nodes) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node.tagName === "A" && node.getAttribute("href")) {
const href = node.getAttribute("href");
// If the link is a (localised) matrix.to link, replace it with a pill
const Pill = sdk.getComponent('elements.Pill');
if (Pill.isMessagePillUrl(href)) {
const pillContainer = document.createElement('span');
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
const pill = <Pill url={href} inMessage={true} room={room}/>;
ReactDOM.render(pill, pillContainer);
node.parentNode.replaceChild(pillContainer, node);
}
} else if (node.children && node.children.length) {
this.pillifyLinks(node.children);
}
}
},
findLinks: function(nodes) {
var links = [];
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (node.tagName === "A" && node.getAttribute("href"))
{
if (this.isLinkPreviewable(node)) {
links.push(node);
links.push(node.getAttribute("href"));
}
}
else if (node.tagName === "PRE" || node.tagName === "CODE") {
else if (node.tagName === "PRE" || node.tagName === "CODE" ||
node.tagName === "BLOCKQUOTE") {
continue;
}
else if (node.children && node.children.length) {
@ -190,10 +262,9 @@ module.exports = React.createClass({
onEmoteSenderClick: function(event) {
const mxEvent = this.props.mxEvent;
const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
dis.dispatch({
action: 'insert_displayname',
displayname: name.replace(' (IRC)', ''),
action: 'insert_mention',
user_id: mxEvent.getSender(),
});
},
@ -229,14 +300,14 @@ module.exports = React.createClass({
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
let integrationsUrl = SdkConfig.get().integrations_ui_url;
Modal.createDialog(QuestionDialog, {
title: "Add an Integration",
title: _t("Add an Integration"),
description:
<div>
You are about to be taken to a third-party site so you can
authenticate your account for use with {integrationsUrl}.<br/>
Do you wish to continue?
{_t("You are about to be taken to a third-party site so you can " +
"authenticate your account for use with %(integrationsUrl)s. " +
"Do you wish to continue?", { integrationsUrl: integrationsUrl })}
</div>,
button: "Continue",
button: _t("Continue"),
onFinished: function(confirmed) {
if (!confirmed) {
return;

View file

@ -24,6 +24,11 @@ import sdk from '../../../index';
module.exports = React.createClass({
displayName: 'TextualEvent',
propTypes: {
/* the MatrixEvent to show */
mxEvent: React.PropTypes.object.isRequired,
},
render: function() {
const EmojiText = sdk.getComponent('elements.EmojiText');
var text = TextForEvent.textForEvent(this.props.mxEvent);

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: 'UnknownBody',
@ -24,7 +25,7 @@ module.exports = React.createClass({
render: function() {
const text = this.props.mxEvent.getContent().body;
return (
<span className="mx_UnknownBody" title="Redacted or unknown message type">
<span className="mx_UnknownBody" title={_t("Removed or unknown message type")}>
{text}
</span>
);

View file

@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var q = require("q");
import Promise from 'bluebird';
var React = require('react');
var ObjectUtils = require("../../../ObjectUtils");
var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require("../../../index");
import { _t } from '../../../languageHandler';
var Modal = require("../../../Modal");
module.exports = React.createClass({
@ -103,7 +104,7 @@ module.exports = React.createClass({
}
if (oldCanonicalAlias !== this.state.canonicalAlias) {
console.log("AliasSettings: Updating canonical alias");
promises = [q.all(promises).then(
promises = [Promise.all(promises).then(
MatrixClientPeg.get().sendStateEvent(
this.props.roomId, "m.room.canonical_alias", {
alias: this.state.canonicalAlias
@ -154,8 +155,8 @@ module.exports = React.createClass({
else {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Invalid alias format",
description: "'" + alias + "' is not a valid format for an alias",
title: _t('Invalid alias format'),
description: _t('\'%(alias)s\' is not a valid format for an alias', { alias: alias }),
});
}
},
@ -170,8 +171,8 @@ module.exports = React.createClass({
else {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Invalid address format",
description: "'" + alias + "' is not a valid format for an address",
title: _t('Invalid address format'),
description: _t('\'%(alias)s\' is not a valid format for an address', { alias: alias }),
});
}
},
@ -203,7 +204,7 @@ module.exports = React.createClass({
if (this.props.canSetCanonicalAlias) {
canonical_alias_section = (
<select onChange={this.onCanonicalAliasChange} defaultValue={ this.state.canonicalAlias }>
<option value="" key="unset">not specified</option>
<option value="" key="unset">{ _t('not specified') }</option>
{
Object.keys(self.state.domainToAliases).map(function(domain, i) {
return self.state.domainToAliases[domain].map(function(alias, j) {
@ -220,7 +221,7 @@ module.exports = React.createClass({
}
else {
canonical_alias_section = (
<b>{ this.state.canonicalAlias || "not set" }</b>
<b>{ this.state.canonicalAlias || _t('not set') }</b>
);
}
@ -229,7 +230,7 @@ module.exports = React.createClass({
remote_aliases_section = (
<div>
<div className="mx_RoomSettings_aliasLabel">
Remote addresses for this room:
{_t("Remote addresses for this room:")}
</div>
<div className="mx_RoomSettings_aliasesTable">
{ this.state.remoteDomains.map((domain, i) => {
@ -254,13 +255,13 @@ module.exports = React.createClass({
<div>
<h3>Addresses</h3>
<div className="mx_RoomSettings_aliasLabel">
The main address for this room is: { canonical_alias_section }
{ _t('The main address for this room is') }: { canonical_alias_section }
</div>
<div className="mx_RoomSettings_aliasLabel">
{ (this.state.domainToAliases[localDomain] &&
this.state.domainToAliases[localDomain].length > 0)
? "Local addresses for this room:"
: "This room has no local addresses" }
? _t('Local addresses for this room:')
: _t('This room has no local addresses') }
</div>
<div className="mx_RoomSettings_aliasesTable">
{ (this.state.domainToAliases[localDomain] || []).map((alias, i) => {
@ -268,7 +269,7 @@ module.exports = React.createClass({
if (this.props.canSetAliases) {
deleteButton = (
<img src="img/cancel-small.svg" width="14" height="14"
alt="Delete" onClick={ self.onAliasDeleted.bind(self, localDomain, i) } />
alt={ _t('Delete') } onClick={ self.onAliasDeleted.bind(self, localDomain, i) } />
);
}
return (
@ -276,7 +277,7 @@ module.exports = React.createClass({
<EditableText
className="mx_RoomSettings_alias mx_RoomSettings_editable"
placeholderClassName="mx_RoomSettings_aliasPlaceholder"
placeholder={ "New address (e.g. #foo:" + localDomain + ")" }
placeholder={ _t('New address (e.g. #foo:%(localDomain)s)', { localDomain: localDomain}) }
blurToCancel={ false }
onValueChanged={ self.onAliasChanged.bind(self, localDomain, i) }
editable={ self.props.canSetAliases }
@ -294,7 +295,7 @@ module.exports = React.createClass({
ref="add_alias"
className="mx_RoomSettings_alias mx_RoomSettings_editable"
placeholderClassName="mx_RoomSettings_aliasPlaceholder"
placeholder={ "New address (e.g. #foo:" + localDomain + ")" }
placeholder={ _t('New address (e.g. #foo:%(localDomain)s)', { localDomain: localDomain}) }
blurToCancel={ false }
onValueChanged={ self.onAliasAdded } />
<div className="mx_RoomSettings_addAlias mx_filterFlipColor">

View file

@ -13,7 +13,7 @@ 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 q = require("q");
import Promise from 'bluebird';
var React = require('react');
var sdk = require('../../../index');
@ -21,6 +21,8 @@ var Tinter = require('../../../Tinter');
var MatrixClientPeg = require("../../../MatrixClientPeg");
var Modal = require("../../../Modal");
import dis from '../../../dispatcher';
var ROOM_COLORS = [
// magic room default values courtesy of Ribot
["#76cfa6", "#eaf5f0"],
@ -70,7 +72,7 @@ module.exports = React.createClass({
saveSettings: function() { // : Promise
if (!this.state.hasChanged) {
return q(); // They didn't explicitly give a color to save.
return Promise.resolve(); // They didn't explicitly give a color to save.
}
var originalState = this.getInitialState();
if (originalState.primary_color !== this.state.primary_color ||
@ -86,15 +88,11 @@ module.exports = React.createClass({
}
).catch(function(err) {
if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "Saving room color settings is only available to registered users"
});
dis.dispatch({action: 'view_set_mxid'});
}
});
}
return q(); // no color diff
return Promise.resolve(); // no color diff
},
_getColorIndex: function(scheme) {

View file

@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var q = require("q");
import Promise from 'bluebird';
var React = require('react');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require("../../../index");
var Modal = require("../../../Modal");
var UserSettingsStore = require('../../../UserSettingsStore');
import { _t, _tJsx } from '../../../languageHandler';
module.exports = React.createClass({
@ -120,35 +121,47 @@ module.exports = React.createClass({
<input type="checkbox" ref="globalDisableUrlPreview"
onChange={ this.onGlobalDisableUrlPreviewChange }
checked={ this.state.globalDisableUrlPreview } />
Disable URL previews by default for participants in this room
{_t("Disable URL previews by default for participants in this room")}
</label>;
}
else {
disableRoomPreviewUrls =
<label>
URL previews are { this.state.globalDisableUrlPreview ? "disabled" : "enabled" } by default for participants in this room.
{_t("URL previews are %(globalDisableUrlPreview)s by default for participants in this room.", {globalDisableUrlPreview: this.state.globalDisableUrlPreview ? _t("disabled") : _t("enabled")})}
</label>;
}
let urlPreviewText = null;
if (UserSettingsStore.getUrlPreviewsDisabled()) {
urlPreviewText = (
_tJsx("You have <a>disabled</a> URL previews by default.", /<a>(.*?)<\/a>/, (sub)=><a href="#/settings">{sub}</a>)
);
}
else {
urlPreviewText = (
_tJsx("You have <a>enabled</a> URL previews by default.", /<a>(.*?)<\/a>/, (sub)=><a href="#/settings">{sub}</a>)
);
}
return (
<div className="mx_RoomSettings_toggles">
<h3>URL Previews</h3>
<h3>{_t("URL Previews")}</h3>
<label>
You have <a href="#/settings">{ UserSettingsStore.getUrlPreviewsDisabled() ? 'disabled' : 'enabled' }</a> URL previews by default.
{urlPreviewText}
</label>
{ disableRoomPreviewUrls }
<label>
<input type="checkbox" ref="userEnableUrlPreview"
onChange={ this.onUserEnableUrlPreviewChange }
checked={ this.state.userEnableUrlPreview } />
Enable URL previews for this room (affects only you)
{_t("Enable URL previews for this room (affects only you)")}
</label>
<label>
<input type="checkbox" ref="userDisableUrlPreview"
onChange={ this.onUserDisableUrlPreviewChange }
checked={ this.state.userDisableUrlPreview } />
Disable URL previews for this room (affects only you)
{_t("Disable URL previews for this room (affects only you)")}
</label>
</div>
);

View file

@ -0,0 +1,207 @@
/*
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.
*/
'use strict';
import React from 'react';
import MatrixClientPeg from '../../../MatrixClientPeg';
import AppTile from '../elements/AppTile';
import Modal from '../../../Modal';
import dis from '../../../dispatcher';
import sdk from '../../../index';
import SdkConfig from '../../../SdkConfig';
import ScalarAuthClient from '../../../ScalarAuthClient';
import ScalarMessaging from '../../../ScalarMessaging';
import { _t } from '../../../languageHandler';
import WidgetUtils from '../../../WidgetUtils';
module.exports = React.createClass({
displayName: 'AppsDrawer',
propTypes: {
room: React.PropTypes.object.isRequired,
},
getInitialState: function() {
return {
apps: this._getApps(),
};
},
componentWillMount: function() {
ScalarMessaging.startListening();
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
},
componentDidMount: function() {
this.scalarClient = null;
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
this.scalarClient = new ScalarAuthClient();
this.scalarClient.connect().done(() => {
this.forceUpdate();
if (this.state.apps && this.state.apps.length < 1) {
this.onClickAddWidget();
}
// TODO -- Handle Scalar errors
// },
// (err) => {
// this.setState({
// scalar_error: err,
// });
});
}
},
componentWillUnmount: function() {
ScalarMessaging.stopListening();
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
}
},
/**
* Encodes a URI according to a set of template variables. Variables will be
* passed through encodeURIComponent.
* @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'.
* @param {Object} variables The key/value pairs to replace the template
* variables with. E.g. { "$bar": "baz" }.
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
*/
encodeUri: function(pathTemplate, variables) {
for (const key in variables) {
if (!variables.hasOwnProperty(key)) {
continue;
}
pathTemplate = pathTemplate.replace(
key, encodeURIComponent(variables[key]),
);
}
return pathTemplate;
},
_initAppConfig: function(appId, app) {
const user = MatrixClientPeg.get().getUser(this.props.userId);
const params = {
'$matrix_user_id': this.props.userId,
'$matrix_room_id': this.props.room.roomId,
'$matrix_display_name': user ? user.displayName : this.props.userId,
'$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '',
};
if(app.data) {
Object.keys(app.data).forEach((key) => {
params['$' + key] = app.data[key];
});
}
app.id = appId;
app.name = app.name || app.type;
app.url = this.encodeUri(app.url, params);
return app;
},
onRoomStateEvents: function(ev, state) {
if (ev.getRoomId() !== this.props.room.roomId || ev.getType() !== 'im.vector.modular.widgets') {
return;
}
this._updateApps();
},
_getApps: function() {
const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets');
if (!appsStateEvents) {
return [];
}
return appsStateEvents.filter((ev) => {
return ev.getContent().type && ev.getContent().url;
}).map((ev) => {
return this._initAppConfig(ev.getStateKey(), ev.getContent());
});
},
_updateApps: function() {
const apps = this._getApps();
if (apps.length < 1) {
dis.dispatch({
action: 'appsDrawer',
show: false,
});
}
this.setState({
apps: apps,
});
},
_canUserModify: function() {
try {
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
} catch(err) {
console.error(err);
return false;
}
},
onClickAddWidget: function(e) {
if (e) {
e.preventDefault();
}
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId, 'add_integ') :
null;
Modal.createDialog(IntegrationsManager, {
src: src,
}, "mx_IntegrationsManager");
},
render: function() {
const apps = this.state.apps.map(
(app, index, arr) => {
return (<AppTile
key={app.id}
id={app.id}
url={app.url}
name={app.name}
type={app.type}
fullWidth={arr.length<2 ? true : false}
room={this.props.room}
userId={this.props.userId}
/>);
});
const addWidget = this.state.apps && this.state.apps.length < 2 && this._canUserModify() &&
(<div onClick={this.onClickAddWidget}
role="button"
tabIndex="0"
className="mx_AddWidget_button"
title={_t('Add a widget')}>
[+] {_t('Add a widget')}
</div>);
return (
<div className="mx_AppsDrawer">
<div id="apps" className="mx_AppsContainer">
{apps}
</div>
{addWidget}
</div>
);
},
});

View file

@ -4,8 +4,9 @@ import classNames from 'classnames';
import flatMap from 'lodash/flatMap';
import isEqual from 'lodash/isEqual';
import sdk from '../../../index';
import type {Completion, SelectionRange} from '../../../autocomplete/Autocompleter';
import Q from 'q';
import type {Completion} from '../../../autocomplete/Autocompleter';
import Promise from 'bluebird';
import UserSettingsStore from '../../../UserSettingsStore';
import {getCompletions} from '../../../autocomplete/Autocompleter';
@ -39,26 +40,62 @@ export default class Autocomplete extends React.Component {
};
}
async componentWillReceiveProps(props, state) {
if (props.query === this.props.query) {
return null;
}
return await this.complete(props.query, props.selection);
}
async complete(query, selection) {
let forceComplete = this.state.forceComplete;
const completionPromise = getCompletions(query, selection, forceComplete);
this.completionPromise = completionPromise;
const completions = await this.completionPromise;
// There's a newer completion request, so ignore results.
if (completionPromise !== this.completionPromise) {
componentWillReceiveProps(newProps, state) {
// Query hasn't changed so don't try to complete it
if (newProps.query === this.props.query) {
return;
}
const completionList = flatMap(completions, provider => provider.completions);
this.complete(newProps.query, newProps.selection);
}
complete(query, selection) {
this.queryRequested = query;
if (this.debounceCompletionsRequest) {
clearTimeout(this.debounceCompletionsRequest);
}
if (query === "") {
this.setState({
// Clear displayed completions
completions: [],
completionList: [],
// Reset selected completion
selectionOffset: COMPOSER_SELECTED,
// Hide the autocomplete box
hide: true,
});
return Promise.resolve(null);
}
let autocompleteDelay = UserSettingsStore.getLocalSetting('autocompleteDelay', 200);
// Don't debounce if we are already showing completions
if (this.state.completions.length > 0 || this.state.forceComplete) {
autocompleteDelay = 0;
}
const deferred = Promise.defer();
this.debounceCompletionsRequest = setTimeout(() => {
this.processQuery(query, selection).then(() => {
deferred.resolve();
});
}, autocompleteDelay);
return deferred.promise;
}
processQuery(query, selection) {
return getCompletions(
query, selection, this.state.forceComplete,
).then((completions) => {
// Only ever process the completions for the most recent query being processed
if (query !== this.queryRequested) {
return;
}
this.processCompletions(completions);
});
}
processCompletions(completions) {
const completionList = flatMap(completions, (provider) => provider.completions);
// Reset selection when completion list becomes empty.
let selectionOffset = COMPOSER_SELECTED;
@ -69,33 +106,26 @@ export default class Autocomplete extends React.Component {
const currentSelection = this.state.selectionOffset === 0 ? null :
this.state.completionList[this.state.selectionOffset - 1].completion;
selectionOffset = completionList.findIndex(
completion => completion.completion === currentSelection);
(completion) => completion.completion === currentSelection);
if (selectionOffset === -1) {
selectionOffset = COMPOSER_SELECTED;
} else {
selectionOffset++; // selectionOffset is 1-indexed!
}
} else {
// If no completions were returned, we should turn off force completion.
forceComplete = false;
}
let hide = this.state.hide;
// These are lists of booleans that indicate whether whether the corresponding provider had a matching pattern
const oldMatches = this.state.completions.map(completion => !!completion.command.command),
newMatches = completions.map(completion => !!completion.command.command);
// So, essentially, we re-show autocomplete if any provider finds a new pattern or stops finding an old one
if (!isEqual(oldMatches, newMatches)) {
hide = false;
}
// If `completion.command.command` is truthy, then a provider has matched with the query
const anyMatches = completions.some((completion) => !!completion.command.command);
hide = !anyMatches;
this.setState({
completions,
completionList,
selectionOffset,
hide,
forceComplete,
// Force complete is turned off each time since we can't edit the query in that case
forceComplete: false,
});
}
@ -146,12 +176,13 @@ export default class Autocomplete extends React.Component {
}
forceComplete() {
const done = Q.defer();
const done = Promise.defer();
this.setState({
forceComplete: true,
hide: false,
}, () => {
this.complete(this.props.query, this.props.selection).then(() => {
done.resolve();
done.resolve(this.countCompletions());
});
});
return done.promise;
@ -169,7 +200,7 @@ export default class Autocomplete extends React.Component {
}
setSelection(selectionOffset: number) {
this.setState({selectionOffset});
this.setState({selectionOffset, hide: false});
}
componentDidUpdate() {
@ -185,21 +216,24 @@ export default class Autocomplete extends React.Component {
}
}
setState(state, func) {
super.setState(state, func);
}
render() {
const EmojiText = sdk.getComponent('views.elements.EmojiText');
let position = 1;
let renderedCompletions = this.state.completions.map((completionResult, i) => {
let completions = completionResult.completions.map((completion, i) => {
const renderedCompletions = this.state.completions.map((completionResult, i) => {
const completions = completionResult.completions.map((completion, i) => {
const className = classNames('mx_Autocomplete_Completion', {
'selected': position === this.state.selectionOffset,
});
let componentPosition = position;
const componentPosition = position;
position++;
let onMouseOver = () => this.setSelection(componentPosition);
let onClick = () => {
const onMouseOver = () => this.setSelection(componentPosition);
const onClick = () => {
this.setSelection(componentPosition);
this.onCompletionClicked();
};
@ -220,7 +254,7 @@ export default class Autocomplete extends React.Component {
{completionResult.provider.renderCompletions(completions)}
</div>
) : null;
}).filter(completion => !!completion);
}).filter((completion) => !!completion);
return !this.state.hide && renderedCompletions.length > 0 ? (
<div className="mx_Autocomplete" ref={(e) => this.container = e}>

View file

@ -14,11 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require('react');
var MatrixClientPeg = require("../../../MatrixClientPeg");
var sdk = require('../../../index');
var dis = require("../../../dispatcher");
var ObjectUtils = require('../../../ObjectUtils');
import React from 'react';
import MatrixClientPeg from "../../../MatrixClientPeg";
import sdk from '../../../index';
import dis from "../../../dispatcher";
import ObjectUtils from '../../../ObjectUtils';
import AppsDrawer from './AppsDrawer';
import { _t, _tJsx} from '../../../languageHandler';
import UserSettingsStore from '../../../UserSettingsStore';
module.exports = React.createClass({
displayName: 'AuxPanel',
@ -26,6 +30,8 @@ module.exports = React.createClass({
propTypes: {
// js-sdk room object
room: React.PropTypes.object.isRequired,
userId: React.PropTypes.string.isRequired,
showApps: React.PropTypes.bool,
// Conference Handler implementation
conferenceHandler: React.PropTypes.object,
@ -68,45 +74,53 @@ module.exports = React.createClass({
},
render: function() {
var CallView = sdk.getComponent("voip.CallView");
var TintableSvg = sdk.getComponent("elements.TintableSvg");
const CallView = sdk.getComponent("voip.CallView");
const TintableSvg = sdk.getComponent("elements.TintableSvg");
var fileDropTarget = null;
let fileDropTarget = null;
if (this.props.draggingFile) {
fileDropTarget = (
<div className="mx_RoomView_fileDropTarget">
<div className="mx_RoomView_fileDropTargetLabel"
title="Drop File Here">
title={_t("Drop File Here")}>
<TintableSvg src="img/upload-big.svg" width="45" height="59"/>
<br/>
Drop file here to upload
{_t("Drop file here to upload")}
</div>
</div>
);
}
var conferenceCallNotification = null;
let conferenceCallNotification = null;
if (this.props.displayConfCallNotification) {
var supportedText, joinText;
let supportedText = '';
let joinNode;
if (!MatrixClientPeg.get().supportsVoip()) {
supportedText = " (unsupported)";
}
else {
joinText = (<span>
Join as <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}}
href="#">voice</a> or <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video'); }}
href="#">video</a>.
supportedText = _t(" (unsupported)");
} else {
joinNode = (<span>
{_tJsx(
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
[/<voiceText>(.*?)<\/voiceText>/, /<videoText>(.*?)<\/videoText>/],
[
(sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}} href="#">{sub}</a>,
(sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video');}} href="#">{sub}</a>,
]
)}
</span>);
}
// XXX: the translation here isn't great: appending ' (unsupported)' is likely to not make sense in many languages,
// but there are translations for this in the languages we do have so I'm leaving it for now.
conferenceCallNotification = (
<div className="mx_RoomView_ongoingConfCallNotification">
Ongoing conference call{ supportedText }. { joinText }
{_t("Ongoing conference call%(supportedText)s.", {supportedText: supportedText})}
&nbsp;
{joinNode}
</div>
);
}
var callView = (
const callView = (
<CallView ref="callView" room={this.props.room}
ConferenceHandler={this.props.conferenceHandler}
onResize={this.props.onResize}
@ -114,8 +128,17 @@ module.exports = React.createClass({
/>
);
let appsDrawer = null;
if(UserSettingsStore.isFeatureEnabled('matrix_apps') && this.props.showApps) {
appsDrawer = <AppsDrawer ref="appsDrawer"
room={this.props.room}
userId={this.props.userId}
maxHeight={this.props.maxHeight}/>;
}
return (
<div className="mx_RoomView_auxPanel" style={{maxHeight: this.props.maxHeight}} >
{ appsDrawer }
{ fileDropTarget }
{ callView }
{ conferenceCallNotification }

View file

@ -21,6 +21,7 @@ var React = require('react');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index');
import AccessibleButton from '../elements/AccessibleButton';
import { _t } from '../../../languageHandler';
var PRESENCE_CLASS = {
@ -115,7 +116,7 @@ module.exports = React.createClass({
nameEl = (
<div className="mx_EntityTile_details">
<img className="mx_EntityTile_chevron" src="img/member_chevron.png" width="8" height="12"/>
<EmojiText element="div" className="mx_EntityTile_name_hover">{name}</EmojiText>
<EmojiText element="div" className="mx_EntityTile_name_hover" dir="auto">{name}</EmojiText>
<PresenceLabel activeAgo={ activeAgo }
currentlyActive={this.props.presenceCurrentlyActive}
presenceState={this.props.presenceState} />
@ -124,7 +125,7 @@ module.exports = React.createClass({
}
else {
nameEl = (
<EmojiText element="div" className="mx_EntityTile_name">{name}</EmojiText>
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">{name}</EmojiText>
);
}
@ -140,10 +141,10 @@ module.exports = React.createClass({
var power;
var powerLevel = this.props.powerLevel;
if (powerLevel >= 50 && powerLevel < 99) {
power = <img src="img/mod.svg" className="mx_EntityTile_power" width="16" height="17" alt="Mod"/>;
power = <img src="img/mod.svg" className="mx_EntityTile_power" width="16" height="17" alt={_t("Moderator")}/>;
}
if (powerLevel >= 99) {
power = <img src="img/admin.svg" className="mx_EntityTile_power" width="16" height="17" alt="Admin"/>;
power = <img src="img/admin.svg" className="mx_EntityTile_power" width="16" height="17" alt={_t("Admin")}/>;
}

View file

@ -16,13 +16,15 @@ limitations under the License.
'use strict';
var React = require('react');
var classNames = require("classnames");
import { _t } from '../../../languageHandler';
var Modal = require('../../../Modal');
var sdk = require('../../../index');
var TextForEvent = require('../../../TextForEvent');
import WithMatrixClient from '../../../wrappers/WithMatrixClient';
import withMatrixClient from '../../../wrappers/withMatrixClient';
var ContextualMenu = require('../../structures/ContextualMenu');
import dis from '../../../dispatcher';
@ -36,10 +38,12 @@ var eventTileTypes = {
'm.call.answer' : 'messages.TextualEvent',
'm.call.hangup' : 'messages.TextualEvent',
'm.room.name' : 'messages.TextualEvent',
'm.room.avatar' : 'messages.RoomAvatarEvent',
'm.room.topic' : 'messages.TextualEvent',
'm.room.third_party_invite' : 'messages.TextualEvent',
'm.room.history_visibility' : 'messages.TextualEvent',
'm.room.encryption' : 'messages.TextualEvent',
'm.room.power_levels' : 'messages.TextualEvent',
};
var MAX_READ_AVATARS = 5;
@ -55,7 +59,7 @@ var MAX_READ_AVATARS = 5;
// | '--------------------------------------' |
// '----------------------------------------------------------'
module.exports = WithMatrixClient(React.createClass({
module.exports = withMatrixClient(React.createClass({
displayName: 'EventTile',
propTypes: {
@ -128,6 +132,9 @@ module.exports = WithMatrixClient(React.createClass({
* for now.
*/
tileShape: React.PropTypes.string,
// show twelve hour timestamps
isTwelveHour: React.PropTypes.bool,
},
getInitialState: function() {
@ -186,13 +193,12 @@ module.exports = WithMatrixClient(React.createClass({
}
},
_verifyEvent: function(mxEvent) {
var verified = null;
if (mxEvent.isEncrypted()) {
verified = this.props.matrixClient.isEventSenderVerified(mxEvent);
_verifyEvent: async function(mxEvent) {
if (!mxEvent.isEncrypted()) {
return;
}
const verified = await this.props.matrixClient.isEventSenderVerified(mxEvent);
this.setState({
verified: verified
});
@ -283,21 +289,17 @@ module.exports = WithMatrixClient(React.createClass({
},
getReadAvatars: function() {
// return early if there are no read receipts
if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
return (<span className="mx_EventTile_readAvatars"></span>);
}
const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
const avatars = [];
const receiptOffset = 15;
let left = 0;
// It's possible that the receipt was sent several days AFTER the event.
// If it is, we want to display the complete date along with the HH:MM:SS,
// rather than just HH:MM:SS.
let dayAfterEvent = new Date(this.props.mxEvent.getTs());
dayAfterEvent.setDate(dayAfterEvent.getDate() + 1);
dayAfterEvent.setHours(0);
dayAfterEvent.setMinutes(0);
dayAfterEvent.setSeconds(0);
let dayAfterEventTime = dayAfterEvent.getTime();
var receipts = this.props.readReceipts || [];
for (var i = 0; i < receipts.length; ++i) {
var receipt = receipts[i];
@ -333,7 +335,7 @@ module.exports = WithMatrixClient(React.createClass({
suppressAnimation={this._suppressReadReceiptAnimation}
onClick={this.toggleAllReadAvatars}
timestamp={receipt.ts}
showFullTimestamp={receipt.ts >= dayAfterEventTime}
showTwelveHour={this.props.isTwelveHour}
/>
);
}
@ -355,10 +357,10 @@ module.exports = WithMatrixClient(React.createClass({
},
onSenderProfileClick: function(event) {
var mxEvent = this.props.mxEvent;
const mxEvent = this.props.mxEvent;
dis.dispatch({
action: 'insert_displayname',
displayname: (mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()).replace(' (IRC)', ''),
action: 'insert_mention',
user_id: mxEvent.getSender(),
});
},
@ -379,6 +381,7 @@ module.exports = WithMatrixClient(React.createClass({
dis.dispatch({
action: 'view_room',
event_id: this.props.mxEvent.getId(),
highlighted: true,
room_id: this.props.mxEvent.getRoomId(),
});
},
@ -394,8 +397,7 @@ module.exports = WithMatrixClient(React.createClass({
var msgtype = content.msgtype;
var eventType = this.props.mxEvent.getType();
// Info messages are basically information about commands processed on a
// room, or emote messages
// Info messages are basically information about commands processed on a room
var isInfoMessage = (eventType !== 'm.room.message');
var EventTileType = sdk.getComponent(eventTileTypes[eventType]);
@ -409,9 +411,10 @@ module.exports = WithMatrixClient(React.createClass({
var isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
const isRedacted = (eventType === 'm.room.message') && this.props.isRedacted;
var classes = classNames({
const classes = classNames({
mx_EventTile: true,
mx_EventTile_info: isInfoMessage,
mx_EventTile_12hr: this.props.isTwelveHour,
mx_EventTile_encrypting: this.props.eventSendStatus == 'encrypting',
mx_EventTile_sending: isSending,
mx_EventTile_notSent: this.props.eventSendStatus == 'not_sent',
@ -423,7 +426,8 @@ module.exports = WithMatrixClient(React.createClass({
menu: this.state.menu,
mx_EventTile_verified: this.state.verified == true,
mx_EventTile_unverified: this.state.verified == false,
mx_EventTile_bad: this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted',
mx_EventTile_bad: msgtype === 'm.bad.encrypted',
mx_EventTile_emote: msgtype === 'm.emote',
mx_EventTile_redacted: isRedacted,
});
@ -468,9 +472,9 @@ module.exports = WithMatrixClient(React.createClass({
if (needsSenderProfile) {
let aux = null;
if (!this.props.tileShape) {
if (msgtype === 'm.image') aux = "sent an image";
else if (msgtype === 'm.video') aux = "sent a video";
else if (msgtype === 'm.file') aux = "uploaded a file";
if (msgtype === 'm.image') aux = _t('sent an image');
else if (msgtype === 'm.video') aux = _t('sent a video');
else if (msgtype === 'm.file') aux = _t('uploaded a file');
sender = <SenderProfile onClick={ this.onSenderProfileClick } mxEvent={this.props.mxEvent} aux={aux} />;
}
else {
@ -478,36 +482,34 @@ module.exports = WithMatrixClient(React.createClass({
}
}
var editButton = (
<span className="mx_EventTile_editButton" title="Options" onClick={this.onEditClicked} />
const editButton = (
<span className="mx_EventTile_editButton" title={ _t("Options") } onClick={this.onEditClicked} />
);
var e2e;
let e2e;
// cosmetic padlocks:
if ((e2eEnabled && this.props.eventSendStatus) || this.props.mxEvent.getType() === 'm.room.encryption') {
e2e = <img style={{ cursor: 'initial', marginLeft: '-1px' }} className="mx_EventTile_e2eIcon" src="img/e2e-verified.svg" width="10" height="12" />;
e2e = <img style={{ cursor: 'initial', marginLeft: '-1px' }} className="mx_EventTile_e2eIcon" alt={_t("Encrypted by a verified device")} src="img/e2e-verified.svg" width="10" height="12" />;
}
// real padlocks
else if (this.props.mxEvent.isEncrypted() || (e2eEnabled && this.props.eventSendStatus)) {
if (this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted') {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} />;
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt={_t("Undecryptable")} src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} />;
}
else if (this.state.verified == true || (e2eEnabled && this.props.eventSendStatus)) {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" src="img/e2e-verified.svg" width="10" height="12"/>;
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt={_t("Encrypted by a verified device")} src="img/e2e-verified.svg" width="10" height="12"/>;
}
else {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" src="img/e2e-warning.svg" width="15" height="12" style={{ marginLeft: "-2px" }}/>;
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt={_t("Encrypted by an unverified device")} src="img/e2e-warning.svg" width="15" height="12" style={{ marginLeft: "-2px" }}/>;
}
}
else if (e2eEnabled) {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12"/>;
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt={_t("Unencrypted message")} src="img/e2e-unencrypted.svg" width="12" height="12"/>;
}
const timestamp = this.props.mxEvent.getTs() ?
<MessageTimestamp ts={this.props.mxEvent.getTs()} /> : null;
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
if (this.props.tileShape === "notif") {
var room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
return (
<div className={classes}>
<div className="mx_EventTile_roomName">

View file

@ -0,0 +1,68 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2017 Michael Telatynski
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 { _t } from '../../../languageHandler';
import dis from '../../../dispatcher';
import KeyCode from '../../../KeyCode';
module.exports = React.createClass({
displayName: 'ForwardMessage',
propTypes: {
onCancelClick: React.PropTypes.func.isRequired,
},
componentWillMount: function() {
dis.dispatch({
action: 'ui_opacity',
leftOpacity: 1.0,
rightOpacity: 0.3,
middleOpacity: 0.5,
});
},
componentDidMount: function() {
document.addEventListener('keydown', this._onKeyDown);
},
componentWillUnmount: function() {
dis.dispatch({
action: 'ui_opacity',
sideOpacity: 1.0,
middleOpacity: 1.0,
});
document.removeEventListener('keydown', this._onKeyDown);
},
_onKeyDown: function(ev) {
switch (ev.keyCode) {
case KeyCode.ESCAPE:
this.props.onCancelClick();
break;
}
},
render: function() {
return (
<div className="mx_ForwardMessage">
<h1>{_t('Please select the destination room for this message')}</h1>
</div>
);
},
});

View file

@ -100,7 +100,9 @@ module.exports = React.createClass({
render: function() {
var p = this.state.preview;
if (!p) return <div/>;
if (!p || Object.keys(p).length === 0) {
return <div/>;
}
// FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing?
var image = p["og:image"];

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default class MemberDeviceInfo extends React.Component {
render() {
@ -25,19 +26,19 @@ export default class MemberDeviceInfo extends React.Component {
if (this.props.device.isBlocked()) {
indicator = (
<div className="mx_MemberDeviceInfo_blacklisted">
<img src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} alt="Blacklisted"/>
<img src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} alt={_t("Blacklisted")}/>
</div>
);
} else if (this.props.device.isVerified()) {
indicator = (
<div className="mx_MemberDeviceInfo_verified">
<img src="img/e2e-verified.svg" width="10" height="12" alt="Verified"/>
<img src="img/e2e-verified.svg" width="10" height="12" alt={_t("Verified")}/>
</div>
);
} else {
indicator = (
<div className="mx_MemberDeviceInfo_unverified">
<img src="img/e2e-warning.svg" width="15" height="12" style={{ marginLeft: "-2px" }} alt="Unverified"/>
<img src="img/e2e-warning.svg" width="15" height="12" style={{ marginLeft: "-2px" }} alt={_t("Unverified")}/>
</div>
);
}
@ -49,7 +50,7 @@ export default class MemberDeviceInfo extends React.Component {
// add the deviceId as a titletext to help with debugging
return (
<div className="mx_MemberDeviceInfo"
title={"device id: " + this.props.device.deviceId} >
title={_t("device id: ") + this.props.device.deviceId} >
<div className="mx_MemberDeviceInfo_deviceInfo">
<div className="mx_MemberDeviceInfo_deviceId">
{deviceName}

View file

@ -31,14 +31,17 @@ import classNames from 'classnames';
import dis from '../../../dispatcher';
import Modal from '../../../Modal';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import createRoom from '../../../createRoom';
import DMRoomMap from '../../../utils/DMRoomMap';
import Unread from '../../../Unread';
import { findReadReceiptFromUserId } from '../../../utils/Receipt';
import WithMatrixClient from '../../../wrappers/WithMatrixClient';
import withMatrixClient from '../../../wrappers/withMatrixClient';
import AccessibleButton from '../elements/AccessibleButton';
import GeminiScrollbar from 'react-gemini-scrollbar';
module.exports = WithMatrixClient(React.createClass({
module.exports = withMatrixClient(React.createClass({
displayName: 'MemberInfo',
propTypes: {
@ -133,8 +136,12 @@ module.exports = WithMatrixClient(React.createClass({
if (userId == this.props.member.userId) {
// no need to re-download the whole thing; just update our copy of
// the list.
var devices = this.props.matrixClient.getStoredDevicesForUser(userId);
this.setState({devices: devices});
// Promise.resolve to handle transition from static result to promise; can be removed
// in future
Promise.resolve(this.props.matrixClient.getStoredDevicesForUser(userId)).then((devices) => {
this.setState({devices: devices});
});
}
},
@ -201,14 +208,15 @@ module.exports = WithMatrixClient(React.createClass({
var client = this.props.matrixClient;
var self = this;
client.downloadKeys([member.userId], true).finally(function() {
client.downloadKeys([member.userId], true).then(() => {
return client.getStoredDevicesForUser(member.userId);
}).finally(function() {
self._cancelDeviceList = null;
}).done(function() {
}).done(function(devices) {
if (cancelled) {
// we got cancelled - presumably a different user now
return;
}
var devices = client.getStoredDevicesForUser(member.userId);
self._disambiguateDevices(devices);
self.setState({devicesLoading: false, devices: devices});
}, function(err) {
@ -219,7 +227,7 @@ module.exports = WithMatrixClient(React.createClass({
onKick: function() {
const membership = this.props.member.membership;
const kickLabel = membership === "invite" ? "Disinvite" : "Kick";
const kickLabel = membership === "invite" ? _t("Disinvite") : _t("Kick");
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
Modal.createDialog(ConfirmUserActionDialog, {
member: this.props.member,
@ -241,8 +249,8 @@ module.exports = WithMatrixClient(React.createClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Kick error: " + err);
Modal.createDialog(ErrorDialog, {
title: "Error",
description: "Failed to kick user",
title: _t("Failed to kick"),
description: ((err && err.message) ? err.message : "Operation failed"),
});
}
).finally(()=>{
@ -256,7 +264,7 @@ module.exports = WithMatrixClient(React.createClass({
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
Modal.createDialog(ConfirmUserActionDialog, {
member: this.props.member,
action: this.props.member.membership == 'ban' ? 'Unban' : 'Ban',
action: this.props.member.membership == 'ban' ? _t("Unban") : _t("Ban"),
askReason: this.props.member.membership != 'ban',
danger: this.props.member.membership != 'ban',
onFinished: (proceed, reason) => {
@ -283,8 +291,8 @@ module.exports = WithMatrixClient(React.createClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Ban error: " + err);
Modal.createDialog(ErrorDialog, {
title: "Error",
description: "Failed to ban user",
title: _t("Error"),
description: _t("Failed to ban user"),
});
}
).finally(()=>{
@ -333,8 +341,8 @@ module.exports = WithMatrixClient(React.createClass({
}, function(err) {
console.error("Mute error: " + err);
Modal.createDialog(ErrorDialog, {
title: "Error",
description: "Failed to mute user",
title: _t("Error"),
description: _t("Failed to mute user"),
});
}
).finally(()=>{
@ -374,16 +382,12 @@ module.exports = WithMatrixClient(React.createClass({
console.log("Mod toggle success");
}, function(err) {
if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "This action cannot be performed by a guest user. Please register to be able to do this."
});
dis.dispatch({action: 'view_set_mxid'});
} else {
console.error("Toggle moderator error:" + err);
Modal.createDialog(ErrorDialog, {
title: "Error",
description: "Failed to toggle moderator status",
title: _t("Error"),
description: _t("Failed to toggle moderator status"),
});
}
}
@ -403,8 +407,8 @@ module.exports = WithMatrixClient(React.createClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to change power level " + err);
Modal.createDialog(ErrorDialog, {
title: "Error",
description: "Failed to change power level",
title: _t("Error"),
description: _t("Failed to change power level"),
});
}
).finally(()=>{
@ -432,13 +436,13 @@ module.exports = WithMatrixClient(React.createClass({
if (parseInt(myPower) === parseInt(powerLevel)) {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: "Warning",
title: _t("Warning!"),
description:
<div>
You will not be able to undo this change as you are promoting the user to have the same power level as yourself.<br/>
Are you sure?
{ _t("You will not be able to undo this change as you are promoting the user to have the same power level as yourself.") }<br/>
{ _t("Are you sure?") }
</div>,
button: "Continue",
button: _t("Continue"),
onFinished: function(confirmed) {
if (confirmed) {
self._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
@ -581,9 +585,9 @@ module.exports = WithMatrixClient(React.createClass({
// still loading
devComponents = <Spinner />;
} else if (devices === null) {
devComponents = "Unable to load device list";
devComponents = _t("Unable to load device list");
} else if (devices.length === 0) {
devComponents = "No devices with registered encryption keys";
devComponents = _t("No devices with registered encryption keys");
} else {
devComponents = [];
for (var i = 0; i < devices.length; i++) {
@ -595,7 +599,7 @@ module.exports = WithMatrixClient(React.createClass({
return (
<div>
<h3>Devices</h3>
<h3>{ _t("Devices") }</h3>
<div className="mx_MemberInfo_devices">
{devComponents}
</div>
@ -644,11 +648,11 @@ module.exports = WithMatrixClient(React.createClass({
<div className="mx_RoomTile_avatar">
<img src="img/create-big.svg" width="26" height="26" />
</div>
<div className={labelClasses}><i>Start new chat</i></div>
<div className={labelClasses}><i>{ _t("Start a chat") }</i></div>
</AccessibleButton>;
startChat = <div>
<h3>Direct chats</h3>
<h3>{ _t("Direct chats") }</h3>
{tiles}
{startNewChat}
</div>;
@ -661,7 +665,7 @@ module.exports = WithMatrixClient(React.createClass({
if (this.state.can.kick) {
const membership = this.props.member.membership;
const kickLabel = membership === "invite" ? "Disinvite" : "Kick";
const kickLabel = membership === "invite" ? _t("Disinvite") : _t("Kick");
kickButton = (
<AccessibleButton className="mx_MemberInfo_field"
onClick={this.onKick}>
@ -670,9 +674,9 @@ module.exports = WithMatrixClient(React.createClass({
);
}
if (this.state.can.ban) {
let label = 'Ban';
let label = _t("Ban");
if (this.props.member.membership == 'ban') {
label = 'Unban';
label = _t("Unban");
}
banButton = (
<AccessibleButton className="mx_MemberInfo_field"
@ -682,7 +686,7 @@ module.exports = WithMatrixClient(React.createClass({
);
}
if (this.state.can.mute) {
const muteLabel = this.state.muted ? "Unmute" : "Mute";
const muteLabel = this.state.muted ? _t("Unmute") : _t("Mute");
muteButton = (
<AccessibleButton className="mx_MemberInfo_field"
onClick={this.onMuteToggle}>
@ -691,7 +695,7 @@ module.exports = WithMatrixClient(React.createClass({
);
}
if (this.state.can.toggleMod) {
var giveOpLabel = this.state.isTargetMod ? "Revoke Moderator" : "Make Moderator";
var giveOpLabel = this.state.isTargetMod ? _t("Revoke Moderator") : _t("Make Moderator");
giveModButton = <AccessibleButton className="mx_MemberInfo_field" onClick={this.onModToggle}>
{giveOpLabel}
</AccessibleButton>;
@ -704,7 +708,7 @@ module.exports = WithMatrixClient(React.createClass({
if (kickButton || banButton || muteButton || giveModButton) {
adminTools =
<div>
<h3>Admin tools</h3>
<h3>{_t("Admin tools")}</h3>
<div className="mx_MemberInfo_buttons">
{muteButton}
@ -717,34 +721,49 @@ module.exports = WithMatrixClient(React.createClass({
const memberName = this.props.member.name;
if (this.props.member.user) {
var presenceState = this.props.member.user.presence;
var presenceLastActiveAgo = this.props.member.user.lastActiveAgo;
var presenceLastTs = this.props.member.user.lastPresenceTs;
var presenceCurrentlyActive = this.props.member.user.currentlyActive;
}
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
var PowerSelector = sdk.getComponent('elements.PowerSelector');
var PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
const EmojiText = sdk.getComponent('elements.EmojiText');
return (
<div className="mx_MemberInfo">
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}> <img src="img/cancel.svg" width="18" height="18"/></AccessibleButton>
<div className="mx_MemberInfo_avatar">
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />
</div>
<EmojiText element="h2">{memberName}</EmojiText>
<div className="mx_MemberInfo_profile">
<div className="mx_MemberInfo_profileField">
{ this.props.member.userId }
<GeminiScrollbar autoshow={true}>
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}> <img src="img/cancel.svg" width="18" height="18"/></AccessibleButton>
<div className="mx_MemberInfo_avatar">
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />
</div>
<div className="mx_MemberInfo_profileField">
Level: <b><PowerSelector controlled={true} value={ parseInt(this.props.member.powerLevel) } disabled={ !this.state.can.modifyLevel } onChange={ this.onPowerChange }/></b>
<EmojiText element="h2">{memberName}</EmojiText>
<div className="mx_MemberInfo_profile">
<div className="mx_MemberInfo_profileField">
{ this.props.member.userId }
</div>
<div className="mx_MemberInfo_profileField">
{ _t("Level:") } <b><PowerSelector controlled={true} value={ parseInt(this.props.member.powerLevel) } disabled={ !this.state.can.modifyLevel } onChange={ this.onPowerChange }/></b>
</div>
<div className="mx_MemberInfo_profileField">
<PresenceLabel activeAgo={ presenceLastActiveAgo }
currentlyActive={ presenceCurrentlyActive }
presenceState={ presenceState } />
</div>
</div>
</div>
{ adminTools }
{ adminTools }
{ startChat }
{ startChat }
{ this._renderDevices() }
{ this._renderDevices() }
{ spinner }
{ spinner }
</GeminiScrollbar>
</div>
);
}

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.
@ -14,9 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require('react');
import { _t } from '../../../languageHandler';
var classNames = require('classnames');
var Matrix = require("matrix-js-sdk");
var q = require('q');
import Promise from 'bluebird';
var MatrixClientPeg = require("../../../MatrixClientPeg");
var Modal = require("../../../Modal");
var Entities = require("../../../Entities");
@ -27,12 +29,6 @@ var CallHandler = require("../../../CallHandler");
var Invite = require("../../../Invite");
var INITIAL_LOAD_NUM_MEMBERS = 30;
var SHARE_HISTORY_WARNING =
<span>
Newly invited users will see the history of this room. <br/>
If you'd prefer invited users not to see messages that were sent before they joined, <br/>
turn off, 'Share message history with new users' in the settings for this room.
</span>;
module.exports = React.createClass({
displayName: 'MemberList',
@ -205,9 +201,9 @@ module.exports = React.createClass({
_createOverflowTile: function(overflowCount, totalCount) {
// For now we'll pretend this is any entity. It should probably be a separate tile.
var EntityTile = sdk.getComponent("rooms.EntityTile");
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
var text = "and " + overflowCount + " other" + (overflowCount > 1 ? "s" : "") + "...";
const EntityTile = sdk.getComponent("rooms.EntityTile");
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
const text = _t("and %(count)s others...", { count: overflowCount });
return (
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
<BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} />
@ -352,7 +348,7 @@ module.exports = React.createClass({
if (invitedMemberTiles.length > 0) {
invitedSection = (
<div className="mx_MemberList_invited">
<h2>Invited</h2>
<h2>{ _t("Invited") }</h2>
<div className="mx_MemberList_wrapper">
{invitedMemberTiles}
</div>
@ -363,8 +359,8 @@ module.exports = React.createClass({
var inputBox = (
<form autoComplete="off">
<input className="mx_MemberList_query" id="mx_MemberList_query" type="text"
onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
placeholder="Filter room members" />
onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
placeholder={ _t('Filter room members') } />
</form>
);

View file

@ -22,6 +22,7 @@ var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index');
var dis = require('../../../dispatcher');
var Modal = require("../../../Modal");
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'MemberTile',
@ -63,7 +64,7 @@ module.exports = React.createClass({
},
getPowerLabel: function() {
return this.props.member.userId + " (power " + this.props.member.powerLevel + ")";
return _t("%(userName)s (power %(powerLevelNumber)s)", {userName: this.props.member.userId, powerLevelNumber: this.props.member.powerLevel});
},
render: function() {

View file

@ -13,16 +13,14 @@ 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 CallHandler = require('../../../CallHandler');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var Modal = require('../../../Modal');
var sdk = require('../../../index');
var dis = require('../../../dispatcher');
import React from 'react';
import { _t } from '../../../languageHandler';
import CallHandler from '../../../CallHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import Autocomplete from './Autocomplete';
import classNames from 'classnames';
import UserSettingsStore from '../../../UserSettingsStore';
@ -32,12 +30,12 @@ export default class MessageComposer extends React.Component {
this.onCallClick = this.onCallClick.bind(this);
this.onHangupClick = this.onHangupClick.bind(this);
this.onUploadClick = this.onUploadClick.bind(this);
this.onShowAppsClick = this.onShowAppsClick.bind(this);
this.onHideAppsClick = this.onHideAppsClick.bind(this);
this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
this.uploadFiles = this.uploadFiles.bind(this);
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
this.onInputContentChanged = this.onInputContentChanged.bind(this);
this.onUpArrow = this.onUpArrow.bind(this);
this.onDownArrow = this.onDownArrow.bind(this);
this._tryComplete = this._tryComplete.bind(this);
this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this);
this.onToggleFormattingClicked = this.onToggleFormattingClicked.bind(this);
this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this);
@ -50,12 +48,11 @@ export default class MessageComposer extends React.Component {
inputState: {
style: [],
blockType: null,
isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true),
isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false),
wordCount: 0,
},
showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false),
};
}
componentDidMount() {
@ -80,36 +77,33 @@ export default class MessageComposer extends React.Component {
onUploadClick(ev) {
if (MatrixClientPeg.get().isGuest()) {
let NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "Guest users can't upload files. Please register to upload.",
});
dis.dispatch({action: 'view_set_mxid'});
return;
}
this.refs.uploadInput.click();
}
onUploadFileSelected(files, isPasted) {
if (!isPasted)
files = files.target.files;
onUploadFileSelected(files) {
this.uploadFiles(files.target.files);
}
uploadFiles(files) {
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
let TintableSvg = sdk.getComponent("elements.TintableSvg");
let fileList = [];
for (let i=0; i<files.length; i++) {
fileList.push(<li key={i}>
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> {files[i].name || 'Attachment'}
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> {files[i].name || _t('Attachment')}
</li>);
}
Modal.createDialog(QuestionDialog, {
title: "Upload Files",
title: _t('Upload Files'),
description: (
<div>
<p>Are you sure you want upload the following files?</p>
<p>{ _t('Are you sure you want to upload the following files?') }</p>
<ul style={{listStyle: 'none', textAlign: 'left'}}>
{fileList}
</ul>
@ -119,7 +113,7 @@ export default class MessageComposer extends React.Component {
if(shouldUpload) {
// MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file
if (files) {
for(var i=0; i<files.length; i++) {
for(let i=0; i<files.length; i++) {
this.props.uploadFile(files[i]);
}
}
@ -131,7 +125,7 @@ export default class MessageComposer extends React.Component {
}
onHangupClick() {
var call = CallHandler.getCallForRoom(this.props.room.roomId);
const call = CallHandler.getCallForRoom(this.props.room.roomId);
//var call = CallHandler.getAnyActiveCall();
if (!call) {
return;
@ -144,20 +138,68 @@ export default class MessageComposer extends React.Component {
});
}
// _startCallApp(isAudioConf) {
// dis.dispatch({
// action: 'appsDrawer',
// show: true,
// });
// const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets', '');
// let appsStateEvent = {};
// if (appsStateEvents) {
// appsStateEvent = appsStateEvents.getContent();
// }
// if (!appsStateEvent.videoConf) {
// appsStateEvent.videoConf = {
// type: 'jitsi',
// // FIXME -- This should not be localhost
// url: 'http://localhost:8000/jitsi.html',
// data: {
// confId: this.props.room.roomId.replace(/[^A-Za-z0-9]/g, '_') + Date.now(),
// isAudioConf: isAudioConf,
// },
// };
// MatrixClientPeg.get().sendStateEvent(
// this.props.room.roomId,
// 'im.vector.modular.widgets',
// appsStateEvent,
// '',
// ).then(() => console.log('Sent state'), (e) => console.error(e));
// }
// }
onCallClick(ev) {
// NOTE -- Will be replaced by Jitsi code (currently commented)
dis.dispatch({
action: 'place_call',
type: ev.shiftKey ? "screensharing" : "video",
room_id: this.props.room.roomId,
});
// this._startCallApp(false);
}
onVoiceCallClick(ev) {
// NOTE -- Will be replaced by Jitsi code (currently commented)
dis.dispatch({
action: 'place_call',
type: 'voice',
type: "voice",
room_id: this.props.room.roomId,
});
// this._startCallApp(true);
}
onShowAppsClick(ev) {
dis.dispatch({
action: 'appsDrawer',
show: true,
});
}
onHideAppsClick(ev) {
dis.dispatch({
action: 'appsDrawer',
show: false,
});
}
onInputContentChanged(content: string, selection: {start: number, end: number}) {
@ -171,21 +213,6 @@ export default class MessageComposer extends React.Component {
this.setState({inputState});
}
onUpArrow() {
return this.refs.autocomplete.onUpArrow();
}
onDownArrow() {
return this.refs.autocomplete.onDownArrow();
}
_tryComplete(): boolean {
if (this.refs.autocomplete) {
return this.refs.autocomplete.onCompletionClicked();
}
return false;
}
_onAutocompleteConfirm(range, completion) {
if (this.messageComposerInput) {
this.messageComposerInput.setDisplayedCompletion(range, completion);
@ -208,19 +235,18 @@ export default class MessageComposer extends React.Component {
}
render() {
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
var uploadInputStyle = {display: 'none'};
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
var TintableSvg = sdk.getComponent("elements.TintableSvg");
var MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput" +
(UserSettingsStore.isFeatureEnabled('rich_text_editor') ? "" : "Old"));
const me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
const uploadInputStyle = {display: 'none'};
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput");
var controls = [];
const controls = [];
controls.push(
<div key="controls_avatar" className="mx_MessageComposer_avatar">
<MemberAvatar member={me} width={24} height={24} />
</div>
</div>,
);
let e2eImg, e2eTitle, e2eClass;
@ -228,47 +254,61 @@ export default class MessageComposer extends React.Component {
if (roomIsEncrypted) {
// FIXME: show a /!\ if there are untrusted devices in the room...
e2eImg = 'img/e2e-verified.svg';
e2eTitle = 'Encrypted room';
e2eTitle = _t('Encrypted room');
e2eClass = 'mx_MessageComposer_e2eIcon';
} else {
e2eImg = 'img/e2e-unencrypted.svg';
e2eTitle = 'Unencrypted room';
e2eTitle = _t('Unencrypted room');
e2eClass = 'mx_MessageComposer_e2eIcon mx_filterFlipColor';
}
controls.push(
<img key="e2eIcon" className={e2eClass} src={e2eImg} width="12" height="12"
alt={e2eTitle} title={e2eTitle}
/>
/>,
);
var callButton, videoCallButton, hangupButton;
let callButton, videoCallButton, hangupButton, showAppsButton, hideAppsButton;
if (this.props.callState && this.props.callState !== 'ended') {
hangupButton =
<div key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
<img src="img/hangup.svg" alt="Hangup" title="Hangup" width="25" height="26"/>
<img src="img/hangup.svg" alt={ _t('Hangup') } title={ _t('Hangup') } width="25" height="26"/>
</div>;
}
else {
} else {
callButton =
<div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title="Voice call">
<div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title={ _t('Voice call') }>
<TintableSvg src="img/icon-call.svg" width="35" height="35"/>
</div>;
videoCallButton =
<div key="controls_videocall" className="mx_MessageComposer_videocall" onClick={this.onCallClick} title="Video call">
<div key="controls_videocall" className="mx_MessageComposer_videocall" onClick={this.onCallClick} title={ _t('Video call') }>
<TintableSvg src="img/icons-video.svg" width="35" height="35"/>
</div>;
}
var canSendMessages = this.props.room.currentState.maySendMessage(
// Apps
if (UserSettingsStore.isFeatureEnabled('matrix_apps')) {
if (this.props.showApps) {
hideAppsButton =
<div key="controls_hide_apps" className="mx_MessageComposer_apps" onClick={this.onHideAppsClick} title={_t("Hide Apps")}>
<TintableSvg src="img/icons-apps-active.svg" width="35" height="35"/>
</div>;
} else {
showAppsButton =
<div key="show_apps" className="mx_MessageComposer_apps" onClick={this.onShowAppsClick} title={_t("Show Apps")}>
<TintableSvg src="img/icons-apps.svg" width="35" height="35"/>
</div>;
}
}
const canSendMessages = this.props.room.currentState.maySendMessage(
MatrixClientPeg.get().credentials.userId);
if (canSendMessages) {
// This also currently includes the call buttons. Really we should
// check separately for whether we can call, but this is slightly
// complex because of conference calls.
var uploadButton = (
const uploadButton = (
<div key="controls_upload" className="mx_MessageComposer_upload"
onClick={this.onUploadClick} title="Upload file">
onClick={this.onUploadClick} title={ _t('Upload file') }>
<TintableSvg src="img/icons-upload.svg" width="35" height="35"/>
<input ref="uploadInput" type="file"
style={uploadInputStyle}
@ -279,71 +319,52 @@ export default class MessageComposer extends React.Component {
const formattingButton = (
<img className="mx_MessageComposer_formatting"
title="Show Text Formatting Toolbar"
title={_t("Show Text Formatting Toolbar")}
src="img/button-text-formatting.svg"
onClick={this.onToggleFormattingClicked}
style={{visibility: this.state.showFormatting ||
!UserSettingsStore.isFeatureEnabled('rich_text_editor') ? 'hidden' : 'visible'}}
style={{visibility: this.state.showFormatting ? 'hidden' : 'visible'}}
key="controls_formatting" />
);
const placeholderText = roomIsEncrypted ?
"Send an encrypted message…" : "Send a message (unencrypted)…";
_t('Send an encrypted message') + '…' : _t('Send a message (unencrypted)') + '…';
controls.push(
<MessageComposerInput
ref={c => this.messageComposerInput = c}
ref={(c) => this.messageComposerInput = c}
key="controls_input"
onResize={this.props.onResize}
room={this.props.room}
placeholder={placeholderText}
tryComplete={this._tryComplete}
onUpArrow={this.onUpArrow}
onDownArrow={this.onDownArrow}
onUploadFileSelected={this.onUploadFileSelected}
tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
onFilesPasted={this.uploadFiles}
onContentChanged={this.onInputContentChanged}
onInputStateChanged={this.onInputStateChanged} />,
formattingButton,
uploadButton,
hangupButton,
callButton,
videoCallButton
videoCallButton,
showAppsButton,
hideAppsButton,
);
} else {
controls.push(
<div key="controls_error" className="mx_MessageComposer_noperm_error">
You do not have permission to post to this room
</div>
{ _t('You do not have permission to post to this room') }
</div>,
);
}
let autoComplete;
if (UserSettingsStore.isFeatureEnabled('rich_text_editor')) {
autoComplete = <div className="mx_MessageComposer_autocomplete_wrapper">
<Autocomplete
ref="autocomplete"
onConfirm={this._onAutocompleteConfirm}
query={this.state.autocompleteQuery}
selection={this.state.selection} />
</div>;
}
const {style, blockType} = this.state.inputState;
const formatButtons = ["bold", "italic", "strike", "underline", "code", "quote", "bullet", "numbullet"].map(
name => {
(name) => {
const active = style.includes(name) || blockType === name;
const suffix = active ? '-o-n' : '';
const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
const disabled = !this.state.inputState.isRichtextEnabled && 'underline' === name;
const className = classNames("mx_MessageComposer_format_button", {
mx_MessageComposer_format_button_disabled: disabled,
mx_filterFlipColor: true,
});
const className = 'mx_MessageComposer_format_button mx_filterFlipColor';
return <img className={className}
title={name}
onMouseDown={disabled ? null : onFormatButtonClicked}
title={ _t(name) }
onMouseDown={onFormatButtonClicked}
key={name}
src={`img/button-text-${name}${suffix}.svg`}
height="17" />;
@ -357,30 +378,26 @@ export default class MessageComposer extends React.Component {
{controls}
</div>
</div>
{UserSettingsStore.isFeatureEnabled('rich_text_editor') ?
<div className="mx_MessageComposer_formatbar_wrapper">
<div className="mx_MessageComposer_formatbar" style={this.state.showFormatting ? {} : {display: 'none'}}>
{formatButtons}
<div style={{flex: 1}}></div>
<img title={`Turn Markdown ${this.state.inputState.isRichtextEnabled ? 'on' : 'off'}`}
onMouseDown={this.onToggleMarkdownClicked}
className="mx_MessageComposer_formatbar_markdown mx_filterFlipColor"
src={`img/button-md-${!this.state.inputState.isRichtextEnabled}.png`} />
<img title="Hide Text Formatting Toolbar"
onClick={this.onToggleFormattingClicked}
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
src="img/icon-text-cancel.svg" />
</div>
</div>: null
}
<div className="mx_MessageComposer_formatbar_wrapper">
<div className="mx_MessageComposer_formatbar" style={this.state.showFormatting ? {} : {display: 'none'}}>
{formatButtons}
<div style={{flex: 1}}></div>
<img title={ this.state.inputState.isRichtextEnabled ? _t("Turn Markdown on") : _t("Turn Markdown off") }
onMouseDown={this.onToggleMarkdownClicked}
className="mx_MessageComposer_formatbar_markdown mx_filterFlipColor"
src={`img/button-md-${!this.state.inputState.isRichtextEnabled}.png`} />
<img title={ _t("Hide Text Formatting Toolbar") }
onClick={this.onToggleFormattingClicked}
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
src="img/icon-text-cancel.svg" />
</div>
</div>
</div>
);
}
}
MessageComposer.propTypes = {
tabComplete: React.PropTypes.any,
// a callback which is called when the height of the composer is
// changed due to a change in content.
onResize: React.PropTypes.func,
@ -395,5 +412,8 @@ MessageComposer.propTypes = {
uploadFile: React.PropTypes.func.isRequired,
// opacity for dynamic UI fading effects
opacity: React.PropTypes.number
opacity: React.PropTypes.number,
// string representing the current room app drawer state
showApps: React.PropTypes.bool,
};

File diff suppressed because it is too large Load diff

View file

@ -1,447 +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 MatrixClientPeg = require("../../../MatrixClientPeg");
var SlashCommands = require("../../../SlashCommands");
var Modal = require("../../../Modal");
var MemberEntry = require("../../../TabCompleteEntries").MemberEntry;
var sdk = require('../../../index');
var dis = require("../../../dispatcher");
var KeyCode = require("../../../KeyCode");
var Markdown = require("../../../Markdown");
var TYPING_USER_TIMEOUT = 10000;
var TYPING_SERVER_TIMEOUT = 30000;
var MARKDOWN_ENABLED = true;
export function onSendMessageFailed(err, room) {
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148
console.log('MessageComposer got send failure: ' + err.name + '('+err+')');
if (err.name === "UnknownDeviceError") {
dis.dispatch({
action: 'unknown_device_error',
err: err,
room: room,
});
}
dis.dispatch({
action: 'message_send_failed',
});
}
/*
* The textInput part of the MessageComposer
*/
export default React.createClass({
displayName: 'MessageComposerInput',
statics: {
// the height we limit the composer to
MAX_HEIGHT: 100,
},
propTypes: {
tabComplete: React.PropTypes.any,
// a callback which is called when the height of the composer is
// changed due to a change in content.
onResize: React.PropTypes.func,
// js-sdk Room object
room: React.PropTypes.object.isRequired,
// The text to use a placeholder in the input box
placeholder: React.PropTypes.string.isRequired,
},
componentWillMount: function() {
this.oldScrollHeight = 0;
this.markdownEnabled = MARKDOWN_ENABLED;
var self = this;
this.sentHistory = {
// The list of typed messages. Index 0 is more recent
data: [],
// The position in data currently displayed
position: -1,
// The room the history is for.
roomId: null,
// The original text before they hit UP
originalText: null,
// The textarea element to set text to.
element: null,
init: function(element, roomId) {
this.roomId = roomId;
this.element = element;
this.position = -1;
var storedData = window.sessionStorage.getItem(
"history_" + roomId
);
if (storedData) {
this.data = JSON.parse(storedData);
}
if (this.roomId) {
this.setLastTextEntry();
}
},
push: function(text) {
// store a message in the sent history
this.data.unshift(text);
window.sessionStorage.setItem(
"history_" + this.roomId,
JSON.stringify(this.data)
);
// reset history position
this.position = -1;
this.originalText = null;
},
// move in the history. Returns true if we managed to move.
next: function(offset) {
if (this.position === -1) {
// user is going into the history, save the current line.
this.originalText = this.element.value;
}
else {
// user may have modified this line in the history; remember it.
this.data[this.position] = this.element.value;
}
if (offset > 0 && this.position === (this.data.length - 1)) {
// we've run out of history
return false;
}
// retrieve the next item (bounded).
var newPosition = this.position + offset;
newPosition = Math.max(-1, newPosition);
newPosition = Math.min(newPosition, this.data.length - 1);
this.position = newPosition;
if (this.position !== -1) {
// show the message
this.element.value = this.data[this.position];
}
else if (this.originalText !== undefined) {
// restore the original text the user was typing.
this.element.value = this.originalText;
}
self.resizeInput();
return true;
},
saveLastTextEntry: function() {
// save the currently entered text in order to restore it later.
// NB: This isn't 'originalText' because we want to restore
// sent history items too!
var text = this.element.value;
window.sessionStorage.setItem("input_" + this.roomId, text);
},
setLastTextEntry: function() {
var text = window.sessionStorage.getItem("input_" + this.roomId);
if (text) {
this.element.value = text;
self.resizeInput();
}
}
};
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
this.sentHistory.init(
this.refs.textarea,
this.props.room.roomId
);
this.resizeInput();
if (this.props.tabComplete) {
this.props.tabComplete.setTextArea(this.refs.textarea);
}
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
this.sentHistory.saveLastTextEntry();
},
onAction: function(payload) {
var textarea = this.refs.textarea;
switch (payload.action) {
case 'focus_composer':
textarea.focus();
break;
case 'insert_displayname':
if (textarea.value.length) {
var left = textarea.value.substring(0, textarea.selectionStart);
var right = textarea.value.substring(textarea.selectionEnd);
if (right.length) {
left += payload.displayname;
}
else {
left = left.replace(/( ?)$/, " " + payload.displayname);
}
textarea.value = left + right;
textarea.focus();
textarea.setSelectionRange(left.length, left.length);
}
else {
textarea.value = payload.displayname + ": ";
textarea.focus();
}
break;
}
},
onKeyDown: function(ev) {
if (ev.keyCode === KeyCode.ENTER && !ev.shiftKey) {
var input = this.refs.textarea.value;
if (input.length === 0) {
ev.preventDefault();
return;
}
this.sentHistory.push(input);
this.onEnter(ev);
}
else if (ev.keyCode === KeyCode.UP || ev.keyCode === KeyCode.DOWN) {
var oldSelectionStart = this.refs.textarea.selectionStart;
// Remember the keyCode because React will recycle the synthetic event
var keyCode = ev.keyCode;
// set a callback so we can see if the cursor position changes as
// a result of this event. If it doesn't, we cycle history.
setTimeout(() => {
if (this.refs.textarea.selectionStart == oldSelectionStart) {
this.sentHistory.next(keyCode === KeyCode.UP ? 1 : -1);
this.resizeInput();
}
}, 0);
}
if (this.props.tabComplete) {
this.props.tabComplete.onKeyDown(ev);
}
var self = this;
setTimeout(function() {
if (self.refs.textarea && self.refs.textarea.value != '') {
self.onTypingActivity();
} else {
self.onFinishedTyping();
}
}, 10); // XXX: what is this 10ms setTimeout doing? Looks hacky :(
},
resizeInput: function() {
// scrollHeight is at least equal to clientHeight, so we have to
// temporarily crimp clientHeight to 0 to get an accurate scrollHeight value
this.refs.textarea.style.height = "20px"; // 20 hardcoded from CSS
var newHeight = Math.min(this.refs.textarea.scrollHeight,
this.constructor.MAX_HEIGHT);
this.refs.textarea.style.height = Math.ceil(newHeight) + "px";
this.oldScrollHeight = this.refs.textarea.scrollHeight;
if (this.props.onResize) {
// kick gemini-scrollbar to re-layout
this.props.onResize();
}
},
onKeyUp: function(ev) {
if (this.refs.textarea.scrollHeight !== this.oldScrollHeight ||
ev.keyCode === KeyCode.DELETE ||
ev.keyCode === KeyCode.BACKSPACE)
{
this.resizeInput();
}
},
onEnter: function(ev) {
var contentText = this.refs.textarea.value;
// bodge for now to set markdown state on/off. We probably want a separate
// area for "local" commands which don't hit out to the server.
if (contentText.indexOf("/markdown") === 0) {
ev.preventDefault();
this.refs.textarea.value = '';
if (contentText.indexOf("/markdown on") === 0) {
this.markdownEnabled = true;
}
else if (contentText.indexOf("/markdown off") === 0) {
this.markdownEnabled = false;
}
else {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Unknown command",
description: "Usage: /markdown on|off"
});
}
return;
}
var cmd = SlashCommands.processInput(this.props.room.roomId, contentText);
if (cmd) {
ev.preventDefault();
if (!cmd.error) {
this.refs.textarea.value = '';
}
if (cmd.promise) {
cmd.promise.done(function() {
console.log("Command success.");
}, function(err) {
console.error("Command failure: %s", err);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Server error",
description: "Server unavailable, overloaded, or something else went wrong.",
});
});
}
else if (cmd.error) {
console.error(cmd.error);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Command error",
description: cmd.error
});
}
return;
}
var isEmote = /^\/me( |$)/i.test(contentText);
var sendMessagePromise;
if (isEmote) {
contentText = contentText.substring(4);
}
else if (contentText[0] === '/') {
contentText = contentText.substring(1);
}
let send_markdown = false;
let mdown;
if (this.markdownEnabled) {
mdown = new Markdown(contentText);
send_markdown = !mdown.isPlainText();
}
if (send_markdown) {
const htmlText = mdown.toHTML();
sendMessagePromise = isEmote ?
MatrixClientPeg.get().sendHtmlEmote(this.props.room.roomId, contentText, htmlText) :
MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, htmlText);
}
else {
if (mdown) contentText = mdown.toPlaintext();
sendMessagePromise = isEmote ?
MatrixClientPeg.get().sendEmoteMessage(this.props.room.roomId, contentText) :
MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText);
}
sendMessagePromise.done(function(res) {
dis.dispatch({
action: 'message_sent'
});
}, (e) => onSendMessageFailed(e, this.props.room));
this.refs.textarea.value = '';
this.resizeInput();
ev.preventDefault();
},
onTypingActivity: function() {
this.isTyping = true;
if (!this.userTypingTimer) {
this.sendTyping(true);
}
this.startUserTypingTimer();
this.startServerTypingTimer();
},
onFinishedTyping: function() {
this.isTyping = false;
this.sendTyping(false);
this.stopUserTypingTimer();
this.stopServerTypingTimer();
},
startUserTypingTimer: function() {
this.stopUserTypingTimer();
var self = this;
this.userTypingTimer = setTimeout(function() {
self.isTyping = false;
self.sendTyping(self.isTyping);
self.userTypingTimer = null;
}, TYPING_USER_TIMEOUT);
},
stopUserTypingTimer: function() {
if (this.userTypingTimer) {
clearTimeout(this.userTypingTimer);
this.userTypingTimer = null;
}
},
startServerTypingTimer: function() {
if (!this.serverTypingTimer) {
var self = this;
this.serverTypingTimer = setTimeout(function() {
if (self.isTyping) {
self.sendTyping(self.isTyping);
self.startServerTypingTimer();
}
}, TYPING_SERVER_TIMEOUT / 2);
}
},
stopServerTypingTimer: function() {
if (this.serverTypingTimer) {
clearTimeout(this.servrTypingTimer);
this.serverTypingTimer = null;
}
},
sendTyping: function(isTyping) {
MatrixClientPeg.get().sendTyping(
this.props.room.roomId,
this.isTyping, TYPING_SERVER_TIMEOUT
).done();
},
refreshTyping: function() {
if (this.typingTimeout) {
clearTimeout(this.typingTimeout);
this.typingTimeout = null;
}
},
onInputClick: function(ev) {
this.refs.textarea.focus();
},
render: function() {
return (
<div className="mx_MessageComposer_input" onClick={ this.onInputClick }>
<textarea autoFocus ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder={this.props.placeholder} />
</div>
);
}
});

View file

@ -16,10 +16,10 @@ limitations under the License.
'use strict';
var React = require('react');
import React from 'react';
import { _t } from '../../../languageHandler';
var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index');
module.exports = React.createClass({
displayName: 'PresenceLabel',
@ -53,31 +53,30 @@ module.exports = React.createClass({
var d = parseInt(t / (60 * 60 * 24));
if (t < 60) {
if (t < 0) {
return "0s";
return _t("for %(amount)ss", {amount: 0});
}
return s + "s";
return _t("for %(amount)ss", {amount: s});
}
if (t < 60 * 60) {
return m + "m";
return _t("for %(amount)sm", {amount: m});
}
if (t < 24 * 60 * 60) {
return h + "h";
return _t("for %(amount)sh", {amount: h});
}
return d + "d ";
return _t("for %(amount)sd", {amount: d});
},
getPrettyPresence: function(presence) {
if (presence === "online") return "Online";
if (presence === "unavailable") return "Idle"; // XXX: is this actually right?
if (presence === "offline") return "Offline";
if (presence === "online") return _t("Online");
if (presence === "unavailable") return _t("Idle"); // XXX: is this actually right?
if (presence === "offline") return _t("Offline");
return "Unknown";
},
render: function() {
if (this.props.activeAgo >= 0) {
var ago = this.props.currentlyActive ? "now" : (this.getDuration(this.props.activeAgo) + " ago");
// var ago = this.getDuration(this.props.activeAgo) + " ago";
// if (this.props.currentlyActive) ago += " (now?)";
let duration = this.getDuration(this.props.activeAgo);
let ago = this.props.currentlyActive || !duration ? "" : duration;
return (
<div className="mx_PresenceLabel">
{ this.getPrettyPresence(this.props.presenceState) } { ago }

View file

@ -23,6 +23,9 @@ var sdk = require('../../../index');
var Velociraptor = require('../../../Velociraptor');
require('../../../VelocityBounce');
import { _t } from '../../../languageHandler';
import DateUtils from '../../../DateUtils';
var bounce = false;
try {
@ -64,8 +67,8 @@ module.exports = React.createClass({
// Timestamp when the receipt was read
timestamp: React.PropTypes.number,
// True to show the full date/time rather than just the time
showFullTimestamp: React.PropTypes.bool,
// True to show twelve hour format, false otherwise
showTwelveHour: React.PropTypes.bool,
},
getDefaultProps: function() {
@ -170,16 +173,10 @@ module.exports = React.createClass({
let title;
if (this.props.timestamp) {
const prefix = "Seen by " + this.props.member.userId + " at ";
let ts = new Date(this.props.timestamp);
if (this.props.showFullTimestamp) {
// "15/12/2016, 7:05:45 PM (@alice:matrix.org)"
title = prefix + ts.toLocaleString();
}
else {
// "7:05:45 PM (@alice:matrix.org)"
title = prefix + ts.toLocaleTimeString();
}
title = _t(
"Seen by %(userName)s at %(dateTime)s",
{userName: this.props.member.userId, dateTime: DateUtils.formatDate(new Date(this.props.timestamp), this.props.showTwelveHour)}
);
}
return (

View file

@ -16,16 +16,18 @@ limitations under the License.
'use strict';
var React = require('react');
var sdk = require('../../../index');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var Modal = require("../../../Modal");
var dis = require("../../../dispatcher");
var rate_limited_func = require('../../../ratelimitedfunc');
import React from 'react';
import classNames from 'classnames';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Modal from "../../../Modal";
import dis from "../../../dispatcher";
import RateLimitedFunc from '../../../ratelimitedfunc';
var linkify = require('linkifyjs');
var linkifyElement = require('linkifyjs/element');
var linkifyMatrix = require('../../../linkify-matrix');
import * as linkify from 'linkifyjs';
import linkifyElement from 'linkifyjs/element';
import linkifyMatrix from '../../../linkify-matrix';
import AccessibleButton from '../elements/AccessibleButton';
import {CancelButton} from './SimpleRoomHeader';
@ -39,6 +41,7 @@ module.exports = React.createClass({
oobData: React.PropTypes.object,
editing: React.PropTypes.bool,
saving: React.PropTypes.bool,
inRoom: React.PropTypes.bool,
collapsedRhs: React.PropTypes.bool,
onSettingsClick: React.PropTypes.func,
onSaveClick: React.PropTypes.func,
@ -49,13 +52,13 @@ module.exports = React.createClass({
getDefaultProps: function() {
return {
editing: false,
onSettingsClick: function() {},
inRoom: false,
onSaveClick: function() {},
};
},
componentDidMount: function() {
var cli = MatrixClientPeg.get();
const cli = MatrixClientPeg.get();
cli.on("RoomState.events", this._onRoomStateEvents);
// When a room name occurs, RoomState.events is fired *before*
@ -76,14 +79,14 @@ module.exports = React.createClass({
if (this.props.room) {
this.props.room.removeListener("Room.name", this._onRoomNameChange);
}
var cli = MatrixClientPeg.get();
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomState.events", this._onRoomStateEvents);
}
},
_onRoomStateEvents: function(event, state) {
if (!this.props.room || event.getRoomId() != this.props.room.roomId) {
if (!this.props.room || event.getRoomId() !== this.props.room.roomId) {
return;
}
@ -91,7 +94,8 @@ module.exports = React.createClass({
this._rateLimitedUpdate();
},
_rateLimitedUpdate: new rate_limited_func(function() {
_rateLimitedUpdate: new RateLimitedFunc(function() {
/* eslint-disable babel/no-invalid-this */
this.forceUpdate();
}, 500),
@ -106,19 +110,18 @@ module.exports = React.createClass({
},
onAvatarSelected: function(ev) {
var self = this;
var changeAvatar = this.refs.changeAvatar;
const changeAvatar = this.refs.changeAvatar;
if (!changeAvatar) {
console.error("No ChangeAvatar found to upload image to!");
return;
}
changeAvatar.onFileSelected(ev).catch(function(err) {
var errMsg = (typeof err === "string") ? err : (err.error || "");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const errMsg = (typeof err === "string") ? err : (err.error || "");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to set avatar: " + errMsg);
Modal.createDialog(ErrorDialog, {
title: "Error",
description: "Failed to set avatar.",
title: _t("Error"),
description: _t("Failed to set avatar."),
});
}).done();
},
@ -130,10 +133,10 @@ module.exports = React.createClass({
/**
* After editing the settings, get the new name for the room
*
* Returns undefined if we didn't let the user edit the room name
* @return {?string} newName or undefined if we didn't let the user edit the room name
*/
getEditedName: function() {
var newName;
let newName;
if (this.refs.nameEditor) {
newName = this.refs.nameEditor.getRoomName();
}
@ -143,10 +146,10 @@ module.exports = React.createClass({
/**
* After editing the settings, get the new topic for the room
*
* Returns undefined if we didn't let the user edit the room topic
* @return {?string} newTopic or undefined if we didn't let the user edit the room topic
*/
getEditedTopic: function() {
var newTopic;
let newTopic;
if (this.refs.topicEditor) {
newTopic = this.refs.topicEditor.getTopic();
}
@ -154,103 +157,106 @@ module.exports = React.createClass({
},
render: function() {
var RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
var ChangeAvatar = sdk.getComponent("settings.ChangeAvatar");
var TintableSvg = sdk.getComponent("elements.TintableSvg");
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
const ChangeAvatar = sdk.getComponent("settings.ChangeAvatar");
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const EmojiText = sdk.getComponent('elements.EmojiText');
var header;
var name = null;
var searchStatus = null;
var topic_el = null;
var cancel_button = null;
var spinner = null;
var save_button = null;
var settings_button = null;
let name = null;
let searchStatus = null;
let topicElement = null;
let cancelButton = null;
let spinner = null;
let saveButton = null;
let settingsButton = null;
let canSetRoomName;
let canSetRoomAvatar;
let canSetRoomTopic;
if (this.props.editing) {
// calculate permissions. XXX: this should be done on mount or something
var user_id = MatrixClientPeg.get().credentials.userId;
const userId = MatrixClientPeg.get().credentials.userId;
var can_set_room_name = this.props.room.currentState.maySendStateEvent(
'm.room.name', user_id
);
var can_set_room_avatar = this.props.room.currentState.maySendStateEvent(
'm.room.avatar', user_id
);
var can_set_room_topic = this.props.room.currentState.maySendStateEvent(
'm.room.topic', user_id
);
var can_set_room_name = this.props.room.currentState.maySendStateEvent(
'm.room.name', user_id
);
canSetRoomName = this.props.room.currentState.maySendStateEvent('m.room.name', userId);
canSetRoomAvatar = this.props.room.currentState.maySendStateEvent('m.room.avatar', userId);
canSetRoomTopic = this.props.room.currentState.maySendStateEvent('m.room.topic', userId);
save_button = <AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save</AccessibleButton>;
cancel_button = <CancelButton onClick={this.props.onCancelClick}/>;
saveButton = (
<AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>
{_t("Save")}
</AccessibleButton>
);
}
if (this.props.onCancelClick) {
cancelButton = <CancelButton onClick={this.props.onCancelClick}/>;
}
if (this.props.saving) {
var Spinner = sdk.getComponent("elements.Spinner");
const Spinner = sdk.getComponent("elements.Spinner");
spinner = <div className="mx_RoomHeader_spinner"><Spinner/></div>;
}
if (can_set_room_name) {
var RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor");
if (canSetRoomName) {
const RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor");
name = <RoomNameEditor ref="nameEditor" room={this.props.room} />;
}
else {
var searchStatus;
} else {
// don't display the search count until the search completes and
// gives us a valid (possibly zero) searchCount.
if (this.props.searchInfo && this.props.searchInfo.searchCount !== undefined && this.props.searchInfo.searchCount !== null) {
searchStatus = <div className="mx_RoomHeader_searchStatus">&nbsp;(~{ this.props.searchInfo.searchCount } results)</div>;
if (this.props.searchInfo &&
this.props.searchInfo.searchCount !== undefined &&
this.props.searchInfo.searchCount !== null) {
searchStatus = <div className="mx_RoomHeader_searchStatus">&nbsp;
{ _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) }
</div>;
}
// XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'...
var settingsHint = false;
var members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
let settingsHint = false;
const members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
if (members) {
if (members.length === 1 && members[0].userId === MatrixClientPeg.get().credentials.userId) {
var name = this.props.room.currentState.getStateEvents('m.room.name', '');
if (!name || !name.getContent().name) {
const nameEvent = this.props.room.currentState.getStateEvents('m.room.name', '');
if (!nameEvent || !nameEvent.getContent().name) {
settingsHint = true;
}
}
}
var roomName = 'Join Room';
let roomName = _t("Join Room");
if (this.props.oobData && this.props.oobData.name) {
roomName = this.props.oobData.name;
} else if (this.props.room) {
roomName = this.props.room.name;
}
const emojiTextClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
name =
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
<EmojiText element="div" className={ "mx_RoomHeader_nametext " + (settingsHint ? "mx_RoomHeader_settingsHint" : "") } title={ roomName }>{roomName}</EmojiText>
<EmojiText dir="auto" element="div" className={emojiTextClasses} title={roomName}>{ roomName }</EmojiText>
{ searchStatus }
</div>;
}
if (can_set_room_topic) {
var RoomTopicEditor = sdk.getComponent("rooms.RoomTopicEditor");
topic_el = <RoomTopicEditor ref="topicEditor" room={this.props.room} />;
if (canSetRoomTopic) {
const RoomTopicEditor = sdk.getComponent("rooms.RoomTopicEditor");
topicElement = <RoomTopicEditor ref="topicEditor" room={this.props.room} />;
} else {
var topic;
let topic;
if (this.props.room) {
var ev = this.props.room.currentState.getStateEvents('m.room.topic', '');
const ev = this.props.room.currentState.getStateEvents('m.room.topic', '');
if (ev) {
topic = ev.getContent().topic;
}
}
if (topic) {
topic_el = <div className="mx_RoomHeader_topic" ref="topic" title={ topic }>{ topic }</div>;
topicElement =
<div className="mx_RoomHeader_topic" ref="topic" title={ topic } dir="auto">{ topic }</div>;
}
}
var roomAvatar = null;
if (can_set_room_avatar) {
let roomAvatar = null;
if (canSetRoomAvatar) {
roomAvatar = (
<div className="mx_RoomHeader_avatarPicker">
<div onClick={ this.onAvatarPickerClick }>
@ -259,15 +265,14 @@ module.exports = React.createClass({
<div className="mx_RoomHeader_avatarPicker_edit">
<label htmlFor="avatarInput" ref="file_label">
<img src="img/camera.svg"
alt="Upload avatar" title="Upload avatar"
alt={ _t("Upload avatar") } title={ _t("Upload avatar") }
width="17" height="15" />
</label>
<input id="avatarInput" type="file" onChange={ this.onAvatarSelected }/>
</div>
</div>
);
}
else if (this.props.room || (this.props.oobData && this.props.oobData.name)) {
} else if (this.props.room || (this.props.oobData && this.props.oobData.name)) {
roomAvatar = (
<div onClick={this.props.onSettingsClick}>
<RoomAvatar room={this.props.room} width={48} height={48} oobData={this.props.oobData} />
@ -275,10 +280,9 @@ module.exports = React.createClass({
);
}
var settings_button;
if (this.props.onSettingsClick) {
settings_button =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSettingsClick} title="Settings">
settingsButton =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSettingsClick} title={_t("Settings")}>
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
</AccessibleButton>;
}
@ -291,55 +295,58 @@ module.exports = React.createClass({
// </div>;
// }
var forget_button;
let forgetButton;
if (this.props.onForgetClick) {
forget_button =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title="Forget room">
forgetButton =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title={ _t("Forget room") }>
<TintableSvg src="img/leave.svg" width="26" height="20"/>
</AccessibleButton>;
}
var rightPanel_buttons;
let searchButton;
if (this.props.onSearchClick && this.props.inRoom) {
searchButton =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title={ _t("Search") }>
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
</AccessibleButton>;
}
let rightPanelButtons;
if (this.props.collapsedRhs) {
rightPanel_buttons =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title="Show panel">
rightPanelButtons =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title={ _t('Show panel') }>
<TintableSvg src="img/maximise.svg" width="10" height="16"/>
</AccessibleButton>;
}
var right_row;
let rightRow;
if (!this.props.editing) {
right_row =
rightRow =
<div className="mx_RoomHeader_rightRow">
{ settings_button }
{ forget_button }
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search">
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
</AccessibleButton>
{ rightPanel_buttons }
{ settingsButton }
{ forgetButton }
{ searchButton }
{ rightPanelButtons }
</div>;
}
header =
<div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_leftRow">
<div className="mx_RoomHeader_avatar">
{ roomAvatar }
</div>
<div className="mx_RoomHeader_info">
{ name }
{ topic_el }
</div>
</div>
{spinner}
{save_button}
{cancel_button}
{right_row}
</div>;
return (
<div className={ "mx_RoomHeader " + (this.props.editing ? "mx_RoomHeader_editing" : "") }>
{ header }
<div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_leftRow">
<div className="mx_RoomHeader_avatar">
{ roomAvatar }
</div>
<div className="mx_RoomHeader_info">
{ name }
{ topicElement }
</div>
</div>
{spinner}
{saveButton}
{cancelButton}
{rightRow}
</div>
</div>
);
},

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.
@ -17,6 +18,7 @@ limitations under the License.
'use strict';
var React = require("react");
var ReactDOM = require("react-dom");
import { _t, _tJsx } from '../../../languageHandler';
var GeminiScrollbar = require('react-gemini-scrollbar');
var MatrixClientPeg = require("../../../MatrixClientPeg");
var CallHandler = require('../../../CallHandler');
@ -29,7 +31,31 @@ var Rooms = require('../../../Rooms');
import DMRoomMap from '../../../utils/DMRoomMap';
var Receipt = require('../../../utils/Receipt');
var HIDE_CONFERENCE_CHANS = true;
const HIDE_CONFERENCE_CHANS = true;
function phraseForSection(section) {
// These would probably be better as individual strings,
// but for some reason we have translations for these strings
// as-is, so keeping it like this for now.
let verb;
switch (section) {
case 'm.favourite':
verb = _t('to favourite');
break;
case 'im.vector.fake.direct':
verb = _t('to tag direct chat');
break;
case 'im.vector.fake.recent':
verb = _t('to restore');
break;
case 'm.lowpriority':
verb = _t('to demote');
break;
default:
return _t('Drop here to tag %(section)s', {section: section});
}
return _t('Drop here %(toAction)s', {toAction: verb});
};
module.exports = React.createClass({
displayName: 'RoomList',
@ -44,12 +70,15 @@ module.exports = React.createClass({
getInitialState: function() {
return {
isLoadingLeftRooms: false,
totalRoomCount: null,
lists: {},
incomingCall: null,
};
},
componentWillMount: function() {
this.mounted = false;
var cli = MatrixClientPeg.get();
cli.on("Room", this.onRoom);
cli.on("deleteRoom", this.onDeleteRoom);
@ -61,14 +90,22 @@ module.exports = React.createClass({
cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("accountData", this.onAccountData);
var s = this.getRoomLists();
this.setState(s);
this.refreshRoomList();
// order of the sublists
//this.listOrder = [];
// loop count to stop a stack overflow if the user keeps waggling the
// mouse for >30s in a row, or if running under mocha
this._delayedRefreshRoomListLoopCount = 0
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
// Initialise the stickyHeaders when the component is created
this._updateStickyHeaders(true);
this.mounted = true;
},
componentDidUpdate: function() {
@ -106,6 +143,8 @@ module.exports = React.createClass({
},
componentWillUnmount: function() {
this.mounted = false;
dis.unregister(this.dispatcherRef);
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room", this.onRoom);
@ -196,30 +235,33 @@ module.exports = React.createClass({
}, 500),
refreshRoomList: function() {
// console.log("DEBUG: Refresh room list delta=%s ms",
// (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs))
// );
// TODO: ideally we'd calculate this once at start, and then maintain
// any changes to it incrementally, updating the appropriate sublists
// as needed.
// Alternatively we'd do something magical with Immutable.js or similar.
const lists = this.getRoomLists();
let totalRooms = 0;
for (const l of Object.values(lists)) {
totalRooms += l.length;
}
this.setState({
lists: this.getRoomLists(),
totalRoomCount: totalRooms,
});
// TODO: rather than bluntly regenerating and re-sorting everything
// every time we see any kind of room change from the JS SDK
// we could do incremental updates on our copy of the state
// based on the room which has actually changed. This would stop
// us re-rendering all the sublists every time anything changes anywhere
// in the state of the client.
this.setState(this.getRoomLists());
this._lastRefreshRoomListTs = Date.now();
// this._lastRefreshRoomListTs = Date.now();
},
getRoomLists: function() {
var self = this;
var s = { lists: {} };
const lists = {};
s.lists["im.vector.fake.invite"] = [];
s.lists["m.favourite"] = [];
s.lists["im.vector.fake.recent"] = [];
s.lists["im.vector.fake.direct"] = [];
s.lists["m.lowpriority"] = [];
s.lists["im.vector.fake.archived"] = [];
lists["im.vector.fake.invite"] = [];
lists["m.favourite"] = [];
lists["im.vector.fake.recent"] = [];
lists["im.vector.fake.direct"] = [];
lists["m.lowpriority"] = [];
lists["im.vector.fake.archived"] = [];
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
@ -233,7 +275,7 @@ module.exports = React.createClass({
// ", prevMembership = " + me.events.member.getPrevContent().membership);
if (me.membership == "invite") {
s.lists["im.vector.fake.invite"].push(room);
lists["im.vector.fake.invite"].push(room);
}
else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) {
// skip past this room & don't put it in any lists
@ -247,69 +289,48 @@ module.exports = React.createClass({
if (tagNames.length) {
for (var i = 0; i < tagNames.length; i++) {
var tagName = tagNames[i];
s.lists[tagName] = s.lists[tagName] || [];
s.lists[tagNames[i]].push(room);
lists[tagName] = lists[tagName] || [];
lists[tagName].push(room);
}
}
else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
// "Direct Message" rooms (that we're still in and that aren't otherwise tagged)
s.lists["im.vector.fake.direct"].push(room);
lists["im.vector.fake.direct"].push(room);
}
else {
s.lists["im.vector.fake.recent"].push(room);
lists["im.vector.fake.recent"].push(room);
}
}
else if (me.membership === "leave") {
s.lists["im.vector.fake.archived"].push(room);
lists["im.vector.fake.archived"].push(room);
}
else {
console.error("unrecognised membership: " + me.membership + " - this should never happen");
}
});
if (s.lists["im.vector.fake.direct"].length == 0 &&
MatrixClientPeg.get().getAccountData('m.direct') === undefined &&
!MatrixClientPeg.get().isGuest())
{
// scan through the 'recents' list for any rooms which look like DM rooms
// and make them DM rooms
const oldRecents = s.lists["im.vector.fake.recent"];
s.lists["im.vector.fake.recent"] = [];
for (const room of oldRecents) {
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (me && Rooms.looksLikeDirectMessageRoom(room, me)) {
s.lists["im.vector.fake.direct"].push(room);
} else {
s.lists["im.vector.fake.recent"].push(room);
}
}
// save these new guessed DM rooms into the account data
const newMDirectEvent = {};
for (const room of s.lists["im.vector.fake.direct"]) {
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
const otherPerson = Rooms.getOnlyOtherMember(room, me);
if (!otherPerson) continue;
const roomList = newMDirectEvent[otherPerson.userId] || [];
roomList.push(room.roomId);
newMDirectEvent[otherPerson.userId] = roomList;
}
// if this fails, fine, we'll just do the same thing next time we get the room lists
MatrixClientPeg.get().setAccountData('m.direct', newMDirectEvent).done();
}
//console.log("calculated new roomLists; im.vector.fake.recent = " + s.lists["im.vector.fake.recent"]);
// we actually apply the sorting to this when receiving the prop in RoomSubLists.
return s;
// we'll need this when we get to iterating through lists programatically - e.g. ctrl-shift-up/down
/*
this.listOrder = [
"im.vector.fake.invite",
"m.favourite",
"im.vector.fake.recent",
"im.vector.fake.direct",
Object.keys(otherTagNames).filter(tagName=>{
return (!tagName.match(/^m\.(favourite|lowpriority)$/));
}).sort(),
"m.lowpriority",
"im.vector.fake.archived"
];
*/
return lists;
},
_getScrollNode: function() {
if (!this.mounted) return null;
var panel = ReactDOM.findDOMNode(this);
if (!panel) return null;
@ -337,10 +358,11 @@ module.exports = React.createClass({
var incomingCallBox = document.getElementById("incomingCallBox");
if (incomingCallBox && incomingCallBox.parentElement) {
var scrollArea = this._getScrollNode();
if (!scrollArea) return;
// Use the offset of the top of the scroll area from the window
// as this is used to calculate the CSS fixed top position for the stickies
var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
// Use the offset of the top of the componet from the window
// Use the offset of the top of the component from the window
// as this is used to calculate the CSS fixed top position for the stickies
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
@ -360,6 +382,7 @@ module.exports = React.createClass({
// properly through React
_initAndPositionStickyHeaders: function(initialise, scrollToPosition) {
var scrollArea = this._getScrollNode();
if (!scrollArea) return;
// Use the offset of the top of the scroll area from the window
// as this is used to calculate the CSS fixed top position for the stickies
var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
@ -457,16 +480,79 @@ module.exports = React.createClass({
this.refs.gemscroll.forceUpdate();
},
_getEmptyContent: function(section) {
const RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget');
if (this.props.collapsed) {
return <RoomDropTarget label="" />;
}
const StartChatButton = sdk.getComponent('elements.StartChatButton');
const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
const TintableSvg = sdk.getComponent('elements.TintableSvg');
switch (section) {
case 'im.vector.fake.direct':
return <div className="mx_RoomList_emptySubListTip">
{_tJsx(
"Press <StartChatButton> to start a chat with someone",
[/<StartChatButton>/],
[
(sub) => <StartChatButton size="16" callout={true}/>
]
)}
</div>;
case 'im.vector.fake.recent':
return <div className="mx_RoomList_emptySubListTip">
{_tJsx(
"You're not in any rooms yet! Press <CreateRoomButton> to make a room or"+
" <RoomDirectoryButton> to browse the directory",
[/<CreateRoomButton>/, /<RoomDirectoryButton>/],
[
(sub) => <CreateRoomButton size="16" callout={true}/>,
(sub) => <RoomDirectoryButton size="16" callout={true}/>
]
)}
</div>;
}
// We don't want to display drop targets if there are no room tiles to drag'n'drop
if (this.state.totalRoomCount === 0) {
return null;
}
const labelText = phraseForSection(section);
return <RoomDropTarget label={labelText} />;
},
_getHeaderItems: function(section) {
const StartChatButton = sdk.getComponent('elements.StartChatButton');
const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
switch (section) {
case 'im.vector.fake.direct':
return <span className="mx_RoomList_headerButtons">
<StartChatButton size="16" />
</span>;
case 'im.vector.fake.recent':
return <span className="mx_RoomList_headerButtons">
<RoomDirectoryButton size="16" />
<CreateRoomButton size="16" />
</span>;
}
},
render: function() {
var RoomSubList = sdk.getComponent('structures.RoomSubList');
var self = this;
return (
<GeminiScrollbar className="mx_RoomList_scrollbar"
autoshow={true} onScroll={ self._whenScrolling } ref="gemscroll">
<div className="mx_RoomList">
<RoomSubList list={ self.state.lists['im.vector.fake.invite'] }
label="Invites"
label={ _t('Invites') }
editable={ false }
order="recent"
selectedRoom={ self.props.selectedRoom }
@ -477,9 +563,9 @@ module.exports = React.createClass({
onShowMoreRooms={ self.onShowMoreRooms } />
<RoomSubList list={ self.state.lists['m.favourite'] }
label="Favourites"
label={ _t('Favourites') }
tagName="m.favourite"
verb="favourite"
emptyContent={this._getEmptyContent('m.favourite')}
editable={ true }
order="manual"
selectedRoom={ self.props.selectedRoom }
@ -490,9 +576,10 @@ module.exports = React.createClass({
onShowMoreRooms={ self.onShowMoreRooms } />
<RoomSubList list={ self.state.lists['im.vector.fake.direct'] }
label="People"
label={ _t('People') }
tagName="im.vector.fake.direct"
verb="tag direct chat"
emptyContent={this._getEmptyContent('im.vector.fake.direct')}
headerItems={this._getHeaderItems('im.vector.fake.direct')}
editable={ true }
order="recent"
selectedRoom={ self.props.selectedRoom }
@ -504,9 +591,10 @@ module.exports = React.createClass({
onShowMoreRooms={ self.onShowMoreRooms } />
<RoomSubList list={ self.state.lists['im.vector.fake.recent'] }
label="Rooms"
label={ _t('Rooms') }
editable={ true }
verb="restore"
emptyContent={this._getEmptyContent('im.vector.fake.recent')}
headerItems={this._getHeaderItems('im.vector.fake.recent')}
order="recent"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
@ -515,13 +603,13 @@ module.exports = React.createClass({
onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } />
{ Object.keys(self.state.lists).map(function(tagName) {
{ Object.keys(self.state.lists).map((tagName) => {
if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
return <RoomSubList list={ self.state.lists[tagName] }
key={ tagName }
label={ tagName }
tagName={ tagName }
verb={ "tag as " + tagName }
emptyContent={this._getEmptyContent(tagName)}
editable={ true }
order="manual"
selectedRoom={ self.props.selectedRoom }
@ -535,9 +623,9 @@ module.exports = React.createClass({
}) }
<RoomSubList list={ self.state.lists['m.lowpriority'] }
label="Low priority"
label={ _t('Low priority') }
tagName="m.lowpriority"
verb="demote"
emptyContent={this._getEmptyContent('m.lowpriority')}
editable={ true }
order="recent"
selectedRoom={ self.props.selectedRoom }
@ -548,7 +636,7 @@ module.exports = React.createClass({
onShowMoreRooms={ self.onShowMoreRooms } />
<RoomSubList list={ self.state.lists['im.vector.fake.archived'] }
label="Historical"
label={ _t('Historical') }
editable={ false }
order="recent"
selectedRoom={ self.props.selectedRoom }

View file

@ -19,6 +19,7 @@ limitations under the License.
var React = require('react');
var sdk = require('../../../index');
var MatrixClientPeg = require('../../../MatrixClientPeg');
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'RoomNameEditor',
@ -35,8 +36,8 @@ module.exports = React.createClass({
this._initialName = name ? name.getContent().name : '';
this._placeholderName = "Unnamed Room";
if (defaultName && defaultName !== 'Empty room') {
this._placeholderName = _t("Unnamed Room");
if (defaultName && defaultName !== 'Empty room') { // default name from JS SDK, needs no translation as we don't ever show it.
this._placeholderName += " (" + defaultName + ")";
}
},
@ -55,9 +56,9 @@ module.exports = React.createClass({
placeholderClassName="mx_RoomHeader_placeholder"
placeholder={ this._placeholderName }
blurToCancel={ false }
initialValue={ this._initialName }/>
initialValue={ this._initialName }
dir="auto" />
</div>
);
},
});

View file

@ -21,6 +21,8 @@ var React = require('react');
var sdk = require('../../../index');
var MatrixClientPeg = require('../../../MatrixClientPeg');
import { _t, _tJsx } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'RoomPreviewBar',
@ -47,7 +49,7 @@ module.exports = React.createClass({
// The alias that was used to access this room, if appropriate
// If given, this will be how the room is referred to (eg.
// in error messages).
roomAlias: React.PropTypes.object,
roomAlias: React.PropTypes.string,
},
getDefaultProps: function() {
@ -82,9 +84,9 @@ module.exports = React.createClass({
},
_roomNameElement: function(fallback) {
fallback = fallback || 'a room';
fallback = fallback || _t('a room');
const name = this.props.room ? this.props.room.name : (this.props.room_alias || "");
return name ? <b>{ name }</b> : fallback;
return name ? name : fallback;
},
render: function() {
@ -112,8 +114,7 @@ module.exports = React.createClass({
if (this.props.invitedEmail) {
if (this.state.threePidFetchError) {
emailMatchBlock = <div className="error">
Unable to ascertain that the address this invite was
sent to matches one associated with your account.
{_t("Unable to ascertain that the address this invite was sent to matches one associated with your account.")}
</div>;
} else if (this.state.invitedEmailMxid != MatrixClientPeg.get().credentials.userId) {
emailMatchBlock =
@ -122,8 +123,10 @@ module.exports = React.createClass({
<img src="img/warning.svg" width="24" height="23" title= "/!\\" alt="/!\\" />
</div>
<div className="mx_RoomPreviewBar_warningText">
This invitation was sent to <b><span className="email">{this.props.invitedEmail}</span></b>, which is not associated with this account.<br/>
You may wish to login with a different account, or add this email to this account.
{_t("This invitation was sent to an email address which is not associated with this account:")}
<b><span className="email">{this.props.invitedEmail}</span></b>
<br/>
{_t("You may wish to login with a different account, or add this email to this account.")}
</div>
</div>;
}
@ -131,18 +134,24 @@ module.exports = React.createClass({
joinBlock = (
<div>
<div className="mx_RoomPreviewBar_invite_text">
You have been invited to join this room by <b>{ this.props.inviterName }</b>
{ _t('You have been invited to join this room by %(inviterName)s', {inviterName: this.props.inviterName}) }
</div>
<div className="mx_RoomPreviewBar_join_text">
Would you like to <a onClick={ this.props.onJoinClick }>accept</a> or <a onClick={ this.props.onRejectClick }>decline</a> this invitation?
{ _tJsx(
'Would you like to <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?',
[/<acceptText>(.*?)<\/acceptText>/, /<declineText>(.*?)<\/declineText>/],
[
(sub) => <a onClick={ this.props.onJoinClick }>{sub}</a>,
(sub) => <a onClick={ this.props.onRejectClick }>{sub}</a>
]
)}
</div>
{emailMatchBlock}
</div>
);
} else if (kicked || banned) {
const verb = kicked ? 'kicked' : 'banned';
const roomName = this._roomNameElement('this room');
const roomName = this._roomNameElement(_t('This room'));
const kickerMember = this.props.room.currentState.getMember(
myMember.events.member.getSender()
);
@ -150,29 +159,39 @@ module.exports = React.createClass({
kickerMember.name : myMember.events.member.getSender();
let reason;
if (myMember.events.member.getContent().reason) {
reason = <div>Reason: {myMember.events.member.getContent().reason}</div>
reason = <div>{_t("Reason: %(reasonText)s", {reasonText: myMember.events.member.getContent().reason})}</div>
}
let rejoinBlock;
if (!banned) {
rejoinBlock = <div><a onClick={ this.props.onJoinClick }><b>Rejoin</b></a></div>;
rejoinBlock = <div><a onClick={ this.props.onJoinClick }><b>{_t("Rejoin")}</b></a></div>;
}
let actionText;
if (kicked) {
actionText = _t("You have been kicked from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName});
}
else if (banned) {
actionText = _t("You have been banned from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName});
} // no other options possible due to the kicked || banned check above.
joinBlock = (
<div>
<div className="mx_RoomPreviewBar_join_text">
You have been {verb} from {roomName} by {kickerName}.<br />
{actionText}
<br />
{reason}
{rejoinBlock}
<a onClick={ this.props.onForgetClick }><b>Forget</b></a>
<a onClick={ this.props.onForgetClick }><b>{_t("Forget room")}</b></a>
</div>
</div>
);
} else if (this.props.error) {
var name = this.props.roomAlias || "This room";
var name = this.props.roomAlias || _t("This room");
var error;
if (this.props.error.errcode == 'M_NOT_FOUND') {
error = name + " does not exist";
error = _t("%(roomName)s does not exist.", {roomName: name});
} else {
error = name + " is not accessible at this time";
error = _t("%(roomName)s is not accessible at this time.", {roomName: name});
}
joinBlock = (
<div>
@ -186,8 +205,12 @@ module.exports = React.createClass({
joinBlock = (
<div>
<div className="mx_RoomPreviewBar_join_text">
You are trying to access { name }.<br/>
<a onClick={ this.props.onJoinClick }><b>Click here</b></a> to join the discussion!
{ _t('You are trying to access %(roomName)s.', {roomName: name}) }
<br/>
{ _tJsx("<a>Click here</a> to join the discussion!",
/<a>(.*?)<\/a>/,
(sub) => <a onClick={ this.props.onJoinClick }><b>{sub}</b></a>
)}
</div>
</div>
);
@ -196,7 +219,7 @@ module.exports = React.createClass({
if (this.props.canPreview) {
previewBlock = (
<div className="mx_RoomPreviewBar_preview_text">
This is a preview of this room. Room interactions have been disabled.
{ _t('This is a preview of this room. Room interactions have been disabled') }.
</div>
);
}

View file

@ -15,8 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import q from 'q';
import Promise from 'bluebird';
import React from 'react';
import { _t, _tJsx } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import SdkConfig from '../../../SdkConfig';
import sdk from '../../../index';
@ -38,14 +39,16 @@ function parseIntWithDefault(val, def) {
const BannedUser = React.createClass({
propTypes: {
canUnban: React.PropTypes.bool,
member: React.PropTypes.object.isRequired, // js-sdk RoomMember
reason: React.PropTypes.string,
},
_onUnbanClick: function() {
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
Modal.createDialog(ConfirmUserActionDialog, {
member: this.props.member,
action: 'Unban',
action: _t('Unban'),
danger: false,
onFinished: (proceed) => {
if (!proceed) return;
@ -56,8 +59,8 @@ const BannedUser = React.createClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to unban: " + err);
Modal.createDialog(ErrorDialog, {
title: "Error",
description: "Failed to unban",
title: _t('Error'),
description: _t('Failed to unban'),
});
}).done();
},
@ -65,17 +68,22 @@ const BannedUser = React.createClass({
},
render: function() {
let unbanButton;
if (this.props.canUnban) {
unbanButton = <AccessibleButton className="mx_RoomSettings_unbanButton" onClick={this._onUnbanClick}>
{ _t('Unban') }
</AccessibleButton>;
}
return (
<li>
<AccessibleButton className="mx_RoomSettings_unbanButton"
onClick={this._onUnbanClick}
>
Unban
</AccessibleButton>
{this.props.member.userId}
{ unbanButton }
<strong>{this.props.member.name}</strong> {this.props.member.userId}
{this.props.reason ? " " +_t('Reason') + ": " + this.props.reason : ""}
</li>
);
}
},
});
module.exports = React.createClass({
@ -129,14 +137,17 @@ module.exports = React.createClass({
console.error("Failed to get room visibility: " + err);
});
this.scalarClient = new ScalarAuthClient();
this.scalarClient.connect().done(() => {
this.forceUpdate();
}, (err) => {
this.setState({
scalar_error: err
this.scalarClient = null;
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
this.scalarClient = new ScalarAuthClient();
this.scalarClient.connect().done(() => {
this.forceUpdate();
}, (err) => {
this.setState({
scalar_error: err
});
});
});
}
dis.dispatch({
action: 'ui_opacity',
@ -172,8 +183,14 @@ module.exports = React.createClass({
});
},
/**
* Returns a promise which resolves once all of the save operations have completed or failed.
*
* The result is a list of promise state snapshots, each with the form
* `{ state: "fulfilled", value: v }` or `{ state: "rejected", reason: r }`.
*/
save: function() {
var stateWasSetDefer = q.defer();
var stateWasSetDefer = Promise.defer();
// the caller may have JUST called setState on stuff, so we need to re-render before saving
// else we won't use the latest values of things.
// We can be a bit cheeky here and set a loading flag, and listen for the callback on that
@ -183,8 +200,18 @@ module.exports = React.createClass({
this.setState({ _loading: false});
});
function mapPromiseToSnapshot(p) {
return p.then((r) => {
return { state: "fulfilled", value: r };
}, (e) => {
return { state: "rejected", reason: e };
});
}
return stateWasSetDefer.promise.then(() => {
return q.allSettled(this._calcSavePromises());
return Promise.all(
this._calcSavePromises().map(mapPromiseToSnapshot),
);
});
},
@ -271,7 +298,7 @@ module.exports = React.createClass({
// color scheme
var p;
p = this.saveColor();
if (!q.isFulfilled(p)) {
if (!p.isFulfilled()) {
promises.push(p);
}
@ -283,7 +310,7 @@ module.exports = React.createClass({
// encryption
p = this.saveEnableEncryption();
if (!q.isFulfilled(p)) {
if (!p.isFulfilled()) {
promises.push(p);
}
@ -294,25 +321,25 @@ module.exports = React.createClass({
},
saveAliases: function() {
if (!this.refs.alias_settings) { return [q()]; }
if (!this.refs.alias_settings) { return [Promise.resolve()]; }
return this.refs.alias_settings.saveSettings();
},
saveColor: function() {
if (!this.refs.color_settings) { return q(); }
if (!this.refs.color_settings) { return Promise.resolve(); }
return this.refs.color_settings.saveSettings();
},
saveUrlPreviewSettings: function() {
if (!this.refs.url_preview_settings) { return q(); }
if (!this.refs.url_preview_settings) { return Promise.resolve(); }
return this.refs.url_preview_settings.saveSettings();
},
saveEnableEncryption: function() {
if (!this.refs.encrypt) { return q(); }
if (!this.refs.encrypt) { return Promise.resolve(); }
var encrypt = this.refs.encrypt.checked;
if (!encrypt) { return q(); }
if (!encrypt) { return Promise.resolve(); }
var roomId = this.props.room.roomId;
return MatrixClientPeg.get().sendStateEvent(
@ -397,13 +424,13 @@ module.exports = React.createClass({
var value = ev.target.value;
Modal.createDialog(QuestionDialog, {
title: "Privacy warning",
title: _t('Privacy warning'),
description:
<div>
Changes to who can read history will only apply to future messages in this room.<br/>
The visibility of existing history will be unchanged.
{ _t('Changes to who can read history will only apply to future messages in this room') }.<br/>
{ _t('The visibility of existing history will be unchanged') }.
</div>,
button: "Continue",
button: _t('Continue'),
onFinished: function(confirmed) {
if (confirmed) {
self.setState({
@ -490,7 +517,7 @@ module.exports = React.createClass({
ev.preventDefault();
var IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
Modal.createDialog(IntegrationsManager, {
src: this.scalarClient.hasCredentials() ?
src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId) :
null,
onFinished: ()=>{
@ -520,11 +547,11 @@ module.exports = React.createClass({
MatrixClientPeg.get().forget(this.props.room.roomId).done(function() {
dis.dispatch({ action: 'view_next_room' });
}, function(err) {
var errCode = err.errcode || "unknown error code";
var errCode = err.errcode || _t('unknown error code');
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Error",
description: `Failed to forget room (${errCode})`
title: _t('Error'),
description: _t("Failed to forget room %(errCode)s", { errCode: errCode }),
});
});
},
@ -534,14 +561,14 @@ module.exports = React.createClass({
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: "Warning!",
title: _t('Warning!'),
description: (
<div>
<p>End-to-end encryption is in beta and may not be reliable.</p>
<p>You should <b>not</b> yet trust it to secure data.</p>
<p>Devices will <b>not</b> yet be able to decrypt history from before they joined the room.</p>
<p>Once encryption is enabled for a room it <b>cannot</b> be turned off again (for now).</p>
<p>Encrypted messages will not be visible on clients that do not yet implement encryption.</p>
<p>{ _t('End-to-end encryption is in beta and may not be reliable') }.</p>
<p>{ _t('You should not yet trust it to secure data') }.</p>
<p>{ _t('Devices will not yet be able to decrypt history from before they joined the room') }.</p>
<p>{ _t('Once encryption is enabled for a room it cannot be turned off again (for now)') }.</p>
<p>{ _t('Encrypted messages will not be visible on clients that do not yet implement encryption') }.</p>
</div>
),
onFinished: confirm=>{
@ -569,31 +596,29 @@ module.exports = React.createClass({
<input type="checkbox" ref="blacklistUnverified"
defaultChecked={ isGlobalBlacklistUnverified || isRoomBlacklistUnverified }
disabled={ isGlobalBlacklistUnverified || (this.refs.encrypt && !this.refs.encrypt.checked) }/>
Never send encrypted messages to unverified devices in this room from this device.
{ _t('Never send encrypted messages to unverified devices in this room from this device') }.
</label>;
if (!isEncrypted &&
roomState.mayClientSendStateEvent("m.room.encryption", cli)) {
if (!isEncrypted && roomState.mayClientSendStateEvent("m.room.encryption", cli)) {
return (
<div>
<label>
<input type="checkbox" ref="encrypt" onClick={ this.onEnableEncryptionClick }/>
<img className="mx_RoomSettings_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12" />
Enable encryption (warning: cannot be disabled again!)
<img className="mx_RoomSettings_e2eIcon mx_filterFlipColor" src="img/e2e-unencrypted.svg" width="12" height="12" />
{ _t('Enable encryption') } { _t('(warning: cannot be disabled again!)') }
</label>
{ settings }
</div>
);
}
else {
} else {
return (
<div>
<label>
{ isEncrypted
? <img className="mx_RoomSettings_e2eIcon" src="img/e2e-verified.svg" width="10" height="12" />
: <img className="mx_RoomSettings_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12" />
: <img className="mx_RoomSettings_e2eIcon mx_filterFlipColor" src="img/e2e-unencrypted.svg" width="12" height="12" />
}
Encryption is { isEncrypted ? "" : "not " } enabled in this room.
{ isEncrypted ? _t("Encryption is enabled in this room") : _t("Encryption is not enabled in this room") }.
</label>
{ settings }
</div>
@ -644,12 +669,12 @@ module.exports = React.createClass({
if (Object.keys(user_levels).length) {
userLevelsSection =
<div>
<h3>Privileged Users</h3>
<h3>{ _t('Privileged Users') }</h3>
<ul className="mx_RoomSettings_userLevels">
{Object.keys(user_levels).map(function(user, i) {
return (
<li className="mx_RoomSettings_userLevel" key={user}>
{ user } is a <PowerSelector value={ user_levels[user] } disabled={true}/>
{ _t("%(user)s is a", {user: user}) } <PowerSelector value={ user_levels[user] } disabled={true}/>
</li>
);
})}
@ -657,19 +682,21 @@ module.exports = React.createClass({
</div>;
}
else {
userLevelsSection = <div>No users have specific privileges in this room.</div>;
userLevelsSection = <div>{ _t('No users have specific privileges in this room') }.</div>;
}
var banned = this.props.room.getMembersWithMembership("ban");
var bannedUsersSection;
const banned = this.props.room.getMembersWithMembership("ban");
let bannedUsersSection;
if (banned.length) {
const canBanUsers = current_user_level >= ban_level;
bannedUsersSection =
<div>
<h3>Banned users</h3>
<h3>{ _t('Banned users') }</h3>
<ul className="mx_RoomSettings_banned">
{banned.map(function(member) {
const banEvent = member.events.member.getContent();
return (
<BannedUser key={member.userId} member={member} />
<BannedUser key={member.userId} canUnban={canBanUsers} member={member} reason={banEvent.reason} />
);
})}
</ul>
@ -680,7 +707,7 @@ module.exports = React.createClass({
if (this._yankValueFromEvent("m.room.create", "m.federate") === false) {
unfederatableSection = (
<div className="mx_RoomSettings_powerLevel">
Ths room is not accessible by remote Matrix servers.
{ _t('This room is not accessible by remote Matrix servers') }.
</div>
);
}
@ -691,14 +718,14 @@ module.exports = React.createClass({
if (myMember.membership === "join") {
leaveButton = (
<AccessibleButton className="mx_RoomSettings_leaveButton" onClick={ this.onLeaveClick }>
Leave room
{ _t('Leave room') }
</AccessibleButton>
);
}
else if (myMember.membership === "leave") {
leaveButton = (
<AccessibleButton className="mx_RoomSettings_leaveButton" onClick={ this.onForgetClick }>
Forget room
{ _t('Forget room') }
</AccessibleButton>
);
}
@ -708,8 +735,8 @@ module.exports = React.createClass({
// TODO: support editing custom user_levels
var tags = [
{ name: "m.favourite", label: "Favourite", ref: "tag_favourite" },
{ name: "m.lowpriority", label: "Low priority", ref: "tag_lowpriority" },
{ name: "m.favourite", label: _t('Favourite'), ref: "tag_favourite" },
{ name: "m.lowpriority", label: _t('Low priority'), ref: "tag_lowpriority" },
];
Object.keys(this.state.tags).sort().forEach(function(tagName) {
@ -722,7 +749,7 @@ module.exports = React.createClass({
if (canSetTag || self.state.tags) {
var tagsSection =
<div className="mx_RoomSettings_tags">
Tagged as: { canSetTag ?
{_t("Tagged as: ")}{ canSetTag ?
(tags.map(function(tag, i) {
return (<label key={ i }>
<input type="checkbox"
@ -750,7 +777,11 @@ module.exports = React.createClass({
if (this.state.join_rule === "public" && aliasCount == 0) {
addressWarning =
<div className="mx_RoomSettings_warning">
To link to a room it must have <a href="#addresses">an address</a>.
{ _tJsx(
'To link to a room it must have <a>an address</a>.',
/<a>(.*?)<\/a>/,
(sub) => <a href="#addresses">{sub}</a>
)}
</div>;
}
@ -758,43 +789,46 @@ module.exports = React.createClass({
if (this.state.join_rule !== "public" && this.state.guest_access === "forbidden") {
inviteGuestWarning =
<div className="mx_RoomSettings_warning">
Guests cannot join this room even if explicitly invited. <a href="#" onClick={ (e) => {
{ _t('Guests cannot join this room even if explicitly invited.') } <a href="#" onClick={ (e) => {
this.setState({ join_rule: "invite", guest_access: "can_join" });
e.preventDefault();
}}>Click here to fix</a>.
}}>{ _t('Click here to fix') }</a>.
</div>;
}
var integrationsButton;
var integrationsError;
if (this.state.showIntegrationsError && this.state.scalar_error) {
console.error(this.state.scalar_error);
integrationsError = (
<span className="mx_RoomSettings_integrationsButton_errorPopup">
Could not connect to the integration server
</span>
);
}
let integrationsButton;
let integrationsError;
if (this.scalarClient.hasCredentials()) {
integrationsButton = (
if (this.scalarClient !== null) {
if (this.state.showIntegrationsError && this.state.scalar_error) {
console.error(this.state.scalar_error);
integrationsError = (
<span className="mx_RoomSettings_integrationsButton_errorPopup">
{ _t('Could not connect to the integration server') }
</span>
);
}
if (this.scalarClient.hasCredentials()) {
integrationsButton = (
<div className="mx_RoomSettings_integrationsButton" onClick={ this.onManageIntegrations }>
Manage Integrations
</div>
);
} else if (this.state.scalar_error) {
integrationsButton = (
{ _t('Manage Integrations') }
</div>
);
} else if (this.state.scalar_error) {
integrationsButton = (
<div className="mx_RoomSettings_integrationsButton_error" onClick={ this.onShowIntegrationsError }>
Integrations Error <img src="img/warning.svg" width="17"/>
{ integrationsError }
</div>
);
} else {
integrationsButton = (
<div className="mx_RoomSettings_integrationsButton" style={{ opacity: 0.5 }}>
Manage Integrations
</div>
);
Integrations Error <img src="img/warning.svg" width="17"/>
{ integrationsError }
</div>
);
} else {
integrationsButton = (
<div className="mx_RoomSettings_integrationsButton" style={{opacity: 0.5}}>
{ _t('Manage Integrations') }
</div>
);
}
}
return (
@ -807,28 +841,28 @@ module.exports = React.createClass({
<div className="mx_RoomSettings_toggles">
<div className="mx_RoomSettings_settings">
<h3>Who can access this room?</h3>
<h3>{ _t('Who can access this room?') }</h3>
{ inviteGuestWarning }
<label>
<input type="radio" name="roomVis" value="invite_only"
disabled={ !this.mayChangeRoomAccess() }
onChange={this._onRoomAccessRadioToggle}
checked={this.state.join_rule !== "public"}/>
Only people who have been invited
{ _t('Only people who have been invited') }
</label>
<label>
<input type="radio" name="roomVis" value="public_no_guests"
disabled={ !this.mayChangeRoomAccess() }
onChange={this._onRoomAccessRadioToggle}
checked={this.state.join_rule === "public" && this.state.guest_access !== "can_join"}/>
Anyone who knows the room's link, apart from guests
{ _t('Anyone who knows the room\'s link, apart from guests') }
</label>
<label>
<input type="radio" name="roomVis" value="public_with_guests"
disabled={ !this.mayChangeRoomAccess() }
onChange={this._onRoomAccessRadioToggle}
checked={this.state.join_rule === "public" && this.state.guest_access === "can_join"}/>
Anyone who knows the room's link, including guests
{ _t('Anyone who knows the room\'s link, including guests') }
</label>
{ addressWarning }
<br/>
@ -837,45 +871,45 @@ module.exports = React.createClass({
<input type="checkbox" disabled={ !roomState.mayClientSendStateEvent("m.room.aliases", cli) }
onChange={ this._onToggle.bind(this, "isRoomPublished", true, false)}
checked={this.state.isRoomPublished}/>
List this room in { MatrixClientPeg.get().getDomain() }'s room directory?
{_t("List this room in %(domain)s's room directory?", { domain: MatrixClientPeg.get().getDomain() })}
</label>
</div>
<div className="mx_RoomSettings_settings">
<h3>Who can read history?</h3>
<h3>{ _t('Who can read history?') }</h3>
<label>
<input type="radio" name="historyVis" value="world_readable"
disabled={ !roomState.mayClientSendStateEvent("m.room.history_visibility", cli) }
checked={historyVisibility === "world_readable"}
onChange={this._onHistoryRadioToggle} />
Anyone
{_t("Anyone")}
</label>
<label>
<input type="radio" name="historyVis" value="shared"
disabled={ !roomState.mayClientSendStateEvent("m.room.history_visibility", cli) }
checked={historyVisibility === "shared"}
onChange={this._onHistoryRadioToggle} />
Members only (since the point in time of selecting this option)
{ _t('Members only') } ({ _t('since the point in time of selecting this option') })
</label>
<label>
<input type="radio" name="historyVis" value="invited"
disabled={ !roomState.mayClientSendStateEvent("m.room.history_visibility", cli) }
checked={historyVisibility === "invited"}
onChange={this._onHistoryRadioToggle} />
Members only (since they were invited)
{ _t('Members only') } ({ _t('since they were invited') })
</label>
<label >
<input type="radio" name="historyVis" value="joined"
disabled={ !roomState.mayClientSendStateEvent("m.room.history_visibility", cli) }
checked={historyVisibility === "joined"}
onChange={this._onHistoryRadioToggle} />
Members only (since they joined)
{ _t('Members only') } ({ _t('since they joined') })
</label>
</div>
</div>
<div>
<h3>Room Colour</h3>
<h3>{ _t('Room Colour') }</h3>
<ColorSettings ref="color_settings" room={this.props.room} />
</div>
@ -893,41 +927,41 @@ module.exports = React.createClass({
<UrlPreviewSettings ref="url_preview_settings" room={this.props.room} />
<h3>Permissions</h3>
<h3>{ _t('Permissions') }</h3>
<div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings">
<div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">The default role for new room members is </span>
<span className="mx_RoomSettings_powerLevelKey">{ _t('The default role for new room members is') } </span>
<PowerSelector ref="users_default" value={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < default_user_level} onChange={this.onPowerLevelsChanged}/>
</div>
<div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">To send messages, you must be a </span>
<span className="mx_RoomSettings_powerLevelKey">{ _t('To send messages') }, { _t('you must be a') } </span>
<PowerSelector ref="events_default" value={send_level} controlled={false} disabled={!can_change_levels || current_user_level < send_level} onChange={this.onPowerLevelsChanged}/>
</div>
<div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">To invite users into the room, you must be a </span>
<span className="mx_RoomSettings_powerLevelKey">{ _t('To invite users into the room') }, { _t('you must be a') } </span>
<PowerSelector ref="invite" value={invite_level} controlled={false} disabled={!can_change_levels || current_user_level < invite_level} onChange={this.onPowerLevelsChanged}/>
</div>
<div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">To configure the room, you must be a </span>
<span className="mx_RoomSettings_powerLevelKey">{ _t('To configure the room') }, { _t('you must be a') } </span>
<PowerSelector ref="state_default" value={state_level} controlled={false} disabled={!can_change_levels || current_user_level < state_level} onChange={this.onPowerLevelsChanged}/>
</div>
<div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">To kick users, you must be a </span>
<span className="mx_RoomSettings_powerLevelKey">{ _t('To kick users') }, { _t('you must be a') } </span>
<PowerSelector ref="kick" value={kick_level} controlled={false} disabled={!can_change_levels || current_user_level < kick_level} onChange={this.onPowerLevelsChanged}/>
</div>
<div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">To ban users, you must be a </span>
<span className="mx_RoomSettings_powerLevelKey">{ _t('To ban users') }, { _t('you must be a') } </span>
<PowerSelector ref="ban" value={ban_level} controlled={false} disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged}/>
</div>
<div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">To redact messages, you must be a </span>
<span className="mx_RoomSettings_powerLevelKey">{ _t('To remove other users\' messages') }, { _t('you must be a') } </span>
<PowerSelector ref="redact" value={redact_level} controlled={false} disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged}/>
</div>
{Object.keys(events_levels).map(function(event_type, i) {
return (
<div className="mx_RoomSettings_powerLevel" key={event_type}>
<span className="mx_RoomSettings_powerLevelKey">To send events of type <code>{ event_type }</code>, you must be a </span>
<span className="mx_RoomSettings_powerLevelKey">{ _t('To send events of type') } <code>{ event_type }</code>, { _t('you must be a') } </span>
<PowerSelector value={ events_levels[event_type] } controlled={false} disabled={true} onChange={self.onPowerLevelsChanged}/>
</div>
);
@ -940,9 +974,9 @@ module.exports = React.createClass({
{ bannedUsersSection }
<h3>Advanced</h3>
<h3>{ _t('Advanced') }</h3>
<div className="mx_RoomSettings_settings">
This room's internal ID is <code>{ this.props.room.roomId }</code>
{ _t('This room\'s internal ID is') } <code>{ this.props.room.roomId }</code>
</div>
</div>
);

View file

@ -98,9 +98,9 @@ module.exports = React.createClass({
}
},
onClick: function() {
onClick: function(ev) {
if (this.props.onClick) {
this.props.onClick(this.props.room.roomId);
this.props.onClick(this.props.room.roomId, ev);
}
},
@ -224,13 +224,13 @@ module.exports = React.createClass({
if (this.props.selected) {
let nameSelected = <EmojiText>{name}</EmojiText>;
label = <div title={ name } className={ nameClasses }>{ nameSelected }</div>;
label = <div title={ name } className={ nameClasses } dir="auto">{ nameSelected }</div>;
} else {
label = <EmojiText element="div" title={ name } className={ nameClasses }>{name}</EmojiText>;
label = <EmojiText element="div" title={ name } className={ nameClasses } dir="auto">{name}</EmojiText>;
}
} else if (this.state.hover) {
var RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
tooltip = <RoomTooltip className="mx_RoomTile_tooltip" room={this.props.room} />;
tooltip = <RoomTooltip className="mx_RoomTile_tooltip" room={this.props.room} dir="auto" />;
}
//var incomingCallBox;

View file

@ -18,6 +18,7 @@ limitations under the License.
var React = require('react');
var sdk = require('../../../index');
import { _t } from "../../../languageHandler";
module.exports = React.createClass({
displayName: 'RoomTopicEditor',
@ -43,9 +44,10 @@ module.exports = React.createClass({
<EditableText ref="editor"
className="mx_RoomHeader_topic mx_RoomHeader_editable"
placeholderClassName="mx_RoomHeader_placeholder"
placeholder="Add a topic"
placeholder={_t("Add a topic")}
blurToCancel={ false }
initialValue={ this._initialTopic }/>
initialValue={ this._initialTopic }
dir="auto" />
);
},
});

View file

@ -60,7 +60,7 @@ module.exports = React.createClass({
}
}
return (
<li data-scroll-token={eventId+"+"+j}>
<li data-scroll-tokens={eventId+"+"+j}>
{ret}
</li>);
},

View file

@ -17,6 +17,7 @@ var React = require('react');
var MatrixClientPeg = require("../../../MatrixClientPeg");
var Modal = require("../../../Modal");
var sdk = require("../../../index");
import { _t } from '../../../languageHandler';
var GeminiScrollbar = require('react-gemini-scrollbar');
// A list capable of displaying entities which conform to the SearchableEntity
@ -25,7 +26,6 @@ var SearchableEntityList = React.createClass({
displayName: 'SearchableEntityList',
propTypes: {
searchPlaceholderText: React.PropTypes.string,
emptyQueryShowsAll: React.PropTypes.bool,
showInputBox: React.PropTypes.bool,
onQueryChanged: React.PropTypes.func, // fn(inputText)
@ -37,7 +37,6 @@ var SearchableEntityList = React.createClass({
getDefaultProps: function() {
return {
showInputBox: true,
searchPlaceholderText: "Search",
entities: [],
emptyQueryShowsAll: false,
onSubmit: function() {},
@ -118,7 +117,7 @@ var SearchableEntityList = React.createClass({
_createOverflowEntity: function(overflowCount, totalCount) {
var EntityTile = sdk.getComponent("rooms.EntityTile");
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
var text = "and " + overflowCount + " other" + (overflowCount > 1 ? "s" : "") + "...";
const text = _t("and %(count)s others...", { count: overflowCount });
return (
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
<BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} />
@ -137,7 +136,7 @@ var SearchableEntityList = React.createClass({
onChange={this.onQueryChanged} value={this.state.query}
onFocus= {() => { this.setState({ focused: true }); }}
onBlur= {() => { this.setState({ focused: false }); }}
placeholder={this.props.searchPlaceholderText} />
placeholder={ _t("Search") } />
</form>
);
}

View file

@ -14,11 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import React from 'react';
import dis from '../../../dispatcher';
import AccessibleButton from '../elements/AccessibleButton';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
// cancel button which is shared between room header and simple room header
export function CancelButton(props) {
@ -27,7 +26,7 @@ export function CancelButton(props) {
return (
<AccessibleButton className='mx_RoomHeader_cancelButton' onClick={onClick}>
<img src="img/cancel.svg" className='mx_filterFlipColor'
width="18" height="18" alt="Cancel"/>
width="18" height="18" alt={_t("Cancel")}/>
</AccessibleButton>
);
}
@ -43,38 +42,30 @@ export default React.createClass({
title: React.PropTypes.string,
onCancelClick: React.PropTypes.func,
// is the RightPanel collapsed?
collapsedRhs: React.PropTypes.bool,
},
onShowRhsClick: function(ev) {
dis.dispatch({ action: 'show_right_panel' });
// `src` to a TintableSvg. Optional.
icon: React.PropTypes.string,
},
render: function() {
let cancelButton;
let icon;
if (this.props.onCancelClick) {
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
}
let showRhsButton;
/* // don't bother cluttering things up with this for now.
const TintableSvg = sdk.getComponent("elements.TintableSvg");
if (this.props.collapsedRhs) {
showRhsButton =
<div className="mx_RoomHeader_button" style={{ float: 'right' }} onClick={this.onShowRhsClick} title=">">
<TintableSvg src="img/minimise.svg" width="10" height="16"/>
</div>
if (this.props.icon) {
const TintableSvg = sdk.getComponent('elements.TintableSvg');
icon = <TintableSvg
className="mx_RoomHeader_icon" src={this.props.icon}
width="25" height="25"
/>;
}
*/
return (
<div className="mx_RoomHeader" >
<div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_simpleHeader">
{ icon }
{ this.props.title }
{ showRhsButton }
{ cancelButton }
</div>
</div>

View file

@ -1,48 +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.
*/
'use strict';
var React = require('react');
var MatrixClientPeg = require("../../../MatrixClientPeg");
var CommandEntry = require("../../../TabCompleteEntries").CommandEntry;
module.exports = React.createClass({
displayName: 'TabCompleteBar',
propTypes: {
tabComplete: React.PropTypes.object.isRequired
},
render: function() {
return (
<div className="mx_TabCompleteBar">
{this.props.tabComplete.peek(6).map((entry, i) => {
return (
<div key={entry.getKey() || i + ""}
className={ "mx_TabCompleteBar_item " + (entry instanceof CommandEntry ? "mx_TabCompleteBar_command" : "") }
onClick={this.props.tabComplete.onEntryClick.bind(this.props.tabComplete, entry)} >
{entry.getImageJsx()}
<span className="mx_TabCompleteBar_text">
{entry.getText()}
</span>
</div>
);
})}
</div>
);
}
});

View file

@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -17,6 +18,7 @@ limitations under the License.
'use strict';
var React = require('react');
import { _t } from '../../../languageHandler';
var sdk = require('../../../index');
module.exports = React.createClass({
@ -32,14 +34,16 @@ module.exports = React.createClass({
<div className="mx_TopUnreadMessagesBar">
<div className="mx_TopUnreadMessagesBar_scrollUp"
onClick={this.props.onScrollUpClick}>
Jump to first unread message. <span style={{ textDecoration: 'underline' }} onClick={this.props.onCloseClick}>Mark all read</span>
<img src="img/scrollto.svg" width="24" height="24"
alt={ _t('Scroll to unread messages') }
title={ _t('Scroll to unread messages') }/>
{ _t("Jump to first unread message.") }
</div>
<img className="mx_TopUnreadMessagesBar_close"
<img className="mx_TopUnreadMessagesBar_close mx_filterFlipColor"
src="img/cancel.svg" width="18" height="18"
alt="Close" title="Close"
alt={_t("Close")} title={_t("Close")}
onClick={this.props.onCloseClick} />
</div>
);
},
});

View file

@ -15,14 +15,14 @@ limitations under the License.
*/
import React from 'react';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import AddThreepid from '../../../AddThreepid';
import WithMatrixClient from '../../../wrappers/WithMatrixClient';
import withMatrixClient from '../../../wrappers/withMatrixClient';
import Modal from '../../../Modal';
export default WithMatrixClient(React.createClass({
export default withMatrixClient(React.createClass({
displayName: 'AddPhoneNumber',
propTypes: {
@ -50,7 +50,7 @@ export default WithMatrixClient(React.createClass({
},
_onPhoneCountryChange: function(phoneCountry) {
this.setState({ phoneCountry: phoneCountry });
this.setState({ phoneCountry: phoneCountry.iso2 });
},
_onPhoneNumberChange: function(ev) {
@ -83,7 +83,7 @@ export default WithMatrixClient(React.createClass({
console.error("Unable to add phone number: " + err);
let msg = err.message;
Modal.createDialog(ErrorDialog, {
title: "Error",
title: _t("Error"),
description: msg,
});
}).finally(() => {
@ -98,20 +98,19 @@ export default WithMatrixClient(React.createClass({
if (this._unmounted) return;
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
let msgElements = [
<div key="_static" >A text message has been sent to +{msisdn}.
Please enter the verification code it contains</div>
<div key="_static" >{ _t("A text message has been sent to +%(msisdn)s. Please enter the verification code it contains", { msisdn: msisdn} ) }</div>
];
if (err) {
let msg = err.error;
if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
msg = "Incorrect verification code";
msg = _t("Incorrect verification code");
}
msgElements.push(<div key="_error" className="error">{msg}</div>);
}
Modal.createDialog(TextInputDialog, {
title: "Enter Code",
title: _t("Enter Code"),
description: <div>{msgElements}</div>,
button: "Submit",
button: _t("Submit"),
onFinished: (should_verify, token) => {
if (!should_verify) {
this._addThreepid = null;
@ -147,24 +146,26 @@ export default WithMatrixClient(React.createClass({
return (
<form className="mx_UserSettings_profileTableRow" onSubmit={this._onAddMsisdnSubmit}>
<div className="mx_UserSettings_profileLabelCell">
<label>{_t('Phone')}</label>
</div>
<div className="mx_UserSettings_profileInputCell">
<div className="mx_Login_phoneSection">
<div className="mx_UserSettings_phoneSection">
<CountryDropdown onOptionChange={this._onPhoneCountryChange}
className="mx_Login_phoneCountry"
className="mx_UserSettings_phoneCountry"
value={this.state.phoneCountry}
isSmall={true}
/>
<input type="text"
ref={this._collectAddMsisdnInput}
className="mx_UserSettings_phoneNumberField"
placeholder="Add phone number"
placeholder={ _t('Add phone number') }
value={this.state.phoneNumber}
onChange={this._onPhoneNumberChange}
/>
</div>
</div>
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
<input type="image" value="Add" src="img/plus.svg" width="14" height="14" />
<input type="image" value={_t("Add")} src="img/plus.svg" width="14" height="14" />
</div>
</form>
);

View file

@ -17,6 +17,7 @@ limitations under the License.
var React = require('react');
var MatrixClientPeg = require("../../../MatrixClientPeg");
var sdk = require('../../../index');
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'ChangeAvatar',
@ -105,7 +106,7 @@ module.exports = React.createClass({
onError: function(error) {
this.setState({
errorText: "Failed to upload profile picture!"
errorText: _t("Failed to upload profile picture!")
});
},
@ -127,7 +128,7 @@ module.exports = React.createClass({
if (this.props.showUploadSection) {
uploadSection = (
<div className={this.props.className}>
Upload new:
{_t("Upload new:")}
<input type="file" accept="image/*" onChange={this.onFileSelected}/>
{this.state.errorText}
</div>

View file

@ -18,6 +18,7 @@ limitations under the License.
var React = require('react');
var sdk = require('../../../index');
var MatrixClientPeg = require("../../../MatrixClientPeg");
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'ChangeDisplayName',
@ -52,7 +53,7 @@ module.exports = React.createClass({
return (
<EditableTextContainer
getInitialValue={this._getDisplayName}
placeholder="No display name"
placeholder={_t("No display name")}
blurToSubmit={true}
onSubmit={this._changeDisplayName} />
);

View file

@ -20,7 +20,12 @@ var React = require('react');
var MatrixClientPeg = require("../../../MatrixClientPeg");
var Modal = require("../../../Modal");
var sdk = require("../../../index");
import Promise from 'bluebird';
import AccessibleButton from '../elements/AccessibleButton';
import { _t } from '../../../languageHandler';
import sessionStore from '../../../stores/SessionStore';
module.exports = React.createClass({
displayName: 'ChangePassword',
@ -31,7 +36,10 @@ module.exports = React.createClass({
rowClassName: React.PropTypes.string,
rowLabelClassName: React.PropTypes.string,
rowInputClassName: React.PropTypes.string,
buttonClassName: React.PropTypes.string
buttonClassName: React.PropTypes.string,
confirm: React.PropTypes.bool,
// Whether to autoFocus the new password input
autoFocusNewPasswordInput: React.PropTypes.bool,
},
Phases: {
@ -47,70 +55,125 @@ module.exports = React.createClass({
onCheckPassword: function(oldPass, newPass, confirmPass) {
if (newPass !== confirmPass) {
return {
error: "New passwords don't match."
error: _t("New passwords don't match")
};
} else if (!newPass || newPass.length === 0) {
return {
error: "Passwords can't be empty"
error: _t("Passwords can't be empty")
};
}
}
},
confirm: true,
};
},
getInitialState: function() {
return {
phase: this.Phases.Edit
phase: this.Phases.Edit,
cachedPassword: null,
};
},
changePassword: function(old_password, new_password) {
var cli = MatrixClientPeg.get();
componentWillMount: function() {
this._sessionStore = sessionStore;
this._sessionStoreToken = this._sessionStore.addListener(
this._setStateFromSessionStore,
);
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
this._setStateFromSessionStore();
},
componentWillUnmount: function() {
if (this._sessionStoreToken) {
this._sessionStoreToken.remove();
}
},
_setStateFromSessionStore: function() {
this.setState({
cachedPassword: this._sessionStore.getCachedPassword(),
});
},
changePassword: function(oldPassword, newPassword) {
const cli = MatrixClientPeg.get();
if (!this.props.confirm) {
this._changePassword(cli, oldPassword, newPassword);
return;
}
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: "Warning",
title: _t("Warning!"),
description:
<div>
Changing password will currently reset any end-to-end encryption keys on all devices,
making encrypted chat history unreadable, unless you first export your room keys
and re-import them afterwards.
In future this <a href="https://github.com/vector-im/riot-web/issues/2671">will be improved</a>.
{ _t(
'Changing password will currently reset any end-to-end encryption keys on all devices, ' +
'making encrypted chat history unreadable, unless you first export your room keys ' +
'and re-import them afterwards. ' +
'In future this will be improved.'
) } (<a href="https://github.com/vector-im/riot-web/issues/2671">https://github.com/vector-im/riot-web/issues/2671</a>)
</div>,
button: "Continue",
button: _t("Continue"),
extraButtons: [
<button className="mx_Dialog_primary"
onClick={this._onExportE2eKeysClicked}>
Export E2E room keys
{ _t('Export E2E room keys') }
</button>
],
onFinished: (confirmed) => {
if (confirmed) {
var authDict = {
type: 'm.login.password',
user: cli.credentials.userId,
password: old_password
};
this.setState({
phase: this.Phases.Uploading
});
var self = this;
cli.setPassword(authDict, new_password).then(function() {
self.props.onFinished();
}, function(err) {
self.props.onError(err);
}).finally(function() {
self.setState({
phase: self.Phases.Edit
});
}).done();
this._changePassword(cli, oldPassword, newPassword);
}
},
});
},
_changePassword: function(cli, oldPassword, newPassword) {
const authDict = {
type: 'm.login.password',
user: cli.credentials.userId,
password: oldPassword,
};
this.setState({
phase: this.Phases.Uploading,
});
cli.setPassword(authDict, newPassword).then(() => {
if (this.props.shouldAskForEmail) {
return this._optionallySetEmail().then((confirmed) => {
this.props.onFinished({
didSetEmail: confirmed,
});
});
} else {
this.props.onFinished();
}
}, (err) => {
this.props.onError(err);
}).finally(() => {
this.setState({
phase: this.Phases.Edit,
});
}).done();
},
_optionallySetEmail: function() {
const deferred = Promise.defer();
// Ask for an email otherwise the user has no way to reset their password
const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog");
Modal.createDialog(SetEmailDialog, {
title: _t('Do you want to set an email address?'),
onFinished: (confirmed) => {
// ignore confirmed, setting an email is optional
deferred.resolve(confirmed);
},
});
return deferred.promise;
},
_onExportE2eKeysClicked: function() {
Modal.createDialogAsync(
(cb) => {
@ -121,62 +184,69 @@ module.exports = React.createClass({
matrixClient: MatrixClientPeg.get(),
}
);
},
},
onClickChange: function() {
var old_password = this.refs.old_input.value;
var new_password = this.refs.new_input.value;
var confirm_password = this.refs.confirm_input.value;
var err = this.props.onCheckPassword(
old_password, new_password, confirm_password
const oldPassword = this.state.cachedPassword || this.refs.old_input.value;
const newPassword = this.refs.new_input.value;
const confirmPassword = this.refs.confirm_input.value;
const err = this.props.onCheckPassword(
oldPassword, newPassword, confirmPassword,
);
if (err) {
this.props.onError(err);
}
else {
this.changePassword(old_password, new_password);
} else {
this.changePassword(oldPassword, newPassword);
}
},
render: function() {
var rowClassName = this.props.rowClassName;
var rowLabelClassName = this.props.rowLabelClassName;
var rowInputClassName = this.props.rowInputClassName;
var buttonClassName = this.props.buttonClassName;
const rowClassName = this.props.rowClassName;
const rowLabelClassName = this.props.rowLabelClassName;
const rowInputClassName = this.props.rowInputClassName;
const buttonClassName = this.props.buttonClassName;
let currentPassword = null;
if (!this.state.cachedPassword) {
currentPassword = <div className={rowClassName}>
<div className={rowLabelClassName}>
<label htmlFor="passwordold">Current password</label>
</div>
<div className={rowInputClassName}>
<input id="passwordold" type="password" ref="old_input" />
</div>
</div>;
}
switch (this.state.phase) {
case this.Phases.Edit:
const passwordLabel = this.state.cachedPassword ?
_t('Password') : _t('New Password');
return (
<div className={this.props.className}>
<form className={this.props.className} onSubmit={this.onClickChange}>
{ currentPassword }
<div className={rowClassName}>
<div className={rowLabelClassName}>
<label htmlFor="passwordold">Current password</label>
<label htmlFor="password1">{ passwordLabel }</label>
</div>
<div className={rowInputClassName}>
<input id="passwordold" type="password" ref="old_input" />
<input id="password1" type="password" ref="new_input" autoFocus={this.props.autoFocusNewPasswordInput} />
</div>
</div>
<div className={rowClassName}>
<div className={rowLabelClassName}>
<label htmlFor="password1">New password</label>
</div>
<div className={rowInputClassName}>
<input id="password1" type="password" ref="new_input" />
</div>
</div>
<div className={rowClassName}>
<div className={rowLabelClassName}>
<label htmlFor="password2">Confirm password</label>
<label htmlFor="password2">{ _t('Confirm password') }</label>
</div>
<div className={rowInputClassName}>
<input id="password2" type="password" ref="confirm_input" />
</div>
</div>
<AccessibleButton className={buttonClassName}
onClick={this.onClickChange}>
Change Password
onClick={this.onClickChange}
element="button">
{ _t('Change Password') }
</AccessibleButton>
</div>
</form>
);
case this.Phases.Uploading:
var Loader = sdk.getComponent("elements.Spinner");

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