diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ '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 @@ '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 @@ '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', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3291,6 +3291,8 @@ '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 @@ '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 @@ 'PhabricatorMultiFactorSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorMultimeterApplication' => 'PhabricatorApplication', 'PhabricatorMustVerifyEmailController' => 'PhabricatorAuthController', + 'PhabricatorMutedByEdgeType' => 'PhabricatorEdgeType', + 'PhabricatorMutedEdgeType' => 'PhabricatorEdgeType', 'PhabricatorMySQLConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorMySQLFileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorMySQLSearchHost' => 'PhabricatorSearchHost', @@ -9960,6 +9965,7 @@ 'PhabricatorSubscriptionsHeraldAction' => 'HeraldAction', 'PhabricatorSubscriptionsListController' => 'PhabricatorController', 'PhabricatorSubscriptionsMailEngineExtension' => 'PhabricatorMailEngineExtension', + 'PhabricatorSubscriptionsMuteController' => 'PhabricatorController', 'PhabricatorSubscriptionsRemoveSelfHeraldAction' => 'PhabricatorSubscriptionsHeraldAction', 'PhabricatorSubscriptionsRemoveSubscribersHeraldAction' => 'PhabricatorSubscriptionsHeraldAction', 'PhabricatorSubscriptionsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', diff --git a/src/applications/metamta/query/PhabricatorMetaMTAActor.php b/src/applications/metamta/query/PhabricatorMetaMTAActor.php --- a/src/applications/metamta/query/PhabricatorMetaMTAActor.php +++ b/src/applications/metamta/query/PhabricatorMetaMTAActor.php @@ -21,6 +21,7 @@ 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 @@ 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 @@ '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)); diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php --- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php @@ -160,6 +160,15 @@ 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 @@ } } + // 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 diff --git a/src/applications/subscriptions/application/PhabricatorSubscriptionsApplication.php b/src/applications/subscriptions/application/PhabricatorSubscriptionsApplication.php --- a/src/applications/subscriptions/application/PhabricatorSubscriptionsApplication.php +++ b/src/applications/subscriptions/application/PhabricatorSubscriptionsApplication.php @@ -24,7 +24,10 @@ return array( '/subscriptions/' => array( '(?Padd|delete)/'. - '(?P[^/]+)/' => 'PhabricatorSubscriptionsEditController', + '(?P[^/]+)/' => 'PhabricatorSubscriptionsEditController', + 'mute/' => array( + '(?P[^/]+)/' => 'PhabricatorSubscriptionsMuteController', + ), 'list/(?P[^/]+)/' => 'PhabricatorSubscriptionsListController', 'transaction/(?Padd|rem)/(?[^/]+)/' => 'PhabricatorSubscriptionsTransactionController', diff --git a/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php b/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php new file mode 100644 --- /dev/null +++ b/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php @@ -0,0 +1,92 @@ +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; + } + + +} diff --git a/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php b/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php --- a/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php +++ b/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php @@ -42,6 +42,28 @@ 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 @@ ->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 @@ } } + $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); } diff --git a/src/applications/transactions/edges/PhabricatorMutedByEdgeType.php b/src/applications/transactions/edges/PhabricatorMutedByEdgeType.php new file mode 100644 --- /dev/null +++ b/src/applications/transactions/edges/PhabricatorMutedByEdgeType.php @@ -0,0 +1,16 @@ +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 @@ $mail_xactions); } + $muted_phids = $this->mailMutedPHIDs; + if (!is_array($muted_phids)) { + $muted_phids = array(); + } + $mail ->setSensitiveContent(false) ->setFrom($this->getActingAsPHID()) @@ -2670,6 +2684,7 @@ ->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 @@ $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 @@ 'mustEncrypt', 'mailStamps', 'mailUnexpandablePHIDs', + 'mailMutedPHIDs', ); } diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -643,6 +643,8 @@ case PhabricatorObjectMentionsObjectEdgeType::EDGECONST: case ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST: case ManiphestTaskIsDuplicateOfTaskEdgeType::EDGECONST: + case PhabricatorMutedEdgeType::EDGECONST: + case PhabricatorMutedByEdgeType::EDGECONST: return true; break; case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST: diff --git a/src/view/layout/PhabricatorActionView.php b/src/view/layout/PhabricatorActionView.php --- a/src/view/layout/PhabricatorActionView.php +++ b/src/view/layout/PhabricatorActionView.php @@ -21,6 +21,7 @@ private $order; private $color; private $type; + private $highlight; const TYPE_DIVIDER = 'type-divider'; const TYPE_LABEL = 'label'; @@ -72,6 +73,15 @@ 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; diff --git a/webroot/rsrc/css/phui/phui-action-list.css b/webroot/rsrc/css/phui/phui-action-list.css --- a/webroot/rsrc/css/phui/phui-action-list.css +++ b/webroot/rsrc/css/phui/phui-action-list.css @@ -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}; +} + +.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-href.action-item-red:hover +.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,