merge fx-team to mozilla-central a=merge

This commit is contained in:
Carsten "Tomcat" Book 2015-09-01 14:15:49 +02:00
commit 933c80e85e
45 changed files with 1256 additions and 438 deletions

View File

@ -13,6 +13,7 @@ support-files =
share.html
share_activate.html
social_activate.html
social_activate_basic.html
social_activate_iframe.html
social_chat.html
social_crash_content_helper.js
@ -45,6 +46,7 @@ skip-if = (os == 'linux' && e10s) # Bug 1072669 context menu relies on target el
[browser_social_flyout.js]
[browser_social_isVisible.js]
[browser_social_marks.js]
[browser_social_marks_context.js]
[browser_social_multiprovider.js]
[browser_social_multiworker.js]
[browser_social_perwindowPB.js]

View File

@ -67,7 +67,7 @@ function sendActivationEvent(tab, callback, nullManifest) {
}
function activateProvider(domain, callback, nullManifest) {
let activationURL = domain+"/browser/browser/base/content/test/social/social_activate.html"
let activationURL = domain+"/browser/browser/base/content/test/social/social_activate_basic.html"
addTab(activationURL, function(tab) {
sendActivationEvent(tab, callback, nullManifest);
});
@ -263,7 +263,7 @@ var tests = {
info("first activation completed");
is(gBrowser.contentDocument.location.href, gProviders[0].origin + "/browser/browser/base/content/test/social/social_postActivation.html", "postActivationURL loaded");
gBrowser.removeTab(gBrowser.selectedTab);
is(gBrowser.contentDocument.location.href, gProviders[0].origin + "/browser/browser/base/content/test/social/social_activate.html", "activation page selected");
is(gBrowser.contentDocument.location.href, gProviders[0].origin + "/browser/browser/base/content/test/social/social_activate_basic.html", "activation page selected");
gBrowser.removeTab(gBrowser.selectedTab);
tabsToRemove.pop();
// uninstall the provider

View File

@ -204,8 +204,7 @@ var tests = {
// chat to appear, and thus become selected.
chatbar.selectedChat.close();
is(chatbar.selectedChat, second, "second chat is selected");
closeAllChats();
next();
Task.spawn(closeAllChats).then(next);
});
});
});
@ -219,8 +218,7 @@ var tests = {
chatbar.showChat(first);
ok(!first.collapsed, "first should no longer be collapsed");
is(second.collapsed || third.collapsed, true, "one of the others should be collapsed");
closeAllChats();
next();
Task.spawn(closeAllChats).then(next);
});
},
@ -262,13 +260,7 @@ var tests = {
port2.postMessage({topic: "test-logout"});
waitForCondition(function() chats.children.length == Social.providers.length - 1,
function() {
closeAllChats();
waitForCondition(function() chats.children.length == 0,
function() {
ok(!chats.selectedChat, "multiprovider chats are all closed");
next();
},
"chat windows didn't close");
Task.spawn(closeAllChats).then(next);
},
"chat window didn't close");
}, "chat windows did not open");

View File

@ -79,8 +79,7 @@ var tests = {
[chatWidth*3+popupWidth+2, 3, "now a large jump to make all 3 visible (ie, affects 2)"],
[chatWidth*1.5, 1, "and a large jump back down to 1 visible (ie, affects 2)"],
], function() {
closeAllChats();
next();
Task.spawn(closeAllChats).then(next);
});
});
},

View File

@ -110,8 +110,7 @@ function test() {
waitForCondition(function() isTabFocused(), cb, "tab should have focus");
}
let postSubTest = function(cb) {
closeAllChats();
cb();
Task.spawn(closeAllChats).then(cb);
}
// and run the tests.
runSocialTestWithProvider(manifest, function (finishcb) {

View File

@ -4,13 +4,6 @@
let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
let manifest = { // builtin provider
name: "provider example.com",
origin: "https://example.com",
sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html",
workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js",
iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png"
};
let manifest2 = { // used for testing install
name: "provider test1",
origin: "https://test1.example.com",
@ -29,34 +22,14 @@ let manifest3 = { // used for testing install
iconURL: "https://test2.example.com/browser/browser/base/content/test/general/moz.png",
version: 1
};
function makeMarkProvider(origin) {
return { // used for testing install
name: "mark provider " + origin,
origin: "https://" + origin + ".example.com",
workerURL: "https://" + origin + ".example.com/browser/browser/base/content/test/social/social_worker.js",
markURL: "https://" + origin + ".example.com/browser/browser/base/content/test/social/social_mark.html?url=%{url}",
markedIcon: "https://" + origin + ".example.com/browser/browser/base/content/test/social/unchecked.jpg",
unmarkedIcon: "https://" + origin + ".example.com/browser/browser/base/content/test/social/checked.jpg",
iconURL: "https://" + origin + ".example.com/browser/browser/base/content/test/general/moz.png",
version: 1
}
}
function test() {
waitForExplicitFinish();
let toolbar = document.getElementById("nav-bar");
let currentsetAtStart = toolbar.currentSet;
runSocialTestWithProvider(manifest, function (finishcb) {
runSocialTests(tests, undefined, undefined, function () {
Services.prefs.clearUserPref("social.remote-install.enabled");
// just in case the tests failed, clear these here as well
Services.prefs.clearUserPref("social.whitelist");
ok(CustomizableUI.inDefaultState, "Should be in the default state when we finish");
CustomizableUI.reset();
finishcb();
});
runSocialTests(tests, undefined, undefined, function () {
ok(CustomizableUI.inDefaultState, "Should be in the default state when we finish");
CustomizableUI.reset();
finish();
});
}
@ -110,8 +83,7 @@ var tests = {
let widget = CustomizableUI.getWidget(id);
ok(!widget || !widget.forWindow(window).node, "no button added to widget set");
Social.uninstallProvider(manifest3.origin, function() {
gBrowser.removeTab(tab);
next();
ensureBrowserTabClosed(tab).then(next);
});
});
});
@ -152,8 +124,7 @@ var tests = {
is(button.hidden, false, "mark button is visible");
checkSocialUI(window);
gBrowser.removeTab(tab);
next();
ensureBrowserTabClosed(tab).then(next);
});
});
});
@ -327,85 +298,5 @@ var tests = {
Social.uninstallProvider(manifest2.origin, next);
}, "button does not exist after disabling the provider");
});
},
testContextSubmenu: function(next) {
// install 4 providers to test that the menu's are added as submenus
let manifests = [
makeMarkProvider("sub1.test1"),
makeMarkProvider("sub2.test1"),
makeMarkProvider("sub1.test2"),
makeMarkProvider("sub2.test2")
];
let installed = [];
let markLinkMenu = document.getElementById("context-marklinkMenu").firstChild;
let markPageMenu = document.getElementById("context-markpageMenu").firstChild;
function addProviders(callback) {
let manifest = manifests.pop();
if (!manifest) {
info("INSTALLATION FINISHED");
executeSoon(callback);
return;
}
info("INSTALLING " + manifest.origin);
let panel = document.getElementById("servicesInstall-notification");
ensureEventFired(PopupNotifications.panel, "popupshown").then(() => {
info("servicesInstall-notification panel opened");
panel.button.click();
});
let activationURL = manifest.origin + "/browser/browser/base/content/test/social/social_activate.html"
let id = SocialMarks._toolbarHelper.idFromOrigin(manifest.origin);
let toolbar = document.getElementById("nav-bar");
addTab(activationURL, function(tab) {
let doc = tab.linkedBrowser.contentDocument;
let data = {
origin: doc.nodePrincipal.origin,
url: doc.location.href,
manifest: manifest,
window: window
}
Social.installProvider(data, function(addonManifest) {
// enable the provider so we know the button would have appeared
SocialService.enableProvider(manifest.origin, function(provider) {
waitForCondition(function() { return CustomizableUI.getWidget(id) },
function() {
gBrowser.removeTab(tab);
installed.push(manifest.origin);
// checkSocialUI will properly check where the menus are located
checkSocialUI(window);
executeSoon(function() {
addProviders(callback);
});
}, "button exists after enabling social");
});
});
});
}
function removeProviders(callback) {
let origin = installed.pop();
if (!origin) {
executeSoon(callback);
return;
}
Social.uninstallProvider(origin, function(provider) {
executeSoon(function() {
removeProviders(callback);
});
});
}
addProviders(function() {
removeProviders(function() {
is(SocialMarks.getProviders().length, 0, "mark providers removed");
is(markLinkMenu.childNodes.length, 0, "marklink menu ok");
is(markPageMenu.childNodes.length, 0, "markpage menu ok");
checkSocialUI(window);
next();
});
});
}
}

View File

@ -0,0 +1,105 @@
let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
function makeMarkProvider(origin) {
return { // used for testing install
name: "mark provider " + origin,
origin: "https://" + origin + ".example.com",
markURL: "https://" + origin + ".example.com/browser/browser/base/content/test/social/social_mark.html?url=%{url}",
markedIcon: "https://" + origin + ".example.com/browser/browser/base/content/test/social/unchecked.jpg",
unmarkedIcon: "https://" + origin + ".example.com/browser/browser/base/content/test/social/checked.jpg",
iconURL: "https://" + origin + ".example.com/browser/browser/base/content/test/general/moz.png",
version: 1
}
}
function test() {
waitForExplicitFinish();
runSocialTests(tests, undefined, undefined, function () {
ok(CustomizableUI.inDefaultState, "Should be in the default state when we finish");
CustomizableUI.reset();
finish();
});
}
var tests = {
testContextSubmenu: function(next) {
// install 4 providers to test that the menu's are added as submenus
let manifests = [
makeMarkProvider("sub1.test1"),
makeMarkProvider("sub2.test1"),
makeMarkProvider("sub1.test2"),
makeMarkProvider("sub2.test2")
];
let installed = [];
let markLinkMenu = document.getElementById("context-marklinkMenu").firstChild;
let markPageMenu = document.getElementById("context-markpageMenu").firstChild;
function addProviders(callback) {
let manifest = manifests.pop();
if (!manifest) {
info("INSTALLATION FINISHED");
executeSoon(callback);
return;
}
info("INSTALLING " + manifest.origin);
let panel = document.getElementById("servicesInstall-notification");
ensureEventFired(PopupNotifications.panel, "popupshown").then(() => {
info("servicesInstall-notification panel opened");
panel.button.click();
});
let activationURL = manifest.origin + "/browser/browser/base/content/test/social/social_activate.html"
let id = SocialMarks._toolbarHelper.idFromOrigin(manifest.origin);
let toolbar = document.getElementById("nav-bar");
addTab(activationURL, function(tab) {
let doc = tab.linkedBrowser.contentDocument;
let data = {
origin: doc.nodePrincipal.origin,
url: doc.location.href,
manifest: manifest,
window: window
}
Social.installProvider(data, function(addonManifest) {
// enable the provider so we know the button would have appeared
SocialService.enableProvider(manifest.origin, function(provider) {
waitForCondition(function() { return CustomizableUI.getWidget(id) },
function() {
gBrowser.removeTab(tab);
installed.push(manifest.origin);
// checkSocialUI will properly check where the menus are located
checkSocialUI(window);
executeSoon(function() {
addProviders(callback);
});
}, "button exists after enabling social");
});
});
});
}
function removeProviders(callback) {
let origin = installed.pop();
if (!origin) {
executeSoon(callback);
return;
}
Social.uninstallProvider(origin, function(provider) {
executeSoon(function() {
removeProviders(callback);
});
});
}
addProviders(function() {
removeProviders(function() {
is(SocialMarks.getProviders().length, 0, "mark providers removed");
is(markLinkMenu.childNodes.length, 0, "marklink menu ok");
is(markPageMenu.childNodes.length, 0, "markpage menu ok");
checkSocialUI(window);
next();
});
});
}
}

View File

@ -474,7 +474,9 @@ function get3ChatsForCollapsing(mode, cb) {
let second = chatbar.childNodes[2];
let first = chatbar.childNodes[1];
let third = chatbar.childNodes[0];
ok(first.collapsed && !second.collapsed && !third.collapsed, "collapsed state as promised");
is(first.collapsed, true, "first collapsed state as promised");
is(second.collapsed, false, "second collapsed state as promised");
is(third.collapsed, false, "third collapsed state as promised");
is(chatbar.selectedChat, third, "third is selected as promised")
info("have 3 chats for collapse testing - starting actual test...");
cb(first, second, third);
@ -618,10 +620,31 @@ function getPopupWidth() {
return popup.parentNode.getBoundingClientRect().width + margins;
}
function promiseCloseChat(chat) {
let deferred = Promise.defer();
let parent = chat.parentNode;
let observer = new MutationObserver(function onMutatations(mutations) {
for (let mutation of mutations) {
for (let i = 0; i < mutation.removedNodes.length; i++) {
let node = mutation.removedNodes.item(i);
if (node != chat) {
continue;
}
observer.disconnect();
deferred.resolve();
}
}
});
observer.observe(parent, {childList: true});
chat.close();
return deferred.promise;
}
function closeAllChats() {
let chatbar = getChatBar();
while (chatbar.selectedChat) {
chatbar.selectedChat.close();
yield promiseCloseChat(chatbar.selectedChat);
}
}

View File

@ -0,0 +1,40 @@
<html>
<head>
<title>Activation test</title>
</head>
<script>
// icons from http://findicons.com/icon/158311/firefox?id=356182 by ipapun
var data = {
// currently required
"name": "Demo Social Service",
"iconURL": "chrome://branding/content/icon16.png",
"icon32URL": "chrome://branding/content/favicon32.png",
"icon64URL": "chrome://branding/content/icon64.png",
// at least one of these must be defined
"sidebarURL": "/browser/browser/base/content/test/social/social_sidebar_empty.html",
"postActivationURL": "/browser/browser/base/content/test/social/social_postActivation.html",
// should be available for display purposes
"description": "A short paragraph about this provider",
"author": "Shane Caraveo, Mozilla",
// optional
"version": 1
}
function activate(node) {
node.setAttribute("data-service", JSON.stringify(data));
var event = new CustomEvent("ActivateSocialFeature");
node.dispatchEvent(event);
}
</script>
<body>
nothing to see here
<button id="activation" onclick="activate(this)">Activate The Demo Provider</button>
</body>
</html>

View File

@ -5,7 +5,7 @@
<body>
<iframe src="social_activate.html"/>
<iframe src="social_activate_basic.html"/>
</body>
</html>

View File

@ -79,6 +79,7 @@ html[dir="rtl"] .contact-filter {
color: #666;
font-weight: 500;
font-size: .9em;
margin-top: 1rem;
}
.contact,
@ -214,8 +215,9 @@ html[dir="rtl"] .contact-filter {
background-image: url("../shared/img/empty_contacts.svg");
background-repeat: no-repeat;
background-position: top center;
background-size: 128px;
margin-top: 4rem;
padding-top: 10rem;
padding-top: 12rem;
padding-bottom: 5rem;
text-align: center;
color: #4a4a4a;

View File

@ -297,6 +297,8 @@ html[dir="rtl"] .tab-view li:nth-child(2).selected ~ .slide-bar {
/* Rooms */
.rooms {
min-height: 100px;
/* To hold the conversation dropdown menu. */
position: relative;
}
.rooms > h1 {
@ -372,7 +374,8 @@ html[dir="rtl"] .tab-view li:nth-child(2).selected ~ .slide-bar {
.room-list {
max-height: 335px; /* XXX better computation needed */
min-height: 7px;
/* At least high enough to be able to contain the conversation menu. */
min-height: 100px;
overflow: auto;
}
@ -391,8 +394,9 @@ html[dir="rtl"] .tab-view li:nth-child(2).selected ~ .slide-bar {
font-size: 1.3rem;
line-height: 2.4rem;
color: #000;
/* See .room-entry-context-item for the margin/size reductions. */
width: calc(100% - 1rem - 16px);
/* See .room-entry-context-item for the margin/size reductions.
* An extra 40px to make space for the call button and chevron. */
width: calc(100% - 1rem - 56px);
}
.room-list > .room-entry.room-active > h2 {
@ -487,6 +491,65 @@ html[dir="rtl"] .tab-view li:nth-child(2).selected ~ .slide-bar {
background-image: url("../shared/img/icons-14x14.svg#hello-active");
}
/* Room entry context button (call button + chevron) */
.room-entry-context-actions {
display: none;
border-radius: 30px;
background: #56b397;
vertical-align: top;
}
.room-entry:hover .room-entry-context-actions {
display: inline-block;
}
.room-entry:hover .room-entry-context-actions:hover {
background: #50e3c2;
}
/* Room entry call button */
.room-entry-call-btn {
border-top-left-radius: 30px;
border-bottom-left-radius: 30px;
background-color: transparent;
background-image: url("../shared/img/icons-14x14.svg#video-white");
background-position: right center;
}
html[dir="rtl"] .room-entry-call-btn {
background-position: left center;
}
/* Room entry context menu */
.room-entry-context-menu-chevron {
display: inline-block;
border-top-right-radius: 30px;
border-bottom-right-radius: 30px;
background-image: url("../shared/img/icons-10x10.svg#dropdown-white");
background-position: center;
cursor: pointer;
}
/* Common styles for chevron and call button. */
.room-entry-context-menu-chevron,
.room-entry-call-btn {
width: 30px;
height: 24px;
background-size: 12px;
background-repeat: no-repeat;
vertical-align: middle;
}
html[dir="rtl"] .room-entry-context-actions > .dropdown-menu {
right: auto;
left: 45px;
}
.room-entry-context-actions > .dropdown-menu {
right: 45px;
bottom: auto;
left: auto;
}
/* Keep ".room-list > .room-entry > h2" in sync with these. */
.room-entry-context-item {
@ -496,6 +559,10 @@ html[dir="rtl"] .tab-view li:nth-child(2).selected ~ .slide-bar {
height: 16px;
}
.room-entry:hover .room-entry-context-item {
display: none;
}
.room-entry-context-item > a > img {
height: 16px;
width: 16px;

View File

@ -655,11 +655,8 @@ loop.contacts = (function(_, mozL10n) {
this.state.filter) {
return (
React.createElement("div", {className: "contact-search-list-empty"},
React.createElement("p", {className: "panel-text-large"},
mozL10n.get("no_search_results_message_heading")
),
React.createElement("p", {className: "panel-text-medium"},
mozL10n.get("no_search_results_message_subheading")
mozL10n.get("contacts_no_search_results")
)
)
);
@ -670,7 +667,7 @@ loop.contacts = (function(_, mozL10n) {
!this.state.filter) {
return (
React.createElement("div", {className: "contact-list-empty"},
React.createElement("p", {className: "panel-text-large"},
React.createElement("p", {className: "panel-text-medium"},
mozL10n.get("no_contacts_message_heading2")
),
React.createElement("p", {className: "panel-text-medium"},
@ -685,6 +682,7 @@ loop.contacts = (function(_, mozL10n) {
!this.state.filter ? React.createElement("div", {className: "contact-list-title"},
mozL10n.get("contact_list_title")
) : null,
this._renderGravatarPromoMessage(),
React.createElement("ul", {className: "contact-list"},
shownContacts.available ?
shownContacts.available.sort(this.sortContacts).map(viewForItem) :
@ -739,7 +737,6 @@ loop.contacts = (function(_, mozL10n) {
return (
React.createElement("div", null,
this._renderContactsFilter(),
this._renderGravatarPromoMessage(),
this._renderContactsList(),
this._renderAddContactButtons()
)

View File

@ -655,11 +655,8 @@ loop.contacts = (function(_, mozL10n) {
this.state.filter) {
return (
<div className="contact-search-list-empty">
<p className="panel-text-large">
{mozL10n.get("no_search_results_message_heading")}
</p>
<p className="panel-text-medium">
{mozL10n.get("no_search_results_message_subheading")}
{mozL10n.get("contacts_no_search_results")}
</p>
</div>
);
@ -670,7 +667,7 @@ loop.contacts = (function(_, mozL10n) {
!this.state.filter) {
return (
<div className="contact-list-empty">
<p className="panel-text-large">
<p className="panel-text-medium">
{mozL10n.get("no_contacts_message_heading2")}
</p>
<p className="panel-text-medium">
@ -685,6 +682,7 @@ loop.contacts = (function(_, mozL10n) {
{!this.state.filter ? <div className="contact-list-title">
{mozL10n.get("contact_list_title")}
</div> : null}
{this._renderGravatarPromoMessage()}
<ul className="contact-list">
{shownContacts.available ?
shownContacts.available.sort(this.sortContacts).map(viewForItem) :
@ -739,7 +737,6 @@ loop.contacts = (function(_, mozL10n) {
return (
<div>
{this._renderContactsFilter()}
{this._renderGravatarPromoMessage()}
{this._renderContactsList()}
{this._renderAddContactButtons()}
</div>

View File

@ -517,63 +517,49 @@ loop.panel = (function(_, mozL10n) {
room: React.PropTypes.instanceOf(loop.store.Room).isRequired
},
mixins: [loop.shared.mixins.WindowCloseMixin],
mixins: [
loop.shared.mixins.WindowCloseMixin,
sharedMixins.DropdownMenuMixin()
],
getInitialState: function() {
return { urlCopied: false };
return {
eventPosY: 0
};
},
shouldComponentUpdate: function(nextProps, nextState) {
return (nextProps.room.ctime > this.props.room.ctime) ||
(nextState.urlCopied !== this.state.urlCopied);
_isActive: function() {
return this.props.room.participants.length > 0;
},
handleClickEntry: function(event) {
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.OpenRoom({
roomToken: this.props.room.roomToken
}));
this.closeWindow();
},
handleCopyButtonClick: function(event) {
event.stopPropagation();
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.CopyRoomUrl({
roomUrl: this.props.room.roomUrl,
from: "panel"
}));
this.setState({urlCopied: true});
handleContextChevronClick: function(e) {
e.preventDefault();
e.stopPropagation();
this.setState({
eventPosY: e.pageY
});
this.toggleDropdownMenu();
},
handleDeleteButtonClick: function(event) {
event.stopPropagation();
event.preventDefault();
this.props.mozLoop.confirm({
message: mozL10n.get("rooms_list_deleteConfirmation_label"),
okButton: null,
cancelButton: null
}, function(err, result) {
if (err) {
throw err;
}
if (!result) {
return;
}
this.props.dispatcher.dispatch(new sharedActions.DeleteRoom({
roomToken: this.props.room.roomToken
}));
}.bind(this));
},
handleMouseLeave: function(event) {
this.setState({urlCopied: false});
},
_isActive: function() {
return this.props.room.participants.length > 0;
/**
* Callback called when moving cursor away from the conversation entry.
* Will close the dropdown menu.
*/
_handleMouseOut: function() {
if (this.state.showMenu) {
this.toggleDropdownMenu();
}
},
render: function() {
@ -581,31 +567,188 @@ loop.panel = (function(_, mozL10n) {
"room-entry": true,
"room-active": this._isActive()
});
var copyButtonClasses = React.addons.classSet({
"copy-link": true,
"checked": this.state.urlCopied
});
return (
React.createElement("div", {className: roomClasses, onClick: this.handleClickEntry,
onMouseLeave: this.handleMouseLeave},
React.createElement("div", {className: roomClasses,
onClick: this.handleClickEntry,
onMouseLeave: this._handleMouseOut,
ref: "roomEntry"},
React.createElement("h2", null,
this.props.room.decryptedContext.roomName,
React.createElement("button", {className: copyButtonClasses,
onClick: this.handleCopyButtonClick,
title: mozL10n.get("rooms_list_copy_url_tooltip")}),
React.createElement("button", {className: "delete-link",
onClick: this.handleDeleteButtonClick,
title: mozL10n.get("rooms_list_delete_tooltip")})
this.props.room.decryptedContext.roomName
),
React.createElement(RoomEntryContextItem, {mozLoop: this.props.mozLoop,
roomUrls: this.props.room.decryptedContext.urls})
React.createElement(RoomEntryContextItem, {
mozLoop: this.props.mozLoop,
roomUrls: this.props.room.decryptedContext.urls}),
React.createElement(RoomEntryContextButtons, {
dispatcher: this.props.dispatcher,
eventPosY: this.state.eventPosY,
handleClickEntry: this.handleClickEntry,
handleContextChevronClick: this.handleContextChevronClick,
ref: "contextActions",
room: this.props.room,
showMenu: this.state.showMenu,
toggleDropdownMenu: this.toggleDropdownMenu})
)
);
}
});
/*
/**
* Buttons corresponding to each conversation entry.
* This component renders the video icon call button and chevron button for
* displaying contextual dropdown menu for conversation entries.
* It also holds the dropdown menu.
*/
var RoomEntryContextButtons = React.createClass({displayName: "RoomEntryContextButtons",
propTypes: {
dispatcher: React.PropTypes.object.isRequired,
eventPosY: React.PropTypes.number.isRequired,
handleClickEntry: React.PropTypes.func.isRequired,
handleContextChevronClick: React.PropTypes.func.isRequired,
room: React.PropTypes.object.isRequired,
showMenu: React.PropTypes.bool.isRequired,
toggleDropdownMenu: React.PropTypes.func.isRequired
},
handleEmailButtonClick: function(event) {
event.preventDefault();
event.stopPropagation();
this.props.dispatcher.dispatch(
new sharedActions.EmailRoomUrl({
roomUrl: this.props.room.roomUrl,
from: "panel"
})
);
this.props.toggleDropdownMenu();
},
handleCopyButtonClick: function(event) {
event.stopPropagation();
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.CopyRoomUrl({
roomUrl: this.props.room.roomUrl,
from: "panel"
}));
this.props.toggleDropdownMenu();
},
handleDeleteButtonClick: function(event) {
event.stopPropagation();
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.DeleteRoom({
roomToken: this.props.room.roomToken
}));
this.props.toggleDropdownMenu();
},
render: function() {
return (
React.createElement("div", {className: "room-entry-context-actions"},
React.createElement("button", {
className: "btn room-entry-call-btn",
onClick: this.props.handleClickEntry,
ref: "callButton"}),
React.createElement("div", {
className: "room-entry-context-menu-chevron dropdown-menu-button",
onClick: this.props.handleContextChevronClick,
ref: "menu-button"}),
this.props.showMenu ?
React.createElement(ConversationDropdown, {
eventPosY: this.props.eventPosY,
handleCopyButtonClick: this.handleCopyButtonClick,
handleDeleteButtonClick: this.handleDeleteButtonClick,
handleEmailButtonClick: this.handleEmailButtonClick,
ref: "menu"}) :
null
)
);
}
});
/**
* Dropdown menu for each conversation entry.
* Because the container element has overflow we need to position the menu
* absolutely and have a different element as offset parent for it. We need
* eventPosY to make sure the position on the Y Axis is correct while for the
* X axis there can be only 2 different positions based on being RTL or not.
*/
var ConversationDropdown = React.createClass({displayName: "ConversationDropdown",
propTypes: {
eventPosY: React.PropTypes.number.isRequired,
handleCopyButtonClick: React.PropTypes.func.isRequired,
handleDeleteButtonClick: React.PropTypes.func.isRequired,
handleEmailButtonClick: React.PropTypes.func.isRequired
},
getInitialState: function() {
return {
openDirUp: false
};
},
componentDidMount: function() {
var menuNode = this.getDOMNode();
var menuNodeRect = menuNode.getBoundingClientRect();
// Get the parent element and make sure the menu does not overlow its
// container.
var listNode = loop.shared.utils.findParentNode(this.getDOMNode(),
".rooms");
var listNodeRect = listNode.getBoundingClientRect();
// Click offset to not display the menu right next to the area clicked.
var offset = 10;
if (this.props.eventPosY + menuNodeRect.height >=
listNodeRect.top + listNodeRect.height) {
// Position above click area.
menuNode.style.top = this.props.eventPosY - menuNodeRect.height -
listNodeRect.top - offset + "px";
} else {
// Position below click area.
menuNode.style.top = this.props.eventPosY - listNodeRect.top +
offset + "px";
}
},
render: function() {
var dropdownClasses = React.addons.classSet({
"dropdown-menu": true,
"dropdown-menu-up": this.state.openDirUp
});
return (
React.createElement("ul", {className: dropdownClasses},
React.createElement("li", {
className: "dropdown-menu-item",
onClick: this.props.handleCopyButtonClick,
ref: "copyButton"},
mozL10n.get("copy_url_button2")
),
React.createElement("li", {
className: "dropdown-menu-item",
onClick: this.props.handleEmailButtonClick,
ref: "emailButton"},
mozL10n.get("email_link_button")
),
React.createElement("li", {
className: "dropdown-menu-item",
onClick: this.props.handleDeleteButtonClick,
ref: "deleteButton"},
mozL10n.get("rooms_list_delete_tooltip")
)
)
);
}
});
/**
* User profile prop can be either an object or null as per mozLoopAPI
* and there is no way to express this with React 0.12.2
*/
@ -1037,11 +1180,13 @@ loop.panel = (function(_, mozL10n) {
return {
AccountLink: AccountLink,
AvailabilityDropdown: AvailabilityDropdown,
ConversationDropdown: ConversationDropdown,
GettingStartedView: GettingStartedView,
init: init,
NewRoomView: NewRoomView,
PanelView: PanelView,
RoomEntry: RoomEntry,
RoomEntryContextButtons: RoomEntryContextButtons,
RoomList: RoomList,
SettingsDropdown: SettingsDropdown,
SignInRequestView: SignInRequestView,

View File

@ -517,63 +517,49 @@ loop.panel = (function(_, mozL10n) {
room: React.PropTypes.instanceOf(loop.store.Room).isRequired
},
mixins: [loop.shared.mixins.WindowCloseMixin],
mixins: [
loop.shared.mixins.WindowCloseMixin,
sharedMixins.DropdownMenuMixin()
],
getInitialState: function() {
return { urlCopied: false };
return {
eventPosY: 0
};
},
shouldComponentUpdate: function(nextProps, nextState) {
return (nextProps.room.ctime > this.props.room.ctime) ||
(nextState.urlCopied !== this.state.urlCopied);
_isActive: function() {
return this.props.room.participants.length > 0;
},
handleClickEntry: function(event) {
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.OpenRoom({
roomToken: this.props.room.roomToken
}));
this.closeWindow();
},
handleCopyButtonClick: function(event) {
event.stopPropagation();
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.CopyRoomUrl({
roomUrl: this.props.room.roomUrl,
from: "panel"
}));
this.setState({urlCopied: true});
handleContextChevronClick: function(e) {
e.preventDefault();
e.stopPropagation();
this.setState({
eventPosY: e.pageY
});
this.toggleDropdownMenu();
},
handleDeleteButtonClick: function(event) {
event.stopPropagation();
event.preventDefault();
this.props.mozLoop.confirm({
message: mozL10n.get("rooms_list_deleteConfirmation_label"),
okButton: null,
cancelButton: null
}, function(err, result) {
if (err) {
throw err;
}
if (!result) {
return;
}
this.props.dispatcher.dispatch(new sharedActions.DeleteRoom({
roomToken: this.props.room.roomToken
}));
}.bind(this));
},
handleMouseLeave: function(event) {
this.setState({urlCopied: false});
},
_isActive: function() {
return this.props.room.participants.length > 0;
/**
* Callback called when moving cursor away from the conversation entry.
* Will close the dropdown menu.
*/
_handleMouseOut: function() {
if (this.state.showMenu) {
this.toggleDropdownMenu();
}
},
render: function() {
@ -581,31 +567,188 @@ loop.panel = (function(_, mozL10n) {
"room-entry": true,
"room-active": this._isActive()
});
var copyButtonClasses = React.addons.classSet({
"copy-link": true,
"checked": this.state.urlCopied
});
return (
<div className={roomClasses} onClick={this.handleClickEntry}
onMouseLeave={this.handleMouseLeave}>
<div className={roomClasses}
onClick={this.handleClickEntry}
onMouseLeave={this._handleMouseOut}
ref="roomEntry">
<h2>
{this.props.room.decryptedContext.roomName}
<button className={copyButtonClasses}
onClick={this.handleCopyButtonClick}
title={mozL10n.get("rooms_list_copy_url_tooltip")} />
<button className="delete-link"
onClick={this.handleDeleteButtonClick}
title={mozL10n.get("rooms_list_delete_tooltip")} />
</h2>
<RoomEntryContextItem mozLoop={this.props.mozLoop}
roomUrls={this.props.room.decryptedContext.urls} />
<RoomEntryContextItem
mozLoop={this.props.mozLoop}
roomUrls={this.props.room.decryptedContext.urls} />
<RoomEntryContextButtons
dispatcher={this.props.dispatcher}
eventPosY={this.state.eventPosY}
handleClickEntry={this.handleClickEntry}
handleContextChevronClick={this.handleContextChevronClick}
ref="contextActions"
room={this.props.room}
showMenu={this.state.showMenu}
toggleDropdownMenu={this.toggleDropdownMenu} />
</div>
);
}
});
/*
/**
* Buttons corresponding to each conversation entry.
* This component renders the video icon call button and chevron button for
* displaying contextual dropdown menu for conversation entries.
* It also holds the dropdown menu.
*/
var RoomEntryContextButtons = React.createClass({
propTypes: {
dispatcher: React.PropTypes.object.isRequired,
eventPosY: React.PropTypes.number.isRequired,
handleClickEntry: React.PropTypes.func.isRequired,
handleContextChevronClick: React.PropTypes.func.isRequired,
room: React.PropTypes.object.isRequired,
showMenu: React.PropTypes.bool.isRequired,
toggleDropdownMenu: React.PropTypes.func.isRequired
},
handleEmailButtonClick: function(event) {
event.preventDefault();
event.stopPropagation();
this.props.dispatcher.dispatch(
new sharedActions.EmailRoomUrl({
roomUrl: this.props.room.roomUrl,
from: "panel"
})
);
this.props.toggleDropdownMenu();
},
handleCopyButtonClick: function(event) {
event.stopPropagation();
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.CopyRoomUrl({
roomUrl: this.props.room.roomUrl,
from: "panel"
}));
this.props.toggleDropdownMenu();
},
handleDeleteButtonClick: function(event) {
event.stopPropagation();
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.DeleteRoom({
roomToken: this.props.room.roomToken
}));
this.props.toggleDropdownMenu();
},
render: function() {
return (
<div className="room-entry-context-actions">
<button
className="btn room-entry-call-btn"
onClick={this.props.handleClickEntry}
ref="callButton" />
<div
className="room-entry-context-menu-chevron dropdown-menu-button"
onClick={this.props.handleContextChevronClick}
ref="menu-button" />
{this.props.showMenu ?
<ConversationDropdown
eventPosY={this.props.eventPosY}
handleCopyButtonClick={this.handleCopyButtonClick}
handleDeleteButtonClick={this.handleDeleteButtonClick}
handleEmailButtonClick={this.handleEmailButtonClick}
ref="menu" /> :
null}
</div>
);
}
});
/**
* Dropdown menu for each conversation entry.
* Because the container element has overflow we need to position the menu
* absolutely and have a different element as offset parent for it. We need
* eventPosY to make sure the position on the Y Axis is correct while for the
* X axis there can be only 2 different positions based on being RTL or not.
*/
var ConversationDropdown = React.createClass({
propTypes: {
eventPosY: React.PropTypes.number.isRequired,
handleCopyButtonClick: React.PropTypes.func.isRequired,
handleDeleteButtonClick: React.PropTypes.func.isRequired,
handleEmailButtonClick: React.PropTypes.func.isRequired
},
getInitialState: function() {
return {
openDirUp: false
};
},
componentDidMount: function() {
var menuNode = this.getDOMNode();
var menuNodeRect = menuNode.getBoundingClientRect();
// Get the parent element and make sure the menu does not overlow its
// container.
var listNode = loop.shared.utils.findParentNode(this.getDOMNode(),
".rooms");
var listNodeRect = listNode.getBoundingClientRect();
// Click offset to not display the menu right next to the area clicked.
var offset = 10;
if (this.props.eventPosY + menuNodeRect.height >=
listNodeRect.top + listNodeRect.height) {
// Position above click area.
menuNode.style.top = this.props.eventPosY - menuNodeRect.height -
listNodeRect.top - offset + "px";
} else {
// Position below click area.
menuNode.style.top = this.props.eventPosY - listNodeRect.top +
offset + "px";
}
},
render: function() {
var dropdownClasses = React.addons.classSet({
"dropdown-menu": true,
"dropdown-menu-up": this.state.openDirUp
});
return (
<ul className={dropdownClasses}>
<li
className="dropdown-menu-item"
onClick={this.props.handleCopyButtonClick}
ref="copyButton">
{mozL10n.get("copy_url_button2")}
</li>
<li
className="dropdown-menu-item"
onClick={this.props.handleEmailButtonClick}
ref="emailButton">
{mozL10n.get("email_link_button")}
</li>
<li
className="dropdown-menu-item"
onClick={this.props.handleDeleteButtonClick}
ref="deleteButton">
{mozL10n.get("rooms_list_delete_tooltip")}
</li>
</ul>
);
}
});
/**
* User profile prop can be either an object or null as per mozLoopAPI
* and there is no way to express this with React 0.12.2
*/
@ -1037,11 +1180,13 @@ loop.panel = (function(_, mozL10n) {
return {
AccountLink: AccountLink,
AvailabilityDropdown: AvailabilityDropdown,
ConversationDropdown: ConversationDropdown,
GettingStartedView: GettingStartedView,
init: init,
NewRoomView: NewRoomView,
PanelView: PanelView,
RoomEntry: RoomEntry,
RoomEntryContextButtons: RoomEntryContextButtons,
RoomList: RoomList,
SettingsDropdown: SettingsDropdown,
SignInRequestView: SignInRequestView,

View File

@ -453,11 +453,11 @@ html[dir="rtl"] .dropdown-menu {
}
.dropdown-menu-item:first-child {
padding-top: .8rem;
padding-top: .5rem;
}
.dropdown-menu-item:last-child {
padding-bottom: .8rem;
padding-bottom: .5rem;
}
.dropdown-menu-item:first-child:hover {

View File

@ -1 +1 @@
<svg width="117" height="91" viewBox="0 0 117 91" xmlns="http://www.w3.org/2000/svg"><g fill="#D8D8D8"><path d="M116.431 59.357c-4.793 5.459-11.824 8.905-19.66 8.905-7.222 0-13.761-2.927-18.494-7.66l-.278-.282c4.116-6.441 11.33-10.712 19.541-10.712 7.795 0 14.69 3.848 18.891 9.749zm-18.891-12.736c6.799 0 12.311-5.512 12.311-12.311s-5.512-12.311-12.311-12.311-12.311 5.512-12.311 12.311 5.512 12.311 12.311 12.311zM38.431 59.357c-4.793 5.459-11.824 8.905-19.66 8.905-7.222 0-13.761-2.927-18.494-7.66l-.278-.282c4.116-6.441 11.33-10.712 19.541-10.712 7.795 0 14.69 3.848 18.891 9.749zm-18.891-12.736c6.799 0 12.311-5.512 12.311-12.311s-5.512-12.311-12.311-12.311-12.311 5.512-12.311 12.311 5.512 12.311 12.311 12.311z" id="Mask-Copy-5" fill-opacity=".8"/><path d="M91.495 70.608c-8.418 9.588-20.766 15.639-34.528 15.639-12.684 0-24.167-5.141-32.479-13.453l-.488-.495c7.229-11.312 19.898-18.812 34.318-18.812 13.689 0 25.8 6.759 33.177 17.121zm-33.177-22.367c11.941 0 21.621-9.68 21.621-21.621 0-11.941-9.68-21.621-21.621-21.621-11.941 0-21.621 9.68-21.621 21.621 0 11.941 9.68 21.621 21.621 21.621z" stroke="#FBFBFB" stroke-width="4"/></g></svg>
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><title>Hello_Contacts@1x</title><g fill="none" fill-rule="evenodd"><path d="M100.775283,86.186984 C101.434135,86.2367068 102.099801,86.2620192 102.77138,86.2620192 C110.607172,86.2620192 117.638125,82.8160926 122.431499,77.3569783 L122.431499,77.3569783 C118.230985,71.4565775 111.335219,67.6081731 103.54061,67.6081731 C97.4351053,67.6081731 91.8810912,69.9693867 87.741918,73.8284641 C93.9310034,78.1269515 98.0505206,82.473669 100.775283,86.186984 Z M103.54061,64.6213942 C110.339621,64.6213942 115.851308,59.1097074 115.851308,52.3106971 C115.851308,45.5116868 110.339621,40 103.54061,40 C96.7416002,40 91.2299134,45.5116868 91.2299134,52.3106971 C91.2299134,59.1097074 96.7416002,64.6213942 103.54061,64.6213942 Z" fill-opacity=".8" fill="#D8D8D8"/><path d="M27.813984,86.0869317 C26.8158308,86.2025746 25.8005473,86.2620192 24.7713797,86.2620192 C17.5491945,86.2620192 11.010733,83.3346503 6.27781775,78.601735 C6.18450717,78.5084245 6.09189839,78.4144121 6,78.3197065 C10.1161798,71.878651 17.3296834,67.6081731 25.5406105,67.6081731 C31.5276188,67.6081731 36.9843353,69.8786223 41.0967088,73.6054698 C34.7836878,77.9378687 30.5867176,82.3312911 27.813984,86.0869317 Z M25.5406105,64.6213942 C32.3396208,64.6213942 37.8513076,59.1097074 37.8513076,52.3106971 C37.8513076,45.5116868 32.3396208,40 25.5406105,40 C18.7416002,40 13.2299134,45.5116868 13.2299134,52.3106971 C13.2299134,59.1097074 18.7416002,64.6213942 25.5406105,64.6213942 Z" fill-opacity=".8" fill="#D8D8D8"/><path d="M97.4953223,88.6081931 C89.0769582,98.1957627 76.7288474,104.247671 62.9672376,104.247671 C50.2832749,104.247671 38.8001018,99.1064796 30.4879194,90.7942972 C30.324042,90.6304198 30.1613972,90.4653099 30,90.2989825 C37.2290427,78.9868808 49.8977583,71.486854 64.3181991,71.486854 C78.0074805,71.486854 90.1181688,78.2456142 97.4953223,88.6081931 L97.4953223,88.6081931 Z M64.3181991,66.2413236 C76.2589609,66.2413236 85.9388609,56.5614236 85.9388609,44.6206618 C85.9388609,32.6799 76.2589609,23 64.3181991,23 C52.3774373,23 42.6975373,32.6799 42.6975373,44.6206618 C42.6975373,56.5614236 52.3774373,66.2413236 64.3181991,66.2413236 Z" fill="#00A9DC"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,9 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="92px" height="99px" viewBox="0 0 92 99" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M16,25.4921875 C16,21.7812314 17.1913943,18.0215034 19.5742188,14.2128906 C21.9570432,10.4042778 25.4335709,7.2500125 30.0039062,4.75 C34.5742416,2.2499875 39.9062195,1 46,1 C51.6640908,1 56.6640408,2.04491143 61,4.13476562 C65.3359592,6.22461982 68.6855351,9.06638828 71.0488281,12.6601562 C73.4121212,16.2539242 74.59375,20.1601352 74.59375,24.3789062 C74.59375,27.6992354 73.9199286,30.6093625 72.5722656,33.109375 C71.2246026,35.6093875 69.6230562,37.767569 67.7675781,39.5839844 C65.9121001,41.4003997 62.5820553,44.4570098 57.7773438,48.7539062 C56.4492121,49.9648498 55.3847696,51.0292923 54.5839844,51.9472656 C53.7831991,52.865239 53.187502,53.7050743 52.796875,54.4667969 C52.406248,55.2285194 52.1035167,55.9902306 51.8886719,56.7519531 C51.6738271,57.5136757 51.3515646,58.8515529 50.921875,60.765625 C50.1796838,64.8281453 47.8554883,66.859375 43.9492188,66.859375 C41.9179586,66.859375 40.2089913,66.1953191 38.8222656,64.8671875 C37.4355399,63.5390559 36.7421875,61.5664193 36.7421875,58.9492188 C36.7421875,55.6679523 37.2499949,52.8261839 38.265625,50.4238281 C39.2812551,48.0214724 40.6288979,45.9121185 42.3085938,44.0957031 C43.9882896,42.2792878 46.253892,40.1211062 49.1054688,37.6210938 C51.6054813,35.4335828 53.4121038,33.7832087 54.5253906,32.6699219 C55.6386774,31.5566351 56.5761681,30.3164131 57.3378906,28.9492188 C58.0996132,27.5820244 58.4804688,26.0976643 58.4804688,24.4960938 C58.4804688,21.3710781 57.318371,18.7343857 54.9941406,16.5859375 C52.6699103,14.4374893 49.6718934,13.3632812 46,13.3632812 C41.7031035,13.3632812 38.5390727,14.4472548 36.5078125,16.6152344 C34.4765523,18.783214 32.7578195,21.9765414 31.3515625,26.1953125 C30.0234309,30.6093971 27.5039248,32.8164062 23.7929688,32.8164062 C21.6054578,32.8164062 19.7597731,32.0449296 18.2558594,30.5019531 C16.7519456,28.9589767 16,27.2890715 16,25.4921875 L16,25.4921875 Z M44.59375,89.7109375 C42.2109256,89.7109375 40.1308683,88.9394608 38.3535156,87.3964844 C36.576163,85.8535079 35.6875,83.6953264 35.6875,80.921875 C35.6875,78.4609252 36.5468664,76.3906334 38.265625,74.7109375 C39.9843836,73.0312416 42.0937375,72.1914062 44.59375,72.1914062 C47.0546998,72.1914062 49.1249916,73.0312416 50.8046875,74.7109375 C52.4843834,76.3906334 53.3242188,78.4609252 53.3242188,80.921875 C53.3242188,83.6562637 52.4453213,85.8046797 50.6875,87.3671875 C48.9296787,88.9296953 46.898449,89.7109375 44.59375,89.7109375 L44.59375,89.7109375 Z" id="?-copy" fill="#D8D8D8"></path>
<path d="M30.0307824,90.7518487 C26.2851884,95.2457309 20.7911271,98.0823793 14.6681559,98.0823793 C9.0246635,98.0823793 3.91544008,95.6726018 0.217089748,91.7765187 C0.144175866,91.6997064 0.0718103773,91.6223165 -1.41595069e-13,91.5443559 C3.2164267,86.2421507 8.85313514,82.7267408 15.269241,82.7267408 C21.3600311,82.7267408 26.7484556,85.8947045 30.0307824,90.7518487 L30.0307824,90.7518487 Z M15.269241,80.2680577 C20.5820599,80.2680577 24.8889507,75.7308984 24.8889507,70.1340288 C24.8889507,64.5371593 20.5820599,60 15.269241,60 C9.95642199,60 5.64953124,64.5371593 5.64953124,70.1340288 C5.64953124,75.7308984 9.95642199,80.2680577 15.269241,80.2680577 Z" id="Mask-Copy-7" fill-opacity="0.8" fill="#D8D8D8"></path>
<path d="M91.0307824,90.7518487 C87.2851884,95.2457309 81.7911271,98.0823793 75.6681559,98.0823793 C70.0246635,98.0823793 64.9154401,95.6726018 61.2170897,91.7765187 C61.1441759,91.6997064 61.0718104,91.6223165 61,91.5443559 C64.2164267,86.2421507 69.8531351,82.7267408 76.269241,82.7267408 C82.3600311,82.7267408 87.7484556,85.8947045 91.0307824,90.7518487 L91.0307824,90.7518487 Z M76.269241,80.2680577 C81.5820599,80.2680577 85.8889507,75.7308984 85.8889507,70.1340288 C85.8889507,64.5371593 81.5820599,60 76.269241,60 C70.956422,60 66.6495312,64.5371593 66.6495312,70.1340288 C66.6495312,75.7308984 70.956422,80.2680577 76.269241,80.2680577 Z" id="Mask-Copy-8" fill-opacity="0.8" fill="#D8D8D8"></path>
</g>
</svg>
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><title>Hello_Nomatch@1x</title><g fill="none" fill-rule="evenodd"><path d="M16.2734375,25.1289062 C16.2734375,21.4179502 17.4648318,17.6582222 19.8476562,13.8496094 C22.2304807,10.0409966 25.7070084,6.88673125 30.2773438,4.38671875 C34.8476791,1.88670625 40.179657,0.63671875 46.2734375,0.63671875 C51.9375283,0.63671875 56.9374783,1.68163018 61.2734375,3.77148438 C65.6093967,5.86133857 68.9589726,8.70310703 71.3222656,12.296875 C73.6855587,15.890643 74.8671875,19.7968539 74.8671875,24.015625 C74.8671875,27.3359541 74.1933661,30.2460813 72.8457031,32.7460938 C71.4980401,35.2461062 69.8964937,37.4042878 68.0410156,39.2207031 C66.1855376,41.0371185 62.8554928,44.0937285 58.0507812,48.390625 C56.7226496,49.6015686 55.6582071,50.666011 54.8574219,51.5839844 C54.0566366,52.5019577 53.4609395,53.3417931 53.0703125,54.1035156 C52.6796855,54.8652382 52.3769542,55.6269493 52.1621094,56.3886719 C51.9472646,57.1503944 51.6250021,58.4882717 51.1953125,60.4023438 C50.4531213,64.4648641 48.1289258,66.4960938 44.2226562,66.4960938 C42.1913961,66.4960938 40.4824288,65.8320379 39.0957031,64.5039062 C37.7089774,63.1757746 37.015625,61.2031381 37.015625,58.5859375 C37.015625,55.3046711 37.5234324,52.4629026 38.5390625,50.0605469 C39.5546926,47.6581911 40.9023354,45.5488372 42.5820312,43.7324219 C44.2617271,41.9160065 46.5273295,39.757825 49.3789062,37.2578125 C51.8789188,35.0703016 53.6855413,33.4199274 54.7988281,32.3066406 C55.9121149,31.1933538 56.8496056,29.9531318 57.6113281,28.5859375 C58.3730507,27.2187432 58.7539062,25.734383 58.7539062,24.1328125 C58.7539062,21.0077969 57.5918085,18.3711045 55.2675781,16.2226562 C52.9433478,14.074208 49.9453309,13 46.2734375,13 C41.976541,13 38.8125102,14.0839735 36.78125,16.2519531 C34.7499898,18.4199327 33.031257,21.6132602 31.625,25.8320312 C30.2968684,30.2461158 27.7773623,32.453125 24.0664062,32.453125 C21.8788953,32.453125 20.0332106,31.6816483 18.5292969,30.1386719 C17.0253831,28.5956954 16.2734375,26.9257902 16.2734375,25.1289062 L16.2734375,25.1289062 Z M44.8671875,89.3476562 C42.4843631,89.3476562 40.4043058,88.5761796 38.6269531,87.0332031 C36.8496005,85.4902267 35.9609375,83.3320451 35.9609375,80.5585938 C35.9609375,78.0976439 36.8203039,76.0273521 38.5390625,74.3476562 C40.2578211,72.6679604 42.367175,71.828125 44.8671875,71.828125 C47.3281373,71.828125 49.3984291,72.6679604 51.078125,74.3476562 C52.7578209,76.0273521 53.5976562,78.0976439 53.5976562,80.5585938 C53.5976562,83.2929824 52.7187588,85.4413984 50.9609375,87.0039062 C49.2031162,88.5664141 47.1718865,89.3476562 44.8671875,89.3476562 L44.8671875,89.3476562 Z" fill="#00A9DC" transform="translate(19 15)"/><path d="M30.0307824,90.6694694 C26.2851884,95.1633516 20.7911271,98 14.6681559,98 C9.0246635,98 3.91544008,95.5902225 0.217089748,91.6941393 C0.144175866,91.6173271 0.0718103773,91.5399371 -1.41595069e-13,91.4619766 C3.2164267,86.1597713 8.85313514,82.6443615 15.269241,82.6443615 C21.3600311,82.6443615 26.7484556,85.8123252 30.0307824,90.6694694 L30.0307824,90.6694694 Z M15.269241,80.1856784 C20.5820599,80.1856784 24.8889507,75.6485191 24.8889507,70.0516495 C24.8889507,64.4547799 20.5820599,59.9176207 15.269241,59.9176207 C9.95642199,59.9176207 5.64953124,64.4547799 5.64953124,70.0516495 C5.64953124,75.6485191 9.95642199,80.1856784 15.269241,80.1856784 Z" fill-opacity=".8" fill="#D8D8D8" transform="translate(19 15)"/><path d="M90.7157496,90.6694694 C86.9701557,95.1633516 81.4760943,98 75.3531231,98 C69.7096307,98 64.6004073,95.5902225 60.902057,91.6941393 C60.8291431,91.6173271 60.7567776,91.5399371 60.6849672,91.4619766 C63.9013939,86.1597713 69.5381024,82.6443615 75.9542082,82.6443615 C82.0449983,82.6443615 87.4334229,85.8123252 90.7157496,90.6694694 L90.7157496,90.6694694 Z M75.9542082,80.1856784 C81.2670271,80.1856784 85.5739179,75.6485191 85.5739179,70.0516495 C85.5739179,64.4547799 81.2670271,59.9176207 75.9542082,59.9176207 C70.6413892,59.9176207 66.3344985,64.4547799 66.3344985,70.0516495 C66.3344985,75.6485191 70.6413892,80.1856784 75.9542082,80.1856784 Z" fill-opacity=".8" fill="#D8D8D8" transform="translate(19 15)"/></g></svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -100,13 +100,30 @@ loop.shared.mixins = (function() {
};
},
/*
* Event listener callback in charge of closing panels when the users
* clicks on something that is not a dropdown trigger button or menu item.
*/
_onBodyClick: function(event) {
var menuButton = this.refs["menu-button"] && this.refs["menu-button"].getDOMNode();
if (this.refs.anchor) {
menuButton = this.refs.anchor.getDOMNode();
}
// If a menu button/ anchor is defined and clicked on, it will be in charge
// of hiding or showing the popup.
/*
* XXX Because the mixin is inherited by multiple components there are
* multiple such listeners at one time. This means that this.refs is not
* relevant when you click inside component A but the listener that is
* running is in component B and does not recognise event.target. This
* should be refactored to only be attached once to the document and use
* classList instead of refs.
*/
if (event.target.classList.contains("dropdown-menu-item") ||
event.target.classList.contains("dropdown-menu-button")) {
return;
}
if (event.target !== menuButton) {
this.setState({ showMenu: false });
}

View File

@ -754,6 +754,31 @@ var inChrome = typeof Components != "undefined" && "utils" in Components;
return str;
}
/**
* Look up the DOM hierarchy for a node matching `selector`.
* If it is not found return the parent node, this is a sane default so
* that subsequent queries on the result do no fail.
* Better choice than the alternative `document.querySelector(selector)`
* because we ensure it works in the UI showcase as well.
*
* @param {HTMLElement} node Child element of the node we are looking for.
* @param {String} selector CSS class value of element we are looking for.
* @return {HTMLElement} Parent of node that matches selector query.
*/
function findParentNode(node, selector) {
var parentNode = node.parentNode;
while (parentNode) {
if (parentNode.classList.contains(selector)) {
return parentNode;
}
parentNode = parentNode.parentNode;
}
return node;
}
this.utils = {
CALL_TYPES: CALL_TYPES,
FAILURE_DETAILS: FAILURE_DETAILS,
@ -764,6 +789,7 @@ var inChrome = typeof Components != "undefined" && "utils" in Components;
ROOM_INFO_FAILURES: ROOM_INFO_FAILURES,
setRootObjects: setRootObjects,
composeCallUrlEmail: composeCallUrlEmail,
findParentNode: findParentNode,
formatDate: formatDate,
formatURL: formatURL,
getBoolPreference: getBoolPreference,

View File

@ -508,9 +508,7 @@ describe("loop.contacts", function() {
});
sinon.assert.calledWithExactly(mozL10nGetSpy,
"no_search_results_message_heading");
sinon.assert.calledWithExactly(mozL10nGetSpy,
"no_search_results_message_subheading");
"contacts_no_search_results");
});
it("should filter the user name correctly", function() {

View File

@ -11,7 +11,7 @@ describe("loop.panel", function() {
var sharedUtils = loop.shared.utils;
var sandbox, notifications;
var fakeXHR, fakeWindow, fakeMozLoop;
var fakeXHR, fakeWindow, fakeMozLoop, fakeEvent;
var requests = [];
var mozL10nGetSpy;
@ -24,6 +24,12 @@ describe("loop.panel", function() {
requests.push(xhr);
};
fakeEvent = {
preventDefault: sandbox.stub(),
stopPropagation: sandbox.stub(),
pageY: 42
};
fakeWindow = {
close: sandbox.stub(),
addEventListener: function() {},
@ -587,100 +593,86 @@ describe("loop.panel", function() {
React.createElement(loop.panel.RoomEntry, props));
}
describe("Copy button", function() {
var roomStore, roomEntry, copyButton;
describe("handleContextChevronClick", function() {
var view;
beforeEach(function() {
roomStore = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop
});
roomStore.setStoreState({
pendingCreation: false,
pendingInitialRetrieval: false,
rooms: [],
error: undefined
});
// Stub to prevent warnings due to stores not being set up to handle
// the actions we are triggering.
sandbox.stub(dispatcher, "dispatch");
view = mountRoomEntry({room: new loop.store.Room(roomData)});
});
// XXX Current version of React cannot use TestUtils.Simulate, please
// enable when we upgrade.
it.skip("should close the menu when you move out the cursor", function() {
expect(view.refs.contextActions.state.showMenu).to.eql(false);
});
it("should set eventPosY when handleContextChevronClick is called", function() {
view.handleContextChevronClick(fakeEvent);
expect(view.state.eventPosY).to.eql(fakeEvent.pageY);
});
it("toggle state.showMenu when handleContextChevronClick is called", function() {
var prevState = view.state.showMenu;
view.handleContextChevronClick(fakeEvent);
expect(view.state.showMenu).to.eql(!prevState);
});
it("should toggle the menu when the button is clicked", function() {
var prevState = view.state.showMenu;
var node = view.refs.contextActions.refs["menu-button"].getDOMNode();
TestUtils.Simulate.click(node, fakeEvent);
expect(view.state.showMenu).to.eql(!prevState);
});
});
describe("Copy button", function() {
var roomEntry;
beforeEach(function() {
// Stub to prevent warnings where no stores are set up to handle the
// actions we are testing.
sandbox.stub(dispatcher, "dispatch");
roomEntry = mountRoomEntry({
deleteRoom: sandbox.stub(),
room: new loop.store.Room(roomData)
});
copyButton = roomEntry.getDOMNode().querySelector("button.copy-link");
});
it("should not display a copy button by default", function() {
expect(copyButton).to.not.equal(null);
it("should render context actions button", function() {
expect(roomEntry.refs.contextActions).to.not.eql(null);
});
it("should copy the URL when the click event fires", function() {
sandbox.stub(dispatcher, "dispatch");
describe("OpenRoom", function() {
it("should dispatch an OpenRoom action when button is clicked", function() {
TestUtils.Simulate.click(roomEntry.refs.roomEntry.getDOMNode());
TestUtils.Simulate.click(copyButton);
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.CopyRoomUrl({
roomUrl: roomData.roomUrl,
from: "panel"
}));
});
it("should set state.urlCopied when the click event fires", function() {
TestUtils.Simulate.click(copyButton);
expect(roomEntry.state.urlCopied).to.equal(true);
});
it("should switch to displaying a check icon when the URL has been copied",
function() {
TestUtils.Simulate.click(copyButton);
expect(copyButton.classList.contains("checked")).eql(true);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.OpenRoom({roomToken: roomData.roomToken}));
});
it("should not display a check icon after mouse leaves the entry",
function() {
var roomNode = roomEntry.getDOMNode();
TestUtils.Simulate.click(copyButton);
it("should dispatch an OpenRoom action when callback is called", function() {
roomEntry.handleClickEntry(fakeEvent);
TestUtils.SimulateNative.mouseOut(roomNode);
expect(copyButton.classList.contains("checked")).eql(false);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.OpenRoom({roomToken: roomData.roomToken}));
});
});
describe("Delete button click", function() {
var roomEntry, deleteButton;
it("should call window.close", function() {
roomEntry.handleClickEntry(fakeEvent);
beforeEach(function() {
roomEntry = mountRoomEntry({
room: new loop.store.Room(roomData)
sinon.assert.calledOnce(fakeWindow.close);
});
deleteButton = roomEntry.getDOMNode().querySelector("button.delete-link");
});
it("should not display a delete button by default", function() {
expect(deleteButton).to.not.equal(null);
});
it("should dispatch a delete action when confirmation is granted", function() {
sandbox.stub(dispatcher, "dispatch");
navigator.mozLoop.confirm.callsArgWith(1, null, true);
TestUtils.Simulate.click(deleteButton);
sinon.assert.calledOnce(navigator.mozLoop.confirm);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.DeleteRoom({roomToken: roomData.roomToken}));
});
it("should not dispatch an action when the confirmation is cancelled", function() {
sandbox.stub(dispatcher, "dispatch");
navigator.mozLoop.confirm.callsArgWith(1, null, false);
TestUtils.Simulate.click(deleteButton);
sinon.assert.calledOnce(navigator.mozLoop.confirm);
sinon.assert.notCalled(dispatcher.dispatch);
});
});
@ -754,19 +746,6 @@ describe("loop.panel", function() {
roomEntryNode = roomEntry.getDOMNode();
});
it("should dispatch an OpenRoom action", function() {
TestUtils.Simulate.click(roomEntryNode);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.OpenRoom({roomToken: roomData.roomToken}));
});
it("should call window.close", function() {
TestUtils.Simulate.click(roomEntryNode);
sinon.assert.calledOnce(fakeWindow.close);
});
});
describe("Room name updated", function() {
@ -792,7 +771,7 @@ describe("loop.panel", function() {
});
describe("loop.panel.RoomList", function() {
var roomStore, dispatcher, fakeEmail, dispatch;
var roomStore, dispatcher, fakeEmail, dispatch, roomData;
beforeEach(function() {
fakeEmail = "fakeEmail@example.com";
@ -806,7 +785,26 @@ describe("loop.panel", function() {
rooms: [],
error: undefined
});
dispatch = sandbox.stub(dispatcher, "dispatch");
roomData = {
roomToken: "QzBbvGmIZWU",
roomUrl: "http://sample/QzBbvGmIZWU",
decryptedContext: {
roomName: "Second Room Name"
},
maxSize: 2,
participants: [{
displayName: "Alexis",
account: "alexis@example.com",
roomConnectionId: "2a1787a6-4a73-43b5-ae3e-906ec1e763cb"
}, {
displayName: "Adam",
roomConnectionId: "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7"
}],
ctime: 1405517418
};
});
function createTestComponent() {
@ -1076,4 +1074,139 @@ describe("loop.panel", function() {
sinon.assert.calledOnce(fakeMozLoop.logOutFromFxA);
});
});
describe("ConversationDropdown", function() {
var view;
function createTestComponent() {
return TestUtils.renderIntoDocument(
React.createElement(loop.panel.ConversationDropdown, {
handleCopyButtonClick: sandbox.stub(),
handleDeleteButtonClick: sandbox.stub(),
handleEmailButtonClick: sandbox.stub(),
eventPosY: 0
}));
}
beforeEach(function() {
view = createTestComponent();
});
it("should trigger handleCopyButtonClick when copy button is clicked",
function() {
TestUtils.Simulate.click(view.refs.copyButton.getDOMNode());
sinon.assert.calledOnce(view.props.handleCopyButtonClick);
});
it("should trigger handleEmailButtonClick when email button is clicked",
function() {
TestUtils.Simulate.click(view.refs.emailButton.getDOMNode());
sinon.assert.calledOnce(view.props.handleEmailButtonClick);
});
it("should trigger handleDeleteButtonClick when delete button is clicked",
function() {
TestUtils.Simulate.click(view.refs.deleteButton.getDOMNode());
sinon.assert.calledOnce(view.props.handleDeleteButtonClick);
});
});
describe("RoomEntryContextButtons", function() {
var view, dispatcher, roomData;
function createTestComponent(extraProps) {
var props = _.extend({
dispatcher: dispatcher,
eventPosY: 0,
handleClickEntry: sandbox.stub(),
showMenu: false,
room: roomData,
toggleDropdownMenu: sandbox.stub(),
handleContextChevronClick: sandbox.stub()
}, extraProps);
return TestUtils.renderIntoDocument(
React.createElement(loop.panel.RoomEntryContextButtons, props));
}
beforeEach(function() {
roomData = {
roomToken: "QzBbvGmIZWU",
roomUrl: "http://sample/QzBbvGmIZWU",
decryptedContext: {
roomName: "Second Room Name"
},
maxSize: 2,
participants: [{
displayName: "Alexis",
account: "alexis@example.com",
roomConnectionId: "2a1787a6-4a73-43b5-ae3e-906ec1e763cb"
}, {
displayName: "Adam",
roomConnectionId: "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7"
}],
ctime: 1405517418
};
dispatcher = new loop.Dispatcher();
sandbox.stub(dispatcher, "dispatch");
view = createTestComponent();
});
it("should render ConversationDropdown if state.showMenu=true", function() {
view = createTestComponent({showMenu: true});
expect(view.refs.menu).to.not.eql(undefined);
});
it("should not render ConversationDropdown by default", function() {
view = createTestComponent({showMenu: false});
expect(view.refs.menu).to.eql(undefined);
});
it("should call toggleDropdownMenu after link is emailed", function() {
view.handleEmailButtonClick(fakeEvent);
sinon.assert.calledOnce(view.props.toggleDropdownMenu);
});
it("should call toggleDropdownMenu after conversation deleted", function() {
view.handleDeleteButtonClick(fakeEvent);
sinon.assert.calledOnce(view.props.toggleDropdownMenu);
});
it("should call toggleDropdownMenu after link is copied", function() {
view.handleCopyButtonClick(fakeEvent);
sinon.assert.calledOnce(view.props.toggleDropdownMenu);
});
it("should copy the URL when the callback is called", function() {
view.handleCopyButtonClick(fakeEvent);
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.CopyRoomUrl({
roomUrl: roomData.roomUrl,
from: "panel"
}));
});
it("should dispatch a delete action when callback is called", function() {
view.handleDeleteButtonClick(fakeEvent);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.DeleteRoom({roomToken: roomData.roomToken}));
});
it("should trigger handleClickEntry when button is clicked", function() {
TestUtils.Simulate.click(view.refs.callButton.getDOMNode());
sinon.assert.calledOnce(view.props.handleClickEntry);
});
});
});

View File

@ -609,6 +609,23 @@ describe("loop.shared.views", function() {
sinon.assert.calledOnce(model.startSession);
});
// Test loop.shared.utils.findParentNode.
// Added here to take advantage of having markup.
it("should find '.video-layout-wrapper'", function() {
var view = mountTestComponent({
initiate: false,
sdk: fakeSDK,
model: model,
video: {enabled: true}
});
var menu = view.getDOMNode().querySelector(".btn-hangup-entry");
var result = loop.shared.utils.findParentNode(menu,
"video-layout-wrapper");
expect(result.classList.contains("video-layout-wrapper")).to.eql(true);
});
it("shouldn't start a session if initiate is false", function() {
sandbox.stub(model, "startSession");

View File

@ -7,17 +7,17 @@
// Check that server log appears in the console panel - bug 1168872
let test = asyncTest(function* () {
const PREF = "devtools.webconsole.filter.serverlog";
const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-server-logging.sjs";
Services.prefs.setBoolPref(PREF, true);
registerCleanupFunction(() => Services.prefs.clearUserPref(PREF));
yield loadTab(TEST_URI);
let hud = yield openConsole();
BrowserReload();
// Set logging filter and wait till it's set on the backend
hud.setFilterState("serverlog", true);
yield updateServerLoggingListener(hud);
BrowserReloadSkipCache();
// Note that the test is also checking out the (printf like)
// formatters and encoding of UTF8 characters (see the one at the end).
@ -31,4 +31,16 @@ let test = asyncTest(function* () {
severity: SEVERITY_LOG,
}],
})
// Clean up filter
hud.setFilterState("serverlog", false);
yield updateServerLoggingListener(hud);
});
function updateServerLoggingListener(hud) {
let deferred = promise.defer();
hud.ui._updateServerLoggingListener(response => {
deferred.resolve(response);
});
return deferred.promise;
}

View File

@ -143,10 +143,9 @@ no_conversations_message_heading=There are no conversations yet
## LOCALIZATION NOTE(no_converastions_start_message): Subheading inviting the
## user to start a new conversation.
no_conversations_start_message=start a new conversation!
## LOCALIZATION NOTE(no_search_results_message_heading): Title to show when
## LOCALIZATION NOTE(contacts_no_search_results): Message shown when contacts
## search returned no matching results.
no_search_results_message_heading=No matching results
no_search_results_message_subheading=with your search, try again!
contacts_no_search_results=No matching results.
## LOCALIZATION NOTE(import_failed_description simple): Displayed when an import of
## contacts fails. This is displayed in the error field.

View File

@ -10,6 +10,7 @@ import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.DynamicToolbar.PinReason;
import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
import org.mozilla.gecko.PrintHelper;
import org.mozilla.gecko.Tabs.TabEvents;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.animation.TransitionsTracker;
@ -3167,6 +3168,7 @@ public class BrowserApp extends GeckoApp
final MenuItem share = aMenu.findItem(R.id.share);
final MenuItem quickShare = aMenu.findItem(R.id.quickshare);
final MenuItem saveAsPDF = aMenu.findItem(R.id.save_as_pdf);
final MenuItem print = aMenu.findItem(R.id.print);
final MenuItem charEncoding = aMenu.findItem(R.id.char_encoding);
final MenuItem findInPage = aMenu.findItem(R.id.find_in_page);
final MenuItem desktopMode = aMenu.findItem(R.id.desktop_mode);
@ -3192,6 +3194,7 @@ public class BrowserApp extends GeckoApp
share.setEnabled(false);
quickShare.setEnabled(false);
saveAsPDF.setEnabled(false);
print.setEnabled(false);
findInPage.setEnabled(false);
// NOTE: Use MenuUtils.safeSetEnabled because some actions might
@ -3344,10 +3347,13 @@ public class BrowserApp extends GeckoApp
final boolean privateTabVisible = RestrictedProfiles.isAllowed(this, Restriction.DISALLOW_PRIVATE_BROWSING);
MenuUtils.safeSetVisible(aMenu, R.id.new_private_tab, privateTabVisible);
// Disable save as PDF for about:home and xul pages.
saveAsPDF.setEnabled(!(isAboutHome(tab) ||
// Disable PDF generation (save and print) for about:home and xul pages.
boolean allowPDF = (!(isAboutHome(tab) ||
tab.getContentType().equals("application/vnd.mozilla.xul+xml") ||
tab.getContentType().startsWith("video/")));
saveAsPDF.setEnabled(allowPDF);
print.setEnabled(allowPDF);
print.setVisible(Versions.feature19Plus && AppConstants.NIGHTLY_BUILD);
// Disable find in page for about:home, since it won't work on Java content.
findInPage.setEnabled(!isAboutHome(tab));
@ -3471,6 +3477,11 @@ public class BrowserApp extends GeckoApp
return true;
}
if (itemId == R.id.print) {
PrintHelper.printPDF(this);
return true;
}
if (itemId == R.id.settings) {
intent = new Intent(this, GeckoPreferences.class);

View File

@ -6,7 +6,6 @@
package org.mozilla.gecko;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
@ -47,6 +46,7 @@ import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.GeckoRequest;
import org.mozilla.gecko.util.HardwareCodecCapabilityUtils;
import org.mozilla.gecko.util.HardwareUtils;
import org.mozilla.gecko.util.IOUtils;
import org.mozilla.gecko.util.NativeEventListener;
import org.mozilla.gecko.util.NativeJSContainer;
import org.mozilla.gecko.util.NativeJSObject;
@ -1004,13 +1004,6 @@ public class GeckoAppShell
return type + "/" + subType;
}
static void safeStreamClose(Closeable stream) {
try {
if (stream != null)
stream.close();
} catch (IOException e) {}
}
static boolean isUriSafeForScheme(Uri aUri) {
// Bug 794034 - We don't want to pass MWI or USSD codes to the
// dialer, and ensure the Uri class doesn't parse a URI
@ -2576,13 +2569,13 @@ public class GeckoAppShell
// Only alter the intent when we're sure everything has worked
intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(imageFile));
} finally {
safeStreamClose(is);
IOUtils.safeStreamClose(is);
}
}
} catch(IOException ex) {
// If something went wrong, we'll just leave the intent un-changed
} finally {
safeStreamClose(os);
IOUtils.safeStreamClose(os);
}
}

View File

@ -0,0 +1,115 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* 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/. */
package org.mozilla.gecko;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.util.GeckoRequest;
import org.mozilla.gecko.util.IOUtils;
import org.mozilla.gecko.util.NativeJSObject;
import org.mozilla.gecko.util.ThreadUtils;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import android.content.Context;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.print.PrintAttributes;
import android.print.PrintDocumentAdapter;
import android.print.PrintDocumentAdapter.LayoutResultCallback;
import android.print.PrintDocumentAdapter.WriteResultCallback;
import android.print.PrintDocumentInfo;
import android.print.PrintManager;
import android.print.PageRange;
import android.util.Log;
public class PrintHelper {
private static final String LOGTAG = "GeckoPrintUtils";
public static void printPDF(final Context context) {
GeckoAppShell.sendRequestToGecko(new GeckoRequest("Print:PDF", new JSONObject()) {
@Override
public void onResponse(NativeJSObject nativeJSObject) {
final String filePath = nativeJSObject.getString("file");
final String title = nativeJSObject.getString("title");
finish(context, filePath, title);
}
@Override
public void onError(NativeJSObject error) {
// Gecko didn't respond due to state change, javascript error, etc.
Log.d(LOGTAG, "No response from Gecko on request to generate a PDF");
}
private void finish(final Context context, final String filePath, final String title) {
PrintManager printManager = (PrintManager) context.getSystemService(Context.PRINT_SERVICE);
String jobName = title;
// The adapter methods are all called on the UI thread by the PrintManager. Put the heavyweight code
// in onWrite on the background thread.
PrintDocumentAdapter pda = new PrintDocumentAdapter() {
@Override
public void onWrite(final PageRange[] pages, final ParcelFileDescriptor destination, final CancellationSignal cancellationSignal, final WriteResultCallback callback) {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
InputStream input = null;
OutputStream output = null;
try {
File pdfFile = new File(filePath);
input = new FileInputStream(pdfFile);
output = new FileOutputStream(destination.getFileDescriptor());
byte[] buf = new byte[8192];
int bytesRead;
while ((bytesRead = input.read(buf)) > 0) {
output.write(buf, 0, bytesRead);
}
callback.onWriteFinished(new PageRange[] { PageRange.ALL_PAGES });
// File is not really deleted until the input stream closes it
pdfFile.delete();
} catch (FileNotFoundException ee) {
Log.d(LOGTAG, "Unable to find the temporary PDF file.");
} catch (IOException ioe) {
Log.e(LOGTAG, "IOException while transferring temporary PDF file: ", ioe);
} finally {
IOUtils.safeStreamClose(input);
IOUtils.safeStreamClose(output);
}
}
});
}
@Override
public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes, CancellationSignal cancellationSignal, LayoutResultCallback callback, Bundle extras){
if (cancellationSignal.isCanceled()) {
callback.onLayoutCancelled();
return;
}
PrintDocumentInfo pdi = new PrintDocumentInfo.Builder(filePath).setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT).build();
callback.onLayoutFinished(pdi, true);
}
};
printManager.print(jobName, pda, null);
}
});
}
}

View File

@ -19,9 +19,7 @@ import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.favicons.Favicons;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.mozglue.ContextUtils.SafeIntent;
import org.mozilla.gecko.sync.setup.SyncAccounts;
import org.mozilla.gecko.preferences.GeckoPreferences;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.ThreadUtils;
@ -65,7 +63,7 @@ public class Tabs implements GeckoEventListener {
public static final int LOADURL_BACKGROUND = 1 << 6;
public static final int LOADURL_EXTERNAL = 1 << 7;
private static final long PERSIST_TABS_AFTER_MILLISECONDS = 1000 * 5;
private static final long PERSIST_TABS_AFTER_MILLISECONDS = 1000 * 2;
public static final int INVALID_TAB_ID = -1;
@ -90,13 +88,7 @@ public class Tabs implements GeckoEventListener {
@Override
public void run() {
try {
boolean syncIsSetup = SyncAccounts.syncAccountsExist(context) ||
FirefoxAccounts.firefoxAccountsExist(context);
if (syncIsSetup) {
db.getTabsAccessor().persistLocalTabs(context.getContentResolver(), tabs);
}
} catch (SecurityException se) {} // will fail without android.permission.GET_ACCOUNTS
db.getTabsAccessor().persistLocalTabs(context.getContentResolver(), tabs);
}
};
@ -140,7 +132,7 @@ public class Tabs implements GeckoEventListener {
mAccountListener = new OnAccountsUpdateListener() {
@Override
public void onAccountsUpdated(Account[] accounts) {
persistAllTabs();
queuePersistAllTabs();
}
};
@ -698,14 +690,6 @@ public class Tabs implements GeckoEventListener {
}
}
// This method persists the current ordered list of tabs in our tabs content provider.
public void persistAllTabs() {
// If there is already a mPersistTabsRunnable in progress, the backgroundThread will hold onto
// it and ensure these still happen in the correct order.
mPersistTabsRunnable = new PersistTabsRunnable(mAppContext, getTabsInOrder());
ThreadUtils.postToBackgroundThread(mPersistTabsRunnable);
}
/**
* Queues a request to persist tabs after PERSIST_TABS_AFTER_MILLISECONDS
* milliseconds have elapsed. If any existing requests are already queued then

View File

@ -268,8 +268,11 @@ public class BrowserContract {
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/deleted-formhistory";
}
@RobocopTarget
public static final class Tabs implements CommonColumns {
private Tabs() {}
public static final String TABLE_NAME = "tabs";
public static final Uri CONTENT_URI = Uri.withAppendedPath(TABS_AUTHORITY_URI, "tabs");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/tab";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/tab";

View File

@ -16,6 +16,7 @@ import org.mozilla.gecko.db.BrowserContract.FaviconColumns;
import org.mozilla.gecko.db.BrowserContract.Favicons;
import org.mozilla.gecko.db.BrowserContract.History;
import org.mozilla.gecko.db.BrowserContract.Schema;
import org.mozilla.gecko.db.BrowserContract.Tabs;
import org.mozilla.gecko.db.BrowserContract.Thumbnails;
import org.mozilla.gecko.sync.Utils;
@ -57,6 +58,7 @@ public class BrowserProvider extends SharedBrowserDatabaseProvider {
static final String TABLE_HISTORY = History.TABLE_NAME;
static final String TABLE_FAVICONS = Favicons.TABLE_NAME;
static final String TABLE_THUMBNAILS = Thumbnails.TABLE_NAME;
static final String TABLE_TABS = Tabs.TABLE_NAME;
static final String VIEW_COMBINED = Combined.VIEW_NAME;
static final String VIEW_BOOKMARKS_WITH_FAVICONS = Bookmarks.VIEW_WITH_FAVICONS;
@ -313,6 +315,9 @@ public class BrowserProvider extends SharedBrowserDatabaseProvider {
" SELECT " + Bookmarks.URL +
" FROM " + TABLE_BOOKMARKS +
" WHERE " + Bookmarks.PARENT + " = " + Bookmarks.FIXED_PINNED_LIST_ID +
") AND " + Thumbnails.URL + " NOT IN ( " +
" SELECT " + Tabs.URL +
" FROM " + TABLE_TABS +
")";
trace("Clear thumbs using query: " + sql);
db.execSQL(sql);

View File

@ -347,6 +347,7 @@ size. -->
<!ENTITY share_title "Share via">
<!ENTITY share_image_failed "Unable to share this image">
<!ENTITY save_as_pdf "Save as PDF">
<!ENTITY print "Print">
<!ENTITY find_in_page "Find in Page">
<!ENTITY desktop_mode "Request Desktop Site">
<!ENTITY page "Page">

View File

@ -419,6 +419,7 @@ gbjar.sources += [
'preferences/SearchPreferenceCategory.java',
'preferences/SyncPreference.java',
'PrefsHelper.java',
'PrintHelper.java',
'PrivateTab.java',
'prompts/ColorPickerInput.java',
'prompts/IconGridInput.java',

View File

@ -72,6 +72,9 @@
<item android:id="@+id/save_as_pdf"
android:title="@string/save_as_pdf"/>
<item android:id="@+id/print"
android:title="@string/print"/>
<item android:id="@+id/add_search_engine"
android:title="@string/contextmenu_add_search_engine"/>

View File

@ -72,6 +72,9 @@
<item android:id="@+id/save_as_pdf"
android:title="@string/save_as_pdf"/>
<item android:id="@+id/print"
android:title="@string/print"/>
<item android:id="@+id/add_search_engine"
android:title="@string/contextmenu_add_search_engine"/>

View File

@ -73,6 +73,9 @@
<item android:id="@+id/save_as_pdf"
android:title="@string/save_as_pdf"/>
<item android:id="@+id/print"
android:title="@string/print"/>
<item android:id="@+id/add_search_engine"
android:title="@string/contextmenu_add_search_engine"/>

View File

@ -39,6 +39,10 @@
<item android:id="@+id/save_as_pdf"
android:title="@string/save_as_pdf" />
<item android:id="@+id/print"
android:visible="false"
android:title="@string/print" />
<item android:id="@+id/find_in_page"
android:title="@string/find_in_page" />

View File

@ -95,6 +95,7 @@
<string name="share_title">&share_title;</string>
<string name="share_image_failed">&share_image_failed;</string>
<string name="save_as_pdf">&save_as_pdf;</string>
<string name="print">&print;</string>
<string name="find_in_page">&find_in_page;</string>
<string name="find_matchcase">&find_matchcase;</string>
<string name="desktop_mode">&desktop_mode;</string>

View File

@ -7,6 +7,7 @@ package org.mozilla.gecko.util;
import android.util.Log;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
@ -108,4 +109,11 @@ public class IOUtils {
return newBytes;
}
public static void safeStreamClose(Closeable stream) {
try {
if (stream != null)
stream.close();
} catch (IOException e) {}
}
}

View File

@ -0,0 +1,65 @@
// -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
/* 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/. */
"use strict";
var PrintHelper = {
init: function() {
Services.obs.addObserver(this, "Print:PDF", false);
},
observe: function (aSubject, aTopic, aData) {
let browser = BrowserApp.selectedBrowser;
switch (aTopic) {
case "Print:PDF":
Messaging.handleRequest(aTopic, aData, (data) => {
return this.generatePDF(browser);
});
break;
}
},
generatePDF: function(aBrowser) {
// Create the final destination file location
let fileName = ContentAreaUtils.getDefaultFileName(aBrowser.contentTitle, aBrowser.currentURI, null, null);
fileName = fileName.trim() + ".pdf";
let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
file.append(fileName);
file.createUnique(file.NORMAL_FILE_TYPE, parseInt("666", 8));
let printSettings = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(Ci.nsIPrintSettingsService).newPrintSettings;
printSettings.printSilent = true;
printSettings.showPrintProgress = false;
printSettings.printBGImages = false;
printSettings.printBGColors = false;
printSettings.printToFile = true;
printSettings.toFileName = file.path;
printSettings.printFrameType = Ci.nsIPrintSettings.kFramesAsIs;
printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
let webBrowserPrint = aBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebBrowserPrint);
return new Promise((resolve, reject) => {
webBrowserPrint.print(printSettings, {
onStateChange: function(webProgress, request, stateFlags, status) {
// We get two STATE_STOP calls, one for STATE_IS_DOCUMENT and one for STATE_IS_NETWORK
if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP && stateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
if (Components.isSuccessCode(status)) {
// Send the details to Java
resolve({ file: file.path, title: fileName });
} else {
reject();
}
}
},
onProgressChange: function () {},
onLocationChange: function () {},
onStatusChange: function () {},
onSecurityChange: function () {},
});
});
}
};

View File

@ -153,6 +153,7 @@ let lazilyLoadedObserverScripts = [
["SelectionHandler", ["TextSelection:Get"], "chrome://browser/content/SelectionHandler.js"],
["EmbedRT", ["GeckoView:ImportScript"], "chrome://browser/content/EmbedRT.js"],
["Reader", ["Reader:FetchContent", "Reader:Added", "Reader:Removed"], "chrome://browser/content/Reader.js"],
["PrintHelper", ["Print:PDF"], "chrome://browser/content/PrintHelper.js"],
];
if (AppConstants.NIGHTLY_BUILD) {
lazilyLoadedObserverScripts.push(

View File

@ -41,6 +41,7 @@ chrome.jar:
content/MemoryObserver.js (content/MemoryObserver.js)
content/ConsoleAPI.js (content/ConsoleAPI.js)
content/PluginHelper.js (content/PluginHelper.js)
content/PrintHelper.js (content/PrintHelper.js)
content/OfflineApps.js (content/OfflineApps.js)
content/MasterPassword.js (content/MasterPassword.js)
content/FindHelper.js (content/FindHelper.js)

View File

@ -112,6 +112,43 @@ let Messaging = {
this.sendRequest(aMessage);
});
},
/**
* Handles a request from Java, using the given listener method.
* This is mainly an internal method used by the RequestHandler object, but can be
* used in nsIObserver.observe implmentations that fall outside the normal usage
* patterns.
*
* @param aTopic The string name of the message
* @param aData The data sent to the observe method from Java
* @param aListener A function that takes a JSON data argument and returns a
* response which is sent to Java.
*/
handleRequest: Task.async(function* (aTopic, aData, aListener) {
let wrapper = JSON.parse(aData);
try {
let response = yield aListener(wrapper.data);
if (typeof response !== "object" || response === null) {
throw new Error("Gecko request listener did not return an object");
}
Messaging.sendRequest({
type: "Gecko:Request" + wrapper.id,
response: response
});
} catch (e) {
Cu.reportError("Error in Messaging handler for " + aTopic + ": " + e);
Messaging.sendRequest({
type: "Gecko:Request" + wrapper.id,
error: {
message: e.message || (e && e.toString()),
stack: e.stack || Components.stack.formattedStack,
}
});
}
})
};
let requestHandler = {
@ -139,30 +176,8 @@ let requestHandler = {
Services.obs.removeObserver(this, aMessage);
},
observe: Task.async(function* (aSubject, aTopic, aData) {
let wrapper = JSON.parse(aData);
observe: function(aSubject, aTopic, aData) {
let listener = this._listeners[aTopic];
try {
let response = yield listener(wrapper.data);
if (typeof response !== "object" || response === null) {
throw new Error("Gecko request listener did not return an object");
}
Messaging.sendRequest({
type: "Gecko:Request" + wrapper.id,
response: response
});
} catch (e) {
Cu.reportError("Error in Messaging handler for " + aTopic + ": " + e);
Messaging.sendRequest({
type: "Gecko:Request" + wrapper.id,
error: {
message: e.message || (e && e.toString()),
stack: e.stack || Components.stack.formattedStack,
}
});
}
})
Messaging.handleRequest(aTopic, aData, listener);
}
};

View File

@ -165,8 +165,11 @@ let AddonWatcher = {
// by the user. Don't waste time with it.
continue;
}
let previous = this._previousPerformanceIndicators[addonId];
this._previousPerformanceIndicators[addonId] = item;
// Store the activity for the group not the entire add-on, as we
// can have one group per process for each add-on.
let previous = this._previousPerformanceIndicators[item.groupId];
this._previousPerformanceIndicators[item.groupId] = item;
if (!previous) {
// This is the first time we see the addon, so we are probably