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

at recaptime-dev/main 647 lines 19 kB view raw
1<?php 2 3final class PhabricatorCalendarEventViewController 4 extends PhabricatorCalendarController { 5 6 public function shouldAllowPublic() { 7 return true; 8 } 9 10 public function handleRequest(AphrontRequest $request) { 11 $viewer = $request->getViewer(); 12 13 $event = $this->loadEvent(); 14 if (!$event) { 15 return new Aphront404Response(); 16 } 17 18 // If we looked up or generated a stub event, redirect to that event's 19 // canonical URI. 20 $id = $request->getURIData('id'); 21 if ($event->getID() != $id) { 22 $uri = $event->getURI(); 23 return id(new AphrontRedirectResponse())->setURI($uri); 24 } 25 26 $monogram = $event->getMonogram(); 27 $page_title = $monogram.' '.$event->getName(); 28 $crumbs = $this->buildApplicationCrumbs(); 29 30 $start = $event->newStartDateTime() 31 ->newPHPDateTime(); 32 33 $crumbs->addTextCrumb( 34 $start->format('F Y'), 35 '/calendar/query/month/'.$start->format('Y/m/')); 36 37 $crumbs->addTextCrumb( 38 $start->format('D jS'), 39 '/calendar/query/month/'.$start->format('Y/m/d/')); 40 41 $crumbs->addTextCrumb($monogram); 42 $crumbs->setBorder(true); 43 44 $timeline = $this->buildTransactionTimeline( 45 $event, 46 new PhabricatorCalendarEventTransactionQuery()); 47 48 $header = $this->buildHeaderView($event); 49 $subheader = $this->buildSubheaderView($event); 50 $curtain = $this->buildCurtain($event); 51 $details = $this->buildPropertySection($event); 52 $recurring = $this->buildRecurringSection($event); 53 $description = $this->buildDescriptionView($event); 54 55 $comment_view = id(new PhabricatorCalendarEventEditEngine()) 56 ->setViewer($viewer) 57 ->buildEditEngineCommentView($event); 58 59 $timeline->setQuoteRef($monogram); 60 $comment_view->setTransactionTimeline($timeline); 61 62 $details_header = id(new PHUIHeaderView()) 63 ->setHeader(pht('Details')); 64 $recurring_header = $this->buildRecurringHeader($event); 65 66 // NOTE: This is a bit hacky: for imported events, we're just hiding the 67 // comment form without actually preventing comments. Users could still 68 // submit a request to add comments to these events. This isn't really a 69 // major problem since they can't do anything truly bad and there isn't an 70 // easy way to selectively disable this or some other similar behaviors 71 // today, but it would probably be nice to fully disable these 72 // "pseudo-edits" (like commenting and probably subscribing and awarding 73 // tokens) at some point. 74 if ($event->isImportedEvent()) { 75 $comment_view = null; 76 $timeline->setShouldTerminate(true); 77 } 78 79 $view = id(new PHUITwoColumnView()) 80 ->setHeader($header) 81 ->setSubheader($subheader) 82 ->setMainColumn( 83 array( 84 $timeline, 85 $comment_view, 86 )) 87 ->setCurtain($curtain) 88 ->addPropertySection(pht('Description'), $description) 89 ->addPropertySection($recurring_header, $recurring) 90 ->addPropertySection($details_header, $details); 91 92 return $this->newPage() 93 ->setTitle($page_title) 94 ->setCrumbs($crumbs) 95 ->setPageObjectPHIDs(array($event->getPHID())) 96 ->appendChild($view); 97 } 98 99 private function buildHeaderView( 100 PhabricatorCalendarEvent $event) { 101 $viewer = $this->getViewer(); 102 $id = $event->getID(); 103 104 if ($event->getIsCancelled()) { 105 $icon = 'fa-ban'; 106 $color = 'red'; 107 $status = pht('Cancelled'); 108 } else { 109 $icon = 'fa-check'; 110 $color = 'bluegrey'; 111 $status = pht('Active'); 112 } 113 114 $header = id(new PHUIHeaderView()) 115 ->setViewer($viewer) 116 ->setHeader($event->getName()) 117 ->setStatus($icon, $color, $status) 118 ->setPolicyObject($event) 119 ->setHeaderIcon($event->getIcon()); 120 121 if ($event->isImportedEvent()) { 122 $header->addTag( 123 id(new PHUITagView()) 124 ->setType(PHUITagView::TYPE_SHADE) 125 ->setName(pht('Imported')) 126 ->setIcon('fa-download') 127 ->setHref($event->getImportSource()->getURI()) 128 ->setColor(PHUITagView::COLOR_ORANGE)); 129 } 130 131 foreach ($this->buildRSVPActions($event) as $action) { 132 $header->addActionLink($action); 133 } 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 ->setViewer($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 $availability_select->setDisabled($event->isImportedEvent()); 170 $header->addActionLink($availability_select); 171 } 172 173 return $header; 174 } 175 176 private function buildCurtain(PhabricatorCalendarEvent $event) { 177 $viewer = $this->getRequest()->getUser(); 178 $id = $event->getID(); 179 $is_attending = $event->getIsUserAttending($viewer->getPHID()); 180 181 $can_edit = PhabricatorPolicyFilter::hasCapability( 182 $viewer, 183 $event, 184 PhabricatorPolicyCapability::CAN_EDIT); 185 186 $edit_uri = "event/edit/{$id}/"; 187 $edit_uri = $this->getApplicationURI($edit_uri); 188 $is_recurring = $event->getIsRecurring(); 189 $edit_label = pht('Edit Event'); 190 191 $curtain = $this->newCurtainView($event); 192 193 if ($edit_label && $edit_uri) { 194 $curtain->addAction( 195 id(new PhabricatorActionView()) 196 ->setName($edit_label) 197 ->setIcon('fa-pencil') 198 ->setHref($edit_uri) 199 ->setDisabled(!$can_edit) 200 ->setWorkflow(!$can_edit || $is_recurring)); 201 } 202 203 $recurring_uri = "{$edit_uri}page/recurring/"; 204 $can_recurring = $can_edit && !$event->isChildEvent(); 205 206 if ($event->getIsRecurring()) { 207 $recurring_label = pht('Edit Recurrence'); 208 } else { 209 $recurring_label = pht('Make Recurring'); 210 } 211 212 $curtain->addAction( 213 id(new PhabricatorActionView()) 214 ->setName($recurring_label) 215 ->setIcon('fa-repeat') 216 ->setHref($recurring_uri) 217 ->setDisabled(!$can_recurring) 218 ->setWorkflow(true)); 219 220 $can_attend = !$event->isImportedEvent(); 221 222 if ($is_attending) { 223 $curtain->addAction( 224 id(new PhabricatorActionView()) 225 ->setName(pht('Decline Event')) 226 ->setIcon('fa-user-times') 227 ->setHref($this->getApplicationURI("event/join/{$id}/")) 228 ->setDisabled(!$can_attend) 229 ->setWorkflow(true)); 230 } else { 231 $curtain->addAction( 232 id(new PhabricatorActionView()) 233 ->setName(pht('Join Event')) 234 ->setIcon('fa-user-plus') 235 ->setHref($this->getApplicationURI("event/join/{$id}/")) 236 ->setDisabled(!$can_attend) 237 ->setWorkflow(true)); 238 } 239 240 $cancel_uri = $this->getApplicationURI("event/cancel/{$id}/"); 241 $cancel_disabled = !$can_edit; 242 243 $cancel_label = pht('Cancel Event'); 244 $reinstate_label = pht('Reinstate Event'); 245 246 if ($event->getIsCancelled()) { 247 $curtain->addAction( 248 id(new PhabricatorActionView()) 249 ->setName($reinstate_label) 250 ->setIcon('fa-plus') 251 ->setHref($cancel_uri) 252 ->setDisabled($cancel_disabled) 253 ->setWorkflow(true)); 254 } else { 255 $curtain->addAction( 256 id(new PhabricatorActionView()) 257 ->setName($cancel_label) 258 ->setIcon('fa-times') 259 ->setHref($cancel_uri) 260 ->setDisabled($cancel_disabled) 261 ->setWorkflow(true)); 262 } 263 264 $ics_name = $event->getICSFilename(); 265 $export_uri = $this->getApplicationURI("event/export/{$id}/{$ics_name}"); 266 267 $curtain->addAction( 268 id(new PhabricatorActionView()) 269 ->setName(pht('Export as .ics')) 270 ->setIcon('fa-download') 271 ->setHref($export_uri)); 272 273 return $curtain; 274 } 275 276 private function buildPropertySection( 277 PhabricatorCalendarEvent $event) { 278 $viewer = $this->getViewer(); 279 280 $properties = id(new PHUIPropertyListView()) 281 ->setViewer($viewer); 282 283 $invitees = $event->getInvitees(); 284 foreach ($invitees as $key => $invitee) { 285 if ($invitee->isUninvited()) { 286 unset($invitees[$key]); 287 } 288 } 289 290 if ($invitees) { 291 $invitee_list = new PHUIStatusListView(); 292 293 $icon_invited = PHUIStatusItemView::ICON_OPEN; 294 $icon_attending = PHUIStatusItemView::ICON_ACCEPT; 295 $icon_declined = PHUIStatusItemView::ICON_REJECT; 296 297 $status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED; 298 $status_attending = PhabricatorCalendarEventInvitee::STATUS_ATTENDING; 299 $status_declined = PhabricatorCalendarEventInvitee::STATUS_DECLINED; 300 301 $icon_map = array( 302 $status_invited => $icon_invited, 303 $status_attending => $icon_attending, 304 $status_declined => $icon_declined, 305 ); 306 307 $icon_color_map = array( 308 $status_invited => null, 309 $status_attending => 'green', 310 $status_declined => 'red', 311 ); 312 313 $viewer_phid = $viewer->getPHID(); 314 $is_rsvp_invited = $event->isRSVPInvited($viewer_phid); 315 $type_user = PhabricatorPeopleUserPHIDType::TYPECONST; 316 317 $head = array(); 318 $tail = array(); 319 foreach ($invitees as $invitee) { 320 $item = new PHUIStatusItemView(); 321 $invitee_phid = $invitee->getInviteePHID(); 322 $status = $invitee->getStatus(); 323 $target = $viewer->renderHandle($invitee_phid); 324 $is_user = (phid_get_type($invitee_phid) == $type_user); 325 326 if (!$is_user) { 327 $icon = 'fa-users'; 328 $icon_color = 'blue'; 329 } else { 330 $icon = $icon_map[$status]; 331 $icon_color = $icon_color_map[$status]; 332 } 333 334 // Highlight invited groups which you're a member of if you have 335 // not RSVP'd to an event yet. 336 if ($is_rsvp_invited) { 337 if ($invitee_phid != $viewer_phid) { 338 if ($event->hasRSVPAuthority($viewer_phid, $invitee_phid)) { 339 $item->setHighlighted(true); 340 } 341 } 342 } 343 344 $item->setIcon($icon, $icon_color) 345 ->setTarget($target); 346 347 if ($is_user) { 348 $tail[] = $item; 349 } else { 350 $head[] = $item; 351 } 352 } 353 354 foreach (array_merge($head, $tail) as $item) { 355 $invitee_list->addItem($item); 356 } 357 } else { 358 $invitee_list = phutil_tag( 359 'em', 360 array(), 361 pht('None')); 362 } 363 364 if ($event->isImportedEvent()) { 365 $properties->addProperty( 366 pht('Imported By'), 367 pht( 368 '%s from %s', 369 $viewer->renderHandle($event->getImportAuthorPHID()), 370 $viewer->renderHandle($event->getImportSourcePHID()))); 371 } 372 373 $properties->addProperty( 374 pht('Invitees'), 375 $invitee_list); 376 377 $properties->invokeWillRenderEvent(); 378 379 return $properties; 380 } 381 382 private function buildRecurringHeader(PhabricatorCalendarEvent $event) { 383 $viewer = $this->getViewer(); 384 385 if (!$event->getIsRecurring()) { 386 return null; 387 } 388 389 $header = id(new PHUIHeaderView()) 390 ->setHeader(pht('Recurring Event')); 391 392 $sequence = $event->getSequenceIndex(); 393 if ($event->isParentEvent()) { 394 $parent = $event; 395 } else { 396 $parent = $event->getParentEvent(); 397 } 398 399 if ($parent->isValidSequenceIndex($viewer, $sequence + 1)) { 400 $next_uri = $parent->getURI().'/'.($sequence + 1); 401 $has_next = true; 402 } else { 403 $next_uri = null; 404 $has_next = false; 405 } 406 407 if ($sequence) { 408 if ($sequence > 1) { 409 $previous_uri = $parent->getURI().'/'.($sequence - 1); 410 } else { 411 $previous_uri = $parent->getURI(); 412 } 413 $has_previous = true; 414 } else { 415 $has_previous = false; 416 $previous_uri = null; 417 } 418 419 $prev_button = id(new PHUIButtonView()) 420 ->setTag('a') 421 ->setIcon('fa-chevron-left') 422 ->setHref($previous_uri) 423 ->setDisabled(!$has_previous) 424 ->setText(pht('Previous')); 425 426 $next_button = id(new PHUIButtonView()) 427 ->setTag('a') 428 ->setIcon('fa-chevron-right') 429 ->setHref($next_uri) 430 ->setDisabled(!$has_next) 431 ->setText(pht('Next')); 432 433 $header 434 ->addActionLink($next_button) 435 ->addActionLink($prev_button); 436 437 return $header; 438 } 439 440 private function buildRecurringSection(PhabricatorCalendarEvent $event) { 441 $viewer = $this->getViewer(); 442 443 if (!$event->getIsRecurring()) { 444 return null; 445 } 446 447 $properties = id(new PHUIPropertyListView()) 448 ->setViewer($viewer); 449 450 $is_parent = $event->isParentEvent(); 451 if ($is_parent) { 452 $parent_link = null; 453 } else { 454 $parent = $event->getParentEvent(); 455 $parent_link = $viewer 456 ->renderHandle($parent->getPHID()) 457 ->render(); 458 } 459 460 $rrule = $event->newRecurrenceRule(); 461 462 if ($rrule) { 463 $frequency = $rrule->getFrequency(); 464 } else { 465 $frequency = null; 466 } 467 468 switch ($frequency) { 469 case PhutilCalendarRecurrenceRule::FREQUENCY_DAILY: 470 if ($is_parent) { 471 $message = pht('This event repeats every day.'); 472 } else { 473 $message = pht( 474 'This event is an instance of %s, and repeats every day.', 475 $parent_link); 476 } 477 break; 478 case PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY: 479 if ($is_parent) { 480 $message = pht('This event repeats every week.'); 481 } else { 482 $message = pht( 483 'This event is an instance of %s, and repeats every week.', 484 $parent_link); 485 } 486 break; 487 case PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY: 488 if ($is_parent) { 489 $message = pht('This event repeats every month.'); 490 } else { 491 $message = pht( 492 'This event is an instance of %s, and repeats every month.', 493 $parent_link); 494 } 495 break; 496 case PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY: 497 if ($is_parent) { 498 $message = pht('This event repeats every year.'); 499 } else { 500 $message = pht( 501 'This event is an instance of %s, and repeats every year.', 502 $parent_link); 503 } 504 break; 505 } 506 507 $properties->addProperty(pht('Event Series'), $message); 508 509 return $properties; 510 } 511 512 private function buildDescriptionView( 513 PhabricatorCalendarEvent $event) { 514 $viewer = $this->getViewer(); 515 516 $properties = id(new PHUIPropertyListView()) 517 ->setViewer($viewer); 518 519 if (strlen($event->getDescription())) { 520 $description = new PHUIRemarkupView($viewer, $event->getDescription()); 521 $properties->addTextContent($description); 522 return $properties; 523 } 524 525 return null; 526 } 527 528 private function loadEvent() { 529 $request = $this->getRequest(); 530 $viewer = $this->getViewer(); 531 532 $id = $request->getURIData('id'); 533 $sequence = $request->getURIData('sequence'); 534 535 // We're going to figure out which event you're trying to look at. Most of 536 // the time this is simple, but you may be looking at an instance of a 537 // recurring event which we haven't generated an object for. 538 539 // If you are, we're going to generate a "stub" event so we have a real 540 // ID and PHID to work with, since the rest of the infrastructure relies 541 // on these identifiers existing. 542 543 // Load the event identified by ID first. 544 $event = id(new PhabricatorCalendarEventQuery()) 545 ->setViewer($viewer) 546 ->withIDs(array($id)) 547 ->needRSVPs(array($viewer->getPHID())) 548 ->executeOne(); 549 if (!$event) { 550 return null; 551 } 552 553 // If we aren't looking at an instance of this event, this is a completely 554 // normal request and we can just return this event. 555 if (!$sequence) { 556 return $event; 557 } 558 559 // When you view "E123/999", E123 is normally the parent event. However, 560 // you might visit a different instance first instead and then fiddle 561 // with the URI. If the event we're looking at is a child, we are going 562 // to act on the parent instead. 563 if ($event->isChildEvent()) { 564 $event = $event->getParentEvent(); 565 } 566 567 // Try to load the instance. If it already exists, we're all done and 568 // can just return it. 569 $instance = id(new PhabricatorCalendarEventQuery()) 570 ->setViewer($viewer) 571 ->withInstanceSequencePairs( 572 array( 573 array($event->getPHID(), $sequence), 574 )) 575 ->executeOne(); 576 if ($instance) { 577 return $instance; 578 } 579 580 if (!$viewer->isLoggedIn()) { 581 throw new Exception( 582 pht( 583 'This event instance has not been created yet. Log in to create '. 584 'it.')); 585 } 586 587 if (!$event->isValidSequenceIndex($viewer, $sequence)) { 588 return null; 589 } 590 591 return $event->newStub($viewer, $sequence); 592 } 593 594 private function buildSubheaderView(PhabricatorCalendarEvent $event) { 595 $viewer = $this->getViewer(); 596 597 $host_phid = $event->getHostPHID(); 598 599 $handles = $viewer->loadHandles(array($host_phid)); 600 $handle = $handles[$host_phid]; 601 602 $host = $viewer->renderHandle($host_phid); 603 $host = phutil_tag('strong', array(), $host); 604 605 $image_uri = $handles[$host_phid]->getImageURI(); 606 $image_href = $handles[$host_phid]->getURI(); 607 608 $date = $event->renderEventDate($viewer, true); 609 610 $content = pht('Hosted by %s on %s.', $host, $date); 611 612 return id(new PHUIHeadThingView()) 613 ->setImage($image_uri) 614 ->setImageHref($image_href) 615 ->setContent($content); 616 } 617 618 619 private function buildRSVPActions(PhabricatorCalendarEvent $event) { 620 $viewer = $this->getViewer(); 621 $id = $event->getID(); 622 623 $is_pending = $event->isRSVPInvited($viewer->getPHID()); 624 if (!$is_pending) { 625 return array(); 626 } 627 628 $decline_button = id(new PHUIButtonView()) 629 ->setTag('a') 630 ->setIcon('fa-times grey') 631 ->setHref($this->getApplicationURI("/event/decline/{$id}/")) 632 ->setWorkflow(true) 633 ->setDisabled($event->isImportedEvent()) 634 ->setText(pht('Decline')); 635 636 $accept_button = id(new PHUIButtonView()) 637 ->setTag('a') 638 ->setIcon('fa-check green') 639 ->setHref($this->getApplicationURI("/event/accept/{$id}/")) 640 ->setWorkflow(true) 641 ->setDisabled($event->isImportedEvent()) 642 ->setText(pht('Accept')); 643 644 return array($decline_button, $accept_button); 645 } 646 647}