Merge branch 'develop' into luke/rts-welcome-pages
Conflicts: src/components/views/avatars/BaseAvatar.js
This commit is contained in:
commit
acde1f3db7
153 changed files with 5544 additions and 1599 deletions
|
@ -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>
|
||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
|||
var React = require('react');
|
||||
var AvatarLogic = require("../../../Avatar");
|
||||
import sdk from '../../../index';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'BaseAvatar',
|
||||
|
@ -41,7 +42,7 @@ module.exports = React.createClass({
|
|||
height: 40,
|
||||
resizeMethod: 'crop',
|
||||
defaultToInitialLetter: true
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -138,7 +139,7 @@ module.exports = React.createClass({
|
|||
|
||||
const {
|
||||
name, idName, title, url, urls, width, height, resizeMethod,
|
||||
defaultToInitialLetter, viewUserOnClick,
|
||||
defaultToInitialLetter, onClick,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
|
@ -156,12 +157,24 @@ module.exports = React.createClass({
|
|||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<img className="mx_BaseAvatar mx_BaseAvatar_image" src={imageUrl}
|
||||
onError={this.onError}
|
||||
width={width} height={height}
|
||||
title={title} alt=""
|
||||
{...otherProps} />
|
||||
);
|
||||
if (onClick != null) {
|
||||
return (
|
||||
<AccessibleButton className="mx_BaseAvatar" onClick={onClick}>
|
||||
<img className="mx_BaseAvatar_image" src={imageUrl}
|
||||
onError={this.onError}
|
||||
width={width} height={height}
|
||||
title={title} alt=""
|
||||
{...otherProps} />
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<img className="mx_BaseAvatar mx_BaseAvatar_image" src={imageUrl}
|
||||
onError={this.onError}
|
||||
width={width} height={height}
|
||||
title={title} alt=""
|
||||
{...otherProps} />
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -33,6 +33,7 @@ module.exports = React.createClass({
|
|||
onClick: React.PropTypes.func,
|
||||
// Whether the onClick of the avatar should be overriden to dispatch 'view_user'
|
||||
viewUserOnClick: React.PropTypes.bool,
|
||||
title: React.PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -41,7 +42,7 @@ module.exports = React.createClass({
|
|||
height: 40,
|
||||
resizeMethod: 'crop',
|
||||
viewUserOnClick: false,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -58,26 +59,26 @@ module.exports = React.createClass({
|
|||
}
|
||||
return {
|
||||
name: props.member.name,
|
||||
title: props.member.userId,
|
||||
title: props.title || props.member.userId,
|
||||
imageUrl: Avatar.avatarUrlForMember(props.member,
|
||||
props.width,
|
||||
props.height,
|
||||
props.resizeMethod)
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
|
||||
var {member, onClick, ...otherProps} = this.props;
|
||||
var {member, onClick, viewUserOnClick, ...otherProps} = this.props;
|
||||
|
||||
if (this.props.viewUserOnClick) {
|
||||
if (viewUserOnClick) {
|
||||
onClick = () => {
|
||||
dispatcher.dispatch({
|
||||
action: 'view_user',
|
||||
member: this.props.member,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -39,7 +39,7 @@ module.exports = React.createClass({
|
|||
height: 36,
|
||||
resizeMethod: 'crop',
|
||||
oobData: {},
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -51,7 +51,7 @@ module.exports = React.createClass({
|
|||
componentWillReceiveProps: function(newProps) {
|
||||
this.setState({
|
||||
urls: this.getImageUrls(newProps)
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
getImageUrls: function(props) {
|
||||
|
|
|
@ -40,7 +40,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onValueChanged: function(ev) {
|
||||
this.props.onChange(ev.target.value)
|
||||
this.props.onChange(ev.target.value);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
|
72
src/components/views/dialogs/BaseDialog.js
Normal file
72
src/components/views/dialogs/BaseDialog.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import * as KeyCode from '../../../KeyCode';
|
||||
|
||||
/**
|
||||
* Basic container for modal dialogs.
|
||||
*
|
||||
* Includes a div for the title, and a keypress handler which cancels the
|
||||
* dialog on escape.
|
||||
*/
|
||||
export default React.createClass({
|
||||
displayName: 'BaseDialog',
|
||||
|
||||
propTypes: {
|
||||
// onFinished callback to call when Escape is pressed
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
|
||||
// callback to call when Enter is pressed
|
||||
onEnterPressed: React.PropTypes.func,
|
||||
|
||||
// CSS class to apply to dialog div
|
||||
className: React.PropTypes.string,
|
||||
|
||||
// Title for the dialog.
|
||||
// (could probably actually be something more complicated than a string if desired)
|
||||
title: React.PropTypes.string.isRequired,
|
||||
|
||||
// children should be the content of the dialog
|
||||
children: React.PropTypes.node,
|
||||
},
|
||||
|
||||
_onKeyDown: function(e) {
|
||||
if (e.keyCode === KeyCode.ESCAPE) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.props.onFinished();
|
||||
} else if (e.keyCode === KeyCode.ENTER) {
|
||||
if (this.props.onEnterPressed) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.props.onEnterPressed(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div onKeyDown={this._onKeyDown} className={this.props.className}>
|
||||
<div className='mx_Dialog_title'>
|
||||
{ this.props.title }
|
||||
</div>
|
||||
{ this.props.children }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -14,19 +14,30 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
var React = require("react");
|
||||
var classNames = require('classnames');
|
||||
var sdk = require("../../../index");
|
||||
var Invite = require("../../../Invite");
|
||||
var createRoom = require("../../../createRoom");
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var DMRoomMap = require('../../../utils/DMRoomMap');
|
||||
var rate_limited_func = require("../../../ratelimitedfunc");
|
||||
var dis = require("../../../dispatcher");
|
||||
var Modal = require('../../../Modal');
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import sdk from '../../../index';
|
||||
import { getAddressType, inviteMultipleToRoom } from '../../../Invite';
|
||||
import createRoom from '../../../createRoom';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import rate_limited_func from '../../../ratelimitedfunc';
|
||||
import dis from '../../../dispatcher';
|
||||
import Modal from '../../../Modal';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import q from 'q';
|
||||
|
||||
const TRUNCATE_QUERY_LIST = 40;
|
||||
|
||||
/*
|
||||
* Escapes a string so it can be used in a RegExp
|
||||
* Basically just replaces: \ ^ $ * + ? . ( ) | { } [ ]
|
||||
* From http://stackoverflow.com/a/6969486
|
||||
*/
|
||||
function escapeRegExp(str) {
|
||||
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: "ChatInviteDialog",
|
||||
propTypes: {
|
||||
|
@ -48,7 +59,7 @@ module.exports = React.createClass({
|
|||
title: "Start a chat",
|
||||
description: "Who would you like to communicate with?",
|
||||
value: "",
|
||||
placeholder: "User ID, Name or email",
|
||||
placeholder: "Email, name or matrix ID",
|
||||
button: "Start Chat",
|
||||
focus: true
|
||||
};
|
||||
|
@ -57,7 +68,14 @@ module.exports = React.createClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
error: false,
|
||||
|
||||
// List of AddressTile.InviteAddressType objects represeting
|
||||
// the list of addresses we're going to invite
|
||||
inviteList: [],
|
||||
|
||||
// List of AddressTile.InviteAddressType objects represeting
|
||||
// the set of autocompletion results for the current search
|
||||
// query.
|
||||
queryList: [],
|
||||
};
|
||||
},
|
||||
|
@ -71,15 +89,12 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onButtonClick: function() {
|
||||
var inviteList = this.state.inviteList.slice();
|
||||
let inviteList = this.state.inviteList.slice();
|
||||
// Check the text input field to see if user has an unconverted address
|
||||
// If there is and it's valid add it to the local inviteList
|
||||
var check = Invite.isValidAddress(this.refs.textinput.value);
|
||||
if (check === true || check === null) {
|
||||
inviteList.push(this.refs.textinput.value);
|
||||
} else if (this.refs.textinput.value.length > 0) {
|
||||
this.setState({ error: true });
|
||||
return;
|
||||
if (this.refs.textinput.value !== '') {
|
||||
inviteList = this._addInputToList();
|
||||
if (inviteList === null) return;
|
||||
}
|
||||
|
||||
if (inviteList.length > 0) {
|
||||
|
@ -119,15 +134,15 @@ module.exports = React.createClass({
|
|||
} else if (e.keyCode === 38) { // up arrow
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.addressSelector.onKeyUp();
|
||||
this.addressSelector.moveSelectionUp();
|
||||
} else if (e.keyCode === 40) { // down arrow
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.addressSelector.onKeyDown();
|
||||
} else if (this.state.queryList.length > 0 && (e.keyCode === 188, e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
|
||||
this.addressSelector.moveSelectionDown();
|
||||
} else if (this.state.queryList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.addressSelector.onKeySelect();
|
||||
this.addressSelector.chooseSelection();
|
||||
} else if (this.refs.textinput.value.length === 0 && this.state.inviteList.length && e.keyCode === 8) { // backspace
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
@ -135,33 +150,56 @@ module.exports = React.createClass({
|
|||
} else if (e.keyCode === 13) { // enter
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.onButtonClick();
|
||||
if (this.refs.textinput.value == '') {
|
||||
// if there's nothing in the input box, submit the form
|
||||
this.onButtonClick();
|
||||
} else {
|
||||
this._addInputToList();
|
||||
}
|
||||
} else if (e.keyCode === 188 || e.keyCode === 9) { // comma or tab
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
var check = Invite.isValidAddress(this.refs.textinput.value);
|
||||
if (check === true || check === null) {
|
||||
var inviteList = this.state.inviteList.slice();
|
||||
inviteList.push(this.refs.textinput.value.trim());
|
||||
this.setState({
|
||||
inviteList: inviteList,
|
||||
queryList: [],
|
||||
});
|
||||
} else {
|
||||
this.setState({ error: true });
|
||||
}
|
||||
this._addInputToList();
|
||||
}
|
||||
},
|
||||
|
||||
onQueryChanged: function(ev) {
|
||||
var query = ev.target.value;
|
||||
var queryList = [];
|
||||
const query = ev.target.value;
|
||||
let queryList = [];
|
||||
|
||||
// Only do search if there is something to search
|
||||
if (query.length > 0) {
|
||||
if (query.length > 0 && query != '@') {
|
||||
// filter the known users list
|
||||
queryList = this._userList.filter((user) => {
|
||||
return this._matches(query, user);
|
||||
}).map((user) => {
|
||||
// Return objects, structure of which is defined
|
||||
// by InviteAddressType
|
||||
return {
|
||||
addressType: 'mx',
|
||||
address: user.userId,
|
||||
displayName: user.displayName,
|
||||
avatarMxc: user.avatarUrl,
|
||||
isKnown: true,
|
||||
}
|
||||
});
|
||||
|
||||
// If the query isn't a user we know about, but is a
|
||||
// valid address, add an entry for that
|
||||
if (queryList.length == 0) {
|
||||
const addrType = getAddressType(query);
|
||||
if (addrType !== null) {
|
||||
queryList[0] = {
|
||||
addressType: addrType,
|
||||
address: query,
|
||||
isKnown: false,
|
||||
};
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
if (addrType == 'email') {
|
||||
this._lookupThreepid(addrType, query).done();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
|
@ -179,7 +217,8 @@ module.exports = React.createClass({
|
|||
inviteList: inviteList,
|
||||
queryList: [],
|
||||
});
|
||||
}
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
};
|
||||
},
|
||||
|
||||
onClick: function(index) {
|
||||
|
@ -191,11 +230,12 @@ module.exports = React.createClass({
|
|||
|
||||
onSelected: function(index) {
|
||||
var inviteList = this.state.inviteList.slice();
|
||||
inviteList.push(this.state.queryList[index].userId);
|
||||
inviteList.push(this.state.queryList[index]);
|
||||
this.setState({
|
||||
inviteList: inviteList,
|
||||
queryList: [],
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
},
|
||||
|
||||
_getDirectMessageRoom: function(addr) {
|
||||
|
@ -226,10 +266,14 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
const addrTexts = addrs.map((addr) => {
|
||||
return addr.address;
|
||||
});
|
||||
|
||||
if (this.props.roomId) {
|
||||
// Invite new user to a room
|
||||
var self = this;
|
||||
Invite.inviteMultipleToRoom(this.props.roomId, addrs)
|
||||
inviteMultipleToRoom(this.props.roomId, addrTexts)
|
||||
.then(function(addrs) {
|
||||
var room = MatrixClientPeg.get().getRoom(self.props.roomId);
|
||||
return self._showAnyInviteErrors(addrs, room);
|
||||
|
@ -244,9 +288,9 @@ module.exports = React.createClass({
|
|||
return null;
|
||||
})
|
||||
.done();
|
||||
} else if (this._isDmChat(addrs)) {
|
||||
} else if (this._isDmChat(addrTexts)) {
|
||||
// Start the DM chat
|
||||
createRoom({dmUserId: addrs[0]})
|
||||
createRoom({dmUserId: addrTexts[0]})
|
||||
.catch(function(err) {
|
||||
console.error(err.stack);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
@ -263,7 +307,7 @@ module.exports = React.createClass({
|
|||
var room;
|
||||
createRoom().then(function(roomId) {
|
||||
room = MatrixClientPeg.get().getRoom(roomId);
|
||||
return Invite.inviteMultipleToRoom(roomId, addrs);
|
||||
return inviteMultipleToRoom(roomId, addrTexts);
|
||||
})
|
||||
.then(function(addrs) {
|
||||
return self._showAnyInviteErrors(addrs, room);
|
||||
|
@ -281,7 +325,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
// Close - this will happen before the above, as that is async
|
||||
this.props.onFinished(true, addrs);
|
||||
this.props.onFinished(true, addrTexts);
|
||||
},
|
||||
|
||||
_updateUserList: new rate_limited_func(function() {
|
||||
|
@ -315,19 +359,27 @@ module.exports = React.createClass({
|
|||
return true;
|
||||
}
|
||||
|
||||
// split spaces in name and try matching constituent parts
|
||||
var parts = name.split(" ");
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
if (parts[i].indexOf(query) === 0) {
|
||||
return true;
|
||||
}
|
||||
// Try to find the query following a "word boundary", except that
|
||||
// this does avoids using \b because it only considers letters from
|
||||
// the roman alphabet to be word characters.
|
||||
// Instead, we look for the query following either:
|
||||
// * The start of the string
|
||||
// * Whitespace, or
|
||||
// * A fixed number of punctuation characters
|
||||
const expr = new RegExp("(?:^|[\\s\\(\)'\",\.-_@\?;:{}\\[\\]\\#~`\\*\\&\\$])" + escapeRegExp(query));
|
||||
if (expr.test(name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
_isOnInviteList: function(uid) {
|
||||
for (let i = 0; i < this.state.inviteList.length; i++) {
|
||||
if (this.state.inviteList[i].toLowerCase() === uid) {
|
||||
if (
|
||||
this.state.inviteList[i].addressType == 'mx' &&
|
||||
this.state.inviteList[i].address.toLowerCase() === uid
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -335,7 +387,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_isDmChat: function(addrs) {
|
||||
if (addrs.length === 1 && Invite.getAddressType(addrs[0]) === "mx" && !this.props.roomId) {
|
||||
if (addrs.length === 1 && getAddressType(addrs[0]) === "mx" && !this.props.roomId) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
@ -361,9 +413,74 @@ module.exports = React.createClass({
|
|||
return addrs;
|
||||
},
|
||||
|
||||
_addInputToList: function() {
|
||||
const addressText = this.refs.textinput.value.trim();
|
||||
const addrType = getAddressType(addressText);
|
||||
const addrObj = {
|
||||
addressType: addrType,
|
||||
address: addressText,
|
||||
isKnown: false,
|
||||
};
|
||||
if (addrType == null) {
|
||||
this.setState({ error: true });
|
||||
return null;
|
||||
} else if (addrType == 'mx') {
|
||||
const user = MatrixClientPeg.get().getUser(addrObj.address);
|
||||
if (user) {
|
||||
addrObj.displayName = user.displayName;
|
||||
addrObj.avatarMxc = user.avatarUrl;
|
||||
addrObj.isKnown = true;
|
||||
}
|
||||
}
|
||||
|
||||
const inviteList = this.state.inviteList.slice();
|
||||
inviteList.push(addrObj);
|
||||
this.setState({
|
||||
inviteList: inviteList,
|
||||
queryList: [],
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
return inviteList;
|
||||
},
|
||||
|
||||
_lookupThreepid: function(medium, address) {
|
||||
let cancelled = false;
|
||||
// Note that we can't safely remove this after we're done
|
||||
// because we don't know that it's the same one, so we just
|
||||
// leave it: it's replacing the old one each time so it's
|
||||
// not like they leak.
|
||||
this._cancelThreepidLookup = function() {
|
||||
cancelled = true;
|
||||
}
|
||||
|
||||
// wait a bit to let the user finish typing
|
||||
return q.delay(500).then(() => {
|
||||
if (cancelled) return null;
|
||||
return MatrixClientPeg.get().lookupThreePid(medium, address);
|
||||
}).then((res) => {
|
||||
if (res === null || !res.mxid) return null;
|
||||
if (cancelled) return null;
|
||||
|
||||
return MatrixClientPeg.get().getProfileInfo(res.mxid);
|
||||
}).then((res) => {
|
||||
if (res === null) return null;
|
||||
if (cancelled) return null;
|
||||
this.setState({
|
||||
queryList: [{
|
||||
// an InviteAddressType
|
||||
addressType: medium,
|
||||
address: address,
|
||||
displayName: res.displayname,
|
||||
avatarMxc: res.avatar_url,
|
||||
isKnown: true,
|
||||
}]
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
var AddressSelector = sdk.getComponent("elements.AddressSelector");
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const AddressSelector = sdk.getComponent("elements.AddressSelector");
|
||||
this.scrollElement = null;
|
||||
|
||||
var query = [];
|
||||
|
@ -394,13 +511,18 @@ module.exports = React.createClass({
|
|||
var error;
|
||||
var addressSelector;
|
||||
if (this.state.error) {
|
||||
error = <div className="mx_ChatInviteDialog_error">You have entered an invalid contact. Try using their Matrix ID or email address.</div>
|
||||
error = <div className="mx_ChatInviteDialog_error">You have entered an invalid contact. Try using their Matrix ID or email address.</div>;
|
||||
} else {
|
||||
const addressSelectorHeader = <div className="mx_ChatInviteDialog_addressSelectHeader">
|
||||
Searching known users
|
||||
</div>;
|
||||
addressSelector = (
|
||||
<AddressSelector ref={(ref) => {this.addressSelector = ref}}
|
||||
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
|
||||
addressList={ this.state.queryList }
|
||||
onSelected={ this.onSelected }
|
||||
truncateAt={ TRUNCATE_QUERY_LIST } />
|
||||
truncateAt={ TRUNCATE_QUERY_LIST }
|
||||
header={ addressSelectorHeader }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -409,9 +531,10 @@ module.exports = React.createClass({
|
|||
<div className="mx_Dialog_title">
|
||||
{this.props.title}
|
||||
</div>
|
||||
<div className="mx_ChatInviteDialog_cancel" onClick={this.onCancel} >
|
||||
<AccessibleButton className="mx_ChatInviteDialog_cancel"
|
||||
onClick={this.onCancel} >
|
||||
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
<div className="mx_ChatInviteDialog_label">
|
||||
<label htmlFor="textinput">{ this.props.description }</label>
|
||||
</div>
|
||||
|
|
|
@ -80,8 +80,8 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
let error = null;
|
||||
if (this.state.errStr) {
|
||||
error = <div className="error">
|
||||
{this.state.err_str}
|
||||
</div>
|
||||
{this.state.errStr}
|
||||
</div>;
|
||||
passwordBoxClass = 'error';
|
||||
}
|
||||
|
||||
|
@ -92,7 +92,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
if (!this.state.busy) {
|
||||
cancelButton = <button onClick={this._onCancel} autoFocus={true}>
|
||||
Cancel
|
||||
</button>
|
||||
</button>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,187 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
var React = require("react");
|
||||
var sdk = require('../../../index');
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'EncryptedEventDialog',
|
||||
|
||||
propTypes: {
|
||||
event: React.PropTypes.object.isRequired,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return { device: this.refreshDevice() };
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
var client = MatrixClientPeg.get();
|
||||
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
|
||||
// no need to redownload keys if we already have the device
|
||||
if (this.state.device) {
|
||||
return;
|
||||
}
|
||||
client.downloadKeys([this.props.event.getSender()], true).done(()=>{
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({ device: this.refreshDevice() });
|
||||
}, (err)=>{
|
||||
console.log("Error downloading devices", err);
|
||||
});
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
var client = MatrixClientPeg.get();
|
||||
if (client) {
|
||||
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
}
|
||||
},
|
||||
|
||||
refreshDevice: function() {
|
||||
return MatrixClientPeg.get().getEventSenderDeviceInfo(this.props.event);
|
||||
},
|
||||
|
||||
onDeviceVerificationChanged: function(userId, device) {
|
||||
if (userId == this.props.event.getSender()) {
|
||||
this.setState({ device: this.refreshDevice() });
|
||||
}
|
||||
},
|
||||
|
||||
onKeyDown: function(e) {
|
||||
if (e.keyCode === 27) { // escape
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
},
|
||||
|
||||
_renderDeviceInfo: function() {
|
||||
var device = this.state.device;
|
||||
if (!device) {
|
||||
return (<i>unknown device</i>);
|
||||
}
|
||||
|
||||
var verificationStatus = (<b>NOT verified</b>);
|
||||
if (device.isBlocked()) {
|
||||
verificationStatus = (<b>Blocked</b>);
|
||||
} else if (device.isVerified()) {
|
||||
verificationStatus = "verified";
|
||||
}
|
||||
|
||||
return (
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{ device.getDisplayName() }</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device ID</td>
|
||||
<td><code>{ device.deviceId }</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Verification</td>
|
||||
<td>{ verificationStatus }</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Ed25519 fingerprint</td>
|
||||
<td><code>{device.getFingerprint()}</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
},
|
||||
|
||||
_renderEventInfo: function() {
|
||||
var event = this.props.event;
|
||||
|
||||
return (
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>User ID</td>
|
||||
<td>{ event.getSender() }</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Curve25519 identity key</td>
|
||||
<td><code>{ event.getSenderKey() || <i>none</i> }</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Claimed Ed25519 fingerprint key</td>
|
||||
<td><code>{ event.getKeysClaimed().ed25519 || <i>none</i> }</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Algorithm</td>
|
||||
<td>{ event.getWireContent().algorithm || <i>unencrypted</i> }</td>
|
||||
</tr>
|
||||
{
|
||||
event.getContent().msgtype === 'm.bad.encrypted' ? (
|
||||
<tr>
|
||||
<td>Decryption error</td>
|
||||
<td>{ event.getContent().body }</td>
|
||||
</tr>
|
||||
) : null
|
||||
}
|
||||
<tr>
|
||||
<td>Session ID</td>
|
||||
<td><code>{ event.getWireContent().session_id || <i>none</i> }</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
|
||||
|
||||
var buttons = null;
|
||||
if (this.state.device) {
|
||||
buttons = (
|
||||
<DeviceVerifyButtons device={ this.state.device }
|
||||
userId={ this.props.event.getSender() }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_EncryptedEventDialog" onKeyDown={ this.onKeyDown }>
|
||||
<div className="mx_Dialog_title">
|
||||
End-to-end encryption information
|
||||
</div>
|
||||
<div className="mx_Dialog_content">
|
||||
<h4>Event information</h4>
|
||||
{this._renderEventInfo()}
|
||||
|
||||
<h4>Sender device information</h4>
|
||||
{this._renderDeviceInfo()}
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={ this.props.onFinished } autoFocus={ true }>
|
||||
OK
|
||||
</button>
|
||||
{buttons}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -25,9 +25,10 @@ limitations under the License.
|
|||
* });
|
||||
*/
|
||||
|
||||
var React = require("react");
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
|
||||
module.exports = React.createClass({
|
||||
export default React.createClass({
|
||||
displayName: 'ErrorDialog',
|
||||
propTypes: {
|
||||
title: React.PropTypes.string,
|
||||
|
@ -49,20 +50,11 @@ module.exports = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
onKeyDown: function(e) {
|
||||
if (e.keyCode === 27) { // escape
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<div className="mx_ErrorDialog" onKeyDown={ this.onKeyDown }>
|
||||
<div className="mx_Dialog_title">
|
||||
{this.props.title}
|
||||
</div>
|
||||
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
||||
title={this.props.title}>
|
||||
<div className="mx_Dialog_content">
|
||||
{this.props.description}
|
||||
</div>
|
||||
|
@ -71,7 +63,7 @@ module.exports = React.createClass({
|
|||
{this.props.button}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -111,20 +111,9 @@ export default React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onKeyDown: function(e) {
|
||||
if (e.keyCode === 27) { // escape
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (!this.state.busy) {
|
||||
this._onCancel();
|
||||
}
|
||||
}
|
||||
else if (e.keyCode === 13) { // enter
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (this.state.submitButtonEnabled && !this.state.busy) {
|
||||
this._onSubmit();
|
||||
}
|
||||
_onEnterPressed: function(e) {
|
||||
if (this.state.submitButtonEnabled && !this.state.busy) {
|
||||
this._onSubmit();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -171,6 +160,7 @@ export default React.createClass({
|
|||
|
||||
render: function() {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
let error = null;
|
||||
if (this.state.errorText) {
|
||||
|
@ -200,10 +190,11 @@ export default React.createClass({
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="mx_InteractiveAuthDialog" onKeyDown={this._onKeyDown}>
|
||||
<div className="mx_Dialog_title">
|
||||
{this.props.title}
|
||||
</div>
|
||||
<BaseDialog className="mx_InteractiveAuthDialog"
|
||||
onEnterPressed={this._onEnterPressed}
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.props.title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>This operation requires additional authentication.</p>
|
||||
{this._renderCurrentStage()}
|
||||
|
@ -213,7 +204,7 @@ export default React.createClass({
|
|||
{submitButton}
|
||||
{cancelButton}
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
var React = require('react');
|
||||
var dis = require("../../../dispatcher");
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'LogoutPrompt',
|
||||
|
||||
propTypes: {
|
||||
onFinished: React.PropTypes.func,
|
||||
},
|
||||
|
||||
logOut: function() {
|
||||
dis.dispatch({action: 'logout'});
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
}
|
||||
},
|
||||
|
||||
cancelPrompt: function() {
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
}
|
||||
},
|
||||
|
||||
onKeyDown: function(e) {
|
||||
if (e.keyCode === 27) { // escape
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.cancelPrompt();
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<div className="mx_Dialog_content">
|
||||
Sign out?
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons" onKeyDown={ this.onKeyDown }>
|
||||
<button className="mx_Dialog_primary" autoFocus onClick={this.logOut}>Sign Out</button>
|
||||
<button onClick={this.cancelPrompt}>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -23,8 +23,9 @@ limitations under the License.
|
|||
* });
|
||||
*/
|
||||
|
||||
var React = require("react");
|
||||
var dis = require("../../../dispatcher");
|
||||
import React from 'react';
|
||||
import dis from '../../../dispatcher';
|
||||
import sdk from '../../../index';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'NeedToRegisterDialog',
|
||||
|
@ -54,11 +55,12 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<div className="mx_NeedToRegisterDialog">
|
||||
<div className="mx_Dialog_title">
|
||||
{this.props.title}
|
||||
</div>
|
||||
<BaseDialog className="mx_NeedToRegisterDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.props.title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
{this.props.description}
|
||||
</div>
|
||||
|
@ -70,7 +72,7 @@ module.exports = React.createClass({
|
|||
Register
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
var React = require("react");
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
|
||||
module.exports = React.createClass({
|
||||
export default React.createClass({
|
||||
displayName: 'QuestionDialog',
|
||||
propTypes: {
|
||||
title: React.PropTypes.string,
|
||||
|
@ -46,25 +47,13 @@ module.exports = React.createClass({
|
|||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
onKeyDown: function(e) {
|
||||
if (e.keyCode === 27) { // escape
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
else if (e.keyCode === 13) { // enter
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<div className="mx_QuestionDialog" onKeyDown={ this.onKeyDown }>
|
||||
<div className="mx_Dialog_title">
|
||||
{this.props.title}
|
||||
</div>
|
||||
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={ this.onOk }
|
||||
title={this.props.title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
{this.props.description}
|
||||
</div>
|
||||
|
@ -77,7 +66,7 @@ module.exports = React.createClass({
|
|||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,11 +14,16 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
var React = require("react");
|
||||
var sdk = require("../../../index.js");
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
module.exports = React.createClass({
|
||||
/**
|
||||
* Prompt the user to set a display name.
|
||||
*
|
||||
* On success, `onFinished(true, newDisplayName)` is called.
|
||||
*/
|
||||
export default React.createClass({
|
||||
displayName: 'SetDisplayNameDialog',
|
||||
propTypes: {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
|
@ -42,10 +47,6 @@ module.exports = React.createClass({
|
|||
this.refs.input_value.select();
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
return this.state.value;
|
||||
},
|
||||
|
||||
onValueChange: function(ev) {
|
||||
this.setState({
|
||||
value: ev.target.value
|
||||
|
@ -54,16 +55,17 @@ module.exports = React.createClass({
|
|||
|
||||
onFormSubmit: function(ev) {
|
||||
ev.preventDefault();
|
||||
this.props.onFinished(true);
|
||||
this.props.onFinished(true, this.state.value);
|
||||
return false;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<div className="mx_SetDisplayNameDialog">
|
||||
<div className="mx_Dialog_title">
|
||||
Set a Display Name
|
||||
</div>
|
||||
<BaseDialog className="mx_SetDisplayNameDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title="Set a Display Name"
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
Your display name is how you'll appear to others when you speak in rooms.<br/>
|
||||
What would you like it to be?
|
||||
|
@ -79,7 +81,7 @@ module.exports = React.createClass({
|
|||
<input className="mx_Dialog_primary" type="submit" value="Set" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
var React = require("react");
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
|
||||
module.exports = React.createClass({
|
||||
export default React.createClass({
|
||||
displayName: 'TextInputDialog',
|
||||
propTypes: {
|
||||
title: React.PropTypes.string,
|
||||
|
@ -27,7 +28,7 @@ module.exports = React.createClass({
|
|||
value: React.PropTypes.string,
|
||||
button: React.PropTypes.string,
|
||||
focus: React.PropTypes.bool,
|
||||
onFinished: React.PropTypes.func.isRequired
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -36,7 +37,7 @@ module.exports = React.createClass({
|
|||
value: "",
|
||||
description: "",
|
||||
button: "OK",
|
||||
focus: true
|
||||
focus: true,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -55,25 +56,13 @@ module.exports = React.createClass({
|
|||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
onKeyDown: function(e) {
|
||||
if (e.keyCode === 27) { // escape
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
else if (e.keyCode === 13) { // enter
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.props.onFinished(true, this.refs.textinput.value);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<div className="mx_TextInputDialog">
|
||||
<div className="mx_Dialog_title">
|
||||
{this.props.title}
|
||||
</div>
|
||||
<BaseDialog className="mx_TextInputDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={this.onOk}
|
||||
title={this.props.title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_TextInputDialog_label">
|
||||
<label htmlFor="textinput"> {this.props.description} </label>
|
||||
|
@ -90,7 +79,7 @@ module.exports = React.createClass({
|
|||
{this.props.button}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
54
src/components/views/elements/AccessibleButton.js
Normal file
54
src/components/views/elements/AccessibleButton.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
Copyright 2016 Jani Mustonen
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* AccessibleButton is a generic wrapper for any element that should be treated
|
||||
* as a button. Identifies the element as a button, setting proper tab
|
||||
* indexing and keyboard activation behavior.
|
||||
*
|
||||
* @param {Object} props react element properties
|
||||
* @returns {Object} rendered react
|
||||
*/
|
||||
export default function AccessibleButton(props) {
|
||||
const {element, onClick, children, ...restProps} = props;
|
||||
restProps.onClick = onClick;
|
||||
restProps.onKeyDown = function(e) {
|
||||
if (e.keyCode == 13 || e.keyCode == 32) return onClick();
|
||||
};
|
||||
restProps.tabIndex = restProps.tabIndex || "0";
|
||||
restProps.role = "button";
|
||||
return React.createElement(element, restProps, children);
|
||||
}
|
||||
|
||||
/**
|
||||
* children: React's magic prop. Represents all children given to the element.
|
||||
* element: (optional) The base element type. "div" by default.
|
||||
* onClick: (required) Event handler for button activation. Should be
|
||||
* implemented exactly like a normal onClick handler.
|
||||
*/
|
||||
AccessibleButton.propTypes = {
|
||||
children: React.PropTypes.node,
|
||||
element: React.PropTypes.string,
|
||||
onClick: React.PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
AccessibleButton.defaultProps = {
|
||||
element: 'div',
|
||||
};
|
||||
|
||||
AccessibleButton.displayName = "AccessibleButton";
|
|
@ -16,18 +16,24 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require("react");
|
||||
var sdk = require("../../../index");
|
||||
var classNames = require('classnames');
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import classNames from 'classnames';
|
||||
import { InviteAddressType } from './AddressTile';
|
||||
|
||||
module.exports = React.createClass({
|
||||
export default React.createClass({
|
||||
displayName: 'AddressSelector',
|
||||
|
||||
propTypes: {
|
||||
onSelected: React.PropTypes.func.isRequired,
|
||||
addressList: React.PropTypes.array.isRequired,
|
||||
|
||||
// List of the addresses to display
|
||||
addressList: React.PropTypes.arrayOf(InviteAddressType).isRequired,
|
||||
truncateAt: React.PropTypes.number.isRequired,
|
||||
selected: React.PropTypes.number,
|
||||
|
||||
// Element to put as a header on top of the list
|
||||
header: React.PropTypes.node,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -55,7 +61,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onKeyUp: function() {
|
||||
moveSelectionUp: function() {
|
||||
if (this.state.selected > 0) {
|
||||
this.setState({
|
||||
selected: this.state.selected - 1,
|
||||
|
@ -64,7 +70,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onKeyDown: function() {
|
||||
moveSelectionDown: function() {
|
||||
if (this.state.selected < this._maxSelected(this.props.addressList)) {
|
||||
this.setState({
|
||||
selected: this.state.selected + 1,
|
||||
|
@ -73,25 +79,19 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onKeySelect: function() {
|
||||
chooseSelection: function() {
|
||||
this.selectAddress(this.state.selected);
|
||||
},
|
||||
|
||||
onClick: function(index) {
|
||||
var self = this;
|
||||
return function() {
|
||||
self.selectAddress(index);
|
||||
};
|
||||
this.selectAddress(index);
|
||||
},
|
||||
|
||||
onMouseEnter: function(index) {
|
||||
var self = this;
|
||||
return function() {
|
||||
self.setState({
|
||||
selected: index,
|
||||
hover: true,
|
||||
});
|
||||
};
|
||||
this.setState({
|
||||
selected: index,
|
||||
hover: true,
|
||||
});
|
||||
},
|
||||
|
||||
onMouseLeave: function() {
|
||||
|
@ -124,8 +124,8 @@ module.exports = React.createClass({
|
|||
// Saving the addressListElement so we can use it to work out, in the componentDidUpdate
|
||||
// method, how far to scroll when using the arrow keys
|
||||
addressList.push(
|
||||
<div className={classes} onClick={this.onClick(i)} onMouseEnter={this.onMouseEnter(i)} onMouseLeave={this.onMouseLeave} key={i} ref={(ref) => { this.addressListElement = ref; }} >
|
||||
<AddressTile address={this.props.addressList[i].userId} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />
|
||||
<div className={classes} onClick={this.onClick.bind(this, i)} onMouseEnter={this.onMouseEnter.bind(this, i)} onMouseLeave={this.onMouseLeave} key={i} ref={(ref) => { this.addressListElement = ref; }} >
|
||||
<AddressTile address={this.props.addressList[i]} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -135,7 +135,7 @@ module.exports = React.createClass({
|
|||
|
||||
_maxSelected: function(list) {
|
||||
var listSize = list.length === 0 ? 0 : list.length - 1;
|
||||
var maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize
|
||||
var maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize;
|
||||
return maxSelected;
|
||||
},
|
||||
|
||||
|
@ -146,7 +146,8 @@ module.exports = React.createClass({
|
|||
});
|
||||
|
||||
return (
|
||||
<div className={classes} ref={(ref) => {this.scrollElement = ref}}>
|
||||
<div className={classes} ref={(ref) => {this.scrollElement = ref;}}>
|
||||
{ this.props.header }
|
||||
{ this.createAddressListTiles() }
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -23,16 +23,33 @@ var Invite = require("../../../Invite");
|
|||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var Avatar = require('../../../Avatar');
|
||||
|
||||
module.exports = React.createClass({
|
||||
// React PropType definition for an object describing
|
||||
// an address that can be invited to a room (which
|
||||
// could be a third party identifier or a matrix ID)
|
||||
// along with some additional information about the
|
||||
// address / target.
|
||||
export const InviteAddressType = React.PropTypes.shape({
|
||||
addressType: React.PropTypes.oneOf([
|
||||
'mx', 'email'
|
||||
]).isRequired,
|
||||
address: React.PropTypes.string.isRequired,
|
||||
displayName: React.PropTypes.string,
|
||||
avatarMxc: React.PropTypes.string,
|
||||
// true if the address is known to be a valid address (eg. is a real
|
||||
// user we've seen) or false otherwise (eg. is just an address the
|
||||
// user has entered)
|
||||
isKnown: React.PropTypes.bool,
|
||||
});
|
||||
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'AddressTile',
|
||||
|
||||
propTypes: {
|
||||
address: React.PropTypes.string.isRequired,
|
||||
address: InviteAddressType.isRequired,
|
||||
canDismiss: React.PropTypes.bool,
|
||||
onDismissed: React.PropTypes.func,
|
||||
justified: React.PropTypes.bool,
|
||||
networkName: React.PropTypes.string,
|
||||
networkUrl: React.PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -40,37 +57,30 @@ module.exports = React.createClass({
|
|||
canDismiss: false,
|
||||
onDismissed: function() {}, // NOP
|
||||
justified: false,
|
||||
networkName: "",
|
||||
networkUrl: "",
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var userId, name, imgUrl, email;
|
||||
var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const address = this.props.address;
|
||||
const name = address.displayName || address.address;
|
||||
|
||||
// Check if the addr is a valid type
|
||||
var addrType = Invite.getAddressType(this.props.address);
|
||||
if (addrType === "mx") {
|
||||
let user = MatrixClientPeg.get().getUser(this.props.address);
|
||||
if (user) {
|
||||
userId = user.userId;
|
||||
name = user.rawDisplayName || userId;
|
||||
imgUrl = Avatar.avatarUrlForUser(user, 25, 25, "crop");
|
||||
} else {
|
||||
name=this.props.address;
|
||||
imgUrl = "img/icon-mx-user.svg";
|
||||
}
|
||||
} else if (addrType === "email") {
|
||||
email = this.props.address;
|
||||
name="email";
|
||||
imgUrl = "img/icon-email-user.svg";
|
||||
} else {
|
||||
name="Unknown";
|
||||
imgUrl = "img/avatar-error.svg";
|
||||
let imgUrl;
|
||||
if (address.avatarMxc) {
|
||||
imgUrl = MatrixClientPeg.get().mxcUrlToHttp(
|
||||
address.avatarMxc, 25, 25, 'crop'
|
||||
);
|
||||
}
|
||||
|
||||
if (address.addressType === "mx") {
|
||||
if (!imgUrl) imgUrl = 'img/icon-mx-user.svg';
|
||||
} else if (address.addressType === 'email') {
|
||||
if (!imgUrl) imgUrl = 'img/icon-email-user.svg';
|
||||
} else {
|
||||
if (!imgUrl) imgUrl = "img/avatar-error.svg";
|
||||
}
|
||||
|
||||
// Removing networks for now as they're not really supported
|
||||
/*
|
||||
var network;
|
||||
if (this.props.networkUrl !== "") {
|
||||
network = (
|
||||
|
@ -79,16 +89,20 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
var info;
|
||||
var error = false;
|
||||
if (addrType === "mx" && userId) {
|
||||
var nameClasses = classNames({
|
||||
"mx_AddressTile_name": true,
|
||||
"mx_AddressTile_justified": this.props.justified,
|
||||
});
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
var idClasses = classNames({
|
||||
const nameClasses = classNames({
|
||||
"mx_AddressTile_name": true,
|
||||
"mx_AddressTile_justified": this.props.justified,
|
||||
});
|
||||
|
||||
let info;
|
||||
let error = false;
|
||||
if (address.addressType === "mx" && address.isKnown) {
|
||||
const idClasses = classNames({
|
||||
"mx_AddressTile_id": true,
|
||||
"mx_AddressTile_justified": this.props.justified,
|
||||
});
|
||||
|
@ -96,26 +110,34 @@ module.exports = React.createClass({
|
|||
info = (
|
||||
<div className="mx_AddressTile_mx">
|
||||
<div className={nameClasses}>{ name }</div>
|
||||
<div className={idClasses}>{ userId }</div>
|
||||
<div className={idClasses}>{ address.address }</div>
|
||||
</div>
|
||||
);
|
||||
} else if (addrType === "mx") {
|
||||
var unknownMxClasses = classNames({
|
||||
} else if (address.addressType === "mx") {
|
||||
const unknownMxClasses = classNames({
|
||||
"mx_AddressTile_unknownMx": true,
|
||||
"mx_AddressTile_justified": this.props.justified,
|
||||
});
|
||||
|
||||
info = (
|
||||
<div className={unknownMxClasses}>{ this.props.address }</div>
|
||||
<div className={unknownMxClasses}>{ this.props.address.address }</div>
|
||||
);
|
||||
} else if (email) {
|
||||
var emailClasses = classNames({
|
||||
} else if (address.addressType === "email") {
|
||||
const emailClasses = classNames({
|
||||
"mx_AddressTile_email": true,
|
||||
"mx_AddressTile_justified": this.props.justified,
|
||||
});
|
||||
|
||||
let nameNode = null;
|
||||
if (address.displayName) {
|
||||
nameNode = <div className={nameClasses}>{ address.displayName }</div>
|
||||
}
|
||||
|
||||
info = (
|
||||
<div className={emailClasses}>{ email }</div>
|
||||
<div className="mx_AddressTile_mx">
|
||||
<div className={emailClasses}>{ address.address }</div>
|
||||
{nameNode}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
error = true;
|
||||
|
@ -129,12 +151,12 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
var classes = classNames({
|
||||
const classes = classNames({
|
||||
"mx_AddressTile": true,
|
||||
"mx_AddressTile_error": error,
|
||||
});
|
||||
|
||||
var dismiss;
|
||||
let dismiss;
|
||||
if (this.props.canDismiss) {
|
||||
dismiss = (
|
||||
<div className="mx_AddressTile_dismiss" onClick={this.props.onDismissed} >
|
||||
|
@ -145,7 +167,6 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<div className={classes}>
|
||||
{ network }
|
||||
<div className="mx_AddressTile_avatar">
|
||||
<BaseAvatar width={25} height={25} name={name} title={name} url={imgUrl} />
|
||||
</div>
|
||||
|
|
|
@ -42,14 +42,14 @@ export default React.createClass({
|
|||
<div className="mx_UserSettings_cryptoSection">
|
||||
<ul>
|
||||
<li><label>Device name:</label> <span>{ this.props.device.getDisplayName() }</span></li>
|
||||
<li><label>Device ID:</label> <span><code>{ this.props.device.deviceId}</code></span></li>
|
||||
<li><label>Device key:</label> <span><code><b>{ this.props.device.getFingerprint() }</b></code></span></li>
|
||||
<li><label>Device ID:</label> <span><code>{ this.props.device.deviceId}</code></span></li>
|
||||
<li><label>Device key:</label> <span><code><b>{ this.props.device.getFingerprint() }</b></code></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
If it matches, press the verify button below.
|
||||
If it doesnt, then someone else is intercepting this device
|
||||
and you probably want to press the block button instead.
|
||||
and you probably want to press the blacklist button instead.
|
||||
</p>
|
||||
<p>
|
||||
In future this verification process will be more sophisticated.
|
||||
|
@ -73,33 +73,33 @@ export default React.createClass({
|
|||
);
|
||||
},
|
||||
|
||||
onBlockClick: function() {
|
||||
onBlacklistClick: function() {
|
||||
MatrixClientPeg.get().setDeviceBlocked(
|
||||
this.props.userId, this.props.device.deviceId, true
|
||||
);
|
||||
},
|
||||
|
||||
onUnblockClick: function() {
|
||||
onUnblacklistClick: function() {
|
||||
MatrixClientPeg.get().setDeviceBlocked(
|
||||
this.props.userId, this.props.device.deviceId, false
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var blockButton = null, verifyButton = null;
|
||||
var blacklistButton = null, verifyButton = null;
|
||||
|
||||
if (this.props.device.isBlocked()) {
|
||||
blockButton = (
|
||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblock"
|
||||
onClick={this.onUnblockClick}>
|
||||
Unblock
|
||||
blacklistButton = (
|
||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblacklist"
|
||||
onClick={this.onUnblacklistClick}>
|
||||
Unblacklist
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
blockButton = (
|
||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_block"
|
||||
onClick={this.onBlockClick}>
|
||||
Block
|
||||
blacklistButton = (
|
||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_blacklist"
|
||||
onClick={this.onBlacklistClick}>
|
||||
Blacklist
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ export default React.createClass({
|
|||
verifyButton = (
|
||||
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify"
|
||||
onClick={this.onVerifyClick}>
|
||||
Verify
|
||||
Verify...
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
@ -124,7 +124,7 @@ export default React.createClass({
|
|||
return (
|
||||
<div className="mx_MemberDeviceInfo mx_DeviceVerifyButtons" >
|
||||
{ verifyButton }
|
||||
{ blockButton }
|
||||
{ blacklistButton }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -89,7 +89,7 @@ export default class DirectorySearchBox extends React.Component {
|
|||
|
||||
return <span className={classnames(searchbox_classes)}>
|
||||
<div className="mx_DirectorySearchBox_container">
|
||||
<input type="text" value={this.state.value}
|
||||
<input type="text" name="dirsearch" value={this.state.value}
|
||||
className="mx_DirectorySearchBox_input"
|
||||
ref={this._collectInput}
|
||||
onChange={this._onChange} onKeyUp={this._onKeyUp}
|
||||
|
|
|
@ -57,7 +57,7 @@ module.exports = React.createClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
phase: this.Phases.Display,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
|
@ -164,7 +164,7 @@ module.exports = React.createClass({
|
|||
|
||||
this.setState({
|
||||
phase: this.Phases.Edit,
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
onFocus: function(ev) {
|
||||
|
@ -197,9 +197,9 @@ module.exports = React.createClass({
|
|||
sel.removeAllRanges();
|
||||
|
||||
if (this.props.blurToCancel)
|
||||
this.cancelEdit();
|
||||
{this.cancelEdit();}
|
||||
else
|
||||
this.onFinish(ev);
|
||||
{this.onFinish(ev);}
|
||||
|
||||
this.showPlaceholder(!this.value);
|
||||
},
|
||||
|
|
|
@ -24,7 +24,7 @@ module.exports = React.createClass({
|
|||
events: React.PropTypes.array.isRequired,
|
||||
// An array of EventTiles to render when expanded
|
||||
children: React.PropTypes.array.isRequired,
|
||||
// The maximum number of names to show in either the join or leave summaries
|
||||
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
|
||||
summaryLength: React.PropTypes.number,
|
||||
// The maximum number of avatars to display in the summary
|
||||
avatarsMaxLength: React.PropTypes.number,
|
||||
|
@ -40,93 +40,229 @@ module.exports = React.createClass({
|
|||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
summaryLength: 3,
|
||||
summaryLength: 1,
|
||||
threshold: 3,
|
||||
avatarsMaxLength: 5
|
||||
avatarsMaxLength: 5,
|
||||
};
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
// Update if
|
||||
// - The number of summarised events has changed
|
||||
// - or if the summary is currently expanded
|
||||
// - or if the summary is about to toggle to become collapsed
|
||||
// - or if there are fewEvents, meaning the child eventTiles are shown as-is
|
||||
return (
|
||||
nextProps.events.length !== this.props.events.length ||
|
||||
this.state.expanded || nextState.expanded ||
|
||||
nextProps.events.length < this.props.threshold
|
||||
);
|
||||
},
|
||||
|
||||
_toggleSummary: function() {
|
||||
this.setState({
|
||||
expanded: !this.state.expanded,
|
||||
});
|
||||
},
|
||||
|
||||
_getEventSenderName: function(ev) {
|
||||
if (!ev) {
|
||||
return 'undefined';
|
||||
}
|
||||
return ev.sender.name || ev.event.content.displayname || ev.getSender();
|
||||
},
|
||||
/**
|
||||
* Render the JSX for users aggregated by their transition sequences (`eventAggregates`) where
|
||||
* the sequences are ordered by `orderedTransitionSequences`.
|
||||
* @param {object[]} eventAggregates a map of transition sequence to array of user display names
|
||||
* or user IDs.
|
||||
* @param {string[]} orderedTransitionSequences an array which is some ordering of
|
||||
* `Object.keys(eventAggregates)`.
|
||||
* @returns {ReactElement} a single <span> containing the textual summary of the aggregated
|
||||
* events that occurred.
|
||||
*/
|
||||
_renderSummary: function(eventAggregates, orderedTransitionSequences) {
|
||||
const summaries = orderedTransitionSequences.map((transitions) => {
|
||||
const userNames = eventAggregates[transitions];
|
||||
const nameList = this._renderNameList(userNames);
|
||||
const plural = userNames.length > 1;
|
||||
|
||||
_renderNameList: function(events) {
|
||||
if (events.length === 0) {
|
||||
const splitTransitions = transitions.split(',');
|
||||
|
||||
// Some neighbouring transitions are common, so canonicalise some into "pair"
|
||||
// transitions
|
||||
const canonicalTransitions = this._getCanonicalTransitions(splitTransitions);
|
||||
// Transform into consecutive repetitions of the same transition (like 5
|
||||
// consecutive 'joined_and_left's)
|
||||
const coalescedTransitions = this._coalesceRepeatedTransitions(
|
||||
canonicalTransitions
|
||||
);
|
||||
|
||||
const descs = coalescedTransitions.map((t) => {
|
||||
return this._getDescriptionForTransition(
|
||||
t.transitionType, plural, t.repeats
|
||||
);
|
||||
});
|
||||
|
||||
const desc = this._renderCommaSeparatedList(descs);
|
||||
|
||||
return nameList + " " + desc;
|
||||
});
|
||||
|
||||
if (!summaries) {
|
||||
return null;
|
||||
}
|
||||
let originalNumber = events.length;
|
||||
events = events.slice(0, this.props.summaryLength);
|
||||
let lastEvent = events.pop();
|
||||
|
||||
let names = events.map((ev) => {
|
||||
return this._getEventSenderName(ev);
|
||||
}).join(', ');
|
||||
|
||||
let lastName = this._getEventSenderName(lastEvent);
|
||||
if (names.length === 0) {
|
||||
// special-case for a single event
|
||||
return lastName;
|
||||
}
|
||||
|
||||
let remaining = originalNumber - this.props.summaryLength;
|
||||
if (remaining > 0) {
|
||||
// name1, name2, name3, and 100 others
|
||||
return names + ', ' + lastName + ', and ' + remaining + ' others';
|
||||
} else {
|
||||
// name1, name2 and name3
|
||||
return names + ' and ' + lastName;
|
||||
}
|
||||
},
|
||||
|
||||
_renderSummary: function(joinEvents, leaveEvents) {
|
||||
let joiners = this._renderNameList(joinEvents);
|
||||
let leavers = this._renderNameList(leaveEvents);
|
||||
|
||||
let joinSummary = null;
|
||||
if (joiners) {
|
||||
joinSummary = (
|
||||
<span>
|
||||
{joiners} joined the room
|
||||
</span>
|
||||
);
|
||||
}
|
||||
let leaveSummary = null;
|
||||
if (leavers) {
|
||||
leaveSummary = (
|
||||
<span>
|
||||
{leavers} left the room
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span>
|
||||
{joinSummary}{joinSummary && leaveSummary?'; ':''}
|
||||
{leaveSummary}
|
||||
{summaries.join(", ")}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
|
||||
_renderAvatars: function(events) {
|
||||
let avatars = events.slice(0, this.props.avatarsMaxLength).map((e) => {
|
||||
/**
|
||||
* @param {string[]} users an array of user display names or user IDs.
|
||||
* @returns {string} a comma-separated list that ends with "and [n] others" if there are
|
||||
* more items in `users` than `this.props.summaryLength`, which is the number of names
|
||||
* included before "and [n] others".
|
||||
*/
|
||||
_renderNameList: function(users) {
|
||||
return this._renderCommaSeparatedList(users, this.props.summaryLength);
|
||||
},
|
||||
|
||||
/**
|
||||
* Canonicalise an array of transitions such that some pairs of transitions become
|
||||
* single transitions. For example an input ['joined','left'] would result in an output
|
||||
* ['joined_and_left'].
|
||||
* @param {string[]} transitions an array of transitions.
|
||||
* @returns {string[]} an array of transitions.
|
||||
*/
|
||||
_getCanonicalTransitions: function(transitions) {
|
||||
const modMap = {
|
||||
'joined': {
|
||||
'after': 'left',
|
||||
'newTransition': 'joined_and_left',
|
||||
},
|
||||
'left': {
|
||||
'after': 'joined',
|
||||
'newTransition': 'left_and_joined',
|
||||
},
|
||||
// $currentTransition : {
|
||||
// 'after' : $nextTransition,
|
||||
// 'newTransition' : 'new_transition_type',
|
||||
// },
|
||||
};
|
||||
const res = [];
|
||||
|
||||
for (let i = 0; i < transitions.length; i++) {
|
||||
const t = transitions[i];
|
||||
const t2 = transitions[i + 1];
|
||||
|
||||
let transition = t;
|
||||
|
||||
if (i < transitions.length - 1 && modMap[t] && modMap[t].after === t2) {
|
||||
transition = modMap[t].newTransition;
|
||||
i++;
|
||||
}
|
||||
|
||||
res.push(transition);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
|
||||
/**
|
||||
* Transform an array of transitions into an array of transitions and how many times
|
||||
* they are repeated consecutively.
|
||||
*
|
||||
* An array of 123 "joined_and_left" transitions, would result in:
|
||||
* ```
|
||||
* [{
|
||||
* transitionType: "joined_and_left"
|
||||
* repeats: 123
|
||||
* }]
|
||||
* ```
|
||||
* @param {string[]} transitions the array of transitions to transform.
|
||||
* @returns {object[]} an array of coalesced transitions.
|
||||
*/
|
||||
_coalesceRepeatedTransitions: function(transitions) {
|
||||
const res = [];
|
||||
for (let i = 0; i < transitions.length; i++) {
|
||||
if (res.length > 0 && res[res.length - 1].transitionType === transitions[i]) {
|
||||
res[res.length - 1].repeats += 1;
|
||||
} else {
|
||||
res.push({
|
||||
transitionType: transitions[i],
|
||||
repeats: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
return res;
|
||||
},
|
||||
|
||||
/**
|
||||
* For a certain transition, t, describe what happened to the users that
|
||||
* underwent the transition.
|
||||
* @param {string} t the transition type.
|
||||
* @param {boolean} plural whether there were multiple users undergoing the same
|
||||
* transition.
|
||||
* @param {number} repeats the number of times the transition was repeated in a row.
|
||||
* @returns {string} the written English equivalent of the transition.
|
||||
*/
|
||||
_getDescriptionForTransition(t, plural, repeats) {
|
||||
const beConjugated = plural ? "were" : "was";
|
||||
const invitation = "their invitation" + (plural || (repeats > 1) ? "s" : "");
|
||||
|
||||
let res = null;
|
||||
const map = {
|
||||
"joined": "joined",
|
||||
"left": "left",
|
||||
"joined_and_left": "joined and left",
|
||||
"left_and_joined": "left and rejoined",
|
||||
"invite_reject": "rejected " + invitation,
|
||||
"invite_withdrawal": "had " + invitation + " withdrawn",
|
||||
"invited": beConjugated + " invited",
|
||||
"banned": beConjugated + " banned",
|
||||
"unbanned": beConjugated + " unbanned",
|
||||
"kicked": beConjugated + " kicked",
|
||||
};
|
||||
|
||||
if (Object.keys(map).includes(t)) {
|
||||
res = map[t] + (repeats > 1 ? " " + repeats + " times" : "" );
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
|
||||
/**
|
||||
* Constructs a written English string representing `items`, with an optional limit on
|
||||
* the number of items included in the result. If specified and if the length of
|
||||
*`items` is greater than the limit, the string "and n others" will be appended onto
|
||||
* the result.
|
||||
* If `items` is empty, returns the empty string. If there is only one item, return
|
||||
* it.
|
||||
* @param {string[]} items the items to construct a string from.
|
||||
* @param {number?} itemLimit the number by which to limit the list.
|
||||
* @returns {string} a string constructed by joining `items` with a comma between each
|
||||
* item, but with the last item appended as " and [lastItem]".
|
||||
*/
|
||||
_renderCommaSeparatedList(items, itemLimit) {
|
||||
const remaining = itemLimit === undefined ? 0 : Math.max(
|
||||
items.length - itemLimit, 0
|
||||
);
|
||||
if (items.length === 0) {
|
||||
return "";
|
||||
} else if (items.length === 1) {
|
||||
return items[0];
|
||||
} else if (remaining) {
|
||||
items = items.slice(0, itemLimit);
|
||||
const other = " other" + (remaining > 1 ? "s" : "");
|
||||
return items.join(', ') + ' and ' + remaining + other;
|
||||
} else {
|
||||
const lastItem = items.pop();
|
||||
return items.join(', ') + ' and ' + lastItem;
|
||||
}
|
||||
},
|
||||
|
||||
_renderAvatars: function(roomMembers) {
|
||||
const avatars = roomMembers.slice(0, this.props.avatarsMaxLength).map((m) => {
|
||||
return (
|
||||
<MemberAvatar
|
||||
key={e.getId()}
|
||||
member={e.sender}
|
||||
width={14}
|
||||
height={14}
|
||||
/>
|
||||
<MemberAvatar key={m.userId} member={m} width={14} height={14} />
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<span>
|
||||
{avatars}
|
||||
|
@ -134,74 +270,155 @@ module.exports = React.createClass({
|
|||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let summary = null;
|
||||
_getTransitionSequence: function(events) {
|
||||
return events.map(this._getTransition);
|
||||
},
|
||||
|
||||
// Reorder events so that joins come before leaves
|
||||
let eventsToRender = this.props.events;
|
||||
|
||||
// Filter out those who joined, then left
|
||||
let filteredEvents = eventsToRender.filter(
|
||||
(e) => {
|
||||
return eventsToRender.filter(
|
||||
(e2) => {
|
||||
return e.getSender() === e2.getSender()
|
||||
&& e.event.content.membership !== e2.event.content.membership;
|
||||
/**
|
||||
* Label a given membership event, `e`, where `getContent().membership` has
|
||||
* changed for each transition allowed by the Matrix protocol. This attempts to
|
||||
* label the membership changes that occur in `../../../TextForEvent.js`.
|
||||
* @param {MatrixEvent} e the membership change event to label.
|
||||
* @returns {string?} the transition type given to this event. This defaults to `null`
|
||||
* if a transition is not recognised.
|
||||
*/
|
||||
_getTransition: function(e) {
|
||||
switch (e.mxEvent.getContent().membership) {
|
||||
case 'invite': return 'invited';
|
||||
case 'ban': return 'banned';
|
||||
case 'join': return 'joined';
|
||||
case 'leave':
|
||||
if (e.mxEvent.getSender() === e.mxEvent.getStateKey()) {
|
||||
switch (e.mxEvent.getPrevContent().membership) {
|
||||
case 'invite': return 'invite_reject';
|
||||
default: return 'left';
|
||||
}
|
||||
).length === 0;
|
||||
}
|
||||
switch (e.mxEvent.getPrevContent().membership) {
|
||||
case 'invite': return 'invite_withdrawal';
|
||||
case 'ban': return 'unbanned';
|
||||
case 'join': return 'kicked';
|
||||
default: return 'left';
|
||||
}
|
||||
default: return null;
|
||||
}
|
||||
},
|
||||
|
||||
_getAggregate: function(userEvents) {
|
||||
// A map of aggregate type to arrays of display names. Each aggregate type
|
||||
// is a comma-delimited string of transitions, e.g. "joined,left,kicked".
|
||||
// The array of display names is the array of users who went through that
|
||||
// sequence during eventsToRender.
|
||||
const aggregate = {
|
||||
// $aggregateType : []:string
|
||||
};
|
||||
// A map of aggregate types to the indices that order them (the index of
|
||||
// the first event for a given transition sequence)
|
||||
const aggregateIndices = {
|
||||
// $aggregateType : int
|
||||
};
|
||||
|
||||
const users = Object.keys(userEvents);
|
||||
users.forEach(
|
||||
(userId) => {
|
||||
const firstEvent = userEvents[userId][0];
|
||||
const displayName = firstEvent.displayName;
|
||||
|
||||
const seq = this._getTransitionSequence(userEvents[userId]);
|
||||
if (!aggregate[seq]) {
|
||||
aggregate[seq] = [];
|
||||
aggregateIndices[seq] = -1;
|
||||
}
|
||||
|
||||
aggregate[seq].push(displayName);
|
||||
|
||||
if (aggregateIndices[seq] === -1 ||
|
||||
firstEvent.index < aggregateIndices[seq]) {
|
||||
aggregateIndices[seq] = firstEvent.index;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
let joinAndLeft = (eventsToRender.length - filteredEvents.length) / 2;
|
||||
if (joinAndLeft <= 0 || joinAndLeft % 1 !== 0) {
|
||||
joinAndLeft = null;
|
||||
}
|
||||
return {
|
||||
names: aggregate,
|
||||
indices: aggregateIndices,
|
||||
};
|
||||
},
|
||||
|
||||
let joinEvents = filteredEvents.filter((ev) => {
|
||||
return ev.event.content.membership === 'join';
|
||||
});
|
||||
render: function() {
|
||||
const eventsToRender = this.props.events;
|
||||
const fewEvents = eventsToRender.length < this.props.threshold;
|
||||
const expanded = this.state.expanded || fewEvents;
|
||||
|
||||
let leaveEvents = filteredEvents.filter((ev) => {
|
||||
return ev.event.content.membership === 'leave';
|
||||
});
|
||||
|
||||
let fewEvents = eventsToRender.length < this.props.threshold;
|
||||
let expanded = this.state.expanded || fewEvents;
|
||||
let expandedEvents = null;
|
||||
|
||||
if (expanded) {
|
||||
expandedEvents = this.props.children;
|
||||
}
|
||||
|
||||
let avatars = this._renderAvatars(joinEvents.concat(leaveEvents));
|
||||
|
||||
let toggleButton = null;
|
||||
let summaryContainer = null;
|
||||
if (!fewEvents) {
|
||||
summary = this._renderSummary(joinEvents, leaveEvents);
|
||||
toggleButton = (
|
||||
<a className="mx_MemberEventListSummary_toggle" onClick={this._toggleSummary}>
|
||||
{expanded ? 'collapse' : 'expand'}
|
||||
</a>
|
||||
);
|
||||
let plural = (joinEvents.length + leaveEvents.length > 0) ? 'others' : 'users';
|
||||
let noun = (joinAndLeft === 1 ? 'user' : plural);
|
||||
|
||||
summaryContainer = (
|
||||
<div className="mx_EventTile_line">
|
||||
<div className="mx_EventTile_info">
|
||||
<span className="mx_MemberEventListSummary_avatars">
|
||||
{avatars}
|
||||
</span>
|
||||
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
|
||||
{summary}{joinAndLeft? '. ' + joinAndLeft + ' ' + noun + ' joined and left' : ''}
|
||||
</span>
|
||||
{toggleButton}
|
||||
</div>
|
||||
if (fewEvents) {
|
||||
return (
|
||||
<div className="mx_MemberEventListSummary">
|
||||
{expandedEvents}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Map user IDs to an array of objects:
|
||||
const userEvents = {
|
||||
// $userId : [{
|
||||
// // The original event
|
||||
// mxEvent: e,
|
||||
// // The display name of the user (if not, then user ID)
|
||||
// displayName: e.target.name || userId,
|
||||
// // The original index of the event in this.props.events
|
||||
// index: index,
|
||||
// }]
|
||||
};
|
||||
|
||||
const avatarMembers = [];
|
||||
eventsToRender.forEach((e, index) => {
|
||||
const userId = e.getStateKey();
|
||||
// Initialise a user's events
|
||||
if (!userEvents[userId]) {
|
||||
userEvents[userId] = [];
|
||||
avatarMembers.push(e.target);
|
||||
}
|
||||
userEvents[userId].push({
|
||||
mxEvent: e,
|
||||
displayName: e.target.name || userId,
|
||||
index: index,
|
||||
});
|
||||
});
|
||||
|
||||
const aggregate = this._getAggregate(userEvents);
|
||||
|
||||
// Sort types by order of lowest event index within sequence
|
||||
const orderedTransitionSequences = Object.keys(aggregate.names).sort(
|
||||
(seq1, seq2) => aggregate.indices[seq1] > aggregate.indices[seq2]
|
||||
);
|
||||
|
||||
const avatars = this._renderAvatars(avatarMembers);
|
||||
const summary = this._renderSummary(aggregate.names, orderedTransitionSequences);
|
||||
const toggleButton = (
|
||||
<a className="mx_MemberEventListSummary_toggle" onClick={this._toggleSummary}>
|
||||
{expanded ? 'collapse' : 'expand'}
|
||||
</a>
|
||||
);
|
||||
|
||||
const summaryContainer = (
|
||||
<div className="mx_EventTile_line">
|
||||
<div className="mx_EventTile_info">
|
||||
<span className="mx_MemberEventListSummary_avatars">
|
||||
{avatars}
|
||||
</span>
|
||||
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
|
||||
{summary}
|
||||
</span>
|
||||
{toggleButton}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx_MemberEventListSummary">
|
||||
{summaryContainer}
|
||||
|
|
|
@ -73,7 +73,7 @@ module.exports = React.createClass({
|
|||
getValue: function() {
|
||||
var value;
|
||||
if (this.refs.select) {
|
||||
value = reverseRoles[ this.refs.select.value ];
|
||||
value = reverseRoles[this.refs.select.value];
|
||||
if (this.refs.custom) {
|
||||
if (value === undefined) value = parseInt( this.refs.custom.value );
|
||||
}
|
||||
|
@ -86,10 +86,10 @@ module.exports = React.createClass({
|
|||
if (this.state.custom) {
|
||||
var input;
|
||||
if (this.props.disabled) {
|
||||
input = <span>{ this.props.value }</span>
|
||||
input = <span>{ this.props.value }</span>;
|
||||
}
|
||||
else {
|
||||
input = <input ref="custom" type="text" size="3" defaultValue={ this.props.value } onBlur={ this.onCustomBlur } onKeyDown={ this.onCustomKeyDown }/>
|
||||
input = <input ref="custom" type="text" size="3" defaultValue={ this.props.value } onBlur={ this.onCustomBlur } onKeyDown={ this.onCustomKeyDown }/>;
|
||||
}
|
||||
customPicker = <span> of { input }</span>;
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ module.exports = React.createClass({
|
|||
<option value="Moderator">Moderator (50)</option>
|
||||
<option value="Admin">Admin (100)</option>
|
||||
<option value="Custom">Custom level</option>
|
||||
</select>
|
||||
</select>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -35,4 +35,4 @@ module.exports = React.createClass({
|
|||
<div className="mx_ProgressBar"><div className="mx_ProgressBar_fill" style={progressStyle}></div></div>
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -69,9 +69,19 @@ var TintableSvg = React.createClass({
|
|||
width={ this.props.width }
|
||||
height={ this.props.height }
|
||||
onLoad={ this.onLoad }
|
||||
tabIndex="-1"
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Register with the Tinter so that we will be told if the tint changes
|
||||
Tinter.registerTintable(function() {
|
||||
if (TintableSvg.mounts) {
|
||||
Object.keys(TintableSvg.mounts).forEach((id) => {
|
||||
TintableSvg.mounts[id].tint();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = TintableSvg;
|
||||
|
|
|
@ -55,7 +55,7 @@ module.exports = React.createClass({
|
|||
overflowJsx = this.props.createOverflowElement(
|
||||
overflowCount, childCount
|
||||
);
|
||||
|
||||
|
||||
// cut out the overflow elements
|
||||
childArray.splice(childCount - overflowCount, overflowCount);
|
||||
childsJsx = childArray; // use what is left
|
||||
|
|
|
@ -56,7 +56,7 @@ module.exports = React.createClass({
|
|||
<div>
|
||||
<ul className="mx_UserSelector_UserIdList" ref="list">
|
||||
{this.props.selected_users.map(function(user_id, i) {
|
||||
return <li key={user_id}>{user_id} - <span onClick={function() {self.removeUser(user_id);}}>X</span></li>
|
||||
return <li key={user_id}>{user_id} - <span onClick={function() {self.removeUser(user_id);}}>X</span></li>;
|
||||
})}
|
||||
</ul>
|
||||
<input type="text" ref="user_id_input" defaultValue="" className="mx_UserSelector_userIdInput" placeholder="ex. @bob:example.com"/>
|
||||
|
|
|
@ -52,12 +52,24 @@ module.exports = React.createClass({
|
|||
this._onCaptchaLoaded();
|
||||
} else {
|
||||
console.log("Loading recaptcha script...");
|
||||
var scriptTag = document.createElement('script');
|
||||
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded()};
|
||||
scriptTag.setAttribute(
|
||||
'src', global.location.protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit"
|
||||
);
|
||||
this.refs.recaptchaContainer.appendChild(scriptTag);
|
||||
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();};
|
||||
var protocol = global.location.protocol;
|
||||
if (protocol === "file:") {
|
||||
var warning = document.createElement('div');
|
||||
// XXX: fix hardcoded app URL. Better solutions include:
|
||||
// * jumping straight to a hosted captcha page (but we don't support that yet)
|
||||
// * embedding the captcha in an iframe (if that works)
|
||||
// * using a better captcha lib
|
||||
warning.innerHTML = "Robot check is currently unavailable on desktop - please use a <a href='https://riot.im/app'>web browser</a>.";
|
||||
this.refs.recaptchaContainer.appendChild(warning);
|
||||
}
|
||||
else {
|
||||
var scriptTag = document.createElement('script');
|
||||
scriptTag.setAttribute(
|
||||
'src', protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit"
|
||||
);
|
||||
this.refs.recaptchaContainer.appendChild(scriptTag);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -89,7 +101,7 @@ module.exports = React.createClass({
|
|||
} catch (e) {
|
||||
this.setState({
|
||||
errorText: e.toString(),
|
||||
})
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -106,6 +118,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div ref="recaptchaContainer">
|
||||
This Home Server would like to make sure you are not a robot
|
||||
<br/>
|
||||
<div id={DIV_ID}></div>
|
||||
{error}
|
||||
</div>
|
||||
|
|
|
@ -70,7 +70,7 @@ export const PasswordAuthEntry = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onPasswordFieldChange: function (ev) {
|
||||
_onPasswordFieldChange: function(ev) {
|
||||
// enable the submit button iff the password is non-empty
|
||||
this.props.setSubmitButtonEnabled(Boolean(ev.target.value));
|
||||
},
|
||||
|
@ -209,4 +209,4 @@ export function getEntryComponentForLoginType(loginType) {
|
|||
}
|
||||
}
|
||||
return FallbackAuthEntry;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -38,6 +38,16 @@ module.exports = React.createClass({
|
|||
defaultEmail: React.PropTypes.string,
|
||||
defaultUsername: React.PropTypes.string,
|
||||
defaultPassword: 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 domain of team email addresses
|
||||
"domain": React.PropTypes.string,
|
||||
})).required,
|
||||
}),
|
||||
|
||||
// A username that will be used if no username is entered.
|
||||
// Specifying this param will also warn the user that entering
|
||||
|
@ -62,7 +72,8 @@ module.exports = React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
fieldValid: {}
|
||||
fieldValid: {},
|
||||
selectedTeam: null,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -105,10 +116,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_doSubmit: function() {
|
||||
let email = this.refs.email.value.trim();
|
||||
var promise = this.props.onRegisterClick({
|
||||
username: this.refs.username.value.trim() || this.props.guestUsername,
|
||||
password: this.refs.password.value.trim(),
|
||||
email: this.refs.email.value.trim()
|
||||
email: email,
|
||||
});
|
||||
|
||||
if (promise) {
|
||||
|
@ -133,17 +145,37 @@ module.exports = React.createClass({
|
|||
return true;
|
||||
},
|
||||
|
||||
_isUniEmail: function(email) {
|
||||
return email.endsWith('.ac.uk') || email.endsWith('.edu');
|
||||
},
|
||||
|
||||
validateField: function(field_id) {
|
||||
var pwd1 = this.refs.password.value.trim();
|
||||
var pwd2 = this.refs.passwordConfirm.value.trim()
|
||||
var pwd2 = this.refs.passwordConfirm.value.trim();
|
||||
|
||||
switch (field_id) {
|
||||
case FIELD_EMAIL:
|
||||
this.markFieldValid(
|
||||
field_id,
|
||||
this.refs.email.value == '' || Email.looksValid(this.refs.email.value),
|
||||
"RegistrationForm.ERR_EMAIL_INVALID"
|
||||
);
|
||||
const email = this.refs.email.value;
|
||||
if (this.props.teamsConfig && this._isUniEmail(email)) {
|
||||
const matchingTeam = this.props.teamsConfig.teams.find(
|
||||
(team) => {
|
||||
return email.split('@').pop() === team.domain;
|
||||
}
|
||||
) || null;
|
||||
this.setState({
|
||||
selectedTeam: matchingTeam,
|
||||
showSupportEmail: !matchingTeam,
|
||||
});
|
||||
this.props.onTeamSelected(matchingTeam);
|
||||
} else {
|
||||
this.props.onTeamSelected(null);
|
||||
this.setState({
|
||||
selectedTeam: null,
|
||||
showSupportEmail: false,
|
||||
});
|
||||
}
|
||||
const valid = email === '' || Email.looksValid(email);
|
||||
this.markFieldValid(field_id, valid, "RegistrationForm.ERR_EMAIL_INVALID");
|
||||
break;
|
||||
case FIELD_USERNAME:
|
||||
// XXX: SPEC-1
|
||||
|
@ -224,15 +256,36 @@ module.exports = React.createClass({
|
|||
|
||||
render: function() {
|
||||
var self = this;
|
||||
var emailSection, registerButton;
|
||||
var emailSection, belowEmailSection, registerButton;
|
||||
if (this.props.showEmail) {
|
||||
emailSection = (
|
||||
<input type="text" ref="email"
|
||||
autoFocus={true} placeholder="Email address (optional)"
|
||||
defaultValue={this.props.defaultEmail}
|
||||
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
|
||||
onBlur={function() {self.validateField(FIELD_EMAIL)}} />
|
||||
onBlur={function() {self.validateField(FIELD_EMAIL);}}
|
||||
value={self.state.email}/>
|
||||
);
|
||||
if (this.props.teamsConfig) {
|
||||
if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) {
|
||||
belowEmailSection = (
|
||||
<p className="mx_Login_support">
|
||||
Sorry, but your university is not registered with us just yet.
|
||||
Email us on
|
||||
<a href={"mailto:" + this.props.teamsConfig.supportEmail}>
|
||||
{this.props.teamsConfig.supportEmail}
|
||||
</a>
|
||||
to get your university signed up. Or continue to register with Riot to enjoy our open source platform.
|
||||
</p>
|
||||
);
|
||||
} else if (this.state.selectedTeam) {
|
||||
belowEmailSection = (
|
||||
<p className="mx_Login_support">
|
||||
You are registering with {this.state.selectedTeam.name}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.props.onRegisterClick) {
|
||||
registerButton = (
|
||||
|
@ -242,31 +295,31 @@ module.exports = React.createClass({
|
|||
|
||||
var placeholderUserName = "User name";
|
||||
if (this.props.guestUsername) {
|
||||
placeholderUserName += " (default: " + this.props.guestUsername + ")"
|
||||
placeholderUserName += " (default: " + this.props.guestUsername + ")";
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
{emailSection}
|
||||
<br />
|
||||
{belowEmailSection}
|
||||
<input type="text" ref="username"
|
||||
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername}
|
||||
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
|
||||
onBlur={function() {self.validateField(FIELD_USERNAME)}} />
|
||||
onBlur={function() {self.validateField(FIELD_USERNAME);}} />
|
||||
<br />
|
||||
{ this.props.guestUsername ?
|
||||
<div className="mx_Login_fieldLabel">Setting a user name will create a fresh account</div> : null
|
||||
}
|
||||
<input type="password" ref="password"
|
||||
className={this._classForField(FIELD_PASSWORD, 'mx_Login_field')}
|
||||
onBlur={function() {self.validateField(FIELD_PASSWORD)}}
|
||||
onBlur={function() {self.validateField(FIELD_PASSWORD);}}
|
||||
placeholder="Password" defaultValue={this.props.defaultPassword} />
|
||||
<br />
|
||||
<input type="password" ref="passwordConfirm"
|
||||
placeholder="Confirm password"
|
||||
className={this._classForField(FIELD_PASSWORD_CONFIRM, 'mx_Login_field')}
|
||||
onBlur={function() {self.validateField(FIELD_PASSWORD_CONFIRM)}}
|
||||
onBlur={function() {self.validateField(FIELD_PASSWORD_CONFIRM);}}
|
||||
defaultValue={this.props.defaultPassword} />
|
||||
<br />
|
||||
{registerButton}
|
||||
|
|
|
@ -64,10 +64,10 @@ module.exports = React.createClass({
|
|||
hs_url: this.props.customHsUrl,
|
||||
is_url: this.props.customIsUrl,
|
||||
// if withToggleButton is false, then show the config all the time given we have no way otherwise of making it visible
|
||||
configVisible: !this.props.withToggleButton ||
|
||||
configVisible: !this.props.withToggleButton ||
|
||||
(this.props.customHsUrl !== this.props.defaultHsUrl) ||
|
||||
(this.props.customIsUrl !== this.props.defaultIsUrl)
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
onHomeserverChanged: function(ev) {
|
||||
|
|
|
@ -21,7 +21,7 @@ import MFileBody from './MFileBody';
|
|||
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from '../../../index';
|
||||
import { decryptFile } from '../../../utils/DecryptFile';
|
||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||
|
||||
export default class MAudioBody extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -29,7 +29,9 @@ export default class MAudioBody extends React.Component {
|
|||
this.state = {
|
||||
playing: false,
|
||||
decryptedUrl: null,
|
||||
}
|
||||
decryptedBlob: null,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
onPlayToggle() {
|
||||
this.setState({
|
||||
|
@ -49,29 +51,45 @@ export default class MAudioBody extends React.Component {
|
|||
componentDidMount() {
|
||||
var content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
decryptFile(content.file).done((url) => {
|
||||
var decryptedBlob;
|
||||
decryptFile(content.file).then(function(blob) {
|
||||
decryptedBlob = blob;
|
||||
return readBlobAsDataUri(decryptedBlob);
|
||||
}).done((url) => {
|
||||
this.setState({
|
||||
decryptedUrl: url
|
||||
decryptedUrl: url,
|
||||
decryptedBlob: decryptedBlob,
|
||||
});
|
||||
}, (err) => {
|
||||
console.warn("Unable to decrypt attachment: ", err)
|
||||
// Set a placeholder image when we can't decrypt the image.
|
||||
this.refs.image.src = "img/warning.svg";
|
||||
console.warn("Unable to decrypt attachment: ", err);
|
||||
this.setState({
|
||||
error: err,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const content = this.props.mxEvent.getContent();
|
||||
|
||||
if (this.state.error !== null) {
|
||||
return (
|
||||
<span className="mx_MAudioBody" ref="body">
|
||||
<img src="img/warning.svg" width="16" height="16"/>
|
||||
Error decrypting audio
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
// Need to decrypt the attachment
|
||||
// The attachment is decrypted in componentDidMount.
|
||||
// For now add an img tag with a spinner.
|
||||
// For now add an img tag with a 16x16 spinner.
|
||||
// Not sure how tall the audio player is so not sure how tall it should actually be.
|
||||
return (
|
||||
<span className="mx_MAudioBody">
|
||||
<img src="img/spinner.gif" ref="image"
|
||||
alt={content.body} />
|
||||
<img src="img/spinner.gif" alt={content.body} width="16" height="16"/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -81,7 +99,7 @@ export default class MAudioBody extends React.Component {
|
|||
return (
|
||||
<span className="mx_MAudioBody">
|
||||
<audio src={contentUrl} controls />
|
||||
<MFileBody {...this.props} decryptedUrl={this.state.decryptedUrl} />
|
||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -21,120 +21,332 @@ import filesize from 'filesize';
|
|||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from '../../../index';
|
||||
import {decryptFile} from '../../../utils/DecryptFile';
|
||||
import Tinter from '../../../Tinter';
|
||||
import request from 'browser-request';
|
||||
import q from 'q';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
|
||||
// A cached tinted copy of "img/download.svg"
|
||||
var tintedDownloadImageURL;
|
||||
// Track a list of mounted MFileBody instances so that we can update
|
||||
// the "img/download.svg" when the tint changes.
|
||||
var nextMountId = 0;
|
||||
const mounts = {};
|
||||
|
||||
/**
|
||||
* Updates the tinted copy of "img/download.svg" when the tint changes.
|
||||
*/
|
||||
function updateTintedDownloadImage() {
|
||||
// Download the svg as an XML document.
|
||||
// We could cache the XML response here, but since the tint rarely changes
|
||||
// it's probably not worth it.
|
||||
// Also note that we can't use fetch here because fetch doesn't support
|
||||
// file URLs, which the download image will be if we're running from
|
||||
// the filesystem (like in an Electron wrapper).
|
||||
request({uri: "img/download.svg"}, (err, response, body) => {
|
||||
if (err) return;
|
||||
|
||||
const svg = new DOMParser().parseFromString(body, "image/svg+xml");
|
||||
// Apply the fixups to the XML.
|
||||
const fixups = Tinter.calcSvgFixups([{contentDocument: svg}]);
|
||||
Tinter.applySvgFixups(fixups);
|
||||
// Encoded the fixed up SVG as a data URL.
|
||||
const svgString = new XMLSerializer().serializeToString(svg);
|
||||
tintedDownloadImageURL = "data:image/svg+xml;base64," + window.btoa(svgString);
|
||||
// Notify each mounted MFileBody that the URL has changed.
|
||||
Object.keys(mounts).forEach(function(id) {
|
||||
mounts[id].tint();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Tinter.registerTintable(updateTintedDownloadImage);
|
||||
|
||||
// User supplied content can contain scripts, we have to be careful that
|
||||
// we don't accidentally run those script within the same origin as the
|
||||
// client. Otherwise those scripts written by remote users can read
|
||||
// the access token and end-to-end keys that are in local storage.
|
||||
//
|
||||
// For attachments downloaded directly from the homeserver we can use
|
||||
// Content-Security-Policy headers to disable script execution.
|
||||
//
|
||||
// But attachments with end-to-end encryption are more difficult to handle.
|
||||
// We need to decrypt the attachment on the client and then display it.
|
||||
// To display the attachment we need to turn the decrypted bytes into a URL.
|
||||
//
|
||||
// There are two ways to turn bytes into URLs, data URL and blob URLs.
|
||||
// Data URLs aren't suitable for downloading a file because Chrome has a
|
||||
// 2MB limit on the size of URLs that can be viewed in the browser or
|
||||
// downloaded. This limit does not seem to apply when the url is used as
|
||||
// the source attribute of an image tag.
|
||||
//
|
||||
// Blob URLs are generated using window.URL.createObjectURL and unforuntately
|
||||
// for our purposes they inherit the origin of the page that created them.
|
||||
// This means that any scripts that run when the URL is viewed will be able
|
||||
// to access local storage.
|
||||
//
|
||||
// The easiest solution is to host the code that generates the blob URL on
|
||||
// a different domain to the client.
|
||||
// Another possibility is to generate the blob URL within a sandboxed iframe.
|
||||
// The downside of using a second domain is that it complicates hosting,
|
||||
// the downside of using a sandboxed iframe is that the browers are overly
|
||||
// restrictive in what you are allowed to do with the generated URL.
|
||||
//
|
||||
// For now given how unusable the blobs generated in sandboxed iframes are we
|
||||
// default to using a renderer hosted on "usercontent.riot.im". This is
|
||||
// overridable so that people running their own version of the client can
|
||||
// choose a different renderer.
|
||||
//
|
||||
// To that end the first version of the blob generation will be the following
|
||||
// html:
|
||||
//
|
||||
// <html><head><script>
|
||||
// window.onmessage=function(e){eval("("+e.data.code+")")(e)}
|
||||
// </script></head><body></body></html>
|
||||
//
|
||||
// This waits to receive a message event sent using the window.postMessage API.
|
||||
// When it receives the event it evals a javascript function in data.code and
|
||||
// runs the function passing the event as an argument.
|
||||
//
|
||||
// In particular it means that the rendering function can be written as a
|
||||
// ordinary javascript function which then is turned into a string using
|
||||
// toString().
|
||||
//
|
||||
const DEFAULT_CROSS_ORIGIN_RENDERER = "https://usercontent.riot.im/v1.html";
|
||||
|
||||
/**
|
||||
* Render the attachment inside the iframe.
|
||||
* We can't use imported libraries here so this has to be vanilla JS.
|
||||
*/
|
||||
function remoteRender(event) {
|
||||
const data = event.data;
|
||||
|
||||
const img = document.createElement("img");
|
||||
img.id = "img";
|
||||
img.src = data.imgSrc;
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.id = "a";
|
||||
a.rel = data.rel;
|
||||
a.target = data.target;
|
||||
a.download = data.download;
|
||||
a.style = data.style;
|
||||
a.href = window.URL.createObjectURL(data.blob);
|
||||
a.appendChild(img);
|
||||
a.appendChild(document.createTextNode(data.textContent));
|
||||
|
||||
const body = document.body;
|
||||
// Don't display scrollbars if the link takes more than one line
|
||||
// to display.
|
||||
body.style = "margin: 0px; overflow: hidden";
|
||||
body.appendChild(a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the tint inside the iframe.
|
||||
* We can't use imported libraries here so this has to be vanilla JS.
|
||||
*/
|
||||
function remoteSetTint(event) {
|
||||
const data = event.data;
|
||||
|
||||
const img = document.getElementById("img");
|
||||
img.src = data.imgSrc;
|
||||
img.style = data.imgStyle;
|
||||
|
||||
const a = document.getElementById("a");
|
||||
a.style = data.style;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the current CSS style for a DOMElement.
|
||||
* @param {HTMLElement} element The element to get the current style of.
|
||||
* @return {string} The CSS style encoded as a string.
|
||||
*/
|
||||
function computedStyle(element) {
|
||||
if (!element) {
|
||||
return "";
|
||||
}
|
||||
const style = window.getComputedStyle(element, null);
|
||||
var cssText = style.cssText;
|
||||
if (cssText == "") {
|
||||
// Firefox doesn't implement ".cssText" for computed styles.
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=137687
|
||||
for (var i = 0; i < style.length; i++) {
|
||||
cssText += style[i] + ":";
|
||||
cssText += style.getPropertyValue(style[i]) + ";";
|
||||
}
|
||||
}
|
||||
return cssText;
|
||||
}
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MFileBody',
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
decryptedUrl: (this.props.decryptedUrl ? this.props.decryptedUrl : null),
|
||||
decryptedBlob: (this.props.decryptedBlob ? this.props.decryptedBlob : null),
|
||||
};
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
appConfig: React.PropTypes.object,
|
||||
},
|
||||
|
||||
/**
|
||||
* Extracts a human readable label for the file attachment to use as
|
||||
* link text.
|
||||
*
|
||||
* @params {Object} content The "content" key of the matrix event.
|
||||
* @return {string} the human readable link text for the attachment.
|
||||
*/
|
||||
presentableTextForFile: function(content) {
|
||||
var linkText = 'Attachment';
|
||||
if (content.body && content.body.length > 0) {
|
||||
// The content body should be the name of the file including a
|
||||
// file extension.
|
||||
linkText = content.body;
|
||||
}
|
||||
|
||||
var additionals = [];
|
||||
if (content.info) {
|
||||
// if (content.info.mimetype && content.info.mimetype.length > 0) {
|
||||
// additionals.push(content.info.mimetype);
|
||||
// }
|
||||
if (content.info.size) {
|
||||
additionals.push(filesize(content.info.size));
|
||||
}
|
||||
}
|
||||
|
||||
if (additionals.length > 0) {
|
||||
linkText += ' (' + additionals.join(', ') + ')';
|
||||
if (content.info && content.info.size) {
|
||||
// If we know the size of the file then add it as human readable
|
||||
// string to the end of the link text so that the user knows how
|
||||
// big a file they are downloading.
|
||||
// The content.info also contains a MIME-type but we don't display
|
||||
// it since it is "ugly", users generally aren't aware what it
|
||||
// means and the type of the attachment can usually be inferrered
|
||||
// from the file extension.
|
||||
linkText += ' (' + filesize(content.info.size) + ')';
|
||||
}
|
||||
return linkText;
|
||||
},
|
||||
|
||||
_getContentUrl: function() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
return this.state.decryptedUrl;
|
||||
} else {
|
||||
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
|
||||
}
|
||||
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
decryptFile(content.file).done((url) => {
|
||||
this.setState({
|
||||
decryptedUrl: url,
|
||||
});
|
||||
}, (err) => {
|
||||
console.warn("Unable to decrypt attachment: ", err)
|
||||
// Set a placeholder image when we can't decrypt the image.
|
||||
this.refs.image.src = "img/warning.svg";
|
||||
});
|
||||
// Add this to the list of mounted components to receive notifications
|
||||
// when the tint changes.
|
||||
this.id = nextMountId++;
|
||||
mounts[this.id] = this;
|
||||
this.tint();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
// Remove this from the list of mounted components
|
||||
delete mounts[this.id];
|
||||
},
|
||||
|
||||
tint: function() {
|
||||
// Update our tinted copy of "img/download.svg"
|
||||
if (this.refs.downloadImage) {
|
||||
this.refs.downloadImage.src = tintedDownloadImageURL;
|
||||
}
|
||||
if (this.refs.iframe) {
|
||||
// If the attachment is encrypted then the download image
|
||||
// will be inside the iframe so we wont be able to update
|
||||
// it directly.
|
||||
this.refs.iframe.contentWindow.postMessage({
|
||||
code: remoteSetTint.toString(),
|
||||
imgSrc: tintedDownloadImageURL,
|
||||
style: computedStyle(this.refs.dummyLink),
|
||||
}, "*");
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
|
||||
const text = this.presentableTextForFile(content);
|
||||
const isEncrypted = content.file !== undefined;
|
||||
const fileName = content.body && content.body.length > 0 ? content.body : "Attachment";
|
||||
const contentUrl = this._getContentUrl();
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
if (isEncrypted) {
|
||||
if (this.state.decryptedBlob === null) {
|
||||
// Need to decrypt the attachment
|
||||
// Wait for the user to click on the link before downloading
|
||||
// and decrypting the attachment.
|
||||
var decrypting = false;
|
||||
const decrypt = () => {
|
||||
if (decrypting) {
|
||||
return false;
|
||||
}
|
||||
decrypting = true;
|
||||
decryptFile(content.file).then((blob) => {
|
||||
this.setState({
|
||||
decryptedBlob: blob,
|
||||
});
|
||||
}).catch((err) => {
|
||||
console.warn("Unable to decrypt attachment: ", err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
description: "Error decrypting attachment"
|
||||
});
|
||||
}).finally(() => {
|
||||
decrypting = false;
|
||||
return;
|
||||
});
|
||||
};
|
||||
|
||||
// Need to decrypt the attachment
|
||||
// The attachment is decrypted in componentDidMount.
|
||||
// For now add an img tag with a spinner.
|
||||
return (
|
||||
<span className="mx_MFileBody" ref="body">
|
||||
<div className="mx_MImageBody_download">
|
||||
<a href="javascript:void(0)" onClick={decrypt}>
|
||||
Decrypt {text}
|
||||
</a>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// When the iframe loads we tell it to render a download link
|
||||
const onIframeLoad = (ev) => {
|
||||
ev.target.contentWindow.postMessage({
|
||||
code: remoteRender.toString(),
|
||||
imgSrc: tintedDownloadImageURL,
|
||||
style: computedStyle(this.refs.dummyLink),
|
||||
blob: this.state.decryptedBlob,
|
||||
// Set a download attribute for encrypted files so that the file
|
||||
// will have the correct name when the user tries to download it.
|
||||
// We can't provide a Content-Disposition header like we would for HTTP.
|
||||
download: fileName,
|
||||
target: "_blank",
|
||||
textContent: "Download " + text,
|
||||
}, "*");
|
||||
};
|
||||
|
||||
// If the attachment is encryped then put the link inside an iframe.
|
||||
let renderer_url = DEFAULT_CROSS_ORIGIN_RENDERER;
|
||||
if (this.context.appConfig && this.context.appConfig.cross_origin_renderer_url) {
|
||||
renderer_url = this.context.appConfig.cross_origin_renderer_url;
|
||||
}
|
||||
return (
|
||||
<span className="mx_MFileBody" ref="body">
|
||||
<img src="img/spinner.gif" ref="image"
|
||||
alt={content.body} />
|
||||
<span className="mx_MFileBody">
|
||||
<div className="mx_MImageBody_download">
|
||||
<div style={{display: "none"}}>
|
||||
{/*
|
||||
* Add dummy copy of the "a" tag
|
||||
* We'll use it to learn how the download link
|
||||
* would have been styled if it was rendered inline.
|
||||
*/}
|
||||
<a ref="dummyLink"/>
|
||||
</div>
|
||||
<iframe src={renderer_url} onLoad={onIframeLoad} ref="iframe"/>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const contentUrl = this._getContentUrl();
|
||||
|
||||
const fileName = content.body && content.body.length > 0 ? content.body : "Attachment";
|
||||
|
||||
var downloadAttr = undefined;
|
||||
if (this.state.decryptedUrl) {
|
||||
// If the file is encrypted then we MUST download it rather than displaying it
|
||||
// because Firefox is vunerable to XSS attacks in data:// URLs
|
||||
// and all browsers are vunerable to XSS attacks in blob: URLs
|
||||
// created with window.URL.createObjectURL
|
||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=255107
|
||||
// See https://w3c.github.io/FileAPI/#originOfBlobURL
|
||||
//
|
||||
// This is not a problem for unencrypted links because they are
|
||||
// either fetched from a different domain so are safe because of
|
||||
// the same-origin policy or they are fetch from the same domain,
|
||||
// in which case we trust that the homeserver will set a
|
||||
// Content-Security-Policy that disables script execution.
|
||||
// It is reasonable to trust the homeserver in that case since
|
||||
// it is the same domain that controls this javascript.
|
||||
//
|
||||
// We can't apply the same workaround for encrypted files because
|
||||
// we can't supply HTTP headers when the user clicks on a blob:
|
||||
// or data:// uri.
|
||||
//
|
||||
// We should probably provide a download attribute anyway so that
|
||||
// the file will have the correct name when the user tries to
|
||||
// download it. We can't provide a Content-Disposition header
|
||||
// like we would for HTTP.
|
||||
downloadAttr = fileName;
|
||||
}
|
||||
|
||||
if (contentUrl) {
|
||||
} else if (contentUrl) {
|
||||
// If the attachment is not encrypted then we check whether we
|
||||
// are being displayed in the room timeline or in a list of
|
||||
// files in the right hand side of the screen.
|
||||
if (this.props.tileShape === "file_grid") {
|
||||
return (
|
||||
<span className="mx_MFileBody">
|
||||
<div className="mx_MImageBody_download">
|
||||
<a className="mx_ImageBody_downloadLink" href={contentUrl} target="_blank" rel="noopener" download={downloadAttr}>
|
||||
<a className="mx_ImageBody_downloadLink" href={contentUrl} target="_blank">
|
||||
{ fileName }
|
||||
</a>
|
||||
<div className="mx_MImageBody_size">
|
||||
|
@ -148,8 +360,8 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<span className="mx_MFileBody">
|
||||
<div className="mx_MImageBody_download">
|
||||
<a href={contentUrl} target="_blank" rel="noopener" download={downloadAttr}>
|
||||
<TintableSvg src="img/download.svg" width="12" height="14"/>
|
||||
<a href={contentUrl} target="_blank" rel="noopener">
|
||||
<img src={tintedDownloadImageURL} width="12" height="14" ref="downloadImage"/>
|
||||
Download {text}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -160,7 +372,7 @@ module.exports = React.createClass({
|
|||
var extra = text ? (': ' + text) : '';
|
||||
return <span className="mx_MFileBody">
|
||||
Invalid file{extra}
|
||||
</span>
|
||||
</span>;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -23,7 +23,8 @@ import ImageUtils from '../../../ImageUtils';
|
|||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import {decryptFile} from '../../../utils/DecryptFile';
|
||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||
import q from 'q';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MImageBody',
|
||||
|
@ -31,11 +32,17 @@ module.exports = React.createClass({
|
|||
propTypes: {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: React.PropTypes.object.isRequired,
|
||||
|
||||
/* called when the image has loaded */
|
||||
onWidgetLoad: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
decryptedUrl: null,
|
||||
decryptedThumbnailUrl: null,
|
||||
decryptedBlob: null,
|
||||
error: null
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -94,7 +101,9 @@ module.exports = React.createClass({
|
|||
_getThumbUrl: function() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
// TODO: Decrypt and use the thumbnail file if one is present.
|
||||
if (this.state.decryptedThumbnailUrl) {
|
||||
return this.state.decryptedThumbnailUrl;
|
||||
}
|
||||
return this.state.decryptedUrl;
|
||||
} else {
|
||||
return MatrixClientPeg.get().mxcUrlToHttp(content.url, 800, 600);
|
||||
|
@ -106,15 +115,34 @@ module.exports = React.createClass({
|
|||
this.fixupHeight();
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
decryptFile(content.file).done((url) => {
|
||||
this.setState({
|
||||
decryptedUrl: url,
|
||||
var thumbnailPromise = q(null);
|
||||
if (content.info.thumbnail_file) {
|
||||
thumbnailPromise = decryptFile(
|
||||
content.info.thumbnail_file
|
||||
).then(function(blob) {
|
||||
return readBlobAsDataUri(blob);
|
||||
});
|
||||
}, (err) => {
|
||||
console.warn("Unable to decrypt attachment: ", err)
|
||||
}
|
||||
var decryptedBlob;
|
||||
thumbnailPromise.then((thumbnailUrl) => {
|
||||
return decryptFile(content.file).then(function(blob) {
|
||||
decryptedBlob = blob;
|
||||
return readBlobAsDataUri(blob);
|
||||
}).then((contentUrl) => {
|
||||
this.setState({
|
||||
decryptedUrl: contentUrl,
|
||||
decryptedThumbnailUrl: thumbnailUrl,
|
||||
decryptedBlob: decryptedBlob,
|
||||
});
|
||||
this.props.onWidgetLoad();
|
||||
});
|
||||
}).catch((err) => {
|
||||
console.warn("Unable to decrypt attachment: ", err);
|
||||
// Set a placeholder image when we can't decrypt the image.
|
||||
this.refs.image.src = "img/warning.svg";
|
||||
});
|
||||
this.setState({
|
||||
error: err,
|
||||
});
|
||||
}).done();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -152,6 +180,15 @@ module.exports = React.createClass({
|
|||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const content = this.props.mxEvent.getContent();
|
||||
|
||||
if (this.state.error !== null) {
|
||||
return (
|
||||
<span className="mx_MImageBody" ref="body">
|
||||
<img src="img/warning.svg" width="16" height="16"/>
|
||||
Error decrypting image
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
|
||||
// Need to decrypt the attachment
|
||||
|
@ -159,8 +196,15 @@ module.exports = React.createClass({
|
|||
// For now add an img tag with a spinner.
|
||||
return (
|
||||
<span className="mx_MImageBody" ref="body">
|
||||
<img className="mx_MImageBody_thumbnail" src="img/spinner.gif" ref="image"
|
||||
alt={content.body} />
|
||||
<div className="mx_MImageBody_thumbnail" ref="image" style={{
|
||||
"display": "flex",
|
||||
"alignItems": "center",
|
||||
"width": "100%",
|
||||
}}>
|
||||
<img src="img/spinner.gif" alt={content.body} width="32" height="32" style={{
|
||||
"margin": "auto",
|
||||
}}/>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -177,7 +221,7 @@ module.exports = React.createClass({
|
|||
onMouseEnter={this.onImageEnter}
|
||||
onMouseLeave={this.onImageLeave} />
|
||||
</a>
|
||||
<MFileBody {...this.props} decryptedUrl={this.state.decryptedUrl} />
|
||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
|
||||
</span>
|
||||
);
|
||||
} else if (content.body) {
|
||||
|
|
|
@ -21,16 +21,26 @@ import MFileBody from './MFileBody';
|
|||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Model from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
import { decryptFile } from '../../../utils/DecryptFile';
|
||||
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
|
||||
import q from 'q';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MVideoBody',
|
||||
|
||||
propTypes: {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: React.PropTypes.object.isRequired,
|
||||
|
||||
/* called when the video has loaded */
|
||||
onWidgetLoad: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
decryptedUrl: null,
|
||||
decryptedThumbnailUrl: null,
|
||||
decryptedBlob: null,
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -83,19 +93,29 @@ module.exports = React.createClass({
|
|||
if (content.info.thumbnail_file) {
|
||||
thumbnailPromise = decryptFile(
|
||||
content.info.thumbnail_file
|
||||
);
|
||||
).then(function(blob) {
|
||||
return readBlobAsDataUri(blob);
|
||||
});
|
||||
}
|
||||
var decryptedBlob;
|
||||
thumbnailPromise.then((thumbnailUrl) => {
|
||||
decryptFile(content.file).then((contentUrl) => {
|
||||
return decryptFile(content.file).then(function(blob) {
|
||||
decryptedBlob = blob;
|
||||
return readBlobAsDataUri(blob);
|
||||
}).then((contentUrl) => {
|
||||
this.setState({
|
||||
decryptedUrl: contentUrl,
|
||||
decryptedThumbnailUrl: thumbnailUrl,
|
||||
decryptedBlob: decryptedBlob,
|
||||
});
|
||||
this.props.onWidgetLoad();
|
||||
});
|
||||
}).catch((err) => {
|
||||
console.warn("Unable to decrypt attachment: ", err)
|
||||
console.warn("Unable to decrypt attachment: ", err);
|
||||
// Set a placeholder image when we can't decrypt the image.
|
||||
this.refs.image.src = "img/warning.svg";
|
||||
this.setState({
|
||||
error: err,
|
||||
});
|
||||
}).done();
|
||||
}
|
||||
},
|
||||
|
@ -103,14 +123,29 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
|
||||
if (this.state.error !== null) {
|
||||
return (
|
||||
<span className="mx_MVideoBody" ref="body">
|
||||
<img src="img/warning.svg" width="16" height="16"/>
|
||||
Error decrypting video
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
// Need to decrypt the attachment
|
||||
// The attachment is decrypted in componentDidMount.
|
||||
// For now add an img tag with a spinner.
|
||||
return (
|
||||
<span className="mx_MImageBody" ref="body">
|
||||
<img className="mx_MImageBody_thumbnail" src="img/spinner.gif" ref="image"
|
||||
alt={content.body} />
|
||||
<span className="mx_MVideoBody" ref="body">
|
||||
<div className="mx_MImageBody_thumbnail" ref="image" style={{
|
||||
"display": "flex",
|
||||
"align-items": "center",
|
||||
"justify-items": "center",
|
||||
"width": "100%",
|
||||
}}>
|
||||
<img src="img/spinner.gif" alt={content.body} width="16" height="16"/>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -141,7 +176,7 @@ module.exports = React.createClass({
|
|||
controls preload={preload} autoPlay={false}
|
||||
height={height} width={width} poster={poster}>
|
||||
</video>
|
||||
<MFileBody {...this.props} decryptedUrl={this.state.decryptedUrl} />
|
||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
|
||||
</span>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -27,7 +27,6 @@ var sdk = require('../../../index');
|
|||
var ScalarAuthClient = require("../../../ScalarAuthClient");
|
||||
var Modal = require("../../../Modal");
|
||||
var SdkConfig = require('../../../SdkConfig');
|
||||
var UserSettingsStore = require('../../../UserSettingsStore');
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
|
@ -201,7 +200,7 @@ module.exports = React.createClass({
|
|||
global.localStorage.removeItem("hide_preview_" + self.props.mxEvent.getId());
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
onStarterLinkClick: function(starterLink, ev) {
|
||||
|
@ -213,16 +212,6 @@ module.exports = React.createClass({
|
|||
// which requires the user to click through and THEN we can open the link in a new tab because
|
||||
// the window.open command occurs in the same stack frame as the onClick callback.
|
||||
|
||||
let integrationsEnabled = UserSettingsStore.isFeatureEnabled("integration_management");
|
||||
if (!integrationsEnabled) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Integrations disabled",
|
||||
description: "You need to enable the Labs option 'Integrations Management' in your Riot user settings first.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Go fetch a scalar token
|
||||
let scalarClient = new ScalarAuthClient();
|
||||
scalarClient.connect().then(() => {
|
||||
|
@ -241,8 +230,8 @@ module.exports = React.createClass({
|
|||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
let width = window.screen.width > 1024 ? 1024 : window.screen.width;
|
||||
let height = window.screen.height > 800 ? 800 : window.screen.height;
|
||||
let width = window.screen.width > 1024 ? 1024 : window.screen.width;
|
||||
let height = window.screen.height > 800 ? 800 : window.screen.height;
|
||||
let left = (window.screen.width - width) / 2;
|
||||
let top = (window.screen.height - height) / 2;
|
||||
window.open(completeUrl, '_blank', `height=${height}, width=${width}, top=${top}, left=${left},`);
|
||||
|
@ -304,4 +293,3 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -103,13 +103,13 @@ module.exports = React.createClass({
|
|||
}
|
||||
if (oldCanonicalAlias !== this.state.canonicalAlias) {
|
||||
console.log("AliasSettings: Updating canonical alias");
|
||||
promises = [ q.all(promises).then(
|
||||
promises = [q.all(promises).then(
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.props.roomId, "m.room.canonical_alias", {
|
||||
alias: this.state.canonicalAlias
|
||||
}, ""
|
||||
)
|
||||
) ];
|
||||
)];
|
||||
}
|
||||
|
||||
return promises;
|
||||
|
@ -144,7 +144,7 @@ module.exports = React.createClass({
|
|||
// XXX: do we need to deep copy aliases before editing it?
|
||||
this.state.domainToAliases[domain] = this.state.domainToAliases[domain] || [];
|
||||
this.state.domainToAliases[domain].push(alias);
|
||||
this.setState({
|
||||
this.setState({
|
||||
domainToAliases: this.state.domainToAliases
|
||||
});
|
||||
|
||||
|
@ -152,9 +152,9 @@ module.exports = React.createClass({
|
|||
this.refs.add_alias.setValue(''); // FIXME
|
||||
}
|
||||
else {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Invalid alias format",
|
||||
title: "Invalid alias format",
|
||||
description: "'" + alias + "' is not a valid format for an alias",
|
||||
});
|
||||
}
|
||||
|
@ -168,9 +168,9 @@ module.exports = React.createClass({
|
|||
this.state.domainToAliases[domain][index] = alias;
|
||||
}
|
||||
else {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Invalid address format",
|
||||
title: "Invalid address format",
|
||||
description: "'" + alias + "' is not a valid format for an address",
|
||||
});
|
||||
}
|
||||
|
@ -183,7 +183,7 @@ module.exports = React.createClass({
|
|||
// would be to arbitrarily deepcopy to a temp variable and then setState
|
||||
// that, but why bother when we can cut this corner.
|
||||
var alias = this.state.domainToAliases[domain].splice(index, 1);
|
||||
this.setState({
|
||||
this.setState({
|
||||
domainToAliases: this.state.domainToAliases
|
||||
});
|
||||
},
|
||||
|
@ -281,7 +281,7 @@ module.exports = React.createClass({
|
|||
onValueChanged={ self.onAliasChanged.bind(self, localDomain, i) }
|
||||
editable={ self.props.canSetAliases }
|
||||
initialValue={ alias } />
|
||||
<div className="mx_RoomSettings_deleteAlias">
|
||||
<div className="mx_RoomSettings_deleteAlias mx_filterFlipColor">
|
||||
{ deleteButton }
|
||||
</div>
|
||||
</div>
|
||||
|
@ -297,7 +297,7 @@ module.exports = React.createClass({
|
|||
placeholder={ "New address (e.g. #foo:" + localDomain + ")" }
|
||||
blurToCancel={ false }
|
||||
onValueChanged={ self.onAliasAdded } />
|
||||
<div className="mx_RoomSettings_addAlias">
|
||||
<div className="mx_RoomSettings_addAlias mx_filterFlipColor">
|
||||
<img src="img/plus.svg" width="14" height="14" alt="Add"
|
||||
onClick={ self.onAliasAdded.bind(self, undefined) }/>
|
||||
</div>
|
||||
|
@ -310,4 +310,4 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -135,7 +135,7 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
var boundClick = this._onColorSchemeChanged.bind(this, i)
|
||||
var boundClick = this._onColorSchemeChanged.bind(this, i);
|
||||
return (
|
||||
<div className="mx_RoomSettings_roomColor"
|
||||
key={ "room_color_" + i }
|
||||
|
|
|
@ -121,13 +121,13 @@ module.exports = React.createClass({
|
|||
onChange={ this.onGlobalDisableUrlPreviewChange }
|
||||
checked={ this.state.globalDisableUrlPreview } />
|
||||
Disable URL previews by default for participants in this room
|
||||
</label>
|
||||
</label>;
|
||||
}
|
||||
else {
|
||||
disableRoomPreviewUrls =
|
||||
<label>
|
||||
URL previews are { this.state.globalDisableUrlPreview ? "disabled" : "enabled" } by default for participants in this room.
|
||||
</label>
|
||||
</label>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -154,4 +154,4 @@ module.exports = React.createClass({
|
|||
);
|
||||
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -93,8 +93,8 @@ module.exports = React.createClass({
|
|||
}
|
||||
else {
|
||||
joinText = (<span>
|
||||
Join as <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice')}}
|
||||
href="#">voice</a> or <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video') }}
|
||||
Join as <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}}
|
||||
href="#">voice</a> or <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video'); }}
|
||||
href="#">video</a>.
|
||||
</span>);
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ var React = require('react');
|
|||
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var sdk = require('../../../index');
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
|
||||
var PRESENCE_CLASS = {
|
||||
|
@ -152,7 +153,7 @@ module.exports = React.createClass({
|
|||
var av = this.props.avatarJsx || <BaseAvatar name={this.props.name} width={36} height={36} />;
|
||||
|
||||
return (
|
||||
<div className={mainClassName} title={ this.props.title }
|
||||
<AccessibleButton className={mainClassName} title={ this.props.title }
|
||||
onClick={ this.props.onClick } onMouseEnter={ this.mouseEnter }
|
||||
onMouseLeave={ this.mouseLeave }>
|
||||
<div className="mx_EntityTile_avatar">
|
||||
|
@ -161,7 +162,7 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
{ nameEl }
|
||||
{ inviteButton }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -21,8 +21,8 @@ var classNames = require("classnames");
|
|||
var Modal = require('../../../Modal');
|
||||
|
||||
var sdk = require('../../../index');
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg')
|
||||
var TextForEvent = require('../../../TextForEvent');
|
||||
import WithMatrixClient from '../../../wrappers/WithMatrixClient';
|
||||
|
||||
var ContextualMenu = require('../../structures/ContextualMenu');
|
||||
var dispatcher = require("../../../dispatcher");
|
||||
|
@ -63,22 +63,13 @@ var MAX_READ_AVATARS = 5;
|
|||
// | '--------------------------------------' |
|
||||
// '----------------------------------------------------------'
|
||||
|
||||
module.exports = React.createClass({
|
||||
module.exports = WithMatrixClient(React.createClass({
|
||||
displayName: 'EventTile',
|
||||
|
||||
statics: {
|
||||
haveTileForEvent: function(e) {
|
||||
if (e.isRedacted()) return false;
|
||||
if (eventTileTypes[e.getType()] == undefined) return false;
|
||||
if (eventTileTypes[e.getType()] == 'messages.TextualEvent') {
|
||||
return TextForEvent.textForEvent(e) !== '';
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
/* MatrixClient instance for sender verification etc */
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: React.PropTypes.object.isRequired,
|
||||
|
||||
|
@ -112,7 +103,7 @@ module.exports = React.createClass({
|
|||
/* callback called when dynamic content in events are loaded */
|
||||
onWidgetLoad: React.PropTypes.func,
|
||||
|
||||
/* a list of Room Members whose read-receipts we should show */
|
||||
/* a list of read-receipts we should show. Each object has a 'roomMember' and 'ts'. */
|
||||
readReceipts: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||
|
||||
/* opaque readreceipt info for each userId; used by ReadReceiptMarker
|
||||
|
@ -153,17 +144,18 @@ module.exports = React.createClass({
|
|||
|
||||
componentDidMount: function() {
|
||||
this._suppressReadReceiptAnimation = false;
|
||||
MatrixClientPeg.get().on("deviceVerificationChanged",
|
||||
this.props.matrixClient.on("deviceVerificationChanged",
|
||||
this.onDeviceVerificationChanged);
|
||||
this.props.mxEvent.on("Event.decrypted", this._onDecrypted);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function (nextProps) {
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
if (nextProps.mxEvent !== this.props.mxEvent) {
|
||||
this._verifyEvent(nextProps.mxEvent);
|
||||
}
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function (nextProps, nextState) {
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
if (!ObjectUtils.shallowEqual(this.state, nextState)) {
|
||||
return true;
|
||||
}
|
||||
|
@ -176,11 +168,18 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
var client = MatrixClientPeg.get();
|
||||
if (client) {
|
||||
client.removeListener("deviceVerificationChanged",
|
||||
this.onDeviceVerificationChanged);
|
||||
}
|
||||
var client = this.props.matrixClient;
|
||||
client.removeListener("deviceVerificationChanged",
|
||||
this.onDeviceVerificationChanged);
|
||||
this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted);
|
||||
},
|
||||
|
||||
/** called when the event is decrypted after we show it.
|
||||
*/
|
||||
_onDecrypted: function() {
|
||||
// we need to re-verify the sending device.
|
||||
this._verifyEvent(this.props.mxEvent);
|
||||
this.forceUpdate();
|
||||
},
|
||||
|
||||
onDeviceVerificationChanged: function(userId, device) {
|
||||
|
@ -193,7 +192,7 @@ module.exports = React.createClass({
|
|||
var verified = null;
|
||||
|
||||
if (mxEvent.isEncrypted()) {
|
||||
verified = MatrixClientPeg.get().isEventSenderVerified(mxEvent);
|
||||
verified = this.props.matrixClient.isEventSenderVerified(mxEvent);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
|
@ -232,7 +231,7 @@ module.exports = React.createClass({
|
|||
return false;
|
||||
}
|
||||
for (var j = 0; j < rA.length; j++) {
|
||||
if (rA[j].userId !== rB[j].userId) {
|
||||
if (rA[j].roomMember.userId !== rB[j].roomMember.userId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -246,11 +245,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
shouldHighlight: function() {
|
||||
var actions = MatrixClientPeg.get().getPushActionsForEvent(this.props.mxEvent);
|
||||
var actions = this.props.matrixClient.getPushActionsForEvent(this.props.mxEvent);
|
||||
if (!actions || !actions.tweaks) { return false; }
|
||||
|
||||
// don't show self-highlights from another of our clients
|
||||
if (this.props.mxEvent.getSender() === MatrixClientPeg.get().credentials.userId)
|
||||
if (this.props.mxEvent.getSender() === this.props.matrixClient.credentials.userId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
@ -260,11 +259,11 @@ module.exports = React.createClass({
|
|||
|
||||
onEditClicked: function(e) {
|
||||
var MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
|
||||
var buttonRect = e.target.getBoundingClientRect()
|
||||
var buttonRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
var x = buttonRect.right + window.pageXOffset;
|
||||
var y = (buttonRect.top + (e.target.height / 2) + window.pageYOffset) - 19;
|
||||
var y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
||||
var self = this;
|
||||
ContextualMenu.createMenu(MessageContextMenu, {
|
||||
chevronOffset: 10,
|
||||
|
@ -288,19 +287,28 @@ module.exports = React.createClass({
|
|||
getReadAvatars: function() {
|
||||
var ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
|
||||
var avatars = [];
|
||||
|
||||
var left = 0;
|
||||
|
||||
// It's possible that the receipt was sent several days AFTER the event.
|
||||
// If it is, we want to display the complete date along with the HH:MM:SS,
|
||||
// rather than just HH:MM:SS.
|
||||
let dayAfterEvent = new Date(this.props.mxEvent.getTs());
|
||||
dayAfterEvent.setDate(dayAfterEvent.getDate() + 1);
|
||||
dayAfterEvent.setHours(0);
|
||||
dayAfterEvent.setMinutes(0);
|
||||
dayAfterEvent.setSeconds(0);
|
||||
let dayAfterEventTime = dayAfterEvent.getTime();
|
||||
|
||||
var receipts = this.props.readReceipts || [];
|
||||
for (var i = 0; i < receipts.length; ++i) {
|
||||
var member = receipts[i];
|
||||
var receipt = receipts[i];
|
||||
|
||||
var hidden = true;
|
||||
if ((i < MAX_READ_AVATARS) || this.state.allReadAvatars) {
|
||||
hidden = false;
|
||||
}
|
||||
|
||||
var userId = member.userId;
|
||||
var userId = receipt.roomMember.userId;
|
||||
var readReceiptInfo;
|
||||
|
||||
if (this.props.readReceiptMap) {
|
||||
|
@ -312,15 +320,16 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
//console.log("i = " + i + ", MAX_READ_AVATARS = " + MAX_READ_AVATARS + ", allReadAvatars = " + this.state.allReadAvatars + " visibility = " + style.visibility);
|
||||
|
||||
// add to the start so the most recent is on the end (ie. ends up rightmost)
|
||||
avatars.unshift(
|
||||
<ReadReceiptMarker key={userId} member={member}
|
||||
<ReadReceiptMarker key={userId} member={receipt.roomMember}
|
||||
leftOffset={left} hidden={hidden}
|
||||
readReceiptInfo={readReceiptInfo}
|
||||
checkUnmounting={this.props.checkUnmounting}
|
||||
suppressAnimation={this._suppressReadReceiptAnimation}
|
||||
onClick={this.toggleAllReadAvatars}
|
||||
timestamp={receipt.ts}
|
||||
showFullTimestamp={receipt.ts >= dayAfterEventTime}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -352,15 +361,16 @@ module.exports = React.createClass({
|
|||
var mxEvent = this.props.mxEvent;
|
||||
dispatcher.dispatch({
|
||||
action: 'insert_displayname',
|
||||
displayname: mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(),
|
||||
displayname: (mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()).replace(' (IRC)', ''),
|
||||
});
|
||||
},
|
||||
|
||||
onCryptoClicked: function(e) {
|
||||
var EncryptedEventDialog = sdk.getComponent("dialogs.EncryptedEventDialog");
|
||||
var event = this.props.mxEvent;
|
||||
|
||||
Modal.createDialog(EncryptedEventDialog, {
|
||||
Modal.createDialogAsync((cb) => {
|
||||
require(['../../../async-components/views/dialogs/EncryptedEventDialog'], cb);
|
||||
}, {
|
||||
event: event,
|
||||
});
|
||||
},
|
||||
|
@ -387,7 +397,7 @@ module.exports = React.createClass({
|
|||
throw new Error("Event type not supported");
|
||||
}
|
||||
|
||||
var e2eEnabled = MatrixClientPeg.get().isRoomEncrypted(this.props.mxEvent.getRoomId());
|
||||
var e2eEnabled = this.props.matrixClient.isRoomEncrypted(this.props.mxEvent.getRoomId());
|
||||
var isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
|
||||
|
||||
var classes = classNames({
|
||||
|
@ -456,7 +466,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
var editButton = (
|
||||
<img className="mx_EventTile_editButton" src="img/icon_context_message.svg" width="19" height="19" alt="Options" title="Options" onClick={this.onEditClicked} />
|
||||
<span className="mx_EventTile_editButton" title="Options" onClick={this.onEditClicked} />
|
||||
);
|
||||
|
||||
var e2e;
|
||||
|
@ -481,7 +491,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
if (this.props.tileShape === "notif") {
|
||||
var room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
var room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
|
@ -554,4 +564,14 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
module.exports.haveTileForEvent = function(e) {
|
||||
if (e.isRedacted()) return false;
|
||||
if (eventTileTypes[e.getType()] == undefined) return false;
|
||||
if (eventTileTypes[e.getType()] == 'messages.TextualEvent') {
|
||||
return TextForEvent.textForEvent(e) !== '';
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -60,13 +60,15 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
if (this.refs.description)
|
||||
if (this.refs.description) {
|
||||
linkifyElement(this.refs.description, linkifyMatrix.options);
|
||||
}
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
if (this.refs.description)
|
||||
if (this.refs.description) {
|
||||
linkifyElement(this.refs.description, linkifyMatrix.options);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
|
@ -116,7 +118,7 @@ module.exports = React.createClass({
|
|||
if (image) {
|
||||
img = <div className="mx_LinkPreviewWidget_image" style={{ height: thumbHeight }}>
|
||||
<img style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }} src={ image } onClick={ this.onImageClick }/>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -24,8 +24,8 @@ export default class MemberDeviceInfo extends React.Component {
|
|||
|
||||
if (this.props.device.isBlocked()) {
|
||||
indicator = (
|
||||
<div className="mx_MemberDeviceInfo_blocked">
|
||||
<img src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} alt="Blocked"/>
|
||||
<div className="mx_MemberDeviceInfo_blacklisted">
|
||||
<img src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} alt="Blacklisted"/>
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.device.isVerified()) {
|
||||
|
@ -60,7 +60,7 @@ export default class MemberDeviceInfo extends React.Component {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
MemberDeviceInfo.displayName = 'MemberDeviceInfo';
|
||||
MemberDeviceInfo.propTypes = {
|
||||
|
|
|
@ -30,12 +30,12 @@ var classNames = require('classnames');
|
|||
var dis = require("../../../dispatcher");
|
||||
var Modal = require("../../../Modal");
|
||||
var sdk = require('../../../index');
|
||||
var UserSettingsStore = require('../../../UserSettingsStore');
|
||||
var createRoom = require('../../../createRoom');
|
||||
var DMRoomMap = require('../../../utils/DMRoomMap');
|
||||
var Unread = require('../../../Unread');
|
||||
var Receipt = require('../../../utils/Receipt');
|
||||
var WithMatrixClient = require('../../../wrappers/WithMatrixClient');
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
module.exports = WithMatrixClient(React.createClass({
|
||||
displayName: 'MemberInfo',
|
||||
|
@ -65,16 +65,14 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
updating: 0,
|
||||
devicesLoading: true,
|
||||
devices: null,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._cancelDeviceList = null;
|
||||
|
||||
// only display the devices list if our client supports E2E *and* the
|
||||
// feature is enabled in the user settings
|
||||
this._enableDevices = this.props.matrixClient.isCryptoEnabled() &&
|
||||
UserSettingsStore.isFeatureEnabled("e2e_encryption");
|
||||
// only display the devices list if our client supports E2E
|
||||
this._enableDevices = this.props.matrixClient.isCryptoEnabled();
|
||||
|
||||
const cli = this.props.matrixClient;
|
||||
cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
|
@ -205,7 +203,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
}
|
||||
|
||||
var cancelled = false;
|
||||
this._cancelDeviceList = function() { cancelled = true; }
|
||||
this._cancelDeviceList = function() { cancelled = true; };
|
||||
|
||||
var client = this.props.matrixClient;
|
||||
var self = this;
|
||||
|
@ -379,6 +377,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
// get out of sync if we force setState here!
|
||||
console.log("Power change success");
|
||||
}, function(err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failure to change power level",
|
||||
description: err.message
|
||||
|
@ -386,7 +385,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
}
|
||||
).finally(()=>{
|
||||
this.setState({ updating: this.state.updating - 1 });
|
||||
});
|
||||
}).done();
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
|
@ -531,7 +530,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onMemberAvatarClick: function () {
|
||||
onMemberAvatarClick: function() {
|
||||
var avatarUrl = this.props.member.user ? this.props.member.user.avatarUrl : this.props.member.events.member.getContent().avatar_url;
|
||||
if(!avatarUrl) return;
|
||||
|
||||
|
@ -614,7 +613,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
mx_MemberInfo_createRoom_label: true,
|
||||
mx_RoomTile_name: true,
|
||||
});
|
||||
const startNewChat = <div
|
||||
const startNewChat = <AccessibleButton
|
||||
className="mx_MemberInfo_createRoom"
|
||||
onClick={this.onNewDMClick}
|
||||
>
|
||||
|
@ -622,7 +621,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
<img src="img/create-big.svg" width="26" height="26" />
|
||||
</div>
|
||||
<div className={labelClasses}><i>Start new chat</i></div>
|
||||
</div>
|
||||
</AccessibleButton>;
|
||||
|
||||
startChat = <div>
|
||||
<h3>Direct chats</h3>
|
||||
|
@ -637,26 +636,37 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
}
|
||||
|
||||
if (this.state.can.kick) {
|
||||
kickButton = <div className="mx_MemberInfo_field" onClick={this.onKick}>
|
||||
{ this.props.member.membership === "invite" ? "Disinvite" : "Kick" }
|
||||
</div>;
|
||||
const membership = this.props.member.membership;
|
||||
const kickLabel = membership === "invite" ? "Disinvite" : "Kick";
|
||||
kickButton = (
|
||||
<AccessibleButton className="mx_MemberInfo_field"
|
||||
onClick={this.onKick}>
|
||||
{kickLabel}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
if (this.state.can.ban) {
|
||||
banButton = <div className="mx_MemberInfo_field" onClick={this.onBan}>
|
||||
Ban
|
||||
</div>;
|
||||
banButton = (
|
||||
<AccessibleButton className="mx_MemberInfo_field"
|
||||
onClick={this.onBan}>
|
||||
Ban
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
if (this.state.can.mute) {
|
||||
var muteLabel = this.state.muted ? "Unmute" : "Mute";
|
||||
muteButton = <div className="mx_MemberInfo_field" onClick={this.onMuteToggle}>
|
||||
{muteLabel}
|
||||
</div>;
|
||||
const muteLabel = this.state.muted ? "Unmute" : "Mute";
|
||||
muteButton = (
|
||||
<AccessibleButton className="mx_MemberInfo_field"
|
||||
onClick={this.onMuteToggle}>
|
||||
{muteLabel}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
if (this.state.can.toggleMod) {
|
||||
var giveOpLabel = this.state.isTargetMod ? "Revoke Moderator" : "Make Moderator";
|
||||
giveModButton = <div className="mx_MemberInfo_field" onClick={this.onModToggle}>
|
||||
giveModButton = <AccessibleButton className="mx_MemberInfo_field" onClick={this.onModToggle}>
|
||||
{giveOpLabel}
|
||||
</div>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
// TODO: we should have an invite button if this MemberInfo is showing a user who isn't actually in the current room yet
|
||||
|
@ -674,7 +684,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
{banButton}
|
||||
{giveModButton}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const memberName = this.props.member.name;
|
||||
|
@ -684,7 +694,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
return (
|
||||
<div className="mx_MemberInfo">
|
||||
<img className="mx_MemberInfo_cancel" src="img/cancel.svg" width="18" height="18" onClick={this.onCancel}/>
|
||||
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}> <img src="img/cancel.svg" width="18" height="18"/></AccessibleButton>
|
||||
<div className="mx_MemberInfo_avatar">
|
||||
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />
|
||||
</div>
|
||||
|
|
|
@ -32,7 +32,7 @@ var SHARE_HISTORY_WARNING =
|
|||
Newly invited users will see the history of this room. <br/>
|
||||
If you'd prefer invited users not to see messages that were sent before they joined, <br/>
|
||||
turn off, 'Share message history with new users' in the settings for this room.
|
||||
</span>
|
||||
</span>;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MemberList',
|
||||
|
@ -207,7 +207,7 @@ module.exports = React.createClass({
|
|||
// For now we'll pretend this is any entity. It should probably be a separate tile.
|
||||
var EntityTile = sdk.getComponent("rooms.EntityTile");
|
||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
var text = "and " + overflowCount + " other" + (overflowCount > 1 ? "s" : "") + "...";
|
||||
var text = "and " + overflowCount + " other" + (overflowCount > 1 ? "s" : "") + "...";
|
||||
return (
|
||||
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||
<BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} />
|
||||
|
@ -338,8 +338,8 @@ module.exports = React.createClass({
|
|||
}
|
||||
memberList.push(
|
||||
<EntityTile key={e.getStateKey()} name={e.getContent().display_name} />
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ module.exports = React.createClass({
|
|||
(this.user_last_modified_time === undefined ||
|
||||
this.user_last_modified_time < nextProps.member.user.getLastModifiedTime())
|
||||
) {
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
|
|
@ -222,13 +222,24 @@ export default class MessageComposer extends React.Component {
|
|||
</div>
|
||||
);
|
||||
|
||||
let e2eImg, e2eTitle, e2eClass;
|
||||
|
||||
if (MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId)) {
|
||||
// FIXME: show a /!\ if there are untrusted devices in the room...
|
||||
controls.push(
|
||||
<img key="e2eIcon" className="mx_MessageComposer_e2eIcon" src="img/e2e-verified.svg" width="10" height="12" alt="Encrypted room"/>
|
||||
);
|
||||
e2eImg = 'img/e2e-verified.svg';
|
||||
e2eTitle = 'Encrypted room';
|
||||
e2eClass = 'mx_MessageComposer_e2eIcon';
|
||||
} else {
|
||||
e2eImg = 'img/e2e-unencrypted.svg';
|
||||
e2eTitle = 'Unencrypted room';
|
||||
e2eClass = 'mx_MessageComposer_e2eIcon mx_filterFlipColor';
|
||||
}
|
||||
|
||||
controls.push(
|
||||
<img key="e2eIcon" className={e2eClass} src={e2eImg} width="12" height="12"
|
||||
alt={e2eTitle} title={e2eTitle}
|
||||
/>
|
||||
);
|
||||
var callButton, videoCallButton, hangupButton;
|
||||
if (this.props.callState && this.props.callState !== 'ended') {
|
||||
hangupButton =
|
||||
|
@ -322,6 +333,7 @@ export default class MessageComposer extends React.Component {
|
|||
const disabled = !this.state.inputState.isRichtextEnabled && 'underline' === name;
|
||||
const className = classNames("mx_MessageComposer_format_button", {
|
||||
mx_MessageComposer_format_button_disabled: disabled,
|
||||
mx_filterFlipColor: true,
|
||||
});
|
||||
return <img className={className}
|
||||
title={name}
|
||||
|
@ -346,11 +358,11 @@ export default class MessageComposer extends React.Component {
|
|||
<div style={{flex: 1}}></div>
|
||||
<img title={`Turn Markdown ${this.state.inputState.isRichtextEnabled ? 'on' : 'off'}`}
|
||||
onMouseDown={this.onToggleMarkdownClicked}
|
||||
className="mx_MessageComposer_formatbar_markdown"
|
||||
className="mx_MessageComposer_formatbar_markdown mx_filterFlipColor"
|
||||
src={`img/button-md-${!this.state.inputState.isRichtextEnabled}.png`} />
|
||||
<img title="Hide Text Formatting Toolbar"
|
||||
onClick={this.onToggleFormattingClicked}
|
||||
className="mx_MessageComposer_formatbar_cancel"
|
||||
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
|
||||
src="img/icon-text-cancel.svg" />
|
||||
</div>
|
||||
</div>: null
|
||||
|
@ -358,7 +370,7 @@ export default class MessageComposer extends React.Component {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
MessageComposer.propTypes = {
|
||||
tabComplete: React.PropTypes.any,
|
||||
|
|
|
@ -272,7 +272,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
break;
|
||||
|
||||
case 'quote': {
|
||||
let {event: {content: {body, formatted_body}}} = payload.event || {};
|
||||
let {body, formatted_body} = payload.event.getContent();
|
||||
formatted_body = formatted_body || escape(body);
|
||||
if (formatted_body) {
|
||||
let content = RichText.HTMLtoContentState(`<blockquote>${formatted_body}</blockquote>`);
|
||||
|
@ -443,12 +443,12 @@ export default class MessageComposerInput extends React.Component {
|
|||
selection = this.state.editorState.getSelection();
|
||||
|
||||
let modifyFn = {
|
||||
bold: text => `**${text}**`,
|
||||
italic: text => `*${text}*`,
|
||||
underline: text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug*
|
||||
strike: text => `~~${text}~~`,
|
||||
code: text => `\`${text}\``,
|
||||
blockquote: text => text.split('\n').map(line => `> ${line}\n`).join(''),
|
||||
'bold': text => `**${text}**`,
|
||||
'italic': text => `*${text}*`,
|
||||
'underline': text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug*
|
||||
'strike': text => `~~${text}~~`,
|
||||
'code': text => `\`${text}\``,
|
||||
'blockquote': text => text.split('\n').map(line => `> ${line}\n`).join(''),
|
||||
'unordered-list-item': text => text.split('\n').map(line => `- ${line}\n`).join(''),
|
||||
'ordered-list-item': text => text.split('\n').map((line, i) => `${i+1}. ${line}\n`).join(''),
|
||||
}[command];
|
||||
|
@ -462,8 +462,9 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
if (newState == null)
|
||||
if (newState == null) {
|
||||
newState = RichUtils.handleKeyCommand(this.state.editorState, command);
|
||||
}
|
||||
|
||||
if (newState != null) {
|
||||
this.setEditorState(newState);
|
||||
|
@ -523,7 +524,9 @@ export default class MessageComposerInput extends React.Component {
|
|||
);
|
||||
} else {
|
||||
const md = new Markdown(contentText);
|
||||
if (!md.isPlainText()) {
|
||||
if (md.isPlainText()) {
|
||||
contentText = md.toPlaintext();
|
||||
} else {
|
||||
contentHTML = md.toHTML();
|
||||
}
|
||||
}
|
||||
|
@ -663,7 +666,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
const blockName = {
|
||||
'code-block': 'code',
|
||||
blockquote: 'quote',
|
||||
'blockquote': 'quote',
|
||||
'unordered-list-item': 'bullet',
|
||||
'ordered-list-item': 'numbullet',
|
||||
};
|
||||
|
@ -716,7 +719,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
selection={selection} />
|
||||
</div>
|
||||
<div className={className}>
|
||||
<img className="mx_MessageComposer_input_markdownIndicator"
|
||||
<img className="mx_MessageComposer_input_markdownIndicator mx_filterFlipColor"
|
||||
onMouseDown={this.onMarkdownToggleClicked}
|
||||
title={`Markdown is ${this.state.isRichtextEnabled ? 'disabled' : 'enabled'}`}
|
||||
src={`img/button-md-${!this.state.isRichtextEnabled}.png`} />
|
||||
|
@ -738,7 +741,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
MessageComposerInput.propTypes = {
|
||||
tabComplete: React.PropTypes.any,
|
||||
|
|
|
@ -192,7 +192,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onKeyDown: function (ev) {
|
||||
onKeyDown: function(ev) {
|
||||
if (ev.keyCode === KeyCode.ENTER && !ev.shiftKey) {
|
||||
var input = this.refs.textarea.value;
|
||||
if (input.length === 0) {
|
||||
|
@ -331,6 +331,7 @@ module.exports = React.createClass({
|
|||
MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, htmlText);
|
||||
}
|
||||
else {
|
||||
const contentText = mdown.toPlaintext();
|
||||
sendMessagePromise = isEmote ?
|
||||
MatrixClientPeg.get().sendEmoteMessage(this.props.room.roomId, contentText) :
|
||||
MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText);
|
||||
|
|
|
@ -60,12 +60,18 @@ module.exports = React.createClass({
|
|||
|
||||
// callback for clicks on this RR
|
||||
onClick: React.PropTypes.func,
|
||||
|
||||
// Timestamp when the receipt was read
|
||||
timestamp: React.PropTypes.number,
|
||||
|
||||
// True to show the full date/time rather than just the time
|
||||
showFullTimestamp: React.PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
leftOffset: 0,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -75,7 +81,7 @@ module.exports = React.createClass({
|
|||
// position.
|
||||
return {
|
||||
suppressDisplay: !this.props.suppressAnimation,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
|
@ -162,17 +168,33 @@ module.exports = React.createClass({
|
|||
visibility: this.props.hidden ? 'hidden' : 'visible',
|
||||
};
|
||||
|
||||
let title;
|
||||
if (this.props.timestamp) {
|
||||
let suffix = " (" + this.props.member.userId + ")";
|
||||
let ts = new Date(this.props.timestamp);
|
||||
if (this.props.showFullTimestamp) {
|
||||
// "15/12/2016, 7:05:45 PM (@alice:matrix.org)"
|
||||
title = ts.toLocaleString() + suffix;
|
||||
}
|
||||
else {
|
||||
// "7:05:45 PM (@alice:matrix.org)"
|
||||
title = ts.toLocaleTimeString() + suffix;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Velociraptor
|
||||
startStyles={this.state.startStyles}
|
||||
enterTransitionOpts={this.state.enterTransitionOpts} >
|
||||
<MemberAvatar
|
||||
member={this.props.member}
|
||||
aria-hidden="true"
|
||||
width={14} height={14} resizeMethod="crop"
|
||||
style={style}
|
||||
onClick={this.props.onClick}
|
||||
title={title}
|
||||
/>
|
||||
</Velociraptor>
|
||||
);
|
||||
/* onClick={this.props.onClick} */
|
||||
},
|
||||
});
|
||||
|
|
|
@ -26,6 +26,8 @@ var rate_limited_func = require('../../../ratelimitedfunc');
|
|||
var linkify = require('linkifyjs');
|
||||
var linkifyElement = require('linkifyjs/element');
|
||||
var linkifyMatrix = require('../../../linkify-matrix');
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import {CancelButton} from './SimpleRoomHeader';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
|
@ -182,8 +184,8 @@ module.exports = React.createClass({
|
|||
'm.room.name', user_id
|
||||
);
|
||||
|
||||
save_button = <div className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save</div>
|
||||
cancel_button = <div className="mx_RoomHeader_cancelButton" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" alt="Cancel"/> </div>
|
||||
save_button = <AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save</AccessibleButton>;
|
||||
cancel_button = <CancelButton onClick={this.props.onCancelClick}/>;
|
||||
}
|
||||
|
||||
if (this.props.saving) {
|
||||
|
@ -193,7 +195,7 @@ module.exports = React.createClass({
|
|||
|
||||
if (can_set_room_name) {
|
||||
var RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor");
|
||||
name = <RoomNameEditor ref="nameEditor" room={this.props.room} />
|
||||
name = <RoomNameEditor ref="nameEditor" room={this.props.room} />;
|
||||
}
|
||||
else {
|
||||
var searchStatus;
|
||||
|
@ -232,7 +234,7 @@ module.exports = React.createClass({
|
|||
|
||||
if (can_set_room_topic) {
|
||||
var RoomTopicEditor = sdk.getComponent("rooms.RoomTopicEditor");
|
||||
topic_el = <RoomTopicEditor ref="topicEditor" room={this.props.room} />
|
||||
topic_el = <RoomTopicEditor ref="topicEditor" room={this.props.room} />;
|
||||
} else {
|
||||
var topic;
|
||||
if (this.props.room) {
|
||||
|
@ -275,9 +277,9 @@ module.exports = React.createClass({
|
|||
var settings_button;
|
||||
if (this.props.onSettingsClick) {
|
||||
settings_button =
|
||||
<div className="mx_RoomHeader_button" onClick={this.props.onSettingsClick} title="Settings">
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSettingsClick} title="Settings">
|
||||
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
|
||||
</div>;
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
// var leave_button;
|
||||
|
@ -291,17 +293,17 @@ module.exports = React.createClass({
|
|||
var forget_button;
|
||||
if (this.props.onForgetClick) {
|
||||
forget_button =
|
||||
<div className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title="Forget room">
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title="Forget room">
|
||||
<TintableSvg src="img/leave.svg" width="26" height="20"/>
|
||||
</div>;
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
var rightPanel_buttons;
|
||||
if (this.props.collapsedRhs) {
|
||||
rightPanel_buttons =
|
||||
<div className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title="<">
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title="<">
|
||||
<TintableSvg src="img/minimise.svg" width="10" height="16"/>
|
||||
</div>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
var right_row;
|
||||
|
@ -310,9 +312,9 @@ module.exports = React.createClass({
|
|||
<div className="mx_RoomHeader_rightRow">
|
||||
{ settings_button }
|
||||
{ forget_button }
|
||||
<div className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search">
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search">
|
||||
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
{ rightPanel_buttons }
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ module.exports = React.createClass({
|
|||
isLoadingLeftRooms: false,
|
||||
lists: {},
|
||||
incomingCall: null,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
|
@ -338,7 +338,7 @@ module.exports = React.createClass({
|
|||
// as this is used to calculate the CSS fixed top position for the stickies
|
||||
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
|
||||
|
||||
var top = (incomingCallBox.parentElement.getBoundingClientRect().top + window.pageYOffset)
|
||||
var top = (incomingCallBox.parentElement.getBoundingClientRect().top + window.pageYOffset);
|
||||
// Make sure we don't go too far up, if the headers aren't sticky
|
||||
top = (top < scrollAreaOffset) ? scrollAreaOffset : top;
|
||||
// make sure we don't go too far down, if the headers aren't sticky
|
||||
|
@ -401,7 +401,7 @@ module.exports = React.createClass({
|
|||
var stickyHeight = sticky.dataset.originalHeight;
|
||||
var stickyHeader = sticky.childNodes[0];
|
||||
var topStuckHeight = stickyHeight * i;
|
||||
var bottomStuckHeight = stickyHeight * (stickyWrappers.length - i)
|
||||
var bottomStuckHeight = stickyHeight * (stickyWrappers.length - i);
|
||||
|
||||
if (self.scrollAreaSufficient && stickyPosition < (scrollArea.scrollTop + topStuckHeight)) {
|
||||
// Top stickies
|
||||
|
@ -520,7 +520,7 @@ module.exports = React.createClass({
|
|||
collapsed={ self.props.collapsed }
|
||||
searchFilter={ self.props.searchFilter }
|
||||
onHeaderClick={ self.onSubListHeaderClick }
|
||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||
onShowMoreRooms={ self.onShowMoreRooms } />;
|
||||
|
||||
}
|
||||
}) }
|
||||
|
|
|
@ -58,7 +58,7 @@ module.exports = React.createClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
busy: false
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
|
@ -94,9 +94,9 @@ module.exports = React.createClass({
|
|||
if (this.props.invitedEmail) {
|
||||
if (this.state.threePidFetchError) {
|
||||
emailMatchBlock = <div className="error">
|
||||
Riot was unable to ascertain that the address this invite was
|
||||
Unable to ascertain that the address this invite was
|
||||
sent to matches one associated with your account.
|
||||
</div>
|
||||
</div>;
|
||||
} else if (this.state.invitedEmailMxid != MatrixClientPeg.get().credentials.userId) {
|
||||
emailMatchBlock =
|
||||
<div className="mx_RoomPreviewBar_warning">
|
||||
|
@ -107,7 +107,7 @@ module.exports = React.createClass({
|
|||
This invitation was sent to <b><span className="email">{this.props.invitedEmail}</span></b>, which is not associated with this account.<br/>
|
||||
You may wish to login with a different account, or add this email to this account.
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
joinBlock = (
|
||||
|
|
|
@ -24,7 +24,6 @@ var ObjectUtils = require("../../../ObjectUtils");
|
|||
var dis = require("../../../dispatcher");
|
||||
var ScalarAuthClient = require("../../../ScalarAuthClient");
|
||||
var ScalarMessaging = require('../../../ScalarMessaging');
|
||||
var UserSettingsStore = require('../../../UserSettingsStore');
|
||||
|
||||
// parse a string as an integer; if the input is undefined, or cannot be parsed
|
||||
// as an integer, return a default.
|
||||
|
@ -81,16 +80,14 @@ module.exports = React.createClass({
|
|||
console.error("Failed to get room visibility: " + err);
|
||||
});
|
||||
|
||||
if (UserSettingsStore.isFeatureEnabled("integration_management")) {
|
||||
this.scalarClient = new ScalarAuthClient();
|
||||
this.scalarClient.connect().done(() => {
|
||||
this.forceUpdate();
|
||||
}, (err) => {
|
||||
this.setState({
|
||||
scalar_error: err
|
||||
});
|
||||
})
|
||||
}
|
||||
this.scalarClient = new ScalarAuthClient();
|
||||
this.scalarClient.connect().done(() => {
|
||||
this.forceUpdate();
|
||||
}, (err) => {
|
||||
this.setState({
|
||||
scalar_error: err
|
||||
});
|
||||
});
|
||||
|
||||
dis.dispatch({
|
||||
action: 'ui_opacity',
|
||||
|
@ -255,7 +252,7 @@ module.exports = React.createClass({
|
|||
return this.refs.url_preview_settings.saveSettings();
|
||||
},
|
||||
|
||||
saveEncryption: function () {
|
||||
saveEncryption: function() {
|
||||
if (!this.refs.encrypt) { return q(); }
|
||||
|
||||
var encrypt = this.refs.encrypt.checked;
|
||||
|
@ -407,7 +404,7 @@ module.exports = React.createClass({
|
|||
var cli = MatrixClientPeg.get();
|
||||
var roomState = this.props.room.currentState;
|
||||
return (roomState.mayClientSendStateEvent("m.room.join_rules", cli) &&
|
||||
roomState.mayClientSendStateEvent("m.room.guest_access", cli))
|
||||
roomState.mayClientSendStateEvent("m.room.guest_access", cli));
|
||||
},
|
||||
|
||||
onManageIntegrations(ev) {
|
||||
|
@ -462,11 +459,10 @@ module.exports = React.createClass({
|
|||
description: (
|
||||
<div>
|
||||
<p>End-to-end encryption is in beta and may not be reliable.</p>
|
||||
<p>You should <b>not</b> yet trust it to secure data. File transfers and calls are not yet encrypted.</p>
|
||||
<p>You should <b>not</b> yet trust it to secure data.</p>
|
||||
<p>Devices will <b>not</b> yet be able to decrypt history from before they joined the room.</p>
|
||||
<p>Once encryption is enabled for a room it <b>cannot</b> be turned off again (for now).</p>
|
||||
<p>Encrypted messages will not be visible on clients that do not yet implement encryption<br/>
|
||||
(e.g. Riot/iOS and Riot/Android).</p>
|
||||
<p>Encrypted messages will not be visible on clients that do not yet implement encryption.</p>
|
||||
</div>
|
||||
),
|
||||
onFinished: confirm=>{
|
||||
|
@ -478,10 +474,6 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_renderEncryptionSection: function() {
|
||||
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var cli = MatrixClientPeg.get();
|
||||
var roomState = this.props.room.currentState;
|
||||
var isEncrypted = cli.isRoomEncrypted(this.props.room.roomId);
|
||||
|
@ -518,7 +510,7 @@ module.exports = React.createClass({
|
|||
var UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
|
||||
var EditableText = sdk.getComponent('elements.EditableText');
|
||||
var PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||
var Loader = sdk.getComponent("elements.Spinner")
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
|
||||
var cli = MatrixClientPeg.get();
|
||||
var roomState = this.props.room.currentState;
|
||||
|
@ -565,7 +557,7 @@ module.exports = React.createClass({
|
|||
</div>;
|
||||
}
|
||||
else {
|
||||
userLevelsSection = <div>No users have specific privileges in this room.</div>
|
||||
userLevelsSection = <div>No users have specific privileges in this room.</div>;
|
||||
}
|
||||
|
||||
var banned = this.props.room.getMembersWithMembership("ban");
|
||||
|
@ -643,7 +635,7 @@ module.exports = React.createClass({
|
|||
</label>);
|
||||
})) : (self.state.tags && self.state.tags.join) ? self.state.tags.join(", ") : ""
|
||||
}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
// If there is no history_visibility, it is assumed to be 'shared'.
|
||||
|
@ -661,7 +653,7 @@ module.exports = React.createClass({
|
|||
addressWarning =
|
||||
<div className="mx_RoomSettings_warning">
|
||||
To link to a room it must have <a href="#addresses">an address</a>.
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
var inviteGuestWarning;
|
||||
|
@ -672,7 +664,7 @@ module.exports = React.createClass({
|
|||
this.setState({ join_rule: "invite", guest_access: "can_join" });
|
||||
e.preventDefault();
|
||||
}}>Click here to fix</a>.
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
var integrationsButton;
|
||||
|
@ -685,27 +677,26 @@ module.exports = React.createClass({
|
|||
</span>
|
||||
);
|
||||
}
|
||||
if (UserSettingsStore.isFeatureEnabled("integration_management")) {
|
||||
if (this.scalarClient.hasCredentials()) {
|
||||
integrationsButton = (
|
||||
|
||||
if (this.scalarClient.hasCredentials()) {
|
||||
integrationsButton = (
|
||||
<div className="mx_RoomSettings_integrationsButton" onClick={ this.onManageIntegrations }>
|
||||
Manage Integrations
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.scalar_error) {
|
||||
integrationsButton = (
|
||||
Manage Integrations
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.scalar_error) {
|
||||
integrationsButton = (
|
||||
<div className="mx_RoomSettings_integrationsButton_error" onClick={ this.onShowIntegrationsError }>
|
||||
Integrations Error <img src="img/warning.svg" width="17"/>
|
||||
{ integrationsError }
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
integrationsButton = (
|
||||
Integrations Error <img src="img/warning.svg" width="17"/>
|
||||
{ integrationsError }
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
integrationsButton = (
|
||||
<div className="mx_RoomSettings_integrationsButton" style={{ opacity: 0.5 }}>
|
||||
Manage Integrations
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Manage Integrations
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -796,7 +787,7 @@ module.exports = React.createClass({
|
|||
roomId={this.props.room.roomId}
|
||||
canSetCanonicalAlias={ roomState.mayClientSendStateEvent("m.room.canonical_alias", cli) }
|
||||
canSetAliases={
|
||||
true
|
||||
true
|
||||
/* Originally, we arbitrarily restricted creating aliases to room admins: roomState.mayClientSendStateEvent("m.room.aliases", cli) */
|
||||
}
|
||||
canonicalAliasEvent={this.props.room.currentState.getStateEvents('m.room.canonical_alias', '')}
|
||||
|
|
|
@ -26,6 +26,8 @@ var sdk = require('../../../index');
|
|||
var ContextualMenu = require('../../structures/ContextualMenu');
|
||||
var RoomNotifs = require('../../../RoomNotifs');
|
||||
var FormattingUtils = require('../../../utils/FormattingUtils');
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
var UserSettingsStore = require('../../../UserSettingsStore');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomTile',
|
||||
|
@ -176,7 +178,8 @@ module.exports = React.createClass({
|
|||
var self = this;
|
||||
ContextualMenu.createMenu(RoomTagMenu, {
|
||||
chevronOffset: 10,
|
||||
menuColour: "#FFFFFF",
|
||||
// XXX: fix horrid hardcoding
|
||||
menuColour: UserSettingsStore.getSyncedSettings().theme === 'dark' ? "#2d2d2d" : "#FFFFFF",
|
||||
left: x,
|
||||
top: y,
|
||||
room: this.props.room,
|
||||
|
@ -219,7 +222,7 @@ module.exports = React.createClass({
|
|||
var avatarContainerClasses = classNames({
|
||||
'mx_RoomTile_avatar_container': true,
|
||||
'mx_RoomTile_avatar_roomTagMenu': this.state.roomTagMenu,
|
||||
})
|
||||
});
|
||||
|
||||
var badgeClasses = classNames({
|
||||
'mx_RoomTile_badge': true,
|
||||
|
@ -286,8 +289,10 @@ module.exports = React.createClass({
|
|||
var connectDragSource = this.props.connectDragSource;
|
||||
var connectDropTarget = this.props.connectDropTarget;
|
||||
|
||||
|
||||
let ret = (
|
||||
<div className={classes} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
||||
<div> { /* Only native elements can be wrapped in a DnD object. */}
|
||||
<AccessibleButton className={classes} tabIndex="0" onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
||||
<div className={avatarClasses}>
|
||||
<div className="mx_RoomTile_avatar_menu" onClick={this.onAvatarClicked}>
|
||||
<div className={avatarContainerClasses}>
|
||||
|
@ -302,6 +307,7 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
{/* { incomingCallBox } */}
|
||||
{ tooltip }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -118,7 +118,7 @@ var SearchableEntityList = React.createClass({
|
|||
_createOverflowEntity: function(overflowCount, totalCount) {
|
||||
var EntityTile = sdk.getComponent("rooms.EntityTile");
|
||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
var text = "and " + overflowCount + " other" + (overflowCount > 1 ? "s" : "") + "...";
|
||||
var text = "and " + overflowCount + " other" + (overflowCount > 1 ? "s" : "") + "...";
|
||||
return (
|
||||
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||
<BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} />
|
||||
|
@ -135,8 +135,8 @@ var SearchableEntityList = React.createClass({
|
|||
<form onSubmit={this.onQuerySubmit} autoComplete="off">
|
||||
<input className="mx_SearchableEntityList_query" id="mx_SearchableEntityList_query" type="text"
|
||||
onChange={this.onQueryChanged} value={this.state.query}
|
||||
onFocus= {() => { this.setState({ focused: true }) }}
|
||||
onBlur= {() => { this.setState({ focused: false }) }}
|
||||
onFocus= {() => { this.setState({ focused: true }); }}
|
||||
onBlur= {() => { this.setState({ focused: false }); }}
|
||||
placeholder={this.props.searchPlaceholderText} />
|
||||
</form>
|
||||
);
|
||||
|
|
|
@ -16,15 +16,27 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var sdk = require('../../../index');
|
||||
var dis = require("../../../dispatcher");
|
||||
import React from 'react';
|
||||
import dis from '../../../dispatcher';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
// cancel button which is shared between room header and simple room header
|
||||
export function CancelButton(props) {
|
||||
const {onClick} = props;
|
||||
|
||||
return (
|
||||
<AccessibleButton className='mx_RoomHeader_cancelButton' onClick={onClick}>
|
||||
<img src="img/cancel.svg" className='mx_filterFlipColor'
|
||||
width="18" height="18" alt="Cancel"/>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* A stripped-down room header used for things like the user settings
|
||||
* and room directory.
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
export default React.createClass({
|
||||
displayName: 'SimpleRoomHeader',
|
||||
|
||||
propTypes: {
|
||||
|
@ -40,15 +52,15 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
var cancelButton;
|
||||
let cancelButton;
|
||||
if (this.props.onCancelClick) {
|
||||
cancelButton = <div className="mx_RoomHeader_cancelButton" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" alt="Cancel"/> </div>
|
||||
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
||||
}
|
||||
|
||||
var showRhsButton;
|
||||
let showRhsButton;
|
||||
/* // don't bother cluttering things up with this for now.
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
if (this.props.collapsedRhs) {
|
||||
showRhsButton =
|
||||
<div className="mx_RoomHeader_button" style={{ float: 'right' }} onClick={this.onShowRhsClick} title=">">
|
||||
|
@ -70,4 +82,3 @@ module.exports = React.createClass({
|
|||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ module.exports = React.createClass({
|
|||
var active = -1;
|
||||
|
||||
// FIXME: make presence data update whenever User.presence changes...
|
||||
active = user.lastActiveAgo ?
|
||||
active = user.lastActiveAgo ?
|
||||
(Date.now() - (user.lastPresenceTs - user.lastActiveAgo)) : -1;
|
||||
|
||||
var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
|
|
|
@ -49,7 +49,7 @@ module.exports = React.createClass({
|
|||
return {
|
||||
avatarUrl: this.props.initialAvatarUrl,
|
||||
phase: this.Phases.Display,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
|
@ -120,7 +120,7 @@ module.exports = React.createClass({
|
|||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
// XXX: FIXME: once we track in the JS what our own displayname is(!) then use it here rather than ?
|
||||
avatarImg = <BaseAvatar width={this.props.width} height={this.props.height} resizeMethod='crop'
|
||||
name='?' idName={ MatrixClientPeg.get().getUserIdLocalpart() } url={this.state.avatarUrl} />
|
||||
name='?' idName={ MatrixClientPeg.get().getUserIdLocalpart() } url={this.state.avatarUrl} />;
|
||||
}
|
||||
|
||||
var uploadSection;
|
||||
|
@ -130,7 +130,7 @@ module.exports = React.createClass({
|
|||
Upload new:
|
||||
<input type="file" onChange={this.onFileSelected}/>
|
||||
{this.state.errorText}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,9 @@ limitations under the License.
|
|||
|
||||
var React = require('react');
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var Modal = require("../../../Modal");
|
||||
var sdk = require("../../../index");
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ChangePassword',
|
||||
|
@ -59,32 +61,48 @@ module.exports = React.createClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
phase: this.Phases.Edit
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
changePassword: function(old_password, new_password) {
|
||||
var cli = MatrixClientPeg.get();
|
||||
|
||||
var authDict = {
|
||||
type: 'm.login.password',
|
||||
user: cli.credentials.userId,
|
||||
password: old_password
|
||||
};
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: "Warning",
|
||||
description:
|
||||
<div>
|
||||
Changing password will currently reset any end-to-end encryption keys on all devices,
|
||||
making encrypted chat history unreadable.
|
||||
This will be <a href="https://github.com/vector-im/riot-web/issues/2671">improved shortly</a>,
|
||||
but for now be warned.
|
||||
</div>,
|
||||
button: "Continue",
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
var authDict = {
|
||||
type: 'm.login.password',
|
||||
user: cli.credentials.userId,
|
||||
password: old_password
|
||||
};
|
||||
|
||||
this.setState({
|
||||
phase: this.Phases.Uploading
|
||||
this.setState({
|
||||
phase: this.Phases.Uploading
|
||||
});
|
||||
|
||||
var self = this;
|
||||
cli.setPassword(authDict, new_password).then(function() {
|
||||
self.props.onFinished();
|
||||
}, function(err) {
|
||||
self.props.onError(err);
|
||||
}).finally(function() {
|
||||
self.setState({
|
||||
phase: self.Phases.Edit
|
||||
});
|
||||
}).done();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
var self = this;
|
||||
cli.setPassword(authDict, new_password).then(function() {
|
||||
self.props.onFinished();
|
||||
}, function(err) {
|
||||
self.props.onError(err);
|
||||
}).finally(function() {
|
||||
self.setState({
|
||||
phase: self.Phases.Edit
|
||||
});
|
||||
}).done();
|
||||
},
|
||||
|
||||
onClickChange: function() {
|
||||
|
@ -105,7 +123,7 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
var rowClassName = this.props.rowClassName;
|
||||
var rowLabelClassName = this.props.rowLabelClassName;
|
||||
var rowInputClassName = this.props.rowInputClassName
|
||||
var rowInputClassName = this.props.rowInputClassName;
|
||||
var buttonClassName = this.props.buttonClassName;
|
||||
|
||||
switch (this.state.phase) {
|
||||
|
@ -136,9 +154,10 @@ module.exports = React.createClass({
|
|||
<input id="password2" type="password" ref="confirm_input" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={buttonClassName} onClick={this.onClickChange}>
|
||||
<AccessibleButton className={buttonClassName}
|
||||
onClick={this.onClickChange}>
|
||||
Change Password
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
case this.Phases.Uploading:
|
||||
|
|
|
@ -88,7 +88,7 @@ export default class DevicesPanel extends React.Component {
|
|||
const removed_id = device.device_id;
|
||||
this.setState((state, props) => {
|
||||
const newDevices = state.devices.filter(
|
||||
d => { return d.device_id != removed_id }
|
||||
d => { return d.device_id != removed_id; }
|
||||
);
|
||||
return { devices: newDevices };
|
||||
});
|
||||
|
@ -98,7 +98,7 @@ export default class DevicesPanel extends React.Component {
|
|||
var DevicesPanelEntry = sdk.getComponent('settings.DevicesPanelEntry');
|
||||
return (
|
||||
<DevicesPanelEntry key={device.device_id} device={device}
|
||||
onDeleted={()=>{this._onDeviceDeleted(device)}} />
|
||||
onDeleted={()=>{this._onDeviceDeleted(device);}} />
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -15,12 +15,9 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import q from 'q';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import DateUtils from '../../../DateUtils';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
export default class DevicesPanelEntry extends React.Component {
|
||||
|
@ -61,7 +58,7 @@ export default class DevicesPanelEntry extends React.Component {
|
|||
if (this._unmounted) { return; }
|
||||
if (error.httpStatus !== 401 || !error.data || !error.data.flows) {
|
||||
// doesn't look like an interactive-auth failure
|
||||
throw e;
|
||||
throw error;
|
||||
}
|
||||
|
||||
// pop up an interactive auth dialog
|
||||
|
@ -121,7 +118,7 @@ export default class DevicesPanelEntry extends React.Component {
|
|||
|
||||
let deleteButton;
|
||||
if (this.state.deleteError) {
|
||||
deleteButton = <div className="error">{this.state.deleteError}</div>
|
||||
deleteButton = <div className="error">{this.state.deleteError}</div>;
|
||||
} else {
|
||||
deleteButton = (
|
||||
<div className="mx_textButton"
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
'use strict';
|
||||
var React = require("react");
|
||||
var Notifier = require("../../../Notifier");
|
||||
var sdk = require('../../../index');
|
||||
var dis = require("../../../dispatcher");
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
|
|
@ -124,7 +124,7 @@ module.exports = React.createClass({
|
|||
return this.refs.video;
|
||||
},
|
||||
|
||||
render: function(){
|
||||
render: function() {
|
||||
var VideoView = sdk.getComponent('voip.VideoView');
|
||||
|
||||
var voice;
|
||||
|
|
|
@ -114,7 +114,7 @@ module.exports = React.createClass({
|
|||
<VideoFeed ref="remote" onResize={this.props.onResize}
|
||||
maxHeight={maxVideoHeight} />
|
||||
</div>
|
||||
<div className="mx_VideoView_localVideoFeed">
|
||||
<div className="mx_VideoView_localVideoFeed">
|
||||
<VideoFeed ref="local"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue