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

Make subproject/milestone watch rules work better

Summary:
Ref T10349. These got sort of half-weirded-up before I separated subscriptions and watching fully. New rules are:

- You can watch whatever you want.
- Watching a parent watches everything inside it.
- If you're watching "Stonework" and go to "Stonework > Masonry", you'll see a "Watching Ancestor" hint to let you know you're already watching a parent or ancestor.

Test Plan:
- Watched and unwatched "Stonework".
- Watched and unwatched "Stonework > Iteration IV".
- While watching "Stonework", visited "Iteration IV" and saw "Watching Ancestor" hint.
- Created a task tagged "Stonework > Iteration IV". Got notified about it because I watch "Stonework".

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T10349

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

+111 -37
+18 -4
src/applications/project/controller/PhabricatorProjectProfileController.php
··· 162 162 163 163 private function renderWatchAction(PhabricatorProject $project) { 164 164 $viewer = $this->getViewer(); 165 - $viewer_phid = $viewer->getPHID(); 166 165 $id = $project->getID(); 167 166 168 - $is_watcher = ($viewer_phid && $project->isUserWatcher($viewer_phid)); 167 + if (!$viewer->isLoggedIn()) { 168 + $is_watcher = false; 169 + $is_ancestor = false; 170 + } else { 171 + $viewer_phid = $viewer->getPHID(); 172 + $is_watcher = $project->isUserWatcher($viewer_phid); 173 + $is_ancestor = $project->isUserAncestorWatcher($viewer_phid); 174 + } 169 175 170 - if (!$is_watcher) { 176 + if ($is_ancestor && !$is_watcher) { 177 + $watch_icon = 'fa-eye'; 178 + $watch_text = pht('Watching Ancestor'); 179 + $watch_href = "/project/watch/{$id}/?via=profile"; 180 + $watch_disabled = true; 181 + } else if (!$is_watcher) { 171 182 $watch_icon = 'fa-eye'; 172 183 $watch_text = pht('Watch Project'); 173 184 $watch_href = "/project/watch/{$id}/?via=profile"; 185 + $watch_disabled = false; 174 186 } else { 175 187 $watch_icon = 'fa-eye-slash'; 176 188 $watch_text = pht('Unwatch Project'); 177 189 $watch_href = "/project/unwatch/{$id}/?via=profile"; 190 + $watch_disabled = false; 178 191 } 179 192 180 193 $watch_icon = id(new PHUIIconView()) ··· 185 198 ->setWorkflow(true) 186 199 ->setIcon($watch_icon) 187 200 ->setText($watch_text) 188 - ->setHref($watch_href); 201 + ->setHref($watch_href) 202 + ->setDisabled($watch_disabled); 189 203 } 190 204 191 205 private function buildMilestoneList(PhabricatorProject $project) {
+30 -4
src/applications/project/controller/PhabricatorProjectWatchController.php
··· 25 25 $done_uri = "/project/members/{$id}/"; 26 26 } 27 27 28 + $is_watcher = $project->isUserWatcher($viewer->getPHID()); 29 + $is_ancestor = $project->isUserAncestorWatcher($viewer->getPHID()); 30 + if ($is_ancestor && !$is_watcher) { 31 + $ancestor_phid = $project->getWatchedAncestorPHID($viewer->getPHID()); 32 + $handles = $viewer->loadHandles(array($ancestor_phid)); 33 + $ancestor_handle = $handles[$ancestor_phid]; 34 + 35 + return $this->newDialog() 36 + ->setTitle(pht('Watching Ancestor')) 37 + ->appendParagraph( 38 + pht( 39 + 'You are already watching %s, an ancestor of this project, and '. 40 + 'are thus watching all of its subprojects.', 41 + $ancestor_handle->renderTag()->render())) 42 + ->addCancelbutton($done_uri); 43 + } 44 + 28 45 if ($request->isDialogFormPost()) { 29 46 $edge_action = null; 30 47 switch ($action) { ··· 61 78 switch ($action) { 62 79 case 'watch': 63 80 $title = pht('Watch Project?'); 64 - $body = pht( 81 + $body = array(); 82 + $body[] = pht( 65 83 'Watching a project will let you monitor it closely. You will '. 66 84 'receive email and notifications about changes to every object '. 67 - 'associated with projects you watch.'); 85 + 'tagged with projects you watch.'); 86 + $body[] = pht( 87 + 'Watching a project also watches all subprojects and milestones of '. 88 + 'that project.'); 68 89 $submit = pht('Watch Project'); 69 90 break; 70 91 case 'unwatch': ··· 78 99 return new Aphront404Response(); 79 100 } 80 101 81 - return $this->newDialog() 102 + $dialog = $this->newDialog() 82 103 ->setTitle($title) 83 104 ->addHiddenInput('via', $via) 84 - ->appendParagraph($body) 85 105 ->addCancelButton($done_uri) 86 106 ->addSubmitButton($submit); 107 + 108 + foreach ((array)$body as $paragraph) { 109 + $dialog->appendParagraph($paragraph); 110 + } 111 + 112 + return $dialog; 87 113 } 88 114 89 115 }
+7 -5
src/applications/project/query/PhabricatorProjectQuery.php
··· 247 247 248 248 $all_graph = $this->getAllReachableAncestors($projects); 249 249 250 - if ($this->needAncestorMembers) { 250 + if ($this->needAncestorMembers || $this->needWatchers) { 251 251 $src_projects = $all_graph; 252 252 } else { 253 253 $src_projects = $projects; ··· 255 255 256 256 $all_sources = array(); 257 257 foreach ($src_projects as $project) { 258 + // For milestones, we need parent members. 258 259 if ($project->isMilestone()) { 259 - $phid = $project->getParentProjectPHID(); 260 - } else { 261 - $phid = $project->getPHID(); 260 + $parent_phid = $project->getParentProjectPHID(); 261 + $all_sources[$parent_phid] = $parent_phid; 262 262 } 263 + 264 + $phid = $project->getPHID(); 263 265 $all_sources[$phid] = $phid; 264 266 } 265 267 ··· 318 320 319 321 if ($this->needWatchers) { 320 322 $watcher_phids = $edge_query->getDestinationPHIDs( 321 - $source_phids, 323 + array($project_phid), 322 324 array($watcher_type)); 323 325 $project->attachWatcherPHIDs($watcher_phids); 324 326 $project->setIsUserWatcher(
+41
src/applications/project/storage/PhabricatorProject.php
··· 287 287 return $this->assertAttachedKey($this->sparseWatchers, $user_phid); 288 288 } 289 289 290 + public function isUserAncestorWatcher($user_phid) { 291 + $is_watcher = $this->isUserWatcher($user_phid); 292 + 293 + if (!$is_watcher) { 294 + $parent = $this->getParentProject(); 295 + if ($parent) { 296 + return $parent->isUserWatcher($user_phid); 297 + } 298 + } 299 + 300 + return $is_watcher; 301 + } 302 + 303 + public function getWatchedAncestorPHID($user_phid) { 304 + if ($this->isUserWatcher($user_phid)) { 305 + return $this->getPHID(); 306 + } 307 + 308 + $parent = $this->getParentProject(); 309 + if ($parent) { 310 + return $parent->getWatchedAncestorPHID($user_phid); 311 + } 312 + 313 + return null; 314 + } 315 + 290 316 public function setIsUserWatcher($user_phid, $is_watcher) { 291 317 if ($this->sparseWatchers === self::ATTACHABLE) { 292 318 $this->sparseWatchers = array(); ··· 302 328 303 329 public function getWatcherPHIDs() { 304 330 return $this->assertAttached($this->watcherPHIDs); 331 + } 332 + 333 + public function getAllAncestorWatcherPHIDs() { 334 + $parent = $this->getParentProject(); 335 + if ($parent) { 336 + $watchers = $parent->getAllAncestorWatcherPHIDs(); 337 + } else { 338 + $watchers = array(); 339 + } 340 + 341 + foreach ($this->getWatcherPHIDs() as $phid) { 342 + $watchers[$phid] = $phid; 343 + } 344 + 345 + return $watchers; 305 346 } 306 347 307 348 public function attachSlugs(array $slugs) {
+4 -18
src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php
··· 28 28 ->withPHIDs(array($phid)) 29 29 ->executeOne(); 30 30 31 - if (phid_get_type($phid) == PhabricatorProjectProjectPHIDType::TYPECONST) { 32 - // TODO: This is a big hack, but a weak argument for adding some kind 33 - // of "load for role" feature to ObjectQuery, and also not a really great 34 - // argument for adding some kind of "load extra stuff" feature to 35 - // SubscriberInterface. Do this for now and wait for the best way forward 36 - // to become more clear? 37 - 38 - $object = id(new PhabricatorProjectQuery()) 39 - ->setViewer($viewer) 40 - ->withPHIDs(array($phid)) 41 - ->needWatchers(true) 42 - ->executeOne(); 43 - } else { 44 - $object = id(new PhabricatorObjectQuery()) 45 - ->setViewer($viewer) 46 - ->withPHIDs(array($phid)) 47 - ->executeOne(); 48 - } 31 + $object = id(new PhabricatorObjectQuery()) 32 + ->setViewer($viewer) 33 + ->withPHIDs(array($phid)) 34 + ->executeOne(); 49 35 50 36 if (!($object instanceof PhabricatorSubscribableInterface)) { 51 37 return $this->buildErrorResponse(
+11 -6
src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
··· 2607 2607 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); 2608 2608 2609 2609 if ($project_phids) { 2610 - $watcher_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST; 2610 + $projects = id(new PhabricatorProjectQuery()) 2611 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 2612 + ->withPHIDs($project_phids) 2613 + ->needWatchers(true) 2614 + ->execute(); 2611 2615 2612 - $query = id(new PhabricatorEdgeQuery()) 2613 - ->withSourcePHIDs($project_phids) 2614 - ->withEdgeTypes(array($watcher_type)); 2615 - $query->execute(); 2616 + $watcher_phids = array(); 2617 + foreach ($projects as $project) { 2618 + foreach ($project->getAllAncestorWatcherPHIDs() as $phid) { 2619 + $watcher_phids[$phid] = $phid; 2620 + } 2621 + } 2616 2622 2617 - $watcher_phids = $query->getDestinationPHIDs(); 2618 2623 if ($watcher_phids) { 2619 2624 // We need to do a visibility check for all the watchers, as 2620 2625 // watching a project is not a guarantee that you can see objects