diff --git a/resources/builtin/conpherence.png b/resources/builtin/conpherence.png new file mode 100644 index 0000000000..78bb8bdc05 Binary files /dev/null and b/resources/builtin/conpherence.png differ diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 78ec532eef..badb3d06fb 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,10 +7,10 @@ */ return array( 'names' => array( - 'conpherence.pkg.css' => 'b1547973', + 'conpherence.pkg.css' => '4601645d', 'conpherence.pkg.js' => '11f3e07e', - 'core.pkg.css' => 'cfc3eabe', - 'core.pkg.js' => '975d6a27', + 'core.pkg.css' => 'de918edf', + 'core.pkg.js' => '30185d95', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => 'e1d704ce', 'differential.pkg.js' => '634399e9', @@ -35,9 +35,9 @@ return array( 'rsrc/css/application/almanac/almanac.css' => 'dbb9b3af', 'rsrc/css/application/auth/auth.css' => '0877ed6e', 'rsrc/css/application/base/main-menu-view.css' => 'f03e17be', - 'rsrc/css/application/base/notification-menu.css' => 'b3ab500d', + 'rsrc/css/application/base/notification-menu.css' => '1e055865', 'rsrc/css/application/base/phabricator-application-launch-view.css' => '95351601', - 'rsrc/css/application/base/phui-theme.css' => '027ba77e', + 'rsrc/css/application/base/phui-theme.css' => '798c69b8', 'rsrc/css/application/base/standard-page-view.css' => '79176f5a', 'rsrc/css/application/chatlog/chatlog.css' => 'd295b020', 'rsrc/css/application/conduit/conduit-api.css' => '7bc725c4', @@ -46,11 +46,11 @@ return array( 'rsrc/css/application/config/config-template.css' => '8f18fa41', 'rsrc/css/application/config/setup-issue.css' => 'f794cfc3', 'rsrc/css/application/config/unhandled-exception.css' => '4c96257a', - 'rsrc/css/application/conpherence/durable-column.css' => 'af11a2a7', - 'rsrc/css/application/conpherence/header-pane.css' => '517de9fe', - 'rsrc/css/application/conpherence/menu.css' => '78c7b811', + 'rsrc/css/application/conpherence/durable-column.css' => '44bcaa19', + 'rsrc/css/application/conpherence/header-pane.css' => '20a7028c', + 'rsrc/css/application/conpherence/menu.css' => '4f51db5a', 'rsrc/css/application/conpherence/message-pane.css' => '0d7dff02', - 'rsrc/css/application/conpherence/notification.css' => '6cdcc253', + 'rsrc/css/application/conpherence/notification.css' => '965db05b', 'rsrc/css/application/conpherence/participant-pane.css' => '7bba0b56', 'rsrc/css/application/conpherence/transaction.css' => '46253e19', 'rsrc/css/application/conpherence/update.css' => '53bc527a', @@ -154,7 +154,7 @@ return array( 'rsrc/css/phui/phui-object-item-list-view.css' => '87278fa0', 'rsrc/css/phui/phui-pager.css' => 'bea33d23', 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', - 'rsrc/css/phui/phui-profile-menu.css' => '8a3fc181', + 'rsrc/css/phui/phui-profile-menu.css' => '4768721a', 'rsrc/css/phui/phui-property-list-view.css' => '6d8e58ac', 'rsrc/css/phui/phui-remarkup-preview.css' => '1a8f2591', 'rsrc/css/phui/phui-segment-bar-view.css' => '46342871', @@ -428,7 +428,7 @@ return array( 'rsrc/js/application/aphlict/Aphlict.js' => '5359e785', 'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => '49e20786', 'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => 'fb20ac8d', - 'rsrc/js/application/aphlict/behavior-aphlict-status.js' => 'ea681761', + 'rsrc/js/application/aphlict/behavior-aphlict-status.js' => '5e2634b9', 'rsrc/js/application/aphlict/behavior-desktop-notifications-control.js' => 'edd1ba66', 'rsrc/js/application/auth/behavior-persona-login.js' => '9414ff18', 'rsrc/js/application/calendar/behavior-day-view.js' => '4b3c4443', @@ -438,7 +438,7 @@ return array( 'rsrc/js/application/config/behavior-reorder-fields.js' => 'b6993408', 'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '01774ab2', 'rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js' => 'cf86d16a', - 'rsrc/js/application/conpherence/behavior-durable-column.js' => 'e287689f', + 'rsrc/js/application/conpherence/behavior-durable-column.js' => 'c5238acb', 'rsrc/js/application/conpherence/behavior-menu.js' => '9eb55204', 'rsrc/js/application/conpherence/behavior-participant-pane.js' => '8604caa8', 'rsrc/js/application/conpherence/behavior-pontificate.js' => 'f2e58483', @@ -547,7 +547,6 @@ return array( 'rsrc/js/core/behavior-autofocus.js' => '7319e029', 'rsrc/js/core/behavior-badge-view.js' => '8ff5e24c', 'rsrc/js/core/behavior-choose-control.js' => '327a00d1', - 'rsrc/js/core/behavior-crop.js' => 'fa0f4fc2', 'rsrc/js/core/behavior-dark-console.js' => 'f411b6ae', 'rsrc/js/core/behavior-detect-timezone.js' => '4c193c96', 'rsrc/js/core/behavior-device.js' => 'bb1dd507', @@ -618,11 +617,11 @@ return array( 'conduit-api-css' => '7bc725c4', 'config-options-css' => '0ede4c9b', 'config-page-css' => '8798e14f', - 'conpherence-durable-column-view' => 'af11a2a7', - 'conpherence-header-pane-css' => '517de9fe', - 'conpherence-menu-css' => '78c7b811', + 'conpherence-durable-column-view' => '44bcaa19', + 'conpherence-header-pane-css' => '20a7028c', + 'conpherence-menu-css' => '4f51db5a', 'conpherence-message-pane-css' => '0d7dff02', - 'conpherence-notification-css' => '6cdcc253', + 'conpherence-notification-css' => '965db05b', 'conpherence-participant-pane-css' => '7bba0b56', 'conpherence-thread-manager' => '01774ab2', 'conpherence-transaction-css' => '46253e19', @@ -653,9 +652,8 @@ return array( 'javelin-behavior' => '61cbc29a', 'javelin-behavior-aphlict-dropdown' => '49e20786', 'javelin-behavior-aphlict-listen' => 'fb20ac8d', - 'javelin-behavior-aphlict-status' => 'ea681761', + 'javelin-behavior-aphlict-status' => '5e2634b9', 'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884', - 'javelin-behavior-aphront-crop' => 'fa0f4fc2', 'javelin-behavior-aphront-drag-and-drop-textarea' => '484a6e22', 'javelin-behavior-aphront-form-disable-on-submit' => '5c54cbf3', 'javelin-behavior-aphront-more' => 'a80d0378', @@ -699,7 +697,7 @@ return array( 'javelin-behavior-diffusion-pull-lastmodified' => 'f01586dc', 'javelin-behavior-doorkeeper-tag' => 'e5822781', 'javelin-behavior-drydock-live-operation-status' => '901935ef', - 'javelin-behavior-durable-column' => 'e287689f', + 'javelin-behavior-durable-column' => 'c5238acb', 'javelin-behavior-editengine-reorder-configs' => 'd7a74243', 'javelin-behavior-editengine-reorder-fields' => 'b59e1e96', 'javelin-behavior-error-log' => '6882e80a', @@ -860,7 +858,7 @@ return array( 'phabricator-nav-view-css' => 'b29426e9', 'phabricator-notification' => 'ccf1cbf8', 'phabricator-notification-css' => '3f6c89c9', - 'phabricator-notification-menu-css' => 'b3ab500d', + 'phabricator-notification-menu-css' => '1e055865', 'phabricator-object-selector-css' => '85ee8ce6', 'phabricator-phtize' => 'd254d646', 'phabricator-prefab' => 'cfd23f37', @@ -932,14 +930,14 @@ return array( 'phui-object-item-list-view-css' => '87278fa0', 'phui-pager-css' => 'bea33d23', 'phui-pinboard-view-css' => '2495140e', - 'phui-profile-menu-css' => '8a3fc181', + 'phui-profile-menu-css' => '4768721a', 'phui-property-list-view-css' => '6d8e58ac', 'phui-remarkup-preview-css' => '1a8f2591', 'phui-segment-bar-view-css' => '46342871', 'phui-spacing-css' => '042804d6', 'phui-status-list-view-css' => 'd5263e49', 'phui-tag-view-css' => '6bbd83e2', - 'phui-theme-css' => '027ba77e', + 'phui-theme-css' => '798c69b8', 'phui-timeline-view-css' => 'bc523970', 'phui-two-column-view-css' => 'fcfbe347', 'phui-workboard-color-css' => 'ac6fe6a7', @@ -1424,6 +1422,12 @@ return array( 'javelin-stratcom', 'javelin-dom', ), + '5e2634b9' => array( + 'javelin-behavior', + 'javelin-aphlict', + 'phabricator-phtize', + 'javelin-dom', + ), '5e9f347c' => array( 'javelin-behavior', 'multirow-row-manager', @@ -1957,6 +1961,16 @@ return array( 'javelin-install', 'javelin-dom', ), + 'c5238acb' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-behavior-device', + 'javelin-scrollbar', + 'javelin-quicksand', + 'phabricator-keyboard-shortcut', + 'conpherence-thread-manager', + ), 'c587b80f' => array( 'javelin-install', ), @@ -2109,16 +2123,6 @@ return array( 'javelin-stratcom', 'javelin-dom', ), - 'e287689f' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-behavior-device', - 'javelin-scrollbar', - 'javelin-quicksand', - 'phabricator-keyboard-shortcut', - 'conpherence-thread-manager', - ), 'e292eaf4' => array( 'javelin-install', ), @@ -2169,12 +2173,6 @@ return array( 'javelin-dom', 'phabricator-draggable-list', ), - 'ea681761' => array( - 'javelin-behavior', - 'javelin-aphlict', - 'phabricator-phtize', - 'javelin-dom', - ), 'edd1ba66' => array( 'javelin-behavior', 'javelin-stratcom', @@ -2243,12 +2241,6 @@ return array( 'javelin-util', 'phabricator-busy', ), - 'fa0f4fc2' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-vector', - 'javelin-magical-init', - ), 'fb20ac8d' => array( 'javelin-behavior', 'javelin-aphlict', diff --git a/resources/sql/autopatches/20160715.event.03.allday.php b/resources/sql/autopatches/20160715.event.03.allday.php index 8bc3ffe568..4c2d73a368 100644 --- a/resources/sql/autopatches/20160715.event.03.allday.php +++ b/resources/sql/autopatches/20160715.event.03.allday.php @@ -1,52 +1,3 @@ establishConnection('w'); - -// Previously, "All Day" events were stored with a start and end date set to -// the earliest possible start and end seconds for the corresponding days. We -// now store all day events with their "date" epochs as UTC, separate from -// individual event times. -$zone_min = new DateTimeZone('Pacific/Midway'); -$zone_max = new DateTimeZone('Pacific/Kiritimati'); -$zone_utc = new DateTimeZone('UTC'); - -foreach (new LiskMigrationIterator($table) as $event) { - // If this event has already migrated, skip it. - if ($event->getAllDayDateFrom()) { - continue; - } - - $is_all_day = $event->getIsAllDay(); - - $epoch_min = $event->getDateFrom(); - $epoch_max = $event->getDateTo(); - - $date_min = new DateTime('@'.$epoch_min); - $date_max = new DateTime('@'.$epoch_max); - - if ($is_all_day) { - $date_min->setTimeZone($zone_min); - $date_min->modify('+2 days'); - $date_max->setTimeZone($zone_max); - $date_max->modify('-2 days'); - } else { - $date_min->setTimeZone($zone_utc); - $date_max->setTimeZone($zone_utc); - } - - $string_min = $date_min->format('Y-m-d'); - $string_max = $date_max->format('Y-m-d 23:59:00'); - - $allday_min = id(new DateTime($string_min, $zone_utc))->format('U'); - $allday_max = id(new DateTime($string_max, $zone_utc))->format('U'); - - queryfx( - $conn, - 'UPDATE %T SET allDayDateFrom = %d, allDayDateTo = %d - WHERE id = %d', - $table->getTableName(), - $allday_min, - $allday_max, - $event->getID()); -} +// This migration was replaced by "20161004.cal.01.noepoch.php". diff --git a/resources/sql/autopatches/20161003.cal.01.utcepoch.sql b/resources/sql/autopatches/20161003.cal.01.utcepoch.sql new file mode 100644 index 0000000000..0e1aa0d534 --- /dev/null +++ b/resources/sql/autopatches/20161003.cal.01.utcepoch.sql @@ -0,0 +1,8 @@ +ALTER TABLE {$NAMESPACE}_calendar.calendar_event + ADD utcInitialEpoch INT UNSIGNED NOT NULL; + +ALTER TABLE {$NAMESPACE}_calendar.calendar_event + ADD utcUntilEpoch INT UNSIGNED; + +ALTER TABLE {$NAMESPACE}_calendar.calendar_event + ADD utcInstanceEpoch INT UNSIGNED; diff --git a/resources/sql/autopatches/20161003.cal.02.parameters.sql b/resources/sql/autopatches/20161003.cal.02.parameters.sql new file mode 100644 index 0000000000..9fd3783a5b --- /dev/null +++ b/resources/sql/autopatches/20161003.cal.02.parameters.sql @@ -0,0 +1,5 @@ +ALTER TABLE {$NAMESPACE}_calendar.calendar_event + ADD parameters LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}; + +UPDATE {$NAMESPACE}_calendar.calendar_event + SET parameters = '{}' WHERE parameters = ''; diff --git a/resources/sql/autopatches/20161004.cal.01.noepoch.php b/resources/sql/autopatches/20161004.cal.01.noepoch.php new file mode 100644 index 0000000000..6013376062 --- /dev/null +++ b/resources/sql/autopatches/20161004.cal.01.noepoch.php @@ -0,0 +1,125 @@ +establishConnection('w'); +$table_name = 'calendar_event'; + +// Long ago, "All Day" events were stored with a start and end date set to +// the earliest possible start and end seconds for the corresponding days. We +// then moved to store all day events with their "date" epochs as UTC, separate +// from individual event times. Both systems were later replaced with use of +// CalendarDateTime. +$zone_min = new DateTimeZone('Pacific/Midway'); +$zone_max = new DateTimeZone('Pacific/Kiritimati'); +$zone_utc = new DateTimeZone('UTC'); + +foreach (new LiskRawMigrationIterator($conn, $table_name) as $row) { + $parameters = phutil_json_decode($row['parameters']); + if (isset($parameters['startDateTime'])) { + // This event has already been migrated. + continue; + } + + $is_all_day = $row['isAllDay']; + + if (empty($row['allDayDateFrom'])) { + // No "allDayDateFrom" means this is an old event which was never migrated + // by the earlier "20160715.event.03.allday.php" migration. The dateFrom + // and dateTo will be minimum and maximum earthly seconds for the event. We + // convert them to UTC if they were in extreme timezones. + $epoch_min = $row['dateFrom']; + $epoch_max = $row['dateTo']; + + if ($is_all_day) { + $date_min = new DateTime('@'.$epoch_min); + $date_max = new DateTime('@'.$epoch_max); + + $date_min->setTimeZone($zone_min); + $date_min->modify('+2 days'); + $date_max->setTimeZone($zone_max); + $date_max->modify('-2 days'); + + $string_min = $date_min->format('Y-m-d'); + $string_max = $date_max->format('Y-m-d 23:59:00'); + + $utc_min = id(new DateTime($string_min, $zone_utc))->format('U'); + $utc_max = id(new DateTime($string_max, $zone_utc))->format('U'); + } else { + $utc_min = $epoch_min; + $utc_max = $epoch_max; + } + } else { + // This is an event which was migrated already. We can pick the correct + // epoch timestamps based on the "isAllDay" flag. + if ($is_all_day) { + $utc_min = $row['allDayDateFrom']; + $utc_max = $row['allDayDateTo']; + } else { + $utc_min = $row['dateFrom']; + $utc_max = $row['dateTo']; + } + } + + $utc_until = $row['recurrenceEndDate']; + + $start_datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch($utc_min); + if ($is_all_day) { + $start_datetime->setIsAllDay(true); + } + + $end_datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch($utc_max); + if ($is_all_day) { + $end_datetime->setIsAllDay(true); + } + + if ($utc_until) { + $until_datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch($utc_until); + } else { + $until_datetime = null; + } + + $parameters['startDateTime'] = $start_datetime->toDictionary(); + $parameters['endDateTime'] = $end_datetime->toDictionary(); + if ($until_datetime) { + $parameters['untilDateTime'] = $until_datetime->toDictionary(); + } + + queryfx( + $conn, + 'UPDATE %T SET parameters = %s WHERE id = %d', + $table_name, + phutil_json_encode($parameters), + $row['id']); +} + +// Generate UTC epochs for all events. We can't readily do this one at a +// time because instance UTC epochs rely on having the parent event. +$viewer = PhabricatorUser::getOmnipotentUser(); + +$all_events = id(new PhabricatorCalendarEventQuery()) + ->setViewer($viewer) + ->execute(); +foreach ($all_events as $event) { + if ($event->getUTCInitialEpoch()) { + // Already migrated. + continue; + } + + try { + $event->updateUTCEpochs(); + } catch (Exception $ex) { + continue; + } + + queryfx( + $conn, + 'UPDATE %T SET + utcInitialEpoch = %d, + utcUntilEpoch = %nd, + utcInstanceEpoch = %nd WHERE id = %d', + $table_name, + $event->getUTCInitialEpoch(), + $event->getUTCUntilEpoch(), + $event->getUTCInstanceEpoch(), + $event->getID()); +} diff --git a/resources/sql/autopatches/20161005.cal.01.rrules.php b/resources/sql/autopatches/20161005.cal.01.rrules.php new file mode 100644 index 0000000000..e2e61ba30a --- /dev/null +++ b/resources/sql/autopatches/20161005.cal.01.rrules.php @@ -0,0 +1,44 @@ +establishConnection('w'); +$table_name = 'calendar_event'; + +foreach (new LiskRawMigrationIterator($conn, $table_name) as $row) { + $parameters = phutil_json_decode($row['parameters']); + if (isset($parameters['recurrenceRule'])) { + // This event has already been migrated. + continue; + } + + if (!$row['isRecurring']) { + continue; + } + + $old_rule = $row['recurrenceFrequency']; + if (!$old_rule) { + continue; + } + + try { + $frequency = phutil_json_decode($old_rule); + if ($frequency) { + $frequency_rule = $frequency['rule']; + $frequency_rule = phutil_utf8_strtoupper($frequency_rule); + + $rrule = id(new PhutilCalendarRecurrenceRule()) + ->setFrequency($frequency_rule); + } + } catch (Exception $ex) { + continue; + } + + $parameters['recurrenceRule'] = $rrule->toDictionary(); + + queryfx( + $conn, + 'UPDATE %T SET parameters = %s WHERE id = %d', + $table_name, + phutil_json_encode($parameters), + $row['id']); +} diff --git a/resources/sql/autopatches/20161005.cal.02.export.sql b/resources/sql/autopatches/20161005.cal.02.export.sql new file mode 100644 index 0000000000..bd1c031165 --- /dev/null +++ b/resources/sql/autopatches/20161005.cal.02.export.sql @@ -0,0 +1,14 @@ +CREATE TABLE {$NAMESPACE}_calendar.calendar_export ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + name LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + authorPHID VARBINARY(64) NOT NULL, + policyMode VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT}, + queryKey VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT}, + secretKey BINARY(20) NOT NULL, + isDisabled BOOL NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + KEY `key_author` (authorPHID), + UNIQUE KEY `key_secret` (secretKey) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20161005.cal.03.exportxaction.sql b/resources/sql/autopatches/20161005.cal.03.exportxaction.sql new file mode 100644 index 0000000000..1161534015 --- /dev/null +++ b/resources/sql/autopatches/20161005.cal.03.exportxaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_calendar.calendar_exporttransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64) DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL, + oldValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + newValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + contentSource LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + metadata LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20161005.conpherence.image.1.sql b/resources/sql/autopatches/20161005.conpherence.image.1.sql new file mode 100644 index 0000000000..17950986b8 --- /dev/null +++ b/resources/sql/autopatches/20161005.conpherence.image.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_conpherence.conpherence_thread + ADD profileImagePHID VARBINARY(64); diff --git a/resources/sql/autopatches/20161005.conpherence.image.2.php b/resources/sql/autopatches/20161005.conpherence.image.2.php new file mode 100644 index 0000000000..4345cdd08b --- /dev/null +++ b/resources/sql/autopatches/20161005.conpherence.image.2.php @@ -0,0 +1,34 @@ +establishConnection('w'); +$table_name = 'conpherence_thread'; + +foreach (new LiskRawMigrationIterator($conn, $table_name) as $row) { + + $images = phutil_json_decode($row['imagePHIDs']); + if (!$images) { + continue; + } + + $file_phid = idx($images, 'original'); + + $file = id(new PhabricatorFileQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs(array($file_phid)) + ->executeOne(); + + $xform = PhabricatorFileTransform::getTransformByKey( + PhabricatorFileThumbnailTransform::TRANSFORM_PROFILE); + $xformed = $xform->executeTransform($file); + $new_phid = $xformed->getPHID(); + + queryfx( + $conn, + 'UPDATE %T SET profileImagePHID = %s WHERE id = %d', + $table->getTableName(), + $new_phid, + $row['id']); +} diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index f4a7ba2fd3..085e86c84b 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -292,7 +292,6 @@ phutil_register_library_map(array( 'ConpherenceEditor' => 'applications/conpherence/editor/ConpherenceEditor.php', 'ConpherenceFormDragAndDropUploadControl' => 'applications/conpherence/view/ConpherenceFormDragAndDropUploadControl.php', 'ConpherenceFulltextQuery' => 'applications/conpherence/query/ConpherenceFulltextQuery.php', - 'ConpherenceImageData' => 'applications/conpherence/constants/ConpherenceImageData.php', 'ConpherenceIndex' => 'applications/conpherence/storage/ConpherenceIndex.php', 'ConpherenceLayoutView' => 'applications/conpherence/view/ConpherenceLayoutView.php', 'ConpherenceListController' => 'applications/conpherence/controller/ConpherenceListController.php', @@ -305,11 +304,11 @@ phutil_register_library_map(array( 'ConpherenceParticipantQuery' => 'applications/conpherence/query/ConpherenceParticipantQuery.php', 'ConpherenceParticipantView' => 'applications/conpherence/view/ConpherenceParticipantView.php', 'ConpherenceParticipationStatus' => 'applications/conpherence/constants/ConpherenceParticipationStatus.php', - 'ConpherencePicCropControl' => 'applications/conpherence/view/ConpherencePicCropControl.php', 'ConpherenceQueryThreadConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceQueryThreadConduitAPIMethod.php', 'ConpherenceQueryTransactionConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceQueryTransactionConduitAPIMethod.php', 'ConpherenceReplyHandler' => 'applications/conpherence/mail/ConpherenceReplyHandler.php', 'ConpherenceRoomListController' => 'applications/conpherence/controller/ConpherenceRoomListController.php', + 'ConpherenceRoomPictureController' => 'applications/conpherence/controller/ConpherenceRoomPictureController.php', 'ConpherenceRoomTestCase' => 'applications/conpherence/__tests__/ConpherenceRoomTestCase.php', 'ConpherenceSchemaSpec' => 'applications/conpherence/storage/ConpherenceSchemaSpec.php', 'ConpherenceTestCase' => 'applications/conpherence/__tests__/ConpherenceTestCase.php', @@ -2076,8 +2075,27 @@ phutil_register_library_map(array( 'PhabricatorCalendarEventTransactionType' => 'applications/calendar/xaction/PhabricatorCalendarEventTransactionType.php', 'PhabricatorCalendarEventUntilDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventUntilDateTransaction.php', 'PhabricatorCalendarEventViewController' => 'applications/calendar/controller/PhabricatorCalendarEventViewController.php', + 'PhabricatorCalendarExport' => 'applications/calendar/storage/PhabricatorCalendarExport.php', + 'PhabricatorCalendarExportDisableController' => 'applications/calendar/controller/PhabricatorCalendarExportDisableController.php', + 'PhabricatorCalendarExportDisableTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportDisableTransaction.php', + 'PhabricatorCalendarExportEditController' => 'applications/calendar/controller/PhabricatorCalendarExportEditController.php', + 'PhabricatorCalendarExportEditEngine' => 'applications/calendar/editor/PhabricatorCalendarExportEditEngine.php', + 'PhabricatorCalendarExportEditor' => 'applications/calendar/editor/PhabricatorCalendarExportEditor.php', + 'PhabricatorCalendarExportICSController' => 'applications/calendar/controller/PhabricatorCalendarExportICSController.php', + 'PhabricatorCalendarExportListController' => 'applications/calendar/controller/PhabricatorCalendarExportListController.php', + 'PhabricatorCalendarExportModeTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportModeTransaction.php', + 'PhabricatorCalendarExportNameTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportNameTransaction.php', + 'PhabricatorCalendarExportPHIDType' => 'applications/calendar/phid/PhabricatorCalendarExportPHIDType.php', + 'PhabricatorCalendarExportQuery' => 'applications/calendar/query/PhabricatorCalendarExportQuery.php', + 'PhabricatorCalendarExportQueryKeyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportQueryKeyTransaction.php', + 'PhabricatorCalendarExportSearchEngine' => 'applications/calendar/query/PhabricatorCalendarExportSearchEngine.php', + 'PhabricatorCalendarExportTransaction' => 'applications/calendar/storage/PhabricatorCalendarExportTransaction.php', + 'PhabricatorCalendarExportTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarExportTransactionQuery.php', + 'PhabricatorCalendarExportTransactionType' => 'applications/calendar/xaction/PhabricatorCalendarExportTransactionType.php', + 'PhabricatorCalendarExportViewController' => 'applications/calendar/controller/PhabricatorCalendarExportViewController.php', 'PhabricatorCalendarHoliday' => 'applications/calendar/storage/PhabricatorCalendarHoliday.php', 'PhabricatorCalendarHolidayTestCase' => 'applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php', + 'PhabricatorCalendarICSWriter' => 'applications/calendar/util/PhabricatorCalendarICSWriter.php', 'PhabricatorCalendarIconSet' => 'applications/calendar/icon/PhabricatorCalendarIconSet.php', 'PhabricatorCalendarRemarkupRule' => 'applications/calendar/remarkup/PhabricatorCalendarRemarkupRule.php', 'PhabricatorCalendarReplyHandler' => 'applications/calendar/mail/PhabricatorCalendarReplyHandler.php', @@ -4769,7 +4787,6 @@ phutil_register_library_map(array( 'ConpherenceEditor' => 'PhabricatorApplicationTransactionEditor', 'ConpherenceFormDragAndDropUploadControl' => 'AphrontFormControl', 'ConpherenceFulltextQuery' => 'PhabricatorOffsetPagedQuery', - 'ConpherenceImageData' => 'ConpherenceConstants', 'ConpherenceIndex' => 'ConpherenceDAO', 'ConpherenceLayoutView' => 'AphrontTagView', 'ConpherenceListController' => 'ConpherenceController', @@ -4782,11 +4799,11 @@ phutil_register_library_map(array( 'ConpherenceParticipantQuery' => 'PhabricatorOffsetPagedQuery', 'ConpherenceParticipantView' => 'AphrontView', 'ConpherenceParticipationStatus' => 'ConpherenceConstants', - 'ConpherencePicCropControl' => 'AphrontFormControl', 'ConpherenceQueryThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod', 'ConpherenceQueryTransactionConduitAPIMethod' => 'ConpherenceConduitAPIMethod', 'ConpherenceReplyHandler' => 'PhabricatorMailReplyHandler', 'ConpherenceRoomListController' => 'ConpherenceController', + 'ConpherenceRoomPictureController' => 'ConpherenceController', 'ConpherenceRoomTestCase' => 'ConpherenceTestCase', 'ConpherenceSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'ConpherenceTestCase' => 'PhabricatorTestCase', @@ -6827,8 +6844,32 @@ phutil_register_library_map(array( 'PhabricatorCalendarEventTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorCalendarEventUntilDateTransaction' => 'PhabricatorCalendarEventDateTransaction', 'PhabricatorCalendarEventViewController' => 'PhabricatorCalendarController', + 'PhabricatorCalendarExport' => array( + 'PhabricatorCalendarDAO', + 'PhabricatorPolicyInterface', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorDestructibleInterface', + ), + 'PhabricatorCalendarExportDisableController' => 'PhabricatorCalendarController', + 'PhabricatorCalendarExportDisableTransaction' => 'PhabricatorCalendarExportTransactionType', + 'PhabricatorCalendarExportEditController' => 'PhabricatorCalendarController', + 'PhabricatorCalendarExportEditEngine' => 'PhabricatorEditEngine', + 'PhabricatorCalendarExportEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhabricatorCalendarExportICSController' => 'PhabricatorCalendarController', + 'PhabricatorCalendarExportListController' => 'PhabricatorCalendarController', + 'PhabricatorCalendarExportModeTransaction' => 'PhabricatorCalendarExportTransactionType', + 'PhabricatorCalendarExportNameTransaction' => 'PhabricatorCalendarExportTransactionType', + 'PhabricatorCalendarExportPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorCalendarExportQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorCalendarExportQueryKeyTransaction' => 'PhabricatorCalendarExportTransactionType', + 'PhabricatorCalendarExportSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'PhabricatorCalendarExportTransaction' => 'PhabricatorModularTransaction', + 'PhabricatorCalendarExportTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorCalendarExportTransactionType' => 'PhabricatorModularTransactionType', + 'PhabricatorCalendarExportViewController' => 'PhabricatorCalendarController', 'PhabricatorCalendarHoliday' => 'PhabricatorCalendarDAO', 'PhabricatorCalendarHolidayTestCase' => 'PhabricatorTestCase', + 'PhabricatorCalendarICSWriter' => 'Phobject', 'PhabricatorCalendarIconSet' => 'PhabricatorIconSet', 'PhabricatorCalendarRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PhabricatorCalendarReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', diff --git a/src/applications/calendar/application/PhabricatorCalendarApplication.php b/src/applications/calendar/application/PhabricatorCalendarApplication.php index e948d70198..208fc83ae9 100644 --- a/src/applications/calendar/application/PhabricatorCalendarApplication.php +++ b/src/applications/calendar/application/PhabricatorCalendarApplication.php @@ -62,6 +62,19 @@ final class PhabricatorCalendarApplication extends PhabricatorApplication { 'export/(?P[1-9]\d*)/(?P[^/]*)' => 'PhabricatorCalendarEventExportController', ), + 'export/' => array( + $this->getQueryRoutePattern() + => 'PhabricatorCalendarExportListController', + $this->getEditRoutePattern('edit/') + => 'PhabricatorCalendarExportEditController', + '(?P[1-9]\d*)/' + => 'PhabricatorCalendarExportViewController', + 'ics/(?P[^/]+)/(?P[^/]*)' + => 'PhabricatorCalendarExportICSController', + 'disable/(?P[1-9]\d*)/' + => 'PhabricatorCalendarExportDisableController', + + ), ), ); } diff --git a/src/applications/calendar/controller/PhabricatorCalendarController.php b/src/applications/calendar/controller/PhabricatorCalendarController.php index 01ea367aeb..36a9bfbbe9 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarController.php @@ -2,4 +2,20 @@ abstract class PhabricatorCalendarController extends PhabricatorController { + protected function newICSResponse( + PhabricatorUser $viewer, + $file_name, + array $events) { + + $ics_data = id(new PhabricatorCalendarICSWriter()) + ->setViewer($viewer) + ->setEvents($events) + ->writeICSDocument(); + + return id(new AphrontFileResponse()) + ->setDownload($file_name) + ->setMimeType('text/calendar') + ->setContent($ics_data); + } + } diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventExportController.php b/src/applications/calendar/controller/PhabricatorCalendarEventExportController.php index 8163e4abeb..5e29479169 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventExportController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventExportController.php @@ -19,22 +19,16 @@ final class PhabricatorCalendarEventExportController return new Aphront404Response(); } - $file_name = $event->getICSFilename(); - $event_node = $event->newIntermediateEventNode($viewer); + if ($event->isChildEvent()) { + $target = $event->getParentEvent(); + } else { + $target = $event; + } - $document_node = id(new PhutilCalendarDocumentNode()) - ->appendChild($event_node); - - $root_node = id(new PhutilCalendarRootNode()) - ->appendChild($document_node); - - $ics_data = id(new PhutilICSWriter()) - ->writeICSDocument($root_node); - - return id(new AphrontFileResponse()) - ->setDownload($file_name) - ->setMimeType('text/calendar') - ->setContent($ics_data); + return $this->newICSResponse( + $viewer, + $target->getICSFileName(), + array($target)); } } diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventListController.php b/src/applications/calendar/controller/PhabricatorCalendarEventListController.php index a2915b0be9..ac9015fbcf 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventListController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventListController.php @@ -11,17 +11,19 @@ final class PhabricatorCalendarEventListController $year = $request->getURIData('year'); $month = $request->getURIData('month'); $day = $request->getURIData('day'); + $engine = new PhabricatorCalendarEventSearchEngine(); if ($month && $year) { $engine->setCalendarYearAndMonthAndDay($year, $month, $day); } - $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($request->getURIData('queryKey')) - ->setSearchEngine($engine); + $nav_items = $this->buildNavigationItems(); - return $this->delegateToController($controller); + return $engine + ->setNavigationItems($nav_items) + ->setController($this) + ->buildResponse(); } protected function buildApplicationCrumbs() { @@ -34,4 +36,18 @@ final class PhabricatorCalendarEventListController return $crumbs; } + protected function buildNavigationItems() { + $items = array(); + + $items[] = id(new PHUIListItemView()) + ->setType(PHUIListItemView::TYPE_LABEL) + ->setName(pht('Import/Export')); + + $items[] = id(new PHUIListItemView()) + ->setName('Exports') + ->setHref('/calendar/export/'); + + return $items; + } + } diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php index 57792e6717..89e8109a4c 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php @@ -27,8 +27,8 @@ final class PhabricatorCalendarEventViewController $page_title = $monogram.' '.$event->getName(); $crumbs = $this->buildApplicationCrumbs(); - $start = new DateTime('@'.$event->getViewerDateFrom()); - $start->setTimeZone($viewer->getTimeZone()); + $start = $event->newStartDateTime() + ->newPHPDateTime(); $crumbs->addTextCrumb( $start->format('F Y'), @@ -72,9 +72,9 @@ final class PhabricatorCalendarEventViewController $comment_view, )) ->setCurtain($curtain) - ->addPropertySection($details_header, $details) + ->addPropertySection(pht('Description'), $description) ->addPropertySection($recurring_header, $recurring) - ->addPropertySection(pht('Description'), $description); + ->addPropertySection($details_header, $details); return $this->newPage() ->setTitle($page_title) @@ -348,9 +348,16 @@ final class PhabricatorCalendarEventViewController ->render(); } - $rule = $event->getFrequencyRule(); - switch ($rule) { - case PhabricatorCalendarEvent::FREQUENCY_DAILY: + $rrule = $event->newRecurrenceRule(); + + if ($rrule) { + $frequency = $rrule->getFrequency(); + } else { + $frequency = null; + } + + switch ($frequency) { + case PhutilCalendarRecurrenceRule::FREQUENCY_DAILY: if ($is_parent) { $message = pht('This event repeats every day.'); } else { @@ -359,7 +366,7 @@ final class PhabricatorCalendarEventViewController $parent_link); } break; - case PhabricatorCalendarEvent::FREQUENCY_WEEKLY: + case PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY: if ($is_parent) { $message = pht('This event repeats every week.'); } else { @@ -368,7 +375,7 @@ final class PhabricatorCalendarEventViewController $parent_link); } break; - case PhabricatorCalendarEvent::FREQUENCY_MONTHLY: + case PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY: if ($is_parent) { $message = pht('This event repeats every month.'); } else { @@ -377,7 +384,7 @@ final class PhabricatorCalendarEventViewController $parent_link); } break; - case PhabricatorCalendarEvent::FREQUENCY_YEARLY: + case PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY: if ($is_parent) { $message = pht('This event repeats every year.'); } else { diff --git a/src/applications/calendar/controller/PhabricatorCalendarExportDisableController.php b/src/applications/calendar/controller/PhabricatorCalendarExportDisableController.php new file mode 100644 index 0000000000..6be28499c4 --- /dev/null +++ b/src/applications/calendar/controller/PhabricatorCalendarExportDisableController.php @@ -0,0 +1,63 @@ +getViewer(); + + $export = id(new PhabricatorCalendarExportQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('id'))) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$export) { + return new Aphront404Response(); + } + + $export_uri = $export->getURI(); + $is_disable = !$export->getIsDisabled(); + + if ($request->isFormPost()) { + $xactions = array(); + $xactions[] = id(new PhabricatorCalendarExportTransaction()) + ->setTransactionType( + PhabricatorCalendarExportDisableTransaction::TRANSACTIONTYPE) + ->setNewValue($is_disable ? 1 : 0); + + $editor = id(new PhabricatorCalendarExportEditor()) + ->setActor($viewer) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->setContentSourceFromRequest($request); + + $editor->applyTransactions($export, $xactions); + + return id(new AphrontRedirectResponse())->setURI($export_uri); + } + + if ($is_disable) { + $title = pht('Disable Export'); + $body = pht( + 'Disable this export? The export URI will no longer function.'); + $button = pht('Disable Export'); + } else { + $title = pht('Enable Export'); + $body = pht( + 'Enable this export? Anyone who knows the export URI will be able '. + 'to export the data.'); + $button = pht('Enable Export'); + } + + return $this->newDialog() + ->setTitle($title) + ->appendParagraph($body) + ->addCancelButton($export_uri) + ->addSubmitButton($button); + } + +} diff --git a/src/applications/calendar/controller/PhabricatorCalendarExportEditController.php b/src/applications/calendar/controller/PhabricatorCalendarExportEditController.php new file mode 100644 index 0000000000..4bd673576d --- /dev/null +++ b/src/applications/calendar/controller/PhabricatorCalendarExportEditController.php @@ -0,0 +1,12 @@ +setController($this) + ->buildResponse(); + } + +} diff --git a/src/applications/calendar/controller/PhabricatorCalendarExportICSController.php b/src/applications/calendar/controller/PhabricatorCalendarExportICSController.php new file mode 100644 index 0000000000..a1264693bd --- /dev/null +++ b/src/applications/calendar/controller/PhabricatorCalendarExportICSController.php @@ -0,0 +1,97 @@ +setViewer($omnipotent) + ->withSecretKeys(array($request->getURIData('secretKey'))) + ->executeOne(); + if (!$export) { + return new Aphront404Response(); + } + + if ($export->getIsDisabled()) { + return new Aphront404Response(); + } + + $author = id(new PhabricatorPeopleQuery()) + ->setViewer($omnipotent) + ->withPHIDs(array($export->getAuthorPHID())) + ->needUserSettings(true) + ->executeOne(); + if (!$author) { + return new Aphront404Response(); + } + + $mode = $export->getPolicyMode(); + switch ($mode) { + case PhabricatorCalendarExport::MODE_PUBLIC: + $viewer = new PhabricatorUser(); + break; + case PhabricatorCalendarExport::MODE_PRIVILEGED: + $viewer = $author; + break; + default: + throw new Exception( + pht( + 'This export has an invalid mode ("%s").', + $mode)); + } + + $engine = id(new PhabricatorCalendarEventSearchEngine()) + ->setViewer($viewer); + + $query_key = $export->getQueryKey(); + $saved = id(new PhabricatorSavedQueryQuery()) + ->setViewer($omnipotent) + ->withEngineClassNames(array(get_class($engine))) + ->withQueryKeys(array($query_key)) + ->executeOne(); + if (!$saved) { + $saved = $engine->buildSavedQueryFromBuiltin($query_key); + } + + if (!$saved) { + return new Aphront404Response(); + } + + $saved = clone $saved; + + // Mark this as a query for export, so we get the correct ghost/recurring + // behaviors. We also want to load all matching events. + $saved->setParameter('export', true); + $saved->setParameter('limit', 0xFFFF); + + // Remove any range constraints. We always export all matching events into + // ICS files. + $saved->setParameter('rangeStart', null); + $saved->setParameter('rangeEnd', null); + $saved->setParameter('upcoming', null); + + $query = $engine->buildQueryFromSavedQuery($saved); + + $events = $query + ->setViewer($viewer) + ->execute(); + + return $this->newICSResponse( + $viewer, + $export->getICSFilename(), + $events); + } + +} diff --git a/src/applications/calendar/controller/PhabricatorCalendarExportListController.php b/src/applications/calendar/controller/PhabricatorCalendarExportListController.php new file mode 100644 index 0000000000..473a6277ba --- /dev/null +++ b/src/applications/calendar/controller/PhabricatorCalendarExportListController.php @@ -0,0 +1,27 @@ +setController($this) + ->buildResponse(); + } + + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + $doc_name = 'Calendar User Guide: Exporting Events'; + $doc_href = PhabricatorEnv::getDoclink($doc_name); + + $crumbs->addAction( + id(new PHUIListItemView()) + ->setName(pht('Guide: Exporting Events')) + ->setIcon('fa-book') + ->setHref($doc_href)); + + return $crumbs; + } + +} diff --git a/src/applications/calendar/controller/PhabricatorCalendarExportViewController.php b/src/applications/calendar/controller/PhabricatorCalendarExportViewController.php new file mode 100644 index 0000000000..1dc2a8d1be --- /dev/null +++ b/src/applications/calendar/controller/PhabricatorCalendarExportViewController.php @@ -0,0 +1,176 @@ +getViewer(); + + $export = id(new PhabricatorCalendarExportQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('id'))) + ->executeOne(); + if (!$export) { + return new Aphront404Response(); + } + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb( + pht('Exports'), + '/calendar/export/'); + $crumbs->addTextCrumb(pht('Export %d', $export->getID())); + $crumbs->setBorder(true); + + $timeline = $this->buildTransactionTimeline( + $export, + new PhabricatorCalendarExportTransactionQuery()); + $timeline->setShouldTerminate(true); + + $header = $this->buildHeaderView($export); + $curtain = $this->buildCurtain($export); + $details = $this->buildPropertySection($export); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setMainColumn( + array( + $timeline, + )) + ->setCurtain($curtain) + ->addPropertySection(pht('Details'), $details); + + $page_title = pht('Export %d %s', $export->getID(), $export->getName()); + + return $this->newPage() + ->setTitle($page_title) + ->setCrumbs($crumbs) + ->setPageObjectPHIDs(array($export->getPHID())) + ->appendChild($view); + } + + private function buildHeaderView( + PhabricatorCalendarExport $export) { + $viewer = $this->getViewer(); + $id = $export->getID(); + + if ($export->getIsDisabled()) { + $icon = 'fa-ban'; + $color = 'red'; + $status = pht('Disabled'); + } else { + $icon = 'fa-check'; + $color = 'bluegrey'; + $status = pht('Active'); + } + + $header = id(new PHUIHeaderView()) + ->setUser($viewer) + ->setHeader($export->getName()) + ->setStatus($icon, $color, $status) + ->setPolicyObject($export); + + return $header; + } + + private function buildCurtain(PhabricatorCalendarExport $export) { + $viewer = $this->getRequest()->getUser(); + $id = $export->getID(); + + $curtain = $this->newCurtainView($export); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $export, + PhabricatorPolicyCapability::CAN_EDIT); + + $ics_uri = $export->getICSURI(); + + $edit_uri = "export/edit/{$id}/"; + $edit_uri = $this->getApplicationURI($edit_uri); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Export')) + ->setIcon('fa-pencil') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit) + ->setHref($edit_uri)); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Export as .ics')) + ->setIcon('fa-download') + ->setHref($ics_uri)); + + $disable_uri = "export/disable/{$id}/"; + $disable_uri = $this->getApplicationURI($disable_uri); + if ($export->getIsDisabled()) { + $disable_name = pht('Enable Export'); + $disable_icon = 'fa-check'; + } else { + $disable_name = pht('Disable Export'); + $disable_icon = 'fa-ban'; + } + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName($disable_name) + ->setIcon($disable_icon) + ->setDisabled(!$can_edit) + ->setWorkflow(true) + ->setHref($disable_uri)); + + return $curtain; + } + + private function buildPropertySection( + PhabricatorCalendarExport $export) { + $viewer = $this->getViewer(); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer); + + $mode = $export->getPolicyMode(); + + $policy_icon = PhabricatorCalendarExport::getPolicyModeIcon($mode); + $policy_name = PhabricatorCalendarExport::getPolicyModeName($mode); + $policy_desc = PhabricatorCalendarExport::getPolicyModeDescription($mode); + $policy_color = PhabricatorCalendarExport::getPolicyModeColor($mode); + + $policy_view = id(new PHUIStatusListView()) + ->addItem( + id(new PHUIStatusItemView()) + ->setIcon($policy_icon, $policy_color) + ->setTarget($policy_name) + ->setNote($policy_desc)); + + $properties->addProperty(pht('Mode'), $policy_view); + + $query_key = $export->getQueryKey(); + $query_link = phutil_tag( + 'a', + array( + 'href' => $this->getApplicationURI("/query/{$query_key}/"), + ), + $query_key); + $properties->addProperty(pht('Query'), $query_link); + + $ics_uri = $export->getICSURI(); + $ics_uri = PhabricatorEnv::getURI($ics_uri); + + if ($export->getIsDisabled()) { + $ics_href = phutil_tag('em', array(), $ics_uri); + } else { + $ics_href = phutil_tag( + 'a', + array( + 'href' => $ics_uri, + ), + $ics_uri); + } + + $properties->addProperty(pht('ICS URI'), $ics_href); + + return $properties; + } +} diff --git a/src/applications/calendar/editor/PhabricatorCalendarEventEditEngine.php b/src/applications/calendar/editor/PhabricatorCalendarEventEditEngine.php index 5fa6e95732..17ea7552e9 100644 --- a/src/applications/calendar/editor/PhabricatorCalendarEventEditEngine.php +++ b/src/applications/calendar/editor/PhabricatorCalendarEventEditEngine.php @@ -68,10 +68,10 @@ final class PhabricatorCalendarEventEditEngine } $frequency_options = array( - PhabricatorCalendarEvent::FREQUENCY_DAILY => pht('Daily'), - PhabricatorCalendarEvent::FREQUENCY_WEEKLY => pht('Weekly'), - PhabricatorCalendarEvent::FREQUENCY_MONTHLY => pht('Monthly'), - PhabricatorCalendarEvent::FREQUENCY_YEARLY => pht('Yearly'), + PhutilCalendarRecurrenceRule::FREQUENCY_DAILY => pht('Daily'), + PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY => pht('Weekly'), + PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY => pht('Monthly'), + PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY => pht('Yearly'), ); $fields = array( @@ -142,6 +142,14 @@ final class PhabricatorCalendarEventEditEngine ->setConduitTypeDescription(pht('Mark the event as a recurring event.')) ->setValue($object->getIsRecurring()); + + $rrule = $object->newRecurrenceRule(); + if ($rrule) { + $frequency = $rrule->getFrequency(); + } else { + $frequency = null; + } + $fields[] = id(new PhabricatorSelectEditField()) ->setKey('frequency') ->setLabel(pht('Frequency')) @@ -151,7 +159,7 @@ final class PhabricatorCalendarEventEditEngine ->setDescription(pht('Recurring event frequency.')) ->setConduitDescription(pht('Change the event frequency.')) ->setConduitTypeDescription(pht('New event frequency.')) - ->setValue($object->getFrequencyRule()); + ->setValue($frequency); } if ($this->getIsCreate() || $object->getIsRecurring()) { @@ -164,50 +172,50 @@ final class PhabricatorCalendarEventEditEngine ->setDescription(pht('Last instance of the event.')) ->setConduitDescription(pht('Change when the event repeats until.')) ->setConduitTypeDescription(pht('New final event time.')) - ->setValue($object->getRecurrenceEndDate()); + ->setValue($object->getUntilDateTimeEpoch()); } $fields[] = id(new PhabricatorBoolEditField()) - ->setKey('isAllDay') - ->setLabel(pht('All Day')) - ->setOptions(pht('Normal Event'), pht('All Day Event')) - ->setTransactionType( - PhabricatorCalendarEventAllDayTransaction::TRANSACTIONTYPE) - ->setDescription(pht('Marks this as an all day event.')) - ->setConduitDescription(pht('Make the event an all day event.')) - ->setConduitTypeDescription(pht('Mark the event as an all day event.')) - ->setValue($object->getIsAllDay()); + ->setKey('isAllDay') + ->setLabel(pht('All Day')) + ->setOptions(pht('Normal Event'), pht('All Day Event')) + ->setTransactionType( + PhabricatorCalendarEventAllDayTransaction::TRANSACTIONTYPE) + ->setDescription(pht('Marks this as an all day event.')) + ->setConduitDescription(pht('Make the event an all day event.')) + ->setConduitTypeDescription(pht('Mark the event as an all day event.')) + ->setValue($object->getIsAllDay()); $fields[] = id(new PhabricatorEpochEditField()) - ->setKey('start') - ->setLabel(pht('Start')) - ->setTransactionType( - PhabricatorCalendarEventStartDateTransaction::TRANSACTIONTYPE) - ->setDescription(pht('Start time of the event.')) - ->setConduitDescription(pht('Change the start time of the event.')) - ->setConduitTypeDescription(pht('New event start time.')) - ->setValue($object->getViewerDateFrom()); + ->setKey('start') + ->setLabel(pht('Start')) + ->setTransactionType( + PhabricatorCalendarEventStartDateTransaction::TRANSACTIONTYPE) + ->setDescription(pht('Start time of the event.')) + ->setConduitDescription(pht('Change the start time of the event.')) + ->setConduitTypeDescription(pht('New event start time.')) + ->setValue($object->getStartDateTimeEpoch()); $fields[] = id(new PhabricatorEpochEditField()) - ->setKey('end') - ->setLabel(pht('End')) - ->setTransactionType( - PhabricatorCalendarEventEndDateTransaction::TRANSACTIONTYPE) - ->setDescription(pht('End time of the event.')) - ->setConduitDescription(pht('Change the end time of the event.')) - ->setConduitTypeDescription(pht('New event end time.')) - ->setValue($object->getViewerDateTo()); + ->setKey('end') + ->setLabel(pht('End')) + ->setTransactionType( + PhabricatorCalendarEventEndDateTransaction::TRANSACTIONTYPE) + ->setDescription(pht('End time of the event.')) + ->setConduitDescription(pht('Change the end time of the event.')) + ->setConduitTypeDescription(pht('New event end time.')) + ->setValue($object->getEndDateTimeEpoch()); $fields[] = id(new PhabricatorIconSetEditField()) - ->setKey('icon') - ->setLabel(pht('Icon')) - ->setIconSet(new PhabricatorCalendarIconSet()) - ->setTransactionType( - PhabricatorCalendarEventIconTransaction::TRANSACTIONTYPE) - ->setDescription(pht('Event icon.')) - ->setConduitDescription(pht('Change the event icon.')) - ->setConduitTypeDescription(pht('New event icon.')) - ->setValue($object->getIcon()); + ->setKey('icon') + ->setLabel(pht('Icon')) + ->setIconSet(new PhabricatorCalendarIconSet()) + ->setTransactionType( + PhabricatorCalendarEventIconTransaction::TRANSACTIONTYPE) + ->setDescription(pht('Event icon.')) + ->setConduitDescription(pht('Change the event icon.')) + ->setConduitTypeDescription(pht('New event icon.')) + ->setValue($object->getIcon()); return $fields; } diff --git a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php index d87746f347..33442b6702 100644 --- a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php +++ b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php @@ -139,7 +139,7 @@ final class PhabricatorCalendarEventEditor WHERE phid IN (%Ls) AND availabilityCacheTTL >= %d', $user->getTableName(), $phids, - $object->getDateFromForCache()); + $object->getStartDateTimeEpochForCache()); } return $xactions; @@ -159,9 +159,9 @@ final class PhabricatorCalendarEventEditor $recurrence_end_xaction = PhabricatorCalendarEventUntilDateTransaction::TRANSACTIONTYPE; - $start_date = $object->getDateFrom(); - $end_date = $object->getDateTo(); - $recurrence_end = $object->getRecurrenceEndDate(); + $start_date = $object->getStartDateTimeEpoch(); + $end_date = $object->getEndDateTimeEpoch(); + $recurrence_end = $object->getUntilDateTimeEpoch(); $is_recurring = $object->getIsRecurring(); $errors = array(); @@ -309,16 +309,10 @@ final class PhabricatorCalendarEventEditor PhabricatorCalendarEvent $event) { $actor = $this->getActor(); - $event_node = $event->newIntermediateEventNode($actor); - - $document_node = id(new PhutilCalendarDocumentNode()) - ->appendChild($event_node); - - $root_node = id(new PhutilCalendarRootNode()) - ->appendChild($document_node); - - $ics_data = id(new PhutilICSWriter()) - ->writeICSDocument($root_node); + $ics_data = id(new PhabricatorCalendarICSWriter()) + ->setViewer($actor) + ->setEvents(array($event)) + ->writeICSDocument(); $ics_attachment = new PhabricatorMetaMTAAttachment( $ics_data, diff --git a/src/applications/calendar/editor/PhabricatorCalendarExportEditEngine.php b/src/applications/calendar/editor/PhabricatorCalendarExportEditEngine.php new file mode 100644 index 0000000000..6ebbc9e1e0 --- /dev/null +++ b/src/applications/calendar/editor/PhabricatorCalendarExportEditEngine.php @@ -0,0 +1,133 @@ +getViewer()); + } + + protected function newObjectQuery() { + return new PhabricatorCalendarExportQuery(); + } + + protected function getObjectCreateTitleText($object) { + return pht('Create New Export'); + } + + protected function getObjectEditTitleText($object) { + return pht('Edit Export: %s', $object->getName()); + } + + protected function getObjectEditShortText($object) { + return pht('Export %d', $object->getID()); + } + + protected function getObjectCreateShortText() { + return pht('Create Export'); + } + + protected function getObjectName() { + return pht('Export'); + } + + protected function getObjectViewURI($object) { + return $object->getURI(); + } + + protected function getEditorURI() { + return $this->getApplication()->getApplicationURI('export/edit/'); + } + + protected function buildCustomEditFields($object) { + $viewer = $this->getViewer(); + + $export_modes = PhabricatorCalendarExport::getAvailablePolicyModes(); + $export_modes = array_fuse($export_modes); + + $current_mode = $object->getPolicyMode(); + if (empty($export_modes[$current_mode])) { + array_shift($export_modes, $current_mode); + } + + $mode_options = array(); + foreach ($export_modes as $export_mode) { + $mode_name = PhabricatorCalendarExport::getPolicyModeName($export_mode); + $mode_summary = PhabricatorCalendarExport::getPolicyModeSummary( + $export_mode); + $mode_options[$export_mode] = pht('%s: %s', $mode_name, $mode_summary); + } + + $fields = array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setDescription(pht('Name of the export.')) + ->setIsRequired(true) + ->setTransactionType( + PhabricatorCalendarExportNameTransaction::TRANSACTIONTYPE) + ->setConduitDescription(pht('Rename the export.')) + ->setConduitTypeDescription(pht('New export name.')) + ->setValue($object->getName()), + id(new PhabricatorBoolEditField()) + ->setKey('disabled') + ->setOptions(pht('Active'), pht('Disabled')) + ->setLabel(pht('Disabled')) + ->setDescription(pht('Disable the export.')) + ->setTransactionType( + PhabricatorCalendarExportDisableTransaction::TRANSACTIONTYPE) + ->setIsConduitOnly(true) + ->setConduitDescription(pht('Disable or restore the export.')) + ->setConduitTypeDescription(pht('True to cancel the export.')) + ->setValue($object->getIsDisabled()), + id(new PhabricatorTextEditField()) + ->setKey('queryKey') + ->setLabel(pht('Query Key')) + ->setDescription(pht('Query to execute.')) + ->setIsRequired(true) + ->setTransactionType( + PhabricatorCalendarExportQueryKeyTransaction::TRANSACTIONTYPE) + ->setConduitDescription(pht('Change the export query key.')) + ->setConduitTypeDescription(pht('New export query key.')) + ->setValue($object->getQueryKey()), + id(new PhabricatorSelectEditField()) + ->setKey('mode') + ->setLabel(pht('Mode')) + ->setTransactionType( + PhabricatorCalendarExportModeTransaction::TRANSACTIONTYPE) + ->setOptions($mode_options) + ->setDescription(pht('Change the policy mode for the export.')) + ->setConduitDescription(pht('Adjust export mode.')) + ->setConduitTypeDescription(pht('New export mode.')) + ->setValue($current_mode), + + ); + + return $fields; + } + + +} diff --git a/src/applications/calendar/editor/PhabricatorCalendarExportEditor.php b/src/applications/calendar/editor/PhabricatorCalendarExportEditor.php new file mode 100644 index 0000000000..6ddd172d58 --- /dev/null +++ b/src/applications/calendar/editor/PhabricatorCalendarExportEditor.php @@ -0,0 +1,18 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $export = $objects[$phid]; + + $id = $export->getID(); + $name = $export->getName(); + $uri = $export->getURI(); + + $handle + ->setName($name) + ->setFullName(pht('Calendar Export %s: %s', $id, $name)) + ->setURI($uri); + + if ($export->getIsDisabled()) { + $handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED); + } + } + } + +} diff --git a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php index d0006ba3fe..9b928c9d1a 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php @@ -13,6 +13,7 @@ final class PhabricatorCalendarEventQuery private $eventsWithNoParent; private $instanceSequencePairs; private $isStub; + private $parentEventPHIDs; private $generateGhosts = false; @@ -71,6 +72,11 @@ final class PhabricatorCalendarEventQuery return $this; } + public function withParentEventPHIDs(array $parent_phids) { + $this->parentEventPHIDs = $parent_phids; + return $this; + } + protected function getDefaultOrderVector() { return array('start', 'id'); } @@ -99,7 +105,7 @@ final class PhabricatorCalendarEventQuery protected function getPagingValueMap($cursor, array $keys) { $event = $this->loadCursorObject($cursor); return array( - 'start' => $event->getViewerDateFrom(), + 'start' => $event->getStartDateTimeEpoch(), 'id' => $event->getID(), ); } @@ -165,70 +171,54 @@ final class PhabricatorCalendarEventQuery // 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; + $start_date = $this->getRecurrenceWindowStart( + $event, + $generate_from - $duration); - if (($this->rangeBegin !== null) && - ($this->rangeBegin > $event->getViewerDateFrom())) { - $max_date = $this->rangeBegin - $duration; - $date = $event->getViewerDateFrom(); - $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer); + $end_date = $this->getRecurrenceWindowEnd( + $event, + $generate_until); - 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'); + $limit = $this->getRecurrenceLimit($event, $raw_limit); + + $set = $event->newRecurrenceSet(); + + $recurrences = $set->getEventsBetween( + null, + $end_date, + $limit + 1); + + // 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; + } + + foreach ($recurrences as $sequence_index => $sequence_datetime) { + if (!$sequence_index) { + // This is the parent event, which we already have. + continue; } - $start = $this->rangeBegin; - } else { - $start = $event->getViewerDateFrom() - $duration; - } - - $date = $start; - $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer); - - // Select the minimum end time we need to generate events until. - $end_times = array(); - if ($this->rangeEnd) { - $end_times[] = $this->rangeEnd; - } - - if ($event->getRecurrenceEndDate()) { - $end_times[] = $event->getRecurrenceEndDate(); - } - - if ($enforced_end) { - $end_times[] = $enforced_end; - } - - 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 @@ -238,9 +228,9 @@ final class PhabricatorCalendarEventQuery if ($raw_limit) { if (count($events) > $raw_limit) { - $events = msort($events, 'getViewerDateFrom'); + $events = msort($events, 'getStartDateTimeEpoch'); $events = array_slice($events, 0, $raw_limit, true); - $enforced_end = last($events)->getViewerDateFrom(); + $generate_until = last($events)->getEndDateTimeEpoch(); } } } @@ -308,7 +298,7 @@ final class PhabricatorCalendarEventQuery } } - $events = msort($events, 'getViewerDateFrom'); + $events = msort($events, 'getStartDateTimeEpoch'); return $events; } @@ -331,14 +321,14 @@ final class PhabricatorCalendarEventQuery protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); - if ($this->ids) { + if ($this->ids !== null) { $where[] = qsprintf( $conn, 'event.id IN (%Ld)', $this->ids); } - if ($this->phids) { + if ($this->phids !== null) { $where[] = qsprintf( $conn, 'event.phid IN (%Ls)', @@ -352,14 +342,14 @@ final class PhabricatorCalendarEventQuery if ($this->rangeBegin) { $where[] = qsprintf( $conn, - 'event.dateTo >= %d OR event.isRecurring = 1', + '(event.utcUntilEpoch >= %d) OR (event.utcUntilEpoch IS NULL)', $this->rangeBegin - phutil_units('16 hours in seconds')); } if ($this->rangeEnd) { $where[] = qsprintf( $conn, - 'event.dateFrom <= %d', + 'event.utcInitialEpoch <= %d', $this->rangeEnd + phutil_units('16 hours in seconds')); } @@ -370,7 +360,7 @@ final class PhabricatorCalendarEventQuery $this->inviteePHIDs); } - if ($this->hostPHIDs) { + if ($this->hostPHIDs !== null) { $where[] = qsprintf( $conn, 'event.hostPHID IN (%Ls)', @@ -414,6 +404,13 @@ final class PhabricatorCalendarEventQuery (int)$this->isStub); } + if ($this->parentEventPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'event.instanceOfEventPHID IN (%Ls)', + $this->parentEventPHIDs); + } + return $where; } @@ -500,7 +497,7 @@ final class PhabricatorCalendarEventQuery } } - $events = msort($events, 'getViewerDateFrom'); + $events = msort($events, 'getStartDateTimeEpoch'); return $events; } @@ -510,8 +507,8 @@ final class PhabricatorCalendarEventQuery $range_end = $this->rangeEnd; foreach ($events as $key => $event) { - $event_start = $event->getViewerDateFrom(); - $event_end = $event->getViewerDateTo(); + $event_start = $event->getStartDateTimeEpoch(); + $event_end = $event->getEndDateTimeEpoch(); if ($range_start && $event_end < $range_start) { unset($events[$key]); @@ -525,4 +522,44 @@ final class PhabricatorCalendarEventQuery 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/query/PhabricatorCalendarEventSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php index 9420261a59..a29b7ea37a 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php @@ -115,8 +115,12 @@ final class PhabricatorCalendarEventSearchEngine } // Generate ghosts (and ignore stub events) if we aren't querying for - // specific events. - if (!$map['ids'] && !$map['phids']) { + // specific events or exporting. + if (!empty($map['export'])) { + // This is a specific mode enabled by event exports. + $query + ->withIsStub(false); + } else if (!$map['ids'] && !$map['phids']) { $query ->withIsStub(false) ->setGenerateGhosts(true); @@ -255,11 +259,20 @@ final class PhabricatorCalendarEventSearchEngine array $handles) { if ($this->isMonthView($query)) { - return $this->buildCalendarMonthView($events, $query); + $result = $this->buildCalendarMonthView($events, $query); } else if ($this->isDayView($query)) { - return $this->buildCalendarDayView($events, $query); + $result = $this->buildCalendarDayView($events, $query); + } else { + $result = $this->buildCalendarListView($events, $query); } + return $result; + } + + private function buildCalendarListView( + array $events, + PhabricatorSavedQuery $query) { + assert_instances_of($events, 'PhabricatorCalendarEvent'); $viewer = $this->requireViewer(); $list = new PHUIObjectItemListView(); @@ -343,8 +356,8 @@ final class PhabricatorCalendarEventSearchEngine $month_view->setUser($viewer); foreach ($events as $event) { - $epoch_min = $event->getViewerDateFrom(); - $epoch_max = $event->getViewerDateTo(); + $epoch_min = $event->getStartDateTimeEpoch(); + $epoch_max = $event->getEndDateTimeEpoch(); $event_view = id(new AphrontCalendarEventView()) ->setHostPHID($event->getHostPHID()) @@ -408,8 +421,8 @@ final class PhabricatorCalendarEventSearchEngine $event, PhabricatorPolicyCapability::CAN_EDIT); - $epoch_min = $event->getViewerDateFrom(); - $epoch_max = $event->getViewerDateTo(); + $epoch_min = $event->getStartDateTimeEpoch(); + $epoch_max = $event->getEndDateTimeEpoch(); $status_icon = $event->getDisplayIcon($viewer); $status_color = $event->getDisplayIconColor($viewer); @@ -562,4 +575,17 @@ final class PhabricatorCalendarEventSearchEngine return false; } + public function newUseResultsActions(PhabricatorSavedQuery $saved) { + $viewer = $this->requireViewer(); + $can_export = $viewer->isLoggedIn(); + + return array( + id(new PhabricatorActionView()) + ->setIcon('fa-download') + ->setName(pht('Export Query as .ics')) + ->setDisabled(!$can_export) + ->setHref('/calendar/export/edit/?queryKey='.$saved->getQueryKey()), + ); + } + } diff --git a/src/applications/calendar/query/PhabricatorCalendarExportQuery.php b/src/applications/calendar/query/PhabricatorCalendarExportQuery.php new file mode 100644 index 0000000000..c51671c806 --- /dev/null +++ b/src/applications/calendar/query/PhabricatorCalendarExportQuery.php @@ -0,0 +1,94 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withAuthorPHIDs(array $phids) { + $this->authorPHIDs = $phids; + return $this; + } + + public function withIsDisabled($is_disabled) { + $this->isDisabled = $is_disabled; + return $this; + } + + public function withSecretKeys(array $keys) { + $this->secretKeys = $keys; + return $this; + } + + public function newResultObject() { + return new PhabricatorCalendarExport(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'export.id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'export.phid IN (%Ls)', + $this->phids); + } + + if ($this->authorPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'export.authorPHID IN (%Ls)', + $this->authorPHIDs); + } + + if ($this->isDisabled !== null) { + $where[] = qsprintf( + $conn, + 'export.isDisabled = %d', + (int)$this->isDisabled); + } + + if ($this->secretKeys !== null) { + $where[] = qsprintf( + $conn, + 'export.secretKey IN (%Ls)', + $this->secretKeys); + } + + return $where; + } + + protected function getPrimaryTableAlias() { + return 'export'; + } + + public function getQueryApplicationClass() { + return 'PhabricatorCalendarApplication'; + } + +} diff --git a/src/applications/calendar/query/PhabricatorCalendarExportSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarExportSearchEngine.php new file mode 100644 index 0000000000..4a65bfd099 --- /dev/null +++ b/src/applications/calendar/query/PhabricatorCalendarExportSearchEngine.php @@ -0,0 +1,119 @@ +requireViewer(); + + return id(new PhabricatorCalendarExportQuery()) + ->withAuthorPHIDs(array($viewer->getPHID())); + } + + protected function buildCustomSearchFields() { + return array(); + } + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + return $query; + } + + protected function getURI($path) { + return '/calendar/export/'.$path; + } + + protected function getBuiltinQueryNames() { + $names = array( + 'all' => pht('All Exports'), + ); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + protected function renderResultList( + array $exports, + PhabricatorSavedQuery $query, + array $handles) { + + assert_instances_of($exports, 'PhabricatorCalendarExport'); + $viewer = $this->requireViewer(); + + $list = new PHUIObjectItemListView(); + foreach ($exports as $export) { + $item = id(new PHUIObjectItemView()) + ->setViewer($viewer) + ->setObjectName(pht('Export %d', $export->getID())) + ->setHeader($export->getName()) + ->setHref($export->getURI()); + + if ($export->getIsDisabled()) { + $item->setDisabled(true); + } + + $mode = $export->getPolicyMode(); + $policy_icon = PhabricatorCalendarExport::getPolicyModeIcon($mode); + $policy_name = PhabricatorCalendarExport::getPolicyModeName($mode); + $policy_color = PhabricatorCalendarExport::getPolicyModeColor($mode); + + $item->addIcon( + "{$policy_icon} {$policy_color}", + $policy_name); + + $list->addItem($item); + } + + $result = new PhabricatorApplicationSearchResultView(); + $result->setObjectList($list); + $result->setNoDataString(pht('No exports found.')); + + return $result; + } + + protected function getNewUserBody() { + $doc_name = 'Calendar User Guide: Exporting Events'; + $doc_href = PhabricatorEnv::getDoclink($doc_name); + + $create_button = id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-book white') + ->setText($doc_name) + ->setHref($doc_href) + ->setColor(PHUIButtonView::GREEN); + + $icon = $this->getApplication()->getIcon(); + $app_name = $this->getApplication()->getName(); + $view = id(new PHUIBigInfoView()) + ->setIcon('fa-download') + ->setTitle(pht('No Exports Configured')) + ->setDescription( + pht( + 'You have not set up any events for export from Calendar yet. '. + 'See the documentation for instructions on how to get started.')) + ->addAction($create_button); + + return $view; + } + +} diff --git a/src/applications/calendar/query/PhabricatorCalendarExportTransactionQuery.php b/src/applications/calendar/query/PhabricatorCalendarExportTransactionQuery.php new file mode 100644 index 0000000000..32b9d71b65 --- /dev/null +++ b/src/applications/calendar/query/PhabricatorCalendarExportTransactionQuery.php @@ -0,0 +1,10 @@ +setTimeZone($actor->getTimeZone()); - - $start->setTime($start->format('H'), 0, 0); - $start->modify('+1 hour'); - $end = id(clone $start)->modify('+1 hour'); - - $epoch_min = $start->format('U'); - $epoch_max = $end->format('U'); - - $now_date = new DateTime('@'.$now); - $now_min = id(clone $now_date)->setTime(0, 0)->format('U'); - $now_max = id(clone $now_date)->setTime(23, 59)->format('U'); - $default_icon = 'fa-calendar'; + $datetime_start = PhutilCalendarAbsoluteDateTime::newFromEpoch( + $now, + $actor->getTimezoneIdentifier()); + $datetime_end = $datetime_start->newRelativeDateTime('PT1H'); + return id(new PhabricatorCalendarEvent()) ->setHostPHID($actor->getPHID()) ->setIsCancelled(0) ->setIsAllDay(0) ->setIsStub(0) ->setIsRecurring(0) - ->setRecurrenceFrequency( - array( - 'rule' => self::FREQUENCY_WEEKLY, - )) ->setIcon($default_icon) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setSpacePHID($actor->getDefaultSpacePHID()) ->attachInvitees(array()) - ->setDateFrom($epoch_min) - ->setDateTo($epoch_max) - ->setAllDayDateFrom($now_min) - ->setAllDayDateTo($now_max) + ->setDateFrom(0) + ->setDateTo(0) + ->setAllDayDateFrom(0) + ->setAllDayDateTo(0) + ->setStartDateTime($datetime_start) + ->setEndDateTime($datetime_end) ->applyViewerTimezone($actor); } - private function newChild(PhabricatorUser $actor, $sequence) { + private function newChild( + PhabricatorUser $actor, + $sequence, + PhutilCalendarDateTime $start = null) { if (!$this->isParentEvent()) { throw new Exception( pht( @@ -118,10 +110,13 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO ->setInstanceOfEventPHID($this->getPHID()) ->setSequenceIndex($sequence) ->setIsRecurring(true) - ->setRecurrenceFrequency($this->getRecurrenceFrequency()) - ->attachParentEvent($this); + ->attachParentEvent($this) + ->setAllDayDateFrom(0) + ->setAllDayDateTo(0) + ->setDateFrom(0) + ->setDateTo(0); - return $child->copyFromParent($actor); + return $child->copyFromParent($actor, $start); } protected function readField($field) { @@ -153,7 +148,10 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO } - public function copyFromParent(PhabricatorUser $actor) { + public function copyFromParent( + PhabricatorUser $actor, + PhutilCalendarDateTime $start = null) { + if (!$this->isChildEvent()) { throw new Exception( pht( @@ -173,65 +171,46 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO ->setDescription($parent->getDescription()); $sequence = $this->getSequenceIndex(); - $duration = $this->getDuration(); - $epochs = $parent->getSequenceIndexEpochs($actor, $sequence, $duration); + + 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(); + $end_datetime = $start_datetime->newRelativeDateTime($duration); $this - ->setDateFrom($epochs['dateFrom']) - ->setDateTo($epochs['dateTo']) - ->setAllDayDateFrom($epochs['allDayDateFrom']) - ->setAllDayDateTo($epochs['allDayDateTo']); + ->setStartDateTime($start_datetime) + ->setEndDateTime($end_datetime); return $this; } public function isValidSequenceIndex(PhabricatorUser $viewer, $sequence) { - try { - $this->getSequenceIndexEpochs($viewer, $sequence, $this->getDuration()); - return true; - } catch (Exception $ex) { - return false; - } + return (bool)$this->newSequenceIndexDateTime($sequence); } - private function getSequenceIndexEpochs( - PhabricatorUser $viewer, - $sequence, - $duration) { - - $frequency = $this->getFrequencyUnit(); - $modify_key = '+'.$sequence.' '.$frequency; - - $date = $this->getDateFrom(); - $date_time = PhabricatorTime::getDateTimeFromEpoch($date, $viewer); - $date_time->modify($modify_key); - $date = $date_time->format('U'); - - $end_date = $this->getRecurrenceEndDate(); - if ($end_date && $date > $end_date) { - throw new Exception( - pht( - 'Sequence "%s" is invalid for this event: it would occur after '. - 'the event stops repeating.', - $sequence)); + public function newSequenceIndexDateTime($sequence) { + $set = $this->newRecurrenceSet(); + if (!$set) { + return null; } - $utc = new DateTimeZone('UTC'); + $instances = $set->getEventsBetween( + null, + $this->newUntilDateTime(), + $sequence + 1); - $allday_from = $this->getAllDayDateFrom(); - $allday_date = new DateTime('@'.$allday_from, $utc); - $allday_date->setTimeZone($utc); - $allday_date->modify($modify_key); - - $allday_min = $allday_date->format('U'); - $allday_duration = ($this->getAllDayDateTo() - $allday_from); - - return array( - 'dateFrom' => $date, - 'dateTo' => $date + $duration, - 'allDayDateFrom' => $allday_min, - 'allDayDateTo' => $allday_min + $allday_duration, - ); + return idx($instances, $sequence, null); } public function newStub(PhabricatorUser $actor, $sequence) { @@ -248,8 +227,12 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO 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) @@ -260,67 +243,62 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO return $ghost; } - public function getViewerDateFrom() { - if ($this->viewerDateFrom === null) { - throw new PhutilInvalidStateException('applyViewerTimezone'); - } - - return $this->viewerDateFrom; - } - - public function getViewerDateTo() { - if ($this->viewerDateTo === null) { - throw new PhutilInvalidStateException('applyViewerTimezone'); - } - - return $this->viewerDateTo; - } - public function applyViewerTimezone(PhabricatorUser $viewer) { - if (!$this->getIsAllDay()) { - $this->viewerDateFrom = $this->getDateFrom(); - $this->viewerDateTo = $this->getDateTo(); - } else { - $zone = $viewer->getTimeZone(); - - $this->viewerDateFrom = $this->getDateEpochForTimezone( - $this->getAllDayDateFrom(), - new DateTimeZone('UTC'), - 'Y-m-d', - null, - $zone); - - $this->viewerDateTo = $this->getDateEpochForTimezone( - $this->getAllDayDateTo(), - new DateTimeZone('UTC'), - 'Y-m-d 23:59:00', - null, - $zone); - } - + $this->viewerTimezone = $viewer->getTimezoneIdentifier(); return $this; } public function getDuration() { - return $this->getDateTo() - $this->getDateFrom(); + return ($this->getEndDateTimeEpoch() - $this->getStartDateTimeEpoch()); } - public function getDateEpochForTimezone( - $epoch, - $src_zone, - $format, - $adjust, - $dst_zone) { + public function updateUTCEpochs() { + // The "intitial" epoch is the start time of the event, in UTC. + $start_date = $this->newStartDateTime() + ->setViewerTimezone('UTC'); + $start_epoch = $start_date->getEpoch(); + $this->setUTCInitialEpoch($start_epoch); - $src = new DateTime('@'.$epoch); - $src->setTimeZone($src_zone); + // The "until" epoch is the last UTC epoch on which any instance of this + // event occurs. For infinitely recurring events, it is `null`. - if (strlen($adjust)) { - $adjust = ' '.$adjust; + if (!$this->getIsRecurring()) { + $end_date = $this->newEndDateTime() + ->setViewerTimezone('UTC'); + $until_epoch = $end_date->getEpoch(); + } else { + $until_epoch = null; + $until_date = $this->newUntilDateTime(); + if ($until_date) { + $until_date->setViewerTimezone('UTC'); + $duration = $this->newDuration(); + $until_epoch = id(new PhutilCalendarRelativeDateTime()) + ->setOrigin($until_date) + ->setDuration($duration) + ->getEpoch(); + } } + $this->setUTCUntilEpoch($until_epoch); - $dst = new DateTime($src->format($format).$adjust, $dst_zone); - return $dst->format('U'); + // The "instance" epoch is a property of instances of recurring events. + // It's the original UTC epoch on which the instance started. Usually that + // is the same as the start date, but they may be different if the instance + // has been edited. + + // The ICS format uses this value (original start time) to identify event + // instances, and must do so because it allows additional arbitrary + // instances to be added (with "RDATE"). + + $instance_epoch = null; + $instance_date = $this->newInstanceDateTime(); + if ($instance_date) { + $instance_epoch = $instance_date + ->setViewerTimezone('UTC') + ->getEpoch(); + } + $this->setUTCInstanceEpoch($instance_epoch); + + return $this; } public function save() { @@ -328,6 +306,8 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO $this->mailKey = Filesystem::readRandomCharacters(20); } + $this->updateUTCEpochs(); + return parent::save(); } @@ -340,8 +320,10 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO * * @return int Event start date for availability caches. */ - public function getDateFromForCache() { - return ($this->getViewerDateFrom() - phutil_units('15 minutes in seconds')); + public function getStartDateTimeEpochForCache() { + $epoch = $this->getStartDateTimeEpoch(); + $window = phutil_units('15 minutes in seconds'); + return ($epoch - $window); } protected function getConfiguration() { @@ -349,20 +331,25 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text', - 'dateFrom' => 'epoch', - 'dateTo' => 'epoch', - 'allDayDateFrom' => 'epoch', - 'allDayDateTo' => 'epoch', 'description' => 'text', 'isCancelled' => 'bool', 'isAllDay' => 'bool', 'icon' => 'text32', 'mailKey' => 'bytes20', 'isRecurring' => 'bool', - 'recurrenceEndDate' => 'epoch?', 'instanceOfEventPHID' => 'phid?', 'sequenceIndex' => 'uint32?', 'isStub' => 'bool', + 'utcInitialEpoch' => 'epoch', + 'utcUntilEpoch' => 'epoch?', + 'utcInstanceEpoch' => 'epoch?', + + // TODO: DEPRECATED. + 'allDayDateFrom' => 'epoch', + 'allDayDateTo' => 'epoch', + 'dateFrom' => 'epoch', + 'dateTo' => 'epoch', + 'recurrenceEndDate' => 'epoch?', ), self::CONFIG_KEY_SCHEMA => array( 'key_date' => array( @@ -372,9 +359,17 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO 'columns' => array('instanceOfEventPHID', 'sequenceIndex'), 'unique' => true, ), + 'key_epoch' => array( + 'columns' => array('utcInitialEpoch', 'utcUntilEpoch'), + ), + 'key_rdate' => array( + 'columns' => array('instanceOfEventPHID', 'utcInstanceEpoch'), + 'unique' => true, + ), ), self::CONFIG_SERIALIZATION => array( 'recurrenceFrequency' => self::SERIALIZATION_JSON, + 'parameters' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } @@ -450,27 +445,6 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO return $this; } - public function getFrequencyRule() { - return idx($this->recurrenceFrequency, 'rule'); - } - - public function getFrequencyUnit() { - $frequency = $this->getFrequencyRule(); - - switch ($frequency) { - case 'daily': - return 'day'; - case 'weekly': - return 'week'; - case 'monthly': - return 'month'; - case 'yearly': - return 'year'; - default: - return 'day'; - } - } - public function getURI() { if ($this->getIsGhostEvent()) { $base = $this->getParentEvent()->getURI(); @@ -516,14 +490,12 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO PhabricatorUser $viewer, $show_end) { - if ($show_end) { - $min_date = PhabricatorTime::getDateTimeFromEpoch( - $this->getViewerDateFrom(), - $viewer); + $start = $this->newStartDateTime(); + $end = $this->newEndDateTime(); - $max_date = PhabricatorTime::getDateTimeFromEpoch( - $this->getViewerDateTo(), - $viewer); + if ($show_end) { + $min_date = $start->newPHPDateTime(); + $max_date = $end->newPHPDateTime(); $min_day = $min_date->format('Y m d'); $max_day = $max_date->format('Y m d'); @@ -533,8 +505,8 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO $show_end_date = false; } - $min_epoch = $this->getViewerDateFrom(); - $max_epoch = $this->getViewerDateTo(); + $min_epoch = $start->getEpoch(); + $max_epoch = $end->getEpoch(); if ($this->getIsAllDay()) { if ($show_end_date) { @@ -629,11 +601,28 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO return $this->getMonogram().'.ics'; } - public function newIntermediateEventNode(PhabricatorUser $viewer) { + public function newIntermediateEventNode( + PhabricatorUser $viewer, + array $children) { + $base_uri = new PhutilURI(PhabricatorEnv::getProductionURI('/')); $domain = $base_uri->getDomain(); - $uid = $this->getPHID().'@'.$domain; + // NOTE: For recurring events, all of the events in the series have the + // same UID (the UID of the parent). The child event instances are + // differentiated by the "RECURRENCE-ID" field. + if ($this->isChildEvent()) { + $parent = $this->getParentEvent(); + $instance_datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch( + $this->getUTCInstanceEpoch()); + $recurrence_id = $instance_datetime->getISO8601(); + $rrule = null; + } else { + $parent = $this; + $recurrence_id = null; + $rrule = $this->newRecurrenceRule(); + } + $uid = $parent->getPHID().'@'.$domain; $created = $this->getDateCreated(); $created = PhutilCalendarAbsoluteDateTime::newFromEpoch($created); @@ -641,11 +630,8 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO $modified = $this->getDateModified(); $modified = PhutilCalendarAbsoluteDateTime::newFromEpoch($modified); - $date_start = $this->getDateFrom(); - $date_start = PhutilCalendarAbsoluteDateTime::newFromEpoch($date_start); - - $date_end = $this->getDateTo(); - $date_end = PhutilCalendarAbsoluteDateTime::newFromEpoch($date_end); + $date_start = $this->newStartDateTime(); + $date_end = $this->newEndDateTime(); if ($this->getIsAllDay()) { $date_start->setIsAllDay(true); @@ -705,6 +691,8 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO ->setStatus($status); } + // TODO: Use $children to generate EXDATE/RDATE information. + $node = id(new PhutilCalendarEventNode()) ->setUID($uid) ->setName($this->getName()) @@ -716,10 +704,186 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO ->setOrganizer($organizer) ->setAttendees($attendees); + if ($rrule) { + $node->setRecurrenceRule($rrule); + } + + if ($recurrence_id) { + $node->setRecurrenceID($recurrence_id); + } + return $node; } + public function newStartDateTime() { + $datetime = $this->getParameter('startDateTime'); + if ($datetime) { + return $this->newDateTimeFromDictionary($datetime); + } + $epoch = $this->getDateFrom(); + return $this->newDateTimeFromEpoch($epoch); + } + + public function getStartDateTimeEpoch() { + return $this->newStartDateTime()->getEpoch(); + } + + public function newEndDateTime() { + $datetime = $this->getParameter('endDateTime'); + if ($datetime) { + return $this->newDateTimeFromDictionary($datetime); + } + + $epoch = $this->getDateTo(); + return $this->newDateTimeFromEpoch($epoch); + } + + public function getEndDateTimeEpoch() { + return $this->newEndDateTime()->getEpoch(); + } + + public function newUntilDateTime() { + $datetime = $this->getParameter('untilDateTime'); + if ($datetime) { + return $this->newDateTimeFromDictionary($datetime); + } + + $epoch = $this->getRecurrenceEndDate(); + if (!$epoch) { + return null; + } + return $this->newDateTimeFromEpoch($epoch); + } + + public function getUntilDateTimeEpoch() { + $datetime = $this->newUntilDateTime(); + + if (!$datetime) { + return null; + } + + return $datetime->getEpoch(); + } + + public function newDuration() { + return id(new PhutilCalendarDuration()) + ->setSeconds($this->getDuration()); + } + + public function newInstanceDateTime() { + if (!$this->getIsRecurring()) { + return null; + } + + $index = $this->getSequenceIndex(); + if (!$index) { + return null; + } + + return $this->newSequenceIndexDateTime($index); + } + + private function newDateTimeFromEpoch($epoch) { + $datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch($epoch); + + if ($this->getIsAllDay()) { + $datetime->setIsAllDay(true); + } + + return $this->newDateTimeFromDateTime($datetime); + } + + private function newDateTimeFromDictionary(array $dict) { + $datetime = PhutilCalendarAbsoluteDateTime::newFromDictionary($dict); + return $this->newDateTimeFromDateTime($datetime); + } + + private function newDateTimeFromDateTime(PhutilCalendarDateTime $datetime) { + $viewer_timezone = $this->viewerTimezone; + if ($viewer_timezone) { + $datetime->setViewerTimezone($viewer_timezone); + } + + return $datetime; + } + + public function getParameter($key, $default = null) { + return idx($this->parameters, $key, $default); + } + + public function setParameter($key, $value) { + $this->parameters[$key] = $value; + return $this; + } + + public function setStartDateTime(PhutilCalendarDateTime $datetime) { + return $this->setParameter( + 'startDateTime', + $datetime->newAbsoluteDateTime()->toDictionary()); + } + + public function setEndDateTime(PhutilCalendarDateTime $datetime) { + return $this->setParameter( + 'endDateTime', + $datetime->newAbsoluteDateTime()->toDictionary()); + } + + public function setUntilDateTime(PhutilCalendarDateTime $datetime) { + return $this->setParameter( + 'untilDateTime', + $datetime->newAbsoluteDateTime()->toDictionary()); + } + + public function setRecurrenceRule(PhutilCalendarRecurrenceRule $rrule) { + return $this->setParameter( + 'recurrenceRule', + $rrule->toDictionary()); + } + + public function newRecurrenceRule() { + if ($this->isChildEvent()) { + return $this->getParentEvent()->newRecurrenceRule(); + } + + if (!$this->getIsRecurring()) { + return null; + } + + $dict = $this->getParameter('recurrenceRule'); + if (!$dict) { + return null; + } + + $rrule = PhutilCalendarRecurrenceRule::newFromDictionary($dict); + + $start = $this->newStartDateTime(); + $rrule->setStartDateTime($start); + + $until = $this->newUntilDateTime(); + if ($until) { + $rrule->setUntil($until); + } + + return $rrule; + } + + public function newRecurrenceSet() { + if ($this->isChildEvent()) { + return $this->getParentEvent()->newRecurrenceSet(); + } + + $set = new PhutilCalendarRecurrenceSet(); + + $rrule = $this->newRecurrenceRule(); + if (!$rrule) { + return null; + } + + $set->addSource($rrule); + + return $set; + } /* -( Markup Interface )--------------------------------------------------- */ diff --git a/src/applications/calendar/storage/PhabricatorCalendarExport.php b/src/applications/calendar/storage/PhabricatorCalendarExport.php new file mode 100644 index 0000000000..75e83dfaf0 --- /dev/null +++ b/src/applications/calendar/storage/PhabricatorCalendarExport.php @@ -0,0 +1,198 @@ +setAuthorPHID($actor->getPHID()) + ->setPolicyMode(self::MODE_PRIVILEGED) + ->setIsDisabled(0); + } + + protected function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_COLUMN_SCHEMA => array( + 'name' => 'text', + 'policyMode' => 'text64', + 'queryKey' => 'text64', + 'secretKey' => 'bytes20', + 'isDisabled' => 'bool', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_author' => array( + 'columns' => array('authorPHID'), + ), + 'key_secret' => array( + 'columns' => array('secretKey'), + 'unique' => true, + ), + ), + ) + parent::getConfiguration(); + } + + public function getPHIDType() { + return PhabricatorCalendarExportPHIDType::TYPECONST; + } + + public function save() { + if (!$this->getSecretKey()) { + $this->setSecretKey(Filesystem::readRandomCharacters(20)); + } + + return parent::save(); + } + + public function getURI() { + $id = $this->getID(); + return "/calendar/export/{$id}/"; + } + + private static function getPolicyModeMap() { + return array( + self::MODE_PUBLIC => array( + 'icon' => 'fa-globe', + 'name' => pht('Public'), + 'color' => 'bluegrey', + 'summary' => pht( + 'Export only public data.'), + 'description' => pht( + 'Only publicly available data is exported.'), + ), + self::MODE_PRIVILEGED => array( + 'icon' => 'fa-unlock-alt', + 'name' => pht('Privileged'), + 'color' => 'red', + 'summary' => pht( + 'Export private data.'), + 'description' => pht( + 'Anyone who knows the URI for this export can view all event '. + 'details as though they were logged in with your account.'), + ), + ); + } + + private static function getPolicyModeSpec($const) { + return idx(self::getPolicyModeMap(), $const, array()); + } + + public static function getPolicyModeName($const) { + $spec = self::getPolicyModeSpec($const); + return idx($spec, 'name', $const); + } + + public static function getPolicyModeIcon($const) { + $spec = self::getPolicyModeSpec($const); + return idx($spec, 'icon', $const); + } + + public static function getPolicyModeColor($const) { + $spec = self::getPolicyModeSpec($const); + return idx($spec, 'color', $const); + } + + public static function getPolicyModeSummary($const) { + $spec = self::getPolicyModeSpec($const); + return idx($spec, 'summary', $const); + } + + public static function getPolicyModeDescription($const) { + $spec = self::getPolicyModeSpec($const); + return idx($spec, 'description', $const); + } + + public static function getPolicyModes() { + return array_keys(self::getPolicyModeMap()); + } + + public static function getAvailablePolicyModes() { + $modes = array(); + + if (PhabricatorEnv::getEnvConfig('policy.allow-public')) { + $modes[] = self::MODE_PUBLIC; + } + + $modes[] = self::MODE_PRIVILEGED; + + return $modes; + } + + public function getICSFilename() { + return PhabricatorSlug::normalizeProjectSlug($this->getName()).'.ics'; + } + + public function getICSURI() { + $secret_key = $this->getSecretKey(); + $ics_name = $this->getICSFilename(); + return "/calendar/export/ics/{$secret_key}/{$ics_name}"; + } + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + return $this->getAuthorPHID(); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + public function describeAutomaticCapability($capability) { + return null; + } + + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhabricatorCalendarExportEditor(); + } + + public function getApplicationTransactionObject() { + return $this; + } + + public function getApplicationTransactionTemplate() { + return new PhabricatorCalendarExportTransaction(); + } + + public function willRenderTimeline( + PhabricatorApplicationTransactionView $timeline, + AphrontRequest $request) { + + return $timeline; + } + + +/* -( PhabricatorDestructibleInterface )----------------------------------- */ + + + public function destroyObjectPermanently( + PhabricatorDestructionEngine $engine) { + $this->delete(); + } + +} diff --git a/src/applications/calendar/storage/PhabricatorCalendarExportTransaction.php b/src/applications/calendar/storage/PhabricatorCalendarExportTransaction.php new file mode 100644 index 0000000000..1bd24ef188 --- /dev/null +++ b/src/applications/calendar/storage/PhabricatorCalendarExportTransaction.php @@ -0,0 +1,18 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setEvents(array $events) { + assert_instances_of($events, 'PhabricatorCalendarEvent'); + $this->events = $events; + return $this; + } + + public function getEvents() { + return $this->events; + } + + public function writeICSDocument() { + $viewer = $this->getViewer(); + $events = $this->getEvents(); + + $events = mpull($events, null, 'getPHID'); + + if ($events) { + $child_map = id(new PhabricatorCalendarEventQuery()) + ->setViewer($viewer) + ->withParentEventPHIDs(array_keys($events)) + ->execute(); + $child_map = mpull($child_map, null, 'getPHID'); + } else { + $child_map = array(); + } + + $all_events = $events + $child_map; + $child_groups = mgroup($child_map, 'getInstanceOfEventPHID'); + + $document_node = new PhutilCalendarDocumentNode(); + + foreach ($all_events as $event) { + $child_events = idx($child_groups, $event->getPHID(), array()); + $event_node = $event->newIntermediateEventNode($viewer, $child_events); + $document_node->appendChild($event_node); + } + + $root_node = id(new PhutilCalendarRootNode()) + ->appendChild($document_node); + + return id(new PhutilICSWriter()) + ->writeICSDocument($root_node); + } +} diff --git a/src/applications/calendar/xaction/PhabricatorCalendarEventAllDayTransaction.php b/src/applications/calendar/xaction/PhabricatorCalendarEventAllDayTransaction.php index 1bcad16adf..eac229c395 100644 --- a/src/applications/calendar/xaction/PhabricatorCalendarEventAllDayTransaction.php +++ b/src/applications/calendar/xaction/PhabricatorCalendarEventAllDayTransaction.php @@ -15,6 +15,25 @@ final class PhabricatorCalendarEventAllDayTransaction public function applyInternalEffects($object, $value) { $object->setIsAllDay($value); + + // Adjust the flags on any other dates the event has. + $keys = array( + 'startDateTime', + 'endDateTime', + 'untilDateTime', + ); + + foreach ($keys as $key) { + $dict = $object->getParameter($key); + if (!$dict) { + continue; + } + + $datetime = PhutilCalendarAbsoluteDateTime::newFromDictionary($dict); + $datetime->setIsAllDay($value); + + $object->setParameter($key, $datetime->toDictionary()); + } } public function getTitle() { diff --git a/src/applications/calendar/xaction/PhabricatorCalendarEventEndDateTransaction.php b/src/applications/calendar/xaction/PhabricatorCalendarEventEndDateTransaction.php index fc7f9859ba..a0d127c32f 100644 --- a/src/applications/calendar/xaction/PhabricatorCalendarEventEndDateTransaction.php +++ b/src/applications/calendar/xaction/PhabricatorCalendarEventEndDateTransaction.php @@ -6,21 +6,18 @@ final class PhabricatorCalendarEventEndDateTransaction const TRANSACTIONTYPE = 'calendar.enddate'; public function generateOldValue($object) { - return $object->getDateTo(); + // TODO: Upgrade this. + return $object->getEndDateTimeEpoch(); } public function applyInternalEffects($object, $value) { $actor = $this->getActor(); - $object->setDateTo($value); - - $object->setAllDayDateTo( - $object->getDateEpochForTimezone( - $value, - $actor->getTimeZone(), - 'Y-m-d 23:59:00', - null, - new DateTimeZone('UTC'))); + $datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch( + $value, + $actor->getTimezoneIdentifier()); + $datetime->setIsAllDay($object->getIsAllDay()); + $object->setEndDateTime($datetime); } public function getTitle() { diff --git a/src/applications/calendar/xaction/PhabricatorCalendarEventFrequencyTransaction.php b/src/applications/calendar/xaction/PhabricatorCalendarEventFrequencyTransaction.php index 9690a97dce..33c954ac9f 100644 --- a/src/applications/calendar/xaction/PhabricatorCalendarEventFrequencyTransaction.php +++ b/src/applications/calendar/xaction/PhabricatorCalendarEventFrequencyTransaction.php @@ -6,32 +6,65 @@ final class PhabricatorCalendarEventFrequencyTransaction const TRANSACTIONTYPE = 'calendar.frequency'; public function generateOldValue($object) { - return $object->getFrequencyRule(); + $rrule = $object->newRecurrenceRule(); + + if (!$rrule) { + return null; + } + + return $rrule->getFrequency(); } public function applyInternalEffects($object, $value) { - $object->setRecurrenceFrequency( - array( - 'rule' => $value, - )); + $rrule = id(new PhutilCalendarRecurrenceRule()) + ->setFrequency($value); + + $object->setRecurrenceRule($rrule); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $valid = array( + PhutilCalendarRecurrenceRule::FREQUENCY_DAILY, + PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY, + PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY, + PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY, + ); + $valid = array_fuse($valid); + + foreach ($xactions as $xaction) { + $value = $xaction->getNewValue(); + + if (!isset($valid[$value])) { + $errors[] = $this->newInvalidError( + pht( + 'Event frequency "%s" is not valid. Valid frequences are: %s.', + $value, + implode(', ', $valid)), + $xaction); + } + } + + return $errors; } public function getTitle() { - $frequency = $this->getFrequencyRule($this->getNewValue()); + $frequency = $this->getFrequency($this->getNewValue()); switch ($frequency) { - case PhabricatorCalendarEvent::FREQUENCY_DAILY: + case PhutilCalendarRecurrenceRule::FREQUENCY_DAILY: return pht( '%s set this event to repeat daily.', $this->renderAuthor()); - case PhabricatorCalendarEvent::FREQUENCY_WEEKLY: + case PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY: return pht( '%s set this event to repeat weekly.', $this->renderAuthor()); - case PhabricatorCalendarEvent::FREQUENCY_MONTHLY: + case PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY: return pht( '%s set this event to repeat monthly.', $this->renderAuthor()); - case PhabricatorCalendarEvent::FREQUENCY_YEARLY: + case PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY: return pht( '%s set this event to repeat yearly.', $this->renderAuthor()); @@ -39,24 +72,24 @@ final class PhabricatorCalendarEventFrequencyTransaction } public function getTitleForFeed() { - $frequency = $this->getFrequencyRule($this->getNewValue()); + $frequency = $this->getFrequency($this->getNewValue()); switch ($frequency) { - case PhabricatorCalendarEvent::FREQUENCY_DAILY: + case PhutilCalendarRecurrenceRule::FREQUENCY_DAILY: return pht( '%s set %s to repeat daily.', $this->renderAuthor(), $this->renderObject()); - case PhabricatorCalendarEvent::FREQUENCY_WEEKLY: + case PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY: return pht( '%s set %s to repeat weekly.', $this->renderAuthor(), $this->renderObject()); - case PhabricatorCalendarEvent::FREQUENCY_MONTHLY: + case PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY: return pht( '%s set %s to repeat monthly.', $this->renderAuthor(), $this->renderObject()); - case PhabricatorCalendarEvent::FREQUENCY_YEARLY: + case PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY: return pht( '%s set %s to repeat yearly.', $this->renderAuthor(), @@ -64,12 +97,18 @@ final class PhabricatorCalendarEventFrequencyTransaction } } - private function getFrequencyRule($value) { + private function getFrequency($value) { + // NOTE: This is normalizing three generations of these transactions + // to use RRULE constants. It would be vaguely nice to migrate them + // for consistency. + if (is_array($value)) { $value = idx($value, 'rule'); } else { - return $value; + $value = $value; } + + return phutil_utf8_strtoupper($value); } } diff --git a/src/applications/calendar/xaction/PhabricatorCalendarEventStartDateTransaction.php b/src/applications/calendar/xaction/PhabricatorCalendarEventStartDateTransaction.php index 9823ec336f..e08bbac780 100644 --- a/src/applications/calendar/xaction/PhabricatorCalendarEventStartDateTransaction.php +++ b/src/applications/calendar/xaction/PhabricatorCalendarEventStartDateTransaction.php @@ -6,21 +6,18 @@ final class PhabricatorCalendarEventStartDateTransaction const TRANSACTIONTYPE = 'calendar.startdate'; public function generateOldValue($object) { - return $object->getDateFrom(); + // TODO: Upgrade this. + return $object->getStartDateTimeEpoch(); } public function applyInternalEffects($object, $value) { $actor = $this->getActor(); - $object->setDateFrom($value); - - $object->setAllDayDateFrom( - $object->getDateEpochForTimezone( - $value, - $actor->getTimeZone(), - 'Y-m-d', - null, - new DateTimeZone('UTC'))); + $datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch( + $value, + $actor->getTimezoneIdentifier()); + $datetime->setIsAllDay($object->getIsAllDay()); + $object->setStartDateTime($datetime); } public function getTitle() { diff --git a/src/applications/calendar/xaction/PhabricatorCalendarEventUntilDateTransaction.php b/src/applications/calendar/xaction/PhabricatorCalendarEventUntilDateTransaction.php index 454d391293..736ed13704 100644 --- a/src/applications/calendar/xaction/PhabricatorCalendarEventUntilDateTransaction.php +++ b/src/applications/calendar/xaction/PhabricatorCalendarEventUntilDateTransaction.php @@ -6,11 +6,21 @@ final class PhabricatorCalendarEventUntilDateTransaction const TRANSACTIONTYPE = 'calendar.recurrenceenddate'; public function generateOldValue($object) { - return $object->getRecurrenceEndDate(); + // TODO: Upgrade this. + return $object->getUntilDateTimeEpoch(); } public function applyInternalEffects($object, $value) { + $actor = $this->getActor(); + + // TODO: DEPRECATED. $object->setRecurrenceEndDate($value); + + $datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch( + $value, + $actor->getTimezoneIdentifier()); + $datetime->setIsAllDay($object->getIsAllDay()); + $object->setUntilDateTime($datetime); } public function getTitle() { diff --git a/src/applications/calendar/xaction/PhabricatorCalendarExportDisableTransaction.php b/src/applications/calendar/xaction/PhabricatorCalendarExportDisableTransaction.php new file mode 100644 index 0000000000..c7f00cbc46 --- /dev/null +++ b/src/applications/calendar/xaction/PhabricatorCalendarExportDisableTransaction.php @@ -0,0 +1,28 @@ +getIsDisabled(); + } + + public function applyInternalEffects($object, $value) { + $object->setIsDisabled((int)$value); + } + + public function getTitle() { + if ($this->getNewValue()) { + return pht( + '%s disabled this export.', + $this->renderAuthor()); + } else { + return pht( + '%s enabled this export.', + $this->renderAuthor()); + } + } + +} diff --git a/src/applications/calendar/xaction/PhabricatorCalendarExportModeTransaction.php b/src/applications/calendar/xaction/PhabricatorCalendarExportModeTransaction.php new file mode 100644 index 0000000000..a721428437 --- /dev/null +++ b/src/applications/calendar/xaction/PhabricatorCalendarExportModeTransaction.php @@ -0,0 +1,54 @@ +getPolicyMode(); + } + + public function applyInternalEffects($object, $value) { + $object->setPolicyMode($value); + } + + public function getTitle() { + $old_value = $this->getOldValue(); + $new_value = $this->getNewValue(); + + $old_name = PhabricatorCalendarExport::getPolicyModeName($old_value); + $new_name = PhabricatorCalendarExport::getPolicyModeName($new_value); + + return pht( + '%s changed the policy mode for this export from %s to %s.', + $this->renderAuthor(), + $this->renderValue($old_name), + $this->renderValue($new_name)); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $valid = PhabricatorCalendarExport::getPolicyModes(); + $valid = array_fuse($valid); + + foreach ($xactions as $xaction) { + $value = $xaction->getNewValue(); + + if (isset($valid[$value])) { + continue; + } + + $errors[] = $this->newInvalidError( + pht( + 'Mode "%s" is not a valid policy mode. Valid modes are: %s.', + $value, + implode(', ', $valid)), + $xaction); + } + + return $errors; + } + +} diff --git a/src/applications/calendar/xaction/PhabricatorCalendarExportNameTransaction.php b/src/applications/calendar/xaction/PhabricatorCalendarExportNameTransaction.php new file mode 100644 index 0000000000..8d624c4a69 --- /dev/null +++ b/src/applications/calendar/xaction/PhabricatorCalendarExportNameTransaction.php @@ -0,0 +1,35 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + return pht( + '%s renamed this export from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + if ($this->isEmptyTextTransaction($object->getName(), $xactions)) { + $errors[] = $this->newRequiredError( + pht('Calendar exports must have a name.')); + } + + return $errors; + } + +} diff --git a/src/applications/calendar/xaction/PhabricatorCalendarExportQueryKeyTransaction.php b/src/applications/calendar/xaction/PhabricatorCalendarExportQueryKeyTransaction.php new file mode 100644 index 0000000000..bcdecf09a3 --- /dev/null +++ b/src/applications/calendar/xaction/PhabricatorCalendarExportQueryKeyTransaction.php @@ -0,0 +1,61 @@ +getQueryKey(); + } + + public function applyInternalEffects($object, $value) { + $object->setQueryKey($value); + } + + public function getTitle() { + return pht( + '%s changed the query for this export.', + $this->renderAuthor()); + } + + public function validateTransactions($object, array $xactions) { + $actor = $this->getActor(); + + $errors = array(); + + foreach ($xactions as $xaction) { + $value = $xaction->getNewValue(); + + $query = id(new PhabricatorSavedQueryQuery()) + ->setViewer($actor) + ->withEngineClassNames(array('PhabricatorCalendarEventSearchEngine')) + ->withQueryKeys(array($value)) + ->executeOne(); + if ($query) { + continue; + } + + $builtin = id(new PhabricatorCalendarEventSearchEngine()) + ->setViewer($actor) + ->getBuiltinQueries($actor); + if (isset($builtin[$value])) { + continue; + } + + $errors[] = $this->newInvalidError( + pht( + 'Query key "%s" does not identify a valid event query.', + $value), + $xaction); + } + + if ($this->isEmptyTextTransaction($object->getQueryKey(), $xactions)) { + $errors[] = $this->newRequiredError( + pht('Calendar exports must have a query key.')); + } + + return $errors; + } + +} diff --git a/src/applications/calendar/xaction/PhabricatorCalendarExportTransactionType.php b/src/applications/calendar/xaction/PhabricatorCalendarExportTransactionType.php new file mode 100644 index 0000000000..a5343f7fab --- /dev/null +++ b/src/applications/calendar/xaction/PhabricatorCalendarExportTransactionType.php @@ -0,0 +1,4 @@ + 'http://php.net/manual/ini.list.php', - 'target' => '_blank', - ), - pht('PHP Documentation')))); + $show_standard = false; + $show_opcache = false; + + foreach ($configs as $key) { + if (preg_match('/^opcache\./', $key)) { + $show_opcache = true; + } else { + $show_standard = true; + } + } + + if ($show_standard) { + $info[] = phutil_tag( + 'p', + array(), + pht( + 'You can find more information about PHP configuration values '. + 'in the %s.', + phutil_tag( + 'a', + array( + 'href' => 'http://php.net/manual/ini.list.php', + 'target' => '_blank', + ), + pht('PHP Documentation')))); + } + + if ($show_opcache) { + $info[] = phutil_tag( + 'p', + array(), + pht( + 'You can find more information about configuring OPCache in '. + 'the %s.', + phutil_tag( + 'a', + array( + 'href' => 'http://php.net/manual/opcache.configuration.php', + 'target' => '_blank', + ), + pht('PHP OPCache Documentation')))); + } $info[] = phutil_tag( 'p', diff --git a/src/applications/conpherence/application/PhabricatorConpherenceApplication.php b/src/applications/conpherence/application/PhabricatorConpherenceApplication.php index 4bc4ab70e1..3081919503 100644 --- a/src/applications/conpherence/application/PhabricatorConpherenceApplication.php +++ b/src/applications/conpherence/application/PhabricatorConpherenceApplication.php @@ -30,20 +30,31 @@ final class PhabricatorConpherenceApplication extends PhabricatorApplication { public function getRoutes() { return array( - '/Z(?P[1-9]\d*)' => 'ConpherenceViewController', + '/Z(?P[1-9]\d*)' + => 'ConpherenceViewController', '/conpherence/' => array( - '' => 'ConpherenceListController', - 'thread/(?P[1-9]\d*)/' => 'ConpherenceListController', - '(?P[1-9]\d*)/' => 'ConpherenceViewController', + '' + => 'ConpherenceListController', + 'thread/(?P[1-9]\d*)/' + => 'ConpherenceListController', + '(?P[1-9]\d*)/' + => 'ConpherenceViewController', '(?P[1-9]\d*)/(?P[1-9]\d*)/' - => 'ConpherenceViewController', - 'columnview/' => 'ConpherenceColumnViewController', - 'new/' => 'ConpherenceNewRoomController', + => 'ConpherenceViewController', + 'columnview/' + => 'ConpherenceColumnViewController', + 'new/' + => 'ConpherenceNewRoomController', + 'picture/(?P[1-9]\d*)/' + => 'ConpherenceRoomPictureController', 'search/(?:query/(?P[^/]+)/)?' - => 'ConpherenceRoomListController', - 'panel/' => 'ConpherenceNotificationPanelController', - 'participant/(?P[1-9]\d*)/' => 'ConpherenceParticipantController', - 'update/(?P[1-9]\d*)/' => 'ConpherenceUpdateController', + => 'ConpherenceRoomListController', + 'panel/' + => 'ConpherenceNotificationPanelController', + 'participant/(?P[1-9]\d*)/' + => 'ConpherenceParticipantController', + 'update/(?P[1-9]\d*)/' + => 'ConpherenceUpdateController', ), ); } diff --git a/src/applications/conpherence/constants/ConpherenceImageData.php b/src/applications/conpherence/constants/ConpherenceImageData.php deleted file mode 100644 index 41f00a9850..0000000000 --- a/src/applications/conpherence/constants/ConpherenceImageData.php +++ /dev/null @@ -1,11 +0,0 @@ -setViewer($user) ->withPHIDs($conpherence_phids) - ->needCropPics(true) + ->needProfileImage(true) ->needParticipantCache(true) ->execute(); $latest_conpherences = mpull($latest_conpherences, null, 'getPHID'); @@ -31,7 +31,7 @@ final class ConpherenceColumnViewController extends $conpherence = id(new ConpherenceThreadQuery()) ->setViewer($user) ->withIDs(array($request->getInt('id'))) - ->needCropPics(true) + ->needProfileImage(true) ->needTransactions(true) ->setTransactionLimit(ConpherenceThreadQuery::TRANSACTION_LIMIT) ->executeOne(); @@ -41,7 +41,7 @@ final class ConpherenceColumnViewController extends $conpherence = id(new ConpherenceThreadQuery()) ->setViewer($user) ->withPHIDs(array($participant->getConpherencePHID())) - ->needCropPics(true) + ->needProfileImage(true) ->needTransactions(true) ->setTransactionLimit(ConpherenceThreadQuery::TRANSACTION_LIMIT) ->executeOne(); diff --git a/src/applications/conpherence/controller/ConpherenceController.php b/src/applications/conpherence/controller/ConpherenceController.php index c4750a08a8..389a8ce9b7 100644 --- a/src/applications/conpherence/controller/ConpherenceController.php +++ b/src/applications/conpherence/controller/ConpherenceController.php @@ -59,13 +59,18 @@ abstract class ConpherenceController extends PhabricatorController { $header = id(new PHUIHeaderView()) ->setHeader($data['title']) ->setSubheader($data['topic']) - ->addClass((!$data['topic']) ? 'conpherence-no-topic' : null); + ->setImage($data['image']); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $conpherence, PhabricatorPolicyCapability::CAN_EDIT); + if ($can_edit) { + $header->setImageURL( + $this->getApplicationURI('picture/'.$conpherence->getID().'/')); + } + $participating = $conpherence->getParticipantIfExists($viewer->getPHID()); $can_join = PhabricatorPolicyFilter::hasCapability( $viewer, diff --git a/src/applications/conpherence/controller/ConpherenceListController.php b/src/applications/conpherence/controller/ConpherenceListController.php index 6f06a36fb0..4e81526004 100644 --- a/src/applications/conpherence/controller/ConpherenceListController.php +++ b/src/applications/conpherence/controller/ConpherenceListController.php @@ -158,7 +158,7 @@ final class ConpherenceListController extends ConpherenceController { $conpherences = id(new ConpherenceThreadQuery()) ->setViewer($user) ->withPHIDs($conpherence_phids) - ->needCropPics(true) + ->needProfileImage(true) ->needParticipantCache(true) ->execute(); diff --git a/src/applications/conpherence/controller/ConpherenceNotificationPanelController.php b/src/applications/conpherence/controller/ConpherenceNotificationPanelController.php index 63e777818c..c72490f60f 100644 --- a/src/applications/conpherence/controller/ConpherenceNotificationPanelController.php +++ b/src/applications/conpherence/controller/ConpherenceNotificationPanelController.php @@ -6,6 +6,7 @@ final class ConpherenceNotificationPanelController public function handleRequest(AphrontRequest $request) { $user = $request->getUser(); $conpherences = array(); + require_celerity_resource('conpherence-notification-css'); $unread_status = ConpherenceParticipationStatus::BEHIND; $participant_data = id(new ConpherenceParticipantQuery()) @@ -17,7 +18,7 @@ final class ConpherenceNotificationPanelController $conpherences = id(new ConpherenceThreadQuery()) ->setViewer($user) ->withPHIDs(array_keys($participant_data)) - ->needCropPics(true) + ->needProfileImage(true) ->needTransactions(true) ->setTransactionLimit(3 * 5) ->needParticipantCache(true) @@ -25,7 +26,6 @@ final class ConpherenceNotificationPanelController } if ($conpherences) { - require_celerity_resource('conpherence-notification-css'); // re-order the conpherences based on participation data $conpherences = array_select_keys( $conpherences, array_keys($participant_data)); @@ -70,13 +70,20 @@ final class ConpherenceNotificationPanelController } $content = $view->render(); } else { + $rooms_uri = phutil_tag( + 'a', + array( + 'href' => '/conpherence/', + 'class' => 'no-room-notification', + ), + pht('You have joined no rooms.')); + $content = phutil_tag_div( - 'phabricator-notification no-notifications', - pht('You have no messages.')); + 'phabricator-notification no-notifications', $rooms_uri); } $content = hsprintf( - '
%s
'. + '
%s%s
'. '%s', phutil_tag( 'a', @@ -84,6 +91,7 @@ final class ConpherenceNotificationPanelController 'href' => '/conpherence/', ), pht('Rooms')), + $this->renderPersistentOption(), $content); $unread = id(new ConpherenceParticipantCountQuery()) @@ -100,4 +108,32 @@ final class ConpherenceNotificationPanelController return id(new AphrontAjaxResponse())->setContent($json); } + private function renderPersistentOption() { + $viewer = $this->getViewer(); + $column_key = PhabricatorConpherenceColumnVisibleSetting::SETTINGKEY; + $show = (bool)$viewer->getUserSetting($column_key, false); + + $view = phutil_tag( + 'div', + array( + 'class' => 'persistent-option', + ), + array( + javelin_tag( + 'input', + array( + 'type' => 'checkbox', + 'checked' => ($show) ? 'checked' : null, + 'value' => !$show, + 'sigil' => 'conpherence-persist-column', + )), + phutil_tag( + 'span', + array(), + pht('Persistent Chat')), + )); + + return $view; + } + } diff --git a/src/applications/conpherence/controller/ConpherenceRoomPictureController.php b/src/applications/conpherence/controller/ConpherenceRoomPictureController.php new file mode 100644 index 0000000000..8ddeae7098 --- /dev/null +++ b/src/applications/conpherence/controller/ConpherenceRoomPictureController.php @@ -0,0 +1,234 @@ +getViewer(); + $id = $request->getURIData('id'); + + $conpherence = id(new ConpherenceThreadQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->needProfileImage(true) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$conpherence) { + return new Aphront404Response(); + } + + $monogram = $conpherence->getMonogram(); + + $supported_formats = PhabricatorFile::getTransformableImageFormats(); + $e_file = true; + $errors = array(); + + if ($request->isFormPost()) { + $phid = $request->getStr('phid'); + $is_default = false; + if ($phid == PhabricatorPHIDConstants::PHID_VOID) { + $phid = null; + $is_default = true; + } else if ($phid) { + $file = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($phid)) + ->executeOne(); + } else { + if ($request->getFileExists('picture')) { + $file = PhabricatorFile::newFromPHPUpload( + $_FILES['picture'], + array( + 'authorPHID' => $viewer->getPHID(), + 'canCDN' => true, + )); + } else { + $e_file = pht('Required'); + $errors[] = pht( + 'You must choose a file when uploading a new room picture.'); + } + } + + if (!$errors && !$is_default) { + if (!$file->isTransformableImage()) { + $e_file = pht('Not Supported'); + $errors[] = pht( + 'This server only supports these image formats: %s.', + implode(', ', $supported_formats)); + } else { + $xform = PhabricatorFileTransform::getTransformByKey( + PhabricatorFileThumbnailTransform::TRANSFORM_PROFILE); + $xformed = $xform->executeTransform($file); + } + } + + if (!$errors) { + if ($is_default) { + $new_value = null; + } else { + $xformed->attachToObject($conpherence->getPHID()); + $new_value = $xformed->getPHID(); + } + + $xactions = array(); + $xactions[] = id(new ConpherenceTransaction()) + ->setTransactionType(ConpherenceTransaction::TYPE_PICTURE) + ->setNewValue($new_value); + + $editor = id(new ConpherenceEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true); + + $editor->applyTransactions($conpherence, $xactions); + + return id(new AphrontRedirectResponse())->setURI('/'.$monogram); + } + } + + $title = pht('Edit Room Picture'); + + $form = id(new PHUIFormLayoutView()) + ->setUser($viewer); + + $default_image = PhabricatorFile::loadBuiltin($viewer, 'conpherence.png'); + + $images = array(); + + $current = $conpherence->getProfileImagePHID(); + $has_current = false; + if ($current) { + $file = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($current)) + ->executeOne(); + if ($file) { + if ($file->isTransformableImage()) { + $has_current = true; + $images[$current] = array( + 'uri' => $file->getBestURI(), + 'tip' => pht('Current Picture'), + ); + } + } + } + + $images[PhabricatorPHIDConstants::PHID_VOID] = array( + 'uri' => $default_image->getBestURI(), + 'tip' => pht('Default Picture'), + ); + + require_celerity_resource('people-profile-css'); + Javelin::initBehavior('phabricator-tooltips', array()); + + $buttons = array(); + foreach ($images as $phid => $spec) { + $button = javelin_tag( + 'button', + array( + 'class' => 'grey profile-image-button', + 'sigil' => 'has-tooltip', + 'meta' => array( + 'tip' => $spec['tip'], + 'size' => 300, + ), + ), + phutil_tag( + 'img', + array( + 'height' => 50, + 'width' => 50, + 'src' => $spec['uri'], + ))); + + $button = array( + phutil_tag( + 'input', + array( + 'type' => 'hidden', + 'name' => 'phid', + 'value' => $phid, + )), + $button, + ); + + $button = phabricator_form( + $viewer, + array( + 'class' => 'profile-image-form', + 'method' => 'POST', + ), + $button); + + $buttons[] = $button; + } + + if ($has_current) { + $form->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Current Picture')) + ->setValue(array_shift($buttons))); + } + + $form->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Use Picture')) + ->setValue($buttons)); + + $form_box = id(new PHUIObjectBoxView()) + ->setHeaderText($title) + ->setFormErrors($errors) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setForm($form); + + $upload_form = id(new AphrontFormView()) + ->setUser($viewer) + ->setEncType('multipart/form-data') + ->appendChild( + id(new AphrontFormFileControl()) + ->setName('picture') + ->setLabel(pht('Upload Picture')) + ->setError($e_file) + ->setCaption( + pht('Supported formats: %s', implode(', ', $supported_formats)))) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->addCancelButton('/'.$monogram) + ->setValue(pht('Upload Picture'))); + + $upload_box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Upload New Picture')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setForm($upload_form); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb($conpherence->getTitle(), '/'.$monogram); + $crumbs->addTextCrumb(pht('Room Picture')); + $crumbs->setBorder(true); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Edit Room Picture')) + ->setHeaderIcon('fa-camera'); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter(array( + $form_box, + $upload_box, + )); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild( + array( + $view, + )); + + } +} diff --git a/src/applications/conpherence/controller/ConpherenceUpdateController.php b/src/applications/conpherence/controller/ConpherenceUpdateController.php index 5a1ad4e8fa..db21b3d02f 100644 --- a/src/applications/conpherence/controller/ConpherenceUpdateController.php +++ b/src/applications/conpherence/controller/ConpherenceUpdateController.php @@ -36,8 +36,6 @@ final class ConpherenceUpdateController $conpherence = id(new ConpherenceThreadQuery()) ->setViewer($user) ->withIDs(array($conpherence_id)) - ->needOrigPics(true) - ->needCropPics(true) ->needParticipants($need_participants) ->requireCapabilities($needed_capabilities) ->executeOne(); @@ -131,57 +129,14 @@ final class ConpherenceUpdateController break; case ConpherenceUpdateActions::METADATA: - $top = $request->getInt('image_y'); - $left = $request->getInt('image_x'); - $file_id = $request->getInt('file_id'); $title = $request->getStr('title'); $topic = $request->getStr('topic'); - if ($file_id) { - $orig_file = id(new PhabricatorFileQuery()) - ->setViewer($user) - ->withIDs(array($file_id)) - ->executeOne(); - $xactions[] = id(new ConpherenceTransaction()) - ->setTransactionType(ConpherenceTransaction::TYPE_PICTURE) - ->setNewValue($orig_file); - $okay = $orig_file->isTransformableImage(); - if ($okay) { - $xformer = new PhabricatorImageTransformer(); - $crop_file = $xformer->executeConpherenceTransform( - $orig_file, - 0, - 0, - ConpherenceImageData::CROP_WIDTH, - ConpherenceImageData::CROP_HEIGHT); - $xactions[] = id(new ConpherenceTransaction()) - ->setTransactionType( - ConpherenceTransaction::TYPE_PICTURE_CROP) - ->setNewValue($crop_file->getPHID()); - } - $response_mode = 'redirect'; - } // all other metadata updates are continue requests if (!$request->isContinueRequest()) { break; } - if ($top !== null || $left !== null) { - $file = $conpherence->getImage(ConpherenceImageData::SIZE_ORIG); - $xformer = new PhabricatorImageTransformer(); - $xformed = $xformer->executeConpherenceTransform( - $file, - $top, - $left, - ConpherenceImageData::CROP_WIDTH, - ConpherenceImageData::CROP_HEIGHT); - $image_phid = $xformed->getPHID(); - - $xactions[] = id(new ConpherenceTransaction()) - ->setTransactionType( - ConpherenceTransaction::TYPE_PICTURE_CROP) - ->setNewValue($image_phid); - } $title = $request->getStr('title'); $topic = $request->getStr('topic'); $xactions[] = id(new ConpherenceTransaction()) @@ -491,35 +446,6 @@ final class ConpherenceUpdateController ->setName('topic') ->setValue($conpherence->getTopic())); - $nopic = $this->getRequest()->getExists('nopic'); - $image = $conpherence->getImage(ConpherenceImageData::SIZE_ORIG); - if ($nopic) { - // do not render any pic related controls - } else if ($image) { - $crop_uri = $conpherence->loadImageURI(ConpherenceImageData::SIZE_CROP); - $form - ->appendChild( - id(new AphrontFormMarkupControl()) - ->setLabel(pht('Image')) - ->setValue(phutil_tag( - 'img', - array( - 'src' => $crop_uri, - )))) - ->appendChild( - id(new ConpherencePicCropControl()) - ->setLabel(pht('Crop Image')) - ->setValue($image)) - ->appendChild( - id(new ConpherenceFormDragAndDropUploadControl()) - ->setLabel(pht('Change Image'))); - } else { - $form - ->appendChild( - id(new ConpherenceFormDragAndDropUploadControl()) - ->setLabel(pht('Image'))); - } - $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($conpherence) @@ -571,7 +497,6 @@ final class ConpherenceUpdateController $latest_transaction_id) { $minimal_display = $this->getRequest()->getExists('minimal_display'); - $need_widget_data = false; $need_transactions = false; $need_participant_cache = true; switch ($action) { @@ -582,7 +507,6 @@ final class ConpherenceUpdateController case ConpherenceUpdateActions::MESSAGE: case ConpherenceUpdateActions::ADD_PERSON: $need_transactions = true; - $need_widget_data = !$minimal_display; break; case ConpherenceUpdateActions::REMOVE_PERSON: case ConpherenceUpdateActions::NOTIFICATIONS: @@ -594,7 +518,7 @@ final class ConpherenceUpdateController $conpherence = id(new ConpherenceThreadQuery()) ->setViewer($user) ->setAfterTransactionID($latest_transaction_id) - ->needCropPics(true) + ->needProfileImage(true) ->needParticipantCache($need_participant_cache) ->needParticipants(true) ->needTransactions($need_transactions) @@ -607,8 +531,13 @@ final class ConpherenceUpdateController $user, $conpherence, !$minimal_display); - $participant_obj = $conpherence->getParticipant($user->getPHID()); - $participant_obj->markUpToDate($conpherence, $data['latest_transaction']); + $key = PhabricatorConpherenceColumnMinimizeSetting::SETTINGKEY; + $minimized = $user->getUserSetting($key); + if (!$minimized) { + $participant_obj = $conpherence->getParticipant($user->getPHID()); + $participant_obj + ->markUpToDate($conpherence, $data['latest_transaction']); + } } else if ($need_transactions) { $non_update = true; $data = array(); diff --git a/src/applications/conpherence/controller/ConpherenceViewController.php b/src/applications/conpherence/controller/ConpherenceViewController.php index 4aec1d0e0a..029a6115a2 100644 --- a/src/applications/conpherence/controller/ConpherenceViewController.php +++ b/src/applications/conpherence/controller/ConpherenceViewController.php @@ -19,7 +19,7 @@ final class ConpherenceViewController extends $query = id(new ConpherenceThreadQuery()) ->setViewer($user) ->withIDs(array($conpherence_id)) - ->needCropPics(true) + ->needProfileImage(true) ->needParticipantCache(true) ->needTransactions(true) ->setTransactionLimit($this->getMainQueryLimit()); diff --git a/src/applications/conpherence/editor/ConpherenceEditor.php b/src/applications/conpherence/editor/ConpherenceEditor.php index 8507e9817f..2a57711840 100644 --- a/src/applications/conpherence/editor/ConpherenceEditor.php +++ b/src/applications/conpherence/editor/ConpherenceEditor.php @@ -91,7 +91,6 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { $types[] = ConpherenceTransaction::TYPE_TOPIC; $types[] = ConpherenceTransaction::TYPE_PARTICIPANTS; $types[] = ConpherenceTransaction::TYPE_PICTURE; - $types[] = ConpherenceTransaction::TYPE_PICTURE_CROP; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; $types[] = PhabricatorTransactions::TYPE_JOIN_POLICY; @@ -109,9 +108,7 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { case ConpherenceTransaction::TYPE_TOPIC: return $object->getTopic(); case ConpherenceTransaction::TYPE_PICTURE: - return $object->getImagePHID(ConpherenceImageData::SIZE_ORIG); - case ConpherenceTransaction::TYPE_PICTURE_CROP: - return $object->getImagePHID(ConpherenceImageData::SIZE_CROP); + return $object->getProfileImagePHID(); case ConpherenceTransaction::TYPE_PARTICIPANTS: if ($this->getIsNewObject()) { return array(); @@ -127,11 +124,8 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { switch ($xaction->getTransactionType()) { case ConpherenceTransaction::TYPE_TITLE: case ConpherenceTransaction::TYPE_TOPIC: - case ConpherenceTransaction::TYPE_PICTURE_CROP: - return $xaction->getNewValue(); case ConpherenceTransaction::TYPE_PICTURE: - $file = $xaction->getNewValue(); - return $file->getPHID(); + return $xaction->getNewValue(); case ConpherenceTransaction::TYPE_PARTICIPANTS: return $this->getPHIDTransactionNewValue($xaction); } @@ -224,14 +218,7 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { $object->setTopic($xaction->getNewValue()); break; case ConpherenceTransaction::TYPE_PICTURE: - $object->setImagePHID( - $xaction->getNewValue(), - ConpherenceImageData::SIZE_ORIG); - break; - case ConpherenceTransaction::TYPE_PICTURE_CROP: - $object->setImagePHID( - $xaction->getNewValue(), - ConpherenceImageData::SIZE_CROP); + $object->setProfileImagePHID($xaction->getNewValue()); break; case ConpherenceTransaction::TYPE_PARTICIPANTS: if (!$this->getIsNewObject()) { @@ -339,6 +326,10 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { PhabricatorLiskDAO $object, array $xactions) { + if (!$xactions) { + return $xactions; + } + $message_count = 0; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { @@ -571,19 +562,6 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { return true; } - protected function extractFilePHIDsFromCustomTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case ConpherenceTransaction::TYPE_PICTURE: - case ConpherenceTransaction::TYPE_PICTURE_CROP: - return array($xaction->getNewValue()); - } - - return parent::extractFilePHIDsFromCustomTransaction($object, $xaction); - } - protected function validateTransaction( PhabricatorLiskDAO $object, $type, @@ -612,21 +590,6 @@ final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor { $errors[] = $error; } break; - case ConpherenceTransaction::TYPE_PICTURE: - foreach ($xactions as $xaction) { - $file = $xaction->getNewValue(); - if (!$file->isTransformableImage()) { - $detail = pht('This server only supports these image formats: %s.', - implode(', ', PhabricatorFile::getTransformableImageFormats())); - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - $detail, - last($xactions)); - $errors[] = $error; - } - } - break; case ConpherenceTransaction::TYPE_PARTICIPANTS: foreach ($xactions as $xaction) { $new_phids = $this->getPHIDTransactionNewValue($xaction, array()); diff --git a/src/applications/conpherence/query/ConpherenceThreadQuery.php b/src/applications/conpherence/query/ConpherenceThreadQuery.php index c3d5322c15..640a19a612 100644 --- a/src/applications/conpherence/query/ConpherenceThreadQuery.php +++ b/src/applications/conpherence/query/ConpherenceThreadQuery.php @@ -9,14 +9,13 @@ final class ConpherenceThreadQuery private $ids; private $participantPHIDs; private $needParticipants; - private $needCropPics; - private $needOrigPics; private $needTransactions; private $needParticipantCache; private $afterTransactionID; private $beforeTransactionID; private $transactionLimit; private $fulltext; + private $needProfileImage; public function needParticipantCache($participant_cache) { $this->needParticipantCache = $participant_cache; @@ -28,13 +27,8 @@ final class ConpherenceThreadQuery return $this; } - public function needCropPics($need) { - $this->needCropPics = $need; - return $this; - } - - public function needOrigPics($need_widget_data) { - $this->needOrigPics = $need_widget_data; + public function needProfileImage($need) { + $this->needProfileImage = $need; return $this; } @@ -110,14 +104,33 @@ final class ConpherenceThreadQuery if ($this->needTransactions) { $this->loadTransactionsAndHandles($conpherences); } - if ($this->needOrigPics || $this->needCropPics) { - $this->initImages($conpherences); - } - if ($this->needOrigPics) { - $this->loadOrigPics($conpherences); - } - if ($this->needCropPics) { - $this->loadCropPics($conpherences); + if ($this->needProfileImage) { + $default = null; + $file_phids = mpull($conpherences, 'getProfileImagePHID'); + $file_phids = array_filter($file_phids); + if ($file_phids) { + $files = id(new PhabricatorFileQuery()) + ->setParentQuery($this) + ->setViewer($this->getViewer()) + ->withPHIDs($file_phids) + ->execute(); + $files = mpull($files, null, 'getPHID'); + } else { + $files = array(); + } + + foreach ($conpherences as $conpherence) { + $file = idx($files, $conpherence->getProfileImagePHID()); + if (!$file) { + if (!$default) { + $default = PhabricatorFile::loadBuiltin( + $this->getViewer(), + 'conpherence.png'); + } + $file = $default; + } + $conpherence->attachProfileImageFile($file); + } } } @@ -266,50 +279,6 @@ final class ConpherenceThreadQuery return $this; } - private function loadOrigPics(array $conpherences) { - return $this->loadPics( - $conpherences, - ConpherenceImageData::SIZE_ORIG); - } - - private function loadCropPics(array $conpherences) { - return $this->loadPics( - $conpherences, - ConpherenceImageData::SIZE_CROP); - } - - private function initImages($conpherences) { - foreach ($conpherences as $conpherence) { - $conpherence->attachImages(array()); - } - } - - private function loadPics(array $conpherences, $size) { - $conpherence_pic_phids = array(); - foreach ($conpherences as $conpherence) { - $phid = $conpherence->getImagePHID($size); - if ($phid) { - $conpherence_pic_phids[$conpherence->getPHID()] = $phid; - } - } - - if (!$conpherence_pic_phids) { - return $this; - } - - $files = id(new PhabricatorFileQuery()) - ->setViewer($this->getViewer()) - ->withPHIDs($conpherence_pic_phids) - ->execute(); - $files = mpull($files, null, 'getPHID'); - - foreach ($conpherence_pic_phids as $conpherence_phid => $pic_phid) { - $conpherences[$conpherence_phid]->setImage($files[$pic_phid], $size); - } - - return $this; - } - public function getQueryApplicationClass() { return 'PhabricatorConpherenceApplication'; } diff --git a/src/applications/conpherence/storage/ConpherenceThread.php b/src/applications/conpherence/storage/ConpherenceThread.php index 80efc92f5a..36930f6151 100644 --- a/src/applications/conpherence/storage/ConpherenceThread.php +++ b/src/applications/conpherence/storage/ConpherenceThread.php @@ -9,7 +9,8 @@ final class ConpherenceThread extends ConpherenceDAO protected $title; protected $topic; - protected $imagePHIDs = array(); + protected $imagePHIDs = array(); // TODO; nuke after migrations + protected $profileImagePHID; protected $messageCount; protected $recentParticipantPHIDs = array(); protected $mailKey; @@ -19,8 +20,8 @@ final class ConpherenceThread extends ConpherenceDAO private $participants = self::ATTACHABLE; private $transactions = self::ATTACHABLE; + private $profileImageFile = self::ATTACHABLE; private $handles = self::ATTACHABLE; - private $images = self::ATTACHABLE; public static function initializeNewRoom(PhabricatorUser $sender) { $default_policy = id(new ConpherenceThreadMembersPolicyRule()) @@ -30,7 +31,6 @@ final class ConpherenceThread extends ConpherenceDAO ->setTitle('') ->setTopic('') ->attachParticipants(array()) - ->attachImages(array()) ->setViewPolicy($default_policy) ->setEditPolicy($default_policy) ->setJoinPolicy($default_policy); @@ -49,6 +49,7 @@ final class ConpherenceThread extends ConpherenceDAO 'messageCount' => 'uint64', 'mailKey' => 'text20', 'joinPolicy' => 'policy', + 'profileImagePHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, @@ -76,46 +77,21 @@ final class ConpherenceThread extends ConpherenceDAO return 'Z'.$this->getID(); } - public function getImagePHID($size) { - $image_phids = $this->getImagePHIDs(); - return idx($image_phids, $size); - } - public function setImagePHID($phid, $size) { - $image_phids = $this->getImagePHIDs(); - $image_phids[$size] = $phid; - return $this->setImagePHIDs($image_phids); - } - - public function getImage($size) { - $images = $this->getImages(); - return idx($images, $size); - } - public function setImage(PhabricatorFile $file, $size) { - $files = $this->getImages(); - $files[$size] = $file; - return $this->attachImages($files); - } - public function attachImages(array $files) { - assert_instances_of($files, 'PhabricatorFile'); - $this->images = $files; - return $this; - } - private function getImages() { - return $this->assertAttached($this->images); - } - public function attachParticipants(array $participants) { assert_instances_of($participants, 'ConpherenceParticipant'); $this->participants = $participants; return $this; } + public function getParticipants() { return $this->assertAttached($this->participants); } + public function getParticipant($phid) { $participants = $this->getParticipants(); return $participants[$phid]; } + public function getParticipantIfExists($phid, $default = null) { $participants = $this->getParticipants(); return idx($participants, $phid, $default); @@ -131,6 +107,7 @@ final class ConpherenceThread extends ConpherenceDAO $this->handles = $handles; return $this; } + public function getHandles() { return $this->assertAttached($this->handles); } @@ -140,9 +117,11 @@ final class ConpherenceThread extends ConpherenceDAO $this->transactions = $transactions; return $this; } + public function getTransactions($assert_attached = true) { return $this->assertAttached($this->transactions); } + public function hasAttachedTransactions() { return $this->transactions !== self::ATTACHABLE; } @@ -156,14 +135,17 @@ final class ConpherenceThread extends ConpherenceDAO $amount); } - public function loadImageURI($size) { - $file = $this->getImage($size); + public function getProfileImageURI() { + return $this->getProfileImageFile()->getBestURI(); + } - if ($file) { - return $file->getBestURI(); - } + public function attachProfileImageFile(PhabricatorFile $file) { + $this->profileImageFile = $file; + return $this; + } - return PhabricatorUser::getDefaultProfileImageURI(); + public function getProfileImageFile() { + return $this->assertAttached($this->profileImageFile); } /** @@ -273,22 +255,26 @@ final class ConpherenceThread extends ConpherenceDAO $lucky_handle = reset($handles); } - $img_src = null; - $size = ConpherenceImageData::SIZE_CROP; - if ($this->getImagePHID($size)) { - $img_src = $this->getImage($size)->getBestURI(); - } else if ($lucky_handle) { - $img_src = $lucky_handle->getImageURI(); - } + $img_src = $this->getProfileImageURI(); $message_title = null; if ($subtitle_mode == 'message') { $message_transaction = null; + $action_transaction = null; foreach ($transactions as $transaction) { + if ($message_transaction || $action_transaction) { + break; + } switch ($transaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: $message_transaction = $transaction; - break 2; + break; + case ConpherenceTransaction::TYPE_TITLE: + case ConpherenceTransaction::TYPE_TOPIC: + case ConpherenceTransaction::TYPE_PICTURE: + case ConpherenceTransaction::TYPE_PARTICIPANTS: + $action_transaction = $transaction; + break; default: break; } @@ -303,6 +289,11 @@ final class ConpherenceThread extends ConpherenceDAO ->truncateString( $message_transaction->getComment()->getContent())); } + if ($action_transaction) { + $message_title = id(clone $action_transaction) + ->setRenderingTarget(PhabricatorApplicationTransaction::TARGET_TEXT) + ->getTitle(); + } } switch ($subtitle_mode) { case 'recent': diff --git a/src/applications/conpherence/storage/ConpherenceTransaction.php b/src/applications/conpherence/storage/ConpherenceTransaction.php index 29d23dfbc3..b311c80ca5 100644 --- a/src/applications/conpherence/storage/ConpherenceTransaction.php +++ b/src/applications/conpherence/storage/ConpherenceTransaction.php @@ -7,7 +7,7 @@ final class ConpherenceTransaction extends PhabricatorApplicationTransaction { const TYPE_PARTICIPANTS = 'participants'; const TYPE_DATE_MARKER = 'date-marker'; const TYPE_PICTURE = 'picture'; - const TYPE_PICTURE_CROP = 'picture-crop'; + const TYPE_PICTURE_CROP = 'picture-crop'; // TODO: Nuke these from DB. public function getApplicationName() { return 'conpherence'; diff --git a/src/applications/conpherence/view/ConpherenceDurableColumnView.php b/src/applications/conpherence/view/ConpherenceDurableColumnView.php index ed5c072c5d..786cbea174 100644 --- a/src/applications/conpherence/view/ConpherenceDurableColumnView.php +++ b/src/applications/conpherence/view/ConpherenceDurableColumnView.php @@ -150,13 +150,14 @@ final class ConpherenceDurableColumnView extends AphrontTagView { $icon_bar = null; if ($this->conpherences) { - $icon_bar = phutil_tag( - 'div', - array( - 'class' => 'conpherence-durable-column-icon-bar', - ), - $this->buildIconBar()); + $icon_bar = $this->buildIconBar(); } + $icon_bar = phutil_tag( + 'div', + array( + 'class' => 'conpherence-durable-column-icon-bar', + ), + $icon_bar); $transactions = $this->buildTransactions(); @@ -198,19 +199,6 @@ final class ConpherenceDurableColumnView extends AphrontTagView { ); } - private function getPolicyIcon( - ConpherenceThread $conpherence, - array $policy_objects) { - - assert_instances_of($policy_objects, 'PhabricatorPolicy'); - - $icon = $conpherence->getPolicyIconName($policy_objects); - $icon = id(new PHUIIconView()) - ->addClass('mmr') - ->setIcon($icon); - return $icon; - } - private function buildIconBar() { $icons = array(); $selected_conpherence = $this->getSelectedConpherence(); @@ -222,12 +210,10 @@ final class ConpherenceDurableColumnView extends AphrontTagView { $classes[] = 'selected'; } $data = $conpherence->getDisplayData($this->getUser()); - $icon = $this->getPolicyIcon($conpherence, $this->getPolicyObjects()); $thread_title = phutil_tag( 'span', array(), array( - $icon, $data['title'], )); $image = $data['image']; @@ -324,17 +310,20 @@ final class ConpherenceDurableColumnView extends AphrontTagView { ->addMenuItem($minimize) ->addClass('phabricator-application-menu'); - $header = null; if ($conpherence) { $data = $conpherence->getDisplayData($this->getUser()); $header = phutil_tag( 'span', array(), - array( - $this->getPolicyIcon($conpherence, $this->getPolicyObjects()), - $data['title'], - )); - } + $data['title']); + } else { + $header = phutil_tag( + 'span', + array(), + pht('Conpherence')); + } + + $status = new PhabricatorNotificationStatusView(); return phutil_tag( @@ -343,6 +332,7 @@ final class ConpherenceDurableColumnView extends AphrontTagView { 'class' => 'conpherence-durable-column-header-inner', ), array( + $status, javelin_tag( 'div', array( @@ -403,22 +393,22 @@ final class ConpherenceDurableColumnView extends AphrontTagView { if (!$this->getVisible() || $this->getInitialLoad()) { return pht('Loading...'); } - return array( + $view = array( phutil_tag( 'div', array( - 'class' => 'mmb', + 'class' => 'column-no-rooms-text', ), - pht('You are not in any rooms yet.')), + pht('You have not joined any rooms yet.')), javelin_tag( 'a', array( - 'href' => '/conpherence/new/', + 'href' => '/conpherence/search/', 'class' => 'button grey', - 'sigil' => 'workflow', ), - pht('Create a Room')), + pht('Find Rooms')), ); + return phutil_tag_div('column-no-rooms', $view); } $data = ConpherenceTransactionRenderer::renderTransactions( diff --git a/src/applications/conpherence/view/ConpherencePicCropControl.php b/src/applications/conpherence/view/ConpherencePicCropControl.php deleted file mode 100644 index 2cb869f93c..0000000000 --- a/src/applications/conpherence/view/ConpherencePicCropControl.php +++ /dev/null @@ -1,78 +0,0 @@ -getValue(); - - if ($file === null) { - return phutil_tag( - 'img', - array( - 'src' => PhabricatorUser::getDefaultProfileImageURI(), - ), - ''); - } - - $c_id = celerity_generate_unique_node_id(); - $metadata = $file->getMetadata(); - $scale = PhabricatorImageTransformer::getScaleForCrop( - $file, - $width, - $height); - - Javelin::initBehavior( - 'aphront-crop', - array( - 'cropBoxID' => $c_id, - 'width' => $width, - 'height' => $height, - 'scale' => $scale, - 'imageH' => $metadata[PhabricatorFile::METADATA_IMAGE_HEIGHT], - 'imageW' => $metadata[PhabricatorFile::METADATA_IMAGE_WIDTH], - )); - - return javelin_tag( - 'div', - array( - 'id' => $c_id, - 'sigil' => 'crop-box', - 'mustcapture' => true, - 'class' => 'crop-box', - ), - array( - javelin_tag( - 'img', - array( - 'src' => $file->getBestURI(), - 'class' => 'crop-image', - 'sigil' => 'crop-image', - ), - ''), - javelin_tag( - 'input', - array( - 'type' => 'hidden', - 'name' => 'image_x', - 'sigil' => 'crop-x', - ), - ''), - javelin_tag( - 'input', - array( - 'type' => 'hidden', - 'name' => 'image_y', - 'sigil' => 'crop-y', - ), - ''), - )); - } - -} diff --git a/src/applications/conpherence/view/ConpherenceTransactionView.php b/src/applications/conpherence/view/ConpherenceTransactionView.php index 3b1c29ebcb..12123aafcd 100644 --- a/src/applications/conpherence/view/ConpherenceTransactionView.php +++ b/src/applications/conpherence/view/ConpherenceTransactionView.php @@ -228,7 +228,6 @@ final class ConpherenceTransactionView extends AphrontView { case ConpherenceTransaction::TYPE_TITLE: case ConpherenceTransaction::TYPE_TOPIC: case ConpherenceTransaction::TYPE_PICTURE: - case ConpherenceTransaction::TYPE_PICTURE_CROP: case ConpherenceTransaction::TYPE_PARTICIPANTS: case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: diff --git a/src/applications/diffusion/management/DiffusionRepositoryActionsManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryActionsManagementPanel.php index 3a82879b26..1f47a68de3 100644 --- a/src/applications/diffusion/management/DiffusionRepositoryActionsManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositoryActionsManagementPanel.php @@ -78,7 +78,7 @@ final class DiffusionRepositoryActionsManagementPanel $autoclose = phutil_tag('em', array(), $autoclose); $view->addProperty(pht('Autoclose'), $autoclose); - return $this->newBox(pht('Branches'), $view); + return $this->newBox(pht('Actions'), $view); } } diff --git a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php index 0022940542..c73ab12690 100644 --- a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php +++ b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php @@ -421,7 +421,7 @@ final class ManiphestTaskSearchEngine 'you need to get done. Tasks assigned to you will appear here.')) ->addAction($create_button); - return $view; + return $view; } } diff --git a/src/applications/notification/view/PhabricatorNotificationStatusView.php b/src/applications/notification/view/PhabricatorNotificationStatusView.php index 988587f65c..e962395bba 100644 --- a/src/applications/notification/view/PhabricatorNotificationStatusView.php +++ b/src/applications/notification/view/PhabricatorNotificationStatusView.php @@ -16,6 +16,20 @@ final class PhabricatorNotificationStatusView extends AphrontTagView { 'open' => pht('Connected'), 'closed' => pht('Disconnected'), ), + 'icon' => array( + 'open' => array( + 'icon' => 'fa-circle', + 'color' => 'green', + ), + 'setup' => array( + 'icon' => 'fa-circle', + 'color' => 'yellow', + ), + 'closed' => array( + 'icon' => 'fa-circle', + 'color' => 'red', + ), + ), )); return array( @@ -26,9 +40,33 @@ final class PhabricatorNotificationStatusView extends AphrontTagView { protected function getTagContent() { $have = PhabricatorEnv::getEnvConfig('notification.servers'); if ($have) { - return pht('Connecting...'); + $icon = id(new PHUIIconView()) + ->setIcon('fa-circle-o yellow'); + $text = pht('Connecting...'); + return phutil_tag( + 'span', + array( + 'class' => 'connection-status-text '. + 'aphlict-connection-status-connecting', + ), + array( + $icon, + $text, + )); } else { - return pht('Notification server not enabled'); + $text = pht('Notification server not enabled'); + $icon = id(new PHUIIconView()) + ->setIcon('fa-circle-o grey'); + return phutil_tag( + 'span', + array( + 'class' => 'connection-status-text '. + 'aphlict-connection-status-notenabled', + ), + array( + $icon, + $text, + )); } } diff --git a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php index 657fd0435e..ece5a88462 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php @@ -205,8 +205,8 @@ final class PhabricatorPeopleProfileViewController $event, PhabricatorPolicyCapability::CAN_EDIT); - $epoch_min = $event->getViewerDateFrom(); - $epoch_max = $event->getViewerDateTo(); + $epoch_min = $event->getStartDateTimeEpoch(); + $epoch_max = $event->getEndDateTimeEpoch(); $event_view = id(new AphrontCalendarEventView()) ->setCanEdit($can_edit) diff --git a/src/applications/people/query/PhabricatorPeopleQuery.php b/src/applications/people/query/PhabricatorPeopleQuery.php index 69ce099111..07fbc8b941 100644 --- a/src/applications/people/query/PhabricatorPeopleQuery.php +++ b/src/applications/people/query/PhabricatorPeopleQuery.php @@ -431,8 +431,8 @@ final class PhabricatorPeopleQuery // because of an event, we check again for events after that one ends. while (true) { foreach ($events as $event) { - $from = $event->getDateFromForCache(); - $to = $event->getViewerDateTo(); + $from = $event->getStartDateTimeEpochForCache(); + $to = $event->getEndDateTimeEpoch(); if (($from <= $cursor) && ($to > $cursor)) { $cursor = $to; continue 2; diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php index d36d279c55..6350df5e02 100644 --- a/src/applications/search/controller/PhabricatorApplicationSearchController.php +++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php @@ -252,12 +252,6 @@ final class PhabricatorApplicationSearchController get_class($engine))); } - if ($list->getActions()) { - foreach ($list->getActions() as $action) { - $header->addActionLink($action); - } - } - if ($list->getObjectList()) { $box->setObjectList($list->getObjectList()); } @@ -274,6 +268,21 @@ final class PhabricatorApplicationSearchController $result_header = $list->getHeader(); if ($result_header) { $box->setHeader($result_header); + $header = $result_header; + } + + if ($list->getActions()) { + foreach ($list->getActions() as $action) { + $header->addActionLink($action); + } + } + + $use_actions = $engine->newUseResultsActions($saved_query); + if ($use_actions) { + $use_dropdown = $this->newUseResultsDropdown( + $saved_query, + $use_actions); + $header->addActionLink($use_dropdown); } $more_crumbs = $list->getCrumbs(); @@ -496,5 +505,24 @@ final class PhabricatorApplicationSearchController return $nux_view; } + private function newUseResultsDropdown( + PhabricatorSavedQuery $query, + array $dropdown_items) { + + $viewer = $this->getViewer(); + + $action_list = id(new PhabricatorActionListView()) + ->setViewer($viewer); + foreach ($dropdown_items as $dropdown_item) { + $action_list->addAction($dropdown_item); + } + + return id(new PHUIButtonView()) + ->setTag('a') + ->setHref('#') + ->setText(pht('Use Results...')) + ->setIcon('fa-road') + ->setDropdownMenu($action_list); + } } diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php index 13606c542f..a1279ad8ae 100644 --- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php +++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php @@ -1390,4 +1390,8 @@ abstract class PhabricatorApplicationSearchEngine extends Phobject { return null; } + public function newUseResultsActions(PhabricatorSavedQuery $saved) { + return array(); + } + } diff --git a/src/docs/user/userguide/calendar_exports.diviner b/src/docs/user/userguide/calendar_exports.diviner new file mode 100644 index 0000000000..340cff863d --- /dev/null +++ b/src/docs/user/userguide/calendar_exports.diviner @@ -0,0 +1,97 @@ +@title Calendar User Guide: Exporting Events +@group userguide + +Exporting events to other calendars. + +Overview +======== + +IMPORTANT: Calendar is a prototype application. See +@{article:User Guide: Prototype Applications}. + +You can export events from Phabricator to other calendar applications like +**Google Calendar** or **Calendar.app**. This document will guide you through +how to export event data from Phabricator. + +When you export events into another application, they generally will not be +editable from that application. Exporting events allows you to create one +calendar that shows all the events you care about in whatever application you +prefer (so you can keep track of everything you need to do), but does not let +you edit Phabricator events from another application. + +When exporting events, you can either export individual events one at a time +or export an entire group of events (for example, all events you are attending). + + +Exporting a Single Event +======================== + +To export a single event, visit the event detail page and click +{nav Export as .ics}. This will download an `.ics` file which you can import +into most other calendar applications. + +Mail you receive about events also has a copy of this `.ics` file attached to +it. You can import this `.ics` file directly. + +In **Google Calendar**, use {nav Other Calendars > Import Calendar} to import +the `.ics` file. + +In **Calendar.app**, use {nav File > Import...} to import the `.ics` file, or +drag the `.ics` file onto your calendar. + +When you export a recurring event, the `.ics` file will contain information +about the entire event series. + +If you want to update event information later, you can just repeat this +process. Calendar applications will update the existing event if you've +previously imported an older version of it. + + +Exporting a Group of Events +=========================== + +You can export a group of events matching an arbitrary query (like all events +you are attending) to keep different calendars in sync. + +To export a group of events: + + - Run a query in Calendar which selects the events you want to export. + - Example: All events you are attending. + - Example: All events you are invited to. + - Example: All events tagged `#meetup`. + - Select the {nav Use Results... > Export Query as .ics} action to turn + the query into an export. + - Name the export with a descritive name. + - Select a policy mode for the export (see below for discussion). + - Click {nav Create New Export} to finish the process. + +The **policy modes** for exports are: + + - **Public**: Only public information (visible to logged-out users) will + be exported. This mode is not available if your install does not have + public information (per `policy.allow-public` in Config). + - **Privileged**: All event information will be exported. This means that + anyone who knows the export URI can see ALL of the related event + information, as though they were logged in with your account. + +WARNING: Anyone who learns the URI for an export can see the data you choose +to export, even if they don't have a Phabricator account! Be careful about how +much data you export and treat the URI as a secret. If you accidentally share +a URI, you can disable the export. + +After finishing the process, you'll see a screen with some details about the +export and an **ICS URI**. This URI allows you to import the events which match +the query into another calendar application. + +In **Google Calendar**, use {nav Other Calendars > Add by URL} to import the +URI. + +In **Calendar.app**, use {nav File > New Calendar Subscription...} to subscribe +to the URI. + +Next Steps +========== + +Continue by: + + - returning to the @{article:Calendar User Guide}. diff --git a/src/infrastructure/storage/lisk/LiskDAO.php b/src/infrastructure/storage/lisk/LiskDAO.php index e795777394..71cfba3d54 100644 --- a/src/infrastructure/storage/lisk/LiskDAO.php +++ b/src/infrastructure/storage/lisk/LiskDAO.php @@ -1339,11 +1339,12 @@ abstract class LiskDAO extends Phobject { * @task hook */ public function generatePHID() { - throw new Exception( - pht( - 'To use %s, you need to overload %s to perform PHID generation.', - 'CONFIG_AUX_PHID', - 'generatePHID()')); + $type = $this->getPHIDType(); + return PhabricatorPHID::generateNewPHID($type); + } + + public function getPHIDType() { + throw new PhutilMethodNotImplementedException(); } diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index dc2dfe186e..8fa73ac9f3 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -805,6 +805,7 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView return array( 'title' => $this->getTitle(), + 'bodyClasses' => $this->getBodyClasses(), 'aphlictDropdownData' => array( $dropdown_query->getNotificationData(), $dropdown_query->getConpherenceData(), diff --git a/webroot/rsrc/css/application/base/notification-menu.css b/webroot/rsrc/css/application/base/notification-menu.css index 84569c7d6b..82a5aa0220 100644 --- a/webroot/rsrc/css/application/base/notification-menu.css +++ b/webroot/rsrc/css/application/base/notification-menu.css @@ -124,10 +124,16 @@ color: {$lightgreytext}; } -.aphlict-connection-status .aphlict-connection-status-connected { - color: {$green}; +.aphlict-connection-status { + position: relative; } -.aphlict-connection-status .aphlict-connection-status-error { - color: {$red}; +.aphlict-connection-status .phui-icon-view { + font-size: 9px; + position: absolute; + top: 4px; +} + +.aphlict-connection-status .connection-status-text { + margin-left: 12px; } diff --git a/webroot/rsrc/css/application/base/phui-theme.css b/webroot/rsrc/css/application/base/phui-theme.css index e0bfbf36a6..5313f2099b 100644 --- a/webroot/rsrc/css/application/base/phui-theme.css +++ b/webroot/rsrc/css/application/base/phui-theme.css @@ -28,36 +28,3 @@ background: #124A1B; } -/*--- Profile Nav Colors -----------------------------------------------------*/ - - -.phui-theme-blindigo .phui-profile-menu .phabricator-side-menu, -.phui-theme-blindigo .phabricator-side-menu .phui-profile-menu-footer-1 { - background: #525867; -} - -.phui-theme-dark .phui-profile-menu .phabricator-side-menu, -.phui-theme-dark .phabricator-side-menu .phui-profile-menu-footer-1 { - background: #30333A; -} - -.phui-theme-indigo .phui-profile-menu .phabricator-side-menu, -.phui-theme-indigo .phabricator-side-menu .phui-profile-menu-footer-1 { - background: #423658; -} - -.phui-theme-red .phui-profile-menu .phabricator-side-menu, -.phui-theme-red .phabricator-side-menu .phui-profile-menu-footer-1 { - background: #420C0C; -} - -.phui-theme-blue .phui-profile-menu .phabricator-side-menu, -.phui-theme-blue .phabricator-side-menu .phui-profile-menu-footer-1 { - background: #062842; -} - -.phui-theme-green .phui-profile-menu .phabricator-side-menu, -.phui-theme-green .phabricator-side-menu .phui-profile-menu-footer-1 { - background: #122916; -} - diff --git a/webroot/rsrc/css/application/conpherence/durable-column.css b/webroot/rsrc/css/application/conpherence/durable-column.css index f4320ed1a2..f94fa1f6b8 100644 --- a/webroot/rsrc/css/application/conpherence/durable-column.css +++ b/webroot/rsrc/css/application/conpherence/durable-column.css @@ -45,6 +45,24 @@ display: none; } +.conpherence-durable-column-header .aphlict-connection-status { + width: 6px; + float: left; + height: 18px; + margin-left: 10px; + margin-top: 8px; +} + +.conpherence-durable-column-header .aphlict-connection-status + .connection-status-text { + display: none; +} + +.conpherence-durable-column-header .aphlict-connection-status + .phui-icon-view { + font-size: 10px; +} + .conpherence-durable-column-header .phabricator-application-menu .phui-list-item-view { margin: 0; @@ -100,11 +118,6 @@ padding: 10px 8px 10px 8px; } -.conpherence-durable-column-header-text .phui-icon-view { - color: #fff; - text-shadow: 1px 1px 0 rgba(0,0,0,.6); -} - .conpherence-durable-column-icon-bar { width: 36px; background-color: {$lightgreybackground}; @@ -153,6 +166,16 @@ overflow-x: hidden; } +.conpherence-durable-column .column-no-rooms { + padding: 12px 8px; +} + +.conpherence-durable-column .column-no-rooms-text { + color: {$greytext}; + font-style: italic; + margin-bottom: 16px; +} + .conpherence-durable-column-transactions { padding: 8px 12px 0; } @@ -300,12 +323,6 @@ img { text-shadow: none; } -.minimize-column .conpherence-durable-column - .conpherence-durable-column-header-text .phui-icon-view { - color: {$darkbluetext}; - text-shadow: none; -} - .minimize-column .conpherence-durable-column .conpherence-durable-column-header .phabricator-application-menu .phui-list-item-icon.phui-font-fa { diff --git a/webroot/rsrc/css/application/conpherence/header-pane.css b/webroot/rsrc/css/application/conpherence/header-pane.css index 9b0c5e98e5..522771cc15 100644 --- a/webroot/rsrc/css/application/conpherence/header-pane.css +++ b/webroot/rsrc/css/application/conpherence/header-pane.css @@ -2,9 +2,6 @@ * @provides conpherence-header-pane-css */ -.conpherence-header-pane { -} - .conpherence-header-pane .phui-header-shell { padding: 8px 16px 10px; min-height: 38px; @@ -23,8 +20,26 @@ margin: 0; } -.conpherence-header-pane .phui-header-shell.conpherence-no-topic { - padding: 15px 16px 5px; +.conpherence-header-pane .phui-header-col1 { + width: 46px; + height: 35px; +} + +.conpherence-header-pane .phui-header-image { + height: 35px; + width: 35px; + background-size: 35px; + position: absolute; + top: 4px; + left: 0; +} + +.conpherence-header-pane .phui-header-image-href { + position: inherit; +} + +.conpherence-header-pane .phui-header-col2 { + height: 40px; } .conpherence-header-pane .phui-header-action-list .phui-header-action-item diff --git a/webroot/rsrc/css/application/conpherence/menu.css b/webroot/rsrc/css/application/conpherence/menu.css index aa5798bfff..e0719cdcc8 100644 --- a/webroot/rsrc/css/application/conpherence/menu.css +++ b/webroot/rsrc/css/application/conpherence/menu.css @@ -26,10 +26,14 @@ padding: 4px 0 4px 8px; } -.conpherence-menu-pane .phui-list-item-view.hidden { +.conpherence-menu-pane.phabricator-side-menu .phui-list-item-view.hidden { display: none; } +.phui-list-view.conpherence-menu { + margin-bottom: 20px; +} + .conpherence-menu-pane.phabricator-side-menu .room-list-href { padding: 10px 0 9px 8px; display: inline-block; diff --git a/webroot/rsrc/css/application/conpherence/notification.css b/webroot/rsrc/css/application/conpherence/notification.css index b88b9ef298..6347b40ee3 100644 --- a/webroot/rsrc/css/application/conpherence/notification.css +++ b/webroot/rsrc/css/application/conpherence/notification.css @@ -2,8 +2,7 @@ * @provides conpherence-notification-css */ -/* kill styles on phabricator-notification */ -.conpherence-notification { +.phabricator-notification.conpherence-notification { padding: 0; } @@ -27,7 +26,6 @@ width: 30px; height: 30px; background-size: 100%; - box-shadow: {$borderinset}; border-radius: 3px; } @@ -71,3 +69,19 @@ padding: 0 5px 1px; font-size: {$smallestfontsize}; } + +.phabricator-notification .no-room-notification { + color: {$lightgreytext}; + display: block; +} + +.phabricator-notification-header .persistent-option { + white-space: nowrap; + float: right; +} + +.phabricator-notification-header .persistent-option span { + margin-left: 4px; + font-weight: normal; + color: {$greytext}; +} diff --git a/webroot/rsrc/css/phui/phui-profile-menu.css b/webroot/rsrc/css/phui/phui-profile-menu.css index 275bff1dde..f056e34f23 100644 --- a/webroot/rsrc/css/phui/phui-profile-menu.css +++ b/webroot/rsrc/css/phui/phui-profile-menu.css @@ -17,10 +17,14 @@ } .phui-profile-menu .phabricator-side-menu { - background: #525867; + background: #dee0e7; width: 240px; } +.phabricator-side-menu .phui-profile-menu-footer-1 { + background: #dee0e7; +} + .phui-profile-menu .phabricator-side-menu .phui-list-item-view { position: relative; } @@ -32,9 +36,9 @@ height: 48px; font-size: {$biggerfontsize}; -webkit-font-smoothing: antialiased; - color: {$menu.profile.text}; line-height: 22px; overflow: hidden; + color: {$darkbluetext}; text-overflow: ellipsis; line-height: 48px; } @@ -50,7 +54,7 @@ height: 24px; line-height: 24px; text-align: center; - color: {$menu.profile.text}; + color: {$darkbluetext}; background-size: 100%; } @@ -81,7 +85,7 @@ .phui-profile-menu .phabricator-side-menu .phui-list-item-disabled .phui-list-item-icon { - color: {$menu.profile.icon.disabled}; + color: {$lightgreytext}; } .phui-profile-menu .phabricator-side-menu .phui-icon-view { @@ -90,28 +94,17 @@ .device-desktop .phui-profile-menu .phabricator-side-menu .phui-list-item-href:hover { - background-color: rgba({$alphablack},0.15); - color: {$menu.profile.text.selected}; -} - -.phui-profile-menu .phabricator-side-menu - .phui-list-item-selected - .phui-list-item-icon, -.device-desktop .phui-profile-menu .phabricator-side-menu - .phui-list-item-href:hover - .phui-list-item-icon { - color: {$menu.profile.text.selected}; + background-color: rgba({$alphablack},0.05); } .phui-profile-menu .phabricator-side-menu .phui-list-item-selected .phui-list-item-href { - background-color: rgba({$alphablack},0.3); - color: {$menu.profile.text.selected}; + background-color: rgba({$alphablack},0.1); } .phui-profile-menu .phabricator-side-menu .phui-list-item-selected .phui-list-item-href:hover { - background-color: rgba({$alphablack},0.45); + background-color: rgba({$alphablack},0.15); } .phui-profile-menu .phabricator-side-menu .phui-divider { @@ -123,13 +116,13 @@ white-space: normal; padding: 18px 15px; font-size: 12px; - color: {$menu.profile.text}; + color: {$darkbluetext}; } .phui-profile-menu .phabricator-side-menu .phui-motivator .phui-icon-view { position: static; font-size: 12px; - color: {$menu.profile.text}; + color: {$darkbluetext}; } .phui-profile-menu .phabricator-side-menu .phui-profile-menu-error { @@ -150,11 +143,11 @@ .phui-list-item-href, .phui-profile-menu .phui-list-sidenav .phui-list-item-disabled .phui-list-item-href { - color: rgba({$alphawhite}, 0.5); + color: rgba({$lightgreytext}); } .phui-profile-menu .phabricator-side-menu .phui-profile-segment-bar { - color: {$menu.profile.text}; + color: {$darkbluetext}; font-size: {$smallerfontsize}; -webkit-font-smoothing: antialiased; padding: 8px 12px 16px; @@ -201,25 +194,11 @@ } .phui-profile-menu .phui-profile-menu-footer .phui-icon-circle { - border-color: {$menu.profile.text}; + border-color: {$darkbluetext}; } .phui-profile-menu .phui-profile-menu-footer .phui-icon-circle .phui-icon-view { - color: {$menu.profile.text}; -} - -.phui-profile-menu .phui-profile-menu-footer .phui-list-item-href:hover - .phui-icon-circle, -.phui-profile-menu .phui-list-item-selected.phui-profile-menu-footer - .phui-icon-circle { - border-color: {$menu.profile.text.selected}; -} - -.phui-profile-menu .phui-profile-menu-footer .phui-list-item-href:hover - .phui-icon-circle .phui-icon-view, -.phui-profile-menu .phui-list-item-selected.phui-profile-menu-footer - .phui-icon-circle .phui-icon-view { - color: {$menu.profile.text.selected}; + color: {$darkbluetext}; } .phui-profile-menu .phui-profile-menu-footer diff --git a/webroot/rsrc/js/application/aphlict/behavior-aphlict-status.js b/webroot/rsrc/js/application/aphlict/behavior-aphlict-status.js index 9b8a87c6c9..cb35df235f 100644 --- a/webroot/rsrc/js/application/aphlict/behavior-aphlict-status.js +++ b/webroot/rsrc/js/application/aphlict/behavior-aphlict-status.js @@ -24,14 +24,22 @@ JX.behavior('aphlict-status', function(config) { } var status = client.getStatus(); + var icon = config.icon[status]; var status_node = JX.$N( 'span', { - className: 'aphlict-connection-status-' + status + className: 'connection-status-text aphlict-connection-status-' + status }, pht(status)); - JX.DOM.setContent(node, status_node); + var icon_node = new JX.PHUIXIconView() + .setIcon(icon['icon']) + .setColor(icon['color']) + .getNode(); + + var content = [icon_node, ' ', status_node]; + + JX.DOM.setContent(node, content); } JX.Aphlict.listen('didChangeStatus', update); diff --git a/webroot/rsrc/js/application/conpherence/behavior-durable-column.js b/webroot/rsrc/js/application/conpherence/behavior-durable-column.js index 812be0f082..a7fe3fbef7 100644 --- a/webroot/rsrc/js/application/conpherence/behavior-durable-column.js +++ b/webroot/rsrc/js/application/conpherence/behavior-durable-column.js @@ -66,6 +66,11 @@ JX.behavior('durable-column', function(config, statics) { JX.DOM.alterClass(document.body, 'minimize-column', userMinimize); JX.Stratcom.invoke('resize'); + if (!userMinimize) { + var messages = _getColumnMessagesNode(); + scrollbar.scrollTo(messages.scrollHeight); + } + new JX.Request(config.minimizeURI) .setData({value: (userMinimize ? 1 : 0)}) .send(); @@ -89,9 +94,10 @@ JX.behavior('durable-column', function(config, statics) { JX.Stratcom.invoke('resize'); } - new JX.KeyboardShortcut('\\', 'Toggle Conpherence Column') - .setHandler(_toggleColumn) - .register(); + JX.Stratcom.listen( + 'click', + 'conpherence-persist-column', + _toggleColumn); JX.Stratcom.listen( 'click', @@ -346,6 +352,11 @@ JX.behavior('durable-column', function(config, statics) { null, function (e) { var new_data = e.getData().newResponse; + var new_classes = new_data.bodyClasses; + if (userMinimize) { + new_classes = new_classes + ' minimize-column'; + } + document.body.className = new_classes; JX.Title.setTitle(new_data.title); }); diff --git a/webroot/rsrc/js/core/behavior-crop.js b/webroot/rsrc/js/core/behavior-crop.js deleted file mode 100644 index 272ccbeff8..0000000000 --- a/webroot/rsrc/js/core/behavior-crop.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * @provides javelin-behavior-aphront-crop - * @requires javelin-behavior - * javelin-dom - * javelin-vector - * javelin-magical-init - */ - - JX.behavior('aphront-crop', function(config) { - - var dragging = false; - var startX, startY; - var finalX, finalY; - - var cropBox = JX.$(config.cropBoxID); - var basePos = JX.$V(cropBox); - cropBox.style.height = config.height + 'px'; - cropBox.style.width = config.width + 'px'; - var baseD = JX.$V(config.width, config.height); - - var image = JX.DOM.find(cropBox, 'img', 'crop-image'); - image.style.height = (config.imageH * config.scale) + 'px'; - image.style.width = (config.imageW * config.scale) + 'px'; - var imageD = JX.$V( - config.imageW * config.scale, - config.imageH * config.scale - ); - var minLeft = baseD.x - imageD.x; - var minTop = baseD.y - imageD.y; - - var ondrag = function(e) { - e.kill(); - dragging = true; - var p = JX.$V(e); - startX = p.x; - startY = p.y; - }; - - var onmove = function(e) { - if (!dragging) { - return; - } - e.kill(); - - var p = JX.$V(e); - var dx = startX - p.x; - var dy = startY - p.y; - var imagePos = JX.$V(image); - var moveLeft = imagePos.x - basePos.x - dx; - var moveTop = imagePos.y - basePos.y - dy; - - image.style.left = Math.min(Math.max(minLeft, moveLeft), 0) + 'px'; - image.style.top = Math.min(Math.max(minTop, moveTop), 0) + 'px'; - - // reset these; a new beginning! - startX = p.x; - startY = p.y; - - // save off where we are right now - imagePos = JX.$V(image); - finalX = Math.abs(imagePos.x - basePos.x); - finalY = Math.abs(imagePos.y - basePos.y); - JX.DOM.find(cropBox, 'input', 'crop-x').value = finalX; - JX.DOM.find(cropBox, 'input', 'crop-y').value = finalY; - }; - - var ondrop = function() { - if (!dragging) { - return; - } - dragging = false; - }; - - // NOTE: Javelin does not dispatch mousemove by default. - JX.enableDispatch(cropBox, 'mousemove'); - - JX.DOM.listen(cropBox, 'mousedown', [], ondrag); - JX.DOM.listen(cropBox, 'mousemove', [], onmove); - JX.DOM.listen(cropBox, 'mouseup', [], ondrop); - JX.DOM.listen(cropBox, 'mouseout', [], ondrop); - -});