1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-28 00:10:57 +01:00

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
This commit is contained in:
Bob Trahan 2015-06-22 13:11:37 -07:00
parent 95fe4f9451
commit 1bb2978a89
15 changed files with 489 additions and 32 deletions

View file

@ -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',

View file

@ -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',

View file

@ -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;
}
}

View file

@ -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),
);

View file

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

View file

@ -1,6 +1,6 @@
<?php
final class PhabricatorNotificationAdHocFeedStory extends PhabricatorFeedStory {
final class PhabricatorNotificationTestFeedStory extends PhabricatorFeedStory {
public function getPrimaryObjectPHID() {
return $this->getAuthorPHID();

View file

@ -0,0 +1,159 @@
<?php
final class PhabricatorDesktopNotificationsSettingsPanel
extends PhabricatorSettingsPanel {
public function isEnabled() {
return PhabricatorEnv::getEnvConfig('notification.enabled') &&
PhabricatorApplication::isClassInstalled(
'PhabricatorNotificationsApplication');
}
public function getPanelKey() {
return 'desktopnotifications';
}
public function getPanelName() {
return pht('Desktop Notifications');
}
public function getPanelGroup() {
return pht('Application Settings');
}
public function processRequest(AphrontRequest $request) {
$user = $request->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,
);
}
}

View file

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

View file

@ -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) {

View file

@ -143,6 +143,7 @@ abstract class AphrontView extends Phobject
$name,
$config,
$this->getDefaultResourceSource());
return $this;
}

View file

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

View file

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

View file

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

View file

@ -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();
});

View file

@ -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 : [],