Bug 1074694 - Allow rooms to be renamed from the conversation window. r=nperriault

This commit is contained in:
Mark Banner 2014-11-17 22:12:27 +00:00
Родитель 8826f41cbe
Коммит 781163fac2
11 изменённых файлов: 301 добавлений и 30 удалений

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

@ -376,6 +376,34 @@ let LoopRoomsInternal = {
}, callback);
},
/**
* Renames a room.
*
* @param {String} roomToken The room token
* @param {String} newRoomName The new name for the room
* @param {Function} callback Function that will be invoked once the operation
* finished. The first argument passed will be an
* `Error` object or `null`.
*/
rename: function(roomToken, newRoomName, callback) {
let room = this.rooms.get(roomToken);
let url = "/rooms/" + encodeURIComponent(roomToken);
let origRoom = this.rooms.get(roomToken);
let patchData = {
roomName: newRoomName,
// XXX We have to supply the max size and room owner due to bug 1099063.
maxSize: origRoom.maxSize,
roomOwner: origRoom.roomOwner
};
MozLoopService.hawkRequest(this.sessionType, url, "PATCH", patchData)
.then(response => {
let data = JSON.parse(response.body);
extend(room, data);
callback(null, room);
}, error => callback(error)).catch(error => callback(error));
},
/**
* Callback used to indicate changes to rooms data on the LoopServer.
*
@ -443,6 +471,10 @@ this.LoopRooms = {
return LoopRoomsInternal.leave(roomToken, sessionToken, callback);
},
rename: function(roomToken, newRoomName, callback) {
return LoopRoomsInternal.rename(roomToken, newRoomName, callback);
},
promise: function(method, ...params) {
return new Promise((resolve, reject) => {
this[method](...params, (error, result) => {

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

@ -59,7 +59,7 @@ loop.roomViews = (function(mozL10n) {
* Desktop room invitation view (overlay).
*/
var DesktopRoomInvitationView = React.createClass({displayName: 'DesktopRoomInvitationView',
mixins: [ActiveRoomStoreMixin],
mixins: [ActiveRoomStoreMixin, React.addons.LinkedStateMixin],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
@ -67,13 +67,23 @@ loop.roomViews = (function(mozL10n) {
getInitialState: function() {
return {
copiedUrl: false
copiedUrl: false,
newRoomName: ""
}
},
handleFormSubmit: function(event) {
event.preventDefault();
// XXX
var newRoomName = this.state.newRoomName;
if (newRoomName && this.state.roomName != newRoomName) {
this.props.dispatcher.dispatch(
new sharedActions.RenameRoom({
roomToken: this.state.roomToken,
newRoomName: newRoomName
}));
}
},
handleEmailButtonClick: function(event) {
@ -96,7 +106,9 @@ loop.roomViews = (function(mozL10n) {
return (
React.DOM.div({className: "room-invitation-overlay"},
React.DOM.form({onSubmit: this.handleFormSubmit},
React.DOM.input({type: "text", ref: "roomName",
React.DOM.input({type: "text", className: "input-room-name",
valueLink: this.linkState("newRoomName"),
onBlur: this.handleFormSubmit,
placeholder: mozL10n.get("rooms_name_this_room_label")})
),
React.DOM.p(null, mozL10n.get("invite_header_text")),

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

@ -59,7 +59,7 @@ loop.roomViews = (function(mozL10n) {
* Desktop room invitation view (overlay).
*/
var DesktopRoomInvitationView = React.createClass({
mixins: [ActiveRoomStoreMixin],
mixins: [ActiveRoomStoreMixin, React.addons.LinkedStateMixin],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
@ -67,13 +67,23 @@ loop.roomViews = (function(mozL10n) {
getInitialState: function() {
return {
copiedUrl: false
copiedUrl: false,
newRoomName: ""
}
},
handleFormSubmit: function(event) {
event.preventDefault();
// XXX
var newRoomName = this.state.newRoomName;
if (newRoomName && this.state.roomName != newRoomName) {
this.props.dispatcher.dispatch(
new sharedActions.RenameRoom({
roomToken: this.state.roomToken,
newRoomName: newRoomName
}));
}
},
handleEmailButtonClick: function(event) {
@ -96,7 +106,9 @@ loop.roomViews = (function(mozL10n) {
return (
<div className="room-invitation-overlay">
<form onSubmit={this.handleFormSubmit}>
<input type="text" ref="roomName"
<input type="text" className="input-room-name"
valueLink={this.linkState("newRoomName")}
onBlur={this.handleFormSubmit}
placeholder={mozL10n.get("rooms_name_this_room_label")} />
</form>
<p>{mozL10n.get("invite_header_text")}</p>

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

@ -242,6 +242,15 @@ loop.shared.actions = (function() {
roomToken: String
}),
/**
* Renames a room.
* XXX: should move to some roomActions module - refs bug 1079284
*/
RenameRoom: Action.define("renameRoom", {
roomToken: String,
newRoomName: String
}),
/**
* Copy a room url into the user's clipboard.
* XXX: should move to some roomActions module - refs bug 1079284
@ -265,6 +274,19 @@ loop.shared.actions = (function() {
error: Object
}),
/**
* Sets up the room information when it is received.
* XXX: should move to some roomActions module - refs bug 1079284
*
* @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
*/
SetupRoomInfo: Action.define("setupRoomInfo", {
roomName: String,
roomOwner: String,
roomToken: String,
roomUrl: String
}),
/**
* Updates the room information when it is received.
* XXX: should move to some roomActions module - refs bug 1079284
@ -274,7 +296,6 @@ loop.shared.actions = (function() {
UpdateRoomInfo: Action.define("updateRoomInfo", {
roomName: String,
roomOwner: String,
roomToken: String,
roomUrl: String
}),

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

@ -150,6 +150,7 @@ loop.store.ActiveRoomStore = (function() {
_registerActions: function() {
this._dispatcher.register(this, [
"roomFailure",
"setupRoomInfo",
"updateRoomInfo",
"joinRoom",
"joinedRoom",
@ -194,7 +195,7 @@ loop.store.ActiveRoomStore = (function() {
}
this._dispatcher.dispatch(
new sharedActions.UpdateRoomInfo({
new sharedActions.SetupRoomInfo({
roomToken: actionData.roomToken,
roomName: roomData.roomName,
roomOwner: roomData.roomOwner,
@ -227,15 +228,18 @@ loop.store.ActiveRoomStore = (function() {
roomToken: actionData.token,
roomState: ROOM_STATES.READY
});
this._mozLoop.rooms.on("update:" + actionData.roomToken,
this._handleRoomUpdate.bind(this));
},
/**
* Handles the updateRoomInfo action. Updates the room data and
* Handles the setupRoomInfo action. Sets up the initial room data and
* sets the state to `READY`.
*
* @param {sharedActions.UpdateRoomInfo} actionData
* @param {sharedActions.SetupRoomInfo} actionData
*/
updateRoomInfo: function(actionData) {
setupRoomInfo: function(actionData) {
this.setStoreState({
roomName: actionData.roomName,
roomOwner: actionData.roomOwner,
@ -243,6 +247,36 @@ loop.store.ActiveRoomStore = (function() {
roomToken: actionData.roomToken,
roomUrl: actionData.roomUrl
});
this._mozLoop.rooms.on("update:" + actionData.roomToken,
this._handleRoomUpdate.bind(this));
},
/**
* Handles the updateRoomInfo action. Updates the room data.
*
* @param {sharedActions.UpdateRoomInfo} actionData
*/
updateRoomInfo: function(actionData) {
this.setStoreState({
roomName: actionData.roomName,
roomOwner: actionData.roomOwner,
roomUrl: actionData.roomUrl
});
},
/**
* Handles room updates notified by the mozLoop rooms API.
*
* @param {String} eventName The name of the event
* @param {Object} roomData The new roomData.
*/
_handleRoomUpdate: function(eventName, roomData) {
this._dispatcher.dispatch(new sharedActions.UpdateRoomInfo({
roomName: roomData.roomName,
roomOwner: roomData.roomOwner,
roomUrl: roomData.roomUrl
}));
},
/**
@ -351,6 +385,10 @@ loop.store.ActiveRoomStore = (function() {
*/
windowUnload: function() {
this._leaveRoom();
// If we're closing the window, we can stop listening to updates.
this._mozLoop.rooms.off("update:" + this.getStoreState().roomToken,
this._handleRoomUpdate.bind(this));
},
/**

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

@ -87,6 +87,7 @@ loop.store = loop.store || {};
"getAllRooms",
"getAllRoomsError",
"openRoom",
"renameRoom",
"updateRoomList"
]);
}
@ -411,6 +412,21 @@ loop.store = loop.store || {};
*/
openRoom: function(actionData) {
this._mozLoop.rooms.open(actionData.roomToken);
},
/**
* Renames a room.
*
* @param {sharedActions.RenameRoom} actionData
*/
renameRoom: function(actionData) {
this._mozLoop.rooms.rename(actionData.roomToken, actionData.newRoomName,
function(err) {
if (err) {
// XXX Give this a proper UI - bug 1100595.
console.error("Failed to rename the room", err);
}
});
}
}, Backbone.Events);

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

@ -170,7 +170,13 @@ loop.StandaloneMozLoop = (function(mozL10n) {
action: "leave",
sessionToken: sessionToken
}, null, callback);
}
},
// Dummy functions to reflect those in the desktop mozLoop.rooms that we
// don't currently use.
on: function() {},
once: function() {},
off: function() {}
};
var StandaloneMozLoop = function(options) {

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

@ -119,6 +119,48 @@ describe("loop.roomViews", function () {
new sharedActions.EmailRoomUrl({roomUrl: "http://invalid"}));
});
describe("Rename Room", function() {
var roomNameBox;
beforeEach(function() {
view = mountTestComponent();
view.setState({
roomToken: "fakeToken",
roomName: "fakeName"
});
roomNameBox = view.getDOMNode().querySelector('.input-room-name');
React.addons.TestUtils.Simulate.change(roomNameBox, { target: {
value: "reallyFake"
}});
});
it("should dispatch a RenameRoom action when the focus is lost",
function() {
React.addons.TestUtils.Simulate.blur(roomNameBox);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.RenameRoom({
roomToken: "fakeToken",
newRoomName: "reallyFake"
}));
});
it("should dispatch a RenameRoom action when enter is pressed",
function() {
React.addons.TestUtils.Simulate.submit(roomNameBox);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.RenameRoom({
roomToken: "fakeToken",
newRoomName: "reallyFake"
}));
});
});
describe("Copy Button", function() {
beforeEach(function() {
view = mountTestComponent();

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

@ -21,10 +21,12 @@ describe("loop.store.ActiveRoomStore", function () {
fakeMozLoop = {
rooms: {
get: sandbox.stub(),
join: sandbox.stub(),
refreshMembership: sandbox.stub(),
leave: sandbox.stub()
get: sinon.stub(),
join: sinon.stub(),
refreshMembership: sinon.stub(),
leave: sinon.stub(),
on: sinon.stub(),
off: sinon.stub()
}
};
@ -161,7 +163,7 @@ describe("loop.store.ActiveRoomStore", function () {
to.have.property('roomState', ROOM_STATES.GATHER);
});
it("should dispatch an UpdateRoomInfo action if the get is successful",
it("should dispatch an SetupRoomInfo action if the get is successful",
function() {
store.setupWindowData(new sharedActions.SetupWindowData({
windowId: "42",
@ -171,7 +173,7 @@ describe("loop.store.ActiveRoomStore", function () {
sinon.assert.calledTwice(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UpdateRoomInfo(_.extend({
new sharedActions.SetupRoomInfo(_.extend({
roomToken: fakeToken
}, fakeRoomData)));
});
@ -233,7 +235,7 @@ describe("loop.store.ActiveRoomStore", function () {
});
});
describe("#updateRoomInfo", function() {
describe("#setupRoomInfo", function() {
var fakeRoomInfo;
beforeEach(function() {
@ -246,18 +248,39 @@ describe("loop.store.ActiveRoomStore", function () {
});
it("should set the state to READY", function() {
store.updateRoomInfo(new sharedActions.UpdateRoomInfo(fakeRoomInfo));
store.setupRoomInfo(new sharedActions.SetupRoomInfo(fakeRoomInfo));
expect(store._storeState.roomState).eql(ROOM_STATES.READY);
});
it("should save the room information", function() {
store.setupRoomInfo(new sharedActions.SetupRoomInfo(fakeRoomInfo));
var state = store.getStoreState();
expect(state.roomName).eql(fakeRoomInfo.roomName);
expect(state.roomOwner).eql(fakeRoomInfo.roomOwner);
expect(state.roomToken).eql(fakeRoomInfo.roomToken);
expect(state.roomUrl).eql(fakeRoomInfo.roomUrl);
});
});
describe("#updateRoomInfo", function() {
var fakeRoomInfo;
beforeEach(function() {
fakeRoomInfo = {
roomName: "Its a room",
roomOwner: "Me",
roomUrl: "http://invalid"
};
});
it("should save the room information", function() {
store.updateRoomInfo(new sharedActions.UpdateRoomInfo(fakeRoomInfo));
var state = store.getStoreState();
expect(state.roomName).eql(fakeRoomInfo.roomName);
expect(state.roomOwner).eql(fakeRoomInfo.roomOwner);
expect(state.roomToken).eql(fakeRoomInfo.roomToken);
expect(state.roomUrl).eql(fakeRoomInfo.roomUrl);
});
});
@ -596,4 +619,33 @@ describe("loop.store.ActiveRoomStore", function () {
expect(store._storeState.roomState).eql(ROOM_STATES.READY);
});
});
describe("Events", function() {
describe("update:{roomToken}", function() {
beforeEach(function() {
store.setupRoomInfo(new sharedActions.SetupRoomInfo({
roomName: "Its a room",
roomOwner: "Me",
roomToken: "fakeToken",
roomUrl: "http://invalid"
}));
});
it("should dispatch an UpdateRoomInfo action", function() {
sinon.assert.calledOnce(fakeMozLoop.rooms.on);
var fakeRoomData = {
roomName: "fakeName",
roomOwner: "you",
roomUrl: "original"
};
fakeMozLoop.rooms.on.callArgWith(1, "update", fakeRoomData);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UpdateRoomInfo(fakeRoomData));
});
});
});
});

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

@ -437,4 +437,31 @@ describe("loop.store.RoomStore", function () {
sinon.assert.calledWithExactly(fakeMozLoop.rooms.open, "42abc");
});
});
describe("#renameRoom", function() {
var store, fakeMozLoop;
beforeEach(function() {
fakeMozLoop = {
rooms: {
rename: sinon.spy()
}
};
store = new loop.store.RoomStore({
dispatcher: dispatcher,
mozLoop: fakeMozLoop
});
});
it("should rename the room via mozLoop", function() {
dispatcher.dispatch(new sharedActions.RenameRoom({
roomToken: "42abc",
newRoomName: "silly name"
}));
sinon.assert.calledOnce(fakeMozLoop.rooms.rename);
sinon.assert.calledWith(fakeMozLoop.rooms.rename, "42abc",
"silly name");
});
});
});

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

@ -209,16 +209,22 @@ add_task(function* setup_server() {
res.finish();
}
function getJSONData(body) {
return JSON.parse(CommonUtils.readBytesFromInputStream(body));
}
// Add a request handler for each room in the list.
[...kRooms.values()].forEach(function(room) {
loopServer.registerPathHandler("/rooms/" + encodeURIComponent(room.roomToken), (req, res) => {
if (req.method == "POST") {
let body = CommonUtils.readBytesFromInputStream(req.bodyInputStream);
let data = JSON.parse(body);
let data = getJSONData(req.bodyInputStream);
res.setStatusLine(null, 200, "OK");
res.write(JSON.stringify(data));
res.processAsync();
res.finish();
} else if (req.method == "PATCH") {
let data = getJSONData(req.bodyInputStream);
returnRoomDetails(res, data.roomName);
} else {
returnRoomDetails(res, room.roomName);
}
@ -363,6 +369,13 @@ add_task(function* test_leaveRoom() {
Assert.equal(leaveData.sessionToken, "fakeLeaveSessionToken");
});
// Test if renaming a room works as expected.
add_task(function* test_renameRoom() {
let roomToken = "_nxD4V4FflQ";
let renameData = yield LoopRooms.promise("rename", roomToken, "fakeName");
Assert.equal(renameData.roomName, "fakeName");
});
// Test if the event emitter implementation doesn't leak and is working as expected.
add_task(function* () {
Assert.strictEqual(gExpectedAdds.length, 0, "No room additions should be expected anymore");