Merge fx-team to m-c a=merge CLOSED TREE

This commit is contained in:
Wes Kocher 2014-10-09 16:46:51 -07:00
commit 30b41e05c3
164 changed files with 1733 additions and 406 deletions

View File

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

View File

@ -461,8 +461,10 @@ this.GoogleImporter.prototype = {
contact.org = [];
contact.jobTitle = [];
for (let [,orgNode] of Iterator(orgNodes)) {
contact.org.push(orgNode.getElementsByTagNameNS(kNS_GD, "orgName")[0].firstChild.nodeValue);
contact.jobTitle.push(orgNode.getElementsByTagNameNS(kNS_GD, "orgTitle")[0].firstChild.nodeValue);
let orgElement = orgNode.getElementsByTagNameNS(kNS_GD, "orgName")[0];
let titleElement = orgNode.getElementsByTagNameNS(kNS_GD, "orgTitle")[0];
contact.org.push(orgElement ? orgElement.firstChild.nodeValue : "")
contact.jobTitle.push(titleElement ? titleElement.firstChild.nodeValue : "");
}
}

View File

@ -77,19 +77,27 @@ XPCOMUtils.defineLazyGetter(this, "log", () => {
return new ConsoleAPI(consoleOptions);
});
function setJSONPref(aName, aValue) {
let value = !!aValue ? JSON.stringify(aValue) : "";
Services.prefs.setCharPref(aName, value);
}
function getJSONPref(aName) {
let value = Services.prefs.getCharPref(aName);
return !!value ? JSON.parse(value) : null;
}
// The current deferred for the registration process. This is set if in progress
// or the registration was successful. This is null if a registration attempt was
// unsuccessful.
let gRegisteredDeferred = null;
let gPushHandler = null;
let gHawkClient = null;
let gLocalizedStrings = null;
let gLocalizedStrings = null;
let gInitializeTimer = null;
let gFxAEnabled = true;
let gFxAOAuthClientPromise = null;
let gFxAOAuthClient = null;
let gFxAOAuthTokenData = null;
let gFxAOAuthProfile = null;
let gErrors = new Map();
/**
@ -306,6 +314,38 @@ let MozLoopServiceInternal = {
return this.expiryTimeSeconds * 1000 > Date.now();
},
/**
* Retrieves MozLoopService Firefox Accounts OAuth token.
*
* @return {Object} OAuth token
*/
get fxAOAuthTokenData() {
return getJSONPref("loop.fxa_oauth.tokendata");
},
/**
* Sets MozLoopService Firefox Accounts OAuth token.
* If the tokenData is being cleared, will also clear the
* profile since the profile is dependent on the token data.
*
* @param {Object} aTokenData OAuth token
*/
set fxAOAuthTokenData(aTokenData) {
setJSONPref("loop.fxa_oauth.tokendata", aTokenData);
if (!aTokenData) {
this.fxAOAuthProfile = null;
}
},
/**
* Sets MozLoopService Firefox Accounts Profile data.
*
* @param {Object} aProfileData Profile data
*/
set fxAOAuthProfile(aProfileData) {
setJSONPref("loop.fxa_oauth.profile", aProfileData);
},
/**
* Retrieves MozLoopService "do not disturb" pref value.
*
@ -420,9 +460,8 @@ let MozLoopServiceInternal = {
let result = gRegisteredDeferred.promise;
gPushHandler = mockPushHandler || MozLoopPushHandler;
gPushHandler.initialize(this.onPushRegistered.bind(this),
this.onHandleNotification.bind(this));
this.onHandleNotification.bind(this));
return result;
},
@ -575,12 +614,15 @@ let MozLoopServiceInternal = {
if (!gRegisteredDeferred) {
return;
}
gRegisteredDeferred.resolve();
gRegisteredDeferred.resolve("registered to guest status");
// No need to clear the promise here, everything was good, so we don't need
// to re-register.
}, (error) => {
}, error => {
log.error("Failed to register with Loop server: ", error);
gRegisteredDeferred.reject(error.errno);
// registerWithLoopServer may have already made this null.
if (gRegisteredDeferred) {
gRegisteredDeferred.reject(error);
}
gRegisteredDeferred = null;
});
},
@ -616,6 +658,8 @@ let MozLoopServiceInternal = {
log.error("Failed to register with the loop server. Error: ", error);
this.setError("registration", error);
gRegisteredDeferred.reject(error);
gRegisteredDeferred = null;
throw error;
}
);
@ -1069,15 +1113,36 @@ let MozLoopServiceInternal = {
};
Object.freeze(MozLoopServiceInternal);
let gInitializeTimerFunc = () => {
// Kick off the push notification service into registering after a timeout
// this ensures we're not doing too much straight after the browser's finished
let gInitializeTimerFunc = (deferredInitialization, mockPushHandler, mockWebSocket) => {
// Kick off the push notification service into registering after a timeout.
// This ensures we're not doing too much straight after the browser's finished
// starting up.
gInitializeTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
gInitializeTimer.initWithCallback(() => {
MozLoopService.register();
gInitializeTimer.initWithCallback(Task.async(function* initializationCallback() {
yield MozLoopService.register(mockPushHandler, mockWebSocket).then(Task.async(function*() {
if (!MozLoopServiceInternal.fxAOAuthTokenData) {
log.debug("MozLoopService: Initialized without an already logged-in account");
deferredInitialization.resolve("initialized to guest status");
return;
}
log.debug("MozLoopService: Initializing with already logged-in account");
let registeredPromise =
MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA,
gPushHandler.pushUrl);
registeredPromise.then(() => {
deferredInitialization.resolve("initialized to logged-in status");
}, error => {
log.debug("MozLoopService: error logging in using cached auth token");
MozLoopServiceInternal.setError("login", error);
deferredInitialization.reject("error logging in using cached auth token");
});
}), error => {
log.debug("MozLoopService: Failure of initial registration", error);
deferredInitialization.reject(error);
});
gInitializeTimer = null;
},
}),
MozLoopServiceInternal.initialRegistrationDelayMilliseconds, Ci.nsITimer.TYPE_ONE_SHOT);
};
@ -1094,9 +1159,10 @@ this.MozLoopService = {
/**
* Initialized the loop service, and starts registration with the
* push and loop servers.
*
* @return {Promise}
*/
initialize: function() {
initialize: Task.async(function*(mockPushHandler, mockWebSocket) {
// Do this here, rather than immediately after definition, so that we can
// stub out API functions for unit testing
Object.freeze(this);
@ -1104,21 +1170,34 @@ this.MozLoopService = {
// Don't do anything if loop is not enabled.
if (!Services.prefs.getBoolPref("loop.enabled") ||
Services.prefs.getBoolPref("loop.throttled")) {
return;
return Promise.reject("loop is not enabled");
}
if (Services.prefs.getPrefType("loop.fxa.enabled") == Services.prefs.PREF_BOOL) {
gFxAEnabled = Services.prefs.getBoolPref("loop.fxa.enabled");
if (!gFxAEnabled) {
this.logOutFromFxA();
yield this.logOutFromFxA();
}
}
// If expiresTime is in the future then kick-off registration.
if (MozLoopServiceInternal.urlExpiryTimeIsInFuture()) {
gInitializeTimerFunc();
// If expiresTime is not in the future and the user hasn't
// previously authenticated then skip registration.
if (!MozLoopServiceInternal.urlExpiryTimeIsInFuture() &&
!MozLoopServiceInternal.fxAOAuthTokenData) {
return Promise.resolve("registration not needed");
}
},
let deferredInitialization = Promise.defer();
gInitializeTimerFunc(deferredInitialization, mockPushHandler, mockWebSocket);
return deferredInitialization.promise.catch(error => {
if (typeof(error) == "object") {
// This never gets cleared since there is no UI to recover. Only restarting will work.
MozLoopServiceInternal.setError("initialization", error);
}
throw error;
});
}),
/**
* If we're operating the service in "soft start" mode, and this browser
@ -1248,7 +1327,7 @@ this.MozLoopService = {
* sooner, this function is a no-op; this ensures we always have the latest
* expiry time for a url.
*
* This is used to deterimine whether or not we should be registering with the
* This is used to determine whether or not we should be registering with the
* push server on start.
*
* @param {Integer} expiryTimeSeconds The seconds since epoch of the expiry time
@ -1305,8 +1384,16 @@ this.MozLoopService = {
return gFxAEnabled;
},
/**
* Gets the user profile, but only if there is
* tokenData present. Without tokenData, the
* profile is meaningless.
*
* @return {Object}
*/
get userProfile() {
return gFxAOAuthProfile;
return getJSONPref("loop.fxa_oauth.tokendata") &&
getJSONPref("loop.fxa_oauth.profile");
},
get errors() {
@ -1435,15 +1522,15 @@ this.MozLoopService = {
* @return {Promise} that resolves when the FxA login flow is complete.
*/
logInToFxA: function() {
log.debug("logInToFxA with gFxAOAuthTokenData:", !!gFxAOAuthTokenData);
if (gFxAOAuthTokenData) {
return Promise.resolve(gFxAOAuthTokenData);
log.debug("logInToFxA with fxAOAuthTokenData:", !!MozLoopServiceInternal.fxAOAuthTokenData);
if (MozLoopServiceInternal.fxAOAuthTokenData) {
return Promise.resolve(MozLoopServiceInternal.fxAOAuthTokenData);
}
return MozLoopServiceInternal.promiseFxAOAuthAuthorization().then(response => {
return MozLoopServiceInternal.promiseFxAOAuthToken(response.code, response.state);
}).then(tokenData => {
gFxAOAuthTokenData = tokenData;
MozLoopServiceInternal.fxAOAuthTokenData = tokenData;
return tokenData;
}).then(tokenData => {
return gRegisteredDeferred.promise.then(Task.async(function*() {
@ -1454,7 +1541,7 @@ this.MozLoopService = {
}
MozLoopServiceInternal.clearError("login");
MozLoopServiceInternal.clearError("profile");
return gFxAOAuthTokenData;
return MozLoopServiceInternal.fxAOAuthTokenData;
}));
}).then(tokenData => {
let client = new FxAccountsProfileClient({
@ -1462,18 +1549,18 @@ this.MozLoopService = {
token: tokenData.access_token
});
client.fetchProfile().then(result => {
gFxAOAuthProfile = result;
MozLoopServiceInternal.fxAOAuthProfile = result;
MozLoopServiceInternal.notifyStatusChanged("login");
}, error => {
log.error("Failed to retrieve profile", error);
this.setError("profile", error);
gFxAOAuthProfile = null;
MozLoopServiceInternal.fxAOAuthProfile = null;
MozLoopServiceInternal.notifyStatusChanged();
});
return tokenData;
}).catch(error => {
gFxAOAuthTokenData = null;
gFxAOAuthProfile = null;
MozLoopServiceInternal.fxAOAuthTokenData = null;
MozLoopServiceInternal.fxAOAuthProfile = null;
throw error;
}).catch((error) => {
MozLoopServiceInternal.setError("login", error);
@ -1498,8 +1585,8 @@ this.MozLoopService = {
MozLoopServiceInternal.clearSessionToken(LOOP_SESSION_TYPE.FXA);
}
gFxAOAuthTokenData = null;
gFxAOAuthProfile = null;
MozLoopServiceInternal.fxAOAuthTokenData = null;
MozLoopServiceInternal.fxAOAuthProfile = null;
// Reset the client since the initial promiseFxAOAuthParameters() call is
// what creates a new session.

View File

@ -14,6 +14,7 @@ loop.panel = (function(_, mozL10n) {
var sharedViews = loop.shared.views;
var sharedModels = loop.shared.models;
var sharedMixins = loop.shared.mixins;
var sharedActions = loop.shared.actions;
var Button = sharedViews.Button;
var ButtonGroup = sharedViews.ButtonGroup;
var ContactsList = loop.contacts.ContactsList;
@ -21,12 +22,23 @@ loop.panel = (function(_, mozL10n) {
var __ = mozL10n.get; // aliasing translation function as __ for concision
var TabView = React.createClass({displayName: 'TabView',
getInitialState: function() {
propTypes: {
buttonsHidden: React.PropTypes.bool,
// The selectedTab prop is used by the UI showcase.
selectedTab: React.PropTypes.string
},
getDefaultProps: function() {
return {
buttonsHidden: false,
selectedTab: "call"
};
},
getInitialState: function() {
return {selectedTab: this.props.selectedTab};
},
handleSelectTab: function(event) {
var tabName = event.target.dataset.tabName;
this.setState({selectedTab: tabName});
@ -37,6 +49,10 @@ loop.panel = (function(_, mozL10n) {
var tabButtons = [];
var tabs = [];
React.Children.forEach(this.props.children, function(tab, i) {
// Filter out null tabs (eg. rooms when the feature is disabled)
if (!tab) {
return;
}
var tabName = tab.props.name;
var isSelected = (this.state.selectedTab == tabName);
if (!tab.props.hidden) {
@ -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.
*/
@ -453,6 +584,10 @@ loop.panel = (function(_, mozL10n) {
callUrl: React.PropTypes.string,
userProfile: React.PropTypes.object,
showTabButtons: React.PropTypes.bool,
selectedTab: React.PropTypes.string,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
roomListStore:
React.PropTypes.instanceOf(loop.store.RoomListStore).isRequired
},
getInitialState: function() {
@ -498,6 +633,22 @@ loop.panel = (function(_, mozL10n) {
this.updateServiceErrors();
},
/**
* The rooms feature is hidden by default for now. Once it gets mainstream,
* this method can be safely removed.
*/
_renderRoomsTab: function() {
if (!navigator.mozLoop.getLoopBoolPref("rooms.enabled")) {
return null;
}
return (
Tab({name: "rooms"},
RoomList({dispatcher: this.props.dispatcher,
store: this.props.roomListStore})
)
);
},
startForm: function(name, contact) {
this.refs[name].initForm(contact);
this.selectTab(name);
@ -527,7 +678,8 @@ loop.panel = (function(_, mozL10n) {
React.DOM.div(null,
NotificationListView({notifications: this.props.notifications,
clearOnDocumentHidden: true}),
TabView({ref: "tabView", buttonsHidden: !this.state.userProfile && !this.props.showTabButtons},
TabView({ref: "tabView", selectedTab: this.props.selectedTab,
buttonsHidden: !this.state.userProfile && !this.props.showTabButtons},
Tab({name: "call"},
React.DOM.div({className: "content-area"},
CallUrlResult({client: this.props.client,
@ -536,6 +688,7 @@ loop.panel = (function(_, mozL10n) {
ToSView(null)
)
),
this._renderRoomsTab(),
Tab({name: "contacts"},
ContactsList({selectTab: this.selectTab,
startForm: this.startForm})
@ -575,11 +728,19 @@ loop.panel = (function(_, mozL10n) {
mozL10n.initialize(navigator.mozLoop);
var client = new loop.Client();
var notifications = new sharedModels.NotificationCollection()
var notifications = new sharedModels.NotificationCollection();
var dispatcher = new loop.Dispatcher();
var roomListStore = new loop.store.RoomListStore({
mozLoop: navigator.mozLoop,
dispatcher: dispatcher
});
React.renderComponent(PanelView({
client: client,
notifications: notifications}), document.querySelector("#main"));
notifications: notifications,
roomListStore: roomListStore,
dispatcher: dispatcher}
), document.querySelector("#main"));
document.body.classList.add(loop.shared.utils.getTargetPlatform());
document.body.setAttribute("dir", mozL10n.getDirection());
@ -597,6 +758,7 @@ loop.panel = (function(_, mozL10n) {
AvailabilityDropdown: AvailabilityDropdown,
CallUrlResult: CallUrlResult,
PanelView: PanelView,
RoomList: RoomList,
SettingsDropdown: SettingsDropdown,
ToSView: ToSView
};

View File

@ -14,6 +14,7 @@ loop.panel = (function(_, mozL10n) {
var sharedViews = loop.shared.views;
var sharedModels = loop.shared.models;
var sharedMixins = loop.shared.mixins;
var sharedActions = loop.shared.actions;
var Button = sharedViews.Button;
var ButtonGroup = sharedViews.ButtonGroup;
var ContactsList = loop.contacts.ContactsList;
@ -21,12 +22,23 @@ loop.panel = (function(_, mozL10n) {
var __ = mozL10n.get; // aliasing translation function as __ for concision
var TabView = React.createClass({
getInitialState: function() {
propTypes: {
buttonsHidden: React.PropTypes.bool,
// The selectedTab prop is used by the UI showcase.
selectedTab: React.PropTypes.string
},
getDefaultProps: function() {
return {
buttonsHidden: false,
selectedTab: "call"
};
},
getInitialState: function() {
return {selectedTab: this.props.selectedTab};
},
handleSelectTab: function(event) {
var tabName = event.target.dataset.tabName;
this.setState({selectedTab: tabName});
@ -37,6 +49,10 @@ loop.panel = (function(_, mozL10n) {
var tabButtons = [];
var tabs = [];
React.Children.forEach(this.props.children, function(tab, i) {
// Filter out null tabs (eg. rooms when the feature is disabled)
if (!tab) {
return;
}
var tabName = tab.props.name;
var isSelected = (this.state.selectedTab == tabName);
if (!tab.props.hidden) {
@ -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.
*/
@ -453,6 +584,10 @@ loop.panel = (function(_, mozL10n) {
callUrl: React.PropTypes.string,
userProfile: React.PropTypes.object,
showTabButtons: React.PropTypes.bool,
selectedTab: React.PropTypes.string,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
roomListStore:
React.PropTypes.instanceOf(loop.store.RoomListStore).isRequired
},
getInitialState: function() {
@ -498,6 +633,22 @@ loop.panel = (function(_, mozL10n) {
this.updateServiceErrors();
},
/**
* The rooms feature is hidden by default for now. Once it gets mainstream,
* this method can be safely removed.
*/
_renderRoomsTab: function() {
if (!navigator.mozLoop.getLoopBoolPref("rooms.enabled")) {
return null;
}
return (
<Tab name="rooms">
<RoomList dispatcher={this.props.dispatcher}
store={this.props.roomListStore} />
</Tab>
);
},
startForm: function(name, contact) {
this.refs[name].initForm(contact);
this.selectTab(name);
@ -527,7 +678,8 @@ loop.panel = (function(_, mozL10n) {
<div>
<NotificationListView notifications={this.props.notifications}
clearOnDocumentHidden={true} />
<TabView ref="tabView" buttonsHidden={!this.state.userProfile && !this.props.showTabButtons}>
<TabView ref="tabView" selectedTab={this.props.selectedTab}
buttonsHidden={!this.state.userProfile && !this.props.showTabButtons}>
<Tab name="call">
<div className="content-area">
<CallUrlResult client={this.props.client}
@ -536,6 +688,7 @@ loop.panel = (function(_, mozL10n) {
<ToSView />
</div>
</Tab>
{this._renderRoomsTab()}
<Tab name="contacts">
<ContactsList selectTab={this.selectTab}
startForm={this.startForm} />
@ -575,11 +728,19 @@ loop.panel = (function(_, mozL10n) {
mozL10n.initialize(navigator.mozLoop);
var client = new loop.Client();
var notifications = new sharedModels.NotificationCollection()
var notifications = new sharedModels.NotificationCollection();
var dispatcher = new loop.Dispatcher();
var roomListStore = new loop.store.RoomListStore({
mozLoop: navigator.mozLoop,
dispatcher: dispatcher
});
React.renderComponent(<PanelView
client={client}
notifications={notifications} />, document.querySelector("#main"));
notifications={notifications}
roomListStore={roomListStore}
dispatcher={dispatcher}
/>, document.querySelector("#main"));
document.body.classList.add(loop.shared.utils.getTargetPlatform());
document.body.setAttribute("dir", mozL10n.getDirection());
@ -597,6 +758,7 @@ loop.panel = (function(_, mozL10n) {
AvailabilityDropdown: AvailabilityDropdown,
CallUrlResult: CallUrlResult,
PanelView: PanelView,
RoomList: RoomList,
SettingsDropdown: SettingsDropdown,
ToSView: ToSView
};

View File

@ -25,6 +25,10 @@
<script type="text/javascript" src="loop/shared/js/models.js"></script>
<script type="text/javascript" src="loop/shared/js/mixins.js"></script>
<script type="text/javascript" src="loop/shared/js/views.js"></script>
<script type="text/javascript" src="loop/shared/js/validate.js"></script>
<script type="text/javascript" src="loop/shared/js/actions.js"></script>
<script type="text/javascript" src="loop/shared/js/dispatcher.js"></script>
<script type="text/javascript" src="loop/shared/js/roomListStore.js"></script>
<script type="text/javascript" src="loop/js/client.js"></script>
<script type="text/javascript;version=1.8" src="loop/js/contacts.js"></script>
<script type="text/javascript" src="loop/js/panel.js"></script>

View File

@ -2,6 +2,10 @@
* 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/. */
body {
background: none;
}
/* Panel styles */
.panel {
@ -26,7 +30,6 @@
flex-direction: row;
padding: 10px;
border-bottom: 1px solid #ccc;
background-color: #fbfbfb;
color: #000;
border-top-right-radius: 2px;
border-top-left-radius: 2px;
@ -120,6 +123,70 @@
box-shadow: 0 0 4px #c43c3e;
}
/* Rooms */
.room-list {
background: #f5f5f5;
}
.room-list > h1 {
font-weight: bold;
color: #999;
padding: .5rem 1rem;
border-bottom: 1px solid #ddd;
}
.room-list > .room-entry {
padding: 1rem 1rem 0 .5rem;
}
.room-list > .room-entry > h2 {
font-size: .85rem;
color: #777;
}
.room-list > .room-entry.room-active > h2 {
font-weight: bold;
color: #000;
}
.room-list > .room-entry > h2 > .room-notification {
display: inline-block;
background: transparent;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: .3rem;
}
.room-list > .room-entry.room-active > h2 > .room-notification {
background-color: #00a0ec;
}
.room-list > .room-entry:hover {
background: #f1f1f1;
}
.room-list > .room-entry:not(:last-child) {
border-bottom: 1px solid #ddd;
}
.room-list > .room-entry > p {
margin: 0;
padding: .2em 0 1rem .8rem;
}
.room-list > .room-entry > p > a {
color: #777;
opacity: .5;
transition: opacity .1s ease-in-out 0s;
text-decoration: none;
}
.room-list > .room-entry > p > a:hover {
opacity: 1;
text-decoration: underline;
}
/* Buttons */
.button-group {

View File

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

View File

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

View File

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

View File

@ -55,6 +55,7 @@ browser.jar:
# Shared scripts
content/browser/loop/shared/js/actions.js (content/shared/js/actions.js)
content/browser/loop/shared/js/conversationStore.js (content/shared/js/conversationStore.js)
content/browser/loop/shared/js/roomListStore.js (content/shared/js/roomListStore.js)
content/browser/loop/shared/js/dispatcher.js (content/shared/js/dispatcher.js)
content/browser/loop/shared/js/feedbackApiClient.js (content/shared/js/feedbackApiClient.js)
content/browser/loop/shared/js/models.js (content/shared/js/models.js)

View File

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

View File

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

View File

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

View File

@ -7,11 +7,6 @@
"use strict";
const {
gFxAOAuthTokenData,
gFxAOAuthProfile,
} = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
const BASE_URL = "http://mochi.test:8888/browser/browser/components/loop/test/mochitest/loop_fxa.sjs?";
function* checkFxA401() {
@ -211,6 +206,8 @@ add_task(function* registrationWithInvalidState() {
},
error => {
is(error.code, 400, "Check error code");
checkFxAOAuthTokenData(null);
is(MozLoopService.userProfile, null, "Profile should be empty after invalid login");
});
});
@ -232,6 +229,8 @@ add_task(function* registrationWith401() {
},
error => {
is(error.code, 401, "Check error code");
checkFxAOAuthTokenData(null);
is(MozLoopService.userProfile, null, "Profile should be empty after invalid login");
});
yield checkFxA401();
@ -321,7 +320,7 @@ add_task(function* loginWithParams401() {
},
error => {
ise(error.code, 401, "Check error code");
ise(gFxAOAuthTokenData, null, "Check there is no saved token data");
checkFxAOAuthTokenData(null);
});
yield checkFxA401();
@ -387,7 +386,7 @@ add_task(function* loginWithRegistration401() {
},
error => {
ise(error.code, 401, "Check error code");
ise(gFxAOAuthTokenData, null, "Check there is no saved token data");
checkFxAOAuthTokenData(null);
});
yield checkFxA401();

View File

@ -9,7 +9,7 @@
registerCleanupFunction(function*() {
MozLoopService.doNotDisturb = false;
setInternalLoopGlobal("gFxAOAuthProfile", null);
MozLoopServiceInternal.fxAOAuthProfile = null;
yield MozLoopServiceInternal.clearError("testing");
});
@ -25,7 +25,8 @@ add_task(function* test_doNotDisturb_with_login() {
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
yield MozLoopService.doNotDisturb = true;
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is in disabled state");
setInternalLoopGlobal("gFxAOAuthProfile", {email: "test@example.com", uid: "abcd1234"});
MozLoopServiceInternal.fxAOAuthTokenData = {token_type:"bearer",access_token:"1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",scope:"profile"};
MozLoopServiceInternal.fxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
yield MozLoopServiceInternal.notifyStatusChanged("login");
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "active", "Check button is in active state");
yield loadLoopPanel();
@ -34,7 +35,7 @@ add_task(function* test_doNotDisturb_with_login() {
loopPanel.hidePopup();
yield MozLoopService.doNotDisturb = false;
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
setInternalLoopGlobal("gFxAOAuthProfile", null);
MozLoopServiceInternal.fxAOAuthTokenData = null;
yield MozLoopServiceInternal.notifyStatusChanged();
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
});
@ -51,26 +52,27 @@ add_task(function* test_error_with_login() {
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
yield MozLoopServiceInternal.setError("testing", {});
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is in error state");
setInternalLoopGlobal("gFxAOAuthProfile", {email: "test@example.com", uid: "abcd1234"});
MozLoopServiceInternal.fxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
MozLoopServiceInternal.notifyStatusChanged("login");
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is in error state");
yield MozLoopServiceInternal.clearError("testing");
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
setInternalLoopGlobal("gFxAOAuthProfile", null);
MozLoopServiceInternal.fxAOAuthProfile = null;
MozLoopServiceInternal.notifyStatusChanged();
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
});
add_task(function* test_active() {
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
setInternalLoopGlobal("gFxAOAuthProfile", {email: "test@example.com", uid: "abcd1234"});
MozLoopServiceInternal.fxAOAuthTokenData = {token_type:"bearer",access_token:"1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",scope:"profile"};
MozLoopServiceInternal.fxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
yield MozLoopServiceInternal.notifyStatusChanged("login");
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "active", "Check button is in active state");
yield loadLoopPanel();
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state after opening panel");
let loopPanel = document.getElementById("loop-notification-panel");
loopPanel.hidePopup();
setInternalLoopGlobal("gFxAOAuthProfile", null);
MozLoopServiceInternal.fxAOAuthTokenData = null;
MozLoopServiceInternal.notifyStatusChanged();
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
});

View File

@ -120,8 +120,8 @@ function* resetFxA() {
global.gHawkClient = null;
global.gFxAOAuthClientPromise = null;
global.gFxAOAuthClient = null;
global.gFxAOAuthTokenData = null;
global.gFxAOAuthProfile = null;
MozLoopServiceInternal.fxAOAuthProfile = null;
MozLoopServiceInternal.fxAOAuthTokenData = null;
const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
Services.prefs.clearUserPref(fxASessionPref);
MozLoopService.errors.clear();
@ -130,17 +130,16 @@ function* resetFxA() {
yield notified;
}
function setInternalLoopGlobal(aName, aValue) {
let global = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
global[aName] = aValue;
function checkFxAOAuthTokenData(aValue) {
ise(MozLoopServiceInternal.fxAOAuthTokenData, aValue, "fxAOAuthTokenData should be " + aValue);
}
function checkLoggedOutState() {
let global = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
ise(global.gFxAOAuthClientPromise, null, "gFxAOAuthClientPromise should be cleared");
ise(global.gFxAOAuthProfile, null, "gFxAOAuthProfile should be cleared");
ise(MozLoopService.userProfile, null, "fxAOAuthProfile should be cleared");
ise(global.gFxAOAuthClient, null, "gFxAOAuthClient should be cleared");
ise(global.gFxAOAuthTokenData, null, "gFxAOAuthTokenData should be cleared");
checkFxAOAuthTokenData(null);
const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
ise(Services.prefs.getPrefType(fxASessionPref), Services.prefs.PREF_INVALID,
"FxA hawk session should be cleared anyways");

View File

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

View File

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

View File

@ -52,9 +52,10 @@ function run_test()
{
setupFakeLoopServer();
// Setup fake login (profile) state so we get FxA requests.
const serviceGlobal = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
serviceGlobal.gFxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
// Setup fake login state so we get FxA requests.
const MozLoopServiceInternal = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}).MozLoopServiceInternal;
MozLoopServiceInternal.fxAOAuthTokenData = {token_type:"bearer",access_token:"1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",scope:"profile"};
MozLoopServiceInternal.fxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
// For each notification received from the PushServer, MozLoopService will first query
// for any pending calls on the FxA hawk session and then again using the guest session.
@ -102,7 +103,7 @@ function run_test()
Chat.open = openChatOrig;
// Revert fake login state
serviceGlobal.gFxAOAuthProfile = null;
MozLoopServiceInternal.fxAOAuthTokenData = null;
// clear test pref
Services.prefs.clearUserPref("loop.seenToS");

View File

@ -10,8 +10,9 @@ var startTimerCalled = false;
add_task(function test_initialize_no_expiry() {
startTimerCalled = false;
MozLoopService.initialize();
let initializedPromise = yield MozLoopService.initialize();
Assert.equal(initializedPromise, "registration not needed",
"Promise should be fulfilled");
Assert.equal(startTimerCalled, false,
"should not register when no expiry time is set");
});

View File

@ -40,7 +40,7 @@ add_test(function test_register_websocket_success_loop_server_fail() {
}, err => {
// 404 is an expected failure indicated by the lack of route being set
// up on the Loop server mock. This is added in the next test.
Assert.equal(err, 404, "Expected no errors in websocket registration");
Assert.equal(err.errno, 404, "Expected no errors in websocket registration");
run_next_test();
});

View File

@ -0,0 +1,111 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const FAKE_FXA_TOKEN_DATA = JSON.stringify({
"token_type": "bearer",
"access_token": "1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",
"scope": "profile"
});
const FAKE_FXA_PROFILE = JSON.stringify({
"email": "test@example.com",
"uid": "999999994d9f4b08a2cbfc0999999999",
"avatar": null
});
const LOOP_FXA_TOKEN_PREF = "loop.fxa_oauth.tokendata";
const LOOP_FXA_PROFILE_PREF = "loop.fxa_oauth.profile";
const LOOP_URL_EXPIRY_PREF = "loop.urlsExpiryTimeSeconds";
const LOOP_INITIAL_DELAY_PREF = "loop.initialDelay";
/**
* This file is to test restart+reauth.
*/
add_task(function test_initialize_with_expired_urls_and_no_auth_token() {
// Set time to be 2 seconds in the past.
var nowSeconds = Date.now() / 1000;
Services.prefs.setIntPref(LOOP_URL_EXPIRY_PREF, nowSeconds - 2);
Services.prefs.clearUserPref(LOOP_FXA_TOKEN_PREF);
yield MozLoopService.initialize(mockPushHandler).then((msg) => {
Assert.equal(msg, "registration not needed", "Initialize should not register when the " +
"URLs are expired and there are no auth tokens");
}, (error) => {
Assert.ok(false, error, "should have resolved the promise that initialize returned");
});
});
add_task(function test_initialize_with_urls_and_no_auth_token() {
Services.prefs.setIntPref(LOOP_URL_EXPIRY_PREF, Date.now() / 1000 + 10);
Services.prefs.clearUserPref(LOOP_FXA_TOKEN_PREF);
loopServer.registerPathHandler("/registration", (request, response) => {
response.setStatusLine(null, 200, "OK");
});
yield MozLoopService.initialize(mockPushHandler).then((msg) => {
Assert.equal(msg, "initialized to guest status", "Initialize should register as a " +
"guest when no auth tokens but expired URLs");
}, (error) => {
Assert.ok(false, error, "should have resolved the promise that initialize returned");
});
});
add_task(function test_initialize_with_invalid_fxa_token() {
Services.prefs.setCharPref(LOOP_FXA_PROFILE_PREF, FAKE_FXA_PROFILE);
Services.prefs.setCharPref(LOOP_FXA_TOKEN_PREF, FAKE_FXA_TOKEN_DATA);
// Only need to implement the FxA registration because the previous
// test registered as a guest.
loopServer.registerPathHandler("/registration", (request, response) => {
response.setStatusLine(null, 401, "Unauthorized");
response.write(JSON.stringify({
code: 401,
errno: 110,
error: "Unauthorized",
message: "Unknown credentials",
}));
});
yield MozLoopService.initialize(mockPushHandler).then(() => {
Assert.ok(false, "Initializing with an invalid token should reject the promise");
},
(error) => {
let pushHandler = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}).gPushHandler;
Assert.equal(pushHandler.pushUrl, kEndPointUrl, "Push URL should match");
Assert.equal(Services.prefs.getCharPref(LOOP_FXA_TOKEN_PREF), "",
"FXA pref should be cleared if token was invalid");
Assert.equal(Services.prefs.getCharPref(LOOP_FXA_PROFILE_PREF), "",
"FXA profile pref should be cleared if token was invalid");
});
});
add_task(function test_initialize_with_fxa_token() {
Services.prefs.setCharPref(LOOP_FXA_PROFILE_PREF, FAKE_FXA_PROFILE);
Services.prefs.setCharPref(LOOP_FXA_TOKEN_PREF, FAKE_FXA_TOKEN_DATA);
loopServer.registerPathHandler("/registration", (request, response) => {
response.setStatusLine(null, 200, "OK");
});
yield MozLoopService.initialize(mockPushHandler).then(() => {
Assert.equal(Services.prefs.getCharPref(LOOP_FXA_TOKEN_PREF), FAKE_FXA_TOKEN_DATA,
"FXA pref should still be set after initialization");
Assert.equal(Services.prefs.getCharPref(LOOP_FXA_PROFILE_PREF), FAKE_FXA_PROFILE,
"FXA profile should still be set after initialization");
});
});
function run_test() {
setupFakeLoopServer();
// Note, this is just used to speed up the test.
Services.prefs.setIntPref(LOOP_INITIAL_DELAY_PREF, 0);
mockPushHandler.pushUrl = kEndPointUrl;
do_register_cleanup(function() {
Services.prefs.clearUserPref(LOOP_INITIAL_DELAY_PREF);
Services.prefs.clearUserPref(LOOP_FXA_TOKEN_PREF);
Services.prefs.clearUserPref(LOOP_FXA_PROFILE_PREF);
Services.prefs.clearUserPref(LOOP_URL_EXPIRY_PREF);
});
run_next_test();
};

View File

@ -18,11 +18,8 @@ add_test(function test_registration_invalid_token() {
response.write(JSON.stringify({
code: 401,
errno: 110,
error: {
error: "Unauthorized",
message: "Unknown credentials",
statusCode: 401
}
error: "Unauthorized",
message: "Unknown credentials",
}));
} else {
// We didn't have an authorization header, so check the pref has been cleared.

View File

@ -15,6 +15,7 @@ skip-if = toolkit == 'gonk'
[test_loopservice_locales.js]
[test_loopservice_notification.js]
[test_loopservice_registration.js]
[test_loopservice_restart.js]
[test_loopservice_token_invalid.js]
[test_loopservice_token_save.js]
[test_loopservice_token_send.js]

View File

@ -9,7 +9,12 @@
navigator.mozLoop = {
ensureRegistered: function() {},
getLoopCharPref: function() {},
getLoopBoolPref: function() {},
getLoopBoolPref: function(pref) {
// Ensure UI for rooms is displayed in the showcase.
if (pref === "rooms.enabled") {
return true;
}
},
releaseCallData: function() {},
contacts: {
getAll: function(callback) {

View File

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

View File

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

View File

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

View File

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

View File

@ -743,7 +743,10 @@ BrowserGlue.prototype = {
// XXX: Temporary hack to allow Loop FxA login after a restart to work.
// Remove this once bug 1071247 is deployed.
Services.prefs.clearUserPref("loop.hawk-session-token.fxa");
if (Services.prefs.getPrefType("loop.autologin-after-restart") != Ci.nsIPrefBranch.PREF_BOOL ||
!Services.prefs.getBoolPref("loop.autologin-after-restart")) {
Services.prefs.clearUserPref("loop.hawk-session-token.fxa");
}
},
// All initial windows have opened.

View File

@ -406,6 +406,10 @@ DevTools.prototype = {
this._toolboxes.set(target, toolbox);
toolbox.once("destroy", () => {
this.emit("toolbox-destroy", target);
});
toolbox.once("destroyed", () => {
this._toolboxes.delete(target);
this.emit("toolbox-destroyed", target);
@ -429,7 +433,7 @@ DevTools.prototype = {
* Target value e.g. the target that owns this toolbox
*
* @return {Toolbox} toolbox
* The toobox that is debugging the given target
* The toolbox that is debugging the given target
*/
getToolbox: function DT_getToolbox(target) {
return this._toolboxes.get(target);
@ -440,7 +444,7 @@ DevTools.prototype = {
*
* @return promise
* This promise will resolve to false if no toolbox was found
* associated to the target. true, if the toolbox was successfuly
* associated to the target. true, if the toolbox was successfully
* closed.
*/
closeToolbox: function DT_closeToolbox(target) {
@ -606,11 +610,11 @@ let gDevToolsBrowser = {
* selectToolCommand's behavior:
* - if the toolbox is closed,
* we open the toolbox and select the tool
* - if the toolbox is open, and the targetted tool is not selected,
* - if the toolbox is open, and the targeted tool is not selected,
* we select it
* - if the toolbox is open, and the targetted tool is selected,
* - if the toolbox is open, and the targeted tool is selected,
* and the host is NOT a window, we close the toolbox
* - if the toolbox is open, and the targetted tool is selected,
* - if the toolbox is open, and the targeted tool is selected,
* and the host is a window, we raise the toolbox window
*/
selectToolCommand: function(gBrowser, toolId) {

View File

@ -8,6 +8,7 @@ support-files =
doc_theme.css
[browser_devtools_api.js]
[browser_devtools_api_destroy.js]
skip-if = e10s # Bug 1070837 - devtools/framework/toolbox.js |doc| getter not e10s friendly
[browser_dynamic_tool_enabling.js]
[browser_keybindings.js]

View File

@ -0,0 +1,71 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests devtools API
const Cu = Components.utils;
function test() {
addTab("about:blank").then(runTests);
}
function runTests(aTab) {
let toolDefinition = {
id: "testTool",
visibilityswitch: "devtools.testTool.enabled",
isTargetSupported: function() true,
url: "about:blank",
label: "someLabel",
build: function(iframeWindow, toolbox) {
let deferred = promise.defer();
executeSoon(() => {
deferred.resolve({
target: toolbox.target,
toolbox: toolbox,
isReady: true,
destroy: function(){},
});
});
return deferred.promise;
},
};
gDevTools.registerTool(toolDefinition);
let collectedEvents = [];
let target = TargetFactory.forTab(aTab);
gDevTools.showToolbox(target, toolDefinition.id).then(function(toolbox) {
let panel = toolbox.getPanel(toolDefinition.id);
ok(panel, "Tool open");
gDevTools.once("toolbox-destroy", (event, toolbox, iframe) => {
collectedEvents.push(event);
});
gDevTools.once(toolDefinition.id + "-destroy", (event, toolbox, iframe) => {
collectedEvents.push("gDevTools-" + event);
});
toolbox.once("destroy", (event) => {
collectedEvents.push(event);
});
toolbox.once(toolDefinition.id + "-destroy", (event) => {
collectedEvents.push("toolbox-" + event);
});
toolbox.destroy().then(function() {
is(collectedEvents.join(":"),
"toolbox-destroy:destroy:gDevTools-testTool-destroy:toolbox-testTool-destroy",
"Found the right amount of collected events.");
gDevTools.unregisterTool(toolDefinition.id);
gBrowser.removeCurrentTab();
executeSoon(function() {
finish();
});
});
});
}

View File

@ -1516,6 +1516,8 @@ Toolbox.prototype = {
return this._destroyer;
}
this.emit("destroy");
this._target.off("navigate", this._refreshHostTitle);
this._target.off("frame-update", this._updateFrames);
this.off("select", this._refreshHostTitle);
@ -1537,6 +1539,9 @@ Toolbox.prototype = {
let outstanding = [];
for (let [id, panel] of this._toolPanels) {
try {
gDevTools.emit(id + "-destroy", this, panel);
this.emit(id + "-destroy", panel);
outstanding.push(panel.destroy());
} catch (e) {
// We don't want to stop here if any panel fail to close.

View File

@ -185,11 +185,12 @@ let test = asyncTest(function* () {
contextMenuClick(getContainerForRawNode(inspector.markup, node).tagLine);
let onNodeReselected = inspector.markup.once("reselectedonremoved");
let menu = inspector.panelDoc.getElementById("node-menu-pasteouterhtml");
dispatchCommandEvent(menu);
info("Waiting for inspector selection to update");
yield inspector.selection.once("new-node");
yield onNodeReselected;
ok(content.document.body.outerHTML.contains(clipboard.get()),
"Clipboard content was pasted into the node's outer HTML.");
@ -198,6 +199,8 @@ let test = asyncTest(function* () {
function* testDeleteNode() {
info("Testing 'Delete Node' menu item for normal elements.");
yield selectNode("p", inspector);
let deleteNode = inspector.panelDoc.getElementById("node-menu-delete");
ok(deleteNode, "the popup menu has a delete menu item");

View File

@ -620,8 +620,6 @@ MarkupView.prototype = {
*/
_mutationObserver: function(aMutations) {
let requiresLayoutChange = false;
let reselectParent;
let reselectChildIndex;
for (let mutation of aMutations) {
let type = mutation.type;
@ -651,22 +649,9 @@ MarkupView.prototype = {
requiresLayoutChange = true;
}
} else if (type === "childList") {
let isFromOuterHTML = mutation.removed.some((n) => {
return n === this._outerHTMLNode;
});
// Keep track of which node should be reselected after mutations.
if (isFromOuterHTML) {
reselectParent = target;
reselectChildIndex = this._outerHTMLChildIndex;
delete this._outerHTMLNode;
delete this._outerHTMLChildIndex;
}
container.childrenDirty = true;
// Update the children to take care of changes in the markup view DOM.
this._updateChildren(container, {flash: !isFromOuterHTML});
this._updateChildren(container, {flash: true});
}
}
@ -680,22 +665,6 @@ MarkupView.prototype = {
// Since the htmlEditor is absolutely positioned, a mutation may change
// the location in which it should be shown.
this.htmlEditor.refresh();
// If a node has had its outerHTML set, the parent node will be selected.
// Reselect the original node immediately.
if (this._inspector.selection.nodeFront === reselectParent) {
this.walker.children(reselectParent).then((o) => {
let node = o.nodes[reselectChildIndex];
let container = this.getContainer(node);
if (node && container) {
this.markNodeAsSelected(node, "outerhtml");
if (container.hasChildren) {
this.expandNode(node);
}
}
});
}
});
},
@ -847,26 +816,70 @@ MarkupView.prototype = {
},
/**
* Retrieve the index of a child within its parent's children list.
* @param aNode The NodeFront to find the index of.
* @returns A promise that will be resolved with the integer index.
* If the child cannot be found, returns -1
* Listen to mutations, expect a given node to be removed and try and select
* the node that sits at the same place instead.
* This is useful when changing the outerHTML or the tag name so that the
* newly inserted node gets selected instead of the one that just got removed.
*/
getNodeChildIndex: function(aNode) {
let def = promise.defer();
let parentNode = aNode.parentNode();
reselectOnRemoved: function(removedNode, reason) {
// Only allow one removed node reselection at a time, so that when there are
// more than 1 request in parallel, the last one wins.
this.cancelReselectOnRemoved();
// Node may have been removed from the DOM, instead of throwing an error,
// return -1 indicating that it isn't inside of its parent children list.
if (!parentNode) {
def.resolve(-1);
} else {
this.walker.children(parentNode).then(children => {
def.resolve(children.nodes.indexOf(aNode));
});
// Get the removedNode index in its parent node to reselect the right node.
let isHTMLTag = removedNode.tagName.toLowerCase() === "html";
let oldContainer = this.getContainer(removedNode);
let parentContainer = this.getContainer(removedNode.parentNode());
let childIndex = parentContainer.getChildContainers().indexOf(oldContainer);
let onMutations = this._removedNodeObserver = (e, mutations) => {
let isNodeRemovalMutation = false;
for (let mutation of mutations) {
let containsRemovedNode = mutation.removed &&
mutation.removed.some(n => n === removedNode);
if (mutation.type === "childList" && (containsRemovedNode || isHTMLTag)) {
isNodeRemovalMutation = true;
break;
}
}
if (!isNodeRemovalMutation) {
return;
}
this._inspector.off("markupmutation", onMutations);
this._removedNodeObserver = null;
// Don't select the new node if the user has already changed the current
// selection.
if (this._inspector.selection.nodeFront === parentContainer.node ||
(this._inspector.selection.nodeFront === removedNode && isHTMLTag)) {
let childContainers = parentContainer.getChildContainers();
if (childContainers && childContainers[childIndex]) {
this.markNodeAsSelected(childContainers[childIndex].node, reason);
if (childContainers[childIndex].hasChildren) {
this.expandNode(childContainers[childIndex].node);
}
this.emit("reselectedonremoved");
}
}
};
// Start listening for mutations until we find a childList change that has
// removedNode removed.
this._inspector.on("markupmutation", onMutations);
},
/**
* Make sure to stop listening for node removal markupmutations and not
* reselect the corresponding node when that happens.
* Useful when the outerHTML/tagname edition failed.
*/
cancelReselectOnRemoved: function() {
if (this._removedNodeObserver) {
this._inspector.off("markupmutation", this._removedNodeObserver);
this._removedNodeObserver = null;
this.emit("canceledreselectonremoved");
}
return def.promise;
},
/**
@ -883,20 +896,12 @@ MarkupView.prototype = {
return promise.reject();
}
let def = promise.defer();
this.getNodeChildIndex(aNode).then((i) => {
this._outerHTMLChildIndex = i;
this._outerHTMLNode = aNode;
container.undo.do(() => {
this.walker.setOuterHTML(aNode, newValue).then(def.resolve, def.reject);
}, () => {
this.walker.setOuterHTML(aNode, oldValue).then(def.resolve, def.reject);
});
// Changing the outerHTML removes the node which outerHTML was changed.
// Listen to this removal to reselect the right node afterwards.
this.reselectOnRemoved(aNode, "outerhtml");
return this.walker.setOuterHTML(aNode, newValue).then(null, () => {
this.cancelReselectOnRemoved();
});
return def.promise;
},
/**
@ -1426,6 +1431,18 @@ MarkupContainer.prototype = {
}
},
/**
* If the node has children, return the list of containers for all these
* children.
*/
getChildContainers: function() {
if (!this.hasChildren) {
return null;
}
return [...this.children.children].map(node => node.container);
},
/**
* True if the node has been visually expanded in the tree.
*/
@ -1828,7 +1845,15 @@ RootContainer.prototype = {
hasChildren: true,
expanded: true,
update: function() {},
destroy: function() {}
destroy: function() {},
/**
* If the node has children, return the list of containers for all these
* children.
*/
getChildContainers: function() {
return [...this.children.children].map(node => node.container);
}
};
/**
@ -1969,13 +1994,9 @@ function ElementEditor(aContainer, aNode) {
// Create the main editor
this.template("element", this);
if (aNode.isLocal_toBeDeprecated()) {
this.rawNode = aNode.rawNode();
}
// Make the tag name editable (unless this is a remote node or
// a document element)
if (this.rawNode && !aNode.isDocumentElement) {
if (!aNode.isDocumentElement) {
this.tag.setAttribute("tabindex", "0");
editableField({
element: this.tag,
@ -2207,57 +2228,19 @@ ElementEditor.prototype = {
/**
* Called when the tag name editor has is done editing.
*/
onTagEdit: function(aVal, aCommit) {
if (!aCommit || aVal == this.rawNode.tagName) {
onTagEdit: function(newTagName, isCommit) {
if (!isCommit || newTagName == this.node.tagName ||
!("editTagName" in this.markup.walker)) {
return;
}
// Create a new element with the same attributes as the
// current element and prepare to replace the current node
// with it.
try {
var newElt = nodeDocument(this.rawNode).createElement(aVal);
} catch(x) {
// Failed to create a new element with that tag name, ignore
// the change.
return;
}
let attrs = this.rawNode.attributes;
for (let i = 0 ; i < attrs.length; i++) {
newElt.setAttribute(attrs[i].name, attrs[i].value);
}
let newFront = this.markup.walker.frontForRawNode(newElt);
let newContainer = this.markup.importNode(newFront);
// Retain the two nodes we care about here so we can undo.
let walker = this.markup.walker;
promise.all([
walker.retainNode(newFront), walker.retainNode(this.node)
]).then(() => {
function swapNodes(aOld, aNew) {
aOld.parentNode.insertBefore(aNew, aOld);
while (aOld.firstChild) {
aNew.appendChild(aOld.firstChild);
}
aOld.parentNode.removeChild(aOld);
}
this.container.undo.do(() => {
swapNodes(this.rawNode, newElt);
this.markup.setNodeExpanded(newFront, this.container.expanded);
if (this.container.selected) {
this.markup.navigate(newContainer);
}
}, () => {
swapNodes(newElt, this.rawNode);
this.markup.setNodeExpanded(this.node, newContainer.expanded);
if (newContainer.selected) {
this.markup.navigate(this.container);
}
});
}).then(null, console.error);
// Changing the tagName removes the node. Make sure the replacing node gets
// selected afterwards.
this.markup.reselectOnRemoved(this.node, "edittagname");
this.markup.walker.editTagName(this.node, newTagName).then(null, () => {
// Failed to edit the tag name, cancel the reselection.
this.markup.cancelReselectOnRemoved();
});
},
destroy: function() {}

View File

@ -80,13 +80,13 @@ skip-if = e10s # Bug 1036409 - The last selected node isn't reselected
[browser_markupview_tag_edit_01.js]
[browser_markupview_tag_edit_02.js]
[browser_markupview_tag_edit_03.js]
skip-if = e10s # Bug 1036421 - Tag editing isn't remote-safe
[browser_markupview_tag_edit_04.js]
[browser_markupview_tag_edit_05.js]
[browser_markupview_tag_edit_06.js]
[browser_markupview_tag_edit_07.js]
[browser_markupview_tag_edit_08.js]
[browser_markupview_tag_edit_09.js]
[browser_markupview_tag_edit_10.js]
[browser_markupview_textcontent_edit_01.js]
[browser_markupview_toggle_01.js]
[browser_markupview_toggle_02.js]

View File

@ -107,10 +107,9 @@ function* testBody(inspector) {
let bodyFront = yield getNodeFront("body", inspector);
let doc = content.document;
let mutated = inspector.once("markupmutation");
inspector.markup.updateNodeOuterHTML(bodyFront, bodyHTML, body.outerHTML);
let mutations = yield mutated;
let onReselected = inspector.markup.once("reselectedonremoved");
yield inspector.markup.updateNodeOuterHTML(bodyFront, bodyHTML, body.outerHTML);
yield onReselected;
is(getNode("body").outerHTML, bodyHTML, "<body> HTML has been updated");
is(doc.querySelectorAll("head").length, 1, "no extra <head>s have been added");
@ -120,14 +119,15 @@ function* testBody(inspector) {
function* testHead(inspector) {
let head = getNode("head");
yield selectNode("head", inspector);
let headHTML = '<head id="updated"><title>New Title</title><script>window.foo="bar";</script></head>';
let headFront = yield getNodeFront("head", inspector);
let doc = content.document;
let mutated = inspector.once("markupmutation");
inspector.markup.updateNodeOuterHTML(headFront, headHTML, head.outerHTML);
let mutations = yield mutated;
let onReselected = inspector.markup.once("reselectedonremoved");
yield inspector.markup.updateNodeOuterHTML(headFront, headHTML, head.outerHTML);
yield onReselected;
is(doc.title, "New Title", "New title has been added");
is(doc.defaultView.foo, undefined, "Script has not been executed");
@ -143,10 +143,9 @@ function* testDocumentElement(inspector) {
let docElementHTML = '<html id="updated" foo="bar"><head><title>Updated from document element</title><script>window.foo="bar";</script></head><body><p>Hello</p></body></html>';
let docElementFront = yield inspector.markup.walker.documentElement();
let mutated = inspector.once("markupmutation");
inspector.markup.updateNodeOuterHTML(docElementFront, docElementHTML, docElement.outerHTML);
let mutations = yield mutated;
let onReselected = inspector.markup.once("reselectedonremoved");
yield inspector.markup.updateNodeOuterHTML(docElementFront, docElementHTML, docElement.outerHTML);
yield onReselected;
is(doc.title, "Updated from document element", "New title has been added");
is(doc.defaultView.foo, undefined, "Script has not been executed");
@ -165,10 +164,9 @@ function* testDocumentElement2(inspector) {
let docElementHTML = '<html class="updated" id="somethingelse"><head><title>Updated again from document element</title><script>window.foo="bar";</script></head><body><p>Hello again</p></body></html>';
let docElementFront = yield inspector.markup.walker.documentElement();
let mutated = inspector.once("markupmutation");
let onReselected = inspector.markup.once("reselectedonremoved");
inspector.markup.updateNodeOuterHTML(docElementFront, docElementHTML, docElement.outerHTML);
let mutations = yield mutated;
yield onReselected;
is(doc.title, "Updated again from document element", "New title has been added");
is(doc.defaultView.foo, undefined, "Script has not been executed");

View File

@ -6,7 +6,7 @@
// Tests that a node's tagname can be edited in the markup-view
const TEST_URL = "data:text/html,<div id='retag-me'><div id='retag-me-2'></div></div>";
const TEST_URL = "data:text/html;charset=utf-8,<div id='retag-me'><div id='retag-me-2'></div></div>";
let test = asyncTest(function*() {
let {toolbox, inspector} = yield addTab(TEST_URL).then(openInspector);

View File

@ -0,0 +1,33 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests that invalid tagname updates are handled correctly
const TEST_URL = "data:text/html;charset=utf-8,<div></div>";
let test = asyncTest(function*() {
let {toolbox, inspector} = yield addTab(TEST_URL).then(openInspector);
yield inspector.markup.expandAll();
yield selectNode("div", inspector);
info("Updating the DIV tagname to an invalid value");
let container = yield getContainerForSelector("div", inspector);
let onCancelReselect = inspector.markup.once("canceledreselectonremoved");
let tagEditor = container.editor.tag;
setEditableFieldValue(tagEditor, "<<<", inspector);
yield onCancelReselect;
ok(true, "The markup-view emitted the canceledreselectonremoved event");
is(inspector.selection.nodeFront, container.node, "The test DIV is still selected");
info("Updating the DIV tagname to a valid value this time");
let onReselect = inspector.markup.once("reselectedonremoved");
setEditableFieldValue(tagEditor, "span", inspector);
yield onReselect;
ok(true, "The markup-view emitted the reselectedonremoved event");
let spanFront = yield getNodeFront("span", inspector);
is(inspector.selection.nodeFront, spanFront, "The seelected node is now the SPAN");
});

View File

@ -44,25 +44,11 @@ function* runEditOuterHTMLTest(test, inspector) {
let onUpdated = inspector.once("inspector-updated");
info("Listening for the markupmutation event");
// This event fires once the outerHTML is set, with a target as the parent node and a type of "childList".
let mutated = inspector.once("markupmutation");
info("Editing the outerHTML");
inspector.markup.updateNodeOuterHTML(inspector.selection.nodeFront, test.newHTML, test.oldHTML);
let mutations = yield mutated;
ok(true, "The markupmutation event has fired, mutation done");
info("Check to make the sure the correct mutation event was fired, and that the parent is selected");
let nodeFront = inspector.selection.nodeFront;
let mutation = mutations[0];
let isFromOuterHTML = mutation.removed.some(n => n === oldNodeFront);
ok(isFromOuterHTML, "The node is in the 'removed' list of the mutation");
is(mutation.type, "childList", "Mutation is a childList after updating outerHTML");
is(mutation.target, nodeFront, "Parent node is selected immediately after setting outerHTML");
// Wait for node to be reselected after outerHTML has been set
yield inspector.selection.once("new-node-front");
info("Listen for reselectedonremoved and edit the outerHTML");
let onReselected = inspector.markup.once("reselectedonremoved");
yield inspector.markup.updateNodeOuterHTML(inspector.selection.nodeFront,
test.newHTML, test.oldHTML);
yield onReselected;
// Typically selectedNode will === pageNode, but if a new element has been injected in front
// of it, this will not be the case. If this happens.

View File

@ -69,14 +69,12 @@ var ResourceContainer = Class({
this.elt.appendChild(this.children);
this.line.addEventListener("click", (evt) => {
if (!this.selected) {
this.select();
this.expanded = true;
evt.stopPropagation();
}
this.select();
this.toggleExpansion();
evt.stopPropagation();
}, false);
this.expander.addEventListener("click", (evt) => {
this.expanded = !this.expanded;
this.toggleExpansion();
this.select();
evt.stopPropagation();
}, true);
@ -87,6 +85,14 @@ var ResourceContainer = Class({
this.update();
},
toggleExpansion: function() {
if (!this.resource.isRoot) {
this.expanded = !this.expanded;
} else {
this.expanded = true;
}
},
destroy: function() {
this.elt.remove();
this.expander.remove();

View File

@ -39,10 +39,16 @@ function selectFileFirstLoad(projecteditor, resource) {
if (resource.isRoot) {
ok (container.expanded, "The root directory is expanded by default.");
container.line.click();
ok (container.expanded, "Clicking on the line does not toggles expansion.");
return;
}
if (resource.isDir) {
ok (!container.expanded, "A directory is not expanded by default.");
container.line.click();
ok (container.expanded, "Clicking on the line toggles expansion.");
container.line.click();
ok (!container.expanded, "Clicking on the line toggles expansion.");
return;
}

View File

@ -272,3 +272,8 @@ feedback_rejoin_button=Rejoin
## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of
## an abusive user.
feedback_report_user_button=Report User
## LOCALIZATION NOTE (rooms_list_current_conversations): We prefer to have no
## number in the string, but if you need it for your language please use {{num}}.
rooms_list_current_conversations=Current conversation;Current conversations
rooms_list_no_current_conversations=No current conversations

View File

@ -85,7 +85,6 @@ import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
@ -160,7 +159,8 @@ public class BrowserApp extends GeckoApp
// Request ID for startActivityForResult.
private static final int ACTIVITY_REQUEST_PREFERENCES = 1001;
public static final String ACTION_NEW_PROFILE = "org.mozilla.gecko.NEW_PROFILE";
public static final String PREF_STARTPANE_ENABLED = "startpane_enabled";
private BrowserSearch mBrowserSearch;
private View mBrowserSearchContainer;
@ -621,8 +621,6 @@ public class BrowserApp extends GeckoApp
"Updater:Launch",
"BrowserToolbar:Visibility");
registerOnboardingReceiver(this);
Distribution distribution = Distribution.init(this);
// Init suggested sites engine in BrowserDB.
@ -688,24 +686,31 @@ public class BrowserApp extends GeckoApp
tintManager.setStatusBarTintEnabled(true);
}
private void registerOnboardingReceiver(Context context) {
final LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
/**
* Check and show Onboarding start pane if Firefox has never been launched and
* is not opening an external link from another application.
*
* @param context Context of application; used to show Start Pane if appropriate
* @param intentAction Intent that launched this activity
*/
private void checkStartPane(Context context, String intentAction) {
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
// Receiver for launching first run start pane on new profile creation.
mOnboardingReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
launchStartPane(BrowserApp.this);
try {
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);
if (prefs.getBoolean(PREF_STARTPANE_ENABLED, false)) {
if (!Intent.ACTION_VIEW.equals(intentAction)) {
final Intent startIntent = new Intent(this, StartPane.class);
context.startActivity(startIntent);
}
// Don't bother trying again to show the v1 minimal first run.
prefs.edit().putBoolean(PREF_STARTPANE_ENABLED, false).apply();
}
};
lbm.registerReceiver(mOnboardingReceiver, new IntentFilter(ACTION_NEW_PROFILE));
}
private void launchStartPane(Context context) {
final Intent startIntent = new Intent(context, StartPane.class);
context.startActivity(startIntent);
}
} finally {
StrictMode.setThreadPolicy(savedPolicy);
}
}
private Class<?> getMediaPlayerManager() {
if (AppConstants.MOZ_MEDIA_PLAYER) {
@ -740,6 +745,12 @@ public class BrowserApp extends GeckoApp
super.onBackPressed();
}
@Override
public void onAttachedToWindow() {
// We can't show Onboarding until Gecko has finished initialization (bug 1077583).
checkStartPane(this, getIntent().getAction());
}
@Override
public void onResume() {
super.onResume();
@ -2668,7 +2679,6 @@ public class BrowserApp extends GeckoApp
// or if the user has explicitly enabled the clear on shutdown pref.
// (We check the pref last to save the pref read.)
// In ICS+, it's easy to kill an app through the task switcher.
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);
final boolean visible = Versions.preICS ||
HardwareUtils.isTelevision() ||
!PrefUtils.getStringSet(GeckoSharedPrefs.forProfile(this),

View File

@ -37,7 +37,7 @@ import android.util.Log;
class ChromeCast implements GeckoMediaPlayer {
private static final boolean SHOW_DEBUG = false;
static final String MIRROR_RECIEVER_APP_ID = "D40D28D6";
static final String MIRROR_RECIEVER_APP_ID = "5F72F863";
private final Context context;
private final RouteInfo route;

View File

@ -27,6 +27,7 @@ import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.Log;
@ -692,9 +693,9 @@ public final class GeckoProfile {
Log.w(LOGTAG, "Couldn't write times.json.", e);
}
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(mApplicationContext);
final Intent intent = new Intent(BrowserApp.ACTION_NEW_PROFILE);
lbm.sendBroadcast(intent);
// Initialize pref flag for displaying the start pane for a new profile.
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(mApplicationContext);
prefs.edit().putBoolean(BrowserApp.PREF_STARTPANE_ENABLED, true).apply();
return profileDir;
}

View File

@ -92,9 +92,7 @@ ALL_JARS += webrtc.jar
endif
ifdef MOZ_ANDROID_SEARCH_ACTIVITY
extra_packages += org.mozilla.search
ALL_JARS += search-activity.jar
generated/org/mozilla/search/R.java: .aapt.deps ;
endif
ifdef MOZ_ANDROID_MLS_STUMBLER

View File

@ -1013,7 +1013,14 @@ public class LocalBrowserDB {
try {
if (c.moveToFirst()) {
return c.getString(c.getColumnIndexOrThrow(History.FAVICON_URL));
// Interrupted page loads can leave History items without a valid favicon_id.
final int columnIndex = c.getColumnIndexOrThrow(History.FAVICON_URL);
if (!c.isNull(columnIndex)) {
final String faviconURL = c.getString(columnIndex);
if (faviconURL != null) {
return faviconURL;
}
}
}
} finally {
c.close();

View File

@ -659,9 +659,6 @@ if CONFIG['MOZ_ANDROID_SEARCH_ACTIVITY']:
search_source_dir = SRCDIR + '/../search'
include('../search/search_activity_sources.mozbuild')
ANDROID_RES_DIRS += [search_source_dir + '/res']
resjar.generated_sources += ['org/mozilla/search/R.java']
search_activity = add_java_jar('search-activity')
search_activity.sources += [search_source_dir + '/' + f for f in search_activity_sources]
search_activity.javac_flags += ['-Xlint:all']
@ -831,17 +828,5 @@ if CONFIG['MOZ_ANDROID_MLS_STUMBLER']:
DEFINES['MOZ_STUMBLER_API_KEY'] = CONFIG['MOZ_MOZILLA_API_KEY']
if CONFIG['MOZ_ANDROID_SEARCH_ACTIVITY']:
searchres = add_android_eclipse_library_project('FennecResourcesSearch')
# Eclipse generates org.mozilla.search.R for this project, which is
# referenced by the search/**/*.java code.
searchres.package_name = 'org.mozilla.search'
searchres.res = SRCDIR + '/../search/res'
searchres.included_projects += ['../' + static.name, '../' + generated.name, '../' + branding.name]
searchres.referenced_projects += ['../' + static.name, '../' + generated.name, '../' + branding.name]
resources.included_projects += ['../' + searchres.name]
resources.referenced_projects += ['../' + searchres.name]
# The Search Activity code is built as part of Fennec, so we follow suit in Eclipse.
main.add_classpathentry('search', TOPSRCDIR + '/mobile/android/search/java', dstdir='search')

View File

@ -43,12 +43,12 @@
android:layout_toRightOf="@id/back"
android:layout_toLeftOf="@id/menu_items"/>
<!-- Values of marginLeft are used to animate the forward button so don't change its value. -->
<!-- Note: * Values of marginLeft are used to animate the forward button so don't change its value.
* We set the padding on the site security icon to increase its tappable area. -->
<org.mozilla.gecko.toolbar.ToolbarDisplayLayout android:id="@+id/display_layout"
style="@style/UrlBar.Button.Container"
android:layout_toRightOf="@id/back"
android:layout_toLeftOf="@id/menu_items"
android:paddingLeft="6dip"
android:paddingRight="4dip"/>
<LinearLayout android:id="@+id/menu_items"

View File

Before

Width:  |  Height:  |  Size: 520 B

After

Width:  |  Height:  |  Size: 520 B

View File

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 282 B

View File

Before

Width:  |  Height:  |  Size: 572 B

After

Width:  |  Height:  |  Size: 572 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 B

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 B

After

Width:  |  Height:  |  Size: 532 B

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Before

Width:  |  Height:  |  Size: 816 B

After

Width:  |  Height:  |  Size: 816 B

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 520 B

After

Width:  |  Height:  |  Size: 740 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 456 B

After

Width:  |  Height:  |  Size: 676 B

View File

Before

Width:  |  Height:  |  Size: 397 B

After

Width:  |  Height:  |  Size: 397 B

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<level-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:maxLevel="0" android:drawable="@drawable/new_tablet_site_security_unknown"/>
<item android:maxLevel="1" android:drawable="@drawable/lock_identified"/>
<item android:maxLevel="2" android:drawable="@drawable/lock_verified"/>
<item android:maxLevel="3" android:drawable="@drawable/shield"/>
<item android:maxLevel="4" android:drawable="@drawable/warning"/>
</level-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<!-- The favicon drawable is not the same dimensions as the site security
lock icons so we offset it using this drawable to compensate. -->
<inset
xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/favicon"
android:insetTop="@dimen/new_tablet_site_security_unknown_inset_top"
android:insetBottom="@dimen/new_tablet_site_security_unknown_inset_bottom"/>

View File

Before

Width:  |  Height:  |  Size: 379 B

After

Width:  |  Height:  |  Size: 379 B

View File

Before

Width:  |  Height:  |  Size: 216 B

After

Width:  |  Height:  |  Size: 216 B

View File

Before

Width:  |  Height:  |  Size: 437 B

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 B

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 B

After

Width:  |  Height:  |  Size: 476 B

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 523 B

After

Width:  |  Height:  |  Size: 523 B

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 B

After

Width:  |  Height:  |  Size: 640 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 B

After

Width:  |  Height:  |  Size: 533 B

View File

Before

Width:  |  Height:  |  Size: 259 B

After

Width:  |  Height:  |  Size: 259 B

View File

Before

Width:  |  Height:  |  Size: 686 B

After

Width:  |  Height:  |  Size: 686 B

View File

Before

Width:  |  Height:  |  Size: 290 B

After

Width:  |  Height:  |  Size: 290 B

View File

Before

Width:  |  Height:  |  Size: 724 B

After

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 485 B

After

Width:  |  Height:  |  Size: 686 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 367 B

After

Width:  |  Height:  |  Size: 570 B

View File

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Some files were not shown because too many files have changed in this diff Show More