Allow projects to be "watched", sort of a super-subscribe
Summary:
Ref T4967. Adds a "Watch" relationship to projects, which is stronger than member/subscribed.
Specifically, when a task is tagged with a project, we'll include all project watchers in the email/notifications. Normally we don't include projects unless they're explicitly CC'd, or have some other active role in the object (like being a reviewer or auditor).
This allows you to closely follow a project without needing to write a Herald rule for every project you care about.
Test Plan:
- Watched/unwatched a project.
- Tested the watch/subscribe/member relationships:
- Watching implies subscribe.
- Joining implies subscribe.
- Leaving implies unsubscribe + unwatch.
- You can't unsubscribe until you unwatch (slightly better would be unsubscribe implies unwatch, but this is a bit tricky).
- Watched a project, then recevied email about a tagged task without otherwise being involved.
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T4967
Differential Revision: https://secure.phabricator.com/D9185
This commit is contained in:
@@ -1962,6 +1962,7 @@ phutil_register_library_map(array(
|
|||||||
'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php',
|
'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php',
|
||||||
'PhabricatorProjectTransactionQuery' => 'applications/project/query/PhabricatorProjectTransactionQuery.php',
|
'PhabricatorProjectTransactionQuery' => 'applications/project/query/PhabricatorProjectTransactionQuery.php',
|
||||||
'PhabricatorProjectUpdateController' => 'applications/project/controller/PhabricatorProjectUpdateController.php',
|
'PhabricatorProjectUpdateController' => 'applications/project/controller/PhabricatorProjectUpdateController.php',
|
||||||
|
'PhabricatorProjectWatchController' => 'applications/project/controller/PhabricatorProjectWatchController.php',
|
||||||
'PhabricatorQuery' => 'infrastructure/query/PhabricatorQuery.php',
|
'PhabricatorQuery' => 'infrastructure/query/PhabricatorQuery.php',
|
||||||
'PhabricatorRecaptchaConfigOptions' => 'applications/config/option/PhabricatorRecaptchaConfigOptions.php',
|
'PhabricatorRecaptchaConfigOptions' => 'applications/config/option/PhabricatorRecaptchaConfigOptions.php',
|
||||||
'PhabricatorRedirectController' => 'applications/base/controller/PhabricatorRedirectController.php',
|
'PhabricatorRedirectController' => 'applications/base/controller/PhabricatorRedirectController.php',
|
||||||
@@ -4776,6 +4777,7 @@ phutil_register_library_map(array(
|
|||||||
'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
|
'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
|
||||||
'PhabricatorProjectTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
|
'PhabricatorProjectTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
|
||||||
'PhabricatorProjectUpdateController' => 'PhabricatorProjectController',
|
'PhabricatorProjectUpdateController' => 'PhabricatorProjectController',
|
||||||
|
'PhabricatorProjectWatchController' => 'PhabricatorProjectController',
|
||||||
'PhabricatorRecaptchaConfigOptions' => 'PhabricatorApplicationConfigOptions',
|
'PhabricatorRecaptchaConfigOptions' => 'PhabricatorApplicationConfigOptions',
|
||||||
'PhabricatorRedirectController' => 'PhabricatorController',
|
'PhabricatorRedirectController' => 'PhabricatorController',
|
||||||
'PhabricatorRefreshCSRFController' => 'PhabricatorAuthController',
|
'PhabricatorRefreshCSRFController' => 'PhabricatorAuthController',
|
||||||
|
|||||||
@@ -333,6 +333,10 @@ final class ManiphestTransactionEditor
|
|||||||
$phids[] = $phid;
|
$phids[] = $phid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (parent::getMailCC($object) as $phid) {
|
||||||
|
$phids[] = $phid;
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($this->heraldEmailPHIDs as $phid) {
|
foreach ($this->heraldEmailPHIDs as $phid) {
|
||||||
$phids[] = $phid;
|
$phids[] = $phid;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ final class PhabricatorApplicationProject extends PhabricatorApplication {
|
|||||||
'update/(?P<id>[1-9]\d*)/(?P<action>[^/]+)/'
|
'update/(?P<id>[1-9]\d*)/(?P<action>[^/]+)/'
|
||||||
=> 'PhabricatorProjectUpdateController',
|
=> 'PhabricatorProjectUpdateController',
|
||||||
'history/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectHistoryController',
|
'history/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectHistoryController',
|
||||||
|
'(?P<action>watch|unwatch)/(?P<id>[1-9]\d*)/'
|
||||||
|
=> 'PhabricatorProjectWatchController',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ final class PhabricatorProjectProfileController
|
|||||||
->setViewer($user)
|
->setViewer($user)
|
||||||
->withIDs(array($this->id))
|
->withIDs(array($this->id))
|
||||||
->needMembers(true)
|
->needMembers(true)
|
||||||
|
->needWatchers(true)
|
||||||
->needImages(true)
|
->needImages(true)
|
||||||
->executeOne();
|
->executeOne();
|
||||||
if (!$project) {
|
if (!$project) {
|
||||||
@@ -222,14 +223,32 @@ final class PhabricatorProjectProfileController
|
|||||||
->setIcon('fa-plus')
|
->setIcon('fa-plus')
|
||||||
->setDisabled(!$can_join)
|
->setDisabled(!$can_join)
|
||||||
->setName(pht('Join Project'));
|
->setName(pht('Join Project'));
|
||||||
|
$view->addAction($action);
|
||||||
} else {
|
} else {
|
||||||
$action = id(new PhabricatorActionView())
|
$action = id(new PhabricatorActionView())
|
||||||
->setWorkflow(true)
|
->setWorkflow(true)
|
||||||
->setHref('/project/update/'.$project->getID().'/leave/')
|
->setHref('/project/update/'.$project->getID().'/leave/')
|
||||||
->setIcon('fa-times')
|
->setIcon('fa-times')
|
||||||
->setName(pht('Leave Project...'));
|
->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;
|
return $view;
|
||||||
}
|
}
|
||||||
@@ -240,7 +259,10 @@ final class PhabricatorProjectProfileController
|
|||||||
$request = $this->getRequest();
|
$request = $this->getRequest();
|
||||||
$viewer = $request->getUser();
|
$viewer = $request->getUser();
|
||||||
|
|
||||||
$this->loadHandles($project->getMemberPHIDs());
|
$this->loadHandles(
|
||||||
|
array_merge(
|
||||||
|
$project->getMemberPHIDs(),
|
||||||
|
$project->getWatcherPHIDs()));
|
||||||
|
|
||||||
$view = id(new PHUIPropertyListView())
|
$view = id(new PHUIPropertyListView())
|
||||||
->setUser($viewer)
|
->setUser($viewer)
|
||||||
@@ -250,8 +272,14 @@ final class PhabricatorProjectProfileController
|
|||||||
$view->addProperty(
|
$view->addProperty(
|
||||||
pht('Members'),
|
pht('Members'),
|
||||||
$project->getMemberPHIDs()
|
$project->getMemberPHIDs()
|
||||||
? $this->renderHandlesForPHIDs($project->getMemberPHIDs(), ',')
|
? $this->renderHandlesForPHIDs($project->getMemberPHIDs(), ',')
|
||||||
: phutil_tag('em', array(), pht('None')));
|
: 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(
|
$field_list = PhabricatorCustomField::getObjectFields(
|
||||||
$project,
|
$project,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -125,14 +125,24 @@ final class PhabricatorProjectTransactionEditor
|
|||||||
case PhabricatorProjectTransaction::TYPE_IMAGE:
|
case PhabricatorProjectTransaction::TYPE_IMAGE:
|
||||||
return;
|
return;
|
||||||
case PhabricatorTransactions::TYPE_EDGE:
|
case PhabricatorTransactions::TYPE_EDGE:
|
||||||
switch ($xaction->getMetadataValue('edge:type')) {
|
$edge_type = $xaction->getMetadataValue('edge:type');
|
||||||
|
switch ($edge_type) {
|
||||||
case PhabricatorEdgeConfig::TYPE_PROJ_MEMBER:
|
case PhabricatorEdgeConfig::TYPE_PROJ_MEMBER:
|
||||||
// When project members are added or removed, add or remove their
|
case PhabricatorEdgeConfig::TYPE_OBJECT_HAS_WATCHER:
|
||||||
// subscriptions.
|
|
||||||
$old = $xaction->getOldValue();
|
$old = $xaction->getOldValue();
|
||||||
$new = $xaction->getNewValue();
|
$new = $xaction->getNewValue();
|
||||||
|
|
||||||
|
// When adding members or watchers, we add subscriptions.
|
||||||
$add = array_keys(array_diff_key($new, $old));
|
$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
|
// NOTE: The subscribe is "explicit" because there's no implicit
|
||||||
// unsubscribe, so Join -> Leave -> Join doesn't resubscribe you
|
// unsubscribe, so Join -> Leave -> Join doesn't resubscribe you
|
||||||
@@ -142,12 +152,28 @@ final class PhabricatorProjectTransactionEditor
|
|||||||
// this, which is a fairly weird edge case and pretty arguable both
|
// this, which is a fairly weird edge case and pretty arguable both
|
||||||
// ways.
|
// ways.
|
||||||
|
|
||||||
|
// Subscriptions caused by watches should also clearly be explicit,
|
||||||
|
// and that case is unambiguous.
|
||||||
|
|
||||||
id(new PhabricatorSubscriptionsEditor())
|
id(new PhabricatorSubscriptionsEditor())
|
||||||
->setActor($this->requireActor())
|
->setActor($this->requireActor())
|
||||||
->setObject($object)
|
->setObject($object)
|
||||||
->subscribeExplicit($add)
|
->subscribeExplicit($add)
|
||||||
->unsubscribe($rem)
|
->unsubscribe($rem)
|
||||||
->save();
|
->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;
|
break;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ final class PhabricatorProjectQuery
|
|||||||
const STATUS_ARCHIVED = 'status-archived';
|
const STATUS_ARCHIVED = 'status-archived';
|
||||||
|
|
||||||
private $needMembers;
|
private $needMembers;
|
||||||
|
private $needWatchers;
|
||||||
private $needImages;
|
private $needImages;
|
||||||
|
|
||||||
public function withIDs(array $ids) {
|
public function withIDs(array $ids) {
|
||||||
@@ -54,6 +55,11 @@ final class PhabricatorProjectQuery
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function needWatchers($need_watchers) {
|
||||||
|
$this->needWatchers = $need_watchers;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function needImages($need_images) {
|
public function needImages($need_images) {
|
||||||
$this->needImages = $need_images;
|
$this->needImages = $need_images;
|
||||||
return $this;
|
return $this;
|
||||||
@@ -100,19 +106,14 @@ final class PhabricatorProjectQuery
|
|||||||
|
|
||||||
if ($projects) {
|
if ($projects) {
|
||||||
$viewer_phid = $this->getViewer()->getPHID();
|
$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) {
|
if ($this->needMembers) {
|
||||||
$etype = PhabricatorEdgeConfig::TYPE_PROJ_MEMBER;
|
$need_edge_types[] = $member_type;
|
||||||
$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]));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
foreach ($data as $row) {
|
foreach ($data as $row) {
|
||||||
$projects[$row['id']]->setIsUserMember(
|
$projects[$row['id']]->setIsUserMember(
|
||||||
@@ -120,6 +121,39 @@ final class PhabricatorProjectQuery
|
|||||||
($row['viewerIsMember'] !== null));
|
($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;
|
return $projects;
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ final class PhabricatorProject extends PhabricatorProjectDAO
|
|||||||
protected $joinPolicy;
|
protected $joinPolicy;
|
||||||
|
|
||||||
private $memberPHIDs = self::ATTACHABLE;
|
private $memberPHIDs = self::ATTACHABLE;
|
||||||
|
private $watcherPHIDs = self::ATTACHABLE;
|
||||||
|
private $sparseWatchers = self::ATTACHABLE;
|
||||||
private $sparseMembers = self::ATTACHABLE;
|
private $sparseMembers = self::ATTACHABLE;
|
||||||
private $customFields = self::ATTACHABLE;
|
private $customFields = self::ATTACHABLE;
|
||||||
private $profileImageFile = self::ATTACHABLE;
|
private $profileImageFile = self::ATTACHABLE;
|
||||||
@@ -159,6 +161,32 @@ final class PhabricatorProject extends PhabricatorProjectDAO
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 )----------------------------------- */
|
/* -( PhabricatorSubscribableInterface )----------------------------------- */
|
||||||
|
|
||||||
|
|
||||||
@@ -171,7 +199,8 @@ final class PhabricatorProject extends PhabricatorProjectDAO
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function shouldAllowSubscription($phid) {
|
public function shouldAllowSubscription($phid) {
|
||||||
return $this->isUserMember($phid);
|
return $this->isUserMember($phid) &&
|
||||||
|
!$this->isUserWatcher($phid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1891,10 +1891,65 @@ abstract class PhabricatorApplicationTransactionEditor
|
|||||||
* @task mail
|
* @task mail
|
||||||
*/
|
*/
|
||||||
protected function getMailCC(PhabricatorLiskDAO $object) {
|
protected function getMailCC(PhabricatorLiskDAO $object) {
|
||||||
|
$phids = array();
|
||||||
|
$has_support = false;
|
||||||
|
|
||||||
if ($object instanceof PhabricatorSubscribableInterface) {
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants {
|
|||||||
const TYPE_DASHBOARD_HAS_PANEL = 45;
|
const TYPE_DASHBOARD_HAS_PANEL = 45;
|
||||||
const TYPE_PANEL_HAS_DASHBOARD = 46;
|
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_TEST_NO_CYCLE = 9000;
|
||||||
|
|
||||||
const TYPE_PHOB_HAS_ASANATASK = 80001;
|
const TYPE_PHOB_HAS_ASANATASK = 80001;
|
||||||
@@ -159,6 +162,9 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants {
|
|||||||
|
|
||||||
self::TYPE_PANEL_HAS_DASHBOARD => self::TYPE_DASHBOARD_HAS_PANEL,
|
self::TYPE_PANEL_HAS_DASHBOARD => self::TYPE_DASHBOARD_HAS_PANEL,
|
||||||
self::TYPE_DASHBOARD_HAS_PANEL => self::TYPE_PANEL_HAS_DASHBOARD,
|
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);
|
return idx($map, $edge_type);
|
||||||
@@ -343,6 +349,8 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants {
|
|||||||
return '%s added %d panel(s): %s.';
|
return '%s added %d panel(s): %s.';
|
||||||
case self::TYPE_PANEL_HAS_DASHBOARD:
|
case self::TYPE_PANEL_HAS_DASHBOARD:
|
||||||
return '%s added %d dashboard(s): %s.';
|
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_SUBSCRIBED_TO_OBJECT:
|
||||||
case self::TYPE_UNSUBSCRIBED_FROM_OBJECT:
|
case self::TYPE_UNSUBSCRIBED_FROM_OBJECT:
|
||||||
case self::TYPE_FILE_HAS_OBJECT:
|
case self::TYPE_FILE_HAS_OBJECT:
|
||||||
@@ -418,6 +426,8 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants {
|
|||||||
return '%s removed %d panel(s): %s.';
|
return '%s removed %d panel(s): %s.';
|
||||||
case self::TYPE_PANEL_HAS_DASHBOARD:
|
case self::TYPE_PANEL_HAS_DASHBOARD:
|
||||||
return '%s removed %d dashboard(s): %s.';
|
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_SUBSCRIBED_TO_OBJECT:
|
||||||
case self::TYPE_UNSUBSCRIBED_FROM_OBJECT:
|
case self::TYPE_UNSUBSCRIBED_FROM_OBJECT:
|
||||||
case self::TYPE_FILE_HAS_OBJECT:
|
case self::TYPE_FILE_HAS_OBJECT:
|
||||||
@@ -491,6 +501,8 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants {
|
|||||||
return '%s updated panels for %s.';
|
return '%s updated panels for %s.';
|
||||||
case self::TYPE_PANEL_HAS_DASHBOARD:
|
case self::TYPE_PANEL_HAS_DASHBOARD:
|
||||||
return '%s updated dashboards for %s.';
|
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_SUBSCRIBED_TO_OBJECT:
|
||||||
case self::TYPE_UNSUBSCRIBED_FROM_OBJECT:
|
case self::TYPE_UNSUBSCRIBED_FROM_OBJECT:
|
||||||
case self::TYPE_FILE_HAS_OBJECT:
|
case self::TYPE_FILE_HAS_OBJECT:
|
||||||
|
|||||||
Reference in New Issue
Block a user