├── .nvmrc ├── dist └── .gitignore ├── .eslintignore ├── .prettierrc ├── content-src ├── .eslintrc.js ├── asrouter │ ├── docs │ │ ├── debugging-guide.png │ │ ├── telemetry-screenshot.png │ │ └── targeting-guide.md │ ├── components │ │ ├── ConditionalWrapper │ │ │ └── ConditionalWrapper.jsx │ │ └── Button │ │ │ ├── Button.jsx │ │ │ └── _Button.scss │ ├── template-utils.js │ ├── templates │ │ ├── template-manifest.jsx │ │ ├── FirstRun │ │ │ └── addUtmParams.js │ │ ├── OnboardingMessage │ │ │ ├── UpdateAction.schema.json │ │ │ ├── ToolbarBadgeMessage.schema.json │ │ │ └── OnboardingMessage.jsx │ │ ├── EOYSnippet │ │ │ └── _EOYSnippet.scss │ │ ├── SendToDeviceSnippet │ │ │ └── isEmailOrPhoneNumber.js │ │ ├── FXASignupSnippet │ │ │ └── FXASignupSnippet.jsx │ │ └── NewsletterSnippet │ │ │ └── NewsletterSnippet.jsx │ ├── README.md │ └── rich-text-strings.js ├── components │ ├── DiscoveryStreamComponents │ │ ├── HorizontalRule │ │ │ ├── _HorizontalRule.scss │ │ │ └── HorizontalRule.jsx │ │ ├── DSPrivacyModal │ │ │ ├── _DSPrivacyModal.scss │ │ │ └── DSPrivacyModal.jsx │ │ ├── DSImage │ │ │ └── _DSImage.scss │ │ ├── SectionTitle │ │ │ ├── _SectionTitle.scss │ │ │ └── SectionTitle.jsx │ │ ├── DSLinkMenu │ │ │ └── _DSLinkMenu.scss │ │ ├── Highlights │ │ │ ├── _Highlights.scss │ │ │ └── Highlights.jsx │ │ ├── DSMessage │ │ │ ├── _DSMessage.scss │ │ │ └── DSMessage.jsx │ │ ├── Navigation │ │ │ ├── _Navigation.scss │ │ │ └── Navigation.jsx │ │ ├── DSDismiss │ │ │ └── _DSDismiss.scss │ │ ├── CardGrid │ │ │ └── _CardGrid.scss │ │ ├── DSTextPromo │ │ │ └── _DSTextPromo.scss │ │ ├── SafeAnchor │ │ │ └── SafeAnchor.jsx │ │ ├── DSContextFooter │ │ │ └── DSContextFooter.jsx │ │ └── DSEmptyState │ │ │ └── _DSEmptyState.scss │ ├── DiscoveryStreamImpressionStats │ │ └── _ImpressionStats.scss │ ├── A11yLinkButton │ │ ├── _A11yLinkButton.scss │ │ └── A11yLinkButton.jsx │ ├── Topics │ │ ├── _Topics.scss │ │ └── Topics.jsx │ ├── ErrorBoundary │ │ ├── _ErrorBoundary.scss │ │ └── ErrorBoundary.jsx │ ├── MoreRecommendations │ │ ├── _MoreRecommendations.scss │ │ └── MoreRecommendations.jsx │ ├── Card │ │ └── types.js │ ├── ASRouterAdmin │ │ └── SimpleHashRouter.jsx │ ├── PocketLoggedInCta │ │ ├── _PocketLoggedInCta.scss │ │ └── PocketLoggedInCta.jsx │ ├── TopSites │ │ └── TopSitesConstants.js │ ├── ConfirmDialog │ │ └── _ConfirmDialog.scss │ ├── DiscoveryStreamBase │ │ └── _DiscoveryStreamBase.scss │ ├── ContextMenu │ │ ├── _ContextMenu.scss │ │ └── ContextMenuButton.jsx │ └── FluentOrText │ │ └── FluentOrText.jsx ├── aboutlibrary │ ├── aboutlibrary.jsx │ └── aboutlibrary.scss ├── styles │ ├── _normalize.scss │ ├── activity-stream-linux.scss │ ├── activity-stream-windows.scss │ ├── activity-stream-mac.scss │ └── _mixins.scss ├── lib │ └── constants.js └── activity-stream.jsx ├── data └── content │ ├── assets │ ├── cfr_wiki_search.png │ ├── cfr_fb_container.png │ ├── whatsnew-send-icon.png │ ├── cfr_enhancer_youtube.png │ ├── cfr_google_translate.png │ ├── cfr_pinnedtab_static.png │ ├── illustration-gift@2x.png │ ├── illustration-sync@2x.png │ ├── cfr_pinnedtab_animated.png │ ├── cfr_pinnedtab_static@2x.png │ ├── cfr_reddit_enhancement.png │ ├── illustration-addons@2x.png │ ├── protection-report-icon.png │ ├── trailhead │ │ ├── benefit-sync.png │ │ ├── firefox-logo.png │ │ ├── benefit-privacy.png │ │ ├── firefox-systems.png │ │ ├── accounts-form-bg.jpg │ │ ├── benefit-knowledge.png │ │ ├── benefit-products.png │ │ ├── card-illo-private.svg │ │ ├── card-illo-tracking.svg │ │ └── card-illo-sendtab.svg │ ├── cfr_pinnedtab_animated@2x.png │ ├── illustration-screenshots@2x.png │ ├── cfr_pinnedtab_animated_darktheme.png │ ├── illustration-privatebrowsing@2x.png │ ├── cfr_pinnedtab_animated_darktheme@2x.png │ ├── glyph-add-16.svg │ ├── topic-show-more-12.svg │ ├── glyph-play-12.svg │ ├── glyph-arrowhead-down-12.svg │ ├── glyph-arrowhead-down-16.svg │ ├── glyph-search-16.svg │ ├── glyph-minimize-16.svg │ ├── glyph-dismiss-16.svg │ ├── glyph-arrow.svg │ ├── glyph-caret-right.svg │ ├── glyph-info-16.svg │ ├── glyph-maximize-16.svg │ ├── glyph-pause-12.svg │ ├── glyph-topsites-16.svg │ ├── glyph-cancel-16.svg │ ├── glyph-pocket-delete-16.svg │ ├── glyph-pocket-16.svg │ ├── glyph-newWindow-16.svg │ ├── glyph-pocket-save-16.svg │ ├── glyph-pocket-archive-16.svg │ ├── glyph-trending-16.svg │ ├── glyph-pin-12.svg │ ├── glyph-open-file-16.svg │ ├── glyph-delete-16.svg │ ├── glyph-cfr-feature-16.svg │ ├── glyph-star-17.svg │ ├── glyph-edit-16.svg │ ├── remote │ │ └── pip-message-icon.svg │ ├── glyph-pin-16.svg │ ├── glyph-modal-delete-32.svg │ ├── glyph-unpin-16.svg │ ├── icon-removed-bookmark.svg │ ├── glyph-webextension-16.svg │ ├── glyph-help-24.svg │ ├── firefox-wordmark.svg │ ├── spinner.svg │ └── glyph-highlights-16.svg │ └── tippytop │ └── images │ ├── amazon@2x.png │ ├── bbc-uk@2x.png │ ├── ebay@2x.png │ ├── ok-ru@2x.png │ ├── olx-pl@2x.png │ ├── vk-com@2x.png │ ├── avito-ru@2x.png │ ├── bing-com@2x.png │ ├── wykop-pl@2x.png │ ├── allegro-pl@2x.png │ ├── baidu-com@2x.png │ ├── google-com@2x.png │ ├── reddit-com@2x.png │ ├── twitter-com@2x.png │ ├── yandex-com@2x.png │ ├── youtube-com@2x.png │ ├── facebook-com@2x.png │ ├── leboncoin-fr@2x.png │ ├── wikipedia-org@2x.png │ ├── aliexpress-com@2x.png │ └── duckduckgo-com@2x.png ├── test ├── browser │ ├── red_page.html │ ├── blue_page.html │ ├── browser_enabled_newtabpage.js │ ├── browser_discovery_render.js │ ├── browser.ini │ ├── browser_as_load_location.js │ ├── browser_asrouter_whatsnewpanel.js │ ├── browser_as_render.js │ └── browser_asrouter_snippets.js ├── xpcshell │ ├── xpcshell.ini │ └── test_ASRouterTargeting_attribution.js ├── .eslintrc.js └── unit │ ├── lib │ ├── LinksCache.test.js │ ├── SystemTickFeed.test.js │ └── FilterAdult.test.js │ ├── content-src │ └── components │ │ ├── DiscoveryStreamComponents │ │ ├── HorizontalRule.test.jsx │ │ ├── SectionTitle.test.jsx │ │ ├── Highlights.test.jsx │ │ ├── DSPrivacyModal.test.jsx │ │ ├── CardGrid.test.jsx │ │ ├── DSTextPromo.test.jsx │ │ ├── Navigation.test.jsx │ │ ├── DSDismiss.test.jsx │ │ └── DSMessage.test.jsx │ │ ├── Topics.test.jsx │ │ ├── MoreRecommendations.test.jsx │ │ ├── addUtmParams.test.js │ │ ├── PocketLoggedInCta.test.jsx │ │ ├── TopSites │ │ └── SearchShortcutsForm.test.jsx │ │ └── FluentOrText.test.jsx │ ├── asrouter │ ├── schemas │ │ └── panel │ │ │ └── cfr-fxa-bookmark.schema.test.js │ ├── template-utils.test.js │ ├── compatibility-reference │ │ └── fx57-compat.test.js │ ├── templates │ │ ├── Interrupt.test.jsx │ │ └── isEmailOrPhoneNumber.test.js │ ├── PanelTestProvider.test.js │ └── SnippetsTestMessageProvider.test.js │ └── common │ └── Dedupe.test.js ├── bin ├── bootstrap └── vendor.js ├── aboutlibrary ├── jar.mn ├── moz.build └── content │ └── aboutlibrary.xhtml ├── docs ├── ISSUE_TEMPLATE.md └── v2-system-addon │ ├── test-merges.md │ ├── geo_locale.md │ └── telemetry.md ├── .gitignore ├── hooks ├── post-commit └── pre-commit ├── .mcignore ├── components.conf ├── webpack.aboutlibrary.config.js ├── CODE_OF_CONDUCT.md ├── moz.build ├── .sass-lint.yml ├── .travis.yml ├── .taskcluster.yml ├── vendor ├── PROP_TYPES_LICENSE ├── REDUX_LICENSE ├── REACT_REDUX_LICENSE ├── REACT_AND_REACT_DOM_LICENSE └── REACT_TRANSITION_GROUP_LICENSE ├── common └── Dedupe.jsm ├── lib ├── SystemTickFeed.jsm ├── ASRouterFeed.jsm ├── NewTabInit.jsm └── TippyTopProvider.jsm ├── jar.mn ├── nsIAboutNewTabService.idl ├── README.md ├── loaders └── inject-loader.js └── mochitest.sh /.nvmrc: -------------------------------------------------------------------------------- 1 | 8.16 2 | -------------------------------------------------------------------------------- /dist/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | data/ 2 | logs/ 3 | vendor/ 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /content-src/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | "import/no-commonjs": 2 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /data/content/assets/cfr_wiki_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/cfr_wiki_search.png -------------------------------------------------------------------------------- /data/content/assets/cfr_fb_container.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/cfr_fb_container.png -------------------------------------------------------------------------------- /data/content/assets/whatsnew-send-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/whatsnew-send-icon.png -------------------------------------------------------------------------------- /data/content/tippytop/images/amazon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/tippytop/images/amazon@2x.png -------------------------------------------------------------------------------- /data/content/tippytop/images/bbc-uk@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/tippytop/images/bbc-uk@2x.png -------------------------------------------------------------------------------- /data/content/tippytop/images/ebay@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/tippytop/images/ebay@2x.png -------------------------------------------------------------------------------- /data/content/tippytop/images/ok-ru@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/tippytop/images/ok-ru@2x.png -------------------------------------------------------------------------------- /data/content/tippytop/images/olx-pl@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/tippytop/images/olx-pl@2x.png -------------------------------------------------------------------------------- /data/content/tippytop/images/vk-com@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/tippytop/images/vk-com@2x.png -------------------------------------------------------------------------------- /data/content/assets/cfr_enhancer_youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/cfr_enhancer_youtube.png -------------------------------------------------------------------------------- /data/content/assets/cfr_google_translate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/cfr_google_translate.png -------------------------------------------------------------------------------- /data/content/assets/cfr_pinnedtab_static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/cfr_pinnedtab_static.png -------------------------------------------------------------------------------- /data/content/assets/illustration-gift@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/illustration-gift@2x.png -------------------------------------------------------------------------------- /data/content/assets/illustration-sync@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/illustration-sync@2x.png -------------------------------------------------------------------------------- /data/content/tippytop/images/avito-ru@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/tippytop/images/avito-ru@2x.png -------------------------------------------------------------------------------- /data/content/tippytop/images/bing-com@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/tippytop/images/bing-com@2x.png -------------------------------------------------------------------------------- /data/content/tippytop/images/wykop-pl@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/tippytop/images/wykop-pl@2x.png -------------------------------------------------------------------------------- /content-src/asrouter/docs/debugging-guide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/content-src/asrouter/docs/debugging-guide.png -------------------------------------------------------------------------------- /data/content/assets/cfr_pinnedtab_animated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/cfr_pinnedtab_animated.png -------------------------------------------------------------------------------- /data/content/assets/cfr_pinnedtab_static@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/cfr_pinnedtab_static@2x.png -------------------------------------------------------------------------------- /data/content/assets/cfr_reddit_enhancement.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/cfr_reddit_enhancement.png -------------------------------------------------------------------------------- /data/content/assets/illustration-addons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/illustration-addons@2x.png -------------------------------------------------------------------------------- /data/content/assets/protection-report-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/protection-report-icon.png -------------------------------------------------------------------------------- /data/content/assets/trailhead/benefit-sync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/trailhead/benefit-sync.png -------------------------------------------------------------------------------- /data/content/assets/trailhead/firefox-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/trailhead/firefox-logo.png -------------------------------------------------------------------------------- /data/content/tippytop/images/allegro-pl@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/tippytop/images/allegro-pl@2x.png -------------------------------------------------------------------------------- /data/content/tippytop/images/baidu-com@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/tippytop/images/baidu-com@2x.png -------------------------------------------------------------------------------- /data/content/tippytop/images/google-com@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/tippytop/images/google-com@2x.png -------------------------------------------------------------------------------- /data/content/tippytop/images/reddit-com@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/tippytop/images/reddit-com@2x.png -------------------------------------------------------------------------------- /data/content/tippytop/images/twitter-com@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/tippytop/images/twitter-com@2x.png -------------------------------------------------------------------------------- /data/content/tippytop/images/yandex-com@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/tippytop/images/yandex-com@2x.png -------------------------------------------------------------------------------- /data/content/tippytop/images/youtube-com@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/tippytop/images/youtube-com@2x.png -------------------------------------------------------------------------------- /test/browser/red_page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /data/content/assets/cfr_pinnedtab_animated@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/cfr_pinnedtab_animated@2x.png -------------------------------------------------------------------------------- /data/content/assets/trailhead/benefit-privacy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/trailhead/benefit-privacy.png -------------------------------------------------------------------------------- /data/content/assets/trailhead/firefox-systems.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/trailhead/firefox-systems.png -------------------------------------------------------------------------------- /data/content/tippytop/images/facebook-com@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/tippytop/images/facebook-com@2x.png -------------------------------------------------------------------------------- /data/content/tippytop/images/leboncoin-fr@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/tippytop/images/leboncoin-fr@2x.png -------------------------------------------------------------------------------- /data/content/tippytop/images/wikipedia-org@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/tippytop/images/wikipedia-org@2x.png -------------------------------------------------------------------------------- /test/browser/blue_page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /content-src/asrouter/docs/telemetry-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/content-src/asrouter/docs/telemetry-screenshot.png -------------------------------------------------------------------------------- /data/content/assets/illustration-screenshots@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/illustration-screenshots@2x.png -------------------------------------------------------------------------------- /data/content/assets/trailhead/accounts-form-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/trailhead/accounts-form-bg.jpg -------------------------------------------------------------------------------- /data/content/assets/trailhead/benefit-knowledge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/trailhead/benefit-knowledge.png -------------------------------------------------------------------------------- /data/content/assets/trailhead/benefit-products.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/trailhead/benefit-products.png -------------------------------------------------------------------------------- /data/content/tippytop/images/aliexpress-com@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/tippytop/images/aliexpress-com@2x.png -------------------------------------------------------------------------------- /data/content/tippytop/images/duckduckgo-com@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/tippytop/images/duckduckgo-com@2x.png -------------------------------------------------------------------------------- /data/content/assets/cfr_pinnedtab_animated_darktheme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/cfr_pinnedtab_animated_darktheme.png -------------------------------------------------------------------------------- /data/content/assets/illustration-privatebrowsing@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/illustration-privatebrowsing@2x.png -------------------------------------------------------------------------------- /data/content/assets/cfr_pinnedtab_animated_darktheme@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/activity-stream/HEAD/data/content/assets/cfr_pinnedtab_animated_darktheme@2x.png -------------------------------------------------------------------------------- /bin/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh -x 2 | 3 | # bootstrap an activity-stream repo 4 | ln -s ../../hooks/pre-commit .git/hooks/pre-commit 5 | ln -s ../../hooks/post-commit .git/hooks/post-commit 6 | -------------------------------------------------------------------------------- /content-src/components/DiscoveryStreamComponents/HorizontalRule/_HorizontalRule.scss: -------------------------------------------------------------------------------- 1 | .ds-hr { 2 | @include ds-border-top { 3 | border: 0; 4 | }; 5 | 6 | height: 0; 7 | } 8 | -------------------------------------------------------------------------------- /content-src/components/DiscoveryStreamImpressionStats/_ImpressionStats.scss: -------------------------------------------------------------------------------- 1 | .impression-observer { 2 | position: absolute; 3 | top: 0; 4 | width: 100%; 5 | height: 100%; 6 | pointer-events: none; 7 | } 8 | -------------------------------------------------------------------------------- /test/xpcshell/xpcshell.ini: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | head = 3 | firefox-appdir = browser 4 | skip-if = toolkit == 'android' 5 | 6 | [test_AboutNewTabService.js] 7 | [test_ASRouterTargeting_attribution.js] 8 | skip-if = toolkit != "cocoa" # osx specific tests 9 | -------------------------------------------------------------------------------- /content-src/components/DiscoveryStreamComponents/DSPrivacyModal/_DSPrivacyModal.scss: -------------------------------------------------------------------------------- 1 | .ds-privacy-modal { 2 | a:hover { 3 | text-decoration: underline; 4 | } 5 | 6 | .privacy-notice { 7 | width: 492px; 8 | padding: 40px 0; 9 | margin: auto; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /aboutlibrary/jar.mn: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | browser.jar: 5 | content/browser/ (content/*) 6 | -------------------------------------------------------------------------------- /content-src/components/A11yLinkButton/_A11yLinkButton.scss: -------------------------------------------------------------------------------- 1 | 2 | .a11y-link-button { 3 | border: 0; 4 | padding: 0; 5 | cursor: pointer; 6 | text-align: unset; 7 | color: var(--newtab-link-primary-color); 8 | 9 | &:hover, 10 | &:focus { 11 | text-decoration: underline; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /content-src/aboutlibrary/aboutlibrary.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | class LibraryRouter extends React.PureComponent { 5 | render() { 6 | return
; 7 | } 8 | } 9 | ReactDOM.render(, document.body); 10 | -------------------------------------------------------------------------------- /content-src/aboutlibrary/aboutlibrary.scss: -------------------------------------------------------------------------------- 1 | 2 | .under-construction { 3 | background-image: url('chrome://browser/content/illustrations/under-construction.svg'); 4 | background-repeat: no-repeat; 5 | background-position: center; 6 | min-height: 300px; 7 | min-width: 300px; 8 | margin-top: 10%; 9 | } 10 | -------------------------------------------------------------------------------- /docs/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please file new bugs in Bugzilla: 2 | https://bugzilla.mozilla.org/enter_bug.cgi?product=Firefox&component=Activity%20Streams%3A%20Newtab 3 | 4 | Activity Stream is no longer accepting new issues via GitHub, but Issues are kept open, so old issues can still be viewed. 5 | 6 | Thanks for contributing! 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | .eslintcache 5 | *.sw[po] 6 | *.xpi 7 | *.pyc 8 | logs/ 9 | dist/ 10 | firefox/ 11 | *.update.rdf 12 | data/content/activity-stream.bundle.js 13 | css/*.css 14 | prerendered/ 15 | aboutlibrary/content/aboutlibrary.bundle.js 16 | aboutlibrary/content/*.map 17 | aboutlibrary/content/*.css 18 | -------------------------------------------------------------------------------- /hooks/post-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Clean up any weirdness left around by prettier execution from pre-commit 4 | # hook. Can happen for some workflows (eg `git commit .`). 5 | # 6 | # Install by executing 7 | # 8 | # ln -s ../../hooks/post-commit .git/hooks/post-commit 9 | # 10 | # at the top-level of the activity-stream github repo. 11 | git update-index -g 12 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "assert": true, 7 | "chai": true, 8 | "sinon": true 9 | }, 10 | "rules": { 11 | "func-name-matching": 0, 12 | "import/no-commonjs": 2, 13 | "lines-between-class-members": 0, 14 | "react/jsx-no-bind": 0, 15 | "require-await": 0 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /.mcignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | .DS_Store 3 | *.sw[po] 4 | *.xpi 5 | *.pyc 6 | *.update.rdf 7 | .gitignore 8 | .eslintcache 9 | 10 | /.git/ 11 | /dist/ 12 | /logs/ 13 | /node_modules/ 14 | 15 | # ignore README since it's GitHub specific 16 | /README.md 17 | 18 | # also ignores ping centre tests 19 | ping-centre/ 20 | 21 | # ignore things from about:library for now 22 | aboutlibrary/ 23 | content-src/aboutlibrary/ 24 | -------------------------------------------------------------------------------- /test/unit/lib/LinksCache.test.js: -------------------------------------------------------------------------------- 1 | import { LinksCache } from "lib/LinksCache.jsm"; 2 | 3 | describe("LinksCache", () => { 4 | it("throws when failing request", async () => { 5 | const cache = new LinksCache(); 6 | 7 | let rejected = false; 8 | try { 9 | await cache.request(); 10 | } catch (error) { 11 | rejected = true; 12 | } 13 | 14 | assert(rejected); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /data/content/assets/glyph-add-16.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /data/content/assets/topic-show-more-12.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /data/content/assets/glyph-play-12.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /content-src/components/Topics/_Topics.scss: -------------------------------------------------------------------------------- 1 | .topics { 2 | ul { 3 | margin: 0; 4 | padding: 0; 5 | @media (min-width: $break-point-large) { 6 | display: inline; 7 | padding-inline-start: 12px; 8 | } 9 | } 10 | 11 | ul li { 12 | display: inline-block; 13 | 14 | &::after { 15 | content: '•'; 16 | padding: 8px; 17 | } 18 | 19 | &:last-child::after { 20 | content: none; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /data/content/assets/glyph-arrowhead-down-12.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /aboutlibrary/moz.build: -------------------------------------------------------------------------------- 1 | # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- 2 | # vim: set filetype=python: 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | 7 | JAR_MANIFESTS += ['jar.mn'] 8 | FINAL_LIBRARY = 'browsercomps' 9 | 10 | with Files('**'): 11 | BUG_COMPONENT = ('Firefox', 'Library') 12 | -------------------------------------------------------------------------------- /content-src/components/DiscoveryStreamComponents/HorizontalRule/HorizontalRule.jsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import React from "react"; 6 | 7 | export class HorizontalRule extends React.PureComponent { 8 | render() { 9 | return
; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /data/content/assets/glyph-arrowhead-down-16.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /data/content/assets/glyph-search-16.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /data/content/assets/glyph-minimize-16.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /content-src/styles/_normalize.scss: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | } 4 | 5 | *, 6 | *::before, 7 | *::after { 8 | box-sizing: inherit; 9 | } 10 | 11 | *::-moz-focus-inner { 12 | border: 0; 13 | } 14 | 15 | body { 16 | margin: 0; 17 | } 18 | 19 | button, 20 | input { 21 | background-color: inherit; 22 | color: inherit; 23 | font-family: inherit; 24 | font-size: inherit; 25 | } 26 | 27 | [hidden] { 28 | display: none !important; // sass-lint:disable-line no-important 29 | } 30 | -------------------------------------------------------------------------------- /content-src/components/DiscoveryStreamComponents/DSImage/_DSImage.scss: -------------------------------------------------------------------------------- 1 | .ds-image { 2 | display: block; 3 | position: relative; 4 | opacity: 0; 5 | 6 | &.use-transition { 7 | transition: opacity 0.8s; 8 | } 9 | 10 | &.loaded { 11 | opacity: 1; 12 | } 13 | 14 | img, 15 | .broken-image { 16 | background-color: var(--newtab-card-placeholder-color); 17 | position: absolute; 18 | top: 0; 19 | width: 100%; 20 | height: 100%; 21 | object-fit: cover; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /data/content/assets/glyph-dismiss-16.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /data/content/assets/glyph-arrow.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /content-src/asrouter/components/ConditionalWrapper/ConditionalWrapper.jsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | // lifted from https://gist.github.com/kitze/23d82bb9eb0baabfd03a6a720b1d637f 6 | const ConditionalWrapper = ({ condition, wrap, children }) => 7 | condition ? wrap(children) : children; 8 | 9 | export default ConditionalWrapper; 10 | -------------------------------------------------------------------------------- /data/content/assets/glyph-caret-right.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /data/content/assets/glyph-info-16.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /content-src/styles/activity-stream-linux.scss: -------------------------------------------------------------------------------- 1 | // sass-lint:disable no-css-comments 2 | /* This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | /* This is the linux variant */ 7 | // sass-lint:enable no-css-comments 8 | 9 | $os-infopanel-arrow-height: 10px; 10 | $os-infopanel-arrow-offset-end: 6px; 11 | $os-infopanel-arrow-width: 20px; 12 | 13 | @import './activity-stream'; 14 | -------------------------------------------------------------------------------- /data/content/assets/glyph-maximize-16.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /content-src/styles/activity-stream-windows.scss: -------------------------------------------------------------------------------- 1 | // sass-lint:disable no-css-comments 2 | /* This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | /* This is the windows variant */ 7 | // sass-lint:enable no-css-comments 8 | 9 | $os-infopanel-arrow-height: 10px; 10 | $os-infopanel-arrow-offset-end: 6px; 11 | $os-infopanel-arrow-width: 20px; 12 | 13 | @import './activity-stream'; 14 | -------------------------------------------------------------------------------- /data/content/assets/glyph-pause-12.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /data/content/assets/glyph-topsites-16.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/unit/content-src/components/DiscoveryStreamComponents/HorizontalRule.test.jsx: -------------------------------------------------------------------------------- 1 | import { HorizontalRule } from "content-src/components/DiscoveryStreamComponents/HorizontalRule/HorizontalRule"; 2 | import React from "react"; 3 | import { shallow } from "enzyme"; 4 | 5 | describe("", () => { 6 | let wrapper; 7 | 8 | beforeEach(() => { 9 | wrapper = shallow(); 10 | }); 11 | 12 | it("should render", () => { 13 | assert.ok(wrapper.exists()); 14 | assert.ok(wrapper.find(".ds-hr").exists()); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /data/content/assets/glyph-cancel-16.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /data/content/assets/glyph-pocket-delete-16.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /content-src/components/DiscoveryStreamComponents/SectionTitle/_SectionTitle.scss: -------------------------------------------------------------------------------- 1 | .ds-section-title { 2 | text-align: center; 3 | margin-top: 24px; 4 | 5 | .title { 6 | @include dark-theme-only { 7 | color: $white; 8 | } 9 | 10 | line-height: 48px; 11 | font-size: 36px; 12 | font-weight: 300; 13 | color: $grey-90; 14 | } 15 | 16 | .subtitle { 17 | @include dark-theme-only { 18 | color: $grey-30; 19 | } 20 | 21 | line-height: 24px; 22 | font-size: 14px; 23 | color: $grey-50; 24 | margin-top: 4px; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /data/content/assets/glyph-pocket-16.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /data/content/assets/glyph-newWindow-16.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /components.conf: -------------------------------------------------------------------------------- 1 | # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- 2 | # vim: set filetype=python: 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | 7 | Classes = [ 8 | { 9 | 'cid': '{dfcd2adc-7867-4d3a-ba70-17501f208142}', 10 | 'contract_ids': ['@mozilla.org/browser/aboutnewtab-service;1'], 11 | 'jsm': 'resource:///modules/AboutNewTabService.jsm', 12 | 'constructor': 'AboutNewTabService', 13 | }, 14 | ] 15 | -------------------------------------------------------------------------------- /data/content/assets/glyph-pocket-save-16.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /content-src/styles/activity-stream-mac.scss: -------------------------------------------------------------------------------- 1 | // sass-lint:disable no-css-comments 2 | /* This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | /* This is the mac variant */ 7 | // sass-lint:enable no-css-comments 8 | 9 | $os-infopanel-arrow-height: 10px; 10 | $os-infopanel-arrow-offset-end: 7px; 11 | $os-infopanel-arrow-width: 18px; 12 | 13 | [lwt-newtab-brighttext] { 14 | -moz-osx-font-smoothing: grayscale; 15 | } 16 | 17 | @import './activity-stream'; 18 | -------------------------------------------------------------------------------- /data/content/assets/glyph-pocket-archive-16.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /data/content/assets/glyph-trending-16.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /content-src/components/ErrorBoundary/_ErrorBoundary.scss: -------------------------------------------------------------------------------- 1 | .as-error-fallback { 2 | align-items: center; 3 | border-radius: $border-radius; 4 | box-shadow: inset $inner-box-shadow; 5 | color: var(--newtab-text-conditional-color); 6 | display: flex; 7 | flex-direction: column; 8 | font-size: $error-fallback-font-size; 9 | justify-content: center; 10 | justify-items: center; 11 | line-height: $error-fallback-line-height; 12 | 13 | &.borderless-error { 14 | box-shadow: none; 15 | } 16 | 17 | a { 18 | color: var(--newtab-text-conditional-color); 19 | text-decoration: underline; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /data/content/assets/glyph-pin-12.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /webpack.aboutlibrary.config.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | const path = require("path"); 6 | const config = require("./webpack.system-addon.config.js"); 7 | const absolute = relPath => path.join(__dirname, relPath); 8 | module.exports = Object.assign({}, config(), { 9 | entry: absolute("content-src/aboutlibrary/aboutlibrary.jsx"), 10 | output: { 11 | path: absolute("aboutlibrary/content"), 12 | filename: "aboutlibrary.bundle.js", 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /content-src/components/DiscoveryStreamComponents/DSLinkMenu/_DSLinkMenu.scss: -------------------------------------------------------------------------------- 1 | .ds-hero-item, 2 | .ds-list-item, 3 | .ds-card { 4 | @include context-menu-button; 5 | 6 | .context-menu { 7 | opacity: 0; 8 | } 9 | 10 | &.active { 11 | .context-menu { 12 | opacity: 1; 13 | } 14 | } 15 | 16 | &.last-item { 17 | @include context-menu-open-left; 18 | 19 | .context-menu { 20 | opacity: 1; 21 | } 22 | } 23 | 24 | &:-moz-any(:hover, :focus, .active) { 25 | @include context-menu-button-hover; 26 | outline: none; 27 | 28 | &.ds-card-grid-border { 29 | @include fade-in-card; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /content-src/components/A11yLinkButton/A11yLinkButton.jsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import React from "react"; 6 | 7 | export function A11yLinkButton(props) { 8 | // function for merging classes, if necessary 9 | let className = "a11y-link-button"; 10 | if (props.className) { 11 | className += ` ${props.className}`; 12 | } 13 | return ( 14 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /content-src/components/MoreRecommendations/_MoreRecommendations.scss: -------------------------------------------------------------------------------- 1 | .more-recommendations { 2 | display: flex; 3 | align-items: center; 4 | white-space: nowrap; 5 | line-height: 1.230769231; // (16 / 13) -> 16px computed 6 | 7 | &::after { 8 | background: url('#{$image-path}topic-show-more-12.svg') no-repeat center center; 9 | content: ''; 10 | -moz-context-properties: fill; 11 | display: inline-block; 12 | fill: var(--newtab-link-secondary-color); 13 | height: 16px; 14 | margin-inline-start: 5px; 15 | vertical-align: top; 16 | width: 12px; 17 | } 18 | 19 | &:dir(rtl)::after { 20 | transform: scaleX(-1); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /data/content/assets/glyph-open-file-16.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /content-src/asrouter/template-utils.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | export function safeURI(url) { 6 | if (!url) { 7 | return ""; 8 | } 9 | const { protocol } = new URL(url); 10 | const isAllowed = [ 11 | "http:", 12 | "https:", 13 | "data:", 14 | "resource:", 15 | "chrome:", 16 | ].includes(protocol); 17 | if (!isAllowed) { 18 | console.warn(`The protocol ${protocol} is not allowed for template URLs.`); // eslint-disable-line no-console 19 | } 20 | return isAllowed ? url : ""; 21 | } 22 | -------------------------------------------------------------------------------- /content-src/components/DiscoveryStreamComponents/SectionTitle/SectionTitle.jsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import React from "react"; 6 | 7 | export class SectionTitle extends React.PureComponent { 8 | render() { 9 | const { 10 | header: { title, subtitle }, 11 | } = this.props; 12 | return ( 13 |
14 |
{title}
15 | {subtitle ?
{subtitle}
: null} 16 |
17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /data/content/assets/glyph-delete-16.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /data/content/assets/glyph-cfr-feature-16.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /content-src/components/MoreRecommendations/MoreRecommendations.jsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import React from "react"; 6 | 7 | export class MoreRecommendations extends React.PureComponent { 8 | render() { 9 | const { read_more_endpoint } = this.props; 10 | if (read_more_endpoint) { 11 | return ( 12 | 17 | ); 18 | } 19 | return null; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /data/content/assets/glyph-star-17.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | 10 | 16 | -------------------------------------------------------------------------------- /content-src/lib/constants.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | export const IS_NEWTAB = 6 | global.document && global.document.documentURI === "about:newtab"; 7 | export const NEWTAB_DARK_THEME = { 8 | ntp_background: { 9 | r: 42, 10 | g: 42, 11 | b: 46, 12 | a: 1, 13 | }, 14 | ntp_text: { 15 | r: 249, 16 | g: 249, 17 | b: 250, 18 | a: 1, 19 | }, 20 | sidebar: { 21 | r: 56, 22 | g: 56, 23 | b: 61, 24 | a: 1, 25 | }, 26 | sidebar_text: { 27 | r: 249, 28 | g: 249, 29 | b: 250, 30 | a: 1, 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /data/content/assets/glyph-edit-16.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/unit/content-src/components/DiscoveryStreamComponents/SectionTitle.test.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SectionTitle } from "content-src/components/DiscoveryStreamComponents/SectionTitle/SectionTitle"; 3 | import { shallow } from "enzyme"; 4 | 5 | describe("", () => { 6 | let wrapper; 7 | 8 | beforeEach(() => { 9 | wrapper = shallow(); 10 | }); 11 | 12 | it("should render", () => { 13 | assert.ok(wrapper.exists()); 14 | assert.ok(wrapper.find(".ds-section-title").exists()); 15 | }); 16 | 17 | it("should render a subtitle", () => { 18 | wrapper.setProps({ header: { title: "Foo", subtitle: "Bar" } }); 19 | 20 | assert.equal(wrapper.find(".subtitle").text(), "Bar"); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /data/content/assets/remote/pip-message-icon.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /data/content/assets/glyph-pin-16.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/browser/browser_enabled_newtabpage.js: -------------------------------------------------------------------------------- 1 | function checkSpec(uri, check, message) { 2 | const { spec } = NetUtil.newChannel({ 3 | loadUsingSystemPrincipal: true, 4 | uri, 5 | }).URI; 6 | 7 | info(`got ${spec} for ${uri}`); 8 | check(spec, "about:blank", message); 9 | } 10 | 11 | add_task(async function test_newtab_enabled() { 12 | checkSpec( 13 | "about:newtab", 14 | isnot, 15 | "did not get blank for default about:newtab" 16 | ); 17 | checkSpec("about:home", isnot, "did not get blank for default about:home"); 18 | 19 | await SpecialPowers.pushPrefEnv({ 20 | set: [["browser.newtabpage.enabled", false]], 21 | }); 22 | 23 | checkSpec("about:newtab", is, "got blank when newtab is not enabled"); 24 | checkSpec("about:home", isnot, "still did not get blank for about:home"); 25 | }); 26 | -------------------------------------------------------------------------------- /data/content/assets/glyph-modal-delete-32.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /data/content/assets/glyph-unpin-16.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/unit/content-src/components/Topics.test.jsx: -------------------------------------------------------------------------------- 1 | import { Topic, Topics } from "content-src/components/Topics/Topics"; 2 | import React from "react"; 3 | import { shallow } from "enzyme"; 4 | 5 | describe("", () => { 6 | it("should render a Topics element", () => { 7 | const wrapper = shallow(); 8 | assert.ok(wrapper.exists()); 9 | }); 10 | it("should render a Topic element for each topic with the right url", () => { 11 | const data = [ 12 | { name: "topic1", url: "https://topic1.com" }, 13 | { name: "topic2", url: "https://topic2.com" }, 14 | ]; 15 | 16 | const wrapper = shallow(); 17 | 18 | const topics = wrapper.find(Topic); 19 | assert.lengthOf(topics, 2); 20 | topics.forEach((topic, i) => assert.equal(topic.props().url, data[i].url)); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /moz.build: -------------------------------------------------------------------------------- 1 | # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- 2 | # vim: set filetype=python: 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | 7 | with Files("**"): 8 | BUG_COMPONENT = ("Firefox", "New Tab Page") 9 | 10 | BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini'] 11 | 12 | SPHINX_TREES['docs'] = 'docs' 13 | 14 | XPCSHELL_TESTS_MANIFESTS += [ 15 | 'test/xpcshell/xpcshell.ini', 16 | ] 17 | 18 | XPIDL_SOURCES += [ 19 | 'nsIAboutNewTabService.idl', 20 | ] 21 | 22 | XPIDL_MODULE = 'browser-newtab' 23 | 24 | EXTRA_JS_MODULES += [ 25 | 'AboutNewTabService.jsm', 26 | ] 27 | 28 | XPCOM_MANIFESTS += [ 29 | 'components.conf', 30 | ] 31 | 32 | JAR_MANIFESTS += ['jar.mn'] 33 | -------------------------------------------------------------------------------- /data/content/assets/icon-removed-bookmark.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /.sass-lint.yml: -------------------------------------------------------------------------------- 1 | options: 2 | merge-default-rules: true 3 | max-warnings: 0 4 | 5 | files: 6 | include: 'content-src/**/*.scss' 7 | 8 | rules: 9 | class-name-format: 0 10 | extends-before-declarations: 2 11 | extends-before-mixins: 2 12 | force-element-nesting: 0 13 | force-pseudo-nesting: 0 14 | hex-notation: [2, {style: uppercase}] 15 | indentation: [2, {size: 2}] 16 | leading-zero: [2, {include: true}] 17 | mixins-before-declarations: [2, {exclude: [breakpoint, mq]}] 18 | nesting-depth: [2, {max-depth: 4}] 19 | no-debug: 1 20 | no-disallowed-properties: [1, {properties: [margin-left, margin-right, text-transform]}] 21 | no-duplicate-properties: 2 22 | no-misspelled-properties: [2, {extra-properties: [-moz-context-properties]}] 23 | no-url-domains: 0 24 | no-vendor-prefixes: 0 25 | no-warn: 1 26 | placeholder-in-extend: 2 27 | property-sort-order: 0 28 | -------------------------------------------------------------------------------- /content-src/components/Card/types.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | export const cardContextTypes = { 6 | history: { 7 | fluentID: "newtab-label-visited", 8 | icon: "history-item", 9 | }, 10 | removedBookmark: { 11 | fluentID: "newtab-label-removed-bookmark", 12 | icon: "bookmark-removed", 13 | }, 14 | bookmark: { 15 | fluentID: "newtab-label-bookmarked", 16 | icon: "bookmark-added", 17 | }, 18 | trending: { 19 | fluentID: "newtab-label-recommended", 20 | icon: "trending", 21 | }, 22 | pocket: { 23 | fluentID: "newtab-label-saved", 24 | icon: "pocket", 25 | }, 26 | download: { 27 | fluentID: "newtab-label-download", 28 | icon: "download", 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /data/content/assets/glyph-webextension-16.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /content-src/components/DiscoveryStreamComponents/Highlights/_Highlights.scss: -------------------------------------------------------------------------------- 1 | .ds-highlights { 2 | .section { 3 | margin: 0 (-$section-horizontal-padding); 4 | 5 | .section-list { 6 | grid-gap: var(--gridRowGap); 7 | grid-template-columns: repeat(4, 1fr); 8 | 9 | .card-outer { 10 | $line-height: 20px; 11 | height: 175px; 12 | 13 | .card-host-name { 14 | font-size: 13px; 15 | line-height: $line-height; 16 | margin-bottom: 2px; 17 | padding-bottom: 0; 18 | text-transform: unset; // sass-lint:disable-line no-disallowed-properties 19 | } 20 | 21 | .card-title { 22 | font-size: 14px; 23 | font-weight: 600; 24 | line-height: $line-height; 25 | max-height: $line-height; 26 | } 27 | } 28 | } 29 | } 30 | 31 | .hide-for-narrow { 32 | display: block; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/browser/browser_discovery_render.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | async function before({ pushPrefs }) { 4 | await pushPrefs([ 5 | "browser.newtabpage.activity-stream.discoverystream.config", 6 | JSON.stringify({ 7 | collapsible: true, 8 | enabled: true, 9 | hardcoded_layout: true, 10 | }), 11 | ]); 12 | } 13 | 14 | test_newtab({ 15 | before, 16 | test: async function test_render_hardcoded() { 17 | const topSites = await ContentTaskUtils.waitForCondition(() => 18 | content.document.querySelector(".ds-top-sites") 19 | ); 20 | ok(topSites, "Got the discovery stream top sites section"); 21 | 22 | const learnMore = content.document.querySelector( 23 | ".ds-layout a[href$=new_tab_learn_more]" 24 | ); 25 | is( 26 | learnMore.textContent, 27 | "What’s Pocket?", 28 | "Got the rendered Message with link text and url within discovery stream" 29 | ); 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /content-src/components/DiscoveryStreamComponents/Highlights/Highlights.jsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import { connect } from "react-redux"; 6 | import React from "react"; 7 | import { SectionIntl } from "content-src/components/Sections/Sections"; 8 | 9 | export class _Highlights extends React.PureComponent { 10 | render() { 11 | const section = this.props.Sections.find(s => s.id === "highlights"); 12 | if (!section || !section.enabled) { 13 | return null; 14 | } 15 | 16 | return ( 17 |
18 | 19 |
20 | ); 21 | } 22 | } 23 | 24 | export const Highlights = connect(state => ({ Sections: state.Sections }))( 25 | _Highlights 26 | ); 27 | -------------------------------------------------------------------------------- /test/unit/asrouter/schemas/panel/cfr-fxa-bookmark.schema.test.js: -------------------------------------------------------------------------------- 1 | import schema from "content-src/asrouter/schemas/panel/cfr-fxa-bookmark.schema.json"; 2 | 3 | const DEFAULT_CONTENT = { 4 | title: "Sync your bookmarks everywhere", 5 | text: "Great find! Now don't be left without this bookmark.", 6 | cta: "Sync bookmarks now", 7 | info_icon: { 8 | tooltiptext: "Learn more", 9 | }, 10 | }; 11 | 12 | const L10N_CONTENT = { 13 | title: { string_id: "cfr-bookmark-title" }, 14 | text: { string_id: "cfr-bookmark-body" }, 15 | cta: { string_id: "cfr-bookmark-link-text" }, 16 | info_icon: { 17 | tooltiptext: { string_id: "cfr-bookmark-tooltip-text" }, 18 | }, 19 | }; 20 | 21 | describe("CFR FxA Message Schema", () => { 22 | it("should validate DEFAULT_CONTENT", () => { 23 | assert.jsonSchema(DEFAULT_CONTENT, schema); 24 | }); 25 | it("should validate L10N_CONTENT", () => { 26 | assert.jsonSchema(L10N_CONTENT, schema); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | # when changing this, be sure to edit .nvrmc and package.json too 5 | - 8 6 | 7 | python: 8 | - "2.7" 9 | 10 | addons: 11 | # Run unit tests in Nightly to be in line with what Firefox tests would run against 12 | firefox: "latest-nightly" 13 | 14 | cache: 15 | directories: 16 | - node_modules 17 | 18 | before_install: 19 | # see https://docs.travis-ci.com/user/gui-and-headless-browsers/#Using-xvfb-to-Run-Tests-That-Require-a-GUI 20 | - "export DISPLAY=:99.0" 21 | - "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16 -extension RANDR" 22 | - export PATH="$PATH:$HOME/.rvm/bin" 23 | - export PATH="$PATH:./node_modules/.bin" 24 | - sleep 3 25 | 26 | install: 27 | - npm config set spin false 28 | - npm install 29 | 30 | script: 31 | - npm test 32 | 33 | notifications: 34 | email: false 35 | -------------------------------------------------------------------------------- /content-src/components/Topics/Topics.jsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import React from "react"; 6 | 7 | export class Topic extends React.PureComponent { 8 | render() { 9 | const { url, name } = this.props; 10 | return ( 11 |
  • 12 | 13 | {name} 14 | 15 |
  • 16 | ); 17 | } 18 | } 19 | 20 | export class Topics extends React.PureComponent { 21 | render() { 22 | const { topics } = this.props; 23 | return ( 24 | 25 | 26 |
      27 | {topics && 28 | topics.map(t => )} 29 |
    30 |
    31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /content-src/components/DiscoveryStreamComponents/DSMessage/_DSMessage.scss: -------------------------------------------------------------------------------- 1 | .ds-message { 2 | margin: 8px 0 0; 3 | 4 | .title { 5 | display: flex; 6 | align-items: center; 7 | 8 | .glyph { 9 | @include dark-theme-only { 10 | fill: $grey-30; 11 | } 12 | 13 | width: 16px; 14 | height: 16px; 15 | margin: 0 6px 0 0; 16 | -moz-context-properties: fill; 17 | fill: $grey-50; 18 | background-position: center center; 19 | background-size: 16px; 20 | background-repeat: no-repeat; 21 | } 22 | 23 | .title-text { 24 | @include dark-theme-only { 25 | color: $grey-30; 26 | } 27 | 28 | line-height: 20px; 29 | font-size: 13px; 30 | color: $grey-50; 31 | font-weight: 600; 32 | padding-right: 12px; 33 | } 34 | 35 | .link { 36 | line-height: 20px; 37 | font-size: 13px; 38 | 39 | &:hover, 40 | &:focus { 41 | text-decoration: underline; 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /content-src/asrouter/components/Button/Button.jsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import React from "react"; 6 | 7 | const ALLOWED_STYLE_TAGS = ["color", "backgroundColor"]; 8 | 9 | export const Button = props => { 10 | const style = {}; 11 | 12 | // Add allowed style tags from props, e.g. props.color becomes style={color: props.color} 13 | for (const tag of ALLOWED_STYLE_TAGS) { 14 | if (typeof props[tag] !== "undefined") { 15 | style[tag] = props[tag]; 16 | } 17 | } 18 | // remove border if bg is set to something custom 19 | if (style.backgroundColor) { 20 | style.border = "0"; 21 | } 22 | 23 | return ( 24 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /data/content/assets/glyph-help-24.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/unit/content-src/components/MoreRecommendations.test.jsx: -------------------------------------------------------------------------------- 1 | import { MoreRecommendations } from "content-src/components/MoreRecommendations/MoreRecommendations"; 2 | import React from "react"; 3 | import { shallow } from "enzyme"; 4 | 5 | describe("", () => { 6 | it("should render a MoreRecommendations element", () => { 7 | const wrapper = shallow(); 8 | assert.ok(wrapper.exists()); 9 | }); 10 | it("should render a link when provided with read_more_endpoint prop", () => { 11 | const wrapper = shallow( 12 | 13 | ); 14 | 15 | const link = wrapper.find(".more-recommendations"); 16 | assert.lengthOf(link, 1); 17 | }); 18 | it("should not render a link when provided with read_more_endpoint prop", () => { 19 | const wrapper = shallow(); 20 | 21 | const link = wrapper.find(".more-recommendations"); 22 | assert.lengthOf(link, 0); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/unit/asrouter/template-utils.test.js: -------------------------------------------------------------------------------- 1 | import { safeURI } from "content-src/asrouter/template-utils"; 2 | 3 | describe("safeURI", () => { 4 | let warnStub; 5 | beforeEach(() => { 6 | warnStub = sinon.stub(console, "warn"); 7 | }); 8 | afterEach(() => { 9 | warnStub.restore(); 10 | }); 11 | it("should allow http: URIs", () => { 12 | assert.equal(safeURI("http://foo.com"), "http://foo.com"); 13 | }); 14 | it("should allow https: URIs", () => { 15 | assert.equal(safeURI("https://foo.com"), "https://foo.com"); 16 | }); 17 | it("should allow data URIs", () => { 18 | assert.equal( 19 | safeURI("data:image/png;base64,iVBO"), 20 | "data:image/png;base64,iVBO" 21 | ); 22 | }); 23 | it("should not allow javascript: URIs", () => { 24 | assert.equal(safeURI("javascript:foo()"), ""); // eslint-disable-line no-script-url 25 | assert.calledOnce(warnStub); 26 | }); 27 | it("should not warn if the URL is falsey ", () => { 28 | assert.equal(safeURI(), ""); 29 | assert.notCalled(warnStub); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /content-src/components/DiscoveryStreamComponents/Navigation/_Navigation.scss: -------------------------------------------------------------------------------- 1 | .ds-navigation { 2 | line-height: 32px; 3 | padding: 4px 0; 4 | font-size: 14px; 5 | font-weight: 600; 6 | 7 | &.ds-navigation-centered { 8 | text-align: center; 9 | } 10 | 11 | &.ds-navigation-right-aligned { 12 | text-align: end; 13 | } 14 | 15 | ul { 16 | margin: 0; 17 | padding: 0; 18 | } 19 | 20 | ul li { 21 | display: inline-block; 22 | 23 | &::after { 24 | content: '·'; 25 | padding: 8px; 26 | color: $grey-50; 27 | } 28 | 29 | &:last-child::after { 30 | content: none; 31 | } 32 | 33 | a { 34 | &:hover { 35 | // text-decoration: underline; didn't quite match comps. 36 | border-bottom: 1px solid var(--newtab-link-primary-color); 37 | 38 | &:active { 39 | border-bottom: 1px solid $blue-70; 40 | } 41 | } 42 | 43 | &:active { 44 | color: $blue-70; 45 | } 46 | } 47 | } 48 | 49 | .ds-header { 50 | margin-bottom: 8px; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /data/content/assets/firefox-wordmark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /content-src/asrouter/templates/template-manifest.jsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import { EOYSnippet } from "./EOYSnippet/EOYSnippet"; 6 | import { FXASignupSnippet } from "./FXASignupSnippet/FXASignupSnippet"; 7 | import { NewsletterSnippet } from "./NewsletterSnippet/NewsletterSnippet"; 8 | import { SendToDeviceSnippet } from "./SendToDeviceSnippet/SendToDeviceSnippet"; 9 | import { SimpleBelowSearchSnippet } from "./SimpleBelowSearchSnippet/SimpleBelowSearchSnippet"; 10 | import { SimpleSnippet } from "./SimpleSnippet/SimpleSnippet"; 11 | 12 | // Key names matching schema name of templates 13 | export const SnippetsTemplates = { 14 | simple_snippet: SimpleSnippet, 15 | newsletter_snippet: NewsletterSnippet, 16 | fxa_signup_snippet: FXASignupSnippet, 17 | send_to_device_snippet: SendToDeviceSnippet, 18 | eoy_snippet: EOYSnippet, 19 | simple_below_search_snippet: SimpleBelowSearchSnippet, 20 | }; 21 | -------------------------------------------------------------------------------- /.taskcluster.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | policy: 3 | pullRequests: public 4 | tasks: 5 | $if: 'tasks_for in ["github-push", "github-pull-request"]' 6 | then: 7 | $let: 8 | repo_url: 9 | $if: 'tasks_for == "github-push"' 10 | then: ${event.repository.clone_url} 11 | else: ${event.pull_request.head.repo.clone_url} 12 | ref: 13 | $if: 'tasks_for == "github-push"' 14 | then: ${event.after} 15 | else: ${event.pull_request.head.sha} 16 | in: 17 | - provisionerId: proj-misc 18 | workerType: ci 19 | deadline: ${fromNow('1 day')} 20 | payload: 21 | maxRunTime: 7200 22 | image: piatra/asmochitests 23 | command: 24 | - /bin/bash 25 | - '--login' 26 | - '-c' 27 | - >- 28 | git clone ${repo_url} /activity-stream && cd /activity-stream && 29 | git checkout ${ref} && bash ./mochitest.sh 30 | metadata: 31 | name: activitystream 32 | description: run mochitests for PRs 33 | owner: noreply@mozilla.com 34 | source: ${repo_url} 35 | -------------------------------------------------------------------------------- /content-src/components/ASRouterAdmin/SimpleHashRouter.jsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import React from "react"; 6 | 7 | export class SimpleHashRouter extends React.PureComponent { 8 | constructor(props) { 9 | super(props); 10 | this.onHashChange = this.onHashChange.bind(this); 11 | this.state = { hash: global.location.hash }; 12 | } 13 | 14 | onHashChange() { 15 | this.setState({ hash: global.location.hash }); 16 | } 17 | 18 | componentWillMount() { 19 | global.addEventListener("hashchange", this.onHashChange); 20 | } 21 | 22 | componentWillUnmount() { 23 | global.removeEventListener("hashchange", this.onHashChange); 24 | } 25 | 26 | render() { 27 | const [, ...routes] = this.state.hash.split("-"); 28 | return React.cloneElement(this.props.children, { 29 | location: { 30 | hash: this.state.hash, 31 | routes, 32 | }, 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /content-src/components/PocketLoggedInCta/_PocketLoggedInCta.scss: -------------------------------------------------------------------------------- 1 | .pocket-logged-in-cta { 2 | $max-button-width: 130px; 3 | $min-button-height: 18px; 4 | font-size: 13px; 5 | margin-inline-end: 20px; 6 | display: flex; 7 | align-items: flex-start; 8 | 9 | .pocket-cta-button { 10 | white-space: nowrap; 11 | background: $blue-60; 12 | letter-spacing: -0.34px; 13 | color: $white; 14 | border-radius: 4px; 15 | cursor: pointer; 16 | max-width: $max-button-width; 17 | // The button height is 2px taller than the rest of the cta text. 18 | // So I move it up by 1px to align with the rest of the cta text. 19 | margin-top: -1px; 20 | min-height: $min-button-height; 21 | padding: 0 8px; 22 | display: inline-flex; 23 | justify-content: center; 24 | align-items: center; 25 | font-size: 11px; 26 | margin-inline-end: 10px; 27 | } 28 | 29 | .cta-text { 30 | font-weight: normal; 31 | font-size: 13px; 32 | line-height: 1.230769231; // (16 / 13) –> 16px computed 33 | } 34 | 35 | .pocket-cta-button, 36 | .cta-text { 37 | vertical-align: top; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /content-src/asrouter/docs/targeting-guide.md: -------------------------------------------------------------------------------- 1 | # Guide to targeting with JEXL 2 | 3 | For a more in-depth explanation of JEXL syntax you can read the [Normady project docs](https://mozilla.github.io/normandy/user/filters.html?highlight=jexl). 4 | 5 | ### How to write JEXL targeting expressions 6 | A message needs to contain the `targeting` property (JEXL string) which is evaluated against the provided attributes. 7 | Examples: 8 | 9 | ```javascript 10 | { 11 | "id": "7864", 12 | "content": {...}, 13 | // simple equality check 14 | "targeting": "usesFirefoxSync == true" 15 | } 16 | 17 | { 18 | "id": "7865", 19 | "content": {...}, 20 | // using JEXL transforms and combining two attributes 21 | "targeting": "usesFirefoxSync == true && profileAgeCreated > '2018-01-07'|date" 22 | } 23 | 24 | { 25 | "id": "7866", 26 | "content": {...}, 27 | // targeting addon information 28 | "targeting": "addonsInfo.addons['activity-stream@mozilla.org'].name == 'Activity Stream'" 29 | } 30 | 31 | { 32 | "id": "7866", 33 | "content": {...}, 34 | // targeting based on time 35 | "targeting": "currentDate > '2018-08-08'|date" 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /content-src/asrouter/templates/FirstRun/addUtmParams.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | /** 6 | * BASE_PARAMS keys/values can be modified from outside this file 7 | */ 8 | export const BASE_PARAMS = { 9 | utm_source: "activity-stream", 10 | utm_campaign: "firstrun", 11 | utm_medium: "referral", 12 | }; 13 | 14 | /** 15 | * Takes in a url as a string or URL object and returns a URL object with the 16 | * utm_* parameters added to it. If a URL object is passed in, the paraemeters 17 | * are added to it (the return value can be ignored in that case as it's the 18 | * same object). 19 | */ 20 | export function addUtmParams(url, utmTerm) { 21 | let returnUrl = url; 22 | if (typeof returnUrl === "string") { 23 | returnUrl = new URL(url); 24 | } 25 | Object.keys(BASE_PARAMS).forEach(key => { 26 | returnUrl.searchParams.append(key, BASE_PARAMS[key]); 27 | }); 28 | returnUrl.searchParams.append("utm_term", utmTerm); 29 | return returnUrl; 30 | } 31 | -------------------------------------------------------------------------------- /test/unit/content-src/components/addUtmParams.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | addUtmParams, 3 | BASE_PARAMS, 4 | } from "content-src/asrouter/templates/FirstRun/addUtmParams"; 5 | 6 | describe("addUtmParams", () => { 7 | it("should convert a string URL", () => { 8 | const result = addUtmParams("https://foo.com", "foo"); 9 | assert.equal(result.hostname, "foo.com"); 10 | }); 11 | it("should add all base params", () => { 12 | assert.match( 13 | addUtmParams(new URL("https://foo.com"), "foo").toString(), 14 | /utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral/ 15 | ); 16 | }); 17 | it("should allow updating base params utm values", () => { 18 | BASE_PARAMS.utm_campaign = "firstrun-default"; 19 | assert.match( 20 | addUtmParams(new URL("https://foo.com"), "foo", "default").toString(), 21 | /utm_source=activity-stream&utm_campaign=firstrun-default&utm_medium=referral/ 22 | ); 23 | }); 24 | it("should add utm_term", () => { 25 | const params = addUtmParams(new URL("https://foo.com"), "foo").searchParams; 26 | assert.equal(params.get("utm_term"), "foo", "utm_term"); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /vendor/PROP_TYPES_LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2013-present, Facebook, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vendor/REDUX_LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Dan Abramov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vendor/REACT_REDUX_LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Dan Abramov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/unit/asrouter/compatibility-reference/fx57-compat.test.js: -------------------------------------------------------------------------------- 1 | import EOYSnippetSchema from "content-src/asrouter/templates/EOYSnippet/EOYSnippet.schema.json"; 2 | import { expectedValues } from "./snippets-fx57"; 3 | import SimpleSnippetSchema from "content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json"; 4 | import SubmitFormSchema from "content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.schema.json"; 5 | 6 | export const SnippetsSchemas = { 7 | eoy_snippet: EOYSnippetSchema, 8 | simple_snippet: SimpleSnippetSchema, 9 | newsletter_snippet: SubmitFormSchema, 10 | fxa_signup_snippet: SubmitFormSchema, 11 | send_to_device_snippet: SubmitFormSchema, 12 | }; 13 | 14 | describe("Firefox 57 compatibility test", () => { 15 | Object.keys(expectedValues).forEach(template => { 16 | describe(template, () => { 17 | const schema = SnippetsSchemas[template]; 18 | it(`should have a schema for ${template}`, () => { 19 | assert.ok(schema); 20 | }); 21 | it(`should validate with the schema for ${template}`, () => { 22 | assert.jsonSchema(expectedValues[template], schema); 23 | }); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /vendor/REACT_AND_REACT_DOM_LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Facebook, Inc. and its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Recommended pre-commit git hook for activity-stream github repo 4 | # 5 | # Install by executing 6 | # 7 | # ln -s ../../hooks/pre-commit .git/hooks/pre-commit 8 | # 9 | # at the top-level of the activity-stream github repo. 10 | # 11 | # Runs `eslint --fix` on all selected files, which, given our current 12 | # prettier configuration, means prettifying these files as well. The 13 | # commit will be aborted if eslint exits with a failure code. 14 | # 15 | # Based on the example script in the prettier docs at 16 | # https://prettier.io/docs/en/precommit.html 17 | 18 | FILES=$(git diff --cached --name-only --diff-filter=ACM "*.js" "*.jsx" "*.jsm" | sed 's| |\\ |g') 19 | [ -z "$FILES" ] && exit 0 20 | 21 | echo "$FILES" | xargs ./node_modules/.bin/eslint --cache --fix 22 | if [ $? -ne 0 ] 23 | then 24 | echo "eslint found errors but was unable to fix them all with --fix." 25 | echo "Please check the output, resolve any issues, and retry." 26 | echo "If you want to commit anyway, pass the --no-verify flag to git commit." 27 | exit -1 28 | fi 29 | 30 | # Add back the modified/prettified files to staging 31 | echo "$FILES" | xargs git add 32 | 33 | exit 0 34 | -------------------------------------------------------------------------------- /content-src/activity-stream.jsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm"; 6 | import { Base } from "content-src/components/Base/Base"; 7 | import { DetectUserSessionStart } from "content-src/lib/detect-user-session-start"; 8 | import { initStore } from "content-src/lib/init-store"; 9 | import { Provider } from "react-redux"; 10 | import React from "react"; 11 | import ReactDOM from "react-dom"; 12 | import { reducers } from "common/Reducers.jsm"; 13 | 14 | const store = initStore(reducers); 15 | 16 | new DetectUserSessionStart(store).sendEventOrAddListener(); 17 | 18 | store.dispatch(ac.AlsoToMain({ type: at.NEW_TAB_STATE_REQUEST })); 19 | 20 | ReactDOM.hydrate( 21 | 22 | 27 | , 28 | document.getElementById("root") 29 | ); 30 | -------------------------------------------------------------------------------- /test/browser/browser.ini: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | support-files = 3 | blue_page.html 4 | red_page.html 5 | head.js 6 | prefs = 7 | browser.newtabpage.activity-stream.debug=false 8 | browser.newtabpage.activity-stream.discoverystream.enabled=true 9 | browser.newtabpage.activity-stream.discoverystream.endpoints=data: 10 | browser.newtabpage.activity-stream.feeds.section.topstories=true 11 | browser.newtabpage.activity-stream.feeds.section.topstories.options={"provider_name":""} 12 | 13 | [browser_aboutwelcome.js] 14 | [browser_as_load_location.js] 15 | [browser_as_render.js] 16 | [browser_asrouter_snippets.js] 17 | [browser_asrouter_targeting.js] 18 | [browser_asrouter_trigger_listeners.js] 19 | [browser_discovery_render.js] 20 | [browser_discovery_styles.js] 21 | [browser_enabled_newtabpage.js] 22 | [browser_highlights_section.js] 23 | [browser_getScreenshots.js] 24 | [browser_newtab_overrides.js] 25 | [browser_onboarding_rtamo.js] 26 | skip-if = (os == "linux") # Test setup only implemented for OSX and Windows 27 | [browser_topsites_contextMenu_options.js] 28 | [browser_topsites_section.js] 29 | [browser_asrouter_cfr.js] 30 | [browser_asrouter_bookmarkpanel.js] 31 | [browser_asrouter_toolbarbadge.js] 32 | [browser_asrouter_whatsnewpanel.js] 33 | -------------------------------------------------------------------------------- /test/unit/common/Dedupe.test.js: -------------------------------------------------------------------------------- 1 | import { Dedupe } from "common/Dedupe.jsm"; 2 | 3 | describe("Dedupe", () => { 4 | let instance; 5 | beforeEach(() => { 6 | instance = new Dedupe(); 7 | }); 8 | describe("group", () => { 9 | it("should remove duplicates inside the groups", () => { 10 | const beforeItems = [[1, 1, 1], [2, 2, 2], [3, 3, 3]]; 11 | const afterItems = [[1], [2], [3]]; 12 | assert.deepEqual(instance.group(...beforeItems), afterItems); 13 | }); 14 | it("should remove duplicates between groups, favouring earlier groups", () => { 15 | const beforeItems = [[1, 2, 3], [2, 3, 4], [3, 4, 5]]; 16 | const afterItems = [[1, 2, 3], [4], [5]]; 17 | assert.deepEqual(instance.group(...beforeItems), afterItems); 18 | }); 19 | it("should remove duplicates from groups of objects", () => { 20 | instance = new Dedupe(item => item.id); 21 | const beforeItems = [ 22 | [{ id: 1 }, { id: 1 }, { id: 2 }], 23 | [{ id: 1 }, { id: 3 }, { id: 2 }], 24 | [{ id: 1 }, { id: 2 }, { id: 5 }], 25 | ]; 26 | const afterItems = [[{ id: 1 }, { id: 2 }], [{ id: 3 }], [{ id: 5 }]]; 27 | assert.deepEqual(instance.group(...beforeItems), afterItems); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /content-src/asrouter/templates/OnboardingMessage/UpdateAction.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "UpdateActionMessage", 3 | "description": "A template for messages that execute predetermined actions.", 4 | "version": "1.0.0", 5 | "type": "object", 6 | "properties": { 7 | "action": { 8 | "type": "object", 9 | "properties": { 10 | "id": { 11 | "type": "string" 12 | }, 13 | "data": { 14 | "type": "object", 15 | "properties": { 16 | "url": { 17 | "type": "string", 18 | "description": "URL data to be used as argument to the action" 19 | }, 20 | "expireDelta": { 21 | "type": "number", 22 | "description": "Expiration timestamp to be used as argument to the action" 23 | } 24 | } 25 | }, 26 | "description": "Additional data provided as argument when executing the action" 27 | }, 28 | "additionalProperties": false, 29 | "description": "Optional action to take in addition to showing the notification" 30 | }, 31 | "additionalProperties": false, 32 | "required": ["id", "action"] 33 | }, 34 | "additionalProperties": false, 35 | "required": ["action"] 36 | } 37 | -------------------------------------------------------------------------------- /content-src/components/TopSites/TopSitesConstants.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | export const TOP_SITES_SOURCE = "TOP_SITES"; 6 | export const TOP_SITES_CONTEXT_MENU_OPTIONS = [ 7 | "CheckPinTopSite", 8 | "EditTopSite", 9 | "Separator", 10 | "OpenInNewWindow", 11 | "OpenInPrivateWindow", 12 | "Separator", 13 | "BlockUrl", 14 | "DeleteUrl", 15 | ]; 16 | export const TOP_SITES_SPOC_CONTEXT_MENU_OPTIONS = [ 17 | "PinSpocTopSite", 18 | "Separator", 19 | "OpenInNewWindow", 20 | "OpenInPrivateWindow", 21 | "Separator", 22 | "BlockUrl", 23 | "ShowPrivacyInfo", 24 | ]; 25 | // the special top site for search shortcut experiment can only have the option to unpin (which removes) the topsite 26 | export const TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS = [ 27 | "CheckPinTopSite", 28 | "Separator", 29 | "BlockUrl", 30 | ]; 31 | // minimum size necessary to show a rich icon instead of a screenshot 32 | export const MIN_RICH_FAVICON_SIZE = 96; 33 | // minimum size necessary to show any icon in the top left corner with a screenshot 34 | export const MIN_CORNER_FAVICON_SIZE = 16; 35 | -------------------------------------------------------------------------------- /data/content/assets/spinner.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /docs/v2-system-addon/test-merges.md: -------------------------------------------------------------------------------- 1 | ## bin/test-merges.js documentation 2 | 3 | A script intended to be run from cron regularly. It notices when a new PR has been merged to github, and then exports the code to a copy of mozilla-central and pushes it to pine, so that all the tests can be run. It annotates the PR with the link to treeherder with the test results. 4 | 5 | Setup, needs to happen before first run: 6 | 7 | Ensure that mozilla/activity-stream has a label called pushed-to-pine. 8 | 9 | ```bash 10 | # mkdir /home/monkey/as-pine-testing 11 | # cd /home/monkey/as-pine-testing 12 | # git clone https://github.com/mozilla/activity-stream.git 13 | # npm install 14 | ``` 15 | 16 | Example usage: 17 | 18 | ```bash 19 | AS_PINE_TOKEN=01234567890 \ 20 | AS_PINE_TEST_DIR=/home/monkey/as-pine-testing \ 21 | node bin/test-merges.js 22 | ``` 23 | 24 | AS_PINE_TOKEN is a github token for accessing mozilla/activity-stream. We use a token from the github user that has access to the mozilla/activity-stream repo (in order to label issues), and nothing else. 25 | 26 | AS_PINE_TEST_DIR should be a single directory which will contain local copies of both the activity-stream github repo and mozilla-central. It's highly advised that AS_PINE_TEST_DIR be used for nothing else, to avoid accidentally clobbering real work. 27 | -------------------------------------------------------------------------------- /aboutlibrary/content/aboutlibrary.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | Library 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /common/Dedupe.jsm: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | this.Dedupe = class Dedupe { 6 | constructor(createKey) { 7 | this.createKey = createKey || this.defaultCreateKey; 8 | } 9 | 10 | defaultCreateKey(item) { 11 | return item; 12 | } 13 | 14 | /** 15 | * Dedupe any number of grouped elements favoring those from earlier groups. 16 | * 17 | * @param {Array} groups Contains an arbitrary number of arrays of elements. 18 | * @returns {Array} A matching array of each provided group deduped. 19 | */ 20 | group(...groups) { 21 | const globalKeys = new Set(); 22 | const result = []; 23 | for (const values of groups) { 24 | const valueMap = new Map(); 25 | for (const value of values) { 26 | const key = this.createKey(value); 27 | if (!globalKeys.has(key) && !valueMap.has(key)) { 28 | valueMap.set(key, value); 29 | } 30 | } 31 | result.push(valueMap); 32 | valueMap.forEach((value, key) => globalKeys.add(key)); 33 | } 34 | return result.map(m => Array.from(m.values())); 35 | } 36 | }; 37 | 38 | const EXPORTED_SYMBOLS = ["Dedupe"]; 39 | -------------------------------------------------------------------------------- /content-src/asrouter/templates/EOYSnippet/_EOYSnippet.scss: -------------------------------------------------------------------------------- 1 | .EOYSnippetForm { 2 | margin: 10px 0 8px; 3 | align-self: start; 4 | font-size: 14px; 5 | display: flex; 6 | align-items: center; 7 | 8 | .donation-amount, 9 | .donation-form-url { 10 | white-space: nowrap; 11 | font-size: 14px; 12 | padding: 8px 20px; 13 | border-radius: 2px; 14 | } 15 | 16 | .donation-amount { 17 | color: $grey-90; 18 | margin-inline-end: 18px; 19 | border: 1px solid $grey-40; 20 | padding: 5px 14px; 21 | background: $grey-10; 22 | cursor: pointer; 23 | } 24 | 25 | input { 26 | &[type='radio'] { 27 | opacity: 0; 28 | margin-inline-end: -18px; 29 | 30 | &:checked + .donation-amount { 31 | background: $grey-50; 32 | color: $white; 33 | border: 1px solid $grey-60; 34 | } 35 | 36 | // accessibility 37 | &:checked:focus + .donation-amount, 38 | &:not(:checked):focus + .donation-amount { 39 | border: 1px dotted var(--newtab-link-primary-color); 40 | } 41 | } 42 | } 43 | 44 | .monthly-checkbox-container { 45 | display: flex; 46 | width: 100%; 47 | } 48 | 49 | .donation-form-url { 50 | margin-inline-start: 18px; 51 | align-self: flex-end; 52 | display: flex; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/SystemTickFeed.jsm: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | "use strict"; 5 | 6 | const { actionTypes: at } = ChromeUtils.import( 7 | "resource://activity-stream/common/Actions.jsm" 8 | ); 9 | 10 | ChromeUtils.defineModuleGetter( 11 | this, 12 | "setInterval", 13 | "resource://gre/modules/Timer.jsm" 14 | ); 15 | ChromeUtils.defineModuleGetter( 16 | this, 17 | "clearInterval", 18 | "resource://gre/modules/Timer.jsm" 19 | ); 20 | 21 | // Frequency at which SYSTEM_TICK events are fired 22 | const SYSTEM_TICK_INTERVAL = 5 * 60 * 1000; 23 | 24 | this.SystemTickFeed = class SystemTickFeed { 25 | init() { 26 | this.intervalId = setInterval( 27 | () => this.store.dispatch({ type: at.SYSTEM_TICK }), 28 | SYSTEM_TICK_INTERVAL 29 | ); 30 | } 31 | 32 | onAction(action) { 33 | switch (action.type) { 34 | case at.INIT: 35 | this.init(); 36 | break; 37 | case at.UNINIT: 38 | clearInterval(this.intervalId); 39 | break; 40 | } 41 | } 42 | }; 43 | 44 | this.SYSTEM_TICK_INTERVAL = SYSTEM_TICK_INTERVAL; 45 | const EXPORTED_SYMBOLS = ["SystemTickFeed", "SYSTEM_TICK_INTERVAL"]; 46 | -------------------------------------------------------------------------------- /test/unit/content-src/components/DiscoveryStreamComponents/Highlights.test.jsx: -------------------------------------------------------------------------------- 1 | import { combineReducers, createStore } from "redux"; 2 | import { INITIAL_STATE, reducers } from "common/Reducers.jsm"; 3 | import { Highlights } from "content-src/components/DiscoveryStreamComponents/Highlights/Highlights"; 4 | import { mount } from "enzyme"; 5 | import { Provider } from "react-redux"; 6 | import React from "react"; 7 | 8 | describe("Discovery Stream ", () => { 9 | let wrapper; 10 | 11 | afterEach(() => { 12 | wrapper.unmount(); 13 | }); 14 | 15 | it("should render nothing with no highlights data", () => { 16 | const store = createStore(combineReducers(reducers), { ...INITIAL_STATE }); 17 | 18 | wrapper = mount( 19 | 20 | 21 | 22 | ); 23 | 24 | assert.ok(wrapper.isEmptyRender()); 25 | }); 26 | 27 | it("should render highlights", () => { 28 | const store = createStore(combineReducers(reducers), { 29 | ...INITIAL_STATE, 30 | Sections: [{ id: "highlights", enabled: true }], 31 | }); 32 | 33 | wrapper = mount( 34 | 35 | 36 | 37 | ); 38 | 39 | assert.lengthOf(wrapper.find(".ds-highlights"), 1); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /content-src/components/DiscoveryStreamComponents/DSMessage/DSMessage.jsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import React from "react"; 6 | import { SafeAnchor } from "../SafeAnchor/SafeAnchor"; 7 | import { FluentOrText } from "content-src/components/FluentOrText/FluentOrText"; 8 | 9 | export class DSMessage extends React.PureComponent { 10 | render() { 11 | return ( 12 |
    13 |
    14 | {this.props.icon && ( 15 |
    19 | )} 20 | {this.props.title && ( 21 | 22 | 23 | 24 | )} 25 | {this.props.link_text && this.props.link_url && ( 26 | 27 | 28 | 29 | )} 30 |
    31 |
    32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /content-src/asrouter/templates/OnboardingMessage/ToolbarBadgeMessage.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "ToolbarBadgeMessage", 3 | "description": "A template that specifies to which element in the browser toolbar to add a notification.", 4 | "version": "1.1.0", 5 | "type": "object", 6 | "properties": { 7 | "target": { 8 | "type": "string" 9 | }, 10 | "action": { 11 | "type": "object", 12 | "properties": { 13 | "id": { 14 | "type": "string" 15 | } 16 | }, 17 | "additionalProperties": false, 18 | "required": ["id"], 19 | "description": "Optional action to take in addition to showing the notification" 20 | }, 21 | "delay": { 22 | "type": "number", 23 | "description": "Optional delay in ms after which to show the notification" 24 | }, 25 | "badgeDescription": { 26 | "type": "object", 27 | "description": "This is used in combination with the badged button to offer a text based alternative to the visual badging. Example 'New Feature: What's New'", 28 | "properties": { 29 | "string_id": { 30 | "type": "string", 31 | "description": "Fluent string id" 32 | } 33 | }, 34 | "required": ["string_id"] 35 | } 36 | }, 37 | "additionalProperties": false, 38 | "required": ["target"] 39 | } 40 | -------------------------------------------------------------------------------- /docs/v2-system-addon/geo_locale.md: -------------------------------------------------------------------------------- 1 | # Setting custom `geo`, `locale`, and update channels 2 | 3 | There are instances where you may need to change your local build's locale, geo, and update channel (such as changes to the visibility of Discovery Stream on a per-geo/locale basis in `ActivityStream.jsm`). 4 | 5 | ## Changing update channel 6 | 7 | - Change `app.update.channel` to desired value (eg: `release`) by editing `LOCAL_BUILD/Contents/Resources/defaults/pref/channel-prefs.js`. (**NOTE:** Changing pref `app.update.channel` from `about:config` seems to have no effect!) 8 | 9 | ## Changing geo 10 | 11 | - Set `browser.search.region` to desired geo (eg `CA`) 12 | 13 | ## Changing locale 14 | 15 | *Note: These prefs are only configurable on a nightly or local build.* 16 | 17 | - Toggle `extensions.langpacks.signatures.required` to `false` 18 | - Toggle `xpinstall.signatures.required` to `false` 19 | - Toggle `intl.multilingual.downloadEnabled` to `true` 20 | - Toggle `intl.multilingual.enabled` to `true` 21 | - Open the [langpack](https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central-l10n/mac/xpi/) for target locale in your local build (eg `firefox-70.0a1.en-CA.langpack.xpi` if you want an `en-CA` locale) 22 | - In `about:preferences` click "Set Alternatives" under "Language", move desired locale to the top position, click OK, click "Apply And Restart" 23 | -------------------------------------------------------------------------------- /content-src/components/ConfirmDialog/_ConfirmDialog.scss: -------------------------------------------------------------------------------- 1 | .confirmation-dialog { 2 | .modal { 3 | box-shadow: 0 2px 2px 0 $black-10; 4 | left: 0; 5 | margin: auto; 6 | position: fixed; 7 | right: 0; 8 | top: 20%; 9 | width: 400px; 10 | } 11 | 12 | section { 13 | margin: 0; 14 | } 15 | 16 | .modal-message { 17 | display: flex; 18 | padding: 16px; 19 | padding-bottom: 0; 20 | 21 | p { 22 | margin: 0; 23 | margin-bottom: 16px; 24 | } 25 | } 26 | 27 | .actions { 28 | border: 0; 29 | display: flex; 30 | flex-wrap: nowrap; 31 | padding: 0 16px; 32 | 33 | button { 34 | margin-inline-end: 16px; 35 | padding-inline-end: 18px; 36 | padding-inline-start: 18px; 37 | white-space: normal; 38 | width: 50%; 39 | 40 | &.done { 41 | margin-inline-end: 0; 42 | margin-inline-start: 0; 43 | } 44 | } 45 | } 46 | 47 | .icon { 48 | margin-inline-end: 16px; 49 | } 50 | } 51 | 52 | .modal-overlay { 53 | background: var(--newtab-overlay-color); 54 | height: 100%; 55 | left: 0; 56 | position: fixed; 57 | top: 0; 58 | width: 100%; 59 | z-index: 11001; 60 | } 61 | 62 | .modal { 63 | background: var(--newtab-modal-color); 64 | border: $border-secondary; 65 | border-radius: 5px; 66 | font-size: 15px; 67 | z-index: 11002; 68 | } 69 | -------------------------------------------------------------------------------- /test/unit/lib/SystemTickFeed.test.js: -------------------------------------------------------------------------------- 1 | import { SYSTEM_TICK_INTERVAL, SystemTickFeed } from "lib/SystemTickFeed.jsm"; 2 | import { actionTypes as at } from "common/Actions.jsm"; 3 | 4 | describe("System Tick Feed", () => { 5 | let instance; 6 | let clock; 7 | 8 | beforeEach(() => { 9 | clock = sinon.useFakeTimers(); 10 | 11 | instance = new SystemTickFeed(); 12 | instance.store = { 13 | getState() { 14 | return {}; 15 | }, 16 | dispatch() {}, 17 | }; 18 | }); 19 | afterEach(() => { 20 | clock.restore(); 21 | }); 22 | it("should create a SystemTickFeed", () => { 23 | assert.instanceOf(instance, SystemTickFeed); 24 | }); 25 | it("should fire SYSTEM_TICK events at configured interval", () => { 26 | let expectation = sinon 27 | .mock(instance.store) 28 | .expects("dispatch") 29 | .twice() 30 | .withExactArgs({ type: at.SYSTEM_TICK }); 31 | 32 | instance.onAction({ type: at.INIT }); 33 | clock.tick(SYSTEM_TICK_INTERVAL * 2); 34 | expectation.verify(); 35 | }); 36 | it("should not fire SYSTEM_TICK events after UNINIT", () => { 37 | let expectation = sinon 38 | .mock(instance.store) 39 | .expects("dispatch") 40 | .never(); 41 | 42 | instance.onAction({ type: at.UNINIT }); 43 | clock.tick(SYSTEM_TICK_INTERVAL * 2); 44 | expectation.verify(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /content-src/styles/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Shared styling of article images shown as background 2 | @mixin image-as-background { 3 | background-color: var(--newtab-card-placeholder-color); 4 | background-position: center; 5 | background-repeat: no-repeat; 6 | background-size: cover; 7 | border-radius: 4px; 8 | box-shadow: inset 0 0 0 0.5px $black-15; 9 | } 10 | 11 | // Note: lineHeight and fontSize should be unitless but can be derived from pixel values 12 | // Bug 1550624 to clean up / remove this mixin to avoid duplicate styles 13 | @mixin limit-visible-lines($line-count, $line-height, $font-size) { 14 | font-size: $font-size * 1px; 15 | -webkit-line-clamp: $line-count; 16 | line-height: $line-height * 1px; 17 | } 18 | 19 | @mixin dark-theme-only { 20 | [lwt-newtab-brighttext] & { 21 | @content; 22 | } 23 | } 24 | 25 | @mixin ds-border-top { 26 | @content; 27 | 28 | @include dark-theme-only { 29 | border-top: 1px solid $grey-60; 30 | } 31 | 32 | border-top: 1px solid $grey-30; 33 | } 34 | 35 | @mixin ds-border-bottom { 36 | @content; 37 | 38 | @include dark-theme-only { 39 | border-bottom: 1px solid $grey-60; 40 | } 41 | 42 | border-bottom: 1px solid $grey-30; 43 | } 44 | 45 | @mixin ds-fade-in($halo-color: $blue-50-30) { 46 | box-shadow: 0 0 0 5px $halo-color; 47 | transition: box-shadow 150ms; 48 | border-radius: 4px; 49 | outline: none; 50 | } 51 | -------------------------------------------------------------------------------- /test/unit/content-src/components/DiscoveryStreamComponents/DSPrivacyModal.test.jsx: -------------------------------------------------------------------------------- 1 | import { DSPrivacyModal } from "content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal"; 2 | import { shallow, mount } from "enzyme"; 3 | import { actionCreators as ac } from "common/Actions.jsm"; 4 | import React from "react"; 5 | 6 | describe("Discovery Stream ", () => { 7 | let sandbox; 8 | let dispatch; 9 | let wrapper; 10 | beforeEach(() => { 11 | sandbox = sinon.createSandbox(); 12 | dispatch = sandbox.stub(); 13 | wrapper = shallow(); 14 | }); 15 | 16 | afterEach(() => { 17 | sandbox.restore(); 18 | }); 19 | 20 | it("should contain a privacy notice", () => { 21 | const modal = mount(); 22 | const child = modal.find(".privacy-notice"); 23 | 24 | assert.lengthOf(child, 1); 25 | }); 26 | 27 | it("should call dispatch when modal is closed", () => { 28 | wrapper.instance().closeModal(); 29 | assert.calledOnce(dispatch); 30 | }); 31 | 32 | it("should call dispatch with the correct events", () => { 33 | wrapper.instance().onLinkClick(); 34 | 35 | assert.calledOnce(dispatch); 36 | assert.calledWith( 37 | dispatch, 38 | ac.UserEvent({ 39 | event: "CLICK_PRIVACY_INFO", 40 | source: "DS_PRIVACY_MODAL", 41 | }) 42 | ); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /content-src/asrouter/README.md: -------------------------------------------------------------------------------- 1 | # Activity Stream Router 2 | 3 | ## Preferences `browser.newtab.activity-stream.asrouter.*` 4 | 5 | Name | Used for | Type | Example value 6 | --- | --- | --- | --- 7 | `whitelistHosts` | Whitelist a host in order to fetch messages from its endpoint | `[String]` | `["gist.github.com", "gist.githubusercontent.com", "localhost:8000"]` 8 | `providers.snippets` | Message provider options for snippets | `Object` | [see below](#message-providers) 9 | `providers.cfr` | Message provider options for cfr | `Object` | [see below](#message-providers) 10 | `providers.onboarding` | Message provider options for onboarding | `Object` | [see below](#message-providers) 11 | `useRemoteL10n` | Controls whether to use the remote Fluent files for l10n, default as `true` | `Boolean` | `[true|false]` 12 | 13 | ### Message providers examples 14 | 15 | ```json 16 | { 17 | "id" : "snippets", 18 | "type" : "remote", 19 | "enabled": true, 20 | "url" : "https://snippets.cdn.mozilla.net/us-west/bundles/bundle_d6d90fb9098ce8b45e60acf601bcb91b68322309.json", 21 | "updateCycleInMs" : 14400000 22 | } 23 | ``` 24 | 25 | ```json 26 | { 27 | "id" : "onboarding", 28 | "enabled": true, 29 | "type" : "local", 30 | "localProvider" : "OnboardingMessageProvider" 31 | } 32 | ``` 33 | 34 | ### [Snippet message format documentation](https://github.com/mozilla/activity-stream/blob/master/content-src/asrouter/schemas/message-format.md) 35 | -------------------------------------------------------------------------------- /lib/ASRouterFeed.jsm: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | const { actionTypes: at } = ChromeUtils.import( 6 | "resource://activity-stream/common/Actions.jsm" 7 | ); 8 | const { ASRouter } = ChromeUtils.import( 9 | "resource://activity-stream/lib/ASRouter.jsm" 10 | ); 11 | 12 | /** 13 | * @class ASRouterFeed - Connects ASRouter singleton (see above) to Activity Stream's 14 | * store so that it can use the RemotePageManager. 15 | */ 16 | class ASRouterFeed { 17 | constructor(options = {}) { 18 | this.router = options.router || ASRouter; 19 | } 20 | 21 | async enable() { 22 | if (!this.router.initialized) { 23 | await this.router.init( 24 | this.store._messageChannel.channel, 25 | this.store.dbStorage.getDbTable("snippets"), 26 | this.store.dispatch 27 | ); 28 | } 29 | } 30 | 31 | disable() { 32 | if (this.router.initialized) { 33 | this.router.uninit(); 34 | } 35 | } 36 | 37 | onAction(action) { 38 | switch (action.type) { 39 | case at.INIT: 40 | this.enable(); 41 | break; 42 | case at.UNINIT: 43 | this.disable(); 44 | break; 45 | } 46 | } 47 | } 48 | this.ASRouterFeed = ASRouterFeed; 49 | 50 | const EXPORTED_SYMBOLS = ["ASRouterFeed"]; 51 | -------------------------------------------------------------------------------- /test/browser/browser_as_load_location.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Helper to test that a newtab page loads its html document. 5 | * 6 | * @param selector {String} CSS selector to find an element in newtab content 7 | * @param message {String} Description of the test printed with the assertion 8 | */ 9 | async function checkNewtabLoads(selector, message) { 10 | // simulate a newtab open as a user would 11 | BrowserOpenTab(); 12 | 13 | // wait until the browser loads 14 | let browser = gBrowser.selectedBrowser; 15 | await waitForPreloaded(browser); 16 | 17 | // check what the content task thinks has been loaded. 18 | let found = await ContentTask.spawn( 19 | browser, 20 | selector, 21 | arg => content.document.querySelector(arg) !== null 22 | ); 23 | ok(found, message); 24 | 25 | // avoid leakage 26 | BrowserTestUtils.removeTab(gBrowser.selectedTab); 27 | } 28 | 29 | // Test with activity stream on 30 | async function checkActivityStreamLoads() { 31 | await checkNewtabLoads( 32 | "body.activity-stream", 33 | "Got Element" 34 | ); 35 | } 36 | 37 | // Run a first time not from a preloaded browser 38 | add_task(async function checkActivityStreamNotPreloadedLoad() { 39 | NewTabPagePreloading.removePreloadedBrowser(window); 40 | await checkActivityStreamLoads(); 41 | }); 42 | 43 | // Run a second time from a preloaded browser 44 | add_task(checkActivityStreamLoads); 45 | -------------------------------------------------------------------------------- /content-src/components/PocketLoggedInCta/PocketLoggedInCta.jsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import { connect } from "react-redux"; 6 | import React from "react"; 7 | 8 | export class _PocketLoggedInCta extends React.PureComponent { 9 | render() { 10 | const { pocketCta } = this.props.Pocket; 11 | return ( 12 | 13 | 17 | {pocketCta.ctaButton ? ( 18 | pocketCta.ctaButton 19 | ) : ( 20 | 21 | )} 22 | 23 | 24 | 27 | 28 | {pocketCta.ctaText ? ( 29 | pocketCta.ctaText 30 | ) : ( 31 | 32 | )} 33 | 34 | 35 | 36 | ); 37 | } 38 | } 39 | 40 | export const PocketLoggedInCta = connect(state => ({ Pocket: state.Pocket }))( 41 | _PocketLoggedInCta 42 | ); 43 | -------------------------------------------------------------------------------- /content-src/components/DiscoveryStreamBase/_DiscoveryStreamBase.scss: -------------------------------------------------------------------------------- 1 | $ds-width: 936px; 2 | 3 | .discovery-stream.ds-layout { 4 | $columns: 12; 5 | --gridColumnGap: 48px; 6 | --gridRowGap: 24px; 7 | display: grid; 8 | grid-template-columns: repeat($columns, 1fr); 9 | grid-column-gap: var(--gridColumnGap); 10 | grid-row-gap: var(--gridRowGap); 11 | width: $ds-width; 12 | margin: 0 auto; 13 | 14 | @while $columns > 0 { 15 | .ds-column-#{$columns} { 16 | grid-column-start: auto; 17 | grid-column-end: span $columns; 18 | } 19 | 20 | $columns: $columns - 1; 21 | } 22 | 23 | .ds-column-grid { 24 | display: grid; 25 | grid-row-gap: var(--gridRowGap); 26 | } 27 | } 28 | 29 | .ds-header { 30 | margin: 8px 0; 31 | } 32 | 33 | .ds-header, 34 | .ds-layout .section-title span { 35 | @include dark-theme-only { 36 | color: $grey-30; 37 | } 38 | 39 | color: $grey-50; 40 | font-size: 13px; 41 | font-weight: 600; 42 | line-height: 20px; 43 | 44 | .icon { 45 | fill: var(--newtab-text-secondary-color); 46 | } 47 | } 48 | 49 | .collapsible-section.ds-layout { 50 | margin: auto; 51 | width: $ds-width + 2 * $section-horizontal-padding; 52 | 53 | .section-top-bar { 54 | .learn-more-link a { 55 | color: var(--newtab-link-primary-color); 56 | font-weight: normal; 57 | 58 | &:-moz-any(:focus, :hover) { 59 | text-decoration: underline; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx: -------------------------------------------------------------------------------- 1 | import { CardGrid } from "content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid"; 2 | import { DSCard } from "content-src/components/DiscoveryStreamComponents/DSCard/DSCard"; 3 | import React from "react"; 4 | import { shallow } from "enzyme"; 5 | 6 | describe("", () => { 7 | let wrapper; 8 | 9 | beforeEach(() => { 10 | wrapper = shallow(); 11 | }); 12 | 13 | it("should render an empty div", () => { 14 | assert.ok(wrapper.exists()); 15 | assert.lengthOf(wrapper.children(), 0); 16 | }); 17 | 18 | it("should render DSCards", () => { 19 | wrapper.setProps({ items: 2, data: { recommendations: [{}, {}] } }); 20 | 21 | assert.lengthOf(wrapper.find(".ds-card-grid").children(), 2); 22 | assert.equal( 23 | wrapper 24 | .find(".ds-card-grid") 25 | .children() 26 | .at(0) 27 | .type(), 28 | DSCard 29 | ); 30 | }); 31 | 32 | it("should add divisible-by-4 to the grid", () => { 33 | wrapper.setProps({ items: 4, data: { recommendations: [{}, {}] } }); 34 | 35 | assert.ok(wrapper.find(".ds-card-grid-divisible-by-4").exists()); 36 | }); 37 | 38 | it("should add divisible-by-3 to the grid", () => { 39 | wrapper.setProps({ items: 3, data: { recommendations: [{}, {}] } }); 40 | 41 | assert.ok(wrapper.find(".ds-card-grid-divisible-by-3").exists()); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /content-src/components/DiscoveryStreamComponents/Navigation/Navigation.jsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import React from "react"; 6 | import { SafeAnchor } from "../SafeAnchor/SafeAnchor"; 7 | import { FluentOrText } from "content-src/components/FluentOrText/FluentOrText"; 8 | 9 | export class Topic extends React.PureComponent { 10 | render() { 11 | const { url, name } = this.props; 12 | return ( 13 |
  • 14 | 15 | {name} 16 | 17 |
  • 18 | ); 19 | } 20 | } 21 | 22 | export class Navigation extends React.PureComponent { 23 | render() { 24 | const { links } = this.props || []; 25 | const { alignment } = this.props || "centered"; 26 | const header = this.props.header || {}; 27 | return ( 28 |
    29 | {header.title ? ( 30 | 31 |
    32 | 33 | ) : null} 34 |
    35 |
      36 | {links && 37 | links.map(t => )} 38 |
    39 |
    40 |
    41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /content-src/components/ContextMenu/_ContextMenu.scss: -------------------------------------------------------------------------------- 1 | .context-menu { 2 | background: var(--newtab-contextmenu-background-color); 3 | border-radius: $context-menu-border-radius; 4 | box-shadow: $context-menu-shadow; 5 | display: block; 6 | font-size: $context-menu-font-size; 7 | margin-inline-start: 5px; 8 | inset-inline-start: 100%; 9 | position: absolute; 10 | top: ($context-menu-button-size / 4); 11 | z-index: 8; 12 | 13 | > ul { 14 | list-style: none; 15 | margin: 0; 16 | padding: $context-menu-outer-padding 0; 17 | 18 | > li { 19 | margin: 0; 20 | width: 100%; 21 | 22 | &.separator { 23 | border-bottom: $border-secondary; 24 | margin: $context-menu-outer-padding 0; 25 | } 26 | 27 | > a, 28 | > button { 29 | align-items: center; 30 | color: inherit; 31 | cursor: pointer; 32 | display: flex; 33 | width: 100%; 34 | line-height: 16px; 35 | outline: none; 36 | border: 0; 37 | padding: $context-menu-item-padding; 38 | white-space: nowrap; 39 | 40 | &:-moz-any(:focus, :hover) { 41 | background: var(--newtab-element-hover-color); 42 | } 43 | 44 | &:active { 45 | background: var(--newtab-element-active-color); 46 | } 47 | 48 | &.disabled { 49 | opacity: 0.4; 50 | pointer-events: none; 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /data/content/assets/glyph-highlights-16.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /content-src/components/FluentOrText/FluentOrText.jsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import React from "react"; 6 | 7 | /** 8 | * Set text on a child element/component depending on if the message is already 9 | * translated plain text or a fluent id with optional args. 10 | */ 11 | export class FluentOrText extends React.PureComponent { 12 | render() { 13 | // Ensure we have a single child to attach attributes 14 | const { children, message } = this.props; 15 | const child = children ? React.Children.only(children) : ; 16 | 17 | // For a string message, just use it as the child's text 18 | let grandChildren = message; 19 | let extraProps; 20 | 21 | // Convert a message object to set desired fluent-dom attributes 22 | if (typeof message === "object") { 23 | const args = message.args || message.values; 24 | extraProps = { 25 | "data-l10n-args": args && JSON.stringify(args), 26 | "data-l10n-id": message.id || message.string_id, 27 | }; 28 | 29 | // Use original children potentially with data-l10n-name attributes 30 | grandChildren = child.props.children; 31 | } 32 | 33 | // Add the message to the child via fluent attributes or text node 34 | return React.cloneElement(child, extraProps, grandChildren); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /data/content/assets/trailhead/card-illo-private.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /test/unit/lib/FilterAdult.test.js: -------------------------------------------------------------------------------- 1 | import { filterAdult } from "lib/FilterAdult.jsm"; 2 | import { GlobalOverrider } from "test/unit/utils"; 3 | 4 | describe("filterAdult", () => { 5 | let hashStub; 6 | let hashValue; 7 | let globals; 8 | 9 | beforeEach(() => { 10 | globals = new GlobalOverrider(); 11 | hashStub = { 12 | finish: sinon.stub().callsFake(() => hashValue), 13 | init: sinon.stub(), 14 | update: sinon.stub(), 15 | }; 16 | globals.set("Cc", { 17 | "@mozilla.org/security/hash;1": { 18 | createInstance() { 19 | return hashStub; 20 | }, 21 | }, 22 | }); 23 | }); 24 | 25 | afterEach(() => { 26 | globals.restore(); 27 | }); 28 | 29 | it("should default to include on unexpected urls", () => { 30 | const empty = {}; 31 | 32 | const result = filterAdult([empty]); 33 | 34 | assert.equal(result.length, 1); 35 | assert.equal(result[0], empty); 36 | }); 37 | it("should not filter out non-adult urls", () => { 38 | const link = { url: "https://mozilla.org/" }; 39 | 40 | const result = filterAdult([link]); 41 | 42 | assert.equal(result.length, 1); 43 | assert.equal(result[0], link); 44 | }); 45 | it("should filter out adult urls", () => { 46 | // Use a hash value that is in the adult set 47 | hashValue = "+/UCpAhZhz368iGioEO8aQ=="; 48 | const link = { url: "https://some-adult-site/" }; 49 | 50 | const result = filterAdult([link]); 51 | 52 | assert.equal(result.length, 0); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /content-src/components/DiscoveryStreamComponents/DSDismiss/_DSDismiss.scss: -------------------------------------------------------------------------------- 1 | .ds-dismiss { 2 | position: relative; 3 | overflow: hidden; 4 | border-radius: 8px; 5 | transition-delay: 100ms; 6 | transition-duration: 200ms; 7 | transition-property: background; 8 | 9 | &.hovering { 10 | @include dark-theme-only { 11 | background: $grey-90-30; 12 | } 13 | 14 | background: $grey-90-10; 15 | } 16 | 17 | &:hover { 18 | .ds-dismiss-button { 19 | opacity: 1; 20 | } 21 | } 22 | 23 | .ds-dismiss-button { 24 | @include dark-theme-only { 25 | background: $grey-90-30; 26 | } 27 | 28 | border: 0; 29 | cursor: pointer; 30 | height: 32px; 31 | width: 32px; 32 | padding: 0; 33 | display: flex; 34 | align-items: center; 35 | justify-content: center; 36 | position: absolute; 37 | right: 0; 38 | top: 0; 39 | border-radius: 50%; 40 | margin: 18px 18px 0 0; 41 | background: $grey-90-10; 42 | 43 | &:hover { 44 | @include dark-theme-only { 45 | background: $grey-90-50; 46 | } 47 | 48 | background: $grey-90-20; 49 | } 50 | 51 | &:active { 52 | @include dark-theme-only { 53 | background: $grey-90-70; 54 | } 55 | 56 | background: $grey-90-30; 57 | } 58 | 59 | &:focus { 60 | @include dark-theme-only { 61 | box-shadow: 0 0 0 2px $grey-80, 0 0 0 5px $blue-50-50; 62 | } 63 | 64 | box-shadow: 0 0 0 2px $white, 0 0 0 5px $blue-50-50; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /bin/vendor.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 4 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | /* eslint-disable no-console */ 7 | 8 | const { cp, set } = require("shelljs"); 9 | const path = require("path"); 10 | 11 | const filesToVendor = { 12 | // XXX currently these two licenses are identical. Perhaps we should check 13 | // in case that changes at some point in the future. 14 | "react/LICENSE": "REACT_AND_REACT_DOM_LICENSE", 15 | "react/umd/react.production.min.js": "react.js", 16 | "react/umd/react.development.js": "react-dev.js", 17 | "react-dom/umd/react-dom.production.min.js": "react-dom.js", 18 | "react-dom/umd/react-dom.development.js": "react-dom-dev.js", 19 | "react-redux/LICENSE.md": "REACT_REDUX_LICENSE", 20 | "react-redux/dist/react-redux.min.js": "react-redux.js", 21 | "react-transition-group/dist/react-transition-group.min.js": 22 | "react-transition-group.js", 23 | "react-transition-group/LICENSE": "REACT_TRANSITION_GROUP_LICENSE", 24 | }; 25 | 26 | set("-v"); // Echo all the copy commands so the user can see what's going on 27 | for (let srcPath of Object.keys(filesToVendor)) { 28 | cp( 29 | path.join("node_modules", srcPath), 30 | path.join("vendor", filesToVendor[srcPath]) 31 | ); 32 | } 33 | 34 | console.log(` 35 | Check to see if any license files have changed, and, if so, be sure to update 36 | https://searchfox.org/mozilla-central/source/toolkit/content/license.html`); 37 | -------------------------------------------------------------------------------- /test/unit/content-src/components/PocketLoggedInCta.test.jsx: -------------------------------------------------------------------------------- 1 | import { combineReducers, createStore } from "redux"; 2 | import { INITIAL_STATE, reducers } from "common/Reducers.jsm"; 3 | import { mount, shallow } from "enzyme"; 4 | import { 5 | PocketLoggedInCta, 6 | _PocketLoggedInCta as PocketLoggedInCtaRaw, 7 | } from "content-src/components/PocketLoggedInCta/PocketLoggedInCta"; 8 | import { Provider } from "react-redux"; 9 | import React from "react"; 10 | 11 | function mountSectionWithProps(props) { 12 | const store = createStore(combineReducers(reducers), INITIAL_STATE); 13 | return mount( 14 | 15 | 16 | 17 | ); 18 | } 19 | 20 | describe("", () => { 21 | it("should render a PocketLoggedInCta element", () => { 22 | const wrapper = mountSectionWithProps({}); 23 | assert.ok(wrapper.exists()); 24 | }); 25 | it("should render Fluent spans when rendered without props", () => { 26 | const wrapper = mountSectionWithProps({}); 27 | 28 | const message = wrapper.find("span[data-l10n-id]"); 29 | assert.lengthOf(message, 2); 30 | }); 31 | it("should not render Fluent spans when rendered with props", () => { 32 | const wrapper = shallow( 33 | 41 | ); 42 | 43 | const message = wrapper.find("span[data-l10n-id]"); 44 | assert.lengthOf(message, 0); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /content-src/asrouter/components/Button/_Button.scss: -------------------------------------------------------------------------------- 1 | .ASRouterButton { 2 | font-weight: 600; 3 | font-size: 14px; 4 | white-space: nowrap; 5 | border-radius: 2px; 6 | border: 0; 7 | font-family: inherit; 8 | padding: 8px 15px; 9 | margin-inline-start: 12px; 10 | color: inherit; 11 | cursor: pointer; 12 | 13 | .tall & { 14 | margin-inline-start: 20px; 15 | } 16 | 17 | &.primary { 18 | border: 1px solid var(--newtab-button-primary-color); 19 | background-color: var(--newtab-button-primary-color); 20 | color: $grey-10; 21 | 22 | &:hover { 23 | background-color: $blue-70; 24 | } 25 | 26 | &:active { 27 | background-color: $blue-80; 28 | } 29 | } 30 | 31 | &.secondary { 32 | background-color: $grey-90-10; 33 | 34 | &:hover { 35 | background-color: $grey-90-20; 36 | } 37 | 38 | &:active { 39 | background-color: $grey-90-30; 40 | } 41 | 42 | &:focus { 43 | box-shadow: 0 0 0 1px $blue-50 inset, 0 0 0 1px $blue-50, 0 0 0 4px $blue-50-30; 44 | } 45 | } 46 | } 47 | 48 | [lwt-newtab-brighttext] { 49 | .secondary { 50 | background-color: $grey-10-10; 51 | 52 | &:hover { 53 | background-color: $grey-10-20; 54 | } 55 | 56 | &:active { 57 | background-color: $grey-10-30; 58 | } 59 | } 60 | 61 | // Snippets scene 2 footer 62 | .footer { 63 | .secondary { 64 | background-color: $grey-10-30; 65 | 66 | &:hover { 67 | background-color: $grey-10-40; 68 | } 69 | 70 | &:active { 71 | background-color: $grey-10-50; 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /content-src/asrouter/templates/SendToDeviceSnippet/isEmailOrPhoneNumber.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | /** 6 | * Checks if a given string is an email or phone number or neither 7 | * @param {string} val The user input 8 | * @param {ASRMessageContent} content .content property on ASR message 9 | * @returns {"email"|"phone"|""} The type of the input 10 | */ 11 | export function isEmailOrPhoneNumber(val, content) { 12 | const { locale } = content; 13 | // http://emailregex.com/ 14 | const email_re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 15 | const check_email = email_re.test(val); 16 | let check_phone; // depends on locale 17 | switch (locale) { 18 | case "en-US": 19 | case "en-CA": 20 | // allow 10-11 digits in case user wants to enter country code 21 | check_phone = val.length >= 10 && val.length <= 11 && !isNaN(val); 22 | break; 23 | case "de": 24 | // allow between 2 and 12 digits for german phone numbers 25 | check_phone = val.length >= 2 && val.length <= 12 && !isNaN(val); 26 | break; 27 | // this case should never be hit, but good to have a fallback just in case 28 | default: 29 | check_phone = !isNaN(val); 30 | break; 31 | } 32 | if (check_email) { 33 | return "email"; 34 | } else if (check_phone) { 35 | return "phone"; 36 | } 37 | return ""; 38 | } 39 | -------------------------------------------------------------------------------- /test/browser/browser_asrouter_whatsnewpanel.js: -------------------------------------------------------------------------------- 1 | const { PanelTestProvider } = ChromeUtils.import( 2 | "resource://activity-stream/lib/PanelTestProvider.jsm" 3 | ); 4 | const { ToolbarPanelHub } = ChromeUtils.import( 5 | "resource://activity-stream/lib/ToolbarPanelHub.jsm" 6 | ); 7 | 8 | add_task(async function test_messages_rendering() { 9 | const msgs = (await PanelTestProvider.getMessages()).filter( 10 | ({ template }) => template === "whatsnew_panel_message" 11 | ); 12 | 13 | Assert.ok(msgs.length, "FxA test message exists"); 14 | 15 | Object.defineProperty(ToolbarPanelHub, "messages", { 16 | get: () => Promise.resolve(msgs), 17 | configurable: true, 18 | }); 19 | 20 | await ToolbarPanelHub.enableAppmenuButton(); 21 | 22 | const mainView = document.getElementById("appMenu-mainView"); 23 | UITour.showMenu(window, "appMenu"); 24 | await BrowserTestUtils.waitForEvent(mainView, "ViewShown"); 25 | 26 | Assert.equal(mainView.hidden, false, "Panel is visible"); 27 | 28 | const whatsNewBtn = document.getElementById("appMenu-whatsnew-button"); 29 | Assert.equal(whatsNewBtn.hidden, false, "What's New is present"); 30 | 31 | // Show the What's New Messages 32 | whatsNewBtn.click(); 33 | 34 | const shownMessages = await BrowserTestUtils.waitForCondition( 35 | () => 36 | document.getElementById("PanelUI-whatsNew-message-container") && 37 | document.querySelectorAll( 38 | "#PanelUI-whatsNew-message-container .whatsNew-message" 39 | ).length 40 | ); 41 | Assert.equal( 42 | shownMessages, 43 | msgs.length, 44 | "Expected number of What's New messages rendered." 45 | ); 46 | 47 | UITour.hideMenu(window, "appMenu"); 48 | }); 49 | -------------------------------------------------------------------------------- /content-src/asrouter/rich-text-strings.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import { FluentBundle } from "fluent"; 6 | 7 | /** 8 | * Properties that allow rich text MUST be added to this list. 9 | * key: the localization_id that should be used 10 | * value: a property or array of properties on the message.content object 11 | */ 12 | const RICH_TEXT_CONFIG = { 13 | text: ["text", "scene1_text"], 14 | success_text: "success_text", 15 | error_text: "error_text", 16 | scene2_text: "scene2_text", 17 | amo_html: "amo_html", 18 | privacy_html: "scene2_privacy_html", 19 | disclaimer_html: "scene2_disclaimer_html", 20 | }; 21 | 22 | export const RICH_TEXT_KEYS = Object.keys(RICH_TEXT_CONFIG); 23 | 24 | /** 25 | * Generates an array of messages suitable for fluent's localization provider 26 | * including all needed strings for rich text. 27 | * @param {object} content A .content object from an ASR message (i.e. message.content) 28 | * @returns {FluentBundle[]} A array containing the fluent message context 29 | */ 30 | export function generateBundles(content) { 31 | const bundle = new FluentBundle("en-US"); 32 | 33 | RICH_TEXT_KEYS.forEach(key => { 34 | const attrs = RICH_TEXT_CONFIG[key]; 35 | const attrsToTry = Array.isArray(attrs) ? [...attrs] : [attrs]; 36 | let string = ""; 37 | while (!string && attrsToTry.length) { 38 | const attr = attrsToTry.pop(); 39 | string = content[attr]; 40 | } 41 | bundle.addMessages(`${key} = ${string}`); 42 | }); 43 | return [bundle]; 44 | } 45 | -------------------------------------------------------------------------------- /content-src/asrouter/templates/FXASignupSnippet/FXASignupSnippet.jsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import React from "react"; 6 | import schema from "./FXASignupSnippet.schema.json"; 7 | import { SubmitFormSnippet } from "../SubmitFormSnippet/SubmitFormSnippet.jsx"; 8 | 9 | export const FXASignupSnippet = props => { 10 | const userAgent = window.navigator.userAgent.match(/Firefox\/([0-9]+)\./); 11 | const firefox_version = userAgent ? parseInt(userAgent[1], 10) : 0; 12 | const extendedContent = { 13 | scene1_button_label: schema.properties.scene1_button_label.default, 14 | retry_button_label: schema.properties.retry_button_label.default, 15 | scene2_email_placeholder_text: 16 | schema.properties.scene2_email_placeholder_text.default, 17 | scene2_button_label: schema.properties.scene2_button_label.default, 18 | scene2_dismiss_button_text: 19 | schema.properties.scene2_dismiss_button_text.default, 20 | ...props.content, 21 | hidden_inputs: { 22 | action: "email", 23 | context: "fx_desktop_v3", 24 | entrypoint: "snippets", 25 | utm_source: "snippet", 26 | utm_content: firefox_version, 27 | utm_campaign: props.content.utm_campaign, 28 | utm_term: props.content.utm_term, 29 | ...props.content.hidden_inputs, 30 | }, 31 | }; 32 | 33 | return ( 34 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /content-src/asrouter/templates/NewsletterSnippet/NewsletterSnippet.jsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import React from "react"; 6 | import schema from "./NewsletterSnippet.schema.json"; 7 | import { SubmitFormSnippet } from "../SubmitFormSnippet/SubmitFormSnippet.jsx"; 8 | 9 | export const NewsletterSnippet = props => { 10 | const extendedContent = { 11 | scene1_button_label: schema.properties.scene1_button_label.default, 12 | retry_button_label: schema.properties.retry_button_label.default, 13 | scene2_email_placeholder_text: 14 | schema.properties.scene2_email_placeholder_text.default, 15 | scene2_button_label: schema.properties.scene2_button_label.default, 16 | scene2_dismiss_button_text: 17 | schema.properties.scene2_dismiss_button_text.default, 18 | scene2_newsletter: schema.properties.scene2_newsletter.default, 19 | ...props.content, 20 | hidden_inputs: { 21 | newsletters: 22 | props.content.scene2_newsletter || 23 | schema.properties.scene2_newsletter.default, 24 | fmt: schema.properties.hidden_inputs.properties.fmt.default, 25 | lang: props.content.locale || schema.properties.locale.default, 26 | source_url: `https://snippets.mozilla.com/show/${props.id}`, 27 | ...props.content.hidden_inputs, 28 | }, 29 | }; 30 | 31 | return ( 32 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /test/unit/asrouter/templates/Interrupt.test.jsx: -------------------------------------------------------------------------------- 1 | import { FullPageInterrupt } from "content-src/asrouter/templates/FullPageInterrupt/FullPageInterrupt"; 2 | import { Interrupt } from "content-src/asrouter/templates/FirstRun/Interrupt"; 3 | import { ReturnToAMO } from "content-src/asrouter/templates/ReturnToAMO/ReturnToAMO"; 4 | import { Trailhead } from "content-src/asrouter/templates//Trailhead/Trailhead"; 5 | import { shallow } from "enzyme"; 6 | import React from "react"; 7 | 8 | describe("", () => { 9 | let wrapper; 10 | it("should render Return TO AMO when the message has a template of return_to_amo_overlay", () => { 11 | wrapper = shallow( 12 | 15 | ); 16 | assert.lengthOf(wrapper.find(ReturnToAMO), 1); 17 | }); 18 | it("should render Trailhead when the message has a template of trailhead", () => { 19 | wrapper = shallow( 20 | 21 | ); 22 | assert.lengthOf(wrapper.find(Trailhead), 1); 23 | }); 24 | it("should render Full Page interrupt when the message has a template of full_page_interrupt", () => { 25 | wrapper = shallow( 26 | 29 | ); 30 | assert.lengthOf(wrapper.find(FullPageInterrupt), 1); 31 | }); 32 | it("should throw an error if another type of message is dispatched", () => { 33 | assert.throws(() => { 34 | wrapper = shallow( 35 | 36 | ); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/unit/asrouter/PanelTestProvider.test.js: -------------------------------------------------------------------------------- 1 | import { PanelTestProvider } from "lib/PanelTestProvider.jsm"; 2 | import schema from "content-src/asrouter/schemas/panel/cfr-fxa-bookmark.schema.json"; 3 | import update_schema from "content-src/asrouter/templates/OnboardingMessage/UpdateAction.schema.json"; 4 | import whats_new_schema from "content-src/asrouter/templates/OnboardingMessage/WhatsNewMessage.schema.json"; 5 | const messages = PanelTestProvider.getMessages(); 6 | 7 | describe("PanelTestProvider", () => { 8 | it("should have a message", () => { 9 | // Careful: when changing this number make sure that new messages also go 10 | // through schema verifications. 11 | assert.lengthOf(messages, 16); 12 | }); 13 | it("should be a valid message", () => { 14 | const fxaMessages = messages.filter( 15 | ({ template }) => template === "fxa_bookmark_panel" 16 | ); 17 | for (let message of fxaMessages) { 18 | assert.jsonSchema(message.content, schema); 19 | } 20 | }); 21 | it("should be a valid message", () => { 22 | const updateMessages = messages.filter( 23 | ({ template }) => template === "update_action" 24 | ); 25 | for (let message of updateMessages) { 26 | assert.jsonSchema(message.content, update_schema); 27 | } 28 | }); 29 | it("should be a valid message", () => { 30 | const whatsNewMessages = messages.filter( 31 | ({ template }) => template === "whatsnew_panel_message" 32 | ); 33 | for (let message of whatsNewMessages) { 34 | assert.jsonSchema(message.content, whats_new_schema); 35 | // Not part of `message.content` so it can't be enforced through schema 36 | assert.property(message, "order"); 37 | } 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/unit/content-src/components/DiscoveryStreamComponents/DSTextPromo.test.jsx: -------------------------------------------------------------------------------- 1 | import { DSTextPromo } from "content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo"; 2 | import React from "react"; 3 | import { shallow } from "enzyme"; 4 | 5 | describe("", () => { 6 | let wrapper; 7 | let sandbox; 8 | let dispatchStub; 9 | 10 | beforeEach(() => { 11 | sandbox = sinon.createSandbox(); 12 | dispatchStub = sandbox.stub(); 13 | wrapper = shallow( 14 | 21 | ); 22 | }); 23 | 24 | afterEach(() => { 25 | sandbox.restore(); 26 | }); 27 | 28 | it("should render", () => { 29 | assert.ok(wrapper.exists()); 30 | assert.ok(wrapper.find(".ds-text-promo").exists()); 31 | }); 32 | 33 | it("should render a header", () => { 34 | wrapper.setProps({ header: "foo" }); 35 | assert.ok(wrapper.find(".text").exists()); 36 | }); 37 | 38 | it("should render a subtitle", () => { 39 | wrapper.setProps({ subtitle: "foo" }); 40 | assert.ok(wrapper.find(".subtitle").exists()); 41 | }); 42 | 43 | it("should dispatch a click event on click", () => { 44 | wrapper.instance().onLinkClick(); 45 | 46 | assert.calledTwice(dispatchStub); 47 | assert.deepEqual(dispatchStub.firstCall.args[0].data, { 48 | event: "CLICK", 49 | source: "TEXTPROMO", 50 | action_position: 0, 51 | }); 52 | assert.deepEqual(dispatchStub.secondCall.args[0].data, { 53 | source: "TEXTPROMO", 54 | click: 0, 55 | tiles: [{ id: "1234", pos: 0 }], 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /vendor/REACT_TRANSITION_GROUP_LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, React Community 4 | Forked from React (https://github.com/facebook/react) Copyright 2013-present, Facebook, Inc. 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /content-src/asrouter/templates/OnboardingMessage/OnboardingMessage.jsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import React from "react"; 6 | 7 | export class OnboardingCard extends React.PureComponent { 8 | constructor(props) { 9 | super(props); 10 | this.onClick = this.onClick.bind(this); 11 | } 12 | 13 | onClick() { 14 | const { props } = this; 15 | const ping = { 16 | event: "CLICK_BUTTON", 17 | message_id: props.id, 18 | id: props.UISurface, 19 | }; 20 | props.sendUserActionTelemetry(ping); 21 | props.onAction(props.content.primary_button.action, props.message); 22 | } 23 | 24 | render() { 25 | const { content } = this.props; 26 | const className = this.props.className || "onboardingMessage"; 27 | return ( 28 |
    29 |
    30 |
    31 | 32 |

    36 |

    40 | 41 | 42 |

    49 |
    50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/unit/asrouter/SnippetsTestMessageProvider.test.js: -------------------------------------------------------------------------------- 1 | import EOYSnippetSchema from "../../../content-src/asrouter/templates/EOYSnippet/EOYSnippet.schema.json"; 2 | import SimpleBelowSearchSnippetSchema from "../../../content-src/asrouter/templates/SimpleBelowSearchSnippet/SimpleBelowSearchSnippet.schema.json"; 3 | import SimpleSnippetSchema from "../../../content-src/asrouter/templates/SimpleSnippet/SimpleSnippet.schema.json"; 4 | import { SnippetsTestMessageProvider } from "../../../lib/SnippetsTestMessageProvider.jsm"; 5 | import SubmitFormSnippetSchema from "../../../content-src/asrouter/templates/SubmitFormSnippet/SubmitFormSnippet.schema.json"; 6 | 7 | const schemas = { 8 | simple_snippet: SimpleSnippetSchema, 9 | newsletter_snippet: SubmitFormSnippetSchema, 10 | fxa_signup_snippet: SubmitFormSnippetSchema, 11 | send_to_device_snippet: SubmitFormSnippetSchema, 12 | eoy_snippet: EOYSnippetSchema, 13 | simple_below_search_snippet: SimpleBelowSearchSnippetSchema, 14 | }; 15 | 16 | describe("SnippetsTestMessageProvider", () => { 17 | let messages = SnippetsTestMessageProvider.getMessages(); 18 | 19 | it("should return an array of messages", () => { 20 | assert.isArray(messages); 21 | }); 22 | 23 | it("should have a valid example of each schema", () => { 24 | Object.keys(schemas).forEach(templateName => { 25 | const example = messages.find( 26 | message => message.template === templateName 27 | ); 28 | assert.ok(example, `has a ${templateName} example`); 29 | }); 30 | }); 31 | 32 | it("should have examples that are valid", () => { 33 | messages.forEach(example => { 34 | assert.jsonSchema( 35 | example.content, 36 | schemas[example.template], 37 | `${example.id} should be valid` 38 | ); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /content-src/components/DiscoveryStreamComponents/CardGrid/_CardGrid.scss: -------------------------------------------------------------------------------- 1 | $col4-header-line-height: 20; 2 | $col4-header-font-size: 14; 3 | 4 | .ds-card-grid { 5 | display: grid; 6 | grid-gap: 24px; 7 | 8 | .ds-card { 9 | @include dark-theme-only { 10 | background: none; 11 | } 12 | 13 | background: $white; 14 | border-radius: 4px; 15 | } 16 | 17 | .ds-card-link:focus { 18 | @include ds-fade-in; 19 | } 20 | 21 | &.ds-card-grid-border { 22 | .ds-card:not(.placeholder) { 23 | @include dark-theme-only { 24 | box-shadow: 0 1px 4px $shadow-10; 25 | background: $grey-70; 26 | } 27 | 28 | box-shadow: 0 1px 4px 0 $grey-90-10; 29 | 30 | .img-wrapper .img img { 31 | border-radius: 4px 4px 0 0; 32 | } 33 | } 34 | } 35 | 36 | &.ds-card-grid-no-border { 37 | .ds-card { 38 | background: none; 39 | 40 | .meta { 41 | padding: 12px 0; 42 | } 43 | } 44 | } 45 | 46 | // "2/3 width layout" 47 | .ds-column-5 &, 48 | .ds-column-6 &, 49 | .ds-column-7 &, 50 | .ds-column-8 & { 51 | grid-template-columns: repeat(2, 1fr); 52 | } 53 | 54 | // "Full width layout" 55 | .ds-column-9 &, 56 | .ds-column-10 &, 57 | .ds-column-11 &, 58 | .ds-column-12 & { 59 | grid-template-columns: repeat(4, 1fr); 60 | 61 | &.ds-card-grid-divisible-by-3 { 62 | grid-template-columns: repeat(3, 1fr); 63 | 64 | .title { 65 | font-size: 17px; 66 | line-height: 24px; 67 | } 68 | 69 | .excerpt { 70 | @include limit-visible-lines(3, 24, 15); 71 | } 72 | } 73 | 74 | &.ds-card-grid-divisible-by-4 .title { 75 | @include limit-visible-lines(3, 20, 15); 76 | } 77 | } 78 | 79 | &.empty { 80 | grid-template-columns: auto; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/browser/browser_as_render.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | test_newtab({ 4 | async before({ pushPrefs }) { 5 | await pushPrefs([ 6 | "browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar", 7 | false, 8 | ]); 9 | }, 10 | test: function test_render_search() { 11 | let search = content.document.getElementById("newtab-search-text"); 12 | ok(search, "Got the search box"); 13 | isnot( 14 | search.placeholder, 15 | "search_web_placeholder", 16 | "Search box is localized" 17 | ); 18 | }, 19 | }); 20 | 21 | test_newtab({ 22 | async before({ pushPrefs }) { 23 | await pushPrefs([ 24 | "browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar", 25 | true, 26 | ]); 27 | }, 28 | test: function test_render_search_handoff() { 29 | let search = content.document.querySelector(".search-handoff-button"); 30 | ok(search, "Got the search handoff button"); 31 | }, 32 | }); 33 | 34 | test_newtab(function test_render_topsites() { 35 | let topSites = content.document.querySelector(".top-sites-list"); 36 | ok(topSites, "Got the top sites section"); 37 | }); 38 | 39 | test_newtab({ 40 | async before({ pushPrefs }) { 41 | await pushPrefs([ 42 | "browser.newtabpage.activity-stream.feeds.topsites", 43 | false, 44 | ]); 45 | }, 46 | test: function test_render_no_topsites() { 47 | let topSites = content.document.querySelector(".top-sites-list"); 48 | ok(!topSites, "No top sites section"); 49 | }, 50 | }); 51 | 52 | // This next test runs immediately after test_render_no_topsites to make sure 53 | // the topsites pref is restored 54 | test_newtab(function test_render_topsites_again() { 55 | let topSites = content.document.querySelector(".top-sites-list"); 56 | ok(topSites, "Got the top sites section again"); 57 | }); 58 | -------------------------------------------------------------------------------- /test/unit/content-src/components/TopSites/SearchShortcutsForm.test.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | SearchShortcutsForm, 3 | SelectableSearchShortcut, 4 | } from "content-src/components/TopSites/SearchShortcutsForm"; 5 | import React from "react"; 6 | import { shallow } from "enzyme"; 7 | 8 | describe("", () => { 9 | let wrapper; 10 | let sandbox; 11 | let dispatchStub; 12 | 13 | beforeEach(() => { 14 | sandbox = sinon.createSandbox(); 15 | dispatchStub = sandbox.stub(); 16 | const defaultProps = { rows: [], searchShortcuts: [] }; 17 | wrapper = shallow( 18 | 19 | ); 20 | }); 21 | 22 | afterEach(() => { 23 | sandbox.restore(); 24 | }); 25 | 26 | it("should render", () => { 27 | assert.ok(wrapper.exists()); 28 | assert.ok(wrapper.find(".topsite-form").exists()); 29 | }); 30 | 31 | it("should render SelectableSearchShortcut components", () => { 32 | wrapper.setState({ shortcuts: [{}, {}] }); 33 | 34 | assert.lengthOf( 35 | wrapper.find(".search-shortcuts-container div").children(), 36 | 2 37 | ); 38 | assert.equal( 39 | wrapper 40 | .find(".search-shortcuts-container div") 41 | .children() 42 | .at(0) 43 | .type(), 44 | SelectableSearchShortcut 45 | ); 46 | }); 47 | 48 | it("should render SelectableSearchShortcut components", () => { 49 | const onCloseStub = sandbox.stub(); 50 | const fakeEvent = { preventDefault: sandbox.stub() }; 51 | wrapper.setState({ shortcuts: [{}, {}] }); 52 | wrapper.setProps({ onClose: onCloseStub }); 53 | 54 | wrapper.find(".done").simulate("click", fakeEvent); 55 | 56 | assert.calledOnce(dispatchStub); 57 | assert.calledOnce(fakeEvent.preventDefault); 58 | assert.calledOnce(onCloseStub); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /docs/v2-system-addon/telemetry.md: -------------------------------------------------------------------------------- 1 | # Adding/Changing Telemetry Checklist 2 | 3 | Adding telemetry generally involves a few steps: 4 | 5 | 1. File a "user story" bug about who wants what question answered. This will be used to track the client-side implementation as well as the data review request. If the server side changes are needed, ask Nan (:nanj / @ncloudio) if in doubt, bugs will be filed separately as dependencies. 6 | 1. Implement as usual... 7 | 1. Update `system-addon/test/schemas/pings.js` with a commented JOI schema of your changes, and add tests to system-addon/test/unit/TelemetryFeed.test.js to exercise the ping creation. 8 | 1. Update [data_events.md](data_events.md) with an example of the data in question. 9 | 1. Update any fields that you've added, deleted, or changed in [data_dictionary.md](data_dictionary.md). 10 | 1. Get review from Nan on the data schema and the documentation changes. 11 | 1. Request `data-review` of your documentation changes from a [data steward](https://wiki.mozilla.org/Firefox/Data_Collection) to ensure suitability for collection controlled by the opt-out `datareporting.healthreport.uploadEnabled` pref. Download and fill out the [data review request form](https://github.com/mozilla/data-review/blob/master/request.md) and then attach it as a text file on Bugzilla so you can r? a data steward. We've been working with Chris H-C (:chutten) for the Firefox specific telemetry, and Kenny Long (kenny@getpocket.com) for the Pocket specific telemetry, they are the best candidates for the review work as they know well about the context. 12 | 1. After landing the implementation, check with Nan to make sure the pings are making it to the database. 13 | 1. Once data flows in, you can build dashboard for the new telemetry on [Redash](https://sql.telemetry.mozilla.org/dashboards). If you're looking for some help about Redash or dashboard building, Nan is the guy for that. 14 | -------------------------------------------------------------------------------- /test/unit/content-src/components/DiscoveryStreamComponents/Navigation.test.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Navigation, 3 | Topic, 4 | } from "content-src/components/DiscoveryStreamComponents/Navigation/Navigation"; 5 | import React from "react"; 6 | import { SafeAnchor } from "content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor"; 7 | import { FluentOrText } from "content-src/components/FluentOrText/FluentOrText"; 8 | import { shallow, mount } from "enzyme"; 9 | 10 | describe("", () => { 11 | let wrapper; 12 | 13 | beforeEach(() => { 14 | wrapper = mount(); 15 | }); 16 | 17 | it("should render", () => { 18 | assert.ok(wrapper.exists()); 19 | }); 20 | 21 | it("should render a title", () => { 22 | wrapper.setProps({ header: { title: "Foo" } }); 23 | 24 | assert.equal(wrapper.find(".ds-header").text(), "Foo"); 25 | }); 26 | 27 | it("should render a FluentOrText", () => { 28 | wrapper.setProps({ header: { title: "Foo" } }); 29 | 30 | assert.equal( 31 | wrapper 32 | .find(".ds-navigation") 33 | .children() 34 | .at(0) 35 | .type(), 36 | FluentOrText 37 | ); 38 | }); 39 | 40 | it("should render 2 Topics", () => { 41 | wrapper.setProps({ 42 | links: [ 43 | { url: "https://foo.com", name: "foo" }, 44 | { url: "https://bar.com", name: "bar" }, 45 | ], 46 | }); 47 | 48 | assert.lengthOf(wrapper.find("ul").children(), 2); 49 | }); 50 | }); 51 | 52 | describe("", () => { 53 | let wrapper; 54 | 55 | beforeEach(() => { 56 | wrapper = shallow(); 57 | }); 58 | 59 | it("should render", () => { 60 | assert.ok(wrapper.exists()); 61 | assert.equal(wrapper.type(), "li"); 62 | assert.equal( 63 | wrapper 64 | .children() 65 | .at(0) 66 | .type(), 67 | SafeAnchor 68 | ); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import React from "react"; 6 | import { actionCreators as ac } from "common/Actions.jsm"; 7 | import { ModalOverlayWrapper } from "content-src/asrouter/components/ModalOverlay/ModalOverlay"; 8 | 9 | export class DSPrivacyModal extends React.PureComponent { 10 | constructor(props) { 11 | super(props); 12 | this.closeModal = this.closeModal.bind(this); 13 | this.onLinkClick = this.onLinkClick.bind(this); 14 | } 15 | 16 | onLinkClick(event) { 17 | this.props.dispatch( 18 | ac.UserEvent({ 19 | event: "CLICK_PRIVACY_INFO", 20 | source: "DS_PRIVACY_MODAL", 21 | }) 22 | ); 23 | } 24 | 25 | closeModal() { 26 | this.props.dispatch({ 27 | type: `HIDE_PRIVACY_INFO`, 28 | data: {}, 29 | }); 30 | } 31 | 32 | render() { 33 | return ( 34 | 38 |
    39 |

    40 |

    41 | 46 |

    47 |
    48 |
    55 |
    56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/NewTabInit.jsm: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | "use strict"; 5 | 6 | const { actionCreators: ac, actionTypes: at } = ChromeUtils.import( 7 | "resource://activity-stream/common/Actions.jsm" 8 | ); 9 | 10 | /** 11 | * NewTabInit - A placeholder for now. This will send a copy of the state to all 12 | * newly opened tabs. 13 | */ 14 | this.NewTabInit = class NewTabInit { 15 | constructor() { 16 | this._repliedEarlyTabs = new Map(); 17 | } 18 | 19 | reply(target) { 20 | // Skip this reply if we already replied to an early tab 21 | if (this._repliedEarlyTabs.get(target)) { 22 | return; 23 | } 24 | 25 | const action = { 26 | type: at.NEW_TAB_INITIAL_STATE, 27 | data: this.store.getState(), 28 | }; 29 | this.store.dispatch(ac.AlsoToOneContent(action, target)); 30 | 31 | // Remember that this early tab has already gotten a rehydration response in 32 | // case it thought we lost its initial REQUEST and asked again 33 | if (this._repliedEarlyTabs.has(target)) { 34 | this._repliedEarlyTabs.set(target, true); 35 | } 36 | } 37 | 38 | onAction(action) { 39 | switch (action.type) { 40 | case at.NEW_TAB_STATE_REQUEST: 41 | this.reply(action.meta.fromTarget); 42 | break; 43 | case at.NEW_TAB_INIT: 44 | // Initialize data for early tabs that might REQUEST twice 45 | if (action.data.simulated) { 46 | this._repliedEarlyTabs.set(action.data.portID, false); 47 | } 48 | break; 49 | case at.NEW_TAB_UNLOAD: 50 | // Clean up for any tab (no-op if not an early tab) 51 | this._repliedEarlyTabs.delete(action.meta.fromTarget); 52 | break; 53 | } 54 | } 55 | }; 56 | 57 | const EXPORTED_SYMBOLS = ["NewTabInit"]; 58 | -------------------------------------------------------------------------------- /content-src/components/DiscoveryStreamComponents/DSTextPromo/_DSTextPromo.scss: -------------------------------------------------------------------------------- 1 | .ds-dismiss-ds-text-promo { 2 | width: 744px; 3 | margin: auto; 4 | } 5 | 6 | .ds-text-promo { 7 | display: flex; 8 | max-width: 640px; 9 | margin: 18px 24px; 10 | 11 | .ds-image { 12 | width: 40px; 13 | height: 40px; 14 | margin: 4px 12px 0 0; 15 | flex-shrink: 0; 16 | 17 | img { 18 | border-radius: 4px; 19 | } 20 | } 21 | 22 | .text { 23 | line-height: 24px; 24 | } 25 | 26 | h3 { 27 | @include dark-theme-only { 28 | color: $grey-10; 29 | } 30 | 31 | margin: 0; 32 | font-weight: 600; 33 | font-size: 15px; 34 | } 35 | 36 | .subtitle { 37 | @include dark-theme-only { 38 | color: $grey-40; 39 | } 40 | 41 | font-size: 13px; 42 | margin: 0; 43 | color: $grey-50; 44 | } 45 | } 46 | 47 | .ds-chevron-link { 48 | color: $blue-60; 49 | display: inline-block; 50 | outline: 0; 51 | 52 | &:hover { 53 | text-decoration: underline; 54 | } 55 | 56 | &:active { 57 | @include dark-theme-only { 58 | color: $blue-50; 59 | } 60 | 61 | color: $blue-70; 62 | 63 | &::after { 64 | @include dark-theme-only { 65 | background-color: $blue-50; 66 | } 67 | 68 | background-color: $blue-70; 69 | } 70 | } 71 | 72 | &:focus { 73 | @include dark-theme-only { 74 | box-shadow: 0 0 0 2px $grey-80, 0 0 0 5px $blue-50-50; 75 | } 76 | 77 | box-shadow: 0 0 0 2px $white, 0 0 0 5px $blue-50-50; 78 | border-radius: 2px; 79 | } 80 | 81 | &::after { 82 | @include dark-theme-only { 83 | background-color: $blue-40; 84 | } 85 | 86 | content: ' '; 87 | mask: url('#{$image-path}glyph-caret-right.svg') 0 -8px no-repeat; 88 | background-color: $blue-60; 89 | margin: 0 0 0 4px; 90 | width: 5px; 91 | height: 8px; 92 | text-decoration: none; 93 | display: inline-block; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /jar.mn: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | browser.jar: 6 | % resource activity-stream %res/activity-stream/ contentaccessible=yes 7 | res/activity-stream/lib/ (./lib/*) 8 | res/activity-stream/common/ (./common/*) 9 | res/activity-stream/vendor/Redux.jsm (./vendor/Redux.jsm) 10 | res/activity-stream/vendor/react.js (./vendor/react.js) 11 | res/activity-stream/vendor/react-dom.js (./vendor/react-dom.js) 12 | #ifndef RELEASE_OR_BETA 13 | res/activity-stream/vendor/react-dev.js (./vendor/react-dev.js) 14 | res/activity-stream/vendor/react-dom-dev.js (./vendor/react-dom-dev.js) 15 | #endif 16 | res/activity-stream/vendor/prop-types.js (./vendor/prop-types.js) 17 | res/activity-stream/vendor/react-transition-group.js (./vendor/react-transition-group.js) 18 | res/activity-stream/vendor/redux.js (./vendor/redux.js) 19 | res/activity-stream/vendor/react-redux.js (./vendor/react-redux.js) 20 | res/activity-stream/data/content/assets/ (./data/content/assets/*) 21 | res/activity-stream/data/content/tippytop/ (./data/content/tippytop/*) 22 | res/activity-stream/data/content/activity-stream.bundle.js (./data/content/activity-stream.bundle.js) 23 | #ifdef XP_MACOSX 24 | res/activity-stream/css/activity-stream.css (./css/activity-stream-mac.css) 25 | #elifdef XP_WIN 26 | res/activity-stream/css/activity-stream.css (./css/activity-stream-windows.css) 27 | #else 28 | res/activity-stream/css/activity-stream.css (./css/activity-stream-linux.css) 29 | #endif 30 | res/activity-stream/prerendered/activity-stream.html (./prerendered/activity-stream.html) 31 | #ifndef RELEASE_OR_BETA 32 | res/activity-stream/prerendered/activity-stream-debug.html (./prerendered/activity-stream-debug.html) 33 | #endif 34 | res/activity-stream/prerendered/activity-stream-noscripts.html (./prerendered/activity-stream-noscripts.html) 35 | -------------------------------------------------------------------------------- /test/unit/content-src/components/DiscoveryStreamComponents/DSDismiss.test.jsx: -------------------------------------------------------------------------------- 1 | import { DSDismiss } from "content-src/components/DiscoveryStreamComponents/DSDismiss/DSDismiss"; 2 | import React from "react"; 3 | import { shallow } from "enzyme"; 4 | 5 | describe("", () => { 6 | const fakeSpoc = { 7 | url: "https://foo.com", 8 | guid: "1234", 9 | }; 10 | let wrapper; 11 | let sandbox; 12 | let dispatchStub; 13 | 14 | beforeEach(() => { 15 | sandbox = sinon.createSandbox(); 16 | dispatchStub = sandbox.stub(); 17 | wrapper = shallow( 18 | 23 | ); 24 | }); 25 | 26 | afterEach(() => { 27 | sandbox.restore(); 28 | }); 29 | 30 | it("should render", () => { 31 | assert.ok(wrapper.exists()); 32 | assert.ok(wrapper.find(".ds-dismiss").exists()); 33 | }); 34 | 35 | it("should render proper hover state", () => { 36 | wrapper.instance().onHover(); 37 | assert.ok(wrapper.find(".hovering").exists()); 38 | wrapper.instance().offHover(); 39 | assert.ok(!wrapper.find(".hovering").exists()); 40 | }); 41 | 42 | it("should dispatch a BlockUrl event on click", () => { 43 | wrapper.instance().onDismissClick(); 44 | 45 | assert.calledThrice(dispatchStub); 46 | assert.deepEqual(dispatchStub.firstCall.args[0].data, { 47 | url: "https://foo.com", 48 | pocket_id: undefined, 49 | }); 50 | assert.deepEqual(dispatchStub.secondCall.args[0].data, { 51 | event: "BLOCK", 52 | source: "DISCOVERY_STREAM", 53 | action_position: 0, 54 | url: "https://foo.com", 55 | guid: "1234", 56 | }); 57 | assert.deepEqual(dispatchStub.thirdCall.args[0].data, { 58 | source: "DISCOVERY_STREAM", 59 | block: 0, 60 | tiles: [ 61 | { 62 | id: "1234", 63 | pos: 0, 64 | }, 65 | ], 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /nsIAboutNewTabService.idl: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | #include "nsISupports.idl" 6 | 7 | /** 8 | * Allows to override about:newtab to point to a different location 9 | * than the one specified within AboutRedirector.cpp 10 | */ 11 | 12 | [scriptable, uuid(dfcd2adc-7867-4d3a-ba70-17501f208142)] 13 | interface nsIAboutNewTabService : nsISupports 14 | { 15 | /** 16 | * Returns the url of the resource for the newtab page if not overridden, 17 | * otherwise a string represenation of the new URL. 18 | */ 19 | attribute ACString newTabURL; 20 | 21 | /** 22 | * Returns the default URL (local or activity stream depending on pref) 23 | */ 24 | attribute ACString defaultURL; 25 | 26 | /** 27 | * Returns the about:welcome URL. 28 | */ 29 | attribute ACString welcomeURL; 30 | 31 | /** 32 | * Returns true if opening the New Tab page will notify the user of a change. 33 | */ 34 | attribute bool willNotifyUser; 35 | 36 | /** 37 | * Returns true if the default resource got overridden. 38 | */ 39 | readonly attribute bool overridden; 40 | 41 | /** 42 | * Returns true if the default resource is activity stream and isn't 43 | * overridden 44 | */ 45 | readonly attribute bool activityStreamEnabled; 46 | 47 | /** 48 | * Returns true if the the debug pref for activity stream is true 49 | */ 50 | readonly attribute bool activityStreamDebug; 51 | 52 | /** 53 | * Resets to the default resource and also resets the 54 | * overridden attribute to false. 55 | */ 56 | void resetNewTabURL(); 57 | 58 | /** 59 | * Records a scalar metric for how long it takes to pain Top Sites, this will 60 | * only record the first timestamp, all the subsequent calls will be ignored. 61 | */ 62 | void maybeRecordTopsitesPainted(in unsigned long long timestamp); 63 | }; 64 | -------------------------------------------------------------------------------- /data/content/assets/trailhead/card-illo-tracking.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Firefox Home (New Tab) [Deprecated Version] 2 | 3 | This repository is no longer updated or used. We're keeping it around for 4 | those few occasions when it's useful for doing code & bugfix archaeology by 5 | looking at issues and PRs. 6 | 7 | Please do not file new issues or PRs; they will not be triaged. Issues are now 8 | tracked on Bugzilla, in `Firefox: New Tab Page` and `Firefox: Messaging 9 | System`. 10 | 11 | More current links: 12 | 13 | * Docs for [Firefox Home (New Tab)](https://firefox-source-docs.mozilla.org/browser/components/newtab/docs/index.html) 14 | * Docs for [Messaging System](https://firefox-source-docs.mozilla.org/browser/components/newtab/content-src/asrouter/docs/index.html) 15 | * [Code](https://searchfox.org/mozilla-central/source/browser/components/newtab) 16 | 17 | -------------- 18 | 19 | The files in this directory, including vendor dependencies, are exported to the 20 | browser/components/newtab/ directory in mozilla central. 21 | 22 | Read [docs/v2-system-addon](https://github.com/mozilla/activity-stream/tree/master/docs/v2-system-addon/1.GETTING_STARTED.md) for more detail on how to develop on and use this repository. 23 | 24 | ## Where should I file bugs? 25 | 26 | We regularly check the ActivityStream:NewTab component on Bugzilla. 27 | 28 | ## For Developers 29 | 30 | If you are interested in contributing, take a look at [this guide](contributing.md) on where to find us and how to contribute, 31 | and [this guide](docs/v2-system-addon/1.GETTING_STARTED.md) for getting your development environment set up. 32 | 33 | ## For Localizers 34 | 35 | Firefox Home localization is managed via [Pontoon](https://pontoon.mozilla.org/projects/activity-stream-new-tab/), not direct pull requests to the repository. If you want to fix a typo, add a new language, or simply know more about localization, please get in touch with the [existing localization team](https://pontoon.mozilla.org/teams/) for your language, or Mozilla’s [l10n-drivers](https://wiki.mozilla.org/L10n:Mozilla_Team#Mozilla_Corporation) for guidance. 36 | -------------------------------------------------------------------------------- /lib/TippyTopProvider.jsm: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | const { XPCOMUtils } = ChromeUtils.import( 6 | "resource://gre/modules/XPCOMUtils.jsm" 7 | ); 8 | 9 | XPCOMUtils.defineLazyGlobalGetters(this, ["fetch", "URL"]); 10 | 11 | const TIPPYTOP_JSON_PATH = 12 | "resource://activity-stream/data/content/tippytop/top_sites.json"; 13 | const TIPPYTOP_URL_PREFIX = 14 | "resource://activity-stream/data/content/tippytop/images/"; 15 | 16 | function getDomain(url) { 17 | let domain; 18 | try { 19 | domain = new URL(url).hostname; 20 | } catch (ex) {} 21 | if (domain && domain.startsWith("www.")) { 22 | domain = domain.slice(4); 23 | } 24 | return domain; 25 | } 26 | 27 | this.TippyTopProvider = class TippyTopProvider { 28 | constructor() { 29 | this._sitesByDomain = new Map(); 30 | this.initialized = false; 31 | } 32 | 33 | async init() { 34 | // Load the Tippy Top sites from the json manifest. 35 | try { 36 | for (const site of await (await fetch(TIPPYTOP_JSON_PATH, { 37 | credentials: "omit", 38 | })).json()) { 39 | // The tippy top manifest can have a url property (string) or a 40 | // urls property (array of strings) 41 | for (const url of site.url ? [site.url] : site.urls || []) { 42 | this._sitesByDomain.set(getDomain(url), site); 43 | } 44 | } 45 | this.initialized = true; 46 | } catch (error) { 47 | Cu.reportError("Failed to load tippy top manifest."); 48 | } 49 | } 50 | 51 | processSite(site) { 52 | const tippyTop = this._sitesByDomain.get(getDomain(site.url)); 53 | if (tippyTop) { 54 | site.tippyTopIcon = TIPPYTOP_URL_PREFIX + tippyTop.image_url; 55 | site.backgroundColor = tippyTop.background_color; 56 | } 57 | return site; 58 | } 59 | }; 60 | 61 | const EXPORTED_SYMBOLS = ["TippyTopProvider", "getDomain"]; 62 | -------------------------------------------------------------------------------- /loaders/inject-loader.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | // Note: this is based on https://github.com/plasticine/inject-loader, 6 | // patched to make istanbul work properly 7 | 8 | const loaderUtils = require("loader-utils"); 9 | const QUOTE_REGEX_STRING = "['|\"]{1}"; 10 | 11 | const hasOnlyExcludeFlags = query => 12 | Object.keys(query).filter(key => query[key] === true).length === 0; 13 | const escapePath = path => path.replace("/", "\\/"); 14 | 15 | function createRequireStringRegex(query) { 16 | const regexArray = []; 17 | 18 | // if there is no query then replace everything 19 | if (Object.keys(query).length === 0) { 20 | regexArray.push("([^\\)]+)"); 21 | } else if (hasOnlyExcludeFlags(query)) { 22 | // if there are only negation matches in the query then replace everything 23 | // except them 24 | Object.keys(query).forEach(key => 25 | regexArray.push(`(?!${QUOTE_REGEX_STRING}${escapePath(key)})`) 26 | ); 27 | regexArray.push("([^\\)]+)"); 28 | } else { 29 | regexArray.push(`(${QUOTE_REGEX_STRING}(`); 30 | regexArray.push( 31 | Object.keys(query) 32 | .map(key => escapePath(key)) 33 | .join("|") 34 | ); 35 | regexArray.push(`)${QUOTE_REGEX_STRING})`); 36 | } 37 | 38 | // Wrap the regex to match `require()` 39 | regexArray.unshift("require\\("); 40 | regexArray.push("\\)"); 41 | 42 | return new RegExp(regexArray.join(""), "g"); 43 | } 44 | 45 | module.exports = function inject(src) { 46 | if (this.cacheable) { 47 | this.cacheable(); 48 | } 49 | const regex = createRequireStringRegex(loaderUtils.getOptions(this) || {}); 50 | 51 | return `module.exports = function inject(injections) { 52 | var module = {exports: {}}; 53 | var exports = module.exports; 54 | ${src.replace(regex, "(injections[$1] || /* istanbul ignore next */ $&)")} 55 | return module.exports; 56 | }\n`; 57 | }; 58 | -------------------------------------------------------------------------------- /mochitest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export SHELL=/bin/bash 4 | export TASKCLUSTER_ROOT_URL="https://taskcluster.net" 5 | # Display required for `browser_parsable_css` tests 6 | export DISPLAY=:99.0 7 | # Required to support the unicode in the output 8 | export LC_ALL=C.UTF-8 9 | /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16 -extension RANDR 10 | 11 | # Pull latest m-c and update tip 12 | cd /mozilla-central && hg pull && hg update -C 13 | 14 | # Build Activity Stream and copy the output to m-c 15 | cd /activity-stream && npm install . && npm run buildmc 16 | 17 | # Build latest m-c with Activity Stream changes 18 | cd /mozilla-central && rm -rf ./objdir-frontend && ./mach build \ 19 | && ./mach lint browser/components/newtab \ 20 | && ./mach lint -l codespell browser/locales/en-US/browser/newtab \ 21 | && ./mach test browser/components/newtab/test/browser --headless \ 22 | && ./mach test browser/components/newtab/test/xpcshell \ 23 | && ./mach test --log-tbpl test_run_log \ 24 | browser/base/content/test/about/browser_aboutHome_search_telemetry.js \ 25 | browser/base/content/test/static/browser_parsable_css.js \ 26 | browser/base/content/test/tabs/browser_new_tab_in_privileged_process_pref.js \ 27 | browser/components/enterprisepolicies/tests/browser/browser_policy_set_homepage.js \ 28 | browser/components/extensions/test/browser/browser_ext_topSites.js \ 29 | browser/components/preferences/in-content/tests/browser_hometab_restore_defaults.js \ 30 | browser/components/preferences/in-content/tests/browser_newtab_menu.js \ 31 | browser/components/preferences/in-content/tests/browser_search_subdialogs_within_preferences_1.js \ 32 | browser/components/search/test/browser/browser_google_behavior.js \ 33 | browser/modules/test/browser/browser_UsageTelemetry_content.js \ 34 | && ! grep -q TEST-UNEXPECTED test_run_log \ 35 | && RUN_FIND_DUPES=1 ./mach package \ 36 | && ./mach test --appname=dist all_files_referenced --headless 37 | -------------------------------------------------------------------------------- /content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor.jsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm"; 6 | import React from "react"; 7 | 8 | export class SafeAnchor extends React.PureComponent { 9 | constructor(props) { 10 | super(props); 11 | this.onClick = this.onClick.bind(this); 12 | } 13 | 14 | onClick(event) { 15 | // Use dispatch instead of normal link click behavior to include referrer 16 | if (this.props.dispatch) { 17 | event.preventDefault(); 18 | const { altKey, button, ctrlKey, metaKey, shiftKey } = event; 19 | this.props.dispatch( 20 | ac.OnlyToMain({ 21 | type: at.OPEN_LINK, 22 | data: { 23 | event: { altKey, button, ctrlKey, metaKey, shiftKey }, 24 | referrer: "https://getpocket.com/recommendations", 25 | // Use the anchor's url, which could have been cleaned up 26 | url: event.currentTarget.href, 27 | }, 28 | }) 29 | ); 30 | } 31 | 32 | // Propagate event if there's a handler 33 | if (this.props.onLinkClick) { 34 | this.props.onLinkClick(event); 35 | } 36 | } 37 | 38 | safeURI(url) { 39 | let protocol = null; 40 | try { 41 | protocol = new URL(url).protocol; 42 | } catch (e) { 43 | return ""; 44 | } 45 | 46 | const isAllowed = ["http:", "https:"].includes(protocol); 47 | if (!isAllowed) { 48 | console.warn(`${url} is not allowed for anchor targets.`); // eslint-disable-line no-console 49 | return ""; 50 | } 51 | return url; 52 | } 53 | 54 | render() { 55 | const { url, className } = this.props; 56 | return ( 57 | 58 | {this.props.children} 59 | 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/unit/asrouter/templates/isEmailOrPhoneNumber.test.js: -------------------------------------------------------------------------------- 1 | import { isEmailOrPhoneNumber } from "content-src/asrouter/templates/SendToDeviceSnippet/isEmailOrPhoneNumber"; 2 | 3 | const CONTENT = {}; 4 | 5 | describe("isEmailOrPhoneNumber", () => { 6 | it("should return 'email' for emails", () => { 7 | assert.equal(isEmailOrPhoneNumber("foobar@asd.com", CONTENT), "email"); 8 | assert.equal(isEmailOrPhoneNumber("foobar@asd.co.uk", CONTENT), "email"); 9 | }); 10 | it("should return 'phone' for valid en-US/en-CA phone numbers", () => { 11 | assert.equal( 12 | isEmailOrPhoneNumber("14582731273", { locale: "en-US" }), 13 | "phone" 14 | ); 15 | assert.equal( 16 | isEmailOrPhoneNumber("4582731273", { locale: "en-CA" }), 17 | "phone" 18 | ); 19 | }); 20 | it("should return an empty string for invalid phone number lengths in en-US/en-CA", () => { 21 | // Not enough digits 22 | assert.equal(isEmailOrPhoneNumber("4522", { locale: "en-US" }), ""); 23 | assert.equal(isEmailOrPhoneNumber("4522", { locale: "en-CA" }), ""); 24 | }); 25 | it("should return 'phone' for valid German phone numbers", () => { 26 | assert.equal( 27 | isEmailOrPhoneNumber("145827312732", { locale: "de" }), 28 | "phone" 29 | ); 30 | }); 31 | it("should return 'phone' for any number of digits in other locales", () => { 32 | assert.equal(isEmailOrPhoneNumber("4", CONTENT), "phone"); 33 | }); 34 | it("should return an empty string for other invalid inputs", () => { 35 | assert.equal( 36 | isEmailOrPhoneNumber("abc", CONTENT), 37 | "", 38 | "abc should be invalid" 39 | ); 40 | assert.equal( 41 | isEmailOrPhoneNumber("abc@", CONTENT), 42 | "", 43 | "abc@ should be invalid" 44 | ); 45 | assert.equal( 46 | isEmailOrPhoneNumber("abc@foo", CONTENT), 47 | "", 48 | "abc@foo should be invalid" 49 | ); 50 | assert.equal( 51 | isEmailOrPhoneNumber("123d1232", CONTENT), 52 | "", 53 | "123d1232 should be invalid" 54 | ); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /data/content/assets/trailhead/card-illo-sendtab.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /test/unit/content-src/components/DiscoveryStreamComponents/DSMessage.test.jsx: -------------------------------------------------------------------------------- 1 | import { DSMessage } from "content-src/components/DiscoveryStreamComponents/DSMessage/DSMessage"; 2 | import React from "react"; 3 | import { SafeAnchor } from "content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor"; 4 | import { FluentOrText } from "content-src/components/FluentOrText/FluentOrText"; 5 | import { mount } from "enzyme"; 6 | 7 | describe("", () => { 8 | let wrapper; 9 | 10 | beforeEach(() => { 11 | wrapper = mount(); 12 | }); 13 | 14 | it("should render", () => { 15 | assert.ok(wrapper.exists()); 16 | assert.ok(wrapper.find(".ds-message").exists()); 17 | }); 18 | 19 | it("should render an icon", () => { 20 | wrapper.setProps({ icon: "foo" }); 21 | 22 | assert.ok(wrapper.find(".glyph").exists()); 23 | assert.propertyVal( 24 | wrapper.find(".glyph").props().style, 25 | "backgroundImage", 26 | `url(foo)` 27 | ); 28 | }); 29 | 30 | it("should render a title", () => { 31 | wrapper.setProps({ title: "foo" }); 32 | 33 | assert.ok(wrapper.find(".title-text").exists()); 34 | assert.equal(wrapper.find(".title-text").text(), "foo"); 35 | }); 36 | 37 | it("should render a SafeAnchor", () => { 38 | wrapper.setProps({ link_text: "foo", link_url: "https://foo.com" }); 39 | 40 | assert.equal( 41 | wrapper 42 | .find(".title") 43 | .children() 44 | .at(0) 45 | .type(), 46 | SafeAnchor 47 | ); 48 | }); 49 | 50 | it("should render a FluentOrText", () => { 51 | wrapper.setProps({ 52 | link_text: "link_text", 53 | title: "title", 54 | link_url: "https://link_url.com", 55 | }); 56 | 57 | assert.equal( 58 | wrapper 59 | .find(".title-text") 60 | .children() 61 | .at(0) 62 | .type(), 63 | FluentOrText 64 | ); 65 | 66 | assert.equal( 67 | wrapper 68 | .find(".link a") 69 | .children() 70 | .at(0) 71 | .type(), 72 | FluentOrText 73 | ); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import { cardContextTypes } from "../../Card/types.js"; 6 | import { CSSTransition, TransitionGroup } from "react-transition-group"; 7 | import React from "react"; 8 | 9 | // Animation time is mirrored in DSContextFooter.scss 10 | const ANIMATION_DURATION = 3000; 11 | 12 | export const StatusMessage = ({ icon, fluentID }) => ( 13 |
    14 | 18 |
    19 |
    20 | ); 21 | 22 | export class DSContextFooter extends React.PureComponent { 23 | render() { 24 | // display_engagement_labels is based on pref `browser.newtabpage.activity-stream.discoverystream.engagementLabelEnabled` 25 | const { 26 | context, 27 | context_type, 28 | engagement, 29 | display_engagement_labels, 30 | } = this.props; 31 | const { icon, fluentID } = cardContextTypes[context_type] || {}; 32 | 33 | return ( 34 |
    35 | {context &&

    {context}

    } 36 | 37 | {!context && 38 | (context_type || (display_engagement_labels && engagement)) && ( 39 | 44 | {engagement && !context_type ? ( 45 |
    {engagement}
    46 | ) : ( 47 | 48 | )} 49 |
    50 | )} 51 |
    52 |
    53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/xpcshell/test_ASRouterTargeting_attribution.js: -------------------------------------------------------------------------------- 1 | /* Any copyright is dedicated to the Public Domain. 2 | * http://creativecommons.org/publicdomain/zero/1.0/ 3 | */ 4 | 5 | "use strict"; 6 | 7 | const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 8 | const { AttributionCode } = ChromeUtils.import( 9 | "resource:///modules/AttributionCode.jsm" 10 | ); 11 | const { ASRouterTargeting } = ChromeUtils.import( 12 | "resource://activity-stream/lib/ASRouterTargeting.jsm" 13 | ); 14 | 15 | add_task(async function check_attribution_data() { 16 | // Some setup to fake the correct attribution data 17 | const appPath = Services.dirsvc.get("GreD", Ci.nsIFile).parent.parent.path; 18 | const attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService( 19 | Ci.nsIMacAttributionService 20 | ); 21 | const campaign = "non-fx-button"; 22 | const source = "addons.mozilla.org"; 23 | const referrer = `https://allizom.org/anything/?utm_campaign=${campaign}&utm_source=${source}`; 24 | attributionSvc.setReferrerUrl(appPath, referrer, true); 25 | AttributionCode._clearCache(); 26 | AttributionCode.getAttrDataAsync(); 27 | 28 | const { 29 | campaign: attributionCampain, 30 | source: attributionSource, 31 | } = ASRouterTargeting.Environment.attributionData; 32 | equal( 33 | attributionCampain, 34 | campaign, 35 | "should get the correct campaign out of attributionData" 36 | ); 37 | equal( 38 | attributionSource, 39 | source, 40 | "should get the correct source out of attributionData" 41 | ); 42 | 43 | const messages = [ 44 | { 45 | id: "foo1", 46 | targeting: 47 | "attributionData.campaign == 'back_to_school' && attributionData.source == 'addons.mozilla.org'", 48 | }, 49 | { 50 | id: "foo2", 51 | targeting: 52 | "attributionData.campaign == 'non-fx-button' && attributionData.source == 'addons.mozilla.org'", 53 | }, 54 | ]; 55 | 56 | equal( 57 | await ASRouterTargeting.findMatchingMessage({ messages }), 58 | messages[1], 59 | "should select the message with the correct campaign and source" 60 | ); 61 | AttributionCode._clearCache(); 62 | }); 63 | -------------------------------------------------------------------------------- /content-src/components/DiscoveryStreamComponents/DSEmptyState/_DSEmptyState.scss: -------------------------------------------------------------------------------- 1 | .section-empty-state { 2 | border: $border-secondary; 3 | border-radius: 4px; 4 | display: flex; 5 | height: $card-height-compact; 6 | width: 100%; 7 | 8 | .empty-state-message { 9 | color: var(--newtab-text-secondary-color); 10 | font-size: 14px; 11 | line-height: 20px; 12 | text-align: center; 13 | margin: auto; 14 | max-width: 936px; 15 | } 16 | 17 | .try-again-button { 18 | margin-top: 12px; 19 | padding: 6px 32px; 20 | border-radius: 2px; 21 | border: 0; 22 | background: var(--newtab-feed-button-background); 23 | color: var(--newtab-feed-button-text); 24 | cursor: pointer; 25 | position: relative; 26 | transition: background 0.2s ease, color 0.2s ease; 27 | 28 | &:not(.waiting) { 29 | &:focus { 30 | @include ds-fade-in; 31 | 32 | @include dark-theme-only { 33 | @include ds-fade-in($blue-40-40); 34 | } 35 | } 36 | 37 | &:hover { 38 | @include ds-fade-in($grey-30); 39 | 40 | @include dark-theme-only { 41 | @include ds-fade-in($grey-60); 42 | } 43 | } 44 | } 45 | 46 | &::after { 47 | content: ''; 48 | height: 20px; 49 | width: 20px; 50 | animation: spinner 1s linear infinite; 51 | opacity: 0; 52 | position: absolute; 53 | top: 50%; 54 | left: 50%; 55 | margin: -10px 0 0 -10px; 56 | mask-image: url('../data/content/assets/spinner.svg'); 57 | mask-size: 20px; 58 | background: var(--newtab-feed-button-spinner); 59 | } 60 | 61 | &.waiting { 62 | cursor: initial; 63 | background: var(--newtab-feed-button-background-faded); 64 | color: var(--newtab-feed-button-text-faded); 65 | transition: background 0.2s ease; 66 | 67 | &::after { 68 | transition: opacity 0.2s ease; 69 | opacity: 1; 70 | } 71 | } 72 | } 73 | 74 | h2 { 75 | font-size: 15px; 76 | font-weight: 600; 77 | margin: 0; 78 | } 79 | 80 | p { 81 | margin: 0; 82 | } 83 | } 84 | 85 | @keyframes spinner { 86 | to { transform: rotate(360deg); } 87 | } 88 | -------------------------------------------------------------------------------- /content-src/components/ErrorBoundary/ErrorBoundary.jsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import { A11yLinkButton } from "content-src/components/A11yLinkButton/A11yLinkButton"; 6 | import React from "react"; 7 | 8 | export class ErrorBoundaryFallback extends React.PureComponent { 9 | constructor(props) { 10 | super(props); 11 | this.windowObj = this.props.windowObj || window; 12 | this.onClick = this.onClick.bind(this); 13 | } 14 | 15 | /** 16 | * Since we only get here if part of the page has crashed, do a 17 | * forced reload to give us the best chance at recovering. 18 | */ 19 | onClick() { 20 | this.windowObj.location.reload(true); 21 | } 22 | 23 | render() { 24 | const defaultClass = "as-error-fallback"; 25 | let className; 26 | if ("className" in this.props) { 27 | className = `${this.props.className} ${defaultClass}`; 28 | } else { 29 | className = defaultClass; 30 | } 31 | 32 | // "A11yLinkButton" to force normal link styling stuff (eg cursor on hover) 33 | return ( 34 |
    35 |
    36 | 37 | 42 | 43 |
    44 | ); 45 | } 46 | } 47 | ErrorBoundaryFallback.defaultProps = { className: "as-error-fallback" }; 48 | 49 | export class ErrorBoundary extends React.PureComponent { 50 | constructor(props) { 51 | super(props); 52 | this.state = { hasError: false }; 53 | } 54 | 55 | componentDidCatch(error, info) { 56 | this.setState({ hasError: true }); 57 | } 58 | 59 | render() { 60 | if (!this.state.hasError) { 61 | return this.props.children; 62 | } 63 | 64 | return ; 65 | } 66 | } 67 | 68 | ErrorBoundary.defaultProps = { FallbackComponent: ErrorBoundaryFallback }; 69 | -------------------------------------------------------------------------------- /test/browser/browser_asrouter_snippets.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { ASRouter } = ChromeUtils.import( 4 | "resource://activity-stream/lib/ASRouter.jsm" 5 | ); 6 | const { SnippetsTestMessageProvider } = ChromeUtils.import( 7 | "resource://activity-stream/lib/SnippetsTestMessageProvider.jsm" 8 | ); 9 | 10 | test_newtab({ 11 | async before() { 12 | let data = SnippetsTestMessageProvider.getMessages().find( 13 | m => m.id === "SIMPLE_BELOW_SEARCH_TEST_1" 14 | ); 15 | ASRouter.messageChannel.sendAsyncMessage("ASRouter:parent-to-child", { 16 | type: "SET_MESSAGE", 17 | data, 18 | }); 19 | }, 20 | test: async function test_simple_below_search_snippet() { 21 | // Verify the simple_below_search_snippet renders in container below searchbox 22 | // and nothing is rendered in the footer. 23 | await ContentTaskUtils.waitForCondition( 24 | () => 25 | content.document.querySelector( 26 | ".below-search-snippet .SimpleBelowSearchSnippet" 27 | ), 28 | "Should find the snippet inside the below search container" 29 | ); 30 | 31 | is( 32 | 0, 33 | content.document.querySelector("#footer-asrouter-container").childNodes 34 | .length, 35 | "Should not find any snippets in the footer container" 36 | ); 37 | }, 38 | }); 39 | 40 | test_newtab({ 41 | async before() { 42 | let data = SnippetsTestMessageProvider.getMessages().find( 43 | m => m.id === "SIMPLE_TEST_1" 44 | ); 45 | ASRouter.messageChannel.sendAsyncMessage("ASRouter:parent-to-child", { 46 | type: "SET_MESSAGE", 47 | data, 48 | }); 49 | }, 50 | test: async function test_simple_snippet() { 51 | // Verify the simple_snippet renders in the footer and the container below 52 | // searchbox is not rendered. 53 | await ContentTaskUtils.waitForCondition( 54 | () => 55 | content.document.querySelector( 56 | "#footer-asrouter-container .SimpleSnippet" 57 | ), 58 | "Should find the snippet inside the footer container" 59 | ); 60 | 61 | ok( 62 | !content.document.querySelector(".below-search-snippet"), 63 | "Should not find any snippets below search" 64 | ); 65 | }, 66 | }); 67 | -------------------------------------------------------------------------------- /content-src/components/ContextMenu/ContextMenuButton.jsx: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | import React from "react"; 6 | 7 | export class ContextMenuButton extends React.PureComponent { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | showContextMenu: false, 12 | contextMenuKeyboard: false, 13 | }; 14 | this.onClick = this.onClick.bind(this); 15 | this.onKeyDown = this.onKeyDown.bind(this); 16 | this.onUpdate = this.onUpdate.bind(this); 17 | } 18 | 19 | openContextMenu(isKeyBoard, event) { 20 | if (this.props.onUpdate) { 21 | this.props.onUpdate(true); 22 | } 23 | this.setState({ 24 | showContextMenu: true, 25 | contextMenuKeyboard: isKeyBoard, 26 | }); 27 | } 28 | 29 | onClick(event) { 30 | event.preventDefault(); 31 | this.openContextMenu(false, event); 32 | } 33 | 34 | onKeyDown(event) { 35 | if (event.key === "Enter" || event.key === " ") { 36 | event.preventDefault(); 37 | this.openContextMenu(true, event); 38 | } 39 | } 40 | 41 | onUpdate(showContextMenu) { 42 | if (this.props.onUpdate) { 43 | this.props.onUpdate(showContextMenu); 44 | } 45 | this.setState({ showContextMenu }); 46 | } 47 | 48 | render() { 49 | const { tooltipArgs, tooltip, children, refFunction } = this.props; 50 | const { showContextMenu, contextMenuKeyboard } = this.state; 51 | 52 | return ( 53 | 54 |