diff --git a/CHANGELOG.md b/CHANGELOG.md index 586e6e18dc..9086006376 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,54 @@ +Changes in [0.5.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.5.1) (2016-04-19) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.4.0...v0.5.1) + + * Upgrade to react 15.0 + * Fix many thinkos in sorting the MemberList + [\#275](https://github.com/matrix-org/matrix-react-sdk/pull/275) + * Don't setState after unmounting a component + [\#276](https://github.com/matrix-org/matrix-react-sdk/pull/276) + * Drop workaround for object.onLoad + [\#274](https://github.com/matrix-org/matrix-react-sdk/pull/274) + * Make sure that we update the room name + [\#272](https://github.com/matrix-org/matrix-react-sdk/pull/272) + * Matthew/design tweaks + [\#273](https://github.com/matrix-org/matrix-react-sdk/pull/273) + * Hack around absence of String.codePointAt on PhantomJS + [\#271](https://github.com/matrix-org/matrix-react-sdk/pull/271) + * RoomView: Handle joining federated rooms + [\#270](https://github.com/matrix-org/matrix-react-sdk/pull/270) + * Stop the MatrixClient when the MatrixChat is unmounted + [\#269](https://github.com/matrix-org/matrix-react-sdk/pull/269) + * make the UI fadable to help with decluttering + [\#268](https://github.com/matrix-org/matrix-react-sdk/pull/268) + * URL previewing support + [\#260](https://github.com/matrix-org/matrix-react-sdk/pull/260) + * Remember to load new timeline events + [\#267](https://github.com/matrix-org/matrix-react-sdk/pull/267) + * Stop trying to paginate after we get a failure + [\#265](https://github.com/matrix-org/matrix-react-sdk/pull/265) + * Improvements to the react-sdk test framework + [\#264](https://github.com/matrix-org/matrix-react-sdk/pull/264) + * Fix password resetting + [\#263](https://github.com/matrix-org/matrix-react-sdk/pull/263) + * Catch pageup/down and ctrl-home/end at the top level + [\#262](https://github.com/matrix-org/matrix-react-sdk/pull/262) + * Fix an issue where the scroll stopped working. + [\#261](https://github.com/matrix-org/matrix-react-sdk/pull/261) + * Fix a bug where we tried to show two ghost read markers at once. + [\#254](https://github.com/matrix-org/matrix-react-sdk/pull/254) + * File upload improvements + [\#258](https://github.com/matrix-org/matrix-react-sdk/pull/258) + * Show full-size avatar on MemberInfo avatar click + [\#257](https://github.com/matrix-org/matrix-react-sdk/pull/257) + * Whitelist \ tag + [\#256](https://github.com/matrix-org/matrix-react-sdk/pull/256) + * Don't reload the DOM if we can jump straight to the RM + [\#253](https://github.com/matrix-org/matrix-react-sdk/pull/253) + +[0.5.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.5.0) was +incorrectly released. + Changes in [0.4.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.4.0) (2016-03-30) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.3.1...v0.4.0) diff --git a/karma.conf.js b/karma.conf.js index 0abbddb71b..52eea45f12 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -147,6 +147,11 @@ module.exports = function (config) { }, resolve: { alias: { + // alias any requires to the react module to the one in our + // path, otherwise we tend to get the react source included + // twice when using npm link. + react: path.resolve('./node_modules/react'), + 'matrix-react-sdk': path.resolve('test/skinned-sdk.js'), 'sinon': 'sinon/pkg/sinon.js', }, diff --git a/package.json b/package.json index c259700c92..c956560739 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.4.0", + "version": "0.5.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -17,7 +17,7 @@ "build": "babel src -d lib --source-maps", "start": "babel src -w -d lib --source-maps", "clean": "rimraf lib", - "prepublish": "npm run build; git rev-parse HEAD > git-revision.txt", + "prepublish": "npm run build && git rev-parse HEAD > git-revision.txt", "test": "karma start --browsers PhantomJS", "test-multi": "karma start --single-run=false" }, @@ -30,20 +30,20 @@ "highlight.js": "^8.9.1", "linkifyjs": "^2.0.0-beta.4", "marked": "^0.3.5", - "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "^0.5.2", "optimist": "^0.6.1", "q": "^1.4.1", - "react": "^0.14.2", - "react-dom": "^0.14.2", - "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#869a86b", + "react": "^15.0.1", + "react-dom": "^15.0.1", + "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#c3d942e", "sanitize-html": "^1.11.1", "velocity-animate": "^1.2.3", "velocity-ui-pack": "^1.2.2" }, "//babelversion": [ - "brief experiments with babel6 seems to show that it generates source ", - "maps which confuse chrome and make setting breakpoints tricky. So ", - "let's stick with v5 for now." + "brief experiments with babel6 seems to show that it generates source ", + "maps which confuse chrome and make setting breakpoints tricky. So ", + "let's stick with v5 for now." ], "devDependencies": { "babel": "^5.8.23", @@ -61,7 +61,7 @@ "karma-webpack": "^1.7.0", "mocha": "^2.4.5", "phantomjs-prebuilt": "^2.1.7", - "react-addons-test-utils": "^0.14.7", + "react-addons-test-utils": "^15.0.1", "require-json": "0.0.1", "rimraf": "^2.4.3", "sinon": "^1.17.3", diff --git a/reskindex.js b/reskindex.js index 37b3345e36..5cf9720373 100755 --- a/reskindex.js +++ b/reskindex.js @@ -37,7 +37,7 @@ if (packageJson['matrix-react-parent']) { strm.write("module.exports.components = {};\n"); } -var files = glob.sync('**/*.js', {cwd: componentsDir}); +var files = glob.sync('**/*.js', {cwd: componentsDir}).sort(); for (var i = 0; i < files.length; ++i) { var file = files[i].replace('.js', ''); diff --git a/src/ContentMessages.js b/src/ContentMessages.js index bbd714fa57..56e3499eae 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -74,10 +74,13 @@ class ContentMessages { var def = q.defer(); if (file.type.indexOf('image/') == 0) { content.msgtype = 'm.image'; - infoForImageFile(file).then(function(imageInfo) { + infoForImageFile(file).then(function (imageInfo) { extend(content.info, imageInfo); def.resolve(); }); + } else if (file.type.indexOf('audio/') == 0) { + content.msgtype = 'm.audio'; + def.resolve(); } else { content.msgtype = 'm.file'; def.resolve(); diff --git a/src/Tinter.js b/src/Tinter.js index a83ccdce74..4c4f90ebc0 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -15,11 +15,14 @@ limitations under the License. */ var dis = require("./dispatcher"); +var sdk = require("./index"); -// FIXME: these vars should be bundled up and attached to +// FIXME: these vars should be bundled up and attached to // module.exports otherwise this will break when included by both // react-sdk and apps layered on top. +var DEBUG = 0; + // The colour keys to be replaced as referred to in SVGs var keyRgb = [ "rgb(118, 207, 166)", // Vector Green @@ -75,6 +78,7 @@ var svgAttrs = [ var cached = false; function calcCssFixups() { + if (DEBUG) console.log("calcSvgFixups start"); for (var i = 0; i < document.styleSheets.length; i++) { var ss = document.styleSheets[i]; if (!ss) continue; // well done safari >:( @@ -105,13 +109,16 @@ function calcCssFixups() { } } } + if (DEBUG) console.log("calcSvgFixups end"); } function applyCssFixups() { + if (DEBUG) console.log("applyCssFixups start"); for (var i = 0; i < cssFixups.length; i++) { var cssFixup = cssFixups[i]; cssFixup.style[cssFixup.attr] = colors[cssFixup.index]; } + if (DEBUG) console.log("applyCssFixups end"); } function hexToRgb(color) { @@ -135,6 +142,7 @@ function rgbToHex(rgb) { module.exports = { tint: function(primaryColor, secondaryColor, tertiaryColor) { + if (!cached) { calcCssFixups(); cached = true; @@ -173,11 +181,19 @@ module.exports = { colors = [primaryColor, secondaryColor, tertiaryColor]; + if (DEBUG) console.log("Tinter.tint"); + // go through manually fixing up the stylesheets. applyCssFixups(); // tell all the SVGs to go fix themselves up - dis.dispatch({ action: 'tint_update' }); + // we don't do this as a dispatch otherwise it will visually lag + var TintableSvg = sdk.getComponent("elements.TintableSvg"); + if (TintableSvg.mounts) { + Object.keys(TintableSvg.mounts).forEach((id) => { + TintableSvg.mounts[id].tint(); + }); + } }, // XXX: we could just move this all into TintableSvg, but as it's so similar @@ -189,6 +205,7 @@ module.exports = { // updated would be a PITA, so just brute-force search for the // key colour; cache the element and apply. + if (DEBUG) console.log("calcSvgFixups start for " + svgs); var fixups = []; for (var i = 0; i < svgs.length; i++) { var svgDoc; @@ -223,14 +240,17 @@ module.exports = { } } } + if (DEBUG) console.log("calcSvgFixups end"); return fixups; }, applySvgFixups: function(fixups) { + if (DEBUG) console.log("applySvgFixups start for " + fixups); for (var i = 0; i < fixups.length; i++) { var svgFixup = fixups[i]; svgFixup.node.setAttribute(svgFixup.attr, colors[svgFixup.index]); } + if (DEBUG) console.log("applySvgFixups end"); }, }; diff --git a/src/component-index.js b/src/component-index.js index b5f5dd0a53..0cb7e257a0 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -68,6 +68,7 @@ module.exports.components['views.messages.MFileBody'] = require('./components/vi module.exports.components['views.messages.MImageBody'] = require('./components/views/messages/MImageBody'); module.exports.components['views.messages.MVideoBody'] = require('./components/views/messages/MVideoBody'); module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent'); +module.exports.components['views.messages.MAudioBody'] = require('./components/views/messages/MAudioBody'); module.exports.components['views.messages.TextualBody'] = require('./components/views/messages/TextualBody'); module.exports.components['views.messages.TextualEvent'] = require('./components/views/messages/TextualEvent'); module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody'); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 497251e5aa..09583951fc 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -67,6 +67,8 @@ module.exports = React.createClass({ collapse_rhs: false, ready: false, width: 10000, + sideOpacity: 1.0, + middleOpacity: 1.0, }; if (s.logged_in) { if (MatrixClientPeg.get().getRooms().length) { @@ -183,6 +185,7 @@ module.exports = React.createClass({ }, componentWillUnmount: function() { + this._stopMatrixClient(); dis.unregister(this.dispatcherRef); document.removeEventListener("keydown", this.onKeyDown); window.removeEventListener("focus", this.onFocus); @@ -258,12 +261,7 @@ module.exports = React.createClass({ window.localStorage.setItem("mx_hs_url", hsUrl); window.localStorage.setItem("mx_is_url", isUrl); } - Notifier.stop(); - UserActivity.stop(); - Presence.stop(); - MatrixClientPeg.get().stopClient(); - MatrixClientPeg.get().removeAllListeners(); - MatrixClientPeg.unset(); + this._stopMatrixClient(); this.notifyNewScreen('login'); this.replaceState({ logged_in: false, @@ -369,7 +367,7 @@ module.exports = React.createClass({ onFinished: function(should_leave) { if (should_leave) { var d = MatrixClientPeg.get().leave(roomId); - + // FIXME: controller shouldn't be loading a view :( var Loader = sdk.getComponent("elements.Spinner"); var modal = Modal.createDialog(Loader); @@ -534,6 +532,12 @@ module.exports = React.createClass({ collapse_rhs: false, }); break; + case 'ui_opacity': + this.setState({ + sideOpacity: payload.sideOpacity, + middleOpacity: payload.middleOpacity, + }); + break; } }, @@ -596,13 +600,15 @@ module.exports = React.createClass({ var theAlias = MatrixTools.getCanonicalAliasForRoom(room); if (theAlias) presentedId = theAlias; - var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme"); - var color_scheme = {}; - if (color_scheme_event) { - color_scheme = color_scheme_event.getContent(); - // XXX: we should validate the event - } - Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); + // No need to do this given RoomView triggers it itself... + // var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme"); + // var color_scheme = {}; + // if (color_scheme_event) { + // color_scheme = color_scheme_event.getContent(); + // // XXX: we should validate the event + // } + // console.log("Tinter.tint from _viewRoom"); + // Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); } if (eventId) { @@ -624,10 +630,13 @@ module.exports = React.createClass({ if (!this.refs.roomView) { return; } - var roomview = this.refs.roomView; + var roomId = this.refs.roomView.getRoomId(); + if (!roomId) { + return; + } var state = roomview.getScrollState(); - this.scrollStateMap[roomview.props.roomId] = state; + this.scrollStateMap[roomId] = state; }, onLoggedIn: function(credentials) { @@ -722,6 +731,16 @@ module.exports = React.createClass({ }); }, + // stop all the background processes related to the current client + _stopMatrixClient: function() { + Notifier.stop(); + UserActivity.stop(); + Presence.stop(); + MatrixClientPeg.get().stopClient(); + MatrixClientPeg.get().removeAllListeners(); + MatrixClientPeg.unset(); + }, + onKeyDown: function(ev) { /* // Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers @@ -887,7 +906,7 @@ module.exports = React.createClass({ dis.dispatch({ action: 'view_user', member: member, - }); + }); }, onLogoutClick: function(event) { @@ -1008,6 +1027,7 @@ 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) { dis.dispatch({ action: 'view_room', @@ -1034,7 +1054,7 @@ module.exports = React.createClass({ var NewVersionBar = sdk.getComponent('globals.NewVersionBar'); var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword'); - // work out the HS URL prompts we should show for + // work out the HS URL prompts we should show for // needs to be before normal PageTypes as you are logged in technically if (this.state.screen == 'post_registration') { @@ -1052,29 +1072,29 @@ module.exports = React.createClass({ page_element = ( ); - right_panel = + right_panel = break; case this.PageTypes.UserSettings: page_element = - right_panel = + right_panel = break; case this.PageTypes.CreateRoom: page_element = - right_panel = + right_panel = break; case this.PageTypes.RoomDirectory: page_element = - right_panel = + right_panel = break; } @@ -1098,7 +1118,7 @@ module.exports = React.createClass({
{topBar}
- +
{page_element}
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 8f5ffd9e56..defc8151a9 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -19,6 +19,8 @@ var ReactDOM = require("react-dom"); var dis = require("../../dispatcher"); var sdk = require('../../index'); +var MatrixClientPeg = require('../../MatrixClientPeg') + /* (almost) stateless UI component which builds the event tiles in the room timeline. */ module.exports = React.createClass({ @@ -65,6 +67,9 @@ module.exports = React.createClass({ // callback which is called when more content is needed. onFillRequest: React.PropTypes.func, + + // opacity for dynamic UI fading effects + opacity: React.PropTypes.number, }, componentWillMount: function() { @@ -147,7 +152,7 @@ module.exports = React.createClass({ this.refs.scrollPanel.scrollToBottom(); } }, - + /** * Page up/down. * @@ -332,13 +337,17 @@ module.exports = React.createClass({ // Local echos have a send "status". var scrollToken = mxEv.status ? undefined : eventId; + var readReceipts = this._getReadReceiptsForEvent(mxEv); + ret.push(
  • + readReceipts={readReceipts} + eventSendStatus={mxEv.status} + last={last} isSelectedEvent={highlight}/>
  • ); @@ -356,6 +365,30 @@ module.exports = React.createClass({ !== new Date(nextEventTs).toDateString()); }, + // get a list of the userids whose read receipts should + // be shown next to this event + _getReadReceiptsForEvent: function(event) { + var myUserId = MatrixClientPeg.get().credentials.userId; + + // get list of read receipts, sorted most recent first + var room = MatrixClientPeg.get().getRoom(event.getRoomId()); + if (!room) { + // huh. + return null; + } + + return room.getReceiptsForEvent(event).filter(function(r) { + return r.type === "m.read" && r.userId != myUserId; + }).sort(function(r1, r2) { + return r2.data.ts - r1.data.ts; + }).map(function(r) { + return room.getMember(r.userId); + }).filter(function(m) { + // check that the user is a known room member + return m; + }); + }, + _getReadMarkerTile: function(visible) { var hr; if (visible) { @@ -423,12 +456,15 @@ module.exports = React.createClass({ bottomSpinner =
  • ; } + var style = this.props.hidden ? { display: 'none' } : {}; + style.opacity = this.props.opacity; + return ( - {topSpinner} {this._getEventTiles()} diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 3749ee3bc6..ac848030af 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -54,11 +54,15 @@ module.exports = React.createClass({ propTypes: { ConferenceHandler: React.PropTypes.any, - roomId: React.PropTypes.string.isRequired, - - // if we are referring to this room by a given alias (e.g. in the URL), track it. - // useful for joining rooms by alias correctly (and fixing https://github.com/vector-im/vector-web/issues/819) - roomAlias: React.PropTypes.string, + // 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. + roomAddress: React.PropTypes.string.isRequired, // An object representing a third party invite to join this room // Fields: @@ -90,10 +94,13 @@ module.exports = React.createClass({ // ID of an event to highlight. If undefined, no event will be highlighted. // Typically this will either be the same as 'eventId', or undefined. highlightedEventId: React.PropTypes.string, + + // is the RightPanel collapsed? + rightPanelCollapsed: React.PropTypes.bool, }, getInitialState: function() { - var room = this.props.roomId ? MatrixClientPeg.get().getRoom(this.props.roomId) : null; + var room = MatrixClientPeg.get().getRoom(this.props.roomAddress); return { room: room, roomLoading: !room, @@ -123,7 +130,6 @@ module.exports = React.createClass({ this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on("Room", this.onRoom); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); - MatrixClientPeg.get().on("Room.name", this.onRoomName); MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData); MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); // xchat-style tab complete, add a colon if tab @@ -146,9 +152,9 @@ 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); + console.log("Attempting to peek into room %s", this.props.roomAddress); - MatrixClientPeg.get().peekInRoom(this.props.roomId).then((room) => { + MatrixClientPeg.get().peekInRoom(this.props.roomAddress).then((room) => { this.setState({ room: room, roomLoading: false, @@ -200,14 +206,15 @@ module.exports = React.createClass({ if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("Room", this.onRoom); MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); - MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData); MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); } window.removeEventListener('resize', this.onResize); - Tinter.tint(); // reset colourscheme + // no need to do this as Dir & Settings are now overlays. It just burnt CPU. + // console.log("Tinter.tint from RoomView.unmount"); + // Tinter.tint(); // reset colourscheme }, onAction: function(payload) { @@ -233,7 +240,7 @@ module.exports = React.createClass({ return; } - var call = CallHandler.getCallForRoom(payload.room_id); + var call = this._getCallForRoom(); var callState; if (call) { @@ -256,7 +263,7 @@ module.exports = React.createClass({ }, componentWillReceiveProps: function(newProps) { - if (newProps.roomId != this.props.roomId) { + if (newProps.roomAddress != this.props.roomAddress) { throw new Error("changing room on a RoomView is not supported"); } @@ -270,7 +277,7 @@ module.exports = React.createClass({ if (this.unmounted) return; // ignore events for other rooms - if (room.roomId != this.props.roomId) return; + if (!this.state.room || room.roomId != this.state.room.roomId) return; // ignore anything but real-time updates at the end of the room: // updates from pagination will happen when the paginate completes. @@ -286,7 +293,7 @@ module.exports = React.createClass({ // no change } else { - this.setState((state, props) => { + this.setState((state, props) => { return {numUnreadMessages: state.numUnreadMessages + 1}; }); } @@ -321,30 +328,18 @@ module.exports = React.createClass({ // set it in our state and start using it (ie. init the timeline) // This will happen if we start off viewing a room we're not joined, // then join it whilst RoomView is looking at that room. - if (room.roomId == this.props.roomId && !this.state.room) { + if (!this.state.room && room.roomId == this._joiningRoomId) { + this._joiningRoomId = undefined; this.setState({ - room: room + room: room, + joining: false, }); this._onRoomLoaded(room); } }, - onRoomName: function(room) { - // NB don't set state.room here. - // - // When peeking, this event lands *before* the timeline is correctly - // synced; if we set state.room here, the TimelinePanel will be - // instantiated, and it will initialise its scroll state, with *no - // events*. In short, the scroll state will be all messed up. - // - // There's no need to set state.room here anyway. - if (room.roomId == this.props.roomId) { - this.forceUpdate(); - } - }, - updateTint: function() { - var room = MatrixClientPeg.get().getRoom(this.props.roomId); + var room = this.state.room; if (!room) return; var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme"); @@ -352,7 +347,8 @@ module.exports = React.createClass({ if (color_scheme_event) { color_scheme = color_scheme_event.getContent(); // XXX: we should validate the event - } + } + console.log("Tinter.tint from updateTint"); Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); }, @@ -361,34 +357,40 @@ module.exports = React.createClass({ if (event.getType === "org.matrix.room.color_scheme") { var color_scheme = event.getContent(); // XXX: we should validate the event + console.log("Tinter.tint from onRoomAccountData"); Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); } } }, onRoomStateMember: function(ev, state, member) { - if (member.roomId === this.props.roomId) { - // a member state changed in this room, refresh the tab complete list - this._updateTabCompleteList(); - - var room = MatrixClientPeg.get().getRoom(this.props.roomId); - if (!room) return; - var me = MatrixClientPeg.get().credentials.userId; - if (this.state.joining && room.hasMembershipState(me, "join")) { - this.setState({ - joining: false - }); - } - } - - if (!this.props.ConferenceHandler) { + // ignore if we don't have a room yet + if (!this.state.room) { return; } - if (member.roomId !== this.props.roomId || - member.userId !== this.props.ConferenceHandler.getConferenceUserIdForRoom(member.roomId)) { + + // ignore members in other rooms + if (member.roomId !== this.state.room.roomId) { return; } - this._updateConfCallNotification(); + + // a member state changed in this room, refresh the tab complete list + this._updateTabCompleteList(); + + // if we are now a member of the room, where we were not before, that + // means we have finished joining a room we were previously peeking + // into. + var me = MatrixClientPeg.get().credentials.userId; + if (this.state.joining && this.state.room.hasMembershipState(me, "join")) { + this.setState({ + joining: false + }); + } + + if (this.props.ConferenceHandler && + member.userId === this.props.ConferenceHandler.getConferenceUserIdForRoom(member.roomId)) { + this._updateConfCallNotification(); + } }, _hasUnsentMessages: function(room) { @@ -403,12 +405,12 @@ module.exports = React.createClass({ }, _updateConfCallNotification: function() { - var room = MatrixClientPeg.get().getRoom(this.props.roomId); + var room = this.state.room; if (!room || !this.props.ConferenceHandler) { return; } var confMember = room.getMember( - this.props.ConferenceHandler.getConferenceUserIdForRoom(this.props.roomId) + this.props.ConferenceHandler.getConferenceUserIdForRoom(room.roomId) ); if (!confMember) { @@ -427,7 +429,7 @@ module.exports = React.createClass({ }, componentDidMount: function() { - var call = CallHandler.getCallForRoom(this.props.roomId); + var call = this._getCallForRoom(); var callState = call ? call.call_state : "ended"; this.setState({ callState: callState @@ -559,25 +561,35 @@ module.exports = React.createClass({ display_name_promise.then(() => { var sign_url = this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : undefined; - return MatrixClientPeg.get().joinRoom(this.props.roomAlias || this.props.roomId, + return MatrixClientPeg.get().joinRoom(this.props.roomAddress, { inviteSignUrl: sign_url } ) - }).done(function() { + }).then(function(resp) { + var roomId = resp.roomId; + // 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. + // joining flag set until we see it. // We'll need to initialise the timeline when joining, but due to // the above, we can't do it here: we do it in onRoom instead, // once we have a useable room object. - var room = MatrixClientPeg.get().getRoom(self.props.roomId); - var me = MatrixClientPeg.get().credentials.userId; - self.setState({ - joining: room ? !room.hasMembershipState(me, "join") : true, - room: room - }); - }, function(error) { + var room = MatrixClientPeg.get().getRoom(roomId); + if (!room) { + // wait for the room to turn up in onRoom. + self._joiningRoomId = roomId; + } else { + // we've got a valid room, but that might also just mean that + // it was peekable (so we had one before anyway). If we are + // not yet a member of the room, we will need to wait for that + // to happen, in onRoomStateMember. + var me = MatrixClientPeg.get().credentials.userId; + self.setState({ + joining: !room.hasMembershipState(me, "join"), + room: room + }); + } + }).catch(function(error) { self.setState({ joining: false, joinError: error @@ -612,7 +624,8 @@ module.exports = React.createClass({ description: msg }); } - }); + }).done(); + this.setState({ joining: true }); @@ -667,7 +680,7 @@ module.exports = React.createClass({ uploadFile: function(file) { var self = this; ContentMessages.sendContentToRoom( - file, this.props.roomId, MatrixClientPeg.get() + file, this.state.room.roomId, MatrixClientPeg.get() ).done(undefined, function(error) { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { @@ -702,7 +715,7 @@ module.exports = React.createClass({ filter = { // XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :( rooms: [ - this.props.roomId + this.state.room.roomId ] }; } @@ -860,6 +873,8 @@ module.exports = React.createClass({ }, onSettingsSaveClick: function() { + if (!this.refs.room_settings) return; + this.setState({ uploadingRoomSettings: true, }); @@ -901,6 +916,7 @@ module.exports = React.createClass({ }, onCancelClick: function() { + console.log("updateTint from onCancelClick"); this.updateTint(); this.setState({editingRoomSettings: false}); }, @@ -908,12 +924,12 @@ module.exports = React.createClass({ onLeaveClick: function() { dis.dispatch({ action: 'leave_room', - room_id: this.props.roomId, + room_id: this.state.room.roomId, }); }, onForgetClick: function() { - MatrixClientPeg.get().forget(this.props.roomId).done(function() { + MatrixClientPeg.get().forget(this.state.room.roomId).done(function() { dis.dispatch({ action: 'view_next_room' }); }, function(err) { var errCode = err.errcode || "unknown error code"; @@ -930,7 +946,7 @@ module.exports = React.createClass({ this.setState({ rejecting: true }); - MatrixClientPeg.get().leave(this.props.roomId).done(function() { + MatrixClientPeg.get().leave(this.props.roomAddress).done(function() { dis.dispatch({ action: 'view_next_room' }); self.setState({ rejecting: false @@ -984,7 +1000,8 @@ module.exports = React.createClass({ }, // update the read marker to match the read-receipt - forgetReadMarker: function() { + forgetReadMarker: function(ev) { + ev.stopPropagation(); this.refs.messagePanel.forgetReadMarker(); }, @@ -1087,7 +1104,7 @@ module.exports = React.createClass({ }, onMuteAudioClick: function() { - var call = CallHandler.getCallForRoom(this.props.roomId); + var call = this._getCallForRoom(); if (!call) { return; } @@ -1099,7 +1116,7 @@ module.exports = React.createClass({ }, onMuteVideoClick: function() { - var call = CallHandler.getCallForRoom(this.props.roomId); + var call = this._getCallForRoom(); if (!call) { return; } @@ -1139,11 +1156,35 @@ module.exports = React.createClass({ } }, + /** + * Get the ID of the displayed room + * + * Returns null if the RoomView was instantiated on a room alias and + * we haven't yet joined the room. + */ + getRoomId: function() { + if (!this.state.room) { + return null; + } + return this.state.room.roomId; + }, + + /** + * get any current call for this room + */ + _getCallForRoom: function() { + if (!this.state.room) { + return null; + } + return CallHandler.getCallForRoom(this.state.room.roomId); + }, + // this has to be a proper method rather than an unnamed function, // otherwise react calls it with null on each update. _gatherTimelinePanelRef: function(r) { this.refs.messagePanel = r; if(r) { + console.log("updateTint from RoomView._gatherTimelinePanelRef"); this.updateTint(); } }, @@ -1161,13 +1202,12 @@ module.exports = React.createClass({ var TimelinePanel = sdk.getComponent("structures.TimelinePanel"); if (!this.state.room) { - if (this.props.roomId) { if (this.state.roomLoading) { return (
    - ); + ); } else { var inviterName = undefined; @@ -1183,7 +1223,11 @@ module.exports = React.createClass({ // We've got to this room by following a link, possibly a third party invite. return (
    - +
    - ); + ); } - } - else { - return ( -
    - ); - } } var myUserId = MatrixClientPeg.get().credentials.userId; @@ -1233,7 +1271,7 @@ module.exports = React.createClass({ inviterName={ inviterName } canJoin={ true } canPreview={ false } spinner={this.state.joining} - room={this.state.room} + room={this.state.room} />
    @@ -1245,7 +1283,7 @@ module.exports = React.createClass({ // We have successfully loaded this room, and are not previewing. // Display the "normal" room view. - var call = CallHandler.getCallForRoom(this.props.roomId); + var call = this._getCallForRoom(); var inCall = false; if (call && (this.state.callState !== 'ended' && this.state.callState !== 'ringing')) { inCall = true; @@ -1257,13 +1295,6 @@ module.exports = React.createClass({ var statusBar; - // for testing UI... - // this.state.upload = { - // uploadedBytes: 123493, - // totalBytes: 347534, - // fileName: "testing_fooble.jpg", - // } - if (ContentMessages.getCurrentUploads().length > 0) { var UploadBar = sdk.getComponent('structures.UploadBar'); statusBar = @@ -1314,7 +1345,7 @@ module.exports = React.createClass({ inviterName={inviterName} invitedEmail={invitedEmail} canPreview={this.state.canPeek} - room={this.state.room} + room={this.state.room} /> ); } @@ -1339,7 +1370,7 @@ module.exports = React.createClass({ messageComposer = + callState={this.state.callState} tabComplete={this.tabComplete} opacity={ this.props.opacity }/> } // TODO: Why aren't we storing the term/scope/count in this format @@ -1394,8 +1425,12 @@ module.exports = React.createClass({ if (this.state.searchResults) { searchResultsPanel = ( - +
  • {this.getSearchResultTiles()}
    @@ -1412,13 +1447,14 @@ module.exports = React.createClass({ eventPixelOffset={this.props.eventPixelOffset} onScroll={ this.onMessageListScroll } onReadMarkerUpdated={ this._updateTopUnreadMessagesBar } + opacity={ this.props.opacity } />); var topUnreadMessagesBar = null; if (this.state.showTopUnreadMessagesBar) { var TopUnreadMessagesBar = sdk.getComponent('rooms.TopUnreadMessagesBar'); topUnreadMessagesBar = ( -
    +
    +
    { statusBar } diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index e62b67e314..77569be3bb 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -540,6 +540,7 @@ module.exports = React.createClass({ // it's not obvious why we have a separate div and ol anyway. return (
      diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 640ad66d72..5367b4208a 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -76,6 +76,9 @@ var TimelinePanel = React.createClass({ // callback which is called when the read-up-to mark is updated. onReadMarkerUpdated: React.PropTypes.func, + + // opacity for dynamic UI fading effects + opacity: React.PropTypes.number, }, statics: { @@ -172,8 +175,27 @@ var TimelinePanel = React.createClass({ }, shouldComponentUpdate: function(nextProps, nextState) { - return (!ObjectUtils.shallowEqual(this.props, nextProps) || - !ObjectUtils.shallowEqual(this.state, nextState)); + if (!ObjectUtils.shallowEqual(this.props, nextProps)) { + if (DEBUG) { + console.group("Timeline.shouldComponentUpdate: props change"); + console.log("props before:", this.props); + console.log("props after:", nextProps); + console.groupEnd(); + } + return true; + } + + if (!ObjectUtils.shallowEqual(this.state, nextState)) { + if (DEBUG) { + console.group("Timeline.shouldComponentUpdate: state change"); + console.log("state before:", this.state); + console.log("state after:", nextState); + console.groupEnd(); + } + return true; + } + + return false; }, componentWillUnmount: function() { @@ -222,8 +244,8 @@ var TimelinePanel = React.createClass({ this.setState({ [paginatingKey]: false, [canPaginateKey]: r, + events: this._getEvents(), }); - this._reloadEvents(); return r; }); }, @@ -264,25 +286,14 @@ var TimelinePanel = React.createClass({ // updates from pagination will happen when the paginate completes. if (toStartOfTimeline || !data || !data.liveEvent) return; - // even if we previously gave up forward-paginating, it's worth - // having another go now. - this.setState({canForwardPaginate: true}); - if (!this.refs.messagePanel) return; - if (!this.refs.messagePanel.getScrollState().stuckAtBottom) return; - - // when a new event arrives when the user is not watching the window, but the - // window is in its auto-scroll mode, make sure the read marker is visible. - // - // We ignore events we have sent ourselves; we don't want to see the - // read-marker when a remote echo of an event we have just sent takes - // more than the timeout on userCurrentlyActive. - // - var myUserId = MatrixClientPeg.get().credentials.userId; - var sender = ev.sender ? ev.sender.userId : null; - if (sender != myUserId && !UserActivity.userCurrentlyActive()) { - this.setState({readMarkerVisible: true}); + if (!this.refs.messagePanel.getScrollState().stuckAtBottom) { + // we won't load this event now, because we don't want to push any + // events off the other end of the timeline. But we need to note + // that we can now paginate. + this.setState({canForwardPaginate: true}); + return; } // tell the timeline window to try to advance itself, but not to make @@ -291,11 +302,46 @@ var TimelinePanel = React.createClass({ // we deliberately avoid going via the ScrollPanel for this call - the // ScrollPanel might already have an active pagination promise, which // will fail, but would stop us passing the pagination request to the - // timeline window. + // timeline window. // // see https://github.com/vector-im/vector-web/issues/1035 - this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false) - .done(this._reloadEvents); + this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).done(() => { + if (this.unmounted) { return; } + + var events = this._timelineWindow.getEvents(); + var lastEv = events[events.length-1]; + + // if we're at the end of the live timeline, append the pending events + if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) { + events.push(... this.props.room.getPendingEvents()); + } + + var updatedState = {events: events}; + + // when a new event arrives when the user is not watching the + // window, but the window is in its auto-scroll mode, make sure the + // read marker is visible. + // + // We ignore events we have sent ourselves; we don't want to see the + // read-marker when a remote echo of an event we have just sent takes + // more than the timeout on userCurrentlyActive. + // + var myUserId = MatrixClientPeg.get().credentials.userId; + var sender = ev.sender ? ev.sender.userId : null; + var callback = null; + if (sender != myUserId && !UserActivity.userCurrentlyActive()) { + updatedState.readMarkerVisible = true; + } else if(lastEv && this.getReadMarkerPosition() === 0) { + // we know we're stuckAtBottom, so we can advance the RM + // immediately, to save a later render cycle + this._setReadMarker(lastEv.getId(), lastEv.getTs(), true); + updatedState.readMarkerVisible = false; + updatedState.readMarkerEventId = lastEv.getId(); + callback = this.props.onReadMarkerUpdated; + } + + this.setState(updatedState, callback); + }); }, onRoomTimelineReset: function(room) { @@ -717,6 +763,13 @@ var TimelinePanel = React.createClass({ // the results if so. if (this.unmounted) return; + this.setState({ + events: this._getEvents(), + }); + }, + + // get the list of events from the timeline window and the pending event list + _getEvents: function() { var events = this._timelineWindow.getEvents(); // if we're at the end of the live timeline, append the pending events @@ -724,9 +777,7 @@ var TimelinePanel = React.createClass({ events.push(... this.props.room.getPendingEvents()); } - this.setState({ - events: events, - }); + return events; }, _indexForEventId: function(evId) { @@ -792,7 +843,7 @@ var TimelinePanel = React.createClass({ return this.props.room.getEventReadUpTo(myUserId, ignoreSynthesized); }, - _setReadMarker: function(eventId, eventTs) { + _setReadMarker: function(eventId, eventTs, inhibitSetState) { if (TimelinePanel.roomReadMarkerMap[this.props.room.roomId] == eventId) { // don't update the state (and cause a re-render) if there is // no change to the RM. @@ -807,6 +858,10 @@ var TimelinePanel = React.createClass({ // above or below the visible timeline, we stash the timestamp. TimelinePanel.roomReadMarkerTsMap[this.props.room.roomId] = eventTs; + if (inhibitSetState) { + return; + } + // run the render cycle before calling the callback, so that // getReadMarkerPosition() returns the right thing. this.setState({ @@ -861,6 +916,7 @@ var TimelinePanel = React.createClass({ stickyBottom={ stickyBottom } onScroll={ this.onMessageListScroll } onFillRequest={ this.onMessageListFillRequest } + opacity={ this.props.opacity } /> ); }, diff --git a/src/components/structures/UploadBar.js b/src/components/structures/UploadBar.js index 5463bc7161..794fcffec7 100644 --- a/src/components/structures/UploadBar.js +++ b/src/components/structures/UploadBar.js @@ -45,6 +45,17 @@ module.exports = React.createClass({displayName: 'UploadBar', render: function() { var uploads = ContentMessages.getCurrentUploads(); + + // for testing UI... - also fix up the ContentMessages.getCurrentUploads().length + // check in RoomView + // + // uploads = [{ + // roomId: this.props.room.roomId, + // loaded: 123493, + // total: 347534, + // fileName: "testing_fooble.jpg", + // }]; + if (uploads.length == 0) { return
      } diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 5c0e6cbf09..e2a28f0cef 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -51,7 +51,11 @@ module.exports = React.createClass({ }, componentWillMount: function() { - var self = this; + dis.dispatch({ + action: 'ui_opacity', + sideOpacity: 0.3, + middleOpacity: 0.3, + }); this._refreshFromServer(); }, @@ -61,6 +65,11 @@ module.exports = React.createClass({ }, componentWillUnmount: function() { + dis.dispatch({ + action: 'ui_opacity', + sideOpacity: 1.0, + middleOpacity: 1.0, + }); dis.unregister(this.dispatcherRef); }, @@ -321,7 +330,7 @@ module.exports = React.createClass({ var notification_area; if (!MatrixClientPeg.get().isGuest() && this.state.threepids !== undefined) { notification_area = (
      -

      Notifications

      +

      Notifications

      @@ -331,11 +340,13 @@ module.exports = React.createClass({ return (
      - + - + -

      Profile

      +

      Profile

      @@ -366,10 +377,10 @@ module.exports = React.createClass({
      -

      Account

      +

      Account

      - +
      Log out
      @@ -379,7 +390,7 @@ module.exports = React.createClass({ {notification_area} -

      Advanced

      +

      Advanced

      diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 52f0b77387..121540a8c0 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -99,15 +99,36 @@ module.exports = React.createClass({ } }, - _getInitialLetter: function() { - var name = this.props.name; - //For large characters (exceeding 2 bytes), this function will get the correct character. - //However, this does NOT get the second character correctly if a large character is before it. - var initial = String.fromCodePoint(name.codePointAt(0)); - if ((initial === '@' || initial === '#') && name[1]) { - initial = String.fromCodePoint(name.codePointAt(1)); + /** + * returns the first (non-sigil) character of 'name', + * converted to uppercase + */ + _getInitialLetter: function(name) { + if (name.length < 1) { + return undefined; } - return initial.toUpperCase(); + + var idx = 0; + var initial = name[0]; + if ((initial === '@' || initial === '#') && name[1]) { + idx++; + } + + // string.codePointAt(0) would do this, but that isn't supported by + // some browsers (notably PhantomJS). + var chars = 1; + var first = name.charCodeAt(idx); + + // check if it’s the start of a surrogate pair + if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) { + var second = name.charCodeAt(idx+1); + if (second >= 0xDC00 && second <= 0xDFFF) { + chars++; + } + } + + var firstChar = name.substring(idx, idx+chars); + return firstChar.toUpperCase(); }, render: function() { @@ -116,7 +137,7 @@ module.exports = React.createClass({ var imageUrl = this.state.imageUrls[this.state.urlsIndex]; if (imageUrl === this.state.defaultImageUrl) { - var initialLetter = this._getInitialLetter(); + var initialLetter = this._getInitialLetter(this.props.name); return (