@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<?php
2
3/**
4 * @task config Configuring the Query
5 * @task exec Query Execution
6 *
7 * @extends PhabricatorCursorPagedPolicyAwareQuery<PhabricatorFeedStory>
8 */
9final class PhabricatorNotificationQuery
10 extends PhabricatorCursorPagedPolicyAwareQuery {
11
12 private $userPHIDs;
13 private $keys;
14 private $unread;
15
16
17/* -( Configuring the Query )---------------------------------------------- */
18
19
20 public function withUserPHIDs(array $user_phids) {
21 $this->userPHIDs = $user_phids;
22 return $this;
23 }
24
25 public function withKeys(array $keys) {
26 $this->keys = $keys;
27 return $this;
28 }
29
30
31 /**
32 * Filter results by read/unread status. Note that `true` means to return
33 * only unread notifications, while `false` means to return only //read//
34 * notifications. The default is `null`, which returns both.
35 *
36 * @param mixed $unread True or false to filter results by read status. Null
37 * to remove the filter.
38 * @return $this
39 * @task config
40 */
41 public function withUnread($unread) {
42 $this->unread = $unread;
43 return $this;
44 }
45
46
47/* -( Query Execution )---------------------------------------------------- */
48
49
50 protected function loadPage() {
51 $story_table = new PhabricatorFeedStoryData();
52 $notification_table = new PhabricatorFeedStoryNotification();
53
54 $conn = $story_table->establishConnection('r');
55
56 $data = queryfx_all(
57 $conn,
58 'SELECT story.*, notification.hasViewed FROM %R notification
59 JOIN %R story ON notification.chronologicalKey = story.chronologicalKey
60 %Q
61 ORDER BY notification.chronologicalKey DESC
62 %Q',
63 $notification_table,
64 $story_table,
65 $this->buildWhereClause($conn),
66 $this->buildLimitClause($conn));
67
68 return $data;
69 }
70
71 protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
72 $where = parent::buildWhereClauseParts($conn);
73
74 if ($this->userPHIDs !== null) {
75 $where[] = qsprintf(
76 $conn,
77 'notification.userPHID IN (%Ls)',
78 $this->userPHIDs);
79 }
80
81 if ($this->unread !== null) {
82 $where[] = qsprintf(
83 $conn,
84 'notification.hasViewed = %d',
85 (int)!$this->unread);
86 }
87
88 if ($this->keys !== null) {
89 $where[] = qsprintf(
90 $conn,
91 'notification.chronologicalKey IN (%Ls)',
92 $this->keys);
93 }
94
95 return $where;
96 }
97
98 protected function willFilterPage(array $rows) {
99 // See T13623. The policy model here is outdated and awkward.
100
101 // Users may have notifications about objects they can no longer see.
102 // Two ways this can arise: destroy an object; or change an object's
103 // view policy to exclude a user.
104
105 // "PhabricatorFeedStory::loadAllFromRows()" does its own policy filtering.
106 // This doesn't align well with modern query sequencing, but we should be
107 // able to get away with it by loading here.
108
109 // See T13623. Although most queries for notifications return unique
110 // stories, this isn't a guarantee.
111 $story_map = ipull($rows, null, 'chronologicalKey');
112
113 $viewer = $this->getViewer();
114 $stories = PhabricatorFeedStory::loadAllFromRows($story_map, $viewer);
115 $stories = mpull($stories, null, 'getChronologicalKey');
116
117 $results = array();
118 foreach ($rows as $row) {
119 $story_key = $row['chronologicalKey'];
120 $has_viewed = $row['hasViewed'];
121
122 if (!isset($stories[$story_key])) {
123 // NOTE: We can't call "didRejectResult()" here because we don't have
124 // a policy object to pass.
125 continue;
126 }
127
128 $story = id(clone $stories[$story_key])
129 ->setHasViewed($has_viewed);
130
131 if (!$story->isVisibleInNotifications()) {
132 continue;
133 }
134
135 $results[] = $story;
136 }
137
138 return $results;
139 }
140
141 protected function getDefaultOrderVector() {
142 return array('key');
143 }
144
145 public function getBuiltinOrders() {
146 return array(
147 'newest' => array(
148 'vector' => array('key'),
149 'name' => pht('Creation (Newest First)'),
150 'aliases' => array('created'),
151 ),
152 'oldest' => array(
153 'vector' => array('-key'),
154 'name' => pht('Creation (Oldest First)'),
155 ),
156 );
157 }
158
159 public function getOrderableColumns() {
160 return array(
161 'key' => array(
162 'table' => 'notification',
163 'column' => 'chronologicalKey',
164 'type' => 'string',
165 'unique' => true,
166 ),
167 );
168 }
169
170 protected function applyExternalCursorConstraintsToQuery(
171 PhabricatorCursorPagedPolicyAwareQuery $subquery,
172 $cursor) {
173
174 $subquery
175 ->withKeys(array($cursor))
176 ->setLimit(1);
177
178 }
179
180 protected function newExternalCursorStringForResult($object) {
181 return $object->getChronologicalKey();
182 }
183
184 protected function newPagingMapFromPartialObject($object) {
185 return array(
186 'key' => $object['chronologicalKey'],
187 );
188 }
189
190 protected function getPrimaryTableAlias() {
191 return 'notification';
192 }
193
194 public function getQueryApplicationClass() {
195 return PhabricatorNotificationsApplication::class;
196 }
197
198}