--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1618,16 +1618,17 @@ pref("loop.debug.websocket", false);
pref("loop.debug.sdk", false);
#ifdef DEBUG
pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src 'self' data: http://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*");
#else
pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src 'self' data: http://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net");
#endif
pref("loop.oauth.google.redirect_uri", "urn:ietf:wg:oauth:2.0:oob:auto");
pref("loop.oauth.google.scope", "https://www.google.com/m8/feeds");
+pref("loop.rooms.enabled", false);
// serverURL to be assigned by services team
pref("services.push.serverURL", "wss://push.services.mozilla.com/");
pref("social.sidebar.unload_timeout_ms", 10000);
pref("dom.identity.enabled", false);
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -9,39 +9,55 @@
var loop = loop || {};
loop.panel = (function(_, mozL10n) {
"use strict";
var sharedViews = loop.shared.views;
var sharedModels = loop.shared.models;
var sharedMixins = loop.shared.mixins;
+ var sharedActions = loop.shared.actions;
var Button = sharedViews.Button;
var ButtonGroup = sharedViews.ButtonGroup;
var ContactsList = loop.contacts.ContactsList;
var ContactDetailsForm = loop.contacts.ContactDetailsForm;
var __ = mozL10n.get; // aliasing translation function as __ for concision
var TabView = React.createClass({displayName: 'TabView',
- getInitialState: function() {
+ propTypes: {
+ buttonsHidden: React.PropTypes.bool,
+ // The selectedTab prop is used by the UI showcase.
+ selectedTab: React.PropTypes.string
+ },
+
+ getDefaultProps: function() {
return {
+ buttonsHidden: false,
selectedTab: "call"
};
},
+ getInitialState: function() {
+ return {selectedTab: this.props.selectedTab};
+ },
+
handleSelectTab: function(event) {
var tabName = event.target.dataset.tabName;
this.setState({selectedTab: tabName});
},
render: function() {
var cx = React.addons.classSet;
var tabButtons = [];
var tabs = [];
React.Children.forEach(this.props.children, function(tab, i) {
+ // Filter out null tabs (eg. rooms when the feature is disabled)
+ if (!tab) {
+ return;
+ }
var tabName = tab.props.name;
var isSelected = (this.state.selectedTab == tabName);
if (!tab.props.hidden) {
tabButtons.push(
React.DOM.li({className: cx({selected: isSelected}),
key: i,
'data-tab-name': tabName,
onClick: this.handleSelectTab})
@@ -438,26 +454,145 @@ loop.panel = (function(_, mozL10n) {
React.DOM.p({className: "user-identity"},
this.props.displayName
)
);
}
});
/**
+ * Room list entry.
+ */
+ var RoomEntry = React.createClass({displayName: 'RoomEntry',
+ propTypes: {
+ openRoom: React.PropTypes.func.isRequired,
+ room: React.PropTypes.instanceOf(loop.store.Room).isRequired
+ },
+
+ shouldComponentUpdate: function(nextProps, nextState) {
+ return nextProps.room.ctime > this.props.room.ctime;
+ },
+
+ handleClickRoom: function(event) {
+ event.preventDefault();
+ this.props.openRoom(this.props.room);
+ },
+
+ _isActive: function() {
+ // XXX bug 1074679 will implement this properly
+ return this.props.room.currSize > 0;
+ },
+
+ render: function() {
+ var room = this.props.room;
+ var roomClasses = React.addons.classSet({
+ "room-entry": true,
+ "room-active": this._isActive()
+ });
+
+ return (
+ React.DOM.div({className: roomClasses},
+ React.DOM.h2(null,
+ React.DOM.span({className: "room-notification"}),
+ room.roomName
+ ),
+ React.DOM.p(null,
+ React.DOM.a({ref: "room", href: "#", onClick: this.handleClickRoom},
+ room.roomUrl
+ )
+ )
+ )
+ );
+ }
+ });
+
+ /**
+ * Room list.
+ */
+ var RoomList = React.createClass({displayName: 'RoomList',
+ mixins: [Backbone.Events],
+
+ propTypes: {
+ store: React.PropTypes.instanceOf(loop.store.RoomListStore).isRequired,
+ dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+ rooms: React.PropTypes.array
+ },
+
+ getInitialState: function() {
+ var storeState = this.props.store.getStoreState();
+ return {
+ error: this.props.error || storeState.error,
+ rooms: this.props.rooms || storeState.rooms,
+ };
+ },
+
+ componentWillMount: function() {
+ this.listenTo(this.props.store, "change", this._onRoomListChanged);
+
+ this.props.dispatcher.dispatch(new sharedActions.GetAllRooms());
+ },
+
+ componentWillUnmount: function() {
+ this.stopListening(this.props.store);
+ },
+
+ _onRoomListChanged: function() {
+ var storeState = this.props.store.getStoreState();
+ this.setState({
+ error: storeState.error,
+ rooms: storeState.rooms
+ });
+ },
+
+ _getListHeading: function() {
+ var numRooms = this.state.rooms.length;
+ if (numRooms === 0) {
+ return mozL10n.get("rooms_list_no_current_conversations");
+ }
+ return mozL10n.get("rooms_list_current_conversations", {num: numRooms});
+ },
+
+ openRoom: function(room) {
+ // XXX implement me; see bug 1074678
+ },
+
+ render: function() {
+ if (this.state.error) {
+ // XXX Better end user reporting of errors.
+ console.error(this.state.error);
+ }
+
+ return (
+ React.DOM.div({className: "room-list"},
+ React.DOM.h1(null, this._getListHeading()),
+
+ this.state.rooms.map(function(room, i) {
+ return RoomEntry({key: i, room: room, openRoom: this.openRoom});
+ }, this)
+
+ )
+ );
+ }
+ });
+
+ /**
* Panel view.
*/
var PanelView = React.createClass({displayName: 'PanelView',
propTypes: {
notifications: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired,
// Mostly used for UI components showcase and unit tests
callUrl: React.PropTypes.string,
userProfile: React.PropTypes.object,
showTabButtons: React.PropTypes.bool,
+ selectedTab: React.PropTypes.string,
+ dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+ roomListStore:
+ React.PropTypes.instanceOf(loop.store.RoomListStore).isRequired
},
getInitialState: function() {
return {
userProfile: this.props.userProfile || navigator.mozLoop.userProfile,
};
},
@@ -493,16 +628,32 @@ loop.panel = (function(_, mozL10n) {
if (profile != this.state.userProfile) {
// On profile change (login, logout), switch back to the default tab.
this.selectTab("call");
}
this.setState({userProfile: profile});
this.updateServiceErrors();
},
+ /**
+ * The rooms feature is hidden by default for now. Once it gets mainstream,
+ * this method can be safely removed.
+ */
+ _renderRoomsTab: function() {
+ if (!navigator.mozLoop.getLoopBoolPref("rooms.enabled")) {
+ return null;
+ }
+ return (
+ Tab({name: "rooms"},
+ RoomList({dispatcher: this.props.dispatcher,
+ store: this.props.roomListStore})
+ )
+ );
+ },
+
startForm: function(name, contact) {
this.refs[name].initForm(contact);
this.selectTab(name);
},
selectTab: function(name) {
this.refs.tabView.setState({ selectedTab: name });
},
@@ -522,25 +673,27 @@ loop.panel = (function(_, mozL10n) {
render: function() {
var NotificationListView = sharedViews.NotificationListView;
var displayName = this.state.userProfile && this.state.userProfile.email ||
__("display_name_guest");
return (
React.DOM.div(null,
NotificationListView({notifications: this.props.notifications,
clearOnDocumentHidden: true}),
- TabView({ref: "tabView", buttonsHidden: !this.state.userProfile && !this.props.showTabButtons},
+ TabView({ref: "tabView", selectedTab: this.props.selectedTab,
+ buttonsHidden: !this.state.userProfile && !this.props.showTabButtons},
Tab({name: "call"},
React.DOM.div({className: "content-area"},
CallUrlResult({client: this.props.client,
notifications: this.props.notifications,
callUrl: this.props.callUrl}),
ToSView(null)
)
),
+ this._renderRoomsTab(),
Tab({name: "contacts"},
ContactsList({selectTab: this.selectTab,
startForm: this.startForm})
),
Tab({name: "contacts_add", hidden: true},
ContactDetailsForm({ref: "contacts_add", mode: "add",
selectTab: this.selectTab})
),
@@ -570,21 +723,29 @@ loop.panel = (function(_, mozL10n) {
* Panel initialisation.
*/
function init() {
// Do the initial L10n setup, we do this before anything
// else to ensure the L10n environment is setup correctly.
mozL10n.initialize(navigator.mozLoop);
var client = new loop.Client();
- var notifications = new sharedModels.NotificationCollection()
+ var notifications = new sharedModels.NotificationCollection();
+ var dispatcher = new loop.Dispatcher();
+ var roomListStore = new loop.store.RoomListStore({
+ mozLoop: navigator.mozLoop,
+ dispatcher: dispatcher
+ });
React.renderComponent(PanelView({
client: client,
- notifications: notifications}), document.querySelector("#main"));
+ notifications: notifications,
+ roomListStore: roomListStore,
+ dispatcher: dispatcher}
+ ), document.querySelector("#main"));
document.body.classList.add(loop.shared.utils.getTargetPlatform());
document.body.setAttribute("dir", mozL10n.getDirection());
// Notify the window that we've finished initalization and initial layout
var evtObject = document.createEvent('Event');
evtObject.initEvent('loopPanelInitialized', true, false);
window.dispatchEvent(evtObject);
@@ -592,14 +753,15 @@ loop.panel = (function(_, mozL10n) {
return {
init: init,
UserIdentity: UserIdentity,
AuthLink: AuthLink,
AvailabilityDropdown: AvailabilityDropdown,
CallUrlResult: CallUrlResult,
PanelView: PanelView,
+ RoomList: RoomList,
SettingsDropdown: SettingsDropdown,
ToSView: ToSView
};
})(_, document.mozL10n);
document.addEventListener('DOMContentLoaded', loop.panel.init);
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -9,39 +9,55 @@
var loop = loop || {};
loop.panel = (function(_, mozL10n) {
"use strict";
var sharedViews = loop.shared.views;
var sharedModels = loop.shared.models;
var sharedMixins = loop.shared.mixins;
+ var sharedActions = loop.shared.actions;
var Button = sharedViews.Button;
var ButtonGroup = sharedViews.ButtonGroup;
var ContactsList = loop.contacts.ContactsList;
var ContactDetailsForm = loop.contacts.ContactDetailsForm;
var __ = mozL10n.get; // aliasing translation function as __ for concision
var TabView = React.createClass({
- getInitialState: function() {
+ propTypes: {
+ buttonsHidden: React.PropTypes.bool,
+ // The selectedTab prop is used by the UI showcase.
+ selectedTab: React.PropTypes.string
+ },
+
+ getDefaultProps: function() {
return {
+ buttonsHidden: false,
selectedTab: "call"
};
},
+ getInitialState: function() {
+ return {selectedTab: this.props.selectedTab};
+ },
+
handleSelectTab: function(event) {
var tabName = event.target.dataset.tabName;
this.setState({selectedTab: tabName});
},
render: function() {
var cx = React.addons.classSet;
var tabButtons = [];
var tabs = [];
React.Children.forEach(this.props.children, function(tab, i) {
+ // Filter out null tabs (eg. rooms when the feature is disabled)
+ if (!tab) {
+ return;
+ }
var tabName = tab.props.name;
var isSelected = (this.state.selectedTab == tabName);
if (!tab.props.hidden) {
tabButtons.push(
<li className={cx({selected: isSelected})}
key={i}
data-tab-name={tabName}
onClick={this.handleSelectTab} />
@@ -438,26 +454,145 @@ loop.panel = (function(_, mozL10n) {
<p className="user-identity">
{this.props.displayName}
</p>
);
}
});
/**
+ * Room list entry.
+ */
+ var RoomEntry = React.createClass({
+ propTypes: {
+ openRoom: React.PropTypes.func.isRequired,
+ room: React.PropTypes.instanceOf(loop.store.Room).isRequired
+ },
+
+ shouldComponentUpdate: function(nextProps, nextState) {
+ return nextProps.room.ctime > this.props.room.ctime;
+ },
+
+ handleClickRoom: function(event) {
+ event.preventDefault();
+ this.props.openRoom(this.props.room);
+ },
+
+ _isActive: function() {
+ // XXX bug 1074679 will implement this properly
+ return this.props.room.currSize > 0;
+ },
+
+ render: function() {
+ var room = this.props.room;
+ var roomClasses = React.addons.classSet({
+ "room-entry": true,
+ "room-active": this._isActive()
+ });
+
+ return (
+ <div className={roomClasses}>
+ <h2>
+ <span className="room-notification" />
+ {room.roomName}
+ </h2>
+ <p>
+ <a ref="room" href="#" onClick={this.handleClickRoom}>
+ {room.roomUrl}
+ </a>
+ </p>
+ </div>
+ );
+ }
+ });
+
+ /**
+ * Room list.
+ */
+ var RoomList = React.createClass({
+ mixins: [Backbone.Events],
+
+ propTypes: {
+ store: React.PropTypes.instanceOf(loop.store.RoomListStore).isRequired,
+ dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+ rooms: React.PropTypes.array
+ },
+
+ getInitialState: function() {
+ var storeState = this.props.store.getStoreState();
+ return {
+ error: this.props.error || storeState.error,
+ rooms: this.props.rooms || storeState.rooms,
+ };
+ },
+
+ componentWillMount: function() {
+ this.listenTo(this.props.store, "change", this._onRoomListChanged);
+
+ this.props.dispatcher.dispatch(new sharedActions.GetAllRooms());
+ },
+
+ componentWillUnmount: function() {
+ this.stopListening(this.props.store);
+ },
+
+ _onRoomListChanged: function() {
+ var storeState = this.props.store.getStoreState();
+ this.setState({
+ error: storeState.error,
+ rooms: storeState.rooms
+ });
+ },
+
+ _getListHeading: function() {
+ var numRooms = this.state.rooms.length;
+ if (numRooms === 0) {
+ return mozL10n.get("rooms_list_no_current_conversations");
+ }
+ return mozL10n.get("rooms_list_current_conversations", {num: numRooms});
+ },
+
+ openRoom: function(room) {
+ // XXX implement me; see bug 1074678
+ },
+
+ render: function() {
+ if (this.state.error) {
+ // XXX Better end user reporting of errors.
+ console.error(this.state.error);
+ }
+
+ return (
+ <div className="room-list">
+ <h1>{this._getListHeading()}</h1>
+ {
+ this.state.rooms.map(function(room, i) {
+ return <RoomEntry key={i} room={room} openRoom={this.openRoom} />;
+ }, this)
+ }
+ </div>
+ );
+ }
+ });
+
+ /**
* Panel view.
*/
var PanelView = React.createClass({
propTypes: {
notifications: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired,
// Mostly used for UI components showcase and unit tests
callUrl: React.PropTypes.string,
userProfile: React.PropTypes.object,
showTabButtons: React.PropTypes.bool,
+ selectedTab: React.PropTypes.string,
+ dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+ roomListStore:
+ React.PropTypes.instanceOf(loop.store.RoomListStore).isRequired
},
getInitialState: function() {
return {
userProfile: this.props.userProfile || navigator.mozLoop.userProfile,
};
},
@@ -493,16 +628,32 @@ loop.panel = (function(_, mozL10n) {
if (profile != this.state.userProfile) {
// On profile change (login, logout), switch back to the default tab.
this.selectTab("call");
}
this.setState({userProfile: profile});
this.updateServiceErrors();
},
+ /**
+ * The rooms feature is hidden by default for now. Once it gets mainstream,
+ * this method can be safely removed.
+ */
+ _renderRoomsTab: function() {
+ if (!navigator.mozLoop.getLoopBoolPref("rooms.enabled")) {
+ return null;
+ }
+ return (
+ <Tab name="rooms">
+ <RoomList dispatcher={this.props.dispatcher}
+ store={this.props.roomListStore} />
+ </Tab>
+ );
+ },
+
startForm: function(name, contact) {
this.refs[name].initForm(contact);
this.selectTab(name);
},
selectTab: function(name) {
this.refs.tabView.setState({ selectedTab: name });
},
@@ -522,25 +673,27 @@ loop.panel = (function(_, mozL10n) {
render: function() {
var NotificationListView = sharedViews.NotificationListView;
var displayName = this.state.userProfile && this.state.userProfile.email ||
__("display_name_guest");
return (
<div>
<NotificationListView notifications={this.props.notifications}
clearOnDocumentHidden={true} />
- <TabView ref="tabView" buttonsHidden={!this.state.userProfile && !this.props.showTabButtons}>
+ <TabView ref="tabView" selectedTab={this.props.selectedTab}
+ buttonsHidden={!this.state.userProfile && !this.props.showTabButtons}>
<Tab name="call">
<div className="content-area">
<CallUrlResult client={this.props.client}
notifications={this.props.notifications}
callUrl={this.props.callUrl} />
<ToSView />
</div>
</Tab>
+ {this._renderRoomsTab()}
<Tab name="contacts">
<ContactsList selectTab={this.selectTab}
startForm={this.startForm} />
</Tab>
<Tab name="contacts_add" hidden={true}>
<ContactDetailsForm ref="contacts_add" mode="add"
selectTab={this.selectTab} />
</Tab>
@@ -570,21 +723,29 @@ loop.panel = (function(_, mozL10n) {
* Panel initialisation.
*/
function init() {
// Do the initial L10n setup, we do this before anything
// else to ensure the L10n environment is setup correctly.
mozL10n.initialize(navigator.mozLoop);
var client = new loop.Client();
- var notifications = new sharedModels.NotificationCollection()
+ var notifications = new sharedModels.NotificationCollection();
+ var dispatcher = new loop.Dispatcher();
+ var roomListStore = new loop.store.RoomListStore({
+ mozLoop: navigator.mozLoop,
+ dispatcher: dispatcher
+ });
React.renderComponent(<PanelView
client={client}
- notifications={notifications} />, document.querySelector("#main"));
+ notifications={notifications}
+ roomListStore={roomListStore}
+ dispatcher={dispatcher}
+ />, document.querySelector("#main"));
document.body.classList.add(loop.shared.utils.getTargetPlatform());
document.body.setAttribute("dir", mozL10n.getDirection());
// Notify the window that we've finished initalization and initial layout
var evtObject = document.createEvent('Event');
evtObject.initEvent('loopPanelInitialized', true, false);
window.dispatchEvent(evtObject);
@@ -592,14 +753,15 @@ loop.panel = (function(_, mozL10n) {
return {
init: init,
UserIdentity: UserIdentity,
AuthLink: AuthLink,
AvailabilityDropdown: AvailabilityDropdown,
CallUrlResult: CallUrlResult,
PanelView: PanelView,
+ RoomList: RoomList,
SettingsDropdown: SettingsDropdown,
ToSView: ToSView
};
})(_, document.mozL10n);
document.addEventListener('DOMContentLoaded', loop.panel.init);
--- a/browser/components/loop/content/panel.html
+++ b/browser/components/loop/content/panel.html
@@ -20,13 +20,17 @@
<script type="text/javascript" src="loop/shared/libs/jquery-2.1.0.js"></script>
<script type="text/javascript" src="loop/shared/libs/lodash-2.4.1.js"></script>
<script type="text/javascript" src="loop/shared/libs/backbone-1.1.2.js"></script>
<script type="text/javascript" src="loop/shared/js/utils.js"></script>
<script type="text/javascript" src="loop/shared/js/models.js"></script>
<script type="text/javascript" src="loop/shared/js/mixins.js"></script>
<script type="text/javascript" src="loop/shared/js/views.js"></script>
+ <script type="text/javascript" src="loop/shared/js/validate.js"></script>
+ <script type="text/javascript" src="loop/shared/js/actions.js"></script>
+ <script type="text/javascript" src="loop/shared/js/dispatcher.js"></script>
+ <script type="text/javascript" src="loop/shared/js/roomListStore.js"></script>
<script type="text/javascript" src="loop/js/client.js"></script>
<script type="text/javascript;version=1.8" src="loop/js/contacts.js"></script>
<script type="text/javascript" src="loop/js/panel.js"></script>
</body>
</html>
--- a/browser/components/loop/content/shared/css/panel.css
+++ b/browser/components/loop/content/shared/css/panel.css
@@ -118,16 +118,80 @@ body {
box-shadow: none;
}
.content-area input:not(.pristine):invalid {
border-color: #d74345;
box-shadow: 0 0 4px #c43c3e;
}
+/* Rooms */
+.room-list {
+ background: #f5f5f5;
+}
+
+.room-list > h1 {
+ font-weight: bold;
+ color: #999;
+ padding: .5rem 1rem;
+ border-bottom: 1px solid #ddd;
+}
+
+.room-list > .room-entry {
+ padding: 1rem 1rem 0 .5rem;
+}
+
+.room-list > .room-entry > h2 {
+ font-size: .85rem;
+ color: #777;
+}
+
+.room-list > .room-entry.room-active > h2 {
+ font-weight: bold;
+ color: #000;
+}
+
+.room-list > .room-entry > h2 > .room-notification {
+ display: inline-block;
+ background: transparent;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ margin-right: .3rem;
+}
+
+.room-list > .room-entry.room-active > h2 > .room-notification {
+ background-color: #00a0ec;
+}
+
+.room-list > .room-entry:hover {
+ background: #f1f1f1;
+}
+
+.room-list > .room-entry:not(:last-child) {
+ border-bottom: 1px solid #ddd;
+}
+
+.room-list > .room-entry > p {
+ margin: 0;
+ padding: .2em 0 1rem .8rem;
+}
+
+.room-list > .room-entry > p > a {
+ color: #777;
+ opacity: .5;
+ transition: opacity .1s ease-in-out 0s;
+ text-decoration: none;
+}
+
+.room-list > .room-entry > p > a:hover {
+ opacity: 1;
+ text-decoration: underline;
+}
+
/* Buttons */
.button-group {
display: flex;
flex-direction: row;
width: 100%;
}
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -113,11 +113,18 @@ loop.shared.actions = (function() {
/**
* Used to mute or unmute a stream
*/
SetMute: Action.define("setMute", {
// The part of the stream to enable, e.g. "audio" or "video"
type: String,
// Whether or not to enable the stream.
enabled: Boolean
+ }),
+
+ /**
+ * Retrieves room list.
+ * XXX: should move to some roomActions module - refs bug 1079284
+ */
+ GetAllRooms: Action.define("getAllRooms", {
})
};
})();
--- a/browser/components/loop/content/shared/js/conversationStore.js
+++ b/browser/components/loop/content/shared/js/conversationStore.js
@@ -1,42 +1,43 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global loop:true */
var loop = loop || {};
-loop.store = (function() {
+loop.store = loop.store || {};
+loop.store.ConversationStore = (function() {
var sharedActions = loop.shared.actions;
var CALL_TYPES = loop.shared.utils.CALL_TYPES;
/**
* Websocket states taken from:
* https://docs.services.mozilla.com/loop/apis.html#call-progress-state-change-progress
*/
- var WS_STATES = {
+ var WS_STATES = loop.store.WS_STATES = {
// The call is starting, and the remote party is not yet being alerted.
INIT: "init",
// The called party is being alerted.
ALERTING: "alerting",
// The call is no longer being set up and has been aborted for some reason.
TERMINATED: "terminated",
// The called party has indicated that he has answered the call,
// but the media is not yet confirmed.
CONNECTING: "connecting",
// One of the two parties has indicated successful media set up,
// but the other has not yet.
HALF_CONNECTED: "half-connected",
// Both endpoints have reported successfully establishing media.
CONNECTED: "connected"
};
- var CALL_STATES = {
+ var CALL_STATES = loop.store.CALL_STATES = {
// The initial state of the view.
INIT: "cs-init",
// The store is gathering the call data from the server.
GATHER: "cs-gather",
// The initial data has been gathered, the websocket is connecting, or has
// connected, and waiting for the other side to connect to the server.
CONNECTING: "cs-connecting",
// The websocket has received information that we're now alerting
@@ -47,17 +48,16 @@ loop.store = (function() {
// The call ended successfully.
FINISHED: "cs-finished",
// The user has finished with the window.
CLOSE: "cs-close",
// The call was terminated due to an issue during connection.
TERMINATED: "cs-terminated"
};
-
var ConversationStore = Backbone.Model.extend({
defaults: {
// The current state of the call
callState: CALL_STATES.INIT,
// The reason if a call was terminated
callStateReason: undefined,
// The error information, if there was a failure
error: undefined,
@@ -397,14 +397,10 @@ loop.store = (function() {
break;
}
}
this.dispatcher.dispatch(action);
}
});
- return {
- CALL_STATES: CALL_STATES,
- ConversationStore: ConversationStore,
- WS_STATES: WS_STATES
- };
+ return ConversationStore;
})();
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/js/roomListStore.js
@@ -0,0 +1,171 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* global loop:true */
+
+var loop = loop || {};
+loop.store = loop.store || {};
+
+(function() {
+ "use strict";
+
+ /**
+ * Room validation schema. See validate.js.
+ * @type {Object}
+ */
+ var roomSchema = {
+ roomToken: String,
+ roomUrl: String,
+ roomName: String,
+ maxSize: Number,
+ currSize: Number,
+ ctime: Number
+ };
+
+ /**
+ * Temporary sample raw room list data.
+ * XXX Should be removed when we plug the real mozLoop API for rooms.
+ * See bug 1074664.
+ * @type {Array}
+ */
+ var temporaryRawRoomList = [{
+ roomToken: "_nxD4V4FflQ",
+ roomUrl: "http://sample/_nxD4V4FflQ",
+ roomName: "First Room Name",
+ maxSize: 2,
+ currSize: 0,
+ ctime: 1405517546
+ }, {
+ roomToken: "QzBbvGmIZWU",
+ roomUrl: "http://sample/QzBbvGmIZWU",
+ roomName: "Second Room Name",
+ maxSize: 2,
+ currSize: 0,
+ ctime: 1405517418
+ }, {
+ roomToken: "3jKS_Els9IU",
+ roomUrl: "http://sample/3jKS_Els9IU",
+ roomName: "Third Room Name",
+ maxSize: 3,
+ clientMaxSize: 2,
+ currSize: 1,
+ ctime: 1405518241
+ }];
+
+ /**
+ * Room type. Basically acts as a typed object constructor.
+ *
+ * @param {Object} values Room property values.
+ */
+ function Room(values) {
+ var validatedData = new loop.validate.Validator(roomSchema || {})
+ .validate(values || {});
+ for (var prop in validatedData) {
+ this[prop] = validatedData[prop];
+ }
+ }
+
+ loop.store.Room = Room;
+
+ /**
+ * Room store.
+ *
+ * Options:
+ * - {loop.Dispatcher} dispatcher The dispatcher for dispatching actions and
+ * registering to consume actions.
+ * - {mozLoop} mozLoop The MozLoop API object.
+ *
+ * @extends {Backbone.Events}
+ * @param {Object} options Options object.
+ */
+ function RoomListStore(options) {
+ options = options || {};
+ this.storeState = {error: null, rooms: []};
+
+ if (!options.dispatcher) {
+ throw new Error("Missing option dispatcher");
+ }
+ this.dispatcher = options.dispatcher;
+
+ if (!options.mozLoop) {
+ throw new Error("Missing option mozLoop");
+ }
+ this.mozLoop = options.mozLoop;
+
+ this.dispatcher.register(this, [
+ "getAllRooms",
+ "openRoom"
+ ]);
+ }
+
+ RoomListStore.prototype = _.extend({
+ /**
+ * Retrieves current store state.
+ *
+ * @return {Object}
+ */
+ getStoreState: function() {
+ return this.storeState;
+ },
+
+ /**
+ * Updates store states and trigger a "change" event.
+ *
+ * @param {Object} state The new store state.
+ */
+ setStoreState: function(state) {
+ this.storeState = state;
+ this.trigger("change");
+ },
+
+ /**
+ * Proxy to navigator.mozLoop.rooms.getAll.
+ * XXX Could probably be removed when bug 1074664 lands.
+ *
+ * @param {Function} cb Callback(error, roomList)
+ */
+ _fetchRoomList: function(cb) {
+ // Faking this.mozLoop.rooms until it's available; bug 1074664.
+ if (!this.mozLoop.hasOwnProperty("rooms")) {
+ cb(null, temporaryRawRoomList);
+ return;
+ }
+ this.mozLoop.rooms.getAll(cb);
+ },
+
+ /**
+ * Maps and sorts the raw room list received from the mozLoop API.
+ *
+ * @param {Array} rawRoomList Raw room list.
+ * @return {Array}
+ */
+ _processRawRoomList: function(rawRoomList) {
+ if (!rawRoomList) {
+ return [];
+ }
+ return rawRoomList
+ .map(function(rawRoom) {
+ return new Room(rawRoom);
+ })
+ .slice()
+ .sort(function(a, b) {
+ return b.ctime - a.ctime;
+ });
+ },
+
+ /**
+ * Gather the list of all available rooms from the MozLoop API.
+ */
+ getAllRooms: function() {
+ this._fetchRoomList(function(err, rawRoomList) {
+ this.setStoreState({
+ error: err,
+ rooms: this._processRawRoomList(rawRoomList)
+ });
+ }.bind(this));
+ }
+ }, Backbone.Events);
+
+ loop.store.RoomListStore = RoomListStore;
+})();
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -50,16 +50,17 @@ browser.jar:
content/browser/loop/shared/img/audio-call-avatar.svg (content/shared/img/audio-call-avatar.svg)
content/browser/loop/shared/img/icons-10x10.svg (content/shared/img/icons-10x10.svg)
content/browser/loop/shared/img/icons-14x14.svg (content/shared/img/icons-14x14.svg)
content/browser/loop/shared/img/icons-16x16.svg (content/shared/img/icons-16x16.svg)
# Shared scripts
content/browser/loop/shared/js/actions.js (content/shared/js/actions.js)
content/browser/loop/shared/js/conversationStore.js (content/shared/js/conversationStore.js)
+ content/browser/loop/shared/js/roomListStore.js (content/shared/js/roomListStore.js)
content/browser/loop/shared/js/dispatcher.js (content/shared/js/dispatcher.js)
content/browser/loop/shared/js/feedbackApiClient.js (content/shared/js/feedbackApiClient.js)
content/browser/loop/shared/js/models.js (content/shared/js/models.js)
content/browser/loop/shared/js/mixins.js (content/shared/js/mixins.js)
content/browser/loop/shared/js/otSdkDriver.js (content/shared/js/otSdkDriver.js)
content/browser/loop/shared/js/views.js (content/shared/js/views.js)
content/browser/loop/shared/js/utils.js (content/shared/js/utils.js)
content/browser/loop/shared/js/validate.js (content/shared/js/validate.js)
--- a/browser/components/loop/test/desktop-local/index.html
+++ b/browser/components/loop/test/desktop-local/index.html
@@ -38,16 +38,17 @@
<script src="../../content/shared/js/models.js"></script>
<script src="../../content/shared/js/mixins.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/websocket.js"></script>
<script src="../../content/shared/js/actions.js"></script>
<script src="../../content/shared/js/validate.js"></script>
<script src="../../content/shared/js/dispatcher.js"></script>
<script src="../../content/shared/js/otSdkDriver.js"></script>
+ <script src="../../content/shared/js/roomListStore.js"></script>
<script src="../../content/js/client.js"></script>
<script src="../../content/js/conversationViews.js"></script>
<script src="../../content/js/conversation.js"></script>
<script type="text/javascript;version=1.8" src="../../content/js/contacts.js"></script>
<script src="../../content/js/panel.js"></script>
<!-- Test scripts -->
<script src="client_test.js"></script>
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -2,23 +2,24 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/*jshint newcap:false*/
/*global loop, sinon */
var expect = chai.expect;
var TestUtils = React.addons.TestUtils;
+var sharedActions = loop.shared.actions;
describe("loop.panel", function() {
"use strict";
var sandbox, notifications, fakeXHR, requests = [];
- beforeEach(function() {
+ beforeEach(function(done) {
sandbox = sinon.sandbox.create();
fakeXHR = sandbox.useFakeXMLHttpRequest();
requests = [];
// https://github.com/cjohansen/Sinon.JS/issues/393
fakeXHR.xhr.onCreate = function (xhr) {
requests.push(xhr);
};
notifications = new loop.shared.models.NotificationCollection();
@@ -27,31 +28,37 @@ describe("loop.panel", function() {
doNotDisturb: true,
fxAEnabled: true,
getStrings: function() {
return JSON.stringify({textContent: "fakeText"});
},
get locale() {
return "en-US";
},
+ getLoopBoolPref: sandbox.stub(),
setLoopCharPref: sandbox.stub(),
getLoopCharPref: sandbox.stub().returns("unseen"),
+ getPluralForm: function() {
+ return "fakeText";
+ },
copyString: sandbox.stub(),
noteCallUrlExpiry: sinon.spy(),
composeEmail: sinon.spy(),
telemetryAdd: sinon.spy(),
contacts: {
getAll: function(callback) {
callback(null, []);
},
on: sandbox.stub()
}
};
document.mozL10n.initialize(navigator.mozLoop);
+ // XXX prevent a race whenever mozL10n hasn't been initialized yet
+ setTimeout(done, 0);
});
afterEach(function() {
delete navigator.mozLoop;
sandbox.restore();
});
describe("#init", function() {
@@ -121,64 +128,129 @@ describe("loop.panel", function() {
TestUtils.Simulate.click(availableMenuOption);
expect(view.state.showMenu).eql(true);
});
});
});
describe("loop.panel.PanelView", function() {
- var fakeClient, callUrlData, view, callTab, contactsTab;
+ var fakeClient, dispatcher, roomListStore, callUrlData;
beforeEach(function() {
callUrlData = {
callUrl: "http://call.invalid/",
expiresAt: 1000
};
fakeClient = {
requestCallUrl: function(_, cb) {
cb(null, callUrlData);
}
};
- view = TestUtils.renderIntoDocument(loop.panel.PanelView({
+ dispatcher = new loop.Dispatcher();
+ roomListStore = new loop.store.RoomListStore({
+ dispatcher: dispatcher,
+ mozLoop: navigator.mozLoop
+ });
+ });
+
+ function createTestPanelView() {
+ return TestUtils.renderIntoDocument(loop.panel.PanelView({
notifications: notifications,
client: fakeClient,
showTabButtons: true,
+ dispatcher: dispatcher,
+ roomListStore: roomListStore
}));
-
- [callTab, contactsTab] =
- TestUtils.scryRenderedDOMComponentsWithClass(view, "tab");
- });
+ }
describe('TabView', function() {
- it("should select contacts tab when clicking tab button", function() {
- TestUtils.Simulate.click(
- view.getDOMNode().querySelector('li[data-tab-name="contacts"]'));
+ var view, callTab, roomsTab, contactsTab;
+
+ describe("loop.rooms.enabled on", function() {
+ beforeEach(function() {
+ navigator.mozLoop.getLoopBoolPref = function(pref) {
+ if (pref === "rooms.enabled") {
+ return true;
+ }
+ };
+
+ view = createTestPanelView();
+
+ [callTab, roomsTab, contactsTab] =
+ TestUtils.scryRenderedDOMComponentsWithClass(view, "tab");
+ });
+
+ it("should select contacts tab when clicking tab button", function() {
+ TestUtils.Simulate.click(
+ view.getDOMNode().querySelector("li[data-tab-name=\"contacts\"]"));
- expect(contactsTab.getDOMNode().classList.contains("selected"))
- .to.be.true;
+ expect(contactsTab.getDOMNode().classList.contains("selected"))
+ .to.be.true;
+ });
+
+ it("should select rooms tab when clicking tab button", function() {
+ TestUtils.Simulate.click(
+ view.getDOMNode().querySelector("li[data-tab-name=\"rooms\"]"));
+
+ expect(roomsTab.getDOMNode().classList.contains("selected"))
+ .to.be.true;
+ });
+
+ it("should select call tab when clicking tab button", function() {
+ TestUtils.Simulate.click(
+ view.getDOMNode().querySelector("li[data-tab-name=\"call\"]"));
+
+ expect(callTab.getDOMNode().classList.contains("selected"))
+ .to.be.true;
+ });
});
- it("should select call tab when clicking tab button", function() {
- TestUtils.Simulate.click(
- view.getDOMNode().querySelector('li[data-tab-name="call"]'));
+ describe("loop.rooms.enabled off", function() {
+ beforeEach(function() {
+ navigator.mozLoop.getLoopBoolPref = function(pref) {
+ if (pref === "rooms.enabled") {
+ return false;
+ }
+ };
+
+ view = createTestPanelView();
+
+ [callTab, contactsTab] =
+ TestUtils.scryRenderedDOMComponentsWithClass(view, "tab");
+ });
- expect(callTab.getDOMNode().classList.contains("selected"))
- .to.be.true;
+ it("should select contacts tab when clicking tab button", function() {
+ TestUtils.Simulate.click(
+ view.getDOMNode().querySelector("li[data-tab-name=\"contacts\"]"));
+
+ expect(contactsTab.getDOMNode().classList.contains("selected"))
+ .to.be.true;
+ });
+
+ it("should select call tab when clicking tab button", function() {
+ TestUtils.Simulate.click(
+ view.getDOMNode().querySelector("li[data-tab-name=\"call\"]"));
+
+ expect(callTab.getDOMNode().classList.contains("selected"))
+ .to.be.true;
+ });
});
});
describe("AuthLink", function() {
it("should trigger the FxA sign in/up process when clicking the link",
function() {
navigator.mozLoop.loggedInToFxA = false;
navigator.mozLoop.logInToFxA = sandbox.stub();
+ var view = createTestPanelView();
+
TestUtils.Simulate.click(
view.getDOMNode().querySelector(".signin-link a"));
sinon.assert.calledOnce(navigator.mozLoop.logInToFxA);
});
it("should be hidden if FxA is not enabled",
function() {
@@ -188,18 +260,16 @@ describe("loop.panel", function() {
});
afterEach(function() {
navigator.mozLoop.fxAEnabled = true;
});
});
describe("SettingsDropdown", function() {
- var view;
-
beforeEach(function() {
navigator.mozLoop.logInToFxA = sandbox.stub();
navigator.mozLoop.logOutFromFxA = sandbox.stub();
navigator.mozLoop.openFxASettings = sandbox.stub();
});
afterEach(function() {
navigator.mozLoop.fxAEnabled = true;
@@ -283,16 +353,18 @@ describe("loop.panel", function() {
view.getDOMNode().querySelector(".icon-signout"));
sinon.assert.calledOnce(navigator.mozLoop.logOutFromFxA);
});
});
describe("#render", function() {
it("should render a ToSView", function() {
+ var view = createTestPanelView();
+
TestUtils.findRenderedComponentWithType(view, loop.panel.ToSView);
});
});
});
describe("loop.panel.CallUrlResult", function() {
var fakeClient, callUrlData, view;
@@ -545,16 +617,44 @@ describe("loop.panel", function() {
sinon.assert.calledOnce(notifications.errorL10n);
sinon.assert.calledWithExactly(notifications.errorL10n,
"unable_retrieve_url");
});
});
});
+ describe("loop.panel.RoomList", function() {
+ var roomListStore, dispatcher;
+
+ beforeEach(function() {
+ dispatcher = new loop.Dispatcher();
+ roomListStore = new loop.store.RoomListStore({
+ dispatcher: dispatcher,
+ mozLoop: navigator.mozLoop
+ });
+ });
+
+ function createTestComponent() {
+ return TestUtils.renderIntoDocument(loop.panel.RoomList({
+ store: roomListStore,
+ dispatcher: dispatcher
+ }));
+ }
+
+ it("should dispatch a GetAllRooms action on mount", function() {
+ var dispatch = sandbox.stub(dispatcher, "dispatch");
+
+ createTestComponent();
+
+ sinon.assert.calledOnce(dispatch);
+ sinon.assert.calledWithExactly(dispatch, new sharedActions.GetAllRooms());
+ });
+ });
+
describe('loop.panel.ToSView', function() {
it("should render when the value of loop.seenToS is not set", function() {
var view = TestUtils.renderIntoDocument(loop.panel.ToSView());
TestUtils.findRenderedDOMComponentWithClass(view, "terms-service");
});
--- a/browser/components/loop/test/functional/test_1_browser_call.py
+++ b/browser/components/loop/test/functional/test_1_browser_call.py
@@ -126,22 +126,22 @@ class Test1BrowserCall(MarionetteTestCas
# expect a video container on desktop side
video = self.wait_for_element_displayed(By.CLASS_NAME, "media")
self.assertEqual(video.tag_name, "div", "expect a video container")
def hangup_call_and_verify_feedback(self):
self.marionette.set_context("chrome")
button = self.marionette.find_element(By.CLASS_NAME, "btn-hangup")
- # XXX For whatever reason, the click doesn't take effect unless we
- # wait for a bit (even if we wait for the element to actually be
- # displayed first, which we're not currently bothering with). It's
- # not entirely clear whether the click is being delivered in this case,
- # or whether there's a Marionette bug here.
- sleep(2)
+ # XXX bug 1080095 For whatever reason, the click doesn't take effect
+ # unless we wait for a bit (even if we wait for the element to
+ # actually be displayed first, which we're not currently bothering
+ # with). It's not entirely clear whether the click is being
+ # delivered in this case, or whether there's a Marionette bug here.
+ sleep(5)
button.click()
# check that the feedback form is displayed
feedback_form = self.wait_for_element_displayed(By.CLASS_NAME, "faces")
self.assertEqual(feedback_form.tag_name, "div", "expect feedback form")
def test_1_browser_call(self):
self.switch_to_panel()
--- a/browser/components/loop/test/shared/index.html
+++ b/browser/components/loop/test/shared/index.html
@@ -39,27 +39,29 @@
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/websocket.js"></script>
<script src="../../content/shared/js/feedbackApiClient.js"></script>
<script src="../../content/shared/js/validate.js"></script>
<script src="../../content/shared/js/actions.js"></script>
<script src="../../content/shared/js/dispatcher.js"></script>
<script src="../../content/shared/js/otSdkDriver.js"></script>
<script src="../../content/shared/js/conversationStore.js"></script>
+ <script src="../../content/shared/js/roomListStore.js"></script>
<!-- Test scripts -->
<script src="models_test.js"></script>
<script src="mixins_test.js"></script>
<script src="utils_test.js"></script>
<script src="views_test.js"></script>
<script src="websocket_test.js"></script>
<script src="feedbackApiClient_test.js"></script>
<script src="validate_test.js"></script>
<script src="dispatcher_test.js"></script>
<script src="conversationStore_test.js"></script>
<script src="otSdkDriver_test.js"></script>
+ <script src="roomListStore_test.js"></script>
<script>
mocha.run(function () {
$("#mocha").append("<p id='complete'>Complete.</p>");
});
</script>
</body>
</html>
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/shared/roomListStore_test.js
@@ -0,0 +1,130 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var expect = chai.expect;
+
+describe("loop.store.Room", function () {
+ "use strict";
+ describe("#constructor", function() {
+ it("should validate room values", function() {
+ expect(function() {
+ new loop.store.Room();
+ }).to.Throw(Error, /missing required/);
+ });
+ });
+});
+
+describe("loop.store.RoomListStore", function () {
+ "use strict";
+
+ var sharedActions = loop.shared.actions;
+ var sandbox, dispatcher;
+
+ beforeEach(function() {
+ sandbox = sinon.sandbox.create();
+ dispatcher = new loop.Dispatcher();
+ });
+
+ afterEach(function() {
+ sandbox.restore();
+ });
+
+ describe("#constructor", function() {
+ it("should throw an error if the dispatcher is missing", function() {
+ expect(function() {
+ new loop.store.RoomListStore({mozLoop: {}});
+ }).to.Throw(/dispatcher/);
+ });
+
+ it("should throw an error if mozLoop is missing", function() {
+ expect(function() {
+ new loop.store.RoomListStore({dispatcher: dispatcher});
+ }).to.Throw(/mozLoop/);
+ });
+ });
+
+ describe("#getAllRooms", function() {
+ var store, fakeMozLoop;
+ var fakeRoomList = [{
+ roomToken: "_nxD4V4FflQ",
+ roomUrl: "http://sample/_nxD4V4FflQ",
+ roomName: "First Room Name",
+ maxSize: 2,
+ currSize: 0,
+ ctime: 1405517546
+ }, {
+ roomToken: "QzBbvGmIZWU",
+ roomUrl: "http://sample/QzBbvGmIZWU",
+ roomName: "Second Room Name",
+ maxSize: 2,
+ currSize: 0,
+ ctime: 1405517418
+ }, {
+ roomToken: "3jKS_Els9IU",
+ roomUrl: "http://sample/3jKS_Els9IU",
+ roomName: "Third Room Name",
+ maxSize: 3,
+ clientMaxSize: 2,
+ currSize: 1,
+ ctime: 1405518241
+ }];
+
+ beforeEach(function() {
+ fakeMozLoop = {
+ rooms: {
+ getAll: function(cb) {
+ cb(null, fakeRoomList);
+ }
+ }
+ };
+ store = new loop.store.RoomListStore({
+ dispatcher: dispatcher,
+ mozLoop: fakeMozLoop
+ });
+ });
+
+ it("should trigger a list:changed event", function(done) {
+ store.on("change", function() {
+ done();
+ });
+
+ dispatcher.dispatch(new sharedActions.GetAllRooms());
+ });
+
+ it("should fetch the room list from the mozLoop API", function(done) {
+ store.once("change", function() {
+ expect(store.getStoreState().error).to.be.a.null;
+ expect(store.getStoreState().rooms).to.have.length.of(3);
+ done();
+ });
+
+ dispatcher.dispatch(new sharedActions.GetAllRooms());
+ });
+
+ it("should order the room list using ctime desc", function(done) {
+ store.once("change", function() {
+ var storeState = store.getStoreState();
+ expect(storeState.error).to.be.a.null;
+ expect(storeState.rooms[0].ctime).eql(1405518241);
+ expect(storeState.rooms[1].ctime).eql(1405517546);
+ expect(storeState.rooms[2].ctime).eql(1405517418);
+ done();
+ });
+
+ dispatcher.dispatch(new sharedActions.GetAllRooms());
+ });
+
+ it("should report an error", function() {
+ fakeMozLoop.rooms.getAll = function(cb) {
+ cb("fakeError");
+ };
+
+ store.once("change", function() {
+ var storeState = store.getStoreState();
+ expect(storeState.error).eql("fakeError");
+ });
+
+ dispatcher.dispatch(new sharedActions.GetAllRooms());
+ });
+ });
+});
--- a/browser/components/loop/ui/fake-mozLoop.js
+++ b/browser/components/loop/ui/fake-mozLoop.js
@@ -4,17 +4,22 @@
/**
* Faking the mozLoop object which doesn't exist in regular web pages.
* @type {Object}
*/
navigator.mozLoop = {
ensureRegistered: function() {},
getLoopCharPref: function() {},
- getLoopBoolPref: function() {},
+ getLoopBoolPref: function(pref) {
+ // Ensure UI for rooms is displayed in the showcase.
+ if (pref === "rooms.enabled") {
+ return true;
+ }
+ },
releaseCallData: function() {},
contacts: {
getAll: function(callback) {
callback(null, []);
},
on: function() {}
}
};
--- a/browser/components/loop/ui/index.html
+++ b/browser/components/loop/ui/index.html
@@ -33,17 +33,20 @@
<script src="../content/shared/libs/backbone-1.1.2.js"></script>
<script src="../content/shared/js/feedbackApiClient.js"></script>
<script src="../content/shared/js/actions.js"></script>
<script src="../content/shared/js/utils.js"></script>
<script src="../content/shared/js/models.js"></script>
<script src="../content/shared/js/mixins.js"></script>
<script src="../content/shared/js/views.js"></script>
<script src="../content/shared/js/websocket.js"></script>
+ <script src="../content/shared/js/validate.js"></script>
+ <script src="../content/shared/js/dispatcher.js"></script>
<script src="../content/shared/js/conversationStore.js"></script>
+ <script src="../content/shared/js/roomListStore.js"></script>
<script src="../content/js/conversationViews.js"></script>
<script src="../content/js/client.js"></script>
<script src="../standalone/content/js/webapp.js"></script>
<script type="text/javascript;version=1.8" src="../content/js/contacts.js"></script>
<script>
if (!loop.contacts) {
// For browsers that don't support ES6 without special flags (all but Fx
// at the moment), we shim the contacts namespace with its most barebone
--- a/browser/components/loop/ui/ui-showcase.css
+++ b/browser/components/loop/ui/ui-showcase.css
@@ -64,19 +64,26 @@
margin: 1.5em 0;
}
.showcase > section .example > h3 {
font-size: 1.2em;
font-weight: bold;
border-bottom: 1px dashed #aaa;
margin: 1em 0;
+ margin-top: -14em;
+ padding-top: 14em;
text-align: left;
}
+.showcase > section .example > h3 a {
+ text-decoration: none;
+ color: #555;
+}
+
.showcase p.note {
margin: 0;
padding: 0;
color: #666;
font-style: italic;
}
.override-position * {
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -51,16 +51,22 @@
// Feedback API client configured to send data to the stage input server,
// which is available at https://input.allizom.org
var stageFeedbackApiClient = new loop.FeedbackAPIClient(
"https://input.allizom.org/api/v1/feedback", {
product: "Loop"
}
);
+ var dispatcher = new loop.Dispatcher();
+ var roomListStore = new loop.store.RoomListStore({
+ dispatcher: dispatcher,
+ mozLoop: {}
+ });
+
// Local mocks
var mockContact = {
name: ["Mr Smith"],
email: [{
value: "smith@invalid.com"
}]
};
@@ -88,21 +94,28 @@
errNotifications.add({
level: "error",
message: "Could Not Authenticate",
details: "Did you change your password?",
detailsButtonLabel: "Retry",
});
var Example = React.createClass({displayName: 'Example',
+ makeId: function(prefix) {
+ return (prefix || "") + this.props.summary.toLowerCase().replace(/\s/g, "-");
+ },
+
render: function() {
var cx = React.addons.classSet;
return (
React.DOM.div({className: "example"},
- React.DOM.h3(null, this.props.summary),
+ React.DOM.h3({id: this.makeId()},
+ this.props.summary,
+ React.DOM.a({href: this.makeId("#")}, " ¶")
+ ),
React.DOM.div({className: cx({comp: true, dashed: this.props.dashed}),
style: this.props.style || {}},
this.props.children
)
)
);
}
});
@@ -145,36 +158,55 @@
return (
ShowCase(null,
Section({name: "PanelView"},
React.DOM.p({className: "note"},
React.DOM.strong(null, "Note:"), " 332px wide."
),
Example({summary: "Call URL retrieved", dashed: "true", style: {width: "332px"}},
PanelView({client: mockClient, notifications: notifications,
- callUrl: "http://invalid.example.url/"})
+ callUrl: "http://invalid.example.url/",
+ dispatcher: dispatcher,
+ roomListStore: roomListStore})
),
Example({summary: "Call URL retrieved - authenticated", dashed: "true", style: {width: "332px"}},
PanelView({client: mockClient, notifications: notifications,
callUrl: "http://invalid.example.url/",
- userProfile: {email: "test@example.com"}})
+ userProfile: {email: "test@example.com"},
+ dispatcher: dispatcher,
+ roomListStore: roomListStore})
),
Example({summary: "Pending call url retrieval", dashed: "true", style: {width: "332px"}},
- PanelView({client: mockClient, notifications: notifications})
+ PanelView({client: mockClient, notifications: notifications,
+ dispatcher: dispatcher,
+ roomListStore: roomListStore})
),
Example({summary: "Pending call url retrieval - authenticated", dashed: "true", style: {width: "332px"}},
PanelView({client: mockClient, notifications: notifications,
- userProfile: {email: "test@example.com"}})
+ userProfile: {email: "test@example.com"},
+ dispatcher: dispatcher,
+ roomListStore: roomListStore})
),
Example({summary: "Error Notification", dashed: "true", style: {width: "332px"}},
- PanelView({client: mockClient, notifications: errNotifications})
+ PanelView({client: mockClient, notifications: errNotifications,
+ dispatcher: dispatcher,
+ roomListStore: roomListStore})
),
Example({summary: "Error Notification - authenticated", dashed: "true", style: {width: "332px"}},
PanelView({client: mockClient, notifications: errNotifications,
- userProfile: {email: "test@example.com"}})
+ userProfile: {email: "test@example.com"},
+ dispatcher: dispatcher,
+ roomListStore: roomListStore})
+ ),
+ Example({summary: "Room list tab", dashed: "true", style: {width: "332px"}},
+ PanelView({client: mockClient, notifications: notifications,
+ userProfile: {email: "test@example.com"},
+ dispatcher: dispatcher,
+ roomListStore: roomListStore,
+ selectedTab: "rooms"})
)
),
Section({name: "IncomingCallView"},
Example({summary: "Default / incoming video call", dashed: "true", style: {width: "260px", height: "254px"}},
React.DOM.div({className: "fx-embedded"},
IncomingCallView({model: mockConversationModel,
video: true})
@@ -242,41 +274,45 @@
publishStream: noop})
)
)
),
Section({name: "PendingConversationView"},
Example({summary: "Pending conversation view (connecting)", dashed: "true"},
React.DOM.div({className: "standalone"},
- PendingConversationView({websocket: mockWebSocket})
+ PendingConversationView({websocket: mockWebSocket,
+ dispatcher: dispatcher})
)
),
Example({summary: "Pending conversation view (ringing)", dashed: "true"},
React.DOM.div({className: "standalone"},
- PendingConversationView({websocket: mockWebSocket, callState: "ringing"})
+ PendingConversationView({websocket: mockWebSocket,
+ dispatcher: dispatcher,
+ callState: "ringing"})
)
)
),
Section({name: "PendingConversationView (Desktop)"},
Example({summary: "Connecting", dashed: "true",
style: {width: "260px", height: "265px"}},
React.DOM.div({className: "fx-embedded"},
DesktopPendingConversationView({callState: "gather",
- contact: mockContact})
+ contact: mockContact,
+ dispatcher: dispatcher})
)
)
),
Section({name: "CallFailedView"},
Example({summary: "Call Failed", dashed: "true",
style: {width: "260px", height: "265px"}},
React.DOM.div({className: "fx-embedded"},
- CallFailedView(null)
+ CallFailedView({dispatcher: dispatcher})
)
)
),
Section({name: "StartConversationView"},
Example({summary: "Start conversation view", dashed: "true"},
React.DOM.div({className: "standalone"},
StartConversationView({conversation: mockConversationModel,
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -51,16 +51,22 @@
// Feedback API client configured to send data to the stage input server,
// which is available at https://input.allizom.org
var stageFeedbackApiClient = new loop.FeedbackAPIClient(
"https://input.allizom.org/api/v1/feedback", {
product: "Loop"
}
);
+ var dispatcher = new loop.Dispatcher();
+ var roomListStore = new loop.store.RoomListStore({
+ dispatcher: dispatcher,
+ mozLoop: {}
+ });
+
// Local mocks
var mockContact = {
name: ["Mr Smith"],
email: [{
value: "smith@invalid.com"
}]
};
@@ -88,21 +94,28 @@
errNotifications.add({
level: "error",
message: "Could Not Authenticate",
details: "Did you change your password?",
detailsButtonLabel: "Retry",
});
var Example = React.createClass({
+ makeId: function(prefix) {
+ return (prefix || "") + this.props.summary.toLowerCase().replace(/\s/g, "-");
+ },
+
render: function() {
var cx = React.addons.classSet;
return (
<div className="example">
- <h3>{this.props.summary}</h3>
+ <h3 id={this.makeId()}>
+ {this.props.summary}
+ <a href={this.makeId("#")}> ¶</a>
+ </h3>
<div className={cx({comp: true, dashed: this.props.dashed})}
style={this.props.style || {}}>
{this.props.children}
</div>
</div>
);
}
});
@@ -145,36 +158,55 @@
return (
<ShowCase>
<Section name="PanelView">
<p className="note">
<strong>Note:</strong> 332px wide.
</p>
<Example summary="Call URL retrieved" dashed="true" style={{width: "332px"}}>
<PanelView client={mockClient} notifications={notifications}
- callUrl="http://invalid.example.url/" />
+ callUrl="http://invalid.example.url/"
+ dispatcher={dispatcher}
+ roomListStore={roomListStore} />
</Example>
<Example summary="Call URL retrieved - authenticated" dashed="true" style={{width: "332px"}}>
<PanelView client={mockClient} notifications={notifications}
callUrl="http://invalid.example.url/"
- userProfile={{email: "test@example.com"}} />
+ userProfile={{email: "test@example.com"}}
+ dispatcher={dispatcher}
+ roomListStore={roomListStore} />
</Example>
<Example summary="Pending call url retrieval" dashed="true" style={{width: "332px"}}>
- <PanelView client={mockClient} notifications={notifications} />
+ <PanelView client={mockClient} notifications={notifications}
+ dispatcher={dispatcher}
+ roomListStore={roomListStore} />
</Example>
<Example summary="Pending call url retrieval - authenticated" dashed="true" style={{width: "332px"}}>
<PanelView client={mockClient} notifications={notifications}
- userProfile={{email: "test@example.com"}} />
+ userProfile={{email: "test@example.com"}}
+ dispatcher={dispatcher}
+ roomListStore={roomListStore} />
</Example>
<Example summary="Error Notification" dashed="true" style={{width: "332px"}}>
- <PanelView client={mockClient} notifications={errNotifications}/>
+ <PanelView client={mockClient} notifications={errNotifications}
+ dispatcher={dispatcher}
+ roomListStore={roomListStore} />
</Example>
<Example summary="Error Notification - authenticated" dashed="true" style={{width: "332px"}}>
<PanelView client={mockClient} notifications={errNotifications}
- userProfile={{email: "test@example.com"}} />
+ userProfile={{email: "test@example.com"}}
+ dispatcher={dispatcher}
+ roomListStore={roomListStore} />
+ </Example>
+ <Example summary="Room list tab" dashed="true" style={{width: "332px"}}>
+ <PanelView client={mockClient} notifications={notifications}
+ userProfile={{email: "test@example.com"}}
+ dispatcher={dispatcher}
+ roomListStore={roomListStore}
+ selectedTab="rooms" />
</Example>
</Section>
<Section name="IncomingCallView">
<Example summary="Default / incoming video call" dashed="true" style={{width: "260px", height: "254px"}}>
<div className="fx-embedded">
<IncomingCallView model={mockConversationModel}
video={true} />
@@ -242,41 +274,45 @@
publishStream={noop} />
</Example>
</div>
</Section>
<Section name="PendingConversationView">
<Example summary="Pending conversation view (connecting)" dashed="true">
<div className="standalone">
- <PendingConversationView websocket={mockWebSocket}/>
+ <PendingConversationView websocket={mockWebSocket}
+ dispatcher={dispatcher} />
</div>
</Example>
<Example summary="Pending conversation view (ringing)" dashed="true">
<div className="standalone">
- <PendingConversationView websocket={mockWebSocket} callState="ringing"/>
+ <PendingConversationView websocket={mockWebSocket}
+ dispatcher={dispatcher}
+ callState="ringing"/>
</div>
</Example>
</Section>
<Section name="PendingConversationView (Desktop)">
<Example summary="Connecting" dashed="true"
style={{width: "260px", height: "265px"}}>
<div className="fx-embedded">
<DesktopPendingConversationView callState={"gather"}
- contact={mockContact} />
+ contact={mockContact}
+ dispatcher={dispatcher} />
</div>
</Example>
</Section>
<Section name="CallFailedView">
<Example summary="Call Failed" dashed="true"
style={{width: "260px", height: "265px"}}>
<div className="fx-embedded">
- <CallFailedView />
+ <CallFailedView dispatcher={dispatcher} />
</div>
</Example>
</Section>
<Section name="StartConversationView">
<Example summary="Start conversation view" dashed="true">
<div className="standalone">
<StartConversationView conversation={mockConversationModel}
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -267,8 +267,13 @@ feedback_back_button=Back
feedback_window_will_close_in2=This window will close in {{countdown}} second;This window will close in {{countdown}} seconds
## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after
## a signed-in to signed-in user call.
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#feedback
feedback_rejoin_button=Rejoin
## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of
## an abusive user.
feedback_report_user_button=Report User
+
+## LOCALIZATION NOTE (rooms_list_current_conversations): We prefer to have no
+## number in the string, but if you need it for your language please use {{num}}.
+rooms_list_current_conversations=Current conversation;Current conversations
+rooms_list_no_current_conversations=No current conversations