Page MenuHomePhabricator

D13219.id31956.diff
No OneTemporary

D13219.id31956.diff

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' => 'd7ecac6d',
- 'core.pkg.js' => '288f6571',
+ 'core.pkg.js' => '0bf99194',
'darkconsole.pkg.js' => 'e7393ebb',
'differential.pkg.css' => '02273347',
'differential.pkg.js' => 'ebef29b1',
@@ -328,8 +328,9 @@
'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' => 'cc7af32b',
'rsrc/js/application/aphlict/behavior-aphlict-status.js' => 'ea681761',
+ 'rsrc/js/application/aphlict/behavior-desktop-notifications-control.js' => 'ae4f02d9',
'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 @@
'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' => '902410d2',
'rsrc/js/core/Prefab.js' => '6920d200',
'rsrc/js/core/ShapedRequest.js' => '7cbe244b',
'rsrc/js/core/TextAreaUtils.js' => '5c93c52c',
@@ -534,7 +535,7 @@
'javelin-aphlict' => '5359e785',
'javelin-behavior' => '61cbc29a',
'javelin-behavior-aphlict-dropdown' => '995ad707',
- 'javelin-behavior-aphlict-listen' => 'b1a59974',
+ 'javelin-behavior-aphlict-listen' => 'cc7af32b',
'javelin-behavior-aphlict-status' => 'ea681761',
'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884',
'javelin-behavior-aphront-crop' => 'fa0f4fc2',
@@ -556,6 +557,7 @@
'javelin-behavior-dashboard-query-panel-select' => '453c5375',
'javelin-behavior-dashboard-tab-panel' => 'd4eecc63',
'javelin-behavior-day-view' => '5c46cff2',
+ 'javelin-behavior-desktop-notifications-control' => 'ae4f02d9',
'javelin-behavior-device' => 'a205cf28',
'javelin-behavior-differential-add-reviewers-and-ccs' => 'e10f8e18',
'javelin-behavior-differential-comment-jump' => '4fdb476d',
@@ -726,7 +728,7 @@
'phabricator-keyboard-shortcut-manager' => 'c1700f6f',
'phabricator-main-menu-view' => '663e3810',
'phabricator-nav-view-css' => '7aeaf435',
- 'phabricator-notification' => '0c6946e7',
+ 'phabricator-notification' => '902410d2',
'phabricator-notification-css' => '9c279160',
'phabricator-notification-menu-css' => '3c9d8aa1',
'phabricator-object-selector-css' => '029a133d',
@@ -901,13 +903,6 @@
'javelin-dom',
'javelin-router',
),
- '0c6946e7' => array(
- 'javelin-install',
- 'javelin-dom',
- 'javelin-stratcom',
- 'javelin-util',
- 'phabricator-notification-css',
- ),
'0f764c35' => array(
'javelin-install',
'javelin-util',
@@ -1536,6 +1531,13 @@
'javelin-dom',
'javelin-stratcom',
),
+ '902410d2' => array(
+ 'javelin-install',
+ 'javelin-dom',
+ 'javelin-stratcom',
+ 'javelin-util',
+ 'phabricator-notification-css',
+ ),
93568464 => array(
'javelin-behavior',
'javelin-dom',
@@ -1675,27 +1677,20 @@
'javelin-stratcom',
'javelin-install',
),
- 'b064af76' => array(
+ 'ae4f02d9' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
- 'javelin-request',
- 'javelin-util',
- 'phabricator-shaped-request',
+ 'javelin-uri',
+ 'phabricator-notification',
),
- 'b1a59974' => array(
+ 'b064af76' => array(
'javelin-behavior',
- 'javelin-aphlict',
'javelin-stratcom',
- 'javelin-request',
- 'javelin-uri',
'javelin-dom',
- 'javelin-json',
- 'javelin-router',
+ 'javelin-request',
'javelin-util',
- 'javelin-leader',
- 'javelin-sound',
- 'phabricator-notification',
+ 'phabricator-shaped-request',
),
'b1f0ccee' => array(
'javelin-install',
@@ -1822,6 +1817,20 @@
'javelin-stratcom',
'phabricator-phtize',
),
+ 'cc7af32b' => 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',
+ ),
'cf86d16a' => array(
'javelin-behavior',
'javelin-dom',
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
@@ -1754,6 +1754,7 @@
'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',
@@ -5182,6 +5183,7 @@
'PhabricatorDatabaseSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorDateTimeSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorDebugController' => 'PhabricatorController',
+ 'PhabricatorDesktopNotificationsSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorDestructionEngine' => 'Phobject',
'PhabricatorDeveloperConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorDeveloperPreferencesSettingsPanel' => 'PhabricatorSettingsPanel',
diff --git a/src/applications/notification/builder/PhabricatorNotificationBuilder.php b/src/applications/notification/builder/PhabricatorNotificationBuilder.php
--- a/src/applications/notification/builder/PhabricatorNotificationBuilder.php
+++ b/src/applications/notification/builder/PhabricatorNotificationBuilder.php
@@ -3,9 +3,11 @@
final class PhabricatorNotificationBuilder {
private $stories;
+ private $parsedStories;
private $user = null;
public function __construct(array $stories) {
+ assert_instances_of($stories, 'PhabricatorFeedStory');
$this->stories = $stories;
}
@@ -14,7 +16,11 @@
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 @@
$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,20 @@
return $null_view;
}
+
+ public function buildDict() {
+ $stories = $this->parseStories();
+ $dict = array();
+
+ foreach ($stories as $story) {
+ $dict[] = array(
+ 'title' => $story->renderText(),
+ 'body' => $story->renderTextBody(),
+ 'href' => $story->getURI(),
+ 'icon' => $story->getImageURI(),
+ );
+ }
+
+ return $dict;
+ }
}
diff --git a/src/applications/notification/controller/PhabricatorNotificationIndividualController.php b/src/applications/notification/controller/PhabricatorNotificationIndividualController.php
--- a/src/applications/notification/controller/PhabricatorNotificationIndividualController.php
+++ b/src/applications/notification/controller/PhabricatorNotificationIndividualController.php
@@ -33,10 +33,16 @@
$builder = new PhabricatorNotificationBuilder(array($story));
$content = $builder->buildView()->render();
+ $dict = $builder->buildDict();
+ $data = $dict[0];
$response = array(
'pertinent' => true,
'primaryObjectPHID' => $story->getPrimaryObjectPHID(),
+ 'icon' => $data['icon'],
+ 'title' => $data['title'],
+ 'body' => $data['body'],
+ 'href' => $data['href'],
'content' => hsprintf('%s', $content),
);
diff --git a/src/applications/settings/panel/PhabricatorDesktopNotificationsSettingsPanel.php b/src/applications/settings/panel/PhabricatorDesktopNotificationsSettingsPanel.php
new file mode 100644
--- /dev/null
+++ b/src/applications/settings/panel/PhabricatorDesktopNotificationsSettingsPanel.php
@@ -0,0 +1,119 @@
+<?php
+
+final class PhabricatorDesktopNotificationsSettingsPanel
+ extends PhabricatorSettingsPanel {
+
+ public function isEnabled() {
+ return 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();
+ $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.');
+ $conflict_settings = pht(
+ 'Permission for desktop notifications was denied, but the preference '.
+ 'was saved to enable desktop notifications too. 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.');
+ $status_box = id(new PHUIInfoView())
+ ->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
+ ->setID($status_id)
+ ->setIsHidden(true)
+ ->appendChild($accept_ask);
+
+ $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',
+ array('controlID' => $control_id,
+ 'statusID' => $status_id,
+ 'defaultMode' => 0,
+ 'desktopMode' => 1,
+ 'cancelAsk' => $cancel_ask,
+ 'grantedAsk' => $accept_ask,
+ 'deniedAsk' => $reject_ask,
+ 'noSupport' => $no_support,
+ )))
+ ->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'));
+
+ return array(
+ $form_box,
+ );
+ }
+
+}
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
@@ -37,6 +37,8 @@
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
--- a/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php
+++ b/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php
@@ -116,6 +116,35 @@
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() {
+ return PhabricatorEnv::getProductionURI(
+ $this->getHandle($this->getPrimaryObjectPHID())->getURI());
+ }
+
public function renderAsTextForDoorkeeper(
DoorkeeperFeedStoryPublisher $publisher) {
diff --git a/src/view/AphrontView.php b/src/view/AphrontView.php
--- a/src/view/AphrontView.php
+++ b/src/view/AphrontView.php
@@ -143,6 +143,7 @@
$name,
$config,
$this->getDefaultResourceSource());
+ return $this;
}
diff --git a/src/view/form/PHUIInfoView.php b/src/view/form/PHUIInfoView.php
--- a/src/view/form/PHUIInfoView.php
+++ b/src/view/form/PHUIInfoView.php
@@ -13,6 +13,7 @@
private $severity;
private $id;
private $buttons = array();
+ private $isHidden;
public function setTitle($title) {
$this->title = $title;
@@ -34,6 +35,11 @@
return $this;
}
+ public function setIsHidden($bool) {
+ $this->isHidden = $bool;
+ return $this;
+ }
+
public function addButton(PHUIButtonView $button) {
$this->buttons[] = $button;
@@ -112,6 +118,7 @@
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
--- a/src/view/phui/PHUIFeedStoryView.php
+++ b/src/view/phui/PHUIFeedStoryView.php
@@ -54,6 +54,10 @@
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
--- a/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js
+++ b/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js
@@ -75,6 +75,11 @@
// Show the notification itself.
new JX.Notification()
.setContent(JX.$H(response.content))
+ .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
--- /dev/null
+++ b/webroot/rsrc/js/application/aphlict/behavior-desktop-notifications-control.js
@@ -0,0 +1,51 @@
+/**
+ * @provides javelin-behavior-desktop-notifications-control
+ * @requires javelin-behavior
+ * javelin-stratcom
+ * javelin-dom
+ * javelin-uri
+ * phabricator-notification
+ */
+
+JX.behavior('desktop-notifications-control', function(config) {
+
+ var controlEl = JX.$(config.controlID);
+ var select = JX.DOM.find(controlEl, 'select');
+ var statusEl = JX.$(config.statusID);
+
+ JX.DOM.listen(
+ select,
+ 'change',
+ null,
+ function (e) {
+ if (!JX.Notification.supportsDesktopNotifications()) {
+ return;
+ }
+ var value = e.getTarget().value;
+ if (value == config.desktopMode) {
+ Notification.requestPermission(
+ function (permission) {
+ 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);
+ });
+ } else {
+ JX.DOM.hide(statusEl);
+ }
+ });
+
+ if (!JX.Notification.supportsDesktopNotifications()) {
+ JX.DOM.setContent(statusEl.firstChild, config.noSupport);
+ JX.DOM.show(statusEl);
+ }
+
+});
diff --git a/webroot/rsrc/js/core/Notification.js b/webroot/rsrc/js/core/Notification.js
--- a/webroot/rsrc/js/core/Notification.js
+++ b/webroot/rsrc/js/core/Notification.js
@@ -26,15 +26,35 @@
_visible : false,
_hideTimer : null,
_duration : 12000,
+ _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()) {
+ var n = new Notification(this._title, {
+ icon: this._icon,
+ body: this._body,
+ tag: this._key,
+ });
+ n.onclick = JX.bind(n, function () {
+ n.close();
+ JX.$U(this._href).go();
+ });
+ // Note: some OS / browsers do this automagically.
+ setTimeout(n.close.bind(n), this._duration);
+ }
return this;
},
@@ -59,6 +79,31 @@
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 +142,12 @@
},
statics : {
+ supportsDesktopNotifications : function () {
+ return 'Notification' in window;
+ },
+ desktopNotificationsEnabled : function () {
+ return window.Notification.permission === 'granted';
+ },
_container : null,
_listening : false,
_active : [],

File Metadata

Mime Type
text/plain
Expires
May 19 2024, 4:42 PM (5 w, 5 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6302480
Default Alt Text
D13219.id31956.diff (21 KB)

Event Timeline