diff --git a/src/applications/conpherence/controller/ConpherenceController.php b/src/applications/conpherence/controller/ConpherenceController.php index ad7f45d58d..764797b26f 100644 --- a/src/applications/conpherence/controller/ConpherenceController.php +++ b/src/applications/conpherence/controller/ConpherenceController.php @@ -1,177 +1,186 @@ 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(); $nav->newLink( pht('New Message'), $this->getApplicationURI('new/')); return $nav; } public function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); $crumbs ->addAction( id(new PhabricatorMenuItemView()) ->setName(pht('New Message')) ->setHref($this->getApplicationURI('new/')) ->setIcon('create')) ->addCrumb( id(new PhabricatorCrumbView()) ->setName(pht('Conpherence'))); return $crumbs; } protected function buildHeaderPaneContent(ConpherenceThread $conpherence) { $user = $this->getRequest()->getUser(); $display_data = $conpherence->getDisplayData( $user, ConpherenceImageData::SIZE_HEAD); $edit_href = $this->getApplicationURI('update/'.$conpherence->getID().'/'); $class_mod = $display_data['image_class']; return array( phutil_tag( 'div', array( 'class' => 'upload-photo' ), pht('Drop photo here to change this Conpherence photo.')), javelin_tag( 'a', array( 'class' => 'edit', 'href' => $edit_href, 'sigil' => 'conpherence-edit-metadata', 'meta' => array( 'action' => 'metadata' ) ), ''), phutil_tag( 'div', array( 'class' => $class_mod.'header-image', 'style' => 'background-image: url('.$display_data['image'].');' ), ''), phutil_tag( 'div', array( 'class' => $class_mod.'title', ), $display_data['title']), phutil_tag( 'div', array( 'class' => $class_mod.'subtitle', ), $display_data['subtitle']), ); } protected function renderConpherenceTransactions( ConpherenceThread $conpherence) { $user = $this->getRequest()->getUser(); $transactions = $conpherence->getTransactions(); + $oldest_transaction_id = 0; + $too_many = ConpherenceThreadQuery::TRANSACTION_LIMIT + 1; + if (count($transactions) == $too_many) { + $last_transaction = end($transactions); + unset($transactions[$last_transaction->getID()]); + } + $transactions = array_reverse($transactions); + $oldest_transaction = reset($transactions); + $oldest_transaction_id = $oldest_transaction->getID(); $handles = $conpherence->getHandles(); $rendered_transactions = array(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($user); foreach ($transactions as $key => $transaction) { if ($transaction->shouldHide()) { unset($transactions[$key]); continue; } if ($transaction->getComment()) { $engine->addObject( $transaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); foreach ($transactions as $transaction) { $rendered_transactions[] = id(new ConpherenceTransactionView()) ->setUser($user) ->setConpherenceTransaction($transaction) ->setHandles($handles) ->setMarkupEngine($engine) ->render(); } $latest_transaction_id = $transaction->getID(); - $rendered_transactions = phutil_implode_html(' ', $rendered_transactions); return array( 'transactions' => $rendered_transactions, - 'latest_transaction_id' => $latest_transaction_id + 'latest_transaction_id' => $latest_transaction_id, + 'oldest_transaction_id' => $oldest_transaction_id ); } } diff --git a/src/applications/conpherence/controller/ConpherenceUpdateController.php b/src/applications/conpherence/controller/ConpherenceUpdateController.php index 30c8b25cac..4fb76990e0 100644 --- a/src/applications/conpherence/controller/ConpherenceUpdateController.php +++ b/src/applications/conpherence/controller/ConpherenceUpdateController.php @@ -1,407 +1,407 @@ conpherenceID = $conpherence_id; return $this; } public function getConpherenceID() { return $this->conpherenceID; } public function willProcessRequest(array $data) { $this->setConpherenceID(idx($data, 'id')); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $conpherence_id = $this->getConpherenceID(); if (!$conpherence_id) { return new Aphront404Response(); } $conpherence = id(new ConpherenceThreadQuery()) ->setViewer($user) ->withIDs(array($conpherence_id)) ->needFilePHIDs(true) ->needOrigPics(true) ->needHeaderPics(true) ->executeOne(); $supported_formats = PhabricatorFile::getTransformableImageFormats(); $action = $request->getStr('action', ConpherenceUpdateActions::METADATA); $latest_transaction_id = null; $response_mode = 'ajax'; $error_view = null; $e_file = array(); $errors = array(); if ($request->isFormPost()) { $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_WEB, array( 'ip' => $request->getRemoteAddr() )); $editor = id(new ConpherenceEditor()) ->setContinueOnNoEffect($request->isContinueRequest()) ->setContentSource($content_source) ->setActor($user); switch ($action) { case ConpherenceUpdateActions::MESSAGE: $message = $request->getStr('text'); $xactions = $editor->generateTransactionsFromText( $conpherence, $message); break; case ConpherenceUpdateActions::ADD_PERSON: $xactions = array(); $person_tokenizer = $request->getArr('add_person'); $person_phid = reset($person_tokenizer); if ($person_phid) { $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType( ConpherenceTransactionType::TYPE_PARTICIPANTS) ->setNewValue(array('+' => array($person_phid))); } break; case ConpherenceUpdateActions::REMOVE_PERSON: $xactions = array(); if (!$request->isContinueRequest()) { // do nothing; we'll display a confirmation dialogue instead break; } $person_phid = $request->getStr('remove_person'); if ($person_phid && $person_phid == $user->getPHID()) { $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType( ConpherenceTransactionType::TYPE_PARTICIPANTS) ->setNewValue(array('-' => array($person_phid))); $response_mode = 'go-home'; } break; case ConpherenceUpdateActions::NOTIFICATIONS: $notifications = $request->getStr('notifications'); $participant = $conpherence->getParticipant($user->getPHID()); $participant->setSettings(array('notifications' => $notifications)); $participant->save(); $result = pht( 'Updated notification settings to "%s".', ConpherenceSettings::getHumanString($notifications)); return id(new AphrontAjaxResponse()) ->setContent($result); break; case ConpherenceUpdateActions::METADATA: $xactions = array(); $top = $request->getInt('image_y'); $left = $request->getInt('image_x'); $file_id = $request->getInt('file_id'); $title = $request->getStr('title'); $updated = false; if ($file_id) { $orig_file = id(new PhabricatorFileQuery()) ->setViewer($user) ->withIDs(array($file_id)) ->executeOne(); $okay = $orig_file->isTransformableImage(); if ($okay) { $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType(ConpherenceTransactionType::TYPE_PICTURE) ->setNewValue($orig_file->getPHID()); // do a transformation "crudely" $xformer = new PhabricatorImageTransformer(); $header_file = $xformer->executeConpherenceTransform( $orig_file, 0, 0, ConpherenceImageData::HEAD_WIDTH, ConpherenceImageData::HEAD_HEIGHT); // this is handled outside the editor for now. no particularly // good reason to move it inside $conpherence->setImagePHIDs( array( ConpherenceImageData::SIZE_HEAD => $header_file->getPHID(), )); $conpherence->setImages( array( ConpherenceImageData::SIZE_HEAD => $header_file, )); } else { $e_file[] = $orig_file; $errors[] = pht('This server only supports these image formats: %s.', implode(', ', $supported_formats)); } // use the existing title in this image upload case $title = $conpherence->getTitle(); $updated = true; $response_mode = 'redirect'; } // all other metadata updates are continue requests if (!$request->isContinueRequest()) { break; } if ($top !== null || $left !== null) { $file = $conpherence->getImage(ConpherenceImageData::SIZE_ORIG); $xformer = new PhabricatorImageTransformer(); $xformed = $xformer->executeConpherenceTransform( $file, $top, $left, ConpherenceImageData::HEAD_WIDTH, ConpherenceImageData::HEAD_HEIGHT); $image_phid = $xformed->getPHID(); $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType( ConpherenceTransactionType::TYPE_PICTURE_CROP) ->setNewValue($image_phid); $updated = true; } if ($title != $conpherence->getTitle()) { $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType(ConpherenceTransactionType::TYPE_TITLE) ->setNewValue($title); $updated = true; } if (!$updated) { $errors[] = pht( 'That was a non-update. Try cancel.'); } break; default: throw new Exception('Unknown action: '.$action); break; } if ($xactions) { try { $xactions = $editor->applyTransactions($conpherence, $xactions); } catch (PhabricatorApplicationTransactionNoEffectException $ex) { return id(new PhabricatorApplicationTransactionNoEffectResponse()) ->setCancelURI($this->getApplicationURI($conpherence_id.'/')) ->setException($ex); } switch ($response_mode) { case 'ajax': $latest_transaction_id = $request->getInt('latest_transaction_id'); $content = $this->loadAndRenderUpdates( $action, $conpherence_id, $latest_transaction_id); return id(new AphrontAjaxResponse()) ->setContent($content); break; case 'go-home': return id(new AphrontRedirectResponse()) ->setURI($this->getApplicationURI()); break; case 'redirect': default: return id(new AphrontRedirectResponse()) ->setURI($this->getApplicationURI($conpherence->getID().'/')); break; } } } if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle(pht('Errors editing conpherence.')) ->setInsideDialogue(true) ->setErrors($errors); } switch ($action) { case ConpherenceUpdateActions::REMOVE_PERSON: $dialogue = $this->renderRemovePersonDialogue($conpherence); break; case ConpherenceUpdateActions::METADATA: default: $dialogue = $this->renderMetadataDialogue($conpherence, $error_view); break; } return id(new AphrontDialogResponse()) ->setDialog($dialogue ->setUser($user) ->setWidth(AphrontDialogView::WIDTH_FORM) ->setSubmitURI($this->getApplicationURI('update/'.$conpherence_id.'/')) ->addSubmitButton() ->addCancelButton($this->getApplicationURI($conpherence->getID().'/'))); } private function renderRemovePersonDialogue( ConpherenceThread $conpherence) { $request = $this->getRequest(); $user = $request->getUser(); $remove_person = $request->getStr('remove_person'); $participants = $conpherence->getParticipants(); $message = pht( 'Are you sure you want to remove yourself from this conpherence? '); if (count($participants) == 1) { $message .= pht( 'The conpherence will be inaccessible forever and ever.'); } else { $message .= pht( 'Someone else in the conpherence can add you back later.'); } $body = phutil_tag( 'p', array( ), $message); require_celerity_resource('conpherence-update-css'); return id(new AphrontDialogView()) ->setTitle(pht('Update Conpherence Participants')) ->addHiddenInput('action', 'remove_person') ->addHiddenInput('__continue__', true) ->addHiddenInput('remove_person', $remove_person) ->appendChild($body); } private function renderMetadataDialogue( ConpherenceThread $conpherence, $error_view) { $form = id(new AphrontFormLayoutView()) ->appendChild($error_view) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Title')) ->setName('title') ->setValue($conpherence->getTitle())); $image = $conpherence->getImage(ConpherenceImageData::SIZE_ORIG); if ($image) { $form ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Image')) ->setValue(phutil_tag( 'img', array( 'src' => $conpherence->loadImageURI(ConpherenceImageData::SIZE_HEAD), )))) ->appendChild( id(new AphrontFormCropControl()) ->setLabel(pht('Crop Image')) ->setValue($image) ->setWidth(ConpherenceImageData::HEAD_WIDTH) ->setHeight(ConpherenceImageData::HEAD_HEIGHT)) ->appendChild( id(new ConpherenceFormDragAndDropUploadControl()) ->setLabel(pht('Change Image'))); } else { $form ->appendChild( id(new ConpherenceFormDragAndDropUploadControl()) ->setLabel(pht('Image'))); } require_celerity_resource('conpherence-update-css'); return id(new AphrontDialogView()) ->setTitle(pht('Update Conpherence')) ->addHiddenInput('action', 'metadata') ->addHiddenInput('__continue__', true) ->appendChild($form); } private function loadAndRenderUpdates( $action, $conpherence_id, $latest_transaction_id) { $need_header_pics = false; $need_widget_data = false; $need_transactions = false; switch ($action) { case ConpherenceUpdateActions::METADATA: $need_header_pics = true; $need_transactions = true; break; case ConpherenceUpdateActions::MESSAGE: case ConpherenceUpdateActions::ADD_PERSON: $need_transactions = true; $need_widget_data = true; break; case ConpherenceUpdateActions::REMOVE_PERSON: case ConpherenceUpdateActions::NOTIFICATIONS: default: break; } $user = $this->getRequest()->getUser(); $conpherence = id(new ConpherenceThreadQuery()) ->setViewer($user) - ->setAfterMessageID($latest_transaction_id) + ->setAfterTransactionID($latest_transaction_id) ->needHeaderPics($need_header_pics) ->needWidgetData($need_widget_data) ->needTransactions($need_transactions) ->withIDs(array($conpherence_id)) ->executeOne(); if ($need_transactions) { $data = $this->renderConpherenceTransactions($conpherence); } else { $data = array(); } $rendered_transactions = idx($data, 'transactions'); $new_latest_transaction_id = idx($data, 'latest_transaction_id'); $widget_uri = $this->getApplicationURI('update/'.$conpherence->getID().'/'); $nav_item = null; $header = null; $people_widget = null; $file_widget = null; switch ($action) { case ConpherenceUpdateActions::METADATA: $header = $this->buildHeaderPaneContent($conpherence); $nav_item = id(new ConpherenceThreadListView()) ->setUser($user) ->setBaseURI($this->getApplicationURI()) ->renderSingleThread($conpherence); break; case ConpherenceUpdateActions::MESSAGE: $file_widget = id(new ConpherenceFileWidgetView()) ->setUser($this->getRequest()->getUser()) ->setConpherence($conpherence) ->setUpdateURI($widget_uri); break; case ConpherenceUpdateActions::ADD_PERSON: $people_widget = id(new ConpherencePeopleWidgetView()) ->setUser($user) ->setConpherence($conpherence) ->setUpdateURI($widget_uri); break; case ConpherenceUpdateActions::REMOVE_PERSON: case ConpherenceUpdateActions::NOTIFICATIONS: default: break; } $content = array( 'transactions' => $rendered_transactions, 'latest_transaction_id' => $new_latest_transaction_id, 'nav_item' => hsprintf('%s', $nav_item), 'conpherence_phid' => $conpherence->getPHID(), 'header' => hsprintf('%s', $header), 'file_widget' => $file_widget ? $file_widget->render() : null, 'people_widget' => $people_widget ? $people_widget->render() : null, ); return $content; } } diff --git a/src/applications/conpherence/controller/ConpherenceViewController.php b/src/applications/conpherence/controller/ConpherenceViewController.php index 13691e400f..0f5857bcb4 100644 --- a/src/applications/conpherence/controller/ConpherenceViewController.php +++ b/src/applications/conpherence/controller/ConpherenceViewController.php @@ -1,147 +1,174 @@ conpherence = $conpherence; return $this; } public function getConpherence() { return $this->conpherence; } public function setConpherenceID($conpherence_id) { $this->conpherenceID = $conpherence_id; return $this; } public function getConpherenceID() { return $this->conpherenceID; } public function willProcessRequest(array $data) { $this->setConpherenceID(idx($data, 'id')); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $conpherence_id = $this->getConpherenceID(); if (!$conpherence_id) { return new Aphront404Response(); } - $conpherence = id(new ConpherenceThreadQuery()) + $query = id(new ConpherenceThreadQuery()) ->setViewer($user) ->withIDs(array($conpherence_id)) ->needHeaderPics(true) + ->needParticipantCache(true) ->needTransactions(true) - ->executeOne(); + ->setTransactionLimit(ConpherenceThreadQuery::TRANSACTION_LIMIT); + $before_transaction_id = $request->getInt('oldest_transaction_id'); + if ($before_transaction_id) { + $query + ->setBeforeTransactionID($before_transaction_id); + } + $conpherence = $query->executeOne(); $this->setConpherence($conpherence); $participant = $conpherence->getParticipant($user->getPHID()); $transactions = $conpherence->getTransactions(); $latest_transaction = end($transactions); $write_guard = AphrontWriteGuard::beginScopedUnguardedWrites(); $participant->markUpToDate($conpherence, $latest_transaction); unset($write_guard); - $header = $this->renderHeaderPaneContent(); - $messages = $this->renderMessagePaneContent(); - $content = $header + $messages; + $data = $this->renderConpherenceTransactions($conpherence); + $messages = $this->renderMessagePaneContent( + $data['transactions'], + $data['oldest_transaction_id']); + if ($before_transaction_id) { + $header = null; + $form = null; + $content = array('messages' => $messages); + } else { + $header = $this->renderHeaderPaneContent(); + $form = $this->renderFormContent($data['latest_transaction_id']); + $content = array( + 'header' => $header, + 'messages' => $messages, + 'form' => $form + ); + } if ($request->isAjax()) { return id(new AphrontAjaxResponse())->setContent($content); } $layout = id(new ConpherenceLayoutView()) ->setBaseURI($this->getApplicationURI()) ->setThread($conpherence) ->setHeader($header) - ->setMessages($messages['messages']) - ->setReplyForm($messages['form']) + ->setMessages($messages) + ->setReplyForm($form) ->setRole('thread'); return $this->buildApplicationPage( $layout, array( 'title' => $conpherence->getTitle(), 'device' => true, )); } private function renderHeaderPaneContent() { require_celerity_resource('conpherence-header-pane-css'); $conpherence = $this->getConpherence(); $header = $this->buildHeaderPaneContent($conpherence); - return array('header' => hsprintf('%s', $header)); + return hsprintf('%s', $header); } - private function renderMessagePaneContent() { + private function renderMessagePaneContent( + array $transactions, + $oldest_transaction_id) { + require_celerity_resource('conpherence-message-pane-css'); - $user = $this->getRequest()->getUser(); - $conpherence = $this->getConpherence(); - $data = $this->renderConpherenceTransactions($conpherence); - $latest_transaction_id = $data['latest_transaction_id']; - $transactions = $data['transactions']; + $scrollbutton = ''; + if ($oldest_transaction_id) { + $scrollbutton = javelin_tag( + 'a', + array( + 'href' => '#', + 'mustcapture' => true, + 'sigil' => 'show-older-messages', + 'class' => 'conpherence-show-older-messages', + 'meta' => array( + 'oldest_transaction_id' => $oldest_transaction_id + ) + ), + pht('Show Older Messages')); + } + return hsprintf('%s%s', $scrollbutton, $transactions); + } + + private function renderFormContent($latest_transaction_id) { + + $conpherence = $this->getConpherence(); + $user = $this->getRequest()->getUser(); $update_uri = $this->getApplicationURI('update/'.$conpherence->getID().'/'); Javelin::initBehavior('conpherence-pontificate'); $form = id(new AphrontFormView()) ->setAction($update_uri) ->setFlexible(true) ->addSigil('conpherence-pontificate') ->setWorkflow(true) ->setUser($user) ->addHiddenInput('action', 'message') ->appendChild( id(new PhabricatorRemarkupControl()) ->setUser($user) ->setName('text')) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Pontificate'))) ->appendChild( javelin_tag( 'input', array( 'type' => 'hidden', 'name' => 'latest_transaction_id', 'value' => $latest_transaction_id, 'sigil' => 'latest-transaction-id', 'meta' => array( 'id' => $latest_transaction_id ) ), '')) ->render(); - $scrollbutton = javelin_tag( - 'a', - array( - 'href' => '#', - 'mustcapture' => true, - 'sigil' => 'show-older-messages', - 'class' => 'conpherence-show-older-messages', - ), - pht('Show Older Messages')); - - return array( - 'messages' => hsprintf('%s%s', $scrollbutton, $transactions), - 'form' => $form - ); - + return $form; } } diff --git a/src/applications/conpherence/query/ConpherenceThreadQuery.php b/src/applications/conpherence/query/ConpherenceThreadQuery.php index b92139760f..12402e6bc0 100644 --- a/src/applications/conpherence/query/ConpherenceThreadQuery.php +++ b/src/applications/conpherence/query/ConpherenceThreadQuery.php @@ -1,318 +1,345 @@ needFilePHIDs = $need_file_phids; return $this; } public function needParticipantCache($participant_cache) { $this->needParticipantCache = $participant_cache; return $this; } public function needOrigPics($need_orig_pics) { $this->needOrigPics = $need_orig_pics; return $this; } public function needHeaderPics($need_header_pics) { $this->needHeaderPics = $need_header_pics; return $this; } public function needWidgetData($need_widget_data) { $this->needWidgetData = $need_widget_data; return $this; } public function needTransactions($need_transactions) { $this->needTransactions = $need_transactions; return $this; } public function withIDs(array $ids) { $this->ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } - // TODO: This is pretty hacky!!!!~~ - public function setAfterMessageID($id) { - $this->afterMessageID = $id; + public function setAfterTransactionID($id) { + $this->afterTransactionID = $id; + return $this; + } + + public function setBeforeTransactionID($id) { + $this->beforeTransactionID = $id; + return $this; + } + + public function setTransactionLimit($transaction_limit) { + $this->transactionLimit = $transaction_limit; return $this; } + public function getTransactionLimit() { + return $this->transactionLimit; + } + protected function loadPage() { $table = new ConpherenceThread(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT conpherence_thread.* FROM %T conpherence_thread %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $conpherences = $table->loadAllFromArray($data); if ($conpherences) { $conpherences = mpull($conpherences, null, 'getPHID'); $this->loadParticipantsAndInitHandles($conpherences); if ($this->needParticipantCache) { $this->loadCoreHandles($conpherences, 'getRecentParticipantPHIDs'); } else if ($this->needWidgetData) { $this->loadCoreHandles($conpherences, 'getParticipantPHIDs'); } if ($this->needTransactions) { $this->loadTransactionsAndHandles($conpherences); } if ($this->needFilePHIDs || $this->needWidgetData) { $this->loadFilePHIDs($conpherences); } if ($this->needWidgetData) { $this->loadWidgetData($conpherences); } if ($this->needOrigPics) { $this->loadOrigPics($conpherences); } if ($this->needHeaderPics) { $this->loadHeaderPics($conpherences); } } return $conpherences; } protected function buildWhereClause($conn_r) { $where = array(); $where[] = $this->buildPagingClause($conn_r); if ($this->ids) { $where[] = qsprintf( $conn_r, 'id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn_r, 'phid IN (%Ls)', $this->phids); } return $this->formatWhereClause($where); } private function loadParticipantsAndInitHandles(array $conpherences) { $participants = id(new ConpherenceParticipant()) ->loadAllWhere('conpherencePHID IN (%Ls)', array_keys($conpherences)); $map = mgroup($participants, 'getConpherencePHID'); foreach ($map as $conpherence_phid => $conpherence_participants) { $current_conpherence = $conpherences[$conpherence_phid]; $conpherence_participants = mpull( $conpherence_participants, null, 'getParticipantPHID'); $current_conpherence->attachParticipants($conpherence_participants); $current_conpherence->attachHandles(array()); } return $this; } private function loadCoreHandles( array $conpherences, $method) { $handle_phids = array(); foreach ($conpherences as $conpherence) { $handle_phids[$conpherence->getPHID()] = $conpherence->$method(); } $flat_phids = array_mergev($handle_phids); $handles = id(new PhabricatorObjectHandleData($flat_phids)) ->setViewer($this->getViewer()) ->loadHandles(); foreach ($handle_phids as $conpherence_phid => $phids) { $conpherence = $conpherences[$conpherence_phid]; $conpherence->attachHandles(array_select_keys($handles, $phids)); } return $this; } private function loadTransactionsAndHandles(array $conpherences) { - $transactions = id(new ConpherenceTransactionQuery()) + $query = id(new ConpherenceTransactionQuery()) ->setViewer($this->getViewer()) ->withObjectPHIDs(array_keys($conpherences)) - ->needHandles(true) - ->setAfterID($this->afterMessageID) - ->execute(); - + ->needHandles(true); + + // We have to flip these for the underyling query class. The semantics of + // paging are tricky business. + if ($this->afterTransactionID) { + $query->setBeforeID($this->afterTransactionID); + } else if ($this->beforeTransactionID) { + $query->setAfterID($this->beforeTransactionID); + } + if ($this->getTransactionLimit()) { + // fetch an extra for "show older" scenarios + $query->setLimit($this->getTransactionLimit() + 1); + } + $transactions = $query->execute(); $transactions = mgroup($transactions, 'getObjectPHID'); foreach ($conpherences as $phid => $conpherence) { $current_transactions = $transactions[$phid]; $handles = array(); foreach ($current_transactions as $transaction) { $handles += $transaction->getHandles(); } $conpherence->attachHandles($conpherence->getHandles() + $handles); $conpherence->attachTransactions($transactions[$phid]); } return $this; } private function loadFilePHIDs(array $conpherences) { $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_FILE; $file_edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array_keys($conpherences)) ->withEdgeTypes(array($edge_type)) ->execute(); foreach ($file_edges as $conpherence_phid => $data) { $conpherence = $conpherences[$conpherence_phid]; $conpherence->attachFilePHIDs(array_keys($data[$edge_type])); } return $this; } private function loadWidgetData(array $conpherences) { $participant_phids = array(); $file_phids = array(); foreach ($conpherences as $conpherence) { $participant_phids[] = array_keys($conpherence->getParticipants()); $file_phids[] = $conpherence->getFilePHIDs(); } $participant_phids = array_mergev($participant_phids); $file_phids = array_mergev($file_phids); // statuses of everyone currently in the conpherence // for a rolling one week window $start_of_week = phabricator_format_local_time( strtotime('last monday', strtotime('tomorrow')), $this->getViewer(), 'U'); $end_of_week = phabricator_format_local_time( strtotime('last monday +1 week', strtotime('tomorrow')), $this->getViewer(), 'U'); $statuses = id(new PhabricatorUserStatus()) ->loadAllWhere( 'userPHID in (%Ls) AND dateTo >= %d AND dateFrom <= %d', $participant_phids, $start_of_week, $end_of_week); $statuses = mgroup($statuses, 'getUserPHID'); // attached files $files = array(); $file_author_phids = array(); $authors = array(); if ($file_phids) { $files = id(new PhabricatorFileQuery()) ->setViewer($this->getViewer()) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); $file_author_phids = mpull($files, 'getAuthorPHID', 'getPHID'); $authors = id(new PhabricatorObjectHandleData($file_author_phids)) ->setViewer($this->getViewer()) ->loadHandles(); $authors = mpull($authors, null, 'getPHID'); } foreach ($conpherences as $phid => $conpherence) { $participant_phids = array_keys($conpherence->getParticipants()); $statuses = array_select_keys($statuses, $participant_phids); $statuses = array_mergev($statuses); $statuses = msort($statuses, 'getDateFrom'); $conpherence_files = array(); $files_authors = array(); foreach ($conpherence->getFilePHIDs() as $curr_phid) { $curr_file = idx($files, $curr_phid); if (!$curr_file) { // this file was deleted or user doesn't have permission to see it // this is generally weird continue; } $conpherence_files[$curr_phid] = $curr_file; // some files don't have authors so be careful $current_author = null; $current_author_phid = idx($file_author_phids, $curr_phid); if ($current_author_phid) { $current_author = $authors[$current_author_phid]; } $files_authors[$curr_phid] = $current_author; } $widget_data = array( 'statuses' => $statuses, 'files' => $conpherence_files, 'files_authors' => $files_authors ); $conpherence->attachWidgetData($widget_data); } return $this; } private function loadOrigPics(array $conpherences) { return $this->loadPics( $conpherences, ConpherenceImageData::SIZE_ORIG); } private function loadHeaderPics(array $conpherences) { return $this->loadPics( $conpherences, ConpherenceImageData::SIZE_HEAD); } private function loadPics(array $conpherences, $size) { $conpherence_pic_phids = array(); foreach ($conpherences as $conpherence) { $phid = $conpherence->getImagePHID($size); if ($phid) { $conpherence_pic_phids[$conpherence->getPHID()] = $phid; } } if (!$conpherence_pic_phids) { return $this; } $files = id(new PhabricatorFileQuery()) ->setViewer($this->getViewer()) ->withPHIDs($conpherence_pic_phids) ->execute(); $files = mpull($files, null, 'getPHID'); foreach ($conpherence_pic_phids as $conpherence_phid => $pic_phid) { $conpherences[$conpherence_phid]->setImage($files[$pic_phid], $size); } return $this; } } diff --git a/src/applications/conpherence/query/ConpherenceTransactionQuery.php b/src/applications/conpherence/query/ConpherenceTransactionQuery.php index dc65c3c70b..c3feefa3d1 100644 --- a/src/applications/conpherence/query/ConpherenceTransactionQuery.php +++ b/src/applications/conpherence/query/ConpherenceTransactionQuery.php @@ -1,13 +1,18 @@ getID(); } protected function getReversePaging() { return false; } protected function nextPage(array $page) { if ($this->beforeID) { $this->beforeID = $this->getPagingValue(last($page)); } else { $this->afterID = $this->getPagingValue(last($page)); } } final public function setAfterID($object_id) { $this->afterID = $object_id; return $this; } + final protected function getAfterID() { + return $this->afterID; + } + final public function setBeforeID($object_id) { $this->beforeID = $object_id; return $this; } + final protected function getBeforeID() { + return $this->beforeID; + } + final protected function buildLimitClause(AphrontDatabaseConnection $conn_r) { if ($this->getRawResultLimit()) { return qsprintf($conn_r, 'LIMIT %d', $this->getRawResultLimit()); } else { return ''; } } final protected function buildPagingClause( AphrontDatabaseConnection $conn_r) { if ($this->beforeID) { return qsprintf( $conn_r, '%Q %Q %s', $this->getPagingColumn(), $this->getReversePaging() ? '<' : '>', $this->beforeID); } else if ($this->afterID) { return qsprintf( $conn_r, '%Q %Q %s', $this->getPagingColumn(), $this->getReversePaging() ? '>' : '<', $this->afterID); } return null; } final protected function buildOrderClause(AphrontDatabaseConnection $conn_r) { if ($this->beforeID) { return qsprintf( $conn_r, 'ORDER BY %Q %Q', $this->getPagingColumn(), $this->getReversePaging() ? 'DESC' : 'ASC'); } else { return qsprintf( $conn_r, 'ORDER BY %Q %Q', $this->getPagingColumn(), $this->getReversePaging() ? 'ASC' : 'DESC'); } } final protected function didLoadResults(array $results) { if ($this->beforeID) { $results = array_reverse($results, $preserve_keys = true); } return $results; } final public function executeWithCursorPager(AphrontCursorPagerView $pager) { $this->setLimit($pager->getPageSize() + 1); if ($pager->getAfterID()) { $this->setAfterID($pager->getAfterID()); } else if ($pager->getBeforeID()) { $this->setBeforeID($pager->getBeforeID()); } $results = $this->execute(); $sliced_results = $pager->sliceResults($results); if ($pager->getBeforeID() || (count($results) > $pager->getPageSize())) { $pager->setNextPageID($this->getPagingValue(last($sliced_results))); } if ($pager->getAfterID() || ($pager->getBeforeID() && (count($results) > $pager->getPageSize()))) { $pager->setPrevPageID($this->getPagingValue(head($sliced_results))); } return $sliced_results; } } diff --git a/webroot/rsrc/js/application/conpherence/behavior-menu.js b/webroot/rsrc/js/application/conpherence/behavior-menu.js index 2a62235d2a..24a5b7f7ac 100644 --- a/webroot/rsrc/js/application/conpherence/behavior-menu.js +++ b/webroot/rsrc/js/application/conpherence/behavior-menu.js @@ -1,314 +1,316 @@ /** * @provides javelin-behavior-conpherence-menu * @requires javelin-behavior * javelin-dom * javelin-util * javelin-request * javelin-stratcom * javelin-workflow * javelin-behavior-device * javelin-history */ JX.behavior('conpherence-menu', function(config) { var thread = { selected: null, node: null, visible: null }; function selectthreadid(id, updatePageData) { var threads = JX.DOM.scry(document.body, 'a', 'conpherence-menu-click'); for (var ii = 0; ii < threads.length; ii++) { var data = JX.Stratcom.getData(threads[ii]); if (data.id == id) { selectthread(threads[ii], updatePageData); return; } } } function selectthread(node, updatePageData) { if (thread.node) { JX.DOM.alterClass(thread.node, 'conpherence-selected', false); // keep the unread-count hidden still. big TODO once we ajax in updates // to threads to make this work right and move threads between read / // unread } JX.DOM.alterClass(node, 'conpherence-selected', true); JX.DOM.alterClass(node, 'hide-unread-count', true); thread.node = node; var data = JX.Stratcom.getData(node); thread.selected = data.id; if (updatePageData) { updatepagedata(data); } redrawthread(); } function updatepagedata(data) { var uri_suffix = thread.selected + '/'; if (data.use_base_uri) { uri_suffix = ''; } JX.History.replace(config.base_uri + uri_suffix); if (data.title) { document.title = data.title; } } JX.Stratcom.listen( 'conpherence-update-page-data', null, function (e) { updatepagedata(e.getData()); } ); JX.Stratcom.listen( 'conpherence-selectthread', null, function (e) { var node = JX.$(e.getData().id); selectthread(node); } ); function redrawthread() { if (!thread.node) { return; } if (thread.visible == thread.selected) { return; } var data = JX.Stratcom.getData(thread.node); if (thread.visible !== null || !config.hasThread) { var uri = config.base_uri + data.id + '/'; new JX.Workflow(uri, {}) .setHandler(onresponse) .start(); } else { didredrawthread(); } if (thread.visible !== null || !config.hasWidgets) { var widget_uri = config.base_uri + 'widget/' + data.id + '/'; new JX.Workflow(widget_uri, {}) .setHandler(onwidgetresponse) .start(); } else { updatetoggledwidget(); } thread.visible = thread.selected; } function onwidgetresponse(response) { var root = JX.DOM.find(document, 'div', 'conpherence-layout'); var widgetsRoot = JX.DOM.find(root, 'div', 'conpherence-widget-pane'); JX.DOM.setContent(widgetsRoot, JX.$H(response.widgets)); updatetoggledwidget(); } function updatetoggledwidget() { var device = JX.Device.getDevice(); if (device != 'desktop') { if (config.role == 'list') { JX.Stratcom.invoke( 'conpherence-toggle-widget', null, { widget : 'conpherence-menu-pane' } ); } else { JX.Stratcom.invoke( 'conpherence-toggle-widget', null, { widget : 'conpherence-message-pane' } ); } } else { JX.Stratcom.invoke( 'conpherence-toggle-widget', null, { widget : 'widgets-files' } ); } } function onresponse(response) { var header = JX.$H(response.header); var messages = JX.$H(response.messages); var form = JX.$H(response.form); var root = JX.DOM.find(document, 'div', 'conpherence-layout'); var headerRoot = JX.DOM.find(root, 'div', 'conpherence-header-pane'); var messagesRoot = JX.DOM.find(root, 'div', 'conpherence-messages'); var formRoot = JX.DOM.find(root, 'div', 'conpherence-form'); JX.DOM.setContent(headerRoot, header); JX.DOM.setContent(messagesRoot, messages); JX.DOM.setContent(formRoot, form); didredrawthread(); } function didredrawthread() { var root = JX.DOM.find(document, 'div', 'conpherence-layout'); var messagesRoot = JX.DOM.find(root, 'div', 'conpherence-messages'); messagesRoot.scrollTop = messagesRoot.scrollHeight; } JX.Stratcom.listen( null, 'conpherence-redraw-thread', function (e) { didredrawthread(); } ); JX.Stratcom.listen( 'click', 'conpherence-menu-click', function(e) { if (!e.isNormalClick()) { return; } // On devices, just follow the link normally. if (JX.Device.getDevice() != 'desktop') { return; } e.kill(); selectthread(e.getNode('conpherence-menu-click'), true); }); JX.Stratcom.listen('click', 'conpherence-edit-metadata', function (e) { e.kill(); var root = e.getNode('conpherence-layout'); var form = JX.DOM.find(root, 'form', 'conpherence-pontificate'); var data = e.getNodeData('conpherence-edit-metadata'); var header = JX.DOM.find(root, 'div', 'conpherence-header-pane'); var messages = JX.DOM.find(root, 'div', 'conpherence-messages'); new JX.Workflow.newFromForm(form, data) .setHandler(JX.bind(this, function(r) { JX.DOM.appendContent(messages, JX.$H(r.transactions)); messages.scrollTop = messages.scrollHeight; JX.DOM.setContent( header, JX.$H(r.header) ); try { // update the menu entry JX.DOM.replace( JX.$(r.conpherence_phid + '-nav-item'), JX.$H(r.nav_item) ); JX.Stratcom.invoke( 'conpherence-selectthread', null, { id : r.conpherence_phid + '-nav-item' } ); } catch (ex) { // Ignore; this view may not have a menu. } })) .start(); }); JX.Stratcom.listen('click', 'show-older-messages', function(e) { e.kill(); - var last_offset = e.getNodeData('show-older-messages').offset; - var conf_id = e.getNodeData('show-older-messages').ID; + var data = e.getNodeData('show-older-messages'); + var oldest_transaction_id = data.oldest_transaction_id; + var conf_id = thread.selected; JX.DOM.remove(e.getNode('show-older-messages')); var root = JX.DOM.find(document, 'div', 'conpherence-layout'); var messages_root = JX.DOM.find(root, 'div', 'conpherence-messages'); new JX.Request(config.base_uri + conf_id + '/', function(r) { var messages = JX.$H(r.messages); - JX.DOM.prependContent(messages_root, - JX.$H(messages)); - }).setData({ offset: last_offset+1 }).send(); + JX.DOM.prependContent( + messages_root, + JX.$H(messages)); + }).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(); if (new_device === old_device) { return; } var update_toggled_widget = new_device == 'desktop' || old_device == 'desktop'; old_device = new_device; if (thread.visible !== null && update_toggled_widget) { updatetoggledwidget(); } if (!config.hasThreadList) { loadthreads(); } else { didloadthreads(); } } JX.Stratcom.listen('phabricator-device-change', null, ondevicechange); ondevicechange(); function loadthreads() { var uri = config.base_uri + 'thread/' + config.selectedID + '/'; new JX.Workflow(uri) .setHandler(onthreadresponse) .start(); } function onthreadresponse(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); } function didloadthreads() { // If there's no thread selected yet, select the current thread or the // first thread. if (!thread.selected) { if (config.selectedID) { selectthreadid(config.selectedID, true); } else { var layout = JX.$(config.layoutID); var threads = JX.DOM.scry(layout, 'a', 'conpherence-menu-click'); if (threads.length) { selectthread(threads[0]); } else { var nothreads = JX.DOM.find(layout, 'div', 'conpherence-no-threads'); nothreads.style.display = 'block'; } } } redrawthread(); } });