Merge branch 'develop' into dbkr/scalar
This commit is contained in:
commit
94aec10512
23 changed files with 1578 additions and 460 deletions
|
@ -38,11 +38,13 @@ var MatrixTools = require('../../MatrixTools');
|
|||
var linkifyMatrix = require("../../linkify-matrix");
|
||||
var KeyCode = require('../../KeyCode');
|
||||
|
||||
var createRoom = require("../../createRoom");
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MatrixChat',
|
||||
|
||||
propTypes: {
|
||||
config: React.PropTypes.object.isRequired,
|
||||
config: React.PropTypes.object,
|
||||
ConferenceHandler: React.PropTypes.any,
|
||||
onNewScreen: React.PropTypes.func,
|
||||
registrationUrl: React.PropTypes.string,
|
||||
|
@ -63,6 +65,13 @@ module.exports = React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
var s = {
|
||||
// If we are viewing a room by alias, this contains the alias
|
||||
currentRoomAlias: null,
|
||||
|
||||
// The ID of the room we're viewing. This is either populated directly
|
||||
// in the case where we view a room by ID or by RoomView when it resolves
|
||||
// what ID an alias points at.
|
||||
currentRoomId: null,
|
||||
logged_in: !!(MatrixClientPeg.get() && MatrixClientPeg.get().credentials),
|
||||
collapse_lhs: false,
|
||||
collapse_rhs: false,
|
||||
|
@ -85,7 +94,8 @@ module.exports = React.createClass({
|
|||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
startingQueryParams: {}
|
||||
startingQueryParams: {},
|
||||
config: {},
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -98,10 +108,9 @@ module.exports = React.createClass({
|
|||
else if (window.localStorage && window.localStorage.getItem("mx_hs_url")) {
|
||||
return window.localStorage.getItem("mx_hs_url");
|
||||
}
|
||||
else if (this.props.config) {
|
||||
return this.props.config.default_hs_url
|
||||
else {
|
||||
return this.props.config.default_hs_url || "https://matrix.org";
|
||||
}
|
||||
return "https://matrix.org";
|
||||
},
|
||||
|
||||
getFallbackHsUrl: function() {
|
||||
|
@ -117,10 +126,9 @@ module.exports = React.createClass({
|
|||
else if (window.localStorage && window.localStorage.getItem("mx_is_url")) {
|
||||
return window.localStorage.getItem("mx_is_url");
|
||||
}
|
||||
else if (this.props.config) {
|
||||
return this.props.config.default_is_url
|
||||
else {
|
||||
return this.props.config.default_is_url || "https://vector.im"
|
||||
}
|
||||
return "https://matrix.org";
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
|
@ -393,6 +401,10 @@ module.exports = React.createClass({
|
|||
});
|
||||
break;
|
||||
case 'view_room':
|
||||
// Takes either a room ID or room alias: if switching to a room the client is already
|
||||
// known to be in (eg. user clicks on a room in the recents panel), supply the ID
|
||||
// If the user is clicking on a room in the context of the alias being presented
|
||||
// to them, supply the room alias. If both are supplied, the room ID will be ignored.
|
||||
this._viewRoom(
|
||||
payload.room_id, payload.room_alias, payload.show_settings, payload.event_id,
|
||||
payload.third_party_invite, payload.oob_data
|
||||
|
@ -406,7 +418,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
var roomIndex = -1;
|
||||
for (var i = 0; i < allRooms.length; ++i) {
|
||||
if (allRooms[i].roomId == this.state.currentRoom) {
|
||||
if (allRooms[i].roomId == this.state.currentRoomId) {
|
||||
roomIndex = i;
|
||||
break;
|
||||
}
|
||||
|
@ -424,42 +436,6 @@ module.exports = React.createClass({
|
|||
this._viewRoom(allRooms[roomIndex].roomId);
|
||||
}
|
||||
break;
|
||||
case 'view_room_alias':
|
||||
if (!this.state.logged_in) {
|
||||
this.starting_room_alias_payload = payload;
|
||||
// Login is the default screen, so we'd do this anyway,
|
||||
// but this will set the URL bar appropriately.
|
||||
dis.dispatch({ action: 'start_login' });
|
||||
return;
|
||||
}
|
||||
|
||||
var foundRoom = MatrixTools.getRoomForAlias(
|
||||
MatrixClientPeg.get().getRooms(), payload.room_alias
|
||||
);
|
||||
if (foundRoom) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: foundRoom.roomId,
|
||||
room_alias: payload.room_alias,
|
||||
event_id: payload.event_id,
|
||||
third_party_invite: payload.third_party_invite,
|
||||
oob_data: payload.oob_data,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// resolve the alias and *then* view it
|
||||
MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias).done(
|
||||
function(result) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: result.room_id,
|
||||
room_alias: payload.room_alias,
|
||||
event_id: payload.event_id,
|
||||
third_party_invite: payload.third_party_invite,
|
||||
oob_data: payload.oob_data,
|
||||
});
|
||||
});
|
||||
break;
|
||||
case 'view_user_settings':
|
||||
this._setPage(this.PageTypes.UserSettings);
|
||||
this.notifyNewScreen('settings');
|
||||
|
@ -468,49 +444,7 @@ module.exports = React.createClass({
|
|||
//this._setPage(this.PageTypes.CreateRoom);
|
||||
//this.notifyNewScreen('new');
|
||||
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
var modal = Modal.createDialog(Loader);
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
Modal.createDialog(NeedToRegisterDialog, {
|
||||
title: "Please Register",
|
||||
description: "Guest users can't create new rooms. Please register to create room and start a chat."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// XXX: FIXME: deduplicate this with MemberInfo's 'start chat' impl
|
||||
MatrixClientPeg.get().createRoom({
|
||||
preset: "private_chat",
|
||||
// Allow guests by default since the room is private and they'd
|
||||
// need an invite. This means clicking on a 3pid invite email can
|
||||
// actually drop you right in to a chat.
|
||||
initial_state: [
|
||||
{
|
||||
content: {
|
||||
guest_access: 'can_join'
|
||||
},
|
||||
type: 'm.room.guest_access',
|
||||
state_key: '',
|
||||
visibility: 'private',
|
||||
}
|
||||
],
|
||||
}).done(function(res) {
|
||||
modal.close();
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: res.room_id,
|
||||
// show_settings: true,
|
||||
});
|
||||
}, function(err) {
|
||||
modal.close();
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to create room",
|
||||
description: err.toString()
|
||||
});
|
||||
});
|
||||
createRoom().done();
|
||||
break;
|
||||
case 'view_room_directory':
|
||||
this._setPage(this.PageTypes.RoomDirectory);
|
||||
|
@ -575,16 +509,19 @@ module.exports = React.createClass({
|
|||
this.focusComposer = true;
|
||||
|
||||
var newState = {
|
||||
currentRoom: roomId,
|
||||
currentRoomAlias: roomAlias,
|
||||
initialEventId: eventId,
|
||||
highlightedEventId: eventId,
|
||||
initialEventPixelOffset: undefined,
|
||||
page_type: this.PageTypes.RoomView,
|
||||
thirdPartyInvite: thirdPartyInvite,
|
||||
roomOobData: oob_data,
|
||||
currentRoomAlias: roomAlias,
|
||||
};
|
||||
|
||||
if (!roomAlias) {
|
||||
newState.currentRoomId = roomId;
|
||||
}
|
||||
|
||||
// if we aren't given an explicit event id, look for one in the
|
||||
// scrollStateMap.
|
||||
if (!eventId) {
|
||||
|
@ -604,7 +541,7 @@ module.exports = React.createClass({
|
|||
var presentedId = roomAlias || roomId;
|
||||
var room = MatrixClientPeg.get().getRoom(roomId);
|
||||
if (room) {
|
||||
var theAlias = MatrixTools.getCanonicalAliasForRoom(room);
|
||||
var theAlias = MatrixTools.getDisplayAliasForRoom(room);
|
||||
if (theAlias) presentedId = theAlias;
|
||||
|
||||
// No need to do this given RoomView triggers it itself...
|
||||
|
@ -677,13 +614,13 @@ module.exports = React.createClass({
|
|||
dis.dispatch(self.starting_room_alias_payload);
|
||||
delete self.starting_room_alias_payload;
|
||||
} else if (!self.state.page_type) {
|
||||
if (!self.state.currentRoom) {
|
||||
if (!self.state.currentRoomId) {
|
||||
var firstRoom = null;
|
||||
if (cli.getRooms() && cli.getRooms().length) {
|
||||
firstRoom = RoomListSorter.mostRecentActivityFirst(
|
||||
cli.getRooms()
|
||||
)[0].roomId;
|
||||
self.setState({ready: true, currentRoom: firstRoom, page_type: self.PageTypes.RoomView});
|
||||
self.setState({ready: true, currentRoomId: firstRoom, page_type: self.PageTypes.RoomView});
|
||||
} else {
|
||||
self.setState({ready: true, page_type: self.PageTypes.RoomDirectory});
|
||||
}
|
||||
|
@ -693,10 +630,10 @@ module.exports = React.createClass({
|
|||
|
||||
// we notifyNewScreen now because now the room will actually be displayed,
|
||||
// and (mostly) now we can get the correct alias.
|
||||
var presentedId = self.state.currentRoom;
|
||||
var room = MatrixClientPeg.get().getRoom(self.state.currentRoom);
|
||||
var presentedId = self.state.currentRoomId;
|
||||
var room = MatrixClientPeg.get().getRoom(self.state.currentRoomId);
|
||||
if (room) {
|
||||
var theAlias = MatrixTools.getCanonicalAliasForRoom(room);
|
||||
var theAlias = MatrixTools.getDisplayAliasForRoom(room);
|
||||
if (theAlias) presentedId = theAlias;
|
||||
}
|
||||
|
||||
|
@ -861,22 +798,28 @@ module.exports = React.createClass({
|
|||
inviterName: params.inviter_name,
|
||||
};
|
||||
|
||||
var payload = {
|
||||
action: 'view_room',
|
||||
event_id: eventId,
|
||||
third_party_invite: third_party_invite,
|
||||
oob_data: oob_data,
|
||||
};
|
||||
if (roomString[0] == '#') {
|
||||
dis.dispatch({
|
||||
action: 'view_room_alias',
|
||||
room_alias: roomString,
|
||||
event_id: eventId,
|
||||
third_party_invite: third_party_invite,
|
||||
oob_data: oob_data,
|
||||
});
|
||||
payload.room_alias = roomString;
|
||||
} else {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomString,
|
||||
event_id: eventId,
|
||||
third_party_invite: third_party_invite,
|
||||
oob_data: oob_data,
|
||||
});
|
||||
payload.room_id = roomString;
|
||||
}
|
||||
|
||||
// we can't view a room unless we're logged in
|
||||
// (a guest account is fine)
|
||||
if (!this.state.logged_in) {
|
||||
this.starting_room_alias_payload = payload;
|
||||
// Login is the default screen, so we'd do this anyway,
|
||||
// but this will set the URL bar appropriately.
|
||||
dis.dispatch({ action: 'start_login' });
|
||||
return;
|
||||
} else {
|
||||
dis.dispatch(payload);
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
@ -892,7 +835,7 @@ module.exports = React.createClass({
|
|||
|
||||
onAliasClick: function(event, alias) {
|
||||
event.preventDefault();
|
||||
dis.dispatch({action: 'view_room_alias', room_alias: alias});
|
||||
dis.dispatch({action: 'view_room', room_alias: alias});
|
||||
},
|
||||
|
||||
onUserClick: function(event, userId) {
|
||||
|
@ -1038,10 +981,10 @@ module.exports = React.createClass({
|
|||
onUserSettingsClose: function() {
|
||||
// XXX: use browser history instead to find the previous room?
|
||||
// or maintain a this.state.pageHistory in _setPage()?
|
||||
if (this.state.currentRoom) {
|
||||
if (this.state.currentRoomId) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.state.currentRoom,
|
||||
room_id: this.state.currentRoomId,
|
||||
});
|
||||
}
|
||||
else {
|
||||
|
@ -1051,6 +994,13 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onRoomIdResolved: function(room_id) {
|
||||
// It's the RoomView's resposibility to look up room aliases, but we need the
|
||||
// ID to pass into things like the Member List, so the Room View tells us when
|
||||
// its done that resolution so we can display things that take a room ID.
|
||||
this.setState({currentRoomId: room_id});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var LeftPanel = sdk.getComponent('structures.LeftPanel');
|
||||
var RoomView = sdk.getComponent('structures.RoomView');
|
||||
|
@ -1081,20 +1031,21 @@ module.exports = React.createClass({
|
|||
page_element = (
|
||||
<RoomView
|
||||
ref="roomView"
|
||||
roomAddress={this.state.currentRoom || this.state.currentRoomAlias}
|
||||
roomAddress={this.state.currentRoomAlias || this.state.currentRoomId}
|
||||
onRoomIdResolved={this.onRoomIdResolved}
|
||||
eventId={this.state.initialEventId}
|
||||
thirdPartyInvite={this.state.thirdPartyInvite}
|
||||
oobData={this.state.roomOobData}
|
||||
highlightedEventId={this.state.highlightedEventId}
|
||||
eventPixelOffset={this.state.initialEventPixelOffset}
|
||||
key={this.state.currentRoom}
|
||||
key={this.state.currentRoomAlias || this.state.currentRoomId}
|
||||
opacity={this.state.middleOpacity}
|
||||
ConferenceHandler={this.props.ConferenceHandler} />
|
||||
);
|
||||
right_panel = <RightPanel roomId={this.state.currentRoom} collapsed={this.state.collapse_rhs} opacity={this.state.sideOpacity} />
|
||||
right_panel = <RightPanel roomId={this.state.currentRoomId} collapsed={this.state.collapse_rhs} opacity={this.state.sideOpacity} />
|
||||
break;
|
||||
case this.PageTypes.UserSettings:
|
||||
page_element = <UserSettings onClose={this.onUserSettingsClose} version={this.state.version} />
|
||||
page_element = <UserSettings onClose={this.onUserSettingsClose} version={this.state.version} brand={this.props.config.brand} />
|
||||
right_panel = <RightPanel collapsed={this.state.collapse_rhs} opacity={this.state.sideOpacity}/>
|
||||
break;
|
||||
case this.PageTypes.CreateRoom:
|
||||
|
@ -1127,7 +1078,7 @@ module.exports = React.createClass({
|
|||
<div className="mx_MatrixChat_wrapper">
|
||||
{topBar}
|
||||
<div className={bodyClasses}>
|
||||
<LeftPanel selectedRoom={this.state.currentRoom} collapsed={this.state.collapse_lhs} opacity={this.state.sideOpacity}/>
|
||||
<LeftPanel selectedRoom={this.state.currentRoomId} collapsed={this.state.collapse_lhs} opacity={this.state.sideOpacity}/>
|
||||
<main className="mx_MatrixChat_middlePanel">
|
||||
{page_element}
|
||||
</main>
|
||||
|
|
|
@ -86,6 +86,10 @@ module.exports = React.createClass({
|
|||
// to manage its animations
|
||||
this._readReceiptMap = {};
|
||||
|
||||
// Remember the read marker ghost node so we can do the cleanup that
|
||||
// Velocity requires
|
||||
this._readMarkerGhostNode = null;
|
||||
|
||||
this._isMounted = true;
|
||||
},
|
||||
|
||||
|
@ -422,9 +426,16 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_startAnimation: function(ghostNode) {
|
||||
Velocity(ghostNode, {opacity: '0', width: '10%'},
|
||||
{duration: 400, easing: 'easeInSine',
|
||||
delay: 1000});
|
||||
if (this._readMarkerGhostNode) {
|
||||
Velocity.Utilities.removeData(this._readMarkerGhostNode);
|
||||
}
|
||||
this._readMarkerGhostNode = ghostNode;
|
||||
|
||||
if (ghostNode) {
|
||||
Velocity(ghostNode, {opacity: '0', width: '10%'},
|
||||
{duration: 400, easing: 'easeInSine',
|
||||
delay: 1000});
|
||||
}
|
||||
},
|
||||
|
||||
_getReadMarkerGhostTile: function() {
|
||||
|
|
|
@ -39,6 +39,7 @@ var dis = require("../../dispatcher");
|
|||
var Tinter = require("../../Tinter");
|
||||
var rate_limited_func = require('../../ratelimitedfunc');
|
||||
var ObjectUtils = require('../../ObjectUtils');
|
||||
var MatrixTools = require('../../MatrixTools');
|
||||
|
||||
var DEBUG = false;
|
||||
|
||||
|
@ -54,16 +55,17 @@ module.exports = React.createClass({
|
|||
propTypes: {
|
||||
ConferenceHandler: React.PropTypes.any,
|
||||
|
||||
// the ID for this room (or, if we don't know it, an alias for it)
|
||||
//
|
||||
// XXX: if this is an alias, we will display a 'join' dialogue,
|
||||
// regardless of whether we are already a member, or if the room is
|
||||
// peekable. Currently there is a big mess, where at least four
|
||||
// different components (RoomView, MatrixChat, RoomDirectory,
|
||||
// SlashCommands) have logic for turning aliases into rooms, and each
|
||||
// of them do it differently and have different edge cases.
|
||||
// Either a room ID or room alias for the room to display.
|
||||
// If the room is being displayed as a result of the user clicking
|
||||
// on a room alias, the alias should be supplied. Otherwise, a room
|
||||
// ID should be supplied.
|
||||
roomAddress: React.PropTypes.string.isRequired,
|
||||
|
||||
// If a room alias is passed to roomAddress, a function can be
|
||||
// provided here that will be called with the ID of the room
|
||||
// once it has been resolved.
|
||||
onRoomIdResolved: React.PropTypes.func,
|
||||
|
||||
// An object representing a third party invite to join this room
|
||||
// Fields:
|
||||
// * inviteSignUrl (string) The URL used to join this room from an email invite
|
||||
|
@ -100,17 +102,17 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getInitialState: function() {
|
||||
var room = MatrixClientPeg.get().getRoom(this.props.roomAddress);
|
||||
return {
|
||||
room: room,
|
||||
roomLoading: !room,
|
||||
room: null,
|
||||
roomId: null,
|
||||
roomLoading: true,
|
||||
editingRoomSettings: false,
|
||||
uploadingRoomSettings: false,
|
||||
numUnreadMessages: 0,
|
||||
draggingFile: false,
|
||||
searching: false,
|
||||
searchResults: null,
|
||||
hasUnsentMessages: this._hasUnsentMessages(room),
|
||||
hasUnsentMessages: false,
|
||||
callState: null,
|
||||
guestsCanJoin: false,
|
||||
canPeek: false,
|
||||
|
@ -142,6 +144,39 @@ module.exports = React.createClass({
|
|||
}
|
||||
});
|
||||
|
||||
if (this.props.roomAddress[0] == '#') {
|
||||
// we always look up the alias from the directory server:
|
||||
// we want the room that the given alias is pointing to
|
||||
// right now. We may have joined that alias before but there's
|
||||
// no guarantee the alias hasn't subsequently been remapped.
|
||||
MatrixClientPeg.get().getRoomIdForAlias(this.props.roomAddress).done((result) => {
|
||||
if (this.props.onRoomIdResolved) {
|
||||
this.props.onRoomIdResolved(result.room_id);
|
||||
}
|
||||
var room = MatrixClientPeg.get().getRoom(result.room_id);
|
||||
this.setState({
|
||||
room: room,
|
||||
roomId: result.room_id,
|
||||
roomLoading: !room,
|
||||
hasUnsentMessages: this._hasUnsentMessages(room),
|
||||
}, this._updatePeeking);
|
||||
}, (err) => {
|
||||
this.setState({
|
||||
roomLoading: false,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
var room = MatrixClientPeg.get().getRoom(this.props.roomAddress);
|
||||
this.setState({
|
||||
roomId: this.props.roomAddress,
|
||||
room: room,
|
||||
roomLoading: !room,
|
||||
hasUnsentMessages: this._hasUnsentMessages(room),
|
||||
}, this._updatePeeking);
|
||||
}
|
||||
},
|
||||
|
||||
_updatePeeking: function() {
|
||||
// if this is an unknown room then we're in one of three states:
|
||||
// - This is a room we can peek into (search engine) (we can /peek)
|
||||
// - This is a room we can publicly join or were invited to. (we can /join)
|
||||
|
@ -149,10 +184,13 @@ module.exports = React.createClass({
|
|||
// We can't try to /join because this may implicitly accept invites (!)
|
||||
// We can /peek though. If it fails then we present the join UI. If it
|
||||
// succeeds then great, show the preview (but we still may be able to /join!).
|
||||
if (!this.state.room) {
|
||||
console.log("Attempting to peek into room %s", this.props.roomAddress);
|
||||
// Note that peeking works by room ID and room ID only, as opposed to joining
|
||||
// which must be by alias or invite wherever possible (peeking currently does
|
||||
// not work over federation).
|
||||
if (!this.state.room && this.state.roomId) {
|
||||
console.log("Attempting to peek into room %s", this.state.roomId);
|
||||
|
||||
MatrixClientPeg.get().peekInRoom(this.props.roomAddress).then((room) => {
|
||||
MatrixClientPeg.get().peekInRoom(this.state.roomId).then((room) => {
|
||||
this.setState({
|
||||
room: room,
|
||||
roomLoading: false,
|
||||
|
@ -171,7 +209,7 @@ module.exports = React.createClass({
|
|||
throw err;
|
||||
}
|
||||
}).done();
|
||||
} else {
|
||||
} else if (this.state.room) {
|
||||
MatrixClientPeg.get().stopPeeking();
|
||||
this._onRoomLoaded(this.state.room);
|
||||
}
|
||||
|
|
|
@ -20,18 +20,36 @@ var MatrixClientPeg = require("../../MatrixClientPeg");
|
|||
var Modal = require('../../Modal');
|
||||
var dis = require("../../dispatcher");
|
||||
var q = require('q');
|
||||
var version = require('../../../package.json').version;
|
||||
var package_json = require('../../../package.json');
|
||||
var UserSettingsStore = require('../../UserSettingsStore');
|
||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
var Email = require('../../email');
|
||||
var AddThreepid = require('../../AddThreepid');
|
||||
|
||||
const LABS_FEATURES = [
|
||||
{
|
||||
name: 'Rich Text Editor',
|
||||
id: 'rich_text_editor'
|
||||
},
|
||||
{
|
||||
name: 'End-to-End Encryption',
|
||||
id: 'e2e_encryption'
|
||||
}
|
||||
];
|
||||
|
||||
// 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>";
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'UserSettings',
|
||||
|
||||
propTypes: {
|
||||
version: React.PropTypes.string,
|
||||
onClose: React.PropTypes.func
|
||||
onClose: React.PropTypes.func,
|
||||
// The brand string given when creating email pushers
|
||||
brand: React.PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -44,7 +62,6 @@ module.exports = React.createClass({
|
|||
return {
|
||||
avatarUrl: null,
|
||||
threePids: [],
|
||||
clientVersion: version,
|
||||
phase: "UserSettings.LOADING", // LOADING, DISPLAY
|
||||
email_add_pending: false,
|
||||
};
|
||||
|
@ -244,6 +261,27 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_renderDeviceInfo: function() {
|
||||
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var client = MatrixClientPeg.get();
|
||||
var deviceId = client.deviceId;
|
||||
var olmKey = client.getDeviceEd25519Key() || "<not supported>";
|
||||
return (
|
||||
<div>
|
||||
<h3>Cryptography</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
<ul>
|
||||
<li>Device ID: {deviceId}</li>
|
||||
<li>Device key: {olmKey}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
|
@ -333,11 +371,35 @@ module.exports = React.createClass({
|
|||
<h3>Notifications</h3>
|
||||
|
||||
<div className="mx_UserSettings_section">
|
||||
<Notifications threepids={this.state.threepids} />
|
||||
<Notifications threepids={this.state.threepids} brand={this.props.brand} />
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
this._renderLabs = function () {
|
||||
let features = LABS_FEATURES.map(feature => (
|
||||
<div key={feature.id}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={feature.id}
|
||||
name={feature.id}
|
||||
defaultChecked={UserSettingsStore.isFeatureEnabled(feature.id)}
|
||||
onChange={e => UserSettingsStore.setFeatureEnabled(feature.id, e.target.checked)} />
|
||||
<label htmlFor={feature.id}>{feature.name}</label>
|
||||
</div>
|
||||
));
|
||||
return (
|
||||
<div>
|
||||
<h3>Labs</h3>
|
||||
|
||||
<div className="mx_UserSettings_section">
|
||||
<p>These are experimental features that may break in unexpected ways. Use with caution.</p>
|
||||
{features}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx_UserSettings">
|
||||
<SimpleRoomHeader title="Settings" onCancelClick={ this.props.onClose }/>
|
||||
|
@ -390,6 +452,10 @@ module.exports = React.createClass({
|
|||
|
||||
{notification_area}
|
||||
|
||||
{this._renderDeviceInfo()}
|
||||
|
||||
{this._renderLabs()}
|
||||
|
||||
<h3>Advanced</h3>
|
||||
|
||||
<div className="mx_UserSettings_section">
|
||||
|
@ -403,7 +469,7 @@ module.exports = React.createClass({
|
|||
Identity Server is { MatrixClientPeg.get().getIdentityServerUrl() }
|
||||
</div>
|
||||
<div className="mx_UserSettings_advanced">
|
||||
matrix-react-sdk version: {this.state.clientVersion}<br/>
|
||||
matrix-react-sdk version: {REACT_SDK_VERSION}<br/>
|
||||
vector-web version: {this.props.version}<br/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -17,8 +17,8 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var Velocity = require('velocity-animate');
|
||||
require('velocity-ui-pack');
|
||||
var Velocity = require('velocity-vector');
|
||||
require('velocity-vector/velocity.ui');
|
||||
var sdk = require('../../../index');
|
||||
var Email = require('../../../email');
|
||||
var Modal = require("../../../Modal");
|
||||
|
|
|
@ -128,16 +128,24 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {menu: false, allReadAvatars: false};
|
||||
return {menu: false, allReadAvatars: false, verified: null};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// don't do RR animations until we are mounted
|
||||
this._suppressReadReceiptAnimation = true;
|
||||
this._verifyEvent(this.props.mxEvent);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._suppressReadReceiptAnimation = false;
|
||||
MatrixClientPeg.get().on("deviceVerified", this.onDeviceVerified);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function (nextProps) {
|
||||
if (nextProps.mxEvent !== this.props.mxEvent) {
|
||||
this._verifyEvent(nextProps.mxEvent);
|
||||
}
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function (nextProps, nextState) {
|
||||
|
@ -152,6 +160,31 @@ module.exports = React.createClass({
|
|||
return false;
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
var client = MatrixClientPeg.get();
|
||||
if (client) {
|
||||
client.removeListener("deviceVerified", this.onDeviceVerified);
|
||||
}
|
||||
},
|
||||
|
||||
onDeviceVerified: function(userId, device) {
|
||||
if (userId == this.props.mxEvent.getSender()) {
|
||||
this._verifyEvent(this.props.mxEvent);
|
||||
}
|
||||
},
|
||||
|
||||
_verifyEvent: function(mxEvent) {
|
||||
var verified = null;
|
||||
|
||||
if (mxEvent.isEncrypted()) {
|
||||
verified = MatrixClientPeg.get().isEventSenderVerified(mxEvent);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
verified: verified
|
||||
});
|
||||
},
|
||||
|
||||
_propsEqual: function(objA, objB) {
|
||||
var keysA = Object.keys(objA);
|
||||
var keysB = Object.keys(objB);
|
||||
|
@ -346,6 +379,8 @@ module.exports = React.createClass({
|
|||
mx_EventTile_last: this.props.last,
|
||||
mx_EventTile_contextual: this.props.contextual,
|
||||
menu: this.state.menu,
|
||||
mx_EventTile_verified: this.state.verified == true,
|
||||
mx_EventTile_unverified: this.state.verified == false,
|
||||
});
|
||||
var timestamp = <a href={ "#/room/" + this.props.mxEvent.getRoomId() +"/"+ this.props.mxEvent.getId() }>
|
||||
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
|
||||
|
|
68
src/components/views/rooms/MemberDeviceInfo.js
Normal file
68
src/components/views/rooms/MemberDeviceInfo.js
Normal file
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
var React = require('react');
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MemberDeviceInfo',
|
||||
propTypes: {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
device: React.PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
onVerifyClick: function() {
|
||||
MatrixClientPeg.get().setDeviceVerified(
|
||||
this.props.userId, this.props.device.id, true
|
||||
);
|
||||
},
|
||||
|
||||
onUnverifyClick: function() {
|
||||
MatrixClientPeg.get().setDeviceVerified(
|
||||
this.props.userId, this.props.device.id, false
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var indicator = null, button = null;
|
||||
if (this.props.device.verified) {
|
||||
indicator = (
|
||||
<div className="mx_MemberDeviceInfo_verified">✔</div>
|
||||
);
|
||||
button = (
|
||||
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
|
||||
onClick={this.onUnverifyClick}>
|
||||
Unverify
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
button = (
|
||||
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify"
|
||||
onClick={this.onVerifyClick}>
|
||||
Verify
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="mx_MemberDeviceInfo">
|
||||
<div className="mx_MemberDeviceInfo_deviceId">{this.props.device.id}</div>
|
||||
<div className="mx_MemberDeviceInfo_deviceKey">{this.props.device.key}</div>
|
||||
{indicator}
|
||||
{button}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -30,27 +30,107 @@ var MatrixClientPeg = require("../../../MatrixClientPeg");
|
|||
var dis = require("../../../dispatcher");
|
||||
var Modal = require("../../../Modal");
|
||||
var sdk = require('../../../index');
|
||||
var UserSettingsStore = require('../../../UserSettingsStore');
|
||||
var createRoom = require('../../../createRoom');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MemberInfo',
|
||||
|
||||
propTypes: {
|
||||
member: React.PropTypes.object.isRequired,
|
||||
onFinished: React.PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onFinished: function() {}
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
// work out the current state
|
||||
if (this.props.member) {
|
||||
var memberState = this._calculateOpsPermissions(this.props.member);
|
||||
this.setState(memberState);
|
||||
getInitialState: function() {
|
||||
return {
|
||||
can: {
|
||||
kick: false,
|
||||
ban: false,
|
||||
mute: false,
|
||||
modifyLevel: false
|
||||
},
|
||||
muted: false,
|
||||
isTargetMod: false,
|
||||
updating: 0,
|
||||
devicesLoading: true,
|
||||
devices: null,
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
componentWillMount: function() {
|
||||
this._cancelDeviceList = null;
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._updateStateForNewMember(this.props.member);
|
||||
MatrixClientPeg.get().on("deviceVerified", this.onDeviceVerified);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
var memberState = this._calculateOpsPermissions(newProps.member);
|
||||
this.setState(memberState);
|
||||
if (this.props.member.userId != newProps.member.userId) {
|
||||
this._updateStateForNewMember(newProps.member);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
var client = MatrixClientPeg.get();
|
||||
if (client) {
|
||||
client.removeListener("deviceVerified", this.onDeviceVerified);
|
||||
}
|
||||
if (this._cancelDeviceList) {
|
||||
this._cancelDeviceList();
|
||||
}
|
||||
},
|
||||
|
||||
onDeviceVerified: function(userId, device) {
|
||||
if (userId == this.props.member.userId) {
|
||||
// no need to re-download the whole thing; just update our copy of
|
||||
// the list.
|
||||
var devices = MatrixClientPeg.get().listDeviceKeys(userId);
|
||||
this.setState({devices: devices});
|
||||
}
|
||||
},
|
||||
|
||||
_updateStateForNewMember: function(member) {
|
||||
var newState = this._calculateOpsPermissions(member);
|
||||
newState.devicesLoading = true;
|
||||
newState.devices = null;
|
||||
this.setState(newState);
|
||||
|
||||
if (this._cancelDeviceList) {
|
||||
this._cancelDeviceList();
|
||||
this._cancelDeviceList = null;
|
||||
}
|
||||
|
||||
this._downloadDeviceList(member);
|
||||
},
|
||||
|
||||
_downloadDeviceList: function(member) {
|
||||
var cancelled = false;
|
||||
this._cancelDeviceList = function() { cancelled = true; }
|
||||
|
||||
var client = MatrixClientPeg.get();
|
||||
var self = this;
|
||||
client.downloadKeys([member.userId], true).finally(function() {
|
||||
self._cancelDeviceList = null;
|
||||
}).done(function() {
|
||||
if (cancelled) {
|
||||
// we got cancelled - presumably a different user now
|
||||
return;
|
||||
}
|
||||
var devices = client.listDeviceKeys(member.userId);
|
||||
self.setState({devicesLoading: false, devices: devices});
|
||||
}, function(err) {
|
||||
console.log("Error downloading devices", err);
|
||||
self.setState({devicesLoading: false});
|
||||
});
|
||||
},
|
||||
|
||||
onKick: function() {
|
||||
|
@ -315,51 +395,15 @@ module.exports = React.createClass({
|
|||
this.props.onFinished();
|
||||
}
|
||||
else {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
||||
Modal.createDialog(NeedToRegisterDialog, {
|
||||
title: "Please Register",
|
||||
description: "Guest users can't create new rooms. Please register to create room and start a chat."
|
||||
});
|
||||
self.props.onFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
self.setState({ updating: self.state.updating + 1 });
|
||||
MatrixClientPeg.get().createRoom({
|
||||
// XXX: FIXME: deduplicate this with "view_create_room" in MatrixChat
|
||||
invite: [this.props.member.userId],
|
||||
preset: "private_chat",
|
||||
// Allow guests by default since the room is private and they'd
|
||||
// need an invite. This means clicking on a 3pid invite email can
|
||||
// actually drop you right in to a chat.
|
||||
initial_state: [
|
||||
{
|
||||
content: {
|
||||
guest_access: 'can_join'
|
||||
},
|
||||
type: 'm.room.guest_access',
|
||||
state_key: '',
|
||||
visibility: 'private',
|
||||
}
|
||||
],
|
||||
}).then(
|
||||
function(res) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: res.room_id
|
||||
});
|
||||
self.props.onFinished();
|
||||
}, function(err) {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failure to start chat",
|
||||
description: err.message
|
||||
});
|
||||
self.props.onFinished();
|
||||
}
|
||||
).finally(()=>{
|
||||
createRoom({
|
||||
createOpts: {
|
||||
invite: [this.props.member.userId],
|
||||
},
|
||||
}).finally(function() {
|
||||
self.props.onFinished();
|
||||
self.setState({ updating: self.state.updating - 1 });
|
||||
});
|
||||
}).done();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -371,20 +415,6 @@ module.exports = React.createClass({
|
|||
this.props.onFinished();
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
can: {
|
||||
kick: false,
|
||||
ban: false,
|
||||
mute: false,
|
||||
modifyLevel: false
|
||||
},
|
||||
muted: false,
|
||||
isTargetMod: false,
|
||||
updating: 0,
|
||||
}
|
||||
},
|
||||
|
||||
_calculateOpsPermissions: function(member) {
|
||||
var defaultPerms = {
|
||||
can: {},
|
||||
|
@ -476,6 +506,40 @@ module.exports = React.createClass({
|
|||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||
},
|
||||
|
||||
_renderDevices: function() {
|
||||
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var devices = this.state.devices;
|
||||
var MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo');
|
||||
var Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
||||
var devComponents;
|
||||
if (this.state.devicesLoading) {
|
||||
// still loading
|
||||
devComponents = <Spinner />;
|
||||
} else if (devices === null) {
|
||||
devComponents = "Unable to load device list";
|
||||
} else if (devices.length === 0) {
|
||||
devComponents = "No registered devices";
|
||||
} else {
|
||||
devComponents = [];
|
||||
for (var i = 0; i < devices.length; i++) {
|
||||
devComponents.push(<MemberDeviceInfo key={i}
|
||||
userId={this.props.member.userId}
|
||||
device={devices[i]}/>);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>Devices</h3>
|
||||
{devComponents}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var startChat, kickButton, banButton, muteButton, giveModButton, spinner;
|
||||
if (this.props.member.userId !== MatrixClientPeg.get().credentials.userId) {
|
||||
|
@ -552,6 +616,8 @@ module.exports = React.createClass({
|
|||
|
||||
{ startChat }
|
||||
|
||||
{ this._renderDevices() }
|
||||
|
||||
{ adminTools }
|
||||
|
||||
{ spinner }
|
||||
|
@ -559,4 +625,3 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -21,6 +21,8 @@ var Modal = require('../../../Modal');
|
|||
var sdk = require('../../../index');
|
||||
var dis = require('../../../dispatcher');
|
||||
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MessageComposer',
|
||||
|
@ -131,7 +133,8 @@ module.exports = React.createClass({
|
|||
var uploadInputStyle = {display: 'none'};
|
||||
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
var MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput");
|
||||
var MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput" +
|
||||
(UserSettingsStore.isFeatureEnabled('rich_text_editor') ? "" : "Old"));
|
||||
|
||||
var controls = [];
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ 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");
|
||||
import React from 'react';
|
||||
|
||||
var marked = require("marked");
|
||||
marked.setOptions({
|
||||
|
@ -27,6 +27,12 @@ marked.setOptions({
|
|||
smartypants: false
|
||||
});
|
||||
|
||||
import {Editor, EditorState, RichUtils, CompositeDecorator,
|
||||
convertFromRaw, convertToRaw, Modifier, EditorChangeType,
|
||||
getDefaultKeyBinding, KeyBindingUtil, ContentState} from 'draft-js';
|
||||
|
||||
import {stateToMarkdown} from 'draft-js-export-markdown';
|
||||
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var SlashCommands = require("../../../SlashCommands");
|
||||
var Modal = require("../../../Modal");
|
||||
|
@ -36,10 +42,13 @@ var sdk = require('../../../index');
|
|||
var dis = require("../../../dispatcher");
|
||||
var KeyCode = require("../../../KeyCode");
|
||||
|
||||
var TYPING_USER_TIMEOUT = 10000;
|
||||
var TYPING_SERVER_TIMEOUT = 30000;
|
||||
var MARKDOWN_ENABLED = true;
|
||||
import * as RichText from '../../../RichText';
|
||||
|
||||
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
|
||||
|
||||
const KEY_M = 77;
|
||||
|
||||
// FIXME Breaks markdown with multiple paragraphs, since it only strips first and last <p>
|
||||
function mdownToHtml(mdown) {
|
||||
var html = marked(mdown) || "";
|
||||
html = html.trim();
|
||||
|
@ -56,29 +65,63 @@ function mdownToHtml(mdown) {
|
|||
/*
|
||||
* The textInput part of the MessageComposer
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MessageComposerInput',
|
||||
export default class MessageComposerInput extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.onAction = this.onAction.bind(this);
|
||||
this.onInputClick = this.onInputClick.bind(this);
|
||||
this.handleReturn = this.handleReturn.bind(this);
|
||||
this.handleKeyCommand = this.handleKeyCommand.bind(this);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
|
||||
statics: {
|
||||
// the height we limit the composer to
|
||||
MAX_HEIGHT: 100,
|
||||
},
|
||||
let isRichtextEnabled = window.localStorage.getItem('mx_editor_rte_enabled');
|
||||
if(isRichtextEnabled == null) {
|
||||
isRichtextEnabled = 'true';
|
||||
}
|
||||
isRichtextEnabled = isRichtextEnabled === 'true';
|
||||
|
||||
propTypes: {
|
||||
tabComplete: React.PropTypes.any,
|
||||
this.state = {
|
||||
isRichtextEnabled: isRichtextEnabled,
|
||||
editorState: null
|
||||
};
|
||||
|
||||
// a callback which is called when the height of the composer is
|
||||
// changed due to a change in content.
|
||||
onResize: React.PropTypes.func,
|
||||
// bit of a hack, but we need to do this here since createEditorState needs isRichtextEnabled
|
||||
this.state.editorState = this.createEditorState();
|
||||
|
||||
// js-sdk Room object
|
||||
room: React.PropTypes.object.isRequired,
|
||||
},
|
||||
this.client = MatrixClientPeg.get();
|
||||
}
|
||||
|
||||
componentWillMount: function() {
|
||||
this.oldScrollHeight = 0;
|
||||
this.markdownEnabled = MARKDOWN_ENABLED;
|
||||
var self = this;
|
||||
static getKeyBinding(e: SyntheticKeyboardEvent): string {
|
||||
// C-m => Toggles between rich text and markdown modes
|
||||
if(e.keyCode == KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
|
||||
return 'toggle-mode';
|
||||
}
|
||||
|
||||
return getDefaultKeyBinding(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* "Does the right thing" to create an EditorState, based on:
|
||||
* - whether we've got rich text mode enabled
|
||||
* - contentState was passed in
|
||||
*/
|
||||
createEditorState(richText: boolean, contentState: ?ContentState): EditorState {
|
||||
let decorators = richText ? RichText.getScopedRTDecorators(this.props) :
|
||||
RichText.getScopedMDDecorators(this.props),
|
||||
compositeDecorator = new CompositeDecorator(decorators);
|
||||
|
||||
let editorState = null;
|
||||
if (contentState) {
|
||||
editorState = EditorState.createWithContent(contentState, compositeDecorator);
|
||||
} else {
|
||||
editorState = EditorState.createEmpty(compositeDecorator);
|
||||
}
|
||||
|
||||
return EditorState.moveFocusToEnd(editorState);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const component = this;
|
||||
this.sentHistory = {
|
||||
// The list of typed messages. Index 0 is more recent
|
||||
data: [],
|
||||
|
@ -96,7 +139,7 @@ module.exports = React.createClass({
|
|||
this.element = element;
|
||||
this.position = -1;
|
||||
var storedData = window.sessionStorage.getItem(
|
||||
"history_" + roomId
|
||||
"mx_messagecomposer_history_" + roomId
|
||||
);
|
||||
if (storedData) {
|
||||
this.data = JSON.parse(storedData);
|
||||
|
@ -110,7 +153,7 @@ module.exports = React.createClass({
|
|||
// store a message in the sent history
|
||||
this.data.unshift(text);
|
||||
window.sessionStorage.setItem(
|
||||
"history_" + this.roomId,
|
||||
"mx_messagecomposer_history_" + this.roomId,
|
||||
JSON.stringify(this.data)
|
||||
);
|
||||
// reset history position
|
||||
|
@ -149,7 +192,6 @@ module.exports = React.createClass({
|
|||
this.element.value = this.originalText;
|
||||
}
|
||||
|
||||
self.resizeInput();
|
||||
return true;
|
||||
},
|
||||
|
||||
|
@ -157,76 +199,68 @@ module.exports = React.createClass({
|
|||
// save the currently entered text in order to restore it later.
|
||||
// NB: This isn't 'originalText' because we want to restore
|
||||
// sent history items too!
|
||||
var text = this.element.value;
|
||||
window.sessionStorage.setItem("input_" + this.roomId, text);
|
||||
let contentJSON = JSON.stringify(convertToRaw(component.state.editorState.getCurrentContent()));
|
||||
window.sessionStorage.setItem("mx_messagecomposer_input_" + this.roomId, contentJSON);
|
||||
},
|
||||
|
||||
setLastTextEntry: function() {
|
||||
var text = window.sessionStorage.getItem("input_" + this.roomId);
|
||||
if (text) {
|
||||
this.element.value = text;
|
||||
self.resizeInput();
|
||||
let contentJSON = window.sessionStorage.getItem("mx_messagecomposer_input_" + this.roomId);
|
||||
if (contentJSON) {
|
||||
let content = convertFromRaw(JSON.parse(contentJSON));
|
||||
component.setState({
|
||||
editorState: component.createEditorState(component.state.isRichtextEnabled, content)
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
componentDidMount: function() {
|
||||
componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this.sentHistory.init(
|
||||
this.refs.textarea,
|
||||
this.refs.editor,
|
||||
this.props.room.roomId
|
||||
);
|
||||
this.resizeInput();
|
||||
if (this.props.tabComplete) {
|
||||
this.props.tabComplete.setTextArea(this.refs.textarea);
|
||||
}
|
||||
},
|
||||
// this is disabled for now, since https://github.com/matrix-org/matrix-react-sdk/pull/296 will land soon
|
||||
// if (this.props.tabComplete) {
|
||||
// this.props.tabComplete.setEditor(this.refs.editor);
|
||||
// }
|
||||
}
|
||||
|
||||
componentWillUnmount: function() {
|
||||
componentWillUnmount() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
this.sentHistory.saveLastTextEntry();
|
||||
},
|
||||
}
|
||||
|
||||
onAction(payload) {
|
||||
var editor = this.refs.editor;
|
||||
|
||||
onAction: function(payload) {
|
||||
var textarea = this.refs.textarea;
|
||||
switch (payload.action) {
|
||||
case 'focus_composer':
|
||||
textarea.focus();
|
||||
editor.focus();
|
||||
break;
|
||||
case 'insert_displayname':
|
||||
if (textarea.value.length) {
|
||||
var left = textarea.value.substring(0, textarea.selectionStart);
|
||||
var right = textarea.value.substring(textarea.selectionEnd);
|
||||
if (right.length) {
|
||||
left += payload.displayname;
|
||||
}
|
||||
else {
|
||||
left = left.replace(/( ?)$/, " " + payload.displayname);
|
||||
}
|
||||
textarea.value = left + right;
|
||||
textarea.focus();
|
||||
textarea.setSelectionRange(left.length, left.length);
|
||||
}
|
||||
else {
|
||||
textarea.value = payload.displayname + ": ";
|
||||
textarea.focus();
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
onKeyDown: function (ev) {
|
||||
if (ev.keyCode === KeyCode.ENTER && !ev.shiftKey) {
|
||||
var input = this.refs.textarea.value;
|
||||
if (input.length === 0) {
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
this.sentHistory.push(input);
|
||||
this.onEnter(ev);
|
||||
// TODO change this so we insert a complete user alias
|
||||
|
||||
case 'insert_displayname':
|
||||
if (this.state.editorState.getCurrentContent().hasText()) {
|
||||
console.log(payload);
|
||||
let contentState = Modifier.replaceText(
|
||||
this.state.editorState.getCurrentContent(),
|
||||
this.state.editorState.getSelection(),
|
||||
payload.displayname
|
||||
);
|
||||
this.setState({
|
||||
editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters')
|
||||
});
|
||||
editor.focus();
|
||||
}
|
||||
break;
|
||||
}
|
||||
else if (ev.keyCode === KeyCode.UP || ev.keyCode === KeyCode.DOWN) {
|
||||
}
|
||||
|
||||
onKeyDown(ev) {
|
||||
if (ev.keyCode === KeyCode.UP || ev.keyCode === KeyCode.DOWN) {
|
||||
var oldSelectionStart = this.refs.textarea.selectionStart;
|
||||
// Remember the keyCode because React will recycle the synthetic event
|
||||
var keyCode = ev.keyCode;
|
||||
|
@ -235,78 +269,167 @@ module.exports = React.createClass({
|
|||
setTimeout(() => {
|
||||
if (this.refs.textarea.selectionStart == oldSelectionStart) {
|
||||
this.sentHistory.next(keyCode === KeyCode.UP ? 1 : -1);
|
||||
this.resizeInput();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.tabComplete) {
|
||||
this.props.tabComplete.onKeyDown(ev);
|
||||
onTypingActivity() {
|
||||
this.isTyping = true;
|
||||
if (!this.userTypingTimer) {
|
||||
this.sendTyping(true);
|
||||
}
|
||||
this.startUserTypingTimer();
|
||||
this.startServerTypingTimer();
|
||||
}
|
||||
|
||||
onFinishedTyping() {
|
||||
this.isTyping = false;
|
||||
this.sendTyping(false);
|
||||
this.stopUserTypingTimer();
|
||||
this.stopServerTypingTimer();
|
||||
}
|
||||
|
||||
startUserTypingTimer() {
|
||||
this.stopUserTypingTimer();
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
if (self.refs.textarea && self.refs.textarea.value != '') {
|
||||
self.onTypingActivity();
|
||||
} else {
|
||||
self.onFinishedTyping();
|
||||
}
|
||||
}, 10); // XXX: what is this 10ms setTimeout doing? Looks hacky :(
|
||||
},
|
||||
this.userTypingTimer = setTimeout(function() {
|
||||
self.isTyping = false;
|
||||
self.sendTyping(self.isTyping);
|
||||
self.userTypingTimer = null;
|
||||
}, TYPING_USER_TIMEOUT);
|
||||
}
|
||||
|
||||
resizeInput: function() {
|
||||
// scrollHeight is at least equal to clientHeight, so we have to
|
||||
// temporarily crimp clientHeight to 0 to get an accurate scrollHeight value
|
||||
this.refs.textarea.style.height = "20px"; // 20 hardcoded from CSS
|
||||
var newHeight = Math.min(this.refs.textarea.scrollHeight,
|
||||
this.constructor.MAX_HEIGHT);
|
||||
this.refs.textarea.style.height = Math.ceil(newHeight) + "px";
|
||||
this.oldScrollHeight = this.refs.textarea.scrollHeight;
|
||||
|
||||
if (this.props.onResize) {
|
||||
// kick gemini-scrollbar to re-layout
|
||||
this.props.onResize();
|
||||
stopUserTypingTimer() {
|
||||
if (this.userTypingTimer) {
|
||||
clearTimeout(this.userTypingTimer);
|
||||
this.userTypingTimer = null;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onKeyUp: function(ev) {
|
||||
if (this.refs.textarea.scrollHeight !== this.oldScrollHeight ||
|
||||
ev.keyCode === KeyCode.DELETE ||
|
||||
ev.keyCode === KeyCode.BACKSPACE)
|
||||
{
|
||||
this.resizeInput();
|
||||
startServerTypingTimer() {
|
||||
if (!this.serverTypingTimer) {
|
||||
var self = this;
|
||||
this.serverTypingTimer = setTimeout(function() {
|
||||
if (self.isTyping) {
|
||||
self.sendTyping(self.isTyping);
|
||||
self.startServerTypingTimer();
|
||||
}
|
||||
}, TYPING_SERVER_TIMEOUT / 2);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onEnter: function(ev) {
|
||||
var contentText = this.refs.textarea.value;
|
||||
|
||||
// bodge for now to set markdown state on/off. We probably want a separate
|
||||
// area for "local" commands which don't hit out to the server.
|
||||
if (contentText.indexOf("/markdown") === 0) {
|
||||
ev.preventDefault();
|
||||
this.refs.textarea.value = '';
|
||||
if (contentText.indexOf("/markdown on") === 0) {
|
||||
this.markdownEnabled = true;
|
||||
}
|
||||
else if (contentText.indexOf("/markdown off") === 0) {
|
||||
this.markdownEnabled = false;
|
||||
}
|
||||
else {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Unknown command",
|
||||
description: "Usage: /markdown on|off"
|
||||
});
|
||||
}
|
||||
return;
|
||||
stopServerTypingTimer() {
|
||||
if (this.serverTypingTimer) {
|
||||
clearTimeout(this.servrTypingTimer);
|
||||
this.serverTypingTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
sendTyping(isTyping) {
|
||||
MatrixClientPeg.get().sendTyping(
|
||||
this.props.room.roomId,
|
||||
this.isTyping, TYPING_SERVER_TIMEOUT
|
||||
).done();
|
||||
}
|
||||
|
||||
refreshTyping() {
|
||||
if (this.typingTimeout) {
|
||||
clearTimeout(this.typingTimeout);
|
||||
this.typingTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
onInputClick(ev) {
|
||||
this.refs.editor.focus();
|
||||
}
|
||||
|
||||
onChange(editorState: EditorState) {
|
||||
this.setState({editorState});
|
||||
|
||||
if(editorState.getCurrentContent().hasText()) {
|
||||
this.onTypingActivity()
|
||||
} else {
|
||||
this.onFinishedTyping();
|
||||
}
|
||||
}
|
||||
|
||||
enableRichtext(enabled: boolean) {
|
||||
if (enabled) {
|
||||
let html = mdownToHtml(this.state.editorState.getCurrentContent().getPlainText());
|
||||
this.setState({
|
||||
editorState: this.createEditorState(enabled, RichText.HTMLtoContentState(html))
|
||||
});
|
||||
} else {
|
||||
let markdown = stateToMarkdown(this.state.editorState.getCurrentContent()),
|
||||
contentState = ContentState.createFromText(markdown);
|
||||
this.setState({
|
||||
editorState: this.createEditorState(enabled, contentState)
|
||||
});
|
||||
}
|
||||
|
||||
window.localStorage.setItem('mx_editor_rte_enabled', enabled);
|
||||
|
||||
this.setState({
|
||||
isRichtextEnabled: enabled
|
||||
});
|
||||
}
|
||||
|
||||
handleKeyCommand(command: string): boolean {
|
||||
if(command === 'toggle-mode') {
|
||||
this.enableRichtext(!this.state.isRichtextEnabled);
|
||||
return true;
|
||||
}
|
||||
|
||||
let newState: ?EditorState = null;
|
||||
|
||||
// Draft handles rich text mode commands by default but we need to do it ourselves for Markdown.
|
||||
if(!this.state.isRichtextEnabled) {
|
||||
let contentState = this.state.editorState.getCurrentContent(),
|
||||
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*
|
||||
code: text => `\`${text}\``
|
||||
}[command];
|
||||
|
||||
if(modifyFn) {
|
||||
newState = EditorState.push(
|
||||
this.state.editorState,
|
||||
RichText.modifyText(contentState, selection, modifyFn),
|
||||
'insert-characters'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if(newState == null)
|
||||
newState = RichUtils.handleKeyCommand(this.state.editorState, command);
|
||||
|
||||
if (newState != null) {
|
||||
this.onChange(newState);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
handleReturn(ev) {
|
||||
if(ev.shiftKey)
|
||||
return false;
|
||||
|
||||
const contentState = this.state.editorState.getCurrentContent();
|
||||
if(!contentState.hasText())
|
||||
return true;
|
||||
|
||||
let contentText = contentState.getPlainText(), contentHTML;
|
||||
|
||||
var cmd = SlashCommands.processInput(this.props.room.roomId, contentText);
|
||||
if (cmd) {
|
||||
ev.preventDefault();
|
||||
if (!cmd.error) {
|
||||
this.refs.textarea.value = '';
|
||||
this.setState({
|
||||
editorState: this.createEditorState()
|
||||
});
|
||||
}
|
||||
if (cmd.promise) {
|
||||
cmd.promise.done(function() {
|
||||
|
@ -328,121 +451,75 @@ module.exports = React.createClass({
|
|||
description: cmd.error
|
||||
});
|
||||
}
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
var isEmote = /^\/me( |$)/i.test(contentText);
|
||||
var sendMessagePromise;
|
||||
|
||||
if (isEmote) {
|
||||
contentText = contentText.substring(4);
|
||||
}
|
||||
else if (contentText[0] === '/') {
|
||||
contentText = contentText.substring(1);
|
||||
if(this.state.isRichtextEnabled) {
|
||||
contentHTML = RichText.contentStateToHTML(contentState);
|
||||
} else {
|
||||
contentHTML = mdownToHtml(contentText);
|
||||
}
|
||||
|
||||
var htmlText;
|
||||
if (this.markdownEnabled && (htmlText = mdownToHtml(contentText)) !== contentText) {
|
||||
sendMessagePromise = isEmote ?
|
||||
MatrixClientPeg.get().sendHtmlEmote(this.props.room.roomId, contentText, htmlText) :
|
||||
MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, htmlText);
|
||||
}
|
||||
else {
|
||||
sendMessagePromise = isEmote ?
|
||||
MatrixClientPeg.get().sendEmoteMessage(this.props.room.roomId, contentText) :
|
||||
MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText);
|
||||
let sendFn = this.client.sendHtmlMessage;
|
||||
|
||||
if (contentText.startsWith('/me')) {
|
||||
contentText = contentText.replace('/me', '');
|
||||
// bit of a hack, but the alternative would be quite complicated
|
||||
contentHTML = contentHTML.replace('/me', '');
|
||||
sendFn = this.client.sendHtmlEmote;
|
||||
}
|
||||
|
||||
sendMessagePromise.done(function() {
|
||||
this.sentHistory.push(contentHTML);
|
||||
let sendMessagePromise = sendFn.call(this.client, this.props.room.roomId, contentText, contentHTML);
|
||||
|
||||
sendMessagePromise.done(() => {
|
||||
dis.dispatch({
|
||||
action: 'message_sent'
|
||||
});
|
||||
}, function() {
|
||||
}, () => {
|
||||
dis.dispatch({
|
||||
action: 'message_send_failed'
|
||||
});
|
||||
});
|
||||
this.refs.textarea.value = '';
|
||||
this.resizeInput();
|
||||
ev.preventDefault();
|
||||
},
|
||||
|
||||
onTypingActivity: function() {
|
||||
this.isTyping = true;
|
||||
if (!this.userTypingTimer) {
|
||||
this.sendTyping(true);
|
||||
this.setState({
|
||||
editorState: this.createEditorState()
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
render() {
|
||||
let className = "mx_MessageComposer_input";
|
||||
|
||||
if(this.state.isRichtextEnabled) {
|
||||
className += " mx_MessageComposer_input_rte"; // placeholder indicator for RTE mode
|
||||
}
|
||||
this.startUserTypingTimer();
|
||||
this.startServerTypingTimer();
|
||||
},
|
||||
|
||||
onFinishedTyping: function() {
|
||||
this.isTyping = false;
|
||||
this.sendTyping(false);
|
||||
this.stopUserTypingTimer();
|
||||
this.stopServerTypingTimer();
|
||||
},
|
||||
|
||||
startUserTypingTimer: function() {
|
||||
this.stopUserTypingTimer();
|
||||
var self = this;
|
||||
this.userTypingTimer = setTimeout(function() {
|
||||
self.isTyping = false;
|
||||
self.sendTyping(self.isTyping);
|
||||
self.userTypingTimer = null;
|
||||
}, TYPING_USER_TIMEOUT);
|
||||
},
|
||||
|
||||
stopUserTypingTimer: function() {
|
||||
if (this.userTypingTimer) {
|
||||
clearTimeout(this.userTypingTimer);
|
||||
this.userTypingTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
startServerTypingTimer: function() {
|
||||
if (!this.serverTypingTimer) {
|
||||
var self = this;
|
||||
this.serverTypingTimer = setTimeout(function() {
|
||||
if (self.isTyping) {
|
||||
self.sendTyping(self.isTyping);
|
||||
self.startServerTypingTimer();
|
||||
}
|
||||
}, TYPING_SERVER_TIMEOUT / 2);
|
||||
}
|
||||
},
|
||||
|
||||
stopServerTypingTimer: function() {
|
||||
if (this.serverTypingTimer) {
|
||||
clearTimeout(this.servrTypingTimer);
|
||||
this.serverTypingTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
sendTyping: function(isTyping) {
|
||||
MatrixClientPeg.get().sendTyping(
|
||||
this.props.room.roomId,
|
||||
this.isTyping, TYPING_SERVER_TIMEOUT
|
||||
).done();
|
||||
},
|
||||
|
||||
refreshTyping: function() {
|
||||
if (this.typingTimeout) {
|
||||
clearTimeout(this.typingTimeout);
|
||||
this.typingTimeout = null;
|
||||
}
|
||||
},
|
||||
|
||||
onInputClick: function(ev) {
|
||||
this.refs.textarea.focus();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="mx_MessageComposer_input" onClick={ this.onInputClick }>
|
||||
<textarea autoFocus ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder="Type a message..." />
|
||||
<div className={className}
|
||||
onClick={ this.onInputClick }>
|
||||
<Editor ref="editor"
|
||||
placeholder="Type a message…"
|
||||
editorState={this.state.editorState}
|
||||
onChange={this.onChange}
|
||||
keyBindingFn={MessageComposerInput.getKeyBinding}
|
||||
handleKeyCommand={this.handleKeyCommand}
|
||||
handleReturn={this.handleReturn}
|
||||
stripPastedStyles={!this.state.isRichtextEnabled}
|
||||
spellCheck={true} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
MessageComposerInput.propTypes = {
|
||||
tabComplete: React.PropTypes.any,
|
||||
|
||||
// a callback which is called when the height of the composer is
|
||||
// changed due to a change in content.
|
||||
onResize: React.PropTypes.func,
|
||||
|
||||
// js-sdk Room object
|
||||
room: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
|
447
src/components/views/rooms/MessageComposerInputOld.js
Normal file
447
src/components/views/rooms/MessageComposerInputOld.js
Normal file
|
@ -0,0 +1,447 @@
|
|||
/*
|
||||
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 marked = require("marked");
|
||||
marked.setOptions({
|
||||
renderer: new marked.Renderer(),
|
||||
gfm: true,
|
||||
tables: true,
|
||||
breaks: true,
|
||||
pedantic: false,
|
||||
sanitize: true,
|
||||
smartLists: true,
|
||||
smartypants: false
|
||||
});
|
||||
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var SlashCommands = require("../../../SlashCommands");
|
||||
var Modal = require("../../../Modal");
|
||||
var MemberEntry = require("../../../TabCompleteEntries").MemberEntry;
|
||||
var sdk = require('../../../index');
|
||||
|
||||
var dis = require("../../../dispatcher");
|
||||
var KeyCode = require("../../../KeyCode");
|
||||
|
||||
var TYPING_USER_TIMEOUT = 10000;
|
||||
var TYPING_SERVER_TIMEOUT = 30000;
|
||||
var MARKDOWN_ENABLED = true;
|
||||
|
||||
function mdownToHtml(mdown) {
|
||||
var html = marked(mdown) || "";
|
||||
html = html.trim();
|
||||
// strip start and end <p> tags else you get 'orrible spacing
|
||||
if (html.indexOf("<p>") === 0) {
|
||||
html = html.substring("<p>".length);
|
||||
}
|
||||
if (html.lastIndexOf("</p>") === (html.length - "</p>".length)) {
|
||||
html = html.substring(0, html.length - "</p>".length);
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
/*
|
||||
* The textInput part of the MessageComposer
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MessageComposerInput',
|
||||
|
||||
statics: {
|
||||
// the height we limit the composer to
|
||||
MAX_HEIGHT: 100,
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
tabComplete: React.PropTypes.any,
|
||||
|
||||
// a callback which is called when the height of the composer is
|
||||
// changed due to a change in content.
|
||||
onResize: React.PropTypes.func,
|
||||
|
||||
// js-sdk Room object
|
||||
room: React.PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.oldScrollHeight = 0;
|
||||
this.markdownEnabled = MARKDOWN_ENABLED;
|
||||
var self = this;
|
||||
this.sentHistory = {
|
||||
// The list of typed messages. Index 0 is more recent
|
||||
data: [],
|
||||
// The position in data currently displayed
|
||||
position: -1,
|
||||
// The room the history is for.
|
||||
roomId: null,
|
||||
// The original text before they hit UP
|
||||
originalText: null,
|
||||
// The textarea element to set text to.
|
||||
element: null,
|
||||
|
||||
init: function(element, roomId) {
|
||||
this.roomId = roomId;
|
||||
this.element = element;
|
||||
this.position = -1;
|
||||
var storedData = window.sessionStorage.getItem(
|
||||
"history_" + roomId
|
||||
);
|
||||
if (storedData) {
|
||||
this.data = JSON.parse(storedData);
|
||||
}
|
||||
if (this.roomId) {
|
||||
this.setLastTextEntry();
|
||||
}
|
||||
},
|
||||
|
||||
push: function(text) {
|
||||
// store a message in the sent history
|
||||
this.data.unshift(text);
|
||||
window.sessionStorage.setItem(
|
||||
"history_" + this.roomId,
|
||||
JSON.stringify(this.data)
|
||||
);
|
||||
// reset history position
|
||||
this.position = -1;
|
||||
this.originalText = null;
|
||||
},
|
||||
|
||||
// move in the history. Returns true if we managed to move.
|
||||
next: function(offset) {
|
||||
if (this.position === -1) {
|
||||
// user is going into the history, save the current line.
|
||||
this.originalText = this.element.value;
|
||||
}
|
||||
else {
|
||||
// user may have modified this line in the history; remember it.
|
||||
this.data[this.position] = this.element.value;
|
||||
}
|
||||
|
||||
if (offset > 0 && this.position === (this.data.length - 1)) {
|
||||
// we've run out of history
|
||||
return false;
|
||||
}
|
||||
|
||||
// retrieve the next item (bounded).
|
||||
var newPosition = this.position + offset;
|
||||
newPosition = Math.max(-1, newPosition);
|
||||
newPosition = Math.min(newPosition, this.data.length - 1);
|
||||
this.position = newPosition;
|
||||
|
||||
if (this.position !== -1) {
|
||||
// show the message
|
||||
this.element.value = this.data[this.position];
|
||||
}
|
||||
else if (this.originalText !== undefined) {
|
||||
// restore the original text the user was typing.
|
||||
this.element.value = this.originalText;
|
||||
}
|
||||
|
||||
self.resizeInput();
|
||||
return true;
|
||||
},
|
||||
|
||||
saveLastTextEntry: function() {
|
||||
// save the currently entered text in order to restore it later.
|
||||
// NB: This isn't 'originalText' because we want to restore
|
||||
// sent history items too!
|
||||
var text = this.element.value;
|
||||
window.sessionStorage.setItem("input_" + this.roomId, text);
|
||||
},
|
||||
|
||||
setLastTextEntry: function() {
|
||||
var text = window.sessionStorage.getItem("input_" + this.roomId);
|
||||
if (text) {
|
||||
this.element.value = text;
|
||||
self.resizeInput();
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this.sentHistory.init(
|
||||
this.refs.textarea,
|
||||
this.props.room.roomId
|
||||
);
|
||||
this.resizeInput();
|
||||
if (this.props.tabComplete) {
|
||||
this.props.tabComplete.setTextArea(this.refs.textarea);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
this.sentHistory.saveLastTextEntry();
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
var textarea = this.refs.textarea;
|
||||
switch (payload.action) {
|
||||
case 'focus_composer':
|
||||
textarea.focus();
|
||||
break;
|
||||
case 'insert_displayname':
|
||||
if (textarea.value.length) {
|
||||
var left = textarea.value.substring(0, textarea.selectionStart);
|
||||
var right = textarea.value.substring(textarea.selectionEnd);
|
||||
if (right.length) {
|
||||
left += payload.displayname;
|
||||
}
|
||||
else {
|
||||
left = left.replace(/( ?)$/, " " + payload.displayname);
|
||||
}
|
||||
textarea.value = left + right;
|
||||
textarea.focus();
|
||||
textarea.setSelectionRange(left.length, left.length);
|
||||
}
|
||||
else {
|
||||
textarea.value = payload.displayname + ": ";
|
||||
textarea.focus();
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
onKeyDown: function (ev) {
|
||||
if (ev.keyCode === KeyCode.ENTER && !ev.shiftKey) {
|
||||
var input = this.refs.textarea.value;
|
||||
if (input.length === 0) {
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
this.sentHistory.push(input);
|
||||
this.onEnter(ev);
|
||||
}
|
||||
else if (ev.keyCode === KeyCode.UP || ev.keyCode === KeyCode.DOWN) {
|
||||
var oldSelectionStart = this.refs.textarea.selectionStart;
|
||||
// Remember the keyCode because React will recycle the synthetic event
|
||||
var keyCode = ev.keyCode;
|
||||
// set a callback so we can see if the cursor position changes as
|
||||
// a result of this event. If it doesn't, we cycle history.
|
||||
setTimeout(() => {
|
||||
if (this.refs.textarea.selectionStart == oldSelectionStart) {
|
||||
this.sentHistory.next(keyCode === KeyCode.UP ? 1 : -1);
|
||||
this.resizeInput();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
if (this.props.tabComplete) {
|
||||
this.props.tabComplete.onKeyDown(ev);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
if (self.refs.textarea && self.refs.textarea.value != '') {
|
||||
self.onTypingActivity();
|
||||
} else {
|
||||
self.onFinishedTyping();
|
||||
}
|
||||
}, 10); // XXX: what is this 10ms setTimeout doing? Looks hacky :(
|
||||
},
|
||||
|
||||
resizeInput: function() {
|
||||
// scrollHeight is at least equal to clientHeight, so we have to
|
||||
// temporarily crimp clientHeight to 0 to get an accurate scrollHeight value
|
||||
this.refs.textarea.style.height = "20px"; // 20 hardcoded from CSS
|
||||
var newHeight = Math.min(this.refs.textarea.scrollHeight,
|
||||
this.constructor.MAX_HEIGHT);
|
||||
this.refs.textarea.style.height = Math.ceil(newHeight) + "px";
|
||||
this.oldScrollHeight = this.refs.textarea.scrollHeight;
|
||||
|
||||
if (this.props.onResize) {
|
||||
// kick gemini-scrollbar to re-layout
|
||||
this.props.onResize();
|
||||
}
|
||||
},
|
||||
|
||||
onKeyUp: function(ev) {
|
||||
if (this.refs.textarea.scrollHeight !== this.oldScrollHeight ||
|
||||
ev.keyCode === KeyCode.DELETE ||
|
||||
ev.keyCode === KeyCode.BACKSPACE)
|
||||
{
|
||||
this.resizeInput();
|
||||
}
|
||||
},
|
||||
|
||||
onEnter: function(ev) {
|
||||
var contentText = this.refs.textarea.value;
|
||||
|
||||
// bodge for now to set markdown state on/off. We probably want a separate
|
||||
// area for "local" commands which don't hit out to the server.
|
||||
if (contentText.indexOf("/markdown") === 0) {
|
||||
ev.preventDefault();
|
||||
this.refs.textarea.value = '';
|
||||
if (contentText.indexOf("/markdown on") === 0) {
|
||||
this.markdownEnabled = true;
|
||||
}
|
||||
else if (contentText.indexOf("/markdown off") === 0) {
|
||||
this.markdownEnabled = false;
|
||||
}
|
||||
else {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Unknown command",
|
||||
description: "Usage: /markdown on|off"
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var cmd = SlashCommands.processInput(this.props.room.roomId, contentText);
|
||||
if (cmd) {
|
||||
ev.preventDefault();
|
||||
if (!cmd.error) {
|
||||
this.refs.textarea.value = '';
|
||||
}
|
||||
if (cmd.promise) {
|
||||
cmd.promise.done(function() {
|
||||
console.log("Command success.");
|
||||
}, function(err) {
|
||||
console.error("Command failure: %s", err);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Server error",
|
||||
description: err.message
|
||||
});
|
||||
});
|
||||
}
|
||||
else if (cmd.error) {
|
||||
console.error(cmd.error);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Command error",
|
||||
description: cmd.error
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var isEmote = /^\/me( |$)/i.test(contentText);
|
||||
var sendMessagePromise;
|
||||
|
||||
if (isEmote) {
|
||||
contentText = contentText.substring(4);
|
||||
}
|
||||
else if (contentText[0] === '/') {
|
||||
contentText = contentText.substring(1);
|
||||
}
|
||||
|
||||
var htmlText;
|
||||
if (this.markdownEnabled && (htmlText = mdownToHtml(contentText)) !== contentText) {
|
||||
sendMessagePromise = isEmote ?
|
||||
MatrixClientPeg.get().sendHtmlEmote(this.props.room.roomId, contentText, htmlText) :
|
||||
MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, htmlText);
|
||||
}
|
||||
else {
|
||||
sendMessagePromise = isEmote ?
|
||||
MatrixClientPeg.get().sendEmoteMessage(this.props.room.roomId, contentText) :
|
||||
MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText);
|
||||
}
|
||||
|
||||
sendMessagePromise.done(function() {
|
||||
dis.dispatch({
|
||||
action: 'message_sent'
|
||||
});
|
||||
}, function() {
|
||||
dis.dispatch({
|
||||
action: 'message_send_failed'
|
||||
});
|
||||
});
|
||||
this.refs.textarea.value = '';
|
||||
this.resizeInput();
|
||||
ev.preventDefault();
|
||||
},
|
||||
|
||||
onTypingActivity: function() {
|
||||
this.isTyping = true;
|
||||
if (!this.userTypingTimer) {
|
||||
this.sendTyping(true);
|
||||
}
|
||||
this.startUserTypingTimer();
|
||||
this.startServerTypingTimer();
|
||||
},
|
||||
|
||||
onFinishedTyping: function() {
|
||||
this.isTyping = false;
|
||||
this.sendTyping(false);
|
||||
this.stopUserTypingTimer();
|
||||
this.stopServerTypingTimer();
|
||||
},
|
||||
|
||||
startUserTypingTimer: function() {
|
||||
this.stopUserTypingTimer();
|
||||
var self = this;
|
||||
this.userTypingTimer = setTimeout(function() {
|
||||
self.isTyping = false;
|
||||
self.sendTyping(self.isTyping);
|
||||
self.userTypingTimer = null;
|
||||
}, TYPING_USER_TIMEOUT);
|
||||
},
|
||||
|
||||
stopUserTypingTimer: function() {
|
||||
if (this.userTypingTimer) {
|
||||
clearTimeout(this.userTypingTimer);
|
||||
this.userTypingTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
startServerTypingTimer: function() {
|
||||
if (!this.serverTypingTimer) {
|
||||
var self = this;
|
||||
this.serverTypingTimer = setTimeout(function() {
|
||||
if (self.isTyping) {
|
||||
self.sendTyping(self.isTyping);
|
||||
self.startServerTypingTimer();
|
||||
}
|
||||
}, TYPING_SERVER_TIMEOUT / 2);
|
||||
}
|
||||
},
|
||||
|
||||
stopServerTypingTimer: function() {
|
||||
if (this.serverTypingTimer) {
|
||||
clearTimeout(this.servrTypingTimer);
|
||||
this.serverTypingTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
sendTyping: function(isTyping) {
|
||||
MatrixClientPeg.get().sendTyping(
|
||||
this.props.room.roomId,
|
||||
this.isTyping, TYPING_SERVER_TIMEOUT
|
||||
).done();
|
||||
},
|
||||
|
||||
refreshTyping: function() {
|
||||
if (this.typingTimeout) {
|
||||
clearTimeout(this.typingTimeout);
|
||||
this.typingTimeout = null;
|
||||
}
|
||||
},
|
||||
|
||||
onInputClick: function(ev) {
|
||||
this.refs.textarea.focus();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="mx_MessageComposer_input" onClick={ this.onInputClick }>
|
||||
<textarea autoFocus ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder="Type a message..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue