mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-18 03:20:59 +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:
parent
ac8b156e4b
commit
e337029769
12 changed files with 269 additions and 8 deletions
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE {$NAMESPACE}_calendar.calendar_eventinvitee
|
||||
ADD availability VARCHAR(64) NOT NULL;
|
|
@ -0,0 +1,3 @@
|
|||
UPDATE {$NAMESPACE}_calendar.calendar_eventinvitee
|
||||
SET availability = 'default'
|
||||
WHERE availability = '';
|
|
@ -2035,6 +2035,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorCalendarEvent' => 'applications/calendar/storage/PhabricatorCalendarEvent.php',
|
||||
'PhabricatorCalendarEventAcceptTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventAcceptTransaction.php',
|
||||
'PhabricatorCalendarEventAllDayTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventAllDayTransaction.php',
|
||||
'PhabricatorCalendarEventAvailabilityController' => 'applications/calendar/controller/PhabricatorCalendarEventAvailabilityController.php',
|
||||
'PhabricatorCalendarEventCancelController' => 'applications/calendar/controller/PhabricatorCalendarEventCancelController.php',
|
||||
'PhabricatorCalendarEventCancelTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventCancelTransaction.php',
|
||||
'PhabricatorCalendarEventDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventDateTransaction.php',
|
||||
|
@ -6881,6 +6882,7 @@ phutil_register_library_map(array(
|
|||
),
|
||||
'PhabricatorCalendarEventAcceptTransaction' => 'PhabricatorCalendarEventReplyTransaction',
|
||||
'PhabricatorCalendarEventAllDayTransaction' => 'PhabricatorCalendarEventTransactionType',
|
||||
'PhabricatorCalendarEventAvailabilityController' => 'PhabricatorCalendarController',
|
||||
'PhabricatorCalendarEventCancelController' => 'PhabricatorCalendarController',
|
||||
'PhabricatorCalendarEventCancelTransaction' => 'PhabricatorCalendarEventTransactionType',
|
||||
'PhabricatorCalendarEventDateTransaction' => 'PhabricatorCalendarEventTransactionType',
|
||||
|
|
|
@ -61,6 +61,8 @@ final class PhabricatorCalendarApplication extends PhabricatorApplication {
|
|||
=> 'PhabricatorCalendarEventJoinController',
|
||||
'export/(?P<id>[1-9]\d*)/(?P<filename>[^/]*)'
|
||||
=> 'PhabricatorCalendarEventExportController',
|
||||
'availability/(?P<id>[1-9]\d*)/(?P<availability>[^/]+)/'
|
||||
=> 'PhabricatorCalendarEventAvailabilityController',
|
||||
),
|
||||
'export/' => array(
|
||||
$this->getQueryRoutePattern()
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -132,6 +132,43 @@ final class PhabricatorCalendarEventViewController
|
|||
$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;
|
||||
}
|
||||
|
||||
|
|
|
@ -448,6 +448,12 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
|
|||
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() {
|
||||
return array(
|
||||
PhutilCalendarRecurrenceRule::FREQUENCY_DAILY => array(
|
||||
|
|
|
@ -7,12 +7,18 @@ final class PhabricatorCalendarEventInvitee extends PhabricatorCalendarDAO
|
|||
protected $inviteePHID;
|
||||
protected $inviterPHID;
|
||||
protected $status;
|
||||
protected $availability = self::AVAILABILITY_DEFAULT;
|
||||
|
||||
const STATUS_INVITED = 'invited';
|
||||
const STATUS_ATTENDING = 'attending';
|
||||
const STATUS_DECLINED = 'declined';
|
||||
const STATUS_UNINVITED = 'uninvited';
|
||||
|
||||
const AVAILABILITY_DEFAULT = 'default';
|
||||
const AVAILABILITY_AVAILABLE = 'available';
|
||||
const AVAILABILITY_BUSY = 'busy';
|
||||
const AVAILABILITY_AWAY = 'away';
|
||||
|
||||
public static function initializeNewCalendarEventInvitee(
|
||||
PhabricatorUser $actor, $event) {
|
||||
return id(new PhabricatorCalendarEventInvitee())
|
||||
|
@ -25,6 +31,7 @@ final class PhabricatorCalendarEventInvitee extends PhabricatorCalendarDAO
|
|||
return array(
|
||||
self::CONFIG_COLUMN_SCHEMA => array(
|
||||
'status' => 'text64',
|
||||
'availability' => 'text64',
|
||||
),
|
||||
self::CONFIG_KEY_SCHEMA => 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 )----------------------------------------- */
|
||||
|
||||
|
||||
|
|
|
@ -23,16 +23,57 @@ final class PHUIUserAvailabilityView
|
|||
return pht('Available');
|
||||
}
|
||||
|
||||
$const = $user->getDisplayAvailability();
|
||||
$name = PhabricatorCalendarEventInvitee::getAvailabilityName($const);
|
||||
$color = PhabricatorCalendarEventInvitee::getAvailabilityColor($const);
|
||||
|
||||
$away_tag = id(new PHUITagView())
|
||||
->setType(PHUITagView::TYPE_SHADE)
|
||||
->setShade(PHUITagView::COLOR_RED)
|
||||
->setName(pht('Away'))
|
||||
->setDotColor(PHUITagView::COLOR_RED);
|
||||
->setShade($color)
|
||||
->setName($name)
|
||||
->setDotColor($color);
|
||||
|
||||
$now = PhabricatorTime::getNow();
|
||||
$description = pht(
|
||||
'Away until %s',
|
||||
$viewer->formatShortDateTime($until, $now));
|
||||
|
||||
// Try to load the event handle. If it's invalid or the user can't see it,
|
||||
// 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(
|
||||
$away_tag,
|
||||
|
|
|
@ -66,7 +66,12 @@ final class PhabricatorPeopleUserPHIDType extends PhabricatorPHIDType {
|
|||
} else {
|
||||
$until = $user->getAwayUntil();
|
||||
if ($until) {
|
||||
$availability = PhabricatorObjectHandle::AVAILABILITY_NONE;
|
||||
$away = PhabricatorCalendarEventInvitee::AVAILABILITY_AWAY;
|
||||
if ($user->getDisplayAvailability() == $away) {
|
||||
$availability = PhabricatorObjectHandle::AVAILABILITY_NONE;
|
||||
} else {
|
||||
$availability = PhabricatorObjectHandle::AVAILABILITY_PARTIAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -395,18 +395,28 @@ final class PhabricatorPeopleQuery
|
|||
// Group all the events by invited user. Only examine events that users
|
||||
// are actually attending.
|
||||
$map = array();
|
||||
$invitee_map = array();
|
||||
foreach ($events as $event) {
|
||||
foreach ($event->getInvitees() as $invitee) {
|
||||
if (!$invitee->isAttending()) {
|
||||
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();
|
||||
if (!isset($rebuild[$invitee_phid])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$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;
|
||||
$next_event = null;
|
||||
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.
|
||||
|
@ -435,6 +446,9 @@ final class PhabricatorPeopleQuery
|
|||
$to = $event->getEndDateTimeEpoch();
|
||||
if (($from <= $cursor) && ($to > $cursor)) {
|
||||
$cursor = $to;
|
||||
if (!$next_event) {
|
||||
$next_event = $event;
|
||||
}
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
@ -443,13 +457,29 @@ final class PhabricatorPeopleQuery
|
|||
}
|
||||
|
||||
if ($cursor > $min_range) {
|
||||
$invitee = $invitee_map[$phid][$next_event->getPHID()];
|
||||
$availability_type = $invitee->getDisplayAvailability($next_event);
|
||||
$availability = array(
|
||||
'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 {
|
||||
$availability = array(
|
||||
'until' => null,
|
||||
'eventPHID' => null,
|
||||
'availability' => null,
|
||||
);
|
||||
$availability_ttl = $max_range;
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
Loading…
Reference in a new issue