Skip to content

Commit 7ce9fea

Browse files
committed
Bug 1812690 - Pocket newtab enabling onboarding experience for new users seeing the Pocket section for the first time. r=gvn,fluent-reviewers,flod
Differential Revision: https://phabricator.services.mozilla.com/D174710 UltraBlame original commit: f1313331d9d9d87f0b181ad305906249dfe53a4a
1 parent fd06b12 commit 7ce9fea

File tree

23 files changed

+934
-200
lines changed

23 files changed

+934
-200
lines changed

browser/app/profile/firefox.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1635,6 +1635,10 @@ pref("browser.newtabpage.activity-stream.discoverystream.recs.personalized", fal
16351635
pref("browser.newtabpage.activity-stream.discoverystream.spocs.personalized", true);
16361636

16371637

1638+
pref("browser.newtabpage.activity-stream.discoverystream.onboardingExperience.dismissed", false);
1639+
pref("browser.newtabpage.activity-stream.discoverystream.onboardingExperience.enabled", false);
1640+
1641+
16381642
pref("browser.newtabpage.activity-stream.feeds.section.topstories", true);
16391643

16401644

browser/components/newtab/common/Actions.sys.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ for (const type of [
7979
"FAKE_FOCUS_SEARCH",
8080
"FILL_SEARCH_TERM",
8181
"HANDOFF_SEARCH_TO_AWESOMEBAR",
82+
"HIDE_PERSONALIZE",
8283
"HIDE_PRIVACY_INFO",
8384
"INIT",
8485
"NEW_TAB_INIT",
@@ -127,6 +128,7 @@ for (const type of [
127128
"SET_PREF",
128129
"SHOW_DOWNLOAD_FILE",
129130
"SHOW_FIREFOX_ACCOUNTS",
131+
"SHOW_PERSONALIZE",
130132
"SHOW_PRIVACY_INFO",
131133
"SHOW_SEARCH",
132134
"SKIPPED_SIGNIN",

browser/components/newtab/common/Reducers.sys.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const INITIAL_STATE = {
1717
initialized: false,
1818
locale: "",
1919
isForStartupCache: false,
20+
customizeMenuVisible: false,
2021
},
2122
ASRouter: { initialized: false },
2223
Snippets: { initialized: false },
@@ -108,6 +109,14 @@ function App(prevState = INITIAL_STATE.App, action) {
108109
return Object.assign({}, prevState, action.data || {}, {
109110
isForStartupCache: false,
110111
});
112+
case at.SHOW_PERSONALIZE:
113+
return Object.assign({}, prevState, {
114+
customizeMenuVisible: true,
115+
});
116+
case at.HIDE_PERSONALIZE:
117+
return Object.assign({}, prevState, {
118+
customizeMenuVisible: false,
119+
});
111120
default:
112121
return prevState;
113122
}

browser/components/newtab/content-src/components/Base/Base.jsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export class BaseContent extends React.PureComponent {
111111
this.handleOnKeyDown = this.handleOnKeyDown.bind(this);
112112
this.onWindowScroll = debounce(this.onWindowScroll.bind(this), 5);
113113
this.setPref = this.setPref.bind(this);
114-
this.state = { fixedSearch: false, customizeMenuVisible: false };
114+
this.state = { fixedSearch: false };
115115
}
116116

117117
componentDidMount() {
@@ -140,13 +140,13 @@ export class BaseContent extends React.PureComponent {
140140
}
141141

142142
openCustomizationMenu() {
143-
this.setState({ customizeMenuVisible: true });
143+
this.props.dispatch({ type: at.SHOW_PERSONALIZE });
144144
this.props.dispatch(ac.UserEvent({ event: "SHOW_PERSONALIZE" }));
145145
}
146146

147147
closeCustomizationMenu() {
148-
if (this.state.customizeMenuVisible) {
149-
this.setState({ customizeMenuVisible: false });
148+
if (this.props.App.customizeMenuVisible) {
149+
this.props.dispatch({ type: at.HIDE_PERSONALIZE });
150150
this.props.dispatch(ac.UserEvent({ event: "HIDE_PERSONALIZE" }));
151151
}
152152
}
@@ -164,7 +164,7 @@ export class BaseContent extends React.PureComponent {
164164
render() {
165165
const { props } = this;
166166
const { App } = props;
167-
const { initialized } = App;
167+
const { initialized, customizeMenuVisible } = App;
168168
const prefs = props.Prefs.values;
169169

170170
const isDiscoveryStream =
@@ -180,7 +180,6 @@ export class BaseContent extends React.PureComponent {
180180
!pocketEnabled &&
181181
filteredSections.filter(section => section.enabled).length === 0;
182182
const searchHandoffEnabled = prefs["improvesearch.handoffToAwesomebar"];
183-
const showCustomizationMenu = this.state.customizeMenuVisible;
184183
const enabledSections = {
185184
topSitesEnabled: prefs["feeds.topsites"],
186185
pocketEnabled: prefs["feeds.section.topstories"],
@@ -224,7 +223,7 @@ export class BaseContent extends React.PureComponent {
224223
enabledSections={enabledSections}
225224
pocketRegion={pocketRegion}
226225
mayHaveSponsoredTopSites={mayHaveSponsoredTopSites}
227-
showing={showCustomizationMenu}
226+
showing={customizeMenuVisible}
228227
/>
229228
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions*/}
230229
<div className={outerClassName} onClick={this.closeCustomizationMenu}>

browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ export class _DiscoveryStreamBase extends React.PureComponent {
192192
fourCardLayout={component.properties.fourCardLayout}
193193
compactGrid={component.properties.compactGrid}
194194
essentialReadsHeader={component.properties.essentialReadsHeader}
195+
onboardingExperience={component.properties.onboardingExperience}
195196
editorsPicksHeader={component.properties.editorsPicksHeader}
196197
recentSavesEnabled={this.props.DiscoveryStream.recentSavesEnabled}
197198
hideDescriptions={this.props.DiscoveryStream.hideDescriptions}

browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import { DSCard, PlaceholderDSCard } from "../DSCard/DSCard.jsx";
66
import { DSEmptyState } from "../DSEmptyState/DSEmptyState.jsx";
7+
import { DSDismiss } from "content-src/components/DiscoveryStreamComponents/DSDismiss/DSDismiss";
78
import { TopicsWidget } from "../TopicsWidget/TopicsWidget.jsx";
89
import { SafeAnchor } from "../SafeAnchor/SafeAnchor";
910
import { FluentOrText } from "../../FluentOrText/FluentOrText.jsx";
@@ -13,6 +14,9 @@ import {
1314
} from "common/Actions.sys.mjs";
1415
import React, { useEffect, useState, useRef, useCallback } from "react";
1516
import { connect, useSelector } from "react-redux";
17+
const PREF_ONBOARDING_EXPERIENCE_DISMISSED =
18+
"discoverystream.onboardingExperience.dismissed";
19+
const INTERSECTION_RATIO = 0.5;
1620
const WIDGET_IDS = {
1721
TOPICS: 1,
1822
};
@@ -25,6 +29,99 @@ export function DSSubHeader({ children }) {
2529
);
2630
}
2731

32+
export function OnboardingExperience({
33+
children,
34+
dispatch,
35+
windowObj = global,
36+
}) {
37+
const [dismissed, setDismissed] = useState(false);
38+
const [maxHeight, setMaxHeight] = useState(null);
39+
const heightElement = useRef(null);
40+
41+
const onDismissClick = useCallback(() => {
42+
// We update this as state and redux.
43+
// The state update is for this newtab,
44+
// and the redux update is for other tabs, offscreen tabs, and future tabs.
45+
// We need the state update for this tab to support the transition.
46+
setDismissed(true);
47+
dispatch(ac.SetPref(PREF_ONBOARDING_EXPERIENCE_DISMISSED, true));
48+
dispatch(
49+
ac.DiscoveryStreamUserEvent({
50+
event: "BLOCK",
51+
source: "POCKET_ONBOARDING",
52+
})
53+
);
54+
}, [dispatch]);
55+
56+
useEffect(() => {
57+
const resizeObserver = new windowObj.ResizeObserver(() => {
58+
if (heightElement.current) {
59+
setMaxHeight(heightElement.current.offsetHeight);
60+
}
61+
});
62+
63+
const options = { threshold: INTERSECTION_RATIO };
64+
const intersectionObserver = new windowObj.IntersectionObserver(entries => {
65+
if (
66+
entries.some(
67+
entry =>
68+
entry.isIntersecting &&
69+
entry.intersectionRatio >= INTERSECTION_RATIO
70+
)
71+
) {
72+
dispatch(
73+
ac.DiscoveryStreamUserEvent({
74+
event: "IMPRESSION",
75+
source: "POCKET_ONBOARDING",
76+
})
77+
);
78+
// Once we have observed an impression, we can stop for this instance of newtab.
79+
intersectionObserver.unobserve(heightElement.current);
80+
}
81+
}, options);
82+
if (heightElement.current) {
83+
resizeObserver.observe(heightElement.current);
84+
intersectionObserver.observe(heightElement.current);
85+
setMaxHeight(heightElement.current.offsetHeight);
86+
}
87+
88+
// Return unmount callback to clean up observers.
89+
return () => {
90+
resizeObserver?.disconnect();
91+
intersectionObserver?.disconnect();
92+
};
93+
}, [dispatch, windowObj]);
94+
95+
const style = {};
96+
if (dismissed) {
97+
style.maxHeight = "0";
98+
style.opacity = "0";
99+
style.transition = "max-height 0.26s ease, opacity 0.26s ease";
100+
} else if (maxHeight) {
101+
style.maxHeight = `${maxHeight}px`;
102+
}
103+
104+
return (
105+
<div style={style}>
106+
<div className="ds-onboarding-ref" ref={heightElement}>
107+
<div className="ds-onboarding">
108+
<DSDismiss
109+
onDismissClick={onDismissClick}
110+
extraClasses={`ds-onboarding-dismiss`}
111+
>
112+
<div className="ds-onboarding-graphic" />
113+
<header>
114+
<span className="icon icon-pocket" />
115+
<span data-l10n-id="newtab-pocket-onboarding-discover" />
116+
</header>
117+
<p data-l10n-id="newtab-pocket-onboarding-cta" />
118+
</DSDismiss>
119+
</div>
120+
</div>
121+
</div>
122+
);
123+
}
124+
28125
export function IntersectionObserver({
29126
children,
30127
windowObj = window,
@@ -204,13 +301,16 @@ export class _CardGrid extends React.PureComponent {
204301
compactGrid,
205302
essentialReadsHeader,
206303
editorsPicksHeader,
304+
onboardingExperience,
207305
widgets,
208306
recentSavesEnabled,
209307
hideDescriptions,
210308
DiscoveryStream,
211309
} = this.props;
212310
const { saveToPocketCard } = DiscoveryStream;
213311
const showRecentSaves = prefs.showRecentSaves && recentSavesEnabled;
312+
const isOnboardingExperienceDismissed =
313+
prefs[PREF_ONBOARDING_EXPERIENCE_DISMISSED];
214314

215315
const recs = this.props.data.recommendations.slice(0, items);
216316
const cards = [];
@@ -319,10 +419,13 @@ export class _CardGrid extends React.PureComponent {
319419
? `ds-card-grid-hybrid-layout`
320420
: ``;
321421

322-
const gridClassName = `ds-card-grid ds-card-grid-border ${hybridLayoutClassName} ${hideCardBackgroundClass} ${fourCardLayoutClass} ${hideDescriptionsClassName} ${compactGridClassName}`;
422+
const gridClassName = `ds-card-grid ${hybridLayoutClassName} ${hideCardBackgroundClass} ${fourCardLayoutClass} ${hideDescriptionsClassName} ${compactGridClassName}`;
323423

324424
return (
325425
<>
426+
{!isOnboardingExperienceDismissed && onboardingExperience && (
427+
<OnboardingExperience dispatch={this.props.dispatch} />
428+
)}
326429
{essentialReadsCards?.length > 0 && (
327430
<div className={gridClassName}>{essentialReadsCards}</div>
328431
)}

0 commit comments

Comments
 (0)