1063 lines
31 KiB
JavaScript
1063 lines
31 KiB
JavaScript
/* 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/. */
|
|
|
|
import { actionTypes as at } from "resource://newtab/common/Actions.mjs";
|
|
import { Dedupe } from "resource:///modules/Dedupe.sys.mjs";
|
|
|
|
export {
|
|
TOP_SITES_DEFAULT_ROWS,
|
|
TOP_SITES_MAX_SITES_PER_ROW,
|
|
} from "resource:///modules/topsites/constants.mjs";
|
|
|
|
const PREF_COLLECTION_DISMISSIBLE = "discoverystream.isCollectionDismissible";
|
|
|
|
const dedupe = new Dedupe(site => site && site.url);
|
|
|
|
export const INITIAL_STATE = {
|
|
App: {
|
|
// Have we received real data from the app yet?
|
|
initialized: false,
|
|
locale: "",
|
|
isForStartupCache: false,
|
|
customizeMenuVisible: false,
|
|
},
|
|
Ads: {
|
|
initialized: false,
|
|
lastUpdated: null,
|
|
topsites: {},
|
|
},
|
|
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: [],
|
|
// The "Share-of-Voice" allocations generated by TopSitesFeed
|
|
sov: {
|
|
ready: false,
|
|
positions: [
|
|
// {position: 0, assignedPartner: "amp"},
|
|
// {position: 1, assignedPartner: "moz-sales"},
|
|
],
|
|
},
|
|
},
|
|
Prefs: {
|
|
initialized: false,
|
|
values: { featureConfig: {} },
|
|
},
|
|
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: [],
|
|
isPrivacyInfoModalVisible: false,
|
|
isCollectionDismissible: false,
|
|
topicsLoading: false,
|
|
feeds: {
|
|
data: {
|
|
// "https://foo.com/feed1": {lastUpdated: 123, data: [], personalized: false}
|
|
},
|
|
loaded: false,
|
|
},
|
|
// Used to show impressions in newtab devtools.
|
|
impressions: {
|
|
feed: {},
|
|
},
|
|
// Used to show blocks in newtab devtools.
|
|
blocks: {},
|
|
spocs: {
|
|
spocs_endpoint: "",
|
|
lastUpdated: null,
|
|
data: {
|
|
// "spocs": {title: "", context: "", items: [], personalized: false},
|
|
// "placement1": {title: "", context: "", items: [], personalized: false},
|
|
},
|
|
loaded: false,
|
|
frequency_caps: [],
|
|
blocked: [],
|
|
placements: [],
|
|
},
|
|
experimentData: {
|
|
utmSource: "pocket-newtab",
|
|
utmCampaign: undefined,
|
|
utmContent: undefined,
|
|
},
|
|
recentSavesData: [],
|
|
isUserLoggedIn: false,
|
|
recentSavesEnabled: false,
|
|
showTopicSelection: false,
|
|
report: {
|
|
visible: false,
|
|
data: {},
|
|
},
|
|
},
|
|
// Messages received from ASRouter to render in newtab
|
|
Messages: {
|
|
// messages received from ASRouter are initially visible
|
|
isHidden: false,
|
|
// portID for that tab that was sent the message
|
|
portID: "",
|
|
// READONLY Message data received from ASRouter
|
|
messageData: {},
|
|
},
|
|
Notifications: {
|
|
showNotifications: false,
|
|
toastCounter: 0,
|
|
toastId: "",
|
|
// This queue is reset each time SHOW_TOAST_MESSAGE is ran.
|
|
// For can be a queue in the future, but for now is one item
|
|
toastQueue: [],
|
|
},
|
|
Personalization: {
|
|
lastUpdated: null,
|
|
initialized: false,
|
|
},
|
|
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,
|
|
},
|
|
Wallpapers: {
|
|
wallpaperList: [],
|
|
highlightSeenCounter: 0,
|
|
categories: [],
|
|
uploadedWallpaper: "",
|
|
},
|
|
Weather: {
|
|
initialized: false,
|
|
lastUpdated: null,
|
|
query: "",
|
|
suggestions: [],
|
|
locationData: {
|
|
city: "",
|
|
adminArea: "",
|
|
country: "",
|
|
},
|
|
// Display search input in Weather widget
|
|
searchActive: false,
|
|
locationSearchString: "",
|
|
suggestedLocations: [],
|
|
},
|
|
};
|
|
|
|
function App(prevState = INITIAL_STATE.App, action) {
|
|
switch (action.type) {
|
|
case at.INIT:
|
|
return Object.assign({}, prevState, action.data || {}, {
|
|
initialized: true,
|
|
});
|
|
case at.TOP_SITES_UPDATED:
|
|
// Toggle `isForStartupCache` when receiving the `TOP_SITES_UPDATE` action
|
|
// so that sponsored tiles can be rendered as usual. See Bug 1826360.
|
|
return Object.assign({}, prevState, action.data || {}, {
|
|
isForStartupCache: false,
|
|
});
|
|
case at.DISCOVERY_STREAM_SPOCS_UPDATE:
|
|
// Toggle `isForStartupCache` when receiving the `DISCOVERY_STREAM_SPOCS_UPDATE_STARTUPCACHE` action
|
|
// so that spoc cards can be rendered as usual.
|
|
return Object.assign({}, prevState, action.data || {}, {
|
|
isForStartupCache: false,
|
|
});
|
|
case at.SHOW_PERSONALIZE:
|
|
return Object.assign({}, prevState, {
|
|
customizeMenuVisible: true,
|
|
});
|
|
case at.HIDE_PERSONALIZE:
|
|
return Object.assign({}, prevState, {
|
|
customizeMenuVisible: false,
|
|
});
|
|
default:
|
|
return prevState;
|
|
}
|
|
}
|
|
|
|
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_BOOKMARKS_REMOVED:
|
|
if (!action.data) {
|
|
return prevState;
|
|
}
|
|
newRows = prevState.rows.map(site => {
|
|
if (site && action.data.urls.includes(site.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_LINKS_DELETED:
|
|
if (!action.data) {
|
|
return prevState;
|
|
}
|
|
newRows = prevState.rows.filter(
|
|
site => !action.data.urls.includes(site.url)
|
|
);
|
|
return Object.assign({}, prevState, { rows: newRows });
|
|
case at.UPDATE_SEARCH_SHORTCUTS:
|
|
return { ...prevState, searchShortcuts: action.data.searchShortcuts };
|
|
case at.SOV_UPDATED: {
|
|
const sov = {
|
|
ready: action.data.ready,
|
|
positions: action.data.positions,
|
|
};
|
|
return { ...prevState, sov };
|
|
}
|
|
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.DIALOG_CLOSE:
|
|
// Reset and hide the confirmation dialog once the action is complete.
|
|
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);
|
|
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 &&
|
|
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_BOOKMARKS_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 (action.data.urls.includes(item.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_LINKS_DELETED:
|
|
if (!action.data) {
|
|
return prevState;
|
|
}
|
|
return prevState.map(section =>
|
|
Object.assign({}, section, {
|
|
rows: section.rows.filter(
|
|
site => !action.data.urls.includes(site.url)
|
|
),
|
|
})
|
|
);
|
|
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
|
|
),
|
|
})
|
|
);
|
|
default:
|
|
return prevState;
|
|
}
|
|
}
|
|
|
|
function Messages(prevState = INITIAL_STATE.Messages, action) {
|
|
switch (action.type) {
|
|
case at.MESSAGE_SET:
|
|
if (prevState.messageData.messageType) {
|
|
return prevState;
|
|
}
|
|
return {
|
|
...prevState,
|
|
messageData: action.data.message,
|
|
portID: action.data.portID || "",
|
|
};
|
|
case at.MESSAGE_TOGGLE_VISIBILITY:
|
|
return { ...prevState, isHidden: action.data };
|
|
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 Personalization(prevState = INITIAL_STATE.Personalization, action) {
|
|
switch (action.type) {
|
|
case at.DISCOVERY_STREAM_PERSONALIZATION_LAST_UPDATED:
|
|
return {
|
|
...prevState,
|
|
lastUpdated: action.data.lastUpdated,
|
|
};
|
|
case at.DISCOVERY_STREAM_PERSONALIZATION_INIT:
|
|
return {
|
|
...prevState,
|
|
initialized: true,
|
|
};
|
|
case at.DISCOVERY_STREAM_PERSONALIZATION_RESET:
|
|
return { ...INITIAL_STATE.Personalization };
|
|
default:
|
|
return prevState;
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line complexity
|
|
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 handlePlacements = handleSites => {
|
|
const { data, placements } = prevState.spocs;
|
|
const result = {};
|
|
|
|
const forPlacement = placement => {
|
|
const placementSpocs = data[placement.name];
|
|
|
|
if (
|
|
!placementSpocs ||
|
|
!placementSpocs.items ||
|
|
!placementSpocs.items.length
|
|
) {
|
|
return;
|
|
}
|
|
|
|
result[placement.name] = {
|
|
...placementSpocs,
|
|
items: handleSites(placementSpocs.items),
|
|
};
|
|
};
|
|
|
|
if (!placements || !placements.length) {
|
|
[{ name: "spocs" }].forEach(forPlacement);
|
|
} else {
|
|
placements.forEach(forPlacement);
|
|
}
|
|
return result;
|
|
};
|
|
|
|
const nextState = handleSites => ({
|
|
...prevState,
|
|
spocs: {
|
|
...prevState.spocs,
|
|
data: handlePlacements(handleSites),
|
|
},
|
|
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:
|
|
// Fall through to 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_EXPERIMENT_DATA:
|
|
return { ...prevState, experimentData: action.data || {} };
|
|
case at.DISCOVERY_STREAM_LAYOUT_UPDATE:
|
|
return {
|
|
...prevState,
|
|
layout: action.data.layout || [],
|
|
};
|
|
case at.DISCOVERY_STREAM_COLLECTION_DISMISSIBLE_TOGGLE:
|
|
return {
|
|
...prevState,
|
|
isCollectionDismissible: action.data.value,
|
|
};
|
|
case at.DISCOVERY_STREAM_TOPICS_LOADING:
|
|
return {
|
|
...prevState,
|
|
topicsLoading: action.data,
|
|
};
|
|
case at.DISCOVERY_STREAM_PREFS_SETUP:
|
|
return {
|
|
...prevState,
|
|
recentSavesEnabled: action.data.recentSavesEnabled,
|
|
pocketButtonEnabled: action.data.pocketButtonEnabled,
|
|
saveToPocketCard: action.data.saveToPocketCard,
|
|
hideDescriptions: action.data.hideDescriptions,
|
|
compactImages: action.data.compactImages,
|
|
imageGradient: action.data.imageGradient,
|
|
newSponsoredLabel: action.data.newSponsoredLabel,
|
|
titleLines: action.data.titleLines,
|
|
descLines: action.data.descLines,
|
|
readTime: action.data.readTime,
|
|
};
|
|
case at.DISCOVERY_STREAM_RECENT_SAVES:
|
|
return {
|
|
...prevState,
|
|
recentSavesData: action.data.recentSaves,
|
|
};
|
|
case at.DISCOVERY_STREAM_POCKET_STATE_SET:
|
|
return {
|
|
...prevState,
|
|
isUserLoggedIn: action.data.isUserLoggedIn,
|
|
};
|
|
case at.HIDE_PRIVACY_INFO:
|
|
return {
|
|
...prevState,
|
|
isPrivacyInfoModalVisible: false,
|
|
};
|
|
case at.SHOW_PRIVACY_INFO:
|
|
return {
|
|
...prevState,
|
|
isPrivacyInfoModalVisible: true,
|
|
};
|
|
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_DEV_IMPRESSIONS:
|
|
return {
|
|
...prevState,
|
|
impressions: {
|
|
...prevState.impressions,
|
|
feed: action.data,
|
|
},
|
|
};
|
|
case at.DISCOVERY_STREAM_DEV_BLOCKS:
|
|
return {
|
|
...prevState,
|
|
blocks: action.data,
|
|
};
|
|
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.url ||
|
|
INITIAL_STATE.DiscoveryStream.spocs.spocs_endpoint,
|
|
},
|
|
};
|
|
case at.DISCOVERY_STREAM_SPOCS_PLACEMENTS:
|
|
return {
|
|
...prevState,
|
|
spocs: {
|
|
...prevState.spocs,
|
|
placements:
|
|
action.data.placements ||
|
|
INITIAL_STATE.DiscoveryStream.spocs.placements,
|
|
},
|
|
};
|
|
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_SPOC_BLOCKED:
|
|
return {
|
|
...prevState,
|
|
spocs: {
|
|
...prevState.spocs,
|
|
blocked: [...prevState.spocs.blocked, action.data.url],
|
|
},
|
|
};
|
|
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,
|
|
context_type: "pocket",
|
|
});
|
|
}
|
|
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,
|
|
context_type: "bookmark",
|
|
});
|
|
}
|
|
return item;
|
|
};
|
|
return isNotReady()
|
|
? prevState
|
|
: nextState(items => items.map(updateBookmarkInfo));
|
|
}
|
|
case at.PLACES_BOOKMARKS_REMOVED: {
|
|
const removeBookmarkInfo = item => {
|
|
if (action.data.urls.includes(item.url)) {
|
|
const newSite = Object.assign({}, item);
|
|
delete newSite.bookmarkGuid;
|
|
delete newSite.bookmarkTitle;
|
|
delete newSite.bookmarkDateCreated;
|
|
if (!newSite.context_type || newSite.context_type === "bookmark") {
|
|
newSite.context_type = "removedBookmark";
|
|
}
|
|
return newSite;
|
|
}
|
|
return item;
|
|
};
|
|
return isNotReady()
|
|
? prevState
|
|
: nextState(items => items.map(removeBookmarkInfo));
|
|
}
|
|
case at.PREF_CHANGED:
|
|
if (action.data.name === PREF_COLLECTION_DISMISSIBLE) {
|
|
return {
|
|
...prevState,
|
|
isCollectionDismissible: action.data.value,
|
|
};
|
|
}
|
|
return prevState;
|
|
case at.TOPIC_SELECTION_SPOTLIGHT_OPEN:
|
|
return {
|
|
...prevState,
|
|
showTopicSelection: true,
|
|
};
|
|
case at.TOPIC_SELECTION_SPOTLIGHT_CLOSE:
|
|
return {
|
|
...prevState,
|
|
showTopicSelection: false,
|
|
};
|
|
case at.SECTION_BLOCKED:
|
|
return {
|
|
...prevState,
|
|
showBlockSectionConfirmation: true,
|
|
sectionData: action.data,
|
|
};
|
|
case at.REPORT_AD_OPEN:
|
|
return {
|
|
...prevState,
|
|
report: {
|
|
...prevState.report,
|
|
card_type: action.data?.card_type,
|
|
position: action.data?.position,
|
|
placement_id: action.data?.placement_id,
|
|
reporting_url: action.data?.reporting_url,
|
|
url: action.data?.url,
|
|
visible: true,
|
|
},
|
|
};
|
|
case at.REPORT_CONTENT_OPEN:
|
|
return {
|
|
...prevState,
|
|
report: {
|
|
...prevState.report,
|
|
card_type: action.data?.card_type,
|
|
corpus_item_id: action.data?.corpus_item_id,
|
|
is_section_followed: action.data?.is_section_followed,
|
|
received_rank: action.data?.received_rank,
|
|
recommended_at: action.data?.recommended_at,
|
|
scheduled_corpus_item_id: action.data?.scheduled_corpus_item_id,
|
|
section_position: action.data?.section_position,
|
|
section: action.data?.section,
|
|
title: action.data?.title,
|
|
topic: action.data?.topic,
|
|
url: action.data?.url,
|
|
visible: true,
|
|
},
|
|
};
|
|
case at.REPORT_CLOSE:
|
|
case at.REPORT_AD_SUBMIT:
|
|
case at.REPORT_CONTENT_SUBMIT:
|
|
return {
|
|
...prevState,
|
|
report: {
|
|
...prevState.report,
|
|
visible: false,
|
|
},
|
|
};
|
|
default:
|
|
return prevState;
|
|
}
|
|
}
|
|
|
|
function Search(prevState = INITIAL_STATE.Search, action) {
|
|
switch (action.type) {
|
|
case at.DISABLE_SEARCH:
|
|
return Object.assign({ ...prevState, disable: true });
|
|
case at.FAKE_FOCUS_SEARCH:
|
|
return Object.assign({ ...prevState, fakeFocus: true });
|
|
case at.SHOW_SEARCH:
|
|
return Object.assign({ ...prevState, disable: false, fakeFocus: false });
|
|
default:
|
|
return prevState;
|
|
}
|
|
}
|
|
|
|
function Wallpapers(prevState = INITIAL_STATE.Wallpapers, action) {
|
|
switch (action.type) {
|
|
case at.WALLPAPERS_SET:
|
|
return {
|
|
...prevState,
|
|
wallpaperList: action.data,
|
|
};
|
|
case at.WALLPAPERS_FEATURE_HIGHLIGHT_COUNTER_INCREMENT:
|
|
return {
|
|
...prevState,
|
|
highlightSeenCounter: action.data,
|
|
};
|
|
case at.WALLPAPERS_CATEGORY_SET:
|
|
return { ...prevState, categories: action.data };
|
|
case at.WALLPAPERS_CUSTOM_SET:
|
|
return { ...prevState, uploadedWallpaper: action.data };
|
|
default:
|
|
return prevState;
|
|
}
|
|
}
|
|
|
|
function Notifications(prevState = INITIAL_STATE.Notifications, action) {
|
|
switch (action.type) {
|
|
case at.SHOW_TOAST_MESSAGE:
|
|
return {
|
|
...prevState,
|
|
showNotifications: action.data.showNotifications,
|
|
toastCounter: prevState.toastCounter + 1,
|
|
toastId: action.data.toastId,
|
|
toastQueue: [action.data.toastId],
|
|
};
|
|
case at.HIDE_TOAST_MESSAGE: {
|
|
const { showNotifications, toastId: hiddenToastId } = action.data;
|
|
const queuedToasts = [...prevState.toastQueue].filter(
|
|
toastId => toastId !== hiddenToastId
|
|
);
|
|
return {
|
|
...prevState,
|
|
toastCounter: queuedToasts.length,
|
|
toastQueue: queuedToasts,
|
|
toastId: "",
|
|
showNotifications,
|
|
};
|
|
}
|
|
default:
|
|
return prevState;
|
|
}
|
|
}
|
|
|
|
function Weather(prevState = INITIAL_STATE.Weather, action) {
|
|
switch (action.type) {
|
|
case at.WEATHER_UPDATE:
|
|
return {
|
|
...prevState,
|
|
suggestions: action.data.suggestions,
|
|
lastUpdated: action.data.date,
|
|
locationData: action.data.locationData || prevState.locationData,
|
|
initialized: true,
|
|
};
|
|
case at.WEATHER_SEARCH_ACTIVE:
|
|
return { ...prevState, searchActive: action.data };
|
|
case at.WEATHER_LOCATION_SEARCH_UPDATE:
|
|
return { ...prevState, locationSearchString: action.data };
|
|
case at.WEATHER_LOCATION_SUGGESTIONS_UPDATE:
|
|
return { ...prevState, suggestedLocations: action.data };
|
|
case at.WEATHER_LOCATION_DATA_UPDATE:
|
|
return { ...prevState, locationData: action.data };
|
|
default:
|
|
return prevState;
|
|
}
|
|
}
|
|
|
|
function Ads(prevState = INITIAL_STATE.Ads, action) {
|
|
switch (action.type) {
|
|
case at.ADS_INIT:
|
|
return {
|
|
...prevState,
|
|
initialized: true,
|
|
};
|
|
case at.ADS_UPDATE_DATA:
|
|
return {
|
|
...prevState,
|
|
topsites: action.data,
|
|
};
|
|
default:
|
|
return prevState;
|
|
}
|
|
}
|
|
|
|
export const reducers = {
|
|
TopSites,
|
|
App,
|
|
Ads,
|
|
Prefs,
|
|
Dialog,
|
|
Sections,
|
|
Messages,
|
|
Notifications,
|
|
Pocket,
|
|
Personalization,
|
|
DiscoveryStream,
|
|
Search,
|
|
Wallpapers,
|
|
Weather,
|
|
};
|