Bug 1142687 - Show context information for current page in the rooms view. r=standard8

This commit is contained in:
Jared Wein 2015-03-24 12:43:49 -04:00
Родитель 9ae3882abb
Коммит eefcc0b4fd
10 изменённых файлов: 306 добавлений и 15 удалений

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

@ -1700,9 +1700,9 @@ pref("loop.debug.websocket", false);
pref("loop.debug.sdk", false);
pref("loop.debug.twoWayMediaTelemetry", false);
#ifdef DEBUG
pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src 'self' data: https://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*; media-src blob:");
pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src *; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*; media-src blob:");
#else
pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src 'self' data: https://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net; media-src blob:");
pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src *; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net; media-src blob:");
#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");

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

@ -21,6 +21,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
"resource:///modules/loop/LoopStorage.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "hookWindowCloseForPanelClose",
"resource://gre/modules/MozSocialAPI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PageMetadata",
"resource://gre/modules/PageMetadata.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
"resource://gre/modules/PluralForm.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "UITour",
@ -844,6 +846,24 @@ function injectLoopAPI(targetWindow) {
}
},
/**
* Gets the metadata related to the currently selected tab in
* the most recent window.
*
* @param {Function} A callback that is passed the metadata.
*/
getSelectedTabMetadata: {
value: function(callback) {
let win = Services.wm.getMostRecentWindow("navigator:browser");
win.messageManager.addMessageListener("PageMetadata:PageDataResult", function onPageDataResult(msg) {
win.messageManager.removeMessageListener("PageMetadata:PageDataResult", onPageDataResult);
let pageData = msg.json;
callback(cloneValueInto(pageData, targetWindow));
});
win.gBrowser.selectedBrowser.messageManager.sendAsyncMessage("PageMetadata:GetPageData");
}
},
/**
* Associates a session-id and a call-id with a window for debugging.
*

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

@ -170,25 +170,71 @@ body {
/* Rooms */
.rooms {
min-height: 100px;
padding: 0 1rem;
}
.rooms > h1 {
font-weight: bold;
color: #999;
padding: .5rem 1rem;
}
.rooms > p {
padding: .5rem 0;
margin: 0;
}
.rooms > p > .btn {
.rooms > div > .context {
margin: .5rem 0 0;
background-color: #DEEFF7;
border-radius: 3px 3px 0 0;
padding: .5rem;
}
.rooms > div > .context > .context-enabled {
margin-bottom: .5rem;
display: block;
}
.rooms > div > .context > .context-enabled > input {
-moz-margin-start: 0;
}
.rooms > div > .context > .context-preview {
float: right;
width: 100px;
max-height: 200px;
-moz-margin-start: 10px;
margin-bottom: 10px;
}
body[dir=rtl] .rooms > div > .context > .context-preview {
float: left;
}
.rooms > div > .context > .context-preview[src=""] {
display: none;
}
.rooms > div > .context > .context-description {
display: block;
color: #707070;
}
.rooms > div > .context > .context-url {
display: block;
color: #59A1D7;
clear: both;
}
.rooms > div > .btn {
display: block;
font-size: 1rem;
margin: 0 auto;
margin: 0 auto .5rem;
width: 100%;
padding: .5rem 1rem;
border-radius: 0 0 3px 3px;
}
/* Remove when bug 1142671 is backed out. */
.rooms > div > :not(.context) + .btn {
border-radius: 3px;
margin-top: 0.5rem;
}
.room-list {
@ -197,6 +243,8 @@ body {
overflow: auto;
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
margin-left: -1rem;
margin-right: -1rem;
}
.room-list:empty {

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

@ -594,6 +594,7 @@ loop.panel = (function(_, mozL10n) {
mixins: [Backbone.Events, sharedMixins.WindowCloseMixin],
propTypes: {
mozLoop: React.PropTypes.object.isRequired,
store: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
userDisplayName: React.PropTypes.string.isRequired // for room creation
@ -666,7 +667,8 @@ loop.panel = (function(_, mozL10n) {
);
}, this)
),
React.createElement("p", null,
React.createElement("div", null,
React.createElement(ContextInfo, {mozLoop: this.props.mozLoop}),
React.createElement("button", {className: "btn btn-info new-room-button",
onClick: this.handleCreateButtonClick,
disabled: this._hasPendingOperation()},
@ -678,6 +680,60 @@ loop.panel = (function(_, mozL10n) {
}
});
/**
* Context info that is offered to be part of a Room.
*/
var ContextInfo = React.createClass({displayName: "ContextInfo",
propTypes: {
mozLoop: React.PropTypes.object.isRequired,
},
mixins: [sharedMixins.DocumentVisibilityMixin],
getInitialState: function() {
return {
previewImage: "",
description: "",
url: ""
};
},
onDocumentVisible: function() {
this.props.mozLoop.getSelectedTabMetadata(function callback(metadata) {
var previewImage = metadata.previews.length ? metadata.previews[0] : "";
var description = metadata.description || metadata.title;
var url = metadata.url;
this.setState({previewImage: previewImage,
description: description,
url: url});
}.bind(this));
},
onDocumentHidden: function() {
this.setState({previewImage: "",
description: "",
url: ""});
},
render: function() {
if (!this.props.mozLoop.getLoopPref("contextInConverations.enabled") ||
!this.state.url) {
return null;
}
return (
React.createElement("div", {className: "context"},
React.createElement("label", {className: "context-enabled"},
React.createElement("input", {type: "checkbox"}),
mozL10n.get("context_offer_label")
),
React.createElement("img", {className: "context-preview", src: this.state.previewImage}),
React.createElement("span", {className: "context-description"}, this.state.description),
React.createElement("span", {className: "context-url"}, this.state.url)
)
);
}
});
/**
* Panel view.
*/
@ -819,7 +875,8 @@ loop.panel = (function(_, mozL10n) {
React.createElement(Tab, {name: "rooms"},
React.createElement(RoomList, {dispatcher: this.props.dispatcher,
store: this.props.roomStore,
userDisplayName: this._getUserDisplayName()}),
userDisplayName: this._getUserDisplayName(),
mozLoop: this.props.mozLoop}),
React.createElement(ToSView, null)
),
React.createElement(Tab, {name: "contacts"},
@ -890,6 +947,7 @@ loop.panel = (function(_, mozL10n) {
init: init,
AuthLink: AuthLink,
AvailabilityDropdown: AvailabilityDropdown,
ContextInfo: ContextInfo,
GettingStartedView: GettingStartedView,
PanelView: PanelView,
RoomEntry: RoomEntry,

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

@ -594,6 +594,7 @@ loop.panel = (function(_, mozL10n) {
mixins: [Backbone.Events, sharedMixins.WindowCloseMixin],
propTypes: {
mozLoop: React.PropTypes.object.isRequired,
store: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
userDisplayName: React.PropTypes.string.isRequired // for room creation
@ -666,13 +667,68 @@ loop.panel = (function(_, mozL10n) {
/>;
}, this)
}</div>
<p>
<div>
<ContextInfo mozLoop={this.props.mozLoop} />
<button className="btn btn-info new-room-button"
onClick={this.handleCreateButtonClick}
disabled={this._hasPendingOperation()}>
{mozL10n.get("rooms_new_room_button_label")}
</button>
</p>
</div>
</div>
);
}
});
/**
* Context info that is offered to be part of a Room.
*/
var ContextInfo = React.createClass({
propTypes: {
mozLoop: React.PropTypes.object.isRequired,
},
mixins: [sharedMixins.DocumentVisibilityMixin],
getInitialState: function() {
return {
previewImage: "",
description: "",
url: ""
};
},
onDocumentVisible: function() {
this.props.mozLoop.getSelectedTabMetadata(function callback(metadata) {
var previewImage = metadata.previews.length ? metadata.previews[0] : "";
var description = metadata.description || metadata.title;
var url = metadata.url;
this.setState({previewImage: previewImage,
description: description,
url: url});
}.bind(this));
},
onDocumentHidden: function() {
this.setState({previewImage: "",
description: "",
url: ""});
},
render: function() {
if (!this.props.mozLoop.getLoopPref("contextInConverations.enabled") ||
!this.state.url) {
return null;
}
return (
<div className="context">
<label className="context-enabled">
<input type="checkbox"/>
{mozL10n.get("context_offer_label")}
</label>
<img className="context-preview" src={this.state.previewImage}/>
<span className="context-description">{this.state.description}</span>
<span className="context-url">{this.state.url}</span>
</div>
);
}
@ -819,7 +875,8 @@ loop.panel = (function(_, mozL10n) {
<Tab name="rooms">
<RoomList dispatcher={this.props.dispatcher}
store={this.props.roomStore}
userDisplayName={this._getUserDisplayName()}/>
userDisplayName={this._getUserDisplayName()}
mozLoop={this.props.mozLoop}/>
<ToSView />
</Tab>
<Tab name="contacts">
@ -890,6 +947,7 @@ loop.panel = (function(_, mozL10n) {
init: init,
AuthLink: AuthLink,
AvailabilityDropdown: AvailabilityDropdown,
ContextInfo: ContextInfo,
GettingStartedView: GettingStartedView,
PanelView: PanelView,
RoomEntry: RoomEntry,

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

@ -651,7 +651,8 @@ describe("loop.panel", function() {
React.createElement(loop.panel.RoomList, {
store: roomStore,
dispatcher: dispatcher,
userDisplayName: fakeEmail
userDisplayName: fakeEmail,
mozLoop: fakeMozLoop
}));
}
@ -708,6 +709,49 @@ describe("loop.panel", function() {
var buttonNode = view.getDOMNode().querySelector("button[disabled]");
expect(buttonNode).to.not.equal(null);
});
it("should show context information when a URL is available",
function() {
navigator.mozLoop.getLoopPref = function() {
return true;
}
var view = TestUtils.renderIntoDocument(
React.createElement(loop.panel.ContextInfo, {
mozLoop: navigator.mozLoop
})
);
view.setState({
previews: [""],
description: "fake description",
url: "https://www.example.com"
});
var contextEnabledCheckbox = view.getDOMNode().querySelector(".context-enabled");
expect(contextEnabledCheckbox).to.not.equal(null);
});
it("should not show context information when a URL is unavailable",
function() {
navigator.mozLoop.getLoopPref = function() {
return true;
}
var view = TestUtils.renderIntoDocument(
React.createElement(loop.panel.ContextInfo, {
mozLoop: navigator.mozLoop
})
);
view.setState({
previews: [""],
description: "fake description",
url: ""
});
var contextInfo = view.getDOMNode();
expect(contextInfo).to.equal(null);
});
});
describe('loop.panel.ToSView', function() {

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

@ -115,6 +115,7 @@ navigator.mozLoop = {
// Ensure we skip FTE completely.
case "gettingStarted.seen":
case "contacts.gravatars.promo":
case "contextInConverations.enabled":
return true;
case "contacts.gravatars.show":
return false;
@ -127,6 +128,13 @@ navigator.mozLoop = {
return "http://www.gravatar.com/avatar/" + (Math.ceil(Math.random() * 3) === 2 ?
"0a996f0fe2727ef1668bdb11897e4459" : "foo") + ".jpg?default=blank&s=40";
},
getSelectedTabMetadata: function(callback) {
callback({
previews: ["chrome://branding/content/about-logo.png"],
description: "sample webpage description",
url: "https://www.example.com"
});
},
contacts: {
getAll: function(callback) {
callback(null, [].concat(fakeContacts));

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

@ -56,6 +56,28 @@
function noop(){}
// We save the visibility change listeners so that we can fake an event
// to the panel once we've loaded all the views.
var visibilityListeners = [];
var rootObject = window;
rootObject.document.addEventListener = function(eventName, func) {
if (eventName === "visibilitychange") {
visibilityListeners.push(func);
}
window.addEventListener(eventName, func);
};
rootObject.document.removeEventListener = function(eventName, func) {
if (eventName === "visibilitychange") {
var index = visibilityListeners.indexOf(func);
visibilityListeners.splice(index, 1);
}
window.removeEventListener(eventName, func);
};
loop.shared.mixins.setRootObject(rootObject);
// Feedback API client configured to send data to the stage input server,
// which is available at https://input.allizom.org
var stageFeedbackApiClient = new loop.FeedbackAPIClient(
@ -757,6 +779,10 @@
window.addEventListener("DOMContentLoaded", function() {
try {
React.renderComponent(React.createElement(App, null), document.getElementById("main"));
for (var listener of visibilityListeners) {
listener({target: {hidden: false}});
}
} catch(err) {
console.error(err);
uncaughtError = err;

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

@ -56,6 +56,28 @@
function noop(){}
// We save the visibility change listeners so that we can fake an event
// to the panel once we've loaded all the views.
var visibilityListeners = [];
var rootObject = window;
rootObject.document.addEventListener = function(eventName, func) {
if (eventName === "visibilitychange") {
visibilityListeners.push(func);
}
window.addEventListener(eventName, func);
};
rootObject.document.removeEventListener = function(eventName, func) {
if (eventName === "visibilitychange") {
var index = visibilityListeners.indexOf(func);
visibilityListeners.splice(index, 1);
}
window.removeEventListener(eventName, func);
};
loop.shared.mixins.setRootObject(rootObject);
// Feedback API client configured to send data to the stage input server,
// which is available at https://input.allizom.org
var stageFeedbackApiClient = new loop.FeedbackAPIClient(
@ -757,6 +779,10 @@
window.addEventListener("DOMContentLoaded", function() {
try {
React.renderComponent(<App />, document.getElementById("main"));
for (var listener of visibilityListeners) {
listener({target: {hidden: false}});
}
} catch(err) {
console.error(err);
uncaughtError = err;

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

@ -331,3 +331,6 @@ infobar_button_gotit_label=Got it!
infobar_button_gotit_accesskey=G
infobar_menuitem_dontshowagain_label=Don't show this again
infobar_menuitem_dontshowagain_accesskey=D
# Context in conversation strings
context_offer_label=Let's talk about this page