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