Merge branch 'develop' into travis/redact-to-remove

This commit is contained in:
Travis Ralston 2017-05-28 17:45:32 -04:00 committed by GitHub
commit 1b6685c5da
120 changed files with 7005 additions and 2054 deletions

View file

@ -16,15 +16,16 @@ limitations under the License.
'use strict';
var React = require("react");
var MatrixClientPeg = require("../../MatrixClientPeg");
var PresetValues = {
import React from 'react';
import q from 'q';
import { _t } from '../../languageHandler';
import sdk from '../../index';
import MatrixClientPeg from '../../MatrixClientPeg';
const PresetValues = {
PrivateChat: "private_chat",
PublicChat: "public_chat",
Custom: "custom",
};
var q = require('q');
var sdk = require('../../index');
module.exports = React.createClass({
displayName: 'CreateRoom',
@ -231,7 +232,7 @@ module.exports = React.createClass({
if (curr_phase == this.phases.ERROR) {
error_box = (
<div className="mx_Error">
An error occured: {this.state.error_string}
{_t('An error occured: %(error_string)s', {error_string: this.state.error_string})}
</div>
);
}
@ -248,27 +249,27 @@ module.exports = React.createClass({
<div className="mx_CreateRoom">
<SimpleRoomHeader title="CreateRoom" collapsedRhs={ this.props.collapsedRhs }/>
<div className="mx_CreateRoom_body">
<input type="text" ref="room_name" value={this.state.room_name} onChange={this.onNameChange} placeholder="Name"/> <br />
<textarea className="mx_CreateRoom_description" ref="topic" value={this.state.topic} onChange={this.onTopicChange} placeholder="Topic"/> <br />
<input type="text" ref="room_name" value={this.state.room_name} onChange={this.onNameChange} placeholder={_t('Name')}/> <br />
<textarea className="mx_CreateRoom_description" ref="topic" value={this.state.topic} onChange={this.onTopicChange} placeholder={_t('Topic')}/> <br />
<RoomAlias ref="alias" alias={this.state.alias} homeserver={ domain } onChange={this.onAliasChanged}/> <br />
<UserSelector ref="user_selector" selected_users={this.state.invited_users} onChange={this.onInviteChanged}/> <br />
<Presets ref="presets" onChange={this.onPresetChanged} preset={this.state.preset}/> <br />
<div>
<label>
<input type="checkbox" ref="is_private" checked={this.state.is_private} onChange={this.onPrivateChanged}/>
Make this room private
{_t('Make this room private')}
</label>
</div>
<div>
<label>
<input type="checkbox" ref="share_history" checked={this.state.share_history} onChange={this.onShareHistoryChanged}/>
Share message history with new users
{_t('Share message history with new users')}
</label>
</div>
<div className="mx_CreateRoom_encrypt">
<label>
<input type="checkbox" ref="encrypt" checked={this.state.encrypt} onChange={this.onEncryptChanged}/>
Encrypt room
{_t('Encrypt room')}
</label>
</div>
<div>

View file

@ -14,13 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require('react');
var ReactDOM = require("react-dom");
import React from 'react';
var Matrix = require("matrix-js-sdk");
var sdk = require('../../index');
var MatrixClientPeg = require("../../MatrixClientPeg");
var dis = require("../../dispatcher");
import Matrix from 'matrix-js-sdk';
import sdk from '../../index';
import MatrixClientPeg from '../../MatrixClientPeg';
import { _t } from '../../languageHandler';
/*
* Component which shows the filtered file using a TimelinePanel
@ -59,6 +58,8 @@ var FilePanel = React.createClass({
var client = MatrixClientPeg.get();
var room = client.getRoom(roomId);
this.noRoom = !room;
if (room) {
var filter = new Matrix.Filter(client.credentials.userId);
filter.setDefinition(
@ -82,13 +83,22 @@ var FilePanel = React.createClass({
console.error("Failed to get or create file panel filter", error);
}
);
}
else {
} else {
console.error("Failed to add filtered timelineSet for FilePanel as no room!");
}
},
render: function() {
if (MatrixClientPeg.get().isGuest()) {
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
<div className="mx_RoomView_empty">You must <a href="#/register">register</a> to use this functionality</div>
</div>;
} else if (this.noRoom) {
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
<div className="mx_RoomView_empty">You must join the room to see its files</div>
</div>;
}
// wrap a TimelinePanel with the jump-to-event bits turned off.
var TimelinePanel = sdk.getComponent("structures.TimelinePanel");
var Loader = sdk.getComponent("elements.Spinner");
@ -105,7 +115,7 @@ var FilePanel = React.createClass({
showUrlPreview = { false }
tileShape="file_grid"
opacity={ this.props.opacity }
empty="There are no visible files in this room"
empty={_t('There are no visible files in this room')}
/>
);
}

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.
@ -106,18 +107,6 @@ export default React.createClass({
var handled = false;
switch (ev.keyCode) {
case KeyCode.ESCAPE:
// Implemented this way so possible handling for other pages is neater
switch (this.props.page_type) {
case PageTypes.UserSettings:
this.props.onUserSettingsClose();
handled = true;
break;
}
break;
case KeyCode.UP:
case KeyCode.DOWN:
if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
@ -162,19 +151,19 @@ export default React.createClass({
},
render: function() {
var LeftPanel = sdk.getComponent('structures.LeftPanel');
var RightPanel = sdk.getComponent('structures.RightPanel');
var RoomView = sdk.getComponent('structures.RoomView');
var UserSettings = sdk.getComponent('structures.UserSettings');
var CreateRoom = sdk.getComponent('structures.CreateRoom');
var RoomDirectory = sdk.getComponent('structures.RoomDirectory');
var HomePage = sdk.getComponent('structures.HomePage');
var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
var GuestWarningBar = sdk.getComponent('globals.GuestWarningBar');
var NewVersionBar = sdk.getComponent('globals.NewVersionBar');
const LeftPanel = sdk.getComponent('structures.LeftPanel');
const RightPanel = sdk.getComponent('structures.RightPanel');
const RoomView = sdk.getComponent('structures.RoomView');
const UserSettings = sdk.getComponent('structures.UserSettings');
const CreateRoom = sdk.getComponent('structures.CreateRoom');
const RoomDirectory = sdk.getComponent('structures.RoomDirectory');
const HomePage = sdk.getComponent('structures.HomePage');
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
const GuestWarningBar = sdk.getComponent('globals.GuestWarningBar');
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
var page_element;
var right_panel = '';
let page_element;
let right_panel = '';
switch (this.props.page_type) {
case PageTypes.RoomView:
@ -194,7 +183,7 @@ export default React.createClass({
ConferenceHandler={this.props.ConferenceHandler}
scrollStateMap={this._scrollStateMap}
/>;
if (!this.props.collapse_rhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.sideOpacity} />;
if (!this.props.collapse_rhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.rightOpacity} />;
break;
case PageTypes.UserSettings:
@ -206,7 +195,7 @@ export default React.createClass({
referralBaseUrl={this.props.config.referralBaseUrl}
teamToken={this.props.teamToken}
/>;
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
break;
case PageTypes.CreateRoom:
@ -214,16 +203,14 @@ export default React.createClass({
onRoomCreated={this.props.onRoomCreated}
collapsedRhs={this.props.collapse_rhs}
/>;
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
break;
case PageTypes.RoomDirectory:
page_element = <RoomDirectory
ref="roomDirectory"
collapsedRhs={this.props.collapse_rhs}
config={this.props.config.roomDirectory}
/>;
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
break;
case PageTypes.HomePage:
@ -232,12 +219,12 @@ export default React.createClass({
teamServerUrl={this.props.config.teamServerConfig.teamServerURL}
teamToken={this.props.teamToken}
/>
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>
break;
case PageTypes.UserView:
page_element = null; // deliberately null for now
right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.sideOpacity} />;
right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.rightOpacity} />;
break;
}
@ -266,7 +253,7 @@ export default React.createClass({
<LeftPanel
selectedRoom={this.props.currentRoomId}
collapsed={this.props.collapse_lhs || false}
opacity={this.props.sideOpacity}
opacity={this.props.leftOpacity}
teamToken={this.props.teamToken}
/>
<main className='mx_MatrixChat_middlePanel'>

View file

@ -17,28 +17,26 @@ limitations under the License.
import q from 'q';
var React = require('react');
var Matrix = require("matrix-js-sdk");
import React from 'react';
import Matrix from "matrix-js-sdk";
var MatrixClientPeg = require("../../MatrixClientPeg");
var PlatformPeg = require("../../PlatformPeg");
var SdkConfig = require("../../SdkConfig");
var ContextualMenu = require("./ContextualMenu");
var RoomListSorter = require("../../RoomListSorter");
var UserActivity = require("../../UserActivity");
var Presence = require("../../Presence");
var dis = require("../../dispatcher");
import MatrixClientPeg from "../../MatrixClientPeg";
import PlatformPeg from "../../PlatformPeg";
import SdkConfig from "../../SdkConfig";
import * as RoomListSorter from "../../RoomListSorter";
import dis from "../../dispatcher";
var Modal = require("../../Modal");
var Tinter = require("../../Tinter");
var sdk = require('../../index');
var Rooms = require('../../Rooms');
var linkifyMatrix = require("../../linkify-matrix");
var Lifecycle = require('../../Lifecycle');
var PageTypes = require('../../PageTypes');
import Modal from "../../Modal";
import Tinter from "../../Tinter";
import sdk from '../../index';
import * as Rooms from '../../Rooms';
import linkifyMatrix from "../../linkify-matrix";
import * as Lifecycle from '../../Lifecycle';
import PageTypes from '../../PageTypes';
var createRoom = require("../../createRoom");
import createRoom from "../../createRoom";
import * as UDEHandler from '../../UnknownDeviceErrorHandler';
import { _t } from '../../languageHandler';
module.exports = React.createClass({
displayName: 'MatrixChat',
@ -89,7 +87,7 @@ module.exports = React.createClass({
},
getInitialState: function() {
var s = {
const s = {
loading: true,
screen: undefined,
screenAfterLogin: this.props.initialScreenAfterLogin,
@ -119,8 +117,9 @@ module.exports = React.createClass({
collapse_rhs: false,
ready: false,
width: 10000,
sideOpacity: 1.0,
leftOpacity: 1.0,
middleOpacity: 1.0,
rightOpacity: 1.0,
version: null,
newVersion: null,
@ -156,11 +155,9 @@ module.exports = React.createClass({
return this.state.register_hs_url;
} else if (MatrixClientPeg.get()) {
return MatrixClientPeg.get().getHomeserverUrl();
}
else if (window.localStorage && window.localStorage.getItem("mx_hs_url")) {
} else if (window.localStorage && window.localStorage.getItem("mx_hs_url")) {
return window.localStorage.getItem("mx_hs_url");
}
else {
} else {
return this.getDefaultHsUrl();
}
},
@ -178,11 +175,9 @@ module.exports = React.createClass({
return this.state.register_is_url;
} else if (MatrixClientPeg.get()) {
return MatrixClientPeg.get().getIdentityServerUrl();
}
else if (window.localStorage && window.localStorage.getItem("mx_is_url")) {
} else if (window.localStorage && window.localStorage.getItem("mx_is_url")) {
return window.localStorage.getItem("mx_is_url");
}
else {
} else {
return this.getDefaultIsUrl();
}
},
@ -253,7 +248,6 @@ module.exports = React.createClass({
UDEHandler.startListening();
this.focusComposer = false;
window.addEventListener("focus", this.onFocus);
// this can technically be done anywhere but doing this here keeps all
// the routing url path logic together.
@ -324,28 +318,14 @@ module.exports = React.createClass({
onAction: function(payload) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
var roomIndexDelta = 1;
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
var self = this;
switch (payload.action) {
case 'logout':
Lifecycle.logout();
break;
case 'start_registration':
const params = payload.params || {};
this.setStateForNewScreen({
screen: 'register',
// these params may be undefined, but if they are,
// unset them from our state: we don't want to
// resume a previous registration session if the
// user just clicked 'register'
register_client_secret: params.client_secret,
register_session_id: params.session_id,
register_hs_url: params.hs_url,
register_is_url: params.is_url,
register_id_sid: params.sid,
});
this.notifyNewScreen('register');
this._startRegistration(payload.params || {});
break;
case 'start_login':
if (MatrixClientPeg.get() &&
@ -362,7 +342,7 @@ module.exports = React.createClass({
break;
case 'start_post_registration':
this.setState({ // don't clobber loggedIn status
screen: 'post_registration'
screen: 'post_registration',
});
break;
case 'start_upgrade_registration':
@ -392,38 +372,12 @@ module.exports = React.createClass({
this.notifyNewScreen('forgot_password');
break;
case 'leave_room':
Modal.createDialog(QuestionDialog, {
title: "Leave room",
description: "Are you sure you want to leave the room?",
onFinished: (should_leave) => {
if (should_leave) {
const d = MatrixClientPeg.get().leave(payload.room_id);
// FIXME: controller shouldn't be loading a view :(
const Loader = sdk.getComponent("elements.Spinner");
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
d.then(() => {
modal.close();
if (this.currentRoomId === payload.room_id) {
dis.dispatch({action: 'view_next_room'});
}
}, (err) => {
modal.close();
console.error("Failed to leave room " + payload.room_id + " " + err);
Modal.createDialog(ErrorDialog, {
title: "Failed to leave room",
description: (err && err.message ? err.message : "Server may be unavailable, overloaded, or you hit a bug."),
});
});
}
}
});
this._leaveRoom(payload.room_id);
break;
case 'reject_invite':
Modal.createDialog(QuestionDialog, {
title: "Reject invitation",
description: "Are you sure you want to reject the invitation?",
title: _t('Reject invitation'),
description: _t('Are you sure you want to reject the invitation?'),
onFinished: (confirm) => {
if (confirm) {
// FIXME: controller shouldn't be loading a view :(
@ -438,12 +392,12 @@ module.exports = React.createClass({
}, (err) => {
modal.close();
Modal.createDialog(ErrorDialog, {
title: "Failed to reject invitation",
description: err.toString()
title: _t('Failed to reject invitation'),
description: err.toString(),
});
});
}
}
},
});
break;
case 'view_user':
@ -468,30 +422,13 @@ module.exports = React.createClass({
this._viewRoom(payload);
break;
case 'view_prev_room':
roomIndexDelta = -1;
this._viewNextRoom(-1);
break;
case 'view_next_room':
var allRooms = RoomListSorter.mostRecentActivityFirst(
MatrixClientPeg.get().getRooms()
);
var roomIndex = -1;
for (var i = 0; i < allRooms.length; ++i) {
if (allRooms[i].roomId == this.state.currentRoomId) {
roomIndex = i;
break;
}
}
roomIndex = (roomIndex + roomIndexDelta) % allRooms.length;
if (roomIndex < 0) roomIndex = allRooms.length - 1;
this._viewRoom({ room_id: allRooms[roomIndex].roomId });
this._viewNextRoom(1);
break;
case 'view_indexed_room':
var allRooms = RoomListSorter.mostRecentActivityFirst(
MatrixClientPeg.get().getRooms()
);
var roomIndex = payload.roomIndex;
if (allRooms[roomIndex]) {
this._viewRoom({ room_id: allRooms[roomIndex].roomId });
}
this._viewIndexedRoom(payload.roomIndex);
break;
case 'view_user_settings':
this._setPage(PageTypes.UserSettings);
@ -500,19 +437,17 @@ module.exports = React.createClass({
case 'view_create_room':
//this._setPage(PageTypes.CreateRoom);
//this.notifyNewScreen('new');
var TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
Modal.createDialog(TextInputDialog, {
title: "Create Room",
description: "Room name (optional)",
button: "Create Room",
title: _t('Create Room'),
description: _t('Room name (optional)'),
button: _t('Create Room'),
onFinished: (should_create, name) => {
if (should_create) {
const createOpts = {};
if (name) createOpts.name = name;
createRoom({createOpts}).done();
}
}
},
});
break;
case 'view_room_directory':
@ -556,12 +491,14 @@ module.exports = React.createClass({
collapse_rhs: false,
});
break;
case 'ui_opacity':
case 'ui_opacity': {
const sideDefault = payload.sideOpacity >= 0.0 ? payload.sideOpacity : 1.0;
this.setState({
sideOpacity: payload.sideOpacity,
middleOpacity: payload.middleOpacity,
leftOpacity: payload.leftOpacity >= 0.0 ? payload.leftOpacity : sideDefault,
middleOpacity: payload.middleOpacity || 1.0,
rightOpacity: payload.rightOpacity >= 0.0 ? payload.rightOpacity : sideDefault,
});
break;
break; }
case 'set_theme':
this._onSetTheme(payload.value);
break;
@ -583,7 +520,7 @@ module.exports = React.createClass({
case 'new_version':
this.onVersion(
payload.currentVersion, payload.newVersion,
payload.releaseNotes
payload.releaseNotes,
);
break;
}
@ -595,6 +532,47 @@ module.exports = React.createClass({
});
},
_startRegistration: function(params) {
this.setStateForNewScreen({
screen: 'register',
// these params may be undefined, but if they are,
// unset them from our state: we don't want to
// resume a previous registration session if the
// user just clicked 'register'
register_client_secret: params.client_secret,
register_session_id: params.session_id,
register_hs_url: params.hs_url,
register_is_url: params.is_url,
register_id_sid: params.sid,
});
this.notifyNewScreen('register');
},
_viewNextRoom: function(roomIndexDelta) {
const allRooms = RoomListSorter.mostRecentActivityFirst(
MatrixClientPeg.get().getRooms(),
);
let roomIndex = -1;
for (let i = 0; i < allRooms.length; ++i) {
if (allRooms[i].roomId == this.state.currentRoomId) {
roomIndex = i;
break;
}
}
roomIndex = (roomIndex + roomIndexDelta) % allRooms.length;
if (roomIndex < 0) roomIndex = allRooms.length - 1;
this._viewRoom({ room_id: allRooms[roomIndex].roomId });
},
_viewIndexedRoom: function(roomIndex) {
const allRooms = RoomListSorter.mostRecentActivityFirst(
MatrixClientPeg.get().getRooms(),
);
if (allRooms[roomIndex]) {
this._viewRoom({ room_id: allRooms[roomIndex].roomId });
}
},
// switch view to the given room
//
// @param {Object} room_info Object containing data about the room to be joined
@ -614,7 +592,7 @@ module.exports = React.createClass({
_viewRoom: function(room_info) {
this.focusComposer = true;
var newState = {
const newState = {
initialEventId: room_info.event_id,
highlightedEventId: room_info.event_id,
initialEventPixelOffset: undefined,
@ -634,7 +612,7 @@ module.exports = React.createClass({
//
// TODO: do this in RoomView rather than here
if (!room_info.event_id && this.refs.loggedInView) {
var scrollState = this.refs.loggedInView.getScrollStateForRoom(room_info.room_id);
const scrollState = this.refs.loggedInView.getScrollStateForRoom(room_info.room_id);
if (scrollState) {
newState.initialEventId = scrollState.focussedEvent;
newState.initialEventPixelOffset = scrollState.pixelOffset;
@ -676,22 +654,61 @@ module.exports = React.createClass({
},
_createChat: function() {
var ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
Modal.createDialog(ChatInviteDialog, {
title: "Start a new chat",
title: _t('Start a chat'),
description: _t("Who would you like to communicate with?"),
placeholder: _t("Email, name or matrix ID"),
button: _t("Start Chat")
});
},
_invite: function(roomId) {
var ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
Modal.createDialog(ChatInviteDialog, {
title: "Invite new room members",
button: "Send Invites",
description: "Who would you like to add to this room?",
title: _t('Invite new room members'),
description: _t('Who would you like to add to this room?'),
button: _t('Send Invites'),
placeholder: _t("Email, name or matrix ID"),
roomId: roomId,
});
},
_leaveRoom: function(roomId) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
Modal.createDialog(QuestionDialog, {
title: "Leave room",
description: <span>Are you sure you want to leave the room <i>{roomToLeave.name}</i>?</span>,
onFinished: (shouldLeave) => {
if (shouldLeave) {
const d = MatrixClientPeg.get().leave(roomId);
// FIXME: controller shouldn't be loading a view :(
const Loader = sdk.getComponent("elements.Spinner");
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
d.then(() => {
modal.close();
if (this.currentRoomId === roomId) {
dis.dispatch({action: 'view_next_room'});
}
}, (err) => {
modal.close();
console.error("Failed to leave room " + roomId + " " + err);
Modal.createDialog(ErrorDialog, {
title: "Failed to leave room",
description: (err && err.message ? err.message :
"Server may be unavailable, overloaded, or you hit a bug."),
});
});
}
},
});
},
/**
* Called when the sessionloader has finished
*/
@ -710,6 +727,8 @@ module.exports = React.createClass({
/**
* Called whenever someone changes the theme
*
* @param {string} theme new theme
*/
_onSetTheme: function(theme) {
if (!theme) {
@ -718,12 +737,12 @@ module.exports = React.createClass({
// look for the stylesheet elements.
// styleElements is a map from style name to HTMLLinkElement.
var styleElements = Object.create(null);
var i, a;
for (i = 0; (a = document.getElementsByTagName("link")[i]); i++) {
var href = a.getAttribute("href");
const styleElements = Object.create(null);
let a;
for (let i = 0; (a = document.getElementsByTagName("link")[i]); i++) {
const href = a.getAttribute("href");
// shouldn't we be using the 'title' tag rather than the href?
var match = href.match(/^bundles\/.*\/theme-(.*)\.css$/);
const match = href.match(/^bundles\/.*\/theme-(.*)\.css$/);
if (match) {
styleElements[match[1]] = a;
}
@ -746,14 +765,15 @@ module.exports = React.createClass({
// abuse the tinter to change all the SVG's #fff to #2d2d2d
// XXX: obviously this shouldn't be hardcoded here.
Tinter.tintSvgWhite('#2d2d2d');
}
else {
} else {
Tinter.tintSvgWhite('#ffffff');
}
},
/**
* Called when a new logged in session has started
*
* @param {string} teamToken
*/
_onLoggedIn: function(teamToken) {
this.setState({
@ -767,8 +787,12 @@ module.exports = React.createClass({
this._teamToken = teamToken;
dis.dispatch({action: 'view_home_page'});
} else if (this._is_registered) {
if (this.props.config.welcomeUserId) {
createRoom({dmUserId: this.props.config.welcomeUserId});
return;
}
// The user has just logged in after registering
dis.dispatch({action: 'view_user_settings'});
dis.dispatch({action: 'view_room_directory'});
} else {
this._showScreenAfterLogin();
}
@ -780,7 +804,7 @@ module.exports = React.createClass({
if (this.state.screenAfterLogin && this.state.screenAfterLogin.screen) {
this.showScreen(
this.state.screenAfterLogin.screen,
this.state.screenAfterLogin.params
this.state.screenAfterLogin.params,
);
this.notifyNewScreen(this.state.screenAfterLogin.screen);
this.setState({screenAfterLogin: null});
@ -821,8 +845,8 @@ module.exports = React.createClass({
* (useful for setting listeners)
*/
_onWillStartClient() {
var self = this;
var cli = MatrixClientPeg.get();
const self = this;
const cli = MatrixClientPeg.get();
// Allow the JS SDK to reap timeline events. This reduces the amount of
// memory consumed as the JS SDK stores multiple distinct copies of room
@ -863,17 +887,17 @@ module.exports = React.createClass({
cli.on('Call.incoming', function(call) {
dis.dispatch({
action: 'incoming_call',
call: call
call: call,
});
});
cli.on('Session.logged_out', function(call) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Signed Out",
description: "For security, this session has been signed out. Please sign in again."
title: _t('Signed Out'),
description: _t('For security, this session has been signed out. Please sign in again.'),
});
dis.dispatch({
action: 'logout'
action: 'logout',
});
});
cli.on("accountData", function(ev) {
@ -888,25 +912,21 @@ module.exports = React.createClass({
});
},
onFocus: function(ev) {
dis.dispatch({action: 'focus_composer'});
},
showScreen: function(screen, params) {
if (screen == 'register') {
dis.dispatch({
action: 'start_registration',
params: params
params: params,
});
} else if (screen == 'login') {
dis.dispatch({
action: 'start_login',
params: params
params: params,
});
} else if (screen == 'forgot_password') {
dis.dispatch({
action: 'start_password_recovery',
params: params
params: params,
});
} else if (screen == 'new') {
dis.dispatch({
@ -929,26 +949,26 @@ module.exports = React.createClass({
action: 'start_post_registration',
});
} else if (screen.indexOf('room/') == 0) {
var segments = screen.substring(5).split('/');
var roomString = segments[0];
var eventId = segments[1]; // undefined if no event id given
const segments = screen.substring(5).split('/');
const roomString = segments[0];
const eventId = segments[1]; // undefined if no event id given
// FIXME: sort_out caseConsistency
var third_party_invite = {
const thirdPartyInvite = {
inviteSignUrl: params.signurl,
invitedEmail: params.email,
};
var oob_data = {
const oobData = {
name: params.room_name,
avatarUrl: params.room_avatar_url,
inviterName: params.inviter_name,
};
var payload = {
const payload = {
action: 'view_room',
event_id: eventId,
third_party_invite: third_party_invite,
oob_data: oob_data,
third_party_invite: thirdPartyInvite,
oob_data: oobData,
};
if (roomString[0] == '#') {
payload.room_alias = roomString;
@ -962,19 +982,18 @@ module.exports = React.createClass({
dis.dispatch(payload);
}
} else if (screen.indexOf('user/') == 0) {
var userId = screen.substring(5);
const userId = screen.substring(5);
this.setState({ viewUserId: userId });
this._setPage(PageTypes.UserView);
this.notifyNewScreen('user/' + userId);
var member = new Matrix.RoomMember(null, userId);
const member = new Matrix.RoomMember(null, userId);
if (member) {
dis.dispatch({
action: 'view_user',
member: member,
});
}
}
else {
} else {
console.info("Ignoring showScreen for '%s'", screen);
}
},
@ -993,7 +1012,7 @@ module.exports = React.createClass({
onUserClick: function(event, userId) {
event.preventDefault();
var member = new Matrix.RoomMember(null, userId);
const member = new Matrix.RoomMember(null, userId);
if (!member) { return; }
dis.dispatch({
action: 'view_user',
@ -1003,17 +1022,17 @@ module.exports = React.createClass({
onLogoutClick: function(event) {
dis.dispatch({
action: 'logout'
action: 'logout',
});
event.stopPropagation();
event.preventDefault();
},
handleResize: function(e) {
var hideLhsThreshold = 1000;
var showLhsThreshold = 1000;
var hideRhsThreshold = 820;
var showRhsThreshold = 820;
const hideLhsThreshold = 1000;
const showLhsThreshold = 1000;
const hideRhsThreshold = 820;
const showRhsThreshold = 820;
if (this.state.width > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) {
dis.dispatch({ action: 'hide_left_panel' });
@ -1031,10 +1050,10 @@ module.exports = React.createClass({
this.setState({width: window.innerWidth});
},
onRoomCreated: function(room_id) {
onRoomCreated: function(roomId) {
dis.dispatch({
action: "view_room",
room_id: room_id,
room_id: roomId,
});
},
@ -1068,7 +1087,7 @@ module.exports = React.createClass({
onFinishPostRegistration: function() {
// Don't confuse this with "PageType" which is the middle window to show
this.setState({
screen: undefined
screen: undefined,
});
this.showScreen("settings");
},
@ -1083,10 +1102,10 @@ module.exports = React.createClass({
},
updateStatusIndicator: function(state, prevState) {
var notifCount = 0;
let notifCount = 0;
var rooms = MatrixClientPeg.get().getRooms();
for (var i = 0; i < rooms.length; ++i) {
const rooms = MatrixClientPeg.get().getRooms();
for (let i = 0; i < rooms.length; ++i) {
if (rooms[i].hasMembershipState(MatrixClientPeg.get().credentials.userId, 'invite')) {
notifCount++;
} else if (rooms[i].getUnreadNotificationCount()) {
@ -1113,19 +1132,18 @@ module.exports = React.createClass({
action: 'view_room',
room_id: this.state.currentRoomId,
});
}
else {
} else {
dis.dispatch({
action: 'view_room_directory',
});
}
},
onRoomIdResolved: function(room_id) {
onRoomIdResolved: function(roomId) {
// It's the RoomView's resposibility to look up room aliases, but we need the
// ID to pass into things like the Member List, so the Room View tells us when
// its done that resolution so we can display things that take a room ID.
this.setState({currentRoomId: room_id});
this.setState({currentRoomId: roomId});
},
_makeRegistrationUrl: function(params) {
@ -1148,14 +1166,20 @@ module.exports = React.createClass({
</div>
);
}
// needs to be before normal PageTypes as you are logged in technically
else if (this.state.screen == 'post_registration') {
if (this.state.screen == 'post_registration') {
const PostRegistration = sdk.getComponent('structures.login.PostRegistration');
return (
<PostRegistration
onComplete={this.onFinishPostRegistration} />
);
} else if (this.state.loggedIn && this.state.ready) {
}
// `ready` and `loggedIn` may be set before `page_type` (because the
// latter is set via the dispatcher). If we don't yet have a `page_type`,
// keep showing the spinner for now.
if (this.state.loggedIn && this.state.ready && this.state.page_type) {
/* for now, we stuff the entirety of our props and state into the LoggedInView.
* we should go through and figure out what we actually need to pass down, as well
* as using something like redux to avoid having a billion bits of state kicking around.
@ -1178,7 +1202,7 @@ module.exports = React.createClass({
<div className="mx_MatrixChat_splash">
<Spinner />
<a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>
Logout
{ _t('Logout') }
</a>
</div>
);
@ -1237,5 +1261,5 @@ module.exports = React.createClass({
/>
);
}
}
},
});

View file

@ -84,6 +84,12 @@ module.exports = React.createClass({
// shape parameter to be passed to EventTiles
tileShape: React.PropTypes.string,
// show twelve hour timestamps
isTwelveHour: React.PropTypes.bool,
// show timestamps always
alwaysShowTimestamps: React.PropTypes.bool,
},
componentWillMount: function() {
@ -230,8 +236,8 @@ module.exports = React.createClass({
},
_getEventTiles: function() {
var EventTile = sdk.getComponent('rooms.EventTile');
var DateSeparator = sdk.getComponent('messages.DateSeparator');
const EventTile = sdk.getComponent('rooms.EventTile');
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
this.eventNodes = {};
@ -279,20 +285,19 @@ module.exports = React.createClass({
this.currentGhostEventId = null;
}
var isMembershipChange = (e) =>
e.getType() === 'm.room.member'
&& (!e.getPrevContent() || e.getContent().membership !== e.getPrevContent().membership);
var isMembershipChange = (e) => e.getType() === 'm.room.member';
for (i = 0; i < this.props.events.length; i++) {
var mxEv = this.props.events[i];
var wantTile = true;
var eventId = mxEv.getId();
let mxEv = this.props.events[i];
let wantTile = true;
let eventId = mxEv.getId();
let readMarkerInMels = false;
if (!EventTile.haveTileForEvent(mxEv)) {
wantTile = false;
}
var last = (i == lastShownEventIndex);
let last = (i == lastShownEventIndex);
// Wrap consecutive member events in a ListSummary, ignore if redacted
if (isMembershipChange(mxEv) &&
@ -334,6 +339,9 @@ module.exports = React.createClass({
let eventTiles = summarisedEvents.map(
(e) => {
if (e.getId() === this.props.readMarkerEventId) {
readMarkerInMels = true;
}
// In order to prevent DateSeparators from appearing in the expanded form
// of MemberEventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the
@ -352,12 +360,16 @@ module.exports = React.createClass({
<MemberEventListSummary
key={key}
events={summarisedEvents}
data-scroll-token={eventId}
onToggle={this._onWidgetLoad} // Update scroll state
>
{eventTiles}
</MemberEventListSummary>
);
if (readMarkerInMels) {
ret.push(this._getReadMarkerTile(visible));
}
continue;
}
@ -407,8 +419,8 @@ module.exports = React.createClass({
},
_getTilesForEvent: function(prevEvent, mxEv, last) {
var EventTile = sdk.getComponent('rooms.EventTile');
var DateSeparator = sdk.getComponent('messages.DateSeparator');
const EventTile = sdk.getComponent('rooms.EventTile');
const DateSeparator = sdk.getComponent('messages.DateSeparator');
var ret = [];
// is this a continuation of the previous message?
@ -462,11 +474,10 @@ module.exports = React.createClass({
if (this.props.manageReadReceipts) {
readReceipts = this._getReadReceiptsForEvent(mxEv);
}
ret.push(
<li key={eventId}
ref={this._collectEventNode.bind(this, eventId)}
data-scroll-token={scrollToken}>
data-scroll-tokens={scrollToken}>
<EventTile mxEvent={mxEv} continuation={continuation}
isRedacted={mxEv.isRedacted()}
onWidgetLoad={this._onWidgetLoad}
@ -476,6 +487,7 @@ module.exports = React.createClass({
checkUnmounting={this._isUnmounting}
eventSendStatus={mxEv.status}
tileShape={this.props.tileShape}
isTwelveHour={this.props.isTwelveHour}
last={last} isSelectedEvent={highlight}/>
</li>
);
@ -609,8 +621,13 @@ module.exports = React.createClass({
var style = this.props.hidden ? { display: 'none' } : {};
style.opacity = this.props.opacity;
var className = this.props.className + " mx_fadable";
if (this.props.alwaysShowTimestamps) {
className += " mx_MessagePanel_alwaysShowTimestamps";
}
return (
<ScrollPanel ref="scrollPanel" className={ this.props.className + " mx_fadable" }
<ScrollPanel ref="scrollPanel" className={ className }
onScroll={ this.props.onScroll }
onResize={ this.onResize }
onFillRequest={ this.props.onFillRequest }

View file

@ -16,7 +16,7 @@ limitations under the License.
var React = require('react');
var ReactDOM = require("react-dom");
import { _t } from '../../languageHandler';
var Matrix = require("matrix-js-sdk");
var sdk = require('../../index');
var MatrixClientPeg = require("../../MatrixClientPeg");
@ -37,7 +37,6 @@ var NotificationPanel = React.createClass({
var Loader = sdk.getComponent("elements.Spinner");
var timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
if (timelineSet) {
return (
<TimelinePanel key={"NotificationPanel_" + this.props.roomId}
@ -48,7 +47,7 @@ var NotificationPanel = React.createClass({
showUrlPreview = { false }
opacity={ this.props.opacity }
tileShape="notif"
empty="You have no visible notifications"
empty={ _t('You have no visible notifications') }
/>
);
}

View file

@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require('react');
var sdk = require('../../index');
var dis = require("../../dispatcher");
var WhoIsTyping = require("../../WhoIsTyping");
var MatrixClientPeg = require("../../MatrixClientPeg");
const MemberAvatar = require("../views/avatars/MemberAvatar");
import React from 'react';
import { _t } from '../../languageHandler';
import sdk from '../../index';
import dis from '../../dispatcher';
import WhoIsTyping from '../../WhoIsTyping';
import MatrixClientPeg from '../../MatrixClientPeg';
import MemberAvatar from '../views/avatars/MemberAvatar';
const HIDE_DEBOUNCE_MS = 10000;
const STATUS_BAR_HIDDEN = 0;
@ -175,8 +176,8 @@ module.exports = React.createClass({
<div className="mx_RoomStatusBar_scrollDownIndicator"
onClick={ this.props.onScrollToBottomClick }>
<img src="img/scrolldown.svg" width="24" height="24"
alt="Scroll to bottom of page"
title="Scroll to bottom of page"/>
alt={ _t("Scroll to bottom of page") }
title={ _t("Scroll to bottom of page") }/>
</div>
);
}
@ -250,10 +251,10 @@ module.exports = React.createClass({
<div className="mx_RoomStatusBar_connectionLostBar">
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ "/>
<div className="mx_RoomStatusBar_connectionLostBar_title">
Connectivity to the server has been lost.
{_t('Connectivity to the server has been lost.')}
</div>
<div className="mx_RoomStatusBar_connectionLostBar_desc">
Sent messages will be stored until your connection has returned.
{_t('Sent messages will be stored until your connection has returned.')}
</div>
</div>
);
@ -266,7 +267,7 @@ module.exports = React.createClass({
<TabCompleteBar tabComplete={this.props.tabComplete} />
<div className="mx_RoomStatusBar_tabCompleteEol" title="->|">
<TintableSvg src="img/eol.svg" width="22" height="16"/>
Auto-complete
{_t('Auto-complete')}
</div>
</div>
</div>
@ -283,13 +284,12 @@ module.exports = React.createClass({
<div className="mx_RoomStatusBar_connectionLostBar_desc">
<a className="mx_RoomStatusBar_resend_link"
onClick={ this.props.onResendAllClick }>
Resend all
</a> or <a
{_t('Resend all')}
</a> {_t('or')} <a
className="mx_RoomStatusBar_resend_link"
onClick={ this.props.onCancelAllClick }>
cancel all
</a> now. You can also select individual messages to
resend or cancel.
{_t('cancel all')}
</a> {_t('now. You can also select individual messages to resend or cancel.')}
</div>
</div>
);
@ -324,7 +324,7 @@ module.exports = React.createClass({
if (this.props.hasActiveCall) {
return (
<div className="mx_RoomStatusBar_callBar">
<b>Active call</b>
<b>{_t('Active call')}</b>
</div>
);
}

View file

@ -25,6 +25,7 @@ var ReactDOM = require("react-dom");
var q = require("q");
var classNames = require("classnames");
var Matrix = require("matrix-js-sdk");
import { _t } from '../../languageHandler';
var UserSettingsStore = require('../../UserSettingsStore');
var MatrixClientPeg = require("../../MatrixClientPeg");
@ -124,6 +125,8 @@ module.exports = React.createClass({
room: null,
roomId: null,
roomLoading: true,
forwardingEvent: null,
editingRoomSettings: false,
uploadingRoomSettings: false,
numUnreadMessages: 0,
@ -271,6 +274,7 @@ module.exports = React.createClass({
this._updateConfCallNotification();
window.addEventListener('beforeunload', this.onPageUnload);
window.addEventListener('resize', this.onResize);
this.onResize();
@ -295,7 +299,7 @@ module.exports = React.createClass({
componentWillReceiveProps: function(newProps) {
if (newProps.roomAddress != this.props.roomAddress) {
throw new Error("changing room on a RoomView is not supported");
throw new Error(_t("changing room on a RoomView is not supported"));
}
if (newProps.eventId != this.props.eventId) {
@ -353,6 +357,7 @@ module.exports = React.createClass({
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
}
window.removeEventListener('beforeunload', this.onPageUnload);
window.removeEventListener('resize', this.onResize);
document.removeEventListener("keydown", this.onKeyDown);
@ -365,6 +370,17 @@ module.exports = React.createClass({
// Tinter.tint(); // reset colourscheme
},
onPageUnload(event) {
if (ContentMessages.getCurrentUploads().length > 0) {
return event.returnValue =
_t("You seem to be uploading files, are you sure you want to quit?");
} else if (this._getCallForRoom() && this.state.callState !== 'ended') {
return event.returnValue =
_t("You seem to be in a call, are you sure you want to quit?");
}
},
onKeyDown: function(ev) {
let handled = false;
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
@ -438,6 +454,11 @@ module.exports = React.createClass({
callState: callState
});
break;
case 'forward_event':
this.setState({
forwardingEvent: payload.content,
});
break;
}
},
@ -517,14 +538,14 @@ module.exports = React.createClass({
if (!userHasUsedEncryption) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: "Warning!",
title: _t("Warning!"),
hasCancelButton: false,
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>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("Encrypted messages will not be visible on clients that do not yet implement encryption") }.</p>
</div>
),
});
@ -695,10 +716,10 @@ module.exports = React.createClass({
if (!unsentMessages.length) return "";
for (const event of unsentMessages) {
if (!event.error || event.error.name !== "UnknownDeviceError") {
return "Some of your messages have not been sent.";
return _t("Some of your messages have not been sent") + ".";
}
}
return "Message not sent due to unknown devices being present";
return _t("Message not sent due to unknown devices being present");
},
_getUnsentMessages: function(room) {
@ -858,15 +879,15 @@ module.exports = React.createClass({
) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Failed to join the room",
description: "This room is private or inaccessible to guests. You may be able to join if you register."
title: _t("Failed to join the room"),
description: _t("This room is private or inaccessible to guests. You may be able to join if you register") + "."
});
} else {
var msg = error.message ? error.message : JSON.stringify(error);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Failed to join room",
description: msg
title: _t("Failed to join room"),
description: msg,
});
}
}).done();
@ -926,8 +947,8 @@ module.exports = React.createClass({
if (MatrixClientPeg.get().isGuest()) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "Guest users can't upload files. Please register to upload."
title: _t("Please Register"),
description: _t("Guest users can't upload files. Please register to upload") + "."
});
return;
}
@ -946,8 +967,8 @@ module.exports = React.createClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to upload file " + file + " " + error);
Modal.createDialog(ErrorDialog, {
title: "Failed to upload file",
description: ((error && error.message) ? error.message : "Server may be unavailable, overloaded, or the file too big"),
title: _t('Failed to upload file'),
description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or the file too big")),
});
});
},
@ -1033,8 +1054,8 @@ module.exports = React.createClass({
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Search failed: " + error);
Modal.createDialog(ErrorDialog, {
title: "Search failed",
description: ((error && error.message) ? error.message : "Server may be unavailable, overloaded, or search timed out :("),
title: _t("Search failed"),
description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or search timed out :(")),
});
}).finally(function() {
self.setState({
@ -1069,12 +1090,12 @@ module.exports = React.createClass({
if (!this.state.searchResults.next_batch) {
if (this.state.searchResults.results.length == 0) {
ret.push(<li key="search-top-marker">
<h2 className="mx_RoomView_topMarker">No results</h2>
<h2 className="mx_RoomView_topMarker">{ _t("No results") }</h2>
</li>
);
} else {
ret.push(<li key="search-top-marker">
<h2 className="mx_RoomView_topMarker">No more results</h2>
<h2 className="mx_RoomView_topMarker">{ _t("No more results") }</h2>
</li>
);
}
@ -1111,10 +1132,10 @@ module.exports = React.createClass({
// it. We should tell the js sdk to go and find out about
// it. But that's not an issue currently, as synapse only
// returns results for rooms we're joined to.
var roomName = room ? room.name : "Unknown room "+roomId;
var roomName = room ? room.name : _t("Unknown room %(roomId)s", { roomId: roomId });
ret.push(<li key={mxEv.getId() + "-room"}>
<h1>Room: { roomName }</h1>
<h1>{ _t("Room") }: { roomName }</h1>
</li>);
lastRoomId = roomId;
}
@ -1160,7 +1181,7 @@ module.exports = React.createClass({
});
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Failed to save settings",
title: _t("Failed to save settings"),
description: fails.map(function(result) { return result.reason; }).join("\n"),
});
// still editing room settings
@ -1181,7 +1202,10 @@ module.exports = React.createClass({
onCancelClick: function() {
console.log("updateTint from onCancelClick");
this.updateTint();
this.setState({editingRoomSettings: false});
this.setState({
editingRoomSettings: false,
forwardingEvent: null,
});
dis.dispatch({action: 'focus_composer'});
},
@ -1196,11 +1220,11 @@ module.exports = React.createClass({
MatrixClientPeg.get().forget(this.state.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 }),
});
});
},
@ -1221,8 +1245,8 @@ module.exports = React.createClass({
var msg = error.message ? error.message : JSON.stringify(error);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Failed to reject invite",
description: msg
title: _t("Failed to reject invite"),
description: msg,
});
self.setState({
@ -1276,12 +1300,7 @@ module.exports = React.createClass({
return;
}
var pos = this.refs.messagePanel.getReadMarkerPosition();
// we want to show the bar if the read-marker is off the top of the
// screen.
var showBar = (pos < 0);
const showBar = this.refs.messagePanel.canJumpToReadMarker();
if (this.state.showTopUnreadMessagesBar != showBar) {
this.setState({showTopUnreadMessagesBar: showBar},
this.onChildResize);
@ -1464,16 +1483,17 @@ module.exports = React.createClass({
},
render: function() {
var RoomHeader = sdk.getComponent('rooms.RoomHeader');
var MessageComposer = sdk.getComponent('rooms.MessageComposer');
var RoomSettings = sdk.getComponent("rooms.RoomSettings");
var AuxPanel = sdk.getComponent("rooms.AuxPanel");
var SearchBar = sdk.getComponent("rooms.SearchBar");
var ScrollPanel = sdk.getComponent("structures.ScrollPanel");
var TintableSvg = sdk.getComponent("elements.TintableSvg");
var RoomPreviewBar = sdk.getComponent("rooms.RoomPreviewBar");
var Loader = sdk.getComponent("elements.Spinner");
var TimelinePanel = sdk.getComponent("structures.TimelinePanel");
const RoomHeader = sdk.getComponent('rooms.RoomHeader');
const MessageComposer = sdk.getComponent('rooms.MessageComposer');
const ForwardMessage = sdk.getComponent("rooms.ForwardMessage");
const RoomSettings = sdk.getComponent("rooms.RoomSettings");
const AuxPanel = sdk.getComponent("rooms.AuxPanel");
const SearchBar = sdk.getComponent("rooms.SearchBar");
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const RoomPreviewBar = sdk.getComponent("rooms.RoomPreviewBar");
const Loader = sdk.getComponent("elements.Spinner");
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
if (!this.state.room) {
if (this.state.roomLoading) {
@ -1601,17 +1621,16 @@ module.exports = React.createClass({
/>;
}
var aux = null;
if (this.state.editingRoomSettings) {
let aux = null;
if (this.state.forwardingEvent !== null) {
aux = <ForwardMessage onCancelClick={this.onCancelClick} currentRoomId={this.state.room.roomId} mxEvent={this.state.forwardingEvent} />;
} else if (this.state.editingRoomSettings) {
aux = <RoomSettings ref="room_settings" onSaveClick={this.onSettingsSaveClick} onCancelClick={this.onCancelClick} room={this.state.room} />;
}
else if (this.state.uploadingRoomSettings) {
} else if (this.state.uploadingRoomSettings) {
aux = <Loader/>;
}
else if (this.state.searching) {
} else if (this.state.searching) {
aux = <SearchBar ref="search_bar" searchInProgress={this.state.searchInProgress } onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch}/>;
}
else if (!myMember || myMember.membership !== "join") {
} else if (!myMember || myMember.membership !== "join") {
// We do have a room object for this room, but we're not currently in it.
// We may have a 3rd party invite to it.
var inviterName = undefined;
@ -1673,7 +1692,7 @@ module.exports = React.createClass({
if (call.type === "video") {
zoomButton = (
<div className="mx_RoomView_voipButton" onClick={this.onFullscreenClick} title="Fill screen">
<div className="mx_RoomView_voipButton" onClick={this.onFullscreenClick} title={ _t("Fill screen") }>
<TintableSvg src="img/fullscreen.svg" width="29" height="22" style={{ marginTop: 1, marginRight: 4 }}/>
</div>
);
@ -1681,14 +1700,14 @@ module.exports = React.createClass({
videoMuteButton =
<div className="mx_RoomView_voipButton" onClick={this.onMuteVideoClick}>
<TintableSvg src={call.isLocalVideoMuted() ? "img/video-unmute.svg" : "img/video-mute.svg"}
alt={call.isLocalVideoMuted() ? "Click to unmute video" : "Click to mute video"}
alt={call.isLocalVideoMuted() ? _t("Click to unmute video") : _t("Click to mute video")}
width="31" height="27"/>
</div>;
}
voiceMuteButton =
<div className="mx_RoomView_voipButton" onClick={this.onMuteAudioClick}>
<TintableSvg src={call.isMicrophoneMuted() ? "img/voice-unmute.svg" : "img/voice-mute.svg"}
alt={call.isMicrophoneMuted() ? "Click to unmute audio" : "Click to mute audio"}
alt={call.isMicrophoneMuted() ? _t("Click to unmute audio") : _t("Click to mute audio")}
width="21" height="26"/>
</div>;
@ -1724,14 +1743,13 @@ module.exports = React.createClass({
}
// console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
var messagePanel = (
<TimelinePanel ref={this._gatherTimelinePanelRef}
timelineSet={this.state.room.getUnfilteredTimelineSet()}
manageReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)}
manageReadMarkers={true}
hidden={hideMessagePanel}
highlightedEventId={this.props.highlightedEventId}
highlightedEventId={this.state.forwardingEvent ? this.state.forwardingEvent.getId() : this.props.highlightedEventId}
eventId={this.props.eventId}
eventPixelOffset={this.props.eventPixelOffset}
onScroll={ this.onMessageListScroll }
@ -1764,11 +1782,12 @@ module.exports = React.createClass({
oobData={this.props.oobData}
editing={this.state.editingRoomSettings}
saving={this.state.uploadingRoomSettings}
inRoom={myMember && myMember.membership === 'join'}
collapsedRhs={ this.props.collapsedRhs }
onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick}
onSaveClick={this.onSettingsSaveClick}
onCancelClick={this.onCancelClick}
onCancelClick={aux ? this.onCancelClick : null}
onForgetClick={
(myMember && myMember.membership === "leave") ? this.onForgetClick : null
}

View file

@ -46,9 +46,13 @@ if (DEBUG_SCROLL) {
* It also provides a hook which allows parents to provide more list elements
* when we get close to the start or end of the list.
*
* Each child element should have a 'data-scroll-token'. This token is used to
* serialise the scroll state, and returned as the 'trackedScrollToken'
* attribute by getScrollState().
* Each child element should have a 'data-scroll-tokens'. This string of
* comma-separated tokens may contain a single token or many, where many indicates
* that the element contains elements that have scroll tokens themselves. The first
* token in 'data-scroll-tokens' is used to serialise the scroll state, and returned
* as the 'trackedScrollToken' attribute by getScrollState().
*
* IMPORTANT: INDIVIDUAL TOKENS WITHIN 'data-scroll-tokens' MUST NOT CONTAIN COMMAS.
*
* Some notes about the implementation:
*
@ -349,8 +353,8 @@ module.exports = React.createClass({
// Subtract height of tile as if it were unpaginated
excessHeight -= tile.clientHeight;
// The tile may not have a scroll token, so guard it
if (tile.dataset.scrollToken) {
markerScrollToken = tile.dataset.scrollToken;
if (tile.dataset.scrollTokens) {
markerScrollToken = tile.dataset.scrollTokens.split(',')[0];
}
if (tile.clientHeight > excessHeight) {
break;
@ -419,7 +423,8 @@ module.exports = React.createClass({
* scroll. false if we are tracking a particular child.
*
* string trackedScrollToken: undefined if stuckAtBottom is true; if it is
* false, the data-scroll-token of the child which we are tracking.
* false, the first token in data-scroll-tokens of the child which we are
* tracking.
*
* number pixelOffset: undefined if stuckAtBottom is true; if it is false,
* the number of pixels the bottom of the tracked child is above the
@ -551,8 +556,10 @@ module.exports = React.createClass({
var messages = this.refs.itemlist.children;
for (var i = messages.length-1; i >= 0; --i) {
var m = messages[i];
if (!m.dataset.scrollToken) continue;
if (m.dataset.scrollToken == scrollToken) {
// 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
// There might only be one scroll token
if (m.dataset.scrollTokens &&
m.dataset.scrollTokens.split(',').indexOf(scrollToken) !== -1) {
node = m;
break;
}
@ -568,7 +575,7 @@ module.exports = React.createClass({
var boundingRect = node.getBoundingClientRect();
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
debuglog("ScrollPanel: scrolling to token '" + node.dataset.scrollToken + "'+" +
debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" +
pixelOffset + " (delta: "+scrollDelta+")");
if(scrollDelta != 0) {
@ -591,12 +598,12 @@ module.exports = React.createClass({
for (var i = messages.length-1; i >= 0; --i) {
var node = messages[i];
if (!node.dataset.scrollToken) continue;
if (!node.dataset.scrollTokens) continue;
var boundingRect = node.getBoundingClientRect();
newScrollState = {
stuckAtBottom: false,
trackedScrollToken: node.dataset.scrollToken,
trackedScrollToken: node.dataset.scrollTokens.split(',')[0],
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
};
// If the bottom of the panel intersects the ClientRect of node, use this node
@ -608,7 +615,7 @@ module.exports = React.createClass({
break;
}
}
// This is only false if there were no nodes with `node.dataset.scrollToken` set.
// This is only false if there were no nodes with `node.dataset.scrollTokens` set.
if (newScrollState) {
this.scrollState = newScrollState;
debuglog("ScrollPanel: saved scroll state", this.scrollState);

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.
@ -22,12 +23,14 @@ var Matrix = require("matrix-js-sdk");
var EventTimeline = Matrix.EventTimeline;
var sdk = require('../../index');
import { _t } from '../../languageHandler';
var MatrixClientPeg = require("../../MatrixClientPeg");
var dis = require("../../dispatcher");
var ObjectUtils = require('../../ObjectUtils');
var Modal = require("../../Modal");
var UserActivity = require("../../UserActivity");
var KeyCode = require('../../KeyCode');
import UserSettingsStore from '../../UserSettingsStore';
var PAGINATE_SIZE = 20;
var INITIAL_SIZE = 20;
@ -121,7 +124,7 @@ var TimelinePanel = React.createClass({
let initialReadMarker = null;
if (this.props.manageReadMarkers) {
const readmarker = this.props.timelineSet.room.getAccountData('m.fully_read');
if (readmarker){
if (readmarker) {
initialReadMarker = readmarker.getContent().event_id;
} else {
initialReadMarker = this._getCurrentReadReceipt();
@ -167,14 +170,23 @@ var TimelinePanel = React.createClass({
backPaginating: false,
forwardPaginating: false,
// cache of matrixClient.getSyncState() (but from the 'sync' event)
clientSyncState: MatrixClientPeg.get().getSyncState(),
// should the event tiles have twelve hour times
isTwelveHour: UserSettingsStore.getSyncedSetting('showTwelveHourTimestamps'),
// always show timestamps on event tiles?
alwaysShowTimestamps: UserSettingsStore.getSyncedSetting('alwaysShowTimestamps'),
};
},
componentWillMount: function() {
debuglog("TimelinePanel: mounting");
this.last_rr_sent_event_id = undefined;
this.last_rm_sent_event_id = undefined;
this.lastRRSentEventId = undefined;
this.lastRMSentEventId = undefined;
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
@ -183,6 +195,7 @@ var TimelinePanel = React.createClass({
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
MatrixClientPeg.get().on("Room.accountData", this.onAccountData);
MatrixClientPeg.get().on("sync", this.onSync);
this._initTimeline(this.props);
},
@ -251,6 +264,7 @@ var TimelinePanel = React.createClass({
client.removeListener("Room.receipt", this.onRoomReceipt);
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
client.removeListener("Room.accountData", this.onAccountData);
client.removeListener("sync", this.onSync);
}
},
@ -487,17 +501,24 @@ var TimelinePanel = React.createClass({
}, this.props.onReadMarkerUpdated);
},
onSync: function(state, prevState, data) {
this.setState({clientSyncState: state});
},
sendReadReceipt: function() {
if (!this.refs.messagePanel) return;
if (!this.props.manageReadReceipts) return;
// This happens on user_activity_end which is delayed, and it's
// very possible have logged out within that timeframe, so check
// we still have a client.
if (!MatrixClientPeg.get()) return;
const cli = MatrixClientPeg.get();
// if no client or client is guest don't send RR or RM
if (!cli || cli.isGuest()) return;
var currentReadUpToEventId = this._getCurrentReadReceipt(true);
var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId);
let shouldSendRR = true;
const currentRREventId = this._getCurrentReadReceipt(true);
const currentRREventIndex = this._indexForEventId(currentRREventId);
// We want to avoid sending out read receipts when we are looking at
// events in the past which are before the latest RR.
//
@ -511,43 +532,60 @@ var TimelinePanel = React.createClass({
// RRs) - but that is a bit of a niche case. It will sort itself out when
// the user eventually hits the live timeline.
//
if (currentReadUpToEventId && currentReadUpToEventIndex === null &&
if (currentRREventId && currentRREventIndex === null &&
this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
return;
shouldSendRR = false;
}
var lastReadEventIndex = this._getLastDisplayedEventIndex({
ignoreOwn: true
const lastReadEventIndex = this._getLastDisplayedEventIndex({
ignoreOwn: true,
});
if (lastReadEventIndex === null) return;
if (lastReadEventIndex === null) {
shouldSendRR = false;
}
let lastReadEvent = this.state.events[lastReadEventIndex];
shouldSendRR = shouldSendRR &&
// Only send a RR if the last read event is ahead in the timeline relative to
// the current RR event.
lastReadEventIndex > currentRREventIndex &&
// Only send a RR if the last RR set != the one we would send
this.lastRRSentEventId != lastReadEvent.getId();
var lastReadEvent = this.state.events[lastReadEventIndex];
// Only send a RM if the last RM sent != the one we would send
const shouldSendRM =
this.lastRMSentEventId != this.state.readMarkerEventId;
// we also remember the last read receipt we sent to avoid spamming the
// same one at the server repeatedly
if ((lastReadEventIndex > currentReadUpToEventIndex &&
this.last_rr_sent_event_id != lastReadEvent.getId()) ||
this.last_rm_sent_event_id != this.state.readMarkerEventId) {
this.last_rr_sent_event_id = lastReadEvent.getId();
this.last_rm_sent_event_id = this.state.readMarkerEventId;
if (shouldSendRR || shouldSendRM) {
if (shouldSendRR) {
this.lastRRSentEventId = lastReadEvent.getId();
} else {
lastReadEvent = null;
}
this.lastRMSentEventId = this.state.readMarkerEventId;
debuglog('TimelinePanel: Sending Read Markers for ',
this.props.timelineSet.room.roomId,
'rm', this.state.readMarkerEventId,
lastReadEvent ? 'rr ' + lastReadEvent.getId() : '',
);
MatrixClientPeg.get().setRoomReadMarkers(
this.props.timelineSet.room.roomId,
this.state.readMarkerEventId,
lastReadEvent
lastReadEvent, // Could be null, in which case no RR is sent
).catch((e) => {
// /read_markers API is not implemented on this HS, fallback to just RR
if (e.errcode === 'M_UNRECOGNIZED') {
if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) {
return MatrixClientPeg.get().sendReadReceipt(
lastReadEvent
lastReadEvent,
).catch(() => {
this.last_rr_sent_event_id = undefined;
this.lastRRSentEventId = undefined;
});
}
// it failed, so allow retries next time the user is active
this.last_rr_sent_event_id = undefined;
this.last_rm_sent_event_id = undefined;
this.lastRRSentEventId = undefined;
this.lastRMSentEventId = undefined;
});
// do a quick-reset of our unreadNotificationCount to avoid having
@ -560,7 +598,6 @@ var TimelinePanel = React.createClass({
this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0);
dis.dispatch({
action: 'on_room_read',
room: this.props.timelineSet.room,
});
}
}
@ -756,6 +793,19 @@ var TimelinePanel = React.createClass({
return null;
},
canJumpToReadMarker: function() {
// 1. Do not show jump bar if neither the RM nor the RR are set.
// 2. Only show jump bar if RR !== RM. If they are the same, there are only fully
// read messages and unread messages. We already have a badge count and the bottom
// bar to jump to "live" when we have unread messages.
// 3. We want to show the bar if the read-marker is off the top of the screen.
// 4. Also, if pos === null, the event might not be paginated - show the unread bar
const pos = this.getReadMarkerPosition();
return this.state.readMarkerEventId !== null && // 1.
this.state.readMarkerEventId !== this._getCurrentReadReceipt() && // 2.
(pos < 0 || pos === null); // 3., 4.
},
/**
* called by the parent component when PageUp/Down/etc is pressed.
*
@ -865,14 +915,11 @@ var TimelinePanel = React.createClass({
});
};
}
var message = "Tried to load a specific point in this room's timeline, but ";
if (error.errcode == 'M_FORBIDDEN') {
message += "you do not have permission to view the message in question.";
} else {
message += "was unable to find it.";
}
var message = (error.errcode == 'M_FORBIDDEN')
? _t("Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question") + "."
: _t("Tried to load a specific point in this room's timeline, but was unable to find it") + ".";
Modal.createDialog(ErrorDialog, {
title: "Failed to load timeline position",
title: _t("Failed to load timeline position"),
description: message,
onFinished: onFinished,
});
@ -1058,11 +1105,17 @@ var TimelinePanel = React.createClass({
// events when viewing historical messages, we get stuck in a loop
// of paginating our way through the entire history of the room.
var stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
// If the state is PREPARED, we're still waiting for the js-sdk to sync with
// the HS and fetch the latest events, so we are effectively forward paginating.
const forwardPaginating = (
this.state.forwardPaginating || this.state.clientSyncState == 'PREPARED'
);
return (
<MessagePanel ref="messagePanel"
hidden={ this.props.hidden }
backPaginating={ this.state.backPaginating }
forwardPaginating={ this.state.forwardPaginating }
forwardPaginating={ forwardPaginating }
events={ this.state.events }
highlightedEventId={ this.props.highlightedEventId }
readMarkerEventId={ this.state.readMarkerEventId }
@ -1076,6 +1129,8 @@ var TimelinePanel = React.createClass({
onFillRequest={ this.onMessageListFillRequest }
onUnfillRequest={ this.onMessageListUnfillRequest }
opacity={ this.props.opacity }
isTwelveHour={ this.state.isTwelveHour }
alwaysShowTimestamps={ this.state.alwaysShowTimestamps }
className={ this.props.className }
tileShape={ this.props.tileShape }
/>

View file

@ -14,35 +14,49 @@ 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 ReactDOM = require('react-dom');
var sdk = require('../../index');
var MatrixClientPeg = require("../../MatrixClientPeg");
var PlatformPeg = require("../../PlatformPeg");
var Modal = require('../../Modal');
var dis = require("../../dispatcher");
var q = require('q');
var package_json = require('../../../package.json');
var UserSettingsStore = require('../../UserSettingsStore');
var GeminiScrollbar = require('react-gemini-scrollbar');
var Email = require('../../email');
var AddThreepid = require('../../AddThreepid');
var SdkConfig = require('../../SdkConfig');
const React = require('react');
const ReactDOM = require('react-dom');
const sdk = require('../../index');
const MatrixClientPeg = require("../../MatrixClientPeg");
const PlatformPeg = require("../../PlatformPeg");
const Modal = require('../../Modal');
const dis = require("../../dispatcher");
const q = require('q');
const packageJson = require('../../../package.json');
const UserSettingsStore = require('../../UserSettingsStore');
const GeminiScrollbar = require('react-gemini-scrollbar');
const Email = require('../../email');
const AddThreepid = require('../../AddThreepid');
const SdkConfig = require('../../SdkConfig');
import AccessibleButton from '../views/elements/AccessibleButton';
import { _t } from '../../languageHandler';
import * as languageHandler from '../../languageHandler';
import * as FormattingUtils from '../../utils/FormattingUtils';
// if this looks like a release, use the 'version' from package.json; else use
// the git sha. Prepend version with v, to look like riot-web version
const REACT_SDK_VERSION = 'dist' in package_json ? `v${package_json.version}` : package_json.gitHead || '<local>';
const REACT_SDK_VERSION = 'dist' in packageJson ? packageJson.version : packageJson.gitHead || '<local>';
// Simple method to help prettify GH Release Tags and Commit Hashes.
const GHVersionUrl = function(repo, token) {
const uriTail = (token.startsWith('v') && token.includes('.')) ? `releases/tag/${token}` : `commit/${token}`;
return `https://github.com/${repo}/${uriTail}`;
}
const semVerRegex = /^v?(\d+\.\d+\.\d+(?:-rc.+)?)(?:-(?:\d+-g)?([0-9a-fA-F]+))?(?:-dirty)?$/i;
const gHVersionLabel = function(repo, token='') {
const match = token.match(semVerRegex);
let url;
if (match && match[1]) { // basic semVer string possibly with commit hash
url = (match.length > 1 && match[2])
? `https://github.com/${repo}/commit/${match[2]}`
: `https://github.com/${repo}/releases/tag/v${match[1]}`;
} else {
url = `https://github.com/${repo}/commit/${token.split('-')[0]}`;
}
return <a target="_blank" rel="noopener" href={url}>{token}</a>;
};
// Enumerate some simple 'flip a bit' UI settings (if any).
// 'id' gives the key name in the im.vector.web.settings account data event
// 'label' is how we describe it in the UI.
// Warning: Each "label" string below must be added to i18n/strings/en_EN.json,
// since they will be translated when rendered.
const SETTINGS_LABELS = [
{
id: 'autoplayGifsAndVideos',
@ -50,13 +64,12 @@ const SETTINGS_LABELS = [
},
{
id: 'hideReadReceipts',
label: 'Hide read receipts'
label: 'Hide read receipts',
},
{
id: 'dontSendTypingNotifications',
label: "Don't send typing notifications",
},
/*
{
id: 'alwaysShowTimestamps',
label: 'Always show message timestamps',
@ -65,6 +78,7 @@ const SETTINGS_LABELS = [
id: 'showTwelveHourTimestamps',
label: 'Show timestamps in 12 hour format (e.g. 2:30pm)',
},
/*
{
id: 'useCompactLayout',
label: 'Use compact timeline layout',
@ -76,6 +90,8 @@ const SETTINGS_LABELS = [
*/
];
// Warning: Each "label" string below must be added to i18n/strings/en_EN.json,
// since they will be translated when rendered.
const CRYPTO_SETTINGS_LABELS = [
{
id: 'blacklistUnverifiedDevices',
@ -106,10 +122,9 @@ const THEMES = [
id: 'theme',
label: 'Dark theme',
value: 'dark',
}
},
];
module.exports = React.createClass({
displayName: 'UserSettings',
@ -142,10 +157,10 @@ module.exports = React.createClass({
getInitialState: function() {
return {
avatarUrl: null,
threePids: [],
threepids: [],
phase: "UserSettings.LOADING", // LOADING, DISPLAY
email_add_pending: false,
vectorVersion: null,
vectorVersion: undefined,
rejectingInvites: false,
};
},
@ -180,13 +195,17 @@ module.exports = React.createClass({
});
this._refreshFromServer();
var syncedSettings = UserSettingsStore.getSyncedSettings();
const syncedSettings = UserSettingsStore.getSyncedSettings();
if (!syncedSettings.theme) {
syncedSettings.theme = 'light';
}
this._syncedSettings = syncedSettings;
this._localSettings = UserSettingsStore.getLocalSettings();
this.setState({
language: languageHandler.getCurrentLanguage(),
});
},
componentDidMount: function() {
@ -202,16 +221,16 @@ module.exports = React.createClass({
middleOpacity: 1.0,
});
dis.unregister(this.dispatcherRef);
let cli = MatrixClientPeg.get();
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomMember.membership", this._onInviteStateChange);
}
},
_refreshFromServer: function() {
var self = this;
const self = this;
q.all([
UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids()
UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids(),
]).done(function(resps) {
self.setState({
avatarUrl: resps[0].avatar_url,
@ -219,11 +238,11 @@ module.exports = React.createClass({
phase: "UserSettings.DISPLAY",
});
}, function(error) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to load user settings: " + error);
Modal.createDialog(ErrorDialog, {
title: "Can't load user settings",
description: ((error && error.message) ? error.message : "Server may be unavailable or overloaded"),
title: _t("Can't load user settings"),
description: ((error && error.message) ? error.message : _t("Server may be unavailable or overloaded")),
});
});
},
@ -236,10 +255,10 @@ module.exports = React.createClass({
onAvatarPickerClick: function(ev) {
if (MatrixClientPeg.get().isGuest()) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "Guests can't set avatars. Please register.",
title: _t("Please Register"),
description: _t("Guests can't set avatars. Please register."),
});
return;
}
@ -250,8 +269,8 @@ module.exports = React.createClass({
},
onAvatarSelected: function(ev) {
var self = this;
var changeAvatar = this.refs.changeAvatar;
const self = this;
const changeAvatar = this.refs.changeAvatar;
if (!changeAvatar) {
console.error("No ChangeAvatar found to upload image to!");
return;
@ -260,33 +279,33 @@ module.exports = React.createClass({
// dunno if the avatar changed, re-check it.
self._refreshFromServer();
}, function(err) {
var errMsg = (typeof err === "string") ? err : (err.error || "");
// const errMsg = (typeof err === "string") ? err : (err.error || "");
console.error("Failed to set avatar: " + err);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Failed to set avatar",
description: ((err && err.message) ? err.message : "Operation failed"),
title: _t("Failed to set avatar."),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
});
},
onLogoutClicked: function(ev) {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: "Sign out?",
title: _t("Sign out"),
description:
<div>
For security, logging out will delete any end-to-end encryption keys from this browser.
If you want to be able to decrypt your conversation history from future Riot sessions,
please export your room keys for safe-keeping.
{ _t("For security, logging out will delete any end-to-end " +
"encryption keys from this browser. If you want to be able " +
"to decrypt your conversation history from future Riot sessions, " +
"please export your room keys for safe-keeping.") }.
</div>,
button: "Sign out",
button: _t("Sign out"),
extraButtons: [
<button key="export" className="mx_Dialog_primary"
onClick={this._onExportE2eKeysClicked}>
Export E2E room keys
</button>
{ _t("Export E2E room keys") }
</button>,
],
onFinished: (confirmed) => {
if (confirmed) {
@ -300,34 +319,31 @@ module.exports = React.createClass({
},
onPasswordChangeError: function(err) {
var errMsg = err.error || "";
let errMsg = err.error || "";
if (err.httpStatus === 403) {
errMsg = "Failed to change password. Is your password correct?";
}
else if (err.httpStatus) {
errMsg = _t("Failed to change password. Is your password correct?");
} else if (err.httpStatus) {
errMsg += ` (HTTP status ${err.httpStatus})`;
}
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to change password: " + errMsg);
Modal.createDialog(ErrorDialog, {
title: "Error",
description: errMsg
title: _t("Error"),
description: errMsg,
});
},
onPasswordChanged: function() {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Success",
description: `Your password was successfully changed. You will not
receive push notifications on other devices until you
log back in to them.`
title: _t("Success"),
description: _t("Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them") + ".",
});
},
onUpgradeClicked: function() {
dis.dispatch({
action: "start_upgrade_registration"
action: "start_upgrade_registration",
});
},
@ -341,33 +357,33 @@ module.exports = React.createClass({
},
_addEmail: function() {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
var email_address = this.refs.add_email_input.value;
if (!Email.looksValid(email_address)) {
const emailAddress = this.refs.add_email_input.value;
if (!Email.looksValid(emailAddress)) {
Modal.createDialog(ErrorDialog, {
title: "Invalid Email Address",
description: "This doesn't appear to be a valid email address",
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(email_address, true).done(() => {
this._addThreepid.addEmailAddress(emailAddress, true).done(() => {
Modal.createDialog(QuestionDialog, {
title: "Verification Pending",
description: "Please check your email and click on the link it contains. Once this is done, click continue.",
button: 'Continue',
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({email_add_pending: false});
console.error("Unable to add email address " + email_address + " " + err);
console.error("Unable to add email address " + emailAddress + " " + err);
Modal.createDialog(ErrorDialog, {
title: "Unable to add email address",
description: ((err && err.message) ? err.message : "Operation failed"),
title: _t("Unable to add email address"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
});
ReactDOM.findDOMNode(this.refs.add_email_input).blur();
@ -377,9 +393,9 @@ module.exports = React.createClass({
onRemoveThreepidClicked: function(threepid) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: "Remove Contact Information?",
description: "Remove " + threepid.address + "?",
button: 'Remove',
title: _t("Remove Contact Information?"),
description: _t("Remove %(threePid)s?", { threePid : threepid.address }),
button: _t('Remove'),
onFinished: (submit) => {
if (submit) {
this.setState({
@ -391,8 +407,8 @@ module.exports = React.createClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to remove contact information: " + err);
Modal.createDialog(ErrorDialog, {
title: "Unable to remove contact information",
description: ((err && err.message) ? err.message : "Operation failed"),
title: _t("Unable to remove contact information"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
}).done();
}
@ -419,21 +435,21 @@ module.exports = React.createClass({
}, (err) => {
this.setState({email_add_pending: false});
if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
var message = "Unable to verify email address. ";
message += "Please check your email and click on the link it contains. Once this is done, click continue.";
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
let 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: "Verification Pending",
title: _t("Verification Pending"),
description: message,
button: 'Continue',
button: _t('Continue'),
onFinished: this.onEmailDialogFinished,
});
} else {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to verify email address: " + err);
Modal.createDialog(ErrorDialog, {
title: "Unable to verify email address",
description: ((err && err.message) ? err.message : "Operation failed"),
title: _t("Unable to verify email address."),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
}
});
@ -469,17 +485,17 @@ module.exports = React.createClass({
_onRejectAllInvitesClicked: function(rooms, ev) {
this.setState({
rejectingInvites: true
rejectingInvites: true,
});
// reject the invites
let promises = rooms.map((room) => {
const promises = rooms.map((room) => {
return MatrixClientPeg.get().leave(room.roomId);
});
// purposefully drop errors to the floor: we'll just have a non-zero number on the UI
// after trying to reject all the invites.
q.allSettled(promises).then(() => {
this.setState({
rejectingInvites: false
rejectingInvites: false,
});
}).done();
},
@ -492,7 +508,7 @@ module.exports = React.createClass({
}, "e2e-export");
}, {
matrixClient: MatrixClientPeg.get(),
}
},
);
},
@ -504,7 +520,7 @@ module.exports = React.createClass({
}, "e2e-export");
}, {
matrixClient: MatrixClientPeg.get(),
}
},
);
},
@ -523,22 +539,43 @@ module.exports = React.createClass({
<div>
<h3>Referral</h3>
<div className="mx_UserSettings_section">
Refer a friend to Riot: <a href={href}>{href}</a>
{_t("Refer a friend to Riot:")} <a href={href}>{href}</a>
</div>
</div>
);
},
_renderUserInterfaceSettings: function() {
var client = MatrixClientPeg.get();
onLanguageChange: function(newLang) {
if(this.state.language !== newLang) {
UserSettingsStore.setLocalSetting('language', newLang);
this.setState({
language: newLang,
});
PlatformPeg.get().reload();
}
},
_renderLanguageSetting: function () {
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
return <div>
<label htmlFor="languageSelector">{_t('Interface Language')}</label>
<LanguageDropdown ref="language" onOptionChange={this.onLanguageChange}
className="mx_UserSettings_language"
value={this.state.language}
/>
</div>;
},
_renderUserInterfaceSettings: function() {
return (
<div>
<h3>User Interface</h3>
<h3>{ _t("User Interface") }</h3>
<div className="mx_UserSettings_section">
{ this._renderUrlPreviewSelector() }
{ SETTINGS_LABELS.map( this._renderSyncedSetting ) }
{ THEMES.map( this._renderThemeSelector ) }
{ this._renderLanguageSetting() }
</div>
</div>
);
@ -549,10 +586,10 @@ module.exports = React.createClass({
<input id="urlPreviewsDisabled"
type="checkbox"
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
onChange={ e => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) }
onChange={ (e) => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) }
/>
<label htmlFor="urlPreviewsDisabled">
Disable inline URL previews by default
{ _t("Disable inline URL previews by default") }
</label>
</div>;
},
@ -562,10 +599,10 @@ module.exports = React.createClass({
<input id={ setting.id }
type="checkbox"
defaultChecked={ this._syncedSettings[setting.id] }
onChange={ e => UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) }
onChange={ (e) => UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) }
/>
<label htmlFor={ setting.id }>
{ setting.label }
{ _t(setting.label) }
</label>
</div>;
},
@ -577,7 +614,7 @@ module.exports = React.createClass({
name={ setting.id }
value={ setting.value }
defaultChecked={ this._syncedSettings[setting.id] === setting.value }
onChange={ e => {
onChange={ (e) => {
if (e.target.checked) {
UserSettingsStore.setSyncedSetting(setting.id, setting.value);
}
@ -597,7 +634,12 @@ module.exports = React.createClass({
_renderCryptoInfo: function() {
const client = MatrixClientPeg.get();
const deviceId = client.deviceId;
const identityKey = client.getDeviceEd25519Key() || "<not supported>";
let identityKey = client.getDeviceEd25519Key();
if (!identityKey) {
identityKey = _t("<not supported>");
} else {
identityKey = FormattingUtils.formatCryptoKey(identityKey);
}
let importExportButtons = null;
@ -606,18 +648,18 @@ module.exports = React.createClass({
<div className="mx_UserSettings_importExportButtons">
<AccessibleButton className="mx_UserSettings_button"
onClick={this._onExportE2eKeysClicked}>
Export E2E room keys
{ _t("Export E2E room keys") }
</AccessibleButton>
<AccessibleButton className="mx_UserSettings_button"
onClick={this._onImportE2eKeysClicked}>
Import E2E room keys
{ _t("Import E2E room keys") }
</AccessibleButton>
</div>
);
}
return (
<div>
<h3>Cryptography</h3>
<h3>{ _t("Cryptography") }</h3>
<div className="mx_UserSettings_section mx_UserSettings_cryptoSection">
<ul>
<li><label>Device ID:</label> <span><code>{deviceId}</code></span></li>
@ -639,8 +681,8 @@ module.exports = React.createClass({
type="checkbox"
defaultChecked={ this._localSettings[setting.id] }
onChange={
e => {
UserSettingsStore.setLocalSetting(setting.id, e.target.checked)
(e) => {
UserSettingsStore.setLocalSetting(setting.id, e.target.checked);
if (setting.id === 'blacklistUnverifiedDevices') { // XXX: this is a bit ugly
client.setGlobalBlacklistUnverifiedDevices(e.target.checked);
}
@ -648,13 +690,13 @@ module.exports = React.createClass({
}
/>
<label htmlFor={ setting.id }>
{ setting.label }
{ _t(setting.label) }
</label>
</div>;
},
_renderDevicesPanel: function() {
var DevicesPanel = sdk.getComponent('settings.DevicesPanel');
const DevicesPanel = sdk.getComponent('settings.DevicesPanel');
return (
<div>
<h3>Devices</h3>
@ -665,15 +707,15 @@ module.exports = React.createClass({
_renderBugReport: function() {
if (!SdkConfig.get().bug_report_endpoint_url) {
return <div />
return <div />;
}
return (
<div>
<h3>Bug Report</h3>
<h3>{ _t("Bug Report") }</h3>
<div className="mx_UserSettings_section">
<p>Found a bug?</p>
<p>{ _t("Found a bug?") }</p>
<button className="mx_UserSettings_button danger"
onClick={this._onBugReportClicked}>Report it
onClick={this._onBugReportClicked}>{_t('Report it')}
</button>
</div>
</div>
@ -684,20 +726,20 @@ module.exports = React.createClass({
// default to enabled if undefined
if (this.props.enableLabs === false) return null;
let features = UserSettingsStore.LABS_FEATURES.map(feature => (
const features = UserSettingsStore.LABS_FEATURES.map((feature) => (
<div key={feature.id} className="mx_UserSettings_toggle">
<input
type="checkbox"
id={feature.id}
name={feature.id}
defaultChecked={ UserSettingsStore.isFeatureEnabled(feature.id) }
onChange={e => {
onChange={(e) => {
if (MatrixClientPeg.get().isGuest()) {
e.target.checked = false;
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "Guests can't use labs features. Please register.",
title: _t("Please Register"),
description: _t("Guests can't use labs features. Please register."),
});
return;
}
@ -710,9 +752,9 @@ module.exports = React.createClass({
));
return (
<div>
<h3>Labs</h3>
<h3>{ _t("Labs") }</h3>
<div className="mx_UserSettings_section">
<p>These are experimental features that may break in unexpected ways. Use with caution.</p>
<p>{ _t("These are experimental features that may break in unexpected ways") }. { _t("Use with caution") }.</p>
{features}
</div>
</div>
@ -724,10 +766,10 @@ module.exports = React.createClass({
if (MatrixClientPeg.get().isGuest()) return null;
return <div>
<h3>Deactivate Account</h3>
<h3>{ _t("Deactivate Account") }</h3>
<div className="mx_UserSettings_section">
<AccessibleButton className="mx_UserSettings_button danger"
onClick={this._onDeactivateAccountClicked}>Deactivate my account
onClick={this._onDeactivateAccountClicked}> { _t("Deactivate my account") }
</AccessibleButton>
</div>
</div>;
@ -735,25 +777,25 @@ module.exports = React.createClass({
_renderClearCache: function() {
return <div>
<h3>Clear Cache</h3>
<h3>{ _t("Clear Cache") }</h3>
<div className="mx_UserSettings_section">
<AccessibleButton className="mx_UserSettings_button danger"
onClick={this._onClearCacheClicked}>
Clear Cache and Reload
{ _t("Clear Cache and Reload") }
</AccessibleButton>
</div>
</div>;
},
_renderBulkOptions: function() {
let invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => {
const invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => {
return r.hasMembershipState(this._me, "invite");
});
if (invitedRooms.length === 0) {
return null;
}
let Spinner = sdk.getComponent("elements.Spinner");
const Spinner = sdk.getComponent("elements.Spinner");
let reject = <Spinner />;
if (!this.state.rejectingInvites) {
@ -768,7 +810,7 @@ module.exports = React.createClass({
}
return <div>
<h3>Bulk Options</h3>
<h3>{ _t("Bulk Options") }</h3>
<div className="mx_UserSettings_section">
{reject}
</div>
@ -777,9 +819,7 @@ module.exports = React.createClass({
_showSpoiler: function(event) {
const target = event.target;
const hidden = target.getAttribute('data-spoiler');
target.innerHTML = hidden;
target.innerHTML = target.getAttribute('data-spoiler');
const range = document.createRange();
range.selectNodeContents(target);
@ -790,12 +830,13 @@ module.exports = React.createClass({
},
nameForMedium: function(medium) {
if (medium == 'msisdn') return 'Phone';
if (medium === 'msisdn') return _t('Phone');
if (medium === 'email') return _t('Email');
return medium[0].toUpperCase() + medium.slice(1);
},
presentableTextForThreepid: function(threepid) {
if (threepid.medium == 'msisdn') {
if (threepid.medium === 'msisdn') {
return '+' + threepid.address;
} else {
return threepid.address;
@ -803,7 +844,7 @@ module.exports = React.createClass({
},
render: function() {
var Loader = sdk.getComponent("elements.Spinner");
const Loader = sdk.getComponent("elements.Spinner");
switch (this.state.phase) {
case "UserSettings.LOADING":
return (
@ -815,18 +856,18 @@ module.exports = React.createClass({
throw new Error("Unknown state.phase => " + this.state.phase);
}
// can only get here if phase is UserSettings.DISPLAY
var SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
var ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName");
var ChangePassword = sdk.getComponent("views.settings.ChangePassword");
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
var Notifications = sdk.getComponent("settings.Notifications");
var EditableText = sdk.getComponent('elements.EditableText');
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
const ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName");
const ChangePassword = sdk.getComponent("views.settings.ChangePassword");
const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
const Notifications = sdk.getComponent("settings.Notifications");
const EditableText = sdk.getComponent('elements.EditableText');
var avatarUrl = (
const avatarUrl = (
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
);
var threepidsSection = this.state.threepids.map((val, pidIndex) => {
const threepidsSection = this.state.threepids.map((val, pidIndex) => {
const id = "3pid-" + val.address;
return (
<div className="mx_UserSettings_profileTableRow" key={pidIndex}>
@ -839,7 +880,7 @@ module.exports = React.createClass({
/>
</div>
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
<img src="img/cancel-small.svg" width="14" height="14" alt="Remove" onClick={this.onRemoveThreepidClicked.bind(this, val)} />
<img src="img/cancel-small.svg" width="14" height="14" alt={ _t("Remove") } onClick={this.onRemoveThreepidClicked.bind(this, val)} />
</div>
</div>
);
@ -851,13 +892,14 @@ module.exports = React.createClass({
addEmailSection = (
<div className="mx_UserSettings_profileTableRow" key="_newEmail">
<div className="mx_UserSettings_profileLabelCell">
<label>{_t('Email')}</label>
</div>
<div className="mx_UserSettings_profileInputCell">
<EditableText
ref="add_email_input"
className="mx_UserSettings_editable"
placeholderClassName="mx_UserSettings_threepidPlaceholder"
placeholder={ "Add email address" }
placeholder={ _t("Add email address") }
blurToCancel={ false }
onValueChanged={ this._onAddEmailEditFinished } />
</div>
@ -874,16 +916,15 @@ module.exports = React.createClass({
threepidsSection.push(addEmailSection);
threepidsSection.push(addMsisdnSection);
var accountJsx;
let accountJsx;
if (MatrixClientPeg.get().isGuest()) {
accountJsx = (
<div className="mx_UserSettings_button" onClick={this.onUpgradeClicked}>
Create an account
{ _t("Create an account") }
</div>
);
}
else {
} else {
accountJsx = (
<ChangePassword
className="mx_UserSettings_accountTable"
@ -895,10 +936,10 @@ module.exports = React.createClass({
onFinished={this.onPasswordChanged} />
);
}
var notification_area;
let notificationArea;
if (!MatrixClientPeg.get().isGuest() && this.state.threepids !== undefined) {
notification_area = (<div>
<h3>Notifications</h3>
notificationArea = (<div>
<h3>{ _t("Notifications") }</h3>
<div className="mx_UserSettings_section">
<Notifications threepids={this.state.threepids} brand={this.props.brand} />
@ -911,13 +952,13 @@ module.exports = React.createClass({
// we are using a version old version of olm. We assume the former.
let olmVersionString = "<not-enabled>";
if (olmVersion !== undefined) {
olmVersionString = `v${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`;
olmVersionString = `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`;
}
return (
<div className="mx_UserSettings">
<SimpleRoomHeader
title="Settings"
title={ _t("Settings") }
collapsedRhs={ this.props.collapsedRhs }
onCancelClick={ this.props.onClose }
/>
@ -925,13 +966,13 @@ module.exports = React.createClass({
<GeminiScrollbar className="mx_UserSettings_body"
autoshow={true}>
<h3>Profile</h3>
<h3>{ _t("Profile") }</h3>
<div className="mx_UserSettings_section">
<div className="mx_UserSettings_profileTable">
<div className="mx_UserSettings_profileTableRow">
<div className="mx_UserSettings_profileLabelCell">
<label htmlFor="displayName">Display name</label>
<label htmlFor="displayName">{ _t('Display name') }</label>
</div>
<div className="mx_UserSettings_profileInputCell">
<ChangeDisplayName />
@ -948,7 +989,7 @@ module.exports = React.createClass({
<div className="mx_UserSettings_avatarPicker_edit">
<label htmlFor="avatarInput" ref="file_label">
<img src="img/camera.svg" className="mx_filterFlipColor"
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}/>
@ -956,12 +997,12 @@ module.exports = React.createClass({
</div>
</div>
<h3>Account</h3>
<h3>{ _t("Account") }</h3>
<div className="mx_UserSettings_section">
<div className="mx_UserSettings_section cadcampoHide">
<AccessibleButton className="mx_UserSettings_logout mx_UserSettings_button" onClick={this.onLogoutClicked}>
Sign out
{ _t("Sign out") }
</AccessibleButton>
{accountJsx}
@ -969,7 +1010,7 @@ module.exports = React.createClass({
{this._renderReferral()}
{notification_area}
{notificationArea}
{this._renderUserInterfaceSettings()}
{this._renderLabs()}
@ -978,31 +1019,31 @@ module.exports = React.createClass({
{this._renderBulkOptions()}
{this._renderBugReport()}
<h3>Advanced</h3>
<h3>{ _t("Advanced") }</h3>
<div className="mx_UserSettings_section">
<div className="mx_UserSettings_advanced">
Logged in as {this._me}
{ _t("Logged in as:") } {this._me}
</div>
<div className="mx_UserSettings_advanced">
Access Token: <span className="mx_UserSettings_advanced_spoiler" onClick={this._showSpoiler} data-spoiler={ MatrixClientPeg.get().getAccessToken() }>&lt;click to reveal&gt;</span>
{_t('Access Token:')} <span className="mx_UserSettings_advanced_spoiler" onClick={this._showSpoiler} data-spoiler={ MatrixClientPeg.get().getAccessToken() }>&lt;{ _t("click to reveal") }&gt;</span>
</div>
<div className="mx_UserSettings_advanced">
Homeserver is { MatrixClientPeg.get().getHomeserverUrl() }
{ _t("Homeserver is") } { MatrixClientPeg.get().getHomeserverUrl() }
</div>
<div className="mx_UserSettings_advanced">
Identity Server is { MatrixClientPeg.get().getIdentityServerUrl() }
{ _t("Identity Server is") } { MatrixClientPeg.get().getIdentityServerUrl() }
</div>
<div className="mx_UserSettings_advanced">
matrix-react-sdk version: {(REACT_SDK_VERSION !== '<local>')
? <a href={ GHVersionUrl('matrix-org/matrix-react-sdk', REACT_SDK_VERSION) }>{REACT_SDK_VERSION}</a>
{_t('matrix-react-sdk version:')} {(REACT_SDK_VERSION !== '<local>')
? gHVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION)
: REACT_SDK_VERSION
}<br/>
riot-web version: {(this.state.vectorVersion !== null)
? <a href={ GHVersionUrl('vector-im/riot-web', this.state.vectorVersion.split('-')[0]) }>{this.state.vectorVersion}</a>
{_t('riot-web version:')} {(this.state.vectorVersion !== undefined)
? gHVersionLabel('vector-im/riot-web', this.state.vectorVersion)
: 'unknown'
}<br/>
olm version: {olmVersionString}<br/>
{ _t("olm version:") } {olmVersionString}<br/>
</div>
</div>
@ -1013,5 +1054,5 @@ module.exports = React.createClass({
</GeminiScrollbar>
</div>
);
}
},
});

View file

@ -17,6 +17,7 @@ limitations under the License.
'use strict';
var React = require('react');
import { _t } from '../../../languageHandler';
var sdk = require('../../../index');
var Modal = require("../../../Modal");
var MatrixClientPeg = require('../../../MatrixClientPeg');
@ -54,7 +55,7 @@ module.exports = React.createClass({
progress: "sent_email"
});
}, (err) => {
this.showErrorDialog("Failed to send email: " + err.message);
this.showErrorDialog(_t('Failed to send email') + ": " + err.message);
this.setState({
progress: null
});
@ -78,30 +79,33 @@ module.exports = React.createClass({
ev.preventDefault();
if (!this.state.email) {
this.showErrorDialog("The email address linked to your account must be entered.");
this.showErrorDialog(_t('The email address linked to your account must be entered.'));
}
else if (!this.state.password || !this.state.password2) {
this.showErrorDialog("A new password must be entered.");
this.showErrorDialog(_t('A new password must be entered.'));
}
else if (this.state.password !== this.state.password2) {
this.showErrorDialog("New passwords must match each other.");
this.showErrorDialog(_t('New passwords must match each other.'));
}
else {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: "Warning",
title: _t('Warning!'),
description:
<div>
Resetting 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(
'Resetting 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.'
) }
</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) => {
@ -150,7 +154,7 @@ module.exports = React.createClass({
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: title,
description: body
description: body,
});
},
@ -168,22 +172,20 @@ module.exports = React.createClass({
else if (this.state.progress === "sent_email") {
resetPasswordJsx = (
<div>
An email has been sent to {this.state.email}. Once you&#39;ve followed
the link it contains, click below.
{ _t('An email has been sent to') } {this.state.email}. { _t('Once you&#39;ve followed the link it contains, click below') }.
<br />
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
value="I have verified my email address" />
value={ _t('I have verified my email address') } />
</div>
);
}
else if (this.state.progress === "complete") {
resetPasswordJsx = (
<div>
<p>Your password has been reset.</p>
<p>You have been logged out of all devices and will no longer receive push notifications.
To re-enable notifications, sign in again on each device.</p>
<p>{ _t('Your password has been reset') }.</p>
<p>{ _t('You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device') }.</p>
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
value="Return to login screen" />
value={ _t('Return to login screen') } />
</div>
);
}
@ -191,7 +193,7 @@ module.exports = React.createClass({
resetPasswordJsx = (
<div>
<div className="mx_Login_prompt">
To reset your password, enter the email address linked to your account:
{ _t('To reset your password, enter the email address linked to your account') }:
</div>
<div>
<form onSubmit={this.onSubmitForm}>
@ -199,21 +201,21 @@ module.exports = React.createClass({
name="reset_email" // define a name so browser's password autofill gets less confused
value={this.state.email}
onChange={this.onInputChanged.bind(this, "email")}
placeholder="Email address" autoFocus />
placeholder={ _t('Email address') } autoFocus />
<br />
<input className="mx_Login_field" ref="pass" type="password"
name="reset_password"
value={this.state.password}
onChange={this.onInputChanged.bind(this, "password")}
placeholder="New password" />
placeholder={ _t('New password') } />
<br />
<input className="mx_Login_field" ref="pass" type="password"
name="reset_password_confirm"
value={this.state.password2}
onChange={this.onInputChanged.bind(this, "password2")}
placeholder="Confirm your new password" />
placeholder={ _t('Confirm your new password') } />
<br />
<input className="mx_Login_submit" type="submit" value="Send Reset Email" />
<input className="mx_Login_submit" type="submit" value={ _t('Send Reset Email') } />
</form>
<ServerConfig ref="serverConfig"
withToggleButton={true}
@ -230,7 +232,7 @@ module.exports = React.createClass({
Return to login
</a>
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
Create a new account
{ _t('Create an account') }
</a>
<LoginFooter />
</div>

View file

@ -18,11 +18,14 @@ limitations under the License.
'use strict';
import React from 'react';
import { _t } from '../../../languageHandler';
import ReactDOM from 'react-dom';
import url from 'url';
import sdk from '../../../index';
import Login from '../../../Login';
// For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
/**
* A wire component which glues together login UI components and Login logic
*/
@ -125,7 +128,16 @@ module.exports = React.createClass({
},
onPhoneNumberChanged: function(phoneNumber) {
this.setState({ phoneNumber: phoneNumber });
// Validate the phone number entered
if (!PHONE_NUMBER_REGEX.test(phoneNumber)) {
this.setState({ errorText: 'The phone number entered looks invalid' });
return;
}
this.setState({
phoneNumber: phoneNumber,
errorText: null,
});
},
onServerConfigChange: function(config) {
@ -210,15 +222,17 @@ module.exports = React.createClass({
(this.state.enteredHomeserverUrl.startsWith("http:") ||
!this.state.enteredHomeserverUrl.startsWith("http")))
{
const urlStart = <a href='https://www.google.com/search?&q=enable%20unsafe%20scripts'>;
const urlEnd = </a>;
errorText = <span>
Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar.
Either use HTTPS or <a href='https://www.google.com/search?&q=enable%20unsafe%20scripts'>enable unsafe scripts</a>
{ _t('Can\'t connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or %(urlStart)s enable unsafe scripts %(urlEnd)s', {urlStart: urlStart, urlEnd: urlEnd})}
</span>;
}
else {
const urlStart = <a href={this.state.enteredHomeserverUrl}>;
const urlEnd = </a>;
errorText = <span>
Can't connect to homeserver - please check your connectivity and ensure
your <a href={ this.state.enteredHomeserverUrl }>homeserver's SSL certificate</a> is trusted.
{ _t('Can\'t connect to homeserver - please check your connectivity and ensure your %(urlStart)s homeserver\'s SSL certificate %(urlEnd)s is trusted', {urlStart: urlStart, urlEnd: urlEnd})}
</span>;
}
}
@ -230,12 +244,6 @@ module.exports = React.createClass({
switch (step) {
case 'm.login.password':
const PasswordLogin = sdk.getComponent('login.PasswordLogin');
// HSs that are not matrix.org may not be configured to have their
// domain name === domain part.
let hsDomain = url.parse(this.state.enteredHomeserverUrl).hostname;
if (hsDomain !== 'matrix.org') {
hsDomain = null;
}
return (
<PasswordLogin
onSubmit={this.onPasswordLogin}
@ -247,7 +255,6 @@ module.exports = React.createClass({
onPhoneNumberChanged={this.onPhoneNumberChanged}
onForgotPasswordClick={this.props.onForgotPasswordClick}
loginIncorrect={this.state.loginIncorrect}
hsDomain={hsDomain}
/>
);
case 'm.login.cas':
@ -261,8 +268,7 @@ module.exports = React.createClass({
}
return (
<div>
Sorry, this homeserver is using a login which is not
recognised ({step})
{ _t('Sorry, this homeserver is using a login which is not recognised ')}({step})
</div>
);
}
@ -279,7 +285,7 @@ module.exports = React.createClass({
if (this.props.enableGuest) {
loginAsGuestJsx =
<a className="mx_Login_create" onClick={this._onLoginAsGuestClick} href="#">
Login as guest
{ _t('Login as guest')}
</a>;
}
@ -287,7 +293,7 @@ module.exports = React.createClass({
if (this.props.onCancelClick) {
returnToAppJsx =
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
Return to app
{ _t('Return to app')}
</a>;
}
@ -296,7 +302,7 @@ module.exports = React.createClass({
<div className="mx_Login_box">
<LoginHeader />
<div>
<h2>Sign in
<h2>{ _t('Sign in')}
{ loader }
</h2>
{ this.componentForStep(this.state.currentFlow) }
@ -312,7 +318,7 @@ module.exports = React.createClass({
{ this.state.errorText }
</div>
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
Create a new account
{ _t('Create an account')}
</a>
{ loginAsGuestJsx }
{ returnToAppJsx }

View file

@ -16,9 +16,10 @@ limitations under the License.
'use strict';
var React = require('react');
var sdk = require('../../../index');
var MatrixClientPeg = require('../../../MatrixClientPeg');
import React from 'react';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'PostRegistration',
@ -64,12 +65,12 @@ module.exports = React.createClass({
<div className="mx_Login_box">
<LoginHeader />
<div className="mx_Login_profile">
Set a display name:
{ _t('Set a display name:') }
<ChangeDisplayName />
Upload an avatar:
{ _t('Upload an avatar:') }
<ChangeAvatar
initialAvatarUrl={this.state.avatarUrl} />
<button onClick={this.props.onComplete}>Continue</button>
<button onClick={this.props.onComplete}>{ _t('Continue') }</button>
{this.state.errorString}
</div>
</div>

View file

@ -27,6 +27,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import RegistrationForm from '../../views/login/RegistrationForm';
import CaptchaForm from '../../views/login/CaptchaForm';
import RtsClient from '../../../RtsClient';
import { _t } from '../../../languageHandler';
const MIN_PASSWORD_LENGTH = 6;
@ -123,18 +124,17 @@ module.exports = React.createClass({
}
},
onHsUrlChanged: function(newHsUrl) {
this.setState({
hsUrl: newHsUrl,
onServerConfigChange: function(config) {
let newState = {};
if (config.hsUrl !== undefined) {
newState.hsUrl = config.hsUrl;
}
if (config.isUrl !== undefined) {
newState.isUrl = config.isUrl;
}
this.setState(newState, function() {
this._replaceClient();
});
this._replaceClient();
},
onIsUrlChanged: function(newIsUrl) {
this.setState({
isUrl: newIsUrl,
});
this._replaceClient();
},
_replaceClient: function() {
@ -163,7 +163,7 @@ module.exports = React.createClass({
msisdn_available |= flow.stages.indexOf('m.login.msisdn') > -1;
}
if (!msisdn_available) {
msg = "This server does not support authentication with a phone number";
msg = _t('This server does not support authentication with a phone number.');
}
}
this.setState({
@ -261,29 +261,29 @@ module.exports = React.createClass({
var errMsg;
switch (errCode) {
case "RegistrationForm.ERR_PASSWORD_MISSING":
errMsg = "Missing password.";
errMsg = _t('Missing password.');
break;
case "RegistrationForm.ERR_PASSWORD_MISMATCH":
errMsg = "Passwords don't match.";
errMsg = _t('Passwords don\'t match.');
break;
case "RegistrationForm.ERR_PASSWORD_LENGTH":
errMsg = `Password too short (min ${MIN_PASSWORD_LENGTH}).`;
errMsg = _t('Password too short (min %(MIN_PASSWORD_LENGTH)s).', {MIN_PASSWORD_LENGTH: $MIN_PASSWORD_LENGTH})
break;
case "RegistrationForm.ERR_EMAIL_INVALID":
errMsg = "This doesn't look like a valid email address";
errMsg = _t('This doesn\'t look like a valid email address.');
break;
case "RegistrationForm.ERR_PHONE_NUMBER_INVALID":
errMsg = "This doesn't look like a valid phone number";
errMsg = _t('This doesn\'t look like a valid phone number.');
break;
case "RegistrationForm.ERR_USERNAME_INVALID":
errMsg = "User names may only contain letters, numbers, dots, hyphens and underscores.";
errMsg = _t('User names may only contain letters, numbers, dots, hyphens and underscores.');
break;
case "RegistrationForm.ERR_USERNAME_BLANK":
errMsg = "You need to enter a user name";
errMsg = _t('You need to enter a user name.');
break;
default:
console.error("Unknown error code: %s", errCode);
errMsg = "An unknown error occurred.";
errMsg = _t('An unknown error occurred.');
break;
}
this.setState({
@ -390,8 +390,7 @@ module.exports = React.createClass({
customIsUrl={this.props.customIsUrl}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
onHsUrlChanged={this.onHsUrlChanged}
onIsUrlChanged={this.onIsUrlChanged}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={1000}
/>
</div>
@ -402,7 +401,7 @@ module.exports = React.createClass({
if (this.props.onCancelClick) {
returnToAppJsx = (
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
Return to app
{_t('Return to app')}
</a>
);
}
@ -415,10 +414,10 @@ module.exports = React.createClass({
this.state.teamSelected.domain + "/icon.png" :
null}
/>
<h2>Create an account</h2>
<h2>{_t('Create an account')}</h2>
{registerBody}
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
I already have an account
{_t('I already have an account')}
</a>
{returnToAppJsx}
<LoginFooter />

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),
@ -74,7 +76,9 @@ module.exports = React.createClass({
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
);
},
@ -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;

View file

@ -47,16 +47,6 @@ export default React.createClass({
children: React.PropTypes.node,
},
componentWillMount: function() {
this.priorActiveElement = document.activeElement;
},
componentWillUnmount: function() {
if (this.priorActiveElement !== null) {
this.priorActiveElement.focus();
}
},
_onKeyDown: function(e) {
if (e.keyCode === KeyCode.ESCAPE) {
e.stopPropagation();
@ -77,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,6 +16,7 @@ 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';
@ -48,11 +49,7 @@ module.exports = React.createClass({
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
};
},

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,76 @@
/*
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';
export default function DeviceVerifyDialog(props) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const key = FormattingUtils.formatCryptoKey(props.device.getFingerprint());
const body = (
<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>{ props.device.getDisplayName() }</span></li>
<li><label>Device ID:</label> <span><code>{ props.device.deviceId}</code></span></li>
<li><label>Device key:</label> <span><code><b>{ key }</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>
);
function onFinished(confirm) {
if (confirm) {
MatrixClientPeg.get().setDeviceVerified(
props.userId, props.device.deviceId, true,
);
}
props.onFinished(confirm);
}
return (
<QuestionDialog
title="Verify device"
description={body}
button="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,10 +44,10 @@ export default React.createClass({
getDefaultProps: function() {
return {
title: "Error",
description: "An error has occurred.",
button: "OK",
focus: true,
title: null,
description: null,
button: null,
};
},
@ -60,13 +61,13 @@ export default React.createClass({
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 ref="button" className="mx_Dialog_primary" onClick={this.props.onFinished}>
{this.props.button}
{this.props.button || _t('OK')}
</button>
</div>
</BaseDialog>

View file

@ -20,6 +20,7 @@ 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 +47,6 @@ export default React.createClass({
title: React.PropTypes.string,
},
getDefaultProps: function() {
return {
title: "Authentication",
};
},
getInitialState: function() {
return {
authError: null,
@ -105,7 +100,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

@ -26,6 +26,7 @@ limitations under the License.
import React from 'react';
import dis from '../../../dispatcher';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'NeedToRegisterDialog',
@ -38,13 +39,6 @@ module.exports = React.createClass({
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",
@ -59,10 +53,10 @@ module.exports = React.createClass({
return (
<BaseDialog className="mx_NeedToRegisterDialog"
onFinished={this.props.onFinished}
title={this.props.title}
title={this.props.title || _t('Registration required')}
>
<div className="mx_Dialog_content">
{this.props.description}
{this.props.description || _t('A registered account is required for this action')}
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={true}>

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,
};
@ -47,12 +47,6 @@ export default React.createClass({
this.props.onFinished(false);
},
componentDidMount: function() {
if (this.props.focus) {
this.refs.button.focus();
}
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const cancelButton = this.props.hasCancelButton ? (
@ -69,8 +63,8 @@ export default React.createClass({
{this.props.description}
</div>
<div className="mx_Dialog_buttons">
<button ref="button" className="mx_Dialog_primary" onClick={this.onOk}>
{this.props.button}
<button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}>
{this.props.button || _t('OK')}
</button>
{this.props.extraButtons}
{cancelButton}

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

@ -149,7 +149,7 @@ export default React.createClass({
>
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
<h4>
This room contains devices that you haven't seen before.
"{this.props.room.name}" contains devices that you haven't seen before.
</h4>
{ warning }
Unknown devices:

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

@ -50,42 +50,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,
});
},

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

@ -114,8 +114,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 +152,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) {
@ -249,7 +254,7 @@ export default class Dropdown extends React.Component {
);
});
if (options.length === 0) {
return [<div className="mx_Dropdown_option">
return [<div key="0" className="mx_Dropdown_option">
No results
</div>];
}

View file

@ -0,0 +1,128 @@
/*
Copyright 2017 Marcel Radzio (MTRNord)
Copyright 2017 Vector Creations Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import sdk from '../../../index';
import UserSettingsStore from '../../../UserSettingsStore';
import { _t } from '../../../languageHandler';
import * as languageHandler from '../../../languageHandler';
function languageMatchesSearchQuery(query, language) {
if (language.label.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
if (language.value.toUpperCase() == query.toUpperCase()) return true;
return false;
}
export default class LanguageDropdown extends React.Component {
constructor(props) {
super(props);
this._onSearchChange = this._onSearchChange.bind(this);
this.state = {
searchQuery: '',
langs: null,
}
}
componentWillMount() {
languageHandler.getAllLanguageKeysFromJson().then((langKeys) => {
const langs = [];
langKeys.forEach((languageKey) => {
langs.push({
value: languageKey,
label: _t(languageKey)
});
});
langs.sort(function(a, b){
if(a.label < b.label) return -1;
if(a.label > b.label) return 1;
return 0;
});
this.setState({langs});
}).catch(() => {
this.setState({langs: ['en']});
}).done();
if (!this.props.value) {
// If no value is given, we start with the first
// country selected, but our parent component
// doesn't know this, therefore we do this.
const _localSettings = UserSettingsStore.getLocalSettings();
if (_localSettings.hasOwnProperty('language')) {
this.props.onOptionChange(_localSettings.language);
}else {
const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser());
this.props.onOptionChange(language);
}
}
}
_onSearchChange(search) {
this.setState({
searchQuery: search,
});
}
render() {
if (this.state.langs === null) {
const Spinner = sdk.getComponent('elements.Spinner');
return <Spinner />;
}
const Dropdown = sdk.getComponent('elements.Dropdown');
let displayedLanguages;
if (this.state.searchQuery) {
displayedLanguages = this.state.langs.filter((lang) => {
return languageMatchesSearchQuery(this.state.searchQuery, lang);
});
} else {
displayedLanguages = this.state.langs;
}
const options = displayedLanguages.map((language) => {
return <div key={language.value}>
{language.label}
</div>;
});
// default value here too, otherwise we need to handle null / undefined
// values between mounting and the initial value propgating
let value = null;
const _localSettings = UserSettingsStore.getLocalSettings();
if (_localSettings.hasOwnProperty('language')) {
value = this.props.value || _localSettings.language;
} else {
const language = navigator.language || navigator.userLanguage;
value = this.props.value || language;
}
return <Dropdown className={this.props.className}
onOptionChange={this.props.onOptionChange} onSearchChange={this._onSearchChange}
searchEnabled={true} value={value}
>
{options}
</Dropdown>
}
}
LanguageDropdown.propTypes = {
className: React.PropTypes.string,
onOptionChange: React.PropTypes.func.isRequired,
value: React.PropTypes.string,
};

View file

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

View file

@ -19,10 +19,8 @@ limitations under the License.
import React from 'react';
import * as Roles from '../../../Roles';
var LEVEL_ROLE_MAP = {};
var reverseRoles = {};
Object.keys(Roles.LEVEL_ROLE_MAP).forEach(function(key) {
reverseRoles[Roles.LEVEL_ROLE_MAP[key]] = key;
});
module.exports = React.createClass({
displayName: 'PowerSelector',
@ -44,9 +42,16 @@ module.exports = React.createClass({
getInitialState: function() {
return {
custom: (Roles.LEVEL_ROLE_MAP[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" });
@ -94,7 +99,7 @@ module.exports = React.createClass({
selectValue = "Custom";
}
else {
selectValue = Roles.LEVEL_ROLE_MAP[this.props.value] || "Custom";
selectValue = LEVEL_ROLE_MAP[this.props.value] || "Custom";
}
var select;
if (this.props.disabled) {
@ -105,7 +110,7 @@ module.exports = React.createClass({
const levels = [0, 50, 100];
let options = levels.map((level) => {
return {
value: Roles.LEVEL_ROLE_MAP[level],
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),

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,9 +26,14 @@ 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;
}
@ -37,10 +41,12 @@ 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() {
@ -48,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]);
}
}
@ -58,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, true,
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() {
@ -94,7 +112,7 @@ export default class CountryDropdown extends React.Component {
const options = displayedCountries.map((country) => {
return <div key={country.iso2}>
{this._flagImgForIso2(country.iso2)}
{country.name}
{country.name} <span>(+{country.prefix})</span>
</div>;
});
@ -102,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}
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

@ -19,6 +19,7 @@ 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';
@ -90,8 +91,11 @@ class PasswordLogin extends React.Component {
}
onPhoneCountryChanged(country) {
this.setState({phoneCountry: country});
this.props.onPhoneCountryChanged(country);
this.setState({
phoneCountry: country.iso2,
phonePrefix: country.prefix,
});
this.props.onPhoneCountryChanged(country.iso2);
}
onPhoneNumberChanged(ev) {
@ -118,42 +122,29 @@ class PasswordLogin extends React.Component {
autoFocus
/>;
case PasswordLogin.LOGIN_FIELD_MXID:
const mxidInputClasses = classNames({
"mx_Login_field": true,
"mx_Login_username": true,
"mx_Login_field_has_suffix": Boolean(this.props.hsDomain),
});
let suffix = null;
if (this.props.hsDomain) {
suffix = <div className="mx_Login_username_suffix">
:{this.props.hsDomain}
</div>;
}
return <div className="mx_Login_username_group">
<div className="mx_Login_username_prefix">@</div>
<input
className={mxidInputClasses}
key="username_input"
type="text"
name="username" // make it a little easier for browser's remember-password
onChange={this.onUsernameChanged}
placeholder="username"
value={this.state.username}
autoFocus
/>
{suffix}
</div>;
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"
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"
className="mx_Login_phoneNumberField mx_Login_field mx_Login_field_has_prefix"
ref="phoneNumber"
key="phone_input"
type="text"
@ -173,7 +164,7 @@ class PasswordLogin extends React.Component {
if (this.props.onForgotPasswordClick) {
forgotPasswordJsx = (
<a className="mx_Login_forgot" onClick={this.props.onForgotPasswordClick} href="#">
Forgot your password?
{ _t('Forgot your password?') }
</a>
);
}
@ -191,24 +182,24 @@ class PasswordLogin extends React.Component {
<div>
<form onSubmit={this.onSubmitForm}>
<div className="mx_Login_type_container">
<label className="mx_Login_type_label">I want to sign in with my</label>
<label className="mx_Login_type_label">{ _t('I want to sign in with') }</label>
<Dropdown
className="mx_Login_type_dropdown"
value={this.state.loginType}
onOptionChange={this.onLoginTypeChange}>
<span key={PasswordLogin.LOGIN_FIELD_MXID}>Matrix ID</span>
<span key={PasswordLogin.LOGIN_FIELD_EMAIL}>Email Address</span>
<span key={PasswordLogin.LOGIN_FIELD_PHONE}>Phone</span>
<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>
{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>
);
@ -231,7 +222,6 @@ PasswordLogin.propTypes = {
onPhoneNumberChanged: React.PropTypes.func,
onPasswordChanged: React.PropTypes.func,
loginIncorrect: React.PropTypes.bool,
hsDomain: React.PropTypes.string,
};
module.exports = PasswordLogin;

View file

@ -100,7 +100,7 @@ module.exports = React.createClass({
if (this.refs.email.value == '') {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: "Warning",
title: "Warning!",
description:
<div>
If you don't specify an email address, you won't be able to reset your password.<br/>
@ -270,7 +270,8 @@ module.exports = React.createClass({
_onPhoneCountryChange(newVal) {
this.setState({
phoneCountry: newVal,
phoneCountry: newVal.iso2,
phonePrefix: newVal.prefix,
});
},
@ -313,14 +314,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)"
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}

View file

@ -20,6 +20,7 @@ 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';
@ -202,7 +203,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 +262,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 +284,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 +297,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 +316,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 }),
}, "*");
};
@ -362,7 +364,7 @@ module.exports = React.createClass({
<div className="mx_MImageBody_download">
<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 +373,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

@ -19,6 +19,7 @@ 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({
@ -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>
);
}
@ -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

@ -16,8 +16,10 @@ 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');
@ -129,6 +131,9 @@ module.exports = WithMatrixClient(React.createClass({
* for now.
*/
tileShape: React.PropTypes.string,
// show twelve hour timestamps
isTwelveHour: React.PropTypes.bool,
},
getInitialState: function() {
@ -295,16 +300,6 @@ module.exports = WithMatrixClient(React.createClass({
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];
@ -340,7 +335,6 @@ module.exports = WithMatrixClient(React.createClass({
suppressAnimation={this._suppressReadReceiptAnimation}
onClick={this.toggleAllReadAvatars}
timestamp={receipt.ts}
showFullTimestamp={receipt.ts >= dayAfterEventTime}
/>
);
}
@ -401,8 +395,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]);
@ -416,9 +409,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',
@ -430,7 +424,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,
});
@ -475,9 +470,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 {
@ -485,36 +480,34 @@ module.exports = WithMatrixClient(React.createClass({
}
}
var editButton = (
const editButton = (
<span className="mx_EventTile_editButton" title="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="Encrypted by 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="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="Encrypted by 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="Encrypted by 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="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,96 @@
/*
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 MatrixClientPeg from '../../../MatrixClientPeg';
import dis from '../../../dispatcher';
import KeyCode from '../../../KeyCode';
module.exports = React.createClass({
displayName: 'ForwardMessage',
propTypes: {
currentRoomId: React.PropTypes.string.isRequired,
/* the MatrixEvent to be forwarded */
mxEvent: React.PropTypes.object.isRequired,
onCancelClick: React.PropTypes.func.isRequired,
},
componentWillMount: function() {
dis.dispatch({
action: 'ui_opacity',
leftOpacity: 1.0,
rightOpacity: 0.3,
middleOpacity: 0.5,
});
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
document.addEventListener('keydown', this._onKeyDown);
},
componentWillUnmount: function() {
dis.dispatch({
action: 'ui_opacity',
sideOpacity: 1.0,
middleOpacity: 1.0,
});
dis.unregister(this.dispatcherRef);
document.removeEventListener('keydown', this._onKeyDown);
},
onAction: function(payload) {
if (payload.action === 'view_room') {
const event = this.props.mxEvent;
const Client = MatrixClientPeg.get();
Client.sendEvent(payload.room_id, event.getType(), event.getContent()).done(() => {
dis.dispatch({action: 'message_sent'});
}, (err) => {
if (err.name === "UnknownDeviceError") {
dis.dispatch({
action: 'unknown_device_error',
err: err,
room: Client.getRoom(payload.room_id),
});
}
dis.dispatch({action: 'message_send_failed'});
});
if (this.props.currentRoomId === payload.room_id) this.props.onCancelClick();
}
},
_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

@ -31,6 +31,7 @@ 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';
@ -219,7 +220,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,7 +242,7 @@ module.exports = WithMatrixClient(React.createClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Kick error: " + err);
Modal.createDialog(ErrorDialog, {
title: "Failed to kick",
title: _t("Failed to kick"),
description: ((err && err.message) ? err.message : "Operation failed"),
});
}
@ -256,7 +257,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 +284,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 +334,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(()=>{
@ -376,14 +377,14 @@ module.exports = WithMatrixClient(React.createClass({
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."
title: _t("Please Register"),
description: _t("This action cannot be performed by a guest user. Please register to be able to do this") + ".",
});
} 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 +404,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 +433,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 +582,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 +596,7 @@ module.exports = WithMatrixClient(React.createClass({
return (
<div>
<h3>Devices</h3>
<h3>{ _t("Devices") }</h3>
<div className="mx_MemberInfo_devices">
{devComponents}
</div>
@ -644,11 +645,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 +662,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 +671,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 +683,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 +692,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>;
@ -717,8 +718,16 @@ 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">
@ -734,7 +743,12 @@ module.exports = WithMatrixClient(React.createClass({
{ this.props.member.userId }
</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>
{ _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>

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';
var classNames = require('classnames');
var Matrix = require("matrix-js-sdk");
var q = require('q');
@ -207,7 +208,9 @@ module.exports = React.createClass({
// 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" : "") + "...";
var text = (overflowCount > 1)
? _t("and %(overflowCount)s others...", { overflowCount: overflowCount })
: _t("and one other...");
return (
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
<BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} />
@ -363,8 +366,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

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require('react');
import { _t } from '../../../languageHandler';
var CallHandler = require('../../../CallHandler');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var Modal = require('../../../Modal');
@ -33,6 +33,7 @@ export default class MessageComposer extends React.Component {
this.onHangupClick = this.onHangupClick.bind(this);
this.onUploadClick = this.onUploadClick.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);
@ -43,6 +44,7 @@ export default class MessageComposer extends React.Component {
this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this);
this.onInputStateChanged = this.onInputStateChanged.bind(this);
this.onEvent = this.onEvent.bind(this);
this.onPageUnload = this.onPageUnload.bind(this);
this.state = {
autocompleteQuery: '',
@ -50,7 +52,7 @@ 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),
@ -64,12 +66,21 @@ export default class MessageComposer extends React.Component {
// marked as encrypted.
// XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something.
MatrixClientPeg.get().on("event", this.onEvent);
window.addEventListener('beforeunload', this.onPageUnload);
}
componentWillUnmount() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("event", this.onEvent);
}
window.removeEventListener('beforeunload', this.onPageUnload);
}
onPageUnload(event) {
if (this.messageComposerInput) {
this.messageComposerInput.sentHistory.saveLastTextEntry();
}
}
onEvent(event) {
@ -82,8 +93,8 @@ export default class MessageComposer extends React.Component {
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.",
title: _t('Please Register'),
description: _t('Guest users can\'t upload files. Please register to upload') + '.',
});
return;
}
@ -91,10 +102,11 @@ export default class MessageComposer extends React.Component {
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");
@ -106,10 +118,10 @@ export default class MessageComposer extends React.Component {
}
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 upload the following files?') }</p>
<ul style={{listStyle: 'none', textAlign: 'left'}}>
{fileList}
</ul>
@ -228,11 +240,11 @@ 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';
}
@ -245,16 +257,16 @@ export default class MessageComposer extends React.Component {
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 {
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>;
}
@ -268,7 +280,7 @@ export default class MessageComposer extends React.Component {
// complex because of conference calls.
var 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}
@ -288,7 +300,7 @@ export default class MessageComposer extends React.Component {
);
const placeholderText = roomIsEncrypted ?
"Send an encrypted message…" : "Send a message (unencrypted)…";
_t('Send an encrypted message') + '…' : _t('Send a message (unencrypted)') + '…';
controls.push(
<MessageComposerInput
@ -300,7 +312,7 @@ export default class MessageComposer extends React.Component {
tryComplete={this._tryComplete}
onUpArrow={this.onUpArrow}
onDownArrow={this.onDownArrow}
onUploadFileSelected={this.onUploadFileSelected}
onFilesPasted={this.uploadFiles}
tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
onContentChanged={this.onInputContentChanged}
onInputStateChanged={this.onInputStateChanged} />,
@ -313,7 +325,7 @@ export default class MessageComposer extends React.Component {
} else {
controls.push(
<div key="controls_error" className="mx_MessageComposer_noperm_error">
You do not have permission to post to this room
{ _t('You do not have permission to post to this room') }
</div>
);
}
@ -342,7 +354,7 @@ export default class MessageComposer extends React.Component {
mx_filterFlipColor: true,
});
return <img className={className}
title={name}
title={ _t(name) }
onMouseDown={disabled ? null : onFormatButtonClicked}
key={name}
src={`img/button-text-${name}${suffix}.svg`}
@ -362,11 +374,11 @@ export default class MessageComposer extends React.Component {
<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'}`}
<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="Hide Text Formatting Toolbar"
<img title={ _t("Hide Text Formatting Toolbar") }
onClick={this.onToggleFormattingClicked}
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
src="img/icon-text-cancel.svg" />

View file

@ -30,6 +30,7 @@ import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
import SlashCommands from '../../../SlashCommands';
import Modal from '../../../Modal';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher';
import KeyCode from '../../../KeyCode';
@ -84,7 +85,6 @@ export default class MessageComposerInput extends React.Component {
this.onAction = this.onAction.bind(this);
this.handleReturn = this.handleReturn.bind(this);
this.handleKeyCommand = this.handleKeyCommand.bind(this);
this.handlePastedFiles = this.handlePastedFiles.bind(this);
this.onEditorContentChanged = this.onEditorContentChanged.bind(this);
this.setEditorState = this.setEditorState.bind(this);
this.onUpArrow = this.onUpArrow.bind(this);
@ -94,7 +94,7 @@ export default class MessageComposerInput extends React.Component {
this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this);
this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true);
const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false);
this.state = {
// whether we're in rich text or markdown mode
@ -477,10 +477,6 @@ export default class MessageComposerInput extends React.Component {
return false;
}
handlePastedFiles(files) {
this.props.onUploadFileSelected(files, true);
}
handleReturn(ev) {
if (ev.shiftKey) {
this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState));
@ -509,8 +505,8 @@ export default class MessageComposerInput extends React.Component {
console.error("Command failure: %s", err);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Server error",
description: ((err && err.message) ? err.message : "Server unavailable, overloaded, or something else went wrong."),
title: _t("Server error"),
description: ((err && err.message) ? err.message : _t("Server unavailable, overloaded, or something else went wrong") + "."),
});
});
}
@ -518,8 +514,8 @@ export default class MessageComposerInput extends React.Component {
console.error(cmd.error);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Command error",
description: cmd.error
title: _t("Command error"),
description: cmd.error,
});
}
return true;
@ -542,9 +538,9 @@ export default class MessageComposerInput extends React.Component {
let sendTextFn = this.client.sendTextMessage;
if (contentText.startsWith('/me')) {
contentText = contentText.replace('/me ', '');
contentText = contentText.substring(4);
// bit of a hack, but the alternative would be quite complicated
if (contentHTML) contentHTML = contentHTML.replace('/me ', '');
if (contentHTML) contentHTML = contentHTML.replace(/\/me ?/, '');
sendHtmlFn = this.client.sendHtmlEmote;
sendTextFn = this.client.sendEmoteMessage;
}
@ -724,7 +720,7 @@ export default class MessageComposerInput extends React.Component {
<div className={className}>
<img className="mx_MessageComposer_input_markdownIndicator mx_filterFlipColor"
onMouseDown={this.onMarkdownToggleClicked}
title={`Markdown is ${this.state.isRichtextEnabled ? 'disabled' : 'enabled'}`}
title={ this.state.isRichtextEnabled ? _t("Markdown is disabled") : _t("Markdown is enabled")}
src={`img/button-md-${!this.state.isRichtextEnabled}.png`} />
<Editor ref="editor"
placeholder={this.props.placeholder}
@ -734,7 +730,7 @@ export default class MessageComposerInput extends React.Component {
keyBindingFn={MessageComposerInput.getKeyBinding}
handleKeyCommand={this.handleKeyCommand}
handleReturn={this.handleReturn}
handlePastedFiles={this.handlePastedFiles}
handlePastedFiles={this.props.onFilesPasted}
stripPastedStyles={!this.state.isRichtextEnabled}
onTab={this.onTab}
onUpArrow={this.onUpArrow}
@ -764,7 +760,7 @@ MessageComposerInput.propTypes = {
onDownArrow: React.PropTypes.func,
onUploadFileSelected: React.PropTypes.func,
onFilesPasted: React.PropTypes.func,
// attempts to confirm currently selected completion, returns whether actually confirmed
tryComplete: React.PropTypes.func,

View file

@ -20,6 +20,7 @@ var SlashCommands = require("../../../SlashCommands");
var Modal = require("../../../Modal");
var MemberEntry = require("../../../TabCompleteEntries").MemberEntry;
var sdk = require('../../../index');
import { _t } from '../../../languageHandler';
import UserSettingsStore from "../../../UserSettingsStore";
var dis = require("../../../dispatcher");
@ -69,6 +70,9 @@ export default React.createClass({
// The text to use a placeholder in the input box
placeholder: React.PropTypes.string.isRequired,
// callback to handle files pasted into the composer
onFilesPasted: React.PropTypes.func,
},
componentWillMount: function() {
@ -291,8 +295,8 @@ export default React.createClass({
else {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Unknown command",
description: "Usage: /markdown on|off"
title: _t("Unknown command"),
description: _t("Usage") + ": /markdown on|off",
});
}
return;
@ -311,8 +315,8 @@ export default React.createClass({
console.error("Command failure: %s", err);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Server error",
description: ((err && err.message) ? err.message : "Server unavailable, overloaded, or something else went wrong."),
title: _t("Server error"),
description: ((err && err.message) ? err.message : _t("Server unavailable, overloaded, or something else went wrong") + "."),
});
});
}
@ -320,8 +324,8 @@ export default React.createClass({
console.error(cmd.error);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Command error",
description: cmd.error
title: _t("Command error"),
description: cmd.error,
});
}
return;
@ -439,10 +443,27 @@ export default React.createClass({
this.refs.textarea.focus();
},
_onPaste: function(ev) {
const items = ev.clipboardData.items;
const files = [];
for (const item of items) {
if (item.kind === 'file') {
files.push(item.getAsFile());
}
}
if (files.length && this.props.onFilesPasted) {
this.props.onFilesPasted(files);
return true;
}
return false;
},
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} />
<textarea autoFocus ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder={this.props.placeholder}
onPaste={this._onPaste}
/>
</div>
);
}

View file

@ -24,6 +24,8 @@ var sdk = require('../../../index');
var Velociraptor = require('../../../Velociraptor');
require('../../../VelocityBounce');
import DateUtils from '../../../DateUtils';
var bounce = false;
try {
if (global.localStorage) {
@ -63,9 +65,6 @@ 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,
},
getDefaultProps: function() {
@ -170,16 +169,8 @@ 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 = "Seen by " + this.props.member.userId + " at " +
DateUtils.formatDate(new Date(this.props.timestamp));
}
return (

View file

@ -17,7 +17,9 @@ limitations under the License.
'use strict';
var React = require('react');
var classNames = require('classnames');
var sdk = require('../../../index');
import { _t } from '../../../languageHandler';
var MatrixClientPeg = require('../../../MatrixClientPeg');
var Modal = require("../../../Modal");
var dis = require("../../../dispatcher");
@ -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,7 +52,7 @@ module.exports = React.createClass({
getDefaultProps: function() {
return {
editing: false,
onSettingsClick: function() {},
inRoom: false,
onSaveClick: function() {},
};
},
@ -117,8 +120,8 @@ module.exports = React.createClass({
var 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();
},
@ -186,6 +189,9 @@ module.exports = React.createClass({
);
save_button = <AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save</AccessibleButton>;
}
if (this.props.onCancelClick) {
cancel_button = <CancelButton onClick={this.props.onCancelClick}/>;
}
@ -203,7 +209,7 @@ module.exports = React.createClass({
// 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>;
searchStatus = <div className="mx_RoomHeader_searchStatus">&nbsp;{ _t("(~%(searchCount)s results)", { searchCount: this.props.searchInfo.searchCount }) }</div>;
}
// XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'...
@ -218,17 +224,17 @@ module.exports = React.createClass({
}
}
var roomName = 'Join Room';
var 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 element="div" className={emojiTextClasses} title={roomName}>{ roomName }</EmojiText>
{ searchStatus }
</div>;
}
@ -259,7 +265,7 @@ 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 }/>
@ -294,15 +300,23 @@ module.exports = React.createClass({
var forget_button;
if (this.props.onForgetClick) {
forget_button =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title="Forget room">
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title={ _t("Forget room") }>
<TintableSvg src="img/leave.svg" width="26" height="20"/>
</AccessibleButton>;
}
let search_button;
if (this.props.onSearchClick && this.props.inRoom) {
search_button =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title={ _t("Search") }>
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
</AccessibleButton>;
}
var rightPanel_buttons;
if (this.props.collapsedRhs) {
rightPanel_buttons =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title="Show panel">
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title={ _t('Show panel') }>
<TintableSvg src="img/maximise.svg" width="10" height="16"/>
</AccessibleButton>;
}
@ -313,9 +327,7 @@ module.exports = React.createClass({
<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>
{ search_button }
{ rightPanel_buttons }
</div>;
}

View file

@ -17,17 +17,18 @@ limitations under the License.
'use strict';
var React = require("react");
var ReactDOM = require("react-dom");
import { _t } from '../../../languageHandler';
var GeminiScrollbar = require('react-gemini-scrollbar');
var MatrixClientPeg = require("../../../MatrixClientPeg");
var CallHandler = require('../../../CallHandler');
var RoomListSorter = require("../../../RoomListSorter");
var Unread = require('../../../Unread');
var dis = require("../../../dispatcher");
var sdk = require('../../../index');
var rate_limited_func = require('../../../ratelimitedfunc');
var Rooms = require('../../../Rooms');
import DMRoomMap from '../../../utils/DMRoomMap';
var Receipt = require('../../../utils/Receipt');
var constantTimeDispatcher = require('../../../ConstantTimeDispatcher');
var HIDE_CONFERENCE_CHANS = true;
@ -37,19 +38,10 @@ module.exports = React.createClass({
propTypes: {
ConferenceHandler: React.PropTypes.any,
collapsed: React.PropTypes.bool.isRequired,
selectedRoom: React.PropTypes.string,
currentRoom: React.PropTypes.string,
searchFilter: React.PropTypes.string,
},
shouldComponentUpdate: function(nextProps, nextState) {
if (nextProps.collapsed !== this.props.collapsed) return true;
if (nextProps.searchFilter !== this.props.searchFilter) return true;
if (nextState.lists !== this.state.lists ||
nextState.isLoadingLeftRooms !== this.state.isLoadingLeftRooms ||
nextState.incomingCall !== this.state.incomingCall) return true;
return false;
},
getInitialState: function() {
return {
isLoadingLeftRooms: false,
@ -59,6 +51,8 @@ module.exports = React.createClass({
},
componentWillMount: function() {
this.mounted = false;
var cli = MatrixClientPeg.get();
cli.on("Room", this.onRoom);
cli.on("deleteRoom", this.onDeleteRoom);
@ -66,46 +60,23 @@ module.exports = React.createClass({
cli.on("Room.name", this.onRoomName);
cli.on("Room.tags", this.onRoomTags);
cli.on("Room.receipt", this.onRoomReceipt);
cli.on("RoomState.members", this.onRoomStateMember);
cli.on("RoomState.events", this.onRoomStateEvents);
cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("accountData", this.onAccountData);
// lookup for which lists a given roomId is currently in.
this.listsForRoomId = {};
var s = this.getRoomLists();
this.setState(s);
// 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;
},
componentWillReceiveProps: function(nextProps) {
// short-circuit react when the room changes
// to avoid rerendering all the sublists everywhere
if (nextProps.selectedRoom !== this.props.selectedRoom) {
if (this.props.selectedRoom) {
constantTimeDispatcher.dispatch(
"RoomTile.select", this.props.selectedRoom, {}
);
}
constantTimeDispatcher.dispatch(
"RoomTile.select", nextProps.selectedRoom, { selected: true }
);
}
},
componentDidUpdate: function(prevProps, prevState) {
componentDidUpdate: function() {
// Reinitialise the stickyHeaders when the component is updated
this._updateStickyHeaders(true);
this._repositionIncomingCallBox(undefined, false);
@ -131,29 +102,17 @@ module.exports = React.createClass({
}
break;
case 'on_room_read':
// poke the right RoomTile to refresh, using the constantTimeDispatcher
// to avoid each and every RoomTile registering to the 'on_room_read' event
// XXX: if we like the constantTimeDispatcher we might want to dispatch
// directly from TimelinePanel rather than needlessly bouncing via here.
constantTimeDispatcher.dispatch(
"RoomTile.refresh", payload.room.roomId, {}
);
// also have to poke the right list(s)
var lists = this.listsForRoomId[payload.room.roomId];
if (lists) {
lists.forEach(list=>{
constantTimeDispatcher.dispatch(
"RoomSubList.refreshHeader", list, { room: payload.room }
);
});
}
// Force an update because the notif count state is too deep to cause
// an update. This forces the local echo of reading notifs to be
// reflected by the RoomTiles.
this.forceUpdate();
break;
}
},
componentWillUnmount: function() {
this.mounted = false;
dis.unregister(this.dispatcherRef);
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room", this.onRoom);
@ -162,7 +121,7 @@ module.exports = React.createClass({
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags);
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
}
@ -171,14 +130,10 @@ module.exports = React.createClass({
},
onRoom: function(room) {
// XXX: this happens rarely; ideally we should only update the correct
// sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
this._delayedRefreshRoomList();
},
onDeleteRoom: function(roomId) {
// XXX: this happens rarely; ideally we should only update the correct
// sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
this._delayedRefreshRoomList();
},
@ -201,10 +156,6 @@ module.exports = React.createClass({
}
},
_onMouseOver: function(ev) {
this._lastMouseOverTs = Date.now();
},
onSubListHeaderClick: function(isHidden, scrollToPosition) {
// The scroll area has expanded or contracted, so re-calculate sticky headers positions
this._updateStickyHeaders(true, scrollToPosition);
@ -214,98 +165,41 @@ module.exports = React.createClass({
if (toStartOfTimeline) return;
if (!room) return;
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
// rather than regenerate our full roomlists, which is very heavy, we poke the
// correct sublists to just re-sort themselves. This isn't enormously reacty,
// but is much faster than the default react reconciler, or having to do voodoo
// with shouldComponentUpdate and a pleaseRefresh property or similar.
var lists = this.listsForRoomId[room.roomId];
if (lists) {
lists.forEach(list=>{
constantTimeDispatcher.dispatch("RoomSubList.sort", list, { room: room });
});
}
// we have to explicitly hit the roomtile which just changed
constantTimeDispatcher.dispatch(
"RoomTile.refresh", room.roomId, {}
);
this._delayedRefreshRoomList();
},
onRoomReceipt: function(receiptEvent, room) {
// because if we read a notification, it will affect notification count
// only bother updating if there's a receipt from us
if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) {
var lists = this.listsForRoomId[room.roomId];
if (lists) {
lists.forEach(list=>{
constantTimeDispatcher.dispatch(
"RoomSubList.refreshHeader", list, { room: room }
);
});
}
// we have to explicitly hit the roomtile which just changed
constantTimeDispatcher.dispatch(
"RoomTile.refresh", room.roomId, {}
);
this._delayedRefreshRoomList();
}
},
onRoomName: function(room) {
constantTimeDispatcher.dispatch(
"RoomTile.refresh", room.roomId, {}
);
},
onRoomTags: function(event, room) {
// XXX: this happens rarely; ideally we should only update the correct
// sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
this._delayedRefreshRoomList();
},
onRoomStateMember: function(ev, state, member) {
if (ev.getStateKey() === MatrixClientPeg.get().credentials.userId &&
ev.getPrevContent() && ev.getPrevContent().membership === "invite")
{
this._delayedRefreshRoomList();
}
else {
constantTimeDispatcher.dispatch(
"RoomTile.refresh", member.roomId, {}
);
}
onRoomTags: function(event, room) {
this._delayedRefreshRoomList();
},
onRoomStateEvents: function(ev, state) {
this._delayedRefreshRoomList();
},
onRoomMemberName: function(ev, member) {
constantTimeDispatcher.dispatch(
"RoomTile.refresh", member.roomId, {}
);
this._delayedRefreshRoomList();
},
onAccountData: function(ev) {
if (ev.getType() == 'm.direct') {
// XXX: this happens rarely; ideally we should only update the correct
// sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
this._delayedRefreshRoomList();
}
else if (ev.getType() == 'm.push_rules') {
this._delayedRefreshRoomList();
}
},
_delayedRefreshRoomList: new rate_limited_func(function() {
// if the mouse has been moving over the RoomList in the last 500ms
// then delay the refresh further to avoid bouncing around under the
// cursor
if (Date.now() - this._lastMouseOverTs > 500 || this._delayedRefreshRoomListLoopCount > 60) {
this.refreshRoomList();
this._delayedRefreshRoomListLoopCount = 0;
}
else {
this._delayedRefreshRoomListLoopCount++;
this._delayedRefreshRoomList();
}
this.refreshRoomList();
}, 500),
refreshRoomList: function() {
@ -313,12 +207,14 @@ module.exports = React.createClass({
// (!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.
// 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();
},
@ -333,26 +229,18 @@ module.exports = React.createClass({
s.lists["m.lowpriority"] = [];
s.lists["im.vector.fake.archived"] = [];
this.listsForRoomId = {};
var otherTagNames = {};
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
MatrixClientPeg.get().getRooms().forEach(function(room) {
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (!me) return;
// console.log("room = " + room.name + ", me.membership = " + me.membership +
// ", sender = " + me.events.member.getSender() +
// ", target = " + me.events.member.getStateKey() +
// ", prevMembership = " + me.events.member.getPrevContent().membership);
if (!self.listsForRoomId[room.roomId]) {
self.listsForRoomId[room.roomId] = [];
}
if (me.membership == "invite") {
self.listsForRoomId[room.roomId].push("im.vector.fake.invite");
s.lists["im.vector.fake.invite"].push(room);
}
else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) {
@ -363,27 +251,23 @@ module.exports = React.createClass({
{
// Used to split rooms via tags
var tagNames = Object.keys(room.tags);
if (tagNames.length) {
for (var i = 0; i < tagNames.length; i++) {
var tagName = tagNames[i];
s.lists[tagName] = s.lists[tagName] || [];
s.lists[tagName].push(room);
self.listsForRoomId[room.roomId].push(tagName);
otherTagNames[tagName] = 1;
s.lists[tagNames[i]].push(room);
}
}
else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
// "Direct Message" rooms (that we're still in and that aren't otherwise tagged)
self.listsForRoomId[room.roomId].push("im.vector.fake.direct");
s.lists["im.vector.fake.direct"].push(room);
}
else {
self.listsForRoomId[room.roomId].push("im.vector.fake.recent");
s.lists["im.vector.fake.recent"].push(room);
}
}
else if (me.membership === "leave") {
self.listsForRoomId[room.roomId].push("im.vector.fake.archived");
s.lists["im.vector.fake.archived"].push(room);
}
else {
@ -404,10 +288,8 @@ module.exports = React.createClass({
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (me && Rooms.looksLikeDirectMessageRoom(room, me)) {
self.listsForRoomId[room.roomId].push("im.vector.fake.direct");
s.lists["im.vector.fake.direct"].push(room);
} else {
self.listsForRoomId[room.roomId].push("im.vector.fake.recent");
s.lists["im.vector.fake.recent"].push(room);
}
}
@ -424,8 +306,6 @@ module.exports = React.createClass({
newMDirectEvent[otherPerson.userId] = roomList;
}
console.warn("Resetting room DM state to be " + JSON.stringify(newMDirectEvent));
// 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();
}
@ -434,32 +314,19 @@ module.exports = React.createClass({
// we actually apply the sorting to this when receiving the prop in RoomSubLists.
// 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 s;
},
_getScrollNode: function() {
if (!this.mounted) return null;
var panel = ReactDOM.findDOMNode(this);
if (!panel) return null;
// empirically, if we have gm-prevented for some reason, the scroll node
// is still the 3rd child (i.e. the view child). This looks to be due
// to vdh's improved resize updater logic...?
return panel.children[2]; // XXX: Fragile!
if (panel.classList.contains('gm-prevented')) {
return panel;
} else {
return panel.children[2]; // XXX: Fragile!
}
},
_whenScrolling: function(e) {
@ -479,6 +346,7 @@ 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;
@ -502,10 +370,11 @@ 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;
// Use the offset of the top of the component from the window
// Use the offset of the top of the componet from the window
// as this is used to calculate the CSS fixed top position for the stickies
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
@ -602,75 +471,72 @@ module.exports = React.createClass({
render: function() {
var RoomSubList = sdk.getComponent('structures.RoomSubList');
var self = this;
return (
<GeminiScrollbar className="mx_RoomList_scrollbar"
autoshow={true} onScroll={ self._whenScrolling } onResize={ self._whenScrolling } ref="gemscroll">
<div className="mx_RoomList" onMouseOver={ this._onMouseOver }>
autoshow={true} onScroll={ self._whenScrolling } ref="gemscroll">
<div className="mx_RoomList">
<RoomSubList list={ self.state.lists['im.vector.fake.invite'] }
label="Invites"
tagName="im.vector.fake.invite"
label={ _t('Invites') }
editable={ false }
order="recent"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } />
<RoomSubList list={ self.state.lists['m.favourite'] }
label="Favourites"
label={ _t('Favourites') }
tagName="m.favourite"
verb="favourite"
verb={ _t('to favourite') }
editable={ true }
order="manual"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick }
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"
verb={ _t('to tag direct chat') }
editable={ true }
order="recent"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
alwaysShowHeader={ true }
searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } />
<RoomSubList list={ self.state.lists['im.vector.fake.recent'] }
label="Rooms"
tagName="im.vector.fake.recent"
label={ _t('Rooms') }
editable={ true }
verb="restore"
verb={ _t('to restore') }
order="recent"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } />
{ Object.keys(self.state.lists).sort().map(function(tagName) {
{ Object.keys(self.state.lists).map(function(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 }
verb={ _t('to tag as %(tagName)s', {tagName: tagName}) }
editable={ true }
order="manual"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } />;
@ -679,25 +545,24 @@ module.exports = React.createClass({
}) }
<RoomSubList list={ self.state.lists['m.lowpriority'] }
label="Low priority"
label={ _t('Low priority') }
tagName="m.lowpriority"
verb="demote"
verb={ _t('to demote') }
editable={ true }
order="recent"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } />
<RoomSubList list={ self.state.lists['im.vector.fake.archived'] }
label="Historical"
tagName="im.vector.fake.archived"
label={ _t('Historical') }
editable={ false }
order="recent"
collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
collapsed={ self.props.collapsed }
alwaysShowHeader={ true }
startAsHidden={ true }
showSpinner={ self.state.isLoadingLeftRooms }

View file

@ -21,6 +21,8 @@ var React = require('react');
var sdk = require('../../../index');
var MatrixClientPeg = require('../../../MatrixClientPeg');
import { _t } 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() {
@ -84,7 +86,7 @@ module.exports = React.createClass({
_roomNameElement: function(fallback) {
fallback = fallback || '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() {
@ -128,13 +130,14 @@ module.exports = React.createClass({
</div>;
}
}
// TODO: find a way to respect HTML in counterpart!
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?
{ _t('Would you like to') } <a onClick={ this.props.onJoinClick }>{ _t('accept') }</a> { _t('or') } <a onClick={ this.props.onRejectClick }>{ _t('decline') }</a> { _t('this invitation?') }
</div>
{emailMatchBlock}
</div>
@ -186,8 +189,8 @@ 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/>
<a onClick={ this.props.onJoinClick }><b>{ _t('Click here') }</b></a> { _t('to join the discussion') }!
</div>
</div>
);
@ -196,7 +199,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

@ -17,6 +17,7 @@ limitations under the License.
import q from 'q';
import React from 'react';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import SdkConfig from '../../../SdkConfig';
import sdk from '../../../index';
@ -56,8 +57,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();
},
@ -70,7 +71,7 @@ const BannedUser = React.createClass({
<AccessibleButton className="mx_RoomSettings_unbanButton"
onClick={this._onUnbanClick}
>
Unban
{ _t('Unban') }
</AccessibleButton>
{this.props.member.userId}
</li>
@ -400,13 +401,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({
@ -523,11 +524,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 }),
});
});
},
@ -537,14 +538,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=>{
@ -572,7 +573,7 @@ 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 &&
@ -582,7 +583,7 @@ module.exports = React.createClass({
<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!)
{ _t('Enable encryption') } { _t('(warning: cannot be disabled again!)') }
</label>
{ settings }
</div>
@ -596,7 +597,7 @@ module.exports = React.createClass({
? <img className="mx_RoomSettings_e2eIcon" src="img/e2e-verified.svg" width="10" height="12" />
: <img className="mx_RoomSettings_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12" />
}
Encryption is { isEncrypted ? "" : "not " } enabled in this room.
{ isEncrypted ? "Encryption is enabled in this room" : "Encryption is not enabled in this room" }.
</label>
{ settings }
</div>
@ -647,12 +648,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}/>
{ user } { _t('is a') } <PowerSelector value={ user_levels[user] } disabled={true}/>
</li>
);
})}
@ -660,7 +661,7 @@ 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");
@ -668,7 +669,7 @@ module.exports = React.createClass({
if (banned.length) {
bannedUsersSection =
<div>
<h3>Banned users</h3>
<h3>{ _t('Banned users') }</h3>
<ul className="mx_RoomSettings_banned">
{banned.map(function(member) {
return (
@ -683,7 +684,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>
);
}
@ -694,14 +695,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>
);
}
@ -711,8 +712,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) {
@ -753,7 +754,7 @@ 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>.
{ _t('To link to a room it must have') } <a href="#addresses"> { _t('an address') }</a>.
</div>;
}
@ -761,10 +762,10 @@ 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>;
}
@ -776,7 +777,7 @@ module.exports = React.createClass({
console.error(this.state.scalar_error);
integrationsError = (
<span className="mx_RoomSettings_integrationsButton_errorPopup">
Could not connect to the integration server
{ _t('Could not connect to the integration server') }
</span>
);
}
@ -784,7 +785,7 @@ module.exports = React.createClass({
if (this.scalarClient.hasCredentials()) {
integrationsButton = (
<div className="mx_RoomSettings_integrationsButton" onClick={ this.onManageIntegrations }>
Manage Integrations
{ _t('Manage Integrations') }
</div>
);
} else if (this.state.scalar_error) {
@ -797,7 +798,7 @@ module.exports = React.createClass({
} else {
integrationsButton = (
<div className="mx_RoomSettings_integrationsButton" style={{opacity: 0.5}}>
Manage Integrations
{ _t('Manage Integrations') }
</div>
);
}
@ -813,28 +814,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/>
@ -847,7 +848,7 @@ module.exports = React.createClass({
</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) }
@ -860,28 +861,28 @@ module.exports = React.createClass({
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>
@ -899,41 +900,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 remove 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>
);
@ -946,9 +947,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

@ -27,8 +27,6 @@ var RoomNotifs = require('../../../RoomNotifs');
var FormattingUtils = require('../../../utils/FormattingUtils');
import AccessibleButton from '../elements/AccessibleButton';
var UserSettingsStore = require('../../../UserSettingsStore');
var constantTimeDispatcher = require('../../../ConstantTimeDispatcher');
var Unread = require('../../../Unread');
module.exports = React.createClass({
displayName: 'RoomTile',
@ -38,10 +36,12 @@ module.exports = React.createClass({
connectDropTarget: React.PropTypes.func,
onClick: React.PropTypes.func,
isDragging: React.PropTypes.bool,
selectedRoom: React.PropTypes.string,
room: React.PropTypes.object.isRequired,
collapsed: React.PropTypes.bool.isRequired,
selected: React.PropTypes.bool.isRequired,
unread: React.PropTypes.bool.isRequired,
highlight: React.PropTypes.bool.isRequired,
isInvite: React.PropTypes.bool.isRequired,
incomingCall: React.PropTypes.object,
},
@ -54,11 +54,10 @@ module.exports = React.createClass({
getInitialState: function() {
return({
hover: false,
badgeHover: false,
hover : false,
badgeHover : false,
menuDisplayed: false,
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
selected: this.props.room ? (this.props.selectedRoom === this.props.room.roomId) : false,
});
},
@ -80,32 +79,23 @@ module.exports = React.createClass({
}
},
onAccountData: function(accountDataEvent) {
if (accountDataEvent.getType() == 'm.push_rules') {
this.setState({
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
});
}
},
componentWillMount: function() {
constantTimeDispatcher.register("RoomTile.refresh", this.props.room.roomId, this.onRefresh);
constantTimeDispatcher.register("RoomTile.select", this.props.room.roomId, this.onSelect);
this.onRefresh();
MatrixClientPeg.get().on("accountData", this.onAccountData);
},
componentWillUnmount: function() {
constantTimeDispatcher.unregister("RoomTile.refresh", this.props.room.roomId, this.onRefresh);
constantTimeDispatcher.unregister("RoomTile.select", this.props.room.roomId, this.onSelect);
},
componentWillReceiveProps: function(nextProps) {
this.onRefresh();
},
onRefresh: function(params) {
this.setState({
unread: Unread.doesRoomHaveUnreadMessages(this.props.room),
highlight: this.props.room.getUnreadNotificationCount('highlight') > 0 || this.props.isInvite,
});
},
onSelect: function(params) {
this.setState({
selected: params.selected,
});
var cli = MatrixClientPeg.get();
if (cli) {
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
}
},
onClick: function(ev) {
@ -179,13 +169,13 @@ module.exports = React.createClass({
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge();
const mentionBadges = this.state.highlight && this._shouldShowMentionBadge();
const mentionBadges = this.props.highlight && this._shouldShowMentionBadge();
const badges = notifBadges || mentionBadges;
var classes = classNames({
'mx_RoomTile': true,
'mx_RoomTile_selected': this.state.selected,
'mx_RoomTile_unread': this.state.unread,
'mx_RoomTile_selected': this.props.selected,
'mx_RoomTile_unread': this.props.unread,
'mx_RoomTile_unreadNotify': notifBadges,
'mx_RoomTile_highlight': mentionBadges,
'mx_RoomTile_invited': (me && me.membership == 'invite'),
@ -231,7 +221,7 @@ module.exports = React.createClass({
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed,
});
if (this.state.selected) {
if (this.props.selected) {
let nameSelected = <EmojiText>{name}</EmojiText>;
label = <div title={ name } className={ nameClasses }>{ nameSelected }</div>;
@ -265,8 +255,7 @@ module.exports = React.createClass({
let ret = (
<div> { /* Only native elements can be wrapped in a DnD object. */}
<AccessibleButton className={classes} tabIndex="0" onClick={this.onClick}
onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<AccessibleButton className={classes} tabIndex="0" onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<div className={avatarClasses}>
<div className="mx_RoomTile_avatar_container">
<RoomAvatar room={this.props.room} width={24} height={24} />

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,9 @@ 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" : "") + "...";
var text = (overflowCount > 1)
? _t("and %(overflowCount)s others...", { overflowCount: overflowCount })
: _t("and one other...");
return (
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
<BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} />
@ -137,7 +138,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

@ -19,6 +19,7 @@ limitations under the License.
import React from 'react';
import dis from '../../../dispatcher';
import AccessibleButton from '../elements/AccessibleButton';
import sdk from '../../../index';
// cancel button which is shared between room header and simple room header
export function CancelButton(props) {
@ -45,6 +46,9 @@ export default React.createClass({
// is the RightPanel collapsed?
collapsedRhs: React.PropTypes.bool,
// `src` to a TintableSvg. Optional.
icon: React.PropTypes.string,
},
onShowRhsClick: function(ev) {
@ -53,9 +57,17 @@ export default React.createClass({
render: function() {
let cancelButton;
let icon;
if (this.props.onCancelClick) {
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
}
if (this.props.icon) {
const TintableSvg = sdk.getComponent('elements.TintableSvg');
icon = <TintableSvg
className="mx_RoomHeader_icon" src={this.props.icon}
width="25" height="25"
/>;
}
let showRhsButton;
/* // don't bother cluttering things up with this for now.
@ -73,6 +85,7 @@ export default React.createClass({
<div className="mx_RoomHeader" >
<div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_simpleHeader">
{ icon }
{ this.props.title }
{ showRhsButton }
{ cancelButton }

View file

@ -18,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({
@ -34,11 +35,11 @@ module.exports = React.createClass({
<div className="mx_TopUnreadMessagesBar_scrollUp"
onClick={this.props.onScrollUpClick}>
<img src="img/scrollto.svg" width="24" height="24"
alt="Scroll to unread messages"
title="Scroll to unread messages"/>
alt={ _t('Scroll to unread messages') }
title={ _t('Scroll to unread messages') }/>
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"
onClick={this.props.onCloseClick} />
@ -46,4 +47,3 @@ module.exports = React.createClass({
);
},
});

View file

@ -15,13 +15,13 @@ 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 Modal from '../../../Modal';
export default WithMatrixClient(React.createClass({
displayName: 'AddPhoneNumber',
@ -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,17 +146,19 @@ 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}
/>

View file

@ -21,6 +21,7 @@ var MatrixClientPeg = require("../../../MatrixClientPeg");
var Modal = require("../../../Modal");
var sdk = require("../../../index");
import AccessibleButton from '../elements/AccessibleButton';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'ChangePassword',
@ -47,11 +48,11 @@ 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")
};
}
}
@ -69,19 +70,21 @@ module.exports = React.createClass({
var 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) => {
@ -150,7 +153,7 @@ module.exports = React.createClass({
<div className={this.props.className}>
<div className={rowClassName}>
<div className={rowLabelClassName}>
<label htmlFor="passwordold">Current password</label>
<label htmlFor="passwordold">{ _t('Current password') }</label>
</div>
<div className={rowInputClassName}>
<input id="passwordold" type="password" ref="old_input" />
@ -158,7 +161,7 @@ module.exports = React.createClass({
</div>
<div className={rowClassName}>
<div className={rowLabelClassName}>
<label htmlFor="password1">New password</label>
<label htmlFor="password1">{ _t('New password') }</label>
</div>
<div className={rowInputClassName}>
<input id="password1" type="password" ref="new_input" />
@ -166,7 +169,7 @@ module.exports = React.createClass({
</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" />
@ -174,7 +177,7 @@ module.exports = React.createClass({
</div>
<AccessibleButton className={buttonClassName}
onClick={this.onClickChange}>
Change Password
{ _t('Change Password') }
</AccessibleButton>
</div>
);

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import DateUtils from '../../../DateUtils';
@ -48,7 +49,7 @@ export default class DevicesPanelEntry extends React.Component {
display_name: value,
}).catch((e) => {
console.error("Error setting device display name", e);
throw new Error("Failed to set display name");
throw new Error(_t("Failed to set display name"));
});
}
@ -71,6 +72,7 @@ export default class DevicesPanelEntry extends React.Component {
var InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
Modal.createDialog(InteractiveAuthDialog, {
title: _t("Authentication"),
matrixClient: MatrixClientPeg.get(),
authData: error.data,
makeRequest: this._makeDeleteRequest,
@ -84,7 +86,7 @@ export default class DevicesPanelEntry extends React.Component {
if (this._unmounted) { return; }
this.setState({
deleting: false,
deleteError: "Failed to delete device",
deleteError: _t("Failed to delete device"),
});
}).done();
}
@ -132,7 +134,7 @@ export default class DevicesPanelEntry extends React.Component {
deleteButton = (
<div className="mx_textButton"
onClick={this._onDeleteClick}>
Delete
{ _t("Delete") }
</div>
);
}