mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-18 19:40:55 +01:00
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
This commit is contained in:
parent
04186e02cf
commit
fef3c778fd
13 changed files with 173 additions and 117 deletions
|
@ -117,7 +117,7 @@ return array(
|
|||
'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 @@ return array(
|
|||
'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',
|
||||
|
|
|
@ -218,18 +218,6 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
|
|||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -38,6 +38,10 @@ final class PhabricatorCalendarEventInvitee extends PhabricatorCalendarDAO
|
|||
) + parent::getConfiguration();
|
||||
}
|
||||
|
||||
public function isAttending() {
|
||||
return ($this->getStatus() == self::STATUS_ATTENDING);
|
||||
}
|
||||
|
||||
public function isUninvited() {
|
||||
if ($this->getStatus() == self::STATUS_UNINVITED) {
|
||||
return true;
|
||||
|
|
|
@ -6,9 +6,7 @@ abstract class UserConduitAPIMethod extends ConduitAPIMethod {
|
|||
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 @@ abstract class UserConduitAPIMethod extends ConduitAPIMethod {
|
|||
'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;
|
||||
|
|
|
@ -43,7 +43,8 @@ final class UserQueryConduitAPIMethod extends UserConduitAPIMethod {
|
|||
|
||||
$query = id(new PhabricatorPeopleQuery())
|
||||
->setViewer($request->getUser())
|
||||
->needProfileImage(true);
|
||||
->needProfileImage(true)
|
||||
->needAvailability(true);
|
||||
|
||||
if ($usernames) {
|
||||
$query->withUsernames($usernames);
|
||||
|
@ -68,14 +69,9 @@ final class UserQueryConduitAPIMethod extends UserConduitAPIMethod {
|
|||
}
|
||||
$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;
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ final class PhabricatorPeopleProfileController
|
|||
->setViewer($viewer)
|
||||
->withUsernames(array($this->username))
|
||||
->needProfileImage(true)
|
||||
->needAvailability(true)
|
||||
->executeOne();
|
||||
if (!$user) {
|
||||
return new Aphront404Response();
|
||||
|
|
|
@ -29,16 +29,7 @@ final class PhabricatorUserStatusField
|
|||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -26,12 +26,15 @@ final class PhabricatorPeopleHovercardEventListener
|
|||
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 @@ final class PhabricatorPeopleHovercardEventListener
|
|||
} 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);
|
||||
}
|
||||
|
|
|
@ -72,16 +72,9 @@ final class PhabricatorMentionRemarkupRule extends PhutilRemarkupRule {
|
|||
$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 @@ final class PhabricatorMentionRemarkupRule extends PhutilRemarkupRule {
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ final class PhabricatorPeopleUserPHIDType extends PhabricatorPHIDType {
|
|||
return id(new PhabricatorPeopleQuery())
|
||||
->withPHIDs($phids)
|
||||
->needProfileImage(true)
|
||||
->needStatus(true);
|
||||
->needAvailability(true);
|
||||
}
|
||||
|
||||
public function loadHandles(
|
||||
|
@ -48,18 +48,9 @@ final class PhabricatorPeopleUserPHIDType extends PhabricatorPHIDType {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ final class PhabricatorPeopleQuery
|
|||
private $needPrimaryEmail;
|
||||
private $needProfile;
|
||||
private $needProfileImage;
|
||||
private $needStatus;
|
||||
private $needAvailability;
|
||||
|
||||
public function withIDs(array $ids) {
|
||||
$this->ids = $ids;
|
||||
|
@ -102,8 +102,8 @@ final class PhabricatorPeopleQuery
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function needStatus($need) {
|
||||
$this->needStatus = $need;
|
||||
public function needAvailability($need) {
|
||||
$this->needAvailability = $need;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -200,15 +200,11 @@ final class PhabricatorPeopleQuery
|
|||
}
|
||||
}
|
||||
|
||||
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 @@ final class PhabricatorPeopleQuery
|
|||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @task availability Availability
|
||||
* @task image-cache Profile Image Cache
|
||||
* @task factors Multi-Factor Authentication
|
||||
* @task handles Managing Handles
|
||||
|
@ -45,7 +46,7 @@ final class PhabricatorUser
|
|||
|
||||
private $profileImage = self::ATTACHABLE;
|
||||
private $profile = null;
|
||||
private $status = self::ATTACHABLE;
|
||||
private $availability = self::ATTACHABLE;
|
||||
private $preferences = null;
|
||||
private $omnipotent = false;
|
||||
private $customFields = self::ATTACHABLE;
|
||||
|
@ -658,19 +659,6 @@ EOBODY;
|
|||
return celerity_get_resource_uri('/rsrc/image/avatar.png');
|
||||
}
|
||||
|
||||
public function attachStatus(PhabricatorCalendarEvent $status) {
|
||||
$this->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 @@ EOBODY;
|
|||
}
|
||||
|
||||
|
||||
/* -( 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 )------------------------------------------------ */
|
||||
|
||||
|
||||
|
|
|
@ -77,6 +77,7 @@
|
|||
height: 50px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
}
|
||||
.phabricator-hovercard-tail {
|
||||
width: 396px;
|
||||
|
|
Loading…
Reference in a new issue