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

Conpherence - paginate thread list

Summary: this is D5750 but just the conpherence part. fixes a few random conpherence bugs / quirks as well. Also messes with ApplicationTransactionEditor to expose the xactions so Conpherence doesn't over-update participation rows. Fixes T2429.

Test Plan: set LIMIT to 3. verified I could scroll down all conpherences. next, picked a conpherence "in the middle" to load. verified I could page up and down. next, picked a conpherence in the middle then had another user update that conpherence. verified as I paged up the conpherence re-loaded properly selected

Reviewers: epriestley

Reviewed By: epriestley

CC: chad, aran, Korvin, vrana

Maniphest Tasks: T2429

Differential Revision: https://secure.phabricator.com/D5783

+570 -193
+4
resources/sql/patches/20130423.conpherenceindices.sql
··· 1 + ALTER TABLE {$NAMESPACE}_conpherence.conpherence_participant 2 + DROP KEY participantPHID, 3 + ADD KEY unreadCount (participantPHID, participationStatus), 4 + ADD KEY participationIndex (participantPHID, dateTouched, id);
+2 -1
src/__celerity_resource_map__.php
··· 1289 1289 ), 1290 1290 'javelin-behavior-conpherence-menu' => 1291 1291 array( 1292 - 'uri' => '/res/ce7bfa44/rsrc/js/application/conpherence/behavior-menu.js', 1292 + 'uri' => '/res/06bfc1a3/rsrc/js/application/conpherence/behavior-menu.js', 1293 1293 'type' => 'js', 1294 1294 'requires' => 1295 1295 array( ··· 1301 1301 5 => 'javelin-workflow', 1302 1302 6 => 'javelin-behavior-device', 1303 1303 7 => 'javelin-history', 1304 + 8 => 'javelin-vector', 1304 1305 ), 1305 1306 'disk' => '/rsrc/js/application/conpherence/behavior-menu.js', 1306 1307 ),
+2
src/__phutil_library_map__.php
··· 238 238 'ConpherenceMenuItemView' => 'applications/conpherence/view/ConpherenceMenuItemView.php', 239 239 'ConpherenceNewController' => 'applications/conpherence/controller/ConpherenceNewController.php', 240 240 'ConpherenceParticipant' => 'applications/conpherence/storage/ConpherenceParticipant.php', 241 + 'ConpherenceParticipantCountQuery' => 'applications/conpherence/query/ConpherenceParticipantCountQuery.php', 241 242 'ConpherenceParticipantQuery' => 'applications/conpherence/query/ConpherenceParticipantQuery.php', 242 243 'ConpherenceParticipationStatus' => 'applications/conpherence/constants/ConpherenceParticipationStatus.php', 243 244 'ConpherencePeopleMenuEventListener' => 'applications/conpherence/events/ConpherencePeopleMenuEventListener.php', ··· 2001 2002 'ConpherenceMenuItemView' => 'AphrontTagView', 2002 2003 'ConpherenceNewController' => 'ConpherenceController', 2003 2004 'ConpherenceParticipant' => 'ConpherenceDAO', 2005 + 'ConpherenceParticipantCountQuery' => 'PhabricatorOffsetPagedQuery', 2004 2006 'ConpherenceParticipantQuery' => 'PhabricatorOffsetPagedQuery', 2005 2007 'ConpherenceParticipationStatus' => 'ConpherenceConstants', 2006 2008 'ConpherencePeopleMenuEventListener' => 'PhutilEventListener',
-56
src/applications/conpherence/controller/ConpherenceController.php
··· 6 6 abstract class ConpherenceController extends PhabricatorController { 7 7 private $conpherences; 8 8 9 - /** 10 - * Try for a full set of unread conpherences, and if we fail 11 - * load read conpherences. Additional conpherences in either category 12 - * are loaded asynchronously. 13 - */ 14 - public function loadStartingConpherences($current_selection_epoch = null) { 15 - $user = $this->getRequest()->getUser(); 16 - 17 - $read_participant_query = id(new ConpherenceParticipantQuery()) 18 - ->withParticipantPHIDs(array($user->getPHID())); 19 - $read_status = ConpherenceParticipationStatus::UP_TO_DATE; 20 - if ($current_selection_epoch) { 21 - $read_one = $read_participant_query 22 - ->withParticipationStatus($read_status) 23 - ->withDateTouched($current_selection_epoch, '>') 24 - ->execute(); 25 - 26 - $read_two = $read_participant_query 27 - ->withDateTouched($current_selection_epoch, '<=') 28 - ->execute(); 29 - 30 - $read = array_merge($read_one, $read_two); 31 - 32 - } else { 33 - $read = $read_participant_query 34 - ->withParticipationStatus($read_status) 35 - ->execute(); 36 - } 37 - 38 - $unread_status = ConpherenceParticipationStatus::BEHIND; 39 - $unread = id(new ConpherenceParticipantQuery()) 40 - ->withParticipantPHIDs(array($user->getPHID())) 41 - ->withParticipationStatus($unread_status) 42 - ->execute(); 43 - 44 - $all_participation = $unread + $read; 45 - $all_conpherence_phids = array_keys($all_participation); 46 - $all_conpherences = array(); 47 - if ($all_conpherence_phids) { 48 - $all_conpherences = id(new ConpherenceThreadQuery()) 49 - ->setViewer($user) 50 - ->withPHIDs($all_conpherence_phids) 51 - ->needParticipantCache(true) 52 - ->execute(); 53 - } 54 - $unread_conpherences = array_select_keys( 55 - $all_conpherences, 56 - array_keys($unread)); 57 - 58 - $read_conpherences = array_select_keys( 59 - $all_conpherences, 60 - array_keys($read)); 61 - 62 - return array($unread_conpherences, $read_conpherences); 63 - } 64 - 65 9 public function buildApplicationMenu() { 66 10 $nav = new PhabricatorMenuView(); 67 11
+206 -34
src/applications/conpherence/controller/ConpherenceListController.php
··· 6 6 final class ConpherenceListController 7 7 extends ConpherenceController { 8 8 9 + const SELECTED_MODE = 'selected'; 10 + const UNSELECTED_MODE = 'unselected'; 11 + const PAGING_MODE = 'paging'; 12 + 9 13 private $conpherenceID; 10 14 11 15 public function setConpherenceID($conpherence_id) { ··· 20 24 $this->setConpherenceID(idx($data, 'id')); 21 25 } 22 26 27 + /** 28 + * Three main modes of operation... 29 + * 30 + * 1 - /conpherence/ - UNSELECTED_MODE 31 + * 2 - /conpherence/<id>/ - SELECTED_MODE 32 + * 3 - /conpherence/?direction='up'&... - PAGING_MODE 33 + * 34 + * UNSELECTED_MODE is not an Ajax request while the other two are Ajax 35 + * requests. 36 + */ 37 + private function determineMode() { 38 + $request = $this->getRequest(); 39 + 40 + $mode = self::UNSELECTED_MODE; 41 + if ($request->isAjax()) { 42 + if ($request->getStr('direction')) { 43 + $mode = self::PAGING_MODE; 44 + } else { 45 + $mode = self::SELECTED_MODE; 46 + } 47 + } 48 + 49 + return $mode; 50 + } 23 51 public function processRequest() { 24 52 $request = $this->getRequest(); 25 53 $user = $request->getUser(); 26 54 $title = pht('Conpherence'); 27 - 28 - $conpherence_id = $this->getConpherenceID(); 29 - $current_selection_epoch = null; 30 55 $conpherence = null; 31 - if ($conpherence_id) { 32 - $conpherence = id(new ConpherenceThreadQuery()) 33 - ->setViewer($user) 34 - ->withIDs(array($conpherence_id)) 35 - ->executeOne(); 36 - if (!$conpherence) { 37 - return new Aphront404Response(); 38 - } 39 56 40 - if ($conpherence->getTitle()) { 41 - $title = $conpherence->getTitle(); 42 - } 57 + $scroll_up_participant = $this->getEmptyParticipant(); 58 + $scroll_down_participant = $this->getEmptyParticipant(); 59 + $too_many = ConpherenceParticipantQuery::LIMIT + 1; 60 + $all_participation = array(); 43 61 44 - $participant = $conpherence->getParticipant($user->getPHID()); 45 - $current_selection_epoch = $participant->getDateTouched(); 62 + $mode = $this->determineMode(); 63 + switch ($mode) { 64 + case self::SELECTED_MODE: 65 + $conpherence_id = $this->getConpherenceID(); 66 + $conpherence = id(new ConpherenceThreadQuery()) 67 + ->setViewer($user) 68 + ->withIDs(array($conpherence_id)) 69 + ->executeOne(); 70 + if (!$conpherence) { 71 + return new Aphront404Response(); 72 + } 73 + if ($conpherence->getTitle()) { 74 + $title = $conpherence->getTitle(); 75 + } 76 + $cursor = $conpherence->getParticipant($user->getPHID()); 77 + $data = $this->loadParticipationWithMidCursor($cursor); 78 + $all_participation = $data['participation']; 79 + $scroll_up_participant = $data['scroll_up_participant']; 80 + $scroll_down_participant = $data['scroll_down_participant']; 81 + break; 82 + case self::PAGING_MODE: 83 + $direction = $request->getStr('direction'); 84 + $id = $request->getInt('participant_id'); 85 + $date_touched = $request->getInt('date_touched'); 86 + $conpherence_phid = $request->getStr('conpherence_phid'); 87 + if ($direction == 'up') { 88 + $order = ConpherenceParticipantQuery::ORDER_NEWER; 89 + } else { 90 + $order = ConpherenceParticipantQuery::ORDER_OLDER; 91 + } 92 + $scroller_participant = id(new ConpherenceParticipant()) 93 + ->makeEphemeral() 94 + ->setID($id) 95 + ->setDateTouched($date_touched) 96 + ->setConpherencePHID($conpherence_phid); 97 + $participation = id(new ConpherenceParticipantQuery()) 98 + ->withParticipantPHIDs(array($user->getPHID())) 99 + ->withParticipantCursor($scroller_participant) 100 + ->setOrder($order) 101 + ->setLimit($too_many) 102 + ->execute(); 103 + if (count($participation) == $too_many) { 104 + if ($direction == 'up') { 105 + $node = $scroll_up_participant = reset($participation); 106 + } else { 107 + $node = $scroll_down_participant = end($participation); 108 + } 109 + unset($participation[$node->getConpherencePHID()]); 110 + } 111 + $all_participation = $participation; 112 + break; 113 + case self::UNSELECTED_MODE: 114 + default: 115 + $too_many = ConpherenceParticipantQuery::LIMIT + 1; 116 + $all_participation = id(new ConpherenceParticipantQuery()) 117 + ->withParticipantPHIDs(array($user->getPHID())) 118 + ->setLimit($too_many) 119 + ->execute(); 120 + if (count($all_participation) == $too_many) { 121 + $node = end($participation); 122 + unset($all_participation[$node->getConpherencePHID()]); 123 + $scroll_down_participant = $node; 124 + } 125 + break; 46 126 } 47 127 48 - list($unread, $read) = $this->loadStartingConpherences( 49 - $current_selection_epoch); 128 + $threads = $this->loadConpherenceThreadData( 129 + $all_participation); 50 130 51 131 $thread_view = id(new ConpherenceThreadListView()) 52 132 ->setUser($user) 53 133 ->setBaseURI($this->getApplicationURI()) 54 - ->setUnreadThreads($unread) 55 - ->setReadThreads($read); 134 + ->setThreads($threads) 135 + ->setScrollUpParticipant($scroll_up_participant) 136 + ->setScrollDownParticipant($scroll_down_participant); 56 137 57 - if ($request->isAjax()) { 58 - return id(new AphrontAjaxResponse())->setContent($thread_view); 138 + switch ($mode) { 139 + case self::SELECTED_MODE: 140 + $response = id(new AphrontAjaxResponse())->setContent($thread_view); 141 + break; 142 + case self::PAGING_MODE: 143 + $thread_html = $thread_view->renderThreadsHTML(); 144 + $phids = array_keys($participation); 145 + $content = array( 146 + 'html' => $thread_html, 147 + 'phids' => $phids); 148 + $response = id(new AphrontAjaxResponse())->setContent($content); 149 + break; 150 + case self::UNSELECTED_MODE: 151 + default: 152 + $layout = id(new ConpherenceLayoutView()) 153 + ->setBaseURI($this->getApplicationURI()) 154 + ->setThreadView($thread_view) 155 + ->setRole('list'); 156 + if ($conpherence) { 157 + $layout->setThread($conpherence); 158 + } 159 + $response = $this->buildApplicationPage( 160 + $layout, 161 + array( 162 + 'title' => $title, 163 + 'device' => true, 164 + )); 165 + break; 59 166 } 60 167 61 - $layout = id(new ConpherenceLayoutView()) 62 - ->setBaseURI($this->getApplicationURI()) 63 - ->setThreadView($thread_view) 64 - ->setRole('list'); 168 + return $response; 169 + 170 + } 65 171 66 - if ($conpherence) { 67 - $layout->setThread($conpherence); 172 + /** 173 + * Handles the curious case when we are visiting a conpherence directly 174 + * by issuing two separate queries. Otherwise, additional conpherences 175 + * are fetched asynchronously. Note these can be earlier or later 176 + * (up or down), depending on what conpherence was selected on initial 177 + * load. 178 + */ 179 + private function loadParticipationWithMidCursor( 180 + ConpherenceParticipant $cursor) { 181 + 182 + $user = $this->getRequest()->getUser(); 183 + 184 + $scroll_up_participant = $this->getEmptyParticipant(); 185 + $scroll_down_participant = $this->getEmptyParticipant(); 186 + 187 + // Note this is a bit dodgy since there may be less than this 188 + // amount in either the up or down direction, thus having us fail 189 + // to fetch LIMIT in total. Whatevs for now and re-visit if we're 190 + // fine-tuning this loading process. 191 + $too_many = ceil(ConpherenceParticipantQuery::LIMIT / 2) + 1; 192 + $participant_query = id(new ConpherenceParticipantQuery()) 193 + ->withParticipantPHIDs(array($user->getPHID())) 194 + ->setLimit($too_many); 195 + $current_selection_epoch = $cursor->getDateTouched(); 196 + $set_one = $participant_query 197 + ->withParticipantCursor($cursor) 198 + ->setOrder(ConpherenceParticipantQuery::ORDER_NEWER) 199 + ->execute(); 200 + 201 + if (count($set_one) == $too_many) { 202 + $node = reset($set_one); 203 + unset($set_one[$node->getConpherencePHID()]); 204 + $scroll_up_participant = $node; 68 205 } 69 206 70 - return $this->buildApplicationPage( 71 - $layout, 72 - array( 73 - 'title' => $title, 74 - 'device' => true, 75 - )); 207 + $set_two = $participant_query 208 + ->withParticipantCursor($cursor) 209 + ->setOrder(ConpherenceParticipantQuery::ORDER_OLDER) 210 + ->execute(); 211 + 212 + if (count($set_two) == $too_many) { 213 + $node = end($set_two); 214 + unset($set_two[$node->getConpherencePHID()]); 215 + $scroll_down_participant = $node; 216 + } 217 + 218 + $participation = array_merge( 219 + $set_one, 220 + $set_two); 221 + 222 + return array( 223 + 'scroll_up_participant' => $scroll_up_participant, 224 + 'scroll_down_participant' => $scroll_down_participant, 225 + 'participation' => $participation); 226 + } 227 + 228 + private function loadConpherenceThreadData($participation) { 229 + $user = $this->getRequest()->getUser(); 230 + $conpherence_phids = array_keys($participation); 231 + if ($conpherence_phids) { 232 + $conpherences = id(new ConpherenceThreadQuery()) 233 + ->setViewer($user) 234 + ->withPHIDs($conpherence_phids) 235 + ->needParticipantCache(true) 236 + ->execute(); 237 + } 238 + 239 + // this will re-sort by participation data 240 + $conpherences = array_select_keys($conpherences, $conpherence_phids); 241 + 242 + return $conpherences; 243 + } 244 + 245 + private function getEmptyParticipant() { 246 + return id(new ConpherenceParticipant()) 247 + ->makeEphemeral(); 76 248 } 77 249 78 250 }
+3
src/applications/conpherence/controller/ConpherenceViewController.php
··· 50 50 ->setBeforeTransactionID($before_transaction_id); 51 51 } 52 52 $conpherence = $query->executeOne(); 53 + if (!$conpherence) { 54 + return new Aphront404Response(); 55 + } 53 56 $this->setConpherence($conpherence); 54 57 55 58 $participant = $conpherence->getParticipant($user->getPHID());
+31 -29
src/applications/conpherence/editor/ConpherenceEditor.php
··· 140 140 $object->setRecentParticipantPHIDs($participants); 141 141 } 142 142 143 - /** 144 - * For now this only supports adding more files and participants. 145 - */ 146 143 protected function applyCustomExternalTransaction( 147 144 PhabricatorLiskDAO $object, 148 145 PhabricatorApplicationTransaction $xaction) { ··· 169 166 $file_phid); 170 167 } 171 168 $editor->save(); 172 - // fallthrough 173 - case PhabricatorTransactions::TYPE_COMMENT: 174 - $xaction_phid = $xaction->getPHID(); 175 - $behind = ConpherenceParticipationStatus::BEHIND; 176 - $up_to_date = ConpherenceParticipationStatus::UP_TO_DATE; 177 - $participants = $object->getParticipants(); 178 - $user = $this->getActor(); 179 - $time = time(); 180 - foreach ($participants as $phid => $participant) { 181 - if ($phid != $user->getPHID()) { 182 - if ($participant->getParticipationStatus() != $behind) { 183 - $participant->setBehindTransactionPHID($xaction_phid); 184 - // decrement one as this is the message putting them behind! 185 - $participant->setSeenMessageCount($object->getMessageCount() - 1); 186 - } 187 - $participant->setParticipationStatus($behind); 188 - $participant->setDateTouched($time); 189 - } else { 190 - $participant->setSeenMessageCount($object->getMessageCount()); 191 - $participant->setParticipationStatus($up_to_date); 192 - $participant->setDateTouched($time); 193 - } 194 - $participant->save(); 195 - } 196 169 break; 197 170 case ConpherenceTransactionType::TYPE_PARTICIPANTS: 198 - 199 171 $participants = $object->getParticipants(); 200 172 201 173 $old_map = array_fuse($xaction->getOldValue()); ··· 229 201 } 230 202 $object->attachParticipants($participants); 231 203 break; 232 - } 204 + } 205 + } 206 + 207 + protected function applyFinalEffects( 208 + PhabricatorLiskDAO $object, 209 + array $xactions) { 210 + 211 + // update everyone's participation status on the last xaction -only- 212 + $xaction = end($xactions); 213 + $xaction_phid = $xaction->getPHID(); 214 + $behind = ConpherenceParticipationStatus::BEHIND; 215 + $up_to_date = ConpherenceParticipationStatus::UP_TO_DATE; 216 + $participants = $object->getParticipants(); 217 + $user = $this->getActor(); 218 + $time = time(); 219 + foreach ($participants as $phid => $participant) { 220 + if ($phid != $user->getPHID()) { 221 + if ($participant->getParticipationStatus() != $behind) { 222 + $participant->setBehindTransactionPHID($xaction_phid); 223 + // decrement one as this is the message putting them behind! 224 + $participant->setSeenMessageCount($object->getMessageCount() - 1); 225 + } 226 + $participant->setParticipationStatus($behind); 227 + $participant->setDateTouched($time); 228 + } else { 229 + $participant->setSeenMessageCount($object->getMessageCount()); 230 + $participant->setParticipationStatus($up_to_date); 231 + $participant->setDateTouched($time); 232 + } 233 + $participant->save(); 234 + } 233 235 } 234 236 235 237 protected function mergeTransactions(
+76
src/applications/conpherence/query/ConpherenceParticipantCountQuery.php
··· 1 + <?php 2 + 3 + /** 4 + * Query class that answers the question: 5 + * 6 + * - Q: How many unread conpherences am I participating in? 7 + * - A: 8 + * id(new ConpherenceParticipantCountQuery()) 9 + * ->withParticipantPHIDs(array($my_phid)) 10 + * ->withParticipationStatus(ConpherenceParticipationStatus::BEHIND) 11 + * ->execute(); 12 + * 13 + * @group conpherence 14 + */ 15 + final class ConpherenceParticipantCountQuery 16 + extends PhabricatorOffsetPagedQuery { 17 + 18 + private $participantPHIDs; 19 + private $participationStatus; 20 + 21 + public function withParticipantPHIDs(array $phids) { 22 + $this->participantPHIDs = $phids; 23 + return $this; 24 + } 25 + 26 + public function withParticipationStatus($participation_status) { 27 + $this->participationStatus = $participation_status; 28 + return $this; 29 + } 30 + 31 + public function execute() { 32 + $table = new ConpherenceParticipant(); 33 + $conn_r = $table->establishConnection('r'); 34 + 35 + $rows = queryfx_all( 36 + $conn_r, 37 + 'SELECT COUNT(*) as count, participantPHID '. 38 + 'FROM %T participant %Q %Q %Q', 39 + $table->getTableName(), 40 + $this->buildWhereClause($conn_r), 41 + $this->buildGroupByClause($conn_r), 42 + $this->buildLimitClause($conn_r)); 43 + 44 + return ipull($rows, 'count', 'participantPHID'); 45 + } 46 + 47 + private function buildWhereClause($conn_r) { 48 + $where = array(); 49 + 50 + if ($this->participantPHIDs) { 51 + $where[] = qsprintf( 52 + $conn_r, 53 + 'participantPHID IN (%Ls)', 54 + $this->participantPHIDs); 55 + } 56 + 57 + if ($this->participationStatus !== null) { 58 + $where[] = qsprintf( 59 + $conn_r, 60 + 'participationStatus = %d', 61 + $this->participationStatus); 62 + } 63 + 64 + return $this->formatWhereClause($where); 65 + } 66 + 67 + private function buildGroupByClause(AphrontDatabaseConnection $conn_r) { 68 + 69 + $group_by = qsprintf( 70 + $conn_r, 71 + 'GROUP BY participantPHID'); 72 + 73 + return $group_by; 74 + } 75 + 76 + }
+71 -35
src/applications/conpherence/query/ConpherenceParticipantQuery.php
··· 1 1 <?php 2 2 3 3 /** 4 + * Query class that answers these questions: 5 + * 6 + * - Q: What are the conpherences to show when I land on /conpherence/ ? 7 + * - A: 8 + * 9 + * id(new ConpherenceParticipantQuery()) 10 + * ->withParticipantPHIDs(array($my_phid)) 11 + * ->execute(); 12 + * 13 + * - Q: What are the next set of conpherences as I scroll up (more recent) or 14 + * down (less recent) this list of conpherences? 15 + * - A: 16 + * 17 + * id(new ConpherenceParticipantQuery()) 18 + * ->withParticipantPHIDs(array($my_phid)) 19 + * ->withParticipantCursor($top_participant) 20 + * ->setOrder(ConpherenceParticipantQuery::ORDER_NEWER) 21 + * ->execute(); 22 + * 23 + * -or- 24 + * 25 + * id(new ConpherenceParticipantQuery()) 26 + * ->withParticipantPHIDs(array($my_phid)) 27 + * ->withParticipantCursor($bottom_participant) 28 + * ->setOrder(ConpherenceParticipantQuery::ORDER_OLDER) 29 + * ->execute(); 30 + * 31 + * For counts of read, un-read, or all conpherences by participant, see 32 + * @{class:ConpherenceParticipantCountQuery}. 33 + * 4 34 * @group conpherence 5 35 */ 6 36 final class ConpherenceParticipantQuery 7 37 extends PhabricatorOffsetPagedQuery { 8 38 9 - private $conpherencePHIDs; 10 - private $participantPHIDs; 11 - private $dateTouched; 12 - private $dateTouchedSort; 13 - private $participationStatus; 39 + const LIMIT = 100; 40 + const ORDER_NEWER = 'newer'; 41 + const ORDER_OLDER = 'older'; 14 42 15 - public function withConpherencePHIDs(array $phids) { 16 - $this->conpherencePHIDs = $phids; 17 - return $this; 18 - } 43 + private $participantPHIDs; 44 + private $participantCursor; 45 + private $order = self::ORDER_OLDER; 19 46 20 47 public function withParticipantPHIDs(array $phids) { 21 48 $this->participantPHIDs = $phids; 22 49 return $this; 23 50 } 24 51 25 - public function withDateTouched($date, $sort = null) { 26 - $this->dateTouched = $date; 27 - $this->dateTouchedSort = $sort ? $sort : '<'; 52 + public function withParticipantCursor(ConpherenceParticipant $participant) { 53 + $this->participantCursor = $participant; 28 54 return $this; 29 55 } 30 56 31 - public function withParticipationStatus($participation_status) { 32 - $this->participationStatus = $participation_status; 57 + public function setOrder($order) { 58 + $this->order = $order; 33 59 return $this; 34 60 } 35 61 ··· 49 75 50 76 $participants = mpull($participants, null, 'getConpherencePHID'); 51 77 78 + if ($this->order == self::ORDER_NEWER) { 79 + $participants = array_reverse($participants); 80 + } 81 + 52 82 return $participants; 53 83 } 54 84 55 85 private function buildWhereClause($conn_r) { 56 86 $where = array(); 57 87 58 - if ($this->conpherencePHIDs) { 59 - $where[] = qsprintf( 60 - $conn_r, 61 - 'conpherencePHID IN (%Ls)', 62 - $this->conpherencePHIDs); 63 - } 64 - 65 88 if ($this->participantPHIDs) { 66 89 $where[] = qsprintf( 67 90 $conn_r, ··· 69 92 $this->participantPHIDs); 70 93 } 71 94 72 - if ($this->participationStatus !== null) { 95 + if ($this->participantCursor) { 96 + $date_touched = $this->participantCursor->getDateTouched(); 97 + $id = $this->participantCursor->getID(); 98 + if ($this->order == self::ORDER_OLDER) { 99 + $compare_date = '<'; 100 + $compare_id = '<='; 101 + } else { 102 + $compare_date = '>'; 103 + $compare_id = '>='; 104 + } 73 105 $where[] = qsprintf( 74 106 $conn_r, 75 - 'participationStatus = %d', 76 - $this->participationStatus); 77 - } 78 - 79 - if ($this->dateTouched) { 80 - if ($this->dateTouchedSort) { 81 - $where[] = qsprintf( 82 - $conn_r, 83 - 'dateTouched %Q %d', 84 - $this->dateTouchedSort, 85 - $this->dateTouched); 86 - } 107 + '(dateTouched %Q %d OR (dateTouched = %d AND id %Q %d))', 108 + $compare_date, 109 + $date_touched, 110 + $date_touched, 111 + $compare_id, 112 + $id); 87 113 } 88 114 89 115 return $this->formatWhereClause($where); 90 116 } 91 117 92 118 private function buildOrderClause(AphrontDatabaseConnection $conn_r) { 93 - return 'ORDER BY dateTouched DESC'; 119 + 120 + $order_word = ($this->order == self::ORDER_OLDER) ? 'DESC' : 'ASC'; 121 + // if these are different direction we won't get as efficient a query 122 + // see http://dev.mysql.com/doc/refman/5.5/en/order-by-optimization.html 123 + $order = qsprintf( 124 + $conn_r, 125 + 'ORDER BY dateTouched %Q, id %Q', 126 + $order_word, 127 + $order_word); 128 + 129 + return $order; 94 130 } 95 131 96 132 }
+78 -20
src/applications/conpherence/view/ConpherenceThreadListView.php
··· 3 3 final class ConpherenceThreadListView extends AphrontView { 4 4 5 5 private $baseURI; 6 - private $unreadThreads; 7 - private $readThreads; 6 + private $threads; 7 + private $scrollUpParticipant; 8 + private $scrollDownParticipant; 9 + 10 + public function setThreads(array $threads) { 11 + assert_instances_of($threads, 'ConpherenceThread'); 12 + $this->threads = $threads; 13 + return $this; 14 + } 8 15 9 - public function setBaseURI($base_uri) { 10 - $this->baseURI = $base_uri; 16 + public function setScrollUpParticipant( 17 + ConpherenceParticipant $participant) { 18 + $this->scrollUpParticipant = $participant; 11 19 return $this; 12 20 } 13 21 14 - public function setUnreadThreads(array $unread_threads) { 15 - assert_instances_of($unread_threads, 'ConpherenceThread'); 16 - $this->unreadThreads = $unread_threads; 22 + public function setScrollDownParticipant( 23 + ConpherenceParticipant $participant) { 24 + $this->scrollDownParticipant = $participant; 17 25 return $this; 18 26 } 19 27 20 - public function setReadThreads(array $read_threads) { 21 - assert_instances_of($read_threads, 'ConpherenceThread'); 22 - $this->readThreads = $read_threads; 28 + public function setBaseURI($base_uri) { 29 + $this->baseURI = $base_uri; 23 30 return $this; 24 31 } 25 32 ··· 39 46 ->setHref($this->baseURI.'new/') 40 47 ->setType(PhabricatorMenuItemView::TYPE_BUTTON)); 41 48 42 - $menu->newLabel(pht('Unread')); 43 - $this->addThreadsToMenu($menu, $this->unreadThreads, $read = false); 44 - $menu->newLabel(pht('Read')); 45 - $this->addThreadsToMenu($menu, $this->readThreads, $read = true); 49 + $menu->newLabel(''); 50 + $this->addThreadsToMenu($menu, $this->threads); 46 51 47 52 return $menu; 48 53 } ··· 51 56 return $this->renderThread($thread); 52 57 } 53 58 59 + public function renderThreadsHTML() { 60 + $thread_html = array(); 61 + 62 + if ($this->scrollUpParticipant->getID()) { 63 + $thread_html[] = $this->getScrollMenuItem( 64 + $this->scrollUpParticipant, 65 + 'up'); 66 + } 67 + 68 + foreach ($this->threads as $thread) { 69 + $thread_html[] = $this->renderSingleThread($thread); 70 + } 71 + 72 + if ($this->scrollDownParticipant->getID()) { 73 + $thread_html[] = $this->getScrollMenuItem( 74 + $this->scrollDownParticipant, 75 + 'down'); 76 + } 77 + 78 + return phutil_implode_html('', $thread_html); 79 + } 80 + 54 81 private function renderThreadItem(ConpherenceThread $thread) { 55 82 return id(new PhabricatorMenuItemView()) 56 83 ->setType(PhabricatorMenuItemView::TYPE_CUSTOM) ··· 87 114 88 115 private function addThreadsToMenu( 89 116 PhabricatorMenuView $menu, 90 - array $conpherences, 91 - $read = false) { 117 + array $conpherences) { 118 + 119 + if ($this->scrollUpParticipant->getID()) { 120 + $item = $this->getScrollMenuItem($this->scrollUpParticipant, 'up'); 121 + $menu->addMenuItem($item); 122 + } 92 123 93 124 foreach ($conpherences as $conpherence) { 94 125 $item = $this->renderThreadItem($conpherence); 95 126 $menu->addMenuItem($item); 96 127 } 97 128 98 - if (empty($conpherences) || $read) { 99 - $menu->addMenuItem($this->getNoConpherencesBlock()); 129 + if (empty($conpherences)) { 130 + $menu->addMenuItem($this->getNoConpherencesMenuItem()); 131 + } 132 + 133 + if ($this->scrollDownParticipant->getID()) { 134 + $item = $this->getScrollMenuItem($this->scrollDownParticipant, 'down'); 135 + $menu->addMenuItem($item); 100 136 } 101 137 102 138 return $menu; 103 139 } 104 140 105 - private function getNoConpherencesBlock() { 141 + public function getScrollMenuItem( 142 + ConpherenceParticipant $participant, 143 + $direction) { 144 + 145 + if ($direction == 'up') { 146 + $name = pht('Load Newer Threads'); 147 + } else { 148 + $name = pht('Load Older Threads'); 149 + } 150 + $item = id(new PhabricatorMenuItemView()) 151 + ->addSigil('conpherence-menu-scroller') 152 + ->setName($name) 153 + ->setHref($this->baseURI) 154 + ->setType(PhabricatorMenuItemView::TYPE_BUTTON) 155 + ->setMetadata(array( 156 + 'participant_id' => $participant->getID(), 157 + 'conpherence_phid' => $participant->getConpherencePHID(), 158 + 'date_touched' => $participant->getDateTouched(), 159 + 'direction' => $direction)); 160 + return $item; 161 + } 162 + 163 + private function getNoConpherencesMenuItem() { 106 164 $message = phutil_tag( 107 165 'div', 108 166 array( 109 167 'class' => 'no-conpherences-menu-item' 110 168 ), 111 - pht('No more conpherences.')); 169 + pht('No conpherences.')); 112 170 113 171 return id(new PhabricatorMenuItemView()) 114 172 ->setType(PhabricatorMenuItemView::TYPE_CUSTOM)
+7
src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
··· 257 257 throw new Exception("Capability not supported!"); 258 258 } 259 259 260 + protected function applyFinalEffects( 261 + PhabricatorLiskDAO $object, 262 + array $xactions) { 263 + } 264 + 260 265 public function setContentSource(PhabricatorContentSource $content_source) { 261 266 $this->contentSource = $content_source; 262 267 return $this; ··· 385 390 foreach ($xactions as $xaction) { 386 391 $this->applyExternalEffects($object, $xaction); 387 392 } 393 + 394 + $this->applyFinalEffects($object, $xactions); 388 395 389 396 if ($read_locking) { 390 397 $object->endReadLocking();
+4
src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
··· 1250 1250 'type' => 'sql', 1251 1251 'name' => $this->getPatchPath('20130423.phortunepaymentrevised.sql'), 1252 1252 ), 1253 + '20130423.conpherenceindices.sql' => array( 1254 + 'type' => 'sql', 1255 + 'name' => $this->getPatchPath('20130423.conpherenceindices.sql'), 1256 + ), 1253 1257 ); 1254 1258 } 1255 1259 }
+2 -2
src/view/page/menu/PhabricatorMainMenuView.php
··· 267 267 $message_count_id = celerity_generate_unique_node_id(); 268 268 269 269 $unread_status = ConpherenceParticipationStatus::BEHIND; 270 - $unread = id(new ConpherenceParticipantQuery()) 270 + $unread = id(new ConpherenceParticipantCountQuery()) 271 271 ->withParticipantPHIDs(array($user->getPHID())) 272 272 ->withParticipationStatus($unread_status) 273 273 ->execute(); 274 - $message_count_number = count($unread); 274 + $message_count_number = $unread[$user->getPHID()]; 275 275 if ($message_count_number > 999) { 276 276 $message_count_number = "\xE2\x88\x9E"; 277 277 }
+84 -16
webroot/rsrc/js/application/conpherence/behavior-menu.js
··· 8 8 * javelin-workflow 9 9 * javelin-behavior-device 10 10 * javelin-history 11 + * javelin-vector 11 12 */ 12 13 13 14 JX.behavior('conpherence-menu', function(config) { ··· 53 54 redrawthread(); 54 55 } 55 56 57 + JX.Stratcom.listen( 58 + 'conpherence-selectthread', 59 + null, 60 + function (e) { 61 + var node = JX.$(e.getData().id); 62 + selectthread(node); 63 + } 64 + ); 65 + 56 66 function updatepagedata(data) { 57 67 var uri_suffix = thread.selected + '/'; 58 68 if (data.use_base_uri) { ··· 75 85 } 76 86 ); 77 87 78 - JX.Stratcom.listen( 79 - 'conpherence-selectthread', 80 - null, 81 - function (e) { 82 - var node = JX.$(e.getData().id); 83 - selectthread(node); 84 - } 85 - ); 86 - 87 88 function redrawthread() { 88 89 if (!thread.node) { 89 90 return; ··· 96 97 var data = JX.Stratcom.getData(thread.node); 97 98 98 99 if (thread.visible !== null || !config.hasThread) { 99 - var uri = config.base_uri + data.id + '/'; 100 + var uri = config.base_uri + data.id + '/'; 100 101 new JX.Workflow(uri, {}) 101 - .setHandler(onresponse) 102 + .setHandler(onloadthreadresponse) 102 103 .start(); 103 104 } else { 104 105 didredrawthread(); ··· 154 155 } 155 156 } 156 157 157 - function onresponse(response) { 158 + function onloadthreadresponse(response) { 158 159 var header = JX.$H(response.header); 159 160 var messages = JX.$H(response.messages); 160 161 var form = JX.$H(response.form); ··· 252 253 }).setData({ oldest_transaction_id : oldest_transaction_id }).send(); 253 254 }); 254 255 255 - 256 256 // On mobile, we just show a thread list, so we don't want to automatically 257 257 // select or load any threads. On Desktop, we automatically select the first 258 258 // thread. 259 - 260 259 var old_device = null; 261 260 function ondevicechange() { 262 261 var new_device = JX.Device.getDevice(); ··· 284 283 function loadthreads() { 285 284 var uri = config.base_uri + 'thread/' + config.selectedID + '/'; 286 285 new JX.Workflow(uri) 287 - .setHandler(onthreadresponse) 286 + .setHandler(onloadthreadsresponse) 288 287 .start(); 289 288 } 290 289 291 - function onthreadresponse(r) { 290 + function onloadthreadsresponse(r) { 292 291 var layout = JX.$(config.layoutID); 293 292 var menu = JX.DOM.find(layout, 'div', 'conpherence-menu-pane'); 294 293 JX.DOM.setContent(menu, JX.$H(r)); 295 294 296 295 config.selectedID && selectthreadid(config.selectedID); 296 + 297 + thread.node.scrollIntoView(); 297 298 } 298 299 299 300 function didloadthreads() { ··· 315 316 } 316 317 redrawthread(); 317 318 } 319 + 320 + var handlethreadscrollers = function (e) { 321 + e.kill(); 322 + 323 + var data = e.getNodeData('conpherence-menu-scroller'); 324 + var scroller = e.getNode('conpherence-menu-scroller'); 325 + new JX.Workflow(scroller.href, data) 326 + .setHandler( 327 + JX.bind(null, threadscrollerresponse, scroller, data.direction)) 328 + .start(); 329 + }; 330 + 331 + var threadscrollerresponse = function (scroller, direction, r) { 332 + var html = JX.$H(r.html); 333 + 334 + var threadPhids = r.phids; 335 + var reselectId = null; 336 + // remove any threads that are in the list that we just got back 337 + // in the result set; things have changed and they'll be in the 338 + // right place soon 339 + for (var ii = 0; ii < threadPhids.length; ii++) { 340 + try { 341 + var nodeId = threadPhids[ii] + '-nav-item'; 342 + var node = JX.$(nodeId); 343 + var nodeData = JX.Stratcom.getData(node); 344 + if (nodeData.id == thread.selected) { 345 + reselectId = nodeId; 346 + } 347 + JX.DOM.remove(node); 348 + } catch (ex) { 349 + // ignore , just haven't seen this thread yet 350 + } 351 + } 352 + 353 + var root = JX.DOM.find(document, 'div', 'conpherence-layout'); 354 + var menuRoot = JX.DOM.find(root, 'div', 'conpherence-menu-pane'); 355 + var scrollY = 0; 356 + // we have to do some hyjinx in the up case to make the menu scroll to 357 + // where it should 358 + if (direction == 'up') { 359 + var style = { 360 + position: 'absolute', 361 + left: '-10000px' 362 + }; 363 + var test_size = JX.$N('div', {style: style}, html); 364 + document.body.appendChild(test_size); 365 + var html_size = JX.Vector.getDim(test_size); 366 + JX.DOM.remove(test_size); 367 + scrollY = html_size.y; 368 + } 369 + JX.DOM.replace(scroller, html); 370 + menuRoot.scrollTop += scrollY; 371 + 372 + if (reselectId) { 373 + JX.Stratcom.invoke( 374 + 'conpherence-selectthread', 375 + null, 376 + { id : reselectId } 377 + ); 378 + } 379 + }; 380 + 381 + JX.Stratcom.listen( 382 + ['click'], 383 + 'conpherence-menu-scroller', 384 + handlethreadscrollers 385 + ); 318 386 319 387 });