Page MenuHomePhabricator

D16802.id40468.diff
No OneTemporary

D16802.id40468.diff

diff --git a/resources/sql/autopatches/20161104.calendar.01.availability.sql b/resources/sql/autopatches/20161104.calendar.01.availability.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20161104.calendar.01.availability.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_calendar.calendar_eventinvitee
+ ADD availability VARCHAR(64) NOT NULL;
diff --git a/resources/sql/autopatches/20161104.calendar.02.availdefault.sql b/resources/sql/autopatches/20161104.calendar.02.availdefault.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20161104.calendar.02.availdefault.sql
@@ -0,0 +1,3 @@
+UPDATE {$NAMESPACE}_calendar.calendar_eventinvitee
+ SET availability = 'default'
+ WHERE availability = '';
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -2035,6 +2035,7 @@
'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 @@
),
'PhabricatorCalendarEventAcceptTransaction' => 'PhabricatorCalendarEventReplyTransaction',
'PhabricatorCalendarEventAllDayTransaction' => 'PhabricatorCalendarEventTransactionType',
+ 'PhabricatorCalendarEventAvailabilityController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventCancelController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventCancelTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventDateTransaction' => 'PhabricatorCalendarEventTransactionType',
diff --git a/src/applications/calendar/application/PhabricatorCalendarApplication.php b/src/applications/calendar/application/PhabricatorCalendarApplication.php
--- a/src/applications/calendar/application/PhabricatorCalendarApplication.php
+++ b/src/applications/calendar/application/PhabricatorCalendarApplication.php
@@ -61,6 +61,8 @@
=> 'PhabricatorCalendarEventJoinController',
'export/(?P<id>[1-9]\d*)/(?P<filename>[^/]*)'
=> 'PhabricatorCalendarEventExportController',
+ 'availability/(?P<id>[1-9]\d*)/(?P<availability>[^/]+)/'
+ => 'PhabricatorCalendarEventAvailabilityController',
),
'export/' => array(
$this->getQueryRoutePattern()
diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventAvailabilityController.php b/src/applications/calendar/controller/PhabricatorCalendarEventAvailabilityController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/calendar/controller/PhabricatorCalendarEventAvailabilityController.php
@@ -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);
+ }
+}
diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php
--- a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php
+++ b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php
@@ -132,6 +132,43 @@
$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;
}
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
@@ -448,6 +448,12 @@
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(
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
@@ -7,12 +7,18 @@
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 @@
return array(
self::CONFIG_COLUMN_SCHEMA => array(
'status' => 'text64',
+ 'availability' => 'text64',
),
self::CONFIG_KEY_SCHEMA => array(
'key_event' => array(
@@ -50,6 +57,50 @@
}
}
+ 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 )----------------------------------------- */
diff --git a/src/applications/calendar/view/PHUIUserAvailabilityView.php b/src/applications/calendar/view/PHUIUserAvailabilityView.php
--- a/src/applications/calendar/view/PHUIUserAvailabilityView.php
+++ b/src/applications/calendar/view/PHUIUserAvailabilityView.php
@@ -23,16 +23,57 @@
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,
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
@@ -66,7 +66,12 @@
} 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;
+ }
}
}
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
@@ -395,18 +395,28 @@
// 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 @@
}
$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 @@
$to = $event->getEndDateTimeEpoch();
if (($from <= $cursor) && ($to > $cursor)) {
$cursor = $to;
+ if (!$next_event) {
+ $next_event = $event;
+ }
continue 2;
}
}
@@ -443,13 +457,29 @@
}
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;
}
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
@@ -960,6 +960,32 @@
}
+ 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.
*

File Metadata

Mime Type
text/plain
Expires
Sun, Oct 20, 4:39 AM (1 w, 4 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6728113
Default Alt Text
D16802.id40468.diff (16 KB)

Event Timeline