forked from mirrors/gecko-dev
583 lines
18 KiB
JavaScript
583 lines
18 KiB
JavaScript
/* 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/. */
|
|
|
|
var loop = loop || {};
|
|
loop.store = loop.store || {};
|
|
|
|
(function(mozL10n) {
|
|
"use strict";
|
|
|
|
/**
|
|
* Shared actions.
|
|
* @type {Object}
|
|
*/
|
|
var sharedActions = loop.shared.actions;
|
|
|
|
/**
|
|
* Maximum size given to createRoom; only 2 is supported (and is
|
|
* always passed) because that's what the user-experience is currently
|
|
* designed and tested to handle.
|
|
* @type {Number}
|
|
*/
|
|
var MAX_ROOM_CREATION_SIZE = loop.store.MAX_ROOM_CREATION_SIZE = 2;
|
|
|
|
/**
|
|
* Room validation schema. See validate.js.
|
|
* @type {Object}
|
|
*/
|
|
var roomSchema = {
|
|
roomToken: String,
|
|
roomUrl: String,
|
|
// roomName: String - Optional.
|
|
// roomKey: String - Optional.
|
|
maxSize: Number,
|
|
participants: Array,
|
|
ctime: Number
|
|
};
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @param {loop.Dispatcher} dispatcher The dispatcher for dispatching actions
|
|
* and registering to consume actions.
|
|
* @param {Object} options Options object:
|
|
* - {mozLoop} mozLoop The MozLoop API object.
|
|
* - {ActiveRoomStore} activeRoomStore An optional substore for active room
|
|
* state.
|
|
* - {Notifications} notifications An optional notifications item that is
|
|
* required if create actions are to be used
|
|
*/
|
|
loop.store.RoomStore = loop.store.createStore({
|
|
/**
|
|
* Maximum size given to createRoom; only 2 is supported (and is
|
|
* always passed) because that's what the user-experience is currently
|
|
* designed and tested to handle.
|
|
* @type {Number}
|
|
*/
|
|
maxRoomCreationSize: MAX_ROOM_CREATION_SIZE,
|
|
|
|
/**
|
|
* Registered actions.
|
|
* @type {Array}
|
|
*/
|
|
actions: [
|
|
"addSocialShareProvider",
|
|
"createRoom",
|
|
"createdRoom",
|
|
"createRoomError",
|
|
"copyRoomUrl",
|
|
"deleteRoom",
|
|
"deleteRoomError",
|
|
"emailRoomUrl",
|
|
"getAllRooms",
|
|
"getAllRoomsError",
|
|
"openRoom",
|
|
"shareRoomUrl",
|
|
"updateRoomContext",
|
|
"updateRoomContextDone",
|
|
"updateRoomContextError",
|
|
"updateRoomList"
|
|
],
|
|
|
|
initialize: function(options) {
|
|
if (!options.mozLoop) {
|
|
throw new Error("Missing option mozLoop");
|
|
}
|
|
this._mozLoop = options.mozLoop;
|
|
this._notifications = options.notifications;
|
|
|
|
if (options.activeRoomStore) {
|
|
this.activeRoomStore = options.activeRoomStore;
|
|
this.activeRoomStore.on("change",
|
|
this._onActiveRoomStoreChange.bind(this));
|
|
}
|
|
},
|
|
|
|
getInitialStoreState: function() {
|
|
return {
|
|
activeRoom: this.activeRoomStore ? this.activeRoomStore.getStoreState() : {},
|
|
error: null,
|
|
pendingCreation: false,
|
|
pendingInitialRetrieval: true,
|
|
rooms: [],
|
|
savingContext: false
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Registers mozLoop.rooms events.
|
|
*/
|
|
startListeningToRoomEvents: function() {
|
|
// Rooms event registration
|
|
this._mozLoop.rooms.on("add", this._onRoomAdded.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));
|
|
},
|
|
|
|
/**
|
|
* Updates active room store state.
|
|
*/
|
|
_onActiveRoomStoreChange: function() {
|
|
this.setStoreState({activeRoom: this.activeRoomStore.getStoreState()});
|
|
},
|
|
|
|
/**
|
|
* Updates current room list when a new room is available.
|
|
*
|
|
* @param {String} eventName The event name (unused).
|
|
* @param {Object} addedRoomData The added room data.
|
|
*/
|
|
_onRoomAdded: function(eventName, addedRoomData) {
|
|
addedRoomData.participants = addedRoomData.participants || [];
|
|
addedRoomData.ctime = addedRoomData.ctime || new Date().getTime();
|
|
this.dispatchAction(new sharedActions.UpdateRoomList({
|
|
// Ensure the room isn't part of the list already, then add it.
|
|
roomList: this._storeState.rooms.filter(function(room) {
|
|
return addedRoomData.roomToken !== room.roomToken;
|
|
}).concat(new Room(addedRoomData))
|
|
}));
|
|
},
|
|
|
|
/**
|
|
* Executed when a room is updated.
|
|
*
|
|
* @param {String} eventName The event name (unused).
|
|
* @param {Object} updatedRoomData The updated room data.
|
|
*/
|
|
_onRoomUpdated: function(eventName, updatedRoomData) {
|
|
this.dispatchAction(new sharedActions.UpdateRoomList({
|
|
roomList: this._storeState.rooms.map(function(room) {
|
|
return room.roomToken === updatedRoomData.roomToken ?
|
|
updatedRoomData : room;
|
|
})
|
|
}));
|
|
},
|
|
|
|
/**
|
|
* Executed when a room is deleted.
|
|
*
|
|
* @param {String} eventName The event name (unused).
|
|
* @param {Object} removedRoomData The removed room data.
|
|
*/
|
|
_onRoomRemoved: function(eventName, removedRoomData) {
|
|
this.dispatchAction(new sharedActions.UpdateRoomList({
|
|
roomList: this._storeState.rooms.filter(function(room) {
|
|
return room.roomToken !== removedRoomData.roomToken;
|
|
})
|
|
}));
|
|
},
|
|
|
|
/**
|
|
* Executed when the user switches accounts.
|
|
*
|
|
* @param {String} eventName The event name (unused).
|
|
*/
|
|
_onRoomsRefresh: function(eventName) {
|
|
this.dispatchAction(new sharedActions.UpdateRoomList({
|
|
roomList: []
|
|
}));
|
|
},
|
|
|
|
/**
|
|
* Maps and sorts the raw room list received from the mozLoop API.
|
|
*
|
|
* @param {Array} rawRoomList Raw room list.
|
|
* @return {Array}
|
|
*/
|
|
_processRoomList: function(rawRoomList) {
|
|
if (!rawRoomList) {
|
|
return [];
|
|
}
|
|
return rawRoomList
|
|
.map(function(rawRoom) {
|
|
return new Room(rawRoom);
|
|
})
|
|
.slice()
|
|
.sort(function(a, b) {
|
|
return b.ctime - a.ctime;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Finds the next available room number in the provided room list.
|
|
*
|
|
* @param {String} nameTemplate The room name template; should contain a
|
|
* {{conversationLabel}} placeholder.
|
|
* @return {Number}
|
|
*/
|
|
findNextAvailableRoomNumber: function(nameTemplate) {
|
|
var searchTemplate = nameTemplate.replace("{{conversationLabel}}", "");
|
|
var searchRegExp = new RegExp("^" + searchTemplate + "(\\d+)$");
|
|
|
|
var roomNumbers = this._storeState.rooms.map(function(room) {
|
|
var match = searchRegExp.exec(room.decryptedContext.roomName);
|
|
return match && match[1] ? parseInt(match[1], 10) : 0;
|
|
});
|
|
|
|
if (!roomNumbers.length) {
|
|
return 1;
|
|
}
|
|
|
|
return Math.max.apply(null, roomNumbers) + 1;
|
|
},
|
|
|
|
/**
|
|
* Generates a room names against the passed template string.
|
|
*
|
|
* @param {String} nameTemplate The room name template.
|
|
* @return {String}
|
|
*/
|
|
_generateNewRoomName: function(nameTemplate) {
|
|
var roomLabel = this.findNextAvailableRoomNumber(nameTemplate);
|
|
return nameTemplate.replace("{{conversationLabel}}", roomLabel);
|
|
},
|
|
|
|
/**
|
|
* Creates a new room.
|
|
*
|
|
* @param {sharedActions.CreateRoom} actionData The new room information.
|
|
*/
|
|
createRoom: function(actionData) {
|
|
this.setStoreState({
|
|
pendingCreation: true,
|
|
error: null
|
|
});
|
|
|
|
var roomCreationData = {
|
|
decryptedContext: {
|
|
roomName: this._generateNewRoomName(actionData.nameTemplate)
|
|
},
|
|
maxSize: this.maxRoomCreationSize
|
|
};
|
|
|
|
if ("urls" in actionData) {
|
|
roomCreationData.decryptedContext.urls = actionData.urls;
|
|
}
|
|
|
|
this._notifications.remove("create-room-error");
|
|
|
|
this._mozLoop.rooms.create(roomCreationData, function(err, createdRoom) {
|
|
var buckets = this._mozLoop.ROOM_CREATE;
|
|
if (err) {
|
|
this._mozLoop.telemetryAddValue("LOOP_ROOM_CREATE", buckets.CREATE_FAIL);
|
|
this.dispatchAction(new sharedActions.CreateRoomError({error: err}));
|
|
return;
|
|
}
|
|
|
|
this.dispatchAction(new sharedActions.CreatedRoom({
|
|
roomToken: createdRoom.roomToken
|
|
}));
|
|
this._mozLoop.telemetryAddValue("LOOP_ROOM_CREATE", buckets.CREATE_SUCCESS);
|
|
|
|
// Since creating a room with context is only possible from the panel,
|
|
// we can record that as the action here.
|
|
var URLs = roomCreationData.decryptedContext.urls;
|
|
if (URLs && URLs.length) {
|
|
buckets = this._mozLoop.ROOM_CONTEXT_ADD;
|
|
this._mozLoop.telemetryAddValue("LOOP_ROOM_CONTEXT_ADD",
|
|
buckets.ADD_FROM_PANEL);
|
|
}
|
|
}.bind(this));
|
|
},
|
|
|
|
/**
|
|
* Executed when a room has been created
|
|
*/
|
|
createdRoom: function(actionData) {
|
|
this.setStoreState({pendingCreation: false});
|
|
|
|
// Opens the newly created room
|
|
this.dispatchAction(new sharedActions.OpenRoom({
|
|
roomToken: actionData.roomToken
|
|
}));
|
|
},
|
|
|
|
/**
|
|
* Executed when a room creation error occurs.
|
|
*
|
|
* @param {sharedActions.CreateRoomError} actionData The action data.
|
|
*/
|
|
createRoomError: function(actionData) {
|
|
this.setStoreState({
|
|
error: actionData.error,
|
|
pendingCreation: false
|
|
});
|
|
|
|
// XXX Needs a more descriptive error - bug 1109151.
|
|
this._notifications.set({
|
|
id: "create-room-error",
|
|
level: "error",
|
|
message: mozL10n.get("generic_failure_message")
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Copy a room url.
|
|
*
|
|
* @param {sharedActions.CopyRoomUrl} actionData The action data.
|
|
*/
|
|
copyRoomUrl: function(actionData) {
|
|
this._mozLoop.copyString(actionData.roomUrl);
|
|
this._mozLoop.notifyUITour("Loop:RoomURLCopied");
|
|
|
|
var from = actionData.from;
|
|
var bucket = this._mozLoop.SHARING_ROOM_URL["COPY_FROM_" + from.toUpperCase()];
|
|
if (typeof bucket === "undefined") {
|
|
console.error("No URL sharing type bucket found for '" + from + "'");
|
|
return;
|
|
}
|
|
this._mozLoop.telemetryAddValue("LOOP_SHARING_ROOM_URL", bucket);
|
|
},
|
|
|
|
/**
|
|
* Emails a room url.
|
|
*
|
|
* @param {sharedActions.EmailRoomUrl} actionData The action data.
|
|
*/
|
|
emailRoomUrl: function(actionData) {
|
|
loop.shared.utils.composeCallUrlEmail(actionData.roomUrl, null,
|
|
actionData.roomDescription, actionData.from);
|
|
this._mozLoop.notifyUITour("Loop:RoomURLEmailed");
|
|
},
|
|
|
|
/**
|
|
* Share a room url.
|
|
*
|
|
* @param {sharedActions.ShareRoomUrl} actionData The action data.
|
|
*/
|
|
shareRoomUrl: function(actionData) {
|
|
var providerOrigin = new URL(actionData.provider.origin).hostname;
|
|
var shareTitle = "";
|
|
var shareBody = null;
|
|
|
|
switch (providerOrigin) {
|
|
case "mail.google.com":
|
|
shareTitle = mozL10n.get("share_email_subject6");
|
|
shareBody = mozL10n.get("share_email_body6", {
|
|
callUrl: actionData.roomUrl
|
|
});
|
|
shareBody += mozL10n.get("share_email_footer");
|
|
break;
|
|
case "twitter.com":
|
|
default:
|
|
shareTitle = mozL10n.get("share_tweet", {
|
|
clientShortname2: mozL10n.get("clientShortname2")
|
|
});
|
|
break;
|
|
}
|
|
|
|
this._mozLoop.socialShareRoom(actionData.provider.origin, actionData.roomUrl,
|
|
shareTitle, shareBody);
|
|
this._mozLoop.notifyUITour("Loop:RoomURLShared");
|
|
},
|
|
|
|
/**
|
|
* Open the share panel to add a Social share provider.
|
|
*
|
|
* @param {sharedActions.AddSocialShareProvider} actionData The action data.
|
|
*/
|
|
addSocialShareProvider: function(actionData) {
|
|
this._mozLoop.addSocialShareProvider();
|
|
},
|
|
|
|
/**
|
|
* Creates a new room.
|
|
*
|
|
* @param {sharedActions.DeleteRoom} actionData The action data.
|
|
*/
|
|
deleteRoom: function(actionData) {
|
|
this._mozLoop.rooms.delete(actionData.roomToken, function(err) {
|
|
var buckets = this._mozLoop.ROOM_DELETE;
|
|
if (err) {
|
|
this.dispatchAction(new sharedActions.DeleteRoomError({error: err}));
|
|
}
|
|
this._mozLoop.telemetryAddValue("LOOP_ROOM_DELETE", buckets[err ?
|
|
"DELETE_FAIL" : "DELETE_SUCCESS"]);
|
|
}.bind(this));
|
|
},
|
|
|
|
/**
|
|
* Executed when a room deletion error occurs.
|
|
*
|
|
* @param {sharedActions.DeleteRoomError} actionData The action data.
|
|
*/
|
|
deleteRoomError: function(actionData) {
|
|
this.setStoreState({error: actionData.error});
|
|
},
|
|
|
|
/**
|
|
* Gather the list of all available rooms from the MozLoop API.
|
|
*/
|
|
getAllRooms: function() {
|
|
this._mozLoop.rooms.getAll(null, function(err, rawRoomList) {
|
|
var action;
|
|
|
|
this.setStoreState({pendingInitialRetrieval: false});
|
|
|
|
if (err) {
|
|
action = new sharedActions.GetAllRoomsError({error: err});
|
|
} else {
|
|
action = new sharedActions.UpdateRoomList({roomList: rawRoomList});
|
|
}
|
|
|
|
this.dispatchAction(action);
|
|
|
|
// We can only start listening to room events after getAll() has been
|
|
// called executed first.
|
|
this.startListeningToRoomEvents();
|
|
}.bind(this));
|
|
},
|
|
|
|
/**
|
|
* Updates current error state in case getAllRooms failed.
|
|
*
|
|
* @param {sharedActions.GetAllRoomsError} actionData The action data.
|
|
*/
|
|
getAllRoomsError: function(actionData) {
|
|
this.setStoreState({error: actionData.error});
|
|
},
|
|
|
|
/**
|
|
* Updates current room list.
|
|
*
|
|
* @param {sharedActions.UpdateRoomList} actionData The action data.
|
|
*/
|
|
updateRoomList: function(actionData) {
|
|
this.setStoreState({
|
|
error: undefined,
|
|
rooms: this._processRoomList(actionData.roomList)
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Opens a room
|
|
*
|
|
* @param {sharedActions.OpenRoom} actionData The action data.
|
|
*/
|
|
openRoom: function(actionData) {
|
|
this._mozLoop.rooms.open(actionData.roomToken);
|
|
},
|
|
|
|
/**
|
|
* Updates the context data attached to a room.
|
|
*
|
|
* @param {sharedActions.UpdateRoomContext} actionData
|
|
*/
|
|
updateRoomContext: function(actionData) {
|
|
this.setStoreState({ savingContext: true });
|
|
this._mozLoop.rooms.get(actionData.roomToken, function(err, room) {
|
|
if (err) {
|
|
this.dispatchAction(new sharedActions.UpdateRoomContextError({
|
|
error: err
|
|
}));
|
|
return;
|
|
}
|
|
|
|
var roomData = {};
|
|
var context = room.decryptedContext;
|
|
var oldRoomName = context.roomName;
|
|
var newRoomName = actionData.newRoomName.trim();
|
|
if (newRoomName && oldRoomName !== newRoomName) {
|
|
roomData.roomName = newRoomName;
|
|
}
|
|
var oldRoomURLs = context.urls;
|
|
var oldRoomURL = oldRoomURLs && oldRoomURLs[0];
|
|
// Since we want to prevent storing falsy (i.e. empty) values for context
|
|
// data, there's no need to send that to the server as an update.
|
|
var newRoomURL = loop.shared.utils.stripFalsyValues({
|
|
location: actionData.newRoomURL ? actionData.newRoomURL.trim() : "",
|
|
thumbnail: actionData.newRoomURL ? actionData.newRoomThumbnail.trim() : "",
|
|
description: actionData.newRoomDescription ?
|
|
actionData.newRoomDescription.trim() : ""
|
|
});
|
|
// Only attach a context to the room when
|
|
// 1) there was already a URL set,
|
|
// 2) a new URL is provided as of now,
|
|
// 3) the URL data has changed.
|
|
var diff = loop.shared.utils.objectDiff(oldRoomURL, newRoomURL);
|
|
if (diff.added.length || diff.updated.length) {
|
|
newRoomURL = _.extend(oldRoomURL || {}, newRoomURL);
|
|
var isValidURL = false;
|
|
try {
|
|
isValidURL = new URL(newRoomURL.location);
|
|
} catch(ex) {
|
|
// URL may throw, default to false;
|
|
}
|
|
if (isValidURL) {
|
|
roomData.urls = [newRoomURL];
|
|
}
|
|
}
|
|
// TODO: there currently is no clear UX defined on what to do when all
|
|
// context data was cleared, e.g. when diff.removed contains all the
|
|
// context properties. Until then, we can't deal with context removal here.
|
|
|
|
// When no properties have been set on the roomData object, there's nothing
|
|
// to save.
|
|
if (!Object.getOwnPropertyNames(roomData).length) {
|
|
// Ensure async actions so that we get separate setStoreState events
|
|
// that React components won't miss.
|
|
setTimeout(function() {
|
|
this.dispatchAction(new sharedActions.UpdateRoomContextDone());
|
|
}.bind(this), 0);
|
|
return;
|
|
}
|
|
|
|
var hadContextBefore = !!oldRoomURL;
|
|
|
|
this.setStoreState({error: null});
|
|
this._mozLoop.rooms.update(actionData.roomToken, roomData,
|
|
function(error, data) {
|
|
var action = error ?
|
|
new sharedActions.UpdateRoomContextError({ error: error }) :
|
|
new sharedActions.UpdateRoomContextDone();
|
|
this.dispatchAction(action);
|
|
|
|
if (!err && !hadContextBefore) {
|
|
// Since updating the room context data is only possible from the
|
|
// conversation window, we can assume that any newly added URL was
|
|
// done from there.
|
|
var buckets = this._mozLoop.ROOM_CONTEXT_ADD;
|
|
this._mozLoop.telemetryAddValue("LOOP_ROOM_CONTEXT_ADD",
|
|
buckets.ADD_FROM_CONVERSATION);
|
|
}
|
|
}.bind(this));
|
|
}.bind(this));
|
|
},
|
|
|
|
/**
|
|
* Handles the updateRoomContextDone action.
|
|
*/
|
|
updateRoomContextDone: function() {
|
|
this.setStoreState({ savingContext: false });
|
|
},
|
|
|
|
/**
|
|
* Updating the context data attached to a room error.
|
|
*
|
|
* @param {sharedActions.UpdateRoomContextError} actionData
|
|
*/
|
|
updateRoomContextError: function(actionData) {
|
|
this.setStoreState({
|
|
error: actionData.error,
|
|
savingContext: false
|
|
});
|
|
}
|
|
});
|
|
})(document.mozL10n || navigator.mozL10n);
|