@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Replace user "status" with "availability"

Summary:
Ref T7707. Ref T8183.

- Currently, user status is derived by looking at events they //created//. Instead, look at non-cancelled invites they are attending.
- Prepare for on-user caching.
- Mostly remove "Sporradic" as a status, although I left room for adding more information later.

Test Plan:
- Called user.query.
- Viewed profile.
- Viewed hovercard.
- Used mentions.
- Saw status immediately update when attending/leaving/cancelling a current event.
- Created an event ending at 6 PM and an event from 6:10PM - 7PM, saw "Away until 7PM".

Reviewers: chad, btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T8183, T7707

Differential Revision: https://secure.phabricator.com/D12833

+173 -117
+2 -2
resources/celerity/map.php
··· 117 117 'rsrc/css/font/font-source-sans-pro.css' => '8906c07b', 118 118 'rsrc/css/font/phui-font-icon-base.css' => '3dad2ae3', 119 119 'rsrc/css/layout/phabricator-filetree-view.css' => 'fccf9f82', 120 - 'rsrc/css/layout/phabricator-hovercard-view.css' => '44394670', 120 + 'rsrc/css/layout/phabricator-hovercard-view.css' => 'dd9121a9', 121 121 'rsrc/css/layout/phabricator-side-menu-view.css' => 'c1db9e9c', 122 122 'rsrc/css/layout/phabricator-source-code-view.css' => '2ceee894', 123 123 'rsrc/css/phui/calendar/phui-calendar-day.css' => '38891735', ··· 713 713 'phabricator-filetree-view-css' => 'fccf9f82', 714 714 'phabricator-flag-css' => '5337623f', 715 715 'phabricator-hovercard' => '14ac66f5', 716 - 'phabricator-hovercard-view-css' => '44394670', 716 + 'phabricator-hovercard-view-css' => 'dd9121a9', 717 717 'phabricator-keyboard-shortcut' => '1ae869f2', 718 718 'phabricator-keyboard-shortcut-manager' => 'c1700f6f', 719 719 'phabricator-main-menu-view' => '663e3810',
-12
src/applications/calendar/storage/PhabricatorCalendarEvent.php
··· 218 218 } 219 219 } 220 220 221 - public function loadCurrentStatuses($user_phids) { 222 - if (!$user_phids) { 223 - return array(); 224 - } 225 - 226 - $statuses = $this->loadAllWhere( 227 - 'userPHID IN (%Ls) AND UNIX_TIMESTAMP() BETWEEN dateFrom AND dateTo', 228 - $user_phids); 229 - 230 - return mpull($statuses, null, 'getUserPHID'); 231 - } 232 - 233 221 public function getInvitees() { 234 222 return $this->assertAttached($this->invitees); 235 223 }
+4
src/applications/calendar/storage/PhabricatorCalendarEventInvitee.php
··· 38 38 ) + parent::getConfiguration(); 39 39 } 40 40 41 + public function isAttending() { 42 + return ($this->getStatus() == self::STATUS_ATTENDING); 43 + } 44 + 41 45 public function isUninvited() { 42 46 if ($this->getStatus() == self::STATUS_UNINVITED) { 43 47 return true;
+7 -6
src/applications/people/conduit/UserConduitAPIMethod.php
··· 6 6 return PhabricatorApplication::getByClass('PhabricatorPeopleApplication'); 7 7 } 8 8 9 - protected function buildUserInformationDictionary( 10 - PhabricatorUser $user, 11 - PhabricatorCalendarEvent $current_status = null) { 9 + protected function buildUserInformationDictionary(PhabricatorUser $user) { 12 10 13 11 $roles = array(); 14 12 if ($user->getIsDisabled()) { ··· 48 46 'roles' => $roles, 49 47 ); 50 48 51 - if ($current_status) { 52 - $return['currentStatus'] = $current_status->getTextStatus(); 53 - $return['currentStatusUntil'] = $current_status->getDateTo(); 49 + // TODO: Modernize this once we have a more long-term view of what the 50 + // data looks like. 51 + $until = $user->getAwayUntil(); 52 + if ($until) { 53 + $return['currentStatus'] = 'away'; 54 + $return['currentStatusUntil'] = $until; 54 55 } 55 56 56 57 return $return;
+3 -7
src/applications/people/conduit/UserQueryConduitAPIMethod.php
··· 43 43 44 44 $query = id(new PhabricatorPeopleQuery()) 45 45 ->setViewer($request->getUser()) 46 - ->needProfileImage(true); 46 + ->needProfileImage(true) 47 + ->needAvailability(true); 47 48 48 49 if ($usernames) { 49 50 $query->withUsernames($usernames); ··· 68 69 } 69 70 $users = $query->execute(); 70 71 71 - $statuses = id(new PhabricatorCalendarEvent())->loadCurrentStatuses( 72 - mpull($users, 'getPHID')); 73 - 74 72 $results = array(); 75 73 foreach ($users as $user) { 76 - $results[] = $this->buildUserInformationDictionary( 77 - $user, 78 - idx($statuses, $user->getPHID())); 74 + $results[] = $this->buildUserInformationDictionary($user); 79 75 } 80 76 return $results; 81 77 }
+1
src/applications/people/controller/PhabricatorPeopleProfileController.php
··· 20 20 ->setViewer($viewer) 21 21 ->withUsernames(array($this->username)) 22 22 ->needProfileImage(true) 23 + ->needAvailability(true) 23 24 ->executeOne(); 24 25 if (!$user) { 25 26 return new Aphront404Response();
+1 -10
src/applications/people/customfield/PhabricatorUserStatusField.php
··· 29 29 public function renderPropertyViewValue(array $handles) { 30 30 $user = $this->getObject(); 31 31 $viewer = $this->requireViewer(); 32 - 33 - $statuses = id(new PhabricatorCalendarEvent()) 34 - ->loadCurrentStatuses(array($user->getPHID())); 35 - if (!$statuses) { 36 - return pht('Available'); 37 - } 38 - 39 - $status = head($statuses); 40 - 41 - return $status->getTerseSummary($viewer); 32 + return $user->getAvailabilityDescription($viewer); 42 33 } 43 34 44 35 }
+13 -25
src/applications/people/event/PhabricatorPeopleHovercardEventListener.php
··· 26 26 return; 27 27 } 28 28 29 - $profile = $user->loadUserProfile(); 29 + // Reload to get availability. 30 + $user = id(new PhabricatorPeopleQuery()) 31 + ->setViewer($viewer) 32 + ->withIDs(array($user->getID())) 33 + ->needAvailability(true) 34 + ->executeOne(); 30 35 31 36 $hovercard->setTitle($user->getUsername()); 32 - $hovercard->setDetail(pht('%s - %s.', $user->getRealname(), 33 - nonempty($profile->getTitle(), 34 - pht('No title was found befitting of this rare specimen')))); 37 + $hovercard->setDetail($user->getRealName()); 35 38 36 39 if ($user->getIsDisabled()) { 37 40 $hovercard->addField(pht('Account'), pht('Disabled')); ··· 40 43 } else if (PhabricatorApplication::isClassInstalledForViewer( 41 44 'PhabricatorCalendarApplication', 42 45 $viewer)) { 43 - $statuses = id(new PhabricatorCalendarEvent())->loadCurrentStatuses( 44 - array($user->getPHID())); 45 - if ($statuses) { 46 - $current_status = reset($statuses); 47 - $dateto = phabricator_datetime($current_status->getDateTo(), $user); 48 - $hovercard->addField(pht('Status'), 49 - $current_status->getDescription()); 50 - $hovercard->addField(pht('Until'), 51 - $dateto); 52 - } else { 53 - $hovercard->addField(pht('Status'), pht('Available')); 54 - } 46 + $hovercard->addField( 47 + pht('Status'), 48 + $user->getAvailabilityDescription($viewer)); 55 49 } 56 50 57 - $hovercard->addField(pht('User since'), 58 - phabricator_date($user->getDateCreated(), $user)); 59 - 60 - if ($profile->getBlurb()) { 61 - $hovercard->addField(pht('Blurb'), 62 - id(new PhutilUTF8StringTruncator()) 63 - ->setMaximumGlyphs(120) 64 - ->truncateString($profile->getBlurb())); 65 - } 51 + $hovercard->addField( 52 + pht('User Since'), 53 + phabricator_date($user->getDateCreated(), $viewer)); 66 54 67 55 $event->setValue('hovercard', $hovercard); 68 56 }
+3 -16
src/applications/people/markup/PhabricatorMentionRemarkupRule.php
··· 72 72 $users = id(new PhabricatorPeopleQuery()) 73 73 ->setViewer($this->getEngine()->getConfig('viewer')) 74 74 ->withUsernames($usernames) 75 + ->needAvailability(true) 75 76 ->execute(); 76 - 77 - if ($users) { 78 - $user_statuses = id(new PhabricatorCalendarEvent()) 79 - ->loadCurrentStatuses(mpull($users, 'getPHID')); 80 - $user_statuses = mpull($user_statuses, null, 'getUserPHID'); 81 - } else { 82 - $user_statuses = array(); 83 - } 84 77 85 78 $actual_users = array(); 86 79 ··· 156 149 if (!$user->isUserActivated()) { 157 150 $tag->setDotColor(PHUITagView::COLOR_GREY); 158 151 } else { 159 - $status = idx($user_statuses, $user->getPHID()); 160 - if ($status) { 161 - $status = $status->getStatus(); 162 - if ($status == PhabricatorCalendarEvent::STATUS_AWAY) { 163 - $tag->setDotColor(PHUITagView::COLOR_RED); 164 - } else if ($status == PhabricatorCalendarEvent::STATUS_AWAY) { 165 - $tag->setDotColor(PHUITagView::COLOR_ORANGE); 166 - } 152 + if ($user->getAwayUntil()) { 153 + $tag->setDotColor(PHUITagView::COLOR_RED); 167 154 } 168 155 } 169 156 }
+4 -13
src/applications/people/phid/PhabricatorPeopleUserPHIDType.php
··· 27 27 return id(new PhabricatorPeopleQuery()) 28 28 ->withPHIDs($phids) 29 29 ->needProfileImage(true) 30 - ->needStatus(true); 30 + ->needAvailability(true); 31 31 } 32 32 33 33 public function loadHandles( ··· 48 48 if (!$user->isUserActivated()) { 49 49 $availability = PhabricatorObjectHandle::AVAILABILITY_DISABLED; 50 50 } else { 51 - if ($user->hasStatus()) { 52 - // NOTE: This first call returns an event; then we get the event 53 - // status. 54 - $status = $user->getStatus()->getStatus(); 55 - switch ($status) { 56 - case PhabricatorCalendarEvent::STATUS_AWAY: 57 - $availability = PhabricatorObjectHandle::AVAILABILITY_NONE; 58 - break; 59 - case PhabricatorCalendarEvent::STATUS_SPORADIC: 60 - $availability = PhabricatorObjectHandle::AVAILABILITY_PARTIAL; 61 - break; 62 - } 51 + $until = $user->getAwayUntil(); 52 + if ($until) { 53 + $availability = PhabricatorObjectHandle::AVAILABILITY_NONE; 63 54 } 64 55 } 65 56
+85 -12
src/applications/people/query/PhabricatorPeopleQuery.php
··· 20 20 private $needPrimaryEmail; 21 21 private $needProfile; 22 22 private $needProfileImage; 23 - private $needStatus; 23 + private $needAvailability; 24 24 25 25 public function withIDs(array $ids) { 26 26 $this->ids = $ids; ··· 102 102 return $this; 103 103 } 104 104 105 - public function needStatus($need) { 106 - $this->needStatus = $need; 105 + public function needAvailability($need) { 106 + $this->needAvailability = $need; 107 107 return $this; 108 108 } 109 109 ··· 200 200 } 201 201 } 202 202 203 - if ($this->needStatus) { 204 - $user_list = mpull($users, null, 'getPHID'); 205 - $statuses = id(new PhabricatorCalendarEvent())->loadCurrentStatuses( 206 - array_keys($user_list)); 207 - foreach ($user_list as $phid => $user) { 208 - $status = idx($statuses, $phid); 209 - if ($status) { 210 - $user->attachStatus($status); 211 - } 203 + if ($this->needAvailability) { 204 + // TODO: Add caching. 205 + $rebuild = $users; 206 + if ($rebuild) { 207 + $this->rebuildAvailabilityCache($rebuild); 212 208 } 213 209 } 214 210 ··· 375 371 ); 376 372 } 377 373 374 + private function rebuildAvailabilityCache(array $rebuild) { 375 + $rebuild = mpull($rebuild, null, 'getPHID'); 376 + 377 + // Limit the window we look at because far-future events are largely 378 + // irrelevant and this makes the cache cheaper to build and allows it to 379 + // self-heal over time. 380 + $min_range = PhabricatorTime::getNow(); 381 + $max_range = $min_range + phutil_units('72 hours in seconds'); 382 + 383 + $events = id(new PhabricatorCalendarEventQuery()) 384 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 385 + ->withInvitedPHIDs(array_keys($rebuild)) 386 + ->withIsCancelled(false) 387 + ->withDateRange($min_range, $max_range) 388 + ->execute(); 389 + 390 + // Group all the events by invited user. Only examine events that users 391 + // are actually attending. 392 + $map = array(); 393 + foreach ($events as $event) { 394 + foreach ($event->getInvitees() as $invitee) { 395 + if (!$invitee->isAttending()) { 396 + continue; 397 + } 398 + 399 + $invitee_phid = $invitee->getInviteePHID(); 400 + if (!isset($rebuild[$invitee_phid])) { 401 + continue; 402 + } 403 + 404 + $map[$invitee_phid][] = $event; 405 + } 406 + } 407 + 408 + // Margin between meetings: pretend meetings start earlier than they do 409 + // so we mark you away for the entire time if you have a series of 410 + // back-to-back meetings, even if they don't strictly overlap. 411 + $margin = phutil_units('15 minutes in seconds'); 412 + 413 + foreach ($rebuild as $phid => $user) { 414 + $events = idx($map, $phid, array()); 415 + 416 + $cursor = $min_range; 417 + if ($events) { 418 + // Find the next time when the user has no meetings. If we move forward 419 + // because of an event, we check again for events after that one ends. 420 + while (true) { 421 + foreach ($events as $event) { 422 + $from = ($event->getDateFrom() - $margin); 423 + $to = $event->getDateTo(); 424 + if (($from <= $cursor) && ($to > $cursor)) { 425 + $cursor = $to; 426 + continue 2; 427 + } 428 + } 429 + break; 430 + } 431 + } 432 + 433 + if ($cursor > $min_range) { 434 + $availability = array( 435 + 'until' => $cursor, 436 + ); 437 + $availability_ttl = $cursor; 438 + } else { 439 + $availability = null; 440 + $availability_ttl = $max_range; 441 + } 442 + 443 + // Never TTL the cache to longer than the maximum range we examined. 444 + $availability_ttl = min($availability_ttl, $max_range); 445 + 446 + // TODO: Write the cache. 447 + 448 + $user->attachAvailability($availability); 449 + } 450 + } 378 451 379 452 }
+49 -14
src/applications/people/storage/PhabricatorUser.php
··· 1 1 <?php 2 2 3 3 /** 4 + * @task availability Availability 4 5 * @task image-cache Profile Image Cache 5 6 * @task factors Multi-Factor Authentication 6 7 * @task handles Managing Handles ··· 45 46 46 47 private $profileImage = self::ATTACHABLE; 47 48 private $profile = null; 48 - private $status = self::ATTACHABLE; 49 + private $availability = self::ATTACHABLE; 49 50 private $preferences = null; 50 51 private $omnipotent = false; 51 52 private $customFields = self::ATTACHABLE; ··· 658 659 return celerity_get_resource_uri('/rsrc/image/avatar.png'); 659 660 } 660 661 661 - public function attachStatus(PhabricatorCalendarEvent $status) { 662 - $this->status = $status; 663 - return $this; 664 - } 665 - 666 - public function getStatus() { 667 - return $this->assertAttached($this->status); 668 - } 669 - 670 - public function hasStatus() { 671 - return $this->status !== self::ATTACHABLE; 672 - } 673 - 674 662 public function attachProfileImageURI($uri) { 675 663 $this->profileImage = $uri; 676 664 return $this; ··· 724 712 */ 725 713 public function getAuthorities() { 726 714 return $this->authorities; 715 + } 716 + 717 + 718 + /* -( Availability )------------------------------------------------------- */ 719 + 720 + 721 + /** 722 + * @task availability 723 + */ 724 + public function attachAvailability($availability) { 725 + $this->availability = $availability; 726 + return $this; 727 + } 728 + 729 + 730 + /** 731 + * Get the timestamp the user is away until, if they are currently away. 732 + * 733 + * @return int|null Epoch timestamp, or `null` if the user is not away. 734 + * @task availability 735 + */ 736 + public function getAwayUntil() { 737 + $availability = $this->availability; 738 + 739 + $this->assertAttached($availability); 740 + if (!$availability) { 741 + return null; 742 + } 743 + 744 + return idx($availability, 'until'); 745 + } 746 + 747 + 748 + /** 749 + * Describe the user's availability. 750 + * 751 + * @param PhabricatorUser Viewing user. 752 + * @return string Human-readable description of away status. 753 + * @task availability 754 + */ 755 + public function getAvailabilityDescription(PhabricatorUser $viewer) { 756 + $until = $this->getAwayUntil(); 757 + if ($until) { 758 + return pht('Away until %s', phabricator_datetime($until, $viewer)); 759 + } else { 760 + return pht('Available'); 761 + } 727 762 } 728 763 729 764
+1
webroot/rsrc/css/layout/phabricator-hovercard-view.css
··· 77 77 height: 50px; 78 78 background-position: center; 79 79 background-repeat: no-repeat; 80 + background-size: 100%; 80 81 } 81 82 .phabricator-hovercard-tail { 82 83 width: 396px;