Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
LevelingUpHooks
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 5
272
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 onVisualEditorApiVisualEditorEditPostSave
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 onBeforePageDisplay
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
72
 onBeforeCreateEchoEvent
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
2
 onUserGetDefaultOptions
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace GrowthExperiments\LevelingUp;
4
5use GrowthExperiments\ExperimentUserManager;
6use GrowthExperiments\NewcomerTasks\ConfigurationLoader\ConfigurationLoader;
7use GrowthExperiments\VisualEditorHooks;
8use MediaWiki\Config\Config;
9use MediaWiki\Extension\Notifications\AttributeManager;
10use MediaWiki\Extension\Notifications\UserLocator;
11use MediaWiki\Extension\VisualEditor\VisualEditorApiVisualEditorEditPostSaveHook;
12use MediaWiki\Output\Hook\BeforePageDisplayHook;
13use MediaWiki\Page\ProperPageIdentity;
14use MediaWiki\User\Hook\UserGetDefaultOptionsHook;
15use MediaWiki\User\UserIdentity;
16
17/**
18 * Hooks for the "leveling up" feature.
19 * @see https://www.mediawiki.org/wiki/Growth/Positive_reinforcement#Leveling_up
20 */
21class LevelingUpHooks implements
22    BeforePageDisplayHook,
23    VisualEditorApiVisualEditorEditPostSaveHook,
24    UserGetDefaultOptionsHook
25{
26
27    private Config $config;
28    private ConfigurationLoader $configurationLoader;
29    private ExperimentUserManager $experimentUserManager;
30    private LevelingUpManager $levelingUpManager;
31
32    /**
33     * @param Config $config
34     * @param ConfigurationLoader $configurationLoader
35     * @param ExperimentUserManager $experimentUserManager
36     * @param LevelingUpManager $levelingUpManager
37     */
38    public function __construct(
39        Config $config,
40        ConfigurationLoader $configurationLoader,
41        ExperimentUserManager $experimentUserManager,
42        LevelingUpManager $levelingUpManager
43    ) {
44        $this->config = $config;
45        $this->configurationLoader = $configurationLoader;
46        $this->experimentUserManager = $experimentUserManager;
47        $this->levelingUpManager = $levelingUpManager;
48    }
49
50    /**
51     * Load the InviteToSuggestedEdits module after a VisualEditor save.
52     * @inheritDoc
53     */
54    public function onVisualEditorApiVisualEditorEditPostSave(
55        ProperPageIdentity $page,
56        UserIdentity $user,
57        string $wikitext,
58        array $params,
59        array $pluginData,
60        array $saveResult,
61        array &$apiResponse
62    ): void {
63        $taskTypes = $this->configurationLoader->getTaskTypes();
64        $pluginFields = array_map(
65            fn ( $taskTypeId ) => VisualEditorHooks::PLUGIN_PREFIX . $taskTypeId,
66            array_keys( $taskTypes )
67        );
68        $isSuggestedEdit = count( array_intersect( $pluginFields, array_keys( $pluginData ) ) ) > 0;
69
70        // Check that the feature is enabled, we are editing an article and the user
71        // just passed the threshold for the invite.
72        // Also check if the current edit is a suggested edit, as a micro-optimisation
73        // (shouldInviteUserAfterNormalEdit() would discard that case anyway).
74        if ( $page->getNamespace() !== NS_MAIN
75            || $isSuggestedEdit
76            || !LevelingUpManager::isEnabledForUser( $user, $this->config, $this->experimentUserManager )
77            || !$this->levelingUpManager->shouldInviteUserAfterNormalEdit( $user )
78        ) {
79            return;
80        }
81
82        $apiResponse['modules'][] = 'ext.growthExperiments.LevelingUp.InviteToSuggestedEdits';
83        $apiResponse['jsconfigvars']['wgPostEditConfirmationDisabled'] = true;
84    }
85
86    /**
87     * Load the InviteToSuggestedEdits module when VE reloads the page after save (which it
88     * indicates by the use of the 'venotify' parameter).
89     *
90     * @inheritDoc
91     */
92    public function onBeforePageDisplay( $out, $skin ): void {
93        // VE sets a query parameter, but there is no elegant way to detect post-edit reloads
94        // in the wikitext editor. Check the JS variable that it uses to configure the notice.
95        $isPostEditReload = $out->getRequest()->getCheck( 'venotify' )
96            || $out->getRequest()->getCheck( 'mfnotify' )
97            || ( $out->getJsConfigVars()['wgPostEdit'] ?? false );
98
99        if (
100            // Check that the feature is enabled, we are indeed in the post-edit reload of
101            // an article, and the user just passed the threshold for the invite.
102            !$isPostEditReload
103            || ( !$out->getTitle() || !$out->getTitle()->inNamespace( NS_MAIN ) )
104            || !LevelingUpManager::isEnabledForUser( $out->getUser(), $this->config, $this->experimentUserManager )
105            || !$this->levelingUpManager->shouldInviteUserAfterNormalEdit( $out->getUser() )
106        ) {
107            return;
108        }
109
110        $out->addModules( 'ext.growthExperiments.LevelingUp.InviteToSuggestedEdits' );
111        // Disable the default core post-edit notice.
112        $out->addJsConfigVars( 'wgPostEditConfirmationDisabled', true );
113        $out->addJsConfigVars( 'wgGELevelingUpInviteToSuggestedEditsImmediate', true );
114        $out->addJsConfigVars( 'wgCXSectionTranslationRecentEditInvitationSuppressed', true );
115    }
116
117    /**
118     * Add GrowthExperiments events to Echo
119     *
120     * @param array &$notifications array of Echo notifications
121     * @param array &$notificationCategories array of Echo notification categories
122     * @param array &$icons array of icon details
123     */
124    public static function onBeforeCreateEchoEvent(
125        &$notifications, &$notificationCategories, &$icons
126    ) {
127        $notificationCategories['ge-newcomer'] = [
128            'tooltip' => 'echo-pref-tooltip-ge-newcomer',
129        ];
130        $notifications['keep-going'] = [
131            'category' => 'ge-newcomer',
132            'group' => 'positive',
133            'section' => 'message',
134            'canNotifyAgent' => true,
135            'presentation-model' => EchoKeepGoingPresentationModel::class,
136            AttributeManager::ATTR_LOCATORS => [
137                [ UserLocator::class . '::locateEventAgent' ]
138            ]
139        ];
140
141        $icons['growthexperiments-keep-going'] = [
142            'path' => 'GrowthExperiments/images/notifications-keep-going.svg'
143        ];
144
145        $notifications['get-started'] = [
146            'category' => 'ge-newcomer',
147            'group' => 'positive',
148            'section' => 'message',
149            'canNotifyAgent' => true,
150            'presentation-model' => EchoGetStartedPresentationModel::class,
151            AttributeManager::ATTR_LOCATORS => [
152                [ UserLocator::class . '::locateEventAgent' ]
153            ]
154        ];
155
156        $icons['growthexperiments-get-started'] = [
157            'path' => 'GrowthExperiments/images/notifications-get-started.svg'
158        ];
159    }
160
161    /** @inheritDoc */
162    public function onUserGetDefaultOptions( &$defaultOptions ) {
163        $defaultOptions['echo-subscriptions-email-ge-newcomer'] = true;
164        $defaultOptions['echo-subscriptions-web-ge-newcomer'] = true;
165    }
166
167}