mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Echo
synced 2024-09-23 10:22:05 +00:00
Add dynamic secondary action to mute/unmute page-linked notifications
Also adds an API module for muting and unmuting pages (and users). Bug: T46787 Bug: T115264 Change-Id: Icf4e4bfa9fd7fa27b4c40892e3d5ce000eb22d5a
This commit is contained in:
parent
5e9eac03d0
commit
28f432b150
|
@ -24,7 +24,8 @@
|
|||
"APIModules": {
|
||||
"echomarkread": "ApiEchoMarkRead",
|
||||
"echomarkseen": "ApiEchoMarkSeen",
|
||||
"echoarticlereminder": "ApiEchoArticleReminder"
|
||||
"echoarticlereminder": "ApiEchoArticleReminder",
|
||||
"echomute": "ApiEchoMute"
|
||||
},
|
||||
"DefaultUserOptions": {
|
||||
"echo-email-frequency": 0,
|
||||
|
@ -721,6 +722,9 @@
|
|||
"rtl": "Echo/modules/icons/userTalk-rtl.svg"
|
||||
}
|
||||
},
|
||||
"unbell": {
|
||||
"path": "Echo/modules/icons/unbell.svg"
|
||||
},
|
||||
"userSpeechBubble": {
|
||||
"path": "Echo/modules/icons/user-speech-bubble.svg"
|
||||
}
|
||||
|
@ -994,6 +998,7 @@
|
|||
"ApiEchoArticleReminder": "includes/api/ApiEchoArticleReminder.php",
|
||||
"ApiEchoMarkRead": "includes/api/ApiEchoMarkRead.php",
|
||||
"ApiEchoMarkSeen": "includes/api/ApiEchoMarkSeen.php",
|
||||
"ApiEchoMute": "includes/api/ApiEchoMute.php",
|
||||
"ApiEchoNotifications": "includes/api/ApiEchoNotifications.php",
|
||||
"ApiEchoUnreadNotificationPages": "includes/api/ApiEchoUnreadNotificationPages.php",
|
||||
"BackfillUnreadWikis": "maintenance/backfillUnreadWikis.php",
|
||||
|
|
|
@ -24,6 +24,11 @@
|
|||
"apihelp-echomarkseen-example-1": "Mark notifications of all types as seen",
|
||||
"apihelp-echomarkseen-param-type": "Type of notifications to mark as seen: 'alert', 'message' or 'all'.",
|
||||
"apihelp-echomarkseen-param-timestampFormat": "Timestamp format to use for output, 'ISO_8601' or 'MW'. 'MW' is deprecated here, so all clients should switch to 'ISO_8601'. This parameter will be removed, and 'ISO_8601' will become the only output format.",
|
||||
"apihelp-echomute-description": "Mute or unmute notifications from certain users or pages.",
|
||||
"apihelp-echomute-summary": "Mute or unmute notifications from certain users or pages.",
|
||||
"apihelp-echomute-param-type": "Which mute list to add to or remove from",
|
||||
"apihelp-echomute-param-mute": "Pages or users to add to the mute list",
|
||||
"apihelp-echomute-param-unmute": "Pages or users to remove from the mute list",
|
||||
"apihelp-query+notifications-description": "Get notifications waiting for the current user.",
|
||||
"apihelp-query+notifications-summary": "Get notifications waiting for the current user.",
|
||||
"apihelp-query+notifications-param-prop": "Details to request.",
|
||||
|
|
|
@ -26,6 +26,11 @@
|
|||
"apihelp-echomarkseen-example-1": "{{doc-apihelp-example|echomarkseen}}",
|
||||
"apihelp-echomarkseen-param-type": "{{doc-apihelp-param|echomarkseen|type}}",
|
||||
"apihelp-echomarkseen-param-timestampFormat": "{{doc-apihelp-param|echomarkseen|timestampFormat}}",
|
||||
"apihelp-echomute-description": "{{doc-apihelp-description|echomute}}",
|
||||
"apihelp-echomute-summary": "{{doc-apihelp-summary|echomute}}",
|
||||
"apihelp-echomute-param-type": "{{doc-apihelp-param|echomute|type}}",
|
||||
"apihelp-echomute-param-mute": "{{doc-apihelp-param|echomute|mute}}",
|
||||
"apihelp-echomute-param-unmute": "{{doc-apihelp-param|echomute|unmute}}",
|
||||
"apihelp-query+notifications-description": "{{doc-apihelp-description|query+notifications}}",
|
||||
"apihelp-query+notifications-summary": "{{doc-apihelp-summary|query+notifications}}",
|
||||
"apihelp-query+notifications-param-prop": "{{doc-apihelp-param|query+notifications|prop}}",
|
||||
|
|
|
@ -129,6 +129,12 @@
|
|||
"echo-notification-markasunread": "Mark as unread",
|
||||
"echo-notification-markasread-tooltip": "Mark as read",
|
||||
"echo-notification-more-options-tooltip": "More options",
|
||||
"notification-dynamic-actions-mute-page-linked": "{{GENDER:$2|Mute}} link notifications on \"$1\"",
|
||||
"notification-dynamic-actions-mute-page-linked-confirmation": "\"Page link\" notifications are now disabled for the page \"$1\"",
|
||||
"notification-dynamic-actions-mute-page-linked-confirmation-description": "{{GENDER:$2|You}} can manage your muted pages in [$1 your preferences] anytime.",
|
||||
"notification-dynamic-actions-unmute-page-linked": "{{GENDER:$2|Unmute}} link notifications on \"$1\"",
|
||||
"notification-dynamic-actions-unmute-page-linked-confirmation": "\"Page link\" notifications are now enabled for the page \"$1\"",
|
||||
"notification-dynamic-actions-unmute-page-linked-confirmation-description": "{{GENDER:$2|You}} can manage your muted pages in [$1 your preferences] anytime.",
|
||||
"notification-dynamic-actions-unwatch": "{{GENDER:$3|Stop}} watching new activity on \"$1\"",
|
||||
"notification-dynamic-actions-unwatch-confirmation": "{{GENDER:$3|You}} are no longer watching the page \"$1\"",
|
||||
"notification-dynamic-actions-unwatch-confirmation-description": "{{GENDER:$3|You}} can watch [$2 this page] anytime.",
|
||||
|
|
|
@ -131,6 +131,12 @@
|
|||
"echo-notification-markasunread": "Label for the button to mark the notification as unread.",
|
||||
"echo-notification-markasread-tooltip": "Tooltip for the button to mark the notification as read.",
|
||||
"echo-notification-more-options-tooltip": "Tooltip for the button to show the hidden secondary actions.",
|
||||
"notification-dynamic-actions-mute-page-linked": "Text for the action offering the user to mute link notifications for a page from within a notification.\n\nParameters:\n* $1 - Page name\n* $2 - Current user name for gender purposes\n\n{{related|Notification-dynamic}}",
|
||||
"notification-dynamic-actions-mute-page-linked-confirmation": "Title for the confirmation text for muting link notifications for a page from within a notification.\n\nParameters:\n* $1 - Page name\n* $2 - Current user name for gender purposes\n\n{{related|Notification-dynamic}}",
|
||||
"notification-dynamic-actions-mute-page-linked-confirmation-description": "Description for the confirmation text for muting link notifications for a page from within a notification.\n\nParameters:\n* $1 - Page name\n* $2 - Current user name for gender purposes\n\n{{related|Notification-dynamic}}",
|
||||
"notification-dynamic-actions-unmute-page-linked": "Text for the action offering the user to unmute link notifications for a page from within a notification.\n\nParameters:\n* $1 - Page name\n* $2 - Current user name for gender purposes\n\n{{related|Notification-dynamic}}",
|
||||
"notification-dynamic-actions-unmute-page-linked-confirmation": "Title for the confirmation text for unmuting link notifications for a page from within a notification.\n\nParameters:\n* $1 - Page name\n* $2 - Current user name for gender purposes\n\n{{related|Notification-dynamic}}",
|
||||
"notification-dynamic-actions-unmute-page-linked-confirmation-description": "Description for the confirmation text for unmuting link notifications for a page from within a notification.\n\nParameters:\n* $1 - Page name\n* $2 - Current user name for gender purposes\n\n{{related|Notification-dynamic}}",
|
||||
"notification-dynamic-actions-unwatch": "Text for the action offering the user to unwatch a page from within a notification.\n\nParameters:\n* $1 - Page name\n* $2 - Page URL\n* $3 - Current user name for gender purposes\n\n{{related|Notification-dynamic}}",
|
||||
"notification-dynamic-actions-unwatch-confirmation": "Title for the confirmation text for unwatching a page from within a notification.\n\nParameters:\n* $1 - Page name\n* $2 - Page URL\n* $3 - Current user name for gender purposes\n\n{{related|Notification-dynamic}}",
|
||||
"notification-dynamic-actions-unwatch-confirmation-description": "Description for the confirmation text for unwatching a page from within a notification.\n\nParameters:\n* $1 - Page name\n* $2 - Page URL\n* $3 - Current user name for gender purposes\n\n{{related|Notification-dynamic}}",
|
||||
|
|
128
includes/api/ApiEchoMute.php
Normal file
128
includes/api/ApiEchoMute.php
Normal file
|
@ -0,0 +1,128 @@
|
|||
<?php
|
||||
|
||||
use MediaWiki\MediaWikiServices;
|
||||
|
||||
class ApiEchoMute extends ApiBase {
|
||||
|
||||
private $centralIdLookup = null;
|
||||
|
||||
private static $muteLists = [
|
||||
'user' => [
|
||||
'pref' => 'echo-notifications-blacklist',
|
||||
'type' => 'user',
|
||||
],
|
||||
'page-linked-title' => [
|
||||
'pref' => 'echo-notifications-page-linked-title-muted-list',
|
||||
'type' => 'title'
|
||||
],
|
||||
];
|
||||
|
||||
public function execute() {
|
||||
$user = $this->getUser()->getInstanceForUpdate();
|
||||
if ( !$user || $user->isAnon() ) {
|
||||
$this->dieWithError(
|
||||
[ 'apierror-mustbeloggedin', $this->msg( 'action-editmyoptions' ) ],
|
||||
'notloggedin'
|
||||
);
|
||||
}
|
||||
|
||||
$this->checkUserRightsAny( 'editmyoptions' );
|
||||
|
||||
$params = $this->extractRequestParams();
|
||||
$mutelistInfo = self::$muteLists[ $params['type'] ];
|
||||
$prefValue = $user->getOption( $mutelistInfo['pref'] );
|
||||
$ids = $this->parsePref( $prefValue, $mutelistInfo['type'] );
|
||||
|
||||
$changed = false;
|
||||
$addIds = $this->lookupIds( $params['mute'], $mutelistInfo['type'] );
|
||||
foreach ( $addIds as $id ) {
|
||||
if ( !in_array( $id, $ids ) ) {
|
||||
$ids[] = $id;
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
$removeIds = $this->lookupIds( $params['unmute'], $mutelistInfo['type'] );
|
||||
foreach ( $removeIds as $id ) {
|
||||
$index = array_search( $id, $ids );
|
||||
if ( $index !== false ) {
|
||||
array_splice( $ids, $index, 1 );
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $changed ) {
|
||||
$user->setOption( $mutelistInfo['pref'], $this->serializePref( $ids, $mutelistInfo['type'] ) );
|
||||
$user->saveSettings();
|
||||
}
|
||||
|
||||
$this->getResult()->addValue( null, $this->getModuleName(), 'success' );
|
||||
}
|
||||
|
||||
private function getCentralIdLookup() {
|
||||
if ( $this->centralIdLookup === null ) {
|
||||
$this->centralIdLookup = CentralIdLookup::factory();
|
||||
}
|
||||
return $this->centralIdLookup;
|
||||
}
|
||||
|
||||
private function lookupIds( $names, $type ) {
|
||||
if ( $type === 'title' ) {
|
||||
$linkBatch = MediaWikiServices::getInstance()->getLinkBatchFactory()->newLinkBatch();
|
||||
foreach ( $names as $name ) {
|
||||
$linkBatch->addObj( Title::newFromText( $name ) );
|
||||
}
|
||||
$linkBatch->execute();
|
||||
|
||||
$ids = [];
|
||||
foreach ( $names as $name ) {
|
||||
$title = Title::newFromText( $name );
|
||||
if ( $title instanceof Title && $title->getArticleID() > 0 ) {
|
||||
$ids[] = $title->getArticleID();
|
||||
}
|
||||
}
|
||||
return $ids;
|
||||
} elseif ( $type === 'user' ) {
|
||||
return $this->getCentralIdLookup()->centralIdsFromNames( $names, CentralIdLookup::AUDIENCE_PUBLIC );
|
||||
}
|
||||
}
|
||||
|
||||
private function parsePref( $prefValue, $type ) {
|
||||
return preg_split( '/\n/', $prefValue, -1, PREG_SPLIT_NO_EMPTY );
|
||||
}
|
||||
|
||||
private function serializePref( $ids, $type ) {
|
||||
return implode( "\n", $ids );
|
||||
}
|
||||
|
||||
public function getAllowedParams( $flags = 0 ) {
|
||||
return [
|
||||
'type' => [
|
||||
ApiBase::PARAM_REQUIRED => true,
|
||||
ApiBase::PARAM_TYPE => array_keys( self::$muteLists ),
|
||||
],
|
||||
'mute' => [
|
||||
ApiBase::PARAM_ISMULTI => true,
|
||||
],
|
||||
'unmute' => [
|
||||
ApiBase::PARAM_ISMULTI => true,
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function needsToken() {
|
||||
return 'csrf';
|
||||
}
|
||||
|
||||
public function getTokenSalt() {
|
||||
return '';
|
||||
}
|
||||
|
||||
public function mustBePosted() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isWriteMode() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -246,10 +246,6 @@ class EchoNotificationController {
|
|||
self::$blacklistByUser = new MapCacheLRU( self::$maxRecipientCacheSize );
|
||||
}
|
||||
|
||||
if ( self::$mutedPageLinkedTitlesCache === null ) {
|
||||
self::$mutedPageLinkedTitlesCache = new MapCacheLRU( self::$maxUsersTitleCacheSize );
|
||||
}
|
||||
|
||||
// Ensure we have a blacklist for the user
|
||||
if ( !self::$blacklistByUser->has( (string)$user->getId() ) ) {
|
||||
$blacklist = new EchoContainmentSet( $user );
|
||||
|
@ -277,17 +273,20 @@ class EchoNotificationController {
|
|||
return $blacklist->contains( $event->getAgent()->getName() ) ||
|
||||
( $wgEchoPerUserBlacklist &&
|
||||
$event->getType() === 'page-linked' &&
|
||||
self::isPageLinkedTitleMutedByUser( $event, $user ) );
|
||||
self::isPageLinkedTitleMutedByUser( $event->getTitle(), $user ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the event title is in the users page-linked event muted list.
|
||||
* Check if a title is in the user's page-linked event blacklist.
|
||||
*
|
||||
* @param EchoEvent $event
|
||||
* @param Title $title
|
||||
* @param User $user
|
||||
* @return bool
|
||||
*/
|
||||
private static function isPageLinkedTitleMutedByUser( EchoEvent $event, User $user ) {
|
||||
public static function isPageLinkedTitleMutedByUser( Title $title, User $user ) {
|
||||
if ( self::$mutedPageLinkedTitlesCache === null ) {
|
||||
self::$mutedPageLinkedTitlesCache = new MapCacheLRU( self::$maxUsersTitleCacheSize );
|
||||
}
|
||||
if ( !self::$mutedPageLinkedTitlesCache->has( (string)$user->getId() ) ) {
|
||||
$pageLinkedTitleMutedList = new EchoContainmentSet( $user );
|
||||
$pageLinkedTitleMutedList->addTitleIDsFromUserOption(
|
||||
|
@ -297,7 +296,7 @@ class EchoNotificationController {
|
|||
} else {
|
||||
$pageLinkedTitleMutedList = self::$mutedPageLinkedTitlesCache->get( (string)$user->getId() );
|
||||
}
|
||||
return $pageLinkedTitleMutedList->contains( (string)$event->getTitle()->getArticleID() );
|
||||
return $pageLinkedTitleMutedList->contains( (string)$title->getArticleID() );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<?php
|
||||
|
||||
use MediaWiki\MediaWikiServices;
|
||||
|
||||
class EchoPageLinkedPresentationModel extends EchoEventPresentationModel {
|
||||
|
||||
private $pageFrom;
|
||||
|
@ -53,7 +55,60 @@ class EchoPageLinkedPresentationModel extends EchoEventPresentationModel {
|
|||
];
|
||||
}
|
||||
|
||||
return [ $whatLinksHereLink, $diffLink ];
|
||||
return [ $whatLinksHereLink, $diffLink, $this->getMuteLink() ];
|
||||
}
|
||||
|
||||
protected function getMuteLink() {
|
||||
if ( !MediaWikiServices::getInstance()->getMainConfig()->get( 'EchoPerUserBlacklist' ) ) {
|
||||
return null;
|
||||
}
|
||||
$title = $this->event->getTitle();
|
||||
$isPageMuted = EchoNotificationController::isPageLinkedTitleMutedByUser( $title, $this->getUser() );
|
||||
$action = $isPageMuted ? 'unmute' : 'mute';
|
||||
$prefTitle = SpecialPage::getTitleFor( 'Preferences', false, 'mw-prefsection-echo-mutedpageslist' );
|
||||
$data = [
|
||||
'tokenType' => 'csrf',
|
||||
'params' => [
|
||||
'action' => 'echomute',
|
||||
'type' => 'page-linked-title',
|
||||
],
|
||||
'messages' => [
|
||||
'confirmation' => [
|
||||
// notification-dynamic-actions-mute-page-linked-confirmation
|
||||
// notification-dynamic-actions-unmute-page-linked-confirmation
|
||||
'title' => $this
|
||||
->msg( 'notification-dynamic-actions-' . $action . '-page-linked-confirmation' )
|
||||
->params(
|
||||
$this->getTruncatedTitleText( $title ),
|
||||
$this->getViewingUserForGender()
|
||||
),
|
||||
// notification-dynamic-actions-mute-page-linked-confirmation-description
|
||||
// notification-dynamic-actions-unmute-page-linked-confirmation-description
|
||||
'description' => $this
|
||||
->msg( 'notification-dynamic-actions-' . $action . '-page-linked-confirmation-description' )
|
||||
->params(
|
||||
$prefTitle->getFullURL(),
|
||||
$this->getViewingUserForGender()
|
||||
)
|
||||
]
|
||||
]
|
||||
];
|
||||
$data['params'][$isPageMuted ? 'unmute' : 'mute'] = $title->getPrefixedText();
|
||||
|
||||
return $this->getDynamicActionLink(
|
||||
$prefTitle,
|
||||
$isPageMuted ? 'bell' : 'unbell',
|
||||
// notification-dynamic-actions-mute-page-linked
|
||||
// notification-dynamic-actions-unmute-page-linked
|
||||
$this->msg( 'notification-dynamic-actions-' . $action . '-page-linked' )
|
||||
->params(
|
||||
$this->getTruncatedTitleText( $title ),
|
||||
$this->getViewingUserForGender()
|
||||
),
|
||||
null,
|
||||
$data,
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
protected function getHeaderMessageKey() {
|
||||
|
|
7
modules/icons/unbell.svg
Normal file
7
modules/icons/unbell.svg
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewbox="0 0 20 20">
|
||||
<title>
|
||||
unbell
|
||||
</title>
|
||||
<path d="M1.22 0L0 1.22l4.334 4.335A5.38 5.38 0 004 7v6l-2 2v1h12.78l4 4L20 18.78 17.22 16 5.23 4.01 1.22 0zM10 0C8.47 0 8.4 1.46 8.46 2.15a5.38 5.38 0 00-1.92.729l9.46 9.46V7a5.38 5.38 0 00-4.46-4.85C11.6 1.46 11.53 0 10 0zM7 17a3 3 0 003 3 3 3 0 003-3H7z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 420 B |
|
@ -10,34 +10,34 @@ class NotificationControllerUnitTest extends MediaWikiUnitTestCase {
|
|||
/**
|
||||
* @dataProvider PageLinkedTitleMutedByUserDataProvider
|
||||
* @covers ::isPageLinkedTitleMutedByUser
|
||||
* @param EchoEvent $echoEvent
|
||||
* @param Title $title
|
||||
* @param User $user
|
||||
* @param bool $expected
|
||||
*/
|
||||
public function testIsPageLinkedTitleMutedByUser(
|
||||
EchoEvent $echoEvent, User $user, bool $expected ): void {
|
||||
Title $title, User $user, bool $expected ): void {
|
||||
$wrapper = TestingAccessWrapper::newFromClass( EchoNotificationController::class );
|
||||
$wrapper->mutedPageLinkedTitlesCache = $this->getMapCacheLruMock();
|
||||
$this->assertSame(
|
||||
$expected,
|
||||
$wrapper->isPageLinkedTitleMutedByUser( $echoEvent, $user )
|
||||
$wrapper->isPageLinkedTitleMutedByUser( $title, $user )
|
||||
);
|
||||
}
|
||||
|
||||
public function PageLinkedTitleMutedByUserDataProvider() :array {
|
||||
return [
|
||||
[
|
||||
$this->getMockEvent( 123 ),
|
||||
$this->getMockTitle( 123 ),
|
||||
$this->getMockUser( [] ),
|
||||
false
|
||||
],
|
||||
[
|
||||
$this->getMockEvent( 123 ),
|
||||
$this->getMockTitle( 123 ),
|
||||
$this->getMockUser( [ 123, 456, 789 ] ),
|
||||
true
|
||||
],
|
||||
[
|
||||
$this->getMockEvent( 456 ),
|
||||
$this->getMockTitle( 456 ),
|
||||
$this->getMockUser( [ 489 ] ),
|
||||
false
|
||||
]
|
||||
|
@ -45,20 +45,13 @@ class NotificationControllerUnitTest extends MediaWikiUnitTestCase {
|
|||
];
|
||||
}
|
||||
|
||||
private function getMockEvent( int $articleID ) {
|
||||
$event = $this->getMockBuilder( EchoEvent::class )
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$event->method( 'getAgent' )
|
||||
->willReturn( $this->getMockUser() );
|
||||
private function getMockTitle( int $articleID ) {
|
||||
$title = $this->getMockBuilder( Title::class )
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$title->method( 'getArticleID' )
|
||||
->willReturn( $articleID );
|
||||
$event->method( 'getTitle' )
|
||||
->willReturn( $title );
|
||||
return $event;
|
||||
return $title;
|
||||
}
|
||||
|
||||
private function getMockUser( $mutedTitlePreferences = [] ) {
|
||||
|
|
Loading…
Reference in a new issue