@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

Projects - add mail to project updates

Summary:
...which lets all the fancy settings for Email | Notify | Off be possible. Fixes T8164. Wasn't too sure the best way to break things up but members vs watchers felt meaningful to break out to me.

Also fixes a small bug where we were generating bad slug updated stories by messing with the signature of the slug data. Perhaps this fix isn't even good enough (the array_keys()) call and instead we'll need to implement transaction has effect and do a sort?

Test Plan: used ./bin/mail list-outbound and ./bin/mail show-outbound --id XX to verify reasonable emails were being generated. saw new preferences in settings.

Reviewers: epriestley

Reviewed By: epriestley

Subscribers: Korvin, epriestley

Maniphest Tasks: T8164

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

+145 -6
+2
src/__phutil_library_map__.php
··· 3101 3101 'ProjectQueryConduitAPIMethod' => 'applications/project/conduit/ProjectQueryConduitAPIMethod.php', 3102 3102 'ProjectRemarkupRule' => 'applications/project/remarkup/ProjectRemarkupRule.php', 3103 3103 'ProjectRemarkupRuleTestCase' => 'applications/project/remarkup/__tests__/ProjectRemarkupRuleTestCase.php', 3104 + 'ProjectReplyHandler' => 'applications/project/mail/ProjectReplyHandler.php', 3104 3105 'QueryFormattingTestCase' => 'infrastructure/storage/__tests__/QueryFormattingTestCase.php', 3105 3106 'ReleephAuthorFieldSpecification' => 'applications/releeph/field/specification/ReleephAuthorFieldSpecification.php', 3106 3107 'ReleephBranch' => 'applications/releeph/storage/ReleephBranch.php', ··· 6670 6671 'ProjectQueryConduitAPIMethod' => 'ProjectConduitAPIMethod', 6671 6672 'ProjectRemarkupRule' => 'PhabricatorObjectRemarkupRule', 6672 6673 'ProjectRemarkupRuleTestCase' => 'PhabricatorTestCase', 6674 + 'ProjectReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 6673 6675 'QueryFormattingTestCase' => 'PhabricatorTestCase', 6674 6676 'ReleephAuthorFieldSpecification' => 'ReleephFieldSpecification', 6675 6677 'ReleephBranch' => array(
+68 -5
src/applications/project/editor/PhabricatorProjectTransactionEditor.php
··· 41 41 $slugs = $object->getSlugs(); 42 42 $slugs = mpull($slugs, 'getSlug', 'getSlug'); 43 43 unset($slugs[$object->getPrimarySlug()]); 44 - return $slugs; 44 + return array_keys($slugs); 45 45 case PhabricatorProjectTransaction::TYPE_STATUS: 46 46 return $object->getStatus(); 47 47 case PhabricatorProjectTransaction::TYPE_IMAGE: ··· 403 403 return parent::requireCapabilities($object, $xaction); 404 404 } 405 405 406 - /** 407 - * Note: this is implemented for Feed purposes. 408 - */ 406 + protected function loadEdges( 407 + PhabricatorLiskDAO $object, 408 + array $xactions) { 409 + 410 + $member_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( 411 + $object->getPHID(), 412 + PhabricatorProjectProjectHasMemberEdgeType::EDGECONST); 413 + $object->attachMemberPHIDs($member_phids); 414 + } 415 + 416 + protected function shouldSendMail( 417 + PhabricatorLiskDAO $object, 418 + array $xactions) { 419 + return true; 420 + } 421 + 422 + protected function getMailSubjectPrefix() { 423 + return pht('[Project]'); 424 + } 425 + 409 426 protected function getMailTo(PhabricatorLiskDAO $object) { 410 - return array(); 427 + return $object->getMemberPHIDs(); 428 + } 429 + 430 + protected function getMailCC(PhabricatorLiskDAO $object) { 431 + $all = parent::getMailCC($object); 432 + return array_diff($all, $object->getMemberPHIDs()); 433 + } 434 + 435 + public function getMailTagsMap() { 436 + return array( 437 + PhabricatorProjectTransaction::MAILTAG_METADATA => 438 + pht('Project name, hashtags, icon, image, or color changes.'), 439 + PhabricatorProjectTransaction::MAILTAG_MEMBERS => 440 + pht('Project membership changes.'), 441 + PhabricatorProjectTransaction::MAILTAG_WATCHERS => 442 + pht('Project watcher list changes.'), 443 + PhabricatorProjectTransaction::MAILTAG_OTHER => 444 + pht('Other project activity not listed above occurs.'), 445 + ); 446 + } 447 + 448 + protected function buildReplyHandler(PhabricatorLiskDAO $object) { 449 + return id(new ProjectReplyHandler()) 450 + ->setMailReceiver($object); 451 + } 452 + 453 + protected function buildMailTemplate(PhabricatorLiskDAO $object) { 454 + $id = $object->getID(); 455 + $name = $object->getName(); 456 + 457 + return id(new PhabricatorMetaMTAMail()) 458 + ->setSubject("{$name}") 459 + ->addHeader('Thread-Topic', "Project {$id}"); 460 + } 461 + 462 + protected function buildMailBody( 463 + PhabricatorLiskDAO $object, 464 + array $xactions) { 465 + 466 + $body = parent::buildMailBody($object, $xactions); 467 + 468 + $uri = '/project/profile/'.$object->getID().'/'; 469 + $body->addLinkSection( 470 + pht('PROJECT DETAIL'), 471 + PhabricatorEnv::getProductionURI($uri)); 472 + 473 + return $body; 411 474 } 412 475 413 476 protected function shouldPublishFeedStory(
+20
src/applications/project/mail/ProjectReplyHandler.php
··· 1 + <?php 2 + 3 + final class ProjectReplyHandler 4 + extends PhabricatorApplicationTransactionReplyHandler { 5 + 6 + public function validateMailReceiver($mail_receiver) { 7 + if (!($mail_receiver instanceof PhabricatorProject)) { 8 + throw new Exception('Mail receiver is not a PhabricatorProject.'); 9 + } 10 + } 11 + 12 + public function getObjectPrefix() { 13 + return PhabricatorProjectProjectPHIDType::TYPECONST; 14 + } 15 + 16 + protected function shouldCreateCommentFromMailBody() { 17 + return false; 18 + } 19 + 20 + }
+47 -1
src/applications/project/storage/PhabricatorProjectTransaction.php
··· 14 14 // NOTE: This is deprecated, members are just a normal edge now. 15 15 const TYPE_MEMBERS = 'project:members'; 16 16 17 + const MAILTAG_METADATA = 'project-metadata'; 18 + const MAILTAG_MEMBERS = 'project-members'; 19 + const MAILTAG_WATCHERS = 'project-watchers'; 20 + const MAILTAG_OTHER = 'project-other'; 21 + 17 22 public function getApplicationName() { 18 23 return 'project'; 19 24 } ··· 106 111 $old, 107 112 $new); 108 113 } 114 + break; 115 + 109 116 case self::TYPE_STATUS: 110 117 if ($old == 0) { 111 118 return pht( ··· 116 123 '%s activated this project.', 117 124 $author_handle); 118 125 } 126 + break; 127 + 119 128 case self::TYPE_IMAGE: 120 129 // TODO: Some day, it would be nice to show the images. 121 130 if (!$old) { ··· 134 143 $this->renderHandleLink($old), 135 144 $this->renderHandleLink($new)); 136 145 } 146 + break; 137 147 138 148 case self::TYPE_ICON: 139 149 return pht( 140 150 '%s set this project\'s icon to %s.', 141 151 $author_handle, 142 152 PhabricatorProjectIcon::getLabel($new)); 153 + break; 143 154 144 155 case self::TYPE_COLOR: 145 156 return pht( 146 157 '%s set this project\'s color to %s.', 147 158 $author_handle, 148 159 PHUITagView::getShadeName($new)); 160 + break; 149 161 150 162 case self::TYPE_LOCKED: 151 163 if ($new) { ··· 157 169 '%s unlocked this project\'s membership.', 158 170 $author_handle); 159 171 } 172 + break; 160 173 161 174 case self::TYPE_SLUGS: 162 175 $add = array_diff($new, $old); ··· 183 196 count($rem), 184 197 $this->renderSlugList($rem)); 185 198 } 199 + break; 186 200 187 201 case self::TYPE_MEMBERS: 188 202 $add = array_diff($new, $old); ··· 221 235 $this->renderHandleList($rem)); 222 236 } 223 237 } 238 + break; 224 239 } 225 240 226 241 return parent::getTitle(); ··· 339 354 $object_handle, 340 355 $this->renderSlugList($rem)); 341 356 } 342 - 343 357 } 344 358 345 359 return parent::getTitleForFeed(); 360 + } 361 + 362 + public function getMailTags() { 363 + $tags = array(); 364 + switch ($this->getTransactionType()) { 365 + case self::TYPE_NAME: 366 + case self::TYPE_SLUGS: 367 + case self::TYPE_IMAGE: 368 + case self::TYPE_ICON: 369 + case self::TYPE_COLOR: 370 + $tags[] = self::MAILTAG_METADATA; 371 + break; 372 + case PhabricatorTransactions::TYPE_EDGE: 373 + $type = $this->getMetadata('edge:type'); 374 + $type = head($type); 375 + $type_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; 376 + $type_watcher = PhabricatorObjectHasWatcherEdgeType::EDGECONST; 377 + if ($type == $type_member) { 378 + $tags[] = self::MAILTAG_MEMBERS; 379 + } else if ($type == $type_watcher) { 380 + $tags[] = self::MAILTAG_WATCHERS; 381 + } else { 382 + $tags[] = self::MAILTAG_OTHER; 383 + } 384 + break; 385 + case self::TYPE_STATUS: 386 + case self::TYPE_LOCKED: 387 + default: 388 + $tags[] = self::MAILTAG_OTHER; 389 + break; 390 + } 391 + return $tags; 346 392 } 347 393 348 394 private function renderSlugList($slugs) {
+8
src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
··· 840 840 // subscribers to pick up changes caused by Herald (or by other side effects 841 841 // in various transaction phases). 842 842 $this->loadSubscribers($object); 843 + // Hook for other edges that may need (re-)loading 844 + $this->loadEdges($object, $xactions); 843 845 844 846 $this->loadHandles($xactions); 845 847 ··· 963 965 } else { 964 966 $this->subscribers = array(); 965 967 } 968 + } 969 + 970 + protected function loadEdges( 971 + PhabricatorLiskDAO $object, 972 + array $xactions) { 973 + return; 966 974 } 967 975 968 976 private function validateEditParameters(