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

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

+531 -12
+15 -12
scripts/celerity/generate_sprites.php
··· 169 169 ->setSourceSize(16, 16); 170 170 171 171 $action_map = array( 172 - 'file' => 'icon/page_white_text.png', 173 - 'fork' => 'icon/arrow_branch.png', 174 - 'edit' => 'icon/page_white_edit.png', 175 - 'flag-0' => 'icon/flag-0.png', 176 - 'flag-1' => 'icon/flag-1.png', 177 - 'flag-2' => 'icon/flag-2.png', 178 - 'flag-3' => 'icon/flag-3.png', 179 - 'flag-4' => 'icon/flag-4.png', 180 - 'flag-5' => 'icon/flag-5.png', 181 - 'flag-6' => 'icon/flag-6.png', 182 - 'flag-7' => 'icon/flag-7.png', 183 - 'flag-ghost' => 'icon/flag-ghost.png', 172 + 'file' => 'icon/page_white_text.png', 173 + 'fork' => 'icon/arrow_branch.png', 174 + 'edit' => 'icon/page_white_edit.png', 175 + 'flag-0' => 'icon/flag-0.png', 176 + 'flag-1' => 'icon/flag-1.png', 177 + 'flag-2' => 'icon/flag-2.png', 178 + 'flag-3' => 'icon/flag-3.png', 179 + 'flag-4' => 'icon/flag-4.png', 180 + 'flag-5' => 'icon/flag-5.png', 181 + 'flag-6' => 'icon/flag-6.png', 182 + 'flag-7' => 'icon/flag-7.png', 183 + 'flag-ghost' => 'icon/flag-ghost.png', 184 + 'subscribe-auto' => 'icon/unsubscribe.png', 185 + 'subscribe-add' => 'icon/subscribe.png', 186 + 'subscribe-delete' => 'icon/unsubscribe.png', 184 187 ); 185 188 186 189 foreach ($action_map as $icon => $source) {
+11
src/__phutil_library_map__.php
··· 581 581 'PhabricatorApplicationSettings' => 'applications/settings/application/PhabricatorApplicationSettings.php', 582 582 'PhabricatorApplicationSlowvote' => 'applications/slowvote/application/PhabricatorApplicationSlowvote.php', 583 583 'PhabricatorApplicationStatusView' => 'applications/meta/view/PhabricatorApplicationStatusView.php', 584 + 'PhabricatorApplicationSubscriptions' => 'applications/subscriptions/application/PhabricatorApplicationSubscriptions.php', 584 585 'PhabricatorApplicationUIExamples' => 'applications/uiexample/application/PhabricatorApplicationUIExamples.php', 585 586 'PhabricatorApplicationsListController' => 'applications/meta/controller/PhabricatorApplicationsListController.php', 586 587 'PhabricatorAuditActionConstants' => 'applications/audit/constants/PhabricatorAuditActionConstants.php', ··· 1078 1079 'PhabricatorStorageManagementUpgradeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php', 1079 1080 'PhabricatorStorageManagementWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php', 1080 1081 'PhabricatorStoragePatch' => 'infrastructure/storage/management/PhabricatorStoragePatch.php', 1082 + 'PhabricatorSubscribableInterface' => 'applications/subscriptions/interface/PhabricatorSubscribableInterface.php', 1083 + 'PhabricatorSubscribersQuery' => 'applications/subscriptions/query/PhabricatorSubscribersQuery.php', 1084 + 'PhabricatorSubscriptionsEditController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php', 1085 + 'PhabricatorSubscriptionsEditor' => 'applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php', 1086 + 'PhabricatorSubscriptionsUIEventListener' => 'applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php', 1081 1087 'PhabricatorSymbolNameLinter' => 'infrastructure/lint/hook/PhabricatorSymbolNameLinter.php', 1082 1088 'PhabricatorSyntaxHighlighter' => 'infrastructure/markup/PhabricatorSyntaxHighlighter.php', 1083 1089 'PhabricatorTaskmasterDaemon' => 'infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php', ··· 1691 1697 'ManiphestTaskPriority' => 'ManiphestConstants', 1692 1698 'ManiphestTaskProject' => 'ManiphestDAO', 1693 1699 'ManiphestTaskProjectsView' => 'ManiphestView', 1700 + 'ManiphestTaskQuery' => 'PhabricatorQuery', 1694 1701 'ManiphestTaskStatus' => 'ManiphestConstants', 1695 1702 'ManiphestTaskSubscriber' => 'ManiphestDAO', 1696 1703 'ManiphestTaskSummaryView' => 'ManiphestView', ··· 1746 1753 'PhabricatorApplicationSettings' => 'PhabricatorApplication', 1747 1754 'PhabricatorApplicationSlowvote' => 'PhabricatorApplication', 1748 1755 'PhabricatorApplicationStatusView' => 'AphrontView', 1756 + 'PhabricatorApplicationSubscriptions' => 'PhabricatorApplication', 1749 1757 'PhabricatorApplicationUIExamples' => 'PhabricatorApplication', 1750 1758 'PhabricatorApplicationsListController' => 'PhabricatorController', 1751 1759 'PhabricatorAuditAddCommentController' => 'PhabricatorAuditController', ··· 2185 2193 'PhabricatorStorageManagementStatusWorkflow' => 'PhabricatorStorageManagementWorkflow', 2186 2194 'PhabricatorStorageManagementUpgradeWorkflow' => 'PhabricatorStorageManagementWorkflow', 2187 2195 'PhabricatorStorageManagementWorkflow' => 'PhutilArgumentWorkflow', 2196 + 'PhabricatorSubscribersQuery' => 'PhabricatorQuery', 2197 + 'PhabricatorSubscriptionsEditController' => 'PhabricatorController', 2198 + 'PhabricatorSubscriptionsUIEventListener' => 'PhutilEventListener', 2188 2199 'PhabricatorSymbolNameLinter' => 'ArcanistXHPASTLintNamingHook', 2189 2200 'PhabricatorTaskmasterDaemon' => 'PhabricatorDaemon', 2190 2201 'PhabricatorTestCase' => 'ArcanistPhutilTestCase',
+8
src/applications/phid/handle/PhabricatorObjectHandleData.php
··· 102 102 $objects[$revision->getPHID()] = $revision; 103 103 } 104 104 break; 105 + case PhabricatorPHIDConstants::PHID_TYPE_QUES: 106 + $questions = id(new PonderQuestionQuery()) 107 + ->withPHIDs($phids) 108 + ->execute(); 109 + foreach ($questions as $question) { 110 + $objects[$question->getPHID()] = $question; 111 + } 112 + break; 105 113 } 106 114 } 107 115
+41
src/applications/subscriptions/application/PhabricatorApplicationSubscriptions.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + final class PhabricatorApplicationSubscriptions extends PhabricatorApplication { 20 + 21 + public function shouldAppearInLaunchView() { 22 + return false; 23 + } 24 + 25 + public function getEventListeners() { 26 + return array( 27 + new PhabricatorSubscriptionsUIEventListener(), 28 + ); 29 + } 30 + 31 + public function getRoutes() { 32 + return array( 33 + '/subscriptions/' => array( 34 + '(?P<action>add|delete)/'. 35 + '(?P<phid>[^/]+)/' => 'PhabricatorSubscriptionsEditController', 36 + ), 37 + ); 38 + } 39 + 40 + } 41 +
+106
src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + final class PhabricatorSubscriptionsEditController 20 + extends PhabricatorController { 21 + 22 + private $phid; 23 + private $action; 24 + 25 + public function willProcessRequest(array $data) { 26 + $this->phid = idx($data, 'phid'); 27 + $this->action = idx($data, 'action'); 28 + } 29 + 30 + public function processRequest() { 31 + $request = $this->getRequest(); 32 + 33 + if (!$request->isFormPost()) { 34 + return new Aphront400Response(); 35 + } 36 + 37 + switch ($this->action) { 38 + case 'add': 39 + $is_add = true; 40 + break; 41 + case 'delete': 42 + $is_add = false; 43 + break; 44 + default: 45 + return new Aphront400Response(); 46 + } 47 + 48 + $user = $request->getUser(); 49 + $phid = $this->phid; 50 + 51 + // TODO: This is a policy test because `loadObjects()` is not currently 52 + // policy-aware. Once it is, we can collapse this. 53 + $handle = PhabricatorObjectHandleData::loadOneHandle($phid, $user); 54 + if (!$handle->isComplete()) { 55 + return new Aphront404Response(); 56 + } 57 + 58 + $objects = id(new PhabricatorObjectHandleData(array($phid))) 59 + ->loadObjects(); 60 + $object = idx($objects, $phid); 61 + 62 + if (!($object instanceof PhabricatorSubscribableInterface)) { 63 + return $this->buildErrorResponse( 64 + pht('Bad Object'), 65 + pht('This object is not subscribable.'), 66 + $handle->getURI()); 67 + } 68 + 69 + if ($object->isAutomaticallySubscribed($user->getPHID())) { 70 + return $this->buildErrorResponse( 71 + pht('Automatically Subscribed'), 72 + pht('You are automatically subscribed to this object.'), 73 + $handle->getURI()); 74 + } 75 + 76 + $editor = id(new PhabricatorSubscriptionsEditor()) 77 + ->setUser($user) 78 + ->setObject($object); 79 + 80 + if ($is_add) { 81 + $editor->subscribeExplicit(array($user->getPHID()), $explicit = true); 82 + } else { 83 + $editor->unsubscribe(array($user->getPHID())); 84 + } 85 + 86 + $editor->save(); 87 + 88 + // TODO: We should just render the "Unsubscribe" action and swap it out 89 + // in the document for Ajax requests. 90 + return id(new AphrontReloadResponse())->setURI($handle->getURI()); 91 + } 92 + 93 + private function buildErrorResponse($title, $message, $uri) { 94 + $request = $this->getRequest(); 95 + $user = $request->getUser(); 96 + 97 + $dialog = id(new AphrontDialogView()) 98 + ->setUser($user) 99 + ->setTitle($title) 100 + ->appendChild($message) 101 + ->addCancelButton($uri); 102 + 103 + return id(new AphrontDialogResponse())->setDialog($dialog); 104 + } 105 + 106 + }
+128
src/applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + final class PhabricatorSubscriptionsEditor { 20 + 21 + private $object; 22 + private $user; 23 + 24 + private $explicitSubscribePHIDs = array(); 25 + private $implicitSubscribePHIDs = array(); 26 + private $unsubscribePHIDs = array(); 27 + 28 + public function setObject(PhabricatorSubscribableInterface $object) { 29 + $this->object = $object; 30 + return $this; 31 + } 32 + 33 + public function setUser(PhabricatorUser $user) { 34 + $this->user = $user; 35 + return $this; 36 + } 37 + 38 + 39 + /** 40 + * Add explicit subscribers. These subscribers have explicitly subscribed 41 + * (or been subscribed) to the object, and will be added even if they 42 + * had previously unsubscribed. 43 + * 44 + * @param list<phid> List of PHIDs to explicitly subscribe. 45 + * @return this 46 + */ 47 + public function subscribeExplicit(array $phids) { 48 + $this->explicitSubscribePHIDs += array_fill_keys($phids, true); 49 + return $this; 50 + } 51 + 52 + 53 + /** 54 + * Add implicit subscribers. These subscribers have taken some action which 55 + * implicitly subscribes them (e.g., adding a comment) but it will be 56 + * suppressed if they've previously unsubscribed from the object. 57 + * 58 + * @param list<phid> List of PHIDs to implicitly subscribe. 59 + * @return this 60 + */ 61 + public function subscribeImplicit(array $phids) { 62 + $this->implicitSubscribePHIDs += array_fill_keys($phids, true); 63 + return $this; 64 + } 65 + 66 + 67 + /** 68 + * Unsubscribe PHIDs and mark them as unsubscribed, so implicit subscriptions 69 + * will not resubscribe them. 70 + * 71 + * @param list<phid> List of PHIDs to unsubscribe. 72 + * @return this 73 + */ 74 + public function unsubscribe(array $phids) { 75 + $this->unsubscribePHIDs += array_fill_keys($phids, true); 76 + return $this; 77 + } 78 + 79 + 80 + public function save() { 81 + if (!$this->object) { 82 + throw new Exception('Call setObject() before save()!'); 83 + } 84 + if (!$this->user) { 85 + throw new Exception('Call setUser() before save()!'); 86 + } 87 + 88 + $src = $this->object->getPHID(); 89 + 90 + if ($this->implicitSubscribePHIDs) { 91 + $unsub = PhabricatorEdgeQuery::loadDestinationPHIDs( 92 + $src, 93 + PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER); 94 + $unsub = array_fill_keys($unsub, true); 95 + $this->implicitSubscribePHIDs = array_diff_key( 96 + $this->implicitSubscribePHIDs, 97 + $unsub); 98 + } 99 + 100 + $add = $this->implicitSubscribePHIDs + $this->explicitSubscribePHIDs; 101 + $del = $this->unsubscribePHIDs; 102 + 103 + // If a PHID is marked for both subscription and unsubscription, treat 104 + // unsubscription as the stronger action. 105 + $add = array_diff_key($add, $del); 106 + 107 + if ($add || $del) { 108 + $u_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER; 109 + $s_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_SUBSCRIBER; 110 + 111 + $editor = id(new PhabricatorEdgeEditor()) 112 + ->setUser($this->user); 113 + 114 + foreach ($add as $phid => $ignored) { 115 + $editor->removeEdge($src, $u_type, $phid); 116 + $editor->addEdge($src, $s_type, $phid); 117 + } 118 + 119 + foreach ($del as $phid => $ignored) { 120 + $editor->removeEdge($src, $s_type, $phid); 121 + $editor->addEdge($src, $u_type, $phid); 122 + } 123 + 124 + $editor->save(); 125 + } 126 + } 127 + 128 + }
+100
src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + final class PhabricatorSubscriptionsUIEventListener 20 + extends PhutilEventListener { 21 + 22 + public function register() { 23 + $this->listen(PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS); 24 + } 25 + 26 + public function handleEvent(PhutilEvent $event) { 27 + switch ($event->getType()) { 28 + case PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS: 29 + $this->handleActionEvent($event); 30 + break; 31 + } 32 + } 33 + 34 + private function handleActionEvent($event) { 35 + $user = $event->getUser(); 36 + $object = $event->getValue('object'); 37 + 38 + if (!$object || !$object->getPHID()) { 39 + // No object, or the object has no PHID yet. No way to subscribe. 40 + return; 41 + } 42 + 43 + if (!($object instanceof PhabricatorSubscribableInterface)) { 44 + // This object isn't subscribable. 45 + return; 46 + } 47 + 48 + if ($object->isAutomaticallySubscribed($user->getPHID())) { 49 + $sub_action = id(new PhabricatorActionView()) 50 + ->setWorkflow(true) 51 + ->setUser($user) 52 + ->setDisabled(true) 53 + ->setRenderAsForm(true) 54 + ->setHref('/subscriptions/add/'.$object->getPHID().'/') 55 + ->setName(phutil_escape_html('Automatically Subscribed')) 56 + ->setIcon('subscribe-auto'); 57 + } else { 58 + $subscribed = false; 59 + if ($user->isLoggedIn()) { 60 + $src_phid = $object->getPHID(); 61 + $dst_phid = $user->getPHID(); 62 + $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_SUBSCRIBER; 63 + 64 + $edges = id(new PhabricatorEdgeQuery()) 65 + ->withSourcePHIDs(array($src_phid)) 66 + ->withEdgeTypes(array($edge_type)) 67 + ->withDestinationPHIDs(array($user->getPHID())) 68 + ->execute(); 69 + $subscribed = isset($edges[$src_phid][$edge_type][$dst_phid]); 70 + } 71 + 72 + if ($subscribed) { 73 + $sub_action = id(new PhabricatorActionView()) 74 + ->setUser($user) 75 + ->setWorkflow(true) 76 + ->setRenderAsForm(true) 77 + ->setHref('/subscriptions/delete/'.$object->getPHID().'/') 78 + ->setName(phutil_escape_html('Unsubscribe')) 79 + ->setIcon('subscribe-delete'); 80 + } else { 81 + $sub_action = id(new PhabricatorActionView()) 82 + ->setUser($user) 83 + ->setWorkflow(true) 84 + ->setRenderAsForm(true) 85 + ->setHref('/subscriptions/add/'.$object->getPHID().'/') 86 + ->setName(phutil_escape_html('Subscribe')) 87 + ->setIcon('subscribe-add'); 88 + } 89 + 90 + if (!$user->isLoggedIn()) { 91 + $sub_action->setDisabled(true); 92 + } 93 + } 94 + 95 + $actions = $event->getValue('actions'); 96 + $actions[] = $sub_action; 97 + $event->setValue('actions', $actions); 98 + } 99 + 100 + }
+32
src/applications/subscriptions/interface/PhabricatorSubscribableInterface.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + interface PhabricatorSubscribableInterface { 20 + 21 + /** 22 + * Return true to indicate that the given PHID is automatically subscribed 23 + * to the object (for example, they are the author or in some other way 24 + * irrevocably a subscriber). This will, e.g., cause the UI to render 25 + * "Automatically Subscribed" instead of "Subscribe". 26 + * 27 + * @param PHID PHID (presumably a user) to test for automatic subscription. 28 + * @return bool True if the object/user is automatically subscribed. 29 + */ 30 + public function isAutomaticallySubscribed($phid); 31 + 32 + }
+66
src/applications/subscriptions/query/PhabricatorSubscribersQuery.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + final class PhabricatorSubscribersQuery extends PhabricatorQuery { 20 + 21 + private $objectPHIDs; 22 + private $subscriberPHIDs; 23 + 24 + public static function loadSubscribersForPHID($phid) { 25 + $subscribers = id(new PhabricatorSubscribersQuery()) 26 + ->withObjectPHIDs(array($phid)) 27 + ->execute(); 28 + return $subscribers[$phid]; 29 + } 30 + 31 + public function withObjectPHIDs(array $object_phids) { 32 + $this->objectPHIDs = $object_phids; 33 + return $this; 34 + } 35 + 36 + public function withSubscriberPHIDs(array $subscriber_phids) { 37 + $this->subscriberPHIDs = $subscriber_phids; 38 + return $this; 39 + } 40 + 41 + public function execute() { 42 + $query = new PhabricatorEdgeQuery(); 43 + 44 + $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_SUBSCRIBER; 45 + 46 + $query->withSourcePHIDs($this->objectPHIDs); 47 + $query->withEdgeTypes(array($edge_type)); 48 + 49 + if ($this->subscriberPHIDs) { 50 + $query->withDestinationPHIDs($this->subscriberPHIDs); 51 + } 52 + 53 + $edges = $query->execute(); 54 + 55 + $results = array_fill_keys($this->objectPHIDs, array()); 56 + foreach ($edges as $src => $edge_types) { 57 + foreach ($edge_types[$edge_type] as $dst => $data) { 58 + $results[$src][] = $dst; 59 + } 60 + } 61 + 62 + return $results; 63 + } 64 + 65 + 66 + }
+12
src/infrastructure/edges/constants/PhabricatorEdgeConfig.php
··· 49 49 const TYPE_ANSWER_HAS_VOTING_USER = 19; 50 50 const TYPE_VOTING_USER_HAS_ANSWER = 20; 51 51 52 + const TYPE_OBJECT_HAS_SUBSCRIBER = 21; 53 + const TYPE_SUBSCRIBED_TO_OBJECT = 22; 54 + 55 + const TYPE_OBJECT_HAS_UNSUBSCRIBER = 23; 56 + const TYPE_UNSUBSCRIBED_FROM_OBJECT = 24; 57 + 52 58 const TYPE_TEST_NO_CYCLE = 9000; 53 59 54 60 public static function getInverse($edge_type) { ··· 82 88 self::TYPE_QUESTION_HAS_VOTING_USER, 83 89 self::TYPE_ANSWER_HAS_VOTING_USER => self::TYPE_VOTING_USER_HAS_ANSWER, 84 90 self::TYPE_VOTING_USER_HAS_ANSWER => self::TYPE_ANSWER_HAS_VOTING_USER, 91 + 92 + self::TYPE_OBJECT_HAS_SUBSCRIBER => self::TYPE_SUBSCRIBED_TO_OBJECT, 93 + self::TYPE_SUBSCRIBED_TO_OBJECT => self::TYPE_OBJECT_HAS_SUBSCRIBER, 94 + 95 + self::TYPE_OBJECT_HAS_UNSUBSCRIBER => self::TYPE_UNSUBSCRIBED_FROM_OBJECT, 96 + self::TYPE_UNSUBSCRIBED_FROM_OBJECT => self::TYPE_OBJECT_HAS_UNSUBSCRIBER, 85 97 ); 86 98 87 99 return idx($map, $edge_type);
+12
webroot/rsrc/css/autosprite.css
··· 363 363 .action-flag-ghost { 364 364 background-position: 0px -3444px; 365 365 } 366 + 367 + .action-subscribe-auto { 368 + background-position: 0px -3461px; 369 + } 370 + 371 + .action-subscribe-add { 372 + background-position: 0px -3478px; 373 + } 374 + 375 + .action-subscribe-delete { 376 + background-position: 0px -3495px; 377 + }
webroot/rsrc/image/autosprite.png

This is a binary file and will not be displayed.