Bug 1074672 Part 1 - Implement a room list view for Loop, r=mikedeboer.

This commit is contained in:
Nicolas Perriault 2014-10-08 16:59:56 -07:00
Родитель 3f727a6695
Коммит 5a7d5e605f
20 изменённых файлов: 958 добавлений и 65 удалений

Просмотреть файл

@ -1623,6 +1623,7 @@ pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src 'self' data:
#endif #endif
pref("loop.oauth.google.redirect_uri", "urn:ietf:wg:oauth:2.0:oob:auto"); 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.oauth.google.scope", "https://www.google.com/m8/feeds");
pref("loop.rooms.enabled", false);
// serverURL to be assigned by services team // serverURL to be assigned by services team
pref("services.push.serverURL", "wss://push.services.mozilla.com/"); pref("services.push.serverURL", "wss://push.services.mozilla.com/");

Просмотреть файл

@ -14,6 +14,7 @@ loop.panel = (function(_, mozL10n) {
var sharedViews = loop.shared.views; var sharedViews = loop.shared.views;
var sharedModels = loop.shared.models; var sharedModels = loop.shared.models;
var sharedMixins = loop.shared.mixins; var sharedMixins = loop.shared.mixins;
var sharedActions = loop.shared.actions;
var Button = sharedViews.Button; var Button = sharedViews.Button;
var ButtonGroup = sharedViews.ButtonGroup; var ButtonGroup = sharedViews.ButtonGroup;
var ContactsList = loop.contacts.ContactsList; var ContactsList = loop.contacts.ContactsList;
@ -21,12 +22,23 @@ loop.panel = (function(_, mozL10n) {
var __ = mozL10n.get; // aliasing translation function as __ for concision var __ = mozL10n.get; // aliasing translation function as __ for concision
var TabView = React.createClass({displayName: 'TabView', 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 { return {
buttonsHidden: false,
selectedTab: "call" selectedTab: "call"
}; };
}, },
getInitialState: function() {
return {selectedTab: this.props.selectedTab};
},
handleSelectTab: function(event) { handleSelectTab: function(event) {
var tabName = event.target.dataset.tabName; var tabName = event.target.dataset.tabName;
this.setState({selectedTab: tabName}); this.setState({selectedTab: tabName});
@ -37,6 +49,10 @@ loop.panel = (function(_, mozL10n) {
var tabButtons = []; var tabButtons = [];
var tabs = []; var tabs = [];
React.Children.forEach(this.props.children, function(tab, i) { 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 tabName = tab.props.name;
var isSelected = (this.state.selectedTab == tabName); var isSelected = (this.state.selectedTab == tabName);
if (!tab.props.hidden) { if (!tab.props.hidden) {
@ -442,6 +458,121 @@ loop.panel = (function(_, mozL10n) {
} }
}); });
/**
* 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. * Panel view.
*/ */
@ -453,6 +584,10 @@ loop.panel = (function(_, mozL10n) {
callUrl: React.PropTypes.string, callUrl: React.PropTypes.string,
userProfile: React.PropTypes.object, userProfile: React.PropTypes.object,
showTabButtons: React.PropTypes.bool, 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() { getInitialState: function() {
@ -498,6 +633,22 @@ loop.panel = (function(_, mozL10n) {
this.updateServiceErrors(); 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) { startForm: function(name, contact) {
this.refs[name].initForm(contact); this.refs[name].initForm(contact);
this.selectTab(name); this.selectTab(name);
@ -527,7 +678,8 @@ loop.panel = (function(_, mozL10n) {
React.DOM.div(null, React.DOM.div(null,
NotificationListView({notifications: this.props.notifications, NotificationListView({notifications: this.props.notifications,
clearOnDocumentHidden: true}), 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"}, Tab({name: "call"},
React.DOM.div({className: "content-area"}, React.DOM.div({className: "content-area"},
CallUrlResult({client: this.props.client, CallUrlResult({client: this.props.client,
@ -536,6 +688,7 @@ loop.panel = (function(_, mozL10n) {
ToSView(null) ToSView(null)
) )
), ),
this._renderRoomsTab(),
Tab({name: "contacts"}, Tab({name: "contacts"},
ContactsList({selectTab: this.selectTab, ContactsList({selectTab: this.selectTab,
startForm: this.startForm}) startForm: this.startForm})
@ -575,11 +728,19 @@ loop.panel = (function(_, mozL10n) {
mozL10n.initialize(navigator.mozLoop); mozL10n.initialize(navigator.mozLoop);
var client = new loop.Client(); 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({ React.renderComponent(PanelView({
client: client, 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.classList.add(loop.shared.utils.getTargetPlatform());
document.body.setAttribute("dir", mozL10n.getDirection()); document.body.setAttribute("dir", mozL10n.getDirection());
@ -597,6 +758,7 @@ loop.panel = (function(_, mozL10n) {
AvailabilityDropdown: AvailabilityDropdown, AvailabilityDropdown: AvailabilityDropdown,
CallUrlResult: CallUrlResult, CallUrlResult: CallUrlResult,
PanelView: PanelView, PanelView: PanelView,
RoomList: RoomList,
SettingsDropdown: SettingsDropdown, SettingsDropdown: SettingsDropdown,
ToSView: ToSView ToSView: ToSView
}; };

Просмотреть файл

@ -14,6 +14,7 @@ loop.panel = (function(_, mozL10n) {
var sharedViews = loop.shared.views; var sharedViews = loop.shared.views;
var sharedModels = loop.shared.models; var sharedModels = loop.shared.models;
var sharedMixins = loop.shared.mixins; var sharedMixins = loop.shared.mixins;
var sharedActions = loop.shared.actions;
var Button = sharedViews.Button; var Button = sharedViews.Button;
var ButtonGroup = sharedViews.ButtonGroup; var ButtonGroup = sharedViews.ButtonGroup;
var ContactsList = loop.contacts.ContactsList; var ContactsList = loop.contacts.ContactsList;
@ -21,12 +22,23 @@ loop.panel = (function(_, mozL10n) {
var __ = mozL10n.get; // aliasing translation function as __ for concision var __ = mozL10n.get; // aliasing translation function as __ for concision
var TabView = React.createClass({ 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 { return {
buttonsHidden: false,
selectedTab: "call" selectedTab: "call"
}; };
}, },
getInitialState: function() {
return {selectedTab: this.props.selectedTab};
},
handleSelectTab: function(event) { handleSelectTab: function(event) {
var tabName = event.target.dataset.tabName; var tabName = event.target.dataset.tabName;
this.setState({selectedTab: tabName}); this.setState({selectedTab: tabName});
@ -37,6 +49,10 @@ loop.panel = (function(_, mozL10n) {
var tabButtons = []; var tabButtons = [];
var tabs = []; var tabs = [];
React.Children.forEach(this.props.children, function(tab, i) { 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 tabName = tab.props.name;
var isSelected = (this.state.selectedTab == tabName); var isSelected = (this.state.selectedTab == tabName);
if (!tab.props.hidden) { if (!tab.props.hidden) {
@ -442,6 +458,121 @@ loop.panel = (function(_, mozL10n) {
} }
}); });
/**
* 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. * Panel view.
*/ */
@ -453,6 +584,10 @@ loop.panel = (function(_, mozL10n) {
callUrl: React.PropTypes.string, callUrl: React.PropTypes.string,
userProfile: React.PropTypes.object, userProfile: React.PropTypes.object,
showTabButtons: React.PropTypes.bool, 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() { getInitialState: function() {
@ -498,6 +633,22 @@ loop.panel = (function(_, mozL10n) {
this.updateServiceErrors(); 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) { startForm: function(name, contact) {
this.refs[name].initForm(contact); this.refs[name].initForm(contact);
this.selectTab(name); this.selectTab(name);
@ -527,7 +678,8 @@ loop.panel = (function(_, mozL10n) {
<div> <div>
<NotificationListView notifications={this.props.notifications} <NotificationListView notifications={this.props.notifications}
clearOnDocumentHidden={true} /> 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"> <Tab name="call">
<div className="content-area"> <div className="content-area">
<CallUrlResult client={this.props.client} <CallUrlResult client={this.props.client}
@ -536,6 +688,7 @@ loop.panel = (function(_, mozL10n) {
<ToSView /> <ToSView />
</div> </div>
</Tab> </Tab>
{this._renderRoomsTab()}
<Tab name="contacts"> <Tab name="contacts">
<ContactsList selectTab={this.selectTab} <ContactsList selectTab={this.selectTab}
startForm={this.startForm} /> startForm={this.startForm} />
@ -575,11 +728,19 @@ loop.panel = (function(_, mozL10n) {
mozL10n.initialize(navigator.mozLoop); mozL10n.initialize(navigator.mozLoop);
var client = new loop.Client(); 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 React.renderComponent(<PanelView
client={client} 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.classList.add(loop.shared.utils.getTargetPlatform());
document.body.setAttribute("dir", mozL10n.getDirection()); document.body.setAttribute("dir", mozL10n.getDirection());
@ -597,6 +758,7 @@ loop.panel = (function(_, mozL10n) {
AvailabilityDropdown: AvailabilityDropdown, AvailabilityDropdown: AvailabilityDropdown,
CallUrlResult: CallUrlResult, CallUrlResult: CallUrlResult,
PanelView: PanelView, PanelView: PanelView,
RoomList: RoomList,
SettingsDropdown: SettingsDropdown, SettingsDropdown: SettingsDropdown,
ToSView: ToSView ToSView: ToSView
}; };

Просмотреть файл

@ -25,6 +25,10 @@
<script type="text/javascript" src="loop/shared/js/models.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/mixins.js"></script>
<script type="text/javascript" src="loop/shared/js/views.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" src="loop/js/client.js"></script>
<script type="text/javascript;version=1.8" src="loop/js/contacts.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> <script type="text/javascript" src="loop/js/panel.js"></script>

Просмотреть файл

@ -123,6 +123,70 @@ body {
box-shadow: 0 0 4px #c43c3e; 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 */ /* Buttons */
.button-group { .button-group {

Просмотреть файл

@ -118,6 +118,13 @@ loop.shared.actions = (function() {
type: String, type: String,
// Whether or not to enable the stream. // Whether or not to enable the stream.
enabled: Boolean enabled: Boolean
}),
/**
* Retrieves room list.
* XXX: should move to some roomActions module - refs bug 1079284
*/
GetAllRooms: Action.define("getAllRooms", {
}) })
}; };
})(); })();

Просмотреть файл

@ -5,8 +5,9 @@
/* global loop:true */ /* global loop:true */
var loop = loop || {}; var loop = loop || {};
loop.store = (function() { loop.store = loop.store || {};
loop.store.ConversationStore = (function() {
var sharedActions = loop.shared.actions; var sharedActions = loop.shared.actions;
var CALL_TYPES = loop.shared.utils.CALL_TYPES; var CALL_TYPES = loop.shared.utils.CALL_TYPES;
@ -14,7 +15,7 @@ loop.store = (function() {
* Websocket states taken from: * Websocket states taken from:
* https://docs.services.mozilla.com/loop/apis.html#call-progress-state-change-progress * 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. // The call is starting, and the remote party is not yet being alerted.
INIT: "init", INIT: "init",
// The called party is being alerted. // The called party is being alerted.
@ -31,7 +32,7 @@ loop.store = (function() {
CONNECTED: "connected" CONNECTED: "connected"
}; };
var CALL_STATES = { var CALL_STATES = loop.store.CALL_STATES = {
// The initial state of the view. // The initial state of the view.
INIT: "cs-init", INIT: "cs-init",
// The store is gathering the call data from the server. // The store is gathering the call data from the server.
@ -52,7 +53,6 @@ loop.store = (function() {
TERMINATED: "cs-terminated" TERMINATED: "cs-terminated"
}; };
var ConversationStore = Backbone.Model.extend({ var ConversationStore = Backbone.Model.extend({
defaults: { defaults: {
// The current state of the call // The current state of the call
@ -402,9 +402,5 @@ loop.store = (function() {
} }
}); });
return { return ConversationStore;
CALL_STATES: CALL_STATES,
ConversationStore: ConversationStore,
WS_STATES: WS_STATES
};
})(); })();

Просмотреть файл

@ -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;
})();

Просмотреть файл

@ -55,6 +55,7 @@ browser.jar:
# Shared scripts # Shared scripts
content/browser/loop/shared/js/actions.js (content/shared/js/actions.js) 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/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/dispatcher.js (content/shared/js/dispatcher.js)
content/browser/loop/shared/js/feedbackApiClient.js (content/shared/js/feedbackApiClient.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/models.js (content/shared/js/models.js)

Просмотреть файл

@ -43,6 +43,7 @@
<script src="../../content/shared/js/validate.js"></script> <script src="../../content/shared/js/validate.js"></script>
<script src="../../content/shared/js/dispatcher.js"></script> <script src="../../content/shared/js/dispatcher.js"></script>
<script src="../../content/shared/js/otSdkDriver.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/client.js"></script>
<script src="../../content/js/conversationViews.js"></script> <script src="../../content/js/conversationViews.js"></script>
<script src="../../content/js/conversation.js"></script> <script src="../../content/js/conversation.js"></script>

Просмотреть файл

@ -7,13 +7,14 @@
var expect = chai.expect; var expect = chai.expect;
var TestUtils = React.addons.TestUtils; var TestUtils = React.addons.TestUtils;
var sharedActions = loop.shared.actions;
describe("loop.panel", function() { describe("loop.panel", function() {
"use strict"; "use strict";
var sandbox, notifications, fakeXHR, requests = []; var sandbox, notifications, fakeXHR, requests = [];
beforeEach(function() { beforeEach(function(done) {
sandbox = sinon.sandbox.create(); sandbox = sinon.sandbox.create();
fakeXHR = sandbox.useFakeXMLHttpRequest(); fakeXHR = sandbox.useFakeXMLHttpRequest();
requests = []; requests = [];
@ -32,8 +33,12 @@ describe("loop.panel", function() {
get locale() { get locale() {
return "en-US"; return "en-US";
}, },
getLoopBoolPref: sandbox.stub(),
setLoopCharPref: sandbox.stub(), setLoopCharPref: sandbox.stub(),
getLoopCharPref: sandbox.stub().returns("unseen"), getLoopCharPref: sandbox.stub().returns("unseen"),
getPluralForm: function() {
return "fakeText";
},
copyString: sandbox.stub(), copyString: sandbox.stub(),
noteCallUrlExpiry: sinon.spy(), noteCallUrlExpiry: sinon.spy(),
composeEmail: sinon.spy(), composeEmail: sinon.spy(),
@ -47,6 +52,8 @@ describe("loop.panel", function() {
}; };
document.mozL10n.initialize(navigator.mozLoop); document.mozL10n.initialize(navigator.mozLoop);
// XXX prevent a race whenever mozL10n hasn't been initialized yet
setTimeout(done, 0);
}); });
afterEach(function() { afterEach(function() {
@ -126,7 +133,7 @@ describe("loop.panel", function() {
}); });
describe("loop.panel.PanelView", function() { describe("loop.panel.PanelView", function() {
var fakeClient, callUrlData, view, callTab, contactsTab; var fakeClient, dispatcher, roomListStore, callUrlData;
beforeEach(function() { beforeEach(function() {
callUrlData = { callUrlData = {
@ -140,31 +147,94 @@ describe("loop.panel", function() {
} }
}; };
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, notifications: notifications,
client: fakeClient, client: fakeClient,
showTabButtons: true, showTabButtons: true,
dispatcher: dispatcher,
roomListStore: roomListStore
})); }));
}
[callTab, contactsTab] =
TestUtils.scryRenderedDOMComponentsWithClass(view, "tab");
});
describe('TabView', function() { describe('TabView', function() {
it("should select contacts tab when clicking tab button", function() { var view, callTab, roomsTab, contactsTab;
TestUtils.Simulate.click(
view.getDOMNode().querySelector('li[data-tab-name="contacts"]'));
expect(contactsTab.getDOMNode().classList.contains("selected")) describe("loop.rooms.enabled on", function() {
.to.be.true; 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;
});
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() { describe("loop.rooms.enabled off", function() {
TestUtils.Simulate.click( beforeEach(function() {
view.getDOMNode().querySelector('li[data-tab-name="call"]')); navigator.mozLoop.getLoopBoolPref = function(pref) {
if (pref === "rooms.enabled") {
return false;
}
};
expect(callTab.getDOMNode().classList.contains("selected")) view = createTestPanelView();
.to.be.true;
[callTab, 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;
});
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;
});
}); });
}); });
@ -174,6 +244,8 @@ describe("loop.panel", function() {
navigator.mozLoop.loggedInToFxA = false; navigator.mozLoop.loggedInToFxA = false;
navigator.mozLoop.logInToFxA = sandbox.stub(); navigator.mozLoop.logInToFxA = sandbox.stub();
var view = createTestPanelView();
TestUtils.Simulate.click( TestUtils.Simulate.click(
view.getDOMNode().querySelector(".signin-link a")); view.getDOMNode().querySelector(".signin-link a"));
@ -193,8 +265,6 @@ describe("loop.panel", function() {
}); });
describe("SettingsDropdown", function() { describe("SettingsDropdown", function() {
var view;
beforeEach(function() { beforeEach(function() {
navigator.mozLoop.logInToFxA = sandbox.stub(); navigator.mozLoop.logInToFxA = sandbox.stub();
navigator.mozLoop.logOutFromFxA = sandbox.stub(); navigator.mozLoop.logOutFromFxA = sandbox.stub();
@ -288,6 +358,8 @@ describe("loop.panel", function() {
describe("#render", function() { describe("#render", function() {
it("should render a ToSView", function() { it("should render a ToSView", function() {
var view = createTestPanelView();
TestUtils.findRenderedComponentWithType(view, loop.panel.ToSView); TestUtils.findRenderedComponentWithType(view, loop.panel.ToSView);
}); });
}); });
@ -550,6 +622,34 @@ describe("loop.panel", function() {
}); });
}); });
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() { describe('loop.panel.ToSView', function() {
it("should render when the value of loop.seenToS is not set", function() { it("should render when the value of loop.seenToS is not set", function() {

Просмотреть файл

@ -131,12 +131,12 @@ class Test1BrowserCall(MarionetteTestCase):
self.marionette.set_context("chrome") self.marionette.set_context("chrome")
button = self.marionette.find_element(By.CLASS_NAME, "btn-hangup") button = self.marionette.find_element(By.CLASS_NAME, "btn-hangup")
# XXX For whatever reason, the click doesn't take effect unless we # XXX bug 1080095 For whatever reason, the click doesn't take effect
# wait for a bit (even if we wait for the element to actually be # unless we wait for a bit (even if we wait for the element to
# displayed first, which we're not currently bothering with). It's # actually be displayed first, which we're not currently bothering
# not entirely clear whether the click is being delivered in this case, # with). It's not entirely clear whether the click is being
# or whether there's a Marionette bug here. # delivered in this case, or whether there's a Marionette bug here.
sleep(2) sleep(5)
button.click() button.click()
# check that the feedback form is displayed # check that the feedback form is displayed

Просмотреть файл

@ -44,6 +44,7 @@
<script src="../../content/shared/js/dispatcher.js"></script> <script src="../../content/shared/js/dispatcher.js"></script>
<script src="../../content/shared/js/otSdkDriver.js"></script> <script src="../../content/shared/js/otSdkDriver.js"></script>
<script src="../../content/shared/js/conversationStore.js"></script> <script src="../../content/shared/js/conversationStore.js"></script>
<script src="../../content/shared/js/roomListStore.js"></script>
<!-- Test scripts --> <!-- Test scripts -->
<script src="models_test.js"></script> <script src="models_test.js"></script>
@ -56,6 +57,7 @@
<script src="dispatcher_test.js"></script> <script src="dispatcher_test.js"></script>
<script src="conversationStore_test.js"></script> <script src="conversationStore_test.js"></script>
<script src="otSdkDriver_test.js"></script> <script src="otSdkDriver_test.js"></script>
<script src="roomListStore_test.js"></script>
<script> <script>
mocha.run(function () { mocha.run(function () {
$("#mocha").append("<p id='complete'>Complete.</p>"); $("#mocha").append("<p id='complete'>Complete.</p>");

Просмотреть файл

@ -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());
});
});
});

Просмотреть файл

@ -9,7 +9,12 @@
navigator.mozLoop = { navigator.mozLoop = {
ensureRegistered: function() {}, ensureRegistered: function() {},
getLoopCharPref: 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() {}, releaseCallData: function() {},
contacts: { contacts: {
getAll: function(callback) { getAll: function(callback) {

Просмотреть файл

@ -38,7 +38,10 @@
<script src="../content/shared/js/mixins.js"></script> <script src="../content/shared/js/mixins.js"></script>
<script src="../content/shared/js/views.js"></script> <script src="../content/shared/js/views.js"></script>
<script src="../content/shared/js/websocket.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/conversationStore.js"></script>
<script src="../content/shared/js/roomListStore.js"></script>
<script src="../content/js/conversationViews.js"></script> <script src="../content/js/conversationViews.js"></script>
<script src="../content/js/client.js"></script> <script src="../content/js/client.js"></script>
<script src="../standalone/content/js/webapp.js"></script> <script src="../standalone/content/js/webapp.js"></script>

Просмотреть файл

@ -69,9 +69,16 @@
font-weight: bold; font-weight: bold;
border-bottom: 1px dashed #aaa; border-bottom: 1px dashed #aaa;
margin: 1em 0; margin: 1em 0;
margin-top: -14em;
padding-top: 14em;
text-align: left; text-align: left;
} }
.showcase > section .example > h3 a {
text-decoration: none;
color: #555;
}
.showcase p.note { .showcase p.note {
margin: 0; margin: 0;
padding: 0; padding: 0;

Просмотреть файл

@ -56,6 +56,12 @@
} }
); );
var dispatcher = new loop.Dispatcher();
var roomListStore = new loop.store.RoomListStore({
dispatcher: dispatcher,
mozLoop: {}
});
// Local mocks // Local mocks
var mockContact = { var mockContact = {
@ -93,11 +99,18 @@
}); });
var Example = React.createClass({displayName: 'Example', var Example = React.createClass({displayName: 'Example',
makeId: function(prefix) {
return (prefix || "") + this.props.summary.toLowerCase().replace(/\s/g, "-");
},
render: function() { render: function() {
var cx = React.addons.classSet; var cx = React.addons.classSet;
return ( return (
React.DOM.div({className: "example"}, 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}), React.DOM.div({className: cx({comp: true, dashed: this.props.dashed}),
style: this.props.style || {}}, style: this.props.style || {}},
this.props.children this.props.children
@ -150,26 +163,45 @@
), ),
Example({summary: "Call URL retrieved", dashed: "true", style: {width: "332px"}}, Example({summary: "Call URL retrieved", dashed: "true", style: {width: "332px"}},
PanelView({client: mockClient, notifications: notifications, 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"}}, Example({summary: "Call URL retrieved - authenticated", dashed: "true", style: {width: "332px"}},
PanelView({client: mockClient, notifications: notifications, PanelView({client: mockClient, notifications: notifications,
callUrl: "http://invalid.example.url/", 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"}}, 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"}}, Example({summary: "Pending call url retrieval - authenticated", dashed: "true", style: {width: "332px"}},
PanelView({client: mockClient, notifications: notifications, 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"}}, 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"}}, Example({summary: "Error Notification - authenticated", dashed: "true", style: {width: "332px"}},
PanelView({client: mockClient, notifications: errNotifications, 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"})
) )
), ),
@ -247,12 +279,15 @@
Section({name: "PendingConversationView"}, Section({name: "PendingConversationView"},
Example({summary: "Pending conversation view (connecting)", dashed: "true"}, Example({summary: "Pending conversation view (connecting)", dashed: "true"},
React.DOM.div({className: "standalone"}, React.DOM.div({className: "standalone"},
PendingConversationView({websocket: mockWebSocket}) PendingConversationView({websocket: mockWebSocket,
dispatcher: dispatcher})
) )
), ),
Example({summary: "Pending conversation view (ringing)", dashed: "true"}, Example({summary: "Pending conversation view (ringing)", dashed: "true"},
React.DOM.div({className: "standalone"}, React.DOM.div({className: "standalone"},
PendingConversationView({websocket: mockWebSocket, callState: "ringing"}) PendingConversationView({websocket: mockWebSocket,
dispatcher: dispatcher,
callState: "ringing"})
) )
) )
), ),
@ -262,7 +297,8 @@
style: {width: "260px", height: "265px"}}, style: {width: "260px", height: "265px"}},
React.DOM.div({className: "fx-embedded"}, React.DOM.div({className: "fx-embedded"},
DesktopPendingConversationView({callState: "gather", DesktopPendingConversationView({callState: "gather",
contact: mockContact}) contact: mockContact,
dispatcher: dispatcher})
) )
) )
), ),
@ -271,7 +307,7 @@
Example({summary: "Call Failed", dashed: "true", Example({summary: "Call Failed", dashed: "true",
style: {width: "260px", height: "265px"}}, style: {width: "260px", height: "265px"}},
React.DOM.div({className: "fx-embedded"}, React.DOM.div({className: "fx-embedded"},
CallFailedView(null) CallFailedView({dispatcher: dispatcher})
) )
) )
), ),

Просмотреть файл

@ -56,6 +56,12 @@
} }
); );
var dispatcher = new loop.Dispatcher();
var roomListStore = new loop.store.RoomListStore({
dispatcher: dispatcher,
mozLoop: {}
});
// Local mocks // Local mocks
var mockContact = { var mockContact = {
@ -93,11 +99,18 @@
}); });
var Example = React.createClass({ var Example = React.createClass({
makeId: function(prefix) {
return (prefix || "") + this.props.summary.toLowerCase().replace(/\s/g, "-");
},
render: function() { render: function() {
var cx = React.addons.classSet; var cx = React.addons.classSet;
return ( return (
<div className="example"> <div className="example">
<h3>{this.props.summary}</h3> <h3 id={this.makeId()}>
{this.props.summary}
<a href={this.makeId("#")}>&nbsp;</a>
</h3>
<div className={cx({comp: true, dashed: this.props.dashed})} <div className={cx({comp: true, dashed: this.props.dashed})}
style={this.props.style || {}}> style={this.props.style || {}}>
{this.props.children} {this.props.children}
@ -150,26 +163,45 @@
</p> </p>
<Example summary="Call URL retrieved" dashed="true" style={{width: "332px"}}> <Example summary="Call URL retrieved" dashed="true" style={{width: "332px"}}>
<PanelView client={mockClient} notifications={notifications} <PanelView client={mockClient} notifications={notifications}
callUrl="http://invalid.example.url/" /> callUrl="http://invalid.example.url/"
dispatcher={dispatcher}
roomListStore={roomListStore} />
</Example> </Example>
<Example summary="Call URL retrieved - authenticated" dashed="true" style={{width: "332px"}}> <Example summary="Call URL retrieved - authenticated" dashed="true" style={{width: "332px"}}>
<PanelView client={mockClient} notifications={notifications} <PanelView client={mockClient} notifications={notifications}
callUrl="http://invalid.example.url/" callUrl="http://invalid.example.url/"
userProfile={{email: "test@example.com"}} /> userProfile={{email: "test@example.com"}}
dispatcher={dispatcher}
roomListStore={roomListStore} />
</Example> </Example>
<Example summary="Pending call url retrieval" dashed="true" style={{width: "332px"}}> <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>
<Example summary="Pending call url retrieval - authenticated" dashed="true" style={{width: "332px"}}> <Example summary="Pending call url retrieval - authenticated" dashed="true" style={{width: "332px"}}>
<PanelView client={mockClient} notifications={notifications} <PanelView client={mockClient} notifications={notifications}
userProfile={{email: "test@example.com"}} /> userProfile={{email: "test@example.com"}}
dispatcher={dispatcher}
roomListStore={roomListStore} />
</Example> </Example>
<Example summary="Error Notification" dashed="true" style={{width: "332px"}}> <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>
<Example summary="Error Notification - authenticated" dashed="true" style={{width: "332px"}}> <Example summary="Error Notification - authenticated" dashed="true" style={{width: "332px"}}>
<PanelView client={mockClient} notifications={errNotifications} <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> </Example>
</Section> </Section>
@ -247,12 +279,15 @@
<Section name="PendingConversationView"> <Section name="PendingConversationView">
<Example summary="Pending conversation view (connecting)" dashed="true"> <Example summary="Pending conversation view (connecting)" dashed="true">
<div className="standalone"> <div className="standalone">
<PendingConversationView websocket={mockWebSocket}/> <PendingConversationView websocket={mockWebSocket}
dispatcher={dispatcher} />
</div> </div>
</Example> </Example>
<Example summary="Pending conversation view (ringing)" dashed="true"> <Example summary="Pending conversation view (ringing)" dashed="true">
<div className="standalone"> <div className="standalone">
<PendingConversationView websocket={mockWebSocket} callState="ringing"/> <PendingConversationView websocket={mockWebSocket}
dispatcher={dispatcher}
callState="ringing"/>
</div> </div>
</Example> </Example>
</Section> </Section>
@ -262,7 +297,8 @@
style={{width: "260px", height: "265px"}}> style={{width: "260px", height: "265px"}}>
<div className="fx-embedded"> <div className="fx-embedded">
<DesktopPendingConversationView callState={"gather"} <DesktopPendingConversationView callState={"gather"}
contact={mockContact} /> contact={mockContact}
dispatcher={dispatcher} />
</div> </div>
</Example> </Example>
</Section> </Section>
@ -271,7 +307,7 @@
<Example summary="Call Failed" dashed="true" <Example summary="Call Failed" dashed="true"
style={{width: "260px", height: "265px"}}> style={{width: "260px", height: "265px"}}>
<div className="fx-embedded"> <div className="fx-embedded">
<CallFailedView /> <CallFailedView dispatcher={dispatcher} />
</div> </div>
</Example> </Example>
</Section> </Section>

Просмотреть файл

@ -272,3 +272,8 @@ feedback_rejoin_button=Rejoin
## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of ## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of
## an abusive user. ## an abusive user.
feedback_report_user_button=Report 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