Merge branch 'develop' into rob/apps
This commit is contained in:
commit
9e0b476b72
208 changed files with 16199 additions and 3028 deletions
|
@ -16,15 +16,15 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require("react");
|
||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
var PresetValues = {
|
||||
import React from 'react';
|
||||
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 +231,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 occurred: %(error_string)s', {error_string: this.state.error_string})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -246,29 +246,29 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<div className="mx_CreateRoom">
|
||||
<SimpleRoomHeader title="CreateRoom" collapsedRhs={ this.props.collapsedRhs }/>
|
||||
<SimpleRoomHeader title={_t("Create Room")} 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>
|
||||
|
|
|
@ -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, _tJsx } 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,24 @@ 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">
|
||||
{_tJsx("You must <a>register</a> to use this functionality", /<a>(.*?)<\/a>/, (sub) => <a href="#/register" key="sub">{sub}</a>)}
|
||||
</div>
|
||||
</div>;
|
||||
} else if (this.noRoom) {
|
||||
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
|
||||
<div className="mx_RoomView_empty">{_t("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 +117,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')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,8 +19,6 @@ const InteractiveAuth = Matrix.InteractiveAuth;
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import sdk from '../../index';
|
||||
|
||||
import {getEntryComponentForLoginType} from '../views/login/InteractiveAuthEntryComponents';
|
||||
|
||||
export default React.createClass({
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,11 +18,15 @@ limitations under the License.
|
|||
import * as Matrix from 'matrix-js-sdk';
|
||||
import React from 'react';
|
||||
|
||||
import UserSettingsStore from '../../UserSettingsStore';
|
||||
import KeyCode from '../../KeyCode';
|
||||
import Notifier from '../../Notifier';
|
||||
import PageTypes from '../../PageTypes';
|
||||
import CallMediaHandler from '../../CallMediaHandler';
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import sessionStore from '../../stores/SessionStore';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
|
||||
/**
|
||||
* This is what our MatrixChat shows when we are logged in. The precise view is
|
||||
|
@ -38,10 +43,13 @@ export default React.createClass({
|
|||
propTypes: {
|
||||
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
||||
page_type: React.PropTypes.string.isRequired,
|
||||
onRoomIdResolved: React.PropTypes.func,
|
||||
onRoomCreated: React.PropTypes.func,
|
||||
onUserSettingsClose: React.PropTypes.func,
|
||||
|
||||
// Called with the credentials of a registered user (if they were a ROU that
|
||||
// transitioned to PWLU)
|
||||
onRegistered: React.PropTypes.func,
|
||||
|
||||
teamToken: React.PropTypes.string,
|
||||
|
||||
// and lots and lots of other stuff.
|
||||
|
@ -62,6 +70,13 @@ export default React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
// use compact timeline view
|
||||
useCompactLayout: UserSettingsStore.getSyncedSetting('useCompactLayout'),
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// stash the MatrixClient in case we log out before we are unmounted
|
||||
this._matrixClient = this.props.matrixClient;
|
||||
|
@ -70,11 +85,35 @@ export default React.createClass({
|
|||
// RoomView.getScrollState()
|
||||
this._scrollStateMap = {};
|
||||
|
||||
CallMediaHandler.loadDevices();
|
||||
|
||||
document.addEventListener('keydown', this._onKeyDown);
|
||||
|
||||
this._sessionStore = sessionStore;
|
||||
this._sessionStoreToken = this._sessionStore.addListener(
|
||||
this._setStateFromSessionStore,
|
||||
);
|
||||
this._setStateFromSessionStore();
|
||||
|
||||
this._matrixClient.on("accountData", this.onAccountData);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
document.removeEventListener('keydown', this._onKeyDown);
|
||||
this._matrixClient.removeListener("accountData", this.onAccountData);
|
||||
if (this._sessionStoreToken) {
|
||||
this._sessionStoreToken.remove();
|
||||
}
|
||||
},
|
||||
|
||||
// Child components assume that the client peg will not be null, so give them some
|
||||
// sort of assurance here by only allowing a re-render if the client is truthy.
|
||||
//
|
||||
// This is required because `LoggedInView` maintains its own state and if this state
|
||||
// updates after the client peg has been made null (during logout), then it will
|
||||
// attempt to re-render and the children will throw errors.
|
||||
shouldComponentUpdate: function() {
|
||||
return Boolean(MatrixClientPeg.get());
|
||||
},
|
||||
|
||||
getScrollStateForRoom: function(roomId) {
|
||||
|
@ -88,6 +127,20 @@ export default React.createClass({
|
|||
return this.refs.roomView.canResetTimeline();
|
||||
},
|
||||
|
||||
_setStateFromSessionStore() {
|
||||
this.setState({
|
||||
userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()),
|
||||
});
|
||||
},
|
||||
|
||||
onAccountData: function(event) {
|
||||
if (event.getType() === "im.vector.web.settings") {
|
||||
this.setState({
|
||||
useCompactLayout: event.getContent().useCompactLayout,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_onKeyDown: function(ev) {
|
||||
/*
|
||||
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
|
||||
|
@ -108,7 +161,7 @@ export default React.createClass({
|
|||
switch (ev.keyCode) {
|
||||
case KeyCode.UP:
|
||||
case KeyCode.DOWN:
|
||||
if (ev.altKey) {
|
||||
if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
|
||||
var action = ev.keyCode == KeyCode.UP ?
|
||||
'view_prev_room' : 'view_next_room';
|
||||
dis.dispatch({action: action});
|
||||
|
@ -118,13 +171,15 @@ export default React.createClass({
|
|||
|
||||
case KeyCode.PAGE_UP:
|
||||
case KeyCode.PAGE_DOWN:
|
||||
this._onScrollKeyPressed(ev);
|
||||
handled = true;
|
||||
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
this._onScrollKeyPressed(ev);
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyCode.HOME:
|
||||
case KeyCode.END:
|
||||
if (ev.ctrlKey) {
|
||||
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
this._onScrollKeyPressed(ev);
|
||||
handled = true;
|
||||
}
|
||||
|
@ -142,42 +197,44 @@ export default React.createClass({
|
|||
if (this.refs.roomView) {
|
||||
this.refs.roomView.handleScrollKey(ev);
|
||||
}
|
||||
else if (this.refs.roomDirectory) {
|
||||
this.refs.roomDirectory.handleScrollKey(ev);
|
||||
}
|
||||
},
|
||||
|
||||
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 NewVersionBar = sdk.getComponent('globals.NewVersionBar');
|
||||
const PasswordNagBar = sdk.getComponent('globals.PasswordNagBar');
|
||||
|
||||
var page_element;
|
||||
var right_panel = '';
|
||||
let page_element;
|
||||
let right_panel = '';
|
||||
|
||||
switch (this.props.page_type) {
|
||||
case PageTypes.RoomView:
|
||||
page_element = <RoomView
|
||||
ref='roomView'
|
||||
roomAddress={this.props.currentRoomAlias || this.props.currentRoomId}
|
||||
autoJoin={this.props.autoJoin}
|
||||
onRoomIdResolved={this.props.onRoomIdResolved}
|
||||
onRegistered={this.props.onRegistered}
|
||||
eventId={this.props.initialEventId}
|
||||
thirdPartyInvite={this.props.thirdPartyInvite}
|
||||
oobData={this.props.roomOobData}
|
||||
highlightedEventId={this.props.highlightedEventId}
|
||||
eventPixelOffset={this.props.initialEventPixelOffset}
|
||||
key={this.props.currentRoomAlias || this.props.currentRoomId}
|
||||
key={this.props.currentRoomId || 'roomview'}
|
||||
opacity={this.props.middleOpacity}
|
||||
collapsedRhs={this.props.collapse_rhs}
|
||||
ConferenceHandler={this.props.ConferenceHandler}
|
||||
scrollStateMap={this._scrollStateMap}
|
||||
/>;
|
||||
if (!this.props.collapse_rhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.sideOpacity} />;
|
||||
if (!this.props.collapse_rhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.rightOpacity} />;
|
||||
break;
|
||||
|
||||
case PageTypes.UserSettings:
|
||||
|
@ -189,7 +246,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:
|
||||
|
@ -197,42 +254,46 @@ 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
|
||||
collapsedRhs={this.props.collapse_rhs}
|
||||
ref="roomDirectory"
|
||||
config={this.props.config.roomDirectory}
|
||||
/>;
|
||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
|
||||
break;
|
||||
|
||||
case PageTypes.HomePage:
|
||||
// If team server config is present, pass the teamServerURL. props.teamToken
|
||||
// must also be set for the team page to be displayed, otherwise the
|
||||
// welcomePageUrl is used (which might be undefined).
|
||||
const teamServerUrl = this.props.config.teamServerConfig ?
|
||||
this.props.config.teamServerConfig.teamServerURL : null;
|
||||
|
||||
page_element = <HomePage
|
||||
collapsedRhs={this.props.collapse_rhs}
|
||||
teamServerUrl={this.props.config.teamServerConfig.teamServerURL}
|
||||
teamServerUrl={teamServerUrl}
|
||||
teamToken={this.props.teamToken}
|
||||
/>
|
||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>
|
||||
homePageUrl={this.props.config.welcomePageUrl}
|
||||
/>;
|
||||
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;
|
||||
}
|
||||
|
||||
const isGuest = this.props.matrixClient.isGuest();
|
||||
var topBar;
|
||||
if (this.props.hasNewVersion) {
|
||||
topBar = <NewVersionBar version={this.props.version} newVersion={this.props.newVersion}
|
||||
releaseNotes={this.props.newVersionReleaseNotes}
|
||||
/>;
|
||||
}
|
||||
else if (this.props.matrixClient.isGuest()) {
|
||||
topBar = <GuestWarningBar />;
|
||||
}
|
||||
else if (Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) {
|
||||
} else if (this.state.userHasGeneratedPassword) {
|
||||
topBar = <PasswordNagBar />;
|
||||
} else if (!isGuest && Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) {
|
||||
topBar = <MatrixToolbar />;
|
||||
}
|
||||
|
||||
|
@ -240,6 +301,9 @@ export default React.createClass({
|
|||
if (topBar) {
|
||||
bodyClasses += ' mx_MatrixChat_toolbarShowing';
|
||||
}
|
||||
if (this.state.useCompactLayout) {
|
||||
bodyClasses += ' mx_MatrixChat_useCompactLayout';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mx_MatrixChat_wrapper'>
|
||||
|
@ -248,8 +312,7 @@ export default React.createClass({
|
|||
<LeftPanel
|
||||
selectedRoom={this.props.currentRoomId}
|
||||
collapsed={this.props.collapse_lhs || false}
|
||||
opacity={this.props.sideOpacity}
|
||||
teamToken={this.props.teamToken}
|
||||
opacity={this.props.leftOpacity}
|
||||
/>
|
||||
<main className='mx_MatrixChat_middlePanel'>
|
||||
{page_element}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -84,6 +84,15 @@ 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,
|
||||
|
||||
// hide redacted events as per old behaviour
|
||||
hideRedactions: React.PropTypes.bool,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
|
@ -230,8 +239,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 +288,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) &&
|
||||
|
@ -311,7 +319,7 @@ module.exports = React.createClass({
|
|||
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
|
||||
|
||||
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
|
||||
let dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1}/></li>;
|
||||
let dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} showTwelveHour={this.props.isTwelveHour}/></li>;
|
||||
ret.push(dateSeparator);
|
||||
}
|
||||
|
||||
|
@ -334,6 +342,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 +363,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;
|
||||
}
|
||||
|
||||
|
@ -388,6 +403,8 @@ module.exports = React.createClass({
|
|||
isVisibleReadMarker = visible;
|
||||
}
|
||||
|
||||
// XXX: there should be no need for a ghost tile - we should just use a
|
||||
// a dispatch (user_activity_end) to start the RM animation.
|
||||
if (eventId == this.currentGhostEventId) {
|
||||
// if we're showing an animation, continue to show it.
|
||||
ret.push(this._getReadMarkerGhostTile());
|
||||
|
@ -405,8 +422,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?
|
||||
|
@ -444,11 +461,13 @@ module.exports = React.createClass({
|
|||
|
||||
// do we need a date separator since the last event?
|
||||
if (this._wantsDateSeparator(prevEvent, eventDate)) {
|
||||
var dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1}/></li>;
|
||||
var dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} showTwelveHour={this.props.isTwelveHour}/></li>;
|
||||
ret.push(dateSeparator);
|
||||
continuation = false;
|
||||
}
|
||||
|
||||
if (mxEv.isRedacted() && this.props.hideRedactions) return ret;
|
||||
|
||||
var eventId = mxEv.getId();
|
||||
var highlight = (eventId == this.props.highlightedEventId);
|
||||
|
||||
|
@ -460,11 +479,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}
|
||||
|
@ -474,6 +492,7 @@ module.exports = React.createClass({
|
|||
checkUnmounting={this._isUnmounting}
|
||||
eventSendStatus={mxEv.status}
|
||||
tileShape={this.props.tileShape}
|
||||
isTwelveHour={this.props.isTwelveHour}
|
||||
last={last} isSelectedEvent={highlight}/>
|
||||
</li>
|
||||
);
|
||||
|
@ -607,8 +626,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 }
|
||||
|
|
|
@ -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') }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,12 +14,12 @@ 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, _tJsx } from '../../languageHandler';
|
||||
import sdk from '../../index';
|
||||
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 +175,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 +250,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 +266,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>
|
||||
|
@ -281,15 +281,13 @@ module.exports = React.createClass({
|
|||
{ this.props.unsentMessageError }
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||
<a className="mx_RoomStatusBar_resend_link"
|
||||
onClick={ this.props.onResendAllClick }>
|
||||
Resend all
|
||||
</a> 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.
|
||||
{_tJsx("<a>Resend all</a> or <a>cancel all</a> now. You can also select individual messages to resend or cancel.",
|
||||
[/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/],
|
||||
[
|
||||
(sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={ this.props.onResendAllClick }>{sub}</a>,
|
||||
(sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={ this.props.onCancelAllClick }>{sub}</a>,
|
||||
]
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -298,8 +296,8 @@ module.exports = React.createClass({
|
|||
// unread count trumps who is typing since the unread count is only
|
||||
// set when you've scrolled up
|
||||
if (this.props.numUnreadMessages) {
|
||||
var unreadMsgs = this.props.numUnreadMessages + " new message" +
|
||||
(this.props.numUnreadMessages > 1 ? "s" : "");
|
||||
// MUST use var name "count" for pluralization to kick in
|
||||
var unreadMsgs = _t("%(count)s new messages", {count: this.props.numUnreadMessages});
|
||||
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_unreadMessagesBar"
|
||||
|
@ -324,7 +322,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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -25,7 +25,9 @@ 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");
|
||||
var ContentMessages = require("../../ContentMessages");
|
||||
var Modal = require("../../Modal");
|
||||
|
@ -43,7 +45,9 @@ import KeyCode from '../../KeyCode';
|
|||
|
||||
import UserProvider from '../../autocomplete/UserProvider';
|
||||
|
||||
const DEBUG = false;
|
||||
import RoomViewStore from '../../stores/RoomViewStore';
|
||||
|
||||
var DEBUG = false;
|
||||
|
||||
if (DEBUG) {
|
||||
// using bind means that we get to keep useful line numbers in the console
|
||||
|
@ -57,16 +61,9 @@ module.exports = React.createClass({
|
|||
propTypes: {
|
||||
ConferenceHandler: React.PropTypes.any,
|
||||
|
||||
// Either a room ID or room alias for the room to display.
|
||||
// If the room is being displayed as a result of the user clicking
|
||||
// on a room alias, the alias should be supplied. Otherwise, a room
|
||||
// ID should be supplied.
|
||||
roomAddress: React.PropTypes.string.isRequired,
|
||||
|
||||
// If a room alias is passed to roomAddress, a function can be
|
||||
// provided here that will be called with the ID of the room
|
||||
// once it has been resolved.
|
||||
onRoomIdResolved: React.PropTypes.func,
|
||||
// Called with the credentials of a registered user (if they were a ROU that
|
||||
// transitioned to PWLU)
|
||||
onRegistered: React.PropTypes.func,
|
||||
|
||||
// An object representing a third party invite to join this room
|
||||
// Fields:
|
||||
|
@ -124,6 +121,9 @@ module.exports = React.createClass({
|
|||
roomId: null,
|
||||
userId: null,
|
||||
roomLoading: true,
|
||||
peekLoading: false,
|
||||
|
||||
forwardingEvent: null,
|
||||
editingRoomSettings: false,
|
||||
uploadingRoomSettings: false,
|
||||
numUnreadMessages: 0,
|
||||
|
@ -173,39 +173,25 @@ module.exports = React.createClass({
|
|||
},
|
||||
});
|
||||
|
||||
if (this.props.roomAddress[0] == '#') {
|
||||
// we always look up the alias from the directory server:
|
||||
// we want the room that the given alias is pointing to
|
||||
// right now. We may have joined that alias before but there's
|
||||
// no guarantee the alias hasn't subsequently been remapped.
|
||||
MatrixClientPeg.get().getRoomIdForAlias(this.props.roomAddress).done((result) => {
|
||||
if (this.props.onRoomIdResolved) {
|
||||
this.props.onRoomIdResolved(result.room_id);
|
||||
}
|
||||
var room = MatrixClientPeg.get().getRoom(result.room_id);
|
||||
this.setState({
|
||||
room: room,
|
||||
roomId: result.room_id,
|
||||
userId: MatrixClientPeg.get().credentials.userId,
|
||||
roomLoading: !room,
|
||||
unsentMessageError: this._getUnsentMessageError(room),
|
||||
}, this._onHaveRoom);
|
||||
}, (err) => {
|
||||
this.setState({
|
||||
roomLoading: false,
|
||||
roomLoadError: err,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
var room = MatrixClientPeg.get().getRoom(this.props.roomAddress);
|
||||
this.setState({
|
||||
roomId: this.props.roomAddress,
|
||||
userId: MatrixClientPeg.get().credentials.userId,
|
||||
room: room,
|
||||
roomLoading: !room,
|
||||
unsentMessageError: this._getUnsentMessageError(room),
|
||||
}, this._onHaveRoom);
|
||||
// Start listening for RoomViewStore updates
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
this._onRoomViewStoreUpdate(true);
|
||||
},
|
||||
|
||||
_onRoomViewStoreUpdate: function(initial) {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
roomAlias: RoomViewStore.getRoomAlias(),
|
||||
roomLoading: RoomViewStore.isRoomLoading(),
|
||||
roomLoadError: RoomViewStore.getRoomLoadError(),
|
||||
joining: RoomViewStore.isJoining(),
|
||||
}, () => {
|
||||
this._onHaveRoom();
|
||||
this.onRoom(MatrixClientPeg.get().getRoom(this.state.roomId));
|
||||
});
|
||||
},
|
||||
|
||||
_onHaveRoom: function() {
|
||||
|
@ -223,26 +209,29 @@ module.exports = React.createClass({
|
|||
// NB. We peek if we are not in the room, although if we try to peek into
|
||||
// a room in which we have a member event (ie. we've left) synapse will just
|
||||
// send us the same data as we get in the sync (ie. the last events we saw).
|
||||
var user_is_in_room = null;
|
||||
if (this.state.room) {
|
||||
user_is_in_room = this.state.room.hasMembershipState(
|
||||
MatrixClientPeg.get().credentials.userId, 'join'
|
||||
const room = MatrixClientPeg.get().getRoom(this.state.roomId);
|
||||
let isUserJoined = null;
|
||||
if (room) {
|
||||
isUserJoined = room.hasMembershipState(
|
||||
MatrixClientPeg.get().credentials.userId, 'join',
|
||||
);
|
||||
|
||||
this._updateAutoComplete();
|
||||
this.tabComplete.loadEntries(this.state.room);
|
||||
this._updateAutoComplete(room);
|
||||
this.tabComplete.loadEntries(room);
|
||||
}
|
||||
|
||||
if (!user_is_in_room && this.state.roomId) {
|
||||
if (!isUserJoined && !this.state.joining && this.state.roomId) {
|
||||
if (this.props.autoJoin) {
|
||||
this.onJoinButtonClicked();
|
||||
} else if (this.state.roomId) {
|
||||
console.log("Attempting to peek into room %s", this.state.roomId);
|
||||
|
||||
this.setState({
|
||||
peekLoading: true,
|
||||
});
|
||||
MatrixClientPeg.get().peekInRoom(this.state.roomId).then((room) => {
|
||||
this.setState({
|
||||
room: room,
|
||||
roomLoading: false,
|
||||
peekLoading: false,
|
||||
});
|
||||
this._onRoomLoaded(room);
|
||||
}, (err) => {
|
||||
|
@ -252,16 +241,19 @@ module.exports = React.createClass({
|
|||
if (err.errcode == "M_GUEST_ACCESS_FORBIDDEN") {
|
||||
// This is fine: the room just isn't peekable (we assume).
|
||||
this.setState({
|
||||
roomLoading: false,
|
||||
peekLoading: false,
|
||||
});
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}).done();
|
||||
}
|
||||
} else if (user_is_in_room) {
|
||||
} else if (isUserJoined) {
|
||||
MatrixClientPeg.get().stopPeeking();
|
||||
this._onRoomLoaded(this.state.room);
|
||||
this.setState({
|
||||
unsentMessageError: this._getUnsentMessageError(room),
|
||||
});
|
||||
this._onRoomLoaded(room);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -274,6 +266,7 @@ module.exports = React.createClass({
|
|||
|
||||
this._updateConfCallNotification();
|
||||
|
||||
window.addEventListener('beforeunload', this.onPageUnload);
|
||||
window.addEventListener('resize', this.onResize);
|
||||
this.onResize();
|
||||
|
||||
|
@ -297,10 +290,6 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
if (newProps.roomAddress != this.props.roomAddress) {
|
||||
throw new Error("changing room on a RoomView is not supported");
|
||||
}
|
||||
|
||||
if (newProps.eventId != this.props.eventId) {
|
||||
// when we change focussed event id, hide the search results.
|
||||
this.setState({searchResults: null});
|
||||
|
@ -356,10 +345,16 @@ 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);
|
||||
|
||||
// Remove RoomStore listener
|
||||
if (this._roomStoreToken) {
|
||||
this._roomStoreToken.remove();
|
||||
}
|
||||
|
||||
// cancel any pending calls to the rate_limited_funcs
|
||||
this._updateRoomMembers.cancelPendingCall();
|
||||
|
||||
|
@ -368,6 +363,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;
|
||||
|
@ -447,6 +453,11 @@ module.exports = React.createClass({
|
|||
showApps: payload.show ? true : false,
|
||||
});
|
||||
break;
|
||||
case 'forward_event':
|
||||
this.setState({
|
||||
forwardingEvent: payload.content,
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -514,7 +525,7 @@ module.exports = React.createClass({
|
|||
this._updatePreviewUrlVisibility(room);
|
||||
},
|
||||
|
||||
_warnAboutEncryption: function (room) {
|
||||
_warnAboutEncryption: function(room) {
|
||||
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) {
|
||||
return;
|
||||
}
|
||||
|
@ -525,14 +536,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>
|
||||
),
|
||||
});
|
||||
|
@ -595,20 +606,14 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onRoom: function(room) {
|
||||
// This event is fired when the room is 'stored' by the JS SDK, which
|
||||
// means it's now a fully-fledged room object ready to be used, so
|
||||
// set it in our state and start using it (ie. init the timeline)
|
||||
// This will happen if we start off viewing a room we're not joined,
|
||||
// then join it whilst RoomView is looking at that room.
|
||||
if (!this.state.room && room.roomId == this._joiningRoomId) {
|
||||
this._joiningRoomId = undefined;
|
||||
this.setState({
|
||||
room: room,
|
||||
joining: false,
|
||||
});
|
||||
|
||||
this._onRoomLoaded(room);
|
||||
if (!room || room.roomId !== this.state.roomId) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
room: room,
|
||||
}, () => {
|
||||
this._onRoomLoaded(room);
|
||||
});
|
||||
},
|
||||
|
||||
updateTint: function() {
|
||||
|
@ -674,7 +679,7 @@ module.exports = React.createClass({
|
|||
|
||||
// refresh the tab complete list
|
||||
this.tabComplete.loadEntries(this.state.room);
|
||||
this._updateAutoComplete();
|
||||
this._updateAutoComplete(this.state.room);
|
||||
|
||||
// if we are now a member of the room, where we were not before, that
|
||||
// means we have finished joining a room we were previously peeking
|
||||
|
@ -691,10 +696,6 @@ module.exports = React.createClass({
|
|||
// compatability workaround, let's not bother.
|
||||
Rooms.setDMRoom(this.state.room.roomId, me.events.member.getSender()).done();
|
||||
}
|
||||
|
||||
this.setState({
|
||||
joining: false
|
||||
});
|
||||
}
|
||||
}, 500),
|
||||
|
||||
|
@ -703,10 +704,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) {
|
||||
|
@ -769,41 +770,62 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onJoinButtonClicked: function(ev) {
|
||||
var self = this;
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
var cli = MatrixClientPeg.get();
|
||||
var display_name_promise = q();
|
||||
// if this is the first room we're joining, check the user has a display name
|
||||
// and if they don't, prompt them to set one.
|
||||
// NB. This unfortunately does not re-use the ChangeDisplayName component because
|
||||
// it doesn't behave quite as desired here (we want an input field here rather than
|
||||
// content-editable, and we want a default).
|
||||
if (cli.getRooms().filter((r) => {
|
||||
return r.hasMembershipState(cli.credentials.userId, "join");
|
||||
})) {
|
||||
display_name_promise = cli.getProfileInfo(cli.credentials.userId).then((result) => {
|
||||
if (!result.displayname) {
|
||||
var SetDisplayNameDialog = sdk.getComponent('views.dialogs.SetDisplayNameDialog');
|
||||
var dialog_defer = q.defer();
|
||||
Modal.createDialog(SetDisplayNameDialog, {
|
||||
currentDisplayName: result.displayname,
|
||||
onFinished: (submitted, newDisplayName) => {
|
||||
if (submitted) {
|
||||
cli.setDisplayName(newDisplayName).done(() => {
|
||||
dialog_defer.resolve();
|
||||
});
|
||||
}
|
||||
else {
|
||||
dialog_defer.reject();
|
||||
}
|
||||
}
|
||||
});
|
||||
return dialog_defer.promise;
|
||||
}
|
||||
// If the user is a ROU, allow them to transition to a PWLU
|
||||
if (cli && cli.isGuest()) {
|
||||
// Join this room once the user has registered and logged in
|
||||
const signUrl = this.props.thirdPartyInvite ?
|
||||
this.props.thirdPartyInvite.inviteSignUrl : undefined;
|
||||
dis.dispatch({
|
||||
action: 'do_after_sync_prepared',
|
||||
deferred_action: {
|
||||
action: 'join_room',
|
||||
opts: { inviteSignUrl: signUrl },
|
||||
},
|
||||
});
|
||||
|
||||
// Don't peek whilst registering otherwise getPendingEventList complains
|
||||
// Do this by indicating our intention to join
|
||||
dis.dispatch({
|
||||
action: 'will_join',
|
||||
});
|
||||
|
||||
const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
|
||||
const close = Modal.createDialog(SetMxIdDialog, {
|
||||
homeserverUrl: cli.getHomeserverUrl(),
|
||||
onFinished: (submitted, credentials) => {
|
||||
if (submitted) {
|
||||
this.props.onRegistered(credentials);
|
||||
} else {
|
||||
dis.dispatch({
|
||||
action: 'cancel_after_sync_prepared',
|
||||
});
|
||||
dis.dispatch({
|
||||
action: 'cancel_join',
|
||||
});
|
||||
}
|
||||
},
|
||||
onDifferentServerClicked: (ev) => {
|
||||
dis.dispatch({action: 'start_registration'});
|
||||
close();
|
||||
},
|
||||
onLoginClick: (ev) => {
|
||||
dis.dispatch({action: 'start_login'});
|
||||
close();
|
||||
},
|
||||
}).close;
|
||||
return;
|
||||
}
|
||||
|
||||
display_name_promise.then(() => {
|
||||
q().then(() => {
|
||||
const signUrl = this.props.thirdPartyInvite ?
|
||||
this.props.thirdPartyInvite.inviteSignUrl : undefined;
|
||||
dis.dispatch({
|
||||
action: 'join_room',
|
||||
opts: { inviteSignUrl: signUrl },
|
||||
});
|
||||
|
||||
// if this is an invite and has the 'direct' hint set, mark it as a DM room now.
|
||||
if (this.state.room) {
|
||||
const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
|
@ -815,72 +837,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return q();
|
||||
}).then(() => {
|
||||
var sign_url = this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : undefined;
|
||||
return MatrixClientPeg.get().joinRoom(this.props.roomAddress,
|
||||
{ inviteSignUrl: sign_url } );
|
||||
}).then(function(resp) {
|
||||
var roomId = resp.roomId;
|
||||
|
||||
// It is possible that there is no Room yet if state hasn't come down
|
||||
// from /sync - joinRoom will resolve when the HTTP request to join succeeds,
|
||||
// NOT when it comes down /sync. If there is no room, we'll keep the
|
||||
// joining flag set until we see it.
|
||||
|
||||
// We'll need to initialise the timeline when joining, but due to
|
||||
// the above, we can't do it here: we do it in onRoom instead,
|
||||
// once we have a useable room object.
|
||||
var room = MatrixClientPeg.get().getRoom(roomId);
|
||||
if (!room) {
|
||||
// wait for the room to turn up in onRoom.
|
||||
self._joiningRoomId = roomId;
|
||||
} else {
|
||||
// we've got a valid room, but that might also just mean that
|
||||
// it was peekable (so we had one before anyway). If we are
|
||||
// not yet a member of the room, we will need to wait for that
|
||||
// to happen, in onRoomStateMember.
|
||||
var me = MatrixClientPeg.get().credentials.userId;
|
||||
self.setState({
|
||||
joining: !room.hasMembershipState(me, "join"),
|
||||
room: room
|
||||
});
|
||||
}
|
||||
}).catch(function(error) {
|
||||
self.setState({
|
||||
joining: false,
|
||||
joinError: error
|
||||
});
|
||||
|
||||
if (!error) return;
|
||||
|
||||
// https://matrix.org/jira/browse/SYN-659
|
||||
// Need specific error message if joining a room is refused because the user is a guest and guest access is not allowed
|
||||
if (
|
||||
error.errcode == 'M_GUEST_ACCESS_FORBIDDEN' ||
|
||||
(
|
||||
error.errcode == 'M_FORBIDDEN' &&
|
||||
MatrixClientPeg.get().isGuest()
|
||||
)
|
||||
) {
|
||||
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."
|
||||
});
|
||||
} 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
|
||||
});
|
||||
}
|
||||
}).done();
|
||||
|
||||
this.setState({
|
||||
joining: true
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -932,11 +889,7 @@ module.exports = React.createClass({
|
|||
|
||||
uploadFile: function(file) {
|
||||
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."
|
||||
});
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -954,8 +907,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: "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")),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -1041,8 +994,8 @@ module.exports = React.createClass({
|
|||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Search failed: " + error);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Search failed",
|
||||
description: "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({
|
||||
|
@ -1077,12 +1030,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>
|
||||
);
|
||||
}
|
||||
|
@ -1119,10 +1072,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;
|
||||
}
|
||||
|
@ -1168,7 +1121,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
|
||||
|
@ -1189,7 +1142,11 @@ 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'});
|
||||
},
|
||||
|
||||
onLeaveClick: function() {
|
||||
|
@ -1203,11 +1160,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 }),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -1228,8 +1185,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({
|
||||
|
@ -1263,6 +1220,7 @@ module.exports = React.createClass({
|
|||
// jump down to the bottom of this room, where new events are arriving
|
||||
jumpToLiveTimeline: function() {
|
||||
this.refs.messagePanel.jumpToLiveTimeline();
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
},
|
||||
|
||||
// jump up to wherever our read marker is
|
||||
|
@ -1282,12 +1240,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);
|
||||
|
@ -1461,70 +1414,70 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_updateAutoComplete: function() {
|
||||
_updateAutoComplete: function(room) {
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const members = this.state.room.getJoinedMembers().filter(function(member) {
|
||||
const members = room.getJoinedMembers().filter(function(member) {
|
||||
if (member.userId !== myUserId) return true;
|
||||
});
|
||||
UserProvider.getInstance().setUserList(members);
|
||||
},
|
||||
|
||||
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) {
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
if (this.state.roomLoading || this.state.peekLoading) {
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
var inviterName = undefined;
|
||||
if (this.props.oobData) {
|
||||
inviterName = this.props.oobData.inviterName;
|
||||
}
|
||||
var invitedEmail = undefined;
|
||||
if (this.props.thirdPartyInvite) {
|
||||
invitedEmail = this.props.thirdPartyInvite.invitedEmail;
|
||||
}
|
||||
else {
|
||||
var inviterName = undefined;
|
||||
if (this.props.oobData) {
|
||||
inviterName = this.props.oobData.inviterName;
|
||||
}
|
||||
var invitedEmail = undefined;
|
||||
if (this.props.thirdPartyInvite) {
|
||||
invitedEmail = this.props.thirdPartyInvite.invitedEmail;
|
||||
}
|
||||
|
||||
// We have no room object for this room, only the ID.
|
||||
// We've got to this room by following a link, possibly a third party invite.
|
||||
var room_alias = this.props.roomAddress[0] == '#' ? this.props.roomAddress : null;
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
<RoomHeader ref="header"
|
||||
room={this.state.room}
|
||||
oobData={this.props.oobData}
|
||||
collapsedRhs={ this.props.collapsedRhs }
|
||||
// We have no room object for this room, only the ID.
|
||||
// We've got to this room by following a link, possibly a third party invite.
|
||||
var room_alias = this.state.room_alias;
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
<RoomHeader ref="header"
|
||||
room={this.state.room}
|
||||
oobData={this.props.oobData}
|
||||
collapsedRhs={ this.props.collapsedRhs }
|
||||
/>
|
||||
<div className="mx_RoomView_auxPanel">
|
||||
<RoomPreviewBar onJoinClick={ this.onJoinButtonClicked }
|
||||
onForgetClick={ this.onForgetClick }
|
||||
onRejectClick={ this.onRejectThreepidInviteButtonClicked }
|
||||
canPreview={ false } error={ this.state.roomLoadError }
|
||||
roomAlias={room_alias}
|
||||
spinner={this.state.joining}
|
||||
inviterName={inviterName}
|
||||
invitedEmail={invitedEmail}
|
||||
room={this.state.room}
|
||||
/>
|
||||
<div className="mx_RoomView_auxPanel">
|
||||
<RoomPreviewBar onJoinClick={ this.onJoinButtonClicked }
|
||||
onForgetClick={ this.onForgetClick }
|
||||
onRejectClick={ this.onRejectThreepidInviteButtonClicked }
|
||||
canPreview={ false } error={ this.state.roomLoadError }
|
||||
roomAlias={room_alias}
|
||||
spinner={this.state.joining}
|
||||
inviterName={inviterName}
|
||||
invitedEmail={invitedEmail}
|
||||
room={this.state.room}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_RoomView_messagePanel"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<div className="mx_RoomView_messagePanel"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
|
@ -1607,17 +1560,18 @@ module.exports = React.createClass({
|
|||
/>;
|
||||
}
|
||||
|
||||
var aux = null;
|
||||
let aux = null;
|
||||
let hideCancel = false;
|
||||
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.forwardingEvent !== null) {
|
||||
aux = <ForwardMessage onCancelClick={this.onCancelClick} currentRoomId={this.state.room.roomId} mxEvent={this.state.forwardingEvent} />;
|
||||
} else if (this.state.searching) {
|
||||
hideCancel = true; // has own cancel
|
||||
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;
|
||||
|
@ -1628,6 +1582,7 @@ module.exports = React.createClass({
|
|||
if (this.props.thirdPartyInvite) {
|
||||
invitedEmail = this.props.thirdPartyInvite.invitedEmail;
|
||||
}
|
||||
hideCancel = true;
|
||||
aux = (
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||
onForgetClick={ this.onForgetClick }
|
||||
|
@ -1687,7 +1642,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>
|
||||
);
|
||||
|
@ -1695,14 +1650,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>;
|
||||
|
||||
|
@ -1738,14 +1693,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={true}
|
||||
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 }
|
||||
|
@ -1778,17 +1732,15 @@ 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}
|
||||
onForgetClick={
|
||||
(myMember && myMember.membership === "leave") ? this.onForgetClick : null
|
||||
}
|
||||
onLeaveClick={
|
||||
(myMember && myMember.membership === "join") ? this.onLeaveClick : null
|
||||
} />
|
||||
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
|
||||
onForgetClick={(myMember && myMember.membership === "leave") ? this.onForgetClick : null}
|
||||
onLeaveClick={(myMember && myMember.membership === "join") ? this.onLeaveClick : null}
|
||||
/>
|
||||
{ auxPanel }
|
||||
{ topUnreadMessagesBar }
|
||||
{ messagePanel }
|
||||
|
|
|
@ -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
|
||||
|
@ -483,21 +488,25 @@ module.exports = React.createClass({
|
|||
handleScrollKey: function(ev) {
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.PAGE_UP:
|
||||
this.scrollRelative(-1);
|
||||
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
this.scrollRelative(-1);
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyCode.PAGE_DOWN:
|
||||
this.scrollRelative(1);
|
||||
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
this.scrollRelative(1);
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyCode.HOME:
|
||||
if (ev.ctrlKey) {
|
||||
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
this.scrollToTop();
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyCode.END:
|
||||
if (ev.ctrlKey) {
|
||||
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
break;
|
||||
|
@ -547,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;
|
||||
}
|
||||
|
@ -564,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) {
|
||||
|
@ -587,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
|
||||
|
@ -604,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);
|
||||
|
|
|
@ -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;
|
||||
|
@ -102,9 +105,6 @@ var TimelinePanel = React.createClass({
|
|||
},
|
||||
|
||||
statics: {
|
||||
// a map from room id to read marker event ID
|
||||
roomReadMarkerMap: {},
|
||||
|
||||
// a map from room id to read marker event timestamp
|
||||
roomReadMarkerTsMap: {},
|
||||
},
|
||||
|
@ -121,12 +121,18 @@ var TimelinePanel = React.createClass({
|
|||
getInitialState: function() {
|
||||
// XXX: we could track RM per TimelineSet rather than per Room.
|
||||
// but for now we just do it per room for simplicity.
|
||||
let initialReadMarker = null;
|
||||
if (this.props.manageReadMarkers) {
|
||||
var initialReadMarker =
|
||||
TimelinePanel.roomReadMarkerMap[this.props.timelineSet.room.roomId]
|
||||
|| this._getCurrentReadReceipt();
|
||||
const readmarker = this.props.timelineSet.room.getAccountData('m.fully_read');
|
||||
if (readmarker) {
|
||||
initialReadMarker = readmarker.getContent().event_id;
|
||||
} else {
|
||||
initialReadMarker = this._getCurrentReadReceipt();
|
||||
}
|
||||
}
|
||||
|
||||
const syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||
|
||||
return {
|
||||
events: [],
|
||||
timelineLoading: true, // track whether our room timeline is loading
|
||||
|
@ -166,13 +172,26 @@ 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: syncedSettings.showTwelveHourTimestamps,
|
||||
|
||||
// always show timestamps on event tiles?
|
||||
alwaysShowTimestamps: syncedSettings.alwaysShowTimestamps,
|
||||
|
||||
// hide redacted events as per old behaviour
|
||||
hideRedactions: syncedSettings.hideRedactions,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
debuglog("TimelinePanel: mounting");
|
||||
|
||||
this.last_rr_sent_event_id = undefined;
|
||||
this.lastRRSentEventId = undefined;
|
||||
this.lastRMSentEventId = undefined;
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||
|
@ -180,6 +199,8 @@ var TimelinePanel = React.createClass({
|
|||
MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction);
|
||||
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);
|
||||
},
|
||||
|
@ -247,6 +268,8 @@ var TimelinePanel = React.createClass({
|
|||
client.removeListener("Room.redaction", this.onRoomRedaction);
|
||||
client.removeListener("Room.receipt", this.onRoomReceipt);
|
||||
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
||||
client.removeListener("Room.accountData", this.onAccountData);
|
||||
client.removeListener("sync", this.onSync);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -414,6 +437,7 @@ var TimelinePanel = React.createClass({
|
|||
} else if(lastEv && this.getReadMarkerPosition() === 0) {
|
||||
// we know we're stuckAtBottom, so we can advance the RM
|
||||
// immediately, to save a later render cycle
|
||||
|
||||
this._setReadMarker(lastEv.getId(), lastEv.getTs(), true);
|
||||
updatedState.readMarkerVisible = false;
|
||||
updatedState.readMarkerEventId = lastEv.getId();
|
||||
|
@ -466,6 +490,25 @@ var TimelinePanel = React.createClass({
|
|||
this._reloadEvents();
|
||||
},
|
||||
|
||||
onAccountData: function(ev, room) {
|
||||
if (this.unmounted) return;
|
||||
|
||||
// ignore events for other rooms
|
||||
if (room !== this.props.timelineSet.room) return;
|
||||
|
||||
if (ev.getType() !== "m.fully_read") return;
|
||||
|
||||
// XXX: roomReadMarkerTsMap not updated here so it is now inconsistent. Replace
|
||||
// this mechanism of determining where the RM is relative to the view-port with
|
||||
// one supported by the server (the client needs more than an event ID).
|
||||
this.setState({
|
||||
readMarkerEventId: ev.getContent().event_id,
|
||||
}, this.props.onReadMarkerUpdated);
|
||||
},
|
||||
|
||||
onSync: function(state, prevState, data) {
|
||||
this.setState({clientSyncState: state});
|
||||
},
|
||||
|
||||
sendReadReceipt: function() {
|
||||
if (!this.refs.messagePanel) return;
|
||||
|
@ -473,11 +516,14 @@ var TimelinePanel = React.createClass({
|
|||
// 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.
|
||||
//
|
||||
|
@ -491,26 +537,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_rr_sent_event_id = lastReadEvent.getId();
|
||||
MatrixClientPeg.get().sendReadReceipt(lastReadEvent).catch(() => {
|
||||
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, // 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' && lastReadEvent) {
|
||||
return MatrixClientPeg.get().sendReadReceipt(
|
||||
lastReadEvent,
|
||||
).catch(() => {
|
||||
this.lastRRSentEventId = undefined;
|
||||
});
|
||||
}
|
||||
// it failed, so allow retries next time the user is active
|
||||
this.last_rr_sent_event_id = undefined;
|
||||
this.lastRRSentEventId = undefined;
|
||||
this.lastRMSentEventId = undefined;
|
||||
});
|
||||
|
||||
// do a quick-reset of our unreadNotificationCount to avoid having
|
||||
|
@ -706,7 +786,7 @@ var TimelinePanel = React.createClass({
|
|||
|
||||
// the messagePanel doesn't know where the read marker is.
|
||||
// if we know the timestamp of the read marker, make a guess based on that.
|
||||
var rmTs = TimelinePanel.roomReadMarkerTsMap[this.props.timelineSet.roomId];
|
||||
const rmTs = TimelinePanel.roomReadMarkerTsMap[this.props.timelineSet.room.roomId];
|
||||
if (rmTs && this.state.events.length > 0) {
|
||||
if (rmTs < this.state.events[0].getTs()) {
|
||||
return -1;
|
||||
|
@ -718,6 +798,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.
|
||||
*
|
||||
|
@ -728,7 +821,9 @@ var TimelinePanel = React.createClass({
|
|||
|
||||
// jump to the live timeline on ctrl-end, rather than the end of the
|
||||
// timeline window.
|
||||
if (ev.ctrlKey && ev.keyCode == KeyCode.END) {
|
||||
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey &&
|
||||
ev.keyCode == KeyCode.END)
|
||||
{
|
||||
this.jumpToLiveTimeline();
|
||||
} else {
|
||||
this.refs.messagePanel.handleScrollKey(ev);
|
||||
|
@ -807,6 +902,9 @@ var TimelinePanel = React.createClass({
|
|||
|
||||
var onError = (error) => {
|
||||
this.setState({timelineLoading: false});
|
||||
console.error(
|
||||
`Error loading timeline panel at ${eventId}: ${error}`,
|
||||
);
|
||||
var msg = error.message ? error.message : JSON.stringify(error);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
|
@ -825,14 +923,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,
|
||||
});
|
||||
|
@ -956,16 +1051,12 @@ var TimelinePanel = React.createClass({
|
|||
_setReadMarker: function(eventId, eventTs, inhibitSetState) {
|
||||
var roomId = this.props.timelineSet.room.roomId;
|
||||
|
||||
if (TimelinePanel.roomReadMarkerMap[roomId] == eventId) {
|
||||
// don't update the state (and cause a re-render) if there is
|
||||
// no change to the RM.
|
||||
// don't update the state (and cause a re-render) if there is
|
||||
// no change to the RM.
|
||||
if (eventId === this.state.readMarkerEventId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ideally we'd sync these via the server, but for now just stash them
|
||||
// in a map.
|
||||
TimelinePanel.roomReadMarkerMap[roomId] = eventId;
|
||||
|
||||
// in order to later figure out if the read marker is
|
||||
// above or below the visible timeline, we stash the timestamp.
|
||||
TimelinePanel.roomReadMarkerTsMap[roomId] = eventTs;
|
||||
|
@ -974,6 +1065,7 @@ var TimelinePanel = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
// Do the local echo of the RM
|
||||
// run the render cycle before calling the callback, so that
|
||||
// getReadMarkerPosition() returns the right thing.
|
||||
this.setState({
|
||||
|
@ -1022,26 +1114,34 @@ var TimelinePanel = React.createClass({
|
|||
// 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 }
|
||||
events={ this.state.events }
|
||||
highlightedEventId={ this.props.highlightedEventId }
|
||||
readMarkerEventId={ this.state.readMarkerEventId }
|
||||
readMarkerVisible={ this.state.readMarkerVisible }
|
||||
suppressFirstDateSeparator={ this.state.canBackPaginate }
|
||||
showUrlPreview = { this.props.showUrlPreview }
|
||||
manageReadReceipts = { this.props.manageReadReceipts }
|
||||
ourUserId={ MatrixClientPeg.get().credentials.userId }
|
||||
stickyBottom={ stickyBottom }
|
||||
onScroll={ this.onMessageListScroll }
|
||||
onFillRequest={ this.onMessageListFillRequest }
|
||||
onUnfillRequest={ this.onMessageListUnfillRequest }
|
||||
opacity={ this.props.opacity }
|
||||
className={ this.props.className }
|
||||
tileShape={ this.props.tileShape }
|
||||
hidden={ this.props.hidden }
|
||||
hideRedactions={ this.state.hideRedactions }
|
||||
backPaginating={ this.state.backPaginating }
|
||||
forwardPaginating={ forwardPaginating }
|
||||
events={ this.state.events }
|
||||
highlightedEventId={ this.props.highlightedEventId }
|
||||
readMarkerEventId={ this.state.readMarkerEventId }
|
||||
readMarkerVisible={ this.state.readMarkerVisible }
|
||||
suppressFirstDateSeparator={ this.state.canBackPaginate }
|
||||
showUrlPreview = { this.props.showUrlPreview }
|
||||
manageReadReceipts = { this.props.manageReadReceipts }
|
||||
ourUserId={ MatrixClientPeg.get().credentials.userId }
|
||||
stickyBottom={ stickyBottom }
|
||||
onScroll={ this.onMessageListScroll }
|
||||
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 }
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -18,6 +18,7 @@ var React = require('react');
|
|||
var ContentMessages = require('../../ContentMessages');
|
||||
var dis = require('../../dispatcher');
|
||||
var filesize = require('filesize');
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({displayName: 'UploadBar',
|
||||
propTypes: {
|
||||
|
@ -81,10 +82,8 @@ module.exports = React.createClass({displayName: 'UploadBar',
|
|||
uploadedSize = uploadedSize.replace(/ .*/, '');
|
||||
}
|
||||
|
||||
var others;
|
||||
if (uploads.length > 1) {
|
||||
others = ' and ' + (uploads.length - 1) + ' other' + (uploads.length > 2 ? 's' : '');
|
||||
}
|
||||
// MUST use var name 'count' for pluralization to kick in
|
||||
var uploadText = _t("Uploading %(filename)s and %(count)s others", {filename: upload.fileName, count: (uploads.length - 1)});
|
||||
|
||||
return (
|
||||
<div className="mx_UploadBar">
|
||||
|
@ -98,7 +97,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
|
|||
<div className="mx_UploadBar_uploadBytes">
|
||||
{ uploadedSize } / { totalSize }
|
||||
</div>
|
||||
<div className="mx_UploadBar_uploadFilename">Uploading {upload.fileName}{others}</div>
|
||||
<div className="mx_UploadBar_uploadFilename">{uploadText}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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've followed
|
||||
the link it contains, click below.
|
||||
{ _t('An email has been sent to') } {this.state.email}. { _t('Once you'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}
|
||||
|
@ -227,10 +229,10 @@ module.exports = React.createClass({
|
|||
<div className="mx_Login_error">
|
||||
</div>
|
||||
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
|
||||
Return to login
|
||||
{_t('Return to login screen')}
|
||||
</a>
|
||||
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
|
||||
Create a new account
|
||||
{ _t('Create an account') }
|
||||
</a>
|
||||
<LoginFooter />
|
||||
</div>
|
||||
|
|
|
@ -17,13 +17,13 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
var sdk = require('../../../index');
|
||||
var Login = require("../../../Login");
|
||||
var PasswordLogin = require("../../views/login/PasswordLogin");
|
||||
var CasLogin = require("../../views/login/CasLogin");
|
||||
var ServerConfig = require("../../views/login/ServerConfig");
|
||||
import React from 'react';
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
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
|
||||
|
@ -67,6 +67,7 @@ module.exports = React.createClass({
|
|||
username: "",
|
||||
phoneCountry: null,
|
||||
phoneNumber: "",
|
||||
currentFlow: "m.login.password",
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -126,26 +127,31 @@ 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: _t('The phone number entered looks invalid') });
|
||||
return;
|
||||
}
|
||||
|
||||
onHsUrlChanged: function(newHsUrl) {
|
||||
var self = this;
|
||||
this.setState({
|
||||
enteredHomeserverUrl: newHsUrl,
|
||||
errorText: null, // reset err messages
|
||||
}, function() {
|
||||
self._initLoginLogic(newHsUrl);
|
||||
phoneNumber: phoneNumber,
|
||||
errorText: null,
|
||||
});
|
||||
},
|
||||
|
||||
onIsUrlChanged: function(newIsUrl) {
|
||||
onServerConfigChange: function(config) {
|
||||
var self = this;
|
||||
this.setState({
|
||||
enteredIdentityServerUrl: newIsUrl,
|
||||
let newState = {
|
||||
errorText: null, // reset err messages
|
||||
}, function() {
|
||||
self._initLoginLogic(null, newIsUrl);
|
||||
};
|
||||
if (config.hsUrl !== undefined) {
|
||||
newState.enteredHomeserverUrl = config.hsUrl;
|
||||
}
|
||||
if (config.isUrl !== undefined) {
|
||||
newState.enteredIdentityServerUrl = config.isUrl;
|
||||
}
|
||||
this.setState(newState, function() {
|
||||
self._initLoginLogic(config.hsUrl || null, config.isUrl);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -161,25 +167,28 @@ module.exports = React.createClass({
|
|||
});
|
||||
this._loginLogic = loginLogic;
|
||||
|
||||
loginLogic.getFlows().then(function(flows) {
|
||||
// old behaviour was to always use the first flow without presenting
|
||||
// options. This works in most cases (we don't have a UI for multiple
|
||||
// logins so let's skip that for now).
|
||||
loginLogic.chooseFlow(0);
|
||||
}, function(err) {
|
||||
self._setStateFromError(err, false);
|
||||
}).finally(function() {
|
||||
self.setState({
|
||||
busy: false
|
||||
});
|
||||
});
|
||||
|
||||
this.setState({
|
||||
enteredHomeserverUrl: hsUrl,
|
||||
enteredIdentityServerUrl: isUrl,
|
||||
busy: true,
|
||||
loginIncorrect: false,
|
||||
});
|
||||
|
||||
loginLogic.getFlows().then(function(flows) {
|
||||
// old behaviour was to always use the first flow without presenting
|
||||
// options. This works in most cases (we don't have a UI for multiple
|
||||
// logins so let's skip that for now).
|
||||
loginLogic.chooseFlow(0);
|
||||
self.setState({
|
||||
currentFlow: self._getCurrentFlowStep(),
|
||||
});
|
||||
}, function(err) {
|
||||
self._setStateFromError(err, false);
|
||||
}).finally(function() {
|
||||
self.setState({
|
||||
busy: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_getCurrentFlowStep: function() {
|
||||
|
@ -204,8 +213,8 @@ module.exports = React.createClass({
|
|||
errCode = "HTTP " + err.httpStatus;
|
||||
}
|
||||
|
||||
let errorText = "Error: Problem communicating with the given homeserver " +
|
||||
(errCode ? "(" + errCode + ")" : "");
|
||||
let errorText = _t("Error: Problem communicating with the given homeserver.") +
|
||||
(errCode ? " (" + errCode + ")" : "");
|
||||
|
||||
if (err.cors === 'rejected') {
|
||||
if (window.location.protocol === 'https:' &&
|
||||
|
@ -213,14 +222,19 @@ module.exports = React.createClass({
|
|||
!this.state.enteredHomeserverUrl.startsWith("http")))
|
||||
{
|
||||
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>
|
||||
{ _tJsx("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
|
||||
"Either use HTTPS or <a>enable unsafe scripts</a>.",
|
||||
/<a>(.*?)<\/a>/,
|
||||
(sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; }
|
||||
)}
|
||||
</span>;
|
||||
}
|
||||
else {
|
||||
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.
|
||||
{ _tJsx("Can't connect to homeserver - please check your connectivity and ensure your <a>homeserver's SSL certificate</a> is trusted.",
|
||||
/<a>(.*?)<\/a>/,
|
||||
(sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; }
|
||||
)}
|
||||
</span>;
|
||||
}
|
||||
}
|
||||
|
@ -231,6 +245,7 @@ module.exports = React.createClass({
|
|||
componentForStep: function(step) {
|
||||
switch (step) {
|
||||
case 'm.login.password':
|
||||
const PasswordLogin = sdk.getComponent('login.PasswordLogin');
|
||||
return (
|
||||
<PasswordLogin
|
||||
onSubmit={this.onPasswordLogin}
|
||||
|
@ -245,6 +260,7 @@ module.exports = React.createClass({
|
|||
/>
|
||||
);
|
||||
case 'm.login.cas':
|
||||
const CasLogin = sdk.getComponent('login.CasLogin');
|
||||
return (
|
||||
<CasLogin onSubmit={this.onCasLogin} />
|
||||
);
|
||||
|
@ -254,24 +270,24 @@ 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>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
var LoginHeader = sdk.getComponent("login.LoginHeader");
|
||||
var LoginFooter = sdk.getComponent("login.LoginFooter");
|
||||
var loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const LoginHeader = sdk.getComponent("login.LoginHeader");
|
||||
const LoginFooter = sdk.getComponent("login.LoginFooter");
|
||||
const ServerConfig = sdk.getComponent("login.ServerConfig");
|
||||
const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
|
||||
|
||||
var loginAsGuestJsx;
|
||||
if (this.props.enableGuest) {
|
||||
loginAsGuestJsx =
|
||||
<a className="mx_Login_create" onClick={this._onLoginAsGuestClick} href="#">
|
||||
Login as guest
|
||||
{ _t('Login as guest')}
|
||||
</a>;
|
||||
}
|
||||
|
||||
|
@ -279,7 +295,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>;
|
||||
}
|
||||
|
||||
|
@ -288,24 +304,23 @@ module.exports = React.createClass({
|
|||
<div className="mx_Login_box">
|
||||
<LoginHeader />
|
||||
<div>
|
||||
<h2>Sign in
|
||||
<h2>{ _t('Sign in')}
|
||||
{ loader }
|
||||
</h2>
|
||||
{ this.componentForStep(this._getCurrentFlowStep()) }
|
||||
{ this.componentForStep(this.state.currentFlow) }
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
customIsUrl={this.props.customIsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onHsUrlChanged={this.onHsUrlChanged}
|
||||
onIsUrlChanged={this.onIsUrlChanged}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={1000}/>
|
||||
<div className="mx_Login_error">
|
||||
{ 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 }
|
||||
|
|
|
@ -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',
|
||||
|
@ -49,7 +50,7 @@ module.exports = React.createClass({
|
|||
});
|
||||
}, function(error) {
|
||||
self.setState({
|
||||
errorString: "Failed to fetch avatar URL",
|
||||
errorString: _t("Failed to fetch avatar URL"),
|
||||
busy: false
|
||||
});
|
||||
});
|
||||
|
@ -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>
|
||||
|
|
|
@ -21,12 +21,11 @@ import q from 'q';
|
|||
import React from 'react';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import ServerConfig from '../../views/login/ServerConfig';
|
||||
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;
|
||||
|
||||
|
@ -98,7 +97,7 @@ module.exports = React.createClass({
|
|||
this.props.teamServerConfig.teamServerURL &&
|
||||
!this._rtsClient
|
||||
) {
|
||||
this._rtsClient = new RtsClient(this.props.teamServerConfig.teamServerURL);
|
||||
this._rtsClient = this.props.rtsClient || new RtsClient(this.props.teamServerConfig.teamServerURL);
|
||||
|
||||
this.setState({
|
||||
teamServerBusy: true,
|
||||
|
@ -123,18 +122,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 +161,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({
|
||||
|
@ -222,7 +220,6 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
trackPromise.then((teamToken) => {
|
||||
console.info('Team token promise',teamToken);
|
||||
this.props.onLoggedIn({
|
||||
userId: response.user_id,
|
||||
deviceId: response.device_id,
|
||||
|
@ -261,29 +258,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 +387,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 +398,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 +411,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 />
|
||||
|
|
|
@ -32,6 +32,7 @@ module.exports = React.createClass({
|
|||
urls: React.PropTypes.array, // [highest_priority, ... , lowest_priority]
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
// XXX resizeMethod not actually used.
|
||||
resizeMethod: React.PropTypes.string,
|
||||
defaultToInitialLetter: React.PropTypes.bool // true to add default url
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -16,8 +16,8 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
module.exports = React.createClass({
|
||||
displayName: 'CreateRoomButton',
|
||||
propTypes: {
|
||||
|
@ -36,7 +36,7 @@ module.exports = React.createClass({
|
|||
|
||||
render: function() {
|
||||
return (
|
||||
<button className="mx_CreateRoomButton" onClick={this.onClick}>Create Room</button>
|
||||
<button className="mx_CreateRoomButton" onClick={this.onClick}>{_t("Create Room")}</button>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
var Presets = {
|
||||
PrivateChat: "private_chat",
|
||||
|
@ -46,9 +47,9 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
return (
|
||||
<select className="mx_Presets" onChange={this.onValueChanged} value={this.props.preset}>
|
||||
<option value={this.Presets.PrivateChat}>Private Chat</option>
|
||||
<option value={this.Presets.PublicChat}>Public Chat</option>
|
||||
<option value={this.Presets.Custom}>Custom</option>
|
||||
<option value={this.Presets.PrivateChat}>{_t("Private Chat")}</option>
|
||||
<option value={this.Presets.PublicChat}>{_t("Public Chat")}</option>
|
||||
<option value={this.Presets.Custom}>{_t("Custom")}</option>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
var React = require('react');
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomAlias',
|
||||
|
@ -94,7 +95,7 @@ module.exports = React.createClass({
|
|||
|
||||
render: function() {
|
||||
return (
|
||||
<input type="text" className="mx_RoomAlias" placeholder="Alias (optional)"
|
||||
<input type="text" className="mx_RoomAlias" placeholder={_t("Alias (optional)")}
|
||||
onChange={this.onValueChanged} onFocus={this.onFocus} onBlur={this.onBlur}
|
||||
value={this.props.alias}/>
|
||||
);
|
||||
|
|
|
@ -67,7 +67,7 @@ export default React.createClass({
|
|||
|
||||
render: function() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
|
||||
return (
|
||||
<div onKeyDown={this._onKeyDown} className={this.props.className}>
|
||||
<AccessibleButton onClick={this._onCancelClick}
|
||||
|
|
|
@ -16,36 +16,30 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import Unread from '../../../Unread';
|
||||
import classNames from 'classnames';
|
||||
import createRoom from '../../../createRoom';
|
||||
|
||||
export default class ChatCreateOrReuseDialog extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onNewDMClick = this.onNewDMClick.bind(this);
|
||||
this.onRoomTileClick = this.onRoomTileClick.bind(this);
|
||||
|
||||
this.state = {
|
||||
tiles: [],
|
||||
profile: {
|
||||
displayName: null,
|
||||
avatarUrl: null,
|
||||
},
|
||||
profileError: null,
|
||||
};
|
||||
}
|
||||
|
||||
onNewDMClick() {
|
||||
createRoom({dmUserId: this.props.userId});
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
|
||||
onRoomTileClick(roomId) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
});
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
|
||||
render() {
|
||||
componentWillMount() {
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
const dmRoomMap = new DMRoomMap(client);
|
||||
|
@ -70,40 +64,123 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
highlight={highlight}
|
||||
isInvite={me.membership == "invite"}
|
||||
onClick={this.onRoomTileClick}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const labelClasses = classNames({
|
||||
mx_MemberInfo_createRoom_label: true,
|
||||
mx_RoomTile_name: true,
|
||||
this.setState({
|
||||
tiles: tiles,
|
||||
});
|
||||
const startNewChat = <AccessibleButton
|
||||
className="mx_MemberInfo_createRoom"
|
||||
onClick={this.onNewDMClick}
|
||||
>
|
||||
<div className="mx_RoomTile_avatar">
|
||||
<img src="img/create-big.svg" width="26" height="26" />
|
||||
</div>
|
||||
<div className={labelClasses}><i>Start new chat</i></div>
|
||||
</AccessibleButton>;
|
||||
|
||||
if (tiles.length === 0) {
|
||||
this.setState({
|
||||
busyProfile: true,
|
||||
});
|
||||
MatrixClientPeg.get().getProfileInfo(this.props.userId).done((resp) => {
|
||||
const profile = {
|
||||
displayName: resp.displayname,
|
||||
avatarUrl: null,
|
||||
};
|
||||
if (resp.avatar_url) {
|
||||
profile.avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(
|
||||
resp.avatar_url, 48, 48, "crop",
|
||||
);
|
||||
}
|
||||
this.setState({
|
||||
busyProfile: false,
|
||||
profile: profile,
|
||||
});
|
||||
}, (err) => {
|
||||
console.error(
|
||||
'Unable to get profile for user ' + this.props.userId + ':',
|
||||
err,
|
||||
);
|
||||
this.setState({
|
||||
busyProfile: false,
|
||||
profileError: err,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onRoomTileClick(roomId) {
|
||||
this.props.onExistingRoomSelected(roomId);
|
||||
}
|
||||
|
||||
render() {
|
||||
let title = '';
|
||||
let content = null;
|
||||
if (this.state.tiles.length > 0) {
|
||||
// Show the existing rooms with a "+" to add a new dm
|
||||
title = _t('Create a new chat or reuse an existing one');
|
||||
const labelClasses = classNames({
|
||||
mx_MemberInfo_createRoom_label: true,
|
||||
mx_RoomTile_name: true,
|
||||
});
|
||||
const startNewChat = <AccessibleButton
|
||||
className="mx_MemberInfo_createRoom"
|
||||
onClick={this.props.onNewDMClick}
|
||||
>
|
||||
<div className="mx_RoomTile_avatar">
|
||||
<img src="img/create-big.svg" width="26" height="26" />
|
||||
</div>
|
||||
<div className={labelClasses}><i>{ _t("Start new chat") }</i></div>
|
||||
</AccessibleButton>;
|
||||
content = <div className="mx_Dialog_content">
|
||||
{ _t('You already have existing direct chats with this user:') }
|
||||
<div className="mx_ChatCreateOrReuseDialog_tiles">
|
||||
{ this.state.tiles }
|
||||
{ startNewChat }
|
||||
</div>
|
||||
</div>;
|
||||
} else {
|
||||
// Show the avatar, name and a button to confirm that a new chat is requested
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
title = _t('Start chatting');
|
||||
|
||||
let profile = null;
|
||||
if (this.state.busyProfile) {
|
||||
profile = <Spinner />;
|
||||
} else if (this.state.profileError) {
|
||||
profile = <div className="error">
|
||||
Unable to load profile information for { this.props.userId }
|
||||
</div>;
|
||||
} else {
|
||||
profile = <div className="mx_ChatCreateOrReuseDialog_profile">
|
||||
<BaseAvatar
|
||||
name={this.state.profile.displayName || this.props.userId}
|
||||
url={this.state.profile.avatarUrl}
|
||||
width={48} height={48}
|
||||
/>
|
||||
<div className="mx_ChatCreateOrReuseDialog_profile_name">
|
||||
{this.state.profile.displayName || this.props.userId}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
content = <div>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>
|
||||
{ _t('Click on the button below to start chatting!') }
|
||||
</p>
|
||||
{ profile }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this.props.onNewDMClick}>
|
||||
{ _t('Start Chatting') }
|
||||
</button>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog className='mx_ChatCreateOrReuseDialog'
|
||||
onFinished={() => {
|
||||
this.props.onFinished(false)
|
||||
}}
|
||||
title='Create a new chat or reuse an existing one'
|
||||
onFinished={ this.props.onFinished.bind(false) }
|
||||
title={title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
You already have existing direct chats with this user:
|
||||
<div className="mx_ChatCreateOrReuseDialog_tiles">
|
||||
{tiles}
|
||||
{startNewChat}
|
||||
</div>
|
||||
</div>
|
||||
{ content }
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
@ -111,5 +188,8 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
|
||||
ChatCreateOrReuseDialog.propTyps = {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
// Called when clicking outside of the dialog
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
onNewDMClick: React.PropTypes.func.isRequired,
|
||||
onExistingRoomSelected: React.PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
|
@ -15,25 +15,23 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import { getAddressType, inviteMultipleToRoom } from '../../../Invite';
|
||||
import createRoom from '../../../createRoom';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import rate_limited_func from '../../../ratelimitedfunc';
|
||||
import dis from '../../../dispatcher';
|
||||
import Modal from '../../../Modal';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import q from 'q';
|
||||
import Fuse from 'fuse.js';
|
||||
|
||||
const TRUNCATE_QUERY_LIST = 40;
|
||||
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: "ChatInviteDialog",
|
||||
propTypes: {
|
||||
title: React.PropTypes.string,
|
||||
title: React.PropTypes.string.isRequired,
|
||||
description: React.PropTypes.oneOfType([
|
||||
React.PropTypes.element,
|
||||
React.PropTypes.string,
|
||||
|
@ -43,17 +41,13 @@ module.exports = React.createClass({
|
|||
roomId: React.PropTypes.string,
|
||||
button: React.PropTypes.string,
|
||||
focus: React.PropTypes.bool,
|
||||
onFinished: React.PropTypes.func.isRequired
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Start a chat",
|
||||
description: "Who would you like to communicate with?",
|
||||
value: "",
|
||||
placeholder: "Email, name or matrix ID",
|
||||
button: "Start Chat",
|
||||
focus: true
|
||||
focus: true,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -61,12 +55,20 @@ module.exports = React.createClass({
|
|||
return {
|
||||
error: false,
|
||||
|
||||
// List of AddressTile.InviteAddressType objects represeting
|
||||
// List of AddressTile.InviteAddressType objects representing
|
||||
// the list of addresses we're going to invite
|
||||
inviteList: [],
|
||||
|
||||
// List of AddressTile.InviteAddressType objects represeting
|
||||
// the set of autocompletion results for the current search
|
||||
// Whether a search is ongoing
|
||||
busy: false,
|
||||
// An error message generated during the user directory search
|
||||
searchError: null,
|
||||
// Whether the server supports the user_directory API
|
||||
serverSupportsUserDirectory: true,
|
||||
// The query being searched for
|
||||
query: "",
|
||||
// List of AddressTile.InviteAddressType objects representing
|
||||
// the set of auto-completion results for the current search
|
||||
// query.
|
||||
queryList: [],
|
||||
};
|
||||
|
@ -77,20 +79,6 @@ module.exports = React.createClass({
|
|||
// Set the cursor at the end of the text input
|
||||
this.refs.textinput.value = this.props.value;
|
||||
}
|
||||
// Create a Fuse instance for fuzzy searching this._userList
|
||||
this._fuse = new Fuse(
|
||||
// Use an empty list at first that will later be populated
|
||||
// (see this._updateUserList)
|
||||
[],
|
||||
{
|
||||
shouldSort: true,
|
||||
location: 0, // The index of the query in the test string
|
||||
distance: 5, // The distance away from location the query can be
|
||||
// 0.0 = exact match, 1.0 = match anything
|
||||
threshold: 0.3,
|
||||
}
|
||||
);
|
||||
this._updateUserList();
|
||||
},
|
||||
|
||||
onButtonClick: function() {
|
||||
|
@ -112,16 +100,25 @@ module.exports = React.createClass({
|
|||
// A Direct Message room already exists for this user, so select a
|
||||
// room from a list that is similar to the one in MemberInfo panel
|
||||
const ChatCreateOrReuseDialog = sdk.getComponent(
|
||||
"views.dialogs.ChatCreateOrReuseDialog"
|
||||
"views.dialogs.ChatCreateOrReuseDialog",
|
||||
);
|
||||
Modal.createDialog(ChatCreateOrReuseDialog, {
|
||||
userId: userId,
|
||||
onFinished: (success) => {
|
||||
if (success) {
|
||||
this.props.onFinished(true, inviteList[0]);
|
||||
}
|
||||
// else show this ChatInviteDialog again
|
||||
}
|
||||
this.props.onFinished(success);
|
||||
},
|
||||
onNewDMClick: () => {
|
||||
dis.dispatch({
|
||||
action: 'start_chat',
|
||||
user_id: userId,
|
||||
});
|
||||
},
|
||||
onExistingRoomSelected: (roomId) => {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
user_id: roomId,
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this._startChat(inviteList);
|
||||
|
@ -148,15 +145,15 @@ module.exports = React.createClass({
|
|||
} else if (e.keyCode === 38) { // up arrow
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.addressSelector.moveSelectionUp();
|
||||
if (this.addressSelector) this.addressSelector.moveSelectionUp();
|
||||
} else if (e.keyCode === 40) { // down arrow
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.addressSelector.moveSelectionDown();
|
||||
if (this.addressSelector) this.addressSelector.moveSelectionDown();
|
||||
} else if (this.state.queryList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.addressSelector.chooseSelection();
|
||||
if (this.addressSelector) this.addressSelector.chooseSelection();
|
||||
} else if (this.refs.textinput.value.length === 0 && this.state.inviteList.length && e.keyCode === 8) { // backspace
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
@ -178,72 +175,37 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onQueryChanged: function(ev) {
|
||||
const query = ev.target.value;
|
||||
let queryList = [];
|
||||
|
||||
if (query.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const query = ev.target.value.toLowerCase();
|
||||
if (this.queryChangedDebouncer) {
|
||||
clearTimeout(this.queryChangedDebouncer);
|
||||
}
|
||||
this.queryChangedDebouncer = setTimeout(() => {
|
||||
// Only do search if there is something to search
|
||||
if (query.length > 0 && query != '@') {
|
||||
// Weighted keys prefer to match userIds when first char is @
|
||||
this._fuse.options.keys = [{
|
||||
name: 'displayName',
|
||||
weight: query[0] === '@' ? 0.1 : 0.9,
|
||||
},{
|
||||
name: 'userId',
|
||||
weight: query[0] === '@' ? 0.9 : 0.1,
|
||||
}];
|
||||
queryList = this._fuse.search(query).map((user) => {
|
||||
// Return objects, structure of which is defined
|
||||
// by InviteAddressType
|
||||
return {
|
||||
addressType: 'mx',
|
||||
address: user.userId,
|
||||
displayName: user.displayName,
|
||||
avatarMxc: user.avatarUrl,
|
||||
isKnown: true,
|
||||
}
|
||||
});
|
||||
|
||||
// If the query is a valid address, add an entry for that
|
||||
// This is important, otherwise there's no way to invite
|
||||
// a perfectly valid address if there are close matches.
|
||||
const addrType = getAddressType(query);
|
||||
if (addrType !== null) {
|
||||
queryList.unshift({
|
||||
addressType: addrType,
|
||||
address: query,
|
||||
isKnown: false,
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
if (addrType == 'email') {
|
||||
this._lookupThreepid(addrType, query).done();
|
||||
}
|
||||
// Only do search if there is something to search
|
||||
if (query.length > 0 && query != '@' && query.length >= 2) {
|
||||
this.queryChangedDebouncer = setTimeout(() => {
|
||||
if (this.state.serverSupportsUserDirectory) {
|
||||
this._doUserDirectorySearch(query);
|
||||
} else {
|
||||
this._doLocalSearch(query);
|
||||
}
|
||||
}
|
||||
}, QUERY_USER_DIRECTORY_DEBOUNCE_MS);
|
||||
} else {
|
||||
this.setState({
|
||||
queryList: queryList,
|
||||
error: false,
|
||||
}, () => {
|
||||
this.addressSelector.moveSelectionTop();
|
||||
queryList: [],
|
||||
query: "",
|
||||
searchError: null,
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
},
|
||||
|
||||
onDismissed: function(index) {
|
||||
var self = this;
|
||||
return function() {
|
||||
return () => {
|
||||
var inviteList = self.state.inviteList.slice();
|
||||
inviteList.splice(index, 1);
|
||||
self.setState({
|
||||
inviteList: inviteList,
|
||||
queryList: [],
|
||||
query: "",
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
};
|
||||
|
@ -262,10 +224,103 @@ module.exports = React.createClass({
|
|||
this.setState({
|
||||
inviteList: inviteList,
|
||||
queryList: [],
|
||||
query: "",
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
},
|
||||
|
||||
_doUserDirectorySearch: function(query) {
|
||||
this.setState({
|
||||
busy: true,
|
||||
query,
|
||||
searchError: null,
|
||||
});
|
||||
MatrixClientPeg.get().searchUserDirectory({
|
||||
term: query,
|
||||
}).then((resp) => {
|
||||
this._processResults(resp.results, query);
|
||||
}).catch((err) => {
|
||||
console.error('Error whilst searching user directory: ', err);
|
||||
this.setState({
|
||||
searchError: err.errcode ? err.message : _t('Something went wrong!'),
|
||||
});
|
||||
if (err.errcode === 'M_UNRECOGNIZED') {
|
||||
this.setState({
|
||||
serverSupportsUserDirectory: false,
|
||||
});
|
||||
// Do a local search immediately
|
||||
this._doLocalSearch(query);
|
||||
}
|
||||
}).done(() => {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_doLocalSearch: function(query) {
|
||||
this.setState({
|
||||
query,
|
||||
searchError: null,
|
||||
});
|
||||
const results = [];
|
||||
MatrixClientPeg.get().getUsers().forEach((user) => {
|
||||
if (user.userId.toLowerCase().indexOf(query) === -1 &&
|
||||
user.displayName.toLowerCase().indexOf(query) === -1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Put results in the format of the new API
|
||||
results.push({
|
||||
user_id: user.userId,
|
||||
display_name: user.displayName,
|
||||
avatar_url: user.avatarUrl,
|
||||
});
|
||||
});
|
||||
this._processResults(results, query);
|
||||
},
|
||||
|
||||
_processResults: function(results, query) {
|
||||
const queryList = [];
|
||||
results.forEach((user) => {
|
||||
if (user.user_id === MatrixClientPeg.get().credentials.userId) {
|
||||
return;
|
||||
}
|
||||
// Return objects, structure of which is defined
|
||||
// by InviteAddressType
|
||||
queryList.push({
|
||||
addressType: 'mx',
|
||||
address: user.user_id,
|
||||
displayName: user.display_name,
|
||||
avatarMxc: user.avatar_url,
|
||||
isKnown: true,
|
||||
});
|
||||
});
|
||||
|
||||
// If the query is a valid address, add an entry for that
|
||||
// This is important, otherwise there's no way to invite
|
||||
// a perfectly valid address if there are close matches.
|
||||
const addrType = getAddressType(query);
|
||||
if (addrType !== null) {
|
||||
queryList.unshift({
|
||||
addressType: addrType,
|
||||
address: query,
|
||||
isKnown: false,
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
if (addrType == 'email') {
|
||||
this._lookupThreepid(addrType, query).done();
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
queryList,
|
||||
error: false,
|
||||
}, () => {
|
||||
if (this.addressSelector) this.addressSelector.moveSelectionTop();
|
||||
});
|
||||
},
|
||||
|
||||
_getDirectMessageRooms: function(addr) {
|
||||
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
|
||||
const dmRooms = dmRoomMap.getDMRoomsForUserId(addr);
|
||||
|
@ -284,11 +339,7 @@ module.exports = React.createClass({
|
|||
|
||||
_startChat: function(addrs) {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
||||
Modal.createDialog(NeedToRegisterDialog, {
|
||||
title: "Please Register",
|
||||
description: "Guest users can't invite users. Please register."
|
||||
});
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -308,8 +359,8 @@ module.exports = React.createClass({
|
|||
console.error(err.stack);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: "Failed to invite",
|
||||
title: _t("Failed to invite"),
|
||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||
});
|
||||
return null;
|
||||
})
|
||||
|
@ -321,8 +372,8 @@ module.exports = React.createClass({
|
|||
console.error(err.stack);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: "Failed to invite user",
|
||||
title: _t("Failed to invite user"),
|
||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||
});
|
||||
return null;
|
||||
})
|
||||
|
@ -342,8 +393,8 @@ module.exports = React.createClass({
|
|||
console.error(err.stack);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: "Failed to invite",
|
||||
title: _t("Failed to invite"),
|
||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||
});
|
||||
return null;
|
||||
})
|
||||
|
@ -354,18 +405,6 @@ module.exports = React.createClass({
|
|||
this.props.onFinished(true, addrTexts);
|
||||
},
|
||||
|
||||
_updateUserList: new rate_limited_func(function() {
|
||||
// Get all the users
|
||||
this._userList = MatrixClientPeg.get().getUsers();
|
||||
// Remove current user
|
||||
const meIx = this._userList.findIndex((u) => {
|
||||
return u.userId === MatrixClientPeg.get().credentials.userId;
|
||||
});
|
||||
this._userList.splice(meIx, 1);
|
||||
|
||||
this._fuse.set(this._userList);
|
||||
}, 500),
|
||||
|
||||
_isOnInviteList: function(uid) {
|
||||
for (let i = 0; i < this.state.inviteList.length; i++) {
|
||||
if (
|
||||
|
@ -401,7 +440,7 @@ module.exports = React.createClass({
|
|||
if (errorList.length > 0) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to invite the following users to the " + room.name + " room:",
|
||||
title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}),
|
||||
description: errorList.join(", "),
|
||||
});
|
||||
}
|
||||
|
@ -433,6 +472,7 @@ module.exports = React.createClass({
|
|||
this.setState({
|
||||
inviteList: inviteList,
|
||||
queryList: [],
|
||||
query: "",
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
return inviteList;
|
||||
|
@ -468,7 +508,7 @@ module.exports = React.createClass({
|
|||
displayName: res.displayname,
|
||||
avatarMxc: res.avatar_url,
|
||||
isKnown: true,
|
||||
}]
|
||||
}],
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -500,23 +540,27 @@ module.exports = React.createClass({
|
|||
placeholder={this.props.placeholder}
|
||||
defaultValue={this.props.value}
|
||||
autoFocus={this.props.focus}>
|
||||
</textarea>
|
||||
</textarea>,
|
||||
);
|
||||
|
||||
var error;
|
||||
var addressSelector;
|
||||
let error;
|
||||
let addressSelector;
|
||||
if (this.state.error) {
|
||||
error = <div className="mx_ChatInviteDialog_error">You have entered an invalid contact. Try using their Matrix ID or email address.</div>;
|
||||
error = <div className="mx_ChatInviteDialog_error">{_t("You have entered an invalid contact. Try using their Matrix ID or email address.")}</div>;
|
||||
} else if (this.state.searchError) {
|
||||
error = <div className="mx_ChatInviteDialog_error">{this.state.searchError}</div>;
|
||||
} else if (
|
||||
this.state.query.length > 0 &&
|
||||
this.state.queryList.length === 0 &&
|
||||
!this.state.busy
|
||||
) {
|
||||
error = <div className="mx_ChatInviteDialog_error">{_t("No results")}</div>;
|
||||
} else {
|
||||
const addressSelectorHeader = <div className="mx_ChatInviteDialog_addressSelectHeader">
|
||||
Searching known users
|
||||
</div>;
|
||||
addressSelector = (
|
||||
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
|
||||
addressList={ this.state.queryList }
|
||||
onSelected={ this.onSelected }
|
||||
truncateAt={ TRUNCATE_QUERY_LIST }
|
||||
header={ addressSelectorHeader }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import classnames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
/*
|
||||
* A dialog for confirming a redaction.
|
||||
|
@ -42,7 +43,7 @@ export default React.createClass({
|
|||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
const title = "Confirm Redaction";
|
||||
const title = _t("Confirm Removal");
|
||||
|
||||
const confirmButtonClass = classnames({
|
||||
'mx_Dialog_primary': true,
|
||||
|
@ -55,16 +56,16 @@ export default React.createClass({
|
|||
title={title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
Are you sure you wish to redact (delete) this event?
|
||||
Note that if you redact a room name or topic change, it could undo the change.
|
||||
{_t("Are you sure you wish to remove (delete) this event? " +
|
||||
"Note that if you delete a room name or topic change, it could undo the change.")}
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className={confirmButtonClass} onClick={this.onOk}>
|
||||
Redact
|
||||
{_t("Remove")}
|
||||
</button>
|
||||
|
||||
<button onClick={this.onCancel}>
|
||||
Cancel
|
||||
{_t("Cancel")}
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -20,6 +20,7 @@ import sdk from '../../../index';
|
|||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import * as Lifecycle from '../../../Lifecycle';
|
||||
import Velocity from 'velocity-vector';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default class DeactivateAccountDialog extends React.Component {
|
||||
constructor(props, context) {
|
||||
|
@ -56,10 +57,10 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
Lifecycle.onLoggedOut();
|
||||
this.props.onFinished(false);
|
||||
}, (err) => {
|
||||
let errStr = 'Unknown error';
|
||||
let errStr = _t('Unknown error');
|
||||
// https://matrix.org/jira/browse/SYN-744
|
||||
if (err.httpStatus == 401 || err.httpStatus == 403) {
|
||||
errStr = 'Incorrect password';
|
||||
errStr = _t('Incorrect password');
|
||||
Velocity(this._passwordField, "callout.shake", 300);
|
||||
}
|
||||
this.setState({
|
||||
|
@ -85,29 +86,29 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
passwordBoxClass = 'error';
|
||||
}
|
||||
|
||||
const okLabel = this.state.busy ? <Loader /> : 'Deactivate Account';
|
||||
const okLabel = this.state.busy ? <Loader /> : _t('Deactivate Account');
|
||||
const okEnabled = this.state.confirmButtonEnabled && !this.state.busy;
|
||||
|
||||
let cancelButton = null;
|
||||
if (!this.state.busy) {
|
||||
cancelButton = <button onClick={this._onCancel} autoFocus={true}>
|
||||
Cancel
|
||||
{_t("Cancel")}
|
||||
</button>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_DeactivateAccountDialog">
|
||||
<div className="mx_Dialog_title danger">
|
||||
Deactivate Account
|
||||
{_t("Deactivate Account")}
|
||||
</div>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>This will make your account permanently unusable. You will not be able to re-register the same user ID.</p>
|
||||
<p>{_t("This will make your account permanently unusable. You will not be able to re-register the same user ID.")}</p>
|
||||
|
||||
<p>This action is irreversible.</p>
|
||||
<p>{_t("This action is irreversible.")}</p>
|
||||
|
||||
<p>To continue, please enter your password.</p>
|
||||
<p>{_t("To continue, please enter your password.")}</p>
|
||||
|
||||
<p>Password:</p>
|
||||
<p>{_t("Password")}:</p>
|
||||
<input
|
||||
type="password"
|
||||
onChange={this._onPasswordFieldChange}
|
||||
|
|
77
src/components/views/dialogs/DeviceVerifyDialog.js
Normal file
77
src/components/views/dialogs/DeviceVerifyDialog.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from '../../../index';
|
||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default function DeviceVerifyDialog(props) {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
||||
const key = FormattingUtils.formatCryptoKey(props.device.getFingerprint());
|
||||
const body = (
|
||||
<div>
|
||||
<p>
|
||||
{_t("To verify that this device can be trusted, please contact its " +
|
||||
"owner using some other means (e.g. in person or a phone call) " +
|
||||
"and ask them whether the key they see in their User Settings " +
|
||||
"for this device matches the key below:")}
|
||||
</p>
|
||||
<div className="mx_UserSettings_cryptoSection">
|
||||
<ul>
|
||||
<li><label>{_t("Device name")}:</label> <span>{ props.device.getDisplayName() }</span></li>
|
||||
<li><label>{_t("Device ID")}:</label> <span><code>{ props.device.deviceId}</code></span></li>
|
||||
<li><label>{_t("Device key")}:</label> <span><code><b>{ key }</b></code></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
{_t("If it matches, press the verify button below. " +
|
||||
"If it doesn't, then someone else is intercepting this device " +
|
||||
"and you probably want to press the blacklist button instead.")}
|
||||
</p>
|
||||
<p>
|
||||
{_t("In future this verification process will be more sophisticated.")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
function onFinished(confirm) {
|
||||
if (confirm) {
|
||||
MatrixClientPeg.get().setDeviceVerified(
|
||||
props.userId, props.device.deviceId, true,
|
||||
);
|
||||
}
|
||||
props.onFinished(confirm);
|
||||
}
|
||||
|
||||
return (
|
||||
<QuestionDialog
|
||||
title={_t("Verify device")}
|
||||
description={body}
|
||||
button={_t("I verify that the keys match")}
|
||||
onFinished={onFinished}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
DeviceVerifyDialog.propTypes = {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
device: React.PropTypes.object.isRequired,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
};
|
|
@ -27,6 +27,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'ErrorDialog',
|
||||
|
@ -43,24 +44,30 @@ export default React.createClass({
|
|||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Error",
|
||||
description: "An error has occurred.",
|
||||
button: "OK",
|
||||
focus: true,
|
||||
title: null,
|
||||
description: null,
|
||||
button: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
if (this.props.focus) {
|
||||
this.refs.button.focus();
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
||||
title={this.props.title}>
|
||||
title={this.props.title || _t('Error')}>
|
||||
<div className="mx_Dialog_content">
|
||||
{this.props.description}
|
||||
{this.props.description || _t('An error has occurred.')}
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={this.props.focus}>
|
||||
{this.props.button}
|
||||
<button ref="button" className="mx_Dialog_primary" onClick={this.props.onFinished}>
|
||||
{this.props.button || _t('OK')}
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -15,11 +15,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
|
@ -46,12 +45,6 @@ export default React.createClass({
|
|||
title: React.PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Authentication",
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
authError: null,
|
||||
|
@ -85,7 +78,7 @@ export default React.createClass({
|
|||
<AccessibleButton onClick={this._onDismissClick}
|
||||
className="mx_UserSettings_button"
|
||||
>
|
||||
Dismiss
|
||||
{_t("Dismiss")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
|
@ -105,7 +98,7 @@ export default React.createClass({
|
|||
return (
|
||||
<BaseDialog className="mx_InteractiveAuthDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.state.authError ? 'Error' : this.props.title}
|
||||
title={this.state.authError ? 'Error' : (this.props.title || _t('Authentication'))}
|
||||
>
|
||||
{content}
|
||||
</BaseDialog>
|
||||
|
|
|
@ -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,17 +53,17 @@ 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}>
|
||||
Cancel
|
||||
{_t("Cancel")}
|
||||
</button>
|
||||
<button onClick={this.onRegisterClicked}>
|
||||
Register
|
||||
{_t("Register")}
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'QuestionDialog',
|
||||
|
@ -33,7 +34,6 @@ export default React.createClass({
|
|||
title: "",
|
||||
description: "",
|
||||
extraButtons: null,
|
||||
button: "OK",
|
||||
focus: true,
|
||||
hasCancelButton: true,
|
||||
};
|
||||
|
@ -51,7 +51,7 @@ export default React.createClass({
|
|||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const cancelButton = this.props.hasCancelButton ? (
|
||||
<button onClick={this.onCancel}>
|
||||
Cancel
|
||||
{_t("Cancel")}
|
||||
</button>
|
||||
) : null;
|
||||
return (
|
||||
|
@ -64,7 +64,7 @@ export default React.createClass({
|
|||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}>
|
||||
{this.props.button}
|
||||
{this.props.button || _t('OK')}
|
||||
</button>
|
||||
{this.props.extraButtons}
|
||||
{cancelButton}
|
||||
|
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
|||
import sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
|
||||
|
||||
export default React.createClass({
|
||||
|
@ -43,29 +44,32 @@ export default React.createClass({
|
|||
|
||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
bugreport = (
|
||||
<p>Otherwise, <a onClick={this._sendBugReport} href='#'>
|
||||
click here</a> to send a bug report.
|
||||
<p>
|
||||
{_tJsx(
|
||||
"Otherwise, <a>click here</a> to send a bug report.",
|
||||
/<a>(.*?)<\/a>/, (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{sub}</a>,
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
||||
title='Unable to restore session'>
|
||||
title={_t('Unable to restore session')}>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>We encountered an error trying to restore your previous session. If
|
||||
you continue, you will need to log in again, and encrypted chat
|
||||
history will be unreadable.</p>
|
||||
<p>{_t("We encountered an error trying to restore your previous session. If " +
|
||||
"you continue, you will need to log in again, and encrypted chat " +
|
||||
"history will be unreadable.")}</p>
|
||||
|
||||
<p>If you have previously used a more recent version of Riot, your session
|
||||
may be incompatible with this version. Close this window and return
|
||||
to the more recent version.</p>
|
||||
<p>{_t("If you have previously used a more recent version of Riot, your session " +
|
||||
"may be incompatible with this version. Close this window and return " +
|
||||
"to the more recent version.")}</p>
|
||||
|
||||
{bugreport}
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this._continueClicked}>
|
||||
Continue anyway
|
||||
{_t("Continue anyway")}
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
/**
|
||||
* Prompt the user to set a display name.
|
||||
*
|
||||
* On success, `onFinished(true, newDisplayName)` is called.
|
||||
*/
|
||||
export default React.createClass({
|
||||
displayName: 'SetDisplayNameDialog',
|
||||
propTypes: {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
currentDisplayName: React.PropTypes.string,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
if (this.props.currentDisplayName) {
|
||||
return { value: this.props.currentDisplayName };
|
||||
}
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
return { value : "Guest " + MatrixClientPeg.get().getUserIdLocalpart() };
|
||||
}
|
||||
else {
|
||||
return { value : MatrixClientPeg.get().getUserIdLocalpart() };
|
||||
}
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.refs.input_value.select();
|
||||
},
|
||||
|
||||
onValueChange: function(ev) {
|
||||
this.setState({
|
||||
value: ev.target.value
|
||||
});
|
||||
},
|
||||
|
||||
onFormSubmit: function(ev) {
|
||||
ev.preventDefault();
|
||||
this.props.onFinished(true, this.state.value);
|
||||
return false;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog className="mx_SetDisplayNameDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title="Set a Display Name"
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
Your display name is how you'll appear to others when you speak in rooms.<br/>
|
||||
What would you like it to be?
|
||||
</div>
|
||||
<form onSubmit={this.onFormSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
<input type="text" ref="input_value" value={this.state.value}
|
||||
autoFocus={true} onChange={this.onValueChange} size="30"
|
||||
className="mx_SetDisplayNameDialog_input"
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<input className="mx_Dialog_primary" type="submit" value="Set" />
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
294
src/components/views/dialogs/SetMxIdDialog.js
Normal file
294
src/components/views/dialogs/SetMxIdDialog.js
Normal file
|
@ -0,0 +1,294 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import q from 'q';
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import classnames from 'classnames';
|
||||
import KeyCode from '../../../KeyCode';
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
|
||||
// The amount of time to wait for further changes to the input username before
|
||||
// sending a request to the server
|
||||
const USERNAME_CHECK_DEBOUNCE_MS = 250;
|
||||
|
||||
/**
|
||||
* Prompt the user to set a display name.
|
||||
*
|
||||
* On success, `onFinished(true, newDisplayName)` is called.
|
||||
*/
|
||||
export default React.createClass({
|
||||
displayName: 'SetMxIdDialog',
|
||||
propTypes: {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
// Called when the user requests to register with a different homeserver
|
||||
onDifferentServerClicked: React.PropTypes.func.isRequired,
|
||||
// Called if the user wants to switch to login instead
|
||||
onLoginClick: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
// The entered username
|
||||
username: '',
|
||||
// Indicate ongoing work on the username
|
||||
usernameBusy: false,
|
||||
// Indicate error with username
|
||||
usernameError: '',
|
||||
// Assume the homeserver supports username checking until "M_UNRECOGNIZED"
|
||||
usernameCheckSupport: true,
|
||||
|
||||
// Whether the auth UI is currently being used
|
||||
doingUIAuth: false,
|
||||
// Indicate error with auth
|
||||
authError: '',
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.refs.input_value.select();
|
||||
|
||||
this._matrixClient = MatrixClientPeg.get();
|
||||
},
|
||||
|
||||
onValueChange: function(ev) {
|
||||
this.setState({
|
||||
username: ev.target.value,
|
||||
usernameBusy: true,
|
||||
usernameError: '',
|
||||
}, () => {
|
||||
if (!this.state.username || !this.state.usernameCheckSupport) {
|
||||
this.setState({
|
||||
usernameBusy: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Debounce the username check to limit number of requests sent
|
||||
if (this._usernameCheckTimeout) {
|
||||
clearTimeout(this._usernameCheckTimeout);
|
||||
}
|
||||
this._usernameCheckTimeout = setTimeout(() => {
|
||||
this._doUsernameCheck().finally(() => {
|
||||
this.setState({
|
||||
usernameBusy: false,
|
||||
});
|
||||
});
|
||||
}, USERNAME_CHECK_DEBOUNCE_MS);
|
||||
});
|
||||
},
|
||||
|
||||
onKeyUp: function(ev) {
|
||||
if (ev.keyCode === KeyCode.ENTER) {
|
||||
this.onSubmit();
|
||||
}
|
||||
},
|
||||
|
||||
onSubmit: function(ev) {
|
||||
this.setState({
|
||||
doingUIAuth: true,
|
||||
});
|
||||
},
|
||||
|
||||
_doUsernameCheck: function() {
|
||||
// Check if username is available
|
||||
return this._matrixClient.isUsernameAvailable(this.state.username).then(
|
||||
(isAvailable) => {
|
||||
if (isAvailable) {
|
||||
this.setState({usernameError: ''});
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
// Indicate whether the homeserver supports username checking
|
||||
const newState = {
|
||||
usernameCheckSupport: err.errcode !== "M_UNRECOGNIZED",
|
||||
};
|
||||
console.error('Error whilst checking username availability: ', err);
|
||||
switch (err.errcode) {
|
||||
case "M_USER_IN_USE":
|
||||
newState.usernameError = _t('Username not available');
|
||||
break;
|
||||
case "M_INVALID_USERNAME":
|
||||
newState.usernameError = _t(
|
||||
'Username invalid: %(errMessage)s',
|
||||
{ errMessage: err.message},
|
||||
);
|
||||
break;
|
||||
case "M_UNRECOGNIZED":
|
||||
// This homeserver doesn't support username checking, assume it's
|
||||
// fine and rely on the error appearing in registration step.
|
||||
newState.usernameError = '';
|
||||
break;
|
||||
case undefined:
|
||||
newState.usernameError = _t('Something went wrong!');
|
||||
break;
|
||||
default:
|
||||
newState.usernameError = _t(
|
||||
'An error occurred: %(error_string)s',
|
||||
{ error_string: err.message },
|
||||
);
|
||||
break;
|
||||
}
|
||||
this.setState(newState);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
_generatePassword: function() {
|
||||
return Math.random().toString(36).slice(2);
|
||||
},
|
||||
|
||||
_makeRegisterRequest: function(auth) {
|
||||
// Not upgrading - changing mxids
|
||||
const guestAccessToken = null;
|
||||
if (!this._generatedPassword) {
|
||||
this._generatedPassword = this._generatePassword();
|
||||
}
|
||||
return this._matrixClient.register(
|
||||
this.state.username,
|
||||
this._generatedPassword,
|
||||
undefined, // session id: included in the auth dict already
|
||||
auth,
|
||||
{},
|
||||
guestAccessToken,
|
||||
);
|
||||
},
|
||||
|
||||
_onUIAuthFinished: function(success, response) {
|
||||
this.setState({
|
||||
doingUIAuth: false,
|
||||
});
|
||||
|
||||
if (!success) {
|
||||
this.setState({ authError: response.message });
|
||||
return;
|
||||
}
|
||||
|
||||
// XXX Implement RTS /register here
|
||||
const teamToken = null;
|
||||
|
||||
this.props.onFinished(true, {
|
||||
userId: response.user_id,
|
||||
deviceId: response.device_id,
|
||||
homeserverUrl: this._matrixClient.getHomeserverUrl(),
|
||||
identityServerUrl: this._matrixClient.getIdentityServerUrl(),
|
||||
accessToken: response.access_token,
|
||||
password: this._generatedPassword,
|
||||
teamToken: teamToken,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
|
||||
let auth;
|
||||
if (this.state.doingUIAuth) {
|
||||
auth = <InteractiveAuth
|
||||
matrixClient={this._matrixClient}
|
||||
makeRequest={this._makeRegisterRequest}
|
||||
onAuthFinished={this._onUIAuthFinished}
|
||||
inputs={{}}
|
||||
poll={true}
|
||||
/>;
|
||||
}
|
||||
const inputClasses = classnames({
|
||||
"mx_SetMxIdDialog_input": true,
|
||||
"error": Boolean(this.state.usernameError),
|
||||
});
|
||||
|
||||
let usernameIndicator = null;
|
||||
let usernameBusyIndicator = null;
|
||||
if (this.state.usernameBusy) {
|
||||
usernameBusyIndicator = <Spinner w="24" h="24"/>;
|
||||
} else {
|
||||
const usernameAvailable = this.state.username &&
|
||||
this.state.usernameCheckSupport && !this.state.usernameError;
|
||||
const usernameIndicatorClasses = classnames({
|
||||
"error": Boolean(this.state.usernameError),
|
||||
"success": usernameAvailable,
|
||||
});
|
||||
usernameIndicator = <div className={usernameIndicatorClasses}>
|
||||
{ usernameAvailable ? _t('Username available') : this.state.usernameError }
|
||||
</div>;
|
||||
}
|
||||
|
||||
let authErrorIndicator = null;
|
||||
if (this.state.authError) {
|
||||
authErrorIndicator = <div className="error">
|
||||
{ this.state.authError }
|
||||
</div>;
|
||||
}
|
||||
const canContinue = this.state.username &&
|
||||
!this.state.usernameError &&
|
||||
!this.state.usernameBusy;
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_SetMxIdDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title="To get started, please pick a username!"
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_SetMxIdDialog_input_group">
|
||||
<input type="text" ref="input_value" value={this.state.username}
|
||||
autoFocus={true}
|
||||
onChange={this.onValueChange}
|
||||
onKeyUp={this.onKeyUp}
|
||||
size="30"
|
||||
className={inputClasses}
|
||||
/>
|
||||
{ usernameBusyIndicator }
|
||||
</div>
|
||||
{ usernameIndicator }
|
||||
<p>
|
||||
{ _tJsx(
|
||||
'This will be your account name on the <span></span> ' +
|
||||
'homeserver, or you can pick a <a>different server</a>.',
|
||||
[
|
||||
/<span><\/span>/,
|
||||
/<a>(.*?)<\/a>/,
|
||||
],
|
||||
[
|
||||
(sub) => <span>{this.props.homeserverUrl}</span>,
|
||||
(sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{sub}</a>,
|
||||
],
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{ _tJsx(
|
||||
'If you already have a Matrix account you can <a>log in</a> instead.',
|
||||
/<a>(.*?)<\/a>/,
|
||||
[(sub) => <a href="#" onClick={this.props.onLoginClick}>{sub}</a>],
|
||||
)}
|
||||
</p>
|
||||
{ auth }
|
||||
{ authErrorIndicator }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<input className="mx_Dialog_primary"
|
||||
type="submit"
|
||||
value={_t("Continue")}
|
||||
onClick={this.onSubmit}
|
||||
disabled={!canContinue}
|
||||
/>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -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}
|
||||
|
|
|
@ -16,10 +16,10 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import Resend from '../../../Resend';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
function DeviceListEntry(props) {
|
||||
const {userId, device} = props;
|
||||
|
@ -120,17 +120,17 @@ export default React.createClass({
|
|||
if (blacklistUnverified) {
|
||||
warning = (
|
||||
<h4>
|
||||
You are currently blacklisting unverified devices; to send
|
||||
messages to these devices you must verify them.
|
||||
{_t("You are currently blacklisting unverified devices; to send " +
|
||||
"messages to these devices you must verify them.")}
|
||||
</h4>
|
||||
);
|
||||
} else {
|
||||
warning = (
|
||||
<div>
|
||||
<p>
|
||||
We recommend you go through the verification process
|
||||
for each device to confirm they belong to their legitimate owner,
|
||||
but you can resend the message without verifying if you prefer.
|
||||
{_t("We recommend you go through the verification process " +
|
||||
"for each device to confirm they belong to their legitimate owner, " +
|
||||
"but you can resend the message without verifying if you prefer.")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
@ -145,14 +145,14 @@ export default React.createClass({
|
|||
console.log("UnknownDeviceDialog closed by escape");
|
||||
this.props.onFinished();
|
||||
}}
|
||||
title='Room contains unknown devices'
|
||||
title={_t('Room contains unknown devices')}
|
||||
>
|
||||
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
|
||||
<h4>
|
||||
This room contains devices that you haven't seen before.
|
||||
{_t('"%(RoomName)s" contains devices that you haven\'t seen before.', {RoomName: this.props.room.name})}
|
||||
</h4>
|
||||
{ warning }
|
||||
Unknown devices:
|
||||
{_t("Unknown devices")}:
|
||||
|
||||
<UnknownDeviceList devices={this.props.devices} />
|
||||
</GeminiScrollbar>
|
||||
|
@ -162,7 +162,7 @@ export default React.createClass({
|
|||
this.props.onFinished();
|
||||
Resend.resendUnsentEvents(this.props.room);
|
||||
}}>
|
||||
Send anyway
|
||||
{_t("Send anyway")}
|
||||
</button>
|
||||
<button className="mx_Dialog_primary" autoFocus={ true }
|
||||
onClick={() => {
|
||||
|
|
|
@ -32,6 +32,8 @@ export default function AccessibleButton(props) {
|
|||
};
|
||||
restProps.tabIndex = restProps.tabIndex || "0";
|
||||
restProps.role = "button";
|
||||
restProps.className = (restProps.className ? restProps.className + " " : "") +
|
||||
"mx_AccessibleButton";
|
||||
return React.createElement(element, restProps, children);
|
||||
}
|
||||
|
||||
|
|
84
src/components/views/elements/ActionButton.js
Normal file
84
src/components/views/elements/ActionButton.js
Normal file
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import AccessibleButton from './AccessibleButton';
|
||||
import dis from '../../../dispatcher';
|
||||
import sdk from '../../../index';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'RoleButton',
|
||||
|
||||
propTypes: {
|
||||
size: PropTypes.string,
|
||||
tooltip: PropTypes.bool,
|
||||
action: PropTypes.string.isRequired,
|
||||
mouseOverAction: PropTypes.string,
|
||||
label: PropTypes.string.isRequired,
|
||||
iconPath: PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
size: "25",
|
||||
tooltip: false,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
showTooltip: false,
|
||||
};
|
||||
},
|
||||
|
||||
_onClick: function(ev) {
|
||||
ev.stopPropagation();
|
||||
dis.dispatch({action: this.props.action});
|
||||
},
|
||||
|
||||
_onMouseEnter: function() {
|
||||
if (this.props.tooltip) this.setState({showTooltip: true});
|
||||
if (this.props.mouseOverAction) {
|
||||
dis.dispatch({action: this.props.mouseOverAction});
|
||||
}
|
||||
},
|
||||
|
||||
_onMouseLeave: function() {
|
||||
this.setState({showTooltip: false});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
let tooltip;
|
||||
if (this.state.showTooltip) {
|
||||
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
||||
tooltip = <RoomTooltip className="mx_RoleButton_tooltip" label={this.props.label} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AccessibleButton className="mx_RoleButton"
|
||||
onClick={this._onClick}
|
||||
onMouseEnter={this._onMouseEnter}
|
||||
onMouseLeave={this._onMouseLeave}
|
||||
>
|
||||
<TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />
|
||||
{tooltip}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -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" />
|
||||
|
|
|
@ -16,12 +16,11 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var classNames = require('classnames');
|
||||
var sdk = require("../../../index");
|
||||
var Invite = require("../../../Invite");
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var Avatar = require('../../../Avatar');
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import sdk from "../../../index";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
// React PropType definition for an object describing
|
||||
// an address that can be invited to a room (which
|
||||
|
@ -142,7 +141,7 @@ export default React.createClass({
|
|||
});
|
||||
|
||||
info = (
|
||||
<div className={unknownClasses}>Unknown Address</div>
|
||||
<div className={unknownClasses}>{_t("Unknown Address")}</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
40
src/components/views/elements/CreateRoomButton.js
Normal file
40
src/components/views/elements/CreateRoomButton.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
const CreateRoomButton = function(props) {
|
||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||
return (
|
||||
<ActionButton action="view_create_room"
|
||||
mouseOverAction={props.callout ? "callout_create_room" : null}
|
||||
label={ _t("Create new room") }
|
||||
iconPath="img/icons-create-room.svg"
|
||||
size={props.size}
|
||||
tooltip={props.tooltip}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
CreateRoomButton.propTypes = {
|
||||
size: PropTypes.string,
|
||||
tooltip: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default CreateRoomButton;
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
|||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'DeviceVerifyButtons',
|
||||
|
@ -50,42 +51,10 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
onVerifyClick: function() {
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: "Verify device",
|
||||
description: (
|
||||
<div>
|
||||
<p>
|
||||
To verify that this device can be trusted, please contact its
|
||||
owner using some other means (e.g. in person or a phone call)
|
||||
and ask them whether the key they see in their User Settings
|
||||
for this device matches the key below:
|
||||
</p>
|
||||
<div className="mx_UserSettings_cryptoSection">
|
||||
<ul>
|
||||
<li><label>Device name:</label> <span>{ this.state.device.getDisplayName() }</span></li>
|
||||
<li><label>Device ID:</label> <span><code>{ this.state.device.deviceId}</code></span></li>
|
||||
<li><label>Device key:</label> <span><code><b>{ this.state.device.getFingerprint() }</b></code></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
If it matches, press the verify button below.
|
||||
If it doesnt, then someone else is intercepting this device
|
||||
and you probably want to press the blacklist button instead.
|
||||
</p>
|
||||
<p>
|
||||
In future this verification process will be more sophisticated.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
button: "I verify that the keys match",
|
||||
onFinished: confirm=>{
|
||||
if (confirm) {
|
||||
MatrixClientPeg.get().setDeviceVerified(
|
||||
this.props.userId, this.state.device.deviceId, true
|
||||
);
|
||||
}
|
||||
},
|
||||
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
||||
Modal.createDialog(DeviceVerifyDialog, {
|
||||
userId: this.props.userId,
|
||||
device: this.state.device,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -114,14 +83,14 @@ export default React.createClass({
|
|||
blacklistButton = (
|
||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblacklist"
|
||||
onClick={this.onUnblacklistClick}>
|
||||
Unblacklist
|
||||
{_t("Unblacklist")}
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
blacklistButton = (
|
||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_blacklist"
|
||||
onClick={this.onBlacklistClick}>
|
||||
Blacklist
|
||||
{_t("Blacklist")}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
@ -130,14 +99,14 @@ export default React.createClass({
|
|||
verifyButton = (
|
||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
|
||||
onClick={this.onUnverifyClick}>
|
||||
Unverify
|
||||
{_t("Unverify")}
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
verifyButton = (
|
||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify"
|
||||
onClick={this.onVerifyClick}>
|
||||
Verify...
|
||||
{_t("Verify...")}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import AccessibleButton from './AccessibleButton';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
class MenuOption extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -114,8 +115,11 @@ export default class Dropdown extends React.Component {
|
|||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!nextProps.children || nextProps.children.length === 0) {
|
||||
return;
|
||||
}
|
||||
this._reindexChildren(nextProps.children);
|
||||
const firstChild = React.Children.toArray(nextProps.children)[0];
|
||||
const firstChild = nextProps.children[0];
|
||||
this.setState({
|
||||
highlightedOption: firstChild ? firstChild.key : null,
|
||||
});
|
||||
|
@ -149,10 +153,12 @@ export default class Dropdown extends React.Component {
|
|||
}
|
||||
|
||||
_onInputClick(ev) {
|
||||
this.setState({
|
||||
expanded: !this.state.expanded,
|
||||
});
|
||||
ev.preventDefault();
|
||||
if (!this.state.expanded) {
|
||||
this.setState({
|
||||
expanded: true,
|
||||
});
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
_onMenuOptionClick(dropdownKey) {
|
||||
|
@ -248,13 +254,10 @@ export default class Dropdown extends React.Component {
|
|||
</MenuOption>
|
||||
);
|
||||
});
|
||||
|
||||
if (!this.state.searchQuery) {
|
||||
options.push(
|
||||
<div key="_searchprompt" className="mx_Dropdown_searchPrompt">
|
||||
Type to search...
|
||||
</div>
|
||||
);
|
||||
if (options.length === 0) {
|
||||
return [<div key="0" className="mx_Dropdown_option">
|
||||
{_t("No results")}
|
||||
</div>];
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
@ -267,16 +270,20 @@ export default class Dropdown extends React.Component {
|
|||
|
||||
let menu;
|
||||
if (this.state.expanded) {
|
||||
currentValue = <input type="text" className="mx_Dropdown_option"
|
||||
ref={this._collectInputTextBox} onKeyPress={this._onInputKeyPress}
|
||||
onKeyUp={this._onInputKeyUp}
|
||||
onChange={this._onInputChange}
|
||||
value={this.state.searchQuery}
|
||||
/>;
|
||||
if (this.props.searchEnabled) {
|
||||
currentValue = <input type="text" className="mx_Dropdown_option"
|
||||
ref={this._collectInputTextBox} onKeyPress={this._onInputKeyPress}
|
||||
onKeyUp={this._onInputKeyUp}
|
||||
onChange={this._onInputChange}
|
||||
value={this.state.searchQuery}
|
||||
/>;
|
||||
}
|
||||
menu = <div className="mx_Dropdown_menu" style={menuStyle}>
|
||||
{this._getMenuOptions()}
|
||||
</div>;
|
||||
} else {
|
||||
}
|
||||
|
||||
if (!currentValue) {
|
||||
const selectedChild = this.props.getShortOption ?
|
||||
this.props.getShortOption(this.props.value) :
|
||||
this.childrenByKey[this.props.value];
|
||||
|
@ -313,6 +320,7 @@ Dropdown.propTypes = {
|
|||
onOptionChange: React.PropTypes.func.isRequired,
|
||||
// Called when the value of the search field changes
|
||||
onSearchChange: React.PropTypes.func,
|
||||
searchEnabled: React.PropTypes.bool,
|
||||
// Function that, given the key of an option, returns
|
||||
// a node representing that option to be displayed in the
|
||||
// box itself as the currently-selected option (ie. as
|
||||
|
|
39
src/components/views/elements/HomeButton.js
Normal file
39
src/components/views/elements/HomeButton.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
const HomeButton = function(props) {
|
||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||
return (
|
||||
<ActionButton action="view_home_page"
|
||||
label={ _t("Home") }
|
||||
iconPath="img/icons-home.svg"
|
||||
size={props.size}
|
||||
tooltip={props.tooltip}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
HomeButton.propTypes = {
|
||||
size: PropTypes.string,
|
||||
tooltip: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default HomeButton;
|
120
src/components/views/elements/LanguageDropdown.js
Normal file
120
src/components/views/elements/LanguageDropdown.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
Copyright 2017 Marcel Radzio (MTRNord)
|
||||
Copyright 2017 Vector Creations Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import * as languageHandler from '../../../languageHandler';
|
||||
|
||||
function languageMatchesSearchQuery(query, language) {
|
||||
if (language.label.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
|
||||
if (language.value.toUpperCase() == query.toUpperCase()) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export default class LanguageDropdown extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._onSearchChange = this._onSearchChange.bind(this);
|
||||
|
||||
this.state = {
|
||||
searchQuery: '',
|
||||
langs: null,
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
languageHandler.getAllLanguagesFromJson().then((langs) => {
|
||||
langs.sort(function(a, b){
|
||||
if(a.label < b.label) return -1;
|
||||
if(a.label > b.label) return 1;
|
||||
return 0;
|
||||
});
|
||||
this.setState({langs});
|
||||
}).catch(() => {
|
||||
this.setState({langs: ['en']});
|
||||
}).done();
|
||||
|
||||
if (!this.props.value) {
|
||||
// If no value is given, we start with the first
|
||||
// country selected, but our parent component
|
||||
// doesn't know this, therefore we do this.
|
||||
const _localSettings = UserSettingsStore.getLocalSettings();
|
||||
if (_localSettings.hasOwnProperty('language')) {
|
||||
this.props.onOptionChange(_localSettings.language);
|
||||
}else {
|
||||
const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser());
|
||||
this.props.onOptionChange(language);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onSearchChange(search) {
|
||||
this.setState({
|
||||
searchQuery: search,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.langs === null) {
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const Dropdown = sdk.getComponent('elements.Dropdown');
|
||||
|
||||
let displayedLanguages;
|
||||
if (this.state.searchQuery) {
|
||||
displayedLanguages = this.state.langs.filter((lang) => {
|
||||
return languageMatchesSearchQuery(this.state.searchQuery, lang);
|
||||
});
|
||||
} else {
|
||||
displayedLanguages = this.state.langs;
|
||||
}
|
||||
|
||||
const options = displayedLanguages.map((language) => {
|
||||
return <div key={language.value}>
|
||||
{language.label}
|
||||
</div>;
|
||||
});
|
||||
|
||||
// default value here too, otherwise we need to handle null / undefined
|
||||
// values between mounting and the initial value propgating
|
||||
let value = null;
|
||||
const _localSettings = UserSettingsStore.getLocalSettings();
|
||||
if (_localSettings.hasOwnProperty('language')) {
|
||||
value = this.props.value || _localSettings.language;
|
||||
} else {
|
||||
const language = navigator.language || navigator.userLanguage;
|
||||
value = this.props.value || language;
|
||||
}
|
||||
|
||||
return <Dropdown className={this.props.className}
|
||||
onOptionChange={this.props.onOptionChange} onSearchChange={this._onSearchChange}
|
||||
searchEnabled={true} value={value}
|
||||
>
|
||||
{options}
|
||||
</Dropdown>
|
||||
}
|
||||
}
|
||||
|
||||
LanguageDropdown.propTypes = {
|
||||
className: React.PropTypes.string,
|
||||
onOptionChange: React.PropTypes.func.isRequired,
|
||||
value: React.PropTypes.string,
|
||||
};
|
|
@ -14,7 +14,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
const MemberAvatar = require('../avatars/MemberAvatar.js');
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MemberEventListSummary',
|
||||
|
@ -110,9 +112,13 @@ module.exports = React.createClass({
|
|||
return null;
|
||||
}
|
||||
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
return (
|
||||
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
|
||||
{summaries.join(", ")}
|
||||
<EmojiText>
|
||||
{summaries.join(", ")}
|
||||
</EmojiText>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
|
@ -203,28 +209,146 @@ module.exports = React.createClass({
|
|||
* @param {boolean} plural whether there were multiple users undergoing the same
|
||||
* transition.
|
||||
* @param {number} repeats the number of times the transition was repeated in a row.
|
||||
* @returns {string} the written English equivalent of the transition.
|
||||
* @returns {string} the written Human Readable equivalent of the transition.
|
||||
*/
|
||||
_getDescriptionForTransition(t, plural, repeats) {
|
||||
const beConjugated = plural ? "were" : "was";
|
||||
const invitation = "their invitation" + (plural || (repeats > 1) ? "s" : "");
|
||||
|
||||
// The empty interpolations 'severalUsers' and 'oneUser'
|
||||
// are there only to show translators to non-English languages
|
||||
// that the verb is conjugated to plural or singular Subject.
|
||||
let res = null;
|
||||
const map = {
|
||||
"joined": "joined",
|
||||
"left": "left",
|
||||
"joined_and_left": "joined and left",
|
||||
"left_and_joined": "left and rejoined",
|
||||
"invite_reject": "rejected " + invitation,
|
||||
"invite_withdrawal": "had " + invitation + " withdrawn",
|
||||
"invited": beConjugated + " invited",
|
||||
"banned": beConjugated + " banned",
|
||||
"unbanned": beConjugated + " unbanned",
|
||||
"kicked": beConjugated + " kicked",
|
||||
};
|
||||
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 +376,12 @@ module.exports = React.createClass({
|
|||
return items[0];
|
||||
} else if (remaining) {
|
||||
items = items.slice(0, itemLimit);
|
||||
const other = " other" + (remaining > 1 ? "s" : "");
|
||||
return items.join(', ') + ' and ' + remaining + other;
|
||||
return (remaining > 1)
|
||||
? _t("%(items)s and %(remaining)s others", { items: items.join(', '), remaining: remaining } )
|
||||
: _t("%(items)s and one other", { items: items.join(', ') });
|
||||
} else {
|
||||
const lastItem = items.pop();
|
||||
return items.join(', ') + ' and ' + lastItem;
|
||||
return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem });
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -267,7 +392,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
});
|
||||
return (
|
||||
<span className="mx_MemberEventListSummary_avatars">
|
||||
<span className="mx_MemberEventListSummary_avatars" onClick={ this._toggleSummary }>
|
||||
{avatars}
|
||||
</span>
|
||||
);
|
||||
|
@ -289,7 +414,24 @@ module.exports = React.createClass({
|
|||
switch (e.mxEvent.getContent().membership) {
|
||||
case 'invite': return 'invited';
|
||||
case 'ban': return 'banned';
|
||||
case 'join': return 'joined';
|
||||
case 'join':
|
||||
if (e.mxEvent.getPrevContent().membership === 'join') {
|
||||
if (e.mxEvent.getContent().displayname !==
|
||||
e.mxEvent.getPrevContent().displayname)
|
||||
{
|
||||
return 'changed_name';
|
||||
}
|
||||
else if (e.mxEvent.getContent().avatar_url !==
|
||||
e.mxEvent.getPrevContent().avatar_url)
|
||||
{
|
||||
return 'changed_avatar';
|
||||
}
|
||||
// console.log("MELS ignoring duplicate membership join event");
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
return 'joined';
|
||||
}
|
||||
case 'leave':
|
||||
if (e.mxEvent.getSender() === e.mxEvent.getStateKey()) {
|
||||
switch (e.mxEvent.getPrevContent().membership) {
|
||||
|
@ -350,6 +492,7 @@ module.exports = React.createClass({
|
|||
|
||||
render: function() {
|
||||
const eventsToRender = this.props.events;
|
||||
const eventIds = eventsToRender.map(e => e.getId()).join(',');
|
||||
const fewEvents = eventsToRender.length < this.props.threshold;
|
||||
const expanded = this.state.expanded || fewEvents;
|
||||
|
||||
|
@ -360,7 +503,7 @@ module.exports = React.createClass({
|
|||
|
||||
if (fewEvents) {
|
||||
return (
|
||||
<div className="mx_MemberEventListSummary">
|
||||
<div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
|
||||
{expandedEvents}
|
||||
</div>
|
||||
);
|
||||
|
@ -418,7 +561,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="mx_MemberEventListSummary">
|
||||
<div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
|
||||
{toggleButton}
|
||||
{summaryContainer}
|
||||
{expanded ? <div className="mx_MemberEventListSummary_line"> </div> : null}
|
||||
|
|
|
@ -16,18 +16,12 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
|
||||
var roles = {
|
||||
0: 'User',
|
||||
50: 'Moderator',
|
||||
100: 'Admin',
|
||||
};
|
||||
import React from 'react';
|
||||
import * as Roles from '../../../Roles';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
var LEVEL_ROLE_MAP = {};
|
||||
var reverseRoles = {};
|
||||
Object.keys(roles).forEach(function(key) {
|
||||
reverseRoles[roles[key]] = key;
|
||||
});
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'PowerSelector',
|
||||
|
@ -49,9 +43,16 @@ module.exports = React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
custom: (roles[this.props.value] === undefined),
|
||||
custom: (LEVEL_ROLE_MAP[this.props.value] === undefined),
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
LEVEL_ROLE_MAP = Roles.levelRoleMap();
|
||||
Object.keys(LEVEL_ROLE_MAP).forEach(function(key) {
|
||||
reverseRoles[LEVEL_ROLE_MAP[key]] = key;
|
||||
});
|
||||
},
|
||||
|
||||
onSelectChange: function(event) {
|
||||
this.setState({ custom: event.target.value === "Custom" });
|
||||
|
@ -99,22 +100,34 @@ module.exports = React.createClass({
|
|||
selectValue = "Custom";
|
||||
}
|
||||
else {
|
||||
selectValue = roles[this.props.value] || "Custom";
|
||||
selectValue = LEVEL_ROLE_MAP[this.props.value] || "Custom";
|
||||
}
|
||||
var select;
|
||||
if (this.props.disabled) {
|
||||
select = <span>{ selectValue }</span>;
|
||||
}
|
||||
else {
|
||||
// Each level must have a definition in LEVEL_ROLE_MAP
|
||||
const levels = [0, 50, 100];
|
||||
let options = levels.map((level) => {
|
||||
return {
|
||||
value: LEVEL_ROLE_MAP[level],
|
||||
// Give a userDefault (users_default in the power event) of 0 but
|
||||
// because level !== undefined, this should never be used.
|
||||
text: Roles.textualPowerLevel(level, 0),
|
||||
}
|
||||
});
|
||||
options.push({ value: "Custom", text: _t("Custom level") });
|
||||
options = options.map((op) => {
|
||||
return <option value={op.value} key={op.value}>{op.text}</option>;
|
||||
});
|
||||
|
||||
select =
|
||||
<select ref="select"
|
||||
value={ this.props.controlled ? selectValue : undefined }
|
||||
defaultValue={ !this.props.controlled ? selectValue : undefined }
|
||||
onChange={ this.onSelectChange }>
|
||||
<option value="User">User (0)</option>
|
||||
<option value="Moderator">Moderator (50)</option>
|
||||
<option value="Admin">Admin (100)</option>
|
||||
<option value="Custom">Custom level</option>
|
||||
{ options }
|
||||
</select>;
|
||||
}
|
||||
|
||||
|
|
40
src/components/views/elements/RoomDirectoryButton.js
Normal file
40
src/components/views/elements/RoomDirectoryButton.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
const RoomDirectoryButton = function(props) {
|
||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||
return (
|
||||
<ActionButton action="view_room_directory"
|
||||
mouseOverAction={props.callout ? "callout_room_directory" : null}
|
||||
label={ _t("Room directory") }
|
||||
iconPath="img/icons-directory.svg"
|
||||
size={props.size}
|
||||
tooltip={props.tooltip}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
RoomDirectoryButton.propTypes = {
|
||||
size: PropTypes.string,
|
||||
tooltip: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default RoomDirectoryButton;
|
39
src/components/views/elements/SettingsButton.js
Normal file
39
src/components/views/elements/SettingsButton.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
const SettingsButton = function(props) {
|
||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||
return (
|
||||
<ActionButton action="view_user_settings"
|
||||
label={ _t("Settings") }
|
||||
iconPath="img/icons-settings.svg"
|
||||
size={props.size}
|
||||
tooltip={props.tooltip}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
SettingsButton.propTypes = {
|
||||
size: PropTypes.string,
|
||||
tooltip: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default SettingsButton;
|
40
src/components/views/elements/StartChatButton.js
Normal file
40
src/components/views/elements/StartChatButton.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
const StartChatButton = function(props) {
|
||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||
return (
|
||||
<ActionButton action="view_create_chat"
|
||||
mouseOverAction={props.callout ? "callout_start_chat" : null}
|
||||
label={ _t("Start chat") }
|
||||
iconPath="img/icons-people.svg"
|
||||
size={props.size}
|
||||
tooltip={props.tooltip}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
StartChatButton.propTypes = {
|
||||
size: PropTypes.string,
|
||||
tooltip: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default StartChatButton;
|
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
var React = require('react');
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'TruncatedList',
|
||||
|
@ -33,7 +34,7 @@ module.exports = React.createClass({
|
|||
truncateAt: 2,
|
||||
createOverflowElement: function(overflowCount, totalCount) {
|
||||
return (
|
||||
<div>And {overflowCount} more...</div>
|
||||
<div>{_t("And %(count)s more...", {count: overflowCount})}</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -16,7 +16,8 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'UserSelector',
|
||||
|
@ -59,9 +60,9 @@ module.exports = React.createClass({
|
|||
return <li key={user_id}>{user_id} - <span onClick={function() {self.removeUser(user_id);}}>X</span></li>;
|
||||
})}
|
||||
</ul>
|
||||
<input type="text" ref="user_id_input" defaultValue="" className="mx_UserSelector_userIdInput" placeholder="ex. @bob:example.com"/>
|
||||
<input type="text" ref="user_id_input" defaultValue="" className="mx_UserSelector_userIdInput" placeholder={_t("ex. @bob:example.com")}/>
|
||||
<button onClick={this.onAddUserId} className="mx_UserSelector_AddUserId">
|
||||
Add User
|
||||
{_t("Add User")}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -16,7 +16,9 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
var DIV_ID = 'mx_recaptcha';
|
||||
|
||||
/**
|
||||
|
@ -117,7 +119,7 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<div ref="recaptchaContainer">
|
||||
This Home Server would like to make sure you are not a robot
|
||||
{_t("This Home Server would like to make sure you are not a robot")}
|
||||
<br/>
|
||||
<div id={DIV_ID}></div>
|
||||
{error}
|
||||
|
|
|
@ -16,7 +16,8 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'CasLogin',
|
||||
|
@ -28,7 +29,7 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<button onClick={this.props.onSubmit}>Sign in with CAS</button>
|
||||
<button onClick={this.props.onSubmit}>{_t("Sign in with CAS")}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ import React from 'react';
|
|||
import sdk from '../../../index';
|
||||
|
||||
import { COUNTRIES } from '../../../phonenumber';
|
||||
import { charactersToImageNode } from '../../../HtmlUtils';
|
||||
|
||||
const COUNTRIES_BY_ISO2 = new Object(null);
|
||||
for (const c of COUNTRIES) {
|
||||
|
@ -27,22 +26,27 @@ for (const c of COUNTRIES) {
|
|||
}
|
||||
|
||||
function countryMatchesSearchQuery(query, country) {
|
||||
// Remove '+' if present (when searching for a prefix)
|
||||
if (query[0] === '+') {
|
||||
query = query.slice(1);
|
||||
}
|
||||
|
||||
if (country.name.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
|
||||
if (country.iso2 == query.toUpperCase()) return true;
|
||||
if (country.prefix == query) return true;
|
||||
if (country.prefix.indexOf(query) !== -1) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
const MAX_DISPLAYED_ROWS = 2;
|
||||
|
||||
export default class CountryDropdown extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._onSearchChange = this._onSearchChange.bind(this);
|
||||
this._onOptionChange = this._onOptionChange.bind(this);
|
||||
this._getShortOption = this._getShortOption.bind(this);
|
||||
|
||||
this.state = {
|
||||
searchQuery: '',
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
|
@ -50,7 +54,7 @@ export default class CountryDropdown extends React.Component {
|
|||
// If no value is given, we start with the first
|
||||
// country selected, but our parent component
|
||||
// doesn't know this, therefore we do this.
|
||||
this.props.onOptionChange(COUNTRIES[0].iso2);
|
||||
this.props.onOptionChange(COUNTRIES[0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,14 +64,26 @@ export default class CountryDropdown extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
_onOptionChange(iso2) {
|
||||
this.props.onOptionChange(COUNTRIES_BY_ISO2[iso2]);
|
||||
}
|
||||
|
||||
_flagImgForIso2(iso2) {
|
||||
// Unicode Regional Indicator Symbol letter 'A'
|
||||
const RIS_A = 0x1F1E6;
|
||||
const ASCII_A = 65;
|
||||
return charactersToImageNode(iso2,
|
||||
RIS_A + (iso2.charCodeAt(0) - ASCII_A),
|
||||
RIS_A + (iso2.charCodeAt(1) - ASCII_A),
|
||||
);
|
||||
return <img src={`flags/${iso2}.png`}/>;
|
||||
}
|
||||
|
||||
_getShortOption(iso2) {
|
||||
if (!this.props.isSmall) {
|
||||
return undefined;
|
||||
}
|
||||
let countryPrefix;
|
||||
if (this.props.showPrefix) {
|
||||
countryPrefix = '+' + COUNTRIES_BY_ISO2[iso2].prefix;
|
||||
}
|
||||
return <span>
|
||||
{ this._flagImgForIso2(iso2) }
|
||||
{ countryPrefix }
|
||||
</span>;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -93,14 +109,10 @@ export default class CountryDropdown extends React.Component {
|
|||
displayedCountries = COUNTRIES;
|
||||
}
|
||||
|
||||
if (displayedCountries.length > MAX_DISPLAYED_ROWS) {
|
||||
displayedCountries = displayedCountries.slice(0, MAX_DISPLAYED_ROWS);
|
||||
}
|
||||
|
||||
const options = displayedCountries.map((country) => {
|
||||
return <div key={country.iso2}>
|
||||
{this._flagImgForIso2(country.iso2)}
|
||||
{country.name}
|
||||
{country.name} <span>(+{country.prefix})</span>
|
||||
</div>;
|
||||
});
|
||||
|
||||
|
@ -108,18 +120,21 @@ export default class CountryDropdown extends React.Component {
|
|||
// values between mounting and the initial value propgating
|
||||
const value = this.props.value || COUNTRIES[0].iso2;
|
||||
|
||||
return <Dropdown className={this.props.className}
|
||||
onOptionChange={this.props.onOptionChange} onSearchChange={this._onSearchChange}
|
||||
menuWidth={298} getShortOption={this._flagImgForIso2}
|
||||
value={value}
|
||||
return <Dropdown className={this.props.className + " left_aligned"}
|
||||
onOptionChange={this._onOptionChange} onSearchChange={this._onSearchChange}
|
||||
menuWidth={298} getShortOption={this._getShortOption}
|
||||
value={value} searchEnabled={true}
|
||||
>
|
||||
{options}
|
||||
</Dropdown>
|
||||
</Dropdown>;
|
||||
}
|
||||
}
|
||||
|
||||
CountryDropdown.propTypes = {
|
||||
className: React.PropTypes.string,
|
||||
isSmall: React.PropTypes.bool,
|
||||
// if isSmall, show +44 in the selected value
|
||||
showPrefix: React.PropTypes.bool,
|
||||
onOptionChange: React.PropTypes.func.isRequired,
|
||||
value: React.PropTypes.string,
|
||||
};
|
||||
|
|
|
@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
var React = require("react");
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'CustomServerDialog',
|
||||
|
@ -23,24 +24,24 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_ErrorDialog">
|
||||
<div className="mx_Dialog_title">
|
||||
Custom Server Options
|
||||
{_t("Custom Server Options")}
|
||||
</div>
|
||||
<div className="mx_Dialog_content">
|
||||
<span>
|
||||
You can use the custom server options to sign into other Matrix
|
||||
servers by specifying a different Home server URL.
|
||||
{_t("You can use the custom server options to sign into other Matrix " +
|
||||
"servers by specifying a different Home server URL.")}
|
||||
<br/>
|
||||
This allows you to use this app with an existing Matrix account on
|
||||
a different home server.
|
||||
{_t("This allows you to use this app with an existing Matrix account on " +
|
||||
"a different home server.")}
|
||||
<br/>
|
||||
<br/>
|
||||
You can also set a custom identity server but this will typically prevent
|
||||
interaction with users based on email address.
|
||||
{_t("You can also set a custom identity server but this will typically prevent " +
|
||||
"interaction with users based on email address.")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.props.onFinished} autoFocus={true}>
|
||||
Dismiss
|
||||
{_t("Dismiss")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -20,6 +20,7 @@ import url from 'url';
|
|||
import classnames from 'classnames';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
/* This file contains a collection of components which are used by the
|
||||
* InteractiveAuth to prompt the user to enter the information needed
|
||||
|
@ -128,8 +129,8 @@ export const PasswordAuthEntry = React.createClass({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<p>To continue, please enter your password.</p>
|
||||
<p>Password:</p>
|
||||
<p>{_t("To continue, please enter your password.")}</p>
|
||||
<p>{_t("Password:")}</p>
|
||||
<form onSubmit={this._onSubmit}>
|
||||
<input
|
||||
ref="passwordField"
|
||||
|
@ -255,8 +256,8 @@ export const EmailIdentityAuthEntry = React.createClass({
|
|||
} else {
|
||||
return (
|
||||
<div>
|
||||
<p>An email has been sent to <i>{this.props.inputs.emailAddress}</i></p>
|
||||
<p>Please check your email to continue registration.</p>
|
||||
<p>{_t("An email has been sent to")} <i>{this.props.inputs.emailAddress}</i></p>
|
||||
<p>{_t("Please check your email to continue registration.")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -348,7 +349,7 @@ export const MsisdnAuthEntry = React.createClass({
|
|||
});
|
||||
} else {
|
||||
this.setState({
|
||||
errorText: "Token incorrect",
|
||||
errorText: _t("Token incorrect"),
|
||||
});
|
||||
}
|
||||
}).catch((e) => {
|
||||
|
@ -369,8 +370,8 @@ export const MsisdnAuthEntry = React.createClass({
|
|||
});
|
||||
return (
|
||||
<div>
|
||||
<p>A text message has been sent to +<i>{this._msisdn}</i></p>
|
||||
<p>Please enter the code it contains:</p>
|
||||
<p>{_t("A text message has been sent to")} +<i>{this._msisdn}</i></p>
|
||||
<p>{_t("Please enter the code it contains:")}</p>
|
||||
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
|
||||
<form onSubmit={this._onFormSubmit}>
|
||||
<input type="text"
|
||||
|
@ -379,7 +380,7 @@ export const MsisdnAuthEntry = React.createClass({
|
|||
onChange={this._onTokenChange}
|
||||
/>
|
||||
<br />
|
||||
<input type="submit" value="Submit"
|
||||
<input type="submit" value={_t("Submit")}
|
||||
className={submitClasses}
|
||||
disabled={!enableSubmit}
|
||||
/>
|
||||
|
@ -439,7 +440,7 @@ export const FallbackAuthEntry = React.createClass({
|
|||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<a onClick={this._onShowFallbackClick}>Start authentication</a>
|
||||
<a onClick={this._onShowFallbackClick}>{_t("Start authentication")}</a>
|
||||
<div className="error">
|
||||
{this.props.errorText}
|
||||
</div>
|
||||
|
|
|
@ -16,7 +16,8 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
import { _t } from '../../../languageHandler';
|
||||
import React from 'react';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'LoginFooter',
|
||||
|
@ -24,8 +25,8 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
return (
|
||||
<div className="mx_Login_links">
|
||||
<a href="https://matrix.org">powered by Matrix</a>
|
||||
<a href="https://matrix.org">{_t("powered by Matrix")}</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -16,64 +16,58 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {field_input_incorrect} from '../../../UiEffects';
|
||||
|
||||
|
||||
/**
|
||||
* A pure UI component which displays a username/password form.
|
||||
*/
|
||||
module.exports = React.createClass({displayName: 'PasswordLogin',
|
||||
propTypes: {
|
||||
onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
|
||||
onForgotPasswordClick: React.PropTypes.func, // fn()
|
||||
initialUsername: React.PropTypes.string,
|
||||
initialPhoneCountry: React.PropTypes.string,
|
||||
initialPhoneNumber: React.PropTypes.string,
|
||||
initialPassword: React.PropTypes.string,
|
||||
onUsernameChanged: React.PropTypes.func,
|
||||
onPhoneCountryChanged: React.PropTypes.func,
|
||||
onPhoneNumberChanged: React.PropTypes.func,
|
||||
onPasswordChanged: React.PropTypes.func,
|
||||
loginIncorrect: React.PropTypes.bool,
|
||||
},
|
||||
class PasswordLogin extends React.Component {
|
||||
static defaultProps = {
|
||||
onUsernameChanged: function() {},
|
||||
onPasswordChanged: function() {},
|
||||
onPhoneCountryChanged: function() {},
|
||||
onPhoneNumberChanged: function() {},
|
||||
initialUsername: "",
|
||||
initialPhoneCountry: "",
|
||||
initialPhoneNumber: "",
|
||||
initialPassword: "",
|
||||
loginIncorrect: false,
|
||||
hsDomain: "",
|
||||
}
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onUsernameChanged: function() {},
|
||||
onPasswordChanged: function() {},
|
||||
onPhoneCountryChanged: function() {},
|
||||
onPhoneNumberChanged: function() {},
|
||||
initialUsername: "",
|
||||
initialPhoneCountry: "",
|
||||
initialPhoneNumber: "",
|
||||
initialPassword: "",
|
||||
loginIncorrect: false,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
username: this.props.initialUsername,
|
||||
password: this.props.initialPassword,
|
||||
phoneCountry: this.props.initialPhoneCountry,
|
||||
phoneNumber: this.props.initialPhoneNumber,
|
||||
loginType: PasswordLogin.LOGIN_FIELD_MXID,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.onSubmitForm = this.onSubmitForm.bind(this);
|
||||
this.onUsernameChanged = this.onUsernameChanged.bind(this);
|
||||
this.onLoginTypeChange = this.onLoginTypeChange.bind(this);
|
||||
this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this);
|
||||
this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this);
|
||||
this.onPasswordChanged = this.onPasswordChanged.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._passwordField = null;
|
||||
},
|
||||
}
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.props.loginIncorrect && nextProps.loginIncorrect) {
|
||||
field_input_incorrect(this._passwordField);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onSubmitForm: function(ev) {
|
||||
onSubmitForm(ev) {
|
||||
ev.preventDefault();
|
||||
this.props.onSubmit(
|
||||
this.state.username,
|
||||
|
@ -81,35 +75,95 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||
this.state.phoneNumber,
|
||||
this.state.password,
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
onUsernameChanged: function(ev) {
|
||||
onUsernameChanged(ev) {
|
||||
this.setState({username: ev.target.value});
|
||||
this.props.onUsernameChanged(ev.target.value);
|
||||
},
|
||||
}
|
||||
|
||||
onPhoneCountryChanged: function(country) {
|
||||
this.setState({phoneCountry: country});
|
||||
this.props.onPhoneCountryChanged(country);
|
||||
},
|
||||
onLoginTypeChange(loginType) {
|
||||
this.setState({
|
||||
loginType: loginType,
|
||||
username: "" // Reset because email and username use the same state
|
||||
});
|
||||
}
|
||||
|
||||
onPhoneNumberChanged: function(ev) {
|
||||
onPhoneCountryChanged(country) {
|
||||
this.setState({
|
||||
phoneCountry: country.iso2,
|
||||
phonePrefix: country.prefix,
|
||||
});
|
||||
this.props.onPhoneCountryChanged(country.iso2);
|
||||
}
|
||||
|
||||
onPhoneNumberChanged(ev) {
|
||||
this.setState({phoneNumber: ev.target.value});
|
||||
this.props.onPhoneNumberChanged(ev.target.value);
|
||||
},
|
||||
}
|
||||
|
||||
onPasswordChanged: function(ev) {
|
||||
onPasswordChanged(ev) {
|
||||
this.setState({password: ev.target.value});
|
||||
this.props.onPasswordChanged(ev.target.value);
|
||||
},
|
||||
}
|
||||
|
||||
render: function() {
|
||||
renderLoginField(loginType) {
|
||||
switch(loginType) {
|
||||
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
||||
return <input
|
||||
className="mx_Login_field mx_Login_email"
|
||||
key="email_input"
|
||||
type="text"
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
onChange={this.onUsernameChanged}
|
||||
placeholder="joe@example.com"
|
||||
value={this.state.username}
|
||||
autoFocus
|
||||
/>;
|
||||
case PasswordLogin.LOGIN_FIELD_MXID:
|
||||
return <input
|
||||
className="mx_Login_field mx_Login_username"
|
||||
key="username_input"
|
||||
type="text"
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
onChange={this.onUsernameChanged}
|
||||
placeholder={_t('User name')}
|
||||
value={this.state.username}
|
||||
autoFocus
|
||||
/>;
|
||||
case PasswordLogin.LOGIN_FIELD_PHONE:
|
||||
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
||||
return <div className="mx_Login_phoneSection">
|
||||
<CountryDropdown
|
||||
className="mx_Login_phoneCountry mx_Login_field_prefix"
|
||||
ref="phone_country"
|
||||
onOptionChange={this.onPhoneCountryChanged}
|
||||
value={this.state.phoneCountry}
|
||||
isSmall={true}
|
||||
showPrefix={true}
|
||||
/>
|
||||
<input
|
||||
className="mx_Login_phoneNumberField mx_Login_field mx_Login_field_has_prefix"
|
||||
ref="phoneNumber"
|
||||
key="phone_input"
|
||||
type="text"
|
||||
name="phoneNumber"
|
||||
onChange={this.onPhoneNumberChanged}
|
||||
placeholder={_t("Mobile phone number")}
|
||||
value={this.state.phoneNumber}
|
||||
autoFocus
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
var forgotPasswordJsx;
|
||||
|
||||
if (this.props.onForgotPasswordClick) {
|
||||
forgotPasswordJsx = (
|
||||
<a className="mx_Login_forgot" onClick={this.props.onForgotPasswordClick} href="#">
|
||||
Forgot your password?
|
||||
{ _t('Forgot your password?') }
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
@ -119,38 +173,54 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||
error: this.props.loginIncorrect,
|
||||
});
|
||||
|
||||
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
||||
const Dropdown = sdk.getComponent('elements.Dropdown');
|
||||
|
||||
const loginField = this.renderLoginField(this.state.loginType);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
<input className="mx_Login_field mx_Login_username" type="text"
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
value={this.state.username} onChange={this.onUsernameChanged}
|
||||
placeholder="Email or user name" autoFocus />
|
||||
or
|
||||
<div className="mx_Login_phoneSection">
|
||||
<CountryDropdown ref="phone_country" onOptionChange={this.onPhoneCountryChanged}
|
||||
className="mx_Login_phoneCountry"
|
||||
value={this.state.phoneCountry}
|
||||
/>
|
||||
<input type="text" ref="phoneNumber"
|
||||
onChange={this.onPhoneNumberChanged}
|
||||
placeholder="Mobile phone number"
|
||||
className="mx_Login_phoneNumberField mx_Login_field"
|
||||
value={this.state.phoneNumber}
|
||||
name="phoneNumber"
|
||||
/>
|
||||
<div className="mx_Login_type_container">
|
||||
<label className="mx_Login_type_label">{ _t('Sign in with') }</label>
|
||||
<Dropdown
|
||||
className="mx_Login_type_dropdown"
|
||||
value={this.state.loginType}
|
||||
onOptionChange={this.onLoginTypeChange}>
|
||||
<span key={PasswordLogin.LOGIN_FIELD_MXID}>{ _t('my Matrix ID') }</span>
|
||||
<span key={PasswordLogin.LOGIN_FIELD_EMAIL}>{ _t('Email address') }</span>
|
||||
<span key={PasswordLogin.LOGIN_FIELD_PHONE}>{ _t('Phone') }</span>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<br />
|
||||
{loginField}
|
||||
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
|
||||
name="password"
|
||||
value={this.state.password} onChange={this.onPasswordChanged}
|
||||
placeholder="Password" />
|
||||
placeholder={ _t('Password') } />
|
||||
<br />
|
||||
{forgotPasswordJsx}
|
||||
<input className="mx_Login_submit" type="submit" value="Sign in" />
|
||||
<input className="mx_Login_submit" type="submit" value={ _t('Sign in') } />
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
PasswordLogin.LOGIN_FIELD_EMAIL = "login_field_email";
|
||||
PasswordLogin.LOGIN_FIELD_MXID = "login_field_mxid";
|
||||
PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone";
|
||||
|
||||
PasswordLogin.propTypes = {
|
||||
onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
|
||||
onForgotPasswordClick: React.PropTypes.func, // fn()
|
||||
initialUsername: React.PropTypes.string,
|
||||
initialPhoneCountry: React.PropTypes.string,
|
||||
initialPhoneNumber: React.PropTypes.string,
|
||||
initialPassword: React.PropTypes.string,
|
||||
onUsernameChanged: React.PropTypes.func,
|
||||
onPhoneCountryChanged: React.PropTypes.func,
|
||||
onPhoneNumberChanged: React.PropTypes.func,
|
||||
onPasswordChanged: React.PropTypes.func,
|
||||
loginIncorrect: React.PropTypes.bool,
|
||||
};
|
||||
|
||||
module.exports = PasswordLogin;
|
||||
|
|
|
@ -21,6 +21,7 @@ import sdk from '../../../index';
|
|||
import Email from '../../../email';
|
||||
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
const FIELD_EMAIL = 'field_email';
|
||||
const FIELD_PHONE_COUNTRY = 'field_phone_country';
|
||||
|
@ -100,27 +101,26 @@ module.exports = React.createClass({
|
|||
if (this.refs.email.value == '') {
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: "Warning",
|
||||
title: _t("Warning!"),
|
||||
description:
|
||||
<div>
|
||||
If you don't specify an email address, you won't be able to reset your password.<br/>
|
||||
Are you sure?
|
||||
{_t("If you don't specify an email address, you won't be able to reset your password. " +
|
||||
"Are you sure?")}
|
||||
</div>,
|
||||
button: "Continue",
|
||||
button: _t("Continue"),
|
||||
onFinished: function(confirmed) {
|
||||
if (confirmed) {
|
||||
self._doSubmit();
|
||||
self._doSubmit(ev);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
else {
|
||||
self._doSubmit();
|
||||
} else {
|
||||
self._doSubmit(ev);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_doSubmit: function() {
|
||||
_doSubmit: function(ev) {
|
||||
let email = this.refs.email.value.trim();
|
||||
var promise = this.props.onRegisterClick({
|
||||
username: this.refs.username.value.trim() || this.props.guestUsername,
|
||||
|
@ -270,7 +270,8 @@ module.exports = React.createClass({
|
|||
|
||||
_onPhoneCountryChange(newVal) {
|
||||
this.setState({
|
||||
phoneCountry: newVal,
|
||||
phoneCountry: newVal.iso2,
|
||||
phonePrefix: newVal.prefix,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -280,7 +281,7 @@ module.exports = React.createClass({
|
|||
const emailSection = (
|
||||
<div>
|
||||
<input type="text" ref="email"
|
||||
autoFocus={true} placeholder="Email address (optional)"
|
||||
autoFocus={true} placeholder={_t("Email address (optional)")}
|
||||
defaultValue={this.props.defaultEmail}
|
||||
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
|
||||
onBlur={function() {self.validateField(FIELD_EMAIL);}}
|
||||
|
@ -303,7 +304,7 @@ module.exports = React.createClass({
|
|||
} else if (this.state.selectedTeam) {
|
||||
belowEmailSection = (
|
||||
<p className="mx_Login_support">
|
||||
You are registering with {this.state.selectedTeam.name}
|
||||
{_t("You are registering with %(SelectedTeamName)s", {SelectedTeamName: this.state.selectedTeam.name})}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
@ -313,14 +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)"
|
||||
placeholder={_t("Mobile phone number (optional)")}
|
||||
defaultValue={this.props.defaultPhoneNumber}
|
||||
className={this._classForField(
|
||||
FIELD_PHONE_NUMBER, 'mx_Login_phoneNumberField', 'mx_Login_field'
|
||||
FIELD_PHONE_NUMBER,
|
||||
'mx_Login_phoneNumberField',
|
||||
'mx_Login_field',
|
||||
'mx_Login_field_has_prefix'
|
||||
)}
|
||||
onBlur={function() {self.validateField(FIELD_PHONE_NUMBER);}}
|
||||
value={self.state.phoneNumber}
|
||||
|
@ -329,12 +335,12 @@ module.exports = React.createClass({
|
|||
);
|
||||
|
||||
const registerButton = (
|
||||
<input className="mx_Login_submit" type="submit" value="Register" />
|
||||
<input className="mx_Login_submit" type="submit" value={_t("Register")} />
|
||||
);
|
||||
|
||||
let placeholderUserName = "User name";
|
||||
let placeholderUserName = _t("User name");
|
||||
if (this.props.guestUsername) {
|
||||
placeholderUserName += " (default: " + this.props.guestUsername + ")";
|
||||
placeholderUserName += " " + _t("(default: %(userName)s)", {userName: this.props.guestUsername});
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -349,15 +355,15 @@ module.exports = React.createClass({
|
|||
onBlur={function() {self.validateField(FIELD_USERNAME);}} />
|
||||
<br />
|
||||
{ this.props.guestUsername ?
|
||||
<div className="mx_Login_fieldLabel">Setting a user name will create a fresh account</div> : null
|
||||
<div className="mx_Login_fieldLabel">{_t("Setting a user name will create a fresh account")}</div> : null
|
||||
}
|
||||
<input type="password" ref="password"
|
||||
className={this._classForField(FIELD_PASSWORD, 'mx_Login_field')}
|
||||
onBlur={function() {self.validateField(FIELD_PASSWORD);}}
|
||||
placeholder="Password" defaultValue={this.props.defaultPassword} />
|
||||
placeholder={_t("Password")} defaultValue={this.props.defaultPassword} />
|
||||
<br />
|
||||
<input type="password" ref="passwordConfirm"
|
||||
placeholder="Confirm password"
|
||||
placeholder={_t("Confirm password")}
|
||||
className={this._classForField(FIELD_PASSWORD_CONFIRM, 'mx_Login_field')}
|
||||
onBlur={function() {self.validateField(FIELD_PASSWORD_CONFIRM);}}
|
||||
defaultValue={this.props.defaultPassword} />
|
||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
|||
var React = require('react');
|
||||
var Modal = require('../../../Modal');
|
||||
var sdk = require('../../../index');
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
/**
|
||||
* A pure UI component which displays the HS and IS to use.
|
||||
|
@ -27,8 +28,7 @@ module.exports = React.createClass({
|
|||
displayName: 'ServerConfig',
|
||||
|
||||
propTypes: {
|
||||
onHsUrlChanged: React.PropTypes.func,
|
||||
onIsUrlChanged: React.PropTypes.func,
|
||||
onServerConfigChange: React.PropTypes.func,
|
||||
|
||||
// default URLs are defined in config.json (or the hardcoded defaults)
|
||||
// they are used if the user has not overridden them with a custom URL.
|
||||
|
@ -50,8 +50,7 @@ module.exports = React.createClass({
|
|||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onHsUrlChanged: function() {},
|
||||
onIsUrlChanged: function() {},
|
||||
onServerConfigChange: function() {},
|
||||
customHsUrl: "",
|
||||
customIsUrl: "",
|
||||
withToggleButton: false,
|
||||
|
@ -75,7 +74,10 @@ module.exports = React.createClass({
|
|||
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() {
|
||||
var hsUrl = this.state.hs_url.trim().replace(/\/$/, "");
|
||||
if (hsUrl === "") hsUrl = this.props.defaultHsUrl;
|
||||
this.props.onHsUrlChanged(hsUrl);
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl : this.state.hs_url,
|
||||
isUrl : this.state.is_url,
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -85,7 +87,10 @@ module.exports = React.createClass({
|
|||
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() {
|
||||
var isUrl = this.state.is_url.trim().replace(/\/$/, "");
|
||||
if (isUrl === "") isUrl = this.props.defaultIsUrl;
|
||||
this.props.onIsUrlChanged(isUrl);
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl : this.state.hs_url,
|
||||
isUrl : this.state.is_url,
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -102,12 +107,16 @@ module.exports = React.createClass({
|
|||
configVisible: visible
|
||||
});
|
||||
if (!visible) {
|
||||
this.props.onHsUrlChanged(this.props.defaultHsUrl);
|
||||
this.props.onIsUrlChanged(this.props.defaultIsUrl);
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl : this.props.defaultHsUrl,
|
||||
isUrl : this.props.defaultIsUrl,
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.props.onHsUrlChanged(this.state.hs_url);
|
||||
this.props.onIsUrlChanged(this.state.is_url);
|
||||
this.props.onServerConfigChange({
|
||||
hsUrl : this.state.hs_url,
|
||||
isUrl : this.state.is_url,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -123,19 +132,19 @@ module.exports = React.createClass({
|
|||
var toggleButton;
|
||||
if (this.props.withToggleButton) {
|
||||
toggleButton = (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div className="mx_ServerConfig_selector">
|
||||
<input className="mx_Login_radio" id="basic" name="configVisible" type="radio"
|
||||
checked={!this.state.configVisible}
|
||||
onChange={this.onServerConfigVisibleChange.bind(this, false)} />
|
||||
<label className="mx_Login_label" htmlFor="basic">
|
||||
Default server
|
||||
{_t("Default server")}
|
||||
</label>
|
||||
|
||||
<input className="mx_Login_radio" id="advanced" name="configVisible" type="radio"
|
||||
checked={this.state.configVisible}
|
||||
onChange={this.onServerConfigVisibleChange.bind(this, true)} />
|
||||
<label className="mx_Login_label" htmlFor="advanced">
|
||||
Custom server
|
||||
{_t("Custom server")}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
|
@ -147,7 +156,7 @@ module.exports = React.createClass({
|
|||
<div style={serverConfigStyle}>
|
||||
<div className="mx_ServerConfig">
|
||||
<label className="mx_Login_label mx_ServerConfig_hslabel" htmlFor="hsurl">
|
||||
Home server URL
|
||||
{_t("Home server URL")}
|
||||
</label>
|
||||
<input className="mx_Login_field" id="hsurl" type="text"
|
||||
placeholder={this.props.defaultHsUrl}
|
||||
|
@ -155,7 +164,7 @@ module.exports = React.createClass({
|
|||
value={this.state.hs_url}
|
||||
onChange={this.onHomeserverChanged} />
|
||||
<label className="mx_Login_label mx_ServerConfig_islabel" htmlFor="isurl">
|
||||
Identity server URL
|
||||
{_t("Identity server URL")}
|
||||
</label>
|
||||
<input className="mx_Login_field" id="isurl" type="text"
|
||||
placeholder={this.props.defaultIsUrl}
|
||||
|
@ -163,7 +172,7 @@ module.exports = React.createClass({
|
|||
value={this.state.is_url}
|
||||
onChange={this.onIdentityServerChanged} />
|
||||
<a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}>
|
||||
What does this mean?
|
||||
{_t("What does this mean?")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -20,8 +20,8 @@ import React from 'react';
|
|||
import MFileBody from './MFileBody';
|
||||
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from '../../../index';
|
||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default class MAudioBody extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -77,7 +77,7 @@ export default class MAudioBody extends React.Component {
|
|||
return (
|
||||
<span className="mx_MAudioBody" ref="body">
|
||||
<img src="img/warning.svg" width="16" height="16"/>
|
||||
Error decrypting audio
|
||||
{_t("Error decrypting audio")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -20,10 +20,10 @@ import React from 'react';
|
|||
import filesize from 'filesize';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {decryptFile} from '../../../utils/DecryptFile';
|
||||
import Tinter from '../../../Tinter';
|
||||
import request from 'browser-request';
|
||||
import q from 'q';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
|
||||
|
@ -202,7 +202,7 @@ module.exports = React.createClass({
|
|||
* @return {string} the human readable link text for the attachment.
|
||||
*/
|
||||
presentableTextForFile: function(content) {
|
||||
var linkText = 'Attachment';
|
||||
var linkText = _t("Attachment");
|
||||
if (content.body && content.body.length > 0) {
|
||||
// The content body should be the name of the file including a
|
||||
// file extension.
|
||||
|
@ -261,7 +261,7 @@ module.exports = React.createClass({
|
|||
const content = this.props.mxEvent.getContent();
|
||||
const text = this.presentableTextForFile(content);
|
||||
const isEncrypted = content.file !== undefined;
|
||||
const fileName = content.body && content.body.length > 0 ? content.body : "Attachment";
|
||||
const fileName = content.body && content.body.length > 0 ? content.body : _t("Attachment");
|
||||
const contentUrl = this._getContentUrl();
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
|
@ -283,7 +283,8 @@ module.exports = React.createClass({
|
|||
}).catch((err) => {
|
||||
console.warn("Unable to decrypt attachment: ", err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
description: "Error decrypting attachment"
|
||||
title: _t("Error"),
|
||||
description: _t("Error decrypting attachment"),
|
||||
});
|
||||
}).finally(() => {
|
||||
decrypting = false;
|
||||
|
@ -295,7 +296,7 @@ module.exports = React.createClass({
|
|||
<span className="mx_MFileBody" ref="body">
|
||||
<div className="mx_MImageBody_download">
|
||||
<a href="javascript:void(0)" onClick={decrypt}>
|
||||
Decrypt {text}
|
||||
{ _t("Decrypt %(text)s", { text: text }) }
|
||||
</a>
|
||||
</div>
|
||||
</span>
|
||||
|
@ -314,7 +315,7 @@ module.exports = React.createClass({
|
|||
// We can't provide a Content-Disposition header like we would for HTTP.
|
||||
download: fileName,
|
||||
target: "_blank",
|
||||
textContent: "Download " + text,
|
||||
textContent: _t("Download %(text)s", { text: text }),
|
||||
}, "*");
|
||||
};
|
||||
|
||||
|
@ -346,7 +347,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<span className="mx_MFileBody">
|
||||
<div className="mx_MImageBody_download">
|
||||
<a className="mx_ImageBody_downloadLink" href={contentUrl} target="_blank">
|
||||
<a className="mx_ImageBody_downloadLink" href={contentUrl} download={fileName} target="_blank">
|
||||
{ fileName }
|
||||
</a>
|
||||
<div className="mx_MImageBody_size">
|
||||
|
@ -360,9 +361,9 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<span className="mx_MFileBody">
|
||||
<div className="mx_MImageBody_download">
|
||||
<a href={contentUrl} target="_blank" rel="noopener">
|
||||
<a href={contentUrl} download={fileName} target="_blank" rel="noopener">
|
||||
<img src={tintedDownloadImageURL} width="12" height="14" ref="downloadImage"/>
|
||||
Download {text}
|
||||
{ _t("Download %(text)s", { text: text }) }
|
||||
</a>
|
||||
</div>
|
||||
</span>
|
||||
|
@ -371,7 +372,7 @@ module.exports = React.createClass({
|
|||
} else {
|
||||
var extra = text ? (': ' + text) : '';
|
||||
return <span className="mx_MFileBody">
|
||||
Invalid file{extra}
|
||||
{ _t("Invalid file%(extra)s", { extra: extra }) }
|
||||
</span>;
|
||||
}
|
||||
},
|
||||
|
|
|
@ -26,6 +26,7 @@ import dis from '../../../dispatcher';
|
|||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||
import q from 'q';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MImageBody',
|
||||
|
@ -56,6 +57,7 @@ module.exports = React.createClass({
|
|||
const ImageView = sdk.getComponent("elements.ImageView");
|
||||
const params = {
|
||||
src: httpUrl,
|
||||
name: content.body && content.body.length > 0 ? content.body : _t('Attachment'),
|
||||
mxEvent: this.props.mxEvent,
|
||||
};
|
||||
|
||||
|
@ -190,7 +192,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<span className="mx_MImageBody" ref="body">
|
||||
<img src="img/warning.svg" width="16" height="16"/>
|
||||
Error decrypting image
|
||||
{_t("Error decrypting image")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -237,13 +239,13 @@ module.exports = React.createClass({
|
|||
} else if (content.body) {
|
||||
return (
|
||||
<span className="mx_MImageBody">
|
||||
Image '{content.body}' cannot be displayed.
|
||||
{_t("Image '%(Body)s' cannot be displayed.", {Body: content.body})}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span className="mx_MImageBody">
|
||||
This image cannot be displayed.
|
||||
{_t("This image cannot be displayed.")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,11 +19,10 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import MFileBody from './MFileBody';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Model from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||
import q from 'q';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MVideoBody',
|
||||
|
@ -128,7 +127,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<span className="mx_MVideoBody" ref="body">
|
||||
<img src="img/warning.svg" width="16" height="16"/>
|
||||
Error decrypting video
|
||||
{_t("Error decrypting video")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
92
src/components/views/messages/RoomAvatarEvent.js
Normal file
92
src/components/views/messages/RoomAvatarEvent.js
Normal file
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { ContentRepo } from 'matrix-js-sdk';
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomAvatarEvent',
|
||||
|
||||
propTypes: {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: React.PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
onAvatarClick: function(name) {
|
||||
var httpUrl = MatrixClientPeg.get().mxcUrlToHttp(this.props.mxEvent.getContent().url);
|
||||
var ImageView = sdk.getComponent("elements.ImageView");
|
||||
var params = {
|
||||
src: httpUrl,
|
||||
name: name,
|
||||
};
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var ev = this.props.mxEvent;
|
||||
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
|
||||
var room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
var name = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', {
|
||||
senderDisplayName: senderDisplayName,
|
||||
roomName: room ? room.name : '',
|
||||
});
|
||||
|
||||
if (!ev.getContent().url || ev.getContent().url.trim().length === 0) {
|
||||
return (
|
||||
<div className="mx_TextualEvent">
|
||||
{ _t('%(senderDisplayName)s removed the room avatar.', {senderDisplayName: senderDisplayName}) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var url = ContentRepo.getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
ev.getContent().url,
|
||||
14 * window.devicePixelRatio,
|
||||
14 * window.devicePixelRatio,
|
||||
'crop'
|
||||
);
|
||||
|
||||
// it sucks that _tJsx doesn't support normal _t substitutions :((
|
||||
return (
|
||||
<div className="mx_RoomAvatarEvent">
|
||||
{ _tJsx('$senderDisplayName changed the room avatar to <img/>',
|
||||
[
|
||||
/\$senderDisplayName/,
|
||||
/<img\/>/,
|
||||
],
|
||||
[
|
||||
(sub) => senderDisplayName,
|
||||
(sub) =>
|
||||
<AccessibleButton key="avatar" className="mx_RoomAvatarEvent_avatar"
|
||||
onClick={ this.onAvatarClick.bind(this, name) }>
|
||||
<BaseAvatar width={14} height={14} url={ url }
|
||||
name={ name } />
|
||||
</AccessibleButton>,
|
||||
]
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -30,7 +30,7 @@ export default function SenderProfile(props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<EmojiText className="mx_SenderProfile"
|
||||
<EmojiText className="mx_SenderProfile" dir="auto"
|
||||
onClick={props.onClick}>{`${name || ''} ${props.aux || ''}`}</EmojiText>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import ScalarAuthClient from '../../../ScalarAuthClient';
|
|||
import Modal from '../../../Modal';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import dis from '../../../dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
|
@ -62,6 +63,19 @@ module.exports = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
copyToClipboard: function(text) {
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = text;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
} catch (err) {
|
||||
console.log('Unable to copy');
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._unmounted = false;
|
||||
|
||||
|
@ -80,6 +94,14 @@ module.exports = React.createClass({
|
|||
}
|
||||
}, 10);
|
||||
}
|
||||
// add event handlers to the 'copy code' buttons
|
||||
const buttons = ReactDOM.findDOMNode(this).getElementsByClassName("mx_EventTile_copyButton");
|
||||
for (let i = 0; i < buttons.length; i++) {
|
||||
buttons[i].onclick = (e) => {
|
||||
const copyCode = buttons[i].parentNode.getElementsByTagName("code")[0];
|
||||
this.copyToClipboard(copyCode.textContent);
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -132,7 +154,8 @@ module.exports = React.createClass({
|
|||
links.push(node);
|
||||
}
|
||||
}
|
||||
else if (node.tagName === "PRE" || node.tagName === "CODE") {
|
||||
else if (node.tagName === "PRE" || node.tagName === "CODE" ||
|
||||
node.tagName === "BLOCKQUOTE") {
|
||||
continue;
|
||||
}
|
||||
else if (node.children && node.children.length) {
|
||||
|
@ -229,14 +252,14 @@ module.exports = React.createClass({
|
|||
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
let integrationsUrl = SdkConfig.get().integrations_ui_url;
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: "Add an Integration",
|
||||
title: _t("Add an Integration"),
|
||||
description:
|
||||
<div>
|
||||
You are about to be taken to a third-party site so you can
|
||||
authenticate your account for use with {integrationsUrl}.<br/>
|
||||
Do you wish to continue?
|
||||
{_t("You are about to be taken to a third-party site so you can " +
|
||||
"authenticate your account for use with %(integrationsUrl)s. " +
|
||||
"Do you wish to continue?", { integrationsUrl: integrationsUrl })}
|
||||
</div>,
|
||||
button: "Continue",
|
||||
button: _t("Continue"),
|
||||
onFinished: function(confirmed) {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
|
|
|
@ -24,6 +24,11 @@ import sdk from '../../../index';
|
|||
module.exports = React.createClass({
|
||||
displayName: 'TextualEvent',
|
||||
|
||||
propTypes: {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: React.PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
var text = TextForEvent.textForEvent(this.props.mxEvent);
|
||||
|
|
|
@ -16,7 +16,8 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'UnknownBody',
|
||||
|
@ -24,7 +25,7 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
const text = this.props.mxEvent.getContent().body;
|
||||
return (
|
||||
<span className="mx_UnknownBody" title="Redacted or unknown message type">
|
||||
<span className="mx_UnknownBody" title={_t("Removed or unknown message type")}>
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -229,7 +230,7 @@ module.exports = React.createClass({
|
|||
remote_aliases_section = (
|
||||
<div>
|
||||
<div className="mx_RoomSettings_aliasLabel">
|
||||
Remote addresses for this room:
|
||||
{_t("Remote addresses for this room:")}
|
||||
</div>
|
||||
<div className="mx_RoomSettings_aliasesTable">
|
||||
{ this.state.remoteDomains.map((domain, i) => {
|
||||
|
@ -254,13 +255,13 @@ module.exports = React.createClass({
|
|||
<div>
|
||||
<h3>Addresses</h3>
|
||||
<div className="mx_RoomSettings_aliasLabel">
|
||||
The main address for this room is: { canonical_alias_section }
|
||||
{ _t('The main address for this room is') }: { canonical_alias_section }
|
||||
</div>
|
||||
<div className="mx_RoomSettings_aliasLabel">
|
||||
{ (this.state.domainToAliases[localDomain] &&
|
||||
this.state.domainToAliases[localDomain].length > 0)
|
||||
? "Local addresses for this room:"
|
||||
: "This room has no local addresses" }
|
||||
? _t('Local addresses for this room:')
|
||||
: _t('This room has no local addresses') }
|
||||
</div>
|
||||
<div className="mx_RoomSettings_aliasesTable">
|
||||
{ (this.state.domainToAliases[localDomain] || []).map((alias, i) => {
|
||||
|
@ -268,7 +269,7 @@ module.exports = React.createClass({
|
|||
if (this.props.canSetAliases) {
|
||||
deleteButton = (
|
||||
<img src="img/cancel-small.svg" width="14" height="14"
|
||||
alt="Delete" onClick={ self.onAliasDeleted.bind(self, localDomain, i) } />
|
||||
alt={ _t('Delete') } onClick={ self.onAliasDeleted.bind(self, localDomain, i) } />
|
||||
);
|
||||
}
|
||||
return (
|
||||
|
@ -276,7 +277,7 @@ module.exports = React.createClass({
|
|||
<EditableText
|
||||
className="mx_RoomSettings_alias mx_RoomSettings_editable"
|
||||
placeholderClassName="mx_RoomSettings_aliasPlaceholder"
|
||||
placeholder={ "New address (e.g. #foo:" + localDomain + ")" }
|
||||
placeholder={ _t('New address (e.g. #foo:%(localDomain)s)', { localDomain: localDomain}) }
|
||||
blurToCancel={ false }
|
||||
onValueChanged={ self.onAliasChanged.bind(self, localDomain, i) }
|
||||
editable={ self.props.canSetAliases }
|
||||
|
@ -294,7 +295,7 @@ module.exports = React.createClass({
|
|||
ref="add_alias"
|
||||
className="mx_RoomSettings_alias mx_RoomSettings_editable"
|
||||
placeholderClassName="mx_RoomSettings_aliasPlaceholder"
|
||||
placeholder={ "New address (e.g. #foo:" + localDomain + ")" }
|
||||
placeholder={ _t('New address (e.g. #foo:%(localDomain)s)', { localDomain: localDomain}) }
|
||||
blurToCancel={ false }
|
||||
onValueChanged={ self.onAliasAdded } />
|
||||
<div className="mx_RoomSettings_addAlias mx_filterFlipColor">
|
||||
|
|
|
@ -21,6 +21,8 @@ var Tinter = require('../../../Tinter');
|
|||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var Modal = require("../../../Modal");
|
||||
|
||||
import dis from '../../../dispatcher';
|
||||
|
||||
var ROOM_COLORS = [
|
||||
// magic room default values courtesy of Ribot
|
||||
["#76cfa6", "#eaf5f0"],
|
||||
|
@ -86,11 +88,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
).catch(function(err) {
|
||||
if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') {
|
||||
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
||||
Modal.createDialog(NeedToRegisterDialog, {
|
||||
title: "Please Register",
|
||||
description: "Saving room color settings is only available to registered users"
|
||||
});
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ var MatrixClientPeg = require('../../../MatrixClientPeg');
|
|||
var sdk = require("../../../index");
|
||||
var Modal = require("../../../Modal");
|
||||
var UserSettingsStore = require('../../../UserSettingsStore');
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -120,35 +121,47 @@ module.exports = React.createClass({
|
|||
<input type="checkbox" ref="globalDisableUrlPreview"
|
||||
onChange={ this.onGlobalDisableUrlPreviewChange }
|
||||
checked={ this.state.globalDisableUrlPreview } />
|
||||
Disable URL previews by default for participants in this room
|
||||
{_t("Disable URL previews by default for participants in this room")}
|
||||
</label>;
|
||||
}
|
||||
else {
|
||||
disableRoomPreviewUrls =
|
||||
<label>
|
||||
URL previews are { this.state.globalDisableUrlPreview ? "disabled" : "enabled" } by default for participants in this room.
|
||||
{_t("URL previews are %(globalDisableUrlPreview)s by default for participants in this room.", {globalDisableUrlPreview: this.state.globalDisableUrlPreview ? _t("disabled") : _t("enabled")})}
|
||||
</label>;
|
||||
}
|
||||
|
||||
let urlPreviewText = null;
|
||||
if (UserSettingsStore.getUrlPreviewsDisabled()) {
|
||||
urlPreviewText = (
|
||||
_tJsx("You have <a>disabled</a> URL previews by default.", /<a>(.*?)<\/a>/, (sub)=><a href="#/settings">{sub}</a>)
|
||||
);
|
||||
}
|
||||
else {
|
||||
urlPreviewText = (
|
||||
_tJsx("You have <a>enabled</a> URL previews by default.", /<a>(.*?)<\/a>/, (sub)=><a href="#/settings">{sub}</a>)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomSettings_toggles">
|
||||
<h3>URL Previews</h3>
|
||||
<h3>{_t("URL Previews")}</h3>
|
||||
|
||||
<label>
|
||||
You have <a href="#/settings">{ UserSettingsStore.getUrlPreviewsDisabled() ? 'disabled' : 'enabled' }</a> URL previews by default.
|
||||
{urlPreviewText}
|
||||
</label>
|
||||
{ disableRoomPreviewUrls }
|
||||
<label>
|
||||
<input type="checkbox" ref="userEnableUrlPreview"
|
||||
onChange={ this.onUserEnableUrlPreviewChange }
|
||||
checked={ this.state.userEnableUrlPreview } />
|
||||
Enable URL previews for this room (affects only you)
|
||||
{_t("Enable URL previews for this room (affects only you)")}
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" ref="userDisableUrlPreview"
|
||||
onChange={ this.onUserDisableUrlPreviewChange }
|
||||
checked={ this.state.userDisableUrlPreview } />
|
||||
Disable URL previews for this room (affects only you)
|
||||
{_t("Disable URL previews for this room (affects only you)")}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@ import classNames from 'classnames';
|
|||
import flatMap from 'lodash/flatMap';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import sdk from '../../../index';
|
||||
import type {Completion, SelectionRange} from '../../../autocomplete/Autocompleter';
|
||||
import type {Completion} from '../../../autocomplete/Autocompleter';
|
||||
import Q from 'q';
|
||||
|
||||
import {getCompletions} from '../../../autocomplete/Autocompleter';
|
||||
|
|
|
@ -14,12 +14,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
const React = require('react');
|
||||
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
const sdk = require('../../../index');
|
||||
const dis = require("../../../dispatcher");
|
||||
const ObjectUtils = require('../../../ObjectUtils');
|
||||
const AppsDrawer = require('./AppsDrawer');
|
||||
import React from 'react';
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import sdk from '../../../index';
|
||||
import dis from "../../../dispatcher";
|
||||
import ObjectUtils from '../../../ObjectUtils';
|
||||
import AppsDrawer from './AppsDrawer';
|
||||
import { _t, _tJsx} from '../../../languageHandler';
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'AuxPanel',
|
||||
|
@ -77,10 +79,10 @@ module.exports = React.createClass({
|
|||
fileDropTarget = (
|
||||
<div className="mx_RoomView_fileDropTarget">
|
||||
<div className="mx_RoomView_fileDropTargetLabel"
|
||||
title="Drop File Here">
|
||||
title={_t("Drop File Here")}>
|
||||
<TintableSvg src="img/upload-big.svg" width="45" height="59"/>
|
||||
<br/>
|
||||
Drop file here to upload
|
||||
{_t("Drop file here to upload")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -91,17 +93,22 @@ module.exports = React.createClass({
|
|||
let supportedText;
|
||||
let joinText;
|
||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||
supportedText = " (unsupported)";
|
||||
supportedText = _t(" (unsupported)");
|
||||
} else {
|
||||
joinText = (<span>
|
||||
Join as <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}}
|
||||
href="#">voice</a> or <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video'); }}
|
||||
href="#">video</a>.
|
||||
{_tJsx(
|
||||
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
|
||||
[/<voiceText>(.*?)<\/voiceText>/, /<videoText>(.*?)<\/videoText>/],
|
||||
[
|
||||
(sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}} href="#">{sub}</a>,
|
||||
(sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video');}} href="#">{sub}</a>,
|
||||
]
|
||||
)}
|
||||
</span>);
|
||||
}
|
||||
conferenceCallNotification = (
|
||||
<div className="mx_RoomView_ongoingConfCallNotification">
|
||||
Ongoing conference call{ supportedText }. { joinText }
|
||||
{_t("Ongoing conference call%(supportedText)s. %(joinText)s", {supportedText: supportedText, joinText: joinText})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ var React = require('react');
|
|||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var sdk = require('../../../index');
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
|
||||
var PRESENCE_CLASS = {
|
||||
|
@ -115,7 +116,7 @@ module.exports = React.createClass({
|
|||
nameEl = (
|
||||
<div className="mx_EntityTile_details">
|
||||
<img className="mx_EntityTile_chevron" src="img/member_chevron.png" width="8" height="12"/>
|
||||
<EmojiText element="div" className="mx_EntityTile_name_hover">{name}</EmojiText>
|
||||
<EmojiText element="div" className="mx_EntityTile_name_hover" dir="auto">{name}</EmojiText>
|
||||
<PresenceLabel activeAgo={ activeAgo }
|
||||
currentlyActive={this.props.presenceCurrentlyActive}
|
||||
presenceState={this.props.presenceState} />
|
||||
|
@ -124,7 +125,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
else {
|
||||
nameEl = (
|
||||
<EmojiText element="div" className="mx_EntityTile_name">{name}</EmojiText>
|
||||
<EmojiText element="div" className="mx_EntityTile_name" dir="auto">{name}</EmojiText>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -140,10 +141,10 @@ module.exports = React.createClass({
|
|||
var power;
|
||||
var powerLevel = this.props.powerLevel;
|
||||
if (powerLevel >= 50 && powerLevel < 99) {
|
||||
power = <img src="img/mod.svg" className="mx_EntityTile_power" width="16" height="17" alt="Mod"/>;
|
||||
power = <img src="img/mod.svg" className="mx_EntityTile_power" width="16" height="17" alt={_t("Moderator")}/>;
|
||||
}
|
||||
if (powerLevel >= 99) {
|
||||
power = <img src="img/admin.svg" className="mx_EntityTile_power" width="16" height="17" alt="Admin"/>;
|
||||
power = <img src="img/admin.svg" className="mx_EntityTile_power" width="16" height="17" alt={_t("Admin")}/>;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
@ -36,10 +38,12 @@ var eventTileTypes = {
|
|||
'm.call.answer' : 'messages.TextualEvent',
|
||||
'm.call.hangup' : 'messages.TextualEvent',
|
||||
'm.room.name' : 'messages.TextualEvent',
|
||||
'm.room.avatar' : 'messages.RoomAvatarEvent',
|
||||
'm.room.topic' : 'messages.TextualEvent',
|
||||
'm.room.third_party_invite' : 'messages.TextualEvent',
|
||||
'm.room.history_visibility' : 'messages.TextualEvent',
|
||||
'm.room.encryption' : 'messages.TextualEvent',
|
||||
'm.room.power_levels' : 'messages.TextualEvent',
|
||||
};
|
||||
|
||||
var MAX_READ_AVATARS = 5;
|
||||
|
@ -128,6 +132,9 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
* for now.
|
||||
*/
|
||||
tileShape: React.PropTypes.string,
|
||||
|
||||
// show twelve hour timestamps
|
||||
isTwelveHour: React.PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -283,21 +290,17 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
},
|
||||
|
||||
getReadAvatars: function() {
|
||||
|
||||
// return early if there are no read receipts
|
||||
if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
|
||||
return (<span className="mx_EventTile_readAvatars"></span>);
|
||||
}
|
||||
|
||||
const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
|
||||
const avatars = [];
|
||||
const receiptOffset = 15;
|
||||
let left = 0;
|
||||
|
||||
// It's possible that the receipt was sent several days AFTER the event.
|
||||
// If it is, we want to display the complete date along with the HH:MM:SS,
|
||||
// rather than just HH:MM:SS.
|
||||
let dayAfterEvent = new Date(this.props.mxEvent.getTs());
|
||||
dayAfterEvent.setDate(dayAfterEvent.getDate() + 1);
|
||||
dayAfterEvent.setHours(0);
|
||||
dayAfterEvent.setMinutes(0);
|
||||
dayAfterEvent.setSeconds(0);
|
||||
let dayAfterEventTime = dayAfterEvent.getTime();
|
||||
|
||||
var receipts = this.props.readReceipts || [];
|
||||
for (var i = 0; i < receipts.length; ++i) {
|
||||
var receipt = receipts[i];
|
||||
|
@ -333,7 +336,6 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
suppressAnimation={this._suppressReadReceiptAnimation}
|
||||
onClick={this.toggleAllReadAvatars}
|
||||
timestamp={receipt.ts}
|
||||
showFullTimestamp={receipt.ts >= dayAfterEventTime}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -394,8 +396,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
var msgtype = content.msgtype;
|
||||
var eventType = this.props.mxEvent.getType();
|
||||
|
||||
// Info messages are basically information about commands processed on a
|
||||
// room, or emote messages
|
||||
// Info messages are basically information about commands processed on a room
|
||||
var isInfoMessage = (eventType !== 'm.room.message');
|
||||
|
||||
var EventTileType = sdk.getComponent(eventTileTypes[eventType]);
|
||||
|
@ -409,9 +410,10 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
var isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
|
||||
const isRedacted = (eventType === 'm.room.message') && this.props.isRedacted;
|
||||
|
||||
var classes = classNames({
|
||||
const classes = classNames({
|
||||
mx_EventTile: true,
|
||||
mx_EventTile_info: isInfoMessage,
|
||||
mx_EventTile_12hr: this.props.isTwelveHour,
|
||||
mx_EventTile_encrypting: this.props.eventSendStatus == 'encrypting',
|
||||
mx_EventTile_sending: isSending,
|
||||
mx_EventTile_notSent: this.props.eventSendStatus == 'not_sent',
|
||||
|
@ -423,7 +425,8 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
menu: this.state.menu,
|
||||
mx_EventTile_verified: this.state.verified == true,
|
||||
mx_EventTile_unverified: this.state.verified == false,
|
||||
mx_EventTile_bad: this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted',
|
||||
mx_EventTile_bad: msgtype === 'm.bad.encrypted',
|
||||
mx_EventTile_emote: msgtype === 'm.emote',
|
||||
mx_EventTile_redacted: isRedacted,
|
||||
});
|
||||
|
||||
|
@ -468,9 +471,9 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
if (needsSenderProfile) {
|
||||
let aux = null;
|
||||
if (!this.props.tileShape) {
|
||||
if (msgtype === 'm.image') aux = "sent an image";
|
||||
else if (msgtype === 'm.video') aux = "sent a video";
|
||||
else if (msgtype === 'm.file') aux = "uploaded a file";
|
||||
if (msgtype === 'm.image') aux = _t('sent an image');
|
||||
else if (msgtype === 'm.video') aux = _t('sent a video');
|
||||
else if (msgtype === 'm.file') aux = _t('uploaded a file');
|
||||
sender = <SenderProfile onClick={ this.onSenderProfileClick } mxEvent={this.props.mxEvent} aux={aux} />;
|
||||
}
|
||||
else {
|
||||
|
@ -478,36 +481,34 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
var editButton = (
|
||||
<span className="mx_EventTile_editButton" title="Options" onClick={this.onEditClicked} />
|
||||
const editButton = (
|
||||
<span className="mx_EventTile_editButton" title={ _t("Options") } onClick={this.onEditClicked} />
|
||||
);
|
||||
|
||||
var e2e;
|
||||
let e2e;
|
||||
// cosmetic padlocks:
|
||||
if ((e2eEnabled && this.props.eventSendStatus) || this.props.mxEvent.getType() === 'm.room.encryption') {
|
||||
e2e = <img style={{ cursor: 'initial', marginLeft: '-1px' }} className="mx_EventTile_e2eIcon" src="img/e2e-verified.svg" width="10" height="12" />;
|
||||
e2e = <img style={{ cursor: 'initial', marginLeft: '-1px' }} className="mx_EventTile_e2eIcon" alt={_t("Encrypted by a verified device")} src="img/e2e-verified.svg" width="10" height="12" />;
|
||||
}
|
||||
// real padlocks
|
||||
else if (this.props.mxEvent.isEncrypted() || (e2eEnabled && this.props.eventSendStatus)) {
|
||||
if (this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted') {
|
||||
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} />;
|
||||
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt={_t("Undecryptable")} src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} />;
|
||||
}
|
||||
else if (this.state.verified == true || (e2eEnabled && this.props.eventSendStatus)) {
|
||||
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" src="img/e2e-verified.svg" width="10" height="12"/>;
|
||||
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt={_t("Encrypted by a verified device")} src="img/e2e-verified.svg" width="10" height="12"/>;
|
||||
}
|
||||
else {
|
||||
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" src="img/e2e-warning.svg" width="15" height="12" style={{ marginLeft: "-2px" }}/>;
|
||||
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt={_t("Encrypted by an unverified device")} src="img/e2e-warning.svg" width="15" height="12" style={{ marginLeft: "-2px" }}/>;
|
||||
}
|
||||
}
|
||||
else if (e2eEnabled) {
|
||||
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" src="img/e2e-unencrypted.svg" width="12" height="12"/>;
|
||||
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt={_t("Unencrypted message")} src="img/e2e-unencrypted.svg" width="12" height="12"/>;
|
||||
}
|
||||
const timestamp = this.props.mxEvent.getTs() ?
|
||||
<MessageTimestamp ts={this.props.mxEvent.getTs()} /> : null;
|
||||
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
|
||||
|
||||
if (this.props.tileShape === "notif") {
|
||||
var room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
||||
|
||||
const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_EventTile_roomName">
|
||||
|
|
96
src/components/views/rooms/ForwardMessage.js
Normal file
96
src/components/views/rooms/ForwardMessage.js
Normal 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>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -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"];
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default class MemberDeviceInfo extends React.Component {
|
||||
render() {
|
||||
|
@ -25,19 +26,19 @@ export default class MemberDeviceInfo extends React.Component {
|
|||
if (this.props.device.isBlocked()) {
|
||||
indicator = (
|
||||
<div className="mx_MemberDeviceInfo_blacklisted">
|
||||
<img src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} alt="Blacklisted"/>
|
||||
<img src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} alt={_t("Blacklisted")}/>
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.device.isVerified()) {
|
||||
indicator = (
|
||||
<div className="mx_MemberDeviceInfo_verified">
|
||||
<img src="img/e2e-verified.svg" width="10" height="12" alt="Verified"/>
|
||||
<img src="img/e2e-verified.svg" width="10" height="12" alt={_t("Verified")}/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
indicator = (
|
||||
<div className="mx_MemberDeviceInfo_unverified">
|
||||
<img src="img/e2e-warning.svg" width="15" height="12" style={{ marginLeft: "-2px" }} alt="Unverified"/>
|
||||
<img src="img/e2e-warning.svg" width="15" height="12" style={{ marginLeft: "-2px" }} alt={_t("Unverified")}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -49,7 +50,7 @@ export default class MemberDeviceInfo extends React.Component {
|
|||
// add the deviceId as a titletext to help with debugging
|
||||
return (
|
||||
<div className="mx_MemberDeviceInfo"
|
||||
title={"device id: " + this.props.device.deviceId} >
|
||||
title={_t("device id: ") + this.props.device.deviceId} >
|
||||
<div className="mx_MemberDeviceInfo_deviceInfo">
|
||||
<div className="mx_MemberDeviceInfo_deviceId">
|
||||
{deviceName}
|
||||
|
|
|
@ -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,8 +242,8 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Kick error: " + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: "Failed to kick user",
|
||||
title: _t("Failed to kick"),
|
||||
description: ((err && err.message) ? err.message : "Operation failed"),
|
||||
});
|
||||
}
|
||||
).finally(()=>{
|
||||
|
@ -256,7 +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(()=>{
|
||||
|
@ -374,16 +375,12 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
console.log("Mod toggle success");
|
||||
}, function(err) {
|
||||
if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') {
|
||||
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
||||
Modal.createDialog(NeedToRegisterDialog, {
|
||||
title: "Please Register",
|
||||
description: "This action cannot be performed by a guest user. Please register to be able to do this."
|
||||
});
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
} else {
|
||||
console.error("Toggle moderator error:" + err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: "Failed to toggle moderator status",
|
||||
title: _t("Error"),
|
||||
description: _t("Failed to toggle moderator status"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -403,8 +400,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 +429,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 +578,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 +592,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<h3>Devices</h3>
|
||||
<h3>{ _t("Devices") }</h3>
|
||||
<div className="mx_MemberInfo_devices">
|
||||
{devComponents}
|
||||
</div>
|
||||
|
@ -644,11 +641,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 +658,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 +667,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 +679,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 +688,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
);
|
||||
}
|
||||
if (this.state.can.toggleMod) {
|
||||
var giveOpLabel = this.state.isTargetMod ? "Revoke Moderator" : "Make Moderator";
|
||||
var giveOpLabel = this.state.isTargetMod ? _t("Revoke Moderator") : _t("Make Moderator");
|
||||
giveModButton = <AccessibleButton className="mx_MemberInfo_field" onClick={this.onModToggle}>
|
||||
{giveOpLabel}
|
||||
</AccessibleButton>;
|
||||
|
@ -704,7 +701,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
if (kickButton || banButton || muteButton || giveModButton) {
|
||||
adminTools =
|
||||
<div>
|
||||
<h3>Admin tools</h3>
|
||||
<h3>{_t("Admin tools")}</h3>
|
||||
|
||||
<div className="mx_MemberInfo_buttons">
|
||||
{muteButton}
|
||||
|
@ -717,8 +714,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 +739,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>
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
@ -27,12 +28,6 @@ var CallHandler = require("../../../CallHandler");
|
|||
var Invite = require("../../../Invite");
|
||||
|
||||
var INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||
var SHARE_HISTORY_WARNING =
|
||||
<span>
|
||||
Newly invited users will see the history of this room. <br/>
|
||||
If you'd prefer invited users not to see messages that were sent before they joined, <br/>
|
||||
turn off, 'Share message history with new users' in the settings for this room.
|
||||
</span>;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MemberList',
|
||||
|
@ -207,7 +202,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} />
|
||||
|
@ -352,7 +349,7 @@ module.exports = React.createClass({
|
|||
if (invitedMemberTiles.length > 0) {
|
||||
invitedSection = (
|
||||
<div className="mx_MemberList_invited">
|
||||
<h2>Invited</h2>
|
||||
<h2>{ _t("Invited") }</h2>
|
||||
<div className="mx_MemberList_wrapper">
|
||||
{invitedMemberTiles}
|
||||
</div>
|
||||
|
@ -363,8 +360,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>
|
||||
);
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ var MatrixClientPeg = require('../../../MatrixClientPeg');
|
|||
var sdk = require('../../../index');
|
||||
var dis = require('../../../dispatcher');
|
||||
var Modal = require("../../../Modal");
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MemberTile',
|
||||
|
@ -63,7 +64,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getPowerLabel: function() {
|
||||
return this.props.member.userId + " (power " + this.props.member.powerLevel + ")";
|
||||
return _t("%(userName)s (power %(powerLevelNumber)s)", {userName: this.props.member.userId, powerLevelNumber: this.props.member.powerLevel});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
|
|
@ -13,14 +13,14 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
const React = require('react');
|
||||
|
||||
const CallHandler = require('../../../CallHandler');
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
const Modal = require('../../../Modal');
|
||||
const sdk = require('../../../index');
|
||||
const dis = require('../../../dispatcher');
|
||||
// import Autocomplete from './Autocomplete';
|
||||
var React = require('react');
|
||||
import { _t } from '../../../languageHandler';
|
||||
var CallHandler = require('../../../CallHandler');
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var Modal = require('../../../Modal');
|
||||
var sdk = require('../../../index');
|
||||
var dis = require('../../../dispatcher');
|
||||
import Autocomplete from './Autocomplete';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
|
@ -35,6 +35,7 @@ export default class MessageComposer extends React.Component {
|
|||
this.onShowAppsClick = this.onShowAppsClick.bind(this);
|
||||
this.onHideAppsClick = this.onHideAppsClick.bind(this);
|
||||
this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
|
||||
this.uploadFiles = this.uploadFiles.bind(this);
|
||||
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
|
||||
this.onInputContentChanged = this.onInputContentChanged.bind(this);
|
||||
this.onUpArrow = this.onUpArrow.bind(this);
|
||||
|
@ -45,6 +46,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: '',
|
||||
|
@ -52,7 +54,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),
|
||||
|
@ -65,12 +67,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) {
|
||||
|
@ -81,37 +92,33 @@ export default class MessageComposer extends React.Component {
|
|||
|
||||
onUploadClick(ev) {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
||||
Modal.createDialog(NeedToRegisterDialog, {
|
||||
title: "Please Register",
|
||||
description: "Guest users can't upload files. Please register to upload.",
|
||||
});
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
return;
|
||||
}
|
||||
|
||||
this.refs.uploadInput.click();
|
||||
}
|
||||
|
||||
onUploadFileSelected(files, isPasted) {
|
||||
if (!isPasted) {
|
||||
files = files.target.files;
|
||||
}
|
||||
onUploadFileSelected(files) {
|
||||
this.uploadFiles(files.target.files);
|
||||
}
|
||||
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
uploadFiles(files) {
|
||||
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
let TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
const fileList = [];
|
||||
for (let i=0; i<files.length; i++) {
|
||||
fileList.push(<li key={i}>
|
||||
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> {files[i].name || 'Attachment'}
|
||||
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> {files[i].name || _t('Attachment')}
|
||||
</li>);
|
||||
}
|
||||
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: "Upload Files",
|
||||
title: _t('Upload Files'),
|
||||
description: (
|
||||
<div>
|
||||
<p>Are you sure you want upload the following files?</p>
|
||||
<p>{ _t('Are you sure you want to upload the following files?') }</p>
|
||||
<ul style={{listStyle: 'none', textAlign: 'left'}}>
|
||||
{fileList}
|
||||
</ul>
|
||||
|
@ -245,11 +252,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';
|
||||
}
|
||||
|
||||
|
@ -262,15 +269,15 @@ 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>;
|
||||
}
|
||||
|
@ -297,7 +304,7 @@ export default class MessageComposer extends React.Component {
|
|||
// complex because of conference calls.
|
||||
const uploadButton = (
|
||||
<div key="controls_upload" className="mx_MessageComposer_upload"
|
||||
onClick={this.onUploadClick} title="Upload file">
|
||||
onClick={this.onUploadClick} title={ _t('Upload file') }>
|
||||
<TintableSvg src="img/icons-upload.svg" width="35" height="35"/>
|
||||
<input ref="uploadInput" type="file"
|
||||
style={uploadInputStyle}
|
||||
|
@ -308,7 +315,7 @@ export default class MessageComposer extends React.Component {
|
|||
|
||||
const formattingButton = (
|
||||
<img className="mx_MessageComposer_formatting"
|
||||
title="Show Text Formatting Toolbar"
|
||||
title={_t("Show Text Formatting Toolbar")}
|
||||
src="img/button-text-formatting.svg"
|
||||
onClick={this.onToggleFormattingClicked}
|
||||
style={{visibility: this.state.showFormatting ||
|
||||
|
@ -317,7 +324,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
|
||||
|
@ -329,7 +336,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} />,
|
||||
|
@ -344,8 +351,8 @@ 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
|
||||
</div>,
|
||||
{ _t('You do not have permission to post to this room') }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -373,7 +380,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`}
|
||||
|
@ -393,11 +400,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" />
|
||||
|
|
|
@ -28,11 +28,12 @@ import Q from 'q';
|
|||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
|
||||
import SlashCommands from '../../../SlashCommands';
|
||||
import KeyCode from '../../../KeyCode';
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
import dis from '../../../dispatcher';
|
||||
import KeyCode from '../../../KeyCode';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
|
||||
import * as RichText from '../../../RichText';
|
||||
|
@ -44,8 +45,6 @@ import {onSendMessageFailed} from './MessageComposerInputOld';
|
|||
|
||||
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
|
||||
|
||||
const KEY_M = 77;
|
||||
|
||||
const ZWS_CODE = 8203;
|
||||
const ZWS = String.fromCharCode(ZWS_CODE); // zero width space
|
||||
function stateToMarkdown(state) {
|
||||
|
@ -61,7 +60,7 @@ function stateToMarkdown(state) {
|
|||
export default class MessageComposerInput extends React.Component {
|
||||
static getKeyBinding(e: SyntheticKeyboardEvent): string {
|
||||
// C-m => Toggles between rich text and markdown modes
|
||||
if (e.keyCode === KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
|
||||
if (e.keyCode === KeyCode.KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
|
||||
return 'toggle-mode';
|
||||
}
|
||||
|
||||
|
@ -84,7 +83,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 +92,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
|
||||
|
@ -355,6 +353,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
|
||||
sendTyping(isTyping) {
|
||||
if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return;
|
||||
MatrixClientPeg.get().sendTyping(
|
||||
this.props.room.roomId,
|
||||
this.isTyping, TYPING_SERVER_TIMEOUT
|
||||
|
@ -476,10 +475,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));
|
||||
|
@ -508,8 +503,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: "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.")),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -517,8 +512,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;
|
||||
|
@ -541,9 +536,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;
|
||||
}
|
||||
|
@ -723,9 +718,10 @@ 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"
|
||||
dir="auto"
|
||||
placeholder={this.props.placeholder}
|
||||
editorState={this.state.editorState}
|
||||
onChange={this.onEditorContentChanged}
|
||||
|
@ -733,7 +729,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}
|
||||
|
@ -763,7 +759,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,
|
||||
|
|
|
@ -20,6 +20,8 @@ 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");
|
||||
var KeyCode = require("../../../KeyCode");
|
||||
|
@ -27,7 +29,6 @@ var Markdown = require("../../../Markdown");
|
|||
|
||||
var TYPING_USER_TIMEOUT = 10000;
|
||||
var TYPING_SERVER_TIMEOUT = 30000;
|
||||
var MARKDOWN_ENABLED = true;
|
||||
|
||||
export function onSendMessageFailed(err, room) {
|
||||
// XXX: temporary logging to try to diagnose
|
||||
|
@ -68,11 +69,15 @@ 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() {
|
||||
this.oldScrollHeight = 0;
|
||||
this.markdownEnabled = MARKDOWN_ENABLED;
|
||||
this.markdownEnabled = !UserSettingsStore.getSyncedSetting('disableMarkdown', false);
|
||||
|
||||
var self = this;
|
||||
this.sentHistory = {
|
||||
// The list of typed messages. Index 0 is more recent
|
||||
|
@ -290,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;
|
||||
|
@ -310,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: "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.")),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -319,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;
|
||||
|
@ -420,6 +425,7 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
sendTyping: function(isTyping) {
|
||||
if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return;
|
||||
MatrixClientPeg.get().sendTyping(
|
||||
this.props.room.roomId,
|
||||
this.isTyping, TYPING_SERVER_TIMEOUT
|
||||
|
@ -437,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 dir="auto" autoFocus ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder={this.props.placeholder}
|
||||
onPaste={this._onPaste}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,10 +16,10 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
import React from 'react';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var sdk = require('../../../index');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'PresenceLabel',
|
||||
|
@ -53,31 +53,30 @@ module.exports = React.createClass({
|
|||
var d = parseInt(t / (60 * 60 * 24));
|
||||
if (t < 60) {
|
||||
if (t < 0) {
|
||||
return "0s";
|
||||
return _t("for %(amount)ss", {amount: 0});
|
||||
}
|
||||
return s + "s";
|
||||
return _t("for %(amount)ss", {amount: s});
|
||||
}
|
||||
if (t < 60 * 60) {
|
||||
return m + "m";
|
||||
return _t("for %(amount)sm", {amount: m});
|
||||
}
|
||||
if (t < 24 * 60 * 60) {
|
||||
return h + "h";
|
||||
return _t("for %(amount)sh", {amount: h});
|
||||
}
|
||||
return d + "d ";
|
||||
return _t("for %(amount)sd", {amount: d});
|
||||
},
|
||||
|
||||
getPrettyPresence: function(presence) {
|
||||
if (presence === "online") return "Online";
|
||||
if (presence === "unavailable") return "Idle"; // XXX: is this actually right?
|
||||
if (presence === "offline") return "Offline";
|
||||
if (presence === "online") return _t("Online");
|
||||
if (presence === "unavailable") return _t("Idle"); // XXX: is this actually right?
|
||||
if (presence === "offline") return _t("Offline");
|
||||
return "Unknown";
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.props.activeAgo >= 0) {
|
||||
var ago = this.props.currentlyActive ? "now" : (this.getDuration(this.props.activeAgo) + " ago");
|
||||
// var ago = this.getDuration(this.props.activeAgo) + " ago";
|
||||
// if (this.props.currentlyActive) ago += " (now?)";
|
||||
let duration = this.getDuration(this.props.activeAgo);
|
||||
let ago = this.props.currentlyActive || !duration ? "" : duration;
|
||||
return (
|
||||
<div className="mx_PresenceLabel">
|
||||
{ this.getPrettyPresence(this.props.presenceState) } { ago }
|
||||
|
|
|
@ -23,6 +23,9 @@ var sdk = require('../../../index');
|
|||
|
||||
var Velociraptor = require('../../../Velociraptor');
|
||||
require('../../../VelocityBounce');
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
import DateUtils from '../../../DateUtils';
|
||||
|
||||
var bounce = false;
|
||||
try {
|
||||
|
@ -63,9 +66,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 +170,10 @@ module.exports = React.createClass({
|
|||
|
||||
let title;
|
||||
if (this.props.timestamp) {
|
||||
const prefix = "Seen by " + this.props.member.userId + " at ";
|
||||
let ts = new Date(this.props.timestamp);
|
||||
if (this.props.showFullTimestamp) {
|
||||
// "15/12/2016, 7:05:45 PM (@alice:matrix.org)"
|
||||
title = prefix + ts.toLocaleString();
|
||||
}
|
||||
else {
|
||||
// "7:05:45 PM (@alice:matrix.org)"
|
||||
title = prefix + ts.toLocaleTimeString();
|
||||
}
|
||||
title = _t(
|
||||
"Seen by %(userName)s at %(dateTime)s",
|
||||
{userName: this.props.member.userId, dateTime: DateUtils.formatDate(new Date(this.props.timestamp))}
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
|
@ -185,7 +188,14 @@ module.exports = React.createClass({
|
|||
'm.room.name', user_id
|
||||
);
|
||||
|
||||
save_button = <AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save</AccessibleButton>;
|
||||
save_button = (
|
||||
<AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>
|
||||
{_t("Save")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.onCancelClick) {
|
||||
cancel_button = <CancelButton onClick={this.props.onCancelClick}/>;
|
||||
}
|
||||
|
||||
|
@ -203,7 +213,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"> (~{ this.props.searchInfo.searchCount } results)</div>;
|
||||
searchStatus = <div className="mx_RoomHeader_searchStatus"> { _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) }</div>;
|
||||
}
|
||||
|
||||
// XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'...
|
||||
|
@ -218,17 +228,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 dir="auto" element="div" className={emojiTextClasses} title={roomName}>{ roomName }</EmojiText>
|
||||
{ searchStatus }
|
||||
</div>;
|
||||
}
|
||||
|
@ -245,7 +255,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
if (topic) {
|
||||
topic_el = <div className="mx_RoomHeader_topic" ref="topic" title={ topic }>{ topic }</div>;
|
||||
topic_el = <div className="mx_RoomHeader_topic" ref="topic" title={ topic } dir="auto">{ topic }</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -259,7 +269,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 }/>
|
||||
|
@ -278,7 +288,7 @@ module.exports = React.createClass({
|
|||
var settings_button;
|
||||
if (this.props.onSettingsClick) {
|
||||
settings_button =
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSettingsClick} title="Settings">
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSettingsClick} title={_t("Settings")}>
|
||||
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
@ -294,15 +304,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 +331,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>;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,6 +18,7 @@ limitations under the License.
|
|||
'use strict';
|
||||
var React = require("react");
|
||||
var ReactDOM = require("react-dom");
|
||||
import { _t } from '../../../languageHandler';
|
||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var CallHandler = require('../../../CallHandler');
|
||||
|
@ -29,7 +31,14 @@ var Rooms = require('../../../Rooms');
|
|||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
var Receipt = require('../../../utils/Receipt');
|
||||
|
||||
var HIDE_CONFERENCE_CHANS = true;
|
||||
const HIDE_CONFERENCE_CHANS = true;
|
||||
|
||||
const VERBS = {
|
||||
'm.favourite': 'favourite',
|
||||
'im.vector.fake.direct': 'tag direct chat',
|
||||
'im.vector.fake.recent': 'restore',
|
||||
'm.lowpriority': 'demote',
|
||||
};
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomList',
|
||||
|
@ -44,12 +53,15 @@ module.exports = React.createClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
isLoadingLeftRooms: false,
|
||||
totalRoomCount: null,
|
||||
lists: {},
|
||||
incomingCall: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.mounted = false;
|
||||
|
||||
var cli = MatrixClientPeg.get();
|
||||
cli.on("Room", this.onRoom);
|
||||
cli.on("deleteRoom", this.onDeleteRoom);
|
||||
|
@ -61,14 +73,22 @@ module.exports = React.createClass({
|
|||
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||
cli.on("accountData", this.onAccountData);
|
||||
|
||||
var s = this.getRoomLists();
|
||||
this.setState(s);
|
||||
this.refreshRoomList();
|
||||
|
||||
// order of the sublists
|
||||
//this.listOrder = [];
|
||||
|
||||
// loop count to stop a stack overflow if the user keeps waggling the
|
||||
// mouse for >30s in a row, or if running under mocha
|
||||
this._delayedRefreshRoomListLoopCount = 0
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
// Initialise the stickyHeaders when the component is created
|
||||
this._updateStickyHeaders(true);
|
||||
|
||||
this.mounted = true;
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
|
@ -106,6 +126,8 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this.mounted = false;
|
||||
|
||||
dis.unregister(this.dispatcherRef);
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
||||
|
@ -196,30 +218,33 @@ module.exports = React.createClass({
|
|||
}, 500),
|
||||
|
||||
refreshRoomList: function() {
|
||||
// console.log("DEBUG: Refresh room list delta=%s ms",
|
||||
// (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs))
|
||||
// );
|
||||
// TODO: ideally we'd calculate this once at start, and then maintain
|
||||
// any changes to it incrementally, updating the appropriate sublists
|
||||
// as needed.
|
||||
// Alternatively we'd do something magical with Immutable.js or similar.
|
||||
const lists = this.getRoomLists();
|
||||
let totalRooms = 0;
|
||||
for (const l of Object.values(lists)) {
|
||||
totalRooms += l.length;
|
||||
}
|
||||
this.setState({
|
||||
lists: this.getRoomLists(),
|
||||
totalRoomCount: totalRooms,
|
||||
});
|
||||
|
||||
// TODO: rather than bluntly regenerating and re-sorting everything
|
||||
// every time we see any kind of room change from the JS SDK
|
||||
// we could do incremental updates on our copy of the state
|
||||
// based on the room which has actually changed. This would stop
|
||||
// us re-rendering all the sublists every time anything changes anywhere
|
||||
// in the state of the client.
|
||||
this.setState(this.getRoomLists());
|
||||
this._lastRefreshRoomListTs = Date.now();
|
||||
// this._lastRefreshRoomListTs = Date.now();
|
||||
},
|
||||
|
||||
getRoomLists: function() {
|
||||
var self = this;
|
||||
var s = { lists: {} };
|
||||
const lists = {};
|
||||
|
||||
s.lists["im.vector.fake.invite"] = [];
|
||||
s.lists["m.favourite"] = [];
|
||||
s.lists["im.vector.fake.recent"] = [];
|
||||
s.lists["im.vector.fake.direct"] = [];
|
||||
s.lists["m.lowpriority"] = [];
|
||||
s.lists["im.vector.fake.archived"] = [];
|
||||
lists["im.vector.fake.invite"] = [];
|
||||
lists["m.favourite"] = [];
|
||||
lists["im.vector.fake.recent"] = [];
|
||||
lists["im.vector.fake.direct"] = [];
|
||||
lists["m.lowpriority"] = [];
|
||||
lists["im.vector.fake.archived"] = [];
|
||||
|
||||
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
|
||||
|
||||
|
@ -233,7 +258,7 @@ module.exports = React.createClass({
|
|||
// ", prevMembership = " + me.events.member.getPrevContent().membership);
|
||||
|
||||
if (me.membership == "invite") {
|
||||
s.lists["im.vector.fake.invite"].push(room);
|
||||
lists["im.vector.fake.invite"].push(room);
|
||||
}
|
||||
else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) {
|
||||
// skip past this room & don't put it in any lists
|
||||
|
@ -247,69 +272,48 @@ module.exports = React.createClass({
|
|||
if (tagNames.length) {
|
||||
for (var i = 0; i < tagNames.length; i++) {
|
||||
var tagName = tagNames[i];
|
||||
s.lists[tagName] = s.lists[tagName] || [];
|
||||
s.lists[tagNames[i]].push(room);
|
||||
lists[tagName] = lists[tagName] || [];
|
||||
lists[tagName].push(room);
|
||||
}
|
||||
}
|
||||
else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
|
||||
// "Direct Message" rooms (that we're still in and that aren't otherwise tagged)
|
||||
s.lists["im.vector.fake.direct"].push(room);
|
||||
lists["im.vector.fake.direct"].push(room);
|
||||
}
|
||||
else {
|
||||
s.lists["im.vector.fake.recent"].push(room);
|
||||
lists["im.vector.fake.recent"].push(room);
|
||||
}
|
||||
}
|
||||
else if (me.membership === "leave") {
|
||||
s.lists["im.vector.fake.archived"].push(room);
|
||||
lists["im.vector.fake.archived"].push(room);
|
||||
}
|
||||
else {
|
||||
console.error("unrecognised membership: " + me.membership + " - this should never happen");
|
||||
}
|
||||
});
|
||||
|
||||
if (s.lists["im.vector.fake.direct"].length == 0 &&
|
||||
MatrixClientPeg.get().getAccountData('m.direct') === undefined &&
|
||||
!MatrixClientPeg.get().isGuest())
|
||||
{
|
||||
// scan through the 'recents' list for any rooms which look like DM rooms
|
||||
// and make them DM rooms
|
||||
const oldRecents = s.lists["im.vector.fake.recent"];
|
||||
s.lists["im.vector.fake.recent"] = [];
|
||||
|
||||
for (const room of oldRecents) {
|
||||
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
|
||||
if (me && Rooms.looksLikeDirectMessageRoom(room, me)) {
|
||||
s.lists["im.vector.fake.direct"].push(room);
|
||||
} else {
|
||||
s.lists["im.vector.fake.recent"].push(room);
|
||||
}
|
||||
}
|
||||
|
||||
// save these new guessed DM rooms into the account data
|
||||
const newMDirectEvent = {};
|
||||
for (const room of s.lists["im.vector.fake.direct"]) {
|
||||
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
const otherPerson = Rooms.getOnlyOtherMember(room, me);
|
||||
if (!otherPerson) continue;
|
||||
|
||||
const roomList = newMDirectEvent[otherPerson.userId] || [];
|
||||
roomList.push(room.roomId);
|
||||
newMDirectEvent[otherPerson.userId] = roomList;
|
||||
}
|
||||
|
||||
// if this fails, fine, we'll just do the same thing next time we get the room lists
|
||||
MatrixClientPeg.get().setAccountData('m.direct', newMDirectEvent).done();
|
||||
}
|
||||
|
||||
//console.log("calculated new roomLists; im.vector.fake.recent = " + s.lists["im.vector.fake.recent"]);
|
||||
|
||||
// we actually apply the sorting to this when receiving the prop in RoomSubLists.
|
||||
|
||||
return s;
|
||||
// we'll need this when we get to iterating through lists programatically - e.g. ctrl-shift-up/down
|
||||
/*
|
||||
this.listOrder = [
|
||||
"im.vector.fake.invite",
|
||||
"m.favourite",
|
||||
"im.vector.fake.recent",
|
||||
"im.vector.fake.direct",
|
||||
Object.keys(otherTagNames).filter(tagName=>{
|
||||
return (!tagName.match(/^m\.(favourite|lowpriority)$/));
|
||||
}).sort(),
|
||||
"m.lowpriority",
|
||||
"im.vector.fake.archived"
|
||||
];
|
||||
*/
|
||||
|
||||
return lists;
|
||||
},
|
||||
|
||||
_getScrollNode: function() {
|
||||
if (!this.mounted) return null;
|
||||
var panel = ReactDOM.findDOMNode(this);
|
||||
if (!panel) return null;
|
||||
|
||||
|
@ -337,10 +341,11 @@ module.exports = React.createClass({
|
|||
var incomingCallBox = document.getElementById("incomingCallBox");
|
||||
if (incomingCallBox && incomingCallBox.parentElement) {
|
||||
var scrollArea = this._getScrollNode();
|
||||
if (!scrollArea) return;
|
||||
// Use the offset of the top of the scroll area from the window
|
||||
// as this is used to calculate the CSS fixed top position for the stickies
|
||||
var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
|
||||
// Use the offset of the top of the componet from the window
|
||||
// Use the offset of the top of the component from the window
|
||||
// as this is used to calculate the CSS fixed top position for the stickies
|
||||
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
|
||||
|
||||
|
@ -360,6 +365,7 @@ module.exports = React.createClass({
|
|||
// properly through React
|
||||
_initAndPositionStickyHeaders: function(initialise, scrollToPosition) {
|
||||
var scrollArea = this._getScrollNode();
|
||||
if (!scrollArea) return;
|
||||
// Use the offset of the top of the scroll area from the window
|
||||
// as this is used to calculate the CSS fixed top position for the stickies
|
||||
var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
|
||||
|
@ -457,16 +463,71 @@ module.exports = React.createClass({
|
|||
this.refs.gemscroll.forceUpdate();
|
||||
},
|
||||
|
||||
_getEmptyContent: function(section) {
|
||||
const RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget');
|
||||
|
||||
if (this.props.collapsed) {
|
||||
return <RoomDropTarget label="" />;
|
||||
}
|
||||
|
||||
const StartChatButton = sdk.getComponent('elements.StartChatButton');
|
||||
const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
|
||||
const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
|
||||
|
||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
switch (section) {
|
||||
case 'im.vector.fake.direct':
|
||||
return <div className="mx_RoomList_emptySubListTip">
|
||||
Press
|
||||
<StartChatButton size="16" callout={true}/>
|
||||
to start a chat with someone
|
||||
</div>;
|
||||
case 'im.vector.fake.recent':
|
||||
return <div className="mx_RoomList_emptySubListTip">
|
||||
You're not in any rooms yet! Press
|
||||
<CreateRoomButton size="16" callout={true}/>
|
||||
to make a room or
|
||||
<RoomDirectoryButton size="16" callout={true}/>
|
||||
to browse the directory
|
||||
</div>;
|
||||
}
|
||||
|
||||
// We don't want to display drop targets if there are no room tiles to drag'n'drop
|
||||
if (this.state.totalRoomCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const labelText = 'Drop here to ' + (VERBS[section] || 'tag ' + section);
|
||||
|
||||
return <RoomDropTarget label={labelText} />;
|
||||
},
|
||||
|
||||
_getHeaderItems: function(section) {
|
||||
const StartChatButton = sdk.getComponent('elements.StartChatButton');
|
||||
const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
|
||||
const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
|
||||
switch (section) {
|
||||
case 'im.vector.fake.direct':
|
||||
return <span className="mx_RoomList_headerButtons">
|
||||
<StartChatButton size="16" />
|
||||
</span>;
|
||||
case 'im.vector.fake.recent':
|
||||
return <span className="mx_RoomList_headerButtons">
|
||||
<RoomDirectoryButton size="16" />
|
||||
<CreateRoomButton size="16" />
|
||||
</span>;
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var RoomSubList = sdk.getComponent('structures.RoomSubList');
|
||||
var self = this;
|
||||
|
||||
return (
|
||||
<GeminiScrollbar className="mx_RoomList_scrollbar"
|
||||
autoshow={true} onScroll={ self._whenScrolling } ref="gemscroll">
|
||||
<div className="mx_RoomList">
|
||||
<RoomSubList list={ self.state.lists['im.vector.fake.invite'] }
|
||||
label="Invites"
|
||||
label={ _t('Invites') }
|
||||
editable={ false }
|
||||
order="recent"
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
|
@ -477,9 +538,9 @@ module.exports = React.createClass({
|
|||
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||
|
||||
<RoomSubList list={ self.state.lists['m.favourite'] }
|
||||
label="Favourites"
|
||||
label={ _t('Favourites') }
|
||||
tagName="m.favourite"
|
||||
verb="favourite"
|
||||
emptyContent={this._getEmptyContent('m.favourite')}
|
||||
editable={ true }
|
||||
order="manual"
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
|
@ -490,9 +551,10 @@ module.exports = React.createClass({
|
|||
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||
|
||||
<RoomSubList list={ self.state.lists['im.vector.fake.direct'] }
|
||||
label="People"
|
||||
label={ _t('People') }
|
||||
tagName="im.vector.fake.direct"
|
||||
verb="tag direct chat"
|
||||
emptyContent={this._getEmptyContent('im.vector.fake.direct')}
|
||||
headerItems={this._getHeaderItems('im.vector.fake.direct')}
|
||||
editable={ true }
|
||||
order="recent"
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
|
@ -504,9 +566,10 @@ module.exports = React.createClass({
|
|||
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||
|
||||
<RoomSubList list={ self.state.lists['im.vector.fake.recent'] }
|
||||
label="Rooms"
|
||||
label={ _t('Rooms') }
|
||||
editable={ true }
|
||||
verb="restore"
|
||||
emptyContent={this._getEmptyContent('im.vector.fake.recent')}
|
||||
headerItems={this._getHeaderItems('im.vector.fake.recent')}
|
||||
order="recent"
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
incomingCall={ self.state.incomingCall }
|
||||
|
@ -515,13 +578,13 @@ module.exports = React.createClass({
|
|||
onHeaderClick={ self.onSubListHeaderClick }
|
||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||
|
||||
{ Object.keys(self.state.lists).map(function(tagName) {
|
||||
{ Object.keys(self.state.lists).map((tagName) => {
|
||||
if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
|
||||
return <RoomSubList list={ self.state.lists[tagName] }
|
||||
key={ tagName }
|
||||
label={ tagName }
|
||||
tagName={ tagName }
|
||||
verb={ "tag as " + tagName }
|
||||
emptyContent={this._getEmptyContent(tagName)}
|
||||
editable={ true }
|
||||
order="manual"
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
|
@ -535,9 +598,9 @@ module.exports = React.createClass({
|
|||
}) }
|
||||
|
||||
<RoomSubList list={ self.state.lists['m.lowpriority'] }
|
||||
label="Low priority"
|
||||
label={ _t('Low priority') }
|
||||
tagName="m.lowpriority"
|
||||
verb="demote"
|
||||
emptyContent={this._getEmptyContent('m.lowpriority')}
|
||||
editable={ true }
|
||||
order="recent"
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
|
@ -548,7 +611,7 @@ module.exports = React.createClass({
|
|||
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||
|
||||
<RoomSubList list={ self.state.lists['im.vector.fake.archived'] }
|
||||
label="Historical"
|
||||
label={ _t('Historical') }
|
||||
editable={ false }
|
||||
order="recent"
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
|||
var React = require('react');
|
||||
var sdk = require('../../../index');
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomNameEditor',
|
||||
|
@ -35,8 +36,8 @@ module.exports = React.createClass({
|
|||
|
||||
this._initialName = name ? name.getContent().name : '';
|
||||
|
||||
this._placeholderName = "Unnamed Room";
|
||||
if (defaultName && defaultName !== 'Empty room') {
|
||||
this._placeholderName = _t("Unnamed Room");
|
||||
if (defaultName && defaultName !== 'Empty room') { // default name from JS SDK, needs no translation as we don't ever show it.
|
||||
this._placeholderName += " (" + defaultName + ")";
|
||||
}
|
||||
},
|
||||
|
@ -55,9 +56,9 @@ module.exports = React.createClass({
|
|||
placeholderClassName="mx_RoomHeader_placeholder"
|
||||
placeholder={ this._placeholderName }
|
||||
blurToCancel={ false }
|
||||
initialValue={ this._initialName }/>
|
||||
initialValue={ this._initialName }
|
||||
dir="auto" />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -21,6 +21,8 @@ var React = require('react');
|
|||
var sdk = require('../../../index');
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomPreviewBar',
|
||||
|
||||
|
@ -47,7 +49,7 @@ module.exports = React.createClass({
|
|||
// The alias that was used to access this room, if appropriate
|
||||
// If given, this will be how the room is referred to (eg.
|
||||
// in error messages).
|
||||
roomAlias: React.PropTypes.object,
|
||||
roomAlias: React.PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -82,9 +84,9 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_roomNameElement: function(fallback) {
|
||||
fallback = fallback || 'a room';
|
||||
fallback = fallback || _t('a room');
|
||||
const name = this.props.room ? this.props.room.name : (this.props.room_alias || "");
|
||||
return name ? <b>{ name }</b> : fallback;
|
||||
return name ? name : fallback;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
@ -112,8 +114,7 @@ module.exports = React.createClass({
|
|||
if (this.props.invitedEmail) {
|
||||
if (this.state.threePidFetchError) {
|
||||
emailMatchBlock = <div className="error">
|
||||
Unable to ascertain that the address this invite was
|
||||
sent to matches one associated with your account.
|
||||
{_t("Unable to ascertain that the address this invite was sent to matches one associated with your account.")}
|
||||
</div>;
|
||||
} else if (this.state.invitedEmailMxid != MatrixClientPeg.get().credentials.userId) {
|
||||
emailMatchBlock =
|
||||
|
@ -122,8 +123,10 @@ module.exports = React.createClass({
|
|||
<img src="img/warning.svg" width="24" height="23" title= "/!\\" alt="/!\\" />
|
||||
</div>
|
||||
<div className="mx_RoomPreviewBar_warningText">
|
||||
This invitation was sent to <b><span className="email">{this.props.invitedEmail}</span></b>, which is not associated with this account.<br/>
|
||||
You may wish to login with a different account, or add this email to this account.
|
||||
{_t("This invitation was sent to an email address which is not associated with this account:")}
|
||||
<b><span className="email">{this.props.invitedEmail}</span></b>
|
||||
<br/>
|
||||
{_t("You may wish to login with a different account, or add this email to this account.")}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
@ -131,18 +134,24 @@ module.exports = React.createClass({
|
|||
joinBlock = (
|
||||
<div>
|
||||
<div className="mx_RoomPreviewBar_invite_text">
|
||||
You have been invited to join this room by <b>{ this.props.inviterName }</b>
|
||||
{ _t('You have been invited to join this room by %(inviterName)s', {inviterName: this.props.inviterName}) }
|
||||
</div>
|
||||
<div className="mx_RoomPreviewBar_join_text">
|
||||
Would you like to <a onClick={ this.props.onJoinClick }>accept</a> or <a onClick={ this.props.onRejectClick }>decline</a> this invitation?
|
||||
{ _tJsx(
|
||||
'Would you like to <acceptText>accept</acceptText> or <declineText>decline</declineText> this invitation?',
|
||||
[/<acceptText>(.*?)<\/acceptText>/, /<declineText>(.*?)<\/declineText>/],
|
||||
[
|
||||
(sub) => <a onClick={ this.props.onJoinClick }>{sub}</a>,
|
||||
(sub) => <a onClick={ this.props.onRejectClick }>{sub}</a>
|
||||
]
|
||||
)}
|
||||
</div>
|
||||
{emailMatchBlock}
|
||||
</div>
|
||||
);
|
||||
|
||||
} else if (kicked || banned) {
|
||||
const verb = kicked ? 'kicked' : 'banned';
|
||||
const roomName = this._roomNameElement('this room');
|
||||
const roomName = this._roomNameElement(_t('This room'));
|
||||
const kickerMember = this.props.room.currentState.getMember(
|
||||
myMember.events.member.getSender()
|
||||
);
|
||||
|
@ -150,29 +159,39 @@ module.exports = React.createClass({
|
|||
kickerMember.name : myMember.events.member.getSender();
|
||||
let reason;
|
||||
if (myMember.events.member.getContent().reason) {
|
||||
reason = <div>Reason: {myMember.events.member.getContent().reason}</div>
|
||||
reason = <div>{_t("Reason: %(reasonText)s", {reasonText: myMember.events.member.getContent().reason})}</div>
|
||||
}
|
||||
let rejoinBlock;
|
||||
if (!banned) {
|
||||
rejoinBlock = <div><a onClick={ this.props.onJoinClick }><b>Rejoin</b></a></div>;
|
||||
rejoinBlock = <div><a onClick={ this.props.onJoinClick }><b>{_t("Rejoin")}</b></a></div>;
|
||||
}
|
||||
|
||||
let actionText;
|
||||
if (kicked) {
|
||||
actionText = _t("You have been kicked from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName});
|
||||
}
|
||||
else if (banned) {
|
||||
actionText = _t("You have been banned from %(roomName)s by %(userName)s.", {roomName: roomName, userName: kickerName});
|
||||
} // no other options possible due to the kicked || banned check above.
|
||||
|
||||
joinBlock = (
|
||||
<div>
|
||||
<div className="mx_RoomPreviewBar_join_text">
|
||||
You have been {verb} from {roomName} by {kickerName}.<br />
|
||||
{actionText}
|
||||
<br />
|
||||
{reason}
|
||||
{rejoinBlock}
|
||||
<a onClick={ this.props.onForgetClick }><b>Forget</b></a>
|
||||
<a onClick={ this.props.onForgetClick }><b>{_t("Forget room")}</b></a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.error) {
|
||||
var name = this.props.roomAlias || "This room";
|
||||
var name = this.props.roomAlias || _t("This room");
|
||||
var error;
|
||||
if (this.props.error.errcode == 'M_NOT_FOUND') {
|
||||
error = name + " does not exist";
|
||||
error = _t("%(roomName)s does not exist.", {roomName: name});
|
||||
} else {
|
||||
error = name + " is not accessible at this time";
|
||||
error = _t("%(roomName)s is not accessible at this time.", {roomName: name});
|
||||
}
|
||||
joinBlock = (
|
||||
<div>
|
||||
|
@ -186,8 +205,12 @@ module.exports = React.createClass({
|
|||
joinBlock = (
|
||||
<div>
|
||||
<div className="mx_RoomPreviewBar_join_text">
|
||||
You are trying to access { name }.<br/>
|
||||
<a onClick={ this.props.onJoinClick }><b>Click here</b></a> to join the discussion!
|
||||
{ _t('You are trying to access %(roomName)s.', {roomName: name}) }
|
||||
<br/>
|
||||
{ _tJsx("<a>Click here</a> to join the discussion!",
|
||||
/<a>(.*?)<\/a>/,
|
||||
(sub) => <a onClick={ this.props.onJoinClick }><b>{sub}</b></a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -196,7 +219,7 @@ module.exports = React.createClass({
|
|||
if (this.props.canPreview) {
|
||||
previewBlock = (
|
||||
<div className="mx_RoomPreviewBar_preview_text">
|
||||
This is a preview of this room. Room interactions have been disabled.
|
||||
{ _t('This is a preview of this room. Room interactions have been disabled') }.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
|
||||
import q from 'q';
|
||||
import React from 'react';
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import sdk from '../../../index';
|
||||
|
@ -45,7 +46,7 @@ const BannedUser = React.createClass({
|
|||
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
|
||||
Modal.createDialog(ConfirmUserActionDialog, {
|
||||
member: this.props.member,
|
||||
action: 'Unban',
|
||||
action: _t('Unban'),
|
||||
danger: false,
|
||||
onFinished: (proceed) => {
|
||||
if (!proceed) return;
|
||||
|
@ -56,8 +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>
|
||||
|
@ -129,14 +130,17 @@ module.exports = React.createClass({
|
|||
console.error("Failed to get room visibility: " + err);
|
||||
});
|
||||
|
||||
this.scalarClient = new ScalarAuthClient();
|
||||
this.scalarClient.connect().done(() => {
|
||||
this.forceUpdate();
|
||||
}, (err) => {
|
||||
this.setState({
|
||||
scalar_error: err
|
||||
this.scalarClient = null;
|
||||
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
|
||||
this.scalarClient = new ScalarAuthClient();
|
||||
this.scalarClient.connect().done(() => {
|
||||
this.forceUpdate();
|
||||
}, (err) => {
|
||||
this.setState({
|
||||
scalar_error: err
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
dis.dispatch({
|
||||
action: 'ui_opacity',
|
||||
|
@ -397,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({
|
||||
|
@ -490,7 +494,7 @@ module.exports = React.createClass({
|
|||
ev.preventDefault();
|
||||
var IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
Modal.createDialog(IntegrationsManager, {
|
||||
src: this.scalarClient.hasCredentials() ?
|
||||
src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId) :
|
||||
null,
|
||||
onFinished: ()=>{
|
||||
|
@ -520,11 +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 }),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -534,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=>{
|
||||
|
@ -569,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 &&
|
||||
|
@ -579,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>
|
||||
|
@ -593,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 ? _t("Encryption is enabled in this room") : _t("Encryption is not enabled in this room") }.
|
||||
</label>
|
||||
{ settings }
|
||||
</div>
|
||||
|
@ -644,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}/>
|
||||
{ _t("%(user)s is a", {user: user}) } <PowerSelector value={ user_levels[user] } disabled={true}/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
@ -657,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");
|
||||
|
@ -665,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 (
|
||||
|
@ -680,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>
|
||||
);
|
||||
}
|
||||
|
@ -691,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>
|
||||
);
|
||||
}
|
||||
|
@ -708,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) {
|
||||
|
@ -722,7 +726,7 @@ module.exports = React.createClass({
|
|||
if (canSetTag || self.state.tags) {
|
||||
var tagsSection =
|
||||
<div className="mx_RoomSettings_tags">
|
||||
Tagged as: { canSetTag ?
|
||||
{_t("Tagged as: ")}{ canSetTag ?
|
||||
(tags.map(function(tag, i) {
|
||||
return (<label key={ i }>
|
||||
<input type="checkbox"
|
||||
|
@ -750,7 +754,11 @@ module.exports = React.createClass({
|
|||
if (this.state.join_rule === "public" && aliasCount == 0) {
|
||||
addressWarning =
|
||||
<div className="mx_RoomSettings_warning">
|
||||
To link to a room it must have <a href="#addresses">an address</a>.
|
||||
{ _tJsx(
|
||||
'To link to a room it must have <a>an address</a>.',
|
||||
/<a>(.*?)<\/a>/,
|
||||
(sub) => <a href="#addresses">{sub}</a>
|
||||
)}
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
@ -758,43 +766,46 @@ module.exports = React.createClass({
|
|||
if (this.state.join_rule !== "public" && this.state.guest_access === "forbidden") {
|
||||
inviteGuestWarning =
|
||||
<div className="mx_RoomSettings_warning">
|
||||
Guests cannot join this room even if explicitly invited. <a href="#" onClick={ (e) => {
|
||||
{ _t('Guests cannot join this room even if explicitly invited.') } <a href="#" onClick={ (e) => {
|
||||
this.setState({ join_rule: "invite", guest_access: "can_join" });
|
||||
e.preventDefault();
|
||||
}}>Click here to fix</a>.
|
||||
}}>{ _t('Click here to fix') }</a>.
|
||||
</div>;
|
||||
}
|
||||
|
||||
var integrationsButton;
|
||||
var integrationsError;
|
||||
if (this.state.showIntegrationsError && this.state.scalar_error) {
|
||||
console.error(this.state.scalar_error);
|
||||
integrationsError = (
|
||||
<span className="mx_RoomSettings_integrationsButton_errorPopup">
|
||||
Could not connect to the integration server
|
||||
</span>
|
||||
);
|
||||
}
|
||||
let integrationsButton;
|
||||
let integrationsError;
|
||||
|
||||
if (this.scalarClient.hasCredentials()) {
|
||||
integrationsButton = (
|
||||
if (this.scalarClient !== null) {
|
||||
if (this.state.showIntegrationsError && this.state.scalar_error) {
|
||||
console.error(this.state.scalar_error);
|
||||
integrationsError = (
|
||||
<span className="mx_RoomSettings_integrationsButton_errorPopup">
|
||||
{ _t('Could not connect to the integration server') }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.scalarClient.hasCredentials()) {
|
||||
integrationsButton = (
|
||||
<div className="mx_RoomSettings_integrationsButton" onClick={ this.onManageIntegrations }>
|
||||
Manage Integrations
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.scalar_error) {
|
||||
integrationsButton = (
|
||||
{ _t('Manage Integrations') }
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.scalar_error) {
|
||||
integrationsButton = (
|
||||
<div className="mx_RoomSettings_integrationsButton_error" onClick={ this.onShowIntegrationsError }>
|
||||
Integrations Error <img src="img/warning.svg" width="17"/>
|
||||
{ integrationsError }
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
integrationsButton = (
|
||||
<div className="mx_RoomSettings_integrationsButton" style={{ opacity: 0.5 }}>
|
||||
Manage Integrations
|
||||
</div>
|
||||
);
|
||||
Integrations Error <img src="img/warning.svg" width="17"/>
|
||||
{ integrationsError }
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
integrationsButton = (
|
||||
<div className="mx_RoomSettings_integrationsButton" style={{opacity: 0.5}}>
|
||||
{ _t('Manage Integrations') }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -807,28 +818,28 @@ module.exports = React.createClass({
|
|||
|
||||
<div className="mx_RoomSettings_toggles">
|
||||
<div className="mx_RoomSettings_settings">
|
||||
<h3>Who can access this room?</h3>
|
||||
<h3>{ _t('Who can access this room?') }</h3>
|
||||
{ inviteGuestWarning }
|
||||
<label>
|
||||
<input type="radio" name="roomVis" value="invite_only"
|
||||
disabled={ !this.mayChangeRoomAccess() }
|
||||
onChange={this._onRoomAccessRadioToggle}
|
||||
checked={this.state.join_rule !== "public"}/>
|
||||
Only people who have been invited
|
||||
{ _t('Only people who have been invited') }
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="roomVis" value="public_no_guests"
|
||||
disabled={ !this.mayChangeRoomAccess() }
|
||||
onChange={this._onRoomAccessRadioToggle}
|
||||
checked={this.state.join_rule === "public" && this.state.guest_access !== "can_join"}/>
|
||||
Anyone who knows the room's link, apart from guests
|
||||
{ _t('Anyone who knows the room\'s link, apart from guests') }
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="roomVis" value="public_with_guests"
|
||||
disabled={ !this.mayChangeRoomAccess() }
|
||||
onChange={this._onRoomAccessRadioToggle}
|
||||
checked={this.state.join_rule === "public" && this.state.guest_access === "can_join"}/>
|
||||
Anyone who knows the room's link, including guests
|
||||
{ _t('Anyone who knows the room\'s link, including guests') }
|
||||
</label>
|
||||
{ addressWarning }
|
||||
<br/>
|
||||
|
@ -837,45 +848,45 @@ module.exports = React.createClass({
|
|||
<input type="checkbox" disabled={ !roomState.mayClientSendStateEvent("m.room.aliases", cli) }
|
||||
onChange={ this._onToggle.bind(this, "isRoomPublished", true, false)}
|
||||
checked={this.state.isRoomPublished}/>
|
||||
List this room in { MatrixClientPeg.get().getDomain() }'s room directory?
|
||||
{_t("List this room in %(domain)s's room directory?", { domain: MatrixClientPeg.get().getDomain() })}
|
||||
</label>
|
||||
</div>
|
||||
<div className="mx_RoomSettings_settings">
|
||||
<h3>Who can read history?</h3>
|
||||
<h3>{ _t('Who can read history?') }</h3>
|
||||
<label>
|
||||
<input type="radio" name="historyVis" value="world_readable"
|
||||
disabled={ !roomState.mayClientSendStateEvent("m.room.history_visibility", cli) }
|
||||
checked={historyVisibility === "world_readable"}
|
||||
onChange={this._onHistoryRadioToggle} />
|
||||
Anyone
|
||||
{_t("Anyone")}
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="historyVis" value="shared"
|
||||
disabled={ !roomState.mayClientSendStateEvent("m.room.history_visibility", cli) }
|
||||
checked={historyVisibility === "shared"}
|
||||
onChange={this._onHistoryRadioToggle} />
|
||||
Members only (since the point in time of selecting this option)
|
||||
{ _t('Members only') } ({ _t('since the point in time of selecting this option') })
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="historyVis" value="invited"
|
||||
disabled={ !roomState.mayClientSendStateEvent("m.room.history_visibility", cli) }
|
||||
checked={historyVisibility === "invited"}
|
||||
onChange={this._onHistoryRadioToggle} />
|
||||
Members only (since they were invited)
|
||||
{ _t('Members only') } ({ _t('since they were invited') })
|
||||
</label>
|
||||
<label >
|
||||
<input type="radio" name="historyVis" value="joined"
|
||||
disabled={ !roomState.mayClientSendStateEvent("m.room.history_visibility", cli) }
|
||||
checked={historyVisibility === "joined"}
|
||||
onChange={this._onHistoryRadioToggle} />
|
||||
Members only (since they joined)
|
||||
{ _t('Members only') } ({ _t('since they joined') })
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<h3>Room Colour</h3>
|
||||
<h3>{ _t('Room Colour') }</h3>
|
||||
<ColorSettings ref="color_settings" room={this.props.room} />
|
||||
</div>
|
||||
|
||||
|
@ -893,41 +904,41 @@ module.exports = React.createClass({
|
|||
|
||||
<UrlPreviewSettings ref="url_preview_settings" room={this.props.room} />
|
||||
|
||||
<h3>Permissions</h3>
|
||||
<h3>{ _t('Permissions') }</h3>
|
||||
<div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings">
|
||||
<div className="mx_RoomSettings_powerLevel">
|
||||
<span className="mx_RoomSettings_powerLevelKey">The default role for new room members is </span>
|
||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('The default role for new room members is') } </span>
|
||||
<PowerSelector ref="users_default" value={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < default_user_level} onChange={this.onPowerLevelsChanged}/>
|
||||
</div>
|
||||
<div className="mx_RoomSettings_powerLevel">
|
||||
<span className="mx_RoomSettings_powerLevelKey">To send messages, you must be a </span>
|
||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('To send messages') }, { _t('you must be a') } </span>
|
||||
<PowerSelector ref="events_default" value={send_level} controlled={false} disabled={!can_change_levels || current_user_level < send_level} onChange={this.onPowerLevelsChanged}/>
|
||||
</div>
|
||||
<div className="mx_RoomSettings_powerLevel">
|
||||
<span className="mx_RoomSettings_powerLevelKey">To invite users into the room, you must be a </span>
|
||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('To invite users into the room') }, { _t('you must be a') } </span>
|
||||
<PowerSelector ref="invite" value={invite_level} controlled={false} disabled={!can_change_levels || current_user_level < invite_level} onChange={this.onPowerLevelsChanged}/>
|
||||
</div>
|
||||
<div className="mx_RoomSettings_powerLevel">
|
||||
<span className="mx_RoomSettings_powerLevelKey">To configure the room, you must be a </span>
|
||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('To configure the room') }, { _t('you must be a') } </span>
|
||||
<PowerSelector ref="state_default" value={state_level} controlled={false} disabled={!can_change_levels || current_user_level < state_level} onChange={this.onPowerLevelsChanged}/>
|
||||
</div>
|
||||
<div className="mx_RoomSettings_powerLevel">
|
||||
<span className="mx_RoomSettings_powerLevelKey">To kick users, you must be a </span>
|
||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('To kick users') }, { _t('you must be a') } </span>
|
||||
<PowerSelector ref="kick" value={kick_level} controlled={false} disabled={!can_change_levels || current_user_level < kick_level} onChange={this.onPowerLevelsChanged}/>
|
||||
</div>
|
||||
<div className="mx_RoomSettings_powerLevel">
|
||||
<span className="mx_RoomSettings_powerLevelKey">To ban users, you must be a </span>
|
||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('To ban users') }, { _t('you must be a') } </span>
|
||||
<PowerSelector ref="ban" value={ban_level} controlled={false} disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged}/>
|
||||
</div>
|
||||
<div className="mx_RoomSettings_powerLevel">
|
||||
<span className="mx_RoomSettings_powerLevelKey">To redact messages, you must be a </span>
|
||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('To remove other users\' messages') }, { _t('you must be a') } </span>
|
||||
<PowerSelector ref="redact" value={redact_level} controlled={false} disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged}/>
|
||||
</div>
|
||||
|
||||
{Object.keys(events_levels).map(function(event_type, i) {
|
||||
return (
|
||||
<div className="mx_RoomSettings_powerLevel" key={event_type}>
|
||||
<span className="mx_RoomSettings_powerLevelKey">To send events of type <code>{ event_type }</code>, you must be a </span>
|
||||
<span className="mx_RoomSettings_powerLevelKey">{ _t('To send events of type') } <code>{ event_type }</code>, { _t('you must be a') } </span>
|
||||
<PowerSelector value={ events_levels[event_type] } controlled={false} disabled={true} onChange={self.onPowerLevelsChanged}/>
|
||||
</div>
|
||||
);
|
||||
|
@ -940,9 +951,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>
|
||||
);
|
||||
|
|
|
@ -98,9 +98,9 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onClick: function() {
|
||||
onClick: function(ev) {
|
||||
if (this.props.onClick) {
|
||||
this.props.onClick(this.props.room.roomId);
|
||||
this.props.onClick(this.props.room.roomId, ev);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -224,13 +224,13 @@ module.exports = React.createClass({
|
|||
if (this.props.selected) {
|
||||
let nameSelected = <EmojiText>{name}</EmojiText>;
|
||||
|
||||
label = <div title={ name } className={ nameClasses }>{ nameSelected }</div>;
|
||||
label = <div title={ name } className={ nameClasses } dir="auto">{ nameSelected }</div>;
|
||||
} else {
|
||||
label = <EmojiText element="div" title={ name } className={ nameClasses }>{name}</EmojiText>;
|
||||
label = <EmojiText element="div" title={ name } className={ nameClasses } dir="auto">{name}</EmojiText>;
|
||||
}
|
||||
} else if (this.state.hover) {
|
||||
var RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
||||
tooltip = <RoomTooltip className="mx_RoomTile_tooltip" room={this.props.room} />;
|
||||
tooltip = <RoomTooltip className="mx_RoomTile_tooltip" room={this.props.room} dir="auto" />;
|
||||
}
|
||||
|
||||
//var incomingCallBox;
|
||||
|
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
|
||||
var React = require('react');
|
||||
var sdk = require('../../../index');
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomTopicEditor',
|
||||
|
@ -43,9 +44,10 @@ module.exports = React.createClass({
|
|||
<EditableText ref="editor"
|
||||
className="mx_RoomHeader_topic mx_RoomHeader_editable"
|
||||
placeholderClassName="mx_RoomHeader_placeholder"
|
||||
placeholder="Add a topic"
|
||||
placeholder={_t("Add a topic")}
|
||||
blurToCancel={ false }
|
||||
initialValue={ this._initialTopic }/>
|
||||
initialValue={ this._initialTopic }
|
||||
dir="auto" />
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -60,7 +60,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
return (
|
||||
<li data-scroll-token={eventId+"+"+j}>
|
||||
<li data-scroll-tokens={eventId+"+"+j}>
|
||||
{ret}
|
||||
</li>);
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue