@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Detect timezone discrepancies and prompt users to reconcile them

Summary: Ref T3025. This adds a check for different client/server timezone offsets and gives users an option to fix them or ignore them.

Test Plan:
- Fiddled with timezone in Settings and System Preferences.
- Got appropriate prompts and behavior after simulating various trips to and from exotic locales.

In particular, this slightly tricky case seems to work correctly:

- Travel to NY.
- Ignore discrepancy (you're only there for a couple hours for an important meeting, and returning to SF on a later flight).
- Return to SF for a few days.
- Travel back to NY.
- You should be prompted again, since you left the timezone after you ignored the discrepancy.

{F1654528}

{F1654529}

{F1654530}

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T3025

Differential Revision: https://secure.phabricator.com/D15961

+205 -1
+1
resources/celerity/packages.php
··· 81 81 'javelin-behavior-scrollbar', 82 82 'javelin-behavior-durable-column', 83 83 'conpherence-thread-manager', 84 + 'javelin-behavior-detect-timezone', 84 85 ), 85 86 'core.pkg.css' => array( 86 87 'phabricator-core-css',
+2
src/__phutil_library_map__.php
··· 3355 3355 'PhabricatorSettingsMainController' => 'applications/settings/controller/PhabricatorSettingsMainController.php', 3356 3356 'PhabricatorSettingsMainMenuBarExtension' => 'applications/settings/extension/PhabricatorSettingsMainMenuBarExtension.php', 3357 3357 'PhabricatorSettingsPanel' => 'applications/settings/panel/PhabricatorSettingsPanel.php', 3358 + 'PhabricatorSettingsTimezoneController' => 'applications/settings/controller/PhabricatorSettingsTimezoneController.php', 3358 3359 'PhabricatorSetupCheck' => 'applications/config/check/PhabricatorSetupCheck.php', 3359 3360 'PhabricatorSetupCheckTestCase' => 'applications/config/check/__tests__/PhabricatorSetupCheckTestCase.php', 3360 3361 'PhabricatorSetupIssue' => 'applications/config/issue/PhabricatorSetupIssue.php', ··· 8065 8066 'PhabricatorSettingsMainController' => 'PhabricatorController', 8066 8067 'PhabricatorSettingsMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension', 8067 8068 'PhabricatorSettingsPanel' => 'Phobject', 8069 + 'PhabricatorSettingsTimezoneController' => 'PhabricatorController', 8068 8070 'PhabricatorSetupCheck' => 'Phobject', 8069 8071 'PhabricatorSetupCheckTestCase' => 'PhabricatorTestCase', 8070 8072 'PhabricatorSetupIssue' => 'Phobject',
+11
src/applications/people/storage/PhabricatorUser.php
··· 755 755 return new DateTimeZone($this->getTimezoneIdentifier()); 756 756 } 757 757 758 + public function getTimeZoneOffset() { 759 + $timezone = $this->getTimeZone(); 760 + $now = new DateTime('@'.PhabricatorTime::getNow()); 761 + $offset = $timezone->getOffset($now); 762 + 763 + // Javascript offsets are in minutes and have the opposite sign. 764 + $offset = -(int)($offset / 60); 765 + 766 + return $offset; 767 + } 768 + 758 769 public function formatShortDateTime($when, $now = null) { 759 770 if ($now === null) { 760 771 $now = PhabricatorTime::getNow();
+2
src/applications/settings/application/PhabricatorSettingsApplication.php
··· 32 32 '(?:(?P<id>\d+)/)?(?:panel/(?P<key>[^/]+)/)?' 33 33 => 'PhabricatorSettingsMainController', 34 34 'adjust/' => 'PhabricatorSettingsAdjustController', 35 + 'timezone/(?P<offset>[^/]+)/' 36 + => 'PhabricatorSettingsTimezoneController', 35 37 ), 36 38 ); 37 39 }
+108
src/applications/settings/controller/PhabricatorSettingsTimezoneController.php
··· 1 + <?php 2 + 3 + final class PhabricatorSettingsTimezoneController 4 + extends PhabricatorController { 5 + 6 + public function handleRequest(AphrontRequest $request) { 7 + $viewer = $this->getViewer(); 8 + 9 + $client_offset = $request->getURIData('offset'); 10 + $client_offset = (int)$client_offset; 11 + 12 + $timezones = DateTimeZone::listIdentifiers(); 13 + $now = new DateTime('@'.PhabricatorTime::getNow()); 14 + 15 + $options = array( 16 + 'ignore' => pht('Ignore Conflict'), 17 + ); 18 + 19 + foreach ($timezones as $identifier) { 20 + $zone = new DateTimeZone($identifier); 21 + $offset = -($zone->getOffset($now) / 60); 22 + if ($offset == $client_offset) { 23 + $options[$identifier] = $identifier; 24 + } 25 + } 26 + 27 + $settings_help = pht( 28 + 'You can change your date and time preferences in Settings.'); 29 + 30 + if ($request->isFormPost()) { 31 + $timezone = $request->getStr('timezone'); 32 + 33 + $pref_ignore = PhabricatorUserPreferences::PREFERENCE_IGNORE_OFFSET; 34 + 35 + $preferences = $viewer->loadPreferences(); 36 + 37 + if ($timezone == 'ignore') { 38 + $preferences 39 + ->setPreference($pref_ignore, $client_offset) 40 + ->save(); 41 + 42 + return $this->newDialog() 43 + ->setTitle(pht('Conflict Ignored')) 44 + ->appendParagraph( 45 + pht( 46 + 'The conflict between your browser and profile timezone '. 47 + 'settings will be ignored.')) 48 + ->appendParagraph($settings_help) 49 + ->addCancelButton('/', pht('Done')); 50 + } 51 + 52 + if (isset($options[$timezone])) { 53 + $preferences 54 + ->setPreference($pref_ignore, null) 55 + ->save(); 56 + 57 + $viewer 58 + ->setTimezoneIdentifier($timezone) 59 + ->save(); 60 + } 61 + } 62 + 63 + $server_offset = $viewer->getTimeZoneOffset(); 64 + 65 + if ($client_offset == $server_offset) { 66 + return $this->newDialog() 67 + ->setTitle(pht('Timezone Calibrated')) 68 + ->appendParagraph( 69 + pht( 70 + 'Your browser timezone and profile timezone are now '. 71 + 'in agreement (%s).', 72 + $this->formatOffset($client_offset))) 73 + ->appendParagraph($settings_help) 74 + ->addCancelButton('/', pht('Done')); 75 + } 76 + 77 + $form = id(new AphrontFormView()) 78 + ->appendChild( 79 + id(new AphrontFormSelectControl()) 80 + ->setName('timezone') 81 + ->setLabel(pht('Timezone')) 82 + ->setOptions($options)); 83 + 84 + return $this->newDialog() 85 + ->setTitle(pht('Adjust Timezone')) 86 + ->appendParagraph( 87 + pht( 88 + 'Your browser timezone (%s) differs from your profile timezone '. 89 + '(%s). You can ignore this conflict or adjust your profile setting '. 90 + 'to match your client.', 91 + $this->formatOffset($client_offset), 92 + $this->formatOffset($server_offset))) 93 + ->appendForm($form) 94 + ->addCancelButton(pht('Cancel')) 95 + ->addSubmitButton(pht('Submit')); 96 + } 97 + 98 + private function formatOffset($offset) { 99 + $offset = $offset / 60; 100 + 101 + if ($offset >= 0) { 102 + return pht('GMT-%d', $offset); 103 + } else { 104 + return pht('GMT+%d', -$offset); 105 + } 106 + } 107 + 108 + }
+3 -1
src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php
··· 21 21 $pref_time = PhabricatorUserPreferences::PREFERENCE_TIME_FORMAT; 22 22 $pref_date = PhabricatorUserPreferences::PREFERENCE_DATE_FORMAT; 23 23 $pref_week_start = PhabricatorUserPreferences::PREFERENCE_WEEK_START_DAY; 24 + $pref_ignore = PhabricatorUserPreferences::PREFERENCE_IGNORE_OFFSET; 24 25 $preferences = $user->loadPreferences(); 25 26 26 27 $errors = array(); ··· 41 42 $request->getStr($pref_date)) 42 43 ->setPreference( 43 44 $pref_week_start, 44 - $request->getStr($pref_week_start)); 45 + $request->getStr($pref_week_start)) 46 + ->setPreference($pref_ignore, null); 45 47 46 48 if (!$errors) { 47 49 $preferences->save();
+1
src/applications/settings/storage/PhabricatorUserPreferences.php
··· 43 43 44 44 const PREFERENCE_PROFILE_MENU_COLLAPSED = 'profile-menu.collapsed'; 45 45 const PREFERENCE_FAVORITE_POLICIES = 'policy.favorites'; 46 + const PREFERENCE_IGNORE_OFFSET = 'time.offset.ignore'; 46 47 47 48 // These are in an unusual order for historic reasons. 48 49 const MAILTAG_PREFERENCE_NOTIFY = 0;
+24
src/view/page/PhabricatorStandardPageView.php
··· 223 223 } 224 224 225 225 if ($user) { 226 + if ($user->isLoggedIn()) { 227 + $offset = $user->getTimeZoneOffset(); 228 + 229 + $preferences = $user->loadPreferences(); 230 + $ignore_key = PhabricatorUserPreferences::PREFERENCE_IGNORE_OFFSET; 231 + 232 + $ignore = $preferences->getPreference($ignore_key); 233 + if (!strlen($ignore)) { 234 + $ignore = null; 235 + } 236 + 237 + Javelin::initBehavior( 238 + 'detect-timezone', 239 + array( 240 + 'offset' => $offset, 241 + 'uri' => '/settings/timezone/', 242 + 'message' => pht( 243 + 'Your browser timezone setting differs from the timezone '. 244 + 'setting in your profile.'), 245 + 'ignoreKey' => $ignore_key, 246 + 'ignore' => $ignore, 247 + )); 248 + } 249 + 226 250 $default_img_uri = 227 251 celerity_get_resource_uri( 228 252 'rsrc/image/icon/fatcow/document_black.png');
+53
webroot/rsrc/js/core/behavior-detect-timezone.js
··· 1 + /** 2 + * @provides javelin-behavior-detect-timezone 3 + * @requires javelin-behavior 4 + * javelin-uri 5 + * phabricator-notification 6 + */ 7 + 8 + JX.behavior('detect-timezone', function(config) { 9 + 10 + var offset = new Date().getTimezoneOffset(); 11 + var ignore = config.ignore; 12 + 13 + if (ignore !== null) { 14 + // If we're ignoring a client offset and it's the current offset, just 15 + // bail. This means the user has chosen to ignore the clock difference 16 + // between the current client setting and their server setting. 17 + if (offset == ignore) { 18 + return; 19 + } 20 + 21 + // If we're ignoring a client offset but the current offset is different, 22 + // wipe the offset. If you go from SF to NY, ignore the difference, return 23 + // to SF, then travel back to NY a few months later, we want to prompt you 24 + // again. This code will clear the ignored setting upon your return to SF. 25 + new JX.Request('/settings/adjust/', JX.bag) 26 + .setData({key: config.ignoreKey, value: ''}) 27 + .send(); 28 + 29 + ignore = null; 30 + } 31 + 32 + // If the client and server clocks are in sync, we're all set. 33 + if (offset == config.offset) { 34 + return; 35 + } 36 + 37 + var notification = new JX.Notification() 38 + .alterClassName('jx-notification-alert', true) 39 + .setContent(config.message) 40 + .setDuration(0); 41 + 42 + notification.listen('activate', function() { 43 + JX.Stratcom.context().kill(); 44 + notification.hide(); 45 + 46 + var uri = config.uri + offset + '/'; 47 + 48 + new JX.Workflow(uri) 49 + .start(); 50 + }); 51 + 52 + notification.show(); 53 + });