Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 132
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialMobileEditWatchlist
0.00% covered (danger)
0.00%
0 / 132
0.00% covered (danger)
0.00%
0 / 11
650
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 outputSubtitle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLineHtml
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
12
 execute
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getPageOffset
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getPagesToDisplay
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getEmptyListHtml
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
6
 getNextPage
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 executeViewEditWatchlist
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 1
72
 getViewHtml
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getAssociatedNavigationLinks
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3use MediaWiki\FileRepo\RepoGroup;
4use MediaWiki\HookContainer\HookContainer;
5use MediaWiki\Html\Html;
6use MediaWiki\MainConfigNames;
7use MediaWiki\SpecialPage\SpecialPage;
8use MediaWiki\Specials\SpecialEditWatchlist;
9use MediaWiki\Title\Title;
10use MediaWiki\Watchlist\WatchedItemStoreInterface;
11use MobileFrontend\Hooks\HookRunner;
12use MobileFrontend\Models\MobileCollection;
13use MobileFrontend\Models\MobilePage;
14
15/**
16 * The mobile version of the watchlist editing page.
17 * @deprecated in future this should be the core SpecialEditWatchlist page (T109277)
18 */
19class SpecialMobileEditWatchlist extends SpecialEditWatchlist {
20    private const WATCHLIST_TAB_PATHS = [
21        'Special:Watchlist',
22        'Special:EditWatchlist'
23    ];
24    private const LIMIT = 50;
25
26    /** @var string The name of the title to begin listing the watchlist from */
27    protected readonly string $offsetTitle;
28
29    public function __construct(
30        private readonly HookContainer $hookContainer,
31        private readonly RepoGroup $repoGroup,
32        WatchedItemStoreInterface $watchStoreItem,
33    ) {
34        $req = $this->getRequest();
35        $this->offsetTitle = $req->getVal( 'from', '' );
36        parent::__construct( $watchStoreItem );
37    }
38
39    /**
40     * Renders the subheader.
41     */
42    protected function outputSubtitle() {
43        $user = $this->getUser();
44    }
45
46    /**
47     * Gets the HTML fragment for a watched page. The client uses a very different
48     * structure for client-rendered items in PageListItem.hogan.
49     *
50     * @param MobilePage $mp a definition of the page to be rendered.
51     * @return string
52     */
53    protected function getLineHtml( MobilePage $mp ) {
54        $thumb = $mp->getSmallThumbnailHtml( true );
55        $title = $mp->getTitle();
56        if ( !$thumb ) {
57            $thumb = Html::rawElement( 'div', [
58                'class' => 'list-thumb list-thumb-placeholder'
59                ], Html::element( 'span', [
60                    'class' => 'mf-icon-image'
61                ] )
62            );
63        }
64        $timestamp = $mp->getLatestTimestamp();
65        $user = $this->getUser();
66        $titleText = $title->getPrefixedText();
67        if ( $timestamp ) {
68            $className = 'title';
69        } else {
70            $className = 'title new';
71        }
72
73        $html =
74            Html::openElement( 'li', [
75                'class' => 'page-summary',
76                'title' => $titleText,
77                'data-id' => $title->getArticleID()
78            ] ) .
79            Html::openElement( 'a', [ 'href' => $title->getLocalURL(), 'class' => $className ] );
80        $html .= $thumb;
81        $html .=
82            Html::element( 'h3', [], $titleText );
83
84        $html .= Html::closeElement( 'a' ) .
85            Html::closeElement( 'li' );
86
87        return $html;
88    }
89
90    /**
91     * The main entry point for the page.
92     *
93     * @param string|null $mode Whether the user is viewing, editing, or clearing their
94     *  watchlist
95     */
96    public function execute( $mode ) {
97        // Anons don't get a watchlist edit
98        $this->requireLogin( 'mobile-frontend-watchlist-purpose' );
99
100        $out = $this->getOutput();
101        $out->addBodyClasses( 'mw-mf-special-page' );
102        parent::execute( $mode );
103        $out->setPageTitleMsg( $this->msg( 'watchlist' ) );
104    }
105
106    /**
107     * Finds the offset of the page given in this->offsetTitle
108     * If doesn't exist returns 0 to show from beginning of array of pages.
109     *
110     * @param array $pages
111     * @return int where the index of the next page to be shown.
112     */
113    private function getPageOffset( $pages ) {
114        // Deal with messiness of mediawiki
115        $pages = array_keys( $pages );
116
117        if ( $this->offsetTitle ) {
118            // PHP is stupid. strict test to avoid issues when page '0' is watched.
119            $offset = array_search( $this->offsetTitle, $pages, true );
120            // Deal with cases where invalid title given
121            if ( $offset === false ) {
122                $offset = 0;
123            }
124        } else {
125            $offset = 0;
126        }
127        return $offset;
128    }
129
130    /**
131     * Create paginated view of entire watchlist
132     *
133     * @param array $pages
134     * @return array of pages that should be displayed in current view
135     */
136    private function getPagesToDisplay( $pages ) {
137        $offset = $this->getPageOffset( $pages );
138        // Get the slice we are going to display and display it
139        return array_slice( $pages, $offset, self::LIMIT, true );
140    }
141
142    /**
143     * Get the HTML needed to show if a user doesn't watch any page, show information
144     * how to watch pages where no pages have been watched.
145     * @return string
146     */
147    private function getEmptyListHtml() {
148        $dir = $this->getLanguage()->isRTL() ? 'rtl' : 'ltr';
149
150        $imgUrl = $this->getConfig()->get( MainConfigNames::ExtensionAssetsPath ) .
151            "/MobileFrontend/images/emptywatchlist-page-actions-$dir.png";
152
153        $msg = Html::element( 'p', [],
154            $this->msg( 'mobile-frontend-watchlist-a-z-empty-howto' )->plain()
155        );
156        $msg .= Html::element( 'img', [
157            'src' => $imgUrl,
158            'alt' => $this->msg( 'mobile-frontend-watchlist-a-z-empty-howto-alt' )->plain(),
159        ] );
160
161        return Html::openElement( 'div', [ 'class' => 'info empty-page' ] ) .
162            $msg .
163            Html::element( 'a',
164                [ 'class' => 'button', 'href' => Title::newMainPage()->getLocalURL() ],
165                $this->msg( 'mobile-frontend-watchlist-back-home' )->plain()
166            ) .
167            Html::closeElement( 'div' );
168    }
169
170    /**
171     * Identify the next page to be shown
172     *
173     * @param array $pages
174     * @return string|bool representing title of next page to show or
175     *  false if there isn't another page to show.
176     */
177    private function getNextPage( $pages ) {
178        $total = count( $pages );
179        $offset = $this->getPageOffset( $pages );
180        $limit = self::LIMIT;
181
182        // Work out if we need a more button and where to start from
183        if ( $total > $offset + $limit ) {
184            $pageKeys = array_keys( $pages );
185            $from = $pageKeys[$offset + $limit];
186        } else {
187            $from = false;
188        }
189        return $from;
190    }
191
192    /**
193     * Renders the view/edit (normal) mode of the watchlist.
194     */
195    protected function executeViewEditWatchlist() {
196        $ns = NS_MAIN;
197        $images = [];
198
199        $watchlist = $this->getWatchlistInfo();
200
201        if ( isset( $watchlist[$ns] ) ) {
202            $allPages = $watchlist[$ns];
203            $from = $this->getNextPage( $allPages );
204            $allPages = $this->getPagesToDisplay( $allPages );
205        } else {
206            $allPages = [];
207            $from = false;
208        }
209
210        // Begin rendering of watchlist.
211        $watchlist = [ $ns => $allPages ];
212        ( new HookRunner( $this->hookContainer ) )
213            ->onSpecialMobileEditWatchlist__images(
214                $this->getContext(), $watchlist, $images
215            );
216
217        // create list of pages
218        $mobilePages = new MobileCollection();
219        $pageKeys = array_keys( $watchlist[$ns] );
220        foreach ( $pageKeys as $dbkey ) {
221            if ( isset( $images[$ns][$dbkey] ) ) {
222                $page = new MobilePage(
223                    Title::makeTitleSafe( $ns, $dbkey ),
224                    $this->repoGroup->findFile( $images[$ns][$dbkey] )
225                );
226            } else {
227                $page = new MobilePage( Title::makeTitleSafe( $ns, $dbkey ) );
228            }
229            $mobilePages->add( $page );
230        }
231
232        if ( $mobilePages->isEmpty() ) {
233            $html = $this->getEmptyListHtml();
234        } else {
235            $html = $this->getViewHtml( $mobilePages );
236        }
237        if ( $from ) {
238            // show more link if there are more items to show
239            $qs = [ 'from' => $from ];
240            $html .= Html::element( 'a',
241                [
242                    'class' => 'mw-mf-watchlist-more',
243                    'href' => SpecialPage::getTitleFor( 'EditWatchlist' )->getLocalURL( $qs ),
244                ],
245                $this->msg( 'mobile-frontend-watchlist-more' )->text() );
246        }
247        $out = $this->getOutput();
248
249        $config = $out->getConfig();
250        $searchParams = $config->get( 'MFSearchAPIParams' );
251        $mfScriptPath = $config->get( 'MFScriptPath' );
252        $pageProps = $config->get( 'MFQueryPropModules' );
253        // Avoid API warnings and allow integration with optional extensions.
254        if ( $mfScriptPath || ExtensionRegistry::getInstance()->isLoaded( 'PageImages' ) ) {
255            $pageProps[] = 'pageimages';
256            $searchParams = array_merge_recursive( $searchParams, [
257                'piprop' => 'thumbnail',
258                'pithumbsize' => MobilePage::SMALL_IMAGE_WIDTH,
259                'pilimit' => 50,
260            ] );
261        }
262
263        $out->addJSConfigVars( [
264            'wgMFScriptPath' => $mfScriptPath,
265            'wgMFSearchAPIParams' => $searchParams,
266            'wgMFQueryPropModules' => $pageProps,
267        ] );
268        $out->addHTML( $html );
269        $out->addModules( 'mobile.special.watchlist.scripts' );
270    }
271
272    /**
273     * @param MobileCollection $collection Collection of pages to get view for
274     * @return string html representation of collection in watchlist view
275     */
276    protected function getViewHtml( MobileCollection $collection ) {
277        $html = Html::openElement( 'ul', [ 'class' => 'content-unstyled mw-mf-page-list thumbs'
278            . ' page-summary-list mw-mf-watchlist-page-list' ] );
279        foreach ( $collection as $mobilePage ) {
280            $html .= $this->getLineHtml( $mobilePage );
281        }
282        $html .= Html::closeElement( 'ul' );
283        return $html;
284    }
285
286    /**
287     * @inheritDoc
288     */
289    public function getAssociatedNavigationLinks() {
290        return self::WATCHLIST_TAB_PATHS;
291    }
292}