diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 90b596da99..e6ae2c4a75 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -8,7 +8,7 @@ return array( 'names' => array( 'core.pkg.css' => 'eb51e6dc', - 'core.pkg.js' => 'e0117d99', + 'core.pkg.js' => '711e63c0', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '02273347', 'differential.pkg.js' => 'ebef29b1', @@ -328,8 +328,9 @@ return array( 'rsrc/image/texture/table_header_tall.png' => 'd56b434f', 'rsrc/js/application/aphlict/Aphlict.js' => '5359e785', 'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => '995ad707', - 'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => 'b1a59974', + 'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => 'fb20ac8d', 'rsrc/js/application/aphlict/behavior-aphlict-status.js' => 'ea681761', + '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' => '5c46cff2', 'rsrc/js/application/calendar/behavior-event-all-day.js' => '38dcf3c8', @@ -429,7 +430,7 @@ return array( 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', 'rsrc/js/core/KeyboardShortcutManager.js' => 'c1700f6f', 'rsrc/js/core/MultirowRowManager.js' => 'b5d57730', - 'rsrc/js/core/Notification.js' => '0c6946e7', + 'rsrc/js/core/Notification.js' => 'ccf1cbf8', 'rsrc/js/core/Prefab.js' => '6920d200', 'rsrc/js/core/ShapedRequest.js' => '7cbe244b', 'rsrc/js/core/TextAreaUtils.js' => '5c93c52c', @@ -534,7 +535,7 @@ return array( 'javelin-aphlict' => '5359e785', 'javelin-behavior' => '61cbc29a', 'javelin-behavior-aphlict-dropdown' => '995ad707', - 'javelin-behavior-aphlict-listen' => 'b1a59974', + 'javelin-behavior-aphlict-listen' => 'fb20ac8d', 'javelin-behavior-aphlict-status' => 'ea681761', 'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884', 'javelin-behavior-aphront-crop' => 'fa0f4fc2', @@ -556,6 +557,7 @@ return array( 'javelin-behavior-dashboard-query-panel-select' => '453c5375', 'javelin-behavior-dashboard-tab-panel' => 'd4eecc63', 'javelin-behavior-day-view' => '5c46cff2', + 'javelin-behavior-desktop-notifications-control' => 'edd1ba66', 'javelin-behavior-device' => 'a205cf28', 'javelin-behavior-differential-add-reviewers-and-ccs' => 'e10f8e18', 'javelin-behavior-differential-comment-jump' => '4fdb476d', @@ -726,7 +728,7 @@ return array( 'phabricator-keyboard-shortcut-manager' => 'c1700f6f', 'phabricator-main-menu-view' => '663e3810', 'phabricator-nav-view-css' => '7aeaf435', - 'phabricator-notification' => '0c6946e7', + 'phabricator-notification' => 'ccf1cbf8', 'phabricator-notification-css' => '9c279160', 'phabricator-notification-menu-css' => '3c9d8aa1', 'phabricator-object-selector-css' => '029a133d', @@ -892,13 +894,6 @@ return array( 'javelin-dom', 'javelin-router', ), - '0c6946e7' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-util', - 'phabricator-notification-css', - ), '0f764c35' => array( 'javelin-install', 'javelin-util', @@ -1644,20 +1639,6 @@ return array( 'javelin-util', 'phabricator-shaped-request', ), - 'b1a59974' => array( - 'javelin-behavior', - 'javelin-aphlict', - 'javelin-stratcom', - 'javelin-request', - 'javelin-uri', - 'javelin-dom', - 'javelin-json', - 'javelin-router', - 'javelin-util', - 'javelin-leader', - 'javelin-sound', - 'phabricator-notification', - ), 'b1f0ccee' => array( 'javelin-install', 'javelin-dom', @@ -1792,6 +1773,13 @@ return array( 'javelin-stratcom', 'phabricator-phtize', ), + 'ccf1cbf8' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-util', + 'phabricator-notification-css', + ), 'cf86d16a' => array( 'javelin-behavior', 'javelin-dom', @@ -1939,6 +1927,13 @@ return array( 'phabricator-phtize', 'javelin-dom', ), + 'edd1ba66' => array( + 'javelin-behavior', + 'javelin-stratcom', + 'javelin-dom', + 'javelin-uri', + 'phabricator-notification', + ), 'eeaa9e5a' => array( 'javelin-behavior', 'javelin-stratcom', @@ -2014,6 +2009,20 @@ return array( 'javelin-vector', 'javelin-magical-init', ), + 'fb20ac8d' => array( + 'javelin-behavior', + 'javelin-aphlict', + 'javelin-stratcom', + 'javelin-request', + 'javelin-uri', + 'javelin-dom', + 'javelin-json', + 'javelin-router', + 'javelin-util', + 'javelin-leader', + 'javelin-sound', + 'phabricator-notification', + ), 'fbe497e7' => array( 'javelin-behavior', 'javelin-util', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index deeda11c91..344f7f6e42 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1789,6 +1789,7 @@ phutil_register_library_map(array( 'PhabricatorDatabaseSetupCheck' => 'applications/config/check/PhabricatorDatabaseSetupCheck.php', 'PhabricatorDateTimeSettingsPanel' => 'applications/settings/panel/PhabricatorDateTimeSettingsPanel.php', 'PhabricatorDebugController' => 'applications/system/controller/PhabricatorDebugController.php', + 'PhabricatorDesktopNotificationsSettingsPanel' => 'applications/settings/panel/PhabricatorDesktopNotificationsSettingsPanel.php', 'PhabricatorDestructibleInterface' => 'applications/system/interface/PhabricatorDestructibleInterface.php', 'PhabricatorDestructionEngine' => 'applications/system/engine/PhabricatorDestructionEngine.php', 'PhabricatorDeveloperConfigOptions' => 'applications/config/option/PhabricatorDeveloperConfigOptions.php', @@ -2133,7 +2134,6 @@ phutil_register_library_map(array( 'PhabricatorNamedQueryQuery' => 'applications/search/query/PhabricatorNamedQueryQuery.php', 'PhabricatorNavigationRemarkupRule' => 'infrastructure/markup/rule/PhabricatorNavigationRemarkupRule.php', 'PhabricatorNeverTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorNeverTriggerClock.php', - 'PhabricatorNotificationAdHocFeedStory' => 'applications/notification/feed/PhabricatorNotificationAdHocFeedStory.php', 'PhabricatorNotificationBuilder' => 'applications/notification/builder/PhabricatorNotificationBuilder.php', 'PhabricatorNotificationClearController' => 'applications/notification/controller/PhabricatorNotificationClearController.php', 'PhabricatorNotificationClient' => 'applications/notification/client/PhabricatorNotificationClient.php', @@ -2147,6 +2147,7 @@ phutil_register_library_map(array( 'PhabricatorNotificationStatusController' => 'applications/notification/controller/PhabricatorNotificationStatusController.php', 'PhabricatorNotificationStatusView' => 'applications/notification/view/PhabricatorNotificationStatusView.php', 'PhabricatorNotificationTestController' => 'applications/notification/controller/PhabricatorNotificationTestController.php', + 'PhabricatorNotificationTestFeedStory' => 'applications/notification/feed/PhabricatorNotificationTestFeedStory.php', 'PhabricatorNotificationUIExample' => 'applications/uiexample/examples/PhabricatorNotificationUIExample.php', 'PhabricatorNotificationsApplication' => 'applications/notification/application/PhabricatorNotificationsApplication.php', 'PhabricatorNuanceApplication' => 'applications/nuance/application/PhabricatorNuanceApplication.php', @@ -5385,6 +5386,7 @@ phutil_register_library_map(array( 'PhabricatorDatabaseSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorDateTimeSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorDebugController' => 'PhabricatorController', + 'PhabricatorDesktopNotificationsSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorDestructionEngine' => 'Phobject', 'PhabricatorDeveloperConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorDeveloperPreferencesSettingsPanel' => 'PhabricatorSettingsPanel', @@ -5773,7 +5775,6 @@ phutil_register_library_map(array( 'PhabricatorNamedQueryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorNavigationRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorNeverTriggerClock' => 'PhabricatorTriggerClock', - 'PhabricatorNotificationAdHocFeedStory' => 'PhabricatorFeedStory', 'PhabricatorNotificationBuilder' => 'Phobject', 'PhabricatorNotificationClearController' => 'PhabricatorNotificationController', 'PhabricatorNotificationClient' => 'Phobject', @@ -5787,6 +5788,7 @@ phutil_register_library_map(array( 'PhabricatorNotificationStatusController' => 'PhabricatorNotificationController', 'PhabricatorNotificationStatusView' => 'AphrontTagView', 'PhabricatorNotificationTestController' => 'PhabricatorNotificationController', + 'PhabricatorNotificationTestFeedStory' => 'PhabricatorFeedStory', 'PhabricatorNotificationUIExample' => 'PhabricatorUIExample', 'PhabricatorNotificationsApplication' => 'PhabricatorApplication', 'PhabricatorNuanceApplication' => 'PhabricatorApplication', diff --git a/src/applications/notification/builder/PhabricatorNotificationBuilder.php b/src/applications/notification/builder/PhabricatorNotificationBuilder.php index 57cf568777..dd4e19dcfb 100644 --- a/src/applications/notification/builder/PhabricatorNotificationBuilder.php +++ b/src/applications/notification/builder/PhabricatorNotificationBuilder.php @@ -3,9 +3,11 @@ final class PhabricatorNotificationBuilder extends Phobject { private $stories; + private $parsedStories; private $user = null; public function __construct(array $stories) { + assert_instances_of($stories, 'PhabricatorFeedStory'); $this->stories = $stories; } @@ -14,7 +16,11 @@ final class PhabricatorNotificationBuilder extends Phobject { return $this; } - public function buildView() { + private function parseStories() { + + if ($this->parsedStories) { + return $this->parsedStories; + } $stories = $this->stories; $stories = mpull($stories, null, 'getChronologicalKey'); @@ -100,6 +106,12 @@ final class PhabricatorNotificationBuilder extends Phobject { $stories = mpull($stories, null, 'getChronologicalKey'); krsort($stories); + $this->parsedStories = $stories; + return $stories; + } + + public function buildView() { + $stories = $this->parseStories(); $null_view = new AphrontNullView(); foreach ($stories as $story) { @@ -114,4 +126,39 @@ final class PhabricatorNotificationBuilder extends Phobject { return $null_view; } + + public function buildDict() { + $stories = $this->parseStories(); + $dict = array(); + + foreach ($stories as $story) { + if ($story instanceof PhabricatorApplicationTransactionFeedStory) { + $dict[] = array( + 'desktopReady' => true, + 'title' => $story->renderText(), + 'body' => $story->renderTextBody(), + 'href' => $story->getURI(), + 'icon' => $story->getImageURI(), + ); + } else if ($story instanceof PhabricatorNotificationTestFeedStory) { + $dict[] = array( + 'desktopReady' => true, + 'title' => pht('Test Notification'), + 'body' => $story->renderText(), + 'href' => null, + 'icon' => PhabricatorUser::getDefaultProfileImageURI(), + ); + } else { + $dict[] = array( + 'desktopReady' => false, + 'title' => null, + 'body' => null, + 'href' => null, + 'icon' => null, + ); + } + } + + return $dict; + } } diff --git a/src/applications/notification/controller/PhabricatorNotificationIndividualController.php b/src/applications/notification/controller/PhabricatorNotificationIndividualController.php index 684d83c274..6e6f7361df 100644 --- a/src/applications/notification/controller/PhabricatorNotificationIndividualController.php +++ b/src/applications/notification/controller/PhabricatorNotificationIndividualController.php @@ -33,10 +33,17 @@ final class PhabricatorNotificationIndividualController $builder = new PhabricatorNotificationBuilder(array($story)); $content = $builder->buildView()->render(); + $dict = $builder->buildDict(); + $data = $dict[0]; $response = array( 'pertinent' => true, 'primaryObjectPHID' => $story->getPrimaryObjectPHID(), + 'desktopReady' => $data['desktopReady'], + 'href' => $data['href'], + 'icon' => $data['icon'], + 'title' => $data['title'], + 'body' => $data['body'], 'content' => hsprintf('%s', $content), ); diff --git a/src/applications/notification/controller/PhabricatorNotificationTestController.php b/src/applications/notification/controller/PhabricatorNotificationTestController.php index b559144fbe..706242818c 100644 --- a/src/applications/notification/controller/PhabricatorNotificationTestController.php +++ b/src/applications/notification/controller/PhabricatorNotificationTestController.php @@ -7,7 +7,7 @@ final class PhabricatorNotificationTestController $request = $this->getRequest(); $viewer = $request->getUser(); - $story_type = 'PhabricatorNotificationAdHocFeedStory'; + $story_type = 'PhabricatorNotificationTestFeedStory'; $story_data = array( 'title' => pht( 'This is a test notification, sent at %s.', diff --git a/src/applications/notification/feed/PhabricatorNotificationAdHocFeedStory.php b/src/applications/notification/feed/PhabricatorNotificationTestFeedStory.php similarity index 85% rename from src/applications/notification/feed/PhabricatorNotificationAdHocFeedStory.php rename to src/applications/notification/feed/PhabricatorNotificationTestFeedStory.php index 286b81f01f..ad984431ba 100644 --- a/src/applications/notification/feed/PhabricatorNotificationAdHocFeedStory.php +++ b/src/applications/notification/feed/PhabricatorNotificationTestFeedStory.php @@ -1,6 +1,6 @@ getAuthorPHID(); diff --git a/src/applications/settings/panel/PhabricatorDesktopNotificationsSettingsPanel.php b/src/applications/settings/panel/PhabricatorDesktopNotificationsSettingsPanel.php new file mode 100644 index 0000000000..ff1e39577a --- /dev/null +++ b/src/applications/settings/panel/PhabricatorDesktopNotificationsSettingsPanel.php @@ -0,0 +1,159 @@ +getUser(); + $preferences = $user->loadPreferences(); + + $pref = PhabricatorUserPreferences::PREFERENCE_DESKTOP_NOTIFICATIONS; + + if ($request->isFormPost()) { + $notifications = $request->getInt($pref); + $preferences->setPreference($pref, $notifications); + $preferences->save(); + return id(new AphrontRedirectResponse()) + ->setURI($this->getPanelURI('?saved=true')); + } + + $title = pht('Desktop Notifications'); + $control_id = celerity_generate_unique_node_id(); + $status_id = celerity_generate_unique_node_id(); + $browser_status_id = celerity_generate_unique_node_id(); + $cancel_ask = pht( + 'The dialog asking for permission to send desktop notifications was '. + 'closed without granting permission. Only application notifications '. + 'will be sent.'); + $accept_ask = pht( + 'Click "Save Preference" to persist these changes.'); + $reject_ask = pht( + 'Permission for desktop notifications was denied. Only application '. + 'notifications will be sent.'); + $no_support = pht( + 'This web browser does not support desktop notifications. Only '. + 'application notifications will be sent for this browser regardless of '. + 'this preference.'); + $default_status = phutil_tag( + 'span', + array(), + array( + pht('This browser has not yet granted permission to send desktop '. + 'notifications for this Phabricator instance.'), + phutil_tag('br'), + phutil_tag('br'), + javelin_tag( + 'button', + array( + 'sigil' => 'desktop-notifications-permission-button', + 'class' => 'green', + ), + pht('Grant Permission')), + )); + $granted_status = phutil_tag( + 'span', + array(), + pht('This browser has been granted permission to send desktop '. + 'notifications for this Phabricator instance.')); + $denied_status = phutil_tag( + 'span', + array(), + pht('This browser has denied permission to send desktop notifications '. + 'for this Phabricator instance. Consult your browser settings / '. + 'documentation to figure out how to clear this setting, do so, '. + 'and then re-visit this page to grant permission.')); + $status_box = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) + ->setID($status_id) + ->setIsHidden(true) + ->appendChild($accept_ask); + + $control_config = array( + 'controlID' => $control_id, + 'statusID' => $status_id, + 'browserStatusID' => $browser_status_id, + 'defaultMode' => 0, + 'desktopMode' => 1, + 'cancelAsk' => $cancel_ask, + 'grantedAsk' => $accept_ask, + 'deniedAsk' => $reject_ask, + 'defaultStatus' => $default_status, + 'deniedStatus' => $denied_status, + 'grantedStatus' => $granted_status, + 'noSupport' => $no_support, + ); + + $form = id(new AphrontFormView()) + ->setUser($user) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel($title) + ->setControlID($control_id) + ->setName($pref) + ->setValue($preferences->getPreference($pref)) + ->setOptions( + array( + 1 => pht('Send Desktop Notifications Too'), + 0 => pht('Send Application Notifications Only'), + )) + ->setCaption( + pht( + 'Should Phabricator send desktop notifications? These are sent '. + 'in addition to the notifications within the Phabricator '. + 'application.')) + ->initBehavior( + 'desktop-notifications-control', + $control_config)) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Save Preference'))); + + $test_icon = id(new PHUIIconView()) + ->setIconFont('fa-exclamation-triangle'); + $test_button = id(new PHUIButtonView()) + ->setTag('a') + ->setWorkflow(true) + ->setText(pht('Send Test Notification')) + ->setHref('/notification/test/') + ->setIcon($test_icon); + + $form_box = id(new PHUIObjectBoxView()) + ->setHeader( + id(new PHUIHeaderView()) + ->setHeader(pht('Desktop Notifications')) + ->addActionLink($test_button)) + ->setForm($form) + ->setInfoView($status_box) + ->setFormSaved($request->getBool('saved')); + + $browser_status_box = id(new PHUIInfoView()) + ->setID($browser_status_id) + ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) + ->setIsHidden(true) + ->appendChild($default_status); + + return array( + $form_box, + $browser_status_box, + ); + } + +} diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php index cb04f59392..8a39506c93 100644 --- a/src/applications/settings/storage/PhabricatorUserPreferences.php +++ b/src/applications/settings/storage/PhabricatorUserPreferences.php @@ -38,6 +38,8 @@ final class PhabricatorUserPreferences extends PhabricatorUserDAO { const PREFERENCE_CONPH_NOTIFICATIONS = 'conph-notifications'; const PREFERENCE_CONPHERENCE_COLUMN = 'conpherence-column'; + const PREFERENCE_DESKTOP_NOTIFICATIONS = 'desktop-notifications'; + // These are in an unusual order for historic reasons. const MAILTAG_PREFERENCE_NOTIFY = 0; const MAILTAG_PREFERENCE_EMAIL = 1; diff --git a/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php b/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php index bc3d2ffa39..3a840d73a1 100644 --- a/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php +++ b/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php @@ -116,6 +116,35 @@ class PhabricatorApplicationTransactionFeedStory return $text; } + public function renderTextBody() { + $all_bodies = ''; + $new_target = PhabricatorApplicationTransaction::TARGET_TEXT; + $xaction_phids = $this->getValue('transactionPHIDs'); + foreach ($xaction_phids as $xaction_phid) { + $secondary_xaction = $this->getObject($xaction_phid); + $old_target = $secondary_xaction->getRenderingTarget(); + $secondary_xaction->setRenderingTarget($new_target); + $secondary_xaction->setHandles($this->getHandles()); + + $body = $secondary_xaction->getBodyForMail(); + if (nonempty($body)) { + $all_bodies .= $body."\n"; + } + $secondary_xaction->setRenderingTarget($old_target); + } + return trim($all_bodies); + } + + public function getImageURI() { + $author_phid = $this->getPrimaryTransaction()->getAuthorPHID(); + return $this->getHandle($author_phid)->getImageURI(); + } + + public function getURI() { + $handle = $this->getHandle($this->getPrimaryObjectPHID()); + return PhabricatorEnv::getProductionURI($handle->getURI()); + } + public function renderAsTextForDoorkeeper( DoorkeeperFeedStoryPublisher $publisher) { diff --git a/src/view/AphrontView.php b/src/view/AphrontView.php index d4c34e8bda..91565ca6bd 100644 --- a/src/view/AphrontView.php +++ b/src/view/AphrontView.php @@ -143,6 +143,7 @@ abstract class AphrontView extends Phobject $name, $config, $this->getDefaultResourceSource()); + return $this; } diff --git a/src/view/form/PHUIInfoView.php b/src/view/form/PHUIInfoView.php index f6e6f9b054..2ebbbe44e7 100644 --- a/src/view/form/PHUIInfoView.php +++ b/src/view/form/PHUIInfoView.php @@ -13,6 +13,7 @@ final class PHUIInfoView extends AphrontView { private $severity; private $id; private $buttons = array(); + private $isHidden; public function setTitle($title) { $this->title = $title; @@ -34,6 +35,11 @@ final class PHUIInfoView extends AphrontView { return $this; } + public function setIsHidden($bool) { + $this->isHidden = $bool; + return $this; + } + public function addButton(PHUIButtonView $button) { $this->buttons[] = $button; @@ -112,6 +118,7 @@ final class PHUIInfoView extends AphrontView { array( 'id' => $this->id, 'class' => $classes, + 'style' => $this->isHidden ? 'display: none;' : null, ), array( $buttons, diff --git a/src/view/phui/PHUIFeedStoryView.php b/src/view/phui/PHUIFeedStoryView.php index 59b600fa1b..35d0f76680 100644 --- a/src/view/phui/PHUIFeedStoryView.php +++ b/src/view/phui/PHUIFeedStoryView.php @@ -54,6 +54,10 @@ final class PHUIFeedStoryView extends AphrontView { return $this; } + public function getImage() { + return $this->image; + } + public function setImageHref($image_href) { $this->imageHref = $image_href; return $this; diff --git a/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js b/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js index 69ce78893d..2e381d018c 100644 --- a/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js +++ b/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js @@ -75,6 +75,12 @@ JX.behavior('aphlict-listen', function(config) { // Show the notification itself. new JX.Notification() .setContent(JX.$H(response.content)) + .setDesktopReady(response.desktopReady) + .setKey(response.primaryObjectPHID) + .setTitle(response.title) + .setBody(response.body) + .setHref(response.href) + .setIcon(response.icon) .show(); // If the notification affected an object on this page, show a diff --git a/webroot/rsrc/js/application/aphlict/behavior-desktop-notifications-control.js b/webroot/rsrc/js/application/aphlict/behavior-desktop-notifications-control.js new file mode 100644 index 0000000000..d3cedc8615 --- /dev/null +++ b/webroot/rsrc/js/application/aphlict/behavior-desktop-notifications-control.js @@ -0,0 +1,120 @@ +/** + * @provides javelin-behavior-desktop-notifications-control + * @requires javelin-behavior + * javelin-stratcom + * javelin-dom + * javelin-uri + * phabricator-notification + */ + +JX.behavior('desktop-notifications-control', function(config, statics) { + + function findEl(id) { + var el = null; + try { + el = JX.$(id); + } catch (e) { + // not found + } + return el; + } + function updateFormStatus(permission) { + var statusEl = findEl(config.statusID); + if (!statusEl) { + return; + } + switch (permission) { + case 'default': + JX.DOM.setContent(statusEl.firstChild, config.cancelAsk); + break; + case 'granted': + JX.DOM.setContent(statusEl.firstChild, config.grantedAsk); + break; + case 'denied': + JX.DOM.setContent(statusEl.firstChild, config.deniedAsk); + break; + } + JX.DOM.show(statusEl); + } + + function updateBrowserStatus(permission) { + var browserStatusEl = findEl(config.browserStatusID); + if (!browserStatusEl) { + return; + } + switch (permission) { + case 'default': + JX.DOM.alterClass(browserStatusEl, 'phui-info-severity-notice', true); + JX.DOM.alterClass(browserStatusEl, 'phui-info-severity-success', false); + JX.DOM.alterClass(browserStatusEl, 'phui-info-severity-error', false); + JX.DOM.setContent(browserStatusEl, JX.$H(config.defaultStatus)); + break; + case 'granted': + JX.DOM.alterClass(browserStatusEl, 'phui-info-severity-success', true); + JX.DOM.alterClass(browserStatusEl, 'phui-info-severity-notice', false); + JX.DOM.alterClass(browserStatusEl, 'phui-info-severity-error', false); + JX.DOM.setContent(browserStatusEl, JX.$H(config.grantedStatus)); + break; + case 'denied': + JX.DOM.alterClass(browserStatusEl, 'phui-info-severity-error', true); + JX.DOM.alterClass(browserStatusEl, 'phui-info-severity-notice', false); + JX.DOM.alterClass(browserStatusEl, 'phui-info-severity-success', false); + JX.DOM.setContent(browserStatusEl, JX.$H(config.deniedStatus)); + break; + } + JX.DOM.show(browserStatusEl); + } + + function installSelectListener() { + var controlEl = findEl(config.controlID); + if (!controlEl) { + return; + } + var select = JX.DOM.find(controlEl, 'select'); + JX.DOM.listen( + select, + 'change', + null, + function (e) { + if (!JX.Notification.supportsDesktopNotifications()) { + return; + } + var value = e.getTarget().value; + if (value == config.desktopMode) { + window.Notification.requestPermission( + function (permission) { + updateFormStatus(permission); + updateBrowserStatus(permission); + }); + } else { + var statusEl = JX.$(config.statusID); + JX.DOM.hide(statusEl); + } + }); + } + + function install() { + JX.Stratcom.listen( + 'click', + 'desktop-notifications-permission-button', + function () { + window.Notification.requestPermission( + function (permission) { + updateFormStatus(permission); + updateBrowserStatus(permission); + }); + }); + + return true; + } + + statics.installed = statics.installed || install(); + if (!JX.Notification.supportsDesktopNotifications()) { + var statusEl = JX.$(config.statusID); + JX.DOM.setContent(statusEl.firstChild, config.noSupport); + JX.DOM.show(statusEl); + } else { + updateBrowserStatus(window.Notification.permission); + } + installSelectListener(); +}); diff --git a/webroot/rsrc/js/core/Notification.js b/webroot/rsrc/js/core/Notification.js index 0b66825b9f..50585419c5 100644 --- a/webroot/rsrc/js/core/Notification.js +++ b/webroot/rsrc/js/core/Notification.js @@ -26,15 +26,43 @@ JX.install('Notification', { _visible : false, _hideTimer : null, _duration : 12000, + _desktopReady : false, + _key : null, + _title : null, + _body : null, + _href : null, + _icon : null, show : function() { + var self = JX.Notification; if (!this._visible) { this._visible = true; - var self = JX.Notification; self._show(this); this._updateTimer(); } + + if (self.supportsDesktopNotifications() && + self.desktopNotificationsEnabled() && + this._desktopReady) { + // Note: specifying "tag" means that notifications with matching + // keys will aggregate. + var n = new window.Notification(this._title, { + icon: this._icon, + body: this._body, + tag: this._key, + }); + n.onclick = JX.bind(n, function (href) { + this.close(); + window.focus(); + if (href) { + JX.$U(href).go(); + } + }, this._href); + // Note: some OS / browsers do this automagically; make the behavior + // happen everywhere. + setTimeout(n.close.bind(n), this._duration); + } return this; }, @@ -59,6 +87,36 @@ JX.install('Notification', { return this; }, + setDesktopReady : function(ready) { + this._desktopReady = ready; + return this; + }, + + setTitle : function(title) { + this._title = title; + return this; + }, + + setBody : function(body) { + this._body = body; + return this; + }, + + setHref : function(href) { + this._href = href; + return this; + }, + + setKey : function(key) { + this._key = key; + return this; + }, + + setIcon : function(icon) { + this._icon = icon; + return this; + }, + /** * Set duration before the notification fades away, in milliseconds. If set * to 0, the notification persists until dismissed. @@ -97,6 +155,12 @@ JX.install('Notification', { }, statics : { + supportsDesktopNotifications : function () { + return 'Notification' in window; + }, + desktopNotificationsEnabled : function () { + return window.Notification.permission === 'granted'; + }, _container : null, _listening : false, _active : [],