@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

Improve Calendar event behavior for group invites

Summary:
Ref T11816. Projects can be invited to an event, but the UI is currently fairly agnostic about them.

Instead, introduce the idea of "RSVPs", which are basically invites for you as an individual or for any group you're a part of. When we go to check if you're invited, we check for you individually first, then check for any groups you belong to if you haven't already accepted/declined.

On the calendar detail page:

- Show the quick "Join" / "Decline" buttons if any project you're a member of is invited.
- If you're invited, highlight any projects which you're a member of to make that more clear.

On other calendar views:

- If you're invited as part of a project, show the "multiple users" icon.
- If it's just you, continue showing the "add one user" icon.

Test Plan: Viewed month view, day view, detail view. Invited groups and individuals. Invited "Dog Project", accepted invite as user "Dog".

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T11816

Differential Revision: https://secure.phabricator.com/D16868

+178 -17
+37 -6
src/applications/calendar/controller/PhabricatorCalendarEventViewController.php
··· 309 309 $status_declined => 'red', 310 310 ); 311 311 312 + $viewer_phid = $viewer->getPHID(); 313 + $is_rsvp_invited = $event->isRSVPInvited($viewer_phid); 314 + $type_user = PhabricatorPeopleUserPHIDType::TYPECONST; 315 + 316 + $head = array(); 317 + $tail = array(); 312 318 foreach ($invitees as $invitee) { 313 319 $item = new PHUIStatusItemView(); 314 320 $invitee_phid = $invitee->getInviteePHID(); 315 321 $status = $invitee->getStatus(); 316 322 $target = $viewer->renderHandle($invitee_phid); 317 - $icon = $icon_map[$status]; 318 - $icon_color = $icon_color_map[$status]; 323 + $is_user = (phid_get_type($invitee_phid) == $type_user); 324 + 325 + if (!$is_user) { 326 + $icon = 'fa-users'; 327 + $icon_color = 'blue'; 328 + } else { 329 + $icon = $icon_map[$status]; 330 + $icon_color = $icon_color_map[$status]; 331 + } 332 + 333 + // Highlight invited groups which you're a member of if you have 334 + // not RSVP'd to an event yet. 335 + if ($is_rsvp_invited) { 336 + if ($invitee_phid != $viewer_phid) { 337 + if ($event->hasRSVPAuthority($viewer_phid, $invitee_phid)) { 338 + $item->setHighlighted(true); 339 + } 340 + } 341 + } 319 342 320 343 $item->setIcon($icon, $icon_color) 321 344 ->setTarget($target); 345 + 346 + if ($is_user) { 347 + $tail[] = $item; 348 + } else { 349 + $head[] = $item; 350 + } 351 + } 352 + 353 + foreach (array_merge($head, $tail) as $item) { 322 354 $invitee_list->addItem($item); 323 355 } 324 356 } else { ··· 511 543 $event = id(new PhabricatorCalendarEventQuery()) 512 544 ->setViewer($viewer) 513 545 ->withIDs(array($id)) 546 + ->needRSVPs(array($viewer->getPHID())) 514 547 ->executeOne(); 515 548 if (!$event) { 516 549 return null; ··· 586 619 $viewer = $this->getViewer(); 587 620 $id = $event->getID(); 588 621 589 - $invite_status = $event->getUserInviteStatus($viewer->getPHID()); 590 - $status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED; 591 - $is_invite_pending = ($invite_status == $status_invited); 592 - if (!$is_invite_pending) { 622 + $is_pending = $event->isRSVPInvited($viewer->getPHID()); 623 + if (!$is_pending) { 593 624 return array(); 594 625 } 595 626
+1 -1
src/applications/calendar/policyrule/PhabricatorCalendarEventInviteesPolicyRule.php
··· 43 43 if (!isset($this->sourcePHIDs[$viewer_phid])) { 44 44 $source_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( 45 45 $viewer_phid, 46 - PhabricatorProjectMemberOfProjectEdgeType::EDGECONST); 46 + PhabricatorProjectMaterializedMemberEdgeType::EDGECONST); 47 47 $source_phids[] = $viewer_phid; 48 48 $this->sourcePHIDs[$viewer_phid] = $source_phids; 49 49 }
+70
src/applications/calendar/query/PhabricatorCalendarEventQuery.php
··· 20 20 private $utcInitialEpochMin; 21 21 private $utcInitialEpochMax; 22 22 private $isImported; 23 + private $needRSVPs; 23 24 24 25 private $generateGhosts = false; 25 26 ··· 106 107 107 108 public function withIsImported($is_imported) { 108 109 $this->isImported = $is_imported; 110 + return $this; 111 + } 112 + 113 + public function needRSVPs(array $phids) { 114 + $this->needRSVPs = $phids; 109 115 return $this; 110 116 } 111 117 ··· 613 619 } 614 620 615 621 $events = msort($events, 'getStartDateTimeEpoch'); 622 + 623 + if ($this->needRSVPs) { 624 + $rsvp_phids = $this->needRSVPs; 625 + $project_type = PhabricatorProjectProjectPHIDType::TYPECONST; 626 + 627 + $project_phids = array(); 628 + foreach ($events as $event) { 629 + foreach ($event->getInvitees() as $invitee) { 630 + $invitee_phid = $invitee->getInviteePHID(); 631 + if (phid_get_type($invitee_phid) == $project_type) { 632 + $project_phids[] = $invitee_phid; 633 + } 634 + } 635 + } 636 + 637 + if ($project_phids) { 638 + $member_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST; 639 + 640 + $query = id(new PhabricatorEdgeQuery()) 641 + ->withSourcePHIDs($project_phids) 642 + ->withEdgeTypes(array($member_type)) 643 + ->withDestinationPHIDs($rsvp_phids); 644 + 645 + $edges = $query->execute(); 646 + 647 + $project_map = array(); 648 + foreach ($edges as $src => $types) { 649 + foreach ($types as $type => $dsts) { 650 + foreach ($dsts as $dst => $edge) { 651 + $project_map[$dst][] = $src; 652 + } 653 + } 654 + } 655 + } else { 656 + $project_map = array(); 657 + } 658 + 659 + $membership_map = array(); 660 + foreach ($rsvp_phids as $rsvp_phid) { 661 + $membership_map[$rsvp_phid] = array(); 662 + $membership_map[$rsvp_phid][] = $rsvp_phid; 663 + 664 + $project_phids = idx($project_map, $rsvp_phid); 665 + if ($project_phids) { 666 + foreach ($project_phids as $project_phid) { 667 + $membership_map[$rsvp_phid][] = $project_phid; 668 + } 669 + } 670 + } 671 + 672 + foreach ($events as $event) { 673 + $invitees = $event->getInvitees(); 674 + $invitees = mpull($invitees, null, 'getInviteePHID'); 675 + 676 + $rsvp_map = array(); 677 + foreach ($rsvp_phids as $rsvp_phid) { 678 + $membership_phids = $membership_map[$rsvp_phid]; 679 + $rsvps = array_select_keys($invitees, $membership_phids); 680 + $rsvp_map[$rsvp_phid] = $rsvps; 681 + } 682 + 683 + $event->attachRSVPs($rsvp_map); 684 + } 685 + } 616 686 617 687 return $events; 618 688 }
+4 -1
src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php
··· 16 16 } 17 17 18 18 public function newQuery() { 19 - return new PhabricatorCalendarEventQuery(); 19 + $viewer = $this->requireViewer(); 20 + 21 + return id(new PhabricatorCalendarEventQuery()) 22 + ->needRSVPs(array($viewer->getPHID())); 20 23 } 21 24 22 25 protected function shouldShowOrderField() {
+66 -9
src/applications/calendar/storage/PhabricatorCalendarEvent.php
··· 50 50 private $parentEvent = self::ATTACHABLE; 51 51 private $invitees = self::ATTACHABLE; 52 52 private $importSource = self::ATTACHABLE; 53 + private $rsvps = self::ATTACHABLE; 53 54 54 55 private $viewerTimezone; 55 56 ··· 643 644 } 644 645 645 646 if ($viewer->isLoggedIn()) { 646 - $status = $this->getUserInviteStatus($viewer->getPHID()); 647 - switch ($status) { 648 - case PhabricatorCalendarEventInvitee::STATUS_ATTENDING: 649 - return 'fa-check-circle'; 650 - case PhabricatorCalendarEventInvitee::STATUS_INVITED: 651 - return 'fa-user-plus'; 652 - case PhabricatorCalendarEventInvitee::STATUS_DECLINED: 653 - return 'fa-times'; 647 + $viewer_phid = $viewer->getPHID(); 648 + if ($this->isRSVPInvited($viewer_phid)) { 649 + return 'fa-users'; 650 + } else { 651 + $status = $this->getUserInviteStatus($viewer_phid); 652 + switch ($status) { 653 + case PhabricatorCalendarEventInvitee::STATUS_ATTENDING: 654 + return 'fa-check-circle'; 655 + case PhabricatorCalendarEventInvitee::STATUS_INVITED: 656 + return 'fa-user-plus'; 657 + case PhabricatorCalendarEventInvitee::STATUS_DECLINED: 658 + return 'fa-times'; 659 + } 654 660 } 655 661 } 656 662 ··· 671 677 } 672 678 673 679 if ($viewer->isLoggedIn()) { 674 - $status = $this->getUserInviteStatus($viewer->getPHID()); 680 + $viewer_phid = $viewer->getPHID(); 681 + if ($this->isRSVPInvited($viewer_phid)) { 682 + return 'green'; 683 + } 684 + 685 + $status = $this->getUserInviteStatus($viewer_phid); 675 686 switch ($status) { 676 687 case PhabricatorCalendarEventInvitee::STATUS_ATTENDING: 677 688 return 'green'; ··· 1120 1131 1121 1132 return $phids; 1122 1133 } 1134 + 1135 + public function getRSVPs($phid) { 1136 + return $this->assertAttachedKey($this->rsvps, $phid); 1137 + } 1138 + 1139 + public function attachRSVPs(array $rsvps) { 1140 + $this->rsvps = $rsvps; 1141 + return $this; 1142 + } 1143 + 1144 + public function isRSVPInvited($phid) { 1145 + $status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED; 1146 + return ($this->getRSVPStatus($phid) == $status_invited); 1147 + } 1148 + 1149 + public function hasRSVPAuthority($phid, $other_phid) { 1150 + foreach ($this->getRSVPs($phid) as $rsvp) { 1151 + if ($rsvp->getInviteePHID() == $other_phid) { 1152 + return true; 1153 + } 1154 + } 1155 + 1156 + return false; 1157 + } 1158 + 1159 + public function getRSVPStatus($phid) { 1160 + // Check for an individual invitee record first. 1161 + $invitees = $this->invitees; 1162 + $invitees = mpull($invitees, null, 'getInviteePHID'); 1163 + $invitee = idx($invitees, $phid); 1164 + if ($invitee) { 1165 + return $invitee->getStatus(); 1166 + } 1167 + 1168 + // If we don't have one, try to find an invited status for the user's 1169 + // projects. 1170 + $status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED; 1171 + foreach ($this->getRSVPs($phid) as $rsvp) { 1172 + if ($rsvp->getStatus() == $status_invited) { 1173 + return $status_invited; 1174 + } 1175 + } 1176 + 1177 + return PhabricatorCalendarEventInvitee::STATUS_UNINVITED; 1178 + } 1179 + 1123 1180 1124 1181 1125 1182 /* -( Markup Interface )--------------------------------------------------- */