diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -8,7 +8,7 @@ return array( 'names' => array( 'core.pkg.css' => '204cabae', - 'core.pkg.js' => '9f2969e9', + 'core.pkg.js' => 'e08d4d9f', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '33da0633', 'differential.pkg.js' => '4b7d8f19', @@ -477,6 +477,7 @@ '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' => 'ae9f2ec9', 'rsrc/js/core/behavior-device.js' => 'b5b36110', 'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '484a6e22', 'rsrc/js/core/behavior-error-log.js' => '6882e80a', @@ -601,6 +602,7 @@ 'javelin-behavior-dashboard-tab-panel' => 'd4eecc63', 'javelin-behavior-day-view' => '5c46cff2', 'javelin-behavior-desktop-notifications-control' => 'edd1ba66', + 'javelin-behavior-detect-timezone' => 'ae9f2ec9', 'javelin-behavior-device' => 'b5b36110', 'javelin-behavior-differential-add-reviewers-and-ccs' => 'e10f8e18', 'javelin-behavior-differential-comment-jump' => '4fdb476d', @@ -1742,6 +1744,11 @@ 'javelin-util', 'phabricator-busy', ), + 'ae9f2ec9' => array( + 'javelin-behavior', + 'javelin-uri', + 'phabricator-notification', + ), 'b003d4fb' => array( 'javelin-behavior', 'javelin-stratcom', @@ -2332,6 +2339,7 @@ 'javelin-behavior-scrollbar', 'javelin-behavior-durable-column', 'conpherence-thread-manager', + 'javelin-behavior-detect-timezone', ), 'darkconsole.pkg.js' => array( 'javelin-behavior-dark-console', diff --git a/resources/celerity/packages.php b/resources/celerity/packages.php --- a/resources/celerity/packages.php +++ b/resources/celerity/packages.php @@ -81,6 +81,7 @@ 'javelin-behavior-scrollbar', 'javelin-behavior-durable-column', 'conpherence-thread-manager', + 'javelin-behavior-detect-timezone', ), 'core.pkg.css' => array( 'phabricator-core-css', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3355,6 +3355,7 @@ 'PhabricatorSettingsMainController' => 'applications/settings/controller/PhabricatorSettingsMainController.php', 'PhabricatorSettingsMainMenuBarExtension' => 'applications/settings/extension/PhabricatorSettingsMainMenuBarExtension.php', 'PhabricatorSettingsPanel' => 'applications/settings/panel/PhabricatorSettingsPanel.php', + 'PhabricatorSettingsTimezoneController' => 'applications/settings/controller/PhabricatorSettingsTimezoneController.php', 'PhabricatorSetupCheck' => 'applications/config/check/PhabricatorSetupCheck.php', 'PhabricatorSetupCheckTestCase' => 'applications/config/check/__tests__/PhabricatorSetupCheckTestCase.php', 'PhabricatorSetupIssue' => 'applications/config/issue/PhabricatorSetupIssue.php', @@ -8065,6 +8066,7 @@ 'PhabricatorSettingsMainController' => 'PhabricatorController', 'PhabricatorSettingsMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension', 'PhabricatorSettingsPanel' => 'Phobject', + 'PhabricatorSettingsTimezoneController' => 'PhabricatorController', 'PhabricatorSetupCheck' => 'Phobject', 'PhabricatorSetupCheckTestCase' => 'PhabricatorTestCase', 'PhabricatorSetupIssue' => 'Phobject', diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -755,6 +755,17 @@ return new DateTimeZone($this->getTimezoneIdentifier()); } + public function getTimeZoneOffset() { + $timezone = $this->getTimeZone(); + $now = new DateTime('@'.PhabricatorTime::getNow()); + $offset = $timezone->getOffset($now); + + // Javascript offsets are in minutes and have the opposite sign. + $offset = -(int)($offset / 60); + + return $offset; + } + public function formatShortDateTime($when, $now = null) { if ($now === null) { $now = PhabricatorTime::getNow(); diff --git a/src/applications/settings/application/PhabricatorSettingsApplication.php b/src/applications/settings/application/PhabricatorSettingsApplication.php --- a/src/applications/settings/application/PhabricatorSettingsApplication.php +++ b/src/applications/settings/application/PhabricatorSettingsApplication.php @@ -32,6 +32,8 @@ '(?:(?P\d+)/)?(?:panel/(?P[^/]+)/)?' => 'PhabricatorSettingsMainController', 'adjust/' => 'PhabricatorSettingsAdjustController', + 'timezone/(?P[^/]+)/' + => 'PhabricatorSettingsTimezoneController', ), ); } diff --git a/src/applications/settings/controller/PhabricatorSettingsTimezoneController.php b/src/applications/settings/controller/PhabricatorSettingsTimezoneController.php new file mode 100644 --- /dev/null +++ b/src/applications/settings/controller/PhabricatorSettingsTimezoneController.php @@ -0,0 +1,108 @@ +getViewer(); + + $client_offset = $request->getURIData('offset'); + $client_offset = (int)$client_offset; + + $timezones = DateTimeZone::listIdentifiers(); + $now = new DateTime('@'.PhabricatorTime::getNow()); + + $options = array( + 'ignore' => pht('Ignore Conflict'), + ); + + foreach ($timezones as $identifier) { + $zone = new DateTimeZone($identifier); + $offset = -($zone->getOffset($now) / 60); + if ($offset == $client_offset) { + $options[$identifier] = $identifier; + } + } + + $settings_help = pht( + 'You can change your date and time preferences in Settings.'); + + if ($request->isFormPost()) { + $timezone = $request->getStr('timezone'); + + $pref_ignore = PhabricatorUserPreferences::PREFERENCE_IGNORE_OFFSET; + + $preferences = $viewer->loadPreferences(); + + if ($timezone == 'ignore') { + $preferences + ->setPreference($pref_ignore, $client_offset) + ->save(); + + return $this->newDialog() + ->setTitle(pht('Conflict Ignored')) + ->appendParagraph( + pht( + 'The conflict between your browser and profile timezone '. + 'settings will be ignored.')) + ->appendParagraph($settings_help) + ->addCancelButton('/', pht('Done')); + } + + if (isset($options[$timezone])) { + $preferences + ->setPreference($pref_ignore, null) + ->save(); + + $viewer + ->setTimezoneIdentifier($timezone) + ->save(); + } + } + + $server_offset = $viewer->getTimeZoneOffset(); + + if ($client_offset == $server_offset) { + return $this->newDialog() + ->setTitle(pht('Timezone Calibrated')) + ->appendParagraph( + pht( + 'Your browser timezone and profile timezone are now '. + 'in agreement (%s).', + $this->formatOffset($client_offset))) + ->appendParagraph($settings_help) + ->addCancelButton('/', pht('Done')); + } + + $form = id(new AphrontFormView()) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setName('timezone') + ->setLabel(pht('Timezone')) + ->setOptions($options)); + + return $this->newDialog() + ->setTitle(pht('Adjust Timezone')) + ->appendParagraph( + pht( + 'Your browser timezone (%s) differs from your profile timezone '. + '(%s). You can ignore this conflict or adjust your profile setting '. + 'to match your client.', + $this->formatOffset($client_offset), + $this->formatOffset($server_offset))) + ->appendForm($form) + ->addCancelButton(pht('Cancel')) + ->addSubmitButton(pht('Submit')); + } + + private function formatOffset($offset) { + $offset = $offset / 60; + + if ($offset >= 0) { + return pht('GMT-%d', $offset); + } else { + return pht('GMT+%d', -$offset); + } + } + +} diff --git a/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php b/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php --- a/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php @@ -21,6 +21,7 @@ $pref_time = PhabricatorUserPreferences::PREFERENCE_TIME_FORMAT; $pref_date = PhabricatorUserPreferences::PREFERENCE_DATE_FORMAT; $pref_week_start = PhabricatorUserPreferences::PREFERENCE_WEEK_START_DAY; + $pref_ignore = PhabricatorUserPreferences::PREFERENCE_IGNORE_OFFSET; $preferences = $user->loadPreferences(); $errors = array(); @@ -41,7 +42,8 @@ $request->getStr($pref_date)) ->setPreference( $pref_week_start, - $request->getStr($pref_week_start)); + $request->getStr($pref_week_start)) + ->setPreference($pref_ignore, null); if (!$errors) { $preferences->save(); diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php --- a/src/applications/settings/storage/PhabricatorUserPreferences.php +++ b/src/applications/settings/storage/PhabricatorUserPreferences.php @@ -43,6 +43,7 @@ const PREFERENCE_PROFILE_MENU_COLLAPSED = 'profile-menu.collapsed'; const PREFERENCE_FAVORITE_POLICIES = 'policy.favorites'; + const PREFERENCE_IGNORE_OFFSET = 'time.offset.ignore'; // These are in an unusual order for historic reasons. const MAILTAG_PREFERENCE_NOTIFY = 0; diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -223,6 +223,30 @@ } if ($user) { + if ($user->isLoggedIn()) { + $offset = $user->getTimeZoneOffset(); + + $preferences = $user->loadPreferences(); + $ignore_key = PhabricatorUserPreferences::PREFERENCE_IGNORE_OFFSET; + + $ignore = $preferences->getPreference($ignore_key); + if (!strlen($ignore)) { + $ignore = null; + } + + Javelin::initBehavior( + 'detect-timezone', + array( + 'offset' => $offset, + 'uri' => '/settings/timezone/', + 'message' => pht( + 'Your browser timezone setting differs from the timezone '. + 'setting in your profile.'), + 'ignoreKey' => $ignore_key, + 'ignore' => $ignore, + )); + } + $default_img_uri = celerity_get_resource_uri( 'rsrc/image/icon/fatcow/document_black.png'); diff --git a/webroot/rsrc/js/core/behavior-detect-timezone.js b/webroot/rsrc/js/core/behavior-detect-timezone.js new file mode 100644 --- /dev/null +++ b/webroot/rsrc/js/core/behavior-detect-timezone.js @@ -0,0 +1,53 @@ +/** + * @provides javelin-behavior-detect-timezone + * @requires javelin-behavior + * javelin-uri + * phabricator-notification + */ + +JX.behavior('detect-timezone', function(config) { + + var offset = new Date().getTimezoneOffset(); + var ignore = config.ignore; + + if (ignore !== null) { + // If we're ignoring a client offset and it's the current offset, just + // bail. This means the user has chosen to ignore the clock difference + // between the current client setting and their server setting. + if (offset == ignore) { + return; + } + + // If we're ignoring a client offset but the current offset is different, + // wipe the offset. If you go from SF to NY, ignore the difference, return + // to SF, then travel back to NY a few months later, we want to prompt you + // again. This code will clear the ignored setting upon your return to SF. + new JX.Request('/settings/adjust/', JX.bag) + .setData({key: config.ignoreKey, value: ''}) + .send(); + + ignore = null; + } + + // If the client and server clocks are in sync, we're all set. + if (offset == config.offset) { + return; + } + + var notification = new JX.Notification() + .alterClassName('jx-notification-alert', true) + .setContent(config.message) + .setDuration(0); + + notification.listen('activate', function() { + JX.Stratcom.context().kill(); + notification.hide(); + + var uri = config.uri + offset + '/'; + + new JX.Workflow(uri) + .start(); + }); + + notification.show(); +});