@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.

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

+269 -8
+2
resources/sql/autopatches/20161104.calendar.01.availability.sql
··· 1 + ALTER TABLE {$NAMESPACE}_calendar.calendar_eventinvitee 2 + ADD availability VARCHAR(64) NOT NULL;
+3
resources/sql/autopatches/20161104.calendar.02.availdefault.sql
··· 1 + UPDATE {$NAMESPACE}_calendar.calendar_eventinvitee 2 + SET availability = 'default' 3 + WHERE availability = '';
+2
src/__phutil_library_map__.php
··· 2035 2035 'PhabricatorCalendarEvent' => 'applications/calendar/storage/PhabricatorCalendarEvent.php', 2036 2036 'PhabricatorCalendarEventAcceptTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventAcceptTransaction.php', 2037 2037 'PhabricatorCalendarEventAllDayTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventAllDayTransaction.php', 2038 + 'PhabricatorCalendarEventAvailabilityController' => 'applications/calendar/controller/PhabricatorCalendarEventAvailabilityController.php', 2038 2039 'PhabricatorCalendarEventCancelController' => 'applications/calendar/controller/PhabricatorCalendarEventCancelController.php', 2039 2040 'PhabricatorCalendarEventCancelTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventCancelTransaction.php', 2040 2041 'PhabricatorCalendarEventDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventDateTransaction.php', ··· 6881 6882 ), 6882 6883 'PhabricatorCalendarEventAcceptTransaction' => 'PhabricatorCalendarEventReplyTransaction', 6883 6884 'PhabricatorCalendarEventAllDayTransaction' => 'PhabricatorCalendarEventTransactionType', 6885 + 'PhabricatorCalendarEventAvailabilityController' => 'PhabricatorCalendarController', 6884 6886 'PhabricatorCalendarEventCancelController' => 'PhabricatorCalendarController', 6885 6887 'PhabricatorCalendarEventCancelTransaction' => 'PhabricatorCalendarEventTransactionType', 6886 6888 'PhabricatorCalendarEventDateTransaction' => 'PhabricatorCalendarEventTransactionType',
+2
src/applications/calendar/application/PhabricatorCalendarApplication.php
··· 61 61 => 'PhabricatorCalendarEventJoinController', 62 62 'export/(?P<id>[1-9]\d*)/(?P<filename>[^/]*)' 63 63 => 'PhabricatorCalendarEventExportController', 64 + 'availability/(?P<id>[1-9]\d*)/(?P<availability>[^/]+)/' 65 + => 'PhabricatorCalendarEventAvailabilityController', 64 66 ), 65 67 'export/' => array( 66 68 $this->getQueryRoutePattern()
+56
src/applications/calendar/controller/PhabricatorCalendarEventAvailabilityController.php
··· 1 + <?php 2 + 3 + final class PhabricatorCalendarEventAvailabilityController 4 + extends PhabricatorCalendarController { 5 + 6 + public function handleRequest(AphrontRequest $request) { 7 + $viewer = $this->getViewer(); 8 + $id = $request->getURIData('id'); 9 + 10 + $event = id(new PhabricatorCalendarEventQuery()) 11 + ->setViewer($viewer) 12 + ->withIDs(array($id)) 13 + ->executeOne(); 14 + if (!$event) { 15 + return new Aphront404Response(); 16 + } 17 + 18 + $response = $this->newImportedEventResponse($event); 19 + if ($response) { 20 + return $response; 21 + } 22 + 23 + $cancel_uri = $event->getURI(); 24 + 25 + if (!$event->getIsUserAttending($viewer->getPHID())) { 26 + return $this->newDialog() 27 + ->setTitle(pht('Not Attending Event')) 28 + ->appendParagraph( 29 + pht( 30 + 'You can not change your display availability for events you '. 31 + 'are not attending.')) 32 + ->addCancelButton($cancel_uri); 33 + } 34 + 35 + // TODO: This endpoint currently only works via AJAX. It would be vaguely 36 + // nice to provide a plain HTML version of the workflow where we return 37 + // a dialog with a vanilla <select /> in it for cases where all the JS 38 + // breaks. 39 + $request->validateCSRF(); 40 + 41 + $invitee = $event->getInviteeForPHID($viewer->getPHID()); 42 + 43 + $map = PhabricatorCalendarEventInvitee::getAvailabilityMap(); 44 + $new_availability = $request->getURIData('availability'); 45 + if (isset($map[$new_availability])) { 46 + $invitee 47 + ->setAvailability($new_availability) 48 + ->save(); 49 + 50 + // Invalidate the availability cache. 51 + $viewer->writeAvailabilityCache(array(), null); 52 + } 53 + 54 + return id(new AphrontRedirectResponse())->setURI($cancel_uri); 55 + } 56 + }
+37
src/applications/calendar/controller/PhabricatorCalendarEventViewController.php
··· 132 132 $header->addActionLink($action); 133 133 } 134 134 135 + $options = PhabricatorCalendarEventInvitee::getAvailabilityMap(); 136 + 137 + $is_attending = $event->getIsUserAttending($viewer->getPHID()); 138 + if ($is_attending) { 139 + $invitee = $event->getInviteeForPHID($viewer->getPHID()); 140 + 141 + $selected = $invitee->getDisplayAvailability($event); 142 + if (!$selected) { 143 + $selected = PhabricatorCalendarEventInvitee::AVAILABILITY_AVAILABLE; 144 + } 145 + 146 + $selected_option = idx($options, $selected); 147 + 148 + $availability_select = id(new PHUIButtonView()) 149 + ->setTag('a') 150 + ->setIcon('fa-circle '.$selected_option['color']) 151 + ->setText(pht('Availability: %s', $selected_option['name'])); 152 + 153 + $dropdown = id(new PhabricatorActionListView()) 154 + ->setUser($viewer); 155 + 156 + foreach ($options as $key => $option) { 157 + $uri = "event/availability/{$id}/{$key}/"; 158 + $uri = $this->getApplicationURI($uri); 159 + 160 + $dropdown->addAction( 161 + id(new PhabricatorActionView()) 162 + ->setName($option['name']) 163 + ->setIcon('fa-circle '.$option['color']) 164 + ->setHref($uri) 165 + ->setWorkflow(true)); 166 + } 167 + 168 + $availability_select->setDropdownMenu($dropdown); 169 + $header->addActionLink($availability_select); 170 + } 171 + 135 172 return $header; 136 173 } 137 174
+6
src/applications/calendar/storage/PhabricatorCalendarEvent.php
··· 448 448 return $this->assertAttached($this->invitees); 449 449 } 450 450 451 + public function getInviteeForPHID($phid) { 452 + $invitees = $this->getInvitees(); 453 + $invitees = mpull($invitees, null, 'getInviteePHID'); 454 + return idx($invitees, $phid); 455 + } 456 + 451 457 public static function getFrequencyMap() { 452 458 return array( 453 459 PhutilCalendarRecurrenceRule::FREQUENCY_DAILY => array(
+51
src/applications/calendar/storage/PhabricatorCalendarEventInvitee.php
··· 7 7 protected $inviteePHID; 8 8 protected $inviterPHID; 9 9 protected $status; 10 + protected $availability = self::AVAILABILITY_DEFAULT; 10 11 11 12 const STATUS_INVITED = 'invited'; 12 13 const STATUS_ATTENDING = 'attending'; 13 14 const STATUS_DECLINED = 'declined'; 14 15 const STATUS_UNINVITED = 'uninvited'; 16 + 17 + const AVAILABILITY_DEFAULT = 'default'; 18 + const AVAILABILITY_AVAILABLE = 'available'; 19 + const AVAILABILITY_BUSY = 'busy'; 20 + const AVAILABILITY_AWAY = 'away'; 15 21 16 22 public static function initializeNewCalendarEventInvitee( 17 23 PhabricatorUser $actor, $event) { ··· 25 31 return array( 26 32 self::CONFIG_COLUMN_SCHEMA => array( 27 33 'status' => 'text64', 34 + 'availability' => 'text64', 28 35 ), 29 36 self::CONFIG_KEY_SCHEMA => array( 30 37 'key_event' => array( ··· 49 56 return false; 50 57 } 51 58 } 59 + 60 + public function getDisplayAvailability(PhabricatorCalendarEvent $event) { 61 + switch ($this->getAvailability()) { 62 + case self::AVAILABILITY_DEFAULT: 63 + case self::AVAILABILITY_BUSY: 64 + return self::AVAILABILITY_BUSY; 65 + case self::AVAILABILITY_AWAY: 66 + return self::AVAILABILITY_AWAY; 67 + default: 68 + return null; 69 + } 70 + } 71 + 72 + public static function getAvailabilityMap() { 73 + return array( 74 + self::AVAILABILITY_AVAILABLE => array( 75 + 'color' => 'green', 76 + 'name' => pht('Available'), 77 + ), 78 + self::AVAILABILITY_BUSY => array( 79 + 'color' => 'yellow', 80 + 'name' => pht('Busy'), 81 + ), 82 + self::AVAILABILITY_AWAY => array( 83 + 'color' => 'red', 84 + 'name' => pht('Away'), 85 + ), 86 + ); 87 + } 88 + 89 + public static function getAvailabilitySpec($const) { 90 + return idx(self::getAvailabilityMap(), $const, array()); 91 + } 92 + 93 + public static function getAvailabilityName($const) { 94 + $spec = self::getAvailabilitySpec($const); 95 + return idx($spec, 'name', $const); 96 + } 97 + 98 + public static function getAvailabilityColor($const) { 99 + $spec = self::getAvailabilitySpec($const); 100 + return idx($spec, 'color', 'indigo'); 101 + } 102 + 52 103 53 104 /* -( PhabricatorPolicyInterface )----------------------------------------- */ 54 105
+47 -6
src/applications/calendar/view/PHUIUserAvailabilityView.php
··· 23 23 return pht('Available'); 24 24 } 25 25 26 + $const = $user->getDisplayAvailability(); 27 + $name = PhabricatorCalendarEventInvitee::getAvailabilityName($const); 28 + $color = PhabricatorCalendarEventInvitee::getAvailabilityColor($const); 29 + 26 30 $away_tag = id(new PHUITagView()) 27 31 ->setType(PHUITagView::TYPE_SHADE) 28 - ->setShade(PHUITagView::COLOR_RED) 29 - ->setName(pht('Away')) 30 - ->setDotColor(PHUITagView::COLOR_RED); 32 + ->setShade($color) 33 + ->setName($name) 34 + ->setDotColor($color); 31 35 32 36 $now = PhabricatorTime::getNow(); 33 - $description = pht( 34 - 'Away until %s', 35 - $viewer->formatShortDateTime($until, $now)); 37 + 38 + // Try to load the event handle. If it's invalid or the user can't see it, 39 + // we'll just render a generic message. 40 + $object_phid = $user->getAvailabilityEventPHID(); 41 + $handle = null; 42 + if ($object_phid) { 43 + $handles = $viewer->loadHandles(array($object_phid)); 44 + $handle = $handles[$object_phid]; 45 + if (!$handle->isComplete() || $handle->getPolicyFiltered()) { 46 + $handle = null; 47 + } 48 + } 49 + 50 + switch ($const) { 51 + case PhabricatorCalendarEventInvitee::AVAILABILITY_AWAY: 52 + if ($handle) { 53 + $description = pht( 54 + 'Away at %s until %s.', 55 + $handle->renderLink(), 56 + $viewer->formatShortDateTime($until, $now)); 57 + } else { 58 + $description = pht( 59 + 'Away until %s.', 60 + $viewer->formatShortDateTime($until, $now)); 61 + } 62 + break; 63 + case PhabricatorCalendarEventInvitee::AVAILABILITY_BUSY: 64 + default: 65 + if ($handle) { 66 + $description = pht( 67 + 'Busy at %s until %s.', 68 + $handle->renderLink(), 69 + $viewer->formatShortDateTime($until, $now)); 70 + } else { 71 + $description = pht( 72 + 'Busy until %s.', 73 + $viewer->formatShortDateTime($until, $now)); 74 + } 75 + break; 76 + } 36 77 37 78 return array( 38 79 $away_tag,
+6 -1
src/applications/people/phid/PhabricatorPeopleUserPHIDType.php
··· 66 66 } else { 67 67 $until = $user->getAwayUntil(); 68 68 if ($until) { 69 - $availability = PhabricatorObjectHandle::AVAILABILITY_NONE; 69 + $away = PhabricatorCalendarEventInvitee::AVAILABILITY_AWAY; 70 + if ($user->getDisplayAvailability() == $away) { 71 + $availability = PhabricatorObjectHandle::AVAILABILITY_NONE; 72 + } else { 73 + $availability = PhabricatorObjectHandle::AVAILABILITY_PARTIAL; 74 + } 70 75 } 71 76 } 72 77
+31 -1
src/applications/people/query/PhabricatorPeopleQuery.php
··· 395 395 // Group all the events by invited user. Only examine events that users 396 396 // are actually attending. 397 397 $map = array(); 398 + $invitee_map = array(); 398 399 foreach ($events as $event) { 399 400 foreach ($event->getInvitees() as $invitee) { 400 401 if (!$invitee->isAttending()) { 401 402 continue; 402 403 } 403 404 405 + // If the user is set to "Available" for this event, don't consider it 406 + // when computin their away status. 407 + if (!$invitee->getDisplayAvailability($event)) { 408 + continue; 409 + } 410 + 404 411 $invitee_phid = $invitee->getInviteePHID(); 405 412 if (!isset($rebuild[$invitee_phid])) { 406 413 continue; 407 414 } 408 415 409 416 $map[$invitee_phid][] = $event; 417 + 418 + $event_phid = $event->getPHID(); 419 + $invitee_map[$invitee_phid][$event_phid] = $invitee; 410 420 } 411 421 } 412 422 ··· 426 436 } 427 437 428 438 $cursor = $min_range; 439 + $next_event = null; 429 440 if ($events) { 430 441 // Find the next time when the user has no meetings. If we move forward 431 442 // because of an event, we check again for events after that one ends. ··· 435 446 $to = $event->getEndDateTimeEpoch(); 436 447 if (($from <= $cursor) && ($to > $cursor)) { 437 448 $cursor = $to; 449 + if (!$next_event) { 450 + $next_event = $event; 451 + } 438 452 continue 2; 439 453 } 440 454 } ··· 443 457 } 444 458 445 459 if ($cursor > $min_range) { 460 + $invitee = $invitee_map[$phid][$next_event->getPHID()]; 461 + $availability_type = $invitee->getDisplayAvailability($next_event); 446 462 $availability = array( 447 463 'until' => $cursor, 464 + 'eventPHID' => $event->getPHID(), 465 + 'availability' => $availability_type, 448 466 ); 449 - $availability_ttl = $cursor; 467 + 468 + // We only cache this availability until the end of the current event, 469 + // since the event PHID (and possibly the availability type) are only 470 + // valid for that long. 471 + 472 + // NOTE: This doesn't handle overlapping events with the greatest 473 + // possible care. In theory, if you're attenting multiple events 474 + // simultaneously we should accommodate that. However, it's complex 475 + // to compute, rare, and probably not confusing most of the time. 476 + 477 + $availability_ttl = $next_event->getStartDateTimeEpochForCache(); 450 478 } else { 451 479 $availability = array( 452 480 'until' => null, 481 + 'eventPHID' => null, 482 + 'availability' => null, 453 483 ); 454 484 $availability_ttl = $max_range; 455 485 }
+26
src/applications/people/storage/PhabricatorUser.php
··· 960 960 } 961 961 962 962 963 + public function getDisplayAvailability() { 964 + $availability = $this->availability; 965 + 966 + $this->assertAttached($availability); 967 + if (!$availability) { 968 + return null; 969 + } 970 + 971 + $busy = PhabricatorCalendarEventInvitee::AVAILABILITY_BUSY; 972 + 973 + return idx($availability, 'availability', $busy); 974 + } 975 + 976 + 977 + public function getAvailabilityEventPHID() { 978 + $availability = $this->availability; 979 + 980 + $this->assertAttached($availability); 981 + if (!$availability) { 982 + return null; 983 + } 984 + 985 + return idx($availability, 'eventPHID'); 986 + } 987 + 988 + 963 989 /** 964 990 * Get cached availability, if present. 965 991 *