diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -109,6 +109,7 @@ 'rsrc/css/application/search/search-results.css' => 'f240504c', 'rsrc/css/application/settings/settings.css' => 'ea8f5915', 'rsrc/css/application/slowvote/slowvote.css' => '266df6a1', + 'rsrc/css/application/subscriptions/subscribers-list.css' => 'a1abbf06', 'rsrc/css/application/tokens/tokens.css' => 'fb286311', 'rsrc/css/application/uiexample/example.css' => '4741b891', 'rsrc/css/core/core.css' => 'da26ddb2', @@ -804,6 +805,7 @@ 'sprite-projects-css' => '7578fa56', 'sprite-status-css' => '8bce1c97', 'sprite-tokens-css' => '1706b943', + 'subscribers-list-css' => 'a1abbf06', 'syntax-highlighting-css' => '3c18c1cb', 'tokens-css' => 'fb286311', ), diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2092,6 +2092,7 @@ 'PhabricatorSubscribersQuery' => 'applications/subscriptions/query/PhabricatorSubscribersQuery.php', 'PhabricatorSubscriptionsEditController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php', 'PhabricatorSubscriptionsEditor' => 'applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php', + 'PhabricatorSubscriptionsListController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsListController.php', 'PhabricatorSubscriptionsUIEventListener' => 'applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php', 'PhabricatorSymbolNameLinter' => 'infrastructure/lint/hook/PhabricatorSymbolNameLinter.php', 'PhabricatorSyntaxHighlighter' => 'infrastructure/markup/PhabricatorSyntaxHighlighter.php', @@ -2521,6 +2522,8 @@ 'SleepBuildStepImplementation' => 'applications/harbormaster/step/SleepBuildStepImplementation.php', 'SlowvoteEmbedView' => 'applications/slowvote/view/SlowvoteEmbedView.php', 'SlowvoteRemarkupRule' => 'applications/slowvote/remarkup/SlowvoteRemarkupRule.php', + 'SubscriptionListDialogBuilder' => 'applications/subscriptions/view/SubscriptionListDialogBuilder.php', + 'SubscriptionListStringBuilder' => 'applications/subscriptions/view/SubscriptionListStringBuilder.php', 'UploadArtifactBuildStepImplementation' => 'applications/harbormaster/step/UploadArtifactBuildStepImplementation.php', 'VariableBuildStepImplementation' => 'applications/harbormaster/step/VariableBuildStepImplementation.php', 'WaitForPreviousBuildStepImplementation' => 'applications/harbormaster/step/WaitForPreviousBuildStepImplementation.php', @@ -4889,6 +4892,7 @@ 'PhabricatorSubscribersQuery' => 'PhabricatorQuery', 'PhabricatorSubscriptionsEditController' => 'PhabricatorController', 'PhabricatorSubscriptionsEditor' => 'PhabricatorEditor', + 'PhabricatorSubscriptionsListController' => 'PhabricatorController', 'PhabricatorSubscriptionsUIEventListener' => 'PhabricatorEventListener', 'PhabricatorSymbolNameLinter' => 'ArcanistXHPASTLintNamingHook', 'PhabricatorSyntaxHighlightingConfigOptions' => 'PhabricatorApplicationConfigOptions', diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -516,11 +516,13 @@ pht('Priority'), ManiphestTaskPriority::getTaskPriorityName($task->getPriority())); - $view->addProperty( - pht('Subscribers'), - $task->getCCPHIDs() - ? $this->renderHandlesForPHIDs($task->getCCPHIDs(), ',') - : phutil_tag('em', array(), pht('None'))); + $handles = $this->getLoadedHandles(); + $cc_handles = array_select_keys($handles, $task->getCCPHIDs()); + $subscriber_html = id(new SubscriptionListStringBuilder()) + ->setObjectPHID($task->getPHID()) + ->setHandles($cc_handles) + ->buildPropertyString(); + $view->addProperty(pht('Subscribers'), $subscriber_html); $view->addProperty( pht('Author'), diff --git a/src/applications/subscriptions/application/PhabricatorApplicationSubscriptions.php b/src/applications/subscriptions/application/PhabricatorApplicationSubscriptions.php --- a/src/applications/subscriptions/application/PhabricatorApplicationSubscriptions.php +++ b/src/applications/subscriptions/application/PhabricatorApplicationSubscriptions.php @@ -21,6 +21,7 @@ '/subscriptions/' => array( '(?P<action>add|delete)/'. '(?P<phid>[^/]+)/' => 'PhabricatorSubscriptionsEditController', + 'list/(?P<phid>[^/]+)/' => 'PhabricatorSubscriptionsListController', ), ); } diff --git a/src/applications/subscriptions/controller/PhabricatorSubscriptionsListController.php b/src/applications/subscriptions/controller/PhabricatorSubscriptionsListController.php new file mode 100644 --- /dev/null +++ b/src/applications/subscriptions/controller/PhabricatorSubscriptionsListController.php @@ -0,0 +1,52 @@ +<?php + +final class PhabricatorSubscriptionsListController + extends PhabricatorController { + + private $phid; + + public function willProcessRequest(array $data) { + $this->phid = idx($data, 'phid'); + } + + public function processRequest() { + $request = $this->getRequest(); + + $viewer = $request->getUser(); + $phid = $this->phid; + + $object = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withPHIDs(array($phid)) + ->executeOne(); + + if ($object instanceof PhabricatorSubscribableInterface) { + $subscriber_phids = PhabricatorSubscribersQuery::loadSubscribersForPHID( + $phid); + if ($object->isAutomaticallySubscribed($viewer->getPHID())) { + $subscriber_phids[] = $viewer->getPHID(); + } + } else if ($object instanceof ManiphestTask) { + $subscriber_phids = $object->getCCPHIDs(); + } + + $handle_phids = $subscriber_phids; + $handle_phids[] = $phid; + + $handles = id(new PhabricatorHandleQuery()) + ->setViewer($viewer) + ->withPHIDs($handle_phids) + ->execute(); + $object_handle = $handles[$phid]; + + $dialog = id(new SubscriptionListDialogBuilder()) + ->setViewer($viewer) + ->setTitle(pht('Subscribers for %s', $object_handle->getFullName())) + ->setObjectPHID($phid) + ->setHandles($handles) + ->buildDialog(); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + +} diff --git a/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php b/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php --- a/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php +++ b/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php @@ -108,19 +108,21 @@ $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID( $object->getPHID()); + if ($object->isAutomaticallySubscribed($user->getPHID())) { + $subscribers[] = $user->getPHID(); + } if ($subscribers) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($user) ->withPHIDs($subscribers) ->execute(); - $sub_view = array(); - foreach ($subscribers as $subscriber) { - $sub_view[] = $handles[$subscriber]->renderLink(); - } - $sub_view = phutil_implode_html(', ', $sub_view); } else { - $sub_view = phutil_tag('em', array(), pht('None')); + $handles = array(); } + $sub_view = id(new SubscriptionListStringBuilder()) + ->setObjectPHID($object->getPHID()) + ->setHandles($handles) + ->buildPropertyString(); $view = $event->getValue('view'); $view->addProperty(pht('Subscribers'), $sub_view); diff --git a/src/applications/subscriptions/view/SubscriptionListDialogBuilder.php b/src/applications/subscriptions/view/SubscriptionListDialogBuilder.php new file mode 100644 --- /dev/null +++ b/src/applications/subscriptions/view/SubscriptionListDialogBuilder.php @@ -0,0 +1,115 @@ +<?php + +final class SubscriptionListDialogBuilder { + + private $viewer; + private $handles; + private $objectPHID; + private $title; + + public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setHandles(array $handles) { + assert_instances_of($handles, 'PhabricatorObjectHandle'); + $this->handles = $handles; + return $this; + } + + public function getHandles() { + return $this->handles; + } + + public function setObjectPHID($object_phid) { + $this->objectPHID = $object_phid; + return $this; + } + + public function getObjectPHID() { + return $this->objectPHID; + } + + public function setTitle($title) { + $this->title = $title; + return $this; + } + + public function getTitle() { + return $this->title; + } + + public function buildDialog() { + $phid = $this->getObjectPHID(); + $handles = $this->getHandles(); + $object_handle = $handles[$phid]; + unset($handles[$phid]); + + return id(new AphrontDialogView()) + ->setUser($this->getViewer()) + ->setClass('subscriber-list-dialog') + ->setTitle($this->getTitle()) + ->appendChild($this->buildBody($handles)) + ->addCancelButton($object_handle->getURI(), pht('Dismiss')); + } + + private function buildBody(array $handles) { + require_celerity_resource('subscribers-list-css'); + + $first = true; + $last_key = last_key($handles); + $handle_divs = array(); + foreach ($handles as $key => $handle) { + $class = 'subscriber-item'; + if ($first) { + $class .= ' subscriber-item-first'; + $first = false; + } + if ($last_key == $key) { + $class .= ' subscriber-item-last'; + $spacer_div = null; + } else { + $spacer_div = phutil_tag( + 'div', + array( + 'class' => 'subscriber-item-spacer')); + } + + $image_uri = $handle->getImageURI(); + $handle_divs[] = array( + phutil_tag( + 'div', + array( + 'class' => $class), + array( + phutil_tag( + 'a', + array( + 'href' => $handle->getURI()), + phutil_tag( + 'div', + array( + 'class' => 'subscriber-picture-frame', + 'style' => 'background-image: url('.$image_uri.');'), + '')), + phutil_tag( + 'div', + array( + 'class' => 'subscriber-name'), + phutil_tag( + 'a', + array( + 'href' => $handle->getURI()), + $handle->getFullName())))), + $spacer_div); + } + + return $handle_divs; + } + +} diff --git a/src/applications/subscriptions/view/SubscriptionListStringBuilder.php b/src/applications/subscriptions/view/SubscriptionListStringBuilder.php new file mode 100644 --- /dev/null +++ b/src/applications/subscriptions/view/SubscriptionListStringBuilder.php @@ -0,0 +1,68 @@ +<?php + +final class SubscriptionListStringBuilder { + + private $handles; + private $objectPHID; + + public function setHandles(array $handles) { + assert_instances_of($handles, 'PhabricatorObjectHandle'); + $this->handles = $handles; + return $this; + } + + public function getHandles() { + return $this->handles; + } + + public function setObjectPHID($object_phid) { + $this->objectPHID = $object_phid; + return $this; + } + + public function getObjectPHID() { + return $this->objectPHID; + } + + public function buildPropertyString() { + $phid = $this->getObjectPHID(); + $handles = $this->getHandles(); + + if (!$handles) { + return phutil_tag('em', array(), pht('None')); + } + + $html = array(); + $show_count = 3; + $subscribers_count = count($handles); + if ($subscribers_count <= $show_count) { + return phutil_implode_html(', ', mpull($handles, 'renderLink')); + } + + $args = array('%s, %s, %s, and %s'); + $shown = 0; + foreach ($handles as $handle) { + $shown++; + if ($shown > $show_count) { + break; + } + $args[] = $handle->renderLink(); + } + $not_shown_count = $subscribers_count - $show_count; + if ($not_shown_count == 1) { + $not_shown_txt = '1 other'; + } else { + $not_shown_txt = sprintf('%d others', $not_shown_count); + } + $args[] = javelin_tag( + 'a', + array( + 'href' => '/subscriptions/list/'.$phid.'/', + 'sigil' => 'workflow' + ), + $not_shown_txt); + + return call_user_func_array('pht', $args); + } + +} diff --git a/webroot/rsrc/css/application/subscriptions/subscribers-list.css b/webroot/rsrc/css/application/subscriptions/subscribers-list.css new file mode 100644 --- /dev/null +++ b/webroot/rsrc/css/application/subscriptions/subscribers-list.css @@ -0,0 +1,44 @@ +/** + * @provides subscribers-list-css + */ + +.subscriber-list-dialog { + width: 400px; +} + +.subscriber-list-dialog .aphront-dialog-body { + padding: 0px 16px 0px 16px; +} + +.subscriber-item { + float: left; + height: 50px; + width: 100%; + padding: 8px 0px 8px 0px; +} + +.subscriber-item-first { + padding: 12px 0px 8px 0px; +} + +.subscriber-item-last { + padding: 8px 0px 12px 0px; +} + +.subscriber-item .subscriber-picture-frame { + float: left; + margin: 0px 8px 0px 0px; + width: 50px; + height: 50px; +} + +.subscriber-item .subscriber-name { + margin: 15px 0px 0px 0px; +} + +.subscriber-item-spacer { + float: left; + height: 1px; + width: 368px; + background: #BFCFDA; +}