diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 3bb89e7178..d1a24e4ddd 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -146,7 +146,7 @@ return array( 'rsrc/css/phui/phui-comment-form.css' => '68a2d99a', 'rsrc/css/phui/phui-comment-panel.css' => 'ec4e31c0', 'rsrc/css/phui/phui-crumbs-view.css' => '614f43cf', - 'rsrc/css/phui/phui-curtain-object-ref-view.css' => 'e3331b60', + 'rsrc/css/phui/phui-curtain-object-ref-view.css' => '12404744', 'rsrc/css/phui/phui-curtain-view.css' => '68c5efb6', 'rsrc/css/phui/phui-document-pro.css' => 'b9613a10', 'rsrc/css/phui/phui-document-summary.css' => 'b068eed1', @@ -834,7 +834,7 @@ return array( 'phui-comment-form-css' => '68a2d99a', 'phui-comment-panel-css' => 'ec4e31c0', 'phui-crumbs-view-css' => '614f43cf', - 'phui-curtain-object-ref-view-css' => 'e3331b60', + 'phui-curtain-object-ref-view-css' => '12404744', 'phui-curtain-view-css' => '68c5efb6', 'phui-document-summary-view-css' => 'b068eed1', 'phui-document-view-css' => '52b748a5', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 0948e69e06..ddd25bf7e5 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2042,6 +2042,7 @@ phutil_register_library_map(array( 'PHUILauncherView' => 'view/phui/PHUILauncherView.php', 'PHUILeftRightExample' => 'applications/uiexample/examples/PHUILeftRightExample.php', 'PHUILeftRightView' => 'view/phui/PHUILeftRightView.php', + 'PHUILinkView' => 'view/phui/PHUILinkView.php', 'PHUIListExample' => 'applications/uiexample/examples/PHUIListExample.php', 'PHUIListItemView' => 'view/phui/PHUIListItemView.php', 'PHUIListView' => 'view/phui/PHUIListView.php', @@ -8242,6 +8243,7 @@ phutil_register_library_map(array( 'PHUILauncherView' => 'AphrontTagView', 'PHUILeftRightExample' => 'PhabricatorUIExample', 'PHUILeftRightView' => 'AphrontTagView', + 'PHUILinkView' => 'AphrontTagView', 'PHUIListExample' => 'PhabricatorUIExample', 'PHUIListItemView' => 'AphrontTagView', 'PHUIListView' => 'AphrontTagView', diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index 6cd0b967e0..e542cb017a 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -336,6 +336,7 @@ final class ManiphestTaskDetailController extends ManiphestController { $curtain->addAction($relationship_submenu); } + $viewer_phid = $viewer->getPHID(); $owner_phid = $task->getOwnerPHID(); $author_phid = $task->getAuthorPHID(); $handles = $viewer->loadHandles(array($owner_phid, $author_phid)); @@ -346,7 +347,8 @@ final class ManiphestTaskDetailController extends ManiphestController { if ($owner_phid) { $assigned_ref = $assigned_refs->newObjectRefView() - ->setHandle($handles[$owner_phid]); + ->setHandle($handles[$owner_phid]) + ->setHighlighted($owner_phid === $viewer_phid); } $curtain->newPanel() @@ -358,7 +360,8 @@ final class ManiphestTaskDetailController extends ManiphestController { $author_ref = $author_refs->newObjectRefView() ->setHandle($handles[$author_phid]) - ->setEpoch($task->getDateCreated()); + ->setEpoch($task->getDateCreated()) + ->setHighlighted($author_phid === $viewer_phid); $curtain->newPanel() ->setHeaderText(pht('Authored By')) diff --git a/src/applications/subscriptions/engineextension/PhabricatorSubscriptionsCurtainExtension.php b/src/applications/subscriptions/engineextension/PhabricatorSubscriptionsCurtainExtension.php index 6467141de2..639e96eb86 100644 --- a/src/applications/subscriptions/engineextension/PhabricatorSubscriptionsCurtainExtension.php +++ b/src/applications/subscriptions/engineextension/PhabricatorSubscriptionsCurtainExtension.php @@ -15,25 +15,129 @@ final class PhabricatorSubscriptionsCurtainExtension public function buildCurtainPanel($object) { $viewer = $this->getViewer(); + $viewer_phid = $viewer->getPHID(); $object_phid = $object->getPHID(); + $max_handles = 100; + $max_visible = 8; + + // TODO: We should limit the number of subscriber PHIDs we'll load, so + // we degrade gracefully when objects have thousands of subscribers. + $subscriber_phids = PhabricatorSubscribersQuery::loadSubscribersForPHID( $object_phid); + $subscriber_count = count($subscriber_phids); - $handles = $viewer->loadHandles($subscriber_phids); + $subscriber_phids = $this->sortSubscriberPHIDs( + $subscriber_phids, + null); - // TODO: This class can't accept a HandleList yet. - $handles = iterator_to_array($handles); + // If we have fewer subscribers than the maximum number of handles we're + // willing to load, load all the handles and then sort the list based on + // complete handle data. - $susbscribers_view = id(new SubscriptionListStringBuilder()) - ->setObjectPHID($object_phid) - ->setHandles($handles) - ->buildPropertyString(); + // If we have too many PHIDs, we'll skip this step and accept a less + // useful ordering. + $handles = null; + if ($subscriber_count <= $max_handles) { + $handles = $viewer->loadHandles($subscriber_phids); + + $subscriber_phids = $this->sortSubscriberPHIDs( + $subscriber_phids, + $handles); + } + + // If we have more PHIDs to show than visible slots, slice the list. + if ($subscriber_count > $max_visible) { + $visible_phids = array_slice($subscriber_phids, 0, $max_visible - 1); + $show_all = true; + } else { + $visible_phids = $subscriber_phids; + $show_all = false; + } + + // If we didn't load handles earlier because we had too many PHIDs, + // load them now. + if ($handles === null) { + $handles = $viewer->loadHandles($visible_phids); + } + + $ref_list = id(new PHUICurtainObjectRefListView()) + ->setViewer($viewer) + ->setEmptyMessage(pht('None')); + + foreach ($visible_phids as $phid) { + $ref = $ref_list->newObjectRefView() + ->setHandle($handles[$phid]); + + if ($phid === $viewer_phid) { + $ref->setHighlighted(true); + } + } + + if ($show_all) { + $view_all_uri = urisprintf( + '/subscriptions/list/%s/', + $object_phid); + + $ref_list->newTailLink() + ->setURI($view_all_uri) + ->setText(pht('View All %d Subscriber(s)', $subscriber_count)) + ->setWorkflow(true); + } return $this->newPanel() ->setHeaderText(pht('Subscribers')) ->setOrder(20000) - ->appendChild($susbscribers_view); + ->appendChild($ref_list); + } + + private function sortSubscriberPHIDs(array $subscriber_phids, $handles) { + + // Sort subscriber PHIDs with or without handle data. If we have handles, + // we can sort results more comprehensively. + + $viewer = $this->getViewer(); + + $user_type = PhabricatorPeopleUserPHIDType::TYPECONST; + $viewer_phid = $viewer->getPHID(); + + $type_order_map = array( + PhabricatorPeopleUserPHIDType::TYPECONST => 0, + PhabricatorProjectProjectPHIDType::TYPECONST => 1, + PhabricatorOwnersPackagePHIDType::TYPECONST => 2, + ); + $default_type_order = count($type_order_map); + + $subscriber_map = array(); + foreach ($subscriber_phids as $subscriber_phid) { + $is_viewer = ($viewer_phid === $subscriber_phid); + + $subscriber_type = phid_get_type($subscriber_phid); + $type_order = idx($type_order_map, $subscriber_type, $default_type_order); + + $sort_name = ''; + $is_complete = false; + if ($handles) { + if (isset($handles[$subscriber_phid])) { + $handle = $handles[$subscriber_phid]; + if ($handle->isComplete()) { + $is_complete = true; + $sort_name = $handle->getLinkName(); + } + } + } + + $subscriber_map[$subscriber_phid] = id(new PhutilSortVector()) + ->addInt($is_viewer ? 0 : 1) + ->addInt($is_complete ? 0 : 1) + ->addInt($type_order) + ->addString($sort_name); + } + + $subscriber_map = msortv($subscriber_map, 'getSelf'); + + return array_keys($subscriber_map); } } diff --git a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php index ec07d018b7..9c814cb45c 100644 --- a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php +++ b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php @@ -1718,6 +1718,11 @@ final class PhabricatorUSEnglishTranslation 'then try again.', ), + 'View All %d Subscriber(s)' => array( + 'View Subscriber', + 'View All %d Subscribers', + ), + ); } diff --git a/src/view/phui/PHUICurtainObjectRefListView.php b/src/view/phui/PHUICurtainObjectRefListView.php index f58f97c98a..d42a34452f 100644 --- a/src/view/phui/PHUICurtainObjectRefListView.php +++ b/src/view/phui/PHUICurtainObjectRefListView.php @@ -5,6 +5,7 @@ final class PHUICurtainObjectRefListView private $refs = array(); private $emptyMessage; + private $tail = array(); protected function getTagAttributes() { return array( @@ -20,18 +21,31 @@ final class PHUICurtainObjectRefListView protected function getTagContent() { $refs = $this->refs; - if (!$refs) { - if ($this->emptyMessage) { - return phutil_tag( - 'div', - array( - 'class' => 'phui-curtain-object-ref-list-view-empty', - ), - $this->emptyMessage); - } + if (!$refs && ($this->emptyMessage !== null)) { + $view = phutil_tag( + 'div', + array( + 'class' => 'phui-curtain-object-ref-list-view-empty', + ), + $this->emptyMessage); + } else { + $view = $refs; } - return $refs; + $tail = null; + if ($this->tail) { + $tail = phutil_tag( + 'div', + array( + 'class' => 'phui-curtain-object-ref-list-view-tail', + ), + $this->tail); + } + + return array( + $view, + $tail, + ); } public function newObjectRefView() { @@ -43,4 +57,12 @@ final class PHUICurtainObjectRefListView return $ref_view; } + public function newTailLink() { + $link = new PHUILinkView(); + + $this->tail[] = $link; + + return $link; + } + } diff --git a/src/view/phui/PHUICurtainObjectRefView.php b/src/view/phui/PHUICurtainObjectRefView.php index 357e4eec9b..f8a04796ca 100644 --- a/src/view/phui/PHUICurtainObjectRefView.php +++ b/src/view/phui/PHUICurtainObjectRefView.php @@ -5,6 +5,7 @@ final class PHUICurtainObjectRefView private $handle; private $epoch; + private $highlighted; public function setHandle(PhabricatorObjectHandle $handle) { $this->handle = $handle; @@ -16,9 +17,22 @@ final class PHUICurtainObjectRefView return $this; } + public function setHighlighted($highlighted) { + $this->highlighted = $highlighted; + return $this; + } + protected function getTagAttributes() { + $classes = array(); + $classes[] = 'phui-curtain-object-ref-view'; + + if ($this->highlighted) { + $classes[] = 'phui-curtain-object-ref-view-highlighted'; + } + $classes = implode(' ', $classes); + return array( - 'class' => 'phui-curtain-object-ref-view', + 'class' => $classes, ); } @@ -114,6 +128,11 @@ final class PHUICurtainObjectRefView $image_uri = $this->getImageURI(); $target_uri = $this->getTargetURI(); + $icon_view = null; + if ($image_uri == null) { + $icon_view = $this->newIconView(); + } + if ($image_uri !== null) { $image_view = javelin_tag( 'a', @@ -122,6 +141,15 @@ final class PHUICurtainObjectRefView 'href' => $target_uri, 'aural' => false, )); + } else if ($icon_view !== null) { + $image_view = javelin_tag( + 'a', + array( + 'href' => $target_uri, + 'class' => 'phui-curtain-object-ref-view-icon-image', + 'aural' => false, + ), + $icon_view); } else { $image_view = null; } @@ -151,5 +179,16 @@ final class PHUICurtainObjectRefView return $image_uri; } + private function newIconView() { + $handle = $this->handle; + + if ($handle) { + $icon_view = id(new PHUIIconView()) + ->setIcon($handle->getIcon()); + } + + return $icon_view; + } + } diff --git a/src/view/phui/PHUILinkView.php b/src/view/phui/PHUILinkView.php new file mode 100644 index 0000000000..33843c2ae9 --- /dev/null +++ b/src/view/phui/PHUILinkView.php @@ -0,0 +1,50 @@ +uri = $uri; + return $this; + } + + public function getURI() { + return $this->uri; + } + + public function setText($text) { + $this->text = $text; + return $this; + } + + public function setWorkflow($workflow) { + $this->workflow = $workflow; + return $this; + } + + protected function getTagName() { + return 'a'; + } + + protected function getTagAttributes() { + $sigil = array(); + + if ($this->workflow) { + $sigil[] = 'workflow'; + } + + return array( + 'href' => $this->getURI(), + 'sigil' => $sigil, + ); + } + + protected function getTagContent() { + return $this->text; + } + +} diff --git a/webroot/rsrc/css/phui/phui-curtain-object-ref-view.css b/webroot/rsrc/css/phui/phui-curtain-object-ref-view.css index b8ba7e1a6f..3becc1ca84 100644 --- a/webroot/rsrc/css/phui/phui-curtain-object-ref-view.css +++ b/webroot/rsrc/css/phui/phui-curtain-object-ref-view.css @@ -7,9 +7,14 @@ color: {$greytext}; } +.phui-curtain-object-ref-view { + padding: 4px 6px; + border-radius: 3px; +} + .phui-curtain-object-ref-view-image-cell { min-width: 32px; - min-height: 32px; + padding-bottom: 24px; } .phui-curtain-object-ref-view-image-cell > a { @@ -18,6 +23,24 @@ background-size: 100%; border-radius: 3px; display: block; + position: absolute; +} + +.phui-curtain-object-ref-view-image-cell .phui-icon-view { + font-size: 16px; + line-height: 16px; + vertical-align: middle; + text-align: center; + width: 24px; + height: 24px; + top: 3px; + display: block; + position: absolute; + color: #ffffff; +} + +.phui-curtain-object-ref-view-icon-image { + background-color: {$backdrop}; } .phui-curtain-object-ref-view-title-cell { @@ -26,7 +49,7 @@ overflow: hidden; /* This is forcing "text-overflow: ellipsis" to actually work. */ - max-width: 225px; + max-width: 210px; } .phui-curtain-object-ref-view-without-content > @@ -46,3 +69,16 @@ .phui-curtain-object-ref-view-epoch-cell { color: {$greytext}; } + +.phui-curtain-object-ref-list-view-tail { + text-align: center; + margin-top: 8px; + padding: 4px; + background: {$lightgreybackground}; + border-top: 1px dashed {$thinblueborder}; + box-shadow: inset 0 2px 3px rgba(0, 0, 0, 0.04); +} + +.phui-curtain-object-ref-view-highlighted { + background: {$bluebackground}; +}