diff --git a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php --- a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php @@ -165,70 +165,54 @@ // discard anything outside of the time window. $events = $this->getEventsInRange($events); - $enforced_end = null; + $generate_from = $this->rangeBegin; + $generate_until = $this->rangeEnd; foreach ($parents as $key => $event) { - $sequence_start = 0; - $sequence_end = null; - $start = null; - $duration = $event->getDuration(); - $frequency = $event->getFrequencyUnit(); - $modify_key = '+1 '.$frequency; - - if (($this->rangeBegin !== null) && - ($this->rangeBegin > $event->getStartDateTimeEpoch())) { - $max_date = $this->rangeBegin - $duration; - $date = $event->getStartDateTimeEpoch(); - $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer); + $start_date = $this->getRecurrenceWindowStart( + $event, + $generate_from - $duration); - while ($date < $max_date) { - // TODO: optimize this to not loop through all off-screen events - $sequence_start++; - $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer); - $date = $datetime->modify($modify_key)->format('U'); - } + $end_date = $this->getRecurrenceWindowEnd( + $event, + $generate_until); - $start = $this->rangeBegin; - } else { - $start = $event->getStartDateTimeEpoch() - $duration; - } + $limit = $this->getRecurrenceLimit($event, $raw_limit); - $date = $start; - $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer); + $set = $event->newRecurrenceSet(); - // Select the minimum end time we need to generate events until. - $end_times = array(); - if ($this->rangeEnd) { - $end_times[] = $this->rangeEnd; - } + $recurrences = $set->getEventsBetween( + null, + $end_date, + $limit + 1); - if ($event->getUntilDateTimeEpoch()) { - $end_times[] = $event->getUntilDateTimeEpoch(); + // We're generating events from the beginning and then filtering them + // here (instead of only generating events starting at the start date) + // because we need to know the proper sequence indexes to generate ghost + // events. This may change after RDATE support. + if ($start_date) { + $start_epoch = $start_date->getEpoch(); + } else { + $start_epoch = null; } - if ($enforced_end) { - $end_times[] = $enforced_end; - } + foreach ($recurrences as $sequence_index => $sequence_datetime) { + if (!$sequence_index) { + // This is the parent event, which we already have. + continue; + } - if ($end_times) { - $end = min($end_times); - $sequence_end = $sequence_start; - while ($date < $end) { - $sequence_end++; - $datetime->modify($modify_key); - $date = $datetime->format('U'); - if ($sequence_end > $raw_limit + $sequence_start) { - break; + if ($start_epoch) { + if ($sequence_datetime->getEpoch() < $start_epoch) { + continue; } } - } else { - $sequence_end = $raw_limit + $sequence_start; - } - $sequence_start = max(1, $sequence_start); - for ($index = $sequence_start; $index < $sequence_end; $index++) { - $events[] = $event->newGhost($viewer, $index); + $events[] = $event->newGhost( + $viewer, + $sequence_index, + $sequence_datetime); } // NOTE: We're slicing results every time because this makes it cheaper @@ -240,7 +224,7 @@ if (count($events) > $raw_limit) { $events = msort($events, 'getStartDateTimeEpoch'); $events = array_slice($events, 0, $raw_limit, true); - $enforced_end = last($events)->getStartDateTimeEpoch(); + $generate_until = last($events)->getEndDateTimeEpoch(); } } } @@ -525,4 +509,44 @@ return $events; } + private function getRecurrenceWindowStart( + PhabricatorCalendarEvent $event, + $generate_from) { + + if (!$generate_from) { + return null; + } + + return PhutilCalendarAbsoluteDateTime::newFromEpoch($generate_from); + } + + private function getRecurrenceWindowEnd( + PhabricatorCalendarEvent $event, + $generate_until) { + + $end_epochs = array(); + if ($generate_until) { + $end_epochs[] = $generate_until; + } + + $until_epoch = $event->getUntilDateTimeEpoch(); + if ($until_epoch) { + $end_epochs[] = $until_epoch; + } + + if (!$end_epochs) { + return null; + } + + return PhutilCalendarAbsoluteDateTime::newFromEpoch(min($end_epochs)); + } + + private function getRecurrenceLimit( + PhabricatorCalendarEvent $event, + $raw_limit) { + + return $raw_limit; + } + + } 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 @@ -103,7 +103,10 @@ ->applyViewerTimezone($actor); } - private function newChild(PhabricatorUser $actor, $sequence) { + private function newChild( + PhabricatorUser $actor, + $sequence, + PhutilCalendarDateTime $start = null) { if (!$this->isParentEvent()) { throw new Exception( pht( @@ -124,7 +127,7 @@ ->setDateFrom(0) ->setDateTo(0); - return $child->copyFromParent($actor); + return $child->copyFromParent($actor, $start); } protected function readField($field) { @@ -156,7 +159,10 @@ } - public function copyFromParent(PhabricatorUser $actor) { + public function copyFromParent( + PhabricatorUser $actor, + PhutilCalendarDateTime $start = null) { + if (!$this->isChildEvent()) { throw new Exception( pht( @@ -176,11 +182,18 @@ ->setDescription($parent->getDescription()); $sequence = $this->getSequenceIndex(); - $start_datetime = $parent->newSequenceIndexDateTime($sequence); - if (!$start_datetime) { - throw new Exception( - "Sequence {$sequence} does not exist for event!"); + if ($start) { + $start_datetime = $start; + } else { + $start_datetime = $parent->newSequenceIndexDateTime($sequence); + + if (!$start_datetime) { + throw new Exception( + pht( + 'Sequence "%s" is not valid for event!', + $sequence)); + } } $duration = $parent->newDuration(); @@ -225,8 +238,12 @@ return $stub; } - public function newGhost(PhabricatorUser $actor, $sequence) { - $ghost = $this->newChild($actor, $sequence); + public function newGhost( + PhabricatorUser $actor, + $sequence, + PhutilCalendarDateTime $start = null) { + + $ghost = $this->newChild($actor, $sequence, $start); $ghost ->setIsGhostEvent(true)