Bug 1213848 - Change the Loop panel display when a user enters their own room to stop them entering other rooms [r=mikedeboer]

This commit is contained in:
Ed Lee 2015-10-13 01:40:27 -07:00
parent 3c0a8b29d1
commit ae84db17ab
16 changed files with 275 additions and 25 deletions

View File

@ -209,6 +209,16 @@ body {
padding: 1rem;
}
.new-room-view > .stop-sharing-button {
background-color: #d13f1a;
border-color: #d13f1a;
}
.new-room-view > .stop-sharing-button:hover {
background-color: #ef6745;
border-color: #ef6745;
}
.room-list {
/* xxx not sure why flex needs the 3 value setting
but setting flex to just 1, the whole tab including the new room is scrollable.

View File

@ -691,6 +691,7 @@ loop.panel = (function(_, mozL10n) {
_renderNewRoomButton: function() {
return (
React.createElement(NewRoomView, {dispatcher: this.props.dispatcher,
inRoom: this.state.openedRoom !== null,
mozLoop: this.props.mozLoop,
pendingOperation: this.state.pendingCreation ||
this.state.pendingInitialRetrieval})
@ -737,6 +738,7 @@ loop.panel = (function(_, mozL10n) {
var NewRoomView = React.createClass({displayName: "NewRoomView",
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
inRoom: React.PropTypes.bool.isRequired,
mozLoop: React.PropTypes.object.isRequired,
pendingOperation: React.PropTypes.bool.isRequired
},
@ -791,13 +793,23 @@ loop.panel = (function(_, mozL10n) {
this.props.dispatcher.dispatch(createRoomAction);
},
handleStopSharingButtonClick: function() {
this.props.mozLoop.hangupAllChatWindows();
},
render: function() {
return (
React.createElement("div", {className: "new-room-view"},
this.props.inRoom ?
React.createElement("button", {className: "btn btn-info stop-sharing-button",
disabled: this.props.pendingOperation,
onClick: this.handleStopSharingButtonClick},
mozL10n.get("panel_stop_sharing_tabs_button")
) :
React.createElement("button", {className: "btn btn-info new-room-button",
disabled: this.props.pendingOperation,
onClick: this.handleCreateButtonClick},
mozL10n.get("rooms_new_room_button_label")
mozL10n.get("panel_browse_with_friend_button")
)
)
);

View File

@ -691,6 +691,7 @@ loop.panel = (function(_, mozL10n) {
_renderNewRoomButton: function() {
return (
<NewRoomView dispatcher={this.props.dispatcher}
inRoom={this.state.openedRoom !== null}
mozLoop={this.props.mozLoop}
pendingOperation={this.state.pendingCreation ||
this.state.pendingInitialRetrieval} />
@ -737,6 +738,7 @@ loop.panel = (function(_, mozL10n) {
var NewRoomView = React.createClass({
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
inRoom: React.PropTypes.bool.isRequired,
mozLoop: React.PropTypes.object.isRequired,
pendingOperation: React.PropTypes.bool.isRequired
},
@ -791,14 +793,24 @@ loop.panel = (function(_, mozL10n) {
this.props.dispatcher.dispatch(createRoomAction);
},
handleStopSharingButtonClick: function() {
this.props.mozLoop.hangupAllChatWindows();
},
render: function() {
return (
<div className="new-room-view">
{this.props.inRoom ?
<button className="btn btn-info stop-sharing-button"
disabled={this.props.pendingOperation}
onClick={this.handleStopSharingButtonClick}>
{mozL10n.get("panel_stop_sharing_tabs_button")}
</button> :
<button className="btn btn-info new-room-button"
disabled={this.props.pendingOperation}
onClick={this.handleCreateButtonClick}>
{mozL10n.get("rooms_new_room_button_label")}
</button>
{mozL10n.get("panel_browse_with_friend_button")}
</button>}
</div>
);
}

View File

@ -113,6 +113,7 @@ loop.store = loop.store || {};
return {
activeRoom: this.activeRoomStore ? this.activeRoomStore.getStoreState() : {},
error: null,
openedRoom: null,
pendingCreation: false,
pendingInitialRetrieval: true,
rooms: [],
@ -126,6 +127,8 @@ loop.store = loop.store || {};
startListeningToRoomEvents: function() {
// Rooms event registration
this._mozLoop.rooms.on("add", this._onRoomAdded.bind(this));
this._mozLoop.rooms.on("close", this._onRoomClose.bind(this));
this._mozLoop.rooms.on("open", this._onRoomOpen.bind(this));
this._mozLoop.rooms.on("update", this._onRoomUpdated.bind(this));
this._mozLoop.rooms.on("delete", this._onRoomRemoved.bind(this));
this._mozLoop.rooms.on("refresh", this._onRoomsRefresh.bind(this));
@ -155,6 +158,27 @@ loop.store = loop.store || {};
}));
},
/**
* Clears the current active room.
*/
_onRoomClose: function() {
this.setStoreState({
openedRoom: null
});
},
/**
* Updates the current active room.
*
* @param {String} eventName The event name (unused).
* @param {String} roomToken Identifier of the room.
*/
_onRoomOpen: function(eventName, roomToken) {
this.setStoreState({
openedRoom: roomToken
});
},
/**
* Executed when a room is updated.
*

View File

@ -713,7 +713,11 @@ var LoopRoomsInternal = {
type: "room"
};
MozLoopService.openChatWindow(windowData);
eventEmitter.emit("open", roomToken);
MozLoopService.openChatWindow(windowData, () => {
eventEmitter.emit("close");
});
},
/**

View File

@ -550,6 +550,17 @@ function injectLoopAPI(targetWindow) {
}
},
/**
* Hangup and close all chat windows that are open.
*/
hangupAllChatWindows: {
enumerable: true,
writable: true,
value() {
MozLoopService.hangupAllChatWindows();
}
},
/**
* Starts alerting the user about an incoming call
*/

View File

@ -874,6 +874,17 @@ var MozLoopServiceInternal = {
return "about:loopconversation#" + chatWindowId;
},
/**
* Hangup and close all chat windows that are open.
*/
hangupAllChatWindows() {
let isLoopURL = ({ src }) => /^about:loopconversation#/.test(src);
[...Chat.chatboxes].filter(isLoopURL).forEach(chatbox => {
let window = chatbox.content.contentWindow;
window.dispatchEvent(new window.CustomEvent("LoopHangupNow"));
});
},
/**
* Determines if a chat window is already open for a given window id.
*
@ -895,10 +906,12 @@ var MozLoopServiceInternal = {
*
* @param {Object} conversationWindowData The data to be obtained by the
* window when it opens.
* @param {Function} windowCloseCallback Callback function that's invoked
* when the window closes.
* @returns {Number} The id of the window, null if a window could not
* be opened.
*/
openChatWindow: function(conversationWindowData) {
openChatWindow: function(conversationWindowData, windowCloseCallback) {
// So I guess the origin is the loop server!?
let origin = this.loopServerUri;
let windowId = this.getChatWindowID(conversationWindowData);
@ -937,6 +950,8 @@ var MozLoopServiceInternal = {
// we can keep using it here.
let ref = chatbar.chatboxForURL.get(chatbox.src);
chatbox = ref && ref.get() || chatbox;
} else if (eventName == "Loop:ChatWindowClosed") {
windowCloseCallback();
}
}
@ -1386,15 +1401,24 @@ this.MozLoopService = {
yield completedPromise;
}),
/**
* Hangup and close all chat windows that are open.
*/
hangupAllChatWindows() {
return MozLoopServiceInternal.hangupAllChatWindows();
},
/**
* Opens the chat window
*
* @param {Object} conversationWindowData The data to be obtained by the
* window when it opens.
* @param {Function} windowCloseCallback Callback for when the window closes.
* @returns {Number} The id of the window.
*/
openChatWindow: function(conversationWindowData) {
return MozLoopServiceInternal.openChatWindow(conversationWindowData);
openChatWindow: function(conversationWindowData, windowCloseCallback) {
return MozLoopServiceInternal.openChatWindow(conversationWindowData,
windowCloseCallback);
},
/**

View File

@ -51,7 +51,6 @@ rooms_default_room_name_template=Conversation {{conversationLabel}}
## by the user specified conversation name.
rooms_welcome_title=Welcome to {{conversationName}}
rooms_leave_button_label=Leave
rooms_new_room_button_label=Start a conversation
rooms_only_occupant_label2=You're the only one here.
rooms_panel_title=Choose a conversation or start a new one
rooms_room_full_label=There are already two people in this conversation.

View File

@ -64,6 +64,7 @@ describe("loop.panel", function() {
},
confirm: sandbox.stub(),
hasEncryptionKey: true,
hangupAllChatWindows: function() {},
logInToFxA: sandbox.stub(),
logOutFromFxA: sandbox.stub(),
notifyUITour: sandbox.stub(),
@ -853,14 +854,13 @@ describe("loop.panel", function() {
dispatch = sandbox.stub(dispatcher, "dispatch");
});
function createTestComponent(pendingOperation) {
function createTestComponent(extraProps) {
return TestUtils.renderIntoDocument(
React.createElement(loop.panel.NewRoomView, {
React.createElement(loop.panel.NewRoomView, _.extend({
dispatcher: dispatcher,
mozLoop: fakeMozLoop,
pendingOperation: pendingOperation,
userDisplayName: fakeEmail
}));
}, extraProps)));
}
it("should dispatch a CreateRoom action with context when clicking on the " +
@ -876,7 +876,10 @@ describe("loop.panel", function() {
});
};
var view = createTestComponent(false);
var view = createTestComponent({
inRoom: false,
pendingOperation: false
});
// Simulate being visible
view.onDocumentVisible();
@ -897,11 +900,48 @@ describe("loop.panel", function() {
it("should disable the create button when pendingOperation is true",
function() {
var view = createTestComponent(true);
var view = createTestComponent({
inRoom: false,
pendingOperation: true
});
var buttonNode = view.getDOMNode().querySelector(".new-room-button[disabled]");
expect(buttonNode).to.not.equal(null);
});
it("should not have a create button when inRoom is true", function() {
var view = createTestComponent({
inRoom: true,
pendingOperation: false
});
var buttonNode = view.getDOMNode().querySelector(".new-room-button");
expect(buttonNode).to.equal(null);
});
it("should have a stop sharing button when inRoom is true", function() {
var view = createTestComponent({
inRoom: true,
pendingOperation: false
});
var buttonNode = view.getDOMNode().querySelector(".stop-sharing-button");
expect(buttonNode).to.not.equal(null);
});
it("should hangup any window when stop sharing is clicked", function() {
var hangupAllChatWindows = sandbox.stub(fakeMozLoop, "hangupAllChatWindows");
var view = createTestComponent({
inRoom: true,
pendingOperation: false
});
var node = view.getDOMNode();
TestUtils.Simulate.click(node.querySelector(".stop-sharing-button"));
sinon.assert.calledOnce(hangupAllChatWindows);
});
});
describe("loop.panel.SignInRequestView", function() {

View File

@ -157,6 +157,23 @@ describe("loop.store.RoomStore", function() {
});
});
describe("close", function() {
it("should update the openedRoom state to null", function() {
store.setStoreState({ openedRoom: "fake1234" });
fakeMozLoop.rooms.trigger("close", "close");
expect(store.getStoreState().openedRoom).to.eql(null);
});
});
describe("open", function() {
it("should update the openedRoom state to the room token", function() {
fakeMozLoop.rooms.trigger("open", "open", "fake1234");
expect(store.getStoreState().openedRoom).to.eql("fake1234");
});
});
describe("update", function() {
it("should update a room entry", function() {
fakeMozLoop.rooms.trigger("update", "update", {

View File

@ -17,6 +17,7 @@ skip-if = e10s
[browser_loop_fxa_server.js]
[browser_LoopRooms_channel.js]
[browser_mozLoop_appVersionInfo.js]
[browser_mozLoop_chat.js]
[browser_mozLoop_context.js]
[browser_mozLoop_prefs.js]
[browser_mozLoop_doNotDisturb.js]

View File

@ -0,0 +1,44 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* This file contains tests for various chat window helpers in MozLoopAPI.
*/
"use strict";
const { Chat } = Cu.import("resource:///modules/Chat.jsm", {});
function isAnyLoopChatOpen() {
return [...Chat.chatboxes].some(({ src }) => /^about:loopconversation#/.test(src));
}
add_task(loadLoopPanel);
add_task(function* test_mozLoop_nochat() {
Assert.ok(!isAnyLoopChatOpen(), "there should be no chat windows yet");
});
add_task(function* test_mozLoop_openchat() {
let windowData = {
roomToken: "fake1234",
type: "room"
};
LoopRooms.open(windowData);
Assert.ok(isAnyLoopChatOpen(), "chat window should have been opened");
});
add_task(function* test_mozLoop_hangupAllChatWindows() {
let windowData = {
roomToken: "fake2345",
type: "room"
};
LoopRooms.open(windowData);
yield promiseWaitForCondition(() => {
gMozLoopAPI.hangupAllChatWindows();
return !isAnyLoopChatOpen();
});
Assert.ok(!isAnyLoopChatOpen(), "chat window should have been closed");
});

View File

@ -96,9 +96,9 @@ function waitForCondition(condition, nextTest, errorMsg) {
}
function promiseWaitForCondition(aConditionFn) {
let deferred = Promise.defer();
waitForCondition(aConditionFn, deferred.resolve, "Condition didn't pass.");
return deferred.promise;
return new Promise((resolve, reject) => {
waitForCondition(aConditionFn, resolve, "Condition didn't pass.");
});
}
/**

View File

@ -397,6 +397,17 @@
callback(null, []);
};
var roomStoreOpenedRoom = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop,
activeRoomStore: makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS
})
});
roomStoreOpenedRoom.setStoreState({
openedRoom: "3jKS_Els9IU"
});
var roomStoreNoRooms = new loop.store.RoomStore(new loop.Dispatcher(), {
mozLoop: mockMozLoopNoRooms,
activeRoomStore: new loop.store.ActiveRoomStore(new loop.Dispatcher(), {
@ -690,6 +701,20 @@
)
),
React.createElement(FramedExample, {cssClass: "fx-embedded-panel",
dashed: true,
height: 410,
summary: "Room list (active view)",
width: 330},
React.createElement("div", {className: "panel"},
React.createElement(PanelView, {client: mockClient,
dispatcher: dispatcher,
mozLoop: navigator.mozLoop,
notifications: notifications,
roomStore: roomStoreOpenedRoom})
)
),
React.createElement(FramedExample, {cssClass: "fx-embedded-panel",
dashed: true,
height: 410,

View File

@ -397,6 +397,17 @@
callback(null, []);
};
var roomStoreOpenedRoom = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop,
activeRoomStore: makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS
})
});
roomStoreOpenedRoom.setStoreState({
openedRoom: "3jKS_Els9IU"
});
var roomStoreNoRooms = new loop.store.RoomStore(new loop.Dispatcher(), {
mozLoop: mockMozLoopNoRooms,
activeRoomStore: new loop.store.ActiveRoomStore(new loop.Dispatcher(), {
@ -690,6 +701,20 @@
</div>
</FramedExample>
<FramedExample cssClass="fx-embedded-panel"
dashed={true}
height={410}
summary="Room list (active view)"
width={330}>
<div className="panel">
<PanelView client={mockClient}
dispatcher={dispatcher}
mozLoop={navigator.mozLoop}
notifications={notifications}
roomStore={roomStoreOpenedRoom} />
</div>
</FramedExample>
<FramedExample cssClass="fx-embedded-panel"
dashed={true}
height={410}

View File

@ -22,6 +22,9 @@ sign_in_again_button=Sign In
## will be replaced by the super short brandname.
sign_in_again_use_as_guest_button2=Use {{clientSuperShortname}} as a Guest
panel_browse_with_friend_button=Browse this page with a friend
panel_stop_sharing_tabs_button=Stop sharing your tabs
first_time_experience_button_label=Get Started
## LOCALIZATION_NOTE(first_time_experience_subheading): Message inviting the
## user to create his or her first conversation.
@ -184,7 +187,6 @@ rooms_leave_button_label=Leave
## for emphasis reasons, it is a heading. Proceed as appropriate for locale.
rooms_list_recent_conversations=RECENT CONVERSATIONS
rooms_change_failed_label=Conversation cannot be updated
rooms_new_room_button_label=Start a conversation
rooms_panel_title=Choose a conversation or start a new one
rooms_room_full_label=There are already two people in this conversation.
rooms_room_full_call_to_action_nonFx_label=Download {{brandShortname}} to start your own