mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-26 08:42:41 +01:00
Add basic "Subscriptions" application
Summary: Basic infrastructure for generalizing subscriptions/CCs for T1808, T1514 and T1663. - Implement `PhabricatorSubscribableInterface` and you'll get a subscribe/unsubscribe button for free. - If there are any auto-subscribed users (like the question author) you can specify them; this makes more sense for Tasks and Revisions than Ponder probably, but maybe the author should be auto-subscribed. - Subscriptions are either "explicit" (the user clicked 'subscribe') or "implicit" (the user did something which causes them to become subscribed naturally). If a user unsubscribes, they'll no longer be added by implicit subscriptions. This may or may not be relevant to Ponder but is an existing Herald feature in Differential. - Helper method on PhabricatorSubscribersQuery to load subscribers. - This doesn't handle actually sending email, etc. I think that's all so application-specific that it doesn't belong here. - Now seems to work. Test Plan: {F20552} {F20553} Reviewers: pieter, btrahan Reviewed By: pieter CC: aran Maniphest Tasks: T1663, T1514, T1808 Differential Revision: https://secure.phabricator.com/D3637
This commit is contained in:
parent
1fda844c9f
commit
5b74b8b765
12 changed files with 531 additions and 12 deletions
|
@ -181,6 +181,9 @@ $action_map = array(
|
||||||
'flag-6' => 'icon/flag-6.png',
|
'flag-6' => 'icon/flag-6.png',
|
||||||
'flag-7' => 'icon/flag-7.png',
|
'flag-7' => 'icon/flag-7.png',
|
||||||
'flag-ghost' => 'icon/flag-ghost.png',
|
'flag-ghost' => 'icon/flag-ghost.png',
|
||||||
|
'subscribe-auto' => 'icon/unsubscribe.png',
|
||||||
|
'subscribe-add' => 'icon/subscribe.png',
|
||||||
|
'subscribe-delete' => 'icon/unsubscribe.png',
|
||||||
);
|
);
|
||||||
|
|
||||||
foreach ($action_map as $icon => $source) {
|
foreach ($action_map as $icon => $source) {
|
||||||
|
|
|
@ -581,6 +581,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorApplicationSettings' => 'applications/settings/application/PhabricatorApplicationSettings.php',
|
'PhabricatorApplicationSettings' => 'applications/settings/application/PhabricatorApplicationSettings.php',
|
||||||
'PhabricatorApplicationSlowvote' => 'applications/slowvote/application/PhabricatorApplicationSlowvote.php',
|
'PhabricatorApplicationSlowvote' => 'applications/slowvote/application/PhabricatorApplicationSlowvote.php',
|
||||||
'PhabricatorApplicationStatusView' => 'applications/meta/view/PhabricatorApplicationStatusView.php',
|
'PhabricatorApplicationStatusView' => 'applications/meta/view/PhabricatorApplicationStatusView.php',
|
||||||
|
'PhabricatorApplicationSubscriptions' => 'applications/subscriptions/application/PhabricatorApplicationSubscriptions.php',
|
||||||
'PhabricatorApplicationUIExamples' => 'applications/uiexample/application/PhabricatorApplicationUIExamples.php',
|
'PhabricatorApplicationUIExamples' => 'applications/uiexample/application/PhabricatorApplicationUIExamples.php',
|
||||||
'PhabricatorApplicationsListController' => 'applications/meta/controller/PhabricatorApplicationsListController.php',
|
'PhabricatorApplicationsListController' => 'applications/meta/controller/PhabricatorApplicationsListController.php',
|
||||||
'PhabricatorAuditActionConstants' => 'applications/audit/constants/PhabricatorAuditActionConstants.php',
|
'PhabricatorAuditActionConstants' => 'applications/audit/constants/PhabricatorAuditActionConstants.php',
|
||||||
|
@ -1078,6 +1079,11 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorStorageManagementUpgradeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php',
|
'PhabricatorStorageManagementUpgradeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php',
|
||||||
'PhabricatorStorageManagementWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php',
|
'PhabricatorStorageManagementWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php',
|
||||||
'PhabricatorStoragePatch' => 'infrastructure/storage/management/PhabricatorStoragePatch.php',
|
'PhabricatorStoragePatch' => 'infrastructure/storage/management/PhabricatorStoragePatch.php',
|
||||||
|
'PhabricatorSubscribableInterface' => 'applications/subscriptions/interface/PhabricatorSubscribableInterface.php',
|
||||||
|
'PhabricatorSubscribersQuery' => 'applications/subscriptions/query/PhabricatorSubscribersQuery.php',
|
||||||
|
'PhabricatorSubscriptionsEditController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php',
|
||||||
|
'PhabricatorSubscriptionsEditor' => 'applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php',
|
||||||
|
'PhabricatorSubscriptionsUIEventListener' => 'applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php',
|
||||||
'PhabricatorSymbolNameLinter' => 'infrastructure/lint/hook/PhabricatorSymbolNameLinter.php',
|
'PhabricatorSymbolNameLinter' => 'infrastructure/lint/hook/PhabricatorSymbolNameLinter.php',
|
||||||
'PhabricatorSyntaxHighlighter' => 'infrastructure/markup/PhabricatorSyntaxHighlighter.php',
|
'PhabricatorSyntaxHighlighter' => 'infrastructure/markup/PhabricatorSyntaxHighlighter.php',
|
||||||
'PhabricatorTaskmasterDaemon' => 'infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php',
|
'PhabricatorTaskmasterDaemon' => 'infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php',
|
||||||
|
@ -1691,6 +1697,7 @@ phutil_register_library_map(array(
|
||||||
'ManiphestTaskPriority' => 'ManiphestConstants',
|
'ManiphestTaskPriority' => 'ManiphestConstants',
|
||||||
'ManiphestTaskProject' => 'ManiphestDAO',
|
'ManiphestTaskProject' => 'ManiphestDAO',
|
||||||
'ManiphestTaskProjectsView' => 'ManiphestView',
|
'ManiphestTaskProjectsView' => 'ManiphestView',
|
||||||
|
'ManiphestTaskQuery' => 'PhabricatorQuery',
|
||||||
'ManiphestTaskStatus' => 'ManiphestConstants',
|
'ManiphestTaskStatus' => 'ManiphestConstants',
|
||||||
'ManiphestTaskSubscriber' => 'ManiphestDAO',
|
'ManiphestTaskSubscriber' => 'ManiphestDAO',
|
||||||
'ManiphestTaskSummaryView' => 'ManiphestView',
|
'ManiphestTaskSummaryView' => 'ManiphestView',
|
||||||
|
@ -1746,6 +1753,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorApplicationSettings' => 'PhabricatorApplication',
|
'PhabricatorApplicationSettings' => 'PhabricatorApplication',
|
||||||
'PhabricatorApplicationSlowvote' => 'PhabricatorApplication',
|
'PhabricatorApplicationSlowvote' => 'PhabricatorApplication',
|
||||||
'PhabricatorApplicationStatusView' => 'AphrontView',
|
'PhabricatorApplicationStatusView' => 'AphrontView',
|
||||||
|
'PhabricatorApplicationSubscriptions' => 'PhabricatorApplication',
|
||||||
'PhabricatorApplicationUIExamples' => 'PhabricatorApplication',
|
'PhabricatorApplicationUIExamples' => 'PhabricatorApplication',
|
||||||
'PhabricatorApplicationsListController' => 'PhabricatorController',
|
'PhabricatorApplicationsListController' => 'PhabricatorController',
|
||||||
'PhabricatorAuditAddCommentController' => 'PhabricatorAuditController',
|
'PhabricatorAuditAddCommentController' => 'PhabricatorAuditController',
|
||||||
|
@ -2185,6 +2193,9 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorStorageManagementStatusWorkflow' => 'PhabricatorStorageManagementWorkflow',
|
'PhabricatorStorageManagementStatusWorkflow' => 'PhabricatorStorageManagementWorkflow',
|
||||||
'PhabricatorStorageManagementUpgradeWorkflow' => 'PhabricatorStorageManagementWorkflow',
|
'PhabricatorStorageManagementUpgradeWorkflow' => 'PhabricatorStorageManagementWorkflow',
|
||||||
'PhabricatorStorageManagementWorkflow' => 'PhutilArgumentWorkflow',
|
'PhabricatorStorageManagementWorkflow' => 'PhutilArgumentWorkflow',
|
||||||
|
'PhabricatorSubscribersQuery' => 'PhabricatorQuery',
|
||||||
|
'PhabricatorSubscriptionsEditController' => 'PhabricatorController',
|
||||||
|
'PhabricatorSubscriptionsUIEventListener' => 'PhutilEventListener',
|
||||||
'PhabricatorSymbolNameLinter' => 'ArcanistXHPASTLintNamingHook',
|
'PhabricatorSymbolNameLinter' => 'ArcanistXHPASTLintNamingHook',
|
||||||
'PhabricatorTaskmasterDaemon' => 'PhabricatorDaemon',
|
'PhabricatorTaskmasterDaemon' => 'PhabricatorDaemon',
|
||||||
'PhabricatorTestCase' => 'ArcanistPhutilTestCase',
|
'PhabricatorTestCase' => 'ArcanistPhutilTestCase',
|
||||||
|
|
|
@ -102,6 +102,14 @@ final class PhabricatorObjectHandleData {
|
||||||
$objects[$revision->getPHID()] = $revision;
|
$objects[$revision->getPHID()] = $revision;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case PhabricatorPHIDConstants::PHID_TYPE_QUES:
|
||||||
|
$questions = id(new PonderQuestionQuery())
|
||||||
|
->withPHIDs($phids)
|
||||||
|
->execute();
|
||||||
|
foreach ($questions as $question) {
|
||||||
|
$objects[$question->getPHID()] = $question;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright 2012 Facebook, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
final class PhabricatorApplicationSubscriptions extends PhabricatorApplication {
|
||||||
|
|
||||||
|
public function shouldAppearInLaunchView() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEventListeners() {
|
||||||
|
return array(
|
||||||
|
new PhabricatorSubscriptionsUIEventListener(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRoutes() {
|
||||||
|
return array(
|
||||||
|
'/subscriptions/' => array(
|
||||||
|
'(?P<action>add|delete)/'.
|
||||||
|
'(?P<phid>[^/]+)/' => 'PhabricatorSubscriptionsEditController',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright 2012 Facebook, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
final class PhabricatorSubscriptionsEditController
|
||||||
|
extends PhabricatorController {
|
||||||
|
|
||||||
|
private $phid;
|
||||||
|
private $action;
|
||||||
|
|
||||||
|
public function willProcessRequest(array $data) {
|
||||||
|
$this->phid = idx($data, 'phid');
|
||||||
|
$this->action = idx($data, 'action');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processRequest() {
|
||||||
|
$request = $this->getRequest();
|
||||||
|
|
||||||
|
if (!$request->isFormPost()) {
|
||||||
|
return new Aphront400Response();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($this->action) {
|
||||||
|
case 'add':
|
||||||
|
$is_add = true;
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
$is_add = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return new Aphront400Response();
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $request->getUser();
|
||||||
|
$phid = $this->phid;
|
||||||
|
|
||||||
|
// TODO: This is a policy test because `loadObjects()` is not currently
|
||||||
|
// policy-aware. Once it is, we can collapse this.
|
||||||
|
$handle = PhabricatorObjectHandleData::loadOneHandle($phid, $user);
|
||||||
|
if (!$handle->isComplete()) {
|
||||||
|
return new Aphront404Response();
|
||||||
|
}
|
||||||
|
|
||||||
|
$objects = id(new PhabricatorObjectHandleData(array($phid)))
|
||||||
|
->loadObjects();
|
||||||
|
$object = idx($objects, $phid);
|
||||||
|
|
||||||
|
if (!($object instanceof PhabricatorSubscribableInterface)) {
|
||||||
|
return $this->buildErrorResponse(
|
||||||
|
pht('Bad Object'),
|
||||||
|
pht('This object is not subscribable.'),
|
||||||
|
$handle->getURI());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($object->isAutomaticallySubscribed($user->getPHID())) {
|
||||||
|
return $this->buildErrorResponse(
|
||||||
|
pht('Automatically Subscribed'),
|
||||||
|
pht('You are automatically subscribed to this object.'),
|
||||||
|
$handle->getURI());
|
||||||
|
}
|
||||||
|
|
||||||
|
$editor = id(new PhabricatorSubscriptionsEditor())
|
||||||
|
->setUser($user)
|
||||||
|
->setObject($object);
|
||||||
|
|
||||||
|
if ($is_add) {
|
||||||
|
$editor->subscribeExplicit(array($user->getPHID()), $explicit = true);
|
||||||
|
} else {
|
||||||
|
$editor->unsubscribe(array($user->getPHID()));
|
||||||
|
}
|
||||||
|
|
||||||
|
$editor->save();
|
||||||
|
|
||||||
|
// TODO: We should just render the "Unsubscribe" action and swap it out
|
||||||
|
// in the document for Ajax requests.
|
||||||
|
return id(new AphrontReloadResponse())->setURI($handle->getURI());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildErrorResponse($title, $message, $uri) {
|
||||||
|
$request = $this->getRequest();
|
||||||
|
$user = $request->getUser();
|
||||||
|
|
||||||
|
$dialog = id(new AphrontDialogView())
|
||||||
|
->setUser($user)
|
||||||
|
->setTitle($title)
|
||||||
|
->appendChild($message)
|
||||||
|
->addCancelButton($uri);
|
||||||
|
|
||||||
|
return id(new AphrontDialogResponse())->setDialog($dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright 2012 Facebook, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
final class PhabricatorSubscriptionsEditor {
|
||||||
|
|
||||||
|
private $object;
|
||||||
|
private $user;
|
||||||
|
|
||||||
|
private $explicitSubscribePHIDs = array();
|
||||||
|
private $implicitSubscribePHIDs = array();
|
||||||
|
private $unsubscribePHIDs = array();
|
||||||
|
|
||||||
|
public function setObject(PhabricatorSubscribableInterface $object) {
|
||||||
|
$this->object = $object;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUser(PhabricatorUser $user) {
|
||||||
|
$this->user = $user;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add explicit subscribers. These subscribers have explicitly subscribed
|
||||||
|
* (or been subscribed) to the object, and will be added even if they
|
||||||
|
* had previously unsubscribed.
|
||||||
|
*
|
||||||
|
* @param list<phid> List of PHIDs to explicitly subscribe.
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public function subscribeExplicit(array $phids) {
|
||||||
|
$this->explicitSubscribePHIDs += array_fill_keys($phids, true);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add implicit subscribers. These subscribers have taken some action which
|
||||||
|
* implicitly subscribes them (e.g., adding a comment) but it will be
|
||||||
|
* suppressed if they've previously unsubscribed from the object.
|
||||||
|
*
|
||||||
|
* @param list<phid> List of PHIDs to implicitly subscribe.
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public function subscribeImplicit(array $phids) {
|
||||||
|
$this->implicitSubscribePHIDs += array_fill_keys($phids, true);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe PHIDs and mark them as unsubscribed, so implicit subscriptions
|
||||||
|
* will not resubscribe them.
|
||||||
|
*
|
||||||
|
* @param list<phid> List of PHIDs to unsubscribe.
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public function unsubscribe(array $phids) {
|
||||||
|
$this->unsubscribePHIDs += array_fill_keys($phids, true);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function save() {
|
||||||
|
if (!$this->object) {
|
||||||
|
throw new Exception('Call setObject() before save()!');
|
||||||
|
}
|
||||||
|
if (!$this->user) {
|
||||||
|
throw new Exception('Call setUser() before save()!');
|
||||||
|
}
|
||||||
|
|
||||||
|
$src = $this->object->getPHID();
|
||||||
|
|
||||||
|
if ($this->implicitSubscribePHIDs) {
|
||||||
|
$unsub = PhabricatorEdgeQuery::loadDestinationPHIDs(
|
||||||
|
$src,
|
||||||
|
PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER);
|
||||||
|
$unsub = array_fill_keys($unsub, true);
|
||||||
|
$this->implicitSubscribePHIDs = array_diff_key(
|
||||||
|
$this->implicitSubscribePHIDs,
|
||||||
|
$unsub);
|
||||||
|
}
|
||||||
|
|
||||||
|
$add = $this->implicitSubscribePHIDs + $this->explicitSubscribePHIDs;
|
||||||
|
$del = $this->unsubscribePHIDs;
|
||||||
|
|
||||||
|
// If a PHID is marked for both subscription and unsubscription, treat
|
||||||
|
// unsubscription as the stronger action.
|
||||||
|
$add = array_diff_key($add, $del);
|
||||||
|
|
||||||
|
if ($add || $del) {
|
||||||
|
$u_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER;
|
||||||
|
$s_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_SUBSCRIBER;
|
||||||
|
|
||||||
|
$editor = id(new PhabricatorEdgeEditor())
|
||||||
|
->setUser($this->user);
|
||||||
|
|
||||||
|
foreach ($add as $phid => $ignored) {
|
||||||
|
$editor->removeEdge($src, $u_type, $phid);
|
||||||
|
$editor->addEdge($src, $s_type, $phid);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($del as $phid => $ignored) {
|
||||||
|
$editor->removeEdge($src, $s_type, $phid);
|
||||||
|
$editor->addEdge($src, $u_type, $phid);
|
||||||
|
}
|
||||||
|
|
||||||
|
$editor->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright 2012 Facebook, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
final class PhabricatorSubscriptionsUIEventListener
|
||||||
|
extends PhutilEventListener {
|
||||||
|
|
||||||
|
public function register() {
|
||||||
|
$this->listen(PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleEvent(PhutilEvent $event) {
|
||||||
|
switch ($event->getType()) {
|
||||||
|
case PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS:
|
||||||
|
$this->handleActionEvent($event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleActionEvent($event) {
|
||||||
|
$user = $event->getUser();
|
||||||
|
$object = $event->getValue('object');
|
||||||
|
|
||||||
|
if (!$object || !$object->getPHID()) {
|
||||||
|
// No object, or the object has no PHID yet. No way to subscribe.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!($object instanceof PhabricatorSubscribableInterface)) {
|
||||||
|
// This object isn't subscribable.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($object->isAutomaticallySubscribed($user->getPHID())) {
|
||||||
|
$sub_action = id(new PhabricatorActionView())
|
||||||
|
->setWorkflow(true)
|
||||||
|
->setUser($user)
|
||||||
|
->setDisabled(true)
|
||||||
|
->setRenderAsForm(true)
|
||||||
|
->setHref('/subscriptions/add/'.$object->getPHID().'/')
|
||||||
|
->setName(phutil_escape_html('Automatically Subscribed'))
|
||||||
|
->setIcon('subscribe-auto');
|
||||||
|
} else {
|
||||||
|
$subscribed = false;
|
||||||
|
if ($user->isLoggedIn()) {
|
||||||
|
$src_phid = $object->getPHID();
|
||||||
|
$dst_phid = $user->getPHID();
|
||||||
|
$edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_SUBSCRIBER;
|
||||||
|
|
||||||
|
$edges = id(new PhabricatorEdgeQuery())
|
||||||
|
->withSourcePHIDs(array($src_phid))
|
||||||
|
->withEdgeTypes(array($edge_type))
|
||||||
|
->withDestinationPHIDs(array($user->getPHID()))
|
||||||
|
->execute();
|
||||||
|
$subscribed = isset($edges[$src_phid][$edge_type][$dst_phid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($subscribed) {
|
||||||
|
$sub_action = id(new PhabricatorActionView())
|
||||||
|
->setUser($user)
|
||||||
|
->setWorkflow(true)
|
||||||
|
->setRenderAsForm(true)
|
||||||
|
->setHref('/subscriptions/delete/'.$object->getPHID().'/')
|
||||||
|
->setName(phutil_escape_html('Unsubscribe'))
|
||||||
|
->setIcon('subscribe-delete');
|
||||||
|
} else {
|
||||||
|
$sub_action = id(new PhabricatorActionView())
|
||||||
|
->setUser($user)
|
||||||
|
->setWorkflow(true)
|
||||||
|
->setRenderAsForm(true)
|
||||||
|
->setHref('/subscriptions/add/'.$object->getPHID().'/')
|
||||||
|
->setName(phutil_escape_html('Subscribe'))
|
||||||
|
->setIcon('subscribe-add');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$user->isLoggedIn()) {
|
||||||
|
$sub_action->setDisabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$actions = $event->getValue('actions');
|
||||||
|
$actions[] = $sub_action;
|
||||||
|
$event->setValue('actions', $actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright 2012 Facebook, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface PhabricatorSubscribableInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true to indicate that the given PHID is automatically subscribed
|
||||||
|
* to the object (for example, they are the author or in some other way
|
||||||
|
* irrevocably a subscriber). This will, e.g., cause the UI to render
|
||||||
|
* "Automatically Subscribed" instead of "Subscribe".
|
||||||
|
*
|
||||||
|
* @param PHID PHID (presumably a user) to test for automatic subscription.
|
||||||
|
* @return bool True if the object/user is automatically subscribed.
|
||||||
|
*/
|
||||||
|
public function isAutomaticallySubscribed($phid);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright 2012 Facebook, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
final class PhabricatorSubscribersQuery extends PhabricatorQuery {
|
||||||
|
|
||||||
|
private $objectPHIDs;
|
||||||
|
private $subscriberPHIDs;
|
||||||
|
|
||||||
|
public static function loadSubscribersForPHID($phid) {
|
||||||
|
$subscribers = id(new PhabricatorSubscribersQuery())
|
||||||
|
->withObjectPHIDs(array($phid))
|
||||||
|
->execute();
|
||||||
|
return $subscribers[$phid];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withObjectPHIDs(array $object_phids) {
|
||||||
|
$this->objectPHIDs = $object_phids;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withSubscriberPHIDs(array $subscriber_phids) {
|
||||||
|
$this->subscriberPHIDs = $subscriber_phids;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute() {
|
||||||
|
$query = new PhabricatorEdgeQuery();
|
||||||
|
|
||||||
|
$edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_SUBSCRIBER;
|
||||||
|
|
||||||
|
$query->withSourcePHIDs($this->objectPHIDs);
|
||||||
|
$query->withEdgeTypes(array($edge_type));
|
||||||
|
|
||||||
|
if ($this->subscriberPHIDs) {
|
||||||
|
$query->withDestinationPHIDs($this->subscriberPHIDs);
|
||||||
|
}
|
||||||
|
|
||||||
|
$edges = $query->execute();
|
||||||
|
|
||||||
|
$results = array_fill_keys($this->objectPHIDs, array());
|
||||||
|
foreach ($edges as $src => $edge_types) {
|
||||||
|
foreach ($edge_types[$edge_type] as $dst => $data) {
|
||||||
|
$results[$src][] = $dst;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -49,6 +49,12 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants {
|
||||||
const TYPE_ANSWER_HAS_VOTING_USER = 19;
|
const TYPE_ANSWER_HAS_VOTING_USER = 19;
|
||||||
const TYPE_VOTING_USER_HAS_ANSWER = 20;
|
const TYPE_VOTING_USER_HAS_ANSWER = 20;
|
||||||
|
|
||||||
|
const TYPE_OBJECT_HAS_SUBSCRIBER = 21;
|
||||||
|
const TYPE_SUBSCRIBED_TO_OBJECT = 22;
|
||||||
|
|
||||||
|
const TYPE_OBJECT_HAS_UNSUBSCRIBER = 23;
|
||||||
|
const TYPE_UNSUBSCRIBED_FROM_OBJECT = 24;
|
||||||
|
|
||||||
const TYPE_TEST_NO_CYCLE = 9000;
|
const TYPE_TEST_NO_CYCLE = 9000;
|
||||||
|
|
||||||
public static function getInverse($edge_type) {
|
public static function getInverse($edge_type) {
|
||||||
|
@ -82,6 +88,12 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants {
|
||||||
self::TYPE_QUESTION_HAS_VOTING_USER,
|
self::TYPE_QUESTION_HAS_VOTING_USER,
|
||||||
self::TYPE_ANSWER_HAS_VOTING_USER => self::TYPE_VOTING_USER_HAS_ANSWER,
|
self::TYPE_ANSWER_HAS_VOTING_USER => self::TYPE_VOTING_USER_HAS_ANSWER,
|
||||||
self::TYPE_VOTING_USER_HAS_ANSWER => self::TYPE_ANSWER_HAS_VOTING_USER,
|
self::TYPE_VOTING_USER_HAS_ANSWER => self::TYPE_ANSWER_HAS_VOTING_USER,
|
||||||
|
|
||||||
|
self::TYPE_OBJECT_HAS_SUBSCRIBER => self::TYPE_SUBSCRIBED_TO_OBJECT,
|
||||||
|
self::TYPE_SUBSCRIBED_TO_OBJECT => self::TYPE_OBJECT_HAS_SUBSCRIBER,
|
||||||
|
|
||||||
|
self::TYPE_OBJECT_HAS_UNSUBSCRIBER => self::TYPE_UNSUBSCRIBED_FROM_OBJECT,
|
||||||
|
self::TYPE_UNSUBSCRIBED_FROM_OBJECT => self::TYPE_OBJECT_HAS_UNSUBSCRIBER,
|
||||||
);
|
);
|
||||||
|
|
||||||
return idx($map, $edge_type);
|
return idx($map, $edge_type);
|
||||||
|
|
|
@ -363,3 +363,15 @@
|
||||||
.action-flag-ghost {
|
.action-flag-ghost {
|
||||||
background-position: 0px -3444px;
|
background-position: 0px -3444px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-subscribe-auto {
|
||||||
|
background-position: 0px -3461px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-subscribe-add {
|
||||||
|
background-position: 0px -3478px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-subscribe-delete {
|
||||||
|
background-position: 0px -3495px;
|
||||||
|
}
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
Loading…
Reference in a new issue