@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+254 -24
+3 -3
resources/celerity/map.php
··· 9 9 'names' => array( 10 10 'conpherence.pkg.css' => 'e68cf1fa', 11 11 'conpherence.pkg.js' => '15191c65', 12 - 'core.pkg.css' => '51debec3', 12 + 'core.pkg.css' => 'ce8c2a58', 13 13 'core.pkg.js' => '4c79d74f', 14 14 'darkconsole.pkg.js' => '1f9a31bc', 15 15 'differential.pkg.css' => '45951e9e', ··· 136 136 'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '9d9685d6', 137 137 'rsrc/css/phui/object-item/phui-oi-list-view.css' => '6ae18df0', 138 138 'rsrc/css/phui/object-item/phui-oi-simple-ui.css' => 'a8beebea', 139 - 'rsrc/css/phui/phui-action-list.css' => 'f7f61a34', 139 + 'rsrc/css/phui/phui-action-list.css' => '0bcd9a45', 140 140 'rsrc/css/phui/phui-action-panel.css' => 'b4798122', 141 141 'rsrc/css/phui/phui-badge.css' => '22c0cf4f', 142 142 'rsrc/css/phui/phui-basic-nav-view.css' => '98c11ab3', ··· 766 766 'path-typeahead' => 'f7fc67ec', 767 767 'people-picture-menu-item-css' => 'a06f7f34', 768 768 'people-profile-css' => '4df76faf', 769 - 'phabricator-action-list-view-css' => 'f7f61a34', 769 + 'phabricator-action-list-view-css' => '0bcd9a45', 770 770 'phabricator-busy' => '59a7976a', 771 771 'phabricator-chatlog-css' => 'd295b020', 772 772 'phabricator-content-source-view-css' => '4b8b05d4',
+6
src/__phutil_library_map__.php
··· 3291 3291 'PhabricatorMultiFactorSettingsPanel' => 'applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php', 3292 3292 'PhabricatorMultimeterApplication' => 'applications/multimeter/application/PhabricatorMultimeterApplication.php', 3293 3293 'PhabricatorMustVerifyEmailController' => 'applications/auth/controller/PhabricatorMustVerifyEmailController.php', 3294 + 'PhabricatorMutedByEdgeType' => 'applications/transactions/edges/PhabricatorMutedByEdgeType.php', 3295 + 'PhabricatorMutedEdgeType' => 'applications/transactions/edges/PhabricatorMutedEdgeType.php', 3294 3296 'PhabricatorMySQLConfigOptions' => 'applications/config/option/PhabricatorMySQLConfigOptions.php', 3295 3297 'PhabricatorMySQLFileStorageEngine' => 'applications/files/engine/PhabricatorMySQLFileStorageEngine.php', 3296 3298 'PhabricatorMySQLSearchHost' => 'infrastructure/cluster/search/PhabricatorMySQLSearchHost.php', ··· 4240 4242 'PhabricatorSubscriptionsHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsHeraldAction.php', 4241 4243 'PhabricatorSubscriptionsListController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsListController.php', 4242 4244 'PhabricatorSubscriptionsMailEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsMailEngineExtension.php', 4245 + 'PhabricatorSubscriptionsMuteController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php', 4243 4246 'PhabricatorSubscriptionsRemoveSelfHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsRemoveSelfHeraldAction.php', 4244 4247 'PhabricatorSubscriptionsRemoveSubscribersHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsRemoveSubscribersHeraldAction.php', 4245 4248 'PhabricatorSubscriptionsSearchEngineAttachment' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsSearchEngineAttachment.php', ··· 8808 8811 'PhabricatorMultiFactorSettingsPanel' => 'PhabricatorSettingsPanel', 8809 8812 'PhabricatorMultimeterApplication' => 'PhabricatorApplication', 8810 8813 'PhabricatorMustVerifyEmailController' => 'PhabricatorAuthController', 8814 + 'PhabricatorMutedByEdgeType' => 'PhabricatorEdgeType', 8815 + 'PhabricatorMutedEdgeType' => 'PhabricatorEdgeType', 8811 8816 'PhabricatorMySQLConfigOptions' => 'PhabricatorApplicationConfigOptions', 8812 8817 'PhabricatorMySQLFileStorageEngine' => 'PhabricatorFileStorageEngine', 8813 8818 'PhabricatorMySQLSearchHost' => 'PhabricatorSearchHost', ··· 9960 9965 'PhabricatorSubscriptionsHeraldAction' => 'HeraldAction', 9961 9966 'PhabricatorSubscriptionsListController' => 'PhabricatorController', 9962 9967 'PhabricatorSubscriptionsMailEngineExtension' => 'PhabricatorMailEngineExtension', 9968 + 'PhabricatorSubscriptionsMuteController' => 'PhabricatorController', 9963 9969 'PhabricatorSubscriptionsRemoveSelfHeraldAction' => 'PhabricatorSubscriptionsHeraldAction', 9964 9970 'PhabricatorSubscriptionsRemoveSubscribersHeraldAction' => 'PhabricatorSubscriptionsHeraldAction', 9965 9971 'PhabricatorSubscriptionsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
+4
src/applications/metamta/query/PhabricatorMetaMTAActor.php
··· 21 21 const REASON_ROUTE_AS_NOTIFICATION = 'route-as-notification'; 22 22 const REASON_ROUTE_AS_MAIL = 'route-as-mail'; 23 23 const REASON_UNVERIFIED = 'unverified'; 24 + const REASON_MUTED = 'muted'; 24 25 25 26 private $phid; 26 27 private $emailAddress; ··· 116 117 self::REASON_ROUTE_AS_NOTIFICATION => pht('Route as Notification'), 117 118 self::REASON_ROUTE_AS_MAIL => pht('Route as Mail'), 118 119 self::REASON_UNVERIFIED => pht('Address Not Verified'), 120 + self::REASON_MUTED => pht('Muted'), 119 121 ); 120 122 121 123 return idx($names, $reason, pht('Unknown ("%s")', $reason)); ··· 172 174 'in Herald.'), 173 175 self::REASON_UNVERIFIED => pht( 174 176 'This recipient does not have a verified primary email address.'), 177 + self::REASON_MUTED => pht( 178 + 'This recipient has muted notifications for this object.'), 175 179 ); 176 180 177 181 return idx($descriptions, $reason, pht('Unknown Reason ("%s")', $reason));
+21
src/applications/metamta/storage/PhabricatorMetaMTAMail.php
··· 160 160 return $this->getParam('exclude', array()); 161 161 } 162 162 163 + public function setMutedPHIDs(array $muted) { 164 + $this->setParam('muted', $muted); 165 + return $this; 166 + } 167 + 168 + private function getMutedPHIDs() { 169 + return $this->getParam('muted', array()); 170 + } 171 + 163 172 public function setForceHeraldMailRecipientPHIDs(array $force) { 164 173 $this->setParam('herald-force-recipients', $force); 165 174 return $this; ··· 1111 1120 if ($actor->isDeliverable()) { 1112 1121 $deliverable[] = $phid; 1113 1122 } 1123 + } 1124 + 1125 + // Exclude muted recipients. We're doing this after saving deliverability 1126 + // so that Herald "Send me an email" actions can still punch through a 1127 + // mute. 1128 + 1129 + foreach ($this->getMutedPHIDs() as $muted_phid) { 1130 + $muted_actor = idx($actors, $muted_phid); 1131 + if (!$muted_actor) { 1132 + continue; 1133 + } 1134 + $muted_actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_MUTED); 1114 1135 } 1115 1136 1116 1137 // For the rest of the rules, order matters. We're going to run all the
+4 -1
src/applications/subscriptions/application/PhabricatorSubscriptionsApplication.php
··· 24 24 return array( 25 25 '/subscriptions/' => array( 26 26 '(?P<action>add|delete)/'. 27 - '(?P<phid>[^/]+)/' => 'PhabricatorSubscriptionsEditController', 27 + '(?P<phid>[^/]+)/' => 'PhabricatorSubscriptionsEditController', 28 + 'mute/' => array( 29 + '(?P<phid>[^/]+)/' => 'PhabricatorSubscriptionsMuteController', 30 + ), 28 31 'list/(?P<phid>[^/]+)/' => 'PhabricatorSubscriptionsListController', 29 32 'transaction/(?P<type>add|rem)/(?<phid>[^/]+)/' 30 33 => 'PhabricatorSubscriptionsTransactionController',
+92
src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php
··· 1 + <?php 2 + 3 + final class PhabricatorSubscriptionsMuteController 4 + extends PhabricatorController { 5 + 6 + public function handleRequest(AphrontRequest $request) { 7 + $viewer = $request->getViewer(); 8 + $phid = $request->getURIData('phid'); 9 + 10 + $handle = id(new PhabricatorHandleQuery()) 11 + ->setViewer($viewer) 12 + ->withPHIDs(array($phid)) 13 + ->executeOne(); 14 + 15 + $object = id(new PhabricatorObjectQuery()) 16 + ->setViewer($viewer) 17 + ->withPHIDs(array($phid)) 18 + ->executeOne(); 19 + 20 + if (!($object instanceof PhabricatorSubscribableInterface)) { 21 + return new Aphront400Response(); 22 + } 23 + 24 + $muted_type = PhabricatorMutedByEdgeType::EDGECONST; 25 + 26 + $edge_query = id(new PhabricatorEdgeQuery()) 27 + ->withSourcePHIDs(array($object->getPHID())) 28 + ->withEdgeTypes(array($muted_type)) 29 + ->withDestinationPHIDs(array($viewer->getPHID())); 30 + 31 + $edge_query->execute(); 32 + 33 + $is_mute = !$edge_query->getDestinationPHIDs(); 34 + $object_uri = $handle->getURI(); 35 + 36 + if ($request->isFormPost()) { 37 + if ($is_mute) { 38 + $xaction_value = array( 39 + '+' => array_fuse(array($viewer->getPHID())), 40 + ); 41 + } else { 42 + $xaction_value = array( 43 + '-' => array_fuse(array($viewer->getPHID())), 44 + ); 45 + } 46 + 47 + $muted_type = PhabricatorMutedByEdgeType::EDGECONST; 48 + 49 + $xaction = id($object->getApplicationTransactionTemplate()) 50 + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) 51 + ->setMetadataValue('edge:type', $muted_type) 52 + ->setNewValue($xaction_value); 53 + 54 + $editor = id($object->getApplicationTransactionEditor()) 55 + ->setActor($viewer) 56 + ->setContinueOnNoEffect(true) 57 + ->setContinueOnMissingFields(true) 58 + ->setContentSourceFromRequest($request); 59 + 60 + $editor->applyTransactions( 61 + $object->getApplicationTransactionObject(), 62 + array($xaction)); 63 + 64 + return id(new AphrontReloadResponse())->setURI($object_uri); 65 + } 66 + 67 + $dialog = $this->newDialog() 68 + ->addCancelButton($object_uri); 69 + 70 + if ($is_mute) { 71 + $dialog 72 + ->setTitle(pht('Mute Notifications')) 73 + ->appendParagraph( 74 + pht( 75 + 'Mute this object? You will no longer receive notifications or '. 76 + 'email about it.')) 77 + ->addSubmitButton(pht('Mute')); 78 + } else { 79 + $dialog 80 + ->setTitle(pht('Unmute Notifications')) 81 + ->appendParagraph( 82 + pht( 83 + 'Unmute this object? You will receive notifications and email '. 84 + 'again.')) 85 + ->addSubmitButton(pht('Unmute')); 86 + } 87 + 88 + return $dialog; 89 + } 90 + 91 + 92 + }
+41 -14
src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php
··· 42 42 return; 43 43 } 44 44 45 + $src_phid = $object->getPHID(); 46 + $subscribed_type = PhabricatorObjectHasSubscriberEdgeType::EDGECONST; 47 + $muted_type = PhabricatorMutedByEdgeType::EDGECONST; 48 + 49 + $edges = id(new PhabricatorEdgeQuery()) 50 + ->withSourcePHIDs(array($src_phid)) 51 + ->withEdgeTypes( 52 + array( 53 + $subscribed_type, 54 + $muted_type, 55 + )) 56 + ->withDestinationPHIDs(array($user_phid)) 57 + ->execute(); 58 + 59 + if ($user_phid) { 60 + $is_subscribed = isset($edges[$src_phid][$subscribed_type][$user_phid]); 61 + $is_muted = isset($edges[$src_phid][$muted_type][$user_phid]); 62 + } else { 63 + $is_subscribed = false; 64 + $is_muted = false; 65 + } 66 + 45 67 if ($user_phid && $object->isAutomaticallySubscribed($user_phid)) { 46 68 $sub_action = id(new PhabricatorActionView()) 47 69 ->setWorkflow(true) ··· 51 73 ->setName(pht('Automatically Subscribed')) 52 74 ->setIcon('fa-check-circle lightgreytext'); 53 75 } else { 54 - $subscribed = false; 55 - if ($user->isLoggedIn()) { 56 - $src_phid = $object->getPHID(); 57 - $edge_type = PhabricatorObjectHasSubscriberEdgeType::EDGECONST; 58 - 59 - $edges = id(new PhabricatorEdgeQuery()) 60 - ->withSourcePHIDs(array($src_phid)) 61 - ->withEdgeTypes(array($edge_type)) 62 - ->withDestinationPHIDs(array($user_phid)) 63 - ->execute(); 64 - $subscribed = isset($edges[$src_phid][$edge_type][$user_phid]); 65 - } 66 - 67 76 $can_interact = PhabricatorPolicyFilter::canInteract($user, $object); 68 77 69 - if ($subscribed) { 78 + if ($is_subscribed) { 70 79 $sub_action = id(new PhabricatorActionView()) 71 80 ->setWorkflow(true) 72 81 ->setRenderAsForm(true) ··· 89 98 } 90 99 } 91 100 101 + $mute_action = id(new PhabricatorActionView()) 102 + ->setWorkflow(true) 103 + ->setHref('/subscriptions/mute/'.$object->getPHID().'/') 104 + ->setDisabled(!$user_phid); 105 + 106 + if (!$is_muted) { 107 + $mute_action 108 + ->setName(pht('Mute Notifications')) 109 + ->setIcon('fa-volume-up'); 110 + } else { 111 + $mute_action 112 + ->setName(pht('Unmute Notifications')) 113 + ->setIcon('fa-volume-off') 114 + ->setColor(PhabricatorActionView::RED); 115 + } 116 + 117 + 92 118 $actions = $event->getValue('actions'); 93 119 $actions[] = $sub_action; 120 + $actions[] = $mute_action; 94 121 $event->setValue('actions', $actions); 95 122 } 96 123
+16
src/applications/transactions/edges/PhabricatorMutedByEdgeType.php
··· 1 + <?php 2 + 3 + final class PhabricatorMutedByEdgeType 4 + extends PhabricatorEdgeType { 5 + 6 + const EDGECONST = 68; 7 + 8 + public function getInverseEdgeConstant() { 9 + return PhabricatorMutedEdgeType::EDGECONST; 10 + } 11 + 12 + public function shouldWriteInverseTransactions() { 13 + return true; 14 + } 15 + 16 + }
+16
src/applications/transactions/edges/PhabricatorMutedEdgeType.php
··· 1 + <?php 2 + 3 + final class PhabricatorMutedEdgeType 4 + extends PhabricatorEdgeType { 5 + 6 + const EDGECONST = 67; 7 + 8 + public function getInverseEdgeConstant() { 9 + return PhabricatorMutedByEdgeType::EDGECONST; 10 + } 11 + 12 + public function shouldWriteInverseTransactions() { 13 + return true; 14 + } 15 + 16 + }
+28
src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
··· 78 78 private $oldCC = array(); 79 79 private $mailRemovedPHIDs = array(); 80 80 private $mailUnexpandablePHIDs = array(); 81 + private $mailMutedPHIDs = array(); 81 82 82 83 private $transactionQueue = array(); 83 84 ··· 1210 1211 // Add any recipients who were previously on the notification list 1211 1212 // but were removed by this change. 1212 1213 $this->applyOldRecipientLists(); 1214 + 1215 + if ($object instanceof PhabricatorSubscribableInterface) { 1216 + $this->mailMutedPHIDs = PhabricatorEdgeQuery::loadDestinationPHIDs( 1217 + $object->getPHID(), 1218 + PhabricatorMutedByEdgeType::EDGECONST); 1219 + } else { 1220 + $this->mailMutedPHIDs = array(); 1221 + } 1213 1222 1214 1223 $mail_xactions = $this->getTransactionsForMail($object, $xactions); 1215 1224 $stamps = $this->newMailStamps($object, $xactions); ··· 2660 2669 $body, 2661 2670 $object, 2662 2671 $mail_xactions); 2672 + } 2673 + 2674 + $muted_phids = $this->mailMutedPHIDs; 2675 + if (!is_array($muted_phids)) { 2676 + $muted_phids = array(); 2663 2677 } 2664 2678 2665 2679 $mail ··· 2670 2684 ->setThreadID($this->getMailThreadID($object), $this->getIsNewObject()) 2671 2685 ->setRelatedPHID($object->getPHID()) 2672 2686 ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs()) 2687 + ->setMutedPHIDs($muted_phids) 2673 2688 ->setForceHeraldMailRecipientPHIDs($this->heraldForcedEmailPHIDs) 2674 2689 ->setMailTags($mail_tags) 2675 2690 ->setIsBulk(true) ··· 3186 3201 $related_phids = $this->feedRelatedPHIDs; 3187 3202 $subscribed_phids = $this->feedNotifyPHIDs; 3188 3203 3204 + // Remove muted users from the subscription list so they don't get 3205 + // notifications, either. 3206 + $muted_phids = $this->mailMutedPHIDs; 3207 + if (!is_array($muted_phids)) { 3208 + $muted_phids = array(); 3209 + } 3210 + $subscribed_phids = array_fuse($subscribed_phids); 3211 + foreach ($muted_phids as $muted_phid) { 3212 + unset($subscribed_phids[$muted_phid]); 3213 + } 3214 + $subscribed_phids = array_values($subscribed_phids); 3215 + 3189 3216 $story_type = $this->getFeedStoryType(); 3190 3217 $story_data = $this->getFeedStoryData($object, $xactions); 3191 3218 ··· 3632 3659 'mustEncrypt', 3633 3660 'mailStamps', 3634 3661 'mailUnexpandablePHIDs', 3662 + 'mailMutedPHIDs', 3635 3663 ); 3636 3664 } 3637 3665
+2
src/applications/transactions/storage/PhabricatorApplicationTransaction.php
··· 643 643 case PhabricatorObjectMentionsObjectEdgeType::EDGECONST: 644 644 case ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST: 645 645 case ManiphestTaskIsDuplicateOfTaskEdgeType::EDGECONST: 646 + case PhabricatorMutedEdgeType::EDGECONST: 647 + case PhabricatorMutedByEdgeType::EDGECONST: 646 648 return true; 647 649 break; 648 650 case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
+10
src/view/layout/PhabricatorActionView.php
··· 21 21 private $order; 22 22 private $color; 23 23 private $type; 24 + private $highlight; 24 25 25 26 const TYPE_DIVIDER = 'type-divider'; 26 27 const TYPE_LABEL = 'label'; ··· 70 71 71 72 public function getHref() { 72 73 return $this->href; 74 + } 75 + 76 + public function setHighlight($highlight) { 77 + $this->highlight = $highlight; 78 + return $this; 79 + } 80 + 81 + public function getHighlight() { 82 + return $this->highlight; 73 83 } 74 84 75 85 public function setIcon($icon) {
+11 -6
webroot/rsrc/css/phui/phui-action-list.css
··· 95 95 color: {$sky}; 96 96 } 97 97 98 - .device-desktop .phabricator-action-view-href.action-item-red:hover 99 - .phabricator-action-view-item { 100 - background-color: {$sh-redbackground}; 101 - color: {$sh-redtext}; 98 + .phabricator-action-view.action-item-red { 99 + background-color: {$sh-redbackground}; 100 + } 101 + 102 + .phabricator-action-view.action-item-red .phabricator-action-view-item, 103 + .phabricator-action-view.action-item-red .phabricator-action-view-icon { 104 + color: {$sh-redtext}; 102 105 } 103 106 104 - .device-desktop .phabricator-action-view-href.action-item-red:hover 107 + .device-desktop .phabricator-action-view.action-item-red:hover 108 + .phabricator-action-view-item, 109 + .device-desktop .phabricator-action-view.action-item-red:hover 105 110 .phabricator-action-view-icon { 106 - color: {$red}; 111 + color: {$red}; 107 112 } 108 113 109 114 .phabricator-action-view-label .phabricator-action-view-item,