Bug 1126733 - Brief message appears when entering a standalone room that the user is the only person in the room [r=Standard8]

This commit is contained in:
Ed Lee 2015-08-13 14:36:16 -07:00
parent 8ab64f8f73
commit a2a29ccf86
5 changed files with 276 additions and 11 deletions

View File

@ -199,6 +199,12 @@ loop.shared.actions = (function() {
publisherConfig: Object
}),
/**
* Used for notifying that a waiting tile was shown.
*/
TileShown: Action.define("tileShown", {
}),
/**
* Used for notifying that local media has been obtained.
*/

View File

@ -51,6 +51,7 @@ loop.store.StandaloneMetricsStore = (function() {
"recordClick",
"remotePeerConnected",
"retryAfterRoomFailure",
"tileShown",
"windowUnload"
],
@ -201,6 +202,14 @@ loop.store.StandaloneMetricsStore = (function() {
"Retry failed room");
},
/**
* Handles when a tile was finally shown (potentially after a delay)
*/
tileShown: function() {
this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.pageLoad,
"Tile shown");
},
/**
* Handles notifications that the activeRoomStore has changed, updating
* the metrics for room state and mute state as necessary.

View File

@ -77,6 +77,10 @@ loop.standaloneRoomViews = (function(mozL10n) {
});
var StandaloneRoomInfoArea = React.createClass({displayName: "StandaloneRoomInfoArea",
statics: {
RENDER_WAITING_DELAY: 2000
},
propTypes: {
activeRoomStore: React.PropTypes.oneOfType([
React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
@ -90,11 +94,60 @@ loop.standaloneRoomViews = (function(mozL10n) {
roomUsed: React.PropTypes.bool.isRequired
},
getInitialState: function() {
return { waitToRenderWaiting: true };
},
componentDidMount: function() {
// Watch for messages from the waiting-tile iframe
window.addEventListener("message", this.recordTileClick);
},
/**
* Change state to allow for the waiting message to be shown and send an
* event to record that fact.
*/
_allowRenderWaiting: function() {
delete this._waitTimer;
// Only update state if we're still showing a waiting message.
switch (this.props.roomState) {
case ROOM_STATES.JOINING:
case ROOM_STATES.JOINED:
case ROOM_STATES.SESSION_CONNECTED:
this.setState({ waitToRenderWaiting: false });
this.props.dispatcher.dispatch(new sharedActions.TileShown());
break;
}
},
componentDidUpdate: function() {
// Start a timer once from the earliest waiting state if we need to wait
// before showing a message.
if (this.props.roomState === ROOM_STATES.JOINING &&
this.state.waitToRenderWaiting &&
this._waitTimer === undefined) {
this._waitTimer = setTimeout(this._allowRenderWaiting,
this.constructor.RENDER_WAITING_DELAY);
}
},
componentWillReceiveProps: function(nextProps) {
switch (nextProps.roomState) {
// Reset waiting for the next time the user joins.
case ROOM_STATES.ENDED:
case ROOM_STATES.READY:
if (!this.state.waitToRenderWaiting) {
this.setState({ waitToRenderWaiting: true });
}
if (this._waitTimer !== undefined) {
clearTimeout(this._waitTimer);
delete this._waitTimer;
}
break;
}
},
componentWillUnmount: function() {
window.removeEventListener("message", this.recordTileClick);
},
@ -170,6 +223,12 @@ loop.standaloneRoomViews = (function(mozL10n) {
case ROOM_STATES.JOINING:
case ROOM_STATES.JOINED:
case ROOM_STATES.SESSION_CONNECTED: {
// Don't show the waiting display until after a brief wait in case
// there's another participant that will momentarily appear.
if (this.state.waitToRenderWaiting) {
return null;
}
return (
React.createElement("div", {className: "room-inner-info-area"},
React.createElement("p", {className: "empty-room-message"},

View File

@ -77,6 +77,10 @@ loop.standaloneRoomViews = (function(mozL10n) {
});
var StandaloneRoomInfoArea = React.createClass({
statics: {
RENDER_WAITING_DELAY: 2000
},
propTypes: {
activeRoomStore: React.PropTypes.oneOfType([
React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
@ -90,11 +94,60 @@ loop.standaloneRoomViews = (function(mozL10n) {
roomUsed: React.PropTypes.bool.isRequired
},
getInitialState: function() {
return { waitToRenderWaiting: true };
},
componentDidMount: function() {
// Watch for messages from the waiting-tile iframe
window.addEventListener("message", this.recordTileClick);
},
/**
* Change state to allow for the waiting message to be shown and send an
* event to record that fact.
*/
_allowRenderWaiting: function() {
delete this._waitTimer;
// Only update state if we're still showing a waiting message.
switch (this.props.roomState) {
case ROOM_STATES.JOINING:
case ROOM_STATES.JOINED:
case ROOM_STATES.SESSION_CONNECTED:
this.setState({ waitToRenderWaiting: false });
this.props.dispatcher.dispatch(new sharedActions.TileShown());
break;
}
},
componentDidUpdate: function() {
// Start a timer once from the earliest waiting state if we need to wait
// before showing a message.
if (this.props.roomState === ROOM_STATES.JOINING &&
this.state.waitToRenderWaiting &&
this._waitTimer === undefined) {
this._waitTimer = setTimeout(this._allowRenderWaiting,
this.constructor.RENDER_WAITING_DELAY);
}
},
componentWillReceiveProps: function(nextProps) {
switch (nextProps.roomState) {
// Reset waiting for the next time the user joins.
case ROOM_STATES.ENDED:
case ROOM_STATES.READY:
if (!this.state.waitToRenderWaiting) {
this.setState({ waitToRenderWaiting: true });
}
if (this._waitTimer !== undefined) {
clearTimeout(this._waitTimer);
delete this._waitTimer;
}
break;
}
},
componentWillUnmount: function() {
window.removeEventListener("message", this.recordTileClick);
},
@ -170,6 +223,12 @@ loop.standaloneRoomViews = (function(mozL10n) {
case ROOM_STATES.JOINING:
case ROOM_STATES.JOINED:
case ROOM_STATES.SESSION_CONNECTED: {
// Don't show the waiting display until after a brief wait in case
// there's another participant that will momentarily appear.
if (this.state.waitToRenderWaiting) {
return null;
}
return (
<div className="room-inner-info-area">
<p className="empty-room-message">

View File

@ -17,7 +17,7 @@ describe("loop.standaloneRoomViews", function() {
var fixtures = document.querySelector("#fixtures");
var sandbox, dispatcher, activeRoomStore, dispatch;
var fakeWindow;
var clock, fakeWindow;
beforeEach(function() {
sandbox = sinon.sandbox.create();
@ -35,7 +35,7 @@ describe("loop.standaloneRoomViews", function() {
textChatStore: textChatStore
});
sandbox.useFakeTimers();
clock = sandbox.useFakeTimers();
fakeWindow = {
close: sandbox.stub(),
addEventListener: function() {},
@ -61,6 +61,7 @@ describe("loop.standaloneRoomViews", function() {
afterEach(function() {
loop.shared.mixins.setRootObject(window);
sandbox.restore();
clock.restore();
React.unmountComponentAtNode(fixtures);
});
@ -92,23 +93,27 @@ describe("loop.standaloneRoomViews", function() {
loop.config.tilesIframeUrl = "data:text/html,<script>parent.postMessage('tile-click', '*');</script>";
// Render the iframe into the fixture to cause it to load
React.render(
var view = React.render(
React.createElement(
loop.standaloneRoomViews.StandaloneRoomInfoArea, {
activeRoomStore: activeRoomStore,
dispatcher: dispatcher,
isFirefox: true,
joinRoom: sandbox.stub(),
roomState: ROOM_STATES.JOINED,
roomState: ROOM_STATES.INIT,
roomUsed: false
}), fixtures);
// Change states and move time to get the iframe to load
view.setProps({ roomState: ROOM_STATES.JOINING });
clock.tick(loop.standaloneRoomViews.StandaloneRoomInfoArea.RENDER_WAITING_DELAY);
// Wait for the iframe to load and trigger a message that should also
// cause the RecordClick action
window.addEventListener("message", function onMessage() {
window.removeEventListener("message", onMessage);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledTwice(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.RecordClick({
linkInfo: "Tiles iframe click"
@ -166,6 +171,103 @@ describe("loop.standaloneRoomViews", function() {
});
});
describe("#componentDidUpdate", function() {
var view;
beforeEach(function() {
view = mountTestComponent();
activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINING});
activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED});
});
it("should not dispatch a `TileShown` action immediately in the JOINED state",
function() {
sinon.assert.notCalled(dispatch);
});
it("should dispatch a `TileShown` action after a wait when in the JOINED state",
function() {
clock.tick(loop.standaloneRoomViews.StandaloneRoomInfoArea.RENDER_WAITING_DELAY);
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWithExactly(dispatch, new sharedActions.TileShown());
});
it("should dispatch a single `TileShown` action after a wait when going through multiple waiting states",
function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.SESSION_CONNECTED});
clock.tick(loop.standaloneRoomViews.StandaloneRoomInfoArea.RENDER_WAITING_DELAY);
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWithExactly(dispatch, new sharedActions.TileShown());
});
it("should not dispatch a `TileShown` action after a wait when in the HAS_PARTICIPANTS state",
function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.HAS_PARTICIPANTS});
clock.tick(loop.standaloneRoomViews.StandaloneRoomInfoArea.RENDER_WAITING_DELAY);
sinon.assert.notCalled(dispatch);
});
});
describe("#componentWillReceiveProps", function() {
var view;
beforeEach(function() {
view = mountTestComponent();
// Pretend the user waited a little bit
activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINING});
clock.tick(loop.standaloneRoomViews.StandaloneRoomInfoArea.RENDER_WAITING_DELAY - 1);
});
describe("Support multiple joins", function() {
it("should send the first `TileShown` after waiting in JOINING state",
function() {
clock.tick(1);
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWithExactly(dispatch, new sharedActions.TileShown());
});
it("should send the second `TileShown` after ending and rejoining",
function() {
// Trigger the first message then rejoin and wait
clock.tick(1);
activeRoomStore.setStoreState({roomState: ROOM_STATES.ENDED});
activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINING});
clock.tick(loop.standaloneRoomViews.StandaloneRoomInfoArea.RENDER_WAITING_DELAY);
sinon.assert.calledTwice(dispatch);
sinon.assert.calledWithExactly(dispatch, new sharedActions.TileShown());
});
});
describe("Handle leaving quickly", function() {
beforeEach(function() {
// The user left and rejoined
activeRoomStore.setStoreState({roomState: ROOM_STATES.ENDED});
activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINING});
});
it("should not dispatch an old `TileShown` action after leaving and rejoining",
function() {
clock.tick(1);
sinon.assert.notCalled(dispatch);
});
it("should dispatch a new `TileShown` action after leaving and rejoining and waiting",
function() {
clock.tick(loop.standaloneRoomViews.StandaloneRoomInfoArea.RENDER_WAITING_DELAY);
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWithExactly(dispatch, new sharedActions.TileShown());
});
});
});
describe("#publishStream", function() {
var view;
@ -205,29 +307,57 @@ describe("loop.standaloneRoomViews", function() {
beforeEach(function() {
view = mountTestComponent();
activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINING});
});
describe("Empty room message", function() {
it("should display an empty room message on JOINED",
it("should not display an message immediately in the JOINED state",
function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED});
expect(view.getDOMNode().querySelector(".empty-room-message"))
.not.eql(null);
.eql(null);
});
it("should display an empty room message on SESSION_CONNECTED",
it("should display an empty room message after a wait when in the JOINED state",
function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.SESSION_CONNECTED});
activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED});
clock.tick(loop.standaloneRoomViews.StandaloneRoomInfoArea.RENDER_WAITING_DELAY);
expect(view.getDOMNode().querySelector(".empty-room-message"))
.not.eql(null);
});
it("shouldn't display an empty room message on HAS_PARTICIPANTS",
it("should not display an message immediately in the SESSION_CONNECTED state",
function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.SESSION_CONNECTED});
expect(view.getDOMNode().querySelector(".empty-room-message"))
.eql(null);
});
it("should display an empty room message after a wait when in the SESSION_CONNECTED state",
function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.SESSION_CONNECTED});
clock.tick(loop.standaloneRoomViews.StandaloneRoomInfoArea.RENDER_WAITING_DELAY);
expect(view.getDOMNode().querySelector(".empty-room-message"))
.not.eql(null);
});
it("should not display an message immediately in the HAS_PARTICIPANTS state",
function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.HAS_PARTICIPANTS});
expect(view.getDOMNode().querySelector(".empty-room-message"))
.eql(null);
});
it("should not display an empty room message even after a wait when in the HAS_PARTICIPANTS state",
function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.HAS_PARTICIPANTS});
clock.tick(loop.standaloneRoomViews.StandaloneRoomInfoArea.RENDER_WAITING_DELAY);
expect(view.getDOMNode().querySelector(".empty-room-message"))
.eql(null);
});
@ -238,6 +368,7 @@ describe("loop.standaloneRoomViews", function() {
var DUMMY_TILE_URL = "http://tile/";
loop.config.tilesIframeUrl = DUMMY_TILE_URL;
activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED});
clock.tick(loop.standaloneRoomViews.StandaloneRoomInfoArea.RENDER_WAITING_DELAY);
expect(view.getDOMNode().querySelector(".room-waiting-area")).not.eql(null);
@ -248,10 +379,11 @@ describe("loop.standaloneRoomViews", function() {
it("should dispatch a RecordClick action when the tile support link is clicked", function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED});
clock.tick(loop.standaloneRoomViews.StandaloneRoomInfoArea.RENDER_WAITING_DELAY);
TestUtils.Simulate.click(view.getDOMNode().querySelector(".room-waiting-area a"));
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledTwice(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.RecordClick({
linkInfo: "Tiles support link click"