Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
6.71% |
29 / 432 |
|
0.00% |
0 / 36 |
CRAP | |
0.00% |
0 / 1 |
SuggestedEdits | |
6.71% |
29 / 432 |
|
0.00% |
0 / 36 |
8548.26 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
getHeaderTextElement | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
getTasksPaginationText | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
isEnabledForAnyone | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isEnabled | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCssClasses | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
isTopicMatchingEnabled | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
isTopicMatchModeEnabled | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getTopicFiltersPref | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
isGuidanceEnabledForAnyone | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isGuidanceEnabled | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
getHtml | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getJsData | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
90 | |||
isActivated | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getState | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getTaskSet | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
20 | |||
resetTaskCache | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
canRender | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getHeaderText | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getHeaderIconName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getBody | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
6 | |||
getFooter | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
getMobileSummaryBody | |
0.00% |
0 / 45 |
|
0.00% |
0 / 1 |
20 | |||
getFiltersButtonGroupWidget | |
30.21% |
29 / 96 |
|
0.00% |
0 / 1 |
202.83 | |||
getTaskCard | |
0.00% |
0 / 47 |
|
0.00% |
0 / 1 |
12 | |||
getSubheader | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getSubheaderTag | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getModuleStyles | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
getModules | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getSiteViews | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
formatSiteViews | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
getActionData | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
30 | |||
getJsConfigVars | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getPager | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getRedirectParams | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
getNavigationWidgetFactory | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\HomepageModules; |
4 | |
5 | use GrowthExperiments\EditInfoService; |
6 | use GrowthExperiments\ExperimentUserManager; |
7 | use GrowthExperiments\HomepageModules\SuggestedEditsComponents\CardWrapper; |
8 | use GrowthExperiments\HomepageModules\SuggestedEditsComponents\NavigationWidgetFactory; |
9 | use GrowthExperiments\HomepageModules\SuggestedEditsComponents\TaskExplanationWidget; |
10 | use GrowthExperiments\NewcomerTasks\CampaignConfig; |
11 | use GrowthExperiments\NewcomerTasks\ConfigurationLoader\ConfigurationLoader; |
12 | use GrowthExperiments\NewcomerTasks\ConfigurationLoader\PageConfigurationLoader; |
13 | use GrowthExperiments\NewcomerTasks\ImageRecommendationFilter; |
14 | use GrowthExperiments\NewcomerTasks\LinkRecommendationFilter; |
15 | use GrowthExperiments\NewcomerTasks\NewcomerTasksUserOptionsLookup; |
16 | use GrowthExperiments\NewcomerTasks\ProtectionFilter; |
17 | use GrowthExperiments\NewcomerTasks\Task\TaskSet; |
18 | use GrowthExperiments\NewcomerTasks\Task\TaskSetFilters; |
19 | use GrowthExperiments\NewcomerTasks\TaskSuggester\SearchStrategy\SearchStrategy; |
20 | use GrowthExperiments\NewcomerTasks\TaskSuggester\TaskSuggester; |
21 | use GrowthExperiments\NewcomerTasks\TaskType\ImageRecommendationBaseTaskType; |
22 | use GrowthExperiments\NewcomerTasks\TaskType\TaskType; |
23 | use GrowthExperiments\NewcomerTasks\Topic\Topic; |
24 | use IContextSource; |
25 | use MediaWiki\Config\Config; |
26 | use MediaWiki\Deferred\DeferredUpdates; |
27 | use MediaWiki\Extension\PageViewInfo\PageViewService; |
28 | use MediaWiki\Html\Html; |
29 | use MediaWiki\Logger\LoggerFactory; |
30 | use MediaWiki\MediaWikiServices; |
31 | use MediaWiki\Status\Status; |
32 | use MediaWiki\Title\TitleFactory; |
33 | use MediaWiki\User\Options\UserOptionsLookup; |
34 | use MediaWiki\User\UserIdentity; |
35 | use Message; |
36 | use OOUI\ButtonGroupWidget; |
37 | use OOUI\ButtonWidget; |
38 | use OOUI\Exception; |
39 | use OOUI\HtmlSnippet; |
40 | use OOUI\IconWidget; |
41 | use OOUI\Tag; |
42 | use RuntimeException; |
43 | use StatusValue; |
44 | |
45 | /** |
46 | * Homepage module that displays a list of recommended tasks. |
47 | * This is JS-only functionality; most of the logic is in the |
48 | * ext.growthExperiments.Homepage.SuggestedEdits module. |
49 | */ |
50 | class SuggestedEdits extends BaseModule { |
51 | |
52 | /** |
53 | * User preference to track that suggested edits should be shown to the user (instead of an |
54 | * onboarding dialog). Might be ignored in some situations. |
55 | */ |
56 | public const ACTIVATED_PREF = 'growthexperiments-homepage-suggestededits-activated'; |
57 | /** User preference to track that suggested edits were enabled this user automatically on signup. Not used. */ |
58 | public const PREACTIVATED_PREF = 'growthexperiments-homepage-suggestededits-preactivated'; |
59 | /** User preference used to remember the user's topic selection, when using morelike topics. */ |
60 | public const TOPICS_PREF = 'growthexperiments-homepage-se-topic-filters'; |
61 | /** User preference used to remember the user's topic selection, when using ORES topics. */ |
62 | public const TOPICS_ORES_PREF = 'growthexperiments-homepage-se-ores-topic-filters'; |
63 | /** User preference used to remember the user's topic mode selection, when using any type of topics. */ |
64 | public const TOPICS_MATCH_MODE_PREF = 'growthexperiments-homepage-se-topic-filters-mode'; |
65 | /** User preference used to remember the user's task type selection. */ |
66 | public const TASKTYPES_PREF = 'growthexperiments-homepage-se-filters'; |
67 | /** User preference for opting into guidance, when $wgGENewcomerTasksGuidanceRequiresOptIn is true. */ |
68 | public const GUIDANCE_ENABLED_PREF = 'growthexperiments-guidance-enabled'; |
69 | /** |
70 | * Default value for TASKTYPES_PREF. |
71 | * |
72 | * Depending on whether link recommendations are available for the wiki, either 'links' or 'link-recommendation' |
73 | * will be shown, see NewcomerTasksUserOptionsLookup::getTaskTypeFilter(). |
74 | */ |
75 | public const DEFAULT_TASK_TYPES = [ 'copyedit', 'links', 'link-recommendation' ]; |
76 | |
77 | /** |
78 | * Used to keep track of the state of user interactions with suggested edits per type per skin. |
79 | * See also HomepageHooks::onLocalUserCreated |
80 | */ |
81 | public const GUIDANCE_BLUE_DOT_PREF = |
82 | 'growthexperiments-homepage-suggestededits-guidance-blue-dot'; |
83 | |
84 | /** |
85 | * Used to keep track of the whether the user has opted out of seeing Add a Link onboarding |
86 | */ |
87 | public const ADD_LINK_ONBOARDING_PREF = 'growthexperiments-addlink-onboarding'; |
88 | |
89 | /** |
90 | * Used to keep track of the whether the user has opted out of seeing Add an Image onboarding |
91 | */ |
92 | public const ADD_IMAGE_ONBOARDING_PREF = 'growthexperiments-addimage-onboarding'; |
93 | |
94 | /** |
95 | * Used to keep track of the whether the user has opted out of seeing onboarding for |
96 | * the caption step of Add Image |
97 | */ |
98 | public const ADD_IMAGE_CAPTION_ONBOARDING_PREF = 'growthexperiments-addimage-caption-onboarding'; |
99 | |
100 | /** |
101 | * Used to keep track of the whether the user has opted out of seeing "Add a section image" onboarding |
102 | */ |
103 | public const ADD_SECTION_IMAGE_ONBOARDING_PREF = 'growthexperiments-addsectionimage-onboarding'; |
104 | |
105 | /** |
106 | * Used to keep track of the whether the user has opted out of seeing onboarding for |
107 | * the caption step of Add Section Image |
108 | */ |
109 | public const ADD_SECTION_IMAGE_CAPTION_ONBOARDING_PREF = 'growthexperiments-addsectionimage-caption-onboarding'; |
110 | |
111 | /** @var EditInfoService */ |
112 | private $editInfoService; |
113 | |
114 | /** @var ExperimentUserManager */ |
115 | private $experimentUserManager; |
116 | |
117 | /** @var PageViewService|null */ |
118 | private $pageViewService; |
119 | |
120 | /** @var ConfigurationLoader */ |
121 | private $configurationLoader; |
122 | |
123 | /** @var NewcomerTasksUserOptionsLookup */ |
124 | private $newcomerTasksUserOptionsLookup; |
125 | |
126 | /** @var TaskSuggester */ |
127 | private $taskSuggester; |
128 | |
129 | /** @var TitleFactory */ |
130 | private $titleFactory; |
131 | |
132 | /** @var ProtectionFilter */ |
133 | private $protectionFilter; |
134 | |
135 | /** @var string[] cache key => HTML */ |
136 | private $htmlCache = []; |
137 | |
138 | /** @var TaskSet|StatusValue */ |
139 | private $tasks; |
140 | |
141 | /** @var ButtonGroupWidget */ |
142 | private $buttonGroupWidget; |
143 | |
144 | /** @var UserOptionsLookup */ |
145 | private $userOptionsLookup; |
146 | |
147 | /** @var NavigationWidgetFactory */ |
148 | private $navigationWidgetFactory; |
149 | |
150 | /** @var LinkRecommendationFilter */ |
151 | private $linkRecommendationFilter; |
152 | /** |
153 | * @var ImageRecommendationFilter |
154 | */ |
155 | private $imageRecommendationFilter; |
156 | /** |
157 | * @var CampaignConfig |
158 | */ |
159 | private $campaignConfig; |
160 | |
161 | /** |
162 | * @param IContextSource $context |
163 | * @param Config $wikiConfig |
164 | * @param CampaignConfig $campaignConfig |
165 | * @param EditInfoService $editInfoService |
166 | * @param ExperimentUserManager $experimentUserManager |
167 | * @param PageViewService|null $pageViewService |
168 | * @param ConfigurationLoader $configurationLoader |
169 | * @param NewcomerTasksUserOptionsLookup $newcomerTasksUserOptionsLookup |
170 | * @param TaskSuggester $taskSuggester |
171 | * @param TitleFactory $titleFactory |
172 | * @param ProtectionFilter $protectionFilter |
173 | * @param UserOptionsLookup $userOptionsLookup |
174 | * @param LinkRecommendationFilter $linkRecommendationFilter |
175 | * @param ImageRecommendationFilter $imageRecommendationFilter |
176 | */ |
177 | public function __construct( |
178 | IContextSource $context, |
179 | Config $wikiConfig, |
180 | CampaignConfig $campaignConfig, |
181 | EditInfoService $editInfoService, |
182 | ExperimentUserManager $experimentUserManager, |
183 | ?PageViewService $pageViewService, |
184 | ConfigurationLoader $configurationLoader, |
185 | NewcomerTasksUserOptionsLookup $newcomerTasksUserOptionsLookup, |
186 | TaskSuggester $taskSuggester, |
187 | TitleFactory $titleFactory, |
188 | ProtectionFilter $protectionFilter, |
189 | UserOptionsLookup $userOptionsLookup, |
190 | LinkRecommendationFilter $linkRecommendationFilter, |
191 | ImageRecommendationFilter $imageRecommendationFilter |
192 | ) { |
193 | parent::__construct( 'suggested-edits', $context, $wikiConfig, $experimentUserManager ); |
194 | $this->editInfoService = $editInfoService; |
195 | $this->experimentUserManager = $experimentUserManager; |
196 | $this->pageViewService = $pageViewService; |
197 | $this->configurationLoader = $configurationLoader; |
198 | $this->newcomerTasksUserOptionsLookup = $newcomerTasksUserOptionsLookup; |
199 | $this->taskSuggester = $taskSuggester; |
200 | $this->titleFactory = $titleFactory; |
201 | $this->protectionFilter = $protectionFilter; |
202 | $this->userOptionsLookup = $userOptionsLookup; |
203 | $this->linkRecommendationFilter = $linkRecommendationFilter; |
204 | $this->imageRecommendationFilter = $imageRecommendationFilter; |
205 | $this->campaignConfig = $campaignConfig; |
206 | } |
207 | |
208 | /** @inheritDoc */ |
209 | protected function getHeaderTextElement() { |
210 | $context = $this->getContext(); |
211 | if ( $this->getMode() === self::RENDER_DESKTOP ) { |
212 | return Html::element( |
213 | 'div', |
214 | [ 'class' => self::BASE_CSS_CLASS . '-header-text' ], |
215 | $this->getHeaderText() ) . |
216 | new ButtonWidget( [ |
217 | 'id' => 'mw-ge-homepage-suggestededits-info', |
218 | 'icon' => 'info-unpadded', |
219 | 'framed' => false, |
220 | 'title' => $context->msg( 'growthexperiments-homepage-suggestededits-more-info' )->text(), |
221 | 'label' => $context->msg( 'growthexperiments-homepage-suggestededits-more-info' )->text(), |
222 | 'invisibleLabel' => true, |
223 | 'infusable' => true, |
224 | ] ); |
225 | } else { |
226 | return parent::getHeaderTextElement(); |
227 | } |
228 | } |
229 | |
230 | /** |
231 | * Return the pagination text in the form "1 of 30" being 30 the total number of tasks shown |
232 | * @return string |
233 | */ |
234 | protected function getTasksPaginationText() { |
235 | $tasks = $this->getTaskSet(); |
236 | |
237 | return $this->getContext()->msg( 'growthexperiments-homepage-suggestededits-pager' ) |
238 | ->numParams( 1, $tasks->getTotalCount() ) |
239 | ->parse(); |
240 | } |
241 | |
242 | /** |
243 | * Check whether the suggested edits feature is (or could be) enabled for anyone |
244 | * on the wiki. |
245 | * @param Config $config |
246 | * @return bool |
247 | */ |
248 | public static function isEnabledForAnyone( Config $config ) { |
249 | return $config->get( 'GEHomepageSuggestedEditsEnabled' ); |
250 | } |
251 | |
252 | /** |
253 | * Check whether the suggested edits feature is enabled according to the configuration. |
254 | * @param Config $config |
255 | * @return bool |
256 | */ |
257 | public static function isEnabled( Config $config ): bool { |
258 | return self::isEnabledForAnyone( $config ); |
259 | } |
260 | |
261 | /** @inheritDoc */ |
262 | public function getCssClasses() { |
263 | return array_merge( parent::getCssClasses(), |
264 | $this->userOptionsLookup->getOption( $this->getContext()->getUser(), self::ACTIVATED_PREF ) ? |
265 | [ 'activated' ] : |
266 | [ 'unactivated' ] |
267 | ); |
268 | } |
269 | |
270 | /** |
271 | * Check whether topic matching has been enabled for the context user. |
272 | * Note that even with topic matching disabled, all the relevant backend functionality |
273 | * should still work (but logging and UI will be different). |
274 | * @param IContextSource $context |
275 | * @param UserOptionsLookup $userOptionsLookup |
276 | * @return bool |
277 | */ |
278 | public static function isTopicMatchingEnabled( |
279 | IContextSource $context, |
280 | UserOptionsLookup $userOptionsLookup |
281 | ) { |
282 | return self::isEnabled( $context->getConfig() ) && |
283 | $context->getConfig()->get( 'GEHomepageSuggestedEditsEnableTopics' ); |
284 | } |
285 | |
286 | /** |
287 | * Check whether topic match mode has been enabled for the context user. |
288 | * Note that even with topic match mode is disabled, all the relevant backend functionality |
289 | * should still work (but logging and UI will be different). |
290 | * @param IContextSource $context |
291 | * @param UserOptionsLookup $userOptionsLookup |
292 | * @return bool |
293 | */ |
294 | private function isTopicMatchModeEnabled( |
295 | IContextSource $context, |
296 | UserOptionsLookup $userOptionsLookup |
297 | ) { |
298 | return self::isTopicMatchingEnabled( $context, $userOptionsLookup ) && |
299 | $this->campaignConfig->isUserInCampaign( |
300 | $context->getUser(), |
301 | 'growth-glam-2022' |
302 | ); |
303 | } |
304 | |
305 | /** |
306 | * Get the name of the preference to use for storing topic filters. |
307 | * @param Config $config |
308 | * @return string |
309 | */ |
310 | public static function getTopicFiltersPref( Config $config ) { |
311 | $topicType = $config->get( 'GENewcomerTasksTopicType' ); |
312 | if ( $topicType === PageConfigurationLoader::CONFIGURATION_TYPE_ORES ) { |
313 | return self::TOPICS_ORES_PREF; |
314 | } |
315 | return self::TOPICS_PREF; |
316 | } |
317 | |
318 | /** |
319 | * Check if guidance feature is enabled for suggested edits. |
320 | * |
321 | * @param IContextSource $context |
322 | * @return bool |
323 | */ |
324 | public static function isGuidanceEnabledForAnyone( IContextSource $context ): bool { |
325 | return $context->getConfig()->get( 'GENewcomerTasksGuidanceEnabled' ); |
326 | } |
327 | |
328 | /** |
329 | * Check if guidance feature is enabled for suggested edits. |
330 | * |
331 | * @param IContextSource $context |
332 | * @return bool |
333 | */ |
334 | public static function isGuidanceEnabled( IContextSource $context ): bool { |
335 | $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup(); |
336 | return self::isGuidanceEnabledForAnyone( $context ) && ( |
337 | !$context->getConfig()->get( 'GENewcomerTasksGuidanceRequiresOptIn' ) || |
338 | $userOptionsLookup->getBoolOption( $context->getUser(), self::GUIDANCE_ENABLED_PREF ) ); |
339 | } |
340 | |
341 | /** @inheritDoc */ |
342 | public function getHtml() { |
343 | // This method will be called both directly by the homepage and by getJsData() in |
344 | // some cases, so use some lightweight caching. |
345 | $key = $this->getMode() . ':' . $this->getContext()->getLanguage()->getCode(); |
346 | if ( !array_key_exists( $key, $this->htmlCache ) ) { |
347 | $this->htmlCache[$key] = parent::getHtml(); |
348 | } |
349 | return $this->htmlCache[$key]; |
350 | } |
351 | |
352 | /** @inheritDoc */ |
353 | public function getJsData( $mode ) { |
354 | $data = parent::getJsData( $mode ); |
355 | $data['task-preview'] = [ 'noresults' => true ]; |
356 | |
357 | // Preload task card and queue for users who have the module activated |
358 | if ( $this->canRender() ) { |
359 | $tasks = $this->getTaskSet(); |
360 | if ( $tasks instanceof StatusValue ) { |
361 | $data['task-preview'] = [ |
362 | 'error' => Status::wrap( $tasks )->getMessage( false, false, 'en' )->parse(), |
363 | ]; |
364 | } elseif ( $tasks->count() === 0 ) { |
365 | $data['task-preview'] = [ 'noresults' => true ]; |
366 | } else { |
367 | $formattedTasks = []; |
368 | foreach ( $tasks as $task ) { |
369 | $title = $this->titleFactory->newFromLinkTarget( $task->getTitle() ); |
370 | $taskData = [ |
371 | 'tasktype' => $task->getTaskType()->getId(), |
372 | 'difficulty' => $task->getTaskType()->getDifficulty(), |
373 | 'qualityGateIds' => $task->getTaskType()->getQualityGateIds(), |
374 | 'qualityGateConfig' => $tasks->getQualityGateConfig(), |
375 | 'title' => $title->getPrefixedText(), |
376 | 'topics' => $task->getTopicScores(), |
377 | // The front-end code for constructing SuggestedEditCardWidget checks |
378 | // to see if pageId is set in order to construct a tracking URL. |
379 | 'pageId' => $title->getArticleID(), |
380 | 'token' => $task->getToken(), |
381 | ]; |
382 | if ( $task->getTaskType() instanceof ImageRecommendationBaseTaskType ) { |
383 | // Prevent loading of thumbnail for image recommendation tasks. |
384 | // TODO: Maybe there should be a property on the task type to check |
385 | // rather than special casing image recommendation here |
386 | $taskData['thumbnailSource'] = null; |
387 | } |
388 | $formattedTasks[] = $taskData; |
389 | } |
390 | $data['task-queue'] = $formattedTasks; |
391 | $data['task-preview'] = current( $formattedTasks ); |
392 | } |
393 | } |
394 | |
395 | // When the module is not activated yet, but can be, include module HTML in the |
396 | // data, for dynamic loading on activation. |
397 | if ( $this->canRender() && |
398 | !self::isActivated( $this->getContext()->getUser(), $this->userOptionsLookup ) && |
399 | $this->getMode() !== self::RENDER_MOBILE_DETAILS |
400 | ) { |
401 | $data += [ |
402 | 'html' => $this->getHtml(), |
403 | 'rlModules' => $this->getModules(), |
404 | ]; |
405 | } |
406 | |
407 | return $data; |
408 | } |
409 | |
410 | /** |
411 | * Check whether suggested edits have been activated for the given user. |
412 | * Before activation, suggested edits are exposed via the StartEditing module; |
413 | * after activation (which happens by interacting with that module) via this one. |
414 | * @param UserIdentity $user |
415 | * @param UserOptionsLookup $userOptionsLookup |
416 | * @return bool |
417 | */ |
418 | public static function isActivated( |
419 | UserIdentity $user, |
420 | UserOptionsLookup $userOptionsLookup |
421 | ) { |
422 | return $userOptionsLookup->getBoolOption( $user, self::ACTIVATED_PREF ); |
423 | } |
424 | |
425 | /** @inheritDoc */ |
426 | public function getState() { |
427 | return self::isActivated( $this->getContext()->getUser(), $this->userOptionsLookup ) ? |
428 | self::MODULE_STATE_ACTIVATED : |
429 | self::MODULE_STATE_UNACTIVATED; |
430 | } |
431 | |
432 | /** |
433 | * Get a suggested task set, with in-process caching. |
434 | * @return TaskSet|StatusValue |
435 | */ |
436 | private function getTaskSet() { |
437 | if ( $this->tasks ) { |
438 | return $this->tasks; |
439 | } |
440 | $user = $this->getContext()->getUser(); |
441 | $suggesterOptions = [ 'revalidateCache' => false ]; |
442 | if ( $this->getContext()->getRequest()->getCheck( 'resetTaskCache' ) ) { |
443 | $suggesterOptions = [ 'resetCache' => true ]; |
444 | // TODO also reset cache in ImageRecommendationFilter |
445 | } |
446 | $taskTypes = $this->newcomerTasksUserOptionsLookup->getTaskTypeFilter( $user ); |
447 | $topics = $this->newcomerTasksUserOptionsLookup->getTopics( $user ); |
448 | $topicsMatchMode = $this->newcomerTasksUserOptionsLookup->getTopicsMatchMode( $user ); |
449 | $taskSetFilters = new TaskSetFilters( $taskTypes, $topics, $topicsMatchMode ); |
450 | $tasks = $this->taskSuggester->suggest( $user, $taskSetFilters, null, null, |
451 | $suggesterOptions ); |
452 | if ( $tasks instanceof TaskSet ) { |
453 | // If there are link recommendation tasks without corresponding DB entries, these will be removed |
454 | // from the TaskSet. |
455 | $tasks = $this->linkRecommendationFilter->filter( $tasks ); |
456 | $tasks = $this->imageRecommendationFilter->filter( $tasks ); |
457 | $tasks = $this->protectionFilter->filter( $tasks ); |
458 | } |
459 | $this->tasks = $tasks; |
460 | $this->resetTaskCache( $user, $taskSetFilters, $suggesterOptions ); |
461 | return $this->tasks; |
462 | } |
463 | |
464 | /** |
465 | * Refresh the user's task cache in a deferred update. |
466 | * |
467 | * @param UserIdentity $user |
468 | * @param TaskSetFilters $taskSetFilters |
469 | * @param array $suggesterOptions |
470 | * @return void |
471 | */ |
472 | public function resetTaskCache( UserIdentity $user, TaskSetFilters $taskSetFilters, array $suggesterOptions ) { |
473 | DeferredUpdates::addCallableUpdate( function () use ( $user, $taskSetFilters, $suggesterOptions ) { |
474 | $suggesterOptions['resetCache'] = true; |
475 | $this->taskSuggester->suggest( $user, $taskSetFilters, null, null, $suggesterOptions ); |
476 | } ); |
477 | } |
478 | |
479 | /** @inheritDoc */ |
480 | protected function canRender() { |
481 | return self::isEnabled( $this->getContext()->getConfig() ) |
482 | && !$this->configurationLoader->loadTaskTypes() instanceof StatusValue; |
483 | } |
484 | |
485 | /** @inheritDoc */ |
486 | protected function getHeaderText() { |
487 | return $this->getContext() |
488 | ->msg( 'growthexperiments-homepage-suggested-edits-header' ) |
489 | ->text(); |
490 | } |
491 | |
492 | /** @inheritDoc */ |
493 | protected function getHeaderIconName() { |
494 | return 'lightbulb'; |
495 | } |
496 | |
497 | /** @inheritDoc */ |
498 | protected function getBody() { |
499 | $isDesktop = $this->getMode() === self::RENDER_DESKTOP; |
500 | $topicMatchMode = $this->newcomerTasksUserOptionsLookup->getTopicsMatchMode( $this->getUser() ); |
501 | return Html::rawElement( |
502 | 'div', [ 'class' => 'suggested-edits-module-wrapper' ], |
503 | ( new Tag( 'div' ) ) |
504 | ->addClasses( [ 'suggested-edits-filters' ] ) |
505 | ->appendContent( $isDesktop ? $this->getFiltersButtonGroupWidget() : '' ) . |
506 | ( new Tag( 'div' ) ) |
507 | ->addClasses( [ 'suggested-edits-pager' ] ) |
508 | ->appendContent( $this->getPager() ) . |
509 | ( new CardWrapper( |
510 | $this->getContext(), |
511 | self::isTopicMatchingEnabled( $this->getContext(), $this->userOptionsLookup ), |
512 | $topicMatchMode === SearchStrategy::TOPIC_MATCH_MODE_AND, |
513 | $this->getContext()->getLanguage()->getDir(), |
514 | $this->getTaskSet(), |
515 | $this->getNavigationWidgetFactory(), |
516 | $isDesktop |
517 | ) )->render() . |
518 | ( new Tag( 'div' ) )->addClasses( [ 'suggested-edits-task-explanation' ] ) |
519 | ->appendContent( ( new TaskExplanationWidget( [ |
520 | 'taskSet' => $this->getTaskSet(), |
521 | 'localizer' => $this->getContext() |
522 | ] ) ) ) |
523 | ); |
524 | } |
525 | |
526 | /** @inheritDoc */ |
527 | protected function getFooter() { |
528 | if ( $this->getMode() === self::RENDER_DESKTOP ) { |
529 | $siteViewsCount = $this->getSiteViews(); |
530 | $siteViewsMessage = $siteViewsCount ? |
531 | $this->getContext()->msg( 'growthexperiments-homepage-suggestededits-footer' ) |
532 | ->params( $this->formatSiteViews( $siteViewsCount ) ) : |
533 | $this->getContext()->msg( 'growthexperiments-homepage-suggestededits-footer-noviews' ); |
534 | return $siteViewsMessage->parse(); |
535 | } |
536 | return ( new Tag( 'div' ) )->addClasses( [ 'suggested-edits-footer-navigation' ] ) |
537 | ->appendContent( [ |
538 | $this->getNavigationWidgetFactory()->getPreviousNextButtonHtml( 'Previous' ), |
539 | $this->getNavigationWidgetFactory()->getEditButton(), |
540 | $this->getNavigationWidgetFactory()->getPreviousNextButtonHtml( 'Next' ) |
541 | ] ); |
542 | } |
543 | |
544 | /** |
545 | * @inheritDoc |
546 | */ |
547 | protected function getMobileSummaryBody() { |
548 | $tasks = $this->getTaskSet(); |
549 | // If the task cannot be loaded, fall back to the old summary style for now. |
550 | $showTaskPreview = $tasks instanceof TaskSet && $tasks->count() > 0; |
551 | |
552 | if ( $showTaskPreview ) { |
553 | $button = new ButtonWidget( [ |
554 | 'label' => $this->getContext()->msg( |
555 | 'growthexperiments-homepage-suggestededits-mobilesummary-footer-button' )->text(), |
556 | 'classes' => [ 'suggested-edits-preview-cta-button' ], |
557 | 'flags' => [ 'primary', 'progressive' ], |
558 | // Avoid nesting links, browsers will break markup |
559 | 'button' => new Tag( 'span' ), |
560 | ] ); |
561 | $centeredButton = Html::rawElement( 'div', [ 'class' => 'suggested-edits-preview-footer' ], $button ); |
562 | $subheader = Html::rawElement( |
563 | 'div', |
564 | [ 'class' => 'suggested-edits-preview-pager' ], |
565 | $this->getTasksPaginationText() |
566 | ); |
567 | |
568 | return Html::rawElement( 'div', [ 'class' => [ 'growthexperiments-task-preview-widget' ] ], |
569 | $subheader . $this->getTaskCard() . $centeredButton ); |
570 | } else { |
571 | // For some reason phan thinks $siteEditsPerDay and/or $metricNumber get double-escaped, |
572 | // but they are escaped just the right amount. |
573 | $siteEditsPerDay = $this->editInfoService->getEditsPerDay(); |
574 | if ( $siteEditsPerDay instanceof StatusValue ) { |
575 | LoggerFactory::getInstance( 'GrowthExperiments' )->warning( |
576 | 'Failed to load site edits per day stat: {status}', |
577 | [ 'status' => Status::wrap( $siteEditsPerDay )->getWikiText( false, false, 'en' ) ] |
578 | ); |
579 | // TODO probably have some kind of fallback message? |
580 | $siteEditsPerDay = 0; |
581 | } |
582 | $metricNumber = $this->getContext()->getLanguage()->formatNum( $siteEditsPerDay ); |
583 | $metricSubtitle = $this->getContext() |
584 | ->msg( 'growthexperiments-homepage-suggestededits-mobilesummary-metricssubtitle' ) |
585 | ->params( $siteEditsPerDay ) |
586 | ->text(); |
587 | $footerText = $this->getContext() |
588 | ->msg( 'growthexperiments-homepage-suggestededits-mobilesummary-footer' ) |
589 | ->text(); |
590 | $noTaskPreviewContent = Html::rawElement( 'div', [ 'class' => 'suggested-edits-main' ], |
591 | Html::rawElement( 'div', [ 'class' => 'suggested-edits-icon' ] ) . |
592 | Html::rawElement( 'div', [ 'class' => 'suggested-edits-metric' ], |
593 | Html::element( 'div', [ 'class' => 'suggested-edits-metric-number' ], $metricNumber ) . |
594 | Html::element( 'div', [ 'class' => 'suggested-edits-metric-subtitle' ], $metricSubtitle ) |
595 | ) |
596 | ) . Html::element( 'div', [ |
597 | 'class' => 'suggested-edits-footer' |
598 | ], $footerText ); |
599 | return Html::rawElement( 'div', [ |
600 | 'class' => [ 'growthexperiments-last-day-edits-widget' ] |
601 | ], $noTaskPreviewContent ); |
602 | } |
603 | } |
604 | |
605 | /** |
606 | * Generate a button group widget with task and topic filters. |
607 | * |
608 | * This function should be kept in sync with |
609 | * SuggestedEditsFiltersWidget.prototype.updateButtonLabelAndIcon |
610 | * @return ButtonGroupWidget |
611 | */ |
612 | private function getFiltersButtonGroupWidget(): ButtonGroupWidget { |
613 | $buttons = []; |
614 | $user = $this->getContext()->getUser(); |
615 | if ( self::isTopicMatchingEnabled( $this->getContext(), $this->userOptionsLookup ) ) { |
616 | // topicPreferences will be an empty array if the user had saved topics |
617 | // in the past, or null if they have never saved topics |
618 | $topicPreferences = $this->newcomerTasksUserOptionsLookup |
619 | ->getTopicFilterWithoutFallback( $user ); |
620 | $excludedTopics = $this->campaignConfig->getTopicsToExcludeForUser( $user ); |
621 | // Filter out campaign-specific topics that are no longer available |
622 | if ( $topicPreferences && count( $excludedTopics ) ) { |
623 | $topicPreferences = array_diff( $topicPreferences, $excludedTopics ); |
624 | } |
625 | $topicData = $this->configurationLoader->getTopics(); |
626 | $topicLabel = ''; |
627 | $addPulsatingDot = false; |
628 | $topicFilterMode = $this->newcomerTasksUserOptionsLookup->getTopicsMatchMode( $user ); |
629 | $flags = []; |
630 | if ( !$topicPreferences ) { |
631 | if ( $topicPreferences === null ) { |
632 | $flags = [ 'progressive' ]; |
633 | $addPulsatingDot = true; |
634 | } |
635 | $topicLabel = |
636 | $this->getContext() |
637 | ->msg( 'growthexperiments-homepage-suggestededits-topic-filter-select-interests' ) |
638 | ->text(); |
639 | } else { |
640 | $topicMessages = []; |
641 | foreach ( $topicPreferences as $topicPreference ) { |
642 | $topic = $topicData[$topicPreference] ?? null; |
643 | if ( $topic instanceof Topic ) { |
644 | $topicMessages[] = $topic->getName( $this->getContext() ); |
645 | } |
646 | } |
647 | $topicMessages = array_filter( $topicMessages ); |
648 | if ( count( $topicMessages ) ) { |
649 | if ( count( $topicMessages ) < 3 ) { |
650 | $separator = $topicFilterMode === SearchStrategy::TOPIC_MATCH_MODE_OR ? |
651 | $this->getContext()->msg( 'comma-separator' ) : ' + '; |
652 | $topicLabel = implode( $separator, $topicMessages ); |
653 | } else { |
654 | $topicLabel = |
655 | $this->getContext() |
656 | ->msg( 'growthexperiments-homepage-suggestededits-topics-button-topic-count' ) |
657 | ->numParams( count( $topicMessages ) ) |
658 | ->text(); |
659 | } |
660 | } |
661 | } |
662 | |
663 | $topicFilterButtonWidget = new ButtonWidget( [ |
664 | 'label' => $topicLabel, |
665 | 'flags' => $flags, |
666 | 'classes' => [ 'topic-matching', 'topic-filter-button' ], |
667 | 'indicator' => $this->getMode() === self::RENDER_DESKTOP ? null : 'down', |
668 | 'icon' => $topicFilterMode === SearchStrategy::TOPIC_MATCH_MODE_OR ? 'funnel' : 'funnel-add' |
669 | ] ); |
670 | if ( $addPulsatingDot ) { |
671 | $topicFilterButtonWidget->appendContent( |
672 | ( new Tag( 'div' ) )->addClasses( [ 'mw-pulsating-dot' ] ) |
673 | ); |
674 | } |
675 | $buttons[] = $topicFilterButtonWidget; |
676 | } |
677 | $difficultyFilterButtonWidget = new ButtonWidget( [ |
678 | 'icon' => 'difficulty-outline', |
679 | 'classes' => self::isTopicMatchingEnabled( $this->getContext(), $this->userOptionsLookup ) ? |
680 | [ 'topic-matching' ] : [ '' ], |
681 | 'label' => $this->getContext()->msg( |
682 | 'growthexperiments-homepage-suggestededits-difficulty-filters-title' |
683 | )->text(), |
684 | 'indicator' => $this->getMode() === self::RENDER_DESKTOP ? null : 'down' |
685 | ] ); |
686 | |
687 | $levels = []; |
688 | $taskTypeData = $this->configurationLoader->getTaskTypes(); |
689 | foreach ( $this->newcomerTasksUserOptionsLookup->getTaskTypeFilter( $user ) as $taskTypeId ) { |
690 | /** @var TaskType $taskType */ |
691 | $taskType = $taskTypeData[$taskTypeId] ?? null; |
692 | if ( $taskType ) { |
693 | // Sometimes the default task types don't exist on a wiki (T268012) |
694 | $levels[ $taskType->getDifficulty() ] = true; |
695 | } |
696 | } |
697 | $taskTypeMessages = []; |
698 | $messageKey = $this->getMode() === self::RENDER_DESKTOP ? |
699 | 'growthexperiments-homepage-suggestededits-difficulty-filter-label' : |
700 | 'growthexperiments-homepage-suggestededits-difficulty-filter-label-mobile'; |
701 | |
702 | foreach ( [ 'easy', 'medium', 'hard' ] as $level ) { |
703 | if ( !isset( $levels[$level] ) ) { |
704 | continue; |
705 | } |
706 | // The following messages are used here: |
707 | // * growthexperiments-homepage-suggestededits-difficulty-filter-label-easy |
708 | // * growthexperiments-homepage-suggestededits-difficulty-filter-label-medium |
709 | // * growthexperiments-homepage-suggestededits-difficulty-filter-label-hard |
710 | $label = $this->getContext()->msg( |
711 | 'growthexperiments-homepage-suggestededits-difficulty-filter-label-' . $level |
712 | ); |
713 | $message = $this->getContext()->msg( $messageKey ) |
714 | ->params( $label ) |
715 | ->text(); |
716 | $difficultyFilterButtonWidget->setLabel( $message ); |
717 | // Icons: difficulty-easy, difficulty-medium, difficulty-hard |
718 | $difficultyFilterButtonWidget->setIcon( 'difficulty-' . $level ); |
719 | $taskTypeMessages[] = $label; |
720 | } |
721 | if ( count( $taskTypeMessages ) > 1 ) { |
722 | $difficultyFilterButtonWidget->setIcon( 'difficulty-outline' ); |
723 | $messageKey = $this->getMode() === self::RENDER_DESKTOP ? |
724 | 'growthexperiments-homepage-suggestededits-difficulty-filter-label' : |
725 | 'growthexperiments-homepage-suggestededits-difficulty-filter-label-mobile'; |
726 | $message = $this->getContext()->msg( $messageKey ) |
727 | ->params( implode( $this->getContext()->msg( 'comma-separator' ), |
728 | $taskTypeMessages ) ) |
729 | ->text(); |
730 | $difficultyFilterButtonWidget->setLabel( $message ); |
731 | } |
732 | |
733 | $buttons[] = $difficultyFilterButtonWidget; |
734 | $this->buttonGroupWidget = new ButtonGroupWidget( [ |
735 | 'class' => 'suggested-edits-filters', |
736 | 'items' => $buttons, |
737 | 'infusable' => true, |
738 | ] ); |
739 | return $this->buttonGroupWidget; |
740 | } |
741 | |
742 | /** |
743 | * Generate HTML identical to that of mw.libs.ge.SmallTaskCard |
744 | * @return string |
745 | */ |
746 | private function getTaskCard() { |
747 | $tasks = $this->getTaskSet(); |
748 | if ( !$tasks instanceof TaskSet ) { |
749 | throw new RuntimeException( 'Expected to have tasks.' ); |
750 | } |
751 | $task = $tasks[0]; |
752 | $taskTypeId = $task->getTaskType()->getId(); |
753 | $title = $this->titleFactory->newFromLinkTarget( $task->getTitle() ); |
754 | |
755 | $imageClasses = array_merge( |
756 | [ 'mw-ge-small-task-card-image' ], |
757 | $task->getTaskType()->getSmallTaskCardImageCssClasses() |
758 | ); |
759 | $image = Html::element( 'div', [ 'class' => |
760 | implode( " ", $imageClasses ) ] ); |
761 | $title = Html::element( 'span', |
762 | [ 'class' => 'mw-ge-small-task-card-title' ], |
763 | $title->getPrefixedText() ); |
764 | $description = Html::element( 'div', |
765 | [ 'class' => 'mw-ge-small-task-card-description skeleton' ] ); |
766 | $taskIcon = new IconWidget( [ 'icon' => 'difficulty-' . $task->getTaskType()->getDifficulty() ] ); |
767 | $iconData = $task->getTaskType()->getIconData(); |
768 | $taskTypeIcon = array_key_exists( 'icon', $iconData ) |
769 | ? new IconWidget( [ 'icon' => $iconData['icon'] ] ) |
770 | : ''; |
771 | $taskType = Html::rawElement( 'span', |
772 | [ 'class' => 'mw-ge-small-task-card-tasktype ' |
773 | // The following classes are used here: |
774 | // * mw-ge-small-task-card-tasktype-difficulty-easy |
775 | // * mw-ge-small-task-card-tasktype-difficulty-medium |
776 | // * mw-ge-small-task-card-tasktype-difficulty-hard |
777 | . 'mw-ge-small-task-card-tasktype-difficulty-' |
778 | . $task->getTaskType()->getDifficulty() ], |
779 | $taskTypeIcon . $taskIcon . Html::element( 'span', |
780 | [ 'class' => 'mw-ge-small-task-card-tasktype-taskname' ], |
781 | $task->getTaskType()->getName( $this->getContext() ) |
782 | ) ); |
783 | |
784 | $glue = Html::element( 'div', |
785 | [ 'class' => 'mw-ge-small-task-card-glue' ] ); |
786 | $cardMetadataContainer = Html::rawElement( 'div', |
787 | [ 'class' => 'mw-ge-small-task-card-metadata-container' ], |
788 | // Unlike SmallTaskCard, this version does not have pageviews. |
789 | $taskType ); |
790 | $cardTextContainer = Html::rawElement( 'div', |
791 | [ 'class' => 'mw-ge-small-task-card-text-container' ], |
792 | $title . $description . $glue . $cardMetadataContainer ); |
793 | return Html::rawElement( 'div', |
794 | // only called for mobile views |
795 | [ 'class' => 'mw-ge-small-task-card mw-ge-small-task-card-mobile ' |
796 | . "mw-ge-small-task-card mw-ge-tasktype-$taskTypeId" |
797 | ], |
798 | $image . $cardTextContainer ); |
799 | } |
800 | |
801 | /** @inheritDoc */ |
802 | protected function getSubheader() { |
803 | // Ugly hack to get the filters positioned outside of the module wrapper on mobile. |
804 | $mobileDetails = [ self::RENDER_MOBILE_DETAILS, self::RENDER_MOBILE_DETAILS_OVERLAY ]; |
805 | if ( !in_array( $this->getMode(), $mobileDetails, true ) ) { |
806 | return ''; |
807 | } |
808 | return Html::rawElement( 'div', [ 'class' => 'suggested-edits-filters' ] ); |
809 | } |
810 | |
811 | /** @inheritDoc */ |
812 | protected function getSubheaderTag() { |
813 | return 'div'; |
814 | } |
815 | |
816 | /** @inheritDoc */ |
817 | protected function getModuleStyles() { |
818 | return array_merge( |
819 | parent::getModuleStyles(), |
820 | [ |
821 | 'mediawiki.pulsatingdot', |
822 | 'oojs-ui.styles.icons-editing-core' |
823 | ] |
824 | ); |
825 | } |
826 | |
827 | /** @inheritDoc */ |
828 | protected function getModules() { |
829 | return array_merge( |
830 | parent::getModules(), |
831 | [ 'ext.growthExperiments.Homepage.SuggestedEdits' ] |
832 | ); |
833 | } |
834 | |
835 | /** |
836 | * Returns daily unique site views, averaged over the last 30 days. |
837 | * @return int|null |
838 | */ |
839 | protected function getSiteViews() { |
840 | if ( !$this->pageViewService || |
841 | !$this->pageViewService->supports( PageViewService::METRIC_UNIQUE, PageViewService::SCOPE_SITE ) |
842 | ) { |
843 | return null; |
844 | } |
845 | // When PageViewService is a WikimediaPageViewService, the pageviews for the last two days |
846 | // or so will be missing due to AQS processing lag. Get some more days and discard the |
847 | // newest ones. |
848 | $status = $this->pageViewService->getSiteData( 32, PageViewService::METRIC_UNIQUE ); |
849 | if ( !$status->isOK() ) { |
850 | return null; |
851 | } |
852 | $data = $status->getValue(); |
853 | ksort( $data ); |
854 | return (int)( array_sum( array_slice( $data, 0, 30 ) ) / 30 ); |
855 | } |
856 | |
857 | /** |
858 | * Format site views count in a human-readable way. |
859 | * @param int $siteViewsCount |
860 | * @return string|array A Message::params() parameter |
861 | */ |
862 | protected function formatSiteViews( int $siteViewsCount ) { |
863 | // We only get here when $siteViewsCount is not 0 so log is safe. |
864 | $siteViewsCount = (int)round( $siteViewsCount, (int)-floor( log10( $siteViewsCount ) ) ); |
865 | $language = $this->getContext()->getLanguage(); |
866 | if ( $this->getContext()->msg( 'growthexperiments-homepage-suggestededits-footer-suffix' ) |
867 | ->isDisabled() |
868 | ) { |
869 | // This language does not use suffixes, just output the rounded number |
870 | return Message::numParam( $siteViewsCount ); |
871 | } |
872 | // Abuse Language::formatComputingNumbers into displaying large numbers in a human-readable way |
873 | return $language->formatComputingNumbers( $siteViewsCount, 1000, |
874 | 'growthexperiments-homepage-suggestededits-footer-$1suffix' ); |
875 | } |
876 | |
877 | /** @inheritDoc */ |
878 | protected function getActionData() { |
879 | $user = $this->getContext()->getUser(); |
880 | $taskSet = $this->getTaskSet(); |
881 | $taskTypes = $topics = null; |
882 | if ( $taskSet instanceof TaskSet ) { |
883 | $taskTypes = $taskSet->getFilters()->getTaskTypeFilters(); |
884 | $topics = $taskSet->getFilters()->getTopicFilters(); |
885 | $topicsMatchMode = $taskSet->getFilters()->getTopicFiltersMode(); |
886 | } |
887 | |
888 | // these will be updated on the client side as needed |
889 | $data = [ |
890 | 'taskTypes' => $taskTypes ?? $this->newcomerTasksUserOptionsLookup->getTaskTypeFilter( $user ), |
891 | 'taskCount' => ( $taskSet instanceof TaskSet ) ? $taskSet->getTotalCount() : 0, |
892 | 'editCount' => $this->editInfoService->getEditsPerDay(), |
893 | ]; |
894 | if ( self::isTopicMatchingEnabled( $this->getContext(), $this->userOptionsLookup ) ) { |
895 | $data['topics'] = $topics ?? $this->newcomerTasksUserOptionsLookup->getTopics( $user ); |
896 | if ( $this->isTopicMatchModeEnabled( $this->getContext(), $this->userOptionsLookup ) ) { |
897 | $data['topicsMatchMode'] = $topicsMatchMode ?? |
898 | $this->newcomerTasksUserOptionsLookup->getTopicsMatchMode( $user ); |
899 | } |
900 | |
901 | } |
902 | return array_merge( parent::getActionData(), $data ); |
903 | } |
904 | |
905 | /** |
906 | * @inheritDoc |
907 | */ |
908 | protected function getJsConfigVars() { |
909 | return [ |
910 | 'GEHomepageSuggestedEditsEnableTopics' => self::isTopicMatchingEnabled( |
911 | $this->getContext(), |
912 | $this->userOptionsLookup |
913 | ) |
914 | ]; |
915 | } |
916 | |
917 | /** |
918 | * Get the pager text (1 of X) to show on server side render. |
919 | * |
920 | * This code roughly corresponds to SuggestedEditPagerWidget.prototype.setMessage |
921 | * |
922 | * @return string |
923 | * @throws Exception |
924 | */ |
925 | private function getPager() { |
926 | $taskSet = $this->getTaskSet(); |
927 | if ( !$taskSet instanceof TaskSet || !$taskSet->count() ) { |
928 | return ''; |
929 | } |
930 | return new HtmlSnippet( $this->getContext()->msg( 'growthexperiments-homepage-suggestededits-pager' ) |
931 | ->numParams( [ 1, $taskSet->getTotalCount() ] ) |
932 | ->parse() ); |
933 | } |
934 | |
935 | /** |
936 | * Get the query params for the redirect URL for the specified task type ID |
937 | * |
938 | * @param string|null $taskTypeId |
939 | * @return array |
940 | */ |
941 | public function getRedirectParams( ?string $taskTypeId = null ): array { |
942 | $taskType = $this->configurationLoader->getTaskTypes()[ $taskTypeId ]; |
943 | if ( !$taskType ) { |
944 | return []; |
945 | } |
946 | |
947 | $redirectParams = []; |
948 | if ( $taskType->shouldOpenInEditMode() ) { |
949 | $redirectParams[ 'veaction' ] = 'edit'; |
950 | } |
951 | if ( (bool)$taskType->getDefaultEditSection() ) { |
952 | $redirectParams[ 'section' ] = $taskType->getDefaultEditSection(); |
953 | } |
954 | return $redirectParams; |
955 | } |
956 | |
957 | /** |
958 | * @return NavigationWidgetFactory |
959 | */ |
960 | private function getNavigationWidgetFactory(): NavigationWidgetFactory { |
961 | if ( !$this->navigationWidgetFactory ) { |
962 | $this->navigationWidgetFactory = new NavigationWidgetFactory( |
963 | $this->getContext(), |
964 | $this->getTaskSet() |
965 | ); |
966 | } |
967 | return $this->navigationWidgetFactory; |
968 | } |
969 | } |