@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.

Desktop Notification support

Summary:
Fixes T4139. Adds a "Desktop Notifications" panel to settings. For now, we start with "Send Desktop Notifications Too" functionality. We can try to be fancy later and only send desktop notifications if the web app doesn't have focus, etc.

Test Plan:
Made some comments as a test user on a task and got purdy desktop notifications using Chrome. Then did it again with Firefox.

Played around with permissions form with Chrome and got helpful information about what was up. Played around with Firefox and got similar results, except canceling the dialogue didn't invoke my handler code somehow. Oh Firefox!

Reviewers: epriestley

Reviewed By: epriestley

Subscribers: rbalik, tycho.tatitscheff, joshuaspence, epriestley, Korvin

Maniphest Tasks: T4139

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

+489 -32
+35 -26
resources/celerity/map.php
··· 8 8 return array( 9 9 'names' => array( 10 10 'core.pkg.css' => 'eb51e6dc', 11 - 'core.pkg.js' => 'e0117d99', 11 + 'core.pkg.js' => '711e63c0', 12 12 'darkconsole.pkg.js' => 'e7393ebb', 13 13 'differential.pkg.css' => '02273347', 14 14 'differential.pkg.js' => 'ebef29b1', ··· 328 328 'rsrc/image/texture/table_header_tall.png' => 'd56b434f', 329 329 'rsrc/js/application/aphlict/Aphlict.js' => '5359e785', 330 330 'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => '995ad707', 331 - 'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => 'b1a59974', 331 + 'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => 'fb20ac8d', 332 332 'rsrc/js/application/aphlict/behavior-aphlict-status.js' => 'ea681761', 333 + 'rsrc/js/application/aphlict/behavior-desktop-notifications-control.js' => 'edd1ba66', 333 334 'rsrc/js/application/auth/behavior-persona-login.js' => '9414ff18', 334 335 'rsrc/js/application/calendar/behavior-day-view.js' => '5c46cff2', 335 336 'rsrc/js/application/calendar/behavior-event-all-day.js' => '38dcf3c8', ··· 429 430 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', 430 431 'rsrc/js/core/KeyboardShortcutManager.js' => 'c1700f6f', 431 432 'rsrc/js/core/MultirowRowManager.js' => 'b5d57730', 432 - 'rsrc/js/core/Notification.js' => '0c6946e7', 433 + 'rsrc/js/core/Notification.js' => 'ccf1cbf8', 433 434 'rsrc/js/core/Prefab.js' => '6920d200', 434 435 'rsrc/js/core/ShapedRequest.js' => '7cbe244b', 435 436 'rsrc/js/core/TextAreaUtils.js' => '5c93c52c', ··· 534 535 'javelin-aphlict' => '5359e785', 535 536 'javelin-behavior' => '61cbc29a', 536 537 'javelin-behavior-aphlict-dropdown' => '995ad707', 537 - 'javelin-behavior-aphlict-listen' => 'b1a59974', 538 + 'javelin-behavior-aphlict-listen' => 'fb20ac8d', 538 539 'javelin-behavior-aphlict-status' => 'ea681761', 539 540 'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884', 540 541 'javelin-behavior-aphront-crop' => 'fa0f4fc2', ··· 556 557 'javelin-behavior-dashboard-query-panel-select' => '453c5375', 557 558 'javelin-behavior-dashboard-tab-panel' => 'd4eecc63', 558 559 'javelin-behavior-day-view' => '5c46cff2', 560 + 'javelin-behavior-desktop-notifications-control' => 'edd1ba66', 559 561 'javelin-behavior-device' => 'a205cf28', 560 562 'javelin-behavior-differential-add-reviewers-and-ccs' => 'e10f8e18', 561 563 'javelin-behavior-differential-comment-jump' => '4fdb476d', ··· 726 728 'phabricator-keyboard-shortcut-manager' => 'c1700f6f', 727 729 'phabricator-main-menu-view' => '663e3810', 728 730 'phabricator-nav-view-css' => '7aeaf435', 729 - 'phabricator-notification' => '0c6946e7', 731 + 'phabricator-notification' => 'ccf1cbf8', 730 732 'phabricator-notification-css' => '9c279160', 731 733 'phabricator-notification-menu-css' => '3c9d8aa1', 732 734 'phabricator-object-selector-css' => '029a133d', ··· 892 894 'javelin-dom', 893 895 'javelin-router', 894 896 ), 895 - '0c6946e7' => array( 896 - 'javelin-install', 897 - 'javelin-dom', 898 - 'javelin-stratcom', 899 - 'javelin-util', 900 - 'phabricator-notification-css', 901 - ), 902 897 '0f764c35' => array( 903 898 'javelin-install', 904 899 'javelin-util', ··· 1644 1639 'javelin-util', 1645 1640 'phabricator-shaped-request', 1646 1641 ), 1647 - 'b1a59974' => array( 1648 - 'javelin-behavior', 1649 - 'javelin-aphlict', 1650 - 'javelin-stratcom', 1651 - 'javelin-request', 1652 - 'javelin-uri', 1653 - 'javelin-dom', 1654 - 'javelin-json', 1655 - 'javelin-router', 1656 - 'javelin-util', 1657 - 'javelin-leader', 1658 - 'javelin-sound', 1659 - 'phabricator-notification', 1660 - ), 1661 1642 'b1f0ccee' => array( 1662 1643 'javelin-install', 1663 1644 'javelin-dom', ··· 1791 1772 'javelin-dom', 1792 1773 'javelin-stratcom', 1793 1774 'phabricator-phtize', 1775 + ), 1776 + 'ccf1cbf8' => array( 1777 + 'javelin-install', 1778 + 'javelin-dom', 1779 + 'javelin-stratcom', 1780 + 'javelin-util', 1781 + 'phabricator-notification-css', 1794 1782 ), 1795 1783 'cf86d16a' => array( 1796 1784 'javelin-behavior', ··· 1939 1927 'phabricator-phtize', 1940 1928 'javelin-dom', 1941 1929 ), 1930 + 'edd1ba66' => array( 1931 + 'javelin-behavior', 1932 + 'javelin-stratcom', 1933 + 'javelin-dom', 1934 + 'javelin-uri', 1935 + 'phabricator-notification', 1936 + ), 1942 1937 'eeaa9e5a' => array( 1943 1938 'javelin-behavior', 1944 1939 'javelin-stratcom', ··· 2013 2008 'javelin-dom', 2014 2009 'javelin-vector', 2015 2010 'javelin-magical-init', 2011 + ), 2012 + 'fb20ac8d' => array( 2013 + 'javelin-behavior', 2014 + 'javelin-aphlict', 2015 + 'javelin-stratcom', 2016 + 'javelin-request', 2017 + 'javelin-uri', 2018 + 'javelin-dom', 2019 + 'javelin-json', 2020 + 'javelin-router', 2021 + 'javelin-util', 2022 + 'javelin-leader', 2023 + 'javelin-sound', 2024 + 'phabricator-notification', 2016 2025 ), 2017 2026 'fbe497e7' => array( 2018 2027 'javelin-behavior',
+4 -2
src/__phutil_library_map__.php
··· 1789 1789 'PhabricatorDatabaseSetupCheck' => 'applications/config/check/PhabricatorDatabaseSetupCheck.php', 1790 1790 'PhabricatorDateTimeSettingsPanel' => 'applications/settings/panel/PhabricatorDateTimeSettingsPanel.php', 1791 1791 'PhabricatorDebugController' => 'applications/system/controller/PhabricatorDebugController.php', 1792 + 'PhabricatorDesktopNotificationsSettingsPanel' => 'applications/settings/panel/PhabricatorDesktopNotificationsSettingsPanel.php', 1792 1793 'PhabricatorDestructibleInterface' => 'applications/system/interface/PhabricatorDestructibleInterface.php', 1793 1794 'PhabricatorDestructionEngine' => 'applications/system/engine/PhabricatorDestructionEngine.php', 1794 1795 'PhabricatorDeveloperConfigOptions' => 'applications/config/option/PhabricatorDeveloperConfigOptions.php', ··· 2133 2134 'PhabricatorNamedQueryQuery' => 'applications/search/query/PhabricatorNamedQueryQuery.php', 2134 2135 'PhabricatorNavigationRemarkupRule' => 'infrastructure/markup/rule/PhabricatorNavigationRemarkupRule.php', 2135 2136 'PhabricatorNeverTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorNeverTriggerClock.php', 2136 - 'PhabricatorNotificationAdHocFeedStory' => 'applications/notification/feed/PhabricatorNotificationAdHocFeedStory.php', 2137 2137 'PhabricatorNotificationBuilder' => 'applications/notification/builder/PhabricatorNotificationBuilder.php', 2138 2138 'PhabricatorNotificationClearController' => 'applications/notification/controller/PhabricatorNotificationClearController.php', 2139 2139 'PhabricatorNotificationClient' => 'applications/notification/client/PhabricatorNotificationClient.php', ··· 2147 2147 'PhabricatorNotificationStatusController' => 'applications/notification/controller/PhabricatorNotificationStatusController.php', 2148 2148 'PhabricatorNotificationStatusView' => 'applications/notification/view/PhabricatorNotificationStatusView.php', 2149 2149 'PhabricatorNotificationTestController' => 'applications/notification/controller/PhabricatorNotificationTestController.php', 2150 + 'PhabricatorNotificationTestFeedStory' => 'applications/notification/feed/PhabricatorNotificationTestFeedStory.php', 2150 2151 'PhabricatorNotificationUIExample' => 'applications/uiexample/examples/PhabricatorNotificationUIExample.php', 2151 2152 'PhabricatorNotificationsApplication' => 'applications/notification/application/PhabricatorNotificationsApplication.php', 2152 2153 'PhabricatorNuanceApplication' => 'applications/nuance/application/PhabricatorNuanceApplication.php', ··· 5385 5386 'PhabricatorDatabaseSetupCheck' => 'PhabricatorSetupCheck', 5386 5387 'PhabricatorDateTimeSettingsPanel' => 'PhabricatorSettingsPanel', 5387 5388 'PhabricatorDebugController' => 'PhabricatorController', 5389 + 'PhabricatorDesktopNotificationsSettingsPanel' => 'PhabricatorSettingsPanel', 5388 5390 'PhabricatorDestructionEngine' => 'Phobject', 5389 5391 'PhabricatorDeveloperConfigOptions' => 'PhabricatorApplicationConfigOptions', 5390 5392 'PhabricatorDeveloperPreferencesSettingsPanel' => 'PhabricatorSettingsPanel', ··· 5773 5775 'PhabricatorNamedQueryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 5774 5776 'PhabricatorNavigationRemarkupRule' => 'PhutilRemarkupRule', 5775 5777 'PhabricatorNeverTriggerClock' => 'PhabricatorTriggerClock', 5776 - 'PhabricatorNotificationAdHocFeedStory' => 'PhabricatorFeedStory', 5777 5778 'PhabricatorNotificationBuilder' => 'Phobject', 5778 5779 'PhabricatorNotificationClearController' => 'PhabricatorNotificationController', 5779 5780 'PhabricatorNotificationClient' => 'Phobject', ··· 5787 5788 'PhabricatorNotificationStatusController' => 'PhabricatorNotificationController', 5788 5789 'PhabricatorNotificationStatusView' => 'AphrontTagView', 5789 5790 'PhabricatorNotificationTestController' => 'PhabricatorNotificationController', 5791 + 'PhabricatorNotificationTestFeedStory' => 'PhabricatorFeedStory', 5790 5792 'PhabricatorNotificationUIExample' => 'PhabricatorUIExample', 5791 5793 'PhabricatorNotificationsApplication' => 'PhabricatorApplication', 5792 5794 'PhabricatorNuanceApplication' => 'PhabricatorApplication',
+48 -1
src/applications/notification/builder/PhabricatorNotificationBuilder.php
··· 3 3 final class PhabricatorNotificationBuilder extends Phobject { 4 4 5 5 private $stories; 6 + private $parsedStories; 6 7 private $user = null; 7 8 8 9 public function __construct(array $stories) { 10 + assert_instances_of($stories, 'PhabricatorFeedStory'); 9 11 $this->stories = $stories; 10 12 } 11 13 ··· 14 16 return $this; 15 17 } 16 18 17 - public function buildView() { 19 + private function parseStories() { 20 + 21 + if ($this->parsedStories) { 22 + return $this->parsedStories; 23 + } 18 24 19 25 $stories = $this->stories; 20 26 $stories = mpull($stories, null, 'getChronologicalKey'); ··· 100 106 $stories = mpull($stories, null, 'getChronologicalKey'); 101 107 krsort($stories); 102 108 109 + $this->parsedStories = $stories; 110 + return $stories; 111 + } 112 + 113 + public function buildView() { 114 + $stories = $this->parseStories(); 103 115 $null_view = new AphrontNullView(); 104 116 105 117 foreach ($stories as $story) { ··· 113 125 } 114 126 115 127 return $null_view; 128 + } 129 + 130 + public function buildDict() { 131 + $stories = $this->parseStories(); 132 + $dict = array(); 133 + 134 + foreach ($stories as $story) { 135 + if ($story instanceof PhabricatorApplicationTransactionFeedStory) { 136 + $dict[] = array( 137 + 'desktopReady' => true, 138 + 'title' => $story->renderText(), 139 + 'body' => $story->renderTextBody(), 140 + 'href' => $story->getURI(), 141 + 'icon' => $story->getImageURI(), 142 + ); 143 + } else if ($story instanceof PhabricatorNotificationTestFeedStory) { 144 + $dict[] = array( 145 + 'desktopReady' => true, 146 + 'title' => pht('Test Notification'), 147 + 'body' => $story->renderText(), 148 + 'href' => null, 149 + 'icon' => PhabricatorUser::getDefaultProfileImageURI(), 150 + ); 151 + } else { 152 + $dict[] = array( 153 + 'desktopReady' => false, 154 + 'title' => null, 155 + 'body' => null, 156 + 'href' => null, 157 + 'icon' => null, 158 + ); 159 + } 160 + } 161 + 162 + return $dict; 116 163 } 117 164 }
+7
src/applications/notification/controller/PhabricatorNotificationIndividualController.php
··· 33 33 34 34 $builder = new PhabricatorNotificationBuilder(array($story)); 35 35 $content = $builder->buildView()->render(); 36 + $dict = $builder->buildDict(); 37 + $data = $dict[0]; 36 38 37 39 $response = array( 38 40 'pertinent' => true, 39 41 'primaryObjectPHID' => $story->getPrimaryObjectPHID(), 42 + 'desktopReady' => $data['desktopReady'], 43 + 'href' => $data['href'], 44 + 'icon' => $data['icon'], 45 + 'title' => $data['title'], 46 + 'body' => $data['body'], 40 47 'content' => hsprintf('%s', $content), 41 48 ); 42 49
+1 -1
src/applications/notification/controller/PhabricatorNotificationTestController.php
··· 7 7 $request = $this->getRequest(); 8 8 $viewer = $request->getUser(); 9 9 10 - $story_type = 'PhabricatorNotificationAdHocFeedStory'; 10 + $story_type = 'PhabricatorNotificationTestFeedStory'; 11 11 $story_data = array( 12 12 'title' => pht( 13 13 'This is a test notification, sent at %s.',
+1 -1
src/applications/notification/feed/PhabricatorNotificationAdHocFeedStory.php src/applications/notification/feed/PhabricatorNotificationTestFeedStory.php
··· 1 1 <?php 2 2 3 - final class PhabricatorNotificationAdHocFeedStory extends PhabricatorFeedStory { 3 + final class PhabricatorNotificationTestFeedStory extends PhabricatorFeedStory { 4 4 5 5 public function getPrimaryObjectPHID() { 6 6 return $this->getAuthorPHID();
+159
src/applications/settings/panel/PhabricatorDesktopNotificationsSettingsPanel.php
··· 1 + <?php 2 + 3 + final class PhabricatorDesktopNotificationsSettingsPanel 4 + extends PhabricatorSettingsPanel { 5 + 6 + public function isEnabled() { 7 + return PhabricatorEnv::getEnvConfig('notification.enabled') && 8 + PhabricatorApplication::isClassInstalled( 9 + 'PhabricatorNotificationsApplication'); 10 + } 11 + 12 + public function getPanelKey() { 13 + return 'desktopnotifications'; 14 + } 15 + 16 + public function getPanelName() { 17 + return pht('Desktop Notifications'); 18 + } 19 + 20 + public function getPanelGroup() { 21 + return pht('Application Settings'); 22 + } 23 + 24 + public function processRequest(AphrontRequest $request) { 25 + $user = $request->getUser(); 26 + $preferences = $user->loadPreferences(); 27 + 28 + $pref = PhabricatorUserPreferences::PREFERENCE_DESKTOP_NOTIFICATIONS; 29 + 30 + if ($request->isFormPost()) { 31 + $notifications = $request->getInt($pref); 32 + $preferences->setPreference($pref, $notifications); 33 + $preferences->save(); 34 + return id(new AphrontRedirectResponse()) 35 + ->setURI($this->getPanelURI('?saved=true')); 36 + } 37 + 38 + $title = pht('Desktop Notifications'); 39 + $control_id = celerity_generate_unique_node_id(); 40 + $status_id = celerity_generate_unique_node_id(); 41 + $browser_status_id = celerity_generate_unique_node_id(); 42 + $cancel_ask = pht( 43 + 'The dialog asking for permission to send desktop notifications was '. 44 + 'closed without granting permission. Only application notifications '. 45 + 'will be sent.'); 46 + $accept_ask = pht( 47 + 'Click "Save Preference" to persist these changes.'); 48 + $reject_ask = pht( 49 + 'Permission for desktop notifications was denied. Only application '. 50 + 'notifications will be sent.'); 51 + $no_support = pht( 52 + 'This web browser does not support desktop notifications. Only '. 53 + 'application notifications will be sent for this browser regardless of '. 54 + 'this preference.'); 55 + $default_status = phutil_tag( 56 + 'span', 57 + array(), 58 + array( 59 + pht('This browser has not yet granted permission to send desktop '. 60 + 'notifications for this Phabricator instance.'), 61 + phutil_tag('br'), 62 + phutil_tag('br'), 63 + javelin_tag( 64 + 'button', 65 + array( 66 + 'sigil' => 'desktop-notifications-permission-button', 67 + 'class' => 'green', 68 + ), 69 + pht('Grant Permission')), 70 + )); 71 + $granted_status = phutil_tag( 72 + 'span', 73 + array(), 74 + pht('This browser has been granted permission to send desktop '. 75 + 'notifications for this Phabricator instance.')); 76 + $denied_status = phutil_tag( 77 + 'span', 78 + array(), 79 + pht('This browser has denied permission to send desktop notifications '. 80 + 'for this Phabricator instance. Consult your browser settings / '. 81 + 'documentation to figure out how to clear this setting, do so, '. 82 + 'and then re-visit this page to grant permission.')); 83 + $status_box = id(new PHUIInfoView()) 84 + ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) 85 + ->setID($status_id) 86 + ->setIsHidden(true) 87 + ->appendChild($accept_ask); 88 + 89 + $control_config = array( 90 + 'controlID' => $control_id, 91 + 'statusID' => $status_id, 92 + 'browserStatusID' => $browser_status_id, 93 + 'defaultMode' => 0, 94 + 'desktopMode' => 1, 95 + 'cancelAsk' => $cancel_ask, 96 + 'grantedAsk' => $accept_ask, 97 + 'deniedAsk' => $reject_ask, 98 + 'defaultStatus' => $default_status, 99 + 'deniedStatus' => $denied_status, 100 + 'grantedStatus' => $granted_status, 101 + 'noSupport' => $no_support, 102 + ); 103 + 104 + $form = id(new AphrontFormView()) 105 + ->setUser($user) 106 + ->appendChild( 107 + id(new AphrontFormSelectControl()) 108 + ->setLabel($title) 109 + ->setControlID($control_id) 110 + ->setName($pref) 111 + ->setValue($preferences->getPreference($pref)) 112 + ->setOptions( 113 + array( 114 + 1 => pht('Send Desktop Notifications Too'), 115 + 0 => pht('Send Application Notifications Only'), 116 + )) 117 + ->setCaption( 118 + pht( 119 + 'Should Phabricator send desktop notifications? These are sent '. 120 + 'in addition to the notifications within the Phabricator '. 121 + 'application.')) 122 + ->initBehavior( 123 + 'desktop-notifications-control', 124 + $control_config)) 125 + ->appendChild( 126 + id(new AphrontFormSubmitControl()) 127 + ->setValue(pht('Save Preference'))); 128 + 129 + $test_icon = id(new PHUIIconView()) 130 + ->setIconFont('fa-exclamation-triangle'); 131 + $test_button = id(new PHUIButtonView()) 132 + ->setTag('a') 133 + ->setWorkflow(true) 134 + ->setText(pht('Send Test Notification')) 135 + ->setHref('/notification/test/') 136 + ->setIcon($test_icon); 137 + 138 + $form_box = id(new PHUIObjectBoxView()) 139 + ->setHeader( 140 + id(new PHUIHeaderView()) 141 + ->setHeader(pht('Desktop Notifications')) 142 + ->addActionLink($test_button)) 143 + ->setForm($form) 144 + ->setInfoView($status_box) 145 + ->setFormSaved($request->getBool('saved')); 146 + 147 + $browser_status_box = id(new PHUIInfoView()) 148 + ->setID($browser_status_id) 149 + ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) 150 + ->setIsHidden(true) 151 + ->appendChild($default_status); 152 + 153 + return array( 154 + $form_box, 155 + $browser_status_box, 156 + ); 157 + } 158 + 159 + }
+2
src/applications/settings/storage/PhabricatorUserPreferences.php
··· 38 38 const PREFERENCE_CONPH_NOTIFICATIONS = 'conph-notifications'; 39 39 const PREFERENCE_CONPHERENCE_COLUMN = 'conpherence-column'; 40 40 41 + const PREFERENCE_DESKTOP_NOTIFICATIONS = 'desktop-notifications'; 42 + 41 43 // These are in an unusual order for historic reasons. 42 44 const MAILTAG_PREFERENCE_NOTIFY = 0; 43 45 const MAILTAG_PREFERENCE_EMAIL = 1;
+29
src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php
··· 116 116 return $text; 117 117 } 118 118 119 + public function renderTextBody() { 120 + $all_bodies = ''; 121 + $new_target = PhabricatorApplicationTransaction::TARGET_TEXT; 122 + $xaction_phids = $this->getValue('transactionPHIDs'); 123 + foreach ($xaction_phids as $xaction_phid) { 124 + $secondary_xaction = $this->getObject($xaction_phid); 125 + $old_target = $secondary_xaction->getRenderingTarget(); 126 + $secondary_xaction->setRenderingTarget($new_target); 127 + $secondary_xaction->setHandles($this->getHandles()); 128 + 129 + $body = $secondary_xaction->getBodyForMail(); 130 + if (nonempty($body)) { 131 + $all_bodies .= $body."\n"; 132 + } 133 + $secondary_xaction->setRenderingTarget($old_target); 134 + } 135 + return trim($all_bodies); 136 + } 137 + 138 + public function getImageURI() { 139 + $author_phid = $this->getPrimaryTransaction()->getAuthorPHID(); 140 + return $this->getHandle($author_phid)->getImageURI(); 141 + } 142 + 143 + public function getURI() { 144 + $handle = $this->getHandle($this->getPrimaryObjectPHID()); 145 + return PhabricatorEnv::getProductionURI($handle->getURI()); 146 + } 147 + 119 148 public function renderAsTextForDoorkeeper( 120 149 DoorkeeperFeedStoryPublisher $publisher) { 121 150
+1
src/view/AphrontView.php
··· 143 143 $name, 144 144 $config, 145 145 $this->getDefaultResourceSource()); 146 + return $this; 146 147 } 147 148 148 149
+7
src/view/form/PHUIInfoView.php
··· 13 13 private $severity; 14 14 private $id; 15 15 private $buttons = array(); 16 + private $isHidden; 16 17 17 18 public function setTitle($title) { 18 19 $this->title = $title; ··· 31 32 32 33 public function setID($id) { 33 34 $this->id = $id; 35 + return $this; 36 + } 37 + 38 + public function setIsHidden($bool) { 39 + $this->isHidden = $bool; 34 40 return $this; 35 41 } 36 42 ··· 112 118 array( 113 119 'id' => $this->id, 114 120 'class' => $classes, 121 + 'style' => $this->isHidden ? 'display: none;' : null, 115 122 ), 116 123 array( 117 124 $buttons,
+4
src/view/phui/PHUIFeedStoryView.php
··· 54 54 return $this; 55 55 } 56 56 57 + public function getImage() { 58 + return $this->image; 59 + } 60 + 57 61 public function setImageHref($image_href) { 58 62 $this->imageHref = $image_href; 59 63 return $this;
+6
webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js
··· 75 75 // Show the notification itself. 76 76 new JX.Notification() 77 77 .setContent(JX.$H(response.content)) 78 + .setDesktopReady(response.desktopReady) 79 + .setKey(response.primaryObjectPHID) 80 + .setTitle(response.title) 81 + .setBody(response.body) 82 + .setHref(response.href) 83 + .setIcon(response.icon) 78 84 .show(); 79 85 80 86 // If the notification affected an object on this page, show a
+120
webroot/rsrc/js/application/aphlict/behavior-desktop-notifications-control.js
··· 1 + /** 2 + * @provides javelin-behavior-desktop-notifications-control 3 + * @requires javelin-behavior 4 + * javelin-stratcom 5 + * javelin-dom 6 + * javelin-uri 7 + * phabricator-notification 8 + */ 9 + 10 + JX.behavior('desktop-notifications-control', function(config, statics) { 11 + 12 + function findEl(id) { 13 + var el = null; 14 + try { 15 + el = JX.$(id); 16 + } catch (e) { 17 + // not found 18 + } 19 + return el; 20 + } 21 + function updateFormStatus(permission) { 22 + var statusEl = findEl(config.statusID); 23 + if (!statusEl) { 24 + return; 25 + } 26 + switch (permission) { 27 + case 'default': 28 + JX.DOM.setContent(statusEl.firstChild, config.cancelAsk); 29 + break; 30 + case 'granted': 31 + JX.DOM.setContent(statusEl.firstChild, config.grantedAsk); 32 + break; 33 + case 'denied': 34 + JX.DOM.setContent(statusEl.firstChild, config.deniedAsk); 35 + break; 36 + } 37 + JX.DOM.show(statusEl); 38 + } 39 + 40 + function updateBrowserStatus(permission) { 41 + var browserStatusEl = findEl(config.browserStatusID); 42 + if (!browserStatusEl) { 43 + return; 44 + } 45 + switch (permission) { 46 + case 'default': 47 + JX.DOM.alterClass(browserStatusEl, 'phui-info-severity-notice', true); 48 + JX.DOM.alterClass(browserStatusEl, 'phui-info-severity-success', false); 49 + JX.DOM.alterClass(browserStatusEl, 'phui-info-severity-error', false); 50 + JX.DOM.setContent(browserStatusEl, JX.$H(config.defaultStatus)); 51 + break; 52 + case 'granted': 53 + JX.DOM.alterClass(browserStatusEl, 'phui-info-severity-success', true); 54 + JX.DOM.alterClass(browserStatusEl, 'phui-info-severity-notice', false); 55 + JX.DOM.alterClass(browserStatusEl, 'phui-info-severity-error', false); 56 + JX.DOM.setContent(browserStatusEl, JX.$H(config.grantedStatus)); 57 + break; 58 + case 'denied': 59 + JX.DOM.alterClass(browserStatusEl, 'phui-info-severity-error', true); 60 + JX.DOM.alterClass(browserStatusEl, 'phui-info-severity-notice', false); 61 + JX.DOM.alterClass(browserStatusEl, 'phui-info-severity-success', false); 62 + JX.DOM.setContent(browserStatusEl, JX.$H(config.deniedStatus)); 63 + break; 64 + } 65 + JX.DOM.show(browserStatusEl); 66 + } 67 + 68 + function installSelectListener() { 69 + var controlEl = findEl(config.controlID); 70 + if (!controlEl) { 71 + return; 72 + } 73 + var select = JX.DOM.find(controlEl, 'select'); 74 + JX.DOM.listen( 75 + select, 76 + 'change', 77 + null, 78 + function (e) { 79 + if (!JX.Notification.supportsDesktopNotifications()) { 80 + return; 81 + } 82 + var value = e.getTarget().value; 83 + if (value == config.desktopMode) { 84 + window.Notification.requestPermission( 85 + function (permission) { 86 + updateFormStatus(permission); 87 + updateBrowserStatus(permission); 88 + }); 89 + } else { 90 + var statusEl = JX.$(config.statusID); 91 + JX.DOM.hide(statusEl); 92 + } 93 + }); 94 + } 95 + 96 + function install() { 97 + JX.Stratcom.listen( 98 + 'click', 99 + 'desktop-notifications-permission-button', 100 + function () { 101 + window.Notification.requestPermission( 102 + function (permission) { 103 + updateFormStatus(permission); 104 + updateBrowserStatus(permission); 105 + }); 106 + }); 107 + 108 + return true; 109 + } 110 + 111 + statics.installed = statics.installed || install(); 112 + if (!JX.Notification.supportsDesktopNotifications()) { 113 + var statusEl = JX.$(config.statusID); 114 + JX.DOM.setContent(statusEl.firstChild, config.noSupport); 115 + JX.DOM.show(statusEl); 116 + } else { 117 + updateBrowserStatus(window.Notification.permission); 118 + } 119 + installSelectListener(); 120 + });
+65 -1
webroot/rsrc/js/core/Notification.js
··· 26 26 _visible : false, 27 27 _hideTimer : null, 28 28 _duration : 12000, 29 + _desktopReady : false, 30 + _key : null, 31 + _title : null, 32 + _body : null, 33 + _href : null, 34 + _icon : null, 29 35 30 36 show : function() { 37 + var self = JX.Notification; 31 38 if (!this._visible) { 32 39 this._visible = true; 33 40 34 - var self = JX.Notification; 35 41 self._show(this); 36 42 this._updateTimer(); 37 43 } 44 + 45 + if (self.supportsDesktopNotifications() && 46 + self.desktopNotificationsEnabled() && 47 + this._desktopReady) { 48 + // Note: specifying "tag" means that notifications with matching 49 + // keys will aggregate. 50 + var n = new window.Notification(this._title, { 51 + icon: this._icon, 52 + body: this._body, 53 + tag: this._key, 54 + }); 55 + n.onclick = JX.bind(n, function (href) { 56 + this.close(); 57 + window.focus(); 58 + if (href) { 59 + JX.$U(href).go(); 60 + } 61 + }, this._href); 62 + // Note: some OS / browsers do this automagically; make the behavior 63 + // happen everywhere. 64 + setTimeout(n.close.bind(n), this._duration); 65 + } 38 66 return this; 39 67 }, 40 68 ··· 59 87 return this; 60 88 }, 61 89 90 + setDesktopReady : function(ready) { 91 + this._desktopReady = ready; 92 + return this; 93 + }, 94 + 95 + setTitle : function(title) { 96 + this._title = title; 97 + return this; 98 + }, 99 + 100 + setBody : function(body) { 101 + this._body = body; 102 + return this; 103 + }, 104 + 105 + setHref : function(href) { 106 + this._href = href; 107 + return this; 108 + }, 109 + 110 + setKey : function(key) { 111 + this._key = key; 112 + return this; 113 + }, 114 + 115 + setIcon : function(icon) { 116 + this._icon = icon; 117 + return this; 118 + }, 119 + 62 120 /** 63 121 * Set duration before the notification fades away, in milliseconds. If set 64 122 * to 0, the notification persists until dismissed. ··· 97 155 }, 98 156 99 157 statics : { 158 + supportsDesktopNotifications : function () { 159 + return 'Notification' in window; 160 + }, 161 + desktopNotificationsEnabled : function () { 162 + return window.Notification.permission === 'granted'; 163 + }, 100 164 _container : null, 101 165 _listening : false, 102 166 _active : [],