diff --git a/.eslintrc.js b/.eslintrc.js index 429aa24993..fd4d1da631 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -33,6 +33,8 @@ module.exports = { // This just uses the react plugin to help eslint known when // variables have been used in JSX "react/jsx-uses-vars": "error", + // Don't mark React as unused if we're using JSX + "react/jsx-uses-react": "error", // bind or arrow function in props causes performance issues "react/jsx-no-bind": ["error", { diff --git a/package.json b/package.json index 883fdae8d5..e0c8e714c2 100644 --- a/package.json +++ b/package.json @@ -71,9 +71,10 @@ "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.3", "lodash": "^4.13.1", - "matrix-js-sdk": "0.8.5", + "matrix-js-sdk": "0.9.0-rc.1", "optimist": "^0.6.1", "prop-types": "^15.5.8", + "querystring": "^0.2.0", "react": "^15.4.0", "react-addons-css-transition-group": "15.3.2", "react-dom": "^15.4.0", diff --git a/src/UnknownDeviceErrorHandler.js b/src/UnknownDeviceErrorHandler.js index 664fe14eb5..e7d77b3b66 100644 --- a/src/UnknownDeviceErrorHandler.js +++ b/src/UnknownDeviceErrorHandler.js @@ -25,6 +25,7 @@ const onAction = function(payload) { const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog'); isDialogOpen = true; Modal.createTrackedDialog('Unknown Device Error', '', UnknownDeviceDialog, { + devices: payload.err.devices, room: payload.room, onFinished: (r) => { isDialogOpen = false; diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index c1d77b120b..8d60790812 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -26,10 +26,6 @@ import SdkConfig from './SdkConfig'; */ const FEATURES = [ - { - id: 'feature_groups', - name: _td("Communities"), - }, { id: 'feature_pinning', name: _td("Message Pinning"), diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index caa5e7cb01..1b5ebb6b36 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -435,14 +435,15 @@ export default React.createClass({ }, componentWillMount: function() { + this._matrixClient = MatrixClientPeg.get(); + this._matrixClient.on("Group.myMembership", this._onGroupMyMembership); + this._changeAvatarComponent = null; this._initGroupStore(this.props.groupId, true); - - MatrixClientPeg.get().on("Group.myMembership", this._onGroupMyMembership); }, componentWillUnmount: function() { - MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership); + this._matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership); this._groupStore.removeAllListeners(); }, @@ -464,11 +465,11 @@ export default React.createClass({ }, _initGroupStore: function(groupId, firstInit) { - const group = MatrixClientPeg.get().getGroup(groupId); + const group = this._matrixClient.getGroup(groupId); if (group && group.inviter && group.inviter.userId) { this._fetchInviterProfile(group.inviter.userId); } - this._groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId); + this._groupStore = GroupStoreCache.getGroupStore(this._matrixClient, groupId); this._groupStore.registerListener(() => { const summary = this._groupStore.getSummary(); if (summary.profile) { @@ -486,7 +487,7 @@ export default React.createClass({ groupRooms: this._groupStore.getGroupRooms(), groupRoomsLoading: !this._groupStore.isStateReady(GroupStore.STATE_KEY.GroupRooms), isUserMember: this._groupStore.getGroupMembers().some( - (m) => m.userId === MatrixClientPeg.get().credentials.userId, + (m) => m.userId === this._matrixClient.credentials.userId, ), error: null, }); @@ -506,7 +507,7 @@ export default React.createClass({ this.setState({ inviterProfileBusy: true, }); - MatrixClientPeg.get().getProfileInfo(userId).then((resp) => { + this._matrixClient.getProfileInfo(userId).then((resp) => { this.setState({ inviterProfile: { avatarUrl: resp.avatar_url, @@ -571,7 +572,7 @@ export default React.createClass({ if (!file) return; this.setState({uploadingAvatar: true}); - MatrixClientPeg.get().uploadContent(file).then((url) => { + this._matrixClient.uploadContent(file).then((url) => { const newProfileForm = Object.assign(this.state.profileForm, { avatar_url: url }); this.setState({ uploadingAvatar: false, @@ -591,7 +592,7 @@ export default React.createClass({ _onSaveClick: function() { this.setState({saving: true}); const savePromise = this.state.isUserPrivileged ? - MatrixClientPeg.get().setGroupProfile(this.props.groupId, this.state.profileForm) : + this._matrixClient.setGroupProfile(this.props.groupId, this.state.profileForm) : Promise.resolve(); savePromise.then((result) => { this.setState({ @@ -630,7 +631,7 @@ export default React.createClass({ _onRejectInviteClick: function() { this.setState({membershipBusy: true}); - MatrixClientPeg.get().leaveGroup(this.props.groupId).then(() => { + this._matrixClient.leaveGroup(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync }).catch((e) => { this.setState({membershipBusy: false}); @@ -653,7 +654,7 @@ export default React.createClass({ if (!confirmed) return; this.setState({membershipBusy: true}); - MatrixClientPeg.get().leaveGroup(this.props.groupId).then(() => { + this._matrixClient.leaveGroup(this.props.groupId).then(() => { // don't reset membershipBusy here: wait for the membership change to come down the sync }).catch((e) => { this.setState({membershipBusy: false}); @@ -829,7 +830,7 @@ export default React.createClass({ const Spinner = sdk.getComponent("elements.Spinner"); const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); - const group = MatrixClientPeg.get().getGroup(this.props.groupId); + const group = this._matrixClient.getGroup(this.props.groupId); if (!group) return null; if (group.myMembership === 'invite') { @@ -839,7 +840,7 @@ export default React.createClass({ ; } const httpInviterAvatar = this.state.inviterProfile ? - MatrixClientPeg.get().mxcUrlToHttp( + this._matrixClient.mxcUrlToHttp( this.state.inviterProfile.avatarUrl, 36, 36, ) : null; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 5972611af8..268654103d 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -74,6 +74,17 @@ const VIEWS = { LOGGED_IN: 6, }; +// Actions that are redirected through the onboarding process prior to being +// re-dispatched. NOTE: some actions are non-trivial and would require +// re-factoring to be included in this list in future. +const ONBOARDING_FLOW_STARTERS = [ + 'view_user_settings', + 'view_create_chat', + 'view_create_room', + 'view_my_groups', + 'view_group', +]; + module.exports = React.createClass({ // we export this so that the integration tests can use it :-S statics: { @@ -377,6 +388,22 @@ module.exports = React.createClass({ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + // Start the onboarding process for certain actions + if (MatrixClientPeg.get() && MatrixClientPeg.get().isGuest() && + ONBOARDING_FLOW_STARTERS.includes(payload.action) + ) { + // This will cause `payload` to be dispatched later, once a + // sync has reached the "prepared" state. Setting a matrix ID + // will cause a full login and sync and finally the deferred + // action will be dispatched. + dis.dispatch({ + action: 'do_after_sync_prepared', + deferred_action: payload, + }); + dis.dispatch({action: 'view_set_mxid'}); + return; + } + switch (payload.action) { case 'logout': Lifecycle.logout(); @@ -466,16 +493,6 @@ module.exports = React.createClass({ this._viewIndexedRoom(payload.roomIndex); break; case 'view_user_settings': - if (MatrixClientPeg.get().isGuest()) { - dis.dispatch({ - action: 'do_after_sync_prepared', - deferred_action: { - action: 'view_user_settings', - }, - }); - dis.dispatch({action: 'view_set_mxid'}); - break; - } this._setPage(PageTypes.UserSettings); this.notifyNewScreen('settings'); break; @@ -512,7 +529,7 @@ module.exports = React.createClass({ this._chatCreateOrReuse(payload.user_id, payload.go_home_on_cancel); break; case 'view_create_chat': - this._createChat(); + showStartChatInviteDialog(); break; case 'view_invite': showRoomInviteDialog(payload.roomId); @@ -753,31 +770,7 @@ module.exports = React.createClass({ }).close; }, - _createChat: function() { - if (MatrixClientPeg.get().isGuest()) { - dis.dispatch({ - action: 'do_after_sync_prepared', - deferred_action: { - action: 'view_create_chat', - }, - }); - dis.dispatch({action: 'view_set_mxid'}); - return; - } - showStartChatInviteDialog(); - }, - _createRoom: function() { - if (MatrixClientPeg.get().isGuest()) { - dis.dispatch({ - action: 'do_after_sync_prepared', - deferred_action: { - action: 'view_create_room', - }, - }); - dis.dispatch({action: 'view_set_mxid'}); - return; - } const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog'); Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, { onFinished: (shouldCreate, name, noFederate) => { diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js index 64e25df5f1..78d084b709 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.js +++ b/src/components/views/dialogs/ConfirmUserActionDialog.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import { MatrixClient } from 'matrix-js-sdk'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import classnames from 'classnames'; @@ -35,6 +36,8 @@ export default React.createClass({ member: React.PropTypes.object, // group member object. Supply either this or 'member' groupMember: GroupMemberType, + // needed if a group member is specified + matrixClient: React.PropTypes.instanceOf(MatrixClient), action: React.PropTypes.string.isRequired, // eg. 'Ban' title: React.PropTypes.string.isRequired, // eg. 'Ban this user?' @@ -104,10 +107,11 @@ export default React.createClass({ name = this.props.member.name; userId = this.props.member.userId; } else { - // we don't get this info from the API yet - avatar = ; - name = this.props.groupMember.userId; + const httpAvatarUrl = this.props.groupMember.avatarUrl ? + this.props.matrixClient.mxcUrlToHttp(this.props.groupMember.avatarUrl, 48, 48) : null; + name = this.props.groupMember.displayname || this.props.groupMember.userId; userId = this.props.groupMember.userId; + avatar = ; } return ( diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js index ff2accf0b0..ee8f307f76 100644 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -48,9 +48,8 @@ function UserUnknownDeviceList(props) { const {userId, userDevices} = props; const deviceListEntries = Object.keys(userDevices).map((deviceId) => - , + , ); return ( @@ -93,60 +92,26 @@ export default React.createClass({ propTypes: { room: React.PropTypes.object.isRequired, + // map from userid -> deviceid -> deviceinfo + devices: React.PropTypes.object.isRequired, onFinished: React.PropTypes.func.isRequired, }, - componentWillMount: function() { - this._unmounted = false; - - const roomMembers = this.props.room.getJoinedMembers().map((m) => { - return m.userId; - }); - - this.setState({ - // map from userid -> deviceid -> deviceinfo - devices: null, - }); - MatrixClientPeg.get().downloadKeys(roomMembers, false).then((devices) => { - if (this._unmounted) return; - - const unknownDevices = {}; - // This is all devices in this room, so find the unknown ones. - Object.keys(devices).forEach((userId) => { - Object.keys(devices[userId]).map((deviceId) => { - const device = devices[userId][deviceId]; - - if (device.isUnverified() && !device.isKnown()) { - if (unknownDevices[userId] === undefined) { - unknownDevices[userId] = {}; - } - unknownDevices[userId][deviceId] = device; - } - - // Given we've now shown the user the unknown device, it is no longer - // unknown to them. Therefore mark it as 'known'. - if (!device.isKnown()) { - MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true); - } - }); - }); - - this.setState({ - devices: unknownDevices, + componentDidMount: function() { + // Given we've now shown the user the unknown device, it is no longer + // unknown to them. Therefore mark it as 'known'. + Object.keys(this.props.devices).forEach((userId) => { + Object.keys(this.props.devices[userId]).map((deviceId) => { + MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true); }); }); - }, - componentWillUnmount: function() { - this._unmounted = true; + // XXX: temporary logging to try to diagnose + // https://github.com/vector-im/riot-web/issues/3148 + console.log('Opening UnknownDeviceDialog'); }, render: function() { - if (this.state.devices === null) { - const Spinner = sdk.getComponent("elements.Spinner"); - return ; - } - const client = MatrixClientPeg.get(); const blacklistUnverified = client.getGlobalBlacklistUnverifiedDevices() || this.props.room.getBlacklistUnverifiedDevices(); @@ -189,7 +154,7 @@ export default React.createClass({ { warning } { _t("Unknown devices") }: - +