Merge remote-tracking branch 'origin/develop' into rav/hotkey-ux

This commit is contained in:
Richard van der Hoff 2017-01-24 20:47:24 +00:00
commit 6dd46d532a
124 changed files with 1837 additions and 654 deletions

View file

@ -47,7 +47,7 @@ module.exports = {
return container;
},
createMenu: function (Element, props) {
createMenu: function(Element, props) {
var self = this;
var closeMenu = function() {
@ -67,7 +67,7 @@ module.exports = {
chevronOffset.top = props.chevronOffset;
}
// To overide the deafult chevron colour, if it's been set
// To override the default chevron colour, if it's been set
var chevronCSS = "";
if (props.menuColour) {
chevronCSS = `
@ -78,15 +78,15 @@ module.exports = {
.mx_ContextualMenu_chevron_right:after {
border-left-color: ${props.menuColour};
}
`
`;
}
var chevron = null;
if (props.left) {
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_left"></div>
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_left"></div>;
position.left = props.left;
} else {
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_right"></div>
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_right"></div>;
position.right = props.right;
}

View file

@ -118,7 +118,7 @@ module.exports = React.createClass({
var self = this;
deferred.then(function (resp) {
deferred.then(function(resp) {
self.setState({
phase: self.phases.CREATED,
});
@ -210,7 +210,7 @@ module.exports = React.createClass({
onAliasChanged: function(alias) {
this.setState({
alias: alias
})
});
},
onEncryptChanged: function(ev) {

View file

@ -35,7 +35,7 @@ var FilePanel = React.createClass({
getInitialState: function() {
return {
timelineSet: null,
}
};
},
componentWillMount: function() {

View file

@ -160,8 +160,8 @@ export default React.createClass({
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.sideOpacity} />;
break;
case PageTypes.UserSettings:
@ -170,28 +170,28 @@ export default React.createClass({
brand={this.props.config.brand}
collapsedRhs={this.props.collapse_rhs}
enableLabs={this.props.config.enableLabs}
/>
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>
/>;
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
break;
case PageTypes.CreateRoom:
page_element = <CreateRoom
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.sideOpacity}/>;
break;
case PageTypes.RoomDirectory:
page_element = <RoomDirectory
collapsedRhs={this.props.collapse_rhs}
config={this.props.config.roomDirectory}
/>
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>
/>;
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
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.sideOpacity} />;
break;
}

View file

@ -77,7 +77,7 @@ module.exports = React.createClass({
getChildContext: function() {
return {
appConfig: this.props.config,
}
};
},
getInitialState: function() {
@ -456,6 +456,9 @@ module.exports = React.createClass({
middleOpacity: payload.middleOpacity,
});
break;
case 'set_theme':
this._onSetTheme(payload.value);
break;
case 'on_logged_in':
this._onLoggedIn();
break;
@ -586,6 +589,50 @@ module.exports = React.createClass({
this.setState({loading: false});
},
/**
* Called whenever someone changes the theme
*/
_onSetTheme: function(theme) {
if (!theme) {
theme = 'light';
}
// look for the stylesheet elements.
// styleElements is a map from style name to HTMLLinkElement.
var styleElements = Object.create(null);
var i, a;
for (i = 0; (a = document.getElementsByTagName("link")[i]); i++) {
var href = a.getAttribute("href");
// shouldn't we be using the 'title' tag rather than the href?
var match = href.match(/^bundles\/.*\/theme-(.*)\.css$/);
if (match) {
styleElements[match[1]] = a;
}
}
if (!(theme in styleElements)) {
throw new Error("Unknown theme " + theme);
}
// disable all of them first, then enable the one we want. Chrome only
// bothers to do an update on a true->false transition, so this ensures
// that we get exactly one update, at the right time.
Object.values(styleElements).forEach((a) => {
a.disabled = true;
});
styleElements[theme].disabled = false;
if (theme === 'dark') {
// abuse the tinter to change all the SVG's #fff to #2d2d2d
// XXX: obviously this shouldn't be hardcoded here.
Tinter.tintSvgWhite('#2d2d2d');
}
else {
Tinter.tintSvgWhite('#ffffff');
}
},
/**
* Called when a new logged in session has started
*/
@ -687,6 +734,16 @@ module.exports = React.createClass({
action: 'logout'
});
});
cli.on("accountData", function(ev) {
if (ev.getType() === 'im.vector.web.settings') {
if (ev.getContent() && ev.getContent().theme) {
dis.dispatch({
action: 'set_theme',
value: ev.getContent().theme,
});
}
}
});
},
onFocus: function(ev) {
@ -979,7 +1036,7 @@ module.exports = React.createClass({
{...this.props}
{...this.state}
/>
)
);
} else if (this.state.logged_in) {
// we think we are logged in, but are still waiting for the /sync to complete
var Spinner = sdk.getComponent('elements.Spinner');
@ -1003,6 +1060,7 @@ module.exports = React.createClass({
defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()}
brand={this.props.config.brand}
teamsConfig={this.props.config.teamsConfig}
customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()}
registrationUrl={this.props.registrationUrl}

View file

@ -19,7 +19,7 @@ var ReactDOM = require("react-dom");
var dis = require("../../dispatcher");
var sdk = require('../../index');
var MatrixClientPeg = require('../../MatrixClientPeg')
var MatrixClientPeg = require('../../MatrixClientPeg');
const MILLIS_IN_DAY = 86400000;
@ -282,7 +282,7 @@ module.exports = React.createClass({
var isMembershipChange = (e) =>
e.getType() === 'm.room.member'
&& ['join', 'leave'].indexOf(e.getContent().membership) !== -1
&& (!e.getPrevContent() || e.getContent().membership !== e.getPrevContent().membership);
&& (!e.getPrevContent() || e.getContent().membership !== e.getPrevContent().membership);
for (i = 0; i < this.props.events.length; i++) {
var mxEv = this.props.events[i];
@ -340,7 +340,7 @@ module.exports = React.createClass({
prevEvent = e;
return ret;
}
).reduce((a,b) => a.concat(b));
).reduce((a, b) => a.concat(b));
if (eventTiles.length === 0) {
eventTiles = null;

View file

@ -19,6 +19,14 @@ var sdk = require('../../index');
var dis = require("../../dispatcher");
var WhoIsTyping = require("../../WhoIsTyping");
var MatrixClientPeg = require("../../MatrixClientPeg");
const MemberAvatar = require("../views/avatars/MemberAvatar");
const TYPING_AVATARS_LIMIT = 2;
const HIDE_DEBOUNCE_MS = 10000;
const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1;
const STATUS_BAR_EXPANDED_LARGE = 2;
module.exports = React.createClass({
displayName: 'RoomStatusBar',
@ -60,6 +68,13 @@ module.exports = React.createClass({
// status bar. This is used to trigger a re-layout in the parent
// component.
onResize: React.PropTypes.func,
// 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,
},
getInitialState: function() {
@ -78,6 +93,18 @@ module.exports = React.createClass({
if(this.props.onResize && this._checkForResize(prevProps, prevState)) {
this.props.onResize();
}
const size = this._getSize(this.state, this.props);
if (size > 0) {
this.props.onVisible();
} else {
if (this.hideDebouncer) {
clearTimeout(this.hideDebouncer);
}
this.hideDebouncer = setTimeout(() => {
this.props.onHidden();
}, HIDE_DEBOUNCE_MS);
}
},
componentWillUnmount: function() {
@ -104,35 +131,28 @@ module.exports = React.createClass({
});
},
// We don't need the actual height - just whether it is likely to have
// changed - so we use '0' to indicate normal size, and other values to
// indicate other sizes.
_getSize: function(state, props) {
if (state.syncState === "ERROR" ||
state.whoisTypingString ||
props.numUnreadMessages ||
!props.atEndOfLiveTimeline ||
props.hasActiveCall) {
return STATUS_BAR_EXPANDED;
} else if (props.tabCompleteEntries) {
return STATUS_BAR_HIDDEN;
} else if (props.hasUnsentMessages) {
return STATUS_BAR_EXPANDED_LARGE;
}
return STATUS_BAR_HIDDEN;
},
// determine if we need to call onResize
_checkForResize: function(prevProps, prevState) {
// figure out the old height and the new height of the status bar. We
// don't need the actual height - just whether it is likely to have
// changed - so we use '0' to indicate normal size, and other values to
// indicate other sizes.
var oldSize, newSize;
if (prevState.syncState === "ERROR") {
oldSize = 1;
} else if (prevProps.tabCompleteEntries) {
oldSize = 0;
} else if (prevProps.hasUnsentMessages) {
oldSize = 2;
} else {
oldSize = 0;
}
if (this.state.syncState === "ERROR") {
newSize = 1;
} else if (this.props.tabCompleteEntries) {
newSize = 0;
} else if (this.props.hasUnsentMessages) {
newSize = 2;
} else {
newSize = 0;
}
return newSize != oldSize;
// figure out the old height and the new height of the status bar.
return this._getSize(prevProps, prevState) !== this._getSize(this.props, this.state);
},
// return suitable content for the image on the left of the status bar.
@ -173,10 +193,8 @@ module.exports = React.createClass({
if (wantPlaceholder) {
return (
<div className="mx_RoomStatusBar_placeholderIndicator">
<span>.</span>
<span>.</span>
<span>.</span>
<div className="mx_RoomStatusBar_typingIndicatorAvatars">
{this._renderTypingIndicatorAvatars(TYPING_AVATARS_LIMIT)}
</div>
);
}
@ -184,6 +202,36 @@ module.exports = React.createClass({
return null;
},
_renderTypingIndicatorAvatars: function(limit) {
let users = WhoIsTyping.usersTypingApartFromMe(this.props.room);
let othersCount = Math.max(users.length - limit, 0);
users = users.slice(0, limit);
let avatars = users.map((u, index) => {
let showInitial = othersCount === 0 && index === users.length - 1;
return (
<MemberAvatar
key={u.userId}
member={u}
width={24}
height={24}
resizeMethod="crop"
defaultToInitialLetter={showInitial}
/>
);
});
if (othersCount > 0) {
avatars.push(
<span className="mx_RoomStatusBar_typingIndicatorRemaining">
+{othersCount}
</span>
);
}
return avatars;
},
// return suitable content for the main (text) part of the status bar.
_getContent: function() {

View file

@ -48,7 +48,7 @@ if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
var debuglog = console.log.bind(console);
} else {
var debuglog = function () {};
var debuglog = function() {};
}
module.exports = React.createClass({
@ -146,7 +146,9 @@ module.exports = React.createClass({
showTopUnreadMessagesBar: false,
auxPanelMaxHeight: undefined,
}
statusBarVisible: false,
};
},
componentWillMount: function() {
@ -674,8 +676,9 @@ module.exports = React.createClass({
},
onSearchResultsFillRequest: function(backwards) {
if (!backwards)
if (!backwards) {
return q(false);
}
if (this.state.searchResults.next_batch) {
debuglog("requesting more search results");
@ -758,7 +761,7 @@ module.exports = React.createClass({
}).then(() => {
var sign_url = this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : undefined;
return MatrixClientPeg.get().joinRoom(this.props.roomAddress,
{ inviteSignUrl: sign_url } )
{ inviteSignUrl: sign_url } );
}).then(function(resp) {
var roomId = resp.roomId;
@ -962,7 +965,7 @@ module.exports = React.createClass({
// For overlapping highlights,
// favour longer (more specific) terms first
highlights = highlights.sort(function(a, b) {
return b.length - a.length });
return b.length - a.length; });
self.setState({
searchHighlights: highlights,
@ -1025,7 +1028,7 @@ module.exports = React.createClass({
if (scrollPanel) {
scrollPanel.checkScroll();
}
}
};
var lastRoomId;
@ -1090,7 +1093,7 @@ module.exports = React.createClass({
}
this.refs.room_settings.save().then((results) => {
var fails = results.filter(function(result) { return result.state !== "fulfilled" });
var fails = results.filter(function(result) { return result.state !== "fulfilled"; });
console.log("Settings saved with %s errors", fails.length);
if (fails.length) {
fails.forEach(function(result) {
@ -1099,7 +1102,7 @@ module.exports = React.createClass({
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Failed to save settings",
description: fails.map(function(result) { return result.reason }).join("\n"),
description: fails.map(function(result) { return result.reason; }).join("\n"),
});
// still editing room settings
}
@ -1183,7 +1186,7 @@ module.exports = React.createClass({
this.setState({ searching: true });
},
onCancelSearchClick: function () {
onCancelSearchClick: function() {
this.setState({
searching: false,
searchResults: null,
@ -1208,8 +1211,9 @@ module.exports = React.createClass({
// decide whether or not the top 'unread messages' bar should be shown
_updateTopUnreadMessagesBar: function() {
if (!this.refs.messagePanel)
if (!this.refs.messagePanel) {
return;
}
var pos = this.refs.messagePanel.getReadMarkerPosition();
@ -1331,6 +1335,18 @@ module.exports = React.createClass({
// no longer anything to do here
},
onStatusBarVisible: function() {
this.setState({
statusBarVisible: true,
});
},
onStatusBarHidden: function() {
this.setState({
statusBarVisible: false,
});
},
showSettings: function(show) {
// XXX: this is a bit naughty; we should be doing this via props
if (show) {
@ -1498,7 +1514,7 @@ module.exports = React.createClass({
if (ContentMessages.getCurrentUploads().length > 0) {
var UploadBar = sdk.getComponent('structures.UploadBar');
statusBar = <UploadBar room={this.state.room} />
statusBar = <UploadBar room={this.state.room} />;
} else if (!this.state.searchResults) {
var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar');
@ -1513,7 +1529,9 @@ module.exports = React.createClass({
onCancelAllClick={this.onCancelAllClick}
onScrollToBottomClick={this.jumpToLiveTimeline}
onResize={this.onChildResize}
/>
onVisible={this.onStatusBarVisible}
onHidden={this.onStatusBarHidden}
/>;
}
var aux = null;
@ -1569,7 +1587,7 @@ module.exports = React.createClass({
messageComposer =
<MessageComposer
room={this.state.room} onResize={this.onChildResize} uploadFile={this.uploadFile}
callState={this.state.callState} tabComplete={this.tabComplete} opacity={ this.props.opacity }/>
callState={this.state.callState} tabComplete={this.tabComplete} opacity={ this.props.opacity }/>;
}
// TODO: Why aren't we storing the term/scope/count in this format
@ -1597,14 +1615,14 @@ module.exports = React.createClass({
<img src={call.isLocalVideoMuted() ? "img/video-unmute.svg" : "img/video-mute.svg"}
alt={call.isLocalVideoMuted() ? "Click to unmute video" : "Click to mute video"}
width="31" height="27"/>
</div>
</div>;
}
voiceMuteButton =
<div className="mx_RoomView_voipButton" onClick={this.onMuteAudioClick}>
<img src={call.isMicrophoneMuted() ? "img/voice-unmute.svg" : "img/voice-mute.svg"}
alt={call.isMicrophoneMuted() ? "Click to unmute audio" : "Click to mute audio"}
width="21" height="26"/>
</div>
</div>;
// wrap the existing status bar into a 'callStatusBar' which adds more knobs.
statusBar =
@ -1614,7 +1632,7 @@ module.exports = React.createClass({
{ zoomButton }
{ statusBar }
<TintableSvg className="mx_RoomView_voipChevron" src="img/voip-chevron.svg" width="22" height="17"/>
</div>
</div>;
}
// if we have search results, we keep the messagepanel (so that it preserves its
@ -1667,6 +1685,10 @@ module.exports = React.createClass({
</div>
);
}
let statusBarAreaClass = "mx_RoomView_statusArea mx_fadable";
if (this.state.statusBarVisible) {
statusBarAreaClass += " mx_RoomView_statusArea_expanded";
}
return (
<div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") } ref="roomView">
@ -1689,7 +1711,7 @@ module.exports = React.createClass({
{ topUnreadMessagesBar }
{ messagePanel }
{ searchResultsPanel }
<div className="mx_RoomView_statusArea mx_fadable" style={{ opacity: this.props.opacity }}>
<div className={statusBarAreaClass} style={{opacity: this.props.opacity}}>
<div className="mx_RoomView_statusAreaBox">
<div className="mx_RoomView_statusAreaBox_line"></div>
{ statusBar }

View file

@ -34,7 +34,7 @@ if (DEBUG_SCROLL) {
// using bind means that we get to keep useful line numbers in the console
var debuglog = console.log.bind(console);
} else {
var debuglog = function () {};
var debuglog = function() {};
}
/* This component implements an intelligent scrolling list.
@ -600,7 +600,7 @@ module.exports = React.createClass({
stuckAtBottom: false,
trackedScrollToken: node.dataset.scrollToken,
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
}
};
debuglog("Saved scroll state", this.scrollState);
return;
}

View file

@ -38,7 +38,7 @@ if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
var debuglog = console.log.bind(console);
} else {
var debuglog = function () {};
var debuglog = function() {};
}
/*
@ -322,7 +322,7 @@ var TimelinePanel = React.createClass({
});
},
onMessageListScroll: function () {
onMessageListScroll: function() {
if (this.props.onScroll) {
this.props.onScroll();
}
@ -387,7 +387,7 @@ var TimelinePanel = React.createClass({
// if we're at the end of the live timeline, append the pending events
if (this.props.timelineSet.room && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
events.push(... this.props.timelineSet.room.getPendingEvents());
events.push(...this.props.timelineSet.room.getPendingEvents());
}
var updatedState = {events: events};
@ -564,8 +564,9 @@ var TimelinePanel = React.createClass({
// first find where the current RM is
for (var i = 0; i < events.length; i++) {
if (events[i].getId() == this.state.readMarkerEventId)
if (events[i].getId() == this.state.readMarkerEventId) {
break;
}
}
if (i >= events.length) {
return;
@ -644,7 +645,7 @@ var TimelinePanel = React.createClass({
var tl = this.props.timelineSet.getTimelineForEvent(rmId);
var rmTs;
if (tl) {
var event = tl.getEvents().find((e) => { return e.getId() == rmId });
var event = tl.getEvents().find((e) => { return e.getId() == rmId; });
if (event) {
rmTs = event.getTs();
}
@ -821,7 +822,7 @@ var TimelinePanel = React.createClass({
description: message,
onFinished: onFinished,
});
}
};
var prom = this._timelineWindow.load(eventId, INITIAL_SIZE);
@ -843,7 +844,7 @@ var TimelinePanel = React.createClass({
timelineLoading: true,
});
prom = prom.then(onLoaded, onError)
prom = prom.then(onLoaded, onError);
}
prom.done();
@ -868,7 +869,7 @@ var TimelinePanel = React.createClass({
// if we're at the end of the live timeline, append the pending events
if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
events.push(... this.props.timelineSet.getPendingEvents());
events.push(...this.props.timelineSet.getPendingEvents());
}
return events;
@ -930,8 +931,9 @@ var TimelinePanel = React.createClass({
_getCurrentReadReceipt: function(ignoreSynthesized) {
var client = MatrixClientPeg.get();
// the client can be null on logout
if (client == null)
if (client == null) {
return null;
}
var myUserId = client.credentials.userId;
return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized);

View file

@ -57,7 +57,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
// }];
if (uploads.length == 0) {
return <div />
return <div />;
}
var upload;
@ -68,7 +68,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
}
}
if (!upload) {
return <div />
return <div />;
}
var innerProgressStyle = {
@ -76,7 +76,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
};
var uploadedSize = filesize(upload.loaded);
var totalSize = filesize(upload.total);
if (uploadedSize.replace(/^.* /,'') === totalSize.replace(/^.* /,'')) {
if (uploadedSize.replace(/^.* /, '') === totalSize.replace(/^.* /, '')) {
uploadedSize = uploadedSize.replace(/ .*/, '');
}

View file

@ -33,6 +33,53 @@ var AccessibleButton = require('../views/elements/AccessibleButton');
const REACT_SDK_VERSION =
'dist' in package_json ? package_json.version : package_json.gitHead || "<local>";
// Enumerate some simple 'flip a bit' UI settings (if any).
// 'id' gives the key name in the im.vector.web.settings account data event
// 'label' is how we describe it in the UI.
const SETTINGS_LABELS = [
/*
{
id: 'alwaysShowTimestamps',
label: 'Always show message timestamps',
},
{
id: 'showTwelveHourTimestamps',
label: 'Show timestamps in 12 hour format (e.g. 2:30pm)',
},
{
id: 'useCompactLayout',
label: 'Use compact timeline layout',
},
{
id: 'useFixedWidthFont',
label: 'Use fixed width font',
},
*/
];
// Enumerate the available themes, with a nice human text label.
// 'id' gives the key name in the im.vector.web.settings account data event
// 'value' is the value for that key in the event
// 'label' is how we describe it in the UI.
//
// XXX: Ideally we would have a theme manifest or something and they'd be nicely
// packaged up in a single directory, and/or located at the application layer.
// But for now for expedience we just hardcode them here.
const THEMES = [
{
id: 'theme',
label: 'Light theme',
value: 'light',
},
{
id: 'theme',
label: 'Dark theme',
value: 'dark',
}
];
module.exports = React.createClass({
displayName: 'UserSettings',
@ -94,6 +141,12 @@ module.exports = React.createClass({
middleOpacity: 0.3,
});
this._refreshFromServer();
var syncedSettings = UserSettingsStore.getSyncedSettings();
if (!syncedSettings.theme) {
syncedSettings.theme = 'light';
}
this._syncedSettings = syncedSettings;
},
componentDidMount: function() {
@ -294,8 +347,8 @@ module.exports = React.createClass({
this.setState({email_add_pending: false});
if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
var message = "Unable to verify email address. "
message += "Please check your email and click on the link it contains. Once this is done, click continue."
var message = "Unable to verify email address. ";
message += "Please check your email and click on the link it contains. Once this is done, click continue.";
Modal.createDialog(QuestionDialog, {
title: "Verification Pending",
description: message,
@ -343,60 +396,68 @@ module.exports = React.createClass({
_renderUserInterfaceSettings: function() {
var client = MatrixClientPeg.get();
var settingsLabels = [
/*
{
id: 'alwaysShowTimestamps',
label: 'Always show message timestamps',
},
{
id: 'showTwelveHourTimestamps',
label: 'Show timestamps in 12 hour format (e.g. 2:30pm)',
},
{
id: 'useCompactLayout',
label: 'Use compact timeline layout',
},
{
id: 'useFixedWidthFont',
label: 'Use fixed width font',
},
*/
];
var syncedSettings = UserSettingsStore.getSyncedSettings();
return (
<div>
<h3>User Interface</h3>
<div className="mx_UserSettings_section">
<div className="mx_UserSettings_toggle">
<input id="urlPreviewsDisabled"
type="checkbox"
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
onChange={ e => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) }
/>
<label htmlFor="urlPreviewsDisabled">
Disable inline URL previews by default
</label>
</div>
{ this._renderUrlPreviewSelector() }
{ SETTINGS_LABELS.map( this._renderSyncedSetting ) }
{ THEMES.map( this._renderThemeSelector ) }
</div>
{ settingsLabels.forEach( setting => {
<div className="mx_UserSettings_toggle">
<input id={ setting.id }
type="checkbox"
defaultChecked={ syncedSettings[setting.id] }
onChange={ e => UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) }
/>
<label htmlFor={ setting.id }>
{ settings.label }
</label>
</div>
})}
</div>
);
},
_renderUrlPreviewSelector: function() {
return <div className="mx_UserSettings_toggle">
<input id="urlPreviewsDisabled"
type="checkbox"
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
onChange={ e => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) }
/>
<label htmlFor="urlPreviewsDisabled">
Disable inline URL previews by default
</label>
</div>;
},
_renderSyncedSetting: function(setting) {
return <div className="mx_UserSettings_toggle" key={ setting.id }>
<input id={ setting.id }
type="checkbox"
defaultChecked={ this._syncedSettings[setting.id] }
onChange={ e => UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) }
/>
<label htmlFor={ setting.id }>
{ setting.label }
</label>
</div>;
},
_renderThemeSelector: function(setting) {
return <div className="mx_UserSettings_toggle" key={ setting.id + "_" + setting.value }>
<input id={ setting.id + "_" + setting.value }
type="radio"
name={ setting.id }
value={ setting.value }
defaultChecked={ this._syncedSettings[setting.id] === setting.value }
onChange={ e => {
if (e.target.checked) {
UserSettingsStore.setSyncedSetting(setting.id, setting.value);
}
dis.dispatch({
action: 'set_theme',
value: setting.value,
});
}
}
/>
<label htmlFor={ setting.id + "_" + setting.value }>
{ setting.label }
</label>
</div>;
},
_renderCryptoInfo: function() {
const client = MatrixClientPeg.get();
const deviceId = client.deviceId;
@ -407,8 +468,8 @@ module.exports = React.createClass({
<h3>Cryptography</h3>
<div className="mx_UserSettings_section mx_UserSettings_cryptoSection">
<ul>
<li><label>Device ID:</label> <span><code>{deviceId}</code></span></li>
<li><label>Device key:</label> <span><code><b>{identityKey}</b></code></span></li>
<li><label>Device ID:</label> <span><code>{deviceId}</code></span></li>
<li><label>Device key:</label> <span><code><b>{identityKey}</b></code></span></li>
</ul>
</div>
</div>
@ -425,7 +486,7 @@ module.exports = React.createClass({
);
},
_renderLabs: function () {
_renderLabs: function() {
// default to enabled if undefined
if (this.props.enableLabs === false) return null;
@ -461,7 +522,7 @@ module.exports = React.createClass({
{features}
</div>
</div>
)
);
},
_renderDeactivateAccount: function() {
@ -545,10 +606,10 @@ module.exports = React.createClass({
<label htmlFor={id}>{this.nameForMedium(val.medium)}</label>
</div>
<div className="mx_UserSettings_profileInputCell">
<input key={val.address} id={id} value={val.address} disabled />
<input type="text" key={val.address} id={id} value={val.address} disabled />
</div>
<div className="mx_UserSettings_threepidButton">
<img src="img/icon_context_delete.svg" width="14" height="14" alt="Remove" onClick={this.onRemoveThreepidClicked.bind(this, val)} />
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
<img src="img/cancel-small.svg" width="14" height="14" alt="Remove" onClick={this.onRemoveThreepidClicked.bind(this, val)} />
</div>
</div>
);
@ -570,7 +631,7 @@ module.exports = React.createClass({
blurToCancel={ false }
onValueChanged={ this.onAddThreepidClicked } />
</div>
<div className="mx_UserSettings_threepidButton">
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
<img src="img/plus.svg" width="14" height="14" alt="Add" onClick={ this.onAddThreepidClicked.bind(this, undefined, true) }/>
</div>
</div>
@ -651,7 +712,7 @@ module.exports = React.createClass({
</div>
<div className="mx_UserSettings_avatarPicker_edit">
<label htmlFor="avatarInput" ref="file_label">
<img src="img/camera.svg"
<img src="img/camera.svg" className="mx_filterFlipColor"
alt="Upload avatar" title="Upload avatar"
width="17" height="15" />
</label>

View file

@ -58,7 +58,7 @@ module.exports = React.createClass({
this.setState({
progress: null
});
})
});
},
onVerify: function(ev) {
@ -71,7 +71,7 @@ module.exports = React.createClass({
this.setState({ progress: "complete" });
}, (err) => {
this.showErrorDialog(err.message);
})
});
},
onSubmitForm: function(ev) {
@ -129,7 +129,7 @@ module.exports = React.createClass({
var resetPasswordJsx;
if (this.state.progress === "sending_email") {
resetPasswordJsx = <Spinner />
resetPasswordJsx = <Spinner />;
}
else if (this.state.progress === "sent_email") {
resetPasswordJsx = (

View file

@ -173,7 +173,7 @@ module.exports = React.createClass({
},
_getCurrentFlowStep: function() {
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null;
},
_setStateFromError: function(err, isLoginAttempt) {
@ -195,7 +195,7 @@ module.exports = React.createClass({
}
let errorText = "Error: Problem communicating with the given homeserver " +
(errCode ? "(" + errCode + ")" : "")
(errCode ? "(" + errCode + ")" : "");
if (err.cors === 'rejected') {
if (window.location.protocol === 'https:' &&
@ -258,7 +258,7 @@ module.exports = React.createClass({
loginAsGuestJsx =
<a className="mx_Login_create" onClick={this._onLoginAsGuestClick} href="#">
Login as guest
</a>
</a>;
}
var returnToAppJsx;
@ -266,7 +266,7 @@ module.exports = React.createClass({
returnToAppJsx =
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
Return to app
</a>
</a>;
}
return (

View file

@ -49,6 +49,21 @@ module.exports = React.createClass({
email: React.PropTypes.string,
username: React.PropTypes.string,
guestAccessToken: React.PropTypes.string,
teamsConfig: 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,
}),
defaultDeviceDisplayName: React.PropTypes.string,
@ -169,6 +184,26 @@ 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 (!team.rooms) {
break;
}
team.rooms.forEach((room) => {
if (room.autoJoin) {
console.log("Auto-joining " + room.id);
MatrixClientPeg.get().joinRoom(room.id);
}
});
break;
}
}
}
if (self.props.brand) {
MatrixClientPeg.get().getPushers().done((resp)=>{
var pushers = resp.pushers;
@ -254,6 +289,7 @@ module.exports = React.createClass({
defaultUsername={this.state.formVals.username}
defaultEmail={this.state.formVals.email}
defaultPassword={this.state.formVals.password}
teamsConfig={this.props.teamsConfig}
guestUsername={this.props.username}
minPasswordLength={MIN_PASSWORD_LENGTH}
onError={this.onFormValidationFailed}
@ -297,7 +333,7 @@ module.exports = React.createClass({
returnToAppJsx =
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
Return to app
</a>
</a>;
}
return (