Bug 1516272 - Add search hand-off, discovery stream and bug fixes to Activity Stream r=k88hudson

Differential Revision: https://phabricator.services.mozilla.com/D15307

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Ed Lee 2019-01-02 21:09:23 +00:00
Родитель cb1e8ac83e
Коммит 3d659c0a32
37 изменённых файлов: 1867 добавлений и 604 удалений

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

@ -6,6 +6,8 @@ support-files =
searchSuggestionEngine.xml
POSTSearchEngine.xml
dummy_page.html
prefs =
browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar=false
[browser_aboutCertError.js]
[browser_aboutCertError_telemetry.js]

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

@ -33,15 +33,20 @@ for (const type of [
"AS_ROUTER_TELEMETRY_USER_EVENT",
"BLOCK_URL",
"BOOKMARK_URL",
"CONTENT_LAYOUT",
"COPY_DOWNLOAD_LINK",
"DELETE_BOOKMARK_BY_ID",
"DELETE_FROM_POCKET",
"DELETE_HISTORY_URL",
"DIALOG_CANCEL",
"DIALOG_OPEN",
"DISCOVERY_STREAM_CONFIG_CHANGE",
"DISCOVERY_STREAM_CONFIG_SETUP",
"DISCOVERY_STREAM_LAYOUT_UPDATE",
"DOWNLOAD_CHANGED",
"FILL_SEARCH_TERM",
"FOCUS_SEARCH",
"HANDOFF_SEARCH_TO_AWESOMEBAR",
"HIDE_SEARCH",
"INIT",
"MIGRATION_CANCEL",
"MIGRATION_COMPLETED",
@ -91,6 +96,7 @@ for (const type of [
"SET_PREF",
"SHOW_DOWNLOAD_FILE",
"SHOW_FIREFOX_ACCOUNTS",
"SHOW_SEARCH",
"SKIPPED_SIGNIN",
"SNIPPETS_BLOCKLIST_CLEARED",
"SNIPPETS_BLOCKLIST_UPDATED",

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

@ -47,7 +47,18 @@ const INITIAL_STATE = {
pocketCta: {},
waitingForSpoc: true,
},
Layout: [],
// This is the new pocket configurable layout state.
DiscoveryStream: {
// This is a JSON-parsed copy of the discoverystream.config pref value.
config: {enabled: false, layout_endpoint: ""},
layout: [],
},
Search: {
// Pretend the search box is focused after handing off to AwesomeBar.
focus: false,
// Hide the search box after handing off to AwesomeBar and user starts typing.
hide: false,
},
};
function App(prevState = INITIAL_STATE.App, action) {
@ -430,10 +441,27 @@ function Pocket(prevState = INITIAL_STATE.Pocket, action) {
}
}
function Layout(prevState = INITIAL_STATE.Layout, action) {
function DiscoveryStream(prevState = INITIAL_STATE.DiscoveryStream, action) {
switch (action.type) {
case at.CONTENT_LAYOUT:
return action.data;
case at.DISCOVERY_STREAM_CONFIG_CHANGE:
// The reason this is a separate action is so it doesn't trigger a listener update on init
case at.DISCOVERY_STREAM_CONFIG_SETUP:
return {...prevState, config: action.data || {}};
case at.DISCOVERY_STREAM_LAYOUT_UPDATE:
return {...prevState, layout: action.data || []};
default:
return prevState;
}
}
function Search(prevState = INITIAL_STATE.Search, action) {
switch (action.type) {
case at.HIDE_SEARCH:
return Object.assign({...prevState, hide: true});
case at.FOCUS_SEARCH:
return Object.assign({...prevState, focus: true});
case at.SHOW_SEARCH:
return Object.assign({...prevState, hide: false, focus: false});
default:
return prevState;
}
@ -443,6 +471,23 @@ this.INITIAL_STATE = INITIAL_STATE;
this.TOP_SITES_DEFAULT_ROWS = TOP_SITES_DEFAULT_ROWS;
this.TOP_SITES_MAX_SITES_PER_ROW = TOP_SITES_MAX_SITES_PER_ROW;
this.reducers = {TopSites, App, ASRouter, Snippets, Prefs, Dialog, Sections, Pocket, Layout};
this.reducers = {
TopSites,
App,
ASRouter,
Snippets,
Prefs,
Dialog,
Sections,
Pocket,
DiscoveryStream,
Search,
};
const EXPORTED_SYMBOLS = ["reducers", "INITIAL_STATE", "insertPinned", "TOP_SITES_DEFAULT_ROWS", "TOP_SITES_MAX_SITES_PER_ROW"];
const EXPORTED_SYMBOLS = [
"reducers",
"INITIAL_STATE",
"insertPinned",
"TOP_SITES_DEFAULT_ROWS",
"TOP_SITES_MAX_SITES_PER_ROW",
];

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

@ -1,8 +1,10 @@
import {ASRouterUtils} from "../../asrouter/asrouter-content";
import {connect} from "react-redux";
import {ModalOverlay} from "../../asrouter/components/ModalOverlay/ModalOverlay";
import React from "react";
import {SimpleHashRouter} from "./SimpleHashRouter";
export class ASRouterAdmin extends React.PureComponent {
export class ASRouterAdminInner extends React.PureComponent {
constructor(props) {
super(props);
this.onMessage = this.onMessage.bind(this);
@ -374,6 +376,23 @@ export class ASRouterAdmin extends React.PureComponent {
ASRouterUtils.sendMessage({type: "FORCE_ATTRIBUTION", data: this.state.attributionParameters});
}
renderPocketStory(story) {
return (<tr className="message-item" key={story.guid}>
<td className="message-id"><span>{story.guid} <br /></span></td>
<td className="message-summary">
<pre>{JSON.stringify(story, null, 2)}</pre>
</td>
</tr>);
}
renderPocketStories() {
const {rows} = this.props.Sections.find(Section => Section.id === "topstories") || {};
return (<table><tbody>
{rows && rows.map(story => this.renderPocketStory(story))}
</tbody></table>);
}
renderAttributionParamers() {
return (
<div>
@ -399,9 +418,45 @@ export class ASRouterAdmin extends React.PureComponent {
</div>);
}
getSection() {
const [section] = this.props.location.routes;
switch (section) {
case "targeting":
return (<React.Fragment>
<h2>Targeting Utilities</h2>
<button className="button" onClick={this.expireCache}>Expire Cache</button> (This expires the cache in ASR Targeting for bookmarks and top sites)
{this.renderTargetingParameters()}
{this.renderAttributionParamers()}
</React.Fragment>);
case "pocket":
return (<React.Fragment>
<h2>Pocket</h2>
{this.renderPocketStories()}
</React.Fragment>);
default:
return (<React.Fragment>
<h2>Message Providers <button title="Restore all provider settings that ship with Firefox" className="button" onClick={this.resetPref}>Restorear default prefs</button></h2>
{this.state.providers ? this.renderProviders() : null}
<h2>Messages</h2>
{this.renderMessageFilter()}
{this.renderMessages()}
{this.renderPasteModal()}
</React.Fragment>);
}
}
render() {
return (<div className="asrouter-admin outer-wrapper">
return (<div className="asrouter-admin">
<aside className="sidebar">
<ul>
<li><a href="#devtools">General</a></li>
<li><a href="#devtools-targeting">Targeting</a></li>
<li><a href="#devtools-pocket">Pocket</a></li>
</ul>
</aside>
<main className="main-panel">
<h1>AS Router Admin</h1>
<p className="helpLink">
<span className="icon icon-small-spacer icon-info" />
{" "}
@ -409,17 +464,12 @@ export class ASRouterAdmin extends React.PureComponent {
Need help using these tools? Check out our <a target="blank" href="https://github.com/mozilla/activity-stream/blob/master/content-src/asrouter/docs/debugging-docs.md">documentation</a>
</span>
</p>
<h2>Targeting Utilities</h2>
<button className="button" onClick={this.expireCache}>Expire Cache</button> (This expires the cache in ASR Targeting for bookmarks and top sites)
<h2>Message Providers <button title="Restore all provider settings that ship with Firefox" className="button" onClick={this.resetPref}>Restore default prefs</button></h2>
{this.state.providers ? this.renderProviders() : null}
<h2>Messages</h2>
{this.renderMessageFilter()}
{this.renderMessages()}
{this.renderPasteModal()}
{this.renderTargetingParameters()}
{this.renderAttributionParamers()}
{this.getSection()}
</main>
</div>);
}
}
export const _ASRouterAdmin = props => (<SimpleHashRouter><ASRouterAdminInner {...props} /></SimpleHashRouter>);
export const ASRouterAdmin = connect(state => ({Sections: state.Sections}))(_ASRouterAdmin);

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

@ -2,12 +2,32 @@
.asrouter-admin {
$border-color: var(--newtab-border-secondary-color);
$monospace: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', 'Droid Sans Mono', 'Source Code Pro', monospace;
max-width: 996px;
margin: 0 auto;
font-size: 14px;
// Reset .outer-wrapper styles
display: inherit;
padding: 0 0 92px;
display: flex;
.sidebar {
position: fixed;
width: 240px;
padding: 30px 20px;
ul {
margin: 0;
padding: 0;
list-style: none;
}
li a {
padding: 10px 34px;
display: block;
&:hover {
background: $grey-20;
}
}
}
h1 {
font-weight: 200;

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

@ -0,0 +1,31 @@
import React from "react";
export class SimpleHashRouter extends React.PureComponent {
constructor(props) {
super(props);
this.onHashChange = this.onHashChange.bind(this);
this.state = {hash: global.location.hash};
}
onHashChange() {
this.setState({hash: global.location.hash});
}
componentWillMount() {
global.addEventListener("hashchange", this.onHashChange);
}
componentWillUnmount() {
global.removeEventListener("hashchange", this.onHashChange);
}
render() {
const [, ...routes] = this.state.hash.replace("#asrouter", "").split("-");
return React.cloneElement(this.props.children, {
location: {
hash: this.state.hash,
routes,
},
});
}
}

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

@ -3,6 +3,7 @@ import {addLocaleData, injectIntl, IntlProvider} from "react-intl";
import {ASRouterAdmin} from "content-src/components/ASRouterAdmin/ASRouterAdmin";
import {ConfirmDialog} from "content-src/components/ConfirmDialog/ConfirmDialog";
import {connect} from "react-redux";
import {DiscoveryStreamBase} from "content-src/components/DiscoveryStreamBase/DiscoveryStreamBase";
import {ErrorBoundary} from "content-src/components/ErrorBoundary/ErrorBoundary";
import {ManualMigration} from "content-src/components/ManualMigration/ManualMigration";
import {PrerenderData} from "common/PrerenderData.jsm";
@ -82,10 +83,11 @@ export class _Base extends React.PureComponent {
const prefs = props.Prefs.values;
if (prefs["asrouter.devtoolsEnabled"]) {
if (window.location.hash === "#asrouter") {
if (window.location.hash.startsWith("#asrouter") ||
window.location.hash.startsWith("#devtools")) {
return (<ASRouterAdmin />);
}
console.log("ASRouter devtools enabled. To access visit %cabout:newtab#asrouter", "font-weight: bold"); // eslint-disable-line no-console
console.log("Activity Stream devtools enabled. To access visit %cabout:newtab#devtools", "font-weight: bold"); // eslint-disable-line no-console
}
if (!props.isPrerendered && !initialized) {
@ -138,6 +140,8 @@ export class BaseContent extends React.PureComponent {
const shouldBeFixedToTop = PrerenderData.arePrefsValid(name => prefs[name]);
const noSectionsEnabled = !prefs["feeds.topsites"] && props.Sections.filter(section => section.enabled).length === 0;
const isDiscoveryStream = props.DiscoveryStream.config && props.DiscoveryStream.config.enabled;
const searchHandoffEnabled = prefs["improvesearch.handoffToAwesomebar"];
const outerClassName = [
"outer-wrapper",
@ -153,17 +157,17 @@ export class BaseContent extends React.PureComponent {
{prefs.showSearch &&
<div className="non-collapsible-section">
<ErrorBoundary>
<Search showLogo={noSectionsEnabled} />
<Search showLogo={noSectionsEnabled} handoffEnabled={searchHandoffEnabled} {...props.Search} />
</ErrorBoundary>
</div>
}
<div className={`body-wrapper${(initialized ? " on" : "")}`}>
{!prefs.migrationExpired &&
{!isDiscoveryStream && !prefs.migrationExpired &&
<div className="non-collapsible-section">
<ManualMigration />
</div>
}
<Sections />
{isDiscoveryStream ? <DiscoveryStreamBase /> : <Sections />}
<PrefsButton onClick={this.openPreferences} />
</div>
<ConfirmDialog />
@ -173,4 +177,10 @@ export class BaseContent extends React.PureComponent {
}
}
export const Base = connect(state => ({App: state.App, Prefs: state.Prefs, Sections: state.Sections}))(_Base);
export const Base = connect(state => ({
App: state.App,
Prefs: state.Prefs,
Sections: state.Sections,
DiscoveryStream: state.DiscoveryStream,
Search: state.Search,
}))(_Base);

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

@ -0,0 +1,22 @@
import {connect} from "react-redux";
import React from "react";
export class _DiscoveryStreamBase extends React.PureComponent {
render() {
return (
<div className="discovery-stream layout">
{this.props.DiscoveryStream.layout.map((section, sectionIndex) => (
<div key={`section-${sectionIndex}`} className={`column column-${section.width}`}>
{section.components.map((component, componentIndex) => (
<div key={`component-${componentIndex}`}>
<div>{component.type}</div>
</div>
))}
</div>
))}
</div>
);
}
}
export const DiscoveryStreamBase = connect(state => ({DiscoveryStream: state.DiscoveryStream}))(_DiscoveryStreamBase);

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

@ -0,0 +1,20 @@
.discovery-stream.layout {
$columns: 12;
display: grid;
grid-template-columns: repeat($columns, 1fr);
grid-column-gap: 10px;
grid-row-gap: 10px;
.column {
border: 1px solid $black;
}
@while $columns > 0 {
.column-#{$columns} {
grid-column-start: auto;
grid-column-end: span $columns;
}
$columns: $columns - 1;
}
}

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

@ -1,8 +1,8 @@
/* globals ContentSearchUIController */
"use strict";
import {actionCreators as ac, actionTypes as at} from "common/Actions.jsm";
import {FormattedMessage, injectIntl} from "react-intl";
import {actionCreators as ac} from "common/Actions.jsm";
import {connect} from "react-redux";
import {IS_NEWTAB} from "content-src/lib/constants";
import React from "react";
@ -10,7 +10,8 @@ import React from "react";
export class _Search extends React.PureComponent {
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
this.onSearchClick = this.onSearchClick.bind(this);
this.onSearchHandoffClick = this.onSearchHandoffClick.bind(this);
this.onInputMount = this.onInputMount.bind(this);
}
@ -21,10 +22,26 @@ export class _Search extends React.PureComponent {
}
}
onClick(event) {
onSearchClick(event) {
window.gContentSearchController.search(event);
}
onSearchHandoffClick(event) {
// When search hand-off is enabled, we render a big button that is styled to
// look like a search textbox. If the button is clicked with the mouse, we style
// the button as if it was a focused search box and show a fake cursor but
// really focus the awesomebar without the focus styles.
// If the button is clicked from the keyboard, we focus the awesomebar normally.
// This is to minimize confusion with users navigating with the keyboard and
// users using assistive technologoy.
const isKeyboardClick = event.clientX === 0 && event.clientY === 0;
const hiddenFocus = !isKeyboardClick;
this.props.dispatch(ac.OnlyToMain({type: at.HANDOFF_SEARCH_TO_AWESOMEBAR, data: {hiddenFocus}}));
this.props.dispatch({type: at.FOCUS_SEARCH});
// TODO: Send a telemetry ping. BUG 1514732
}
componentWillUnmount() {
delete window.gContentSearchController;
}
@ -63,13 +80,20 @@ export class _Search extends React.PureComponent {
* in order to execute searches in various tests
*/
render() {
return (<div className="search-wrapper">
const wrapperClassName = [
"search-wrapper",
this.props.hide && "search-hidden",
this.props.focus && "search-active",
].filter(v => v).join(" ");
return (<div className={wrapperClassName}>
{this.props.showLogo &&
<div className="logo-and-wordmark">
<div className="logo" />
<div className="wordmark" />
</div>
}
{!this.props.handoffEnabled &&
<div className="search-inner-wrapper">
<label htmlFor="newtab-search-text" className="search-label">
<span className="sr-only"><FormattedMessage id="search_web_placeholder" /></span>
@ -84,11 +108,32 @@ export class _Search extends React.PureComponent {
<button
id="searchSubmit"
className="search-button"
onClick={this.onClick}
onClick={this.onSearchClick}
title={this.props.intl.formatMessage({id: "search_button"})}>
<span className="sr-only"><FormattedMessage id="search_button" /></span>
</button>
</div>
}
{this.props.handoffEnabled &&
<div className="search-inner-wrapper">
<button
className="search-handoff-button"
onClick={this.onSearchHandoffClick}
title={this.props.intl.formatMessage({id: "search_web_placeholder"})}>
<div className="fake-textbox">{this.props.intl.formatMessage({id: "search_web_placeholder"})}</div>
<div className="fake-caret" />
<div className="fake-button" />
</button>
{/*
This dummy and hidden input below is so we can load ContentSearchUIController.
Why? It sets --newtab-search-icon for us and it isn't trivial to port over.
*/}
<input
type="search"
style={{display: "none"}}
ref={this.onInputMount} />
</div>
}
</div>);
}
}

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

@ -1,11 +1,11 @@
.search-wrapper {
$search-height: 48px;
$search-icon-size: 24px;
$search-icon-padding: 12px;
$search-icon-width: 2 * $search-icon-padding + $search-icon-size -2;
$search-button-width: 48px;
$glyph-forward: url('chrome://browser/skin/forward.svg');
$search-height: 48px;
$search-icon-size: 24px;
$search-icon-padding: 12px;
$search-icon-width: 2 * $search-icon-padding + $search-icon-size -2;
$search-button-width: 48px;
$glyph-forward: url('chrome://browser/skin/forward.svg');
.search-wrapper {
padding: 34px 0 64px;
@media (max-height: 700px) {
@ -137,6 +137,85 @@
}
}
.search-handoff-button {
background: var(--newtab-textbox-background-color) var(--newtab-search-icon) $search-icon-padding center no-repeat;
background-size: $search-icon-size;
border: solid 1px var(--newtab-search-border-color);
border-radius: 3px;
box-shadow: $shadow-secondary, 0 0 0 1px $black-15;
cursor: text;
font-size: 15px;
padding: 0;
padding-inline-end: 48px;
padding-inline-start: 46px;
opacity: 1;
transition: opacity 500ms;
width: 100%;
&:dir(rtl) {
background-position-x: right $search-icon-padding;
}
&:hover {
box-shadow: $shadow-secondary, 0 0 0 1px $black-25;
}
&:focus,
.search-active & {
border: $input-border-active;
box-shadow: var(--newtab-textbox-focus-boxshadow);
}
.search-hidden & {
opacity: 0;
visibility: hidden;
}
.fake-textbox {
opacity: 0.54;
text-align: left;
}
.fake-caret {
animation: caret-animation 1.3s steps(5, start) infinite;
background: var(--newtab-text-primary-color);
display: none;
inset-inline-start: 47px;
height: 17px;
position: absolute;
top: 16px;
width: 1px;
@keyframes caret-animation {
to {
visibility: hidden;
}
}
.search-active & {
display: block;
}
}
.fake-button {
background: $glyph-forward no-repeat center center;
background-size: 16px 16px;
border: 0;
border-radius: 0 $border-radius $border-radius 0;
-moz-context-properties: fill;
fill: var(--newtab-search-icon-color);
height: 100%;
inset-inline-end: 0;
position: absolute;
top: 1px;
width: $search-button-width;
&:dir(rtl) {
transform: scaleX(-1);
}
}
}
@media (min-height: 701px) {
.fixed-search {
main {
@ -172,6 +251,19 @@
}
}
}
.search-handoff-button {
background-position-x: $search-icon-padding;
background-size: $search-icon-size;
&:dir(rtl) {
background-position-x: right $search-icon-padding;
}
.fake-caret {
top: 10px;
}
}
}
}

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

@ -306,26 +306,7 @@ export class _Sections extends React.PureComponent {
return sections;
}
renderLayout() {
return (
<div className="sections-list layout">
{this.props.Layout.map((section, sectionIndex) => (
<div key={`section-${sectionIndex}`} className={`column column-${section.width}`}>
{section.components.map((component, componentIndex) => (
<div key={`component-${componentIndex}`}>
<div>{component.type}</div>
</div>
))}
</div>
))}
</div>
);
}
render() {
if (this.props.Layout && this.props.Layout.length) {
return this.renderLayout();
}
return (
<div className="sections-list">
{this.renderSections()}
@ -334,4 +315,4 @@ export class _Sections extends React.PureComponent {
}
}
export const Sections = connect(state => ({Sections: state.Sections, Prefs: state.Prefs, Layout: state.Layout}))(_Sections);
export const Sections = connect(state => ({Sections: state.Sections, Prefs: state.Prefs}))(_Sections);

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

@ -1,25 +1,4 @@
.sections-list {
&.layout {
$columns: 12;
display: grid;
grid-template-columns: repeat($columns, 1fr);
grid-column-gap: 10px;
grid-row-gap: 10px;
.column {
border: 1px solid $black;
}
@while $columns > 0 {
.column-#{$columns} {
grid-column-start: auto;
grid-column-end: span $columns;
}
$columns: $columns - 1;
}
}
.section-list {
display: grid;
grid-gap: $base-gutter;

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

@ -143,6 +143,7 @@ input {
@import '../components/ASRouterAdmin/ASRouterAdmin';
@import '../components/PocketLoggedInCta/PocketLoggedInCta';
@import '../components/MoreRecommendations/MoreRecommendations';
@import '../components/DiscoveryStreamBase/DiscoveryStreamBase';
// AS Router
@import '../asrouter/components/Button/Button';

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

@ -846,50 +846,6 @@ main {
opacity: 1;
transform: translateY(0); } }
.sections-list.layout {
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-column-gap: 10px;
grid-row-gap: 10px; }
.sections-list.layout .column {
border: 1px solid #000; }
.sections-list.layout .column-12 {
grid-column-start: auto;
grid-column-end: span 12; }
.sections-list.layout .column-11 {
grid-column-start: auto;
grid-column-end: span 11; }
.sections-list.layout .column-10 {
grid-column-start: auto;
grid-column-end: span 10; }
.sections-list.layout .column-9 {
grid-column-start: auto;
grid-column-end: span 9; }
.sections-list.layout .column-8 {
grid-column-start: auto;
grid-column-end: span 8; }
.sections-list.layout .column-7 {
grid-column-start: auto;
grid-column-end: span 7; }
.sections-list.layout .column-6 {
grid-column-start: auto;
grid-column-end: span 6; }
.sections-list.layout .column-5 {
grid-column-start: auto;
grid-column-end: span 5; }
.sections-list.layout .column-4 {
grid-column-start: auto;
grid-column-end: span 4; }
.sections-list.layout .column-3 {
grid-column-start: auto;
grid-column-end: span 3; }
.sections-list.layout .column-2 {
grid-column-start: auto;
grid-column-end: span 2; }
.sections-list.layout .column-1 {
grid-column-start: auto;
grid-column-end: span 1; }
.sections-list .section-list {
display: grid;
grid-gap: 32px;
@ -1092,6 +1048,64 @@ main {
.search-wrapper .search-button:dir(rtl) {
transform: scaleX(-1); }
.search-handoff-button {
background: var(--newtab-textbox-background-color) var(--newtab-search-icon) 12px center no-repeat;
background-size: 24px;
border: solid 1px var(--newtab-search-border-color);
border-radius: 3px;
box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.15);
cursor: text;
font-size: 15px;
padding: 0;
padding-inline-end: 48px;
padding-inline-start: 46px;
opacity: 1;
transition: opacity 500ms;
width: 100%; }
.search-handoff-button:dir(rtl) {
background-position-x: right 12px; }
.search-handoff-button:hover {
box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.25); }
.search-handoff-button:focus,
.search-active .search-handoff-button {
border: 1px solid var(--newtab-textbox-focus-color);
box-shadow: var(--newtab-textbox-focus-boxshadow); }
.search-hidden .search-handoff-button {
opacity: 0;
visibility: hidden; }
.search-handoff-button .fake-textbox {
opacity: 0.54;
text-align: left; }
.search-handoff-button .fake-caret {
animation: caret-animation 1.3s steps(5, start) infinite;
background: var(--newtab-text-primary-color);
display: none;
inset-inline-start: 47px;
height: 17px;
position: absolute;
top: 16px;
width: 1px; }
@keyframes caret-animation {
to {
visibility: hidden; } }
.search-active .search-handoff-button .fake-caret {
display: block; }
.search-handoff-button .fake-button {
background: url("chrome://browser/skin/forward.svg") no-repeat center center;
background-size: 16px 16px;
border: 0;
border-radius: 0 3px 3px 0;
-moz-context-properties: fill;
fill: var(--newtab-search-icon-color);
height: 100%;
inset-inline-end: 0;
position: absolute;
top: 1px;
width: 48px; }
.search-handoff-button .fake-button:dir(rtl) {
transform: scaleX(-1); }
@media (min-height: 701px) {
.fixed-search main {
padding-top: 146px; }
@ -1111,7 +1125,14 @@ main {
background-position-x: 16px;
background-size: 16px; }
.fixed-search .search-wrapper input:dir(rtl) {
background-position-x: right 16px; } }
background-position-x: right 16px; }
.fixed-search .search-handoff-button {
background-position-x: 12px;
background-size: 24px; }
.fixed-search .search-handoff-button:dir(rtl) {
background-position-x: right 12px; }
.fixed-search .search-handoff-button .fake-caret {
top: 10px; } }
.contentSearchSuggestionTable {
background-color: var(--newtab-search-dropdown-color);
@ -1610,11 +1631,22 @@ main {
display: none; } }
.asrouter-admin {
max-width: 996px;
margin: 0 auto;
font-size: 14px;
display: inherit;
padding: 0 0 92px; }
display: flex; }
.asrouter-admin .sidebar {
position: fixed;
width: 240px;
padding: 30px 20px; }
.asrouter-admin .sidebar ul {
margin: 0;
padding: 0;
list-style: none; }
.asrouter-admin .sidebar li a {
padding: 10px 34px;
display: block; }
.asrouter-admin .sidebar li a:hover {
background: #EDEDF0; }
.asrouter-admin h1 {
font-weight: 200;
font-size: 32px; }
@ -1726,6 +1758,50 @@ main {
.more-recommendations:dir(rtl)::after {
transform: scaleX(-1); }
.discovery-stream.layout {
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-column-gap: 10px;
grid-row-gap: 10px; }
.discovery-stream.layout .column {
border: 1px solid #000; }
.discovery-stream.layout .column-12 {
grid-column-start: auto;
grid-column-end: span 12; }
.discovery-stream.layout .column-11 {
grid-column-start: auto;
grid-column-end: span 11; }
.discovery-stream.layout .column-10 {
grid-column-start: auto;
grid-column-end: span 10; }
.discovery-stream.layout .column-9 {
grid-column-start: auto;
grid-column-end: span 9; }
.discovery-stream.layout .column-8 {
grid-column-start: auto;
grid-column-end: span 8; }
.discovery-stream.layout .column-7 {
grid-column-start: auto;
grid-column-end: span 7; }
.discovery-stream.layout .column-6 {
grid-column-start: auto;
grid-column-end: span 6; }
.discovery-stream.layout .column-5 {
grid-column-start: auto;
grid-column-end: span 5; }
.discovery-stream.layout .column-4 {
grid-column-start: auto;
grid-column-end: span 4; }
.discovery-stream.layout .column-3 {
grid-column-start: auto;
grid-column-end: span 3; }
.discovery-stream.layout .column-2 {
grid-column-start: auto;
grid-column-end: span 2; }
.discovery-stream.layout .column-1 {
grid-column-start: auto;
grid-column-end: span 1; }
.ASRouterButton {
font-weight: 600;
font-size: 14px;

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -849,50 +849,6 @@ main {
opacity: 1;
transform: translateY(0); } }
.sections-list.layout {
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-column-gap: 10px;
grid-row-gap: 10px; }
.sections-list.layout .column {
border: 1px solid #000; }
.sections-list.layout .column-12 {
grid-column-start: auto;
grid-column-end: span 12; }
.sections-list.layout .column-11 {
grid-column-start: auto;
grid-column-end: span 11; }
.sections-list.layout .column-10 {
grid-column-start: auto;
grid-column-end: span 10; }
.sections-list.layout .column-9 {
grid-column-start: auto;
grid-column-end: span 9; }
.sections-list.layout .column-8 {
grid-column-start: auto;
grid-column-end: span 8; }
.sections-list.layout .column-7 {
grid-column-start: auto;
grid-column-end: span 7; }
.sections-list.layout .column-6 {
grid-column-start: auto;
grid-column-end: span 6; }
.sections-list.layout .column-5 {
grid-column-start: auto;
grid-column-end: span 5; }
.sections-list.layout .column-4 {
grid-column-start: auto;
grid-column-end: span 4; }
.sections-list.layout .column-3 {
grid-column-start: auto;
grid-column-end: span 3; }
.sections-list.layout .column-2 {
grid-column-start: auto;
grid-column-end: span 2; }
.sections-list.layout .column-1 {
grid-column-start: auto;
grid-column-end: span 1; }
.sections-list .section-list {
display: grid;
grid-gap: 32px;
@ -1095,6 +1051,64 @@ main {
.search-wrapper .search-button:dir(rtl) {
transform: scaleX(-1); }
.search-handoff-button {
background: var(--newtab-textbox-background-color) var(--newtab-search-icon) 12px center no-repeat;
background-size: 24px;
border: solid 1px var(--newtab-search-border-color);
border-radius: 3px;
box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.15);
cursor: text;
font-size: 15px;
padding: 0;
padding-inline-end: 48px;
padding-inline-start: 46px;
opacity: 1;
transition: opacity 500ms;
width: 100%; }
.search-handoff-button:dir(rtl) {
background-position-x: right 12px; }
.search-handoff-button:hover {
box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.25); }
.search-handoff-button:focus,
.search-active .search-handoff-button {
border: 1px solid var(--newtab-textbox-focus-color);
box-shadow: var(--newtab-textbox-focus-boxshadow); }
.search-hidden .search-handoff-button {
opacity: 0;
visibility: hidden; }
.search-handoff-button .fake-textbox {
opacity: 0.54;
text-align: left; }
.search-handoff-button .fake-caret {
animation: caret-animation 1.3s steps(5, start) infinite;
background: var(--newtab-text-primary-color);
display: none;
inset-inline-start: 47px;
height: 17px;
position: absolute;
top: 16px;
width: 1px; }
@keyframes caret-animation {
to {
visibility: hidden; } }
.search-active .search-handoff-button .fake-caret {
display: block; }
.search-handoff-button .fake-button {
background: url("chrome://browser/skin/forward.svg") no-repeat center center;
background-size: 16px 16px;
border: 0;
border-radius: 0 3px 3px 0;
-moz-context-properties: fill;
fill: var(--newtab-search-icon-color);
height: 100%;
inset-inline-end: 0;
position: absolute;
top: 1px;
width: 48px; }
.search-handoff-button .fake-button:dir(rtl) {
transform: scaleX(-1); }
@media (min-height: 701px) {
.fixed-search main {
padding-top: 146px; }
@ -1114,7 +1128,14 @@ main {
background-position-x: 16px;
background-size: 16px; }
.fixed-search .search-wrapper input:dir(rtl) {
background-position-x: right 16px; } }
background-position-x: right 16px; }
.fixed-search .search-handoff-button {
background-position-x: 12px;
background-size: 24px; }
.fixed-search .search-handoff-button:dir(rtl) {
background-position-x: right 12px; }
.fixed-search .search-handoff-button .fake-caret {
top: 10px; } }
.contentSearchSuggestionTable {
background-color: var(--newtab-search-dropdown-color);
@ -1613,11 +1634,22 @@ main {
display: none; } }
.asrouter-admin {
max-width: 996px;
margin: 0 auto;
font-size: 14px;
display: inherit;
padding: 0 0 92px; }
display: flex; }
.asrouter-admin .sidebar {
position: fixed;
width: 240px;
padding: 30px 20px; }
.asrouter-admin .sidebar ul {
margin: 0;
padding: 0;
list-style: none; }
.asrouter-admin .sidebar li a {
padding: 10px 34px;
display: block; }
.asrouter-admin .sidebar li a:hover {
background: #EDEDF0; }
.asrouter-admin h1 {
font-weight: 200;
font-size: 32px; }
@ -1729,6 +1761,50 @@ main {
.more-recommendations:dir(rtl)::after {
transform: scaleX(-1); }
.discovery-stream.layout {
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-column-gap: 10px;
grid-row-gap: 10px; }
.discovery-stream.layout .column {
border: 1px solid #000; }
.discovery-stream.layout .column-12 {
grid-column-start: auto;
grid-column-end: span 12; }
.discovery-stream.layout .column-11 {
grid-column-start: auto;
grid-column-end: span 11; }
.discovery-stream.layout .column-10 {
grid-column-start: auto;
grid-column-end: span 10; }
.discovery-stream.layout .column-9 {
grid-column-start: auto;
grid-column-end: span 9; }
.discovery-stream.layout .column-8 {
grid-column-start: auto;
grid-column-end: span 8; }
.discovery-stream.layout .column-7 {
grid-column-start: auto;
grid-column-end: span 7; }
.discovery-stream.layout .column-6 {
grid-column-start: auto;
grid-column-end: span 6; }
.discovery-stream.layout .column-5 {
grid-column-start: auto;
grid-column-end: span 5; }
.discovery-stream.layout .column-4 {
grid-column-start: auto;
grid-column-end: span 4; }
.discovery-stream.layout .column-3 {
grid-column-start: auto;
grid-column-end: span 3; }
.discovery-stream.layout .column-2 {
grid-column-start: auto;
grid-column-end: span 2; }
.discovery-stream.layout .column-1 {
grid-column-start: auto;
grid-column-end: span 1; }
.ASRouterButton {
font-weight: 600;
font-size: 14px;

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -846,50 +846,6 @@ main {
opacity: 1;
transform: translateY(0); } }
.sections-list.layout {
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-column-gap: 10px;
grid-row-gap: 10px; }
.sections-list.layout .column {
border: 1px solid #000; }
.sections-list.layout .column-12 {
grid-column-start: auto;
grid-column-end: span 12; }
.sections-list.layout .column-11 {
grid-column-start: auto;
grid-column-end: span 11; }
.sections-list.layout .column-10 {
grid-column-start: auto;
grid-column-end: span 10; }
.sections-list.layout .column-9 {
grid-column-start: auto;
grid-column-end: span 9; }
.sections-list.layout .column-8 {
grid-column-start: auto;
grid-column-end: span 8; }
.sections-list.layout .column-7 {
grid-column-start: auto;
grid-column-end: span 7; }
.sections-list.layout .column-6 {
grid-column-start: auto;
grid-column-end: span 6; }
.sections-list.layout .column-5 {
grid-column-start: auto;
grid-column-end: span 5; }
.sections-list.layout .column-4 {
grid-column-start: auto;
grid-column-end: span 4; }
.sections-list.layout .column-3 {
grid-column-start: auto;
grid-column-end: span 3; }
.sections-list.layout .column-2 {
grid-column-start: auto;
grid-column-end: span 2; }
.sections-list.layout .column-1 {
grid-column-start: auto;
grid-column-end: span 1; }
.sections-list .section-list {
display: grid;
grid-gap: 32px;
@ -1092,6 +1048,64 @@ main {
.search-wrapper .search-button:dir(rtl) {
transform: scaleX(-1); }
.search-handoff-button {
background: var(--newtab-textbox-background-color) var(--newtab-search-icon) 12px center no-repeat;
background-size: 24px;
border: solid 1px var(--newtab-search-border-color);
border-radius: 3px;
box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.15);
cursor: text;
font-size: 15px;
padding: 0;
padding-inline-end: 48px;
padding-inline-start: 46px;
opacity: 1;
transition: opacity 500ms;
width: 100%; }
.search-handoff-button:dir(rtl) {
background-position-x: right 12px; }
.search-handoff-button:hover {
box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.25); }
.search-handoff-button:focus,
.search-active .search-handoff-button {
border: 1px solid var(--newtab-textbox-focus-color);
box-shadow: var(--newtab-textbox-focus-boxshadow); }
.search-hidden .search-handoff-button {
opacity: 0;
visibility: hidden; }
.search-handoff-button .fake-textbox {
opacity: 0.54;
text-align: left; }
.search-handoff-button .fake-caret {
animation: caret-animation 1.3s steps(5, start) infinite;
background: var(--newtab-text-primary-color);
display: none;
inset-inline-start: 47px;
height: 17px;
position: absolute;
top: 16px;
width: 1px; }
@keyframes caret-animation {
to {
visibility: hidden; } }
.search-active .search-handoff-button .fake-caret {
display: block; }
.search-handoff-button .fake-button {
background: url("chrome://browser/skin/forward.svg") no-repeat center center;
background-size: 16px 16px;
border: 0;
border-radius: 0 3px 3px 0;
-moz-context-properties: fill;
fill: var(--newtab-search-icon-color);
height: 100%;
inset-inline-end: 0;
position: absolute;
top: 1px;
width: 48px; }
.search-handoff-button .fake-button:dir(rtl) {
transform: scaleX(-1); }
@media (min-height: 701px) {
.fixed-search main {
padding-top: 146px; }
@ -1111,7 +1125,14 @@ main {
background-position-x: 16px;
background-size: 16px; }
.fixed-search .search-wrapper input:dir(rtl) {
background-position-x: right 16px; } }
background-position-x: right 16px; }
.fixed-search .search-handoff-button {
background-position-x: 12px;
background-size: 24px; }
.fixed-search .search-handoff-button:dir(rtl) {
background-position-x: right 12px; }
.fixed-search .search-handoff-button .fake-caret {
top: 10px; } }
.contentSearchSuggestionTable {
background-color: var(--newtab-search-dropdown-color);
@ -1610,11 +1631,22 @@ main {
display: none; } }
.asrouter-admin {
max-width: 996px;
margin: 0 auto;
font-size: 14px;
display: inherit;
padding: 0 0 92px; }
display: flex; }
.asrouter-admin .sidebar {
position: fixed;
width: 240px;
padding: 30px 20px; }
.asrouter-admin .sidebar ul {
margin: 0;
padding: 0;
list-style: none; }
.asrouter-admin .sidebar li a {
padding: 10px 34px;
display: block; }
.asrouter-admin .sidebar li a:hover {
background: #EDEDF0; }
.asrouter-admin h1 {
font-weight: 200;
font-size: 32px; }
@ -1726,6 +1758,50 @@ main {
.more-recommendations:dir(rtl)::after {
transform: scaleX(-1); }
.discovery-stream.layout {
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-column-gap: 10px;
grid-row-gap: 10px; }
.discovery-stream.layout .column {
border: 1px solid #000; }
.discovery-stream.layout .column-12 {
grid-column-start: auto;
grid-column-end: span 12; }
.discovery-stream.layout .column-11 {
grid-column-start: auto;
grid-column-end: span 11; }
.discovery-stream.layout .column-10 {
grid-column-start: auto;
grid-column-end: span 10; }
.discovery-stream.layout .column-9 {
grid-column-start: auto;
grid-column-end: span 9; }
.discovery-stream.layout .column-8 {
grid-column-start: auto;
grid-column-end: span 8; }
.discovery-stream.layout .column-7 {
grid-column-start: auto;
grid-column-end: span 7; }
.discovery-stream.layout .column-6 {
grid-column-start: auto;
grid-column-end: span 6; }
.discovery-stream.layout .column-5 {
grid-column-start: auto;
grid-column-end: span 5; }
.discovery-stream.layout .column-4 {
grid-column-start: auto;
grid-column-end: span 4; }
.discovery-stream.layout .column-3 {
grid-column-start: auto;
grid-column-end: span 3; }
.discovery-stream.layout .column-2 {
grid-column-start: auto;
grid-column-end: span 2; }
.discovery-stream.layout .column-1 {
grid-column-start: auto;
grid-column-end: span 1; }
.ASRouterButton {
font-weight: 600;
font-size: 14px;

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -29,6 +29,7 @@ const {TopSitesFeed} = ChromeUtils.import("resource://activity-stream/lib/TopSit
const {TopStoriesFeed} = ChromeUtils.import("resource://activity-stream/lib/TopStoriesFeed.jsm", {});
const {HighlightsFeed} = ChromeUtils.import("resource://activity-stream/lib/HighlightsFeed.jsm", {});
const {ASRouterFeed} = ChromeUtils.import("resource://activity-stream/lib/ASRouterFeed.jsm", {});
const {DiscoveryStreamFeed} = ChromeUtils.import("resource://activity-stream/lib/DiscoveryStreamFeed.jsm", {});
const DEFAULT_SITES = new Map([
// This first item is the global list fallback for any unexpected geos
@ -192,6 +193,10 @@ const PREFS_CONFIG = new Map([
title: "A comma-delimited list of search shortcuts that have previously been pinned",
value: "",
}],
["improvesearch.handoffToAwesomebar", {
title: "Should the search box handoff to the Awesomebar?",
value: true,
}],
["asrouter.devtoolsEnabled", {
title: "Are the asrouter devtools enabled?",
value: false,
@ -210,6 +215,14 @@ const PREFS_CONFIG = new Map([
}),
}],
// See browser/app/profile/firefox.js for other ASR preferences. They must be defined there to enable roll-outs.
["discoverystream.config", {
title: "Configuration for the new pocket new tab",
value: JSON.stringify({
enabled: false,
// Set this to https://gist.githubusercontent.com/ScottDowne/164995d9535b4203846048bdee29d169/raw/0cf538411e6ee898eb116208d70842c62c8d52f1/spoc.json to test
layout_endpoint: "",
}),
}],
]);
// Array of each feed's FEEDS_CONFIG factory and values to add to PREFS_CONFIG
@ -306,6 +319,12 @@ const FEEDS_DATA = [
title: "Handles AS Router messages, such as snippets and onboaridng",
value: true,
},
{
name: "discoverystreamfeed",
factory: () => new DiscoveryStreamFeed(),
title: "Handles new pocket ui for the new tab page",
value: true,
},
];
const FEEDS_CONFIG = new Map();

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

@ -0,0 +1,150 @@
/* 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";
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
ChromeUtils.import("resource://gre/modules/Services.jsm");
const {actionTypes: at, actionCreators: ac} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm", {});
const {PersistentCache} = ChromeUtils.import("resource://activity-stream/lib/PersistentCache.jsm", {});
const CACHE_KEY = "discovery_stream";
const LAYOUT_UPDATE_TIME = 30 * 60 * 1000; // 30 minutes
const CONFIG_PREF_NAME = "browser.newtabpage.activity-stream.discoverystream.config";
this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
constructor() {
// Internal state for checking if we've intialized all our data
this.loaded = false;
// Persistent cache for remote endpoint data.
this.cache = new PersistentCache(CACHE_KEY, true);
// Internal in-memory cache for parsing json prefs.
this._prefCache = {};
}
get config() {
if (this._prefCache.config) {
return this._prefCache.config;
}
try {
this._prefCache.config = JSON.parse(Services.prefs.getStringPref(CONFIG_PREF_NAME, ""));
} catch (e) {
// istanbul ignore next
this._prefCache.config = {};
// istanbul ignore next
Cu.reportError(`Could not parse preference. Try resetting ${CONFIG_PREF_NAME} in about:config.`);
}
return this._prefCache.config;
}
setupPrefs() {
Services.prefs.addObserver(CONFIG_PREF_NAME, this);
// Send the initial state of the pref on our reducer
this.store.dispatch(ac.BroadcastToContent({type: at.DISCOVERY_STREAM_CONFIG_SETUP, data: this.config}));
}
uninitPrefs() {
Services.prefs.removeObserver(CONFIG_PREF_NAME, this);
// Reset in-memory cache
this._prefCache = {};
}
observe(aSubject, aTopic, aPrefName) {
if (aPrefName === CONFIG_PREF_NAME) {
this._prefCache.config = null;
this.store.dispatch(ac.BroadcastToContent({type: at.DISCOVERY_STREAM_CONFIG_CHANGE, data: this.config}));
}
}
async fetchLayout() {
const endpoint = this.config.layout_endpoint;
if (!endpoint) {
Cu.reportError("No endpoint configured for pocket, so could not fetch layout");
return null;
}
try {
const response = await fetch(endpoint, {credentials: "omit"});
if (!response.ok) {
// istanbul ignore next
throw new Error(`Stories endpoint returned unexpected status: ${response.status}`);
}
return response.json();
} catch (error) {
// istanbul ignore next
Cu.reportError(`Failed to fetch layout: ${error.message}`);
}
// istanbul ignore next
return null;
}
async loadCachedData() {
const cachedData = await this.cache.get() || {};
let {layout: layoutResponse} = cachedData;
if (!layoutResponse || !(Date.now() - layoutResponse._timestamp > LAYOUT_UPDATE_TIME)) {
layoutResponse = await this.fetchLayout();
if (layoutResponse && layoutResponse.layout) {
layoutResponse._timestamp = Date.now();
await this.cache.set("layout", layoutResponse);
} else {
Cu.reportError("No response for response.layout prop");
}
}
if (layoutResponse && layoutResponse.layout) {
this.store.dispatch(ac.BroadcastToContent({type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, data: layoutResponse.layout}));
}
}
async enable() {
await this.loadCachedData();
this.loaded = true;
}
async disable() {
// Clear cache
await this.cache.set("layout", {});
// Reset reducer
this.store.dispatch(ac.BroadcastToContent({type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, data: []}));
this.loaded = false;
}
async onPrefChange() {
// Load data from all endpoints if our config.enabled = true.
if (this.config.enabled) {
await this.enable();
}
// Clear state and relevant listeners if config.enabled = false.
if (this.loaded && !this.config.enabled) {
await this.disable();
}
}
async onAction(action) {
switch (action.type) {
case at.INIT:
// During the initialization of Firefox:
// 1. Set-up listeners and initialize the redux state for config;
this.setupPrefs();
// 2. If config.enabled is true, start loading data.
if (this.config.enabled) {
await this.enable();
}
break;
case at.DISCOVERY_STREAM_CONFIG_CHANGE:
// When the config pref changes, load or unload data as needed.
await this.onPrefChange();
break;
case at.UNINIT:
// When this feed is shutting down:
this.uninitPrefs();
break;
}
}
};
const EXPORTED_SYMBOLS = ["DiscoveryStreamFeed"];

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

@ -283,6 +283,38 @@ class PlacesFeed {
_target.browser.ownerGlobal.gURLBar.search(`${data.label} `);
}
handoffSearchToAwesomebar({_target, data, meta}) {
const urlBar = _target.browser.ownerGlobal.gURLBar;
if (!data.hiddenFocus) {
// Do a normal focus of awesomebar and reset the in content search (remove fake focus styles).
urlBar.focus();
this.store.dispatch(ac.OnlyToOneContent({type: at.SHOW_SEARCH}, meta.fromTarget));
return;
}
// Focus the awesomebar without the style changes.
urlBar.hiddenFocus();
const onKeydown = () => {
// Once the user starts typing, we want to hide the in content search box
// and show the focus styles on the awesomebar.
this.store.dispatch(ac.OnlyToOneContent({type: at.HIDE_SEARCH}, meta.fromTarget));
urlBar.removeHiddenFocus();
urlBar.removeEventListener("keydown", onKeydown);
};
const onDone = () => {
// When done, let's cleanup everything.
this.store.dispatch(ac.OnlyToOneContent({type: at.SHOW_SEARCH}, meta.fromTarget));
urlBar.removeHiddenFocus();
urlBar.removeEventListener("keydown", onKeydown);
urlBar.removeEventListener("mousedown", onDone);
urlBar.removeEventListener("blur", onDone);
};
urlBar.addEventListener("keydown", onKeydown);
urlBar.addEventListener("mousedown", onDone);
urlBar.addEventListener("blur", onDone);
}
onAction(action) {
switch (action.type) {
case at.INIT:
@ -323,6 +355,9 @@ class PlacesFeed {
case at.FILL_SEARCH_TERM:
this.fillSearchTopSiteTerm(action);
break;
case at.HANDOFF_SEARCH_TO_AWESOMEBAR:
this.handoffSearchToAwesomebar(action);
break;
case at.OPEN_LINK: {
this.openLink(action);
break;

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

@ -112,13 +112,6 @@ this.TopStoriesFeed = class TopStoriesFeed {
this.store.dispatch(shouldBroadcast ? ac.BroadcastToContent(action) : ac.AlsoToPreloaded(action));
}
maybeDispatchLayoutUpdate(data, shouldBroadcast) {
if (data && data.length) {
const action = {type: at.CONTENT_LAYOUT, data};
this.store.dispatch(shouldBroadcast ? ac.BroadcastToContent(action) : ac.AlsoToPreloaded(action));
}
}
doContentUpdate(shouldBroadcast) {
let updateProps = {};
if (this.stories) {
@ -181,7 +174,6 @@ this.TopStoriesFeed = class TopStoriesFeed {
}
const body = await response.json();
this.maybeDispatchLayoutUpdate(body.layout);
this.updateSettings(body.settings);
this.stories = this.rotate(this.transform(body.recommendations));
this.cleanUpTopRecImpressionPref();
@ -202,9 +194,7 @@ this.TopStoriesFeed = class TopStoriesFeed {
async loadCachedData() {
const data = await this.cache.get();
let stories = data.stories && data.stories.recommendations;
let layout = data.stories && data.stories.layout;
let topics = data.topics && data.topics.topics;
this.maybeDispatchLayoutUpdate(layout);
let affinities = data.domainAffinities;
if (this.personalized && affinities && affinities.scores) {

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

@ -66,5 +66,15 @@ window.gActivityStreamPrerenderedState = {
"pocketCta": {},
"waitingForSpoc": true
},
"Layout": []
"DiscoveryStream": {
"config": {
"enabled": false,
"layout_endpoint": ""
},
"layout": []
},
"Search": {
"focus": false,
"hide": false
}
};

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

@ -1,9 +1,24 @@
"use strict";
test_newtab(function test_render_search() {
let search = content.document.getElementById("newtab-search-text");
ok(search, "Got the search box");
isnot(search.placeholder, "search_web_placeholder", "Search box is localized");
test_newtab({
async before({pushPrefs}) {
await pushPrefs(["browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar", false]);
},
test: function test_render_search() {
let search = content.document.getElementById("newtab-search-text");
ok(search, "Got the search box");
isnot(search.placeholder, "search_web_placeholder", "Search box is localized");
},
});
test_newtab({
async before({pushPrefs}) {
await pushPrefs(["browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar", true]);
},
test: function test_render_search_handoff() {
let search = content.document.querySelector(".search-handoff-button");
ok(search, "Got the search handoff button");
},
});
test_newtab(function test_render_topsites() {

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

@ -1,5 +1,5 @@
import {INITIAL_STATE, insertPinned, reducers} from "common/Reducers.jsm";
const {TopSites, App, Snippets, Prefs, Dialog, Sections, Pocket, Layout} = reducers;
const {TopSites, App, Snippets, Prefs, Dialog, Sections, Pocket, DiscoveryStream, Search} = reducers;
import {actionTypes as at} from "common/Actions.jsm";
describe("Reducers", () => {
@ -655,13 +655,35 @@ describe("Reducers", () => {
assert.equal(state.pocketCta.useCta, data.use_cta);
});
});
describe("Layout", () => {
describe("DiscoveryStream", () => {
it("should return INITIAL_STATE by default", () => {
assert.equal(Layout(undefined, {type: "some_action"}), INITIAL_STATE.Layout);
assert.equal(DiscoveryStream(undefined, {type: "some_action"}), INITIAL_STATE.DiscoveryStream);
});
it("should set layout data with layout.type CONTENT_LAYOUT", () => {
const state = Layout(undefined, {type: at.CONTENT_LAYOUT, data: ["test"]});
assert.equal(state[0], "test");
it("should set layout data with DISCOVERY_STREAM_LAYOUT_UPDATE", () => {
const state = DiscoveryStream(undefined, {type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, data: ["test"]});
assert.equal(state.layout[0], "test");
});
it("should set config data with DISCOVERY_STREAM_CONFIG_CHANGE", () => {
const state = DiscoveryStream(undefined, {type: at.DISCOVERY_STREAM_CONFIG_CHANGE, data: {enabled: true}});
assert.deepEqual(state.config, {enabled: true});
});
});
describe("Search", () => {
it("should return INITIAL_STATE by default", () => {
assert.equal(Search(undefined, {type: "some_action"}), INITIAL_STATE.Search);
});
it("should set hide to true on HIDE_SEARCH", () => {
const nextState = Search(undefined, {type: "HIDE_SEARCH"});
assert.propertyVal(nextState, "hide", true);
});
it("should set focus to true on FOCUS_SEARCH", () => {
const nextState = Search(undefined, {type: "FOCUS_SEARCH"});
assert.propertyVal(nextState, "focus", true);
});
it("should set focus and hide to false on SHOW_SEARCH", () => {
const nextState = Search(undefined, {type: "SHOW_SEARCH"});
assert.propertyVal(nextState, "focus", false);
assert.propertyVal(nextState, "hide", false);
});
});
});

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

@ -1,4 +1,4 @@
import {ASRouterAdmin} from "content-src/components/ASRouterAdmin/ASRouterAdmin";
import {ASRouterAdminInner} from "content-src/components/ASRouterAdmin/ASRouterAdmin";
import {GlobalOverrider} from "test/unit/utils";
import React from "react";
import {shallow} from "enzyme";
@ -9,6 +9,7 @@ describe("ASRouterAdmin", () => {
let sendMessageStub;
let addListenerStub;
let removeListenerStub;
let wrapper;
let FAKE_PROVIDER_PREF = [{
enabled: true,
id: "snippets_local_testing",
@ -32,37 +33,43 @@ describe("ASRouterAdmin", () => {
globals.set("RPMSendAsyncMessage", sendMessageStub);
globals.set("RPMAddMessageListener", addListenerStub);
globals.set("RPMRemoveMessageListener", removeListenerStub);
wrapper = shallow(<ASRouterAdminInner location={{routes: [""]}} />);
});
afterEach(() => {
sandbox.restore();
globals.restore();
});
it("should render ASRouterAdmin component", () => {
const wrapper = shallow(<ASRouterAdmin />);
assert.ok(wrapper.exists());
});
it("should send ADMIN_CONNECT_STATE on mount", () => {
shallow(<ASRouterAdmin />);
assert.calledOnce(sendMessageStub);
assert.propertyVal(sendMessageStub.firstCall.args[1], "type", "ADMIN_CONNECT_STATE");
});
it("should set a listener on mount", () => {
const wrapper = shallow(<ASRouterAdmin />);
assert.calledOnce(addListenerStub);
assert.calledWithExactly(addListenerStub, sinon.match.string, wrapper.instance().onMessage);
});
it("should remove listener on unmount", () => {
const wrapper = shallow(<ASRouterAdmin />);
wrapper.unmount();
assert.calledOnce(removeListenerStub);
});
describe("#getSection", () => {
it("should render a message provider section by default", () => {
assert.equal(wrapper.find("h2").at(1).text(), "Messages");
});
it("should render a targeting section for targeting route", () => {
wrapper = shallow(<ASRouterAdminInner location={{routes: ["targeting"]}} />);
assert.equal(wrapper.find("h2").at(0).text(), "Targeting Utilities");
});
it("should render a pocket section for pocket route", () => {
wrapper = shallow(<ASRouterAdminInner location={{routes: ["pocket"]}} Sections={[]} />);
assert.equal(wrapper.find("h2").at(0).text(), "Pocket");
});
});
describe("#render", () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<ASRouterAdmin />);
wrapper.setState({
providerPrefs: [],
providers: [],

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

@ -5,7 +5,7 @@ import {Search} from "content-src/components/Search/Search";
import {shallow} from "enzyme";
describe("<Base>", () => {
let DEFAULT_PROPS = {store: {getState: () => {}}, App: {initialized: true}, Prefs: {values: {}}, Sections: [], dispatch: () => {}};
let DEFAULT_PROPS = {store: {getState: () => {}}, App: {initialized: true}, Prefs: {values: {}}, Sections: [], DiscoveryStream: {config: {enabled: false}}, dispatch: () => {}};
it("should render Base component", () => {
const wrapper = shallow(<Base {...DEFAULT_PROPS} />);
@ -27,7 +27,7 @@ describe("<Base>", () => {
});
describe("<BaseContent>", () => {
let DEFAULT_PROPS = {store: {getState: () => {}}, App: {initialized: true}, Prefs: {values: {}}, Sections: [], dispatch: () => {}};
let DEFAULT_PROPS = {store: {getState: () => {}}, App: {initialized: true}, Prefs: {values: {}}, Sections: [], DiscoveryStream: {config: {enabled: false}}, dispatch: () => {}};
it("should render an ErrorBoundary with a Search child", () => {
const searchEnabledProps =

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

@ -73,4 +73,34 @@ describe("<Search>", () => {
assert.isUserEventAction(action);
assert.propertyVal(action.data, "event", "SEARCH");
});
describe("Search Hand-off", () => {
it("should render a Search element when hand-off is enabled", () => {
const wrapper = shallowWithIntl(<Search {...DEFAULT_PROPS} handoffEnabled={true} />);
assert.ok(wrapper.exists());
assert.equal(wrapper.find(".search-handoff-button").length, 1);
});
it("should hand-off search when button is clicked with mouse", () => {
const dispatch = sinon.spy();
const wrapper = shallowWithIntl(<Search {...DEFAULT_PROPS} handoffEnabled={true} dispatch={dispatch} />);
wrapper.instance().onSearchHandoffClick({clientX: 101, clientY: 102});
assert.calledWith(dispatch, {
data: {hiddenFocus: true},
meta: {from: "ActivityStream:Content", skipLocal: true, to: "ActivityStream:Main"},
type: "HANDOFF_SEARCH_TO_AWESOMEBAR",
});
assert.calledWith(dispatch, {type: "FOCUS_SEARCH"});
});
it("should hand-off search when button is clicked with keyboard", () => {
const dispatch = sinon.spy();
const wrapper = shallowWithIntl(<Search {...DEFAULT_PROPS} handoffEnabled={true} dispatch={dispatch} />);
wrapper.instance().onSearchHandoffClick({clientX: 0, clientY: 0});
assert.calledWith(dispatch, {
data: {hiddenFocus: false},
meta: {from: "ActivityStream:Content", skipLocal: true, to: "ActivityStream:Main"},
type: "HANDOFF_SEARCH_TO_AWESOMEBAR",
});
assert.calledWith(dispatch, {type: "FOCUS_SEARCH"});
});
});
});

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

@ -0,0 +1,118 @@
import {combineReducers, createStore} from "redux";
import {actionTypes as at} from "common/Actions.jsm";
import {DiscoveryStreamFeed} from "lib/DiscoveryStreamFeed.jsm";
import {reducers} from "common/Reducers.jsm";
const CONFIG_PREF_NAME = "browser.newtabpage.activity-stream.discoverystream.config";
describe("DiscoveryStreamFeed", () => {
let feed;
let sandbox;
let configPrefStub;
let fetchStub;
beforeEach(() => {
// Pref
sandbox = sinon.createSandbox();
configPrefStub = sandbox.stub(global.Services.prefs, "getStringPref")
.withArgs(CONFIG_PREF_NAME)
.returns(JSON.stringify({enabled: false, layout_endpoint: "foo.com"}));
// Fetch
fetchStub = sandbox.stub(global, "fetch");
// Feed
feed = new DiscoveryStreamFeed();
feed.store = createStore(combineReducers(reducers));
});
afterEach(() => {
sandbox.restore();
});
describe("#observe", () => {
it("should update state.DiscoveryStream.config when the pref changes", async () => {
configPrefStub.returns(JSON.stringify({enabled: true, layout_endpoint: "foo"}));
feed.observe(null, null, CONFIG_PREF_NAME);
assert.deepEqual(feed.store.getState().DiscoveryStream.config, {enabled: true, layout_endpoint: "foo"});
});
});
describe("#loadCachedData", () => {
it("should fetch data and populate the cache if it is empty", async () => {
const resp = {layout: ["foo", "bar"]};
const fakeCache = {};
sandbox.stub(feed.cache, "get").returns(Promise.resolve(fakeCache));
sandbox.stub(feed.cache, "set").returns(Promise.resolve());
fetchStub.resolves({ok: true, json: () => Promise.resolve(resp)});
await feed.loadCachedData();
assert.calledOnce(fetchStub);
assert.calledWith(feed.cache.set, "layout", resp);
});
});
describe("#onAction: INIT", () => {
it("should be .loaded=false before initialization", () => {
assert.isFalse(feed.loaded);
});
it("should load data, add pref observer, and set .loaded=true if config.enabled is true", async () => {
configPrefStub.returns(JSON.stringify({enabled: true}));
sandbox.stub(feed, "loadCachedData").returns(Promise.resolve());
sandbox.stub(global.Services.prefs, "addObserver");
await feed.onAction({type: at.INIT});
assert.calledOnce(feed.loadCachedData);
assert.calledWith(global.Services.prefs.addObserver, CONFIG_PREF_NAME, feed);
assert.isTrue(feed.loaded);
});
});
describe("#onAction: DISCOVERY_STREAM_CONFIG_CHANGE", () => {
it("should call this.loadCachedData if config.enabled changes to true ", async () => {
sandbox.stub(feed.cache, "set").returns(Promise.resolve());
// First initialize
await feed.onAction({type: at.INIT});
assert.isFalse(feed.loaded);
// force clear cached pref value
feed._prefCache = {};
configPrefStub.returns(JSON.stringify({enabled: true}));
sandbox.stub(feed, "loadCachedData").returns(Promise.resolve());
await feed.onAction({type: at.DISCOVERY_STREAM_CONFIG_CHANGE});
assert.calledOnce(feed.loadCachedData);
assert.isTrue(feed.loaded);
});
it("should call this.loadCachedData if config.enabled changes to true ", async () => {
sandbox.stub(feed.cache, "set").returns(Promise.resolve());
// force clear cached pref value
feed._prefCache = {};
configPrefStub.returns(JSON.stringify({enabled: true}));
await feed.onAction({type: at.INIT});
assert.isTrue(feed.loaded);
feed._prefCache = {};
configPrefStub.returns(JSON.stringify({enabled: false}));
sandbox.stub(feed, "loadCachedData").returns(Promise.resolve());
await feed.onAction({type: at.DISCOVERY_STREAM_CONFIG_CHANGE});
assert.notCalled(feed.loadCachedData);
assert.isFalse(feed.loaded);
});
});
describe("#onAction: UNINIT", () => {
it("should remove pref listeners", async () => {
sandbox.stub(global.Services.prefs, "removeObserver");
await feed.onAction({type: at.UNINIT});
assert.calledWith(global.Services.prefs.removeObserver, CONFIG_PREF_NAME, feed);
});
});
});

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

@ -306,6 +306,95 @@ describe("PlacesFeed", () => {
await feed.saveToPocket(action.data.site, action._target.browser);
assert.notCalled(feed.store.dispatch);
});
it("should call handoffSearchToAwesomebar on HANDOFF_SEARCH_TO_AWESOMEBAR", () => {
const action = {
type: at.HANDOFF_SEARCH_TO_AWESOMEBAR,
data: {hiddenFocus: false},
meta: {fromTarget: {}},
_target: {browser: {ownerGlobal: {gURLBar: {focus: () => {}}}}},
};
sinon.stub(feed, "handoffSearchToAwesomebar");
feed.onAction(action);
assert.calledWith(feed.handoffSearchToAwesomebar, action);
});
});
describe("handoffSearchToAwesomebar", () => {
let fakeUrlBar;
let listeners;
beforeEach(() => {
fakeUrlBar = {
focus: sinon.spy(),
hiddenFocus: sinon.spy(),
removeHiddenFocus: sinon.spy(),
addEventListener: (ev, cb) => {
listeners[ev] = cb;
},
removeEventListener: sinon.spy(),
};
listeners = {};
});
it("should properly handle hiddenFocus=false", () => {
feed.handoffSearchToAwesomebar({
_target: {browser: {ownerGlobal: {gURLBar: fakeUrlBar}}},
data: {hiddenFocus: false},
meta: {fromTarget: {}},
});
assert.calledOnce(fakeUrlBar.focus);
assert.notCalled(fakeUrlBar.hiddenFocus);
assert.calledOnce(feed.store.dispatch);
assert.calledWith(feed.store.dispatch, {
meta: {
from: "ActivityStream:Main",
skipMain: true,
to: "ActivityStream:Content",
toTarget: {},
},
type: "SHOW_SEARCH",
});
});
it("should properly handle hiddenFocus=true", () => {
feed.handoffSearchToAwesomebar({
_target: {browser: {ownerGlobal: {gURLBar: fakeUrlBar}}},
data: {hiddenFocus: true},
meta: {fromTarget: {}},
});
assert.calledOnce(fakeUrlBar.hiddenFocus);
assert.notCalled(fakeUrlBar.focus);
assert.notCalled(feed.store.dispatch);
// Now call keydown listener.
feed.store.dispatch.resetHistory();
listeners.keydown();
assert.calledOnce(fakeUrlBar.removeHiddenFocus);
assert.calledOnce(feed.store.dispatch);
assert.calledWith(feed.store.dispatch, {
meta: {
from: "ActivityStream:Main",
skipMain: true,
to: "ActivityStream:Content",
toTarget: {},
},
type: "HIDE_SEARCH",
});
// And then call blur listener.
fakeUrlBar.removeHiddenFocus.resetHistory();
feed.store.dispatch.resetHistory();
listeners.blur();
assert.calledOnce(fakeUrlBar.removeHiddenFocus);
assert.calledOnce(feed.store.dispatch);
assert.calledWith(feed.store.dispatch, {
meta: {
from: "ActivityStream:Main",
skipMain: true,
to: "ActivityStream:Content",
toTarget: {},
},
type: "SHOW_SEARCH",
});
});
});
describe("#observe", () => {

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

@ -1460,40 +1460,4 @@ describe("Top Stories Feed", () => {
assert.calledOnce(instance.uninit);
assert.calledOnce(instance.init);
});
describe("#layout", () => {
it("should call maybeDispatchLayoutUpdate from fetchStories", async () => {
instance.stories_endpoint = "stories-endpoint";
let fetchStub = globals.sandbox.stub();
const response = {
"layout": [1, 2],
};
globals.set("fetch", fetchStub);
fetchStub.resolves({ok: true, status: 200, json: () => Promise.resolve(response)});
sinon.spy(instance, "maybeDispatchLayoutUpdate");
await instance.fetchStories();
assert.calledOnce(instance.maybeDispatchLayoutUpdate);
assert.calledWith(instance.maybeDispatchLayoutUpdate, [1, 2]);
});
it("should call maybeDispatchLayoutUpdate from loadCachedData", async () => {
sinon.spy(instance, "maybeDispatchLayoutUpdate");
instance.cache.get = () => ({stories: {layout: [2, 3]}});
await instance.loadCachedData();
assert.calledOnce(instance.maybeDispatchLayoutUpdate);
assert.calledWith(instance.maybeDispatchLayoutUpdate, [2, 3]);
});
it("should call dispatch from maybeDispatchLayoutUpdate with available data", () => {
instance.maybeDispatchLayoutUpdate([1, 2]);
assert.calledOnce(instance.store.dispatch);
const [action] = instance.store.dispatch.firstCall.args;
assert.equal(action.type, "CONTENT_LAYOUT");
assert.deepEqual(action.data, [1, 2]);
});
it("should not call dispatch from maybeDispatchLayoutUpdate with no available data", () => {
instance.maybeDispatchLayoutUpdate([]);
assert.notCalled(instance.store.dispatch);
});
});
});

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

@ -1,5 +1,7 @@
[DEFAULT]
prefs = dom.sidebar.enabled=true
prefs =
browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar=false
dom.sidebar.enabled=true
support-files =
426329.xml
483086-1.xml

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

@ -1,6 +1,8 @@
[DEFAULT]
support-files =
head.js
prefs =
browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar=false
[browser_BrowserErrorReporter.js]
skip-if = (verify && !debug && (os == 'mac' || os == 'win'))