Merge branch 'develop' into luke/rts-welcome-pages
Conflicts: src/components/views/avatars/BaseAvatar.js
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -35,7 +35,7 @@ var FilePanel = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {
|
||||
timelineSet: null,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
|
||||
@@ -21,6 +21,7 @@ import KeyCode from '../../KeyCode';
|
||||
import Notifier from '../../Notifier';
|
||||
import PageTypes from '../../PageTypes';
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
|
||||
/**
|
||||
* This is what our MatrixChat shows when we are logged in. The precise view is
|
||||
@@ -161,8 +162,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:
|
||||
@@ -171,24 +172,25 @@ 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}/>
|
||||
referralBaseUrl={this.props.config.referralBaseUrl}
|
||||
/>;
|
||||
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.HomePage:
|
||||
@@ -201,7 +203,7 @@ export default React.createClass({
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -66,15 +66,28 @@ module.exports = React.createClass({
|
||||
defaultDeviceDisplayName: React.PropTypes.string,
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
appConfig: React.PropTypes.object,
|
||||
},
|
||||
|
||||
AuxPanel: {
|
||||
RoomSettings: "room_settings",
|
||||
},
|
||||
|
||||
getChildContext: function() {
|
||||
return {
|
||||
appConfig: this.props.config,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
var s = {
|
||||
loading: true,
|
||||
screen: undefined,
|
||||
|
||||
// What the LoggedInView would be showing if visible
|
||||
page_type: null,
|
||||
|
||||
// If we are viewing a room by alias, this contains the alias
|
||||
currentRoomAlias: null,
|
||||
|
||||
@@ -235,8 +248,6 @@ module.exports = React.createClass({
|
||||
setStateForNewScreen: function(state) {
|
||||
const newState = {
|
||||
screen: undefined,
|
||||
currentRoomAlias: null,
|
||||
currentRoomId: null,
|
||||
viewUserId: null,
|
||||
logged_in: false,
|
||||
ready: false,
|
||||
@@ -449,6 +460,9 @@ module.exports = React.createClass({
|
||||
middleOpacity: payload.middleOpacity,
|
||||
});
|
||||
break;
|
||||
case 'set_theme':
|
||||
this._onSetTheme(payload.value);
|
||||
break;
|
||||
case 'on_logged_in':
|
||||
this._onLoggedIn();
|
||||
break;
|
||||
@@ -579,6 +593,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
|
||||
*/
|
||||
@@ -601,6 +659,9 @@ module.exports = React.createClass({
|
||||
ready: false,
|
||||
collapse_lhs: false,
|
||||
collapse_rhs: false,
|
||||
currentRoomAlias: null,
|
||||
currentRoomId: null,
|
||||
page_type: PageTypes.RoomDirectory,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -687,6 +748,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) {
|
||||
@@ -983,7 +1054,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');
|
||||
@@ -1002,11 +1073,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}
|
||||
teamServerConfig={this.props.config.teamServerConfig}
|
||||
customHsUrl={this.getCurrentHsUrl()}
|
||||
customIsUrl={this.getCurrentIsUrl()}
|
||||
registrationUrl={this.props.registrationUrl}
|
||||
@@ -1025,6 +1098,7 @@ module.exports = React.createClass({
|
||||
customHsUrl={this.getCurrentHsUrl()}
|
||||
customIsUrl={this.getCurrentIsUrl()}
|
||||
onComplete={this.onLoginClick}
|
||||
onRegisterClick={this.onRegisterClick}
|
||||
onLoginClick={this.onLoginClick} />
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -19,7 +19,9 @@ 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;
|
||||
|
||||
/* (almost) stateless UI component which builds the event tiles in the room timeline.
|
||||
*/
|
||||
@@ -229,6 +231,7 @@ module.exports = React.createClass({
|
||||
|
||||
_getEventTiles: function() {
|
||||
var EventTile = sdk.getComponent('rooms.EventTile');
|
||||
var DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
|
||||
|
||||
this.eventNodes = {};
|
||||
@@ -278,8 +281,7 @@ module.exports = React.createClass({
|
||||
|
||||
var isMembershipChange = (e) =>
|
||||
e.getType() === 'm.room.member'
|
||||
&& ['join', 'leave'].indexOf(e.event.content.membership) !== -1
|
||||
&& (!e.event.prev_content || e.event.content.membership !== e.event.prev_content.membership);
|
||||
&& (!e.getPrevContent() || e.getContent().membership !== e.getPrevContent().membership);
|
||||
|
||||
for (i = 0; i < this.props.events.length; i++) {
|
||||
var mxEv = this.props.events[i];
|
||||
@@ -292,37 +294,63 @@ module.exports = React.createClass({
|
||||
|
||||
var last = (i == lastShownEventIndex);
|
||||
|
||||
// Wrap consecutive member events in a ListSummary
|
||||
if (isMembershipChange(mxEv)) {
|
||||
let summarisedEvents = [mxEv];
|
||||
i++;
|
||||
for (;i < this.props.events.length; i++) {
|
||||
let collapsedMxEv = this.props.events[i];
|
||||
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
||||
if (isMembershipChange(mxEv) && EventTile.haveTileForEvent(mxEv)) {
|
||||
let ts1 = mxEv.getTs();
|
||||
// Ensure that the key of the MemberEventListSummary does not change with new
|
||||
// member events. This will prevent it from being re-created unnecessarily, and
|
||||
// instead will allow new props to be provided. In turn, the shouldComponentUpdate
|
||||
// method on MELS can be used to prevent unnecessary renderings.
|
||||
//
|
||||
// Whilst back-paginating with a MELS at the top of the panel, prevEvent will be null,
|
||||
// so use the key "membereventlistsummary-initial". Otherwise, use the ID of the first
|
||||
// membership event, which will not change during forward pagination.
|
||||
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
|
||||
|
||||
if (!isMembershipChange(collapsedMxEv)) {
|
||||
i--;
|
||||
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
|
||||
let dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1}/></li>;
|
||||
ret.push(dateSeparator);
|
||||
}
|
||||
|
||||
let summarisedEvents = [mxEv];
|
||||
for (;i + 1 < this.props.events.length; i++) {
|
||||
let collapsedMxEv = this.props.events[i + 1];
|
||||
|
||||
// Ignore redacted member events
|
||||
if (!EventTile.haveTileForEvent(collapsedMxEv)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isMembershipChange(collapsedMxEv) ||
|
||||
this._wantsDateSeparator(this.props.events[i], collapsedMxEv.getDate())) {
|
||||
break;
|
||||
}
|
||||
summarisedEvents.push(collapsedMxEv);
|
||||
}
|
||||
// At this point, i = this.props.events.length OR i = the index of the last
|
||||
// MembershipChange in a sequence of MembershipChanges
|
||||
// At this point, i = the index of the last event in the summary sequence
|
||||
|
||||
let eventTiles = summarisedEvents.map(
|
||||
(e) => {
|
||||
let ret = this._getTilesForEvent(prevEvent, e);
|
||||
// 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
|
||||
// timestamp of the current event, and no DateSeperator is inserted.
|
||||
let ret = this._getTilesForEvent(e, e);
|
||||
prevEvent = e;
|
||||
return ret;
|
||||
}
|
||||
).reduce((a,b) => a.concat(b));
|
||||
).reduce((a, b) => a.concat(b));
|
||||
|
||||
if (eventTiles.length === 0) {
|
||||
eventTiles = null;
|
||||
}
|
||||
|
||||
ret.push(
|
||||
<MemberEventListSummary key={mxEv.getId()} events={summarisedEvents}>
|
||||
{eventTiles}
|
||||
<MemberEventListSummary
|
||||
key={key}
|
||||
events={summarisedEvents}
|
||||
data-scroll-token={eventId}>
|
||||
{eventTiles}
|
||||
</MemberEventListSummary>
|
||||
);
|
||||
continue;
|
||||
@@ -405,12 +433,14 @@ module.exports = React.createClass({
|
||||
// local echoes have a fake date, which could even be yesterday. Treat them
|
||||
// as 'today' for the date separators.
|
||||
var ts1 = mxEv.getTs();
|
||||
var eventDate = mxEv.getDate();
|
||||
if (mxEv.status) {
|
||||
ts1 = new Date();
|
||||
eventDate = new Date();
|
||||
ts1 = eventDate.getTime();
|
||||
}
|
||||
|
||||
// do we need a date separator since the last event?
|
||||
if (this._wantsDateSeparator(prevEvent, ts1)) {
|
||||
if (this._wantsDateSeparator(prevEvent, eventDate)) {
|
||||
var dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1}/></li>;
|
||||
ret.push(dateSeparator);
|
||||
continuation = false;
|
||||
@@ -447,38 +477,48 @@ module.exports = React.createClass({
|
||||
return ret;
|
||||
},
|
||||
|
||||
_wantsDateSeparator: function(prevEvent, nextEventTs) {
|
||||
_wantsDateSeparator: function(prevEvent, nextEventDate) {
|
||||
if (prevEvent == null) {
|
||||
// first event in the panel: depends if we could back-paginate from
|
||||
// here.
|
||||
return !this.props.suppressFirstDateSeparator;
|
||||
}
|
||||
|
||||
return (new Date(prevEvent.getTs()).toDateString()
|
||||
!== new Date(nextEventTs).toDateString());
|
||||
},
|
||||
|
||||
// get a list of the userids whose read receipts should
|
||||
// be shown next to this event
|
||||
_getReadReceiptsForEvent: function(event) {
|
||||
var myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
|
||||
// get list of read receipts, sorted most recent first
|
||||
var room = MatrixClientPeg.get().getRoom(event.getRoomId());
|
||||
if (!room) {
|
||||
// huh.
|
||||
return null;
|
||||
// Return early for events that are > 24h apart
|
||||
if (Math.abs(prevEvent.getTs() - nextEventDate.getTime()) > MILLIS_IN_DAY) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return room.getReceiptsForEvent(event).filter(function(r) {
|
||||
return r.type === "m.read" && r.userId != myUserId;
|
||||
}).sort(function(r1, r2) {
|
||||
return r2.data.ts - r1.data.ts;
|
||||
}).map(function(r) {
|
||||
return room.getMember(r.userId);
|
||||
}).filter(function(m) {
|
||||
// check that the user is a known room member
|
||||
return m;
|
||||
// Compare weekdays
|
||||
return prevEvent.getDate().getDay() !== nextEventDate.getDay();
|
||||
},
|
||||
|
||||
// get a list of read receipts that should be shown next to this event
|
||||
// Receipts are objects which have a 'roomMember' and 'ts'.
|
||||
_getReadReceiptsForEvent: function(event) {
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
|
||||
// get list of read receipts, sorted most recent first
|
||||
const room = MatrixClientPeg.get().getRoom(event.getRoomId());
|
||||
if (!room) {
|
||||
return null;
|
||||
}
|
||||
let receipts = [];
|
||||
room.getReceiptsForEvent(event).forEach((r) => {
|
||||
if (!r.userId || r.type !== "m.read" || r.userId === myUserId) {
|
||||
return; // ignore non-read receipts and receipts from self.
|
||||
}
|
||||
let member = room.getMember(r.userId);
|
||||
if (!member) {
|
||||
return; // ignore unknown user IDs
|
||||
}
|
||||
receipts.push({
|
||||
roomMember: member,
|
||||
ts: r.data ? r.data.ts : 0,
|
||||
});
|
||||
});
|
||||
|
||||
return receipts.sort((r1, r2) => {
|
||||
return r2.ts - r1.ts;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -564,6 +604,7 @@ module.exports = React.createClass({
|
||||
onScroll={ this.props.onScroll }
|
||||
onResize={ this.onResize }
|
||||
onFillRequest={ this.props.onFillRequest }
|
||||
onUnfillRequest={ this.props.onUnfillRequest }
|
||||
style={ style }
|
||||
stickyBottom={ this.props.stickyBottom }>
|
||||
{topSpinner}
|
||||
|
||||
@@ -19,6 +19,12 @@ var sdk = require('../../index');
|
||||
var dis = require("../../dispatcher");
|
||||
var WhoIsTyping = require("../../WhoIsTyping");
|
||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
const MemberAvatar = require("../views/avatars/MemberAvatar");
|
||||
|
||||
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',
|
||||
@@ -45,6 +51,10 @@ module.exports = React.createClass({
|
||||
// more interesting)
|
||||
hasActiveCall: React.PropTypes.bool,
|
||||
|
||||
// Number of names to display in typing indication. E.g. set to 3, will
|
||||
// result in "X, Y, Z and 100 others are typing."
|
||||
whoIsTypingLimit: React.PropTypes.number,
|
||||
|
||||
// callback for when the user clicks on the 'resend all' button in the
|
||||
// 'unsent messages' bar
|
||||
onResendAllClick: React.PropTypes.func,
|
||||
@@ -60,12 +70,28 @@ 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,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
whoIsTypingLimit: 2,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
syncState: MatrixClientPeg.get().getSyncState(),
|
||||
whoisTypingString: WhoIsTyping.whoIsTypingString(this.props.room),
|
||||
whoisTypingString: WhoIsTyping.whoIsTypingString(
|
||||
this.props.room,
|
||||
this.props.whoIsTypingLimit
|
||||
),
|
||||
};
|
||||
},
|
||||
|
||||
@@ -78,6 +104,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() {
|
||||
@@ -100,39 +138,35 @@ module.exports = React.createClass({
|
||||
|
||||
onRoomMemberTyping: function(ev, member) {
|
||||
this.setState({
|
||||
whoisTypingString: WhoIsTyping.whoIsTypingString(this.props.room),
|
||||
whoisTypingString: WhoIsTyping.whoIsTypingString(
|
||||
this.props.room,
|
||||
this.props.whoIsTypingLimit
|
||||
),
|
||||
});
|
||||
},
|
||||
|
||||
// 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 +207,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(this.props.whoIsTypingLimit)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -184,6 +216,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() {
|
||||
|
||||
@@ -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");
|
||||
@@ -719,15 +722,11 @@ module.exports = React.createClass({
|
||||
if (!result.displayname) {
|
||||
var SetDisplayNameDialog = sdk.getComponent('views.dialogs.SetDisplayNameDialog');
|
||||
var dialog_defer = q.defer();
|
||||
var dialog_ref;
|
||||
Modal.createDialog(SetDisplayNameDialog, {
|
||||
currentDisplayName: result.displayname,
|
||||
ref: (r) => {
|
||||
dialog_ref = r;
|
||||
},
|
||||
onFinished: (submitted) => {
|
||||
onFinished: (submitted, newDisplayName) => {
|
||||
if (submitted) {
|
||||
cli.setDisplayName(dialog_ref.getValue()).done(() => {
|
||||
cli.setDisplayName(newDisplayName).done(() => {
|
||||
dialog_defer.resolve();
|
||||
});
|
||||
}
|
||||
@@ -758,7 +757,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;
|
||||
|
||||
@@ -810,11 +809,6 @@ module.exports = React.createClass({
|
||||
} else {
|
||||
var msg = error.message ? error.message : JSON.stringify(error);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
if (msg === "No known servers") {
|
||||
// minging kludge until https://matrix.org/jira/browse/SYN-678 is fixed
|
||||
// 'Error when trying to join an empty room should be more explicit'
|
||||
msg = "It is not currently possible to re-join an empty room.";
|
||||
}
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to join room",
|
||||
description: msg
|
||||
@@ -967,7 +961,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,
|
||||
@@ -1030,7 +1024,7 @@ module.exports = React.createClass({
|
||||
if (scrollPanel) {
|
||||
scrollPanel.checkScroll();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var lastRoomId;
|
||||
|
||||
@@ -1095,7 +1089,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) {
|
||||
@@ -1104,7 +1098,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
|
||||
}
|
||||
@@ -1188,7 +1182,7 @@ module.exports = React.createClass({
|
||||
this.setState({ searching: true });
|
||||
},
|
||||
|
||||
onCancelSearchClick: function () {
|
||||
onCancelSearchClick: function() {
|
||||
this.setState({
|
||||
searching: false,
|
||||
searchResults: null,
|
||||
@@ -1213,8 +1207,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();
|
||||
|
||||
@@ -1336,6 +1331,20 @@ module.exports = React.createClass({
|
||||
// no longer anything to do here
|
||||
},
|
||||
|
||||
onStatusBarVisible: function() {
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
statusBarVisible: true,
|
||||
});
|
||||
},
|
||||
|
||||
onStatusBarHidden: function() {
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
statusBarVisible: false,
|
||||
});
|
||||
},
|
||||
|
||||
showSettings: function(show) {
|
||||
// XXX: this is a bit naughty; we should be doing this via props
|
||||
if (show) {
|
||||
@@ -1500,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} />
|
||||
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}
|
||||
@@ -1518,7 +1528,10 @@ module.exports = React.createClass({
|
||||
onCancelAllClick={this.onCancelAllClick}
|
||||
onScrollToBottomClick={this.jumpToLiveTimeline}
|
||||
onResize={this.onChildResize}
|
||||
/>
|
||||
onVisible={this.onStatusBarVisible}
|
||||
onHidden={this.onStatusBarHidden}
|
||||
whoIsTypingLimit={2}
|
||||
/>;
|
||||
}
|
||||
|
||||
var aux = null;
|
||||
@@ -1574,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
|
||||
@@ -1602,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 =
|
||||
@@ -1619,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
|
||||
@@ -1672,6 +1685,10 @@ module.exports = React.createClass({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let statusBarAreaClass = "mx_RoomView_statusArea mx_fadable";
|
||||
if (isStatusAreaExpanded) {
|
||||
statusBarAreaClass += " mx_RoomView_statusArea_expanded";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") } ref="roomView">
|
||||
@@ -1694,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 }
|
||||
|
||||
@@ -23,11 +23,18 @@ var KeyCode = require('../../KeyCode');
|
||||
var DEBUG_SCROLL = false;
|
||||
// var DEBUG_SCROLL = true;
|
||||
|
||||
// The amount of extra scroll distance to allow prior to unfilling.
|
||||
// See _getExcessHeight.
|
||||
const UNPAGINATION_PADDING = 1500;
|
||||
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent
|
||||
// many scroll events causing many unfilling requests.
|
||||
const UNFILL_REQUEST_DEBOUNCE_MS = 200;
|
||||
|
||||
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.
|
||||
@@ -101,6 +108,17 @@ module.exports = React.createClass({
|
||||
*/
|
||||
onFillRequest: React.PropTypes.func,
|
||||
|
||||
/* onUnfillRequest(backwards): a callback which is called on scroll when
|
||||
* there are children elements that are far out of view and could be removed
|
||||
* without causing pagination to occur.
|
||||
*
|
||||
* This function should accept a boolean, which is true to indicate the back/top
|
||||
* of the panel and false otherwise, and a scroll token, which refers to the
|
||||
* first element to remove if removing from the front/bottom, and last element
|
||||
* to remove if removing from the back/top.
|
||||
*/
|
||||
onUnfillRequest: React.PropTypes.func,
|
||||
|
||||
/* onScroll: a callback which is called whenever any scroll happens.
|
||||
*/
|
||||
onScroll: React.PropTypes.func,
|
||||
@@ -124,6 +142,7 @@ module.exports = React.createClass({
|
||||
stickyBottom: true,
|
||||
startAtBottom: true,
|
||||
onFillRequest: function(backwards) { return q(false); },
|
||||
onUnfillRequest: function(backwards, scrollToken) {},
|
||||
onScroll: function() {},
|
||||
};
|
||||
},
|
||||
@@ -226,6 +245,46 @@ module.exports = React.createClass({
|
||||
return sn.scrollHeight - Math.ceil(sn.scrollTop) <= sn.clientHeight + 3;
|
||||
},
|
||||
|
||||
// returns the vertical height in the given direction that can be removed from
|
||||
// the content box (which has a height of scrollHeight, see checkFillState) without
|
||||
// pagination occuring.
|
||||
//
|
||||
// padding* = UNPAGINATION_PADDING
|
||||
//
|
||||
// ### Region determined as excess.
|
||||
//
|
||||
// .---------. - -
|
||||
// |#########| | |
|
||||
// |#########| - | scrollTop |
|
||||
// | | | padding* | |
|
||||
// | | | | |
|
||||
// .-+---------+-. - - | |
|
||||
// : | | : | | |
|
||||
// : | | : | clientHeight | |
|
||||
// : | | : | | |
|
||||
// .-+---------+-. - - |
|
||||
// | | | | | |
|
||||
// | | | | | clientHeight | scrollHeight
|
||||
// | | | | | |
|
||||
// `-+---------+-' - |
|
||||
// : | | : | |
|
||||
// : | | : | clientHeight |
|
||||
// : | | : | |
|
||||
// `-+---------+-' - - |
|
||||
// | | | padding* |
|
||||
// | | | |
|
||||
// |#########| - |
|
||||
// |#########| |
|
||||
// `---------' -
|
||||
_getExcessHeight: function(backwards) {
|
||||
var sn = this._getScrollNode();
|
||||
if (backwards) {
|
||||
return sn.scrollTop - sn.clientHeight - UNPAGINATION_PADDING;
|
||||
} else {
|
||||
return sn.scrollHeight - (sn.scrollTop + 2*sn.clientHeight) - UNPAGINATION_PADDING;
|
||||
}
|
||||
},
|
||||
|
||||
// check the scroll state and send out backfill requests if necessary.
|
||||
checkFillState: function() {
|
||||
if (this.unmounted) {
|
||||
@@ -268,6 +327,55 @@ module.exports = React.createClass({
|
||||
}
|
||||
},
|
||||
|
||||
// check if unfilling is possible and send an unfill request if necessary
|
||||
_checkUnfillState: function(backwards) {
|
||||
let excessHeight = this._getExcessHeight(backwards);
|
||||
if (excessHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
var itemlist = this.refs.itemlist;
|
||||
var tiles = itemlist.children;
|
||||
|
||||
// The scroll token of the first/last tile to be unpaginated
|
||||
let markerScrollToken = null;
|
||||
|
||||
// Subtract clientHeights to simulate the events being unpaginated whilst counting
|
||||
// the events to be unpaginated.
|
||||
if (backwards) {
|
||||
// Iterate forwards from start of tiles, subtracting event tile height
|
||||
let i = 0;
|
||||
while (i < tiles.length && excessHeight > tiles[i].clientHeight) {
|
||||
excessHeight -= tiles[i].clientHeight;
|
||||
if (tiles[i].dataset.scrollToken) {
|
||||
markerScrollToken = tiles[i].dataset.scrollToken;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
// Iterate backwards from end of tiles, subtracting event tile height
|
||||
let i = tiles.length - 1;
|
||||
while (i > 0 && excessHeight > tiles[i].clientHeight) {
|
||||
excessHeight -= tiles[i].clientHeight;
|
||||
if (tiles[i].dataset.scrollToken) {
|
||||
markerScrollToken = tiles[i].dataset.scrollToken;
|
||||
}
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
if (markerScrollToken) {
|
||||
// Use a debouncer to prevent multiple unfill calls in quick succession
|
||||
// This is to make the unfilling process less aggressive
|
||||
if (this._unfillDebouncer) {
|
||||
clearTimeout(this._unfillDebouncer);
|
||||
}
|
||||
this._unfillDebouncer = setTimeout(() => {
|
||||
this._unfillDebouncer = null;
|
||||
this.props.onUnfillRequest(backwards, markerScrollToken);
|
||||
}, UNFILL_REQUEST_DEBOUNCE_MS);
|
||||
}
|
||||
},
|
||||
|
||||
// check if there is already a pending fill request. If not, set one off.
|
||||
_maybeFill: function(backwards) {
|
||||
var dir = backwards ? 'b' : 'f';
|
||||
@@ -285,7 +393,7 @@ module.exports = React.createClass({
|
||||
this._pendingFillRequests[dir] = true;
|
||||
var fillPromise;
|
||||
try {
|
||||
fillPromise = this.props.onFillRequest(backwards);
|
||||
fillPromise = this.props.onFillRequest(backwards);
|
||||
} catch (e) {
|
||||
this._pendingFillRequests[dir] = false;
|
||||
throw e;
|
||||
@@ -294,6 +402,12 @@ module.exports = React.createClass({
|
||||
q.finally(fillPromise, () => {
|
||||
this._pendingFillRequests[dir] = false;
|
||||
}).then((hasMoreResults) => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
// Unpaginate once filling is complete
|
||||
this._checkUnfillState(!backwards);
|
||||
|
||||
debuglog("ScrollPanel: "+dir+" fill complete; hasMoreResults:"+hasMoreResults);
|
||||
if (hasMoreResults) {
|
||||
// further pagination requests have been disabled until now, so
|
||||
@@ -456,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) {
|
||||
@@ -468,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;
|
||||
}
|
||||
|
||||
@@ -486,13 +600,13 @@ module.exports = React.createClass({
|
||||
stuckAtBottom: false,
|
||||
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() {
|
||||
@@ -526,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);
|
||||
},
|
||||
|
||||
@@ -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() {};
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -108,7 +108,9 @@ var TimelinePanel = React.createClass({
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
timelineCap: 250,
|
||||
// By default, disable the timelineCap in favour of unpaginating based on
|
||||
// event tile heights. (See _unpaginateEvents)
|
||||
timelineCap: Number.MAX_VALUE,
|
||||
className: 'mx_RoomView_messagePanel',
|
||||
};
|
||||
},
|
||||
@@ -245,6 +247,34 @@ var TimelinePanel = React.createClass({
|
||||
}
|
||||
},
|
||||
|
||||
onMessageListUnfillRequest: function(backwards, scrollToken) {
|
||||
let dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||
debuglog("TimelinePanel: unpaginating events in direction", dir);
|
||||
|
||||
// All tiles are inserted by MessagePanel to have a scrollToken === eventId
|
||||
let eventId = scrollToken;
|
||||
|
||||
let marker = this.state.events.findIndex(
|
||||
(ev) => {
|
||||
return ev.getId() === eventId;
|
||||
}
|
||||
);
|
||||
|
||||
let count = backwards ? marker + 1 : this.state.events.length - marker;
|
||||
|
||||
if (count > 0) {
|
||||
debuglog("TimelinePanel: Unpaginating", count, "in direction", dir);
|
||||
this._timelineWindow.unpaginate(count, backwards);
|
||||
|
||||
// We can now paginate in the unpaginated direction
|
||||
const canPaginateKey = (backwards) ? 'canBackPaginate' : 'canForwardPaginate';
|
||||
this.setState({
|
||||
[canPaginateKey]: true,
|
||||
events: this._getEvents(),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// set off a pagination request.
|
||||
onMessageListFillRequest: function(backwards) {
|
||||
var dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||
@@ -292,7 +322,7 @@ var TimelinePanel = React.createClass({
|
||||
});
|
||||
},
|
||||
|
||||
onMessageListScroll: function () {
|
||||
onMessageListScroll: function() {
|
||||
if (this.props.onScroll) {
|
||||
this.props.onScroll();
|
||||
}
|
||||
@@ -357,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};
|
||||
@@ -534,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;
|
||||
@@ -614,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();
|
||||
}
|
||||
@@ -780,7 +811,7 @@ var TimelinePanel = React.createClass({
|
||||
});
|
||||
};
|
||||
}
|
||||
var message = "Riot was trying to load a specific point in this room's timeline but ";
|
||||
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 {
|
||||
@@ -791,7 +822,7 @@ var TimelinePanel = React.createClass({
|
||||
description: message,
|
||||
onFinished: onFinished,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var prom = this._timelineWindow.load(eventId, INITIAL_SIZE);
|
||||
|
||||
@@ -813,7 +844,7 @@ var TimelinePanel = React.createClass({
|
||||
timelineLoading: true,
|
||||
});
|
||||
|
||||
prom = prom.then(onLoaded, onError)
|
||||
prom = prom.then(onLoaded, onError);
|
||||
}
|
||||
|
||||
prom.done();
|
||||
@@ -838,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;
|
||||
@@ -900,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);
|
||||
@@ -984,6 +1016,7 @@ var TimelinePanel = React.createClass({
|
||||
stickyBottom={ stickyBottom }
|
||||
onScroll={ this.onMessageListScroll }
|
||||
onFillRequest={ this.onMessageListFillRequest }
|
||||
onUnfillRequest={ this.onMessageListUnfillRequest }
|
||||
opacity={ this.props.opacity }
|
||||
className={ this.props.className }
|
||||
tileShape={ this.props.tileShape }
|
||||
|
||||
@@ -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(/ .*/, '');
|
||||
}
|
||||
|
||||
|
||||
@@ -26,12 +26,61 @@ var UserSettingsStore = require('../../UserSettingsStore');
|
||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
var Email = require('../../email');
|
||||
var AddThreepid = require('../../AddThreepid');
|
||||
var SdkConfig = require('../../SdkConfig');
|
||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||
|
||||
// if this looks like a release, use the 'version' from package.json; else use
|
||||
// the git sha.
|
||||
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',
|
||||
|
||||
@@ -43,6 +92,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,
|
||||
},
|
||||
@@ -61,6 +113,7 @@ module.exports = React.createClass({
|
||||
phase: "UserSettings.LOADING", // LOADING, DISPLAY
|
||||
email_add_pending: false,
|
||||
vectorVersion: null,
|
||||
rejectingInvites: false,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -80,12 +133,24 @@ module.exports = React.createClass({
|
||||
});
|
||||
}
|
||||
|
||||
// Bulk rejecting invites:
|
||||
// /sync won't have had time to return when UserSettings re-renders from state changes, so getRooms()
|
||||
// will still return rooms with invites. To get around this, add a listener for
|
||||
// membership updates and kick the UI.
|
||||
MatrixClientPeg.get().on("RoomMember.membership", this._onInviteStateChange);
|
||||
|
||||
dis.dispatch({
|
||||
action: 'ui_opacity',
|
||||
sideOpacity: 0.3,
|
||||
middleOpacity: 0.3,
|
||||
});
|
||||
this._refreshFromServer();
|
||||
|
||||
var syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||
if (!syncedSettings.theme) {
|
||||
syncedSettings.theme = 'light';
|
||||
}
|
||||
this._syncedSettings = syncedSettings;
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
@@ -101,6 +166,10 @@ module.exports = React.createClass({
|
||||
middleOpacity: 1.0,
|
||||
});
|
||||
dis.unregister(this.dispatcherRef);
|
||||
let cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("RoomMember.membership", this._onInviteStateChange);
|
||||
}
|
||||
},
|
||||
|
||||
_refreshFromServer: function() {
|
||||
@@ -164,8 +233,26 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
onLogoutClicked: function(ev) {
|
||||
var LogoutPrompt = sdk.getComponent('dialogs.LogoutPrompt');
|
||||
this.logoutModal = Modal.createDialog(LogoutPrompt);
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: "Sign out?",
|
||||
description:
|
||||
<div>
|
||||
For security, logging out will delete any end-to-end encryption keys from this browser,
|
||||
making previous encrypted chat history unreadable if you log back in.
|
||||
In future this <a href="https://github.com/vector-im/riot-web/issues/2108">will be improved</a>,
|
||||
but for now be warned.
|
||||
</div>,
|
||||
button: "Sign out",
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
dis.dispatch({action: 'logout'});
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
onPasswordChangeError: function(err) {
|
||||
@@ -237,6 +324,31 @@ module.exports = React.createClass({
|
||||
this.setState({email_add_pending: true});
|
||||
},
|
||||
|
||||
onRemoveThreepidClicked: function(threepid) {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: "Remove Contact Information?",
|
||||
description: "Remove " + threepid.address + "?",
|
||||
button: 'Remove',
|
||||
onFinished: (submit) => {
|
||||
if (submit) {
|
||||
this.setState({
|
||||
phase: "UserSettings.LOADING",
|
||||
});
|
||||
MatrixClientPeg.get().deleteThreePid(threepid.medium, threepid.address).then(() => {
|
||||
return this._refreshFromServer();
|
||||
}).catch((err) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Unable to remove contact information",
|
||||
description: err.toString(),
|
||||
});
|
||||
}).done();
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
onEmailDialogFinished: function(ok) {
|
||||
if (ok) {
|
||||
this.verifyEmailAddress();
|
||||
@@ -257,8 +369,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,
|
||||
@@ -280,91 +392,185 @@ module.exports = React.createClass({
|
||||
Modal.createDialog(DeactivateAccountDialog, {});
|
||||
},
|
||||
|
||||
_onBugReportClicked: function() {
|
||||
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
|
||||
if (!BugReportDialog) {
|
||||
return;
|
||||
}
|
||||
Modal.createDialog(BugReportDialog, {});
|
||||
},
|
||||
|
||||
_onInviteStateChange: function(event, member, oldMembership) {
|
||||
if (member.userId === this._me && oldMembership === "invite") {
|
||||
this.forceUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
_onRejectAllInvitesClicked: function(rooms, ev) {
|
||||
this.setState({
|
||||
rejectingInvites: true
|
||||
});
|
||||
// reject the invites
|
||||
let promises = rooms.map((room) => {
|
||||
return MatrixClientPeg.get().leave(room.roomId);
|
||||
});
|
||||
// purposefully drop errors to the floor: we'll just have a non-zero number on the UI
|
||||
// after trying to reject all the invites.
|
||||
q.allSettled(promises).then(() => {
|
||||
this.setState({
|
||||
rejectingInvites: false
|
||||
});
|
||||
}).done();
|
||||
},
|
||||
|
||||
_onExportE2eKeysClicked: function() {
|
||||
Modal.createDialogAsync(
|
||||
(cb) => {
|
||||
require.ensure(['../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
|
||||
cb(require('../../async-components/views/dialogs/ExportE2eKeysDialog'));
|
||||
}, "e2e-export");
|
||||
}, {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
_onImportE2eKeysClicked: function() {
|
||||
Modal.createDialogAsync(
|
||||
(cb) => {
|
||||
require.ensure(['../../async-components/views/dialogs/ImportE2eKeysDialog'], () => {
|
||||
cb(require('../../async-components/views/dialogs/ImportE2eKeysDialog'));
|
||||
}, "e2e-export");
|
||||
}, {
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
_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();
|
||||
|
||||
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() {
|
||||
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
|
||||
return null;
|
||||
const client = MatrixClientPeg.get();
|
||||
const deviceId = client.deviceId;
|
||||
const identityKey = client.getDeviceEd25519Key() || "<not supported>";
|
||||
|
||||
let exportButton = null,
|
||||
importButton = null;
|
||||
|
||||
if (client.isCryptoEnabled) {
|
||||
exportButton = (
|
||||
<AccessibleButton className="mx_UserSettings_button"
|
||||
onClick={this._onExportE2eKeysClicked}>
|
||||
Export E2E room keys
|
||||
</AccessibleButton>
|
||||
);
|
||||
importButton = (
|
||||
<AccessibleButton className="mx_UserSettings_button"
|
||||
onClick={this._onImportE2eKeysClicked}>
|
||||
Import E2E room keys
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
var client = MatrixClientPeg.get();
|
||||
var deviceId = client.deviceId;
|
||||
var identityKey = client.getDeviceEd25519Key() || "<not supported>";
|
||||
|
||||
var myDevice = client.getStoredDevicesForUser(MatrixClientPeg.get().credentials.userId)[0];
|
||||
return (
|
||||
<div>
|
||||
<h3>Cryptography</h3>
|
||||
<div className="mx_UserSettings_section mx_UserSettings_cryptoSection">
|
||||
<ul>
|
||||
<li><label>Device name:</label> <span>{ myDevice.getDisplayName() }</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>
|
||||
<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>
|
||||
{exportButton}
|
||||
{importButton}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_renderDevicesPanel: function() {
|
||||
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
|
||||
return null;
|
||||
}
|
||||
var DevicesPanel = sdk.getComponent('settings.DevicesPanel');
|
||||
return (
|
||||
<div>
|
||||
@@ -374,7 +580,24 @@ module.exports = React.createClass({
|
||||
);
|
||||
},
|
||||
|
||||
_renderLabs: function () {
|
||||
_renderBugReport: function() {
|
||||
if (!SdkConfig.get().bug_report_endpoint_url) {
|
||||
return <div />
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h3>Bug Report</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
<p>Found a bug?</p>
|
||||
<button className="mx_UserSettings_button danger"
|
||||
onClick={this._onBugReportClicked}>Report it
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_renderLabs: function() {
|
||||
// default to enabled if undefined
|
||||
if (this.props.enableLabs === false) return null;
|
||||
|
||||
@@ -410,7 +633,7 @@ module.exports = React.createClass({
|
||||
{features}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
_renderDeactivateAccount: function() {
|
||||
@@ -420,15 +643,49 @@ module.exports = React.createClass({
|
||||
return <div>
|
||||
<h3>Deactivate Account</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
<button className="mx_UserSettings_button danger"
|
||||
<AccessibleButton className="mx_UserSettings_button danger"
|
||||
onClick={this._onDeactivateAccountClicked}>Deactivate my account
|
||||
</button>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
_renderBulkOptions: function() {
|
||||
let invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => {
|
||||
return r.hasMembershipState(this._me, "invite");
|
||||
});
|
||||
if (invitedRooms.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
||||
let reject = <Spinner />;
|
||||
if (!this.state.rejectingInvites) {
|
||||
// bind() the invited rooms so any new invites that may come in as this button is clicked
|
||||
// don't inadvertently get rejected as well.
|
||||
reject = (
|
||||
<AccessibleButton className="mx_UserSettings_button danger"
|
||||
onClick={this._onRejectAllInvitesClicked.bind(this, invitedRooms)}>
|
||||
Reject all {invitedRooms.length} invites
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return <div>
|
||||
<h3>Bulk Options</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
{reject}
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
nameForMedium: function(medium) {
|
||||
if (medium == 'msisdn') return 'Phone';
|
||||
return medium[0].toUpperCase() + medium.slice(1);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
switch (this.state.phase) {
|
||||
case "UserSettings.LOADING":
|
||||
@@ -452,15 +709,18 @@ module.exports = React.createClass({
|
||||
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
|
||||
);
|
||||
|
||||
var threepidsSection = this.state.threepids.map(function(val, pidIndex) {
|
||||
var id = "email-" + val.address;
|
||||
var threepidsSection = this.state.threepids.map((val, pidIndex) => {
|
||||
const id = "3pid-" + val.address;
|
||||
return (
|
||||
<div className="mx_UserSettings_profileTableRow" key={pidIndex}>
|
||||
<div className="mx_UserSettings_profileLabelCell">
|
||||
<label htmlFor={id}>Email</label>
|
||||
<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 mx_filterFlipColor">
|
||||
<img src="img/cancel-small.svg" width="14" height="14" alt="Remove" onClick={this.onRemoveThreepidClicked.bind(this, val)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -482,7 +742,7 @@ module.exports = React.createClass({
|
||||
blurToCancel={ false }
|
||||
onValueChanged={ this.onAddThreepidClicked } />
|
||||
</div>
|
||||
<div className="mx_UserSettings_addThreepid">
|
||||
<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>
|
||||
@@ -563,7 +823,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>
|
||||
@@ -576,19 +836,23 @@ module.exports = React.createClass({
|
||||
|
||||
<div className="mx_UserSettings_section">
|
||||
|
||||
<div className="mx_UserSettings_logout mx_UserSettings_button" onClick={this.onLogoutClicked}>
|
||||
<AccessibleButton className="mx_UserSettings_logout mx_UserSettings_button" onClick={this.onLogoutClicked}>
|
||||
Sign out
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
|
||||
{accountJsx}
|
||||
</div>
|
||||
|
||||
{this._renderReferral()}
|
||||
|
||||
{notification_area}
|
||||
|
||||
{this._renderUserInterfaceSettings()}
|
||||
{this._renderLabs()}
|
||||
{this._renderDevicesPanel()}
|
||||
{this._renderCryptoInfo()}
|
||||
{this._renderBulkOptions()}
|
||||
{this._renderBugReport()}
|
||||
|
||||
<h3>Advanced</h3>
|
||||
|
||||
|
||||
@@ -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) {
|
||||
@@ -87,10 +87,26 @@ module.exports = React.createClass({
|
||||
this.showErrorDialog("New passwords must match each other.");
|
||||
}
|
||||
else {
|
||||
this.submitPasswordReset(
|
||||
this.state.enteredHomeserverUrl, this.state.enteredIdentityServerUrl,
|
||||
this.state.email, this.state.password
|
||||
);
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: "Warning",
|
||||
description:
|
||||
<div>
|
||||
Resetting password will currently reset any end-to-end encryption keys on all devices,
|
||||
making encrypted chat history unreadable.
|
||||
In future this <a href="https://github.com/vector-im/riot-web/issues/2671">may be improved</a>,
|
||||
but for now be warned.
|
||||
</div>,
|
||||
button: "Continue",
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.submitPasswordReset(
|
||||
this.state.enteredHomeserverUrl, this.state.enteredIdentityServerUrl,
|
||||
this.state.email, this.state.password
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -129,7 +145,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 = (
|
||||
|
||||
@@ -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:' &&
|
||||
@@ -203,7 +203,7 @@ module.exports = React.createClass({
|
||||
!this.state.enteredHomeserverUrl.startsWith("http")))
|
||||
{
|
||||
errorText = <span>
|
||||
Can't connect to homeserver via HTTP when using Riot served by HTTPS.
|
||||
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>
|
||||
</span>;
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,8 +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,
|
||||
teamServerConfig: React.PropTypes.shape({
|
||||
// Email address to request new teams
|
||||
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,
|
||||
|
||||
@@ -60,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
|
||||
@@ -75,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(
|
||||
@@ -88,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() {
|
||||
@@ -169,6 +210,43 @@ module.exports = React.createClass({
|
||||
accessToken: response.access_token
|
||||
});
|
||||
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
// Auto-join rooms
|
||||
team.rooms.forEach((room) => {
|
||||
if (room.auto_join && room.room_id) {
|
||||
console.log(`Auto-joining ${room.room_id}`);
|
||||
MatrixClientPeg.get().joinRoom(room.room_id);
|
||||
}
|
||||
});
|
||||
}, (err) => {
|
||||
console.error('Error getting team config', err);
|
||||
});
|
||||
}, (err) => {
|
||||
console.error('Error tracking referral', err);
|
||||
});
|
||||
}
|
||||
|
||||
if (self.props.brand) {
|
||||
MatrixClientPeg.get().getPushers().done((resp)=>{
|
||||
var pushers = resp.pushers;
|
||||
@@ -238,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) {
|
||||
@@ -248,16 +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.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":
|
||||
@@ -273,6 +366,7 @@ module.exports = React.createClass({
|
||||
if (serverParams && serverParams["m.login.recaptcha"]) {
|
||||
publicKey = serverParams["m.login.recaptcha"].public_key;
|
||||
}
|
||||
|
||||
registerStep = (
|
||||
<CaptchaForm sitePublicKey={publicKey}
|
||||
onCaptchaResponse={this.onCaptchaResponse}
|
||||
@@ -285,7 +379,6 @@ module.exports = React.createClass({
|
||||
}
|
||||
var busySpinner;
|
||||
if (this.state.busy) {
|
||||
var Spinner = sdk.getComponent("elements.Spinner");
|
||||
busySpinner = (
|
||||
<Spinner />
|
||||
);
|
||||
@@ -296,7 +389,7 @@ module.exports = React.createClass({
|
||||
returnToAppJsx =
|
||||
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
||||
Return to app
|
||||
</a>
|
||||
</a>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -330,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>
|
||||
|
||||
Reference in New Issue
Block a user