1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-22 06:42:42 +01:00

Add "Mute/Unmute" for subscribable objects

Summary: Ref T13053. See PHI126. Add an explicit "Mute" action to kill mail and notifications for a particular object.

Test Plan: Muted and umuted an object while interacting with it. Saw mail route appropriately.

Maniphest Tasks: T13053

Differential Revision: https://secure.phabricator.com/D19033
This commit is contained in:
epriestley 2018-02-08 10:37:47 -08:00
parent 0402a79e0e
commit ab04d2179b
13 changed files with 254 additions and 24 deletions

View file

@ -9,7 +9,7 @@ return array(
'names' => array(
'conpherence.pkg.css' => 'e68cf1fa',
'conpherence.pkg.js' => '15191c65',
'core.pkg.css' => '51debec3',
'core.pkg.css' => 'ce8c2a58',
'core.pkg.js' => '4c79d74f',
'darkconsole.pkg.js' => '1f9a31bc',
'differential.pkg.css' => '45951e9e',
@ -136,7 +136,7 @@ return array(
'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '9d9685d6',
'rsrc/css/phui/object-item/phui-oi-list-view.css' => '6ae18df0',
'rsrc/css/phui/object-item/phui-oi-simple-ui.css' => 'a8beebea',
'rsrc/css/phui/phui-action-list.css' => 'f7f61a34',
'rsrc/css/phui/phui-action-list.css' => '0bcd9a45',
'rsrc/css/phui/phui-action-panel.css' => 'b4798122',
'rsrc/css/phui/phui-badge.css' => '22c0cf4f',
'rsrc/css/phui/phui-basic-nav-view.css' => '98c11ab3',
@ -766,7 +766,7 @@ return array(
'path-typeahead' => 'f7fc67ec',
'people-picture-menu-item-css' => 'a06f7f34',
'people-profile-css' => '4df76faf',
'phabricator-action-list-view-css' => 'f7f61a34',
'phabricator-action-list-view-css' => '0bcd9a45',
'phabricator-busy' => '59a7976a',
'phabricator-chatlog-css' => 'd295b020',
'phabricator-content-source-view-css' => '4b8b05d4',

View file

@ -3291,6 +3291,8 @@ phutil_register_library_map(array(
'PhabricatorMultiFactorSettingsPanel' => 'applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php',
'PhabricatorMultimeterApplication' => 'applications/multimeter/application/PhabricatorMultimeterApplication.php',
'PhabricatorMustVerifyEmailController' => 'applications/auth/controller/PhabricatorMustVerifyEmailController.php',
'PhabricatorMutedByEdgeType' => 'applications/transactions/edges/PhabricatorMutedByEdgeType.php',
'PhabricatorMutedEdgeType' => 'applications/transactions/edges/PhabricatorMutedEdgeType.php',
'PhabricatorMySQLConfigOptions' => 'applications/config/option/PhabricatorMySQLConfigOptions.php',
'PhabricatorMySQLFileStorageEngine' => 'applications/files/engine/PhabricatorMySQLFileStorageEngine.php',
'PhabricatorMySQLSearchHost' => 'infrastructure/cluster/search/PhabricatorMySQLSearchHost.php',
@ -4240,6 +4242,7 @@ phutil_register_library_map(array(
'PhabricatorSubscriptionsHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsHeraldAction.php',
'PhabricatorSubscriptionsListController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsListController.php',
'PhabricatorSubscriptionsMailEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsMailEngineExtension.php',
'PhabricatorSubscriptionsMuteController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php',
'PhabricatorSubscriptionsRemoveSelfHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsRemoveSelfHeraldAction.php',
'PhabricatorSubscriptionsRemoveSubscribersHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsRemoveSubscribersHeraldAction.php',
'PhabricatorSubscriptionsSearchEngineAttachment' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsSearchEngineAttachment.php',
@ -8808,6 +8811,8 @@ phutil_register_library_map(array(
'PhabricatorMultiFactorSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorMultimeterApplication' => 'PhabricatorApplication',
'PhabricatorMustVerifyEmailController' => 'PhabricatorAuthController',
'PhabricatorMutedByEdgeType' => 'PhabricatorEdgeType',
'PhabricatorMutedEdgeType' => 'PhabricatorEdgeType',
'PhabricatorMySQLConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorMySQLFileStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorMySQLSearchHost' => 'PhabricatorSearchHost',
@ -9960,6 +9965,7 @@ phutil_register_library_map(array(
'PhabricatorSubscriptionsHeraldAction' => 'HeraldAction',
'PhabricatorSubscriptionsListController' => 'PhabricatorController',
'PhabricatorSubscriptionsMailEngineExtension' => 'PhabricatorMailEngineExtension',
'PhabricatorSubscriptionsMuteController' => 'PhabricatorController',
'PhabricatorSubscriptionsRemoveSelfHeraldAction' => 'PhabricatorSubscriptionsHeraldAction',
'PhabricatorSubscriptionsRemoveSubscribersHeraldAction' => 'PhabricatorSubscriptionsHeraldAction',
'PhabricatorSubscriptionsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',

View file

@ -21,6 +21,7 @@ final class PhabricatorMetaMTAActor extends Phobject {
const REASON_ROUTE_AS_NOTIFICATION = 'route-as-notification';
const REASON_ROUTE_AS_MAIL = 'route-as-mail';
const REASON_UNVERIFIED = 'unverified';
const REASON_MUTED = 'muted';
private $phid;
private $emailAddress;
@ -116,6 +117,7 @@ final class PhabricatorMetaMTAActor extends Phobject {
self::REASON_ROUTE_AS_NOTIFICATION => pht('Route as Notification'),
self::REASON_ROUTE_AS_MAIL => pht('Route as Mail'),
self::REASON_UNVERIFIED => pht('Address Not Verified'),
self::REASON_MUTED => pht('Muted'),
);
return idx($names, $reason, pht('Unknown ("%s")', $reason));
@ -172,6 +174,8 @@ final class PhabricatorMetaMTAActor extends Phobject {
'in Herald.'),
self::REASON_UNVERIFIED => pht(
'This recipient does not have a verified primary email address.'),
self::REASON_MUTED => pht(
'This recipient has muted notifications for this object.'),
);
return idx($descriptions, $reason, pht('Unknown Reason ("%s")', $reason));

View file

@ -160,6 +160,15 @@ final class PhabricatorMetaMTAMail
return $this->getParam('exclude', array());
}
public function setMutedPHIDs(array $muted) {
$this->setParam('muted', $muted);
return $this;
}
private function getMutedPHIDs() {
return $this->getParam('muted', array());
}
public function setForceHeraldMailRecipientPHIDs(array $force) {
$this->setParam('herald-force-recipients', $force);
return $this;
@ -1113,6 +1122,18 @@ final class PhabricatorMetaMTAMail
}
}
// Exclude muted recipients. We're doing this after saving deliverability
// so that Herald "Send me an email" actions can still punch through a
// mute.
foreach ($this->getMutedPHIDs() as $muted_phid) {
$muted_actor = idx($actors, $muted_phid);
if (!$muted_actor) {
continue;
}
$muted_actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_MUTED);
}
// For the rest of the rules, order matters. We're going to run all the
// possible rules in order from weakest to strongest, and let the strongest
// matching rule win. The weaker rules leave annotations behind which help

View file

@ -24,7 +24,10 @@ final class PhabricatorSubscriptionsApplication extends PhabricatorApplication {
return array(
'/subscriptions/' => array(
'(?P<action>add|delete)/'.
'(?P<phid>[^/]+)/' => 'PhabricatorSubscriptionsEditController',
'(?P<phid>[^/]+)/' => 'PhabricatorSubscriptionsEditController',
'mute/' => array(
'(?P<phid>[^/]+)/' => 'PhabricatorSubscriptionsMuteController',
),
'list/(?P<phid>[^/]+)/' => 'PhabricatorSubscriptionsListController',
'transaction/(?P<type>add|rem)/(?<phid>[^/]+)/'
=> 'PhabricatorSubscriptionsTransactionController',

View file

@ -0,0 +1,92 @@
<?php
final class PhabricatorSubscriptionsMuteController
extends PhabricatorController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$phid = $request->getURIData('phid');
$handle = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->executeOne();
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->executeOne();
if (!($object instanceof PhabricatorSubscribableInterface)) {
return new Aphront400Response();
}
$muted_type = PhabricatorMutedByEdgeType::EDGECONST;
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($object->getPHID()))
->withEdgeTypes(array($muted_type))
->withDestinationPHIDs(array($viewer->getPHID()));
$edge_query->execute();
$is_mute = !$edge_query->getDestinationPHIDs();
$object_uri = $handle->getURI();
if ($request->isFormPost()) {
if ($is_mute) {
$xaction_value = array(
'+' => array_fuse(array($viewer->getPHID())),
);
} else {
$xaction_value = array(
'-' => array_fuse(array($viewer->getPHID())),
);
}
$muted_type = PhabricatorMutedByEdgeType::EDGECONST;
$xaction = id($object->getApplicationTransactionTemplate())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $muted_type)
->setNewValue($xaction_value);
$editor = id($object->getApplicationTransactionEditor())
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setContentSourceFromRequest($request);
$editor->applyTransactions(
$object->getApplicationTransactionObject(),
array($xaction));
return id(new AphrontReloadResponse())->setURI($object_uri);
}
$dialog = $this->newDialog()
->addCancelButton($object_uri);
if ($is_mute) {
$dialog
->setTitle(pht('Mute Notifications'))
->appendParagraph(
pht(
'Mute this object? You will no longer receive notifications or '.
'email about it.'))
->addSubmitButton(pht('Mute'));
} else {
$dialog
->setTitle(pht('Unmute Notifications'))
->appendParagraph(
pht(
'Unmute this object? You will receive notifications and email '.
'again.'))
->addSubmitButton(pht('Unmute'));
}
return $dialog;
}
}

View file

@ -42,6 +42,28 @@ final class PhabricatorSubscriptionsUIEventListener
return;
}
$src_phid = $object->getPHID();
$subscribed_type = PhabricatorObjectHasSubscriberEdgeType::EDGECONST;
$muted_type = PhabricatorMutedByEdgeType::EDGECONST;
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($src_phid))
->withEdgeTypes(
array(
$subscribed_type,
$muted_type,
))
->withDestinationPHIDs(array($user_phid))
->execute();
if ($user_phid) {
$is_subscribed = isset($edges[$src_phid][$subscribed_type][$user_phid]);
$is_muted = isset($edges[$src_phid][$muted_type][$user_phid]);
} else {
$is_subscribed = false;
$is_muted = false;
}
if ($user_phid && $object->isAutomaticallySubscribed($user_phid)) {
$sub_action = id(new PhabricatorActionView())
->setWorkflow(true)
@ -51,22 +73,9 @@ final class PhabricatorSubscriptionsUIEventListener
->setName(pht('Automatically Subscribed'))
->setIcon('fa-check-circle lightgreytext');
} else {
$subscribed = false;
if ($user->isLoggedIn()) {
$src_phid = $object->getPHID();
$edge_type = PhabricatorObjectHasSubscriberEdgeType::EDGECONST;
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($src_phid))
->withEdgeTypes(array($edge_type))
->withDestinationPHIDs(array($user_phid))
->execute();
$subscribed = isset($edges[$src_phid][$edge_type][$user_phid]);
}
$can_interact = PhabricatorPolicyFilter::canInteract($user, $object);
if ($subscribed) {
if ($is_subscribed) {
$sub_action = id(new PhabricatorActionView())
->setWorkflow(true)
->setRenderAsForm(true)
@ -89,8 +98,26 @@ final class PhabricatorSubscriptionsUIEventListener
}
}
$mute_action = id(new PhabricatorActionView())
->setWorkflow(true)
->setHref('/subscriptions/mute/'.$object->getPHID().'/')
->setDisabled(!$user_phid);
if (!$is_muted) {
$mute_action
->setName(pht('Mute Notifications'))
->setIcon('fa-volume-up');
} else {
$mute_action
->setName(pht('Unmute Notifications'))
->setIcon('fa-volume-off')
->setColor(PhabricatorActionView::RED);
}
$actions = $event->getValue('actions');
$actions[] = $sub_action;
$actions[] = $mute_action;
$event->setValue('actions', $actions);
}

View file

@ -0,0 +1,16 @@
<?php
final class PhabricatorMutedByEdgeType
extends PhabricatorEdgeType {
const EDGECONST = 68;
public function getInverseEdgeConstant() {
return PhabricatorMutedEdgeType::EDGECONST;
}
public function shouldWriteInverseTransactions() {
return true;
}
}

View file

@ -0,0 +1,16 @@
<?php
final class PhabricatorMutedEdgeType
extends PhabricatorEdgeType {
const EDGECONST = 67;
public function getInverseEdgeConstant() {
return PhabricatorMutedByEdgeType::EDGECONST;
}
public function shouldWriteInverseTransactions() {
return true;
}
}

View file

@ -78,6 +78,7 @@ abstract class PhabricatorApplicationTransactionEditor
private $oldCC = array();
private $mailRemovedPHIDs = array();
private $mailUnexpandablePHIDs = array();
private $mailMutedPHIDs = array();
private $transactionQueue = array();
@ -1211,6 +1212,14 @@ abstract class PhabricatorApplicationTransactionEditor
// but were removed by this change.
$this->applyOldRecipientLists();
if ($object instanceof PhabricatorSubscribableInterface) {
$this->mailMutedPHIDs = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorMutedByEdgeType::EDGECONST);
} else {
$this->mailMutedPHIDs = array();
}
$mail_xactions = $this->getTransactionsForMail($object, $xactions);
$stamps = $this->newMailStamps($object, $xactions);
foreach ($stamps as $stamp) {
@ -2662,6 +2671,11 @@ abstract class PhabricatorApplicationTransactionEditor
$mail_xactions);
}
$muted_phids = $this->mailMutedPHIDs;
if (!is_array($muted_phids)) {
$muted_phids = array();
}
$mail
->setSensitiveContent(false)
->setFrom($this->getActingAsPHID())
@ -2670,6 +2684,7 @@ abstract class PhabricatorApplicationTransactionEditor
->setThreadID($this->getMailThreadID($object), $this->getIsNewObject())
->setRelatedPHID($object->getPHID())
->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())
->setMutedPHIDs($muted_phids)
->setForceHeraldMailRecipientPHIDs($this->heraldForcedEmailPHIDs)
->setMailTags($mail_tags)
->setIsBulk(true)
@ -3186,6 +3201,18 @@ abstract class PhabricatorApplicationTransactionEditor
$related_phids = $this->feedRelatedPHIDs;
$subscribed_phids = $this->feedNotifyPHIDs;
// Remove muted users from the subscription list so they don't get
// notifications, either.
$muted_phids = $this->mailMutedPHIDs;
if (!is_array($muted_phids)) {
$muted_phids = array();
}
$subscribed_phids = array_fuse($subscribed_phids);
foreach ($muted_phids as $muted_phid) {
unset($subscribed_phids[$muted_phid]);
}
$subscribed_phids = array_values($subscribed_phids);
$story_type = $this->getFeedStoryType();
$story_data = $this->getFeedStoryData($object, $xactions);
@ -3632,6 +3659,7 @@ abstract class PhabricatorApplicationTransactionEditor
'mustEncrypt',
'mailStamps',
'mailUnexpandablePHIDs',
'mailMutedPHIDs',
);
}

View file

@ -643,6 +643,8 @@ abstract class PhabricatorApplicationTransaction
case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
case ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST:
case ManiphestTaskIsDuplicateOfTaskEdgeType::EDGECONST:
case PhabricatorMutedEdgeType::EDGECONST:
case PhabricatorMutedByEdgeType::EDGECONST:
return true;
break;
case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:

View file

@ -21,6 +21,7 @@ final class PhabricatorActionView extends AphrontView {
private $order;
private $color;
private $type;
private $highlight;
const TYPE_DIVIDER = 'type-divider';
const TYPE_LABEL = 'label';
@ -72,6 +73,15 @@ final class PhabricatorActionView extends AphrontView {
return $this->href;
}
public function setHighlight($highlight) {
$this->highlight = $highlight;
return $this;
}
public function getHighlight() {
return $this->highlight;
}
public function setIcon($icon) {
$this->icon = $icon;
return $this;

View file

@ -95,15 +95,20 @@
color: {$sky};
}
.device-desktop .phabricator-action-view-href.action-item-red:hover
.phabricator-action-view-item {
background-color: {$sh-redbackground};
color: {$sh-redtext};
.phabricator-action-view.action-item-red {
background-color: {$sh-redbackground};
}
.device-desktop .phabricator-action-view-href.action-item-red:hover
.phabricator-action-view.action-item-red .phabricator-action-view-item,
.phabricator-action-view.action-item-red .phabricator-action-view-icon {
color: {$sh-redtext};
}
.device-desktop .phabricator-action-view.action-item-red:hover
.phabricator-action-view-item,
.device-desktop .phabricator-action-view.action-item-red:hover
.phabricator-action-view-icon {
color: {$red};
color: {$red};
}
.phabricator-action-view-label .phabricator-action-view-item,