diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -117,7 +117,7 @@ 'rsrc/css/font/font-source-sans-pro.css' => '8906c07b', 'rsrc/css/font/phui-font-icon-base.css' => '3dad2ae3', 'rsrc/css/layout/phabricator-filetree-view.css' => 'fccf9f82', - 'rsrc/css/layout/phabricator-hovercard-view.css' => '44394670', + 'rsrc/css/layout/phabricator-hovercard-view.css' => 'dd9121a9', 'rsrc/css/layout/phabricator-side-menu-view.css' => 'c1db9e9c', 'rsrc/css/layout/phabricator-source-code-view.css' => '2ceee894', 'rsrc/css/phui/calendar/phui-calendar-day.css' => '38891735', @@ -713,7 +713,7 @@ 'phabricator-filetree-view-css' => 'fccf9f82', 'phabricator-flag-css' => '5337623f', 'phabricator-hovercard' => '14ac66f5', - 'phabricator-hovercard-view-css' => '44394670', + 'phabricator-hovercard-view-css' => 'dd9121a9', 'phabricator-keyboard-shortcut' => '1ae869f2', 'phabricator-keyboard-shortcut-manager' => 'c1700f6f', 'phabricator-main-menu-view' => '663e3810', diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -218,18 +218,6 @@ } } - public function loadCurrentStatuses($user_phids) { - if (!$user_phids) { - return array(); - } - - $statuses = $this->loadAllWhere( - 'userPHID IN (%Ls) AND UNIX_TIMESTAMP() BETWEEN dateFrom AND dateTo', - $user_phids); - - return mpull($statuses, null, 'getUserPHID'); - } - public function getInvitees() { return $this->assertAttached($this->invitees); } diff --git a/src/applications/calendar/storage/PhabricatorCalendarEventInvitee.php b/src/applications/calendar/storage/PhabricatorCalendarEventInvitee.php --- a/src/applications/calendar/storage/PhabricatorCalendarEventInvitee.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEventInvitee.php @@ -38,6 +38,10 @@ ) + parent::getConfiguration(); } + public function isAttending() { + return ($this->getStatus() == self::STATUS_ATTENDING); + } + public function isUninvited() { if ($this->getStatus() == self::STATUS_UNINVITED) { return true; diff --git a/src/applications/people/conduit/UserConduitAPIMethod.php b/src/applications/people/conduit/UserConduitAPIMethod.php --- a/src/applications/people/conduit/UserConduitAPIMethod.php +++ b/src/applications/people/conduit/UserConduitAPIMethod.php @@ -6,9 +6,7 @@ return PhabricatorApplication::getByClass('PhabricatorPeopleApplication'); } - protected function buildUserInformationDictionary( - PhabricatorUser $user, - PhabricatorCalendarEvent $current_status = null) { + protected function buildUserInformationDictionary(PhabricatorUser $user) { $roles = array(); if ($user->getIsDisabled()) { @@ -48,9 +46,12 @@ 'roles' => $roles, ); - if ($current_status) { - $return['currentStatus'] = $current_status->getTextStatus(); - $return['currentStatusUntil'] = $current_status->getDateTo(); + // TODO: Modernize this once we have a more long-term view of what the + // data looks like. + $until = $user->getAwayUntil(); + if ($until) { + $return['currentStatus'] = 'away'; + $return['currentStatusUntil'] = $until; } return $return; diff --git a/src/applications/people/conduit/UserQueryConduitAPIMethod.php b/src/applications/people/conduit/UserQueryConduitAPIMethod.php --- a/src/applications/people/conduit/UserQueryConduitAPIMethod.php +++ b/src/applications/people/conduit/UserQueryConduitAPIMethod.php @@ -43,7 +43,8 @@ $query = id(new PhabricatorPeopleQuery()) ->setViewer($request->getUser()) - ->needProfileImage(true); + ->needProfileImage(true) + ->needAvailability(true); if ($usernames) { $query->withUsernames($usernames); @@ -68,14 +69,9 @@ } $users = $query->execute(); - $statuses = id(new PhabricatorCalendarEvent())->loadCurrentStatuses( - mpull($users, 'getPHID')); - $results = array(); foreach ($users as $user) { - $results[] = $this->buildUserInformationDictionary( - $user, - idx($statuses, $user->getPHID())); + $results[] = $this->buildUserInformationDictionary($user); } return $results; } diff --git a/src/applications/people/controller/PhabricatorPeopleProfileController.php b/src/applications/people/controller/PhabricatorPeopleProfileController.php --- a/src/applications/people/controller/PhabricatorPeopleProfileController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileController.php @@ -20,6 +20,7 @@ ->setViewer($viewer) ->withUsernames(array($this->username)) ->needProfileImage(true) + ->needAvailability(true) ->executeOne(); if (!$user) { return new Aphront404Response(); diff --git a/src/applications/people/customfield/PhabricatorUserStatusField.php b/src/applications/people/customfield/PhabricatorUserStatusField.php --- a/src/applications/people/customfield/PhabricatorUserStatusField.php +++ b/src/applications/people/customfield/PhabricatorUserStatusField.php @@ -29,16 +29,7 @@ public function renderPropertyViewValue(array $handles) { $user = $this->getObject(); $viewer = $this->requireViewer(); - - $statuses = id(new PhabricatorCalendarEvent()) - ->loadCurrentStatuses(array($user->getPHID())); - if (!$statuses) { - return pht('Available'); - } - - $status = head($statuses); - - return $status->getTerseSummary($viewer); + return $user->getAvailabilityDescription($viewer); } } diff --git a/src/applications/people/event/PhabricatorPeopleHovercardEventListener.php b/src/applications/people/event/PhabricatorPeopleHovercardEventListener.php --- a/src/applications/people/event/PhabricatorPeopleHovercardEventListener.php +++ b/src/applications/people/event/PhabricatorPeopleHovercardEventListener.php @@ -26,12 +26,15 @@ return; } - $profile = $user->loadUserProfile(); + // Reload to get availability. + $user = id(new PhabricatorPeopleQuery()) + ->setViewer($viewer) + ->withIDs(array($user->getID())) + ->needAvailability(true) + ->executeOne(); $hovercard->setTitle($user->getUsername()); - $hovercard->setDetail(pht('%s - %s.', $user->getRealname(), - nonempty($profile->getTitle(), - pht('No title was found befitting of this rare specimen')))); + $hovercard->setDetail($user->getRealName()); if ($user->getIsDisabled()) { $hovercard->addField(pht('Account'), pht('Disabled')); @@ -40,29 +43,14 @@ } else if (PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorCalendarApplication', $viewer)) { - $statuses = id(new PhabricatorCalendarEvent())->loadCurrentStatuses( - array($user->getPHID())); - if ($statuses) { - $current_status = reset($statuses); - $dateto = phabricator_datetime($current_status->getDateTo(), $user); - $hovercard->addField(pht('Status'), - $current_status->getDescription()); - $hovercard->addField(pht('Until'), - $dateto); - } else { - $hovercard->addField(pht('Status'), pht('Available')); - } + $hovercard->addField( + pht('Status'), + $user->getAvailabilityDescription($viewer)); } - $hovercard->addField(pht('User since'), - phabricator_date($user->getDateCreated(), $user)); - - if ($profile->getBlurb()) { - $hovercard->addField(pht('Blurb'), - id(new PhutilUTF8StringTruncator()) - ->setMaximumGlyphs(120) - ->truncateString($profile->getBlurb())); - } + $hovercard->addField( + pht('User Since'), + phabricator_date($user->getDateCreated(), $viewer)); $event->setValue('hovercard', $hovercard); } diff --git a/src/applications/people/markup/PhabricatorMentionRemarkupRule.php b/src/applications/people/markup/PhabricatorMentionRemarkupRule.php --- a/src/applications/people/markup/PhabricatorMentionRemarkupRule.php +++ b/src/applications/people/markup/PhabricatorMentionRemarkupRule.php @@ -72,16 +72,9 @@ $users = id(new PhabricatorPeopleQuery()) ->setViewer($this->getEngine()->getConfig('viewer')) ->withUsernames($usernames) + ->needAvailability(true) ->execute(); - if ($users) { - $user_statuses = id(new PhabricatorCalendarEvent()) - ->loadCurrentStatuses(mpull($users, 'getPHID')); - $user_statuses = mpull($user_statuses, null, 'getUserPHID'); - } else { - $user_statuses = array(); - } - $actual_users = array(); $mentioned_key = self::KEY_MENTIONED; @@ -156,14 +149,8 @@ if (!$user->isUserActivated()) { $tag->setDotColor(PHUITagView::COLOR_GREY); } else { - $status = idx($user_statuses, $user->getPHID()); - if ($status) { - $status = $status->getStatus(); - if ($status == PhabricatorCalendarEvent::STATUS_AWAY) { - $tag->setDotColor(PHUITagView::COLOR_RED); - } else if ($status == PhabricatorCalendarEvent::STATUS_AWAY) { - $tag->setDotColor(PHUITagView::COLOR_ORANGE); - } + if ($user->getAwayUntil()) { + $tag->setDotColor(PHUITagView::COLOR_RED); } } } diff --git a/src/applications/people/phid/PhabricatorPeopleUserPHIDType.php b/src/applications/people/phid/PhabricatorPeopleUserPHIDType.php --- a/src/applications/people/phid/PhabricatorPeopleUserPHIDType.php +++ b/src/applications/people/phid/PhabricatorPeopleUserPHIDType.php @@ -27,7 +27,7 @@ return id(new PhabricatorPeopleQuery()) ->withPHIDs($phids) ->needProfileImage(true) - ->needStatus(true); + ->needAvailability(true); } public function loadHandles( @@ -48,18 +48,9 @@ if (!$user->isUserActivated()) { $availability = PhabricatorObjectHandle::AVAILABILITY_DISABLED; } else { - if ($user->hasStatus()) { - // NOTE: This first call returns an event; then we get the event - // status. - $status = $user->getStatus()->getStatus(); - switch ($status) { - case PhabricatorCalendarEvent::STATUS_AWAY: - $availability = PhabricatorObjectHandle::AVAILABILITY_NONE; - break; - case PhabricatorCalendarEvent::STATUS_SPORADIC: - $availability = PhabricatorObjectHandle::AVAILABILITY_PARTIAL; - break; - } + $until = $user->getAwayUntil(); + if ($until) { + $availability = PhabricatorObjectHandle::AVAILABILITY_NONE; } } diff --git a/src/applications/people/query/PhabricatorPeopleQuery.php b/src/applications/people/query/PhabricatorPeopleQuery.php --- a/src/applications/people/query/PhabricatorPeopleQuery.php +++ b/src/applications/people/query/PhabricatorPeopleQuery.php @@ -20,7 +20,7 @@ private $needPrimaryEmail; private $needProfile; private $needProfileImage; - private $needStatus; + private $needAvailability; public function withIDs(array $ids) { $this->ids = $ids; @@ -102,8 +102,8 @@ return $this; } - public function needStatus($need) { - $this->needStatus = $need; + public function needAvailability($need) { + $this->needAvailability = $need; return $this; } @@ -200,15 +200,11 @@ } } - if ($this->needStatus) { - $user_list = mpull($users, null, 'getPHID'); - $statuses = id(new PhabricatorCalendarEvent())->loadCurrentStatuses( - array_keys($user_list)); - foreach ($user_list as $phid => $user) { - $status = idx($statuses, $phid); - if ($status) { - $user->attachStatus($status); - } + if ($this->needAvailability) { + // TODO: Add caching. + $rebuild = $users; + if ($rebuild) { + $this->rebuildAvailabilityCache($rebuild); } } @@ -375,5 +371,82 @@ ); } + private function rebuildAvailabilityCache(array $rebuild) { + $rebuild = mpull($rebuild, null, 'getPHID'); + + // Limit the window we look at because far-future events are largely + // irrelevant and this makes the cache cheaper to build and allows it to + // self-heal over time. + $min_range = PhabricatorTime::getNow(); + $max_range = $min_range + phutil_units('72 hours in seconds'); + + $events = id(new PhabricatorCalendarEventQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withInvitedPHIDs(array_keys($rebuild)) + ->withIsCancelled(false) + ->withDateRange($min_range, $max_range) + ->execute(); + + // Group all the events by invited user. Only examine events that users + // are actually attending. + $map = array(); + foreach ($events as $event) { + foreach ($event->getInvitees() as $invitee) { + if (!$invitee->isAttending()) { + continue; + } + + $invitee_phid = $invitee->getInviteePHID(); + if (!isset($rebuild[$invitee_phid])) { + continue; + } + + $map[$invitee_phid][] = $event; + } + } + + // Margin between meetings: pretend meetings start earlier than they do + // so we mark you away for the entire time if you have a series of + // back-to-back meetings, even if they don't strictly overlap. + $margin = phutil_units('15 minutes in seconds'); + + foreach ($rebuild as $phid => $user) { + $events = idx($map, $phid, array()); + + $cursor = $min_range; + if ($events) { + // Find the next time when the user has no meetings. If we move forward + // because of an event, we check again for events after that one ends. + while (true) { + foreach ($events as $event) { + $from = ($event->getDateFrom() - $margin); + $to = $event->getDateTo(); + if (($from <= $cursor) && ($to > $cursor)) { + $cursor = $to; + continue 2; + } + } + break; + } + } + + if ($cursor > $min_range) { + $availability = array( + 'until' => $cursor, + ); + $availability_ttl = $cursor; + } else { + $availability = null; + $availability_ttl = $max_range; + } + + // Never TTL the cache to longer than the maximum range we examined. + $availability_ttl = min($availability_ttl, $max_range); + + // TODO: Write the cache. + + $user->attachAvailability($availability); + } + } } diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -1,6 +1,7 @@ status = $status; - return $this; - } - - public function getStatus() { - return $this->assertAttached($this->status); - } - - public function hasStatus() { - return $this->status !== self::ATTACHABLE; - } - public function attachProfileImageURI($uri) { $this->profileImage = $uri; return $this; @@ -727,6 +715,53 @@ } +/* -( Availability )------------------------------------------------------- */ + + + /** + * @task availability + */ + public function attachAvailability($availability) { + $this->availability = $availability; + return $this; + } + + + /** + * Get the timestamp the user is away until, if they are currently away. + * + * @return int|null Epoch timestamp, or `null` if the user is not away. + * @task availability + */ + public function getAwayUntil() { + $availability = $this->availability; + + $this->assertAttached($availability); + if (!$availability) { + return null; + } + + return idx($availability, 'until'); + } + + + /** + * Describe the user's availability. + * + * @param PhabricatorUser Viewing user. + * @return string Human-readable description of away status. + * @task availability + */ + public function getAvailabilityDescription(PhabricatorUser $viewer) { + $until = $this->getAwayUntil(); + if ($until) { + return pht('Away until %s', phabricator_datetime($until, $viewer)); + } else { + return pht('Available'); + } + } + + /* -( Profile Image Cache )------------------------------------------------ */ diff --git a/webroot/rsrc/css/layout/phabricator-hovercard-view.css b/webroot/rsrc/css/layout/phabricator-hovercard-view.css --- a/webroot/rsrc/css/layout/phabricator-hovercard-view.css +++ b/webroot/rsrc/css/layout/phabricator-hovercard-view.css @@ -77,6 +77,7 @@ height: 50px; background-position: center; background-repeat: no-repeat; + background-size: 100%; } .phabricator-hovercard-tail { width: 396px;