Bug 1881588 - Add Wallpaper component r=home-newtab-reviewers,fluent-reviewers,bolsso...
[gecko.git] / browser / components / newtab / common / Reducers.sys.mjs
blob326217538dddb14ca1ca886b709f87a94f2c75b7
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/. */
5 import { actionTypes as at } from "resource://activity-stream/common/Actions.mjs";
6 import { Dedupe } from "resource://activity-stream/common/Dedupe.sys.mjs";
8 export const TOP_SITES_DEFAULT_ROWS = 1;
9 export const TOP_SITES_MAX_SITES_PER_ROW = 8;
10 const PREF_COLLECTION_DISMISSIBLE = "discoverystream.isCollectionDismissible";
12 const dedupe = new Dedupe(site => site && site.url);
14 export const INITIAL_STATE = {
15   App: {
16     // Have we received real data from the app yet?
17     initialized: false,
18     locale: "",
19     isForStartupCache: false,
20     customizeMenuVisible: false,
21   },
22   ASRouter: { initialized: false },
23   TopSites: {
24     // Have we received real data from history yet?
25     initialized: false,
26     // The history (and possibly default) links
27     rows: [],
28     // Used in content only to dispatch action to TopSiteForm.
29     editForm: null,
30     // Used in content only to open the SearchShortcutsForm modal.
31     showSearchShortcutsForm: false,
32     // The list of available search shortcuts.
33     searchShortcuts: [],
34     // The "Share-of-Voice" allocations generated by TopSitesFeed
35     sov: {
36       ready: false,
37       positions: [
38         // {position: 0, assignedPartner: "amp"},
39         // {position: 1, assignedPartner: "moz-sales"},
40       ],
41     },
42   },
43   Prefs: {
44     initialized: false,
45     values: { featureConfig: {} },
46   },
47   Dialog: {
48     visible: false,
49     data: {},
50   },
51   Sections: [],
52   Pocket: {
53     isUserLoggedIn: null,
54     pocketCta: {},
55     waitingForSpoc: true,
56   },
57   // This is the new pocket configurable layout state.
58   DiscoveryStream: {
59     // This is a JSON-parsed copy of the discoverystream.config pref value.
60     config: { enabled: false },
61     layout: [],
62     isPrivacyInfoModalVisible: false,
63     isCollectionDismissible: false,
64     feeds: {
65       data: {
66         // "https://foo.com/feed1": {lastUpdated: 123, data: [], personalized: false}
67       },
68       loaded: false,
69     },
70     spocs: {
71       spocs_endpoint: "",
72       lastUpdated: null,
73       data: {
74         // "spocs": {title: "", context: "", items: [], personalized: false},
75         // "placement1": {title: "", context: "", items: [], personalized: false},
76       },
77       loaded: false,
78       frequency_caps: [],
79       blocked: [],
80       placements: [],
81     },
82     experimentData: {
83       utmSource: "pocket-newtab",
84       utmCampaign: undefined,
85       utmContent: undefined,
86     },
87     recentSavesData: [],
88     isUserLoggedIn: false,
89     recentSavesEnabled: false,
90   },
91   Personalization: {
92     lastUpdated: null,
93     initialized: false,
94   },
95   Search: {
96     // When search hand-off is enabled, we render a big button that is styled to
97     // look like a search textbox. If the button is clicked, we style
98     // the button as if it was a focused search box and show a fake cursor but
99     // really focus the awesomebar without the focus styles ("hidden focus").
100     fakeFocus: false,
101     // Hide the search box after handing off to AwesomeBar and user starts typing.
102     hide: false,
103   },
104   Wallpapers: {
105     wallpaperList: [],
106   },
109 function App(prevState = INITIAL_STATE.App, action) {
110   switch (action.type) {
111     case at.INIT:
112       return Object.assign({}, prevState, action.data || {}, {
113         initialized: true,
114       });
115     case at.TOP_SITES_UPDATED:
116       // Toggle `isForStartupCache` when receiving the `TOP_SITES_UPDATE` action
117       // so that sponsored tiles can be rendered as usual. See Bug 1826360.
118       return Object.assign({}, prevState, action.data || {}, {
119         isForStartupCache: false,
120       });
121     case at.SHOW_PERSONALIZE:
122       return Object.assign({}, prevState, {
123         customizeMenuVisible: true,
124       });
125     case at.HIDE_PERSONALIZE:
126       return Object.assign({}, prevState, {
127         customizeMenuVisible: false,
128       });
129     default:
130       return prevState;
131   }
134 function ASRouter(prevState = INITIAL_STATE.ASRouter, action) {
135   switch (action.type) {
136     case at.AS_ROUTER_INITIALIZED:
137       return { ...action.data, initialized: true };
138     default:
139       return prevState;
140   }
144  * insertPinned - Inserts pinned links in their specified slots
146  * @param {array} a list of links
147  * @param {array} a list of pinned links
148  * @return {array} resulting list of links with pinned links inserted
149  */
150 export function insertPinned(links, pinned) {
151   // Remove any pinned links
152   const pinnedUrls = pinned.map(link => link && link.url);
153   let newLinks = links.filter(link =>
154     link ? !pinnedUrls.includes(link.url) : false
155   );
156   newLinks = newLinks.map(link => {
157     if (link && link.isPinned) {
158       delete link.isPinned;
159       delete link.pinIndex;
160     }
161     return link;
162   });
164   // Then insert them in their specified location
165   pinned.forEach((val, index) => {
166     if (!val) {
167       return;
168     }
169     let link = Object.assign({}, val, { isPinned: true, pinIndex: index });
170     if (index > newLinks.length) {
171       newLinks[index] = link;
172     } else {
173       newLinks.splice(index, 0, link);
174     }
175   });
177   return newLinks;
180 function TopSites(prevState = INITIAL_STATE.TopSites, action) {
181   let hasMatch;
182   let newRows;
183   switch (action.type) {
184     case at.TOP_SITES_UPDATED:
185       if (!action.data || !action.data.links) {
186         return prevState;
187       }
188       return Object.assign(
189         {},
190         prevState,
191         { initialized: true, rows: action.data.links },
192         action.data.pref ? { pref: action.data.pref } : {}
193       );
194     case at.TOP_SITES_PREFS_UPDATED:
195       return Object.assign({}, prevState, { pref: action.data.pref });
196     case at.TOP_SITES_EDIT:
197       return Object.assign({}, prevState, {
198         editForm: {
199           index: action.data.index,
200           previewResponse: null,
201         },
202       });
203     case at.TOP_SITES_CANCEL_EDIT:
204       return Object.assign({}, prevState, { editForm: null });
205     case at.TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL:
206       return Object.assign({}, prevState, { showSearchShortcutsForm: true });
207     case at.TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL:
208       return Object.assign({}, prevState, { showSearchShortcutsForm: false });
209     case at.PREVIEW_RESPONSE:
210       if (
211         !prevState.editForm ||
212         action.data.url !== prevState.editForm.previewUrl
213       ) {
214         return prevState;
215       }
216       return Object.assign({}, prevState, {
217         editForm: {
218           index: prevState.editForm.index,
219           previewResponse: action.data.preview,
220           previewUrl: action.data.url,
221         },
222       });
223     case at.PREVIEW_REQUEST:
224       if (!prevState.editForm) {
225         return prevState;
226       }
227       return Object.assign({}, prevState, {
228         editForm: {
229           index: prevState.editForm.index,
230           previewResponse: null,
231           previewUrl: action.data.url,
232         },
233       });
234     case at.PREVIEW_REQUEST_CANCEL:
235       if (!prevState.editForm) {
236         return prevState;
237       }
238       return Object.assign({}, prevState, {
239         editForm: {
240           index: prevState.editForm.index,
241           previewResponse: null,
242         },
243       });
244     case at.SCREENSHOT_UPDATED:
245       newRows = prevState.rows.map(row => {
246         if (row && row.url === action.data.url) {
247           hasMatch = true;
248           return Object.assign({}, row, { screenshot: action.data.screenshot });
249         }
250         return row;
251       });
252       return hasMatch
253         ? Object.assign({}, prevState, { rows: newRows })
254         : prevState;
255     case at.PLACES_BOOKMARK_ADDED:
256       if (!action.data) {
257         return prevState;
258       }
259       newRows = prevState.rows.map(site => {
260         if (site && site.url === action.data.url) {
261           const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data;
262           return Object.assign({}, site, {
263             bookmarkGuid,
264             bookmarkTitle,
265             bookmarkDateCreated: dateAdded,
266           });
267         }
268         return site;
269       });
270       return Object.assign({}, prevState, { rows: newRows });
271     case at.PLACES_BOOKMARKS_REMOVED:
272       if (!action.data) {
273         return prevState;
274       }
275       newRows = prevState.rows.map(site => {
276         if (site && action.data.urls.includes(site.url)) {
277           const newSite = Object.assign({}, site);
278           delete newSite.bookmarkGuid;
279           delete newSite.bookmarkTitle;
280           delete newSite.bookmarkDateCreated;
281           return newSite;
282         }
283         return site;
284       });
285       return Object.assign({}, prevState, { rows: newRows });
286     case at.PLACES_LINKS_DELETED:
287       if (!action.data) {
288         return prevState;
289       }
290       newRows = prevState.rows.filter(
291         site => !action.data.urls.includes(site.url)
292       );
293       return Object.assign({}, prevState, { rows: newRows });
294     case at.UPDATE_SEARCH_SHORTCUTS:
295       return { ...prevState, searchShortcuts: action.data.searchShortcuts };
296     case at.SOV_UPDATED:
297       const sov = {
298         ready: action.data.ready,
299         positions: action.data.positions,
300       };
301       return { ...prevState, sov };
302     default:
303       return prevState;
304   }
307 function Dialog(prevState = INITIAL_STATE.Dialog, action) {
308   switch (action.type) {
309     case at.DIALOG_OPEN:
310       return Object.assign({}, prevState, { visible: true, data: action.data });
311     case at.DIALOG_CANCEL:
312       return Object.assign({}, prevState, { visible: false });
313     case at.DELETE_HISTORY_URL:
314       return Object.assign({}, INITIAL_STATE.Dialog);
315     default:
316       return prevState;
317   }
320 function Prefs(prevState = INITIAL_STATE.Prefs, action) {
321   let newValues;
322   switch (action.type) {
323     case at.PREFS_INITIAL_VALUES:
324       return Object.assign({}, prevState, {
325         initialized: true,
326         values: action.data,
327       });
328     case at.PREF_CHANGED:
329       newValues = Object.assign({}, prevState.values);
330       newValues[action.data.name] = action.data.value;
331       return Object.assign({}, prevState, { values: newValues });
332     default:
333       return prevState;
334   }
337 function Sections(prevState = INITIAL_STATE.Sections, action) {
338   let hasMatch;
339   let newState;
340   switch (action.type) {
341     case at.SECTION_DEREGISTER:
342       return prevState.filter(section => section.id !== action.data);
343     case at.SECTION_REGISTER:
344       // If section exists in prevState, update it
345       newState = prevState.map(section => {
346         if (section && section.id === action.data.id) {
347           hasMatch = true;
348           return Object.assign({}, section, action.data);
349         }
350         return section;
351       });
352       // Otherwise, append it
353       if (!hasMatch) {
354         const initialized = !!(action.data.rows && !!action.data.rows.length);
355         const section = Object.assign(
356           { title: "", rows: [], enabled: false },
357           action.data,
358           { initialized }
359         );
360         newState.push(section);
361       }
362       return newState;
363     case at.SECTION_UPDATE:
364       newState = prevState.map(section => {
365         if (section && section.id === action.data.id) {
366           // If the action is updating rows, we should consider initialized to be true.
367           // This can be overridden if initialized is defined in the action.data
368           const initialized = action.data.rows ? { initialized: true } : {};
370           // Make sure pinned cards stay at their current position when rows are updated.
371           // Disabling a section (SECTION_UPDATE with empty rows) does not retain pinned cards.
372           if (
373             action.data.rows &&
374             !!action.data.rows.length &&
375             section.rows.find(card => card.pinned)
376           ) {
377             const rows = Array.from(action.data.rows);
378             section.rows.forEach((card, index) => {
379               if (card.pinned) {
380                 // Only add it if it's not already there.
381                 if (rows[index].guid !== card.guid) {
382                   rows.splice(index, 0, card);
383                 }
384               }
385             });
386             return Object.assign(
387               {},
388               section,
389               initialized,
390               Object.assign({}, action.data, { rows })
391             );
392           }
394           return Object.assign({}, section, initialized, action.data);
395         }
396         return section;
397       });
399       if (!action.data.dedupeConfigurations) {
400         return newState;
401       }
403       action.data.dedupeConfigurations.forEach(dedupeConf => {
404         newState = newState.map(section => {
405           if (section.id === dedupeConf.id) {
406             const dedupedRows = dedupeConf.dedupeFrom.reduce(
407               (rows, dedupeSectionId) => {
408                 const dedupeSection = newState.find(
409                   s => s.id === dedupeSectionId
410                 );
411                 const [, newRows] = dedupe.group(dedupeSection.rows, rows);
412                 return newRows;
413               },
414               section.rows
415             );
417             return Object.assign({}, section, { rows: dedupedRows });
418           }
420           return section;
421         });
422       });
424       return newState;
425     case at.SECTION_UPDATE_CARD:
426       return prevState.map(section => {
427         if (section && section.id === action.data.id && section.rows) {
428           const newRows = section.rows.map(card => {
429             if (card.url === action.data.url) {
430               return Object.assign({}, card, action.data.options);
431             }
432             return card;
433           });
434           return Object.assign({}, section, { rows: newRows });
435         }
436         return section;
437       });
438     case at.PLACES_BOOKMARK_ADDED:
439       if (!action.data) {
440         return prevState;
441       }
442       return prevState.map(section =>
443         Object.assign({}, section, {
444           rows: section.rows.map(item => {
445             // find the item within the rows that is attempted to be bookmarked
446             if (item.url === action.data.url) {
447               const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data;
448               return Object.assign({}, item, {
449                 bookmarkGuid,
450                 bookmarkTitle,
451                 bookmarkDateCreated: dateAdded,
452                 type: "bookmark",
453               });
454             }
455             return item;
456           }),
457         })
458       );
459     case at.PLACES_SAVED_TO_POCKET:
460       if (!action.data) {
461         return prevState;
462       }
463       return prevState.map(section =>
464         Object.assign({}, section, {
465           rows: section.rows.map(item => {
466             if (item.url === action.data.url) {
467               return Object.assign({}, item, {
468                 open_url: action.data.open_url,
469                 pocket_id: action.data.pocket_id,
470                 title: action.data.title,
471                 type: "pocket",
472               });
473             }
474             return item;
475           }),
476         })
477       );
478     case at.PLACES_BOOKMARKS_REMOVED:
479       if (!action.data) {
480         return prevState;
481       }
482       return prevState.map(section =>
483         Object.assign({}, section, {
484           rows: section.rows.map(item => {
485             // find the bookmark within the rows that is attempted to be removed
486             if (action.data.urls.includes(item.url)) {
487               const newSite = Object.assign({}, item);
488               delete newSite.bookmarkGuid;
489               delete newSite.bookmarkTitle;
490               delete newSite.bookmarkDateCreated;
491               if (!newSite.type || newSite.type === "bookmark") {
492                 newSite.type = "history";
493               }
494               return newSite;
495             }
496             return item;
497           }),
498         })
499       );
500     case at.PLACES_LINKS_DELETED:
501       if (!action.data) {
502         return prevState;
503       }
504       return prevState.map(section =>
505         Object.assign({}, section, {
506           rows: section.rows.filter(
507             site => !action.data.urls.includes(site.url)
508           ),
509         })
510       );
511     case at.PLACES_LINK_BLOCKED:
512       if (!action.data) {
513         return prevState;
514       }
515       return prevState.map(section =>
516         Object.assign({}, section, {
517           rows: section.rows.filter(site => site.url !== action.data.url),
518         })
519       );
520     case at.DELETE_FROM_POCKET:
521     case at.ARCHIVE_FROM_POCKET:
522       return prevState.map(section =>
523         Object.assign({}, section, {
524           rows: section.rows.filter(
525             site => site.pocket_id !== action.data.pocket_id
526           ),
527         })
528       );
529     default:
530       return prevState;
531   }
534 function Pocket(prevState = INITIAL_STATE.Pocket, action) {
535   switch (action.type) {
536     case at.POCKET_WAITING_FOR_SPOC:
537       return { ...prevState, waitingForSpoc: action.data };
538     case at.POCKET_LOGGED_IN:
539       return { ...prevState, isUserLoggedIn: !!action.data };
540     case at.POCKET_CTA:
541       return {
542         ...prevState,
543         pocketCta: {
544           ctaButton: action.data.cta_button,
545           ctaText: action.data.cta_text,
546           ctaUrl: action.data.cta_url,
547           useCta: action.data.use_cta,
548         },
549       };
550     default:
551       return prevState;
552   }
555 function Personalization(prevState = INITIAL_STATE.Personalization, action) {
556   switch (action.type) {
557     case at.DISCOVERY_STREAM_PERSONALIZATION_LAST_UPDATED:
558       return {
559         ...prevState,
560         lastUpdated: action.data.lastUpdated,
561       };
562     case at.DISCOVERY_STREAM_PERSONALIZATION_INIT:
563       return {
564         ...prevState,
565         initialized: true,
566       };
567     case at.DISCOVERY_STREAM_PERSONALIZATION_RESET:
568       return { ...INITIAL_STATE.Personalization };
569     default:
570       return prevState;
571   }
574 // eslint-disable-next-line complexity
575 function DiscoveryStream(prevState = INITIAL_STATE.DiscoveryStream, action) {
576   // Return if action data is empty, or spocs or feeds data is not loaded
577   const isNotReady = () =>
578     !action.data || !prevState.spocs.loaded || !prevState.feeds.loaded;
580   const handlePlacements = handleSites => {
581     const { data, placements } = prevState.spocs;
582     const result = {};
584     const forPlacement = placement => {
585       const placementSpocs = data[placement.name];
587       if (
588         !placementSpocs ||
589         !placementSpocs.items ||
590         !placementSpocs.items.length
591       ) {
592         return;
593       }
595       result[placement.name] = {
596         ...placementSpocs,
597         items: handleSites(placementSpocs.items),
598       };
599     };
601     if (!placements || !placements.length) {
602       [{ name: "spocs" }].forEach(forPlacement);
603     } else {
604       placements.forEach(forPlacement);
605     }
606     return result;
607   };
609   const nextState = handleSites => ({
610     ...prevState,
611     spocs: {
612       ...prevState.spocs,
613       data: handlePlacements(handleSites),
614     },
615     feeds: {
616       ...prevState.feeds,
617       data: Object.keys(prevState.feeds.data).reduce(
618         (accumulator, feed_url) => {
619           accumulator[feed_url] = {
620             data: {
621               ...prevState.feeds.data[feed_url].data,
622               recommendations: handleSites(
623                 prevState.feeds.data[feed_url].data.recommendations
624               ),
625             },
626           };
627           return accumulator;
628         },
629         {}
630       ),
631     },
632   });
634   switch (action.type) {
635     case at.DISCOVERY_STREAM_CONFIG_CHANGE:
636     // Fall through to a separate action is so it doesn't trigger a listener update on init
637     case at.DISCOVERY_STREAM_CONFIG_SETUP:
638       return { ...prevState, config: action.data || {} };
639     case at.DISCOVERY_STREAM_EXPERIMENT_DATA:
640       return { ...prevState, experimentData: action.data || {} };
641     case at.DISCOVERY_STREAM_LAYOUT_UPDATE:
642       return {
643         ...prevState,
644         layout: action.data.layout || [],
645       };
646     case at.DISCOVERY_STREAM_COLLECTION_DISMISSIBLE_TOGGLE:
647       return {
648         ...prevState,
649         isCollectionDismissible: action.data.value,
650       };
651     case at.DISCOVERY_STREAM_PREFS_SETUP:
652       return {
653         ...prevState,
654         recentSavesEnabled: action.data.recentSavesEnabled,
655         pocketButtonEnabled: action.data.pocketButtonEnabled,
656         saveToPocketCard: action.data.saveToPocketCard,
657         hideDescriptions: action.data.hideDescriptions,
658         compactImages: action.data.compactImages,
659         imageGradient: action.data.imageGradient,
660         newSponsoredLabel: action.data.newSponsoredLabel,
661         titleLines: action.data.titleLines,
662         descLines: action.data.descLines,
663         readTime: action.data.readTime,
664       };
665     case at.DISCOVERY_STREAM_RECENT_SAVES:
666       return {
667         ...prevState,
668         recentSavesData: action.data.recentSaves,
669       };
670     case at.DISCOVERY_STREAM_POCKET_STATE_SET:
671       return {
672         ...prevState,
673         isUserLoggedIn: action.data.isUserLoggedIn,
674       };
675     case at.HIDE_PRIVACY_INFO:
676       return {
677         ...prevState,
678         isPrivacyInfoModalVisible: false,
679       };
680     case at.SHOW_PRIVACY_INFO:
681       return {
682         ...prevState,
683         isPrivacyInfoModalVisible: true,
684       };
685     case at.DISCOVERY_STREAM_LAYOUT_RESET:
686       return { ...INITIAL_STATE.DiscoveryStream, config: prevState.config };
687     case at.DISCOVERY_STREAM_FEEDS_UPDATE:
688       return {
689         ...prevState,
690         feeds: {
691           ...prevState.feeds,
692           loaded: true,
693         },
694       };
695     case at.DISCOVERY_STREAM_FEED_UPDATE:
696       const newData = {};
697       newData[action.data.url] = action.data.feed;
698       return {
699         ...prevState,
700         feeds: {
701           ...prevState.feeds,
702           data: {
703             ...prevState.feeds.data,
704             ...newData,
705           },
706         },
707       };
708     case at.DISCOVERY_STREAM_SPOCS_CAPS:
709       return {
710         ...prevState,
711         spocs: {
712           ...prevState.spocs,
713           frequency_caps: [...prevState.spocs.frequency_caps, ...action.data],
714         },
715       };
716     case at.DISCOVERY_STREAM_SPOCS_ENDPOINT:
717       return {
718         ...prevState,
719         spocs: {
720           ...INITIAL_STATE.DiscoveryStream.spocs,
721           spocs_endpoint:
722             action.data.url ||
723             INITIAL_STATE.DiscoveryStream.spocs.spocs_endpoint,
724         },
725       };
726     case at.DISCOVERY_STREAM_SPOCS_PLACEMENTS:
727       return {
728         ...prevState,
729         spocs: {
730           ...prevState.spocs,
731           placements:
732             action.data.placements ||
733             INITIAL_STATE.DiscoveryStream.spocs.placements,
734         },
735       };
736     case at.DISCOVERY_STREAM_SPOCS_UPDATE:
737       if (action.data) {
738         return {
739           ...prevState,
740           spocs: {
741             ...prevState.spocs,
742             lastUpdated: action.data.lastUpdated,
743             data: action.data.spocs,
744             loaded: true,
745           },
746         };
747       }
748       return prevState;
749     case at.DISCOVERY_STREAM_SPOC_BLOCKED:
750       return {
751         ...prevState,
752         spocs: {
753           ...prevState.spocs,
754           blocked: [...prevState.spocs.blocked, action.data.url],
755         },
756       };
757     case at.DISCOVERY_STREAM_LINK_BLOCKED:
758       return isNotReady()
759         ? prevState
760         : nextState(items =>
761             items.filter(item => item.url !== action.data.url)
762           );
764     case at.PLACES_SAVED_TO_POCKET:
765       const addPocketInfo = item => {
766         if (item.url === action.data.url) {
767           return Object.assign({}, item, {
768             open_url: action.data.open_url,
769             pocket_id: action.data.pocket_id,
770             context_type: "pocket",
771           });
772         }
773         return item;
774       };
775       return isNotReady()
776         ? prevState
777         : nextState(items => items.map(addPocketInfo));
779     case at.DELETE_FROM_POCKET:
780     case at.ARCHIVE_FROM_POCKET:
781       return isNotReady()
782         ? prevState
783         : nextState(items =>
784             items.filter(item => item.pocket_id !== action.data.pocket_id)
785           );
787     case at.PLACES_BOOKMARK_ADDED:
788       const updateBookmarkInfo = item => {
789         if (item.url === action.data.url) {
790           const { bookmarkGuid, bookmarkTitle, dateAdded } = action.data;
791           return Object.assign({}, item, {
792             bookmarkGuid,
793             bookmarkTitle,
794             bookmarkDateCreated: dateAdded,
795             context_type: "bookmark",
796           });
797         }
798         return item;
799       };
800       return isNotReady()
801         ? prevState
802         : nextState(items => items.map(updateBookmarkInfo));
804     case at.PLACES_BOOKMARKS_REMOVED:
805       const removeBookmarkInfo = item => {
806         if (action.data.urls.includes(item.url)) {
807           const newSite = Object.assign({}, item);
808           delete newSite.bookmarkGuid;
809           delete newSite.bookmarkTitle;
810           delete newSite.bookmarkDateCreated;
811           if (!newSite.context_type || newSite.context_type === "bookmark") {
812             newSite.context_type = "removedBookmark";
813           }
814           return newSite;
815         }
816         return item;
817       };
818       return isNotReady()
819         ? prevState
820         : nextState(items => items.map(removeBookmarkInfo));
821     case at.PREF_CHANGED:
822       if (action.data.name === PREF_COLLECTION_DISMISSIBLE) {
823         return {
824           ...prevState,
825           isCollectionDismissible: action.data.value,
826         };
827       }
828       return prevState;
829     default:
830       return prevState;
831   }
834 function Search(prevState = INITIAL_STATE.Search, action) {
835   switch (action.type) {
836     case at.DISABLE_SEARCH:
837       return Object.assign({ ...prevState, disable: true });
838     case at.FAKE_FOCUS_SEARCH:
839       return Object.assign({ ...prevState, fakeFocus: true });
840     case at.SHOW_SEARCH:
841       return Object.assign({ ...prevState, disable: false, fakeFocus: false });
842     default:
843       return prevState;
844   }
847 function Wallpapers(prevState = INITIAL_STATE.Wallpapers, action) {
848   switch (action.type) {
849     case at.WALLPAPERS_SET:
850       return { wallpaperList: action.data };
851     default:
852       return prevState;
853   }
856 export const reducers = {
857   TopSites,
858   App,
859   ASRouter,
860   Prefs,
861   Dialog,
862   Sections,
863   Pocket,
864   Personalization,
865   DiscoveryStream,
866   Search,
867   Wallpapers,