MarkAHershberger has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/386007 )

Change subject: add Echo support
......................................................................

add Echo support

* Sprinkle debug statements throught to help with that.
* Add shim to work w/o WatchedItemStore from MediaWikiServices.
* Make third arg on CategoryAfterPageRemovalHook optional.
* Add title-message key that older Echo requires and silently dies without.

Change-Id: Id0df74ccef1f9c6ea51c22396977f5424cbe19ae
---
M .gitignore
D CategoryWatch.php
A assets/catwatch.svg
M extension.json
M i18n/en.json
M i18n/qqq.json
A src/CategoryWatch.php
A src/EchoEventPresentationModel.php
A src/Hook.php
9 files changed, 905 insertions(+), 464 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/CategoryWatch 
refs/changes/07/386007/1

diff --git a/.gitignore b/.gitignore
index 4a59931..d2d7c2e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@
 *~
 \#*\#
 .\#*
+.tramp_history
 
 # misc
 PHPTAGS.sqlite
diff --git a/CategoryWatch.php b/CategoryWatch.php
deleted file mode 100644
index aca8f23..0000000
--- a/CategoryWatch.php
+++ /dev/null
@@ -1,453 +0,0 @@
-<?php
-/**
- * CategoryWatch extension
- * - Extends watchlist functionality to include notification about membership
- *   changes of watched categories
- *
- * Copyright (C) 2008  Aran Dunkley
- * Copyright (C) 2017  Sean Chen
- * Copyright (C) 2017  Mark A. Hershberger
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 2
- * of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
- * 02110-1301, USA.
- *
- * See https://www.mediawiki.org/Extension:CategoryWatch
- *     for installation and usage details
- * See http://www.organicdesign.co.nz/Extension_talk:CategoryWatch
- *     for development notes and disucssion
- *
- * @file
- * @ingroup Extensions
- * @author Aran Dunkley [http://www.organicdesign.co.nz/nad User:Nad]
- * @copyright © 2008 Aran Dunkley
- * @licence GNU General Public Licence 2.0 or later
- */
-
-class CategoryWatch {
-       // Instance
-       protected static $watcher;
-
-       /**
-        * The extension function.
-        * It has to be the static function in a class now.
-        */
-       public static function setupCategoryWatch() {
-               wfDebugLog( 'CategoryWatch', 'loading extension...' );
-
-               # Instantiate the CategoryWatch singleton now
-               # that the environment is prepared
-               self::$watcher = new CategoryWatch();
-       }
-
-       /**
-        * Get a list of categories before article updated Since MediaWiki
-        * version 1.25.x, we have to use static function for hooks.  the
-        * hook has different signatures.
-        * @param WikiPage $wikiPage the page
-        * @param User $user who is modifying
-        * @param Content $content the new article content
-        * @param string $summary the article summary (comment)
-        * @param bool $isMinor minor flag
-        * @param bool $isWatch watch flag (not used, aka always null)
-        * @param int $section section number (not used, aka always null)
-        * @param int $flags see WikiPage::doEditContent documentation for 
flags' definition
-        * @param Status $status Status (object)
-        */
-       public static function onPageContentSave(
-               $wikiPage, $user, $content, $summary, $isMinor,
-               $isWatch, $section, $flags, $status
-       ) {
-               global $wgCategoryWatchUseAutoCat, 
$wgCategoryWatchUseAutoCatRealName,
-                       $wgCategoryWatch;
-
-               self::$watcher->before = [];
-               $dbr  = wfGetDB( DB_MASTER );
-               $cl   = $dbr->tableName( 'categorylinks' );
-               $id   = $wikiPage->getID();
-               wfDebugLog( 'CategoryWatch', "tablename = $cl" );
-               wfDebugLog( 'CategoryWatch', "page id=$id" );
-               $res  = $dbr->select(
-                       $cl, 'cl_to', "cl_from = $id", __METHOD__,
-                       [ 'ORDER BY' => 'cl_sortkey' ]
-               );
-               $row = $dbr->fetchRow( $res );
-               while ( $row ) {
-                       self::$watcher->before[] = $row[0];
-                       $row = $dbr->fetchRow( $res );
-               }
-               $dbr->freeResult( $res );
-               wfDebugLog( 'CategoryWatch', 'Categories before page saved' );
-               wfDebugLog( 'CategoryWatch', join( ', ', self::$watcher->before 
) );
-
-               # If using the automatically watched category feature, ensure
-               # that all users are watching it
-               if ( $wgCategoryWatchUseAutoCat ) {
-                       $dbr = wfGetDB( DB_SLAVE );
-
-                       # Find all users not watching the autocat
-                       $like = str_replace(
-                               ' ', '_',
-                               trim( wfMessage( 'categorywatch-autocat', '' 
)->text() )
-                       );
-                       $utbl = $dbr->tableName( 'user' );
-                       $wtbl = $dbr->tableName( 'watchlist' );
-                       $sql = "SELECT user_id FROM $utbl LEFT JOIN $wtbl ON "
-                                . "user_id=wl_user AND wl_title LIKE '%$like%' 
"
-                                . "WHERE wl_user IS NULL";
-
-                       # Insert an entry into watchlist for each
-                       $row = $dbr->fetchRow( $res );
-                       while ( $row ) {
-                               $user = User::newFromId( $row[0] );
-                               $name = $wgCategoryWatchUseAutoCatRealName
-                                         ? $user->getRealName()
-                                         : $user->getName();
-                               $wl_title = str_replace(
-                                       ' ', '_', wfMessage( 
'categorywatch-autocat', $name )->text()
-                               );
-                               $dbr->insert(
-                                       $wtbl,
-                                       [
-                                               'wl_user' => $row[0], 
'wl_namespace' => NS_CATEGORY,
-                                               'wl_title' => $wl_title
-                                       ]
-                               );
-                               $row = $dbr->fetchRow( $res );
-                       }
-                       $dbr->freeResult( $res );
-               }
-       }
-
-       /**
-        * the proper hook for save page request.
-        * @see 
https://www.mediawiki.org/wiki/Manual:Hooks/PageContentSaveComplete
-        * @param WikiPage $article Article edited
-        * @param User $user who edited
-        * @param Content $content New article text
-        * @param string $summary Edit summary
-        * @param bool $isMinor Minor edit or not
-        * @param bool $isWatch Watch this article?
-        * @param string $section Section that was edited
-        * @param int $flags Edit flags
-        * @param Revision $revision that was created
-        * @param Status $status of activities
-        * @param int $baseRevId base revision
-        */
-       public static function onPageContentSaveComplete(
-               $article, $user, $content, $summary, $isMinor, $isWatch, 
$section,
-               $flags, $revision, $status, $baseRevId
-       ) {
-               # Get cats after update
-               self::$watcher->after = [];
-
-               $parseTimestamp = $revision->getTimestamp();
-               $content = $revision->getContent();
-               $title = $article->getTitle();
-               $options = $content->getContentHandler()->makeParserOptions( 
'canonical' );
-               $options->setTimestamp( $parseTimestamp );
-               $output = $content->getParserOutput( $title, 
$revision->getId(), $options );
-               self::$watcher->after = array_map(
-                       'strval', array_keys( $output->getCategories() )
-               );
-               wfDebugLog( 'CategoryWatch', 'Categories after page saved' );
-               wfDebugLog( 'CategoryWatch', join( ', ', self::$watcher->after 
) );
-
-               # Get list of added and removed cats
-               $add = array_diff( self::$watcher->after, 
self::$watcher->before );
-               $sub = array_diff( self::$watcher->before, 
self::$watcher->after );
-
-               # Notify watchers of each cat about the addition or removal of 
this article
-               if ( count( $add ) > 0 || count( $sub ) > 0 ) {
-                       $page     = $article->getTitle();
-                       $pagename = $page->getPrefixedText();
-                       $pageurl  = $page->getFullUrl();
-                       $page     = "$pagename ($pageurl)";
-
-                       if ( count( $add ) == 1 && count( $sub ) == 1 ) {
-                               $add = array_shift( $add );
-                               $sub = array_shift( $sub );
-
-                               $title   = Title::newFromText( $add, 
NS_CATEGORY );
-                               $message = wfMessage(
-                                       'categorywatch-catmovein', $page,
-                                       self::$watcher->friendlyCat( $add ),
-                                       self::$watcher->friendlyCat( $sub )
-                               )->text();
-                               self::$watcher->notifyWatchers(
-                                       $title, $user, $message, $summary, 
$medit, $pageurl
-                               );
-
-                               $title   = Title::newFromText( $sub, 
NS_CATEGORY );
-                               $message = wfMessage(
-                                       'categorywatch-catmoveout', $page,
-                                       self::$watcher->friendlyCat( $sub ),
-                                       self::$watcher->friendlyCat( $add )
-                               )->text();
-                               self::$watcher->notifyWatchers(
-                                       $title, $user, $message, $summary, 
$medit, $pageurl
-                               );
-                       } else {
-
-                               foreach ( $add as $cat ) {
-                                       $title   = Title::newFromText( $cat, 
NS_CATEGORY );
-                                       $message = wfMessage(
-                                               'categorywatch-catadd', $page,
-                                               self::$watcher->friendlyCat( 
$cat )
-                                       )->text();
-                                       self::$watcher->notifyWatchers(
-                                               $title, $user, $message, 
$summary, $medit, $pageurl
-                                       );
-                               }
-
-                               foreach ( $sub as $cat ) {
-                                       $title   = Title::newFromText( $cat, 
NS_CATEGORY );
-                                       $message = wfMessage(
-                                               'categorywatch-catsub', $page,
-                                               self::$watcher->friendlyCat( 
$cat )
-                                       )->text();
-                                       self::$watcher->notifyWatchers(
-                                               $title, $user, $message, 
$summary, $medit, $pageurl
-                                       );
-                               }
-                       }
-               }
-
-               global $wgCategoryWatchNotifyParentWatchers;
-               if ( $wgCategoryWatchNotifyParentWatchers ) {
-                       self::notifyParentWatchers();
-               }
-       }
-
-       /**
-        * Notify the watchers of parent categories
-        */
-       protected static function notifyParentWatchers() {
-               self::$watcher->allparents = [];
-               self::$watcher->i = 0;
-               self::$watcher->findCategoryParents( self::$watcher->after );
-               ## For each active parent category, send the mail
-               if ( self::$watcher->allparents ) {
-                       $page     = $article->getTitle();
-                       $pageurl  = $page->getFullUrl();
-                       foreach ( self::$watcher->allparents as $cat ) {
-                               $title   = Title::newFromText( $cat, 
NS_CATEGORY );
-                               $message = wfMessage(
-                                       'categorywatch-catchange', $page,
-                                       self::$watcher->friendlyCat( $cat )
-                               );
-                               self::$watcher->notifyWatchers(
-                                       $title, $user, $message, $summary, 
$medit, $pageurl
-                               );
-                       }
-               }
-       }
-
-       /**
-        * Recursively find all parents of the given categories
-        *
-        * @param array $catarray the categories
-        */
-       protected function findCategoryParents( array $catarray ) {
-               $this->i++;
-               if ( $this->i == 200 ) {
-                       return;
-               }
-
-               if ( $catarray ) {
-                       foreach ( $catarray as $catname ) {
-                               self::$watcher->allparents[] = $catname;
-                               $id = self::$watcher->getCategoryArticleId( 
$catname );
-                               if ( is_numeric( $id ) ) {
-                                       $parentCat = 
self::$watcher->getParentCategories( $id );
-                                       if ( $parentCat ) {
-                                               self::$watcher->allparents[] = 
$parentCat;
-                                               
self::$watcher->findCategoryParents( [ $parentCat ] );
-                                       }
-                               }
-                       }
-                       self::$watcher->allparents = array_unique( 
self::$watcher->allparents );
-               }
-       }
-
-       /**
-        * Return the parent categories
-        * @param int $id Category Article id
-        * @return parents
-        */
-       protected function getParentCategories( $id ) {
-               $dbr  = wfGetDB( DB_SLAVE );
-               $cl   = $dbr->tableName( 'categorylinks' );
-               $res  = $dbr->select(
-                       $cl, 'cl_to', "cl_from = $id", __METHOD__,
-                       [ 'ORDER BY' => 'cl_sortkey' ]
-               );
-               $row = $dbr->fetchRow( $res );
-               $dbr->freeResult( $res );
-               if ( empty( $row[0] ) ) {
-                       return false;
-               }
-               return $row[0];
-       }
-
-       /**
-        * Load page ID of one category
-        *
-        * @param string $catname name of category
-        * @return int
-        */
-       protected function getCategoryArticleId( $catname ) {
-               $dbr = wfGetDB( DB_SLAVE );
-               $cl  = $dbr->tableName( 'page' );
-               $res = $dbr->select( $cl, 'page_id', "page_title = '$catname'", 
__METHOD__ );
-               $row = $dbr->fetchRow( $res );
-               $dbr->freeResult( $res );
-               return $row[0];
-       }
-
-       /**
-        * Return "Category:Cat (URL)" from "Cat"
-        * @param string $cat name of category
-        * @return string
-        */
-       protected function friendlyCat( $cat ) {
-               $cat     = Title::newFromText( $cat, NS_CATEGORY );
-               $catname = $cat->getPrefixedText();
-               $caturl  = $cat->getFullUrl();
-               return "$catname ($caturl)";
-       }
-
-       /**
-        * Notify any watchers
-        * @param Title $title of article
-        * @param User $editor of article
-        * @param string $message for user
-        * @param string $summary editor gave
-        * @param bool $medit true if minor
-        * @param string $pageurl of page
-        */
-       function notifyWatchers( $title, $editor, $message, $summary, $medit, 
$pageurl ) {
-               global $wgLang, $wgNoReplyAddress, $wgCategoryWatchNotifyEditor,
-                       $wgEnotifRevealEditorAddress, $wgEnotifUseRealName, 
$wgPasswordSender,
-                       $wgEnotifFromEditor, $wgPasswordSenderName;
-
-               # Get list of users watching this category
-               $dbr = wfGetDB( DB_SLAVE );
-               $conds = [
-                       'wl_title' => $title->getDBkey(), 'wl_namespace' => 
$title->getNamespace()
-               ];
-               if ( !$wgCategoryWatchNotifyEditor ) {
-                       $conds[] = 'wl_user <> ' . intval( $editor->getId() );
-               }
-               $res = $dbr->select( 'watchlist', [ 'wl_user' ], $conds, 
__METHOD__ );
-
-               # Wrap message with common body and send to each watcher
-               $page = $title->getPrefixedText();
-               $adminAddress   = new MailAddress(
-                       $wgPasswordSender,
-                       isset( $wgPasswordSenderName )
-                       ? $wgPasswordSenderName
-                       : 'WikiAdmin'
-               );
-               $editorAddress  = new MailAddress( $editor );
-               $summary        = $summary
-                                               ? $summary
-                                               : ' - ';
-               $medit          = $medit
-                                               ? wfMessage( 'minoredit' 
)->text()
-                                               : '';
-               $row            = $dbr->fetchRow( $res );
-               while ( $row ) {
-                       $watchingUser   = User::newFromId( $row[0] );
-                       $timecorrection = $watchingUser->getOption( 
'timecorrection' );
-                       $editdate       = $wgLang->timeanddate(
-                               wfTimestampNow(), true, false, $timecorrection
-                       );
-
-                       if (
-                               $watchingUser->getOption( 
'enotifwatchlistpages' )
-                               && $watchingUser->isEmailConfirmed()
-                       ) {
-                               $to      = new MailAddress( $watchingUser );
-                               $subject = wfMessage( 
'categorywatch-emailsubject', $page )->text();
-                               $body    = wfMessage( 'enotif_body' 
)->inContentLanguage()->text();
-
-                               # Reveal the page editor's address as REPLY-TO 
address only if
-                               # the user has not opted-out and the option is 
enabled at the
-                               # global configuration level.
-                               if ( $wgCategoryWatchNoRealName ) {
-                                       $name = $watchingUser->getName();
-                               }
-                               $name = $wgEnotifUseRealName
-                                         ? $watchingUser->getRealName()
-                                         : $watchingUser->getName();
-                               if ( $wgEnotifRevealEditorAddress
-                                        && ( $editor->getEmail() != '' )
-                                        && $editor->getOption( 
'enotifrevealaddr' )
-                               ) {
-                                       if ( $wgEnotifFromEditor ) {
-                                               $from = $editorAddress;
-                                       } else {
-                                               $from = $adminAddress;
-                                               $replyto = $editorAddress;
-                                       }
-                               } else {
-                                       $from = $adminAddress;
-                                       $replyto = new MailAddress( 
$wgNoReplyAddress );
-                               }
-
-                               # Define keys for body message
-                               $userPage = $editor->getUserPage();
-                               $keys = [
-                                       '$WATCHINGUSERNAME' => $name,
-                                       '$NEWPAGE'          => $message,
-                                       '$PAGETITLE'        => $page,
-                                       '$PAGEEDITDATE'     => $editdate,
-                                       '$CHANGEDORCREATED' => wfMessage( 
'changed' )
-                                       ->inContentLanguage()->text(),
-                                       '$PAGETITLE_URL'    => 
$title->getFullUrl(),
-                                       '$PAGEEDITOR_WIKI'  => 
$userPage->getFullUrl(),
-                                       '$PAGESUMMARY'      => $summary,
-                                       '$PAGEMINOREDIT'    => $medit,
-                                       '$OLDID'            => ''
-                               ];
-                               if ( $editor->isIP( $name ) ) {
-                                       $utext = wfMessage(
-                                               'enotif_anon_editor', $name
-                                       )->inContentLanguage()->text();
-                                       $subject = str_replace( '$PAGEEDITOR', 
$utext, $subject );
-                                       $keys['$PAGEEDITOR'] = $utext;
-                                       $keys['$PAGEEDITOR_EMAIL'] = wfMmessage(
-                                               'noemailtitle'
-                                       )->inContentLanguage()->text();
-                               } else {
-                                       $subject = str_replace( '$PAGEEDITOR', 
$name, $subject );
-                                       $keys['$PAGEEDITOR'] = $name;
-                                       $emailPage = 
SpecialPage::getSafeTitleFor( 'Emailuser', $name );
-                                       $keys['$PAGEEDITOR_EMAIL'] = 
$emailPage->getFullUrl();
-                               }
-                               $keys['$PAGESUMMARY'] = $summary;
-
-                               # Replace keys, wrap text and send
-                               $body = strtr( $body, $keys );
-                               $body = wordwrap( $body, 72 );
-                               $options = [];
-                               $options['replyTo'] = $replyto;
-                               UserMailer::send( $to, $from, $subject, $body, 
$options );
-                       }
-               }
-
-               $dbr->freeResult( $res );
-       }
-}
diff --git a/assets/catwatch.svg b/assets/catwatch.svg
new file mode 100644
index 0000000..704e616
--- /dev/null
+++ b/assets/catwatch.svg
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:xlink="http://www.w3.org/1999/xlink";
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
+   width="210mm"
+   height="297mm"
+   viewBox="0 0 210 297"
+   version="1.1"
+   id="svg8"
+   inkscape:version="0.92.1 r15371"
+   sodipodi:docname="drawing.svg">
+  <defs
+     id="defs2" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.35"
+     inkscape:cx="405.71429"
+     inkscape:cy="674.28571"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1920"
+     inkscape:window-height="1043"
+     inkscape:window-x="3120"
+     inkscape:window-y="576"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <path
+       style="fill:#000000;stroke-width:0.35277778"
+       d="m 84.994481,214.31563 c -5.949945,-1.05364 -21.213966,-2.23295 
-13.352206,-10.9714 3.695507,-12.89237 4.88332,-26.42383 5.638505,-39.76417 
-19.025785,1.84272 -39.922515,-1.87712 -54.534241,-14.94703 -10.376185,-8.42312 
-15.0451298,-23.62666 -8.969777,-35.94804 3.623461,-8.80684 
10.448942,-15.705428 17.578613,-21.776377 3.345481,-10.464225 
5.068239,-21.801097 8.460262,-31.982302 4.466092,6.611636 8.174656,13.714598 
12.269483,20.565238 16.66839,-1.648195 33.562264,-3.625288 50.27083,-1.585865 
8.27464,4.824707 13.98201,0.431648 16.53857,-7.829803 2.33085,-3.357656 
6.60157,-15.536359 7.20814,-5.420148 1.69117,8.81785 3.42432,17.627565 
5.14434,26.439813 8.65544,8.252934 16.47454,18.690314 16.23665,31.261414 
0.15052,19.00213 -16.39479,33.26368 -33.43058,38.20593 -6.98098,2.18615 
-14.27459,3.10793 -21.550892,3.59694 6.822478,11.6862 16.162262,22.46563 
18.961012,36.01002 11.7332,-1.60806 23.47262,-9.37933 27.57163,-21.00115 
4.57614,-10.28331 2.7418,-21.81526 3.56869,-32.62977 3.59259,-8.36013 
9.75557,-18.09339 20.08437,-17.61026 11.31681,0.60592 21.72389,8.55681 
26.96142,18.37719 4.27664,7.64106 1.15388,18.96169 -8.02363,21.03954 
-4.43086,1.17288 -13.59254,3.94132 -15.73306,0.24894 6.2893,-4.1808 
17.38926,-5.47314 18.52184,-14.45304 -1.09128,-7.7233 -8.60287,-12.46372 
-14.47271,-16.64506 -7.5555,-5.27554 -17.4013,1.51599 -20.25435,8.90236 
-2.71908,12.67921 2.14599,26.86221 -5.10981,38.59235 -6.58969,10.90246 
-18.14975,19.19652 -30.72988,21.4619 -6.9547,1.43192 -11.31322,10.44678 
-20.201235,8.39584 -2.892543,0.0701 -5.786826,-0.14442 -8.651984,-0.53306 z M 
50.548173,136.37518 c 9.126923,-2.37248 15.298303,-10.49331 20.748169,-17.70152 
-5.627883,-8.11307 -11.761537,-16.94128 -21.327889,-20.587224 
10.698125,7.229604 11.612395,24.495364 3.278572,33.827614 -6.151929,7.34316 
-17.514316,1.94845 -19.376662,-6.47971 -3.402429,-9.5564 -1.364495,-22.65477 
8.336981,-27.712969 -10.159912,2.352789 -15.430016,12.183909 
-21.347193,19.873609 5.109409,10.56059 15.906937,19.04843 27.919299,18.96924 l 
1.768725,-0.18904 z m 71.578187,-1.25594 c 6.9859,-3.84485 12.36892,-9.99874 
16.90292,-16.44614 -5.43052,-7.84042 -11.20344,-16.34394 -20.28566,-20.21139 
8.99047,7.71777 10.23142,22.34175 3.60087,31.91231 -4.0751,6.81567 
-14.54106,6.61987 -18.29916,-0.39976 -6.686348,-9.84327 -5.228428,-26.2426 
5.89534,-32.266383 -10.180009,2.368153 -15.346942,12.210943 
-21.318868,19.857373 5.056277,10.75606 16.025078,18.8559 28.021648,19.13088 
1.93444,-0.0364 3.81243,-0.61346 5.48291,-1.57689 z"
+       id="path168"
+       inkscape:connector-curvature="0" />
+  </g>
+</svg>
diff --git a/extension.json b/extension.json
index 2d06c58..a1f819e 100644
--- a/extension.json
+++ b/extension.json
@@ -1,6 +1,6 @@
 {
        "name": "CategoryWatch",
-       "version": "1.2.2, 2011-12-03",
+       "version": "2.0, 2017-09-19",
        "author": [
                "[http://www.organicdesign.co.nz/User:Nad User:Nad]",
                "Sean Chen",
@@ -11,20 +11,29 @@
        "license-name": "GPL-2.0+",
        "type": "other",
        "AutoloadClasses": {
-               "CategoryWatch": "CategoryWatch.php"
+               "CategoryWatch\\CategoryWatch": "src/CategoryWatch.php",
+               "CategoryWatch\\EchoEventPresentationModel": 
"src/EchoEventPresentationModel.php",
+               "CategoryWatch\\Hook": "src/Hook.php"
        },
-       "ExtensionFunctions": [
-               "CategoryWatch::setupCategoryWatch"
-       ],
        "MessagesDirs": {
                "CategoryWatch": "i18n"
        },
+       "DefaultUserOptions": {
+               "echo-subscriptions-email-categorywatch": true,
+               "echo-subscriptions-web-categorywatch": true
+       },
        "Hooks": {
-               "PageContentSave": [
-                       "CategoryWatch::onPageContentSave"
+               "BeforeCreateEchoEvent": [
+                       "CategoryWatch\\Hook::onBeforeCreateEchoEvent"
                ],
-               "PageContentSaveComplete": [
-                       "CategoryWatch::onPageContentSaveComplete"
+               "EchoGetBundleRules": [
+                       "CategoryWatch\\Hook::onEchoGetBundleRules"
+               ],
+               "CategoryAfterPageAdded": [
+                       "CategoryWatch\\Hook::onCategoryAfterPageAdded"
+               ],
+               "CategoryAfterPageRemoved": [
+                       "CategoryWatch\\Hook::onCategoryAfterPageRemoved"
                ]
        },
        "config": {
diff --git a/i18n/en.json b/i18n/en.json
index 27ed439..0fe568f 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -10,5 +10,17 @@
        "categorywatch-catmoveout": "$1 has moved out of $2 into $3",
        "categorywatch-catadd": "$1 has been added to $2",
        "categorywatch-catsub": "$1 has been removed from $2",
-       "categorywatch-autocat": "Automatically watched by $1"
+       "categorywatch-autocat": "Automatically watched by $1",
+       "categorywatch-notification-link": "Category: [[:$1]]",
+       "categorywatch-notification-categorywatch-add-header": "[[:$1|$2]] 
added to [[:$3|$4]]",
+       "categorywatch-notification-categorywatch-add-summary": "[[:$3|$4]] 
added to [[:$5|$6]]",
+       "categorywatch-notification-categorywatch-add-body": "[[User:$1|$1]] 
added [[:$3|$3]] to [[:$4|$4]]",
+       "categorywatch-notification-categorywatch-remove-header": "[[:$1|$2]] 
removed from [[:$3|$4]]",
+       "categorywatch-notification-categorywatch-remove-summary": 
"[[:$3|$4]]removed from [[:$5|$6]]",
+       "categorywatch-notification-categorywatch-remove-body": "[[User:$1|$1]] 
removed [[:$3|$3]] from [[:$4|$4]]",
+       "categorywatch-notification-bundle": "$1 changes in categorization on 
{{SITENAME}}",
+       "categorywatch-add-title": "Title added to watched category",
+       "categorywatch-remove-title": "Title removed from watched category",
+       "echo-category-title-categorywatch": "Category watch",
+       "echo-pref-tooltip-categorywatch": "Notify me when someone categorizes 
a page into or out of a category that I'm watching."
 }
diff --git a/i18n/qqq.json b/i18n/qqq.json
index abea5f9..91fa34c 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -14,5 +14,12 @@
        "categorywatch-catmoveout": "Substituted as $5 in 
{{msg-mw|categorywatch-emailbody}}.\n* $1 is a page name\n* $2 is the source 
category name\n* $3 is the target category name",
        "categorywatch-catadd": "Substituted as $5 in 
{{msg-mw|categorywatch-emailbody}}.\n* $1 is a page name\n* $2 is a category 
name",
        "categorywatch-catsub": "Substituted as $5 in 
{{msg-mw|categorywatch-emailbody}}.\n* $1 is a page name\n* $2 is a category 
name",
-       "categorywatch-autocat": "If the \"automatically watching\" feature is 
enabled, this message is used as a page title in the watchlist.\n* $1 is a 
username (or a realname)"
+       "categorywatch-autocat": "If the \"automatically watching\" feature is 
enabled, this message is used as a page title in the watchlist.\n* $1 is a 
username (or a realname)",
+       "categorywatch-notification-add": "[[User:$1|$1]] $3 [[:$4|$4]]",
+       "categorywatch-notification-remove": "[[User:$1|$1]] $3 [[:$4|$4]]",
+       "categorywatch-notification-bundle": "$1 changes in categorization on 
{{SITENAME}}",
+       "categorywatch-add-action": "added [[:$1|$1]] to",
+       "categorywatch-remove-action": "removed [[:$1|$1]] from",
+
+       "echo-category-title-categorywatch": "Membership changes in categories 
watched" 
 }
diff --git a/src/CategoryWatch.php b/src/CategoryWatch.php
new file mode 100644
index 0000000..cdcdc7f
--- /dev/null
+++ b/src/CategoryWatch.php
@@ -0,0 +1,403 @@
+<?php
+
+/**
+ * CategoryWatch extension
+ * - Extends watchlist functionality to include notification about membership
+ *   changes of watched categories
+ *
+ * Copyright (C) 2008  Aran Dunkley
+ * Copyright (C) 2017  Sean Chen
+ * Copyright (C) 2017  Mark A. Hershberger
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * See https://www.mediawiki.org/Extension:CategoryWatch
+ *     for installation and usage details
+ * See http://www.organicdesign.co.nz/Extension_talk:CategoryWatch
+ *     for development notes and disucssion
+ *
+ * @file
+ * @ingroup Extensions
+ * @author Aran Dunkley [http://www.organicdesign.co.nz/nad User:Nad]
+ * @copyright © 2008 Aran Dunkley
+ * @licence GNU General Public Licence 2.0 or later
+ */
+
+namespace CategoryWatch;
+
+class CategoryWatch {
+       public $before = [];
+       public $after = [];
+
+       protected $count = 0;
+       protected $allParents = [];
+
+       protected $wikiPage;
+       protected $editor;
+       protected $content;
+       protected $summary;
+       protected $minorEdit;
+       protected $flags;
+
+       /**
+        * Construction
+        * @param WikiPage $wikiPage the page
+        * @param User $user who is modifying
+        * @param Content $content the new article content
+        * @param string $summary the article summary (comment)
+        * @param bool $isMinor minor flag
+        * @param int $flags see WikiPage::doEditContent documentation for 
flags' definition
+        */
+       public function __construct(
+               WikiPage $wikiPage, User $user, Content $content, $summary, 
$isMinor, $flags
+       ) {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               $this->wikiPage;
+               $this->editor;
+               $this->content;
+               $this->summary;
+               $this->minorEdit;
+               $this->flags;
+
+               $this->before = 
$this->wikiPage->getTitle()->getParentCategories();
+               $this->doAutoCat();
+       }
+
+       /**
+        * Notify all category watchers
+        *
+        * @param Revision $revision that was created
+        * @param int $baseRevId base revision
+        */
+       public function notifyCategoryWatchers(
+               Revision $revision, $baseRevId
+       ) {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               # Get cats after update
+               $this->after = 
$this->wikiPage->getTitle()->getParentCategories();
+
+               # Get list of added and removed cats
+               $add = array_diff( $this->after, $this->before );
+               $sub = array_diff( $this->before, $this->after );
+               wfDebugLog( 'CategoryWatch', 'Categories after page saved' );
+               wfDebugLog( 'CategoryWatch', join( ', ', $this->after ) );
+               wfDebugLog( 'CategoryWatch', 'Categories added' );
+               wfDebugLog( 'CategoryWatch', join( ', ', $add ) );
+               wfDebugLog( 'CategoryWatch', 'Categories removed' );
+               wfDebugLog( 'CategoryWatch', join( ', ', $sub ) );
+
+               # Notify watchers of each cat about the addition or removal of 
this article
+               if ( count( $add ) > 0 || count( $sub ) > 0 ) {
+                       $page     = $article->getTitle();
+                       $pagename = $page->getPrefixedText();
+                       $pageurl  = $page->getFullUrl();
+                       $page     = "$pagename ($pageurl)";
+
+                       if ( count( $add ) == 1 && count( $sub ) == 1 ) {
+                               $this->notifyMove( $sub[0], $add[0] );
+                       } else {
+                               $this->notifyAdd( $add );
+
+                               foreach ( $sub as $cat ) {
+                                       $title   = Title::newFromText( $cat, 
NS_CATEGORY );
+                                       $message = wfMessage(
+                                               'categorywatch-catsub', $page,
+                                               $this->friendlyCat( $cat )
+                                       )->text();
+                                       $this->notifyWatchers(
+                                               $title, $user, $message, 
$summary, $medit, $pageurl
+                                       );
+                               }
+                       }
+               }
+
+               if ( $this->shouldNotifyParentWatchers() ) {
+                       $this->notifyParentWatchers();
+               }
+       }
+
+       /**
+        * Should watchers of parent categories be notified?
+        * @return bool
+        */
+       protected function shouldNotifyParentWatchers() {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               global $wgCategoryWatchNotifyParentWatchers;
+               return $wgCategoryWatchNotifyParentWatchers;
+       }
+
+       /**
+        * Should the editor be notified of his own edits?
+        * @return bool
+        */
+       protected function shouldNotifyEditor() {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               global $wgCategoryWatchNotifyEditor;
+               return $wgCategoryWatchNotifyEditor;
+       }
+
+       /**
+        * Should CategoryWatch use the user's real name in email?
+        * @return bool
+        */
+       protected function useRealName() {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               global $wgCategoryWatchNoRealName;
+               return !$wgCategoryWatchNoRealName;
+       }
+
+       /**
+        * Return "Category:Cat (URL)" from "Cat"
+        * @param string $cat name of category
+        * @return string
+        */
+       protected function friendlyCat( $cat ) {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               $cat     = Title::newFromText( $cat, NS_CATEGORY );
+               $catname = $cat->getPrefixedText();
+               $caturl  = $cat->getFullUrl();
+               return "$catname ($caturl)";
+       }
+
+       /**
+        * Notify any watchers
+        * @param Title $title of article
+        * @param User $editor of article
+        * @param string $message for user
+        * @param string $summary editor gave
+        * @param bool $medit true if minor
+        * @param string $pageurl of page
+        */
+       protected function notifyWatchers(
+               $title, $editor, $message, $summary, $medit, $pageurl
+       ) {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               global $wgLang, $wgNoReplyAddress,
+                       $wgEnotifRevealEditorAddress, $wgEnotifUseRealName, 
$wgPasswordSender,
+                       $wgEnotifFromEditor, $wgPasswordSenderName;
+
+               # Get list of users watching this category
+               $dbr = wfGetDB( DB_SLAVE );
+               $conds = [
+                       'wl_title' => $title->getDBkey(), 'wl_namespace' => 
$title->getNamespace()
+               ];
+               if ( !$this->shouldNotifyEditor() ) {
+                       $conds[] = 'wl_user <> ' . intval( $editor->getId() );
+               }
+               $res = $dbr->select( 'watchlist', [ 'wl_user' ], $conds, 
__METHOD__ );
+
+               # Wrap message with common body and send to each watcher
+               $page = $title->getPrefixedText();
+               $adminAddress   = new MailAddress(
+                       $wgPasswordSender,
+                       isset( $wgPasswordSenderName )
+                       ? $wgPasswordSenderName
+                       : 'WikiAdmin'
+               );
+               $editorAddress  = new MailAddress( $editor );
+               $summary        = $summary
+                                               ? $summary
+                                               : ' - ';
+               $medit          = $medit
+                                               ? wfMessage( 'minoredit' 
)->text()
+                                               : '';
+               $row            = $dbr->fetchRow( $res );
+               while ( $row ) {
+                       $watchingUser   = User::newFromId( $row[0] );
+                       $timecorrection = $watchingUser->getOption( 
'timecorrection' );
+                       $editdate       = $wgLang->timeanddate(
+                               wfTimestampNow(), true, false, $timecorrection
+                       );
+
+                       if (
+                               $watchingUser->getOption( 
'enotifwatchlistpages' )
+                               && $watchingUser->isEmailConfirmed()
+                       ) {
+                               $to      = new MailAddress( $watchingUser );
+                               $subject = wfMessage( 
'categorywatch-emailsubject', $page )->text();
+                               $body    = wfMessage( 'enotif_body' 
)->inContentLanguage()->text();
+
+                               # Reveal the page editor's address as REPLY-TO 
address only if
+                               # the user has not opted-out and the option is 
enabled at the
+                               # global configuration level.
+                               $name = $wgEnotifUseRealName
+                                         ? $watchingUser->getRealName()
+                                         : $watchingUser->getName();
+                               if ( $wgEnotifRevealEditorAddress
+                                        && ( $editor->getEmail() != '' )
+                                        && $editor->getOption( 
'enotifrevealaddr' )
+                               ) {
+                                       if ( $wgEnotifFromEditor ) {
+                                               $from = $editorAddress;
+                                       } else {
+                                               $from = $adminAddress;
+                                               $replyto = $editorAddress;
+                                       }
+                               } else {
+                                       $from = $adminAddress;
+                                       $replyto = new MailAddress( 
$wgNoReplyAddress );
+                               }
+
+                               # Define keys for body message
+                               $userPage = $editor->getUserPage();
+                               $keys = [
+                                       '$WATCHINGUSERNAME' => $name,
+                                       '$NEWPAGE'          => $message,
+                                       '$PAGETITLE'        => $page,
+                                       '$PAGEEDITDATE'     => $editdate,
+                                       '$CHANGEDORCREATED' => wfMessage( 
'changed' )
+                                       ->inContentLanguage()->text(),
+                                       '$PAGETITLE_URL'    => 
$title->getFullUrl(),
+                                       '$PAGEEDITOR_WIKI'  => 
$userPage->getFullUrl(),
+                                       '$PAGESUMMARY'      => $summary,
+                                       '$PAGEMINOREDIT'    => $medit,
+                                       '$OLDID'            => ''
+                               ];
+                               if ( $editor->isIP( $name ) ) {
+                                       $utext = wfMessage(
+                                               'enotif_anon_editor', $name
+                                       )->inContentLanguage()->text();
+                                       $subject = str_replace( '$PAGEEDITOR', 
$utext, $subject );
+                                       $keys['$PAGEEDITOR'] = $utext;
+                                       $keys['$PAGEEDITOR_EMAIL'] = wfMmessage(
+                                               'noemailtitle'
+                                       )->inContentLanguage()->text();
+                               } else {
+                                       $subject = str_replace( '$PAGEEDITOR', 
$name, $subject );
+                                       $keys['$PAGEEDITOR'] = $name;
+                                       $emailPage = 
SpecialPage::getSafeTitleFor( 'Emailuser', $name );
+                                       $keys['$PAGEEDITOR_EMAIL'] = 
$emailPage->getFullUrl();
+                               }
+                               $keys['$PAGESUMMARY'] = $summary;
+
+                               # Replace keys, wrap text and send
+                               $body = strtr( $body, $keys );
+                               $body = wordwrap( $body, 72 );
+                               $options = [];
+                               $options['replyTo'] = $replyto;
+                               UserMailer::send( $to, $from, $subject, $body, 
$options );
+                       }
+               }
+
+               $dbr->freeResult( $res );
+       }
+
+       /**
+        * Notify the watchers of parent categories
+        */
+       protected function notifyParentWatchers() {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               $this->allparents = 
$this->wikiPage->getTitle()->getParentCategoryTree();
+               $page = $this->wikiPage->getTitle();
+               $pageUrl = $page->getFullUrl();
+               foreach ( (array)$this->allparents as $cat ) {
+                       $title   = Title::newFromText( $cat, NS_CATEGORY );
+                       $message = wfMessage(
+                               'categorywatch-catchange', $page,
+                               $this->friendlyCat( $cat )
+                       );
+                       $this->notifyWatchers(
+                               $title, $user, $message, $summary, $medit, 
$pageurl
+                       );
+               }
+       }
+
+       /**
+        * Handle autocat option
+        */
+       protected function doAutoCat() {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               global $wgCategoryWatchUseAutoCat;
+               if ( $wgCategoryWatchUseAutoCat ) {
+                       $dbr = wfGetDB( DB_SLAVE );
+
+                       # Find all users not watching the autocat
+                       $like = '%' . str_replace(
+                               ' ', '_', trim( wfMessage( 
'categorywatch-autocat', '' )->text() )
+                       ) . '%';
+                       $res = $dbr->select( [ 'user', 'watchlist' ], 'user_id',
+                                                                'wl_user IS 
NULL', __METHOD__, [],
+                                                                [ 'watchlist' 
=> [ 'LEFT JOIN',
+                                                                               
                        [
+                                                                               
                                'user_id=wl_user',
+                                                                               
                                'wl_tile', $dbr->buildLike( $like )
+                                                                               
                        ] ] ] );
+
+                       # Insert an entry into watchlist for each
+                       $row = $dbr->fetchRow( $res );
+                       while ( $row ) {
+                               $user = User::newFromId( $row[0] );
+                               $name = $user->getName();
+                               $wl_title = str_replace(
+                                       ' ', '_', wfMessage( 
'categorywatch-autocat', $name )->text()
+                               );
+                               $dbr->insert(
+                                       'watchlist',
+                                       [
+                                               'wl_user' => $row[0], 
'wl_namespace' => NS_CATEGORY,
+                                               'wl_title' => $wl_title
+                                       ]
+                               );
+                               $row = $dbr->fetchRow( $res );
+                       }
+                       $dbr->freeResult( $res );
+               }
+       }
+
+       /**
+        * Send a notification that the page's categorization has moved.
+        * @param string $from Category moving from
+        * @param string $to Category moving to
+        */
+       protected function notifyMove( $from, $to ) {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               $title   = Title::newFromText( $to, NS_CATEGORY );
+               $message = wfMessage(
+                       'categorywatch-catmovein', $page,
+                       $this->friendlyCat( $to ),
+                       $this->friendlyCat( $from )
+               )->text();
+               $this->notifyWatchers(
+                       $title, $user, $message, $summary, $medit, $pageurl
+               );
+
+               $title   = Title::newFromText( $from, NS_CATEGORY );
+               $message = wfMessage(
+                       'categorywatch-catmoveout', $page,
+                       $this->friendlyCat( $from ),
+                       $this->friendlyCat( $to )
+               )->text();
+               $this->notifyWatchers(
+                       $title, $user, $message, $summary, $medit, $pageurl
+               );
+       }
+
+       /**
+        * Send a notification that a page has been added to the category
+        * @param array $add Category being added
+        */
+       protected function notifyAdd( $add ) {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               foreach ( $add as $cat ) {
+                       $title   = Title::newFromText( $cat, NS_CATEGORY );
+                       $message = wfMessage(
+                               'categorywatch-catadd', $page,
+                               $this->friendlyCat( $cat )
+                       )->text();
+                       $this->notifyWatchers(
+                               $title, $user, $message, $summary, $medit, 
$pageurl
+                       );
+               }
+       }
+}
diff --git a/src/EchoEventPresentationModel.php 
b/src/EchoEventPresentationModel.php
new file mode 100644
index 0000000..049e928
--- /dev/null
+++ b/src/EchoEventPresentationModel.php
@@ -0,0 +1,180 @@
+<?php
+
+/**
+ * Category watch events
+ *
+ * Copyright (C) 2017  Mark A. Hershberger
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace CategoryWatch;
+
+use RawMessage;
+use Title;
+use WikiPage;
+
+class EchoEventPresentationModel extends \EchoEventPresentationModel {
+       /**
+        * Tell the caller if this event can be rendered.
+        *
+        * @return bool
+        */
+       public function canRender() {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               return (bool)$this->event->getTitle();
+       }
+
+       /**
+        * Which of the registered icons to use.
+        *
+        * @return string
+        */
+       public function getIconType() {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               return 'categorywatch';
+       }
+
+       /**
+        * The header of this event's display
+        *
+        * @return Message
+        */
+       public function getHeaderMessage() {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               if ( $this->isBundled() ) {
+                       $msg = $this->msg( 'categorywatch-notification-bundle' 
);
+                       $msg->params( $this->getBundleCount() );
+                       $msg->params( $this->getTruncatedTitleText( 
$this->event->getTitle(), true ) );
+                       $msg->params( $this->getViewingUserForGender() );
+               } else {
+                       $msg = $this->msg( 'categorywatch-notification-' . 
$this->event->getType() . '-header' );
+                       $msg->params( $this->getPageTitle() );
+                       $msg->params( $this->getTruncatedTitleText( 
$this->getPageTitle(), true ) );
+                       $msg->params( $this->event->getTitle() );
+                       $msg->params( $this->getTruncatedTitleText( 
$this->event->getTitle(), true ) );
+               }
+               return $msg;
+       }
+
+       /**
+        * Shorter display
+        *
+        * @return Message
+        */
+       public function getCompactHeaderMessage() {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               $msg = parent::getCompactHeaderMessage();
+               $msg->params( $this->getViewingUserForGender() );
+               return $msg;
+       }
+
+       /**
+        * Summary of edit
+        *
+        * @return string
+        */
+       public function getRevisionEditSummary() {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               $msg = $this->getMessageWithAgent( 
'categorywatch-notification-' . $this->event->getType() . '-summary' );
+               $msg->params( $this->getPageTitle() );
+               $msg->params( $this->getTruncatedTitleText( 
$this->getPageTitle(), true ) );
+               $msg->params( $this->event->getTitle() );
+               $msg->params( $this->getTruncatedTitleText( 
$this->event->getTitle(), true ) );
+               return $msg;
+       }
+
+       /**
+        * Body to display
+        *
+        * @return Message
+        */
+       public function getBodyMessage() {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               $msg = $this->getMessageWithAgent( 
'categorywatch-notification-' .
+                                                                               
   $this->event->getType() . '-body' );
+               $msg->params( $this->getPageTitle() );
+               $msg->params( $this->event->getTitle() );
+               return $msg;
+       }
+
+       /**
+        * Title of page
+        *
+        * @return Title|string
+        */
+       public function getPageTitle() {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               $page = WikiPage::newFromId( $this->event->getExtraParam( 
"pageid" ) );
+               return $page ? $page->getTitle() : new Title();
+       }
+
+       /**
+        * Provide the main link
+        *
+        * @return string
+        */
+       public function getPrimaryLink() {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               $title = $this->event->getTitle();
+               $msg = $this->msg( 'categorywatch-notification-link' );
+               $msg->params( $title );
+               return [
+                       'url' => $title->getFullURL(),
+                       'label' => $title->getPrefixedText()
+               ];
+       }
+
+       /**
+        * Aux links
+        *
+        * @return array
+        */
+       public function getSecondaryLinks() {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               if ( $this->isBundled() ) {
+                       // For the bundle, we don't need secondary actions
+                       return [];
+               } else {
+                       return [
+                               $this->getAgentLink(),
+                               [
+                                       'url' => 
$this->getPageTitle()->getFullURL(),
+                                       'label' => 
$this->getPageTitle()->getPrefixedText()
+                               ]
+                       ];
+               }
+       }
+
+       /**
+        * override parent
+        * @return array
+        * @throws TimestampException
+        */
+       public function jsonSerialize() {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               $body = $this->getBodyMessage();
+
+               return [
+                       'header' => $this->getHeaderMessage()->parse(),
+                       'compactHeader' => 
$this->getCompactHeaderMessage()->parse(),
+                       'body' => $body ? $body->toString() : '',
+                       'icon' => $this->getIconType(),
+                       'links' => [
+                               'primary' => 
$this->getPrimaryLinkWithMarkAsRead() ?: [],
+                               'secondary' => array_values( array_filter( 
$this->getSecondaryLinks() ) ),
+                       ],
+               ];
+       }
+}
diff --git a/src/Hook.php b/src/Hook.php
new file mode 100644
index 0000000..033f894
--- /dev/null
+++ b/src/Hook.php
@@ -0,0 +1,220 @@
+<?php
+/**
+ * Hooks for CategoryWatch extension
+ *
+ * Copyright (C) 2017  Mark A. Hershberger
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+namespace CategoryWatch;
+
+use Category;
+use Content;
+use EchoDiscussionParser;
+use EchoEvent;
+use MediaWiki\MediaWikiServices;
+use WatchedItemStore;
+use Status;
+use Title;
+use User;
+use WikiPage;
+
+class Hook {
+       // Instance
+       protected static $watcher;
+
+       /**
+        * Explain bundling
+        *
+        * @param Event $event to bundle
+        * @param string &$bundleString to use
+        */
+       public static function onEchoGetBundleRules( EchoEvent $event, 
&$bundleString ) {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               switch ( $event->getType() ) {
+               case 'categorywatch-add':
+               case 'categorywatch-remove':
+                       $bundleString = 'categorywatch';
+                       break;
+               }
+       }
+
+       /**
+        * Define the CategoryWatch notifications
+        *
+        * @param array &$notifications assoc array of notification types
+        * @param array &$notificationCategories assoc array describing
+        *        categories
+        * @param array &$icons assoc array of icons we define
+        */
+       public static function onBeforeCreateEchoEvent(
+               array &$notifications, array &$notificationCategories, array 
&$icons
+       ) {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               $icons['categorywatch']['path'] = 
'CategoryWatch/assets/catwatch.svg';
+
+               $notifications['categorywatch-add'] = [
+                       'bundle' => [
+                               'web' => true,
+                               'email' => true,
+                               'expandable' => true,
+                       ],
+                       'title-message' => 'categorywatch-add-title',
+                       'category' => 'categorywatch',
+                       'group' => 'neutral',
+                       'user-locators' => [ 'CategoryWatch\\Hook::userLocater' 
],
+                       'user-filters' => [ 'CategoryWatch\\Hook::userFilter' ],
+                       'presentation-model' => 
'CategoryWatch\\EchoEventPresentationModel',
+               ];
+
+               $notifications['categorywatch-remove'] = [
+                       'bundle' => [
+                               'web' => true,
+                               'email' => true,
+                               'expandable' => true,
+                       ],
+                       'title-message' => 'categorywatch-remove-title',
+                       'category' => 'categorywatch',
+                       'group' => 'neutral',
+                       'user-locators' => [ 'CategoryWatch\\Hook::userLocater' 
],
+                       'user-filters' => [ 'CategoryWatch\\Hook::userFilter' ],
+                       'presentation-model' => 
'CategoryWatch\\EchoEventPresentationModel',
+               ];
+
+               $notificationCategories['categorywatch'] = [
+                       'priority' => 2,
+                       'tooltip' => 'echo-pref-tooltip-categorywatch'
+               ];
+       }
+
+       /**
+        * Internal compatibility function
+        * @return WatchedItemStore
+        */
+       protected static function getWatchedItemStore() {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               if ( method_exists( 'WatchedItemStore', 'getDefaultInstance' ) 
) {
+                       return WatchedItemStore::getDefaultInstance();
+               } else {
+                       return 
MediaWikiServices::getInstance()->getWatchedItemStore();
+               }
+       }
+
+       /**
+        * Hook for page being added to a category.
+        *
+        * @param Category $cat that page is being add to
+        * @param WikiPage $page that is being added
+        */
+       public static function onCategoryAfterPageAdded(
+               Category $cat, WikiPage $page
+       ) {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               # Is anyone watching the category?
+               if (
+                       self::getWatchedItemStore()
+                       ->countWatchers( $cat->getTitle() ) > 0
+               ) {
+                       # Send them a notification!
+                       $user = User::newFromId( $page->getUser() );
+
+                       EchoEvent::create( [
+                               'type' => 'categorywatch-add',
+                               'title' => $cat->getTitle(),
+                               'agent' => $user,
+                               'extra' => [
+                                       'pageid' => $page->getId(),
+                                       'revid' => 
$page->getRevision()->getId(),
+                               ],
+                       ] );
+               }
+       }
+
+       /**
+        * Hook for page being taken out of a category.
+        *
+        * @param Category $cat that page is being removed from
+        * @param WikiPage $page that is being removed
+        * @param int $id that this happened in. (not given pre 1.27ish)
+        */
+       public static function onCategoryAfterPageRemoved(
+               Category $cat, WikiPage $page, $id = 0
+       ) {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               # Is anyone watching the category?
+               if (
+                       self::getWatchedItemStore()
+                       ->countWatchers( $cat->getTitle() ) > 0
+               ) {
+                       # Send them a notification!
+                       $user = User::newFromId( $page->getUser() );
+
+                       EchoEvent::create( [
+                               'type' => 'categorywatch-remove',
+                               'title' => $cat->getTitle(),
+                               'agent' => $user,
+                               'extra' => [
+                                       'pageid' => $page->getId(),
+                                       'revid' => 
$page->getRevision()->getId(),
+                               ],
+                       ] );
+               }
+       }
+
+       /**
+        * Find the watchers for a title
+        *
+        * @param Title $target to check
+        *
+        * @return array
+        */
+       protected static function getWatchers( Title $target ) {
+               $dbr = wfGetDB( DB_SLAVE );
+               $return = $dbr->selectFieldValues(
+                       'watchlist',
+                       'wl_user',
+                       [
+                               'wl_namespace' => $target->getNamespace(),
+                               'wl_title' => $target->getDBkey(),
+                       ],
+                       __METHOD__
+               );
+
+               return array_map( function ( $id ) {
+                       return User::newFromID( $id );
+               }, $return );
+       }
+
+       /**
+        * Get users that should be notified for this event.
+        *
+        * @param EchoEvent $event to be looked at
+        * @return array
+        */
+       public static function userLocater( EchoEvent $event ) {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               return self::getWatchers( $event->getTitle() );
+       }
+
+       /**
+        * Filter out the person performing the action
+        *
+        * @param EchoEvent $event to be looked at
+        * @return array
+        */
+       public static function userFilter( EchoEvent $event ) {
+               wfDebugLog( 'CategoryWatch', __METHOD__ );
+               return [ $event->getAgent() ];
+       }
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/386007
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Id0df74ccef1f9c6ea51c22396977f5424cbe19ae
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/CategoryWatch
Gerrit-Branch: master
Gerrit-Owner: MarkAHershberger <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to