Skip to content

Commit 5b74b8b

Browse files
author
epriestley
committed
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
1 parent 1fda844 commit 5b74b8b

File tree

12 files changed

+531
-12
lines changed

12 files changed

+531
-12
lines changed

scripts/celerity/generate_sprites.php

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -169,18 +169,21 @@ function gly($y) {
169169
->setSourceSize(16, 16);
170170

171171
$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',
184187
);
185188

186189
foreach ($action_map as $icon => $source) {

src/__phutil_library_map__.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,7 @@
581581
'PhabricatorApplicationSettings' => 'applications/settings/application/PhabricatorApplicationSettings.php',
582582
'PhabricatorApplicationSlowvote' => 'applications/slowvote/application/PhabricatorApplicationSlowvote.php',
583583
'PhabricatorApplicationStatusView' => 'applications/meta/view/PhabricatorApplicationStatusView.php',
584+
'PhabricatorApplicationSubscriptions' => 'applications/subscriptions/application/PhabricatorApplicationSubscriptions.php',
584585
'PhabricatorApplicationUIExamples' => 'applications/uiexample/application/PhabricatorApplicationUIExamples.php',
585586
'PhabricatorApplicationsListController' => 'applications/meta/controller/PhabricatorApplicationsListController.php',
586587
'PhabricatorAuditActionConstants' => 'applications/audit/constants/PhabricatorAuditActionConstants.php',
@@ -1078,6 +1079,11 @@
10781079
'PhabricatorStorageManagementUpgradeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php',
10791080
'PhabricatorStorageManagementWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php',
10801081
'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',
10811087
'PhabricatorSymbolNameLinter' => 'infrastructure/lint/hook/PhabricatorSymbolNameLinter.php',
10821088
'PhabricatorSyntaxHighlighter' => 'infrastructure/markup/PhabricatorSyntaxHighlighter.php',
10831089
'PhabricatorTaskmasterDaemon' => 'infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php',
@@ -1691,6 +1697,7 @@
16911697
'ManiphestTaskPriority' => 'ManiphestConstants',
16921698
'ManiphestTaskProject' => 'ManiphestDAO',
16931699
'ManiphestTaskProjectsView' => 'ManiphestView',
1700+
'ManiphestTaskQuery' => 'PhabricatorQuery',
16941701
'ManiphestTaskStatus' => 'ManiphestConstants',
16951702
'ManiphestTaskSubscriber' => 'ManiphestDAO',
16961703
'ManiphestTaskSummaryView' => 'ManiphestView',
@@ -1746,6 +1753,7 @@
17461753
'PhabricatorApplicationSettings' => 'PhabricatorApplication',
17471754
'PhabricatorApplicationSlowvote' => 'PhabricatorApplication',
17481755
'PhabricatorApplicationStatusView' => 'AphrontView',
1756+
'PhabricatorApplicationSubscriptions' => 'PhabricatorApplication',
17491757
'PhabricatorApplicationUIExamples' => 'PhabricatorApplication',
17501758
'PhabricatorApplicationsListController' => 'PhabricatorController',
17511759
'PhabricatorAuditAddCommentController' => 'PhabricatorAuditController',
@@ -2185,6 +2193,9 @@
21852193
'PhabricatorStorageManagementStatusWorkflow' => 'PhabricatorStorageManagementWorkflow',
21862194
'PhabricatorStorageManagementUpgradeWorkflow' => 'PhabricatorStorageManagementWorkflow',
21872195
'PhabricatorStorageManagementWorkflow' => 'PhutilArgumentWorkflow',
2196+
'PhabricatorSubscribersQuery' => 'PhabricatorQuery',
2197+
'PhabricatorSubscriptionsEditController' => 'PhabricatorController',
2198+
'PhabricatorSubscriptionsUIEventListener' => 'PhutilEventListener',
21882199
'PhabricatorSymbolNameLinter' => 'ArcanistXHPASTLintNamingHook',
21892200
'PhabricatorTaskmasterDaemon' => 'PhabricatorDaemon',
21902201
'PhabricatorTestCase' => 'ArcanistPhutilTestCase',

src/applications/phid/handle/PhabricatorObjectHandleData.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ public function loadObjects() {
102102
$objects[$revision->getPHID()] = $revision;
103103
}
104104
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;
105113
}
106114
}
107115

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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+
}

0 commit comments

Comments
 (0)