@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<?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}