diff --git a/src/Tinter.js b/src/Tinter.js index 3e7949b65d..3612be5b10 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -63,6 +63,7 @@ var cssAttrs = [ "backgroundColor", "borderColor", "borderTopColor", + "borderBottomColor", ]; var svgAttrs = [ diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 462933cbc6..71910b6f31 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -65,6 +65,7 @@ module.exports = React.createClass({ collapse_rhs: false, ready: false, width: 10000, + autoPeek: true, // by default, we peek into rooms when we try to join them }; if (s.logged_in) { if (MatrixClientPeg.get().getRooms().length) { @@ -304,6 +305,9 @@ module.exports = React.createClass({ }); break; case 'view_room': + // by default we autoPeek rooms, unless we were called explicitly with + // autoPeek=false by something like RoomDirectory who has already peeked + this.setState({ autoPeek : payload.auto_peek === false ? false : true }); this._viewRoom(payload.room_id, payload.show_settings); break; case 'view_prev_room': @@ -787,6 +791,7 @@ module.exports = React.createClass({ ); diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index b333a18331..e2a9f8b730 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -57,7 +57,9 @@ if (DEBUG_SCROLL) { module.exports = React.createClass({ displayName: 'RoomView', propTypes: { - ConferenceHandler: React.PropTypes.any + ConferenceHandler: React.PropTypes.any, + roomId: React.PropTypes.string, + autoPeek: React.PropTypes.bool, // should we try to peek the room on mount, or has whoever invoked us already initiated a peek? }, /* properties in RoomView objects include: @@ -78,7 +80,9 @@ module.exports = React.createClass({ syncState: MatrixClientPeg.get().getSyncState(), hasUnsentMessages: this._hasUnsentMessages(room), callState: null, + autoPeekDone: false, // track whether our autoPeek (if any) has completed) guestsCanJoin: false, + canPeek: false, readMarkerEventId: room ? room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId) : null, readMarkerGhostEventId: undefined } @@ -86,6 +90,7 @@ module.exports = React.createClass({ componentWillMount: function() { this.dispatcherRef = dis.register(this.onAction); + MatrixClientPeg.get().on("Room", this.onNewRoom); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().on("Room.name", this.onRoomName); MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData); @@ -111,27 +116,21 @@ module.exports = React.createClass({ // 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.roomId); - MatrixClientPeg.get().peekInRoom(this.props.roomId).done(() => { - // we don't need to do anything - JS SDK will emit Room events - // which will update the UI. We *do* however need to know if we - // can join the room so we can fiddle with the UI appropriately. - var peekedRoom = MatrixClientPeg.get().getRoom(this.props.roomId); - if (!peekedRoom) { - return; - } - var guestAccessEvent = peekedRoom.currentState.getStateEvents("m.room.guest_access", ""); - if (!guestAccessEvent) { - return; - } - if (guestAccessEvent.getContent().guest_access === "can_join") { + if (this.props.autoPeek) { + console.log("Attempting to peek into room %s", this.props.roomId); + MatrixClientPeg.get().peekInRoom(this.props.roomId).catch((err) => { + console.error("Failed to peek into room: %s", err); + }).finally(() => { + // we don't need to do anything - JS SDK will emit Room events + // which will update the UI. this.setState({ - guestsCanJoin: true + autoPeekDone: true }); - } - }, function(err) { - console.error("Failed to peek into room: %s", err); - }); + }); + } + } + else { + this._calculatePeekRules(this.state.room); } }, @@ -155,6 +154,7 @@ module.exports = React.createClass({ } dis.unregister(this.dispatcherRef); if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener("Room", this.onNewRoom); MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData); @@ -278,6 +278,32 @@ module.exports = React.createClass({ }); }, + onNewRoom: function(room) { + if (room.roomId == this.props.roomId) { + this.setState({ + room: room + }); + } + + this._calculatePeekRules(room); + }, + + _calculatePeekRules: function(room) { + var guestAccessEvent = room.currentState.getStateEvents("m.room.guest_access", ""); + if (guestAccessEvent && guestAccessEvent.getContent().guest_access === "can_join") { + this.setState({ + guestsCanJoin: true + }); + } + + var historyVisibility = room.currentState.getStateEvents("m.room.history_visibility", ""); + if (historyVisibility && historyVisibility.getContent().history_visibility === "world_readable") { + this.setState({ + canPeek: true + }); + } + }, + onRoomName: function(room) { if (room.roomId == this.props.roomId) { this.setState({ @@ -349,6 +375,14 @@ module.exports = React.createClass({ if (member.roomId === this.props.roomId) { // a member state changed in this room, refresh the tab complete list this._updateTabCompleteList(this.state.room); + + var room = MatrixClientPeg.get().getRoom(this.props.roomId); + var me = MatrixClientPeg.get().credentials.userId; + if (this.state.joining && room.hasMembershipState(me, "join")) { + this.setState({ + joining: false + }); + } } if (!this.props.ConferenceHandler) { @@ -522,10 +556,17 @@ module.exports = React.createClass({ onJoinButtonClicked: function(ev) { var self = this; - MatrixClientPeg.get().joinRoom(this.props.roomId).then(function() { + MatrixClientPeg.get().joinRoom(this.props.roomId).done(function() { + // It is possible that there is no Room yet if state hasn't come down + // from /sync - joinRoom will resolve when the HTTP request to join succeeds, + // NOT when it comes down /sync. If there is no room, we'll keep the + // joining flag set until we see it. Likewise, if our state is not + // "join" we'll keep this flag set until it comes down /sync. + var room = MatrixClientPeg.get().getRoom(self.props.roomId); + var me = MatrixClientPeg.get().credentials.userId; self.setState({ - joining: false, - room: MatrixClientPeg.get().getRoom(self.props.roomId) + joining: room ? !room.hasMembershipState(me, "join") : true, + room: room }); }, function(error) { self.setState({ @@ -929,15 +970,36 @@ module.exports = React.createClass({ ); } + var visibilityDeferred; if (old_history_visibility != newVals.history_visibility && newVals.history_visibility != undefined) { - deferreds.push( + visibilityDeferred = MatrixClientPeg.get().sendStateEvent( this.state.room.roomId, "m.room.history_visibility", { history_visibility: newVals.history_visibility, }, "" - ) - ); + ); + } + + if (old_guest_read != newVals.guest_read || + old_guest_join != newVals.guest_join) + { + var guestDeferred = + MatrixClientPeg.get().setGuestAccess(this.state.room.roomId, { + allowRead: newVals.guest_read, + allowJoin: newVals.guest_join + }); + + if (visibilityDeferred) { + visibilityDeferred = visibilityDeferred.then(guestDeferred); + } + else { + visibilityDeferred = guestDeferred; + } + } + + if (visibilityDeferred) { + deferreds.push(visibilityDeferred); } // setRoomMutePushRule will do nothing if there is no change @@ -1040,17 +1102,6 @@ module.exports = React.createClass({ ); } - if (old_guest_read != newVals.guest_read || - old_guest_join != newVals.guest_join) - { - deferreds.push( - MatrixClientPeg.get().setGuestAccess(this.state.room.roomId, { - allowRead: newVals.guest_read, - allowJoin: newVals.guest_join - }) - ); - } - if (deferreds.length) { var self = this; q.allSettled(deferreds).then( @@ -1399,12 +1450,30 @@ module.exports = React.createClass({ if (!this.state.room) { if (this.props.roomId) { - return ( -
- -
- ); - } else { + if (this.props.autoPeek && !this.state.autoPeekDone) { + var Loader = sdk.getComponent("elements.Spinner"); + return ( +
+ +
+ ); + } + else { + var joinErrorText = this.state.joinError ? "Failed to join room!" : ""; + return ( +
+ +
+ +
{joinErrorText}
+
+
+
+ ); + } + } + else { return (
); @@ -1425,19 +1494,26 @@ module.exports = React.createClass({ var inviteEvent = myMember.events.member; var inviterName = inviteEvent.sender ? inviteEvent.sender.name : inviteEvent.getSender(); // XXX: Leaving this intentionally basic for now because invites are about to change totally + // FIXME: This comment is now outdated - what do we need to fix? ^ var joinErrorText = this.state.joinError ? "Failed to join room!" : ""; var rejectErrorText = this.state.rejectError ? "Failed to reject invite!" : ""; + + // We deliberately don't try to peek into invites, even if we have permission to peek + // as they could be a spam vector. + // XXX: in future we could give the option of a 'Preview' button which lets them view anyway. + return (
-
-
{inviterName} has invited you to a room
-
- - +
+
{joinErrorText}
{rejectErrorText}
+
); } @@ -1552,6 +1628,12 @@ module.exports = React.createClass({ ); } + else if (this.state.canPeek && + (!myMember || myMember.membership !== "join")) { + aux = ( + + ); + } var conferenceCallNotification = null; if (this.state.displayConfCallNotification) { diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 488e7368d1..d1bb66356c 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -306,7 +306,7 @@ module.exports = React.createClass({ rowClassName="mx_UserSettings_profileTableRow" rowLabelClassName="mx_UserSettings_profileLabelCell" rowInputClassName="mx_UserSettings_profileInputCell" - buttonClassName="mx_UserSettings_button" + buttonClassName="mx_UserSettings_button mx_UserSettings_changePasswordButton" onError={this.onPasswordChangeError} onFinished={this.onPasswordChanged} /> ); diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js index 55629aa8b0..ed0e5cbc41 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.js @@ -72,7 +72,7 @@ module.exports = React.createClass({ }, render: function() { - var presenceClass = PRESENCE_CLASS[this.props.presenceState]; + var presenceClass = PRESENCE_CLASS[this.props.presenceState] || "mx_EntityTile_offline"; var mainClassName = "mx_EntityTile "; mainClassName += presenceClass; if (this.state.hover) { @@ -128,10 +128,10 @@ module.exports = React.createClass({ onClick={ this.props.onClick } onMouseEnter={ this.mouseEnter } onMouseLeave={ this.mouseLeave }>
- {av} + { av } + { power }
{ nameEl } - { power } { inviteButton }
); diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index e38d6ae41d..64bebdeca3 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -362,7 +362,7 @@ module.exports = React.createClass({ invitedSection = (

Invited

-
+
{invitedMemberTiles}
@@ -370,15 +370,15 @@ module.exports = React.createClass({ } return (
- {this.inviteTile()} -
+
{this.makeMemberTiles('join', this.state.searchQuery)}
+ {invitedSection} +
+
- {invitedSection} -
); } diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index 2f12c4c8e2..52e6639f13 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -23,33 +23,60 @@ module.exports = React.createClass({ propTypes: { onJoinClick: React.PropTypes.func, - canJoin: React.PropTypes.bool + onRejectClick: React.PropTypes.func, + inviterName: React.PropTypes.string, + canJoin: React.PropTypes.bool, + canPreview: React.PropTypes.bool, }, getDefaultProps: function() { return { onJoinClick: function() {}, - canJoin: false + canJoin: false, + canPreview: true, }; }, render: function() { - var joinBlock; + var joinBlock, previewBlock; - if (this.props.canJoin) { + if (this.props.inviterName) { joinBlock = ( -
- Would you like to join this room? +
+
+ You have been invited to join this room by { this.props.inviterName } +
+
+ Would you like to accept or decline this invitation? +
+
+ ); + + } + else if (this.props.canJoin) { + joinBlock = ( +
+
+ Would you like to join this room? +
+
+ ); + } + + if (this.props.canPreview) { + previewBlock = ( +
+ This is a preview of this room. Room interactions have been disabled.
); } return (
-
- This is a preview of this room. Room interactions have been disabled. +
+ { joinBlock } + { previewBlock }
- {joinBlock}
); } diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 284bee41c2..74b7ba7e7c 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -295,8 +295,8 @@ module.exports = React.createClass({ else { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Invalid alias format", - description: "'" + alias + "' is not a valid format for an alias", + title: "Invalid address format", + description: "'" + alias + "' is not a valid format for an address", }); } }, @@ -482,11 +482,11 @@ module.exports = React.createClass({ remote_aliases_section =
- This room can be found elsewhere as: + Remote addresses for this room:
{ remote_domains.map(function(state_key, i) { - self.state.aliases[state_key].map(function(alias, j) { + return self.state.aliases[state_key].map(function(alias, j) { return (
-
-
); }); @@ -513,7 +511,7 @@ module.exports = React.createClass({ return }); })} - + } else { @@ -522,24 +520,26 @@ module.exports = React.createClass({ var aliases_section =
-

Directory

+

Addresses

+
The main address for this room is: { canonical_alias_section }
{ this.state.aliases[domain].length - ? "This room can be found on " + domain + " as:" - : "This room is not findable on " + domain } + ? "Local addresses for this room:" + : "This room has no local addresses" }
{ this.state.aliases[domain].map(function(alias, i) { var deleteButton; if (can_set_room_aliases) { - deleteButton = Delete; + deleteButton = Delete; } return (
- Add + Add
{ remote_aliases_section } -
The official way to refer to this room is: { canonical_alias_section }
; var room_colors_section = @@ -597,23 +597,17 @@ module.exports = React.createClass({
; var user_levels_section; - if (user_levels.length) { + if (Object.keys(user_levels).length) { user_levels_section = -
-
- Users with specific roles are: -
-
- {Object.keys(user_levels).map(function(user, i) { - return ( -
- { user } is a - -
- ); - })} -
-
; +
    + {Object.keys(user_levels).map(function(user, i) { + return ( +
  • + { user } is a +
  • + ); + })} +
; } else { user_levels_section =
No users have specific privileges in this room.
@@ -659,7 +653,7 @@ module.exports = React.createClass({ var tags_section =
- This room is tagged as + Tagged as: { can_set_tag ? tags.map(function(tag, i) { return (
+ // FIXME: disable guests_read if the user hasn't turned on shared history return (
-
-
-
-
- { tags_section } +
+ + + + + + +
+ + { room_colors_section } { aliases_section } -

Notifications

-
- -
-

Permissions

@@ -735,12 +730,8 @@ module.exports = React.createClass({ { unfederatable_section }
-

Users

+

Privileged Users

-
- Your role in this room is currently . -
- { user_levels_section }
diff --git a/src/components/views/rooms/SearchableEntityList.js b/src/components/views/rooms/SearchableEntityList.js index b6232362ac..506a8ba92d 100644 --- a/src/components/views/rooms/SearchableEntityList.js +++ b/src/components/views/rooms/SearchableEntityList.js @@ -104,13 +104,16 @@ var SearchableEntityList = React.createClass({ } return ( -
+
{inputBox} -
- {this.state.results.map((entity) => { - return entity.getJsx(); - })} -
+ +
+ {this.state.results.map((entity) => { + return entity.getJsx(); + })} +
+
+ { this.state.results.length ?

: '' }
); } diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js index 7bd3d7754b..89303856b2 100644 --- a/src/components/views/settings/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.js @@ -122,8 +122,7 @@ module.exports = React.createClass({ height: this.props.height, objectFit: 'cover', }; - // FIXME: surely we should be using MemberAvatar or UserAvatar or something here... - avatarImg = ; + avatarImg = ; } var uploadSection;