This commit is contained in:
Matthew Hodgson 2017-02-02 22:05:44 +00:00
commit be41462f3a
35 changed files with 621 additions and 275 deletions

View file

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

View file

@ -171,6 +171,7 @@ export default React.createClass({
brand={this.props.config.brand}
collapsedRhs={this.props.collapse_rhs}
enableLabs={this.props.config.enableLabs}
referralBaseUrl={this.props.config.referralBaseUrl}
/>;
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
break;

View file

@ -1055,12 +1055,13 @@ module.exports = React.createClass({
sessionId={this.state.register_session_id}
idSid={this.state.register_id_sid}
email={this.props.startingFragmentQueryParams.email}
referrer={this.props.startingFragmentQueryParams.referrer}
username={this.state.upgradeUsername}
guestAccessToken={this.state.guestAccessToken}
defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()}
brand={this.props.config.brand}
teamsConfig={this.props.config.teamsConfig}
teamServerConfig={this.props.config.teamServerConfig}
customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()}
registrationUrl={this.props.registrationUrl}

View file

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

View file

@ -74,6 +74,7 @@ module.exports = React.createClass({
// callback for when the status bar can be hidden from view, as it is
// not displaying anything
onHidden: React.PropTypes.func,
// callback for when the status bar is displaying something and should
// be visible
onVisible: React.PropTypes.func,
@ -113,7 +114,9 @@ module.exports = React.createClass({
clearTimeout(this.hideDebouncer);
}
this.hideDebouncer = setTimeout(() => {
this.props.onHidden();
// temporarily stop hiding the statusbar as per
// https://github.com/vector-im/riot-web/issues/1991#issuecomment-276953915
// this.props.onHidden();
}, HIDE_DEBOUNCE_MS);
}
},
@ -238,7 +241,7 @@ module.exports = React.createClass({
if (othersCount > 0) {
avatars.push(
<span className="mx_RoomStatusBar_typingIndicatorRemaining">
<span className="mx_RoomStatusBar_typingIndicatorRemaining" key="others">
+{othersCount}
</span>
);

View file

@ -1332,12 +1332,14 @@ module.exports = React.createClass({
},
onStatusBarVisible: function() {
if (this.unmounted) return;
this.setState({
statusBarVisible: true,
});
},
onStatusBarHidden: function() {
if (this.unmounted) return;
this.setState({
statusBarVisible: false,
});
@ -1507,13 +1509,14 @@ module.exports = React.createClass({
});
var statusBar;
let isStatusAreaExpanded = true;
if (ContentMessages.getCurrentUploads().length > 0) {
var UploadBar = sdk.getComponent('structures.UploadBar');
statusBar = <UploadBar room={this.state.room} />;
} else if (!this.state.searchResults) {
var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar');
isStatusAreaExpanded = this.state.statusBarVisible;
statusBar = <RoomStatusBar
room={this.state.room}
tabComplete={this.tabComplete}
@ -1683,7 +1686,7 @@ module.exports = React.createClass({
);
}
let statusBarAreaClass = "mx_RoomView_statusArea mx_fadable";
if (this.state.statusBarVisible) {
if (isStatusAreaExpanded) {
statusBarAreaClass += " mx_RoomView_statusArea_expanded";
}

View file

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

View file

@ -96,6 +96,9 @@ var TimelinePanel = React.createClass({
// shape property to be passed to EventTiles
tileShape: React.PropTypes.string,
// placeholder text to use if the timeline is empty
empty: React.PropTypes.string,
},
statics: {
@ -990,6 +993,14 @@ var TimelinePanel = React.createClass({
);
}
if (this.state.events.length == 0) {
return (
<div className={ this.props.className + " mx_RoomView_messageListWrapper" }>
<div className="mx_RoomView_empty">{ this.props.empty }</div>
</div>
);
}
// give the messagepanel a stickybottom if we're at the end of the
// live timeline, so that the arrival of new events triggers a
// scroll.

View file

@ -104,6 +104,9 @@ module.exports = React.createClass({
// True to show the 'labs' section of experimental features
enableLabs: React.PropTypes.bool,
// The base URL to use in the referral link. Defaults to window.location.origin.
referralBaseUrl: React.PropTypes.string,
// true if RightPanel is collapsed
collapsedRhs: React.PropTypes.bool,
},
@ -458,6 +461,27 @@ module.exports = React.createClass({
);
},
_renderReferral: function() {
const teamToken = window.localStorage.getItem('mx_team_token');
if (!teamToken) {
return null;
}
if (typeof teamToken !== 'string') {
console.warn('Team token not a string');
return null;
}
const href = (this.props.referralBaseUrl || window.location.origin) +
`/#/register?referrer=${this._me}&team_token=${teamToken}`;
return (
<div>
<h3>Referral</h3>
<div className="mx_UserSettings_section">
Refer a friend to Riot: <a href={href}>{href}</a>
</div>
</div>
);
},
_renderUserInterfaceSettings: function() {
var client = MatrixClientPeg.get();
@ -857,6 +881,8 @@ module.exports = React.createClass({
{accountJsx}
</div>
{this._renderReferral()}
{notification_area}
{this._renderUserInterfaceSettings()}

View file

@ -25,6 +25,7 @@ var ServerConfig = require("../../views/login/ServerConfig");
var MatrixClientPeg = require("../../../MatrixClientPeg");
var RegistrationForm = require("../../views/login/RegistrationForm");
var CaptchaForm = require("../../views/login/CaptchaForm");
var RtsClient = require("../../../RtsClient");
var MIN_PASSWORD_LENGTH = 6;
@ -47,23 +48,16 @@ module.exports = React.createClass({
defaultIsUrl: React.PropTypes.string,
brand: React.PropTypes.string,
email: React.PropTypes.string,
referrer: React.PropTypes.string,
username: React.PropTypes.string,
guestAccessToken: React.PropTypes.string,
teamsConfig: React.PropTypes.shape({
teamServerConfig: React.PropTypes.shape({
// Email address to request new teams
supportEmail: React.PropTypes.string,
teams: React.PropTypes.arrayOf(React.PropTypes.shape({
// The displayed name of the team
"name": React.PropTypes.string,
// The suffix with which every team email address ends
"emailSuffix": React.PropTypes.string,
// The rooms to use during auto-join
"rooms": React.PropTypes.arrayOf(React.PropTypes.shape({
"id": React.PropTypes.string,
"autoJoin": React.PropTypes.bool,
})),
})).required,
supportEmail: React.PropTypes.string.isRequired,
// URL of the riot-team-server to get team configurations and track referrals
teamServerURL: React.PropTypes.string.isRequired,
}),
teamSelected: React.PropTypes.object,
defaultDeviceDisplayName: React.PropTypes.string,
@ -75,6 +69,7 @@ module.exports = React.createClass({
getInitialState: function() {
return {
busy: false,
teamServerBusy: false,
errorText: null,
// We remember the values entered by the user because
// the registration form will be unmounted during the
@ -90,6 +85,7 @@ module.exports = React.createClass({
},
componentWillMount: function() {
this._unmounted = false;
this.dispatcherRef = dis.register(this.onAction);
// attach this to the instance rather than this.state since it isn't UI
this.registerLogic = new Signup.Register(
@ -103,10 +99,40 @@ module.exports = React.createClass({
this.registerLogic.setIdSid(this.props.idSid);
this.registerLogic.setGuestAccessToken(this.props.guestAccessToken);
this.registerLogic.recheckState();
if (
this.props.teamServerConfig &&
this.props.teamServerConfig.teamServerURL &&
!this._rtsClient
) {
this._rtsClient = new RtsClient(this.props.teamServerConfig.teamServerURL);
this.setState({
teamServerBusy: true,
});
// GET team configurations including domains, names and icons
this._rtsClient.getTeamsConfig().then((data) => {
const teamsConfig = {
teams: data,
supportEmail: this.props.teamServerConfig.supportEmail,
};
console.log('Setting teams config to ', teamsConfig);
this.setState({
teamsConfig: teamsConfig,
teamServerBusy: false,
});
}, (err) => {
console.error('Error retrieving config for teams', err);
this.setState({
teamServerBusy: false,
});
});
}
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
this._unmounted = true;
},
componentDidMount: function() {
@ -184,24 +210,41 @@ module.exports = React.createClass({
accessToken: response.access_token
});
// Auto-join rooms
if (self.props.teamsConfig && self.props.teamsConfig.teams) {
for (let i = 0; i < self.props.teamsConfig.teams.length; i++) {
let team = self.props.teamsConfig.teams[i];
if (self.state.formVals.email.endsWith(team.emailSuffix)) {
console.log("User successfully registered with team " + team.name);
if (
self._rtsClient &&
self.props.referrer &&
self.state.teamSelected
) {
// Track referral, get team_token in order to retrieve team config
self._rtsClient.trackReferral(
self.props.referrer,
response.user_id,
self.state.formVals.email
).then((data) => {
const teamToken = data.team_token;
// Store for use /w welcome pages
window.localStorage.setItem('mx_team_token', teamToken);
self._rtsClient.getTeam(teamToken).then((team) => {
console.log(
`User successfully registered with team ${team.name}`
);
if (!team.rooms) {
break;
return;
}
// Auto-join rooms
team.rooms.forEach((room) => {
if (room.autoJoin) {
console.log("Auto-joining " + room.id);
MatrixClientPeg.get().joinRoom(room.id);
if (room.auto_join && room.room_id) {
console.log(`Auto-joining ${room.room_id}`);
MatrixClientPeg.get().joinRoom(room.room_id);
}
});
break;
}
}
}, (err) => {
console.error('Error getting team config', err);
});
}, (err) => {
console.error('Error tracking referral', err);
});
}
if (self.props.brand) {
@ -273,7 +316,15 @@ module.exports = React.createClass({
});
},
onTeamSelected: function(teamSelected) {
if (!this._unmounted) {
this.setState({ teamSelected });
}
},
_getRegisterContentJsx: function() {
const Spinner = sdk.getComponent("elements.Spinner");
var currStep = this.registerLogic.getStep();
var registerStep;
switch (currStep) {
@ -283,17 +334,23 @@ module.exports = React.createClass({
case "Register.STEP_m.login.dummy":
// NB. Our 'username' prop is specifically for upgrading
// a guest account
if (this.state.teamServerBusy) {
registerStep = <Spinner />;
break;
}
registerStep = (
<RegistrationForm
showEmail={true}
defaultUsername={this.state.formVals.username}
defaultEmail={this.state.formVals.email}
defaultPassword={this.state.formVals.password}
teamsConfig={this.props.teamsConfig}
teamsConfig={this.state.teamsConfig}
guestUsername={this.props.username}
minPasswordLength={MIN_PASSWORD_LENGTH}
onError={this.onFormValidationFailed}
onRegisterClick={this.onFormSubmit} />
onRegisterClick={this.onFormSubmit}
onTeamSelected={this.onTeamSelected}
/>
);
break;
case "Register.STEP_m.login.email.identity":
@ -322,7 +379,6 @@ module.exports = React.createClass({
}
var busySpinner;
if (this.state.busy) {
var Spinner = sdk.getComponent("elements.Spinner");
busySpinner = (
<Spinner />
);
@ -367,7 +423,7 @@ module.exports = React.createClass({
return (
<div className="mx_Login">
<div className="mx_Login_box">
<LoginHeader />
<LoginHeader icon={this.state.teamSelected ? this.state.teamSelected.icon : null}/>
{this._getRegisterContentJsx()}
<LoginFooter />
</div>

View file

@ -145,27 +145,48 @@ module.exports = React.createClass({
if (imageUrl === this.state.defaultImageUrl) {
const initialLetter = this._getInitialLetter(name);
return (
<span className="mx_BaseAvatar" {...otherProps}>
<EmojiText className="mx_BaseAvatar_initial" aria-hidden="true"
style={{ fontSize: (width * 0.65) + "px",
width: width + "px",
lineHeight: height + "px" }}>{initialLetter}</EmojiText>
<img className="mx_BaseAvatar_image" src={imageUrl}
alt="" title={title} onError={this.onError}
width={width} height={height} />
</span>
const textNode = (
<EmojiText className="mx_BaseAvatar_initial" aria-hidden="true"
style={{ fontSize: (width * 0.65) + "px",
width: width + "px",
lineHeight: height + "px" }}
>
{initialLetter}
</EmojiText>
);
const imgNode = (
<img className="mx_BaseAvatar_image" src={imageUrl}
alt="" title={title} onError={this.onError}
width={width} height={height} />
);
if (onClick != null) {
return (
<AccessibleButton element='span' className="mx_BaseAvatar"
onClick={onClick} {...otherProps}
>
{textNode}
{imgNode}
</AccessibleButton>
);
} else {
return (
<span className="mx_BaseAvatar" {...otherProps}>
{textNode}
{imgNode}
</span>
);
}
}
if (onClick != null) {
return (
<AccessibleButton className="mx_BaseAvatar" onClick={onClick}>
<img className="mx_BaseAvatar_image" src={imageUrl}
onError={this.onError}
width={width} height={height}
title={title} alt=""
{...otherProps} />
</AccessibleButton>
<AccessibleButton className="mx_BaseAvatar mx_BaseAvatar_image"
element='img'
src={imageUrl}
onClick={onClick}
onError={this.onError}
width={width} height={height}
title={title} alt=""
{...otherProps} />
);
} else {
return (

View file

@ -14,17 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require("react");
var classNames = require('classnames');
var sdk = require("../../../index");
var Invite = require("../../../Invite");
var createRoom = require("../../../createRoom");
var MatrixClientPeg = require("../../../MatrixClientPeg");
var DMRoomMap = require('../../../utils/DMRoomMap');
var rate_limited_func = require("../../../ratelimitedfunc");
var dis = require("../../../dispatcher");
var Modal = require('../../../Modal');
import React from 'react';
import classNames from 'classnames';
import sdk from '../../../index';
import { getAddressType, inviteMultipleToRoom } from '../../../Invite';
import createRoom from '../../../createRoom';
import MatrixClientPeg from '../../../MatrixClientPeg';
import DMRoomMap from '../../../utils/DMRoomMap';
import rate_limited_func from '../../../ratelimitedfunc';
import dis from '../../../dispatcher';
import Modal from '../../../Modal';
import AccessibleButton from '../elements/AccessibleButton';
import q from 'q';
const TRUNCATE_QUERY_LIST = 40;
@ -186,13 +187,17 @@ module.exports = React.createClass({
// If the query isn't a user we know about, but is a
// valid address, add an entry for that
if (queryList.length == 0) {
const addrType = Invite.getAddressType(query);
const addrType = getAddressType(query);
if (addrType !== null) {
queryList.push({
queryList[0] = {
addressType: addrType,
address: query,
isKnown: false,
});
};
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
if (addrType == 'email') {
this._lookupThreepid(addrType, query).done();
}
}
}
}
@ -212,6 +217,7 @@ module.exports = React.createClass({
inviteList: inviteList,
queryList: [],
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
};
},
@ -229,6 +235,7 @@ module.exports = React.createClass({
inviteList: inviteList,
queryList: [],
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
},
_getDirectMessageRoom: function(addr) {
@ -266,7 +273,7 @@ module.exports = React.createClass({
if (this.props.roomId) {
// Invite new user to a room
var self = this;
Invite.inviteMultipleToRoom(this.props.roomId, addrTexts)
inviteMultipleToRoom(this.props.roomId, addrTexts)
.then(function(addrs) {
var room = MatrixClientPeg.get().getRoom(self.props.roomId);
return self._showAnyInviteErrors(addrs, room);
@ -300,7 +307,7 @@ module.exports = React.createClass({
var room;
createRoom().then(function(roomId) {
room = MatrixClientPeg.get().getRoom(roomId);
return Invite.inviteMultipleToRoom(roomId, addrTexts);
return inviteMultipleToRoom(roomId, addrTexts);
})
.then(function(addrs) {
return self._showAnyInviteErrors(addrs, room);
@ -380,7 +387,7 @@ module.exports = React.createClass({
},
_isDmChat: function(addrs) {
if (addrs.length === 1 && Invite.getAddressType(addrs[0]) === "mx" && !this.props.roomId) {
if (addrs.length === 1 && getAddressType(addrs[0]) === "mx" && !this.props.roomId) {
return true;
} else {
return false;
@ -408,7 +415,7 @@ module.exports = React.createClass({
_addInputToList: function() {
const addressText = this.refs.textinput.value.trim();
const addrType = Invite.getAddressType(addressText);
const addrType = getAddressType(addressText);
const addrObj = {
addressType: addrType,
address: addressText,
@ -432,9 +439,45 @@ module.exports = React.createClass({
inviteList: inviteList,
queryList: [],
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
return inviteList;
},
_lookupThreepid: function(medium, address) {
let cancelled = false;
// Note that we can't safely remove this after we're done
// because we don't know that it's the same one, so we just
// leave it: it's replacing the old one each time so it's
// not like they leak.
this._cancelThreepidLookup = function() {
cancelled = true;
}
// wait a bit to let the user finish typing
return q.delay(500).then(() => {
if (cancelled) return null;
return MatrixClientPeg.get().lookupThreePid(medium, address);
}).then((res) => {
if (res === null || !res.mxid) return null;
if (cancelled) return null;
return MatrixClientPeg.get().getProfileInfo(res.mxid);
}).then((res) => {
if (res === null) return null;
if (cancelled) return null;
this.setState({
queryList: [{
// an InviteAddressType
addressType: medium,
address: address,
displayName: res.displayname,
avatarMxc: res.avatar_url,
isKnown: true,
}]
});
});
},
render: function() {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const AddressSelector = sdk.getComponent("elements.AddressSelector");

View file

@ -1,5 +1,5 @@
/*
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 +17,7 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import GeminiScrollbar from 'react-gemini-scrollbar';
function DeviceListEntry(props) {
const {userId, device} = props;
@ -118,7 +119,19 @@ export default React.createClass({
</h4>
);
} else {
warning = <h4>We strongly recommend you verify them before continuing.</h4>;
warning = (
<div>
<p>
This means there is no guarantee that the devices belong
to a rightful user of the room.
</p>
<p>
We recommend you go through the verification process
for each device before continuing, but you can resend
the message without verifying if you prefer.
</p>
</div>
);
}
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
@ -127,15 +140,16 @@ export default React.createClass({
onFinished={this.props.onFinished}
title='Room contains unknown devices'
>
<div className="mx_Dialog_content">
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
<h4>
This room contains unknown devices which have not been
verified.
</h4>
{ warning }
Unknown devices:
<UnknownDeviceList devices={this.props.devices} />
</div>
</GeminiScrollbar>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" autoFocus={ true }
onClick={ this.props.onFinished } >

View file

@ -94,14 +94,14 @@ export default React.createClass({
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const nameClasses = classNames({
"mx_AddressTile_name": true,
"mx_AddressTile_justified": this.props.justified,
});
let info;
let error = false;
if (address.addressType === "mx" && address.isKnown) {
const nameClasses = classNames({
"mx_AddressTile_name": true,
"mx_AddressTile_justified": this.props.justified,
});
const idClasses = classNames({
"mx_AddressTile_id": true,
"mx_AddressTile_justified": this.props.justified,
@ -123,13 +123,21 @@ export default React.createClass({
<div className={unknownMxClasses}>{ this.props.address.address }</div>
);
} else if (address.addressType === "email") {
var emailClasses = classNames({
const emailClasses = classNames({
"mx_AddressTile_email": true,
"mx_AddressTile_justified": this.props.justified,
});
let nameNode = null;
if (address.displayName) {
nameNode = <div className={nameClasses}>{ address.displayName }</div>
}
info = (
<div className={emailClasses}>{ address.address }</div>
<div className="mx_AddressTile_mx">
<div className={emailClasses}>{ address.address }</div>
{nameNode}
</div>
);
} else {
error = true;

View file

@ -44,8 +44,8 @@ module.exports = React.createClass({
teams: React.PropTypes.arrayOf(React.PropTypes.shape({
// The displayed name of the team
"name": React.PropTypes.string,
// The suffix with which every team email address ends
"emailSuffix": React.PropTypes.string,
// The domain of team email addresses
"domain": React.PropTypes.string,
})).required,
}),
@ -117,9 +117,6 @@ module.exports = React.createClass({
_doSubmit: function() {
let email = this.refs.email.value.trim();
if (this.state.selectedTeam) {
email += "@" + this.state.selectedTeam.emailSuffix;
}
var promise = this.props.onRegisterClick({
username: this.refs.username.value.trim() || this.props.guestUsername,
password: this.refs.password.value.trim(),
@ -134,25 +131,6 @@ module.exports = React.createClass({
}
},
onSelectTeam: function(teamIndex) {
let team = this._getSelectedTeam(teamIndex);
if (team) {
this.refs.email.value = this.refs.email.value.split("@")[0];
}
this.setState({
selectedTeam: team,
showSupportEmail: teamIndex === "other",
});
},
_getSelectedTeam: function(teamIndex) {
if (this.props.teamsConfig &&
this.props.teamsConfig.teams[teamIndex]) {
return this.props.teamsConfig.teams[teamIndex];
}
return null;
},
/**
* Returns true if all fields were valid last time
* they were validated.
@ -167,20 +145,36 @@ module.exports = React.createClass({
return true;
},
_isUniEmail: function(email) {
return email.endsWith('.ac.uk') || email.endsWith('.edu');
},
validateField: function(field_id) {
var pwd1 = this.refs.password.value.trim();
var pwd2 = this.refs.passwordConfirm.value.trim();
switch (field_id) {
case FIELD_EMAIL:
let email = this.refs.email.value;
if (this.props.teamsConfig) {
let team = this.state.selectedTeam;
if (team) {
email = email + "@" + team.emailSuffix;
}
const email = this.refs.email.value;
if (this.props.teamsConfig && this._isUniEmail(email)) {
const matchingTeam = this.props.teamsConfig.teams.find(
(team) => {
return email.split('@').pop() === team.domain;
}
) || null;
this.setState({
selectedTeam: matchingTeam,
showSupportEmail: !matchingTeam,
});
this.props.onTeamSelected(matchingTeam);
} else {
this.props.onTeamSelected(null);
this.setState({
selectedTeam: null,
showSupportEmail: false,
});
}
let valid = email === '' || Email.looksValid(email);
const valid = email === '' || Email.looksValid(email);
this.markFieldValid(field_id, valid, "RegistrationForm.ERR_EMAIL_INVALID");
break;
case FIELD_USERNAME:
@ -260,61 +254,35 @@ module.exports = React.createClass({
return cls;
},
_renderEmailInputSuffix: function() {
let suffix = null;
if (!this.state.selectedTeam) {
return suffix;
}
let team = this.state.selectedTeam;
if (team) {
suffix = "@" + team.emailSuffix;
}
return suffix;
},
render: function() {
var self = this;
var emailSection, teamSection, teamAdditionSupport, registerButton;
var emailSection, belowEmailSection, registerButton;
if (this.props.showEmail) {
let emailSuffix = this._renderEmailInputSuffix();
emailSection = (
<div>
<input type="text" ref="email"
autoFocus={true} placeholder="Email address (optional)"
defaultValue={this.props.defaultEmail}
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_EMAIL);}}
value={self.state.email}/>
{emailSuffix ? <input className="mx_Login_field" value={emailSuffix} disabled/> : null }
</div>
<input type="text" ref="email"
autoFocus={true} placeholder="Email address (optional)"
defaultValue={this.props.defaultEmail}
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_EMAIL);}}
value={self.state.email}/>
);
if (this.props.teamsConfig) {
teamSection = (
<select
defaultValue="-1"
className="mx_Login_field"
onBlur={function() {self.validateField(FIELD_EMAIL);}}
onChange={function(ev) {self.onSelectTeam(ev.target.value);}}
>
<option key="-1" value="-1">No team</option>
{this.props.teamsConfig.teams.map((t, index) => {
return (
<option key={index} value={index}>
{t.name}
</option>
);
})}
<option key="-2" value="other">Other</option>
</select>
);
if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) {
teamAdditionSupport = (
<span>
If your team is not listed, email&nbsp;
belowEmailSection = (
<p className="mx_Login_support">
Sorry, but your university is not registered with us just yet.&nbsp;
Email us on&nbsp;
<a href={"mailto:" + this.props.teamsConfig.supportEmail}>
{this.props.teamsConfig.supportEmail}
</a>
</span>
</a>&nbsp;
to get your university signed up. Or continue to register with Riot to enjoy our open source platform.
</p>
);
} else if (this.state.selectedTeam) {
belowEmailSection = (
<p className="mx_Login_support">
You are registering with {this.state.selectedTeam.name}
</p>
);
}
}
@ -333,11 +301,8 @@ module.exports = React.createClass({
return (
<div>
<form onSubmit={this.onSubmit}>
{teamSection}
{teamAdditionSupport}
<br />
{emailSection}
<br />
{belowEmailSection}
<input type="text" ref="username"
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername}
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}

View file

@ -35,7 +35,7 @@ export function onSendMessageFailed(err, room) {
Modal.createDialog(UnknownDeviceDialog, {
devices: err.devices,
room: room,
});
}, "mx_Dialog_unknownDevice");
}
dis.dispatch({
action: 'message_send_failed',

View file

@ -170,15 +170,15 @@ module.exports = React.createClass({
let title;
if (this.props.timestamp) {
let suffix = " (" + this.props.member.userId + ")";
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 = ts.toLocaleString() + suffix;
title = prefix + ts.toLocaleString();
}
else {
// "7:05:45 PM (@alice:matrix.org)"
title = ts.toLocaleTimeString() + suffix;
title = prefix + ts.toLocaleTimeString();
}
}
@ -192,9 +192,9 @@ module.exports = React.createClass({
width={14} height={14} resizeMethod="crop"
style={style}
title={title}
onClick={this.props.onClick}
/>
</Velociraptor>
);
/* onClick={this.props.onClick} */
},
});

View file

@ -301,8 +301,8 @@ module.exports = React.createClass({
var rightPanel_buttons;
if (this.props.collapsedRhs) {
rightPanel_buttons =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title="<">
<TintableSvg src="img/minimise.svg" width="10" height="16"/>
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title="Show panel">
<TintableSvg src="img/maximise.svg" width="10" height="16"/>
</AccessibleButton>;
}

View file

@ -146,7 +146,7 @@ module.exports = React.createClass({
<div>
<div className="mx_RoomPreviewBar_join_text">
You are trying to access { name }.<br/>
Would you like to <a onClick={ this.props.onJoinClick }>join</a> in order to participate in the discussion?
<a onClick={ this.props.onJoinClick }><b>Click here</b></a> to join the discussion!
</div>
</div>
);