@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.

Replace subscribe/unsubscribe for projects with explicit mail setting

Summary:
Ref T10054. Ref T6113. Users can currently subscribe to projects, which causes them to receive:

# mail about project membership changes, description changes, etc; and
# mail to the project, e.g. when the project is added as a subscriber on a task, or a reviewer on a revision.

Almost no one cares about (1), and after D15061 you can use Herald to get this stuff if you really want it. (It will get progressively more annoying in the future with external membership sources causing automated project membership updates.)

A lot of users are confused about (2) and how it relates to membership, watching, etc, and most users who want (2) don't want (1).

Instead, add an explicit option for this and explain what it does.

This is fairly verbose but I've hidden it on the member/watch screen, which is now the "explain how projects work" screen, I guess.

Test Plan:
{F1064929}

{F1064930}

{F1064931}

- Disabled/enabled mail for a project.
- Sent mail to a project with mail disabled, verified I didn't get a copy.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T6113, T10054

Differential Revision: https://secure.phabricator.com/D15065

+225 -76
+8
resources/sql/autopatches/20160119.project.1.silence.sql
··· 1 + /* PhabricatorObjectHasUnsubscriberEdgeType::EDGECONST = 23 */ 2 + /* PhabricatorProjectSilencedEdgeType::EDGECONST = 61 */ 3 + 4 + /* This is converting existing unsubscribes into disabled mail. */ 5 + 6 + INSERT IGNORE INTO {$NAMESPACE}_project.edge (src, type, dst, dateCreated) 7 + SELECT src, 61, dst, dateCreated FROM {$NAMESPACE}_project.edge 8 + WHERE type = 23;
+4 -1
src/__phutil_library_map__.php
··· 2925 2925 'PhabricatorProjectSchemaSpec' => 'applications/project/storage/PhabricatorProjectSchemaSpec.php', 2926 2926 'PhabricatorProjectSearchEngine' => 'applications/project/query/PhabricatorProjectSearchEngine.php', 2927 2927 'PhabricatorProjectSearchField' => 'applications/project/searchfield/PhabricatorProjectSearchField.php', 2928 + 'PhabricatorProjectSilenceController' => 'applications/project/controller/PhabricatorProjectSilenceController.php', 2929 + 'PhabricatorProjectSilencedEdgeType' => 'applications/project/edge/PhabricatorProjectSilencedEdgeType.php', 2928 2930 'PhabricatorProjectSlug' => 'applications/project/storage/PhabricatorProjectSlug.php', 2929 2931 'PhabricatorProjectStandardCustomField' => 'applications/project/customfield/PhabricatorProjectStandardCustomField.php', 2930 2932 'PhabricatorProjectStatus' => 'applications/project/constants/PhabricatorProjectStatus.php', ··· 7237 7239 'PhabricatorFlaggableInterface', 7238 7240 'PhabricatorPolicyInterface', 7239 7241 'PhabricatorExtendedPolicyInterface', 7240 - 'PhabricatorSubscribableInterface', 7241 7242 'PhabricatorCustomFieldInterface', 7242 7243 'PhabricatorDestructibleInterface', 7243 7244 'PhabricatorFulltextInterface', ··· 7329 7330 'PhabricatorProjectSchemaSpec' => 'PhabricatorConfigSchemaSpec', 7330 7331 'PhabricatorProjectSearchEngine' => 'PhabricatorApplicationSearchEngine', 7331 7332 'PhabricatorProjectSearchField' => 'PhabricatorSearchTokenizerField', 7333 + 'PhabricatorProjectSilenceController' => 'PhabricatorProjectController', 7334 + 'PhabricatorProjectSilencedEdgeType' => 'PhabricatorEdgeType', 7332 7335 'PhabricatorProjectSlug' => 'PhabricatorProjectDAO', 7333 7336 'PhabricatorProjectStandardCustomField' => array( 7334 7337 'PhabricatorProjectCustomField',
+33 -5
src/applications/metamta/query/PhabricatorMetaMTAMemberQuery.php
··· 42 42 $projects = id(new PhabricatorProjectQuery()) 43 43 ->setViewer($this->getViewer()) 44 44 ->withPHIDs($phids) 45 + ->needMembers(true) 46 + ->needWatchers(true) 45 47 ->execute(); 46 48 47 - $subscribers = id(new PhabricatorSubscribersQuery()) 48 - ->withObjectPHIDs($phids) 49 - ->execute(); 49 + $edge_type = PhabricatorProjectSilencedEdgeType::EDGECONST; 50 + 51 + $edge_query = id(new PhabricatorEdgeQuery()) 52 + ->withSourcePHIDs($phids) 53 + ->withEdgeTypes( 54 + array( 55 + $edge_type, 56 + )); 57 + 58 + $edge_query->execute(); 50 59 51 60 $projects = mpull($projects, null, 'getPHID'); 52 61 foreach ($phids as $phid) { 53 62 $project = idx($projects, $phid); 63 + 54 64 if (!$project) { 55 65 $results[$phid] = array(); 56 - } else { 57 - $results[$phid] = idx($subscribers, $phid, array()); 66 + continue; 58 67 } 68 + 69 + // Recipients are members who haven't silenced the project, plus 70 + // watchers. 71 + 72 + $members = $project->getMemberPHIDs(); 73 + $members = array_fuse($members); 74 + 75 + $watchers = $project->getWatcherPHIDs(); 76 + $watchers = array_fuse($watchers); 77 + 78 + $silenced = $edge_query->getDestinationPHIDs( 79 + array($phid), 80 + array($edge_type)); 81 + $silenced = array_fuse($silenced); 82 + 83 + $result_map = array_diff_key($members, $silenced); 84 + $result_map = $result_map + $watchers; 85 + 86 + $results[$phid] = array_values($result_map); 59 87 } 60 88 break; 61 89 default:
+2
src/applications/project/application/PhabricatorProjectApplication.php
··· 89 89 'history/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectHistoryController', 90 90 '(?P<action>watch|unwatch)/(?P<id>[1-9]\d*)/' 91 91 => 'PhabricatorProjectWatchController', 92 + 'silence/(?P<id>[1-9]\d*)/' 93 + => 'PhabricatorProjectSilenceController', 92 94 ), 93 95 '/tag/' => array( 94 96 '(?P<slug>[^/]+)/' => 'PhabricatorProjectViewController',
+79 -1
src/applications/project/controller/PhabricatorProjectMembersViewController.php
··· 110 110 $descriptions[PhabricatorPolicyCapability::CAN_JOIN]); 111 111 } 112 112 113 + $viewer_phid = $viewer->getPHID(); 114 + 115 + if ($project->isUserWatcher($viewer_phid)) { 116 + $watch_item = id(new PHUIStatusItemView()) 117 + ->setIcon('fa-eye green') 118 + ->setTarget(phutil_tag('strong', array(), pht('Watching'))) 119 + ->setNote( 120 + pht( 121 + 'You will receive mail about changes made to any related '. 122 + 'object.')); 123 + 124 + $watch_status = id(new PHUIStatusListView()) 125 + ->addItem($watch_item); 126 + 127 + $view->addProperty(pht('Watching'), $watch_status); 128 + } 129 + 130 + if ($project->isUserMember($viewer_phid)) { 131 + $is_silenced = $this->isProjectSilenced($project); 132 + if ($is_silenced) { 133 + $mail_icon = 'fa-envelope-o grey'; 134 + $mail_target = pht('Disabled'); 135 + $mail_note = pht( 136 + 'When mail is sent to project members, you will not receive '. 137 + 'a copy.'); 138 + } else { 139 + $mail_icon = 'fa-envelope-o green'; 140 + $mail_target = pht('Enabled'); 141 + $mail_note = pht( 142 + 'You will receive mail that is sent to project members.'); 143 + } 144 + 145 + $mail_item = id(new PHUIStatusItemView()) 146 + ->setIcon($mail_icon) 147 + ->setTarget(phutil_tag('strong', array(), $mail_target)) 148 + ->setNote($mail_note); 149 + 150 + $mail_status = id(new PHUIStatusListView()) 151 + ->addItem($mail_item); 152 + 153 + $view->addProperty(pht('Mail to Members'), $mail_status); 154 + } 155 + 113 156 return $view; 114 157 } 115 158 ··· 136 179 137 180 $can_leave = $supports_edit && (!$is_locked || $can_edit); 138 181 139 - if (!$project->isUserMember($viewer->getPHID())) { 182 + $viewer_phid = $viewer->getPHID(); 183 + 184 + if (!$project->isUserMember($viewer_phid)) { 140 185 $view->addAction( 141 186 id(new PhabricatorActionView()) 142 187 ->setHref('/project/update/'.$project->getID().'/join/') ··· 170 215 ->setName(pht('Unwatch Project'))); 171 216 } 172 217 218 + $can_silence = $project->isUserMember($viewer_phid); 219 + $is_silenced = $this->isProjectSilenced($project); 220 + 221 + if ($is_silenced) { 222 + $silence_text = pht('Enable Mail'); 223 + } else { 224 + $silence_text = pht('Disable Mail'); 225 + } 226 + 227 + $view->addAction( 228 + id(new PhabricatorActionView()) 229 + ->setName($silence_text) 230 + ->setIcon('fa-envelope-o') 231 + ->setHref("/project/silence/{$id}/") 232 + ->setWorkflow(true) 233 + ->setDisabled(!$can_silence)); 234 + 173 235 $can_add = $can_edit && $supports_edit; 174 236 175 237 $view->addAction( ··· 200 262 ->setWorkflow(true)); 201 263 202 264 return $view; 265 + } 266 + 267 + private function isProjectSilenced(PhabricatorProject $project) { 268 + $viewer = $this->getViewer(); 269 + 270 + $viewer_phid = $viewer->getPHID(); 271 + if (!$viewer_phid) { 272 + return false; 273 + } 274 + 275 + $edge_type = PhabricatorProjectSilencedEdgeType::EDGECONST; 276 + $silenced = PhabricatorEdgeQuery::loadDestinationPHIDs( 277 + $project->getPHID(), 278 + $edge_type); 279 + $silenced = array_fuse($silenced); 280 + return isset($silenced[$viewer_phid]); 203 281 } 204 282 205 283 }
+87
src/applications/project/controller/PhabricatorProjectSilenceController.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectSilenceController 4 + extends PhabricatorProjectController { 5 + 6 + public function handleRequest(AphrontRequest $request) { 7 + $viewer = $request->getViewer(); 8 + $id = $request->getURIData('id'); 9 + $action = $request->getURIData('action'); 10 + 11 + $project = id(new PhabricatorProjectQuery()) 12 + ->setViewer($viewer) 13 + ->withIDs(array($id)) 14 + ->needMembers(true) 15 + ->executeOne(); 16 + if (!$project) { 17 + return new Aphront404Response(); 18 + } 19 + 20 + $edge_type = PhabricatorProjectSilencedEdgeType::EDGECONST; 21 + $done_uri = "/project/members/{$id}/"; 22 + $viewer_phid = $viewer->getPHID(); 23 + 24 + if (!$project->isUserMember($viewer_phid)) { 25 + return $this->newDialog() 26 + ->setTitle(pht('Not a Member')) 27 + ->appendParagraph( 28 + pht( 29 + 'You are not a project member, so you do not receive mail sent '. 30 + 'to members of this project.')) 31 + ->addCancelButton($done_uri); 32 + } 33 + 34 + $silenced = PhabricatorEdgeQuery::loadDestinationPHIDs( 35 + $project->getPHID(), 36 + $edge_type); 37 + $silenced = array_fuse($silenced); 38 + $is_silenced = isset($silenced[$viewer_phid]); 39 + 40 + if ($request->isDialogFormPost()) { 41 + if ($is_silenced) { 42 + $edge_action = '-'; 43 + } else { 44 + $edge_action = '+'; 45 + } 46 + 47 + $xactions = array(); 48 + $xactions[] = id(new PhabricatorProjectTransaction()) 49 + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) 50 + ->setMetadataValue('edge:type', $edge_type) 51 + ->setNewValue( 52 + array( 53 + $edge_action => array($viewer_phid => $viewer_phid), 54 + )); 55 + 56 + $editor = id(new PhabricatorProjectTransactionEditor($project)) 57 + ->setActor($viewer) 58 + ->setContentSourceFromRequest($request) 59 + ->setContinueOnNoEffect(true) 60 + ->setContinueOnMissingFields(true) 61 + ->applyTransactions($project, $xactions); 62 + 63 + return id(new AphrontRedirectResponse())->setURI($done_uri); 64 + } 65 + 66 + if ($is_silenced) { 67 + $title = pht('Enable Mail'); 68 + $body = pht( 69 + 'When mail is sent to members of this project, you will receive a '. 70 + 'copy.'); 71 + $button = pht('Enable Project Mail'); 72 + } else { 73 + $title = pht('Disable Mail'); 74 + $body = pht( 75 + 'When mail is sent to members of this project, you will no longer '. 76 + 'receive a copy.'); 77 + $button = pht('Disable Project Mail'); 78 + } 79 + 80 + return $this->newDialog() 81 + ->setTitle($title) 82 + ->appendParagraph($body) 83 + ->addCancelButton($done_uri) 84 + ->addSubmitButton($button); 85 + } 86 + 87 + }
-2
src/applications/project/controller/PhabricatorProjectWatchController.php
··· 30 30 switch ($action) { 31 31 case 'watch': 32 32 $edge_action = '+'; 33 - $force_subscribe = true; 34 33 break; 35 34 case 'unwatch': 36 35 $edge_action = '-'; 37 - $force_subscribe = false; 38 36 break; 39 37 } 40 38
+8
src/applications/project/edge/PhabricatorProjectSilencedEdgeType.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectSilencedEdgeType 4 + extends PhabricatorEdgeType { 5 + 6 + const EDGECONST = 61; 7 + 8 + }
+4 -47
src/applications/project/editor/PhabricatorProjectTransactionEditor.php
··· 178 178 return parent::applyCustomExternalTransaction($object, $xaction); 179 179 } 180 180 181 - protected function applyBuiltinExternalTransaction( 182 - PhabricatorLiskDAO $object, 183 - PhabricatorApplicationTransaction $xaction) { 184 - 185 - switch ($xaction->getTransactionType()) { 186 - case PhabricatorTransactions::TYPE_EDGE: 187 - $edge_type = $xaction->getMetadataValue('edge:type'); 188 - switch ($edge_type) { 189 - case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST: 190 - case PhabricatorObjectHasWatcherEdgeType::EDGECONST: 191 - $edge_const = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; 192 - if ($edge_type != $edge_const) { 193 - break; 194 - } 195 - 196 - $old = $xaction->getOldValue(); 197 - $new = $xaction->getNewValue(); 198 - 199 - // When adding members, we add subscriptions. When removing 200 - // members, we remove subscriptions. 201 - $add = array_keys(array_diff_key($new, $old)); 202 - $rem = array_keys(array_diff_key($old, $new)); 203 - 204 - // NOTE: The subscribe is "explicit" because there's no implicit 205 - // unsubscribe, so Join -> Leave -> Join doesn't resubscribe you 206 - // if we use an implicit subscribe, even though you never willfully 207 - // unsubscribed. Not sure if adding implicit unsubscribe (which 208 - // would not write the unsubscribe row) is justified to deal with 209 - // this, which is a fairly weird edge case and pretty arguable both 210 - // ways. 211 - 212 - id(new PhabricatorSubscriptionsEditor()) 213 - ->setActor($this->requireActor()) 214 - ->setObject($object) 215 - ->subscribeExplicit($add) 216 - ->unsubscribe($rem) 217 - ->save(); 218 - break; 219 - } 220 - break; 221 - } 222 - 223 - return parent::applyBuiltinExternalTransaction($object, $xaction); 224 - } 225 - 226 181 protected function validateAllTransactions( 227 182 PhabricatorLiskDAO $object, 228 183 array $xactions) { ··· 584 539 ); 585 540 } 586 541 542 + protected function getMailCc(PhabricatorLiskDAO $object) { 543 + return array(); 544 + } 545 + 587 546 public function getMailTagsMap() { 588 547 return array( 589 548 PhabricatorProjectTransaction::MAILTAG_METADATA => ··· 592 551 pht('Project membership changes.'), 593 552 PhabricatorProjectTransaction::MAILTAG_WATCHERS => 594 553 pht('Project watcher list changes.'), 595 - PhabricatorProjectTransaction::MAILTAG_SUBSCRIBERS => 596 - pht('Project subscribers change.'), 597 554 PhabricatorProjectTransaction::MAILTAG_OTHER => 598 555 pht('Other project activity not listed above occurs.'), 599 556 );
-1
src/applications/project/engine/PhabricatorProjectEditEngine.php
··· 148 148 'icon', 149 149 'color', 150 150 'slugs', 151 - 'subscriberPHIDs', 152 151 )); 153 152 154 153 return array(
-15
src/applications/project/storage/PhabricatorProject.php
··· 6 6 PhabricatorFlaggableInterface, 7 7 PhabricatorPolicyInterface, 8 8 PhabricatorExtendedPolicyInterface, 9 - PhabricatorSubscribableInterface, 10 9 PhabricatorCustomFieldInterface, 11 10 PhabricatorDestructibleInterface, 12 11 PhabricatorFulltextInterface, ··· 178 177 179 178 return $extended; 180 179 } 181 - 182 180 183 181 public function isUserMember($user_phid) { 184 182 if ($this->memberPHIDs !== self::ATTACHABLE) { ··· 533 531 ); 534 532 535 533 return idx($map, $color, $color); 536 - } 537 - 538 - 539 - 540 - /* -( PhabricatorSubscribableInterface )----------------------------------- */ 541 - 542 - 543 - public function isAutomaticallySubscribed($phid) { 544 - return false; 545 - } 546 - 547 - public function shouldShowSubscribersProperty() { 548 - return false; 549 534 } 550 535 551 536
-4
src/applications/project/storage/PhabricatorProjectTransaction.php
··· 18 18 19 19 const MAILTAG_METADATA = 'project-metadata'; 20 20 const MAILTAG_MEMBERS = 'project-members'; 21 - const MAILTAG_SUBSCRIBERS = 'project-subscribers'; 22 21 const MAILTAG_WATCHERS = 'project-watchers'; 23 22 const MAILTAG_OTHER = 'project-other'; 24 23 ··· 381 380 case self::TYPE_ICON: 382 381 case self::TYPE_COLOR: 383 382 $tags[] = self::MAILTAG_METADATA; 384 - break; 385 - case PhabricatorTransactions::TYPE_SUBSCRIBERS: 386 - $tags[] = self::MAILTAG_SUBSCRIBERS; 387 383 break; 388 384 case PhabricatorTransactions::TYPE_EDGE: 389 385 $type = $this->getMetadata('edge:type');