1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-01-08 22:01:03 +01:00

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
This commit is contained in:
Bob Trahan 2013-04-26 10:30:41 -07:00
parent 664fe7ef73
commit 11cb2f4f6c
14 changed files with 574 additions and 197 deletions

View file

@ -0,0 +1,4 @@
ALTER TABLE {$NAMESPACE}_conpherence.conpherence_participant
DROP KEY participantPHID,
ADD KEY unreadCount (participantPHID, participationStatus),
ADD KEY participationIndex (participantPHID, dateTouched, id);

View file

@ -1289,7 +1289,7 @@ celerity_register_resource_map(array(
), ),
'javelin-behavior-conpherence-menu' => 'javelin-behavior-conpherence-menu' =>
array( array(
'uri' => '/res/ce7bfa44/rsrc/js/application/conpherence/behavior-menu.js', 'uri' => '/res/06bfc1a3/rsrc/js/application/conpherence/behavior-menu.js',
'type' => 'js', 'type' => 'js',
'requires' => 'requires' =>
array( array(
@ -1301,6 +1301,7 @@ celerity_register_resource_map(array(
5 => 'javelin-workflow', 5 => 'javelin-workflow',
6 => 'javelin-behavior-device', 6 => 'javelin-behavior-device',
7 => 'javelin-history', 7 => 'javelin-history',
8 => 'javelin-vector',
), ),
'disk' => '/rsrc/js/application/conpherence/behavior-menu.js', 'disk' => '/rsrc/js/application/conpherence/behavior-menu.js',
), ),

View file

@ -238,6 +238,7 @@ phutil_register_library_map(array(
'ConpherenceMenuItemView' => 'applications/conpherence/view/ConpherenceMenuItemView.php', 'ConpherenceMenuItemView' => 'applications/conpherence/view/ConpherenceMenuItemView.php',
'ConpherenceNewController' => 'applications/conpherence/controller/ConpherenceNewController.php', 'ConpherenceNewController' => 'applications/conpherence/controller/ConpherenceNewController.php',
'ConpherenceParticipant' => 'applications/conpherence/storage/ConpherenceParticipant.php', 'ConpherenceParticipant' => 'applications/conpherence/storage/ConpherenceParticipant.php',
'ConpherenceParticipantCountQuery' => 'applications/conpherence/query/ConpherenceParticipantCountQuery.php',
'ConpherenceParticipantQuery' => 'applications/conpherence/query/ConpherenceParticipantQuery.php', 'ConpherenceParticipantQuery' => 'applications/conpherence/query/ConpherenceParticipantQuery.php',
'ConpherenceParticipationStatus' => 'applications/conpherence/constants/ConpherenceParticipationStatus.php', 'ConpherenceParticipationStatus' => 'applications/conpherence/constants/ConpherenceParticipationStatus.php',
'ConpherencePeopleMenuEventListener' => 'applications/conpherence/events/ConpherencePeopleMenuEventListener.php', 'ConpherencePeopleMenuEventListener' => 'applications/conpherence/events/ConpherencePeopleMenuEventListener.php',
@ -2001,6 +2002,7 @@ phutil_register_library_map(array(
'ConpherenceMenuItemView' => 'AphrontTagView', 'ConpherenceMenuItemView' => 'AphrontTagView',
'ConpherenceNewController' => 'ConpherenceController', 'ConpherenceNewController' => 'ConpherenceController',
'ConpherenceParticipant' => 'ConpherenceDAO', 'ConpherenceParticipant' => 'ConpherenceDAO',
'ConpherenceParticipantCountQuery' => 'PhabricatorOffsetPagedQuery',
'ConpherenceParticipantQuery' => 'PhabricatorOffsetPagedQuery', 'ConpherenceParticipantQuery' => 'PhabricatorOffsetPagedQuery',
'ConpherenceParticipationStatus' => 'ConpherenceConstants', 'ConpherenceParticipationStatus' => 'ConpherenceConstants',
'ConpherencePeopleMenuEventListener' => 'PhutilEventListener', 'ConpherencePeopleMenuEventListener' => 'PhutilEventListener',

View file

@ -6,62 +6,6 @@
abstract class ConpherenceController extends PhabricatorController { abstract class ConpherenceController extends PhabricatorController {
private $conpherences; 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() { public function buildApplicationMenu() {
$nav = new PhabricatorMenuView(); $nav = new PhabricatorMenuView();

View file

@ -6,6 +6,10 @@
final class ConpherenceListController final class ConpherenceListController
extends ConpherenceController { extends ConpherenceController {
const SELECTED_MODE = 'selected';
const UNSELECTED_MODE = 'unselected';
const PAGING_MODE = 'paging';
private $conpherenceID; private $conpherenceID;
public function setConpherenceID($conpherence_id) { public function setConpherenceID($conpherence_id) {
@ -20,59 +24,227 @@ final class ConpherenceListController
$this->setConpherenceID(idx($data, 'id')); $this->setConpherenceID(idx($data, 'id'));
} }
/**
* Three main modes of operation...
*
* 1 - /conpherence/ - UNSELECTED_MODE
* 2 - /conpherence/<id>/ - 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() { public function processRequest() {
$request = $this->getRequest(); $request = $this->getRequest();
$user = $request->getUser(); $user = $request->getUser();
$title = pht('Conpherence'); $title = pht('Conpherence');
$conpherence_id = $this->getConpherenceID();
$current_selection_epoch = null;
$conpherence = 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()) { $scroll_up_participant = $this->getEmptyParticipant();
$title = $conpherence->getTitle(); $scroll_down_participant = $this->getEmptyParticipant();
} $too_many = ConpherenceParticipantQuery::LIMIT + 1;
$all_participation = array();
$participant = $conpherence->getParticipant($user->getPHID()); $mode = $this->determineMode();
$current_selection_epoch = $participant->getDateTouched(); 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( $threads = $this->loadConpherenceThreadData(
$current_selection_epoch); $all_participation);
$thread_view = id(new ConpherenceThreadListView()) $thread_view = id(new ConpherenceThreadListView())
->setUser($user) ->setUser($user)
->setBaseURI($this->getApplicationURI()) ->setBaseURI($this->getApplicationURI())
->setUnreadThreads($unread) ->setThreads($threads)
->setReadThreads($read); ->setScrollUpParticipant($scroll_up_participant)
->setScrollDownParticipant($scroll_down_participant);
if ($request->isAjax()) { switch ($mode) {
return id(new AphrontAjaxResponse())->setContent($thread_view); 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()) return $response;
->setBaseURI($this->getApplicationURI())
->setThreadView($thread_view)
->setRole('list');
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( $set_two = $participant_query
$layout, ->withParticipantCursor($cursor)
array( ->setOrder(ConpherenceParticipantQuery::ORDER_OLDER)
'title' => $title, ->execute();
'device' => true,
)); 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();
} }
} }

View file

@ -50,6 +50,9 @@ final class ConpherenceViewController extends
->setBeforeTransactionID($before_transaction_id); ->setBeforeTransactionID($before_transaction_id);
} }
$conpherence = $query->executeOne(); $conpherence = $query->executeOne();
if (!$conpherence) {
return new Aphront404Response();
}
$this->setConpherence($conpherence); $this->setConpherence($conpherence);
$participant = $conpherence->getParticipant($user->getPHID()); $participant = $conpherence->getParticipant($user->getPHID());

View file

@ -140,9 +140,6 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor {
$object->setRecentParticipantPHIDs($participants); $object->setRecentParticipantPHIDs($participants);
} }
/**
* For now this only supports adding more files and participants.
*/
protected function applyCustomExternalTransaction( protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object, PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) { PhabricatorApplicationTransaction $xaction) {
@ -169,33 +166,8 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor {
$file_phid); $file_phid);
} }
$editor->save(); $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; break;
case ConpherenceTransactionType::TYPE_PARTICIPANTS: case ConpherenceTransactionType::TYPE_PARTICIPANTS:
$participants = $object->getParticipants(); $participants = $object->getParticipants();
$old_map = array_fuse($xaction->getOldValue()); $old_map = array_fuse($xaction->getOldValue());
@ -229,7 +201,37 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor {
} }
$object->attachParticipants($participants); $object->attachParticipants($participants);
break; 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( protected function mergeTransactions(

View file

@ -0,0 +1,76 @@
<?php
/**
* Query class that answers the question:
*
* - Q: How many unread conpherences am I participating in?
* - A:
* id(new ConpherenceParticipantCountQuery())
* ->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;
}
}

View file

@ -1,35 +1,61 @@
<?php <?php
/** /**
* Query class that answers these questions:
*
* - Q: What are the conpherences to show when I land on /conpherence/ ?
* - A:
*
* id(new ConpherenceParticipantQuery())
* ->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 * @group conpherence
*/ */
final class ConpherenceParticipantQuery final class ConpherenceParticipantQuery
extends PhabricatorOffsetPagedQuery { extends PhabricatorOffsetPagedQuery {
private $conpherencePHIDs; const LIMIT = 100;
private $participantPHIDs; const ORDER_NEWER = 'newer';
private $dateTouched; const ORDER_OLDER = 'older';
private $dateTouchedSort;
private $participationStatus;
public function withConpherencePHIDs(array $phids) { private $participantPHIDs;
$this->conpherencePHIDs = $phids; private $participantCursor;
return $this; private $order = self::ORDER_OLDER;
}
public function withParticipantPHIDs(array $phids) { public function withParticipantPHIDs(array $phids) {
$this->participantPHIDs = $phids; $this->participantPHIDs = $phids;
return $this; return $this;
} }
public function withDateTouched($date, $sort = null) { public function withParticipantCursor(ConpherenceParticipant $participant) {
$this->dateTouched = $date; $this->participantCursor = $participant;
$this->dateTouchedSort = $sort ? $sort : '<';
return $this; return $this;
} }
public function withParticipationStatus($participation_status) { public function setOrder($order) {
$this->participationStatus = $participation_status; $this->order = $order;
return $this; return $this;
} }
@ -49,19 +75,16 @@ final class ConpherenceParticipantQuery
$participants = mpull($participants, null, 'getConpherencePHID'); $participants = mpull($participants, null, 'getConpherencePHID');
if ($this->order == self::ORDER_NEWER) {
$participants = array_reverse($participants);
}
return $participants; return $participants;
} }
private function buildWhereClause($conn_r) { private function buildWhereClause($conn_r) {
$where = array(); $where = array();
if ($this->conpherencePHIDs) {
$where[] = qsprintf(
$conn_r,
'conpherencePHID IN (%Ls)',
$this->conpherencePHIDs);
}
if ($this->participantPHIDs) { if ($this->participantPHIDs) {
$where[] = qsprintf( $where[] = qsprintf(
$conn_r, $conn_r,
@ -69,28 +92,41 @@ final class ConpherenceParticipantQuery
$this->participantPHIDs); $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( $where[] = qsprintf(
$conn_r, $conn_r,
'participationStatus = %d', '(dateTouched %Q %d OR (dateTouched = %d AND id %Q %d))',
$this->participationStatus); $compare_date,
} $date_touched,
$date_touched,
if ($this->dateTouched) { $compare_id,
if ($this->dateTouchedSort) { $id);
$where[] = qsprintf(
$conn_r,
'dateTouched %Q %d',
$this->dateTouchedSort,
$this->dateTouched);
}
} }
return $this->formatWhereClause($where); return $this->formatWhereClause($where);
} }
private function buildOrderClause(AphrontDatabaseConnection $conn_r) { 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;
} }
} }

View file

@ -3,26 +3,33 @@
final class ConpherenceThreadListView extends AphrontView { final class ConpherenceThreadListView extends AphrontView {
private $baseURI; private $baseURI;
private $unreadThreads; private $threads;
private $readThreads; 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) { public function setBaseURI($base_uri) {
$this->baseURI = $base_uri; $this->baseURI = $base_uri;
return $this; 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() { public function render() {
require_celerity_resource('conpherence-menu-css'); require_celerity_resource('conpherence-menu-css');
@ -39,10 +46,8 @@ final class ConpherenceThreadListView extends AphrontView {
->setHref($this->baseURI.'new/') ->setHref($this->baseURI.'new/')
->setType(PhabricatorMenuItemView::TYPE_BUTTON)); ->setType(PhabricatorMenuItemView::TYPE_BUTTON));
$menu->newLabel(pht('Unread')); $menu->newLabel('');
$this->addThreadsToMenu($menu, $this->unreadThreads, $read = false); $this->addThreadsToMenu($menu, $this->threads);
$menu->newLabel(pht('Read'));
$this->addThreadsToMenu($menu, $this->readThreads, $read = true);
return $menu; return $menu;
} }
@ -51,6 +56,28 @@ final class ConpherenceThreadListView extends AphrontView {
return $this->renderThread($thread); 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) { private function renderThreadItem(ConpherenceThread $thread) {
return id(new PhabricatorMenuItemView()) return id(new PhabricatorMenuItemView())
->setType(PhabricatorMenuItemView::TYPE_CUSTOM) ->setType(PhabricatorMenuItemView::TYPE_CUSTOM)
@ -87,28 +114,59 @@ final class ConpherenceThreadListView extends AphrontView {
private function addThreadsToMenu( private function addThreadsToMenu(
PhabricatorMenuView $menu, PhabricatorMenuView $menu,
array $conpherences, array $conpherences) {
$read = false) {
if ($this->scrollUpParticipant->getID()) {
$item = $this->getScrollMenuItem($this->scrollUpParticipant, 'up');
$menu->addMenuItem($item);
}
foreach ($conpherences as $conpherence) { foreach ($conpherences as $conpherence) {
$item = $this->renderThreadItem($conpherence); $item = $this->renderThreadItem($conpherence);
$menu->addMenuItem($item); $menu->addMenuItem($item);
} }
if (empty($conpherences) || $read) { if (empty($conpherences)) {
$menu->addMenuItem($this->getNoConpherencesBlock()); $menu->addMenuItem($this->getNoConpherencesMenuItem());
}
if ($this->scrollDownParticipant->getID()) {
$item = $this->getScrollMenuItem($this->scrollDownParticipant, 'down');
$menu->addMenuItem($item);
} }
return $menu; 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( $message = phutil_tag(
'div', 'div',
array( array(
'class' => 'no-conpherences-menu-item' 'class' => 'no-conpherences-menu-item'
), ),
pht('No more conpherences.')); pht('No conpherences.'));
return id(new PhabricatorMenuItemView()) return id(new PhabricatorMenuItemView())
->setType(PhabricatorMenuItemView::TYPE_CUSTOM) ->setType(PhabricatorMenuItemView::TYPE_CUSTOM)

View file

@ -257,6 +257,11 @@ abstract class PhabricatorApplicationTransactionEditor
throw new Exception("Capability not supported!"); throw new Exception("Capability not supported!");
} }
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
}
public function setContentSource(PhabricatorContentSource $content_source) { public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source; $this->contentSource = $content_source;
return $this; return $this;
@ -386,6 +391,8 @@ abstract class PhabricatorApplicationTransactionEditor
$this->applyExternalEffects($object, $xaction); $this->applyExternalEffects($object, $xaction);
} }
$this->applyFinalEffects($object, $xactions);
if ($read_locking) { if ($read_locking) {
$object->endReadLocking(); $object->endReadLocking();
$read_locking = false; $read_locking = false;

View file

@ -1250,6 +1250,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList {
'type' => 'sql', 'type' => 'sql',
'name' => $this->getPatchPath('20130423.phortunepaymentrevised.sql'), 'name' => $this->getPatchPath('20130423.phortunepaymentrevised.sql'),
), ),
'20130423.conpherenceindices.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('20130423.conpherenceindices.sql'),
),
); );
} }
} }

View file

@ -267,11 +267,11 @@ final class PhabricatorMainMenuView extends AphrontView {
$message_count_id = celerity_generate_unique_node_id(); $message_count_id = celerity_generate_unique_node_id();
$unread_status = ConpherenceParticipationStatus::BEHIND; $unread_status = ConpherenceParticipationStatus::BEHIND;
$unread = id(new ConpherenceParticipantQuery()) $unread = id(new ConpherenceParticipantCountQuery())
->withParticipantPHIDs(array($user->getPHID())) ->withParticipantPHIDs(array($user->getPHID()))
->withParticipationStatus($unread_status) ->withParticipationStatus($unread_status)
->execute(); ->execute();
$message_count_number = count($unread); $message_count_number = $unread[$user->getPHID()];
if ($message_count_number > 999) { if ($message_count_number > 999) {
$message_count_number = "\xE2\x88\x9E"; $message_count_number = "\xE2\x88\x9E";
} }

View file

@ -8,6 +8,7 @@
* javelin-workflow * javelin-workflow
* javelin-behavior-device * javelin-behavior-device
* javelin-history * javelin-history
* javelin-vector
*/ */
JX.behavior('conpherence-menu', function(config) { JX.behavior('conpherence-menu', function(config) {
@ -53,6 +54,15 @@ JX.behavior('conpherence-menu', function(config) {
redrawthread(); redrawthread();
} }
JX.Stratcom.listen(
'conpherence-selectthread',
null,
function (e) {
var node = JX.$(e.getData().id);
selectthread(node);
}
);
function updatepagedata(data) { function updatepagedata(data) {
var uri_suffix = thread.selected + '/'; var uri_suffix = thread.selected + '/';
if (data.use_base_uri) { 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() { function redrawthread() {
if (!thread.node) { if (!thread.node) {
return; return;
@ -96,9 +97,9 @@ JX.behavior('conpherence-menu', function(config) {
var data = JX.Stratcom.getData(thread.node); var data = JX.Stratcom.getData(thread.node);
if (thread.visible !== null || !config.hasThread) { if (thread.visible !== null || !config.hasThread) {
var uri = config.base_uri + data.id + '/'; var uri = config.base_uri + data.id + '/';
new JX.Workflow(uri, {}) new JX.Workflow(uri, {})
.setHandler(onresponse) .setHandler(onloadthreadresponse)
.start(); .start();
} else { } else {
didredrawthread(); didredrawthread();
@ -154,7 +155,7 @@ JX.behavior('conpherence-menu', function(config) {
} }
} }
function onresponse(response) { function onloadthreadresponse(response) {
var header = JX.$H(response.header); var header = JX.$H(response.header);
var messages = JX.$H(response.messages); var messages = JX.$H(response.messages);
var form = JX.$H(response.form); var form = JX.$H(response.form);
@ -252,11 +253,9 @@ JX.behavior('conpherence-menu', function(config) {
}).setData({ oldest_transaction_id : oldest_transaction_id }).send(); }).setData({ oldest_transaction_id : oldest_transaction_id }).send();
}); });
// On mobile, we just show a thread list, so we don't want to automatically // 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 // select or load any threads. On Desktop, we automatically select the first
// thread. // thread.
var old_device = null; var old_device = null;
function ondevicechange() { function ondevicechange() {
var new_device = JX.Device.getDevice(); var new_device = JX.Device.getDevice();
@ -284,16 +283,18 @@ JX.behavior('conpherence-menu', function(config) {
function loadthreads() { function loadthreads() {
var uri = config.base_uri + 'thread/' + config.selectedID + '/'; var uri = config.base_uri + 'thread/' + config.selectedID + '/';
new JX.Workflow(uri) new JX.Workflow(uri)
.setHandler(onthreadresponse) .setHandler(onloadthreadsresponse)
.start(); .start();
} }
function onthreadresponse(r) { function onloadthreadsresponse(r) {
var layout = JX.$(config.layoutID); var layout = JX.$(config.layoutID);
var menu = JX.DOM.find(layout, 'div', 'conpherence-menu-pane'); var menu = JX.DOM.find(layout, 'div', 'conpherence-menu-pane');
JX.DOM.setContent(menu, JX.$H(r)); JX.DOM.setContent(menu, JX.$H(r));
config.selectedID && selectthreadid(config.selectedID); config.selectedID && selectthreadid(config.selectedID);
thread.node.scrollIntoView();
} }
function didloadthreads() { function didloadthreads() {
@ -316,4 +317,71 @@ JX.behavior('conpherence-menu', function(config) {
redrawthread(); 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
);
}); });