diff --git a/resources/sql/patches/20130423.conpherenceindices.sql b/resources/sql/patches/20130423.conpherenceindices.sql new file mode 100644 index 0000000000..605f64ff38 --- /dev/null +++ b/resources/sql/patches/20130423.conpherenceindices.sql @@ -0,0 +1,4 @@ +ALTER TABLE {$NAMESPACE}_conpherence.conpherence_participant + DROP KEY participantPHID, + ADD KEY unreadCount (participantPHID, participationStatus), + ADD KEY participationIndex (participantPHID, dateTouched, id); diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index b9b29d4975..37ea69cb7c 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -1289,7 +1289,7 @@ celerity_register_resource_map(array( ), 'javelin-behavior-conpherence-menu' => array( - 'uri' => '/res/ce7bfa44/rsrc/js/application/conpherence/behavior-menu.js', + 'uri' => '/res/06bfc1a3/rsrc/js/application/conpherence/behavior-menu.js', 'type' => 'js', 'requires' => array( @@ -1301,6 +1301,7 @@ celerity_register_resource_map(array( 5 => 'javelin-workflow', 6 => 'javelin-behavior-device', 7 => 'javelin-history', + 8 => 'javelin-vector', ), 'disk' => '/rsrc/js/application/conpherence/behavior-menu.js', ), diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 6dfd752d10..926f5d58cd 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -238,6 +238,7 @@ phutil_register_library_map(array( 'ConpherenceMenuItemView' => 'applications/conpherence/view/ConpherenceMenuItemView.php', 'ConpherenceNewController' => 'applications/conpherence/controller/ConpherenceNewController.php', 'ConpherenceParticipant' => 'applications/conpherence/storage/ConpherenceParticipant.php', + 'ConpherenceParticipantCountQuery' => 'applications/conpherence/query/ConpherenceParticipantCountQuery.php', 'ConpherenceParticipantQuery' => 'applications/conpherence/query/ConpherenceParticipantQuery.php', 'ConpherenceParticipationStatus' => 'applications/conpherence/constants/ConpherenceParticipationStatus.php', 'ConpherencePeopleMenuEventListener' => 'applications/conpherence/events/ConpherencePeopleMenuEventListener.php', @@ -2001,6 +2002,7 @@ phutil_register_library_map(array( 'ConpherenceMenuItemView' => 'AphrontTagView', 'ConpherenceNewController' => 'ConpherenceController', 'ConpherenceParticipant' => 'ConpherenceDAO', + 'ConpherenceParticipantCountQuery' => 'PhabricatorOffsetPagedQuery', 'ConpherenceParticipantQuery' => 'PhabricatorOffsetPagedQuery', 'ConpherenceParticipationStatus' => 'ConpherenceConstants', 'ConpherencePeopleMenuEventListener' => 'PhutilEventListener', diff --git a/src/applications/conpherence/controller/ConpherenceController.php b/src/applications/conpherence/controller/ConpherenceController.php index 600c658911..4d5b1f8bc9 100644 --- a/src/applications/conpherence/controller/ConpherenceController.php +++ b/src/applications/conpherence/controller/ConpherenceController.php @@ -6,62 +6,6 @@ abstract class ConpherenceController extends PhabricatorController { private $conpherences; - /** - * Try for a full set of unread conpherences, and if we fail - * load read conpherences. Additional conpherences in either category - * are loaded asynchronously. - */ - public function loadStartingConpherences($current_selection_epoch = null) { - $user = $this->getRequest()->getUser(); - - $read_participant_query = id(new ConpherenceParticipantQuery()) - ->withParticipantPHIDs(array($user->getPHID())); - $read_status = ConpherenceParticipationStatus::UP_TO_DATE; - if ($current_selection_epoch) { - $read_one = $read_participant_query - ->withParticipationStatus($read_status) - ->withDateTouched($current_selection_epoch, '>') - ->execute(); - - $read_two = $read_participant_query - ->withDateTouched($current_selection_epoch, '<=') - ->execute(); - - $read = array_merge($read_one, $read_two); - - } else { - $read = $read_participant_query - ->withParticipationStatus($read_status) - ->execute(); - } - - $unread_status = ConpherenceParticipationStatus::BEHIND; - $unread = id(new ConpherenceParticipantQuery()) - ->withParticipantPHIDs(array($user->getPHID())) - ->withParticipationStatus($unread_status) - ->execute(); - - $all_participation = $unread + $read; - $all_conpherence_phids = array_keys($all_participation); - $all_conpherences = array(); - if ($all_conpherence_phids) { - $all_conpherences = id(new ConpherenceThreadQuery()) - ->setViewer($user) - ->withPHIDs($all_conpherence_phids) - ->needParticipantCache(true) - ->execute(); - } - $unread_conpherences = array_select_keys( - $all_conpherences, - array_keys($unread)); - - $read_conpherences = array_select_keys( - $all_conpherences, - array_keys($read)); - - return array($unread_conpherences, $read_conpherences); - } - public function buildApplicationMenu() { $nav = new PhabricatorMenuView(); diff --git a/src/applications/conpherence/controller/ConpherenceListController.php b/src/applications/conpherence/controller/ConpherenceListController.php index 288913b89b..5de32de748 100644 --- a/src/applications/conpherence/controller/ConpherenceListController.php +++ b/src/applications/conpherence/controller/ConpherenceListController.php @@ -6,6 +6,10 @@ final class ConpherenceListController extends ConpherenceController { + const SELECTED_MODE = 'selected'; + const UNSELECTED_MODE = 'unselected'; + const PAGING_MODE = 'paging'; + private $conpherenceID; public function setConpherenceID($conpherence_id) { @@ -20,59 +24,227 @@ final class ConpherenceListController $this->setConpherenceID(idx($data, 'id')); } + /** + * Three main modes of operation... + * + * 1 - /conpherence/ - UNSELECTED_MODE + * 2 - /conpherence// - SELECTED_MODE + * 3 - /conpherence/?direction='up'&... - PAGING_MODE + * + * UNSELECTED_MODE is not an Ajax request while the other two are Ajax + * requests. + */ + private function determineMode() { + $request = $this->getRequest(); + + $mode = self::UNSELECTED_MODE; + if ($request->isAjax()) { + if ($request->getStr('direction')) { + $mode = self::PAGING_MODE; + } else { + $mode = self::SELECTED_MODE; + } + } + + return $mode; + } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $title = pht('Conpherence'); - - $conpherence_id = $this->getConpherenceID(); - $current_selection_epoch = null; $conpherence = null; - if ($conpherence_id) { - $conpherence = id(new ConpherenceThreadQuery()) - ->setViewer($user) - ->withIDs(array($conpherence_id)) - ->executeOne(); - if (!$conpherence) { - return new Aphront404Response(); - } - if ($conpherence->getTitle()) { - $title = $conpherence->getTitle(); - } + $scroll_up_participant = $this->getEmptyParticipant(); + $scroll_down_participant = $this->getEmptyParticipant(); + $too_many = ConpherenceParticipantQuery::LIMIT + 1; + $all_participation = array(); - $participant = $conpherence->getParticipant($user->getPHID()); - $current_selection_epoch = $participant->getDateTouched(); + $mode = $this->determineMode(); + switch ($mode) { + case self::SELECTED_MODE: + $conpherence_id = $this->getConpherenceID(); + $conpherence = id(new ConpherenceThreadQuery()) + ->setViewer($user) + ->withIDs(array($conpherence_id)) + ->executeOne(); + if (!$conpherence) { + return new Aphront404Response(); + } + if ($conpherence->getTitle()) { + $title = $conpherence->getTitle(); + } + $cursor = $conpherence->getParticipant($user->getPHID()); + $data = $this->loadParticipationWithMidCursor($cursor); + $all_participation = $data['participation']; + $scroll_up_participant = $data['scroll_up_participant']; + $scroll_down_participant = $data['scroll_down_participant']; + break; + case self::PAGING_MODE: + $direction = $request->getStr('direction'); + $id = $request->getInt('participant_id'); + $date_touched = $request->getInt('date_touched'); + $conpherence_phid = $request->getStr('conpherence_phid'); + if ($direction == 'up') { + $order = ConpherenceParticipantQuery::ORDER_NEWER; + } else { + $order = ConpherenceParticipantQuery::ORDER_OLDER; + } + $scroller_participant = id(new ConpherenceParticipant()) + ->makeEphemeral() + ->setID($id) + ->setDateTouched($date_touched) + ->setConpherencePHID($conpherence_phid); + $participation = id(new ConpherenceParticipantQuery()) + ->withParticipantPHIDs(array($user->getPHID())) + ->withParticipantCursor($scroller_participant) + ->setOrder($order) + ->setLimit($too_many) + ->execute(); + if (count($participation) == $too_many) { + if ($direction == 'up') { + $node = $scroll_up_participant = reset($participation); + } else { + $node = $scroll_down_participant = end($participation); + } + unset($participation[$node->getConpherencePHID()]); + } + $all_participation = $participation; + break; + case self::UNSELECTED_MODE: + default: + $too_many = ConpherenceParticipantQuery::LIMIT + 1; + $all_participation = id(new ConpherenceParticipantQuery()) + ->withParticipantPHIDs(array($user->getPHID())) + ->setLimit($too_many) + ->execute(); + if (count($all_participation) == $too_many) { + $node = end($participation); + unset($all_participation[$node->getConpherencePHID()]); + $scroll_down_participant = $node; + } + break; } - list($unread, $read) = $this->loadStartingConpherences( - $current_selection_epoch); + $threads = $this->loadConpherenceThreadData( + $all_participation); $thread_view = id(new ConpherenceThreadListView()) ->setUser($user) ->setBaseURI($this->getApplicationURI()) - ->setUnreadThreads($unread) - ->setReadThreads($read); + ->setThreads($threads) + ->setScrollUpParticipant($scroll_up_participant) + ->setScrollDownParticipant($scroll_down_participant); - if ($request->isAjax()) { - return id(new AphrontAjaxResponse())->setContent($thread_view); + switch ($mode) { + case self::SELECTED_MODE: + $response = id(new AphrontAjaxResponse())->setContent($thread_view); + break; + case self::PAGING_MODE: + $thread_html = $thread_view->renderThreadsHTML(); + $phids = array_keys($participation); + $content = array( + 'html' => $thread_html, + 'phids' => $phids); + $response = id(new AphrontAjaxResponse())->setContent($content); + break; + case self::UNSELECTED_MODE: + default: + $layout = id(new ConpherenceLayoutView()) + ->setBaseURI($this->getApplicationURI()) + ->setThreadView($thread_view) + ->setRole('list'); + if ($conpherence) { + $layout->setThread($conpherence); + } + $response = $this->buildApplicationPage( + $layout, + array( + 'title' => $title, + 'device' => true, + )); + break; } - $layout = id(new ConpherenceLayoutView()) - ->setBaseURI($this->getApplicationURI()) - ->setThreadView($thread_view) - ->setRole('list'); + return $response; - if ($conpherence) { - $layout->setThread($conpherence); + } + + /** + * Handles the curious case when we are visiting a conpherence directly + * by issuing two separate queries. Otherwise, additional conpherences + * are fetched asynchronously. Note these can be earlier or later + * (up or down), depending on what conpherence was selected on initial + * load. + */ + private function loadParticipationWithMidCursor( + ConpherenceParticipant $cursor) { + + $user = $this->getRequest()->getUser(); + + $scroll_up_participant = $this->getEmptyParticipant(); + $scroll_down_participant = $this->getEmptyParticipant(); + + // Note this is a bit dodgy since there may be less than this + // amount in either the up or down direction, thus having us fail + // to fetch LIMIT in total. Whatevs for now and re-visit if we're + // fine-tuning this loading process. + $too_many = ceil(ConpherenceParticipantQuery::LIMIT / 2) + 1; + $participant_query = id(new ConpherenceParticipantQuery()) + ->withParticipantPHIDs(array($user->getPHID())) + ->setLimit($too_many); + $current_selection_epoch = $cursor->getDateTouched(); + $set_one = $participant_query + ->withParticipantCursor($cursor) + ->setOrder(ConpherenceParticipantQuery::ORDER_NEWER) + ->execute(); + + if (count($set_one) == $too_many) { + $node = reset($set_one); + unset($set_one[$node->getConpherencePHID()]); + $scroll_up_participant = $node; } - return $this->buildApplicationPage( - $layout, - array( - 'title' => $title, - 'device' => true, - )); + $set_two = $participant_query + ->withParticipantCursor($cursor) + ->setOrder(ConpherenceParticipantQuery::ORDER_OLDER) + ->execute(); + + if (count($set_two) == $too_many) { + $node = end($set_two); + unset($set_two[$node->getConpherencePHID()]); + $scroll_down_participant = $node; + } + + $participation = array_merge( + $set_one, + $set_two); + + return array( + 'scroll_up_participant' => $scroll_up_participant, + 'scroll_down_participant' => $scroll_down_participant, + 'participation' => $participation); + } + + private function loadConpherenceThreadData($participation) { + $user = $this->getRequest()->getUser(); + $conpherence_phids = array_keys($participation); + if ($conpherence_phids) { + $conpherences = id(new ConpherenceThreadQuery()) + ->setViewer($user) + ->withPHIDs($conpherence_phids) + ->needParticipantCache(true) + ->execute(); + } + + // this will re-sort by participation data + $conpherences = array_select_keys($conpherences, $conpherence_phids); + + return $conpherences; + } + + private function getEmptyParticipant() { + return id(new ConpherenceParticipant()) + ->makeEphemeral(); } } diff --git a/src/applications/conpherence/controller/ConpherenceViewController.php b/src/applications/conpherence/controller/ConpherenceViewController.php index 0f5857bcb4..38da84a429 100644 --- a/src/applications/conpherence/controller/ConpherenceViewController.php +++ b/src/applications/conpherence/controller/ConpherenceViewController.php @@ -50,6 +50,9 @@ final class ConpherenceViewController extends ->setBeforeTransactionID($before_transaction_id); } $conpherence = $query->executeOne(); + if (!$conpherence) { + return new Aphront404Response(); + } $this->setConpherence($conpherence); $participant = $conpherence->getParticipant($user->getPHID()); diff --git a/src/applications/conpherence/editor/ConpherenceEditor.php b/src/applications/conpherence/editor/ConpherenceEditor.php index d7a8ee1260..213ac9ff21 100644 --- a/src/applications/conpherence/editor/ConpherenceEditor.php +++ b/src/applications/conpherence/editor/ConpherenceEditor.php @@ -140,9 +140,6 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { $object->setRecentParticipantPHIDs($participants); } - /** - * For now this only supports adding more files and participants. - */ protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { @@ -169,33 +166,8 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { $file_phid); } $editor->save(); - // fallthrough - case PhabricatorTransactions::TYPE_COMMENT: - $xaction_phid = $xaction->getPHID(); - $behind = ConpherenceParticipationStatus::BEHIND; - $up_to_date = ConpherenceParticipationStatus::UP_TO_DATE; - $participants = $object->getParticipants(); - $user = $this->getActor(); - $time = time(); - foreach ($participants as $phid => $participant) { - if ($phid != $user->getPHID()) { - if ($participant->getParticipationStatus() != $behind) { - $participant->setBehindTransactionPHID($xaction_phid); - // decrement one as this is the message putting them behind! - $participant->setSeenMessageCount($object->getMessageCount() - 1); - } - $participant->setParticipationStatus($behind); - $participant->setDateTouched($time); - } else { - $participant->setSeenMessageCount($object->getMessageCount()); - $participant->setParticipationStatus($up_to_date); - $participant->setDateTouched($time); - } - $participant->save(); - } break; case ConpherenceTransactionType::TYPE_PARTICIPANTS: - $participants = $object->getParticipants(); $old_map = array_fuse($xaction->getOldValue()); @@ -229,7 +201,37 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { } $object->attachParticipants($participants); break; - } + } + } + + protected function applyFinalEffects( + PhabricatorLiskDAO $object, + array $xactions) { + + // update everyone's participation status on the last xaction -only- + $xaction = end($xactions); + $xaction_phid = $xaction->getPHID(); + $behind = ConpherenceParticipationStatus::BEHIND; + $up_to_date = ConpherenceParticipationStatus::UP_TO_DATE; + $participants = $object->getParticipants(); + $user = $this->getActor(); + $time = time(); + foreach ($participants as $phid => $participant) { + if ($phid != $user->getPHID()) { + if ($participant->getParticipationStatus() != $behind) { + $participant->setBehindTransactionPHID($xaction_phid); + // decrement one as this is the message putting them behind! + $participant->setSeenMessageCount($object->getMessageCount() - 1); + } + $participant->setParticipationStatus($behind); + $participant->setDateTouched($time); + } else { + $participant->setSeenMessageCount($object->getMessageCount()); + $participant->setParticipationStatus($up_to_date); + $participant->setDateTouched($time); + } + $participant->save(); + } } protected function mergeTransactions( diff --git a/src/applications/conpherence/query/ConpherenceParticipantCountQuery.php b/src/applications/conpherence/query/ConpherenceParticipantCountQuery.php new file mode 100644 index 0000000000..c74ff62b47 --- /dev/null +++ b/src/applications/conpherence/query/ConpherenceParticipantCountQuery.php @@ -0,0 +1,76 @@ +withParticipantPHIDs(array($my_phid)) + * ->withParticipationStatus(ConpherenceParticipationStatus::BEHIND) + * ->execute(); + * + * @group conpherence + */ +final class ConpherenceParticipantCountQuery + extends PhabricatorOffsetPagedQuery { + + private $participantPHIDs; + private $participationStatus; + + public function withParticipantPHIDs(array $phids) { + $this->participantPHIDs = $phids; + return $this; + } + + public function withParticipationStatus($participation_status) { + $this->participationStatus = $participation_status; + return $this; + } + + public function execute() { + $table = new ConpherenceParticipant(); + $conn_r = $table->establishConnection('r'); + + $rows = queryfx_all( + $conn_r, + 'SELECT COUNT(*) as count, participantPHID '. + 'FROM %T participant %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildGroupByClause($conn_r), + $this->buildLimitClause($conn_r)); + + return ipull($rows, 'count', 'participantPHID'); + } + + private function buildWhereClause($conn_r) { + $where = array(); + + if ($this->participantPHIDs) { + $where[] = qsprintf( + $conn_r, + 'participantPHID IN (%Ls)', + $this->participantPHIDs); + } + + if ($this->participationStatus !== null) { + $where[] = qsprintf( + $conn_r, + 'participationStatus = %d', + $this->participationStatus); + } + + return $this->formatWhereClause($where); + } + + private function buildGroupByClause(AphrontDatabaseConnection $conn_r) { + + $group_by = qsprintf( + $conn_r, + 'GROUP BY participantPHID'); + + return $group_by; + } + +} diff --git a/src/applications/conpherence/query/ConpherenceParticipantQuery.php b/src/applications/conpherence/query/ConpherenceParticipantQuery.php index 8492f8ab25..b755a30388 100644 --- a/src/applications/conpherence/query/ConpherenceParticipantQuery.php +++ b/src/applications/conpherence/query/ConpherenceParticipantQuery.php @@ -1,35 +1,61 @@ withParticipantPHIDs(array($my_phid)) + * ->execute(); + * + * - Q: What are the next set of conpherences as I scroll up (more recent) or + * down (less recent) this list of conpherences? + * - A: + * + * id(new ConpherenceParticipantQuery()) + * ->withParticipantPHIDs(array($my_phid)) + * ->withParticipantCursor($top_participant) + * ->setOrder(ConpherenceParticipantQuery::ORDER_NEWER) + * ->execute(); + * + * -or- + * + * id(new ConpherenceParticipantQuery()) + * ->withParticipantPHIDs(array($my_phid)) + * ->withParticipantCursor($bottom_participant) + * ->setOrder(ConpherenceParticipantQuery::ORDER_OLDER) + * ->execute(); + * + * For counts of read, un-read, or all conpherences by participant, see + * @{class:ConpherenceParticipantCountQuery}. + * * @group conpherence */ final class ConpherenceParticipantQuery extends PhabricatorOffsetPagedQuery { - private $conpherencePHIDs; - private $participantPHIDs; - private $dateTouched; - private $dateTouchedSort; - private $participationStatus; + const LIMIT = 100; + const ORDER_NEWER = 'newer'; + const ORDER_OLDER = 'older'; - public function withConpherencePHIDs(array $phids) { - $this->conpherencePHIDs = $phids; - return $this; - } + private $participantPHIDs; + private $participantCursor; + private $order = self::ORDER_OLDER; public function withParticipantPHIDs(array $phids) { $this->participantPHIDs = $phids; return $this; } - public function withDateTouched($date, $sort = null) { - $this->dateTouched = $date; - $this->dateTouchedSort = $sort ? $sort : '<'; + public function withParticipantCursor(ConpherenceParticipant $participant) { + $this->participantCursor = $participant; return $this; } - public function withParticipationStatus($participation_status) { - $this->participationStatus = $participation_status; + public function setOrder($order) { + $this->order = $order; return $this; } @@ -49,19 +75,16 @@ final class ConpherenceParticipantQuery $participants = mpull($participants, null, 'getConpherencePHID'); + if ($this->order == self::ORDER_NEWER) { + $participants = array_reverse($participants); + } + return $participants; } private function buildWhereClause($conn_r) { $where = array(); - if ($this->conpherencePHIDs) { - $where[] = qsprintf( - $conn_r, - 'conpherencePHID IN (%Ls)', - $this->conpherencePHIDs); - } - if ($this->participantPHIDs) { $where[] = qsprintf( $conn_r, @@ -69,28 +92,41 @@ final class ConpherenceParticipantQuery $this->participantPHIDs); } - if ($this->participationStatus !== null) { + if ($this->participantCursor) { + $date_touched = $this->participantCursor->getDateTouched(); + $id = $this->participantCursor->getID(); + if ($this->order == self::ORDER_OLDER) { + $compare_date = '<'; + $compare_id = '<='; + } else { + $compare_date = '>'; + $compare_id = '>='; + } $where[] = qsprintf( $conn_r, - 'participationStatus = %d', - $this->participationStatus); - } - - if ($this->dateTouched) { - if ($this->dateTouchedSort) { - $where[] = qsprintf( - $conn_r, - 'dateTouched %Q %d', - $this->dateTouchedSort, - $this->dateTouched); - } + '(dateTouched %Q %d OR (dateTouched = %d AND id %Q %d))', + $compare_date, + $date_touched, + $date_touched, + $compare_id, + $id); } return $this->formatWhereClause($where); } private function buildOrderClause(AphrontDatabaseConnection $conn_r) { - return 'ORDER BY dateTouched DESC'; + + $order_word = ($this->order == self::ORDER_OLDER) ? 'DESC' : 'ASC'; + // if these are different direction we won't get as efficient a query + // see http://dev.mysql.com/doc/refman/5.5/en/order-by-optimization.html + $order = qsprintf( + $conn_r, + 'ORDER BY dateTouched %Q, id %Q', + $order_word, + $order_word); + + return $order; } } diff --git a/src/applications/conpherence/view/ConpherenceThreadListView.php b/src/applications/conpherence/view/ConpherenceThreadListView.php index 4e3fe6bbba..d52ac2038d 100644 --- a/src/applications/conpherence/view/ConpherenceThreadListView.php +++ b/src/applications/conpherence/view/ConpherenceThreadListView.php @@ -3,26 +3,33 @@ final class ConpherenceThreadListView extends AphrontView { private $baseURI; - private $unreadThreads; - private $readThreads; + private $threads; + private $scrollUpParticipant; + private $scrollDownParticipant; + + public function setThreads(array $threads) { + assert_instances_of($threads, 'ConpherenceThread'); + $this->threads = $threads; + return $this; + } + + public function setScrollUpParticipant( + ConpherenceParticipant $participant) { + $this->scrollUpParticipant = $participant; + return $this; + } + + public function setScrollDownParticipant( + ConpherenceParticipant $participant) { + $this->scrollDownParticipant = $participant; + return $this; + } public function setBaseURI($base_uri) { $this->baseURI = $base_uri; return $this; } - public function setUnreadThreads(array $unread_threads) { - assert_instances_of($unread_threads, 'ConpherenceThread'); - $this->unreadThreads = $unread_threads; - return $this; - } - - public function setReadThreads(array $read_threads) { - assert_instances_of($read_threads, 'ConpherenceThread'); - $this->readThreads = $read_threads; - return $this; - } - public function render() { require_celerity_resource('conpherence-menu-css'); @@ -39,10 +46,8 @@ final class ConpherenceThreadListView extends AphrontView { ->setHref($this->baseURI.'new/') ->setType(PhabricatorMenuItemView::TYPE_BUTTON)); - $menu->newLabel(pht('Unread')); - $this->addThreadsToMenu($menu, $this->unreadThreads, $read = false); - $menu->newLabel(pht('Read')); - $this->addThreadsToMenu($menu, $this->readThreads, $read = true); + $menu->newLabel(''); + $this->addThreadsToMenu($menu, $this->threads); return $menu; } @@ -51,6 +56,28 @@ final class ConpherenceThreadListView extends AphrontView { return $this->renderThread($thread); } + public function renderThreadsHTML() { + $thread_html = array(); + + if ($this->scrollUpParticipant->getID()) { + $thread_html[] = $this->getScrollMenuItem( + $this->scrollUpParticipant, + 'up'); + } + + foreach ($this->threads as $thread) { + $thread_html[] = $this->renderSingleThread($thread); + } + + if ($this->scrollDownParticipant->getID()) { + $thread_html[] = $this->getScrollMenuItem( + $this->scrollDownParticipant, + 'down'); + } + + return phutil_implode_html('', $thread_html); + } + private function renderThreadItem(ConpherenceThread $thread) { return id(new PhabricatorMenuItemView()) ->setType(PhabricatorMenuItemView::TYPE_CUSTOM) @@ -87,28 +114,59 @@ final class ConpherenceThreadListView extends AphrontView { private function addThreadsToMenu( PhabricatorMenuView $menu, - array $conpherences, - $read = false) { + array $conpherences) { + + if ($this->scrollUpParticipant->getID()) { + $item = $this->getScrollMenuItem($this->scrollUpParticipant, 'up'); + $menu->addMenuItem($item); + } foreach ($conpherences as $conpherence) { $item = $this->renderThreadItem($conpherence); $menu->addMenuItem($item); } - if (empty($conpherences) || $read) { - $menu->addMenuItem($this->getNoConpherencesBlock()); + if (empty($conpherences)) { + $menu->addMenuItem($this->getNoConpherencesMenuItem()); + } + + if ($this->scrollDownParticipant->getID()) { + $item = $this->getScrollMenuItem($this->scrollDownParticipant, 'down'); + $menu->addMenuItem($item); } return $menu; } - private function getNoConpherencesBlock() { + public function getScrollMenuItem( + ConpherenceParticipant $participant, + $direction) { + + if ($direction == 'up') { + $name = pht('Load Newer Threads'); + } else { + $name = pht('Load Older Threads'); + } + $item = id(new PhabricatorMenuItemView()) + ->addSigil('conpherence-menu-scroller') + ->setName($name) + ->setHref($this->baseURI) + ->setType(PhabricatorMenuItemView::TYPE_BUTTON) + ->setMetadata(array( + 'participant_id' => $participant->getID(), + 'conpherence_phid' => $participant->getConpherencePHID(), + 'date_touched' => $participant->getDateTouched(), + 'direction' => $direction)); + return $item; + } + + private function getNoConpherencesMenuItem() { $message = phutil_tag( 'div', array( 'class' => 'no-conpherences-menu-item' ), - pht('No more conpherences.')); + pht('No conpherences.')); return id(new PhabricatorMenuItemView()) ->setType(PhabricatorMenuItemView::TYPE_CUSTOM) diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 6e8e9bcb32..ff28167194 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -257,6 +257,11 @@ abstract class PhabricatorApplicationTransactionEditor throw new Exception("Capability not supported!"); } + protected function applyFinalEffects( + PhabricatorLiskDAO $object, + array $xactions) { + } + public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source; return $this; @@ -386,6 +391,8 @@ abstract class PhabricatorApplicationTransactionEditor $this->applyExternalEffects($object, $xaction); } + $this->applyFinalEffects($object, $xactions); + if ($read_locking) { $object->endReadLocking(); $read_locking = false; diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php index 0c0b55a4f4..e4a46bc335 100644 --- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php +++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php @@ -1250,6 +1250,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList { 'type' => 'sql', 'name' => $this->getPatchPath('20130423.phortunepaymentrevised.sql'), ), + '20130423.conpherenceindices.sql' => array( + 'type' => 'sql', + 'name' => $this->getPatchPath('20130423.conpherenceindices.sql'), + ), ); } } diff --git a/src/view/page/menu/PhabricatorMainMenuView.php b/src/view/page/menu/PhabricatorMainMenuView.php index b93fc6025f..46babab524 100644 --- a/src/view/page/menu/PhabricatorMainMenuView.php +++ b/src/view/page/menu/PhabricatorMainMenuView.php @@ -267,11 +267,11 @@ final class PhabricatorMainMenuView extends AphrontView { $message_count_id = celerity_generate_unique_node_id(); $unread_status = ConpherenceParticipationStatus::BEHIND; - $unread = id(new ConpherenceParticipantQuery()) + $unread = id(new ConpherenceParticipantCountQuery()) ->withParticipantPHIDs(array($user->getPHID())) ->withParticipationStatus($unread_status) ->execute(); - $message_count_number = count($unread); + $message_count_number = $unread[$user->getPHID()]; if ($message_count_number > 999) { $message_count_number = "\xE2\x88\x9E"; } diff --git a/webroot/rsrc/js/application/conpherence/behavior-menu.js b/webroot/rsrc/js/application/conpherence/behavior-menu.js index 68c5a29dba..d9b66a2a92 100644 --- a/webroot/rsrc/js/application/conpherence/behavior-menu.js +++ b/webroot/rsrc/js/application/conpherence/behavior-menu.js @@ -8,6 +8,7 @@ * javelin-workflow * javelin-behavior-device * javelin-history + * javelin-vector */ JX.behavior('conpherence-menu', function(config) { @@ -53,6 +54,15 @@ JX.behavior('conpherence-menu', function(config) { redrawthread(); } + JX.Stratcom.listen( + 'conpherence-selectthread', + null, + function (e) { + var node = JX.$(e.getData().id); + selectthread(node); + } + ); + function updatepagedata(data) { var uri_suffix = thread.selected + '/'; if (data.use_base_uri) { @@ -75,15 +85,6 @@ JX.behavior('conpherence-menu', function(config) { } ); - JX.Stratcom.listen( - 'conpherence-selectthread', - null, - function (e) { - var node = JX.$(e.getData().id); - selectthread(node); - } - ); - function redrawthread() { if (!thread.node) { return; @@ -96,9 +97,9 @@ JX.behavior('conpherence-menu', function(config) { var data = JX.Stratcom.getData(thread.node); if (thread.visible !== null || !config.hasThread) { - var uri = config.base_uri + data.id + '/'; + var uri = config.base_uri + data.id + '/'; new JX.Workflow(uri, {}) - .setHandler(onresponse) + .setHandler(onloadthreadresponse) .start(); } else { didredrawthread(); @@ -154,7 +155,7 @@ JX.behavior('conpherence-menu', function(config) { } } - function onresponse(response) { + function onloadthreadresponse(response) { var header = JX.$H(response.header); var messages = JX.$H(response.messages); var form = JX.$H(response.form); @@ -252,11 +253,9 @@ JX.behavior('conpherence-menu', function(config) { }).setData({ oldest_transaction_id : oldest_transaction_id }).send(); }); - // On mobile, we just show a thread list, so we don't want to automatically // select or load any threads. On Desktop, we automatically select the first // thread. - var old_device = null; function ondevicechange() { var new_device = JX.Device.getDevice(); @@ -284,16 +283,18 @@ JX.behavior('conpherence-menu', function(config) { function loadthreads() { var uri = config.base_uri + 'thread/' + config.selectedID + '/'; new JX.Workflow(uri) - .setHandler(onthreadresponse) + .setHandler(onloadthreadsresponse) .start(); } - function onthreadresponse(r) { + function onloadthreadsresponse(r) { var layout = JX.$(config.layoutID); var menu = JX.DOM.find(layout, 'div', 'conpherence-menu-pane'); JX.DOM.setContent(menu, JX.$H(r)); config.selectedID && selectthreadid(config.selectedID); + + thread.node.scrollIntoView(); } function didloadthreads() { @@ -316,4 +317,71 @@ JX.behavior('conpherence-menu', function(config) { redrawthread(); } + var handlethreadscrollers = function (e) { + e.kill(); + + var data = e.getNodeData('conpherence-menu-scroller'); + var scroller = e.getNode('conpherence-menu-scroller'); + new JX.Workflow(scroller.href, data) + .setHandler( + JX.bind(null, threadscrollerresponse, scroller, data.direction)) + .start(); + }; + + var threadscrollerresponse = function (scroller, direction, r) { + var html = JX.$H(r.html); + + var threadPhids = r.phids; + var reselectId = null; + // remove any threads that are in the list that we just got back + // in the result set; things have changed and they'll be in the + // right place soon + for (var ii = 0; ii < threadPhids.length; ii++) { + try { + var nodeId = threadPhids[ii] + '-nav-item'; + var node = JX.$(nodeId); + var nodeData = JX.Stratcom.getData(node); + if (nodeData.id == thread.selected) { + reselectId = nodeId; + } + JX.DOM.remove(node); + } catch (ex) { + // ignore , just haven't seen this thread yet + } + } + + var root = JX.DOM.find(document, 'div', 'conpherence-layout'); + var menuRoot = JX.DOM.find(root, 'div', 'conpherence-menu-pane'); + var scrollY = 0; + // we have to do some hyjinx in the up case to make the menu scroll to + // where it should + if (direction == 'up') { + var style = { + position: 'absolute', + left: '-10000px' + }; + var test_size = JX.$N('div', {style: style}, html); + document.body.appendChild(test_size); + var html_size = JX.Vector.getDim(test_size); + JX.DOM.remove(test_size); + scrollY = html_size.y; + } + JX.DOM.replace(scroller, html); + menuRoot.scrollTop += scrollY; + + if (reselectId) { + JX.Stratcom.invoke( + 'conpherence-selectthread', + null, + { id : reselectId } + ); + } + }; + + JX.Stratcom.listen( + ['click'], + 'conpherence-menu-scroller', + handlethreadscrollers + ); + });