/* 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 { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { connect } from "react-redux";
import React from "react";
// Pref Constants
const PREF_AD_SIZE_MEDIUM_RECTANGLE = "newtabAdSize.mediumRectangle";
const PREF_AD_SIZE_BILLBOARD = "newtabAdSize.billboard";
const PREF_AD_SIZE_LEADERBOARD = "newtabAdSize.leaderboard";
const PREF_SECTIONS_ENABLED = "discoverystream.sections.enabled";
const PREF_SPOC_PLACEMENTS = "discoverystream.placements.spocs";
const PREF_SPOC_COUNTS = "discoverystream.placements.spocs.counts";
const PREF_CONTEXTUAL_ADS_ENABLED =
"discoverystream.sections.contextualAds.enabled";
const PREF_CONTEXTUAL_BANNER_PLACEMENTS =
"discoverystream.placements.contextualBanners";
const PREF_CONTEXTUAL_BANNER_COUNTS =
"discoverystream.placements.contextualBanners.counts";
const PREF_UNIFIED_ADS_ENABLED = "unifiedAds.spocs.enabled";
const PREF_UNIFIED_ADS_ENDPOINT = "unifiedAds.endpoint";
const PREF_ALLOWED_ENDPOINTS = "discoverystream.endpoints";
const PREF_OHTTP_CONFIG = "discoverystream.ohttp.configURL";
const PREF_OHTTP_RELAY = "discoverystream.ohttp.relayURL";
const Row = props => (
{props.children}
);
function relativeTime(timestamp) {
if (!timestamp) {
return "";
}
const seconds = Math.floor((Date.now() - timestamp) / 1000);
const minutes = Math.floor((Date.now() - timestamp) / 60000);
if (seconds < 2) {
return "just now";
} else if (seconds < 60) {
return `${seconds} seconds ago`;
} else if (minutes === 1) {
return "1 minute ago";
} else if (minutes < 600) {
return `${minutes} minutes ago`;
}
return new Date(timestamp).toLocaleString();
}
export class ToggleStoryButton extends React.PureComponent {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.props.onClick(this.props.story);
}
render() {
return ;
}
}
export class TogglePrefCheckbox extends React.PureComponent {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
}
onChange(event) {
this.props.onChange(this.props.pref, event.target.checked);
}
render() {
return (
<>
{" "}
{this.props.pref}{" "}
>
);
}
}
export class DiscoveryStreamAdminUI extends React.PureComponent {
constructor(props) {
super(props);
this.expireCache = this.expireCache.bind(this);
this.refreshCache = this.refreshCache.bind(this);
this.showPlaceholder = this.showPlaceholder.bind(this);
this.idleDaily = this.idleDaily.bind(this);
this.systemTick = this.systemTick.bind(this);
this.syncRemoteSettings = this.syncRemoteSettings.bind(this);
this.onStoryToggle = this.onStoryToggle.bind(this);
this.handleWeatherSubmit = this.handleWeatherSubmit.bind(this);
this.handleWeatherUpdate = this.handleWeatherUpdate.bind(this);
this.resetBlocks = this.resetBlocks.bind(this);
this.refreshInferredPersonalization =
this.refreshInferredPersonalization.bind(this);
this.refreshInferredPersonalizationAndDebug =
this.refreshInferredPersonalizationAndDebug.bind(this);
this.refreshTopicSelectionCache =
this.refreshTopicSelectionCache.bind(this);
this.requestDebugFeatures = this.requestDebugFeatures.bind(this);
this.setDebugOverrides = this.setDebugOverrides.bind(this);
this.handleDebugOverridesToggle =
this.handleDebugOverridesToggle.bind(this);
this.handleDebugOverrideChange = this.handleDebugOverrideChange.bind(this);
this.handleResetAllOverrides = this.handleResetAllOverrides.bind(this);
this.handleSectionsToggle = this.handleSectionsToggle.bind(this);
this.toggleIABBanners = this.toggleIABBanners.bind(this);
this.handleAllizomToggle = this.handleAllizomToggle.bind(this);
this.sendConversionEvent = this.sendConversionEvent.bind(this);
this.state = {
toggledStories: {},
weatherQuery: "",
pendingOverrides: {},
overridesTogglePressed: null,
};
}
componentDidMount() {
this.requestDebugFeatures();
}
refreshCache() {
this.props.dispatch(
ac.OnlyToMain({
type: at.DISCOVERY_STREAM_DEV_REFRESH_CACHE,
})
);
}
refreshInferredPersonalization() {
this.props.dispatch(
ac.OnlyToMain({
type: at.INFERRED_PERSONALIZATION_REFRESH,
})
);
}
refreshInferredPersonalizationAndDebug() {
this.refreshInferredPersonalization();
}
requestDebugFeatures() {
this.props.dispatch(
ac.OnlyToMain({
type: at.INFERRED_PERSONALIZATION_DEBUG_FEATURES_REQUEST,
})
);
}
setDebugOverrides(overrides) {
this.props.dispatch(
ac.OnlyToMain({
type: at.INFERRED_PERSONALIZATION_DEBUG_OVERRIDES_SET,
data: overrides,
})
);
}
getDebugFeaturesList() {
const { debugFeatures } = this.props.state.InferredPersonalization;
if (!debugFeatures) {
return [];
}
return Object.keys(debugFeatures)
.sort()
.filter(featureName => featureName !== "clicks")
.map(featureName => ({
name: featureName,
...debugFeatures[featureName],
}));
}
getOverrideValues(features, fallbackToCurrent = false) {
const overrides = {};
for (const feature of features) {
let value = feature.overrideValue;
if (!Number.isFinite(value) && fallbackToCurrent) {
value = Number.isFinite(feature.currentValue)
? feature.currentValue
: 0;
}
if (Number.isFinite(value)) {
overrides[feature.name] = value;
}
}
return overrides;
}
handleDebugOverridesToggle(e) {
const { pressed } = e.target;
const features = this.getDebugFeaturesList();
const currentOverrides = this.getOverrideValues(features, true);
if (!pressed) {
this.setState({
pendingOverrides: { ...currentOverrides },
overridesTogglePressed: false,
});
this.setDebugOverrides(null);
return;
}
const overrides = Object.keys(this.state.pendingOverrides).length
? { ...this.state.pendingOverrides }
: currentOverrides;
this.setState({ overridesTogglePressed: true });
this.setDebugOverrides(overrides);
}
handleDebugOverrideChange(featureName, value) {
const features = this.getDebugFeaturesList();
const overrides = Object.keys(this.state.pendingOverrides).length
? { ...this.state.pendingOverrides }
: this.getOverrideValues(features, true);
overrides[featureName] = value;
this.setState({ pendingOverrides: { ...overrides } });
if (Object.keys(this.getOverrideValues(features)).length) {
this.setDebugOverrides(overrides);
}
}
handleResetAllOverrides() {
const features = this.getDebugFeaturesList();
const overrides = Object.fromEntries(
features.map(({ name: featureName }) => [featureName, 0])
);
this.setState({ pendingOverrides: { ...overrides } });
if (Object.keys(this.getOverrideValues(features)).length) {
this.setDebugOverrides(overrides);
}
}
refreshTopicSelectionCache() {
this.props.dispatch(
ac.SetPref("discoverystream.topicSelection.onboarding.displayCount", 0)
);
this.props.dispatch(
ac.SetPref("discoverystream.topicSelection.onboarding.maybeDisplay", true)
);
}
dispatchSimpleAction(type) {
this.props.dispatch(
ac.OnlyToMain({
type,
})
);
}
resetBlocks() {
this.props.dispatch(
ac.OnlyToMain({
type: at.DISCOVERY_STREAM_DEV_BLOCKS_RESET,
})
);
}
systemTick() {
this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_SYSTEM_TICK);
}
expireCache() {
this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_EXPIRE_CACHE);
}
showPlaceholder() {
this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_SHOW_PLACEHOLDER);
}
idleDaily() {
this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_IDLE_DAILY);
}
syncRemoteSettings() {
this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_SYNC_RS);
}
handleWeatherUpdate(e) {
this.setState({ weatherQuery: e.target.value || "" });
}
handleWeatherSubmit(e) {
e.preventDefault();
const { weatherQuery } = this.state;
this.props.dispatch(ac.SetPref("weather.query", weatherQuery));
}
toggleIABBanners(e) {
const { pressed, id } = e.target;
// Set the active pref to true/false
switch (id) {
case "newtab_billboard":
// Update boolean pref for billboard ad size
this.props.dispatch(ac.SetPref(PREF_AD_SIZE_BILLBOARD, pressed));
break;
case "newtab_leaderboard":
// Update boolean pref for billboard ad size
this.props.dispatch(ac.SetPref(PREF_AD_SIZE_LEADERBOARD, pressed));
break;
case "newtab_rectangle":
// Update boolean pref for mediumRectangle (MREC) ad size
this.props.dispatch(ac.SetPref(PREF_AD_SIZE_MEDIUM_RECTANGLE, pressed));
break;
}
// Note: The counts array is passively updated whenever the placements array is updated.
// The default pref values for each are:
// PREF_SPOC_PLACEMENTS: "newtab_spocs"
// PREF_SPOC_COUNTS: "6"
const generateSpocPrefValues = () => {
const placements =
this.props.otherPrefs[PREF_SPOC_PLACEMENTS]?.split(",")
.map(item => item.trim())
.filter(item => item) || [];
const counts =
this.props.otherPrefs[PREF_SPOC_COUNTS]?.split(",")
.map(item => item.trim())
.filter(item => item) || [];
// Confirm that the IAB type will have a count value of "1"
const supportIABAdTypes = [
"newtab_leaderboard",
"newtab_rectangle",
"newtab_billboard",
];
let countValue;
if (supportIABAdTypes.includes(id)) {
countValue = "1"; // Default count value for all IAB ad types
} else {
throw new Error("IAB ad type not supported");
}
if (pressed) {
// If pressed is true, add the id to the placements array
if (!placements.includes(id)) {
placements.push(id);
counts.push(countValue);
}
} else {
// If pressed is false, remove the id from the placements array
const index = placements.indexOf(id);
if (index !== -1) {
placements.splice(index, 1);
counts.splice(index, 1);
}
}
return {
placements: placements.join(", "),
counts: counts.join(", "),
};
};
const { placements, counts } = generateSpocPrefValues();
// Update prefs with new values
this.props.dispatch(ac.SetPref(PREF_SPOC_PLACEMENTS, placements));
this.props.dispatch(ac.SetPref(PREF_SPOC_COUNTS, counts));
// If contextual ads, sections, and one of the banners are enabled
// update the contextualBanner prefs to include the banner value and count
// Else, clear the prefs
if (PREF_CONTEXTUAL_ADS_ENABLED && PREF_SECTIONS_ENABLED) {
if (PREF_AD_SIZE_BILLBOARD && placements.includes("newtab_billboard")) {
this.props.dispatch(
ac.SetPref(PREF_CONTEXTUAL_BANNER_PLACEMENTS, "newtab_billboard")
);
this.props.dispatch(ac.SetPref(PREF_CONTEXTUAL_BANNER_COUNTS, "1"));
} else if (
PREF_AD_SIZE_LEADERBOARD &&
placements.includes("newtab_leaderboard")
) {
this.props.dispatch(
ac.SetPref(PREF_CONTEXTUAL_BANNER_PLACEMENTS, "newtab_leaderboard")
);
this.props.dispatch(ac.SetPref(PREF_CONTEXTUAL_BANNER_COUNTS, "1"));
} else {
this.props.dispatch(ac.SetPref(PREF_CONTEXTUAL_BANNER_PLACEMENTS, ""));
this.props.dispatch(ac.SetPref(PREF_CONTEXTUAL_BANNER_COUNTS, ""));
}
}
}
handleSectionsToggle(e) {
const { pressed } = e.target;
this.props.dispatch(ac.SetPref(PREF_SECTIONS_ENABLED, pressed));
this.props.dispatch(
ac.SetPref("discoverystream.sections.cards.enabled", pressed)
);
}
sendConversionEvent() {
const detail = {
partnerId: "295BEEF7-1E3B-4128-B8F8-858E12AA660B",
lookbackDays: 7,
impressionType: "default",
};
const event = new CustomEvent("FirefoxConversionNotification", {
detail,
bubbles: true,
composed: true,
});
window?.dispatchEvent(event);
}
renderComponent(width, component) {
return (