Page MenuHomePhabricator

D9185.diff
No OneTemporary

D9185.diff

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
@@ -1962,6 +1962,7 @@
'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php',
'PhabricatorProjectTransactionQuery' => 'applications/project/query/PhabricatorProjectTransactionQuery.php',
'PhabricatorProjectUpdateController' => 'applications/project/controller/PhabricatorProjectUpdateController.php',
+ 'PhabricatorProjectWatchController' => 'applications/project/controller/PhabricatorProjectWatchController.php',
'PhabricatorQuery' => 'infrastructure/query/PhabricatorQuery.php',
'PhabricatorRecaptchaConfigOptions' => 'applications/config/option/PhabricatorRecaptchaConfigOptions.php',
'PhabricatorRedirectController' => 'applications/base/controller/PhabricatorRedirectController.php',
@@ -4776,6 +4777,7 @@
'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorProjectTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorProjectUpdateController' => 'PhabricatorProjectController',
+ 'PhabricatorProjectWatchController' => 'PhabricatorProjectController',
'PhabricatorRecaptchaConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorRedirectController' => 'PhabricatorController',
'PhabricatorRefreshCSRFController' => 'PhabricatorAuthController',
diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
--- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php
+++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
@@ -333,6 +333,10 @@
$phids[] = $phid;
}
+ foreach (parent::getMailCC($object) as $phid) {
+ $phids[] = $phid;
+ }
+
foreach ($this->heraldEmailPHIDs as $phid) {
$phids[] = $phid;
}
diff --git a/src/applications/project/application/PhabricatorApplicationProject.php b/src/applications/project/application/PhabricatorApplicationProject.php
--- a/src/applications/project/application/PhabricatorApplicationProject.php
+++ b/src/applications/project/application/PhabricatorApplicationProject.php
@@ -62,6 +62,8 @@
'update/(?P<id>[1-9]\d*)/(?P<action>[^/]+)/'
=> 'PhabricatorProjectUpdateController',
'history/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectHistoryController',
+ '(?P<action>watch|unwatch)/(?P<id>[1-9]\d*)/'
+ => 'PhabricatorProjectWatchController',
),
);
}
diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php
--- a/src/applications/project/controller/PhabricatorProjectProfileController.php
+++ b/src/applications/project/controller/PhabricatorProjectProfileController.php
@@ -21,6 +21,7 @@
->setViewer($user)
->withIDs(array($this->id))
->needMembers(true)
+ ->needWatchers(true)
->needImages(true)
->executeOne();
if (!$project) {
@@ -222,14 +223,32 @@
->setIcon('fa-plus')
->setDisabled(!$can_join)
->setName(pht('Join Project'));
+ $view->addAction($action);
} else {
$action = id(new PhabricatorActionView())
->setWorkflow(true)
->setHref('/project/update/'.$project->getID().'/leave/')
->setIcon('fa-times')
->setName(pht('Leave Project...'));
+ $view->addAction($action);
+
+ if (!$project->isUserWatcher($viewer->getPHID())) {
+ $action = id(new PhabricatorActionView())
+ ->setWorkflow(true)
+ ->setHref('/project/watch/'.$project->getID().'/')
+ ->setIcon('fa-eye')
+ ->setName(pht('Watch Project'));
+ $view->addAction($action);
+ } else {
+ $action = id(new PhabricatorActionView())
+ ->setWorkflow(true)
+ ->setHref('/project/unwatch/'.$project->getID().'/')
+ ->setIcon('fa-eye-slash')
+ ->setName(pht('Unwatch Project'));
+ $view->addAction($action);
+ }
}
- $view->addAction($action);
+
return $view;
}
@@ -240,7 +259,10 @@
$request = $this->getRequest();
$viewer = $request->getUser();
- $this->loadHandles($project->getMemberPHIDs());
+ $this->loadHandles(
+ array_merge(
+ $project->getMemberPHIDs(),
+ $project->getWatcherPHIDs()));
$view = id(new PHUIPropertyListView())
->setUser($viewer)
@@ -250,8 +272,14 @@
$view->addProperty(
pht('Members'),
$project->getMemberPHIDs()
- ? $this->renderHandlesForPHIDs($project->getMemberPHIDs(), ',')
- : phutil_tag('em', array(), pht('None')));
+ ? $this->renderHandlesForPHIDs($project->getMemberPHIDs(), ',')
+ : phutil_tag('em', array(), pht('None')));
+
+ $view->addProperty(
+ pht('Watchers'),
+ $project->getWatcherPHIDs()
+ ? $this->renderHandlesForPHIDs($project->getWatcherPHIDs(), ',')
+ : phutil_tag('em', array(), pht('None')));
$field_list = PhabricatorCustomField::getObjectFields(
$project,
diff --git a/src/applications/project/controller/PhabricatorProjectWatchController.php b/src/applications/project/controller/PhabricatorProjectWatchController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/project/controller/PhabricatorProjectWatchController.php
@@ -0,0 +1,97 @@
+<?php
+
+final class PhabricatorProjectWatchController
+ extends PhabricatorProjectController {
+
+ private $id;
+ private $action;
+
+ public function willProcessRequest(array $data) {
+ $this->id = $data['id'];
+ $this->action = $data['action'];
+ }
+
+ public function processRequest() {
+ $request = $this->getRequest();
+ $viewer = $request->getUser();
+
+ $project = id(new PhabricatorProjectQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($this->id))
+ ->needMembers(true)
+ ->needWatchers(true)
+ ->executeOne();
+ if (!$project) {
+ return new Aphront404Response();
+ }
+
+ $project_uri = '/project/view/'.$project->getID().'/';
+
+ // You must be a member of a project to
+ if (!$project->isUserMember($viewer->getPHID())) {
+ return new Aphront400Response();
+ }
+
+ if ($request->isDialogFormPost()) {
+ $edge_action = null;
+ switch ($this->action) {
+ case 'watch':
+ $edge_action = '+';
+ $force_subscribe = true;
+ break;
+ case 'unwatch':
+ $edge_action = '-';
+ $force_subscribe = false;
+ break;
+ }
+
+ $type_member = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_WATCHER;
+ $member_spec = array(
+ $edge_action => array($viewer->getPHID() => $viewer->getPHID()),
+ );
+
+ $xactions = array();
+ $xactions[] = id(new PhabricatorProjectTransaction())
+ ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
+ ->setMetadataValue('edge:type', $type_member)
+ ->setNewValue($member_spec);
+
+ $editor = id(new PhabricatorProjectTransactionEditor($project))
+ ->setActor($viewer)
+ ->setContentSourceFromRequest($request)
+ ->setContinueOnNoEffect(true)
+ ->setContinueOnMissingFields(true)
+ ->applyTransactions($project, $xactions);
+
+ return id(new AphrontRedirectResponse())->setURI($project_uri);
+ }
+
+ $dialog = null;
+ switch ($this->action) {
+ case 'watch':
+ $title = pht('Watch Project?');
+ $body = pht(
+ 'Watching a project will let you monitor it closely. You will '.
+ 'receive email and notifications about changes to every object '.
+ 'associated with projects you watch.');
+ $submit = pht('Watch Project');
+ break;
+ case 'unwatch':
+ $title = pht('Unwatch Project?');
+ $body = pht(
+ 'You will no longer receive email or notifications about every '.
+ 'object associated with this project.');
+ $submit = pht('Unwatch Project');
+ break;
+ default:
+ return new Aphront404Response();
+ }
+
+ return $this->newDialog()
+ ->setTitle($title)
+ ->appendParagraph($body)
+ ->addCancelButton($project_uri)
+ ->addSubmitButton($submit);
+ }
+
+}
diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
--- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
+++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
@@ -125,14 +125,24 @@
case PhabricatorProjectTransaction::TYPE_IMAGE:
return;
case PhabricatorTransactions::TYPE_EDGE:
- switch ($xaction->getMetadataValue('edge:type')) {
+ $edge_type = $xaction->getMetadataValue('edge:type');
+ switch ($edge_type) {
case PhabricatorEdgeConfig::TYPE_PROJ_MEMBER:
- // When project members are added or removed, add or remove their
- // subscriptions.
+ case PhabricatorEdgeConfig::TYPE_OBJECT_HAS_WATCHER:
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
+
+ // When adding members or watchers, we add subscriptions.
$add = array_keys(array_diff_key($new, $old));
- $rem = array_keys(array_diff_key($old, $new));
+
+ // When removing members, we remove their subscription too.
+ // When unwatching, we leave subscriptions, since it's fine to be
+ // subscribed to a project but not be a member of it.
+ if ($edge_type == PhabricatorEdgeConfig::TYPE_PROJ_MEMBER) {
+ $rem = array_keys(array_diff_key($old, $new));
+ } else {
+ $rem = array();
+ }
// NOTE: The subscribe is "explicit" because there's no implicit
// unsubscribe, so Join -> Leave -> Join doesn't resubscribe you
@@ -142,12 +152,28 @@
// this, which is a fairly weird edge case and pretty arguable both
// ways.
+ // Subscriptions caused by watches should also clearly be explicit,
+ // and that case is unambiguous.
+
id(new PhabricatorSubscriptionsEditor())
->setActor($this->requireActor())
->setObject($object)
->subscribeExplicit($add)
->unsubscribe($rem)
->save();
+
+ if ($rem) {
+ // When removing members, also remove any watches on the project.
+ $edge_editor = id(new PhabricatorEdgeEditor())
+ ->setSuppressEvents(true);
+ foreach ($rem as $rem_phid) {
+ $edge_editor->removeEdge(
+ $object->getPHID(),
+ PhabricatorEdgeConfig::TYPE_OBJECT_HAS_WATCHER,
+ $rem_phid);
+ }
+ $edge_editor->save();
+ }
break;
}
return;
diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php
--- a/src/applications/project/query/PhabricatorProjectQuery.php
+++ b/src/applications/project/query/PhabricatorProjectQuery.php
@@ -17,6 +17,7 @@
const STATUS_ARCHIVED = 'status-archived';
private $needMembers;
+ private $needWatchers;
private $needImages;
public function withIDs(array $ids) {
@@ -54,6 +55,11 @@
return $this;
}
+ public function needWatchers($need_watchers) {
+ $this->needWatchers = $need_watchers;
+ return $this;
+ }
+
public function needImages($need_images) {
$this->needImages = $need_images;
return $this;
@@ -100,19 +106,14 @@
if ($projects) {
$viewer_phid = $this->getViewer()->getPHID();
+ $project_phids = mpull($projects, 'getPHID');
+
+ $member_type = PhabricatorEdgeConfig::TYPE_PROJ_MEMBER;
+ $watcher_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_WATCHER;
+
+ $need_edge_types = array();
if ($this->needMembers) {
- $etype = PhabricatorEdgeConfig::TYPE_PROJ_MEMBER;
- $members = id(new PhabricatorEdgeQuery())
- ->withSourcePHIDs(mpull($projects, 'getPHID'))
- ->withEdgeTypes(array($etype))
- ->execute();
- foreach ($projects as $project) {
- $phid = $project->getPHID();
- $project->attachMemberPHIDs(array_keys($members[$phid][$etype]));
- $project->setIsUserMember(
- $viewer_phid,
- isset($members[$phid][$etype][$viewer_phid]));
- }
+ $need_edge_types[] = $member_type;
} else {
foreach ($data as $row) {
$projects[$row['id']]->setIsUserMember(
@@ -120,6 +121,39 @@
($row['viewerIsMember'] !== null));
}
}
+
+ if ($this->needWatchers) {
+ $need_edge_types[] = $watcher_type;
+ }
+
+ if ($need_edge_types) {
+ $edges = id(new PhabricatorEdgeQuery())
+ ->withSourcePHIDs($project_phids)
+ ->withEdgeTypes($need_edge_types)
+ ->execute();
+
+ if ($this->needMembers) {
+ foreach ($projects as $project) {
+ $phid = $project->getPHID();
+ $project->attachMemberPHIDs(
+ array_keys($edges[$phid][$member_type]));
+ $project->setIsUserMember(
+ $viewer_phid,
+ isset($edges[$phid][$member_type][$viewer_phid]));
+ }
+ }
+
+ if ($this->needWatchers) {
+ foreach ($projects as $project) {
+ $phid = $project->getPHID();
+ $project->attachWatcherPHIDs(
+ array_keys($edges[$phid][$watcher_type]));
+ $project->setIsUserWatcher(
+ $viewer_phid,
+ isset($edges[$phid][$watcher_type][$viewer_phid]));
+ }
+ }
+ }
}
return $projects;
diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php
--- a/src/applications/project/storage/PhabricatorProject.php
+++ b/src/applications/project/storage/PhabricatorProject.php
@@ -19,6 +19,8 @@
protected $joinPolicy;
private $memberPHIDs = self::ATTACHABLE;
+ private $watcherPHIDs = self::ATTACHABLE;
+ private $sparseWatchers = self::ATTACHABLE;
private $sparseMembers = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $profileImageFile = self::ATTACHABLE;
@@ -159,6 +161,32 @@
}
+ public function isUserWatcher($user_phid) {
+ if ($this->watcherPHIDs !== self::ATTACHABLE) {
+ return in_array($user_phid, $this->watcherPHIDs);
+ }
+ return $this->assertAttachedKey($this->sparseWatchers, $user_phid);
+ }
+
+ public function setIsUserWatcher($user_phid, $is_watcher) {
+ if ($this->sparseWatchers === self::ATTACHABLE) {
+ $this->sparseWatchers = array();
+ }
+ $this->sparseWatchers[$user_phid] = $is_watcher;
+ return $this;
+ }
+
+ public function attachWatcherPHIDs(array $phids) {
+ $this->watcherPHIDs = $phids;
+ return $this;
+ }
+
+ public function getWatcherPHIDs() {
+ return $this->assertAttached($this->watcherPHIDs);
+ }
+
+
+
/* -( PhabricatorSubscribableInterface )----------------------------------- */
@@ -171,7 +199,8 @@
}
public function shouldAllowSubscription($phid) {
- return $this->isUserMember($phid);
+ return $this->isUserMember($phid) &&
+ !$this->isUserWatcher($phid);
}
diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
--- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
+++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
@@ -1891,10 +1891,65 @@
* @task mail
*/
protected function getMailCC(PhabricatorLiskDAO $object) {
+ $phids = array();
+ $has_support = false;
+
if ($object instanceof PhabricatorSubscribableInterface) {
- return $this->subscribers;
+ $phids[] = $this->subscribers;
+ $has_support = true;
}
- throw new Exception("Capability not supported.");
+
+ // TODO: This should be some interface which specifies that the object
+ // has project associations.
+ if ($object instanceof ManiphestTask) {
+
+ // TODO: This is what normal objects would do, but Maniphest is still
+ // behind the times.
+ if (false) {
+ $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
+ $object->getPHID(),
+ PhabricatorEdgeConfig::TYPE_OBJECT_HAS_PROJECT);
+ } else {
+ $project_phids = $object->getProjectPHIDs();
+ }
+
+ if ($project_phids) {
+ $watcher_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_WATCHER;
+
+ $query = id(new PhabricatorEdgeQuery())
+ ->withSourcePHIDs($project_phids)
+ ->withEdgeTypes(array($watcher_type));
+ $query->execute();
+
+ $watcher_phids = $query->getDestinationPHIDs();
+
+ // We need to do a visibility check for all the watchers, as
+ // watching a project is not a guarantee that you can see objects
+ // associated with it.
+ $users = id(new PhabricatorPeopleQuery())
+ ->setViewer($this->requireActor())
+ ->withPHIDs($watcher_phids)
+ ->execute();
+
+ foreach ($users as $user) {
+ $can_see = PhabricatorPolicyFilter::hasCapability(
+ $user,
+ $object,
+ PhabricatorPolicyCapability::CAN_VIEW);
+ if ($can_see) {
+ $phids[] = $user->getPHID();
+ }
+ }
+ }
+
+ $has_support = true;
+ }
+
+ if (!$has_support) {
+ throw new Exception('Capability not supported.');
+ }
+
+ return array_mergev($phids);
}
diff --git a/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php b/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php
--- a/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php
+++ b/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php
@@ -72,6 +72,9 @@
const TYPE_DASHBOARD_HAS_PANEL = 45;
const TYPE_PANEL_HAS_DASHBOARD = 46;
+ const TYPE_OBJECT_HAS_WATCHER = 47;
+ const TYPE_WATCHER_HAS_OBJECT = 48;
+
const TYPE_TEST_NO_CYCLE = 9000;
const TYPE_PHOB_HAS_ASANATASK = 80001;
@@ -159,6 +162,9 @@
self::TYPE_PANEL_HAS_DASHBOARD => self::TYPE_DASHBOARD_HAS_PANEL,
self::TYPE_DASHBOARD_HAS_PANEL => self::TYPE_PANEL_HAS_DASHBOARD,
+
+ self::TYPE_OBJECT_HAS_WATCHER => self::TYPE_WATCHER_HAS_OBJECT,
+ self::TYPE_WATCHER_HAS_OBJECT => self::TYPE_OBJECT_HAS_WATCHER
);
return idx($map, $edge_type);
@@ -343,6 +349,8 @@
return '%s added %d panel(s): %s.';
case self::TYPE_PANEL_HAS_DASHBOARD:
return '%s added %d dashboard(s): %s.';
+ case self::TYPE_OBJECT_HAS_WATCHER:
+ return '%s added %d watcher(s): %s.';
case self::TYPE_SUBSCRIBED_TO_OBJECT:
case self::TYPE_UNSUBSCRIBED_FROM_OBJECT:
case self::TYPE_FILE_HAS_OBJECT:
@@ -418,6 +426,8 @@
return '%s removed %d panel(s): %s.';
case self::TYPE_PANEL_HAS_DASHBOARD:
return '%s removed %d dashboard(s): %s.';
+ case self::TYPE_OBJECT_HAS_WATCHER:
+ return '%s removed %d watcher(s): %s.';
case self::TYPE_SUBSCRIBED_TO_OBJECT:
case self::TYPE_UNSUBSCRIBED_FROM_OBJECT:
case self::TYPE_FILE_HAS_OBJECT:
@@ -491,6 +501,8 @@
return '%s updated panels for %s.';
case self::TYPE_PANEL_HAS_DASHBOARD:
return '%s updated dashboards for %s.';
+ case self::TYPE_OBJECT_HAS_WATCHER:
+ return '%s updated watchers for %s.';
case self::TYPE_SUBSCRIBED_TO_OBJECT:
case self::TYPE_UNSUBSCRIBED_FROM_OBJECT:
case self::TYPE_FILE_HAS_OBJECT:

File Metadata

Mime Type
text/plain
Expires
Wed, Nov 20, 4:22 PM (5 h, 33 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6717447
Default Alt Text
D9185.diff (19 KB)

Event Timeline