Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14067720
D9185.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
19 KB
Referenced Files
None
Subscribers
None
D9185.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D9185: Allow projects to be "watched", sort of a super-subscribe
Attached
Detach File
Event Timeline
Log In to Comment