Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.56% covered (warning)
76.56%
98 / 128
45.45% covered (danger)
45.45%
5 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
SiteNoticeGenerator
76.56% covered (warning)
76.56%
98 / 128
45.45% covered (danger)
45.45%
5 / 11
45.18
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 setNotice
76.47% covered (warning)
76.47%
13 / 17
0.00% covered (danger)
0.00%
0 / 1
10.06
 maybeShowIfUserAbandonedWelcomeSurvey
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
5.03
 isWelcomeSurveyInReferer
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 setConfirmEmailSiteNotice
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
2
 setDiscoverySiteNotice
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 checkAndMarkMobileDiscoveryNoticeSeen
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 setDesktopDiscoverySiteNotice
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
2
 setMobileDiscoverySiteNotice
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
2
 getHeader
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 getDiscoveryTextWithAvatarIcon
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace GrowthExperiments\Homepage;
4
5use GrowthExperiments\ExperimentUserManager;
6use GrowthExperiments\HomepageHooks;
7use GrowthExperiments\Util;
8use JobQueueGroup;
9use MediaWiki\Html\Html;
10use MediaWiki\Output\OutputPage;
11use MediaWiki\User\Options\UserOptionsLookup;
12use MediaWiki\User\UserIdentity;
13use OOUI\IconWidget;
14use UserOptionsUpdateJob;
15
16class SiteNoticeGenerator {
17
18    private ExperimentUserManager $experimentUserManager;
19    private UserOptionsLookup $userOptionsLookup;
20    private JobQueueGroup $jobQueueGroup;
21    private ?bool $homepageDiscoveryNoticeSeen = null;
22
23    /**
24     * @param ExperimentUserManager $experimentUserManager
25     * @param UserOptionsLookup $userOptionsLookup
26     * @param JobQueueGroup $jobQueueGroup
27     */
28    public function __construct(
29        ExperimentUserManager $experimentUserManager,
30        UserOptionsLookup $userOptionsLookup,
31        JobQueueGroup $jobQueueGroup
32    ) {
33        $this->experimentUserManager = $experimentUserManager;
34        $this->userOptionsLookup = $userOptionsLookup;
35        $this->jobQueueGroup = $jobQueueGroup;
36    }
37
38    /**
39     * @param string $name
40     * @param string &$siteNotice
41     * @param \Skin $skin
42     * @param bool &$minervaEnableSiteNotice Reference to $wgMinervaEnableSiteNotice
43     * @return bool|void Hook return value (ie. false to prevent other notices from displaying)
44     */
45    public function setNotice( $name, &$siteNotice, \Skin $skin, &$minervaEnableSiteNotice ) {
46        if ( $skin->getTitle()->isSpecial( 'WelcomeSurvey' ) ) {
47            // Don't show any notices on the welcome survey.
48            return;
49        }
50        switch ( $name ) {
51            case HomepageHooks::CONFIRMEMAIL_QUERY_PARAM:
52                if ( $skin->getTitle()->isSpecial( 'Homepage' ) ) {
53                    return $this->setConfirmEmailSiteNotice( $siteNotice, $skin, $minervaEnableSiteNotice );
54                }
55                break;
56            case 'specialwelcomesurvey':
57                if ( $skin->getTitle()->isSpecial( 'Homepage' ) ) {
58                    return $this->setDiscoverySiteNotice( $siteNotice, $skin, $name, $minervaEnableSiteNotice );
59                }
60                break;
61            case 'welcomesurvey-originalcontext':
62                if ( !$skin->getTitle()->isSpecial( 'Homepage' ) ) {
63                    return $this->setDiscoverySiteNotice( $siteNotice, $skin, $name, $minervaEnableSiteNotice );
64                }
65                break;
66            default:
67                return $this->maybeShowIfUserAbandonedWelcomeSurvey(
68                    $siteNotice,
69                    $skin,
70                    $minervaEnableSiteNotice );
71        }
72    }
73
74    private function maybeShowIfUserAbandonedWelcomeSurvey(
75        &$siteNotice, \Skin $skin, &$minervaEnableSiteNotice
76    ) {
77        if ( $this->isWelcomeSurveyInReferer( $skin )
78            || ( Util::isMobile( $skin ) && !$this->checkAndMarkMobileDiscoveryNoticeSeen( $skin ) )
79        ) {
80            return $this->setDiscoverySiteNotice(
81                $siteNotice,
82                $skin,
83                $skin->getTitle()->isSpecial( 'Homepage' )
84                    ? 'specialwelcomesurvey'
85                    : 'welcomesurvey-originalcontext',
86                $minervaEnableSiteNotice
87            );
88        }
89    }
90
91    private function isWelcomeSurveyInReferer( \Skin $skin ) {
92        foreach ( $skin->getLanguage()->getSpecialPageAliases()['WelcomeSurvey'] as $alias ) {
93            if ( strpos( $skin->getRequest()->getHeader( 'REFERER' ), $alias ) !== false ) {
94                return true;
95            }
96        }
97        return false;
98    }
99
100    /**
101     * @param string &$siteNotice
102     * @param \Skin $skin
103     * @param bool &$minervaEnableSiteNotice
104     * @return bool|void Hook return value (ie. false to prevent other notices from displaying)
105     */
106    private function setConfirmEmailSiteNotice(
107        &$siteNotice, \Skin $skin, &$minervaEnableSiteNotice
108    ) {
109        $output = $skin->getOutput();
110        $output->addJsConfigVars( 'shouldShowConfirmEmailNotice', true );
111        $output->addModuleStyles( 'ext.growthExperiments.Homepage.styles' );
112        $baseCssClassName = 'mw-ge-homepage-confirmemail-nojs';
113        $cssClasses = [
114            $baseCssClassName,
115            // The following classes are generated here:
116            // * mw-ge-homepage-confirmemail-nojs-mobile
117            // * mw-ge-homepage-confirmemail-nojs-desktop
118            Util::isMobile( $skin ) ? $baseCssClassName . '-mobile' : $baseCssClassName . '-desktop'
119        ];
120        $siteNotice = Html::rawElement( 'div', [ 'class' => $cssClasses ],
121            new IconWidget( [ 'icon' => 'check', 'flags' => 'success' ] ) . ' ' .
122            Html::element( 'span', [ 'class' => 'mw-ge-homepage-confirmemail-nojs-message' ],
123                $output->msg( 'confirmemail_loggedin' )->text() )
124        );
125        $minervaEnableSiteNotice = true;
126        // Only triggered for a specific source query parameter, which the user should see only
127        // once, so it's OK to suppress all other banners.
128        return false;
129    }
130
131    /**
132     * @param string &$siteNotice
133     * @param \Skin $skin
134     * @param string $contextName
135     * @param bool &$minervaEnableSiteNotice
136     * @return bool|void Hook return value (ie. false to prevent other notices from displaying)
137     */
138    private function setDiscoverySiteNotice(
139        &$siteNotice, \Skin $skin, $contextName, &$minervaEnableSiteNotice
140    ) {
141        if ( Util::isMobile( $skin ) ) {
142            $this->setMobileDiscoverySiteNotice( $siteNotice, $skin, $contextName,
143                $minervaEnableSiteNotice );
144            $this->checkAndMarkMobileDiscoveryNoticeSeen( $skin );
145        } else {
146            $this->setDesktopDiscoverySiteNotice( $siteNotice, $skin, $contextName );
147        }
148        // Only triggered for a specific source query parameter, which the user should see only
149        // once, so it's OK to suppress all other banners.
150        return false;
151    }
152
153    /**
154     * Check and set seen flag for the mobile homapage discovery sitenotice.
155     * (Desktop uses a different mechanism based on guided tours, which has its own seen logic.)
156     * @param \Skin $skin
157     * @return bool True if the user has seen the notice already.
158     */
159    private function checkAndMarkMobileDiscoveryNoticeSeen( \Skin $skin ) {
160        // Make multiple calls to this method within the same request a no-op.
161        // Note this would be necessary even if we only called it once, because
162        // Minerva calls sitenotice hooks multiple times.
163        if ( $this->homepageDiscoveryNoticeSeen !== null ) {
164            return $this->homepageDiscoveryNoticeSeen;
165        }
166
167        $user = $skin->getUser();
168        if ( $this->userOptionsLookup->getOption( $user, HomepageHooks::HOMEPAGE_MOBILE_DISCOVERY_NOTICE_SEEN ) ) {
169            $this->homepageDiscoveryNoticeSeen = true;
170            return true;
171        }
172
173        $this->jobQueueGroup->lazyPush( new UserOptionsUpdateJob( [
174            'userId' => $user->getId(),
175            'options' => [ HomepageHooks::HOMEPAGE_MOBILE_DISCOVERY_NOTICE_SEEN => 1 ],
176        ] ) );
177        $this->homepageDiscoveryNoticeSeen = false;
178        return false;
179    }
180
181    /**
182     * @param string &$siteNotice
183     * @param \Skin $skin
184     * @param string $contextName
185     */
186    private function setDesktopDiscoverySiteNotice(
187        &$siteNotice, \Skin $skin, $contextName
188    ) {
189        // No-JS banner (hidden from CSS when there's JS support). The JS version is in
190        // tours/homepageDiscovery.js.
191
192        $output = $skin->getOutput();
193        $output->enableOOUI();
194        $output->addModuleStyles( [
195            'oojs-ui.styles.icons-user',
196            'ext.growthExperiments.HomepageDiscovery.styles'
197        ] );
198
199        $username = $skin->getUser()->getName();
200        if ( $contextName === 'specialwelcomesurvey' ) {
201            $msgHeaderKey = 'growthexperiments-homepage-discovery-banner-header';
202            $msgBodyKey = 'growthexperiments-homepage-discovery-banner-text';
203        } else {
204            $msgHeaderKey = 'growthexperiments-homepage-discovery-thanks-header';
205            $msgBodyKey = 'growthexperiments-homepage-discovery-thanks-text';
206        }
207        $siteNotice = Html::rawElement( 'div', [ 'class' => 'mw-ge-homepage-discovery-banner-nojs' ],
208            Html::element( 'span', [ 'class' => 'mw-ge-homepage-discovery-house' ] ) .
209            Html::rawElement( 'span', [ 'class' => 'mw-ge-homepage-discovery-text-content' ],
210                Html::element( 'h2', [ 'class' => 'mw-ge-homepage-discovery-nojs-message' ],
211                    $output->msg( $msgHeaderKey )->params( $username )->text() ) .
212                $this->getDiscoveryTextWithAvatarIcon(
213                    $output,
214                    $skin->getUser(),
215                    $msgBodyKey,
216                    'mw-ge-homepage-discovery-nojs-banner-text'
217                )
218            )
219        );
220    }
221
222    /**
223     * @param string &$siteNotice
224     * @param \Skin $skin
225     * @param string $contextName
226     * @param bool &$minervaEnableSiteNotice
227     */
228    private function setMobileDiscoverySiteNotice(
229        &$siteNotice, \Skin $skin, $contextName, &$minervaEnableSiteNotice
230    ) {
231        $output = $skin->getOutput();
232        $output->enableOOUI();
233        $output->addModuleStyles( [
234            'oojs-ui.styles.icons-user',
235            'ext.growthExperiments.HomepageDiscovery.styles'
236        ] );
237        $output->addModules( 'ext.growthExperiments.HomepageDiscovery' );
238
239        $user = $skin->getUser();
240        $location = ( $contextName === 'specialwelcomesurvey' ) ? 'homepage' : 'nonhomepage';
241        $msgHeaderKey = "growthexperiments-homepage-discovery-mobile-$location-banner-header";
242        $msgBodyKey = "growthexperiments-homepage-discovery-mobile-$location-banner-text";
243
244        $siteNotice = Html::rawElement( 'div', [ 'class' => 'mw-ge-homepage-discovery-banner-mobile' ],
245            Html::element( 'div', [ 'class' => 'mw-ge-homepage-discovery-arrow ' .
246                'mw-ge-homepage-discovery-arrow-user-variant-' .
247                $this->experimentUserManager->getVariant( $user ) ] ) .
248            Html::rawElement( 'div', [ 'class' => 'mw-ge-homepage-discovery-message' ],
249                $this->getHeader( $output, $user, $msgHeaderKey, $location ) .
250                $this->getDiscoveryTextWithAvatarIcon( $output, $user, $msgBodyKey )
251            ) . new IconWidget( [ 'icon' => 'close',
252                'classes' => [ 'mw-ge-homepage-discovery-banner-close' ] ] )
253        );
254
255        $minervaEnableSiteNotice = true;
256    }
257
258    /**
259     * Get the header (H2) element for the site notice.
260     *
261     * If the user is on the homepage, no header is shown.
262     *
263     * @param OutputPage $output
264     * @param UserIdentity $user
265     * @param string $msgHeaderKey
266     * @param string $location
267     * @return string
268     */
269    private function getHeader(
270        OutputPage $output,
271        UserIdentity $user,
272        string $msgHeaderKey,
273        string $location
274    ): string {
275        if ( $location === 'homepage' ) {
276            return '';
277        }
278        return Html::element( 'h2', [],
279            $output->msg( $msgHeaderKey )->params( $user->getName() )->text()
280        );
281    }
282
283    /**
284     * @param OutputPage $output
285     * @param UserIdentity $user
286     * @param string $msgBodyKey
287     * @param string $class
288     * @return string
289     */
290    private function getDiscoveryTextWithAvatarIcon(
291        OutputPage $output, UserIdentity $user, string $msgBodyKey, $class = ''
292    ): string {
293        return Html::rawElement( 'p', [ 'class' => $class ],
294            $output->msg( $msgBodyKey )
295                ->params( $user->getName() )
296                ->rawParams(
297                    new IconWidget( [ 'icon' => 'userAvatar' ] ) .
298                    // add a word joiner to make the icon stick to the name
299                    \UtfNormal\Utils::codepointToUtf8( 0x2060 ) .
300                    Html::element( 'span', [], $user->getName() )
301                )->parse()
302        );
303    }
304
305}