Bug 1074709 - Notify Loop room users when the room is full. r=Standard8

This commit is contained in:
Nicolas Perriault 2014-11-13 15:33:15 +00:00
parent 3c9e87d1cf
commit f8b3a3aaa8
16 changed files with 260 additions and 76 deletions

View File

@ -232,7 +232,10 @@ loop.roomViews = (function(mozL10n) {
}); });
switch(this.state.roomState) { switch(this.state.roomState) {
case ROOM_STATES.FAILED: { case ROOM_STATES.FAILED:
case ROOM_STATES.FULL: {
// Note: While rooms are set to hold a maximum of 2 participants, the
// FULL case should never happen on desktop.
return loop.conversation.GenericFailureView({ return loop.conversation.GenericFailureView({
cancelCall: this.closeWindow} cancelCall: this.closeWindow}
); );

View File

@ -232,7 +232,10 @@ loop.roomViews = (function(mozL10n) {
}); });
switch(this.state.roomState) { switch(this.state.roomState) {
case ROOM_STATES.FAILED: { case ROOM_STATES.FAILED:
case ROOM_STATES.FULL: {
// Note: While rooms are set to hold a maximum of 2 participants, the
// FULL case should never happen on desktop.
return <loop.conversation.GenericFailureView return <loop.conversation.GenericFailureView
cancelCall={this.closeWindow} cancelCall={this.closeWindow}
/>; />;

View File

@ -753,6 +753,7 @@ html, .fx-embedded, #main,
width: 50%; width: 50%;
color: #fff; color: #fff;
font-weight: bold; font-weight: bold;
font-size: 1.1em;
} }
.standalone .room-inner-info-area button { .standalone .room-inner-info-area button {
@ -762,6 +763,12 @@ html, .fx-embedded, #main,
cursor: pointer; cursor: pointer;
} }
.standalone .room-inner-info-area a.btn {
padding: .5em 3em .3em 3em;
border-radius: 3px;
font-weight: normal;
}
.standalone .room-conversation h2.room-name { .standalone .room-conversation h2.room-name {
position: absolute; position: absolute;
display: inline-block; display: inline-block;

View File

@ -25,7 +25,9 @@ loop.store.ActiveRoomStore = (function() {
// There are participants in the room. // There are participants in the room.
HAS_PARTICIPANTS: "room-has-participants", HAS_PARTICIPANTS: "room-has-participants",
// There was an issue with the room // There was an issue with the room
FAILED: "room-failed" FAILED: "room-failed",
// The room is full
FULL: "room-full"
}; };
/** /**
@ -105,8 +107,7 @@ loop.store.ActiveRoomStore = (function() {
}, },
/** /**
* Handles a room failure. Currently this prints the error to the console * Handles a room failure.
* and sets the roomState to failed.
* *
* @param {sharedActions.RoomFailure} actionData * @param {sharedActions.RoomFailure} actionData
*/ */
@ -116,7 +117,8 @@ loop.store.ActiveRoomStore = (function() {
this.setStoreState({ this.setStoreState({
error: actionData.error, error: actionData.error,
roomState: ROOM_STATES.FAILED roomState: actionData.error.errno === 202 ? ROOM_STATES.FULL
: ROOM_STATES.FAILED
}); });
}, },

View File

@ -18,6 +18,7 @@ LOOP_FEEDBACK_PRODUCT_NAME := $(shell echo $${LOOP_FEEDBACK_PRODUCT_NAME-Loop})
LOOP_BRAND_WEBSITE_URL := $(shell echo $${LOOP_BRAND_WEBSITE_URL-"https://www.mozilla.org/firefox/"}) LOOP_BRAND_WEBSITE_URL := $(shell echo $${LOOP_BRAND_WEBSITE_URL-"https://www.mozilla.org/firefox/"})
LOOP_PRIVACY_WEBSITE_URL := $(shell echo $${LOOP_PRIVACY_WEBSITE_URL-"https://www.mozilla.org/privacy"}) LOOP_PRIVACY_WEBSITE_URL := $(shell echo $${LOOP_PRIVACY_WEBSITE_URL-"https://www.mozilla.org/privacy"})
LOOP_LEGAL_WEBSITE_URL := $(shell echo $${LOOP_LEGAL_WEBSITE_URL-"/legal/terms"}) LOOP_LEGAL_WEBSITE_URL := $(shell echo $${LOOP_LEGAL_WEBSITE_URL-"/legal/terms"})
LOOP_PRODUCT_HOMEPAGE_URL := $(shell echo $${LOOP_PRODUCT_HOMEPAGE_URL-"https://www.firefox.com/hello/"})
NODE_LOCAL_BIN=./node_modules/.bin NODE_LOCAL_BIN=./node_modules/.bin
@ -79,6 +80,7 @@ config:
@echo "loop.config.brandWebsiteUrl = '`echo $(LOOP_BRAND_WEBSITE_URL)`';" >> content/config.js @echo "loop.config.brandWebsiteUrl = '`echo $(LOOP_BRAND_WEBSITE_URL)`';" >> content/config.js
@echo "loop.config.privacyWebsiteUrl = '`echo $(LOOP_PRIVACY_WEBSITE_URL)`';" >> content/config.js @echo "loop.config.privacyWebsiteUrl = '`echo $(LOOP_PRIVACY_WEBSITE_URL)`';" >> content/config.js
@echo "loop.config.legalWebsiteUrl = '`echo $(LOOP_LEGAL_WEBSITE_URL)`';" >> content/config.js @echo "loop.config.legalWebsiteUrl = '`echo $(LOOP_LEGAL_WEBSITE_URL)`';" >> content/config.js
@echo "loop.config.learnMoreUrl = '`echo $(LOOP_PRODUCT_HOMEPAGE_URL)`';" >> content/config.js
@echo "loop.config.fxosApp = loop.config.fxosApp || {};" >> content/config.js @echo "loop.config.fxosApp = loop.config.fxosApp || {};" >> content/config.js
@echo "loop.config.fxosApp.name = 'Loop';" >> content/config.js @echo "loop.config.fxosApp.name = 'Loop';" >> content/config.js
@echo "loop.config.fxosApp.manifestUrl = 'http://fake-market.herokuapp.com/apps/packagedApp/manifest.webapp';" >> content/config.js @echo "loop.config.fxosApp.manifestUrl = 'http://fake-market.herokuapp.com/apps/packagedApp/manifest.webapp';" >> content/config.js

View File

@ -47,7 +47,7 @@ loop.StandaloneMozLoop = (function(mozL10n) {
var message = "HTTP " + jqXHR.status + " " + errorThrown; var message = "HTTP " + jqXHR.status + " " + errorThrown;
// Create an error with server error `errno` code attached as a property // Create an error with server error `errno` code attached as a property
var err = new Error(message); var err = new Error(message + (jsonErr.error ? "; " + jsonErr.error : ""));
err.errno = jsonErr.errno; err.errno = jsonErr.errno;
callback(err); callback(err);

View File

@ -5,6 +5,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */ * You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global loop:true, React */ /* global loop:true, React */
/* jshint newcap:false, maxlen:false */
var loop = loop || {}; var loop = loop || {};
loop.standaloneRoomViews = (function(mozL10n) { loop.standaloneRoomViews = (function(mozL10n) {
@ -14,6 +15,72 @@ loop.standaloneRoomViews = (function(mozL10n) {
var sharedActions = loop.shared.actions; var sharedActions = loop.shared.actions;
var sharedViews = loop.shared.views; var sharedViews = loop.shared.views;
var StandaloneRoomInfoArea = React.createClass({displayName: 'StandaloneRoomInfoArea',
propTypes: {
helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired
},
_renderCallToActionLink: function() {
if (this.props.helper.isFirefox(navigator.userAgent)) {
return (
React.DOM.a({href: loop.config.learnMoreUrl, className: "btn btn-info"},
mozL10n.get("rooms_room_full_call_to_action_label", {
clientShortname: mozL10n.get("clientShortname2")
})
)
);
}
return (
React.DOM.a({href: loop.config.brandWebsiteUrl, className: "btn btn-info"},
mozL10n.get("rooms_room_full_call_to_action_nonFx_label", {
brandShortname: mozL10n.get("brandShortname")
})
)
);
},
_renderContent: function() {
switch(this.props.roomState) {
case ROOM_STATES.INIT:
case ROOM_STATES.READY: {
return (
React.DOM.button({className: "btn btn-join btn-info",
onClick: this.props.joinRoom},
mozL10n.get("rooms_room_join_label")
)
);
}
case ROOM_STATES.JOINED:
case ROOM_STATES.SESSION_CONNECTED: {
return (
React.DOM.p({className: "empty-room-message"},
mozL10n.get("rooms_only_occupant_label")
)
);
}
case ROOM_STATES.FULL:
return (
React.DOM.div(null,
React.DOM.p({className: "full-room-message"},
mozL10n.get("rooms_room_full_label")
),
React.DOM.p(null, this._renderCallToActionLink())
)
);
default:
return null;
}
},
render: function() {
return (
React.DOM.div({className: "room-inner-info-area"},
this._renderContent()
)
);
}
});
var StandaloneRoomView = React.createClass({displayName: 'StandaloneRoomView', var StandaloneRoomView = React.createClass({displayName: 'StandaloneRoomView',
mixins: [Backbone.Events], mixins: [Backbone.Events],
@ -21,6 +88,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
activeRoomStore: activeRoomStore:
React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired, React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired
}, },
getInitialState: function() { getInitialState: function() {
@ -129,35 +197,6 @@ loop.standaloneRoomViews = (function(mozL10n) {
this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS; this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS;
}, },
_renderContextualRoomInfo: function() {
switch(this.state.roomState) {
case ROOM_STATES.INIT:
case ROOM_STATES.READY: {
// Join button
return (
React.DOM.div({className: "room-inner-info-area"},
React.DOM.button({className: "btn btn-join btn-info", onClick: this.joinRoom},
mozL10n.get("rooms_room_join_label")
)
)
);
}
case ROOM_STATES.JOINED:
case ROOM_STATES.SESSION_CONNECTED: {
// Empty room message
return (
React.DOM.div({className: "room-inner-info-area"},
React.DOM.p({className: "empty-room-message"},
mozL10n.get("rooms_only_occupant_label")
)
)
);
}
}
// XXX Render "Start your own" button when room is over capacity (see
// bug 1074709)
},
render: function() { render: function() {
var localStreamClasses = React.addons.classSet({ var localStreamClasses = React.addons.classSet({
hide: !this._roomIsActive(), hide: !this._roomIsActive(),
@ -168,7 +207,9 @@ loop.standaloneRoomViews = (function(mozL10n) {
return ( return (
React.DOM.div({className: "room-conversation-wrapper"}, React.DOM.div({className: "room-conversation-wrapper"},
this._renderContextualRoomInfo(), StandaloneRoomInfoArea({roomState: this.state.roomState,
joinRoom: this.joinRoom,
helper: this.props.helper}),
React.DOM.div({className: "video-layout-wrapper"}, React.DOM.div({className: "video-layout-wrapper"},
React.DOM.div({className: "conversation room-conversation"}, React.DOM.div({className: "conversation room-conversation"},
React.DOM.h2({className: "room-name"}, this.state.roomName), React.DOM.h2({className: "room-name"}, this.state.roomName),

View File

@ -5,6 +5,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */ * You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global loop:true, React */ /* global loop:true, React */
/* jshint newcap:false, maxlen:false */
var loop = loop || {}; var loop = loop || {};
loop.standaloneRoomViews = (function(mozL10n) { loop.standaloneRoomViews = (function(mozL10n) {
@ -14,6 +15,72 @@ loop.standaloneRoomViews = (function(mozL10n) {
var sharedActions = loop.shared.actions; var sharedActions = loop.shared.actions;
var sharedViews = loop.shared.views; var sharedViews = loop.shared.views;
var StandaloneRoomInfoArea = React.createClass({
propTypes: {
helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired
},
_renderCallToActionLink: function() {
if (this.props.helper.isFirefox(navigator.userAgent)) {
return (
<a href={loop.config.learnMoreUrl} className="btn btn-info">
{mozL10n.get("rooms_room_full_call_to_action_label", {
clientShortname: mozL10n.get("clientShortname2")
})}
</a>
);
}
return (
<a href={loop.config.brandWebsiteUrl} className="btn btn-info">
{mozL10n.get("rooms_room_full_call_to_action_nonFx_label", {
brandShortname: mozL10n.get("brandShortname")
})}
</a>
);
},
_renderContent: function() {
switch(this.props.roomState) {
case ROOM_STATES.INIT:
case ROOM_STATES.READY: {
return (
<button className="btn btn-join btn-info"
onClick={this.props.joinRoom}>
{mozL10n.get("rooms_room_join_label")}
</button>
);
}
case ROOM_STATES.JOINED:
case ROOM_STATES.SESSION_CONNECTED: {
return (
<p className="empty-room-message">
{mozL10n.get("rooms_only_occupant_label")}
</p>
);
}
case ROOM_STATES.FULL:
return (
<div>
<p className="full-room-message">
{mozL10n.get("rooms_room_full_label")}
</p>
<p>{this._renderCallToActionLink()}</p>
</div>
);
default:
return null;
}
},
render: function() {
return (
<div className="room-inner-info-area">
{this._renderContent()}
</div>
);
}
});
var StandaloneRoomView = React.createClass({ var StandaloneRoomView = React.createClass({
mixins: [Backbone.Events], mixins: [Backbone.Events],
@ -21,6 +88,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
activeRoomStore: activeRoomStore:
React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired, React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired
}, },
getInitialState: function() { getInitialState: function() {
@ -129,35 +197,6 @@ loop.standaloneRoomViews = (function(mozL10n) {
this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS; this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS;
}, },
_renderContextualRoomInfo: function() {
switch(this.state.roomState) {
case ROOM_STATES.INIT:
case ROOM_STATES.READY: {
// Join button
return (
<div className="room-inner-info-area">
<button className="btn btn-join btn-info" onClick={this.joinRoom}>
{mozL10n.get("rooms_room_join_label")}
</button>
</div>
);
}
case ROOM_STATES.JOINED:
case ROOM_STATES.SESSION_CONNECTED: {
// Empty room message
return (
<div className="room-inner-info-area">
<p className="empty-room-message">
{mozL10n.get("rooms_only_occupant_label")}
</p>
</div>
);
}
}
// XXX Render "Start your own" button when room is over capacity (see
// bug 1074709)
},
render: function() { render: function() {
var localStreamClasses = React.addons.classSet({ var localStreamClasses = React.addons.classSet({
hide: !this._roomIsActive(), hide: !this._roomIsActive(),
@ -168,7 +207,9 @@ loop.standaloneRoomViews = (function(mozL10n) {
return ( return (
<div className="room-conversation-wrapper"> <div className="room-conversation-wrapper">
{this._renderContextualRoomInfo()} <StandaloneRoomInfoArea roomState={this.state.roomState}
joinRoom={this.joinRoom}
helper={this.props.helper} />
<div className="video-layout-wrapper"> <div className="video-layout-wrapper">
<div className="conversation room-conversation"> <div className="conversation room-conversation">
<h2 className="room-name">{this.state.roomName}</h2> <h2 className="room-name">{this.state.roomName}</h2>

View File

@ -939,7 +939,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
return ( return (
loop.standaloneRoomViews.StandaloneRoomView({ loop.standaloneRoomViews.StandaloneRoomView({
activeRoomStore: this.props.activeRoomStore, activeRoomStore: this.props.activeRoomStore,
dispatcher: this.props.dispatcher} dispatcher: this.props.dispatcher,
helper: this.props.helper}
) )
); );
} }

View File

@ -940,6 +940,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
<loop.standaloneRoomViews.StandaloneRoomView <loop.standaloneRoomViews.StandaloneRoomView
activeRoomStore={this.props.activeRoomStore} activeRoomStore={this.props.activeRoomStore}
dispatcher={this.props.dispatcher} dispatcher={this.props.dispatcher}
helper={this.props.helper}
/> />
); );
} }

View File

@ -26,6 +26,7 @@ function getConfigFile(req, res) {
"loop.config.marketplaceUrl = 'http://fake-market.herokuapp.com/iframe-install.html'", "loop.config.marketplaceUrl = 'http://fake-market.herokuapp.com/iframe-install.html'",
"loop.config.brandWebsiteUrl = 'https://www.mozilla.org/firefox/';", "loop.config.brandWebsiteUrl = 'https://www.mozilla.org/firefox/';",
"loop.config.privacyWebsiteUrl = 'https://www.mozilla.org/privacy';", "loop.config.privacyWebsiteUrl = 'https://www.mozilla.org/privacy';",
"loop.config.learnMoreUrl = 'https://www.mozilla.org/hello/';",
"loop.config.legalWebsiteUrl = '/legal/terms';", "loop.config.legalWebsiteUrl = '/legal/terms';",
"loop.config.fxosApp = loop.config.fxosApp || {};", "loop.config.fxosApp = loop.config.fxosApp || {};",
"loop.config.fxosApp.name = 'Loop';", "loop.config.fxosApp.name = 'Loop';",

View File

@ -246,6 +246,16 @@ describe("loop.roomViews", function () {
loop.conversation.GenericFailureView); loop.conversation.GenericFailureView);
}); });
it("should render the GenericFailureView if the roomState is `FULL`",
function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.FULL});
view = mountTestComponent();
TestUtils.findRenderedComponentWithType(view,
loop.conversation.GenericFailureView);
});
it("should render the DesktopRoomInvitationView if roomState is `JOINED`", it("should render the DesktopRoomInvitationView if roomState is `JOINED`",
function() { function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED}); activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED});

View File

@ -82,7 +82,15 @@ describe("loop.store.ActiveRoomStore", function () {
sinon.match(ROOM_STATES.READY), fakeError); sinon.match(ROOM_STATES.READY), fakeError);
}); });
it("should set the state to `FAILED`", function() { it("should set the state to `FULL` on server errno 202", function() {
fakeError.errno = 202;
store.roomFailure({error: fakeError});
expect(store._storeState.roomState).eql(ROOM_STATES.FULL);
});
it("should set the state to `FAILED` on generic error", function() {
store.roomFailure({error: fakeError}); store.roomFailure({error: fakeError});
expect(store._storeState.roomState).eql(ROOM_STATES.FAILED); expect(store._storeState.roomState).eql(ROOM_STATES.FAILED);

View File

@ -34,7 +34,8 @@ describe("loop.standaloneRoomViews", function() {
return TestUtils.renderIntoDocument( return TestUtils.renderIntoDocument(
loop.standaloneRoomViews.StandaloneRoomView({ loop.standaloneRoomViews.StandaloneRoomView({
dispatcher: dispatcher, dispatcher: dispatcher,
activeRoomStore: activeRoomStore activeRoomStore: activeRoomStore,
helper: new loop.shared.utils.Helper()
})); }));
} }
@ -128,6 +129,16 @@ describe("loop.standaloneRoomViews", function() {
}); });
}); });
describe("Full room message", function() {
it("should display a full room message on FULL",
function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.FULL});
expect(view.getDOMNode().querySelector(".full-room-message"))
.not.eql(null);
});
});
describe("Join button", function() { describe("Join button", function() {
function getJoinButton(view) { function getJoinButton(view) {
return view.getDOMNode().querySelector(".btn-join"); return view.getDOMNode().querySelector(".btn-join");
@ -175,6 +186,13 @@ describe("loop.standaloneRoomViews", function() {
expect(getLeaveButton(view).disabled).eql(true); expect(getLeaveButton(view).disabled).eql(true);
}); });
it("should disable the Leave button when the room state is FULL",
function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.FULL});
expect(getLeaveButton(view).disabled).eql(true);
});
it("should enable the Leave button when the room state is SESSION_CONNECTED", it("should enable the Leave button when the room state is SESSION_CONNECTED",
function() { function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.SESSION_CONNECTED}); activeRoomStore.setStoreState({roomState: ROOM_STATES.SESSION_CONNECTED});

View File

@ -565,7 +565,8 @@
StandaloneRoomView({ StandaloneRoomView({
dispatcher: dispatcher, dispatcher: dispatcher,
activeRoomStore: activeRoomStore, activeRoomStore: activeRoomStore,
roomState: ROOM_STATES.READY}) roomState: ROOM_STATES.READY,
helper: {isFirefox: returnTrue}})
) )
), ),
@ -574,7 +575,8 @@
StandaloneRoomView({ StandaloneRoomView({
dispatcher: dispatcher, dispatcher: dispatcher,
activeRoomStore: activeRoomStore, activeRoomStore: activeRoomStore,
roomState: ROOM_STATES.JOINED}) roomState: ROOM_STATES.JOINED,
helper: {isFirefox: returnTrue}})
) )
), ),
@ -583,7 +585,28 @@
StandaloneRoomView({ StandaloneRoomView({
dispatcher: dispatcher, dispatcher: dispatcher,
activeRoomStore: activeRoomStore, activeRoomStore: activeRoomStore,
roomState: ROOM_STATES.HAS_PARTICIPANTS}) roomState: ROOM_STATES.HAS_PARTICIPANTS,
helper: {isFirefox: returnTrue}})
)
),
Example({summary: "Standalone room conversation (full - FFx user)"},
React.DOM.div({className: "standalone"},
StandaloneRoomView({
dispatcher: dispatcher,
activeRoomStore: activeRoomStore,
roomState: ROOM_STATES.FULL,
helper: {isFirefox: returnTrue}})
)
),
Example({summary: "Standalone room conversation (full - non FFx user)"},
React.DOM.div({className: "standalone"},
StandaloneRoomView({
dispatcher: dispatcher,
activeRoomStore: activeRoomStore,
roomState: ROOM_STATES.FULL,
helper: {isFirefox: returnFalse}})
) )
) )
), ),

View File

@ -565,7 +565,8 @@
<StandaloneRoomView <StandaloneRoomView
dispatcher={dispatcher} dispatcher={dispatcher}
activeRoomStore={activeRoomStore} activeRoomStore={activeRoomStore}
roomState={ROOM_STATES.READY} /> roomState={ROOM_STATES.READY}
helper={{isFirefox: returnTrue}} />
</div> </div>
</Example> </Example>
@ -574,7 +575,8 @@
<StandaloneRoomView <StandaloneRoomView
dispatcher={dispatcher} dispatcher={dispatcher}
activeRoomStore={activeRoomStore} activeRoomStore={activeRoomStore}
roomState={ROOM_STATES.JOINED} /> roomState={ROOM_STATES.JOINED}
helper={{isFirefox: returnTrue}} />
</div> </div>
</Example> </Example>
@ -583,7 +585,28 @@
<StandaloneRoomView <StandaloneRoomView
dispatcher={dispatcher} dispatcher={dispatcher}
activeRoomStore={activeRoomStore} activeRoomStore={activeRoomStore}
roomState={ROOM_STATES.HAS_PARTICIPANTS} /> roomState={ROOM_STATES.HAS_PARTICIPANTS}
helper={{isFirefox: returnTrue}} />
</div>
</Example>
<Example summary="Standalone room conversation (full - FFx user)">
<div className="standalone">
<StandaloneRoomView
dispatcher={dispatcher}
activeRoomStore={activeRoomStore}
roomState={ROOM_STATES.FULL}
helper={{isFirefox: returnTrue}} />
</div>
</Example>
<Example summary="Standalone room conversation (full - non FFx user)">
<div className="standalone">
<StandaloneRoomView
dispatcher={dispatcher}
activeRoomStore={activeRoomStore}
roomState={ROOM_STATES.FULL}
helper={{isFirefox: returnFalse}} />
</div> </div>
</Example> </Example>
</Section> </Section>