Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 92 |
|
0.00% |
0 / 6 |
CRAP | |
0.00% |
0 / 1 |
CampaignBenefitsBlock | |
0.00% |
0 / 92 |
|
0.00% |
0 / 6 |
506 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getLegalFooter | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getHtml | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getCampaignTemplateHtml | |
0.00% |
0 / 54 |
|
0.00% |
0 / 1 |
156 | |||
getCampaignValue | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getVideo | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
42 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments; |
4 | |
5 | use GrowthExperiments\NewcomerTasks\CampaignConfig; |
6 | use MediaWiki\Context\IContextSource; |
7 | use MediaWiki\Html\Html; |
8 | use MediaWiki\HTMLForm\HTMLForm; |
9 | use MediaWiki\Linker\Linker; |
10 | use MediaWiki\MediaWikiServices; |
11 | use MediaWiki\Minerva\Skins\SkinMinerva; |
12 | use MediaWiki\Output\OutputPage; |
13 | use MediaWiki\Registration\ExtensionRegistry; |
14 | use MediaWiki\Title\MalformedTitleException; |
15 | use MediaWiki\Title\Title; |
16 | use MessageLocalizer; |
17 | use OOUI\IconWidget; |
18 | use Wikimedia\Assert\Assert; |
19 | |
20 | /** |
21 | * Customized version of the SpecialCreateAccount hero message, for campaigns. |
22 | * Somewhat customizable via community configuration: the 'signupPageTemplate' and |
23 | * 'signupPageTemplateParameters' parameters of the campaign configuration (see the |
24 | * CampaignConfig class) will be passed to CampaignBenefitsBlock::getCampaignTemplateHtml. |
25 | */ |
26 | class CampaignBenefitsBlock { |
27 | |
28 | private IContextSource $context; |
29 | private HTMLForm $authForm; |
30 | private CampaignConfig $campaignConfig; |
31 | |
32 | /** |
33 | * @param IContextSource $context |
34 | * @param HTMLForm $authForm |
35 | * @param CampaignConfig $campaignConfig |
36 | */ |
37 | public function __construct( |
38 | IContextSource $context, |
39 | HTMLForm $authForm, |
40 | CampaignConfig $campaignConfig |
41 | ) { |
42 | $this->context = $context; |
43 | $this->authForm = $authForm; |
44 | $this->campaignConfig = $campaignConfig; |
45 | } |
46 | |
47 | /** |
48 | * Get footer content for the special page. Displayed via SkinAddFooterLinks hook. |
49 | * @param MessageLocalizer $ctx |
50 | * @return string|void |
51 | */ |
52 | public static function getLegalFooter( MessageLocalizer $ctx ) { |
53 | return $ctx->msg( 'growthexperiments-campaigns-footer' )->parse(); |
54 | } |
55 | |
56 | /** |
57 | * @return string HTML to render on Special:CreateAccount. |
58 | */ |
59 | public function getHtml(): string { |
60 | $campaignName = $this->campaignConfig->getCampaignIndexFromCampaignTerm( $this->getCampaignValue() ); |
61 | // If we got here, VariantHooks::shouldShowNewLandingPageHtml() is true |
62 | // so there is a campaign with a template. Make phan happy. |
63 | Assert::invariant( $campaignName !== null, '$campaignName is not null' ); |
64 | $template = $this->campaignConfig->getSignupPageTemplate( $campaignName ); |
65 | Assert::invariant( $template !== null, '$template is not null' ); |
66 | $parameters = $this->campaignConfig->getSignupPageTemplateParameters( $campaignName ); |
67 | |
68 | // We only really have one template at this pont, with small variations. |
69 | return $this->getCampaignTemplateHtml( $template, $parameters ); |
70 | } |
71 | |
72 | /** |
73 | * Known templates/parameters: |
74 | * - hero: welcome text with hero image |
75 | * FIXME: the image should be parametrized (currently it's CSS+SVG) |
76 | * - messageKey: used in th name of various messages: |
77 | * - growthexperiments-{messageKey}-title: title text (h2) |
78 | * - growthexperiments-{messageKey}-body: main welcome text |
79 | * - growthexperiments-{messageKey}-title-mobile and |
80 | * growthexperiments-{messageKey}-body-mobile: alternative text for mobile (to allow for |
81 | * shorter text and avoid pushing the registration form below the fold). Disable (set to |
82 | * '-') to not show anything; omit or blank to show the same text as on desktop. |
83 | * - growthexperiments-{messageKey}-bullet1/2/3: three bullet items after the main text, |
84 | * with the lightbulb, mentor and difficulty-easy-bw icons, meant to highlight Growth |
85 | * features. Only used if showBenefitsList is true (but then all three are required). |
86 | * Also used as a CSS class (.mw-ge-{messageKey}-block) for selecting a specific campaign. |
87 | * - showBenefitsList: whether to show the benefit list (three bullet items highlighting |
88 | * various Growth features), default false |
89 | * - video: welcome text with video on top |
90 | * - messageKey, showBenefitsList: as above |
91 | * - file: video filename from Commons (without namespace) |
92 | * - thumbtime: timestamp to use for still image for the video (default: leave it to MediaWiki) |
93 | * @param string $template |
94 | * @param array $parameters |
95 | * @return string |
96 | */ |
97 | private function getCampaignTemplateHtml( $template, $parameters ) { |
98 | $this->context->getOutput()->enableOOUI(); |
99 | $this->context->getOutput()->addModuleStyles( [ |
100 | 'oojs-ui.styles.icons-interactions', |
101 | 'ext.growthExperiments.icons', |
102 | 'ext.growthExperiments.Account.styles', |
103 | ] ); |
104 | |
105 | $this->context->getOutput()->addBodyClasses( 'mw-ge-customlandingpage' ); |
106 | |
107 | $isMobile = $this->context->getSkin() instanceof SkinMinerva; |
108 | $messageKey = $parameters['messageKey']; |
109 | $shouldShowBenefitsList = $parameters['showBenefitsList'] ?? false; |
110 | $shouldShowBenefitListInPlatform = $shouldShowBenefitsList === true || |
111 | ( $shouldShowBenefitsList === 'desktop' && !$isMobile ); |
112 | $benefitsList = ''; |
113 | $videoHtml = ''; |
114 | if ( $shouldShowBenefitListInPlatform ) { |
115 | foreach ( [ 'lightbulb', 'mentor', 'difficulty-easy-bw' ] as $i => $icon ) { |
116 | $index = $i + 1; |
117 | $benefitMessage = $this->context->msg( "growthexperiments-$messageKey-bullet$index" ); |
118 | if ( !$benefitMessage->exists() ) { |
119 | $benefitMessage = $this->context->msg( "growthexperiments-signupcampaign-bullet$index" ); |
120 | } |
121 | $benefitsList .= Html::rawElement( 'li', [], |
122 | new IconWidget( [ 'icon' => $icon ] ) |
123 | . Html::element( 'span', [], |
124 | // The following message keys are used here: |
125 | // * growthexperiments-signupcampaign-bullet1 |
126 | // * growthexperiments-signupcampaign-bullet2 |
127 | // * growthexperiments-signupcampaign-bullet3 |
128 | $benefitMessage->text() |
129 | ) |
130 | ); |
131 | } |
132 | $benefitsList = Html::rawElement( 'ul', [ 'class' => 'mw-ge-donorsignup-list' ], $benefitsList ); |
133 | } |
134 | if ( $template === 'video' ) { |
135 | $filename = $parameters['file']; |
136 | $thumbtime = $parameters['thumbtime'] ?? null; |
137 | $videoHtml = $this->getVideo( $this->context->getOutput(), $filename, $thumbtime ); |
138 | } |
139 | |
140 | // The following message keys are used here: |
141 | // * growthexperiments-recurringcampaign-title |
142 | // * growthexperiments-signupcampaign-title |
143 | // * growthexperiments-josacampaign-title |
144 | // * growthexperiments-glamcampaign-title |
145 | // * growthexperiments-marketingvideocampaign-title |
146 | $titleMessage = $this->context->msg( "growthexperiments-$messageKey-title" ); |
147 | // The following message keys are used here: |
148 | // * growthexperiments-recurringcampaign-body |
149 | // * growthexperiments-signupcampaign-body |
150 | // * growthexperiments-josacampaign-body |
151 | // * growthexperiments-glamcampaign-body |
152 | // * growthexperiments-marketingvideocampaign-body |
153 | $bodyMessage = $this->context->msg( "growthexperiments-$messageKey-body" ); |
154 | if ( $isMobile ) { |
155 | // use mobile-specific title/body if they exist and aren't empty |
156 | if ( !$this->context->msg( "growthexperiments-$messageKey-title-mobile" )->isBlank() ) { |
157 | // The following message keys are used here: |
158 | // none as of now |
159 | $titleMessage = $this->context->msg( "growthexperiments-$messageKey-title-mobile" ); |
160 | } |
161 | if ( !$this->context->msg( "growthexperiments-$messageKey-body-mobile" )->isBlank() ) { |
162 | // The following message keys are used here: |
163 | // * growthexperiments-marketingvideocampaign-body-mobile |
164 | $bodyMessage = $this->context->msg( "growthexperiments-$messageKey-body-mobile" ); |
165 | } |
166 | } |
167 | |
168 | $campaignTitle = ''; |
169 | $campaignBody = ''; |
170 | // note that a message consisting of a single dash is disabled but not blank |
171 | if ( !$titleMessage->isDisabled() ) { |
172 | $campaignTitle = Html::rawElement( 'h2', [ 'class' => 'mw-ge-donorsignup-title' ], |
173 | $titleMessage->parse() ); |
174 | } |
175 | if ( !$bodyMessage->isDisabled() ) { |
176 | $campaignBody = Html::rawElement( 'p', [ 'class' => 'mw-ge-donorsignup-body' ], |
177 | $bodyMessage->parse() ); |
178 | } |
179 | return Html::rawElement( 'div', [ 'class' => 'mw-createacct-benefits-container' ], |
180 | Html::rawElement( 'div', [ 'class' => "mw-ge-donorsignup-block mw-ge-donorsignup-block-$messageKey" ], |
181 | $campaignTitle |
182 | . $campaignBody |
183 | . $benefitsList |
184 | ) |
185 | . $videoHtml |
186 | ); |
187 | } |
188 | |
189 | /** |
190 | * Get the campaign from the account creation form |
191 | * |
192 | * @return string |
193 | */ |
194 | private function getCampaignValue(): string { |
195 | return $this->authForm->getField( 'campaign' )->getDefault(); |
196 | } |
197 | |
198 | /** |
199 | * Add a video player to the output. |
200 | * |
201 | * @param OutputPage $output Used te register required assets. |
202 | * @param string $filename Video file name (without the 'File:' prefix). |
203 | * @param int|null $thumbtime Optional time position for thumbnail generation, in seconds. |
204 | * Theoretically a float, but non-integer support is broken: T228467 |
205 | * @return string Video player HTML |
206 | */ |
207 | private function getVideo( OutputPage $output, string $filename, ?int $thumbtime = null ) { |
208 | if ( !ExtensionRegistry::getInstance()->isLoaded( 'TimedMediaHandler' ) ) { |
209 | Util::logText( 'TimedMediaHandler not loaded' ); |
210 | return ''; |
211 | } |
212 | try { |
213 | $title = Title::newFromTextThrow( 'File:' . $filename ); |
214 | } catch ( MalformedTitleException $e ) { |
215 | Util::logText( $e->getMessage(), [ 'filename' => $filename ] ); |
216 | return ''; |
217 | } |
218 | $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $title ); |
219 | if ( !$file ) { |
220 | Util::logText( "File not found: $filename" ); |
221 | return ''; |
222 | } |
223 | |
224 | $output->addModules( [ 'ext.tmh.player' ] ); |
225 | $output->addModuleStyles( [ 'ext.tmh.player.styles' ] ); |
226 | |
227 | $params = []; |
228 | if ( Util::isMobile( $this->context->getSkin() ) ) { |
229 | // For mobile, we don't know the width, so we pick a somewhat arbitrary height |
230 | // to keep the controls for the video close to the thumbnail. |
231 | $params['height'] = 200; |
232 | } else { |
233 | // Set same width as benefits container on desktop. |
234 | $params['width'] = 400; |
235 | } |
236 | if ( $thumbtime !== null ) { |
237 | $params['thumbtime'] = $thumbtime; |
238 | } |
239 | $html = Linker::makeImageLink( |
240 | MediaWikiServices::getInstance()->getParser(), |
241 | $title, |
242 | $file, |
243 | [ 'align' => 'center' ], |
244 | $params |
245 | ); |
246 | return Html::rawElement( 'div', [ 'class' => 'mw-ge-video' ], $html ); |
247 | } |
248 | |
249 | } |