123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636 |
- /* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
- "use strict";
- const {actionTypes: at} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm");
- const {Dedupe} = ChromeUtils.import("resource://activity-stream/common/Dedupe.jsm");
- const TOP_SITES_DEFAULT_ROWS = 1;
- const TOP_SITES_MAX_SITES_PER_ROW = 8;
- const dedupe = new Dedupe(site => site && site.url);
- const INITIAL_STATE = {
- App: {
- // Have we received real data from the app yet?
- initialized: false,
- },
- ASRouter: {initialized: false},
- Snippets: {initialized: false},
- TopSites: {
- // Have we received real data from history yet?
- initialized: false,
- // The history (and possibly default) links
- rows: [],
- // Used in content only to dispatch action to TopSiteForm.
- editForm: null,
- // Used in content only to open the SearchShortcutsForm modal.
- showSearchShortcutsForm: false,
- // The list of available search shortcuts.
- searchShortcuts: [],
- },
- Prefs: {
- initialized: false,
- values: {},
- },
- Dialog: {
- visible: false,
- data: {},
- },
- Sections: [],
- Pocket: {
- isUserLoggedIn: null,
- pocketCta: {},
- waitingForSpoc: true,
- },
- // This is the new pocket configurable layout state.
- DiscoveryStream: {
- // This is a JSON-parsed copy of the discoverystream.config pref value.
- config: {enabled: false, layout_endpoint: ""},
- layout: [],
- lastUpdated: null,
- feeds: {
- data: {
- // "https://foo.com/feed1": {lastUpdated: 123, data: []}
- },
- loaded: false,
- },
- spocs: {
- spocs_endpoint: "",
- lastUpdated: null,
- data: {}, // {spocs: []}
- loaded: false,
- frequency_caps: [],
- },
- },
- Search: {
- // When search hand-off is enabled, we render a big button that is styled to
- // look like a search textbox. If the button is clicked, we style
- // the button as if it was a focused search box and show a fake cursor but
- // really focus the awesomebar without the focus styles ("hidden focus").
- fakeFocus: false,
- // Hide the search box after handing off to AwesomeBar and user starts typing.
- hide: false,
- },
- };
- function App(prevState = INITIAL_STATE.App, action) {
- switch (action.type) {
- case at.INIT:
- return Object.assign({}, prevState, action.data || {}, {initialized: true});
- default:
- return prevState;
- }
- }
- function ASRouter(prevState = INITIAL_STATE.ASRouter, action) {
- switch (action.type) {
- case at.AS_ROUTER_INITIALIZED:
- return {...action.data, initialized: true};
- default:
- return prevState;
- }
- }
- /**
- * insertPinned - Inserts pinned links in their specified slots
- *
- * @param {array} a list of links
- * @param {array} a list of pinned links
- * @return {array} resulting list of links with pinned links inserted
- */
- function insertPinned(links, pinned) {
- // Remove any pinned links
- const pinnedUrls = pinned.map(link => link && link.url);
- let newLinks = links.filter(link => (link ? !pinnedUrls.includes(link.url) : false));
- newLinks = newLinks.map(link => {
- if (link && link.isPinned) {
- delete link.isPinned;
- delete link.pinIndex;
- }
- return link;
- });
- // Then insert them in their specified location
- pinned.forEach((val, index) => {
- if (!val) { return; }
- let link = Object.assign({}, val, {isPinned: true, pinIndex: index});
- if (index > newLinks.length) {
- newLinks[index] = link;
- } else {
- newLinks.splice(index, 0, link);
- }
- });
- return newLinks;
- }
- function TopSites(prevState = INITIAL_STATE.TopSites, action) {
- let hasMatch;
- let newRows;
- switch (action.type) {
- case at.TOP_SITES_UPDATED:
- if (!action.data || !action.data.links) {
- return prevState;
- }
- return Object.assign({}, prevState, {initialized: true, rows: action.data.links}, action.data.pref ? {pref: action.data.pref} : {});
- case at.TOP_SITES_PREFS_UPDATED:
- return Object.assign({}, prevState, {pref: action.data.pref});
- case at.TOP_SITES_EDIT:
- return Object.assign({}, prevState, {
- editForm: {
- index: action.data.index,
- previewResponse: null,
- },
- });
- case at.TOP_SITES_CANCEL_EDIT:
- return Object.assign({}, prevState, {editForm: null});
- case at.TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL:
- return Object.assign({}, prevState, {showSearchShortcutsForm: true});
- case at.TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL:
- return Object.assign({}, prevState, {showSearchShortcutsForm: false});
- case at.PREVIEW_RESPONSE:
- if (!prevState.editForm || action.data.url !== prevState.editForm.previewUrl) {
- return prevState;
- }
- return Object.assign({}, prevState, {
- editForm: {
- index: prevState.editForm.index,
- previewResponse: action.data.preview,
- previewUrl: action.data.url,
- },
- });
- case at.PREVIEW_REQUEST:
- if (!prevState.editForm) {
- return prevState;
- }
- return Object.assign({}, prevState, {
- editForm: {
- index: prevState.editForm.index,
- previewResponse: null,
- previewUrl: action.data.url,
- },
- });
- case at.PREVIEW_REQUEST_CANCEL:
- if (!prevState.editForm) {
- return prevState;
- }
- return Object.assign({}, prevState, {
- editForm: {
- index: prevState.editForm.index,
- previewResponse: null,
- },
- });
- case at.SCREENSHOT_UPDATED:
- newRows = prevState.rows.map(row => {
- if (row && row.url === action.data.url) {
- hasMatch = true;
- return Object.assign({}, row, {screenshot: action.data.screenshot});
- }
- return row;
- });
- return hasMatch ? Object.assign({}, prevState, {rows: newRows}) : prevState;
- case at.PLACES_BOOKMARK_ADDED:
- if (!action.data) {
- return prevState;
- }
- newRows = prevState.rows.map(site => {
- if (site && site.url === action.data.url) {
- const {bookmarkGuid, bookmarkTitle, dateAdded} = action.data;
- return Object.assign({}, site, {bookmarkGuid, bookmarkTitle, bookmarkDateCreated: dateAdded});
- }
- return site;
- });
- return Object.assign({}, prevState, {rows: newRows});
- case at.PLACES_BOOKMARK_REMOVED:
- if (!action.data) {
- return prevState;
- }
- newRows = prevState.rows.map(site => {
- if (site && site.url === action.data.url) {
- const newSite = Object.assign({}, site);
- delete newSite.bookmarkGuid;
- delete newSite.bookmarkTitle;
- delete newSite.bookmarkDateCreated;
- return newSite;
- }
- return site;
- });
- return Object.assign({}, prevState, {rows: newRows});
- case at.PLACES_LINK_DELETED:
- if (!action.data) {
- return prevState;
- }
- newRows = prevState.rows.filter(site => action.data.url !== site.url);
- return Object.assign({}, prevState, {rows: newRows});
- case at.UPDATE_SEARCH_SHORTCUTS:
- return {...prevState, searchShortcuts: action.data.searchShortcuts};
- case at.SNIPPETS_PREVIEW_MODE:
- return {...prevState, rows: []};
- default:
- return prevState;
- }
- }
- function Dialog(prevState = INITIAL_STATE.Dialog, action) {
- switch (action.type) {
- case at.DIALOG_OPEN:
- return Object.assign({}, prevState, {visible: true, data: action.data});
- case at.DIALOG_CANCEL:
- return Object.assign({}, prevState, {visible: false});
- case at.DELETE_HISTORY_URL:
- return Object.assign({}, INITIAL_STATE.Dialog);
- default:
- return prevState;
- }
- }
- function Prefs(prevState = INITIAL_STATE.Prefs, action) {
- let newValues;
- switch (action.type) {
- case at.PREFS_INITIAL_VALUES:
- return Object.assign({}, prevState, {initialized: true, values: action.data});
- case at.PREF_CHANGED:
- newValues = Object.assign({}, prevState.values);
- newValues[action.data.name] = action.data.value;
- return Object.assign({}, prevState, {values: newValues});
- default:
- return prevState;
- }
- }
- function Sections(prevState = INITIAL_STATE.Sections, action) {
- let hasMatch;
- let newState;
- switch (action.type) {
- case at.SECTION_DEREGISTER:
- return prevState.filter(section => section.id !== action.data);
- case at.SECTION_REGISTER:
- // If section exists in prevState, update it
- newState = prevState.map(section => {
- if (section && section.id === action.data.id) {
- hasMatch = true;
- return Object.assign({}, section, action.data);
- }
- return section;
- });
- // Otherwise, append it
- if (!hasMatch) {
- const initialized = !!(action.data.rows && action.data.rows.length > 0);
- const section = Object.assign({title: "", rows: [], enabled: false}, action.data, {initialized});
- newState.push(section);
- }
- return newState;
- case at.SECTION_UPDATE:
- newState = prevState.map(section => {
- if (section && section.id === action.data.id) {
- // If the action is updating rows, we should consider initialized to be true.
- // This can be overridden if initialized is defined in the action.data
- const initialized = action.data.rows ? {initialized: true} : {};
- // Make sure pinned cards stay at their current position when rows are updated.
- // Disabling a section (SECTION_UPDATE with empty rows) does not retain pinned cards.
- if (action.data.rows && action.data.rows.length > 0 && section.rows.find(card => card.pinned)) {
- const rows = Array.from(action.data.rows);
- section.rows.forEach((card, index) => {
- if (card.pinned) {
- // Only add it if it's not already there.
- if (rows[index].guid !== card.guid) {
- rows.splice(index, 0, card);
- }
- }
- });
- return Object.assign({}, section, initialized, Object.assign({}, action.data, {rows}));
- }
- return Object.assign({}, section, initialized, action.data);
- }
- return section;
- });
- if (!action.data.dedupeConfigurations) {
- return newState;
- }
- action.data.dedupeConfigurations.forEach(dedupeConf => {
- newState = newState.map(section => {
- if (section.id === dedupeConf.id) {
- const dedupedRows = dedupeConf.dedupeFrom.reduce((rows, dedupeSectionId) => {
- const dedupeSection = newState.find(s => s.id === dedupeSectionId);
- const [, newRows] = dedupe.group(dedupeSection.rows, rows);
- return newRows;
- }, section.rows);
- return Object.assign({}, section, {rows: dedupedRows});
- }
- return section;
- });
- });
- return newState;
- case at.SECTION_UPDATE_CARD:
- return prevState.map(section => {
- if (section && section.id === action.data.id && section.rows) {
- const newRows = section.rows.map(card => {
- if (card.url === action.data.url) {
- return Object.assign({}, card, action.data.options);
- }
- return card;
- });
- return Object.assign({}, section, {rows: newRows});
- }
- return section;
- });
- case at.PLACES_BOOKMARK_ADDED:
- if (!action.data) {
- return prevState;
- }
- return prevState.map(section => Object.assign({}, section, {
- rows: section.rows.map(item => {
- // find the item within the rows that is attempted to be bookmarked
- if (item.url === action.data.url) {
- const {bookmarkGuid, bookmarkTitle, dateAdded} = action.data;
- return Object.assign({}, item, {
- bookmarkGuid,
- bookmarkTitle,
- bookmarkDateCreated: dateAdded,
- type: "bookmark",
- });
- }
- return item;
- }),
- }));
- case at.PLACES_SAVED_TO_POCKET:
- if (!action.data) {
- return prevState;
- }
- return prevState.map(section => Object.assign({}, section, {
- rows: section.rows.map(item => {
- if (item.url === action.data.url) {
- return Object.assign({}, item, {
- open_url: action.data.open_url,
- pocket_id: action.data.pocket_id,
- title: action.data.title,
- type: "pocket",
- });
- }
- return item;
- }),
- }));
- case at.PLACES_BOOKMARK_REMOVED:
- if (!action.data) {
- return prevState;
- }
- return prevState.map(section => Object.assign({}, section, {
- rows: section.rows.map(item => {
- // find the bookmark within the rows that is attempted to be removed
- if (item.url === action.data.url) {
- const newSite = Object.assign({}, item);
- delete newSite.bookmarkGuid;
- delete newSite.bookmarkTitle;
- delete newSite.bookmarkDateCreated;
- if (!newSite.type || newSite.type === "bookmark") {
- newSite.type = "history";
- }
- return newSite;
- }
- return item;
- }),
- }));
- case at.PLACES_LINK_DELETED:
- case at.PLACES_LINK_BLOCKED:
- if (!action.data) {
- return prevState;
- }
- return prevState.map(section =>
- Object.assign({}, section, {rows: section.rows.filter(site => site.url !== action.data.url)}));
- case at.DELETE_FROM_POCKET:
- case at.ARCHIVE_FROM_POCKET:
- return prevState.map(section =>
- Object.assign({}, section, {rows: section.rows.filter(site => site.pocket_id !== action.data.pocket_id)}));
- case at.SNIPPETS_PREVIEW_MODE:
- return prevState.map(section => ({...section, rows: []}));
- default:
- return prevState;
- }
- }
- function Snippets(prevState = INITIAL_STATE.Snippets, action) {
- switch (action.type) {
- case at.SNIPPETS_DATA:
- return Object.assign({}, prevState, {initialized: true}, action.data);
- case at.SNIPPET_BLOCKED:
- return Object.assign({}, prevState, {blockList: prevState.blockList.concat(action.data)});
- case at.SNIPPETS_BLOCKLIST_CLEARED:
- return Object.assign({}, prevState, {blockList: []});
- case at.SNIPPETS_RESET:
- return INITIAL_STATE.Snippets;
- default:
- return prevState;
- }
- }
- function Pocket(prevState = INITIAL_STATE.Pocket, action) {
- switch (action.type) {
- case at.POCKET_WAITING_FOR_SPOC:
- return {...prevState, waitingForSpoc: action.data};
- case at.POCKET_LOGGED_IN:
- return {...prevState, isUserLoggedIn: !!action.data};
- case at.POCKET_CTA:
- return {
- ...prevState,
- pocketCta: {
- ctaButton: action.data.cta_button,
- ctaText: action.data.cta_text,
- ctaUrl: action.data.cta_url,
- useCta: action.data.use_cta,
- },
- };
- default:
- return prevState;
- }
- }
- function DiscoveryStream(prevState = INITIAL_STATE.DiscoveryStream, action) {
- // Return if action data is empty, or spocs or feeds data is not loaded
- const isNotReady = () =>
- !action.data || !prevState.spocs.loaded || !prevState.feeds.loaded;
- const nextState = handleSites => (
- {
- ...prevState,
- spocs: {
- ...prevState.spocs,
- data: prevState.spocs.data.spocs ? {
- spocs: handleSites(prevState.spocs.data.spocs),
- } : {},
- },
- feeds: {
- ...prevState.feeds,
- data: Object.keys(prevState.feeds.data).reduce((accumulator, feed_url) => {
- accumulator[feed_url] = {
- data: {
- ...prevState.feeds.data[feed_url].data,
- recommendations: handleSites(prevState.feeds.data[feed_url].data.recommendations),
- },
- };
- return accumulator;
- }, {}),
- },
- });
- switch (action.type) {
- case at.DISCOVERY_STREAM_CONFIG_CHANGE:
- // The reason this is a separate action is so it doesn't trigger a listener update on init
- case at.DISCOVERY_STREAM_CONFIG_SETUP:
- return {...prevState, config: action.data || {}};
- case at.DISCOVERY_STREAM_LAYOUT_UPDATE:
- return {...prevState, lastUpdated: action.data.lastUpdated || null, layout: action.data.layout || []};
- case at.DISCOVERY_STREAM_LAYOUT_RESET:
- return {...INITIAL_STATE.DiscoveryStream, config: prevState.config};
- case at.DISCOVERY_STREAM_FEEDS_UPDATE:
- return {
- ...prevState,
- feeds: {
- ...prevState.feeds,
- loaded: true,
- },
- };
- case at.DISCOVERY_STREAM_FEED_UPDATE:
- const newData = {};
- newData[action.data.url] = action.data.feed;
- return {
- ...prevState,
- feeds: {
- ...prevState.feeds,
- data: {
- ...prevState.feeds.data,
- ...newData,
- },
- },
- };
- case at.DISCOVERY_STREAM_SPOCS_CAPS:
- return {
- ...prevState,
- spocs: {
- ...prevState.spocs,
- frequency_caps: [...prevState.spocs.frequency_caps, ...action.data],
- },
- };
- case at.DISCOVERY_STREAM_SPOCS_ENDPOINT:
- return {
- ...prevState,
- spocs: {
- ...INITIAL_STATE.DiscoveryStream.spocs,
- spocs_endpoint: action.data || INITIAL_STATE.DiscoveryStream.spocs.spocs_endpoint,
- },
- };
- case at.DISCOVERY_STREAM_SPOCS_UPDATE:
- if (action.data) {
- return {
- ...prevState,
- spocs: {
- ...prevState.spocs,
- lastUpdated: action.data.lastUpdated,
- data: action.data.spocs,
- loaded: true,
- },
- };
- }
- return prevState;
- case at.DISCOVERY_STREAM_LINK_BLOCKED:
- return isNotReady() ? prevState :
- nextState(items => items.filter(item => item.url !== action.data.url));
- case at.PLACES_SAVED_TO_POCKET:
- const addPocketInfo = item => {
- if (item.url === action.data.url) {
- return Object.assign({}, item, {
- open_url: action.data.open_url,
- pocket_id: action.data.pocket_id,
- });
- }
- return item;
- };
- return isNotReady() ? prevState :
- nextState(items => items.map(addPocketInfo));
- case at.DELETE_FROM_POCKET:
- case at.ARCHIVE_FROM_POCKET:
- return isNotReady() ? prevState :
- nextState(items => items.filter(item => item.pocket_id !== action.data.pocket_id));
- case at.PLACES_BOOKMARK_ADDED:
- const updateBookmarkInfo = item => {
- if (item.url === action.data.url) {
- const {bookmarkGuid, bookmarkTitle, dateAdded} = action.data;
- return Object.assign({}, item, {
- bookmarkGuid,
- bookmarkTitle,
- bookmarkDateCreated: dateAdded,
- });
- }
- return item;
- };
- return isNotReady() ? prevState :
- nextState(items => items.map(updateBookmarkInfo));
- case at.PLACES_BOOKMARK_REMOVED:
- const removeBookmarkInfo = item => {
- if (item.url === action.data.url) {
- const newSite = Object.assign({}, item);
- delete newSite.bookmarkGuid;
- delete newSite.bookmarkTitle;
- delete newSite.bookmarkDateCreated;
- return newSite;
- }
- return item;
- };
- return isNotReady() ? prevState :
- nextState(items => items.map(removeBookmarkInfo));
- default:
- return prevState;
- }
- }
- function Search(prevState = INITIAL_STATE.Search, action) {
- switch (action.type) {
- case at.HIDE_SEARCH:
- return Object.assign({...prevState, hide: true});
- case at.FAKE_FOCUS_SEARCH:
- return Object.assign({...prevState, fakeFocus: true});
- case at.SHOW_SEARCH:
- return Object.assign({...prevState, hide: false, fakeFocus: false});
- default:
- return prevState;
- }
- }
- this.INITIAL_STATE = INITIAL_STATE;
- this.TOP_SITES_DEFAULT_ROWS = TOP_SITES_DEFAULT_ROWS;
- this.TOP_SITES_MAX_SITES_PER_ROW = TOP_SITES_MAX_SITES_PER_ROW;
- this.reducers = {
- TopSites,
- App,
- ASRouter,
- Snippets,
- Prefs,
- Dialog,
- Sections,
- Pocket,
- DiscoveryStream,
- Search,
- };
- const EXPORTED_SYMBOLS = [
- "reducers",
- "INITIAL_STATE",
- "insertPinned",
- "TOP_SITES_DEFAULT_ROWS",
- "TOP_SITES_MAX_SITES_PER_ROW",
- ];
|