1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-17 20:32:41 +01:00

Allow users to mark themselves as "Available", "Busy" or "Away" while attending an event

Summary:
Ref T11816.

  - Now that we can do something meaningful with them, bring back the yellow dots for "busy".
  - Default to "busy" when attending events (we could make this "busy" for short events and "away" for long events or something).
  - Let users pick how to display their attending status on the event page.
  - Also show which event the user is attending since I had to mess with the cache code anyway. We can get rid of this again if it doesn't feel good.

Test Plan:
{F1904179}

{F1904180}

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T11816

Differential Revision: https://secure.phabricator.com/D16802
This commit is contained in:
epriestley 2016-11-04 15:42:24 -07:00
parent ac8b156e4b
commit e337029769
12 changed files with 269 additions and 8 deletions

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_calendar.calendar_eventinvitee
ADD availability VARCHAR(64) NOT NULL;

View file

@ -0,0 +1,3 @@
UPDATE {$NAMESPACE}_calendar.calendar_eventinvitee
SET availability = 'default'
WHERE availability = '';

View file

@ -2035,6 +2035,7 @@ phutil_register_library_map(array(
'PhabricatorCalendarEvent' => 'applications/calendar/storage/PhabricatorCalendarEvent.php', 'PhabricatorCalendarEvent' => 'applications/calendar/storage/PhabricatorCalendarEvent.php',
'PhabricatorCalendarEventAcceptTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventAcceptTransaction.php', 'PhabricatorCalendarEventAcceptTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventAcceptTransaction.php',
'PhabricatorCalendarEventAllDayTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventAllDayTransaction.php', 'PhabricatorCalendarEventAllDayTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventAllDayTransaction.php',
'PhabricatorCalendarEventAvailabilityController' => 'applications/calendar/controller/PhabricatorCalendarEventAvailabilityController.php',
'PhabricatorCalendarEventCancelController' => 'applications/calendar/controller/PhabricatorCalendarEventCancelController.php', 'PhabricatorCalendarEventCancelController' => 'applications/calendar/controller/PhabricatorCalendarEventCancelController.php',
'PhabricatorCalendarEventCancelTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventCancelTransaction.php', 'PhabricatorCalendarEventCancelTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventCancelTransaction.php',
'PhabricatorCalendarEventDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventDateTransaction.php', 'PhabricatorCalendarEventDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventDateTransaction.php',
@ -6881,6 +6882,7 @@ phutil_register_library_map(array(
), ),
'PhabricatorCalendarEventAcceptTransaction' => 'PhabricatorCalendarEventReplyTransaction', 'PhabricatorCalendarEventAcceptTransaction' => 'PhabricatorCalendarEventReplyTransaction',
'PhabricatorCalendarEventAllDayTransaction' => 'PhabricatorCalendarEventTransactionType', 'PhabricatorCalendarEventAllDayTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventAvailabilityController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventCancelController' => 'PhabricatorCalendarController', 'PhabricatorCalendarEventCancelController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventCancelTransaction' => 'PhabricatorCalendarEventTransactionType', 'PhabricatorCalendarEventCancelTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventDateTransaction' => 'PhabricatorCalendarEventTransactionType', 'PhabricatorCalendarEventDateTransaction' => 'PhabricatorCalendarEventTransactionType',

View file

@ -61,6 +61,8 @@ final class PhabricatorCalendarApplication extends PhabricatorApplication {
=> 'PhabricatorCalendarEventJoinController', => 'PhabricatorCalendarEventJoinController',
'export/(?P<id>[1-9]\d*)/(?P<filename>[^/]*)' 'export/(?P<id>[1-9]\d*)/(?P<filename>[^/]*)'
=> 'PhabricatorCalendarEventExportController', => 'PhabricatorCalendarEventExportController',
'availability/(?P<id>[1-9]\d*)/(?P<availability>[^/]+)/'
=> 'PhabricatorCalendarEventAvailabilityController',
), ),
'export/' => array( 'export/' => array(
$this->getQueryRoutePattern() $this->getQueryRoutePattern()

View file

@ -0,0 +1,56 @@
<?php
final class PhabricatorCalendarEventAvailabilityController
extends PhabricatorCalendarController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$event = id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$event) {
return new Aphront404Response();
}
$response = $this->newImportedEventResponse($event);
if ($response) {
return $response;
}
$cancel_uri = $event->getURI();
if (!$event->getIsUserAttending($viewer->getPHID())) {
return $this->newDialog()
->setTitle(pht('Not Attending Event'))
->appendParagraph(
pht(
'You can not change your display availability for events you '.
'are not attending.'))
->addCancelButton($cancel_uri);
}
// TODO: This endpoint currently only works via AJAX. It would be vaguely
// nice to provide a plain HTML version of the workflow where we return
// a dialog with a vanilla <select /> in it for cases where all the JS
// breaks.
$request->validateCSRF();
$invitee = $event->getInviteeForPHID($viewer->getPHID());
$map = PhabricatorCalendarEventInvitee::getAvailabilityMap();
$new_availability = $request->getURIData('availability');
if (isset($map[$new_availability])) {
$invitee
->setAvailability($new_availability)
->save();
// Invalidate the availability cache.
$viewer->writeAvailabilityCache(array(), null);
}
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
}
}

View file

@ -132,6 +132,43 @@ final class PhabricatorCalendarEventViewController
$header->addActionLink($action); $header->addActionLink($action);
} }
$options = PhabricatorCalendarEventInvitee::getAvailabilityMap();
$is_attending = $event->getIsUserAttending($viewer->getPHID());
if ($is_attending) {
$invitee = $event->getInviteeForPHID($viewer->getPHID());
$selected = $invitee->getDisplayAvailability($event);
if (!$selected) {
$selected = PhabricatorCalendarEventInvitee::AVAILABILITY_AVAILABLE;
}
$selected_option = idx($options, $selected);
$availability_select = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-circle '.$selected_option['color'])
->setText(pht('Availability: %s', $selected_option['name']));
$dropdown = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($options as $key => $option) {
$uri = "event/availability/{$id}/{$key}/";
$uri = $this->getApplicationURI($uri);
$dropdown->addAction(
id(new PhabricatorActionView())
->setName($option['name'])
->setIcon('fa-circle '.$option['color'])
->setHref($uri)
->setWorkflow(true));
}
$availability_select->setDropdownMenu($dropdown);
$header->addActionLink($availability_select);
}
return $header; return $header;
} }

View file

@ -448,6 +448,12 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
return $this->assertAttached($this->invitees); return $this->assertAttached($this->invitees);
} }
public function getInviteeForPHID($phid) {
$invitees = $this->getInvitees();
$invitees = mpull($invitees, null, 'getInviteePHID');
return idx($invitees, $phid);
}
public static function getFrequencyMap() { public static function getFrequencyMap() {
return array( return array(
PhutilCalendarRecurrenceRule::FREQUENCY_DAILY => array( PhutilCalendarRecurrenceRule::FREQUENCY_DAILY => array(

View file

@ -7,12 +7,18 @@ final class PhabricatorCalendarEventInvitee extends PhabricatorCalendarDAO
protected $inviteePHID; protected $inviteePHID;
protected $inviterPHID; protected $inviterPHID;
protected $status; protected $status;
protected $availability = self::AVAILABILITY_DEFAULT;
const STATUS_INVITED = 'invited'; const STATUS_INVITED = 'invited';
const STATUS_ATTENDING = 'attending'; const STATUS_ATTENDING = 'attending';
const STATUS_DECLINED = 'declined'; const STATUS_DECLINED = 'declined';
const STATUS_UNINVITED = 'uninvited'; const STATUS_UNINVITED = 'uninvited';
const AVAILABILITY_DEFAULT = 'default';
const AVAILABILITY_AVAILABLE = 'available';
const AVAILABILITY_BUSY = 'busy';
const AVAILABILITY_AWAY = 'away';
public static function initializeNewCalendarEventInvitee( public static function initializeNewCalendarEventInvitee(
PhabricatorUser $actor, $event) { PhabricatorUser $actor, $event) {
return id(new PhabricatorCalendarEventInvitee()) return id(new PhabricatorCalendarEventInvitee())
@ -25,6 +31,7 @@ final class PhabricatorCalendarEventInvitee extends PhabricatorCalendarDAO
return array( return array(
self::CONFIG_COLUMN_SCHEMA => array( self::CONFIG_COLUMN_SCHEMA => array(
'status' => 'text64', 'status' => 'text64',
'availability' => 'text64',
), ),
self::CONFIG_KEY_SCHEMA => array( self::CONFIG_KEY_SCHEMA => array(
'key_event' => array( 'key_event' => array(
@ -50,6 +57,50 @@ final class PhabricatorCalendarEventInvitee extends PhabricatorCalendarDAO
} }
} }
public function getDisplayAvailability(PhabricatorCalendarEvent $event) {
switch ($this->getAvailability()) {
case self::AVAILABILITY_DEFAULT:
case self::AVAILABILITY_BUSY:
return self::AVAILABILITY_BUSY;
case self::AVAILABILITY_AWAY:
return self::AVAILABILITY_AWAY;
default:
return null;
}
}
public static function getAvailabilityMap() {
return array(
self::AVAILABILITY_AVAILABLE => array(
'color' => 'green',
'name' => pht('Available'),
),
self::AVAILABILITY_BUSY => array(
'color' => 'yellow',
'name' => pht('Busy'),
),
self::AVAILABILITY_AWAY => array(
'color' => 'red',
'name' => pht('Away'),
),
);
}
public static function getAvailabilitySpec($const) {
return idx(self::getAvailabilityMap(), $const, array());
}
public static function getAvailabilityName($const) {
$spec = self::getAvailabilitySpec($const);
return idx($spec, 'name', $const);
}
public static function getAvailabilityColor($const) {
$spec = self::getAvailabilitySpec($const);
return idx($spec, 'color', 'indigo');
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */ /* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -23,16 +23,57 @@ final class PHUIUserAvailabilityView
return pht('Available'); return pht('Available');
} }
$const = $user->getDisplayAvailability();
$name = PhabricatorCalendarEventInvitee::getAvailabilityName($const);
$color = PhabricatorCalendarEventInvitee::getAvailabilityColor($const);
$away_tag = id(new PHUITagView()) $away_tag = id(new PHUITagView())
->setType(PHUITagView::TYPE_SHADE) ->setType(PHUITagView::TYPE_SHADE)
->setShade(PHUITagView::COLOR_RED) ->setShade($color)
->setName(pht('Away')) ->setName($name)
->setDotColor(PHUITagView::COLOR_RED); ->setDotColor($color);
$now = PhabricatorTime::getNow(); $now = PhabricatorTime::getNow();
$description = pht(
'Away until %s', // Try to load the event handle. If it's invalid or the user can't see it,
$viewer->formatShortDateTime($until, $now)); // we'll just render a generic message.
$object_phid = $user->getAvailabilityEventPHID();
$handle = null;
if ($object_phid) {
$handles = $viewer->loadHandles(array($object_phid));
$handle = $handles[$object_phid];
if (!$handle->isComplete() || $handle->getPolicyFiltered()) {
$handle = null;
}
}
switch ($const) {
case PhabricatorCalendarEventInvitee::AVAILABILITY_AWAY:
if ($handle) {
$description = pht(
'Away at %s until %s.',
$handle->renderLink(),
$viewer->formatShortDateTime($until, $now));
} else {
$description = pht(
'Away until %s.',
$viewer->formatShortDateTime($until, $now));
}
break;
case PhabricatorCalendarEventInvitee::AVAILABILITY_BUSY:
default:
if ($handle) {
$description = pht(
'Busy at %s until %s.',
$handle->renderLink(),
$viewer->formatShortDateTime($until, $now));
} else {
$description = pht(
'Busy until %s.',
$viewer->formatShortDateTime($until, $now));
}
break;
}
return array( return array(
$away_tag, $away_tag,

View file

@ -66,7 +66,12 @@ final class PhabricatorPeopleUserPHIDType extends PhabricatorPHIDType {
} else { } else {
$until = $user->getAwayUntil(); $until = $user->getAwayUntil();
if ($until) { if ($until) {
$availability = PhabricatorObjectHandle::AVAILABILITY_NONE; $away = PhabricatorCalendarEventInvitee::AVAILABILITY_AWAY;
if ($user->getDisplayAvailability() == $away) {
$availability = PhabricatorObjectHandle::AVAILABILITY_NONE;
} else {
$availability = PhabricatorObjectHandle::AVAILABILITY_PARTIAL;
}
} }
} }

View file

@ -395,18 +395,28 @@ final class PhabricatorPeopleQuery
// Group all the events by invited user. Only examine events that users // Group all the events by invited user. Only examine events that users
// are actually attending. // are actually attending.
$map = array(); $map = array();
$invitee_map = array();
foreach ($events as $event) { foreach ($events as $event) {
foreach ($event->getInvitees() as $invitee) { foreach ($event->getInvitees() as $invitee) {
if (!$invitee->isAttending()) { if (!$invitee->isAttending()) {
continue; continue;
} }
// If the user is set to "Available" for this event, don't consider it
// when computin their away status.
if (!$invitee->getDisplayAvailability($event)) {
continue;
}
$invitee_phid = $invitee->getInviteePHID(); $invitee_phid = $invitee->getInviteePHID();
if (!isset($rebuild[$invitee_phid])) { if (!isset($rebuild[$invitee_phid])) {
continue; continue;
} }
$map[$invitee_phid][] = $event; $map[$invitee_phid][] = $event;
$event_phid = $event->getPHID();
$invitee_map[$invitee_phid][$event_phid] = $invitee;
} }
} }
@ -426,6 +436,7 @@ final class PhabricatorPeopleQuery
} }
$cursor = $min_range; $cursor = $min_range;
$next_event = null;
if ($events) { if ($events) {
// Find the next time when the user has no meetings. If we move forward // 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. // because of an event, we check again for events after that one ends.
@ -435,6 +446,9 @@ final class PhabricatorPeopleQuery
$to = $event->getEndDateTimeEpoch(); $to = $event->getEndDateTimeEpoch();
if (($from <= $cursor) && ($to > $cursor)) { if (($from <= $cursor) && ($to > $cursor)) {
$cursor = $to; $cursor = $to;
if (!$next_event) {
$next_event = $event;
}
continue 2; continue 2;
} }
} }
@ -443,13 +457,29 @@ final class PhabricatorPeopleQuery
} }
if ($cursor > $min_range) { if ($cursor > $min_range) {
$invitee = $invitee_map[$phid][$next_event->getPHID()];
$availability_type = $invitee->getDisplayAvailability($next_event);
$availability = array( $availability = array(
'until' => $cursor, 'until' => $cursor,
'eventPHID' => $event->getPHID(),
'availability' => $availability_type,
); );
$availability_ttl = $cursor;
// We only cache this availability until the end of the current event,
// since the event PHID (and possibly the availability type) are only
// valid for that long.
// NOTE: This doesn't handle overlapping events with the greatest
// possible care. In theory, if you're attenting multiple events
// simultaneously we should accommodate that. However, it's complex
// to compute, rare, and probably not confusing most of the time.
$availability_ttl = $next_event->getStartDateTimeEpochForCache();
} else { } else {
$availability = array( $availability = array(
'until' => null, 'until' => null,
'eventPHID' => null,
'availability' => null,
); );
$availability_ttl = $max_range; $availability_ttl = $max_range;
} }

View file

@ -960,6 +960,32 @@ final class PhabricatorUser
} }
public function getDisplayAvailability() {
$availability = $this->availability;
$this->assertAttached($availability);
if (!$availability) {
return null;
}
$busy = PhabricatorCalendarEventInvitee::AVAILABILITY_BUSY;
return idx($availability, 'availability', $busy);
}
public function getAvailabilityEventPHID() {
$availability = $this->availability;
$this->assertAttached($availability);
if (!$availability) {
return null;
}
return idx($availability, 'eventPHID');
}
/** /**
* Get cached availability, if present. * Get cached availability, if present.
* *