From 1bb2978a895a97ce67e406698c5a83d848d9351a Mon Sep 17 00:00:00 2001 From: Bob Trahan Date: Mon, 22 Jun 2015 13:11:37 -0700 Subject: [PATCH 1/4] 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 --- resources/celerity/map.php | 61 ++++--- src/__phutil_library_map__.php | 6 +- .../PhabricatorNotificationBuilder.php | 49 +++++- ...icatorNotificationIndividualController.php | 7 + .../PhabricatorNotificationTestController.php | 2 +- ... PhabricatorNotificationTestFeedStory.php} | 2 +- ...catorDesktopNotificationsSettingsPanel.php | 159 ++++++++++++++++++ .../storage/PhabricatorUserPreferences.php | 2 + ...ricatorApplicationTransactionFeedStory.php | 29 ++++ src/view/AphrontView.php | 1 + src/view/form/PHUIInfoView.php | 7 + src/view/phui/PHUIFeedStoryView.php | 4 + .../aphlict/behavior-aphlict-listen.js | 6 + .../behavior-desktop-notifications-control.js | 120 +++++++++++++ webroot/rsrc/js/core/Notification.js | 66 +++++++- 15 files changed, 489 insertions(+), 32 deletions(-) rename src/applications/notification/feed/{PhabricatorNotificationAdHocFeedStory.php => PhabricatorNotificationTestFeedStory.php} (85%) create mode 100644 src/applications/settings/panel/PhabricatorDesktopNotificationsSettingsPanel.php create mode 100644 webroot/rsrc/js/application/aphlict/behavior-desktop-notifications-control.js 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 : [], From c3efa261f9c1032981217f8ed79fa8434a7bdfeb Mon Sep 17 00:00:00 2001 From: lkassianik Date: Mon, 22 Jun 2015 13:27:37 -0700 Subject: [PATCH 2/4] Refactor Calendar Search, and implement Projects on events Summary: Ref T7950, Refactor Calendar Search, and implement Projects on events Test Plan: Verify that all queries in Calendar search still work, and that events can now have associated Projects that you can search by in Calendar Search. Reviewers: epriestley, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: epriestley, Korvin Maniphest Tasks: T7950 Differential Revision: https://secure.phabricator.com/D13393 --- src/__phutil_library_map__.php | 3 + ...PhabricatorCalendarEventEditController.php | 24 ++ .../query/PhabricatorCalendarEventQuery.php | 4 + .../PhabricatorCalendarEventSearchEngine.php | 368 ++++++++---------- .../storage/PhabricatorCalendarEvent.php | 1 + .../PhabricatorApplicationSearchEngine.php | 6 +- .../PhabricatorSearchDateControlField.php | 39 ++ .../phui/calendar/PHUICalendarDayView.php | 19 +- .../phui/calendar/PHUICalendarMonthView.php | 19 +- 9 files changed, 274 insertions(+), 209 deletions(-) create mode 100644 src/applications/search/field/PhabricatorSearchDateControlField.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 344f7f6e42..a8162414f3 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2549,6 +2549,7 @@ phutil_register_library_map(array( 'PhabricatorSearchDAO' => 'applications/search/storage/PhabricatorSearchDAO.php', 'PhabricatorSearchDatasource' => 'applications/search/typeahead/PhabricatorSearchDatasource.php', 'PhabricatorSearchDatasourceField' => 'applications/search/field/PhabricatorSearchDatasourceField.php', + 'PhabricatorSearchDateControlField' => 'applications/search/field/PhabricatorSearchDateControlField.php', 'PhabricatorSearchDateField' => 'applications/search/field/PhabricatorSearchDateField.php', 'PhabricatorSearchDeleteController' => 'applications/search/controller/PhabricatorSearchDeleteController.php', 'PhabricatorSearchDocument' => 'applications/search/storage/document/PhabricatorSearchDocument.php', @@ -5086,6 +5087,7 @@ phutil_register_library_map(array( 'PhabricatorCalendarEvent' => array( 'PhabricatorCalendarDAO', 'PhabricatorPolicyInterface', + 'PhabricatorProjectInterface', 'PhabricatorMarkupInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorSubscribableInterface', @@ -6286,6 +6288,7 @@ phutil_register_library_map(array( 'PhabricatorSearchDAO' => 'PhabricatorLiskDAO', 'PhabricatorSearchDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorSearchDatasourceField' => 'PhabricatorSearchTokenizerField', + 'PhabricatorSearchDateControlField' => 'PhabricatorSearchField', 'PhabricatorSearchDateField' => 'PhabricatorSearchField', 'PhabricatorSearchDeleteController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchDocument' => 'PhabricatorSearchDAO', diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php b/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php index 054f48a282..68e119f2bd 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php @@ -140,6 +140,15 @@ final class PhabricatorCalendarEventEditController $cancel_uri = '/'.$event->getMonogram(); } + if ($this->isCreate()) { + $projects = array(); + } else { + $projects = PhabricatorEdgeQuery::loadDestinationPHIDs( + $event->getPHID(), + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); + $projects = array_reverse($projects); + } + $name = $event->getName(); $description = $event->getDescription(); $is_all_day = $event->getIsAllDay(); @@ -167,6 +176,7 @@ final class PhabricatorCalendarEventEditController $request, 'recurrenceEndDate'); $recurrence_end_date_value->setOptional(true); + $projects = $request->getArr('projects'); $description = $request->getStr('description'); $subscribers = $request->getArr('subscribers'); $edit_policy = $request->getStr('editPolicy'); @@ -262,6 +272,12 @@ final class PhabricatorCalendarEventEditController ->setContinueOnNoEffect(true); try { + $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; + $xactions[] = id(new PhabricatorCalendarEventTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $proj_edge_type) + ->setNewValue(array('=' => array_fuse($projects))); + $xactions = $editor->applyTransactions($event, $xactions); $response = id(new AphrontRedirectResponse()); switch ($next_workflow) { @@ -437,6 +453,13 @@ final class PhabricatorCalendarEventEditController ->setValue($end_disabled); } + $projects = id(new AphrontFormTokenizerControl()) + ->setLabel(pht('Projects')) + ->setName('projects') + ->setValue($projects) + ->setUser($viewer) + ->setDatasource(new PhabricatorProjectDatasource()); + $description = id(new PhabricatorRemarkupControl()) ->setLabel(pht('Description')) ->setName('description') @@ -511,6 +534,7 @@ final class PhabricatorCalendarEventEditController ->appendControl($edit_policies) ->appendControl($subscribers) ->appendControl($invitees) + ->appendChild($projects) ->appendChild($description) ->appendChild($icon); diff --git a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php index 8b4e8837b1..06416e43a1 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php @@ -15,6 +15,10 @@ final class PhabricatorCalendarEventQuery private $generateGhosts = false; + public function newResultObject() { + return new PhabricatorCalendarEvent(); + } + public function setGenerateGhosts($generate_ghosts) { $this->generateGhosts = $generate_ghosts; return $this; diff --git a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php index cac6c39252..20a848f988 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php @@ -15,66 +15,137 @@ final class PhabricatorCalendarEventSearchEngine return 'PhabricatorCalendarApplication'; } - public function buildSavedQueryFromRequest(AphrontRequest $request) { - $saved = new PhabricatorSavedQuery(); - - $saved->setParameter( - 'rangeStart', - $this->readDateFromRequest($request, 'rangeStart')); - - $saved->setParameter( - 'rangeEnd', - $this->readDateFromRequest($request, 'rangeEnd')); - - $saved->setParameter( - 'upcoming', - $this->readBoolFromRequest($request, 'upcoming')); - - $saved->setParameter( - 'invitedPHIDs', - $this->readUsersFromRequest($request, 'invited')); - - $saved->setParameter( - 'creatorPHIDs', - $this->readUsersFromRequest($request, 'creators')); - - $saved->setParameter( - 'isCancelled', - $request->getStr('isCancelled')); - - $saved->setParameter( - 'display', - $request->getStr('display')); - - return $saved; + public function newQuery() { + return new PhabricatorCalendarEventQuery(); } - public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { - $query = id(new PhabricatorCalendarEventQuery()) - ->setGenerateGhosts(true); + protected function shouldShowOrderField() { + return false; + } + + protected function buildCustomSearchFields() { + return array( + id(new PhabricatorSearchDatasourceField()) + ->setLabel(pht('Created By')) + ->setKey('creatorPHIDs') + ->setDatasource(new PhabricatorPeopleUserFunctionDatasource()), + id(new PhabricatorSearchDatasourceField()) + ->setLabel(pht('Invited')) + ->setKey('invitedPHIDs') + ->setDatasource(new PhabricatorPeopleUserFunctionDatasource()), + id(new PhabricatorSearchDateControlField()) + ->setLabel(pht('Occurs After')) + ->setKey('rangeStart'), + id(new PhabricatorSearchDateControlField()) + ->setLabel(pht('Occurs Before')) + ->setKey('rangeEnd') + ->setAliases(array('rangeEnd')), + id(new PhabricatorSearchCheckboxesField()) + ->setKey('upcoming') + ->setOptions(array( + 'upcoming' => pht('Show only upcoming events.'), + )), + id(new PhabricatorSearchSelectField()) + ->setLabel(pht('Cancelled Events')) + ->setKey('isCancelled') + ->setOptions($this->getCancelledOptions()) + ->setDefault('active'), + id(new PhabricatorSearchSelectField()) + ->setLabel(pht('Display Options')) + ->setKey('display') + ->setOptions($this->getViewOptions()) + ->setDefault('month'), + ); + } + + private function getCancelledOptions() { + return array( + 'active' => pht('Active Events Only'), + 'cancelled' => pht('Cancelled Events Only'), + 'both' => pht('Both Cancelled and Active Events'), + ); + } + + private function getViewOptions() { + return array( + 'month' => pht('Month View'), + 'day' => pht('Day View'), + 'list' => pht('List View'), + ); + } + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + $viewer = $this->requireViewer(); + + if ($map['creatorPHIDs']) { + $query->withCreatorPHIDs($map['creatorPHIDs']); + } + + if ($map['invitedPHIDs']) { + $query->withInvitedPHIDs($map['invitedPHIDs']); + } + + $range_start = $map['rangeStart']; + $range_end = $map['rangeEnd']; + $display = $map['display']; + + if ($map['upcoming'] && $map['upcoming'][0] == 'upcoming') { + $upcoming = true; + } else { + $upcoming = false; + } + + list($range_start, $range_end) = $this->getQueryDateRange( + $range_start, + $range_end, + $display, + $upcoming); + + $query->withDateRange($range_start, $range_end); + + switch ($map['isCancelled']) { + case 'active': + $query->withIsCancelled(false); + break; + case 'cancelled': + $query->withIsCancelled(true); + break; + } + + return $query->setGenerateGhosts(true); + } + + private function getQueryDateRange( + $start_date_wild, + $end_date_wild, + $display, + $upcoming) { + + $start_date_value = $this->getSafeDate($start_date_wild); + $end_date_value = $this->getSafeDate($end_date_wild); + $viewer = $this->requireViewer(); $timezone = new DateTimeZone($viewer->getTimezoneIdentifier()); + $min_range = null; + $max_range = null; - $min_range = $this->getDateFrom($saved)->getEpoch(); - $max_range = $this->getDateTo($saved)->getEpoch(); + $min_range = $start_date_value->getEpoch(); + $max_range = $end_date_value->getEpoch(); - $user_datasource = id(new PhabricatorPeopleUserFunctionDatasource()) - ->setViewer($viewer); - - if ($this->isMonthView($saved) || - $this->isDayView($saved)) { + if ($display == 'month' || $display == 'day') { list($start_year, $start_month, $start_day) = - $this->getDisplayYearAndMonthAndDay($saved); + $this->getDisplayYearAndMonthAndDay($min_range, $max_range, $display); $start_day = new DateTime( "{$start_year}-{$start_month}-{$start_day}", $timezone); $next = clone $start_day; - if ($this->isMonthView($saved)) { + if ($display == 'month') { $next->modify('+1 month'); - } else if ($this->isDayView($saved)) { - $next->modify('+6 day'); + } else if ($display == 'day') { + $next->modify('+7 day'); } $display_start = $start_day->format('U'); @@ -92,7 +163,7 @@ final class PhabricatorCalendarEventSearchEngine if (!$min_range || ($min_range < $display_start)) { $min_range = $display_start; - if ($this->isMonthView($saved) && + if ($display == 'month' && $first_of_month !== $start_of_week) { $interim_day_num = ($first_of_month + 7 - $start_of_week) % 7; $min_range = id(clone $start_day) @@ -103,18 +174,17 @@ final class PhabricatorCalendarEventSearchEngine if (!$max_range || ($max_range > $display_end)) { $max_range = $display_end; - if ($this->isMonthView($saved) && + if ($display == 'month' && $last_of_month !== $end_of_week) { $interim_day_num = ($end_of_week + 7 - $last_of_month) % 7; $max_range = id(clone $next) ->modify('+'.$interim_day_num.' days') ->format('U'); } - } } - if ($saved->getParameter('upcoming')) { + if ($upcoming) { if ($min_range) { $min_range = max(time(), $min_range); } else { @@ -122,128 +192,7 @@ final class PhabricatorCalendarEventSearchEngine } } - if ($min_range || $max_range) { - $query->withDateRange($min_range, $max_range); - } - - $invited_phids = $saved->getParameter('invitedPHIDs', array()); - $invited_phids = $user_datasource->evaluateTokens($invited_phids); - if ($invited_phids) { - $query->withInvitedPHIDs($invited_phids); - } - - $creator_phids = $saved->getParameter('creatorPHIDs', array()); - $creator_phids = $user_datasource->evaluateTokens($creator_phids); - if ($creator_phids) { - $query->withCreatorPHIDs($creator_phids); - } - - $is_cancelled = $saved->getParameter('isCancelled', 'active'); - - switch ($is_cancelled) { - case 'active': - $query->withIsCancelled(false); - break; - case 'cancelled': - $query->withIsCancelled(true); - break; - } - - return $query; - } - - public function buildSearchForm( - AphrontFormView $form, - PhabricatorSavedQuery $saved) { - - $range_start = $this->getDateFrom($saved); - $e_start = null; - - $range_end = $this->getDateTo($saved); - $e_end = null; - - if (!$range_start->isValid()) { - $this->addError(pht('Start date is not valid.')); - $e_start = pht('Invalid'); - } - - if (!$range_end->isValid()) { - $this->addError(pht('End date is not valid.')); - $e_end = pht('Invalid'); - } - - $start_epoch = $range_start->getEpoch(); - $end_epoch = $range_end->getEpoch(); - - if ($start_epoch && $end_epoch && ($start_epoch > $end_epoch)) { - $this->addError(pht('End date must be after start date.')); - $e_start = pht('Invalid'); - $e_end = pht('Invalid'); - } - - $upcoming = $saved->getParameter('upcoming'); - $is_cancelled = $saved->getParameter('isCancelled', 'active'); - $display = $saved->getParameter('display', 'month'); - - $invited_phids = $saved->getParameter('invitedPHIDs', array()); - $creator_phids = $saved->getParameter('creatorPHIDs', array()); - $resolution_types = array( - 'active' => pht('Active Events Only'), - 'cancelled' => pht('Cancelled Events Only'), - 'both' => pht('Both Cancelled and Active Events'), - ); - $display_options = array( - 'month' => pht('Month View'), - 'day' => pht('Day View (beta)'), - 'list' => pht('List View'), - ); - - $form - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setDatasource(new PhabricatorPeopleUserFunctionDatasource()) - ->setName('creators') - ->setLabel(pht('Created By')) - ->setValue($creator_phids)) - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setDatasource(new PhabricatorPeopleUserFunctionDatasource()) - ->setName('invited') - ->setLabel(pht('Invited')) - ->setValue($invited_phids)) - ->appendChild( - id(new AphrontFormDateControl()) - ->setLabel(pht('Occurs After')) - ->setUser($this->requireViewer()) - ->setName('rangeStart') - ->setError($e_start) - ->setValue($range_start)) - ->appendChild( - id(new AphrontFormDateControl()) - ->setLabel(pht('Occurs Before')) - ->setUser($this->requireViewer()) - ->setName('rangeEnd') - ->setError($e_end) - ->setValue($range_end)) - ->appendChild( - id(new AphrontFormCheckboxControl()) - ->addCheckbox( - 'upcoming', - 1, - pht('Show only upcoming events.'), - $upcoming)) - ->appendChild( - id(new AphrontFormSelectControl()) - ->setLabel(pht('Cancelled Events')) - ->setName('isCancelled') - ->setValue($is_cancelled) - ->setOptions($resolution_types)) - ->appendChild( - id(new AphrontFormSelectControl()) - ->setLabel(pht('Display Options')) - ->setName('display') - ->setValue($display) - ->setOptions($display_options)); + return array($min_range, $max_range); } protected function getURI($path) { @@ -279,7 +228,9 @@ final class PhabricatorCalendarEventSearchEngine case 'day': return $query->setParameter('display', 'day'); case 'upcoming': - return $query->setParameter('upcoming', true); + return $query->setParameter('upcoming', array( + 0 => 'upcoming', + )); case 'all': return $query; } @@ -311,6 +262,7 @@ final class PhabricatorCalendarEventSearchEngine assert_instances_of($events, 'PhabricatorCalendarEvent'); $viewer = $this->requireViewer(); $list = new PHUIObjectItemListView(); + foreach ($events as $event) { $from = phabricator_datetime($event->getDateFrom(), $viewer); $duration = ''; @@ -349,11 +301,15 @@ final class PhabricatorCalendarEventSearchEngine array $statuses, PhabricatorSavedQuery $query, array $handles) { + $viewer = $this->requireViewer(); $now = time(); list($start_year, $start_month) = - $this->getDisplayYearAndMonthAndDay($query); + $this->getDisplayYearAndMonthAndDay( + $this->getQueryDateFrom($query)->getEpoch(), + $this->getQueryDateTo($query)->getEpoch(), + $query->getParameter('display')); $now_year = phabricator_format_local_time($now, $viewer, 'Y'); $now_month = phabricator_format_local_time($now, $viewer, 'm'); @@ -361,15 +317,15 @@ final class PhabricatorCalendarEventSearchEngine if ($start_month == $now_month && $start_year == $now_year) { $month_view = new PHUICalendarMonthView( - $this->getDateFrom($query), - $this->getDateTo($query), + $this->getQueryDateFrom($query), + $this->getQueryDateTo($query), $start_month, $start_year, $now_day); } else { $month_view = new PHUICalendarMonthView( - $this->getDateFrom($query), - $this->getDateTo($query), + $this->getQueryDateFrom($query), + $this->getQueryDateTo($query), $start_month, $start_year); } @@ -406,13 +362,18 @@ final class PhabricatorCalendarEventSearchEngine array $statuses, PhabricatorSavedQuery $query, array $handles) { + $viewer = $this->requireViewer(); + list($start_year, $start_month, $start_day) = - $this->getDisplayYearAndMonthAndDay($query); + $this->getDisplayYearAndMonthAndDay( + $this->getQueryDateFrom($query)->getEpoch(), + $this->getQueryDateTo($query)->getEpoch(), + $query->getParameter('display')); $day_view = id(new PHUICalendarDayView( - $this->getDateFrom($query), - $this->getDateTo($query), + $this->getQueryDateFrom($query)->getEpoch(), + $this->getQueryDateTo($query)->getEpoch(), $start_year, $start_month, $start_day)) @@ -454,21 +415,26 @@ final class PhabricatorCalendarEventSearchEngine } private function getDisplayYearAndMonthAndDay( - PhabricatorSavedQuery $query) { + $range_start, + $range_end, + $display) { + $viewer = $this->requireViewer(); + $epoch = null; + if ($this->calendarYear && $this->calendarMonth) { $start_year = $this->calendarYear; $start_month = $this->calendarMonth; $start_day = $this->calendarDay ? $this->calendarDay : 1; } else { - $epoch = $this->getDateFrom($query)->getEpoch(); - if (!$epoch) { - $epoch = $this->getDateTo($query)->getEpoch(); - if (!$epoch) { - $epoch = time(); - } + if ($range_start) { + $epoch = $range_start; + } else if ($range_end) { + $epoch = $range_end; + } else { + $epoch = time(); } - if ($this->isMonthView($query)) { + if ($display == 'month') { $day = 1; } else { $day = phabricator_format_local_time($epoch, $viewer, 'd'); @@ -488,20 +454,30 @@ final class PhabricatorCalendarEventSearchEngine } } - private function getDateFrom(PhabricatorSavedQuery $saved) { - return $this->getDate($saved, 'rangeStart'); + private function getQueryDateFrom(PhabricatorSavedQuery $saved) { + return $this->getQueryDate($saved, 'rangeStart'); } - private function getDateTo(PhabricatorSavedQuery $saved) { - return $this->getDate($saved, 'rangeEnd'); + private function getQueryDateTo(PhabricatorSavedQuery $saved) { + return $this->getQueryDate($saved, 'rangeEnd'); } - private function getDate(PhabricatorSavedQuery $saved, $key) { + private function getQueryDate(PhabricatorSavedQuery $saved, $key) { $viewer = $this->requireViewer(); $wild = $saved->getParameter($key); - if ($wild) { - $value = AphrontFormDateControlValue::newFromWild($viewer, $wild); + return $this->getSafeDate($wild); + } + + private function getSafeDate($value) { + $viewer = $this->requireViewer(); + if ($value) { + // ideally this would be consistent and always pass in the same type + if ($value instanceof AphrontFormDateControlValue) { + return $value; + } else { + $value = AphrontFormDateControlValue::newFromWild($viewer, $value); + } } else { $value = AphrontFormDateControlValue::newFromEpoch( $viewer, diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index 87c792a468..aa5076754c 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -2,6 +2,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO implements PhabricatorPolicyInterface, + PhabricatorProjectInterface, PhabricatorMarkupInterface, PhabricatorApplicationTransactionInterface, PhabricatorSubscribableInterface, diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php index 453b965034..9b0515e7c8 100644 --- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php +++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php @@ -266,7 +266,7 @@ abstract class PhabricatorApplicationSearchEngine extends Phobject { } $query = $this->newQuery(); - if ($query) { + if ($query && $this->shouldShowOrderField()) { $orders = $query->getBuiltinOrders(); $orders = ipull($orders, 'name'); @@ -293,6 +293,10 @@ abstract class PhabricatorApplicationSearchEngine extends Phobject { return $field_map; } + protected function shouldShowOrderField() { + return true; + } + private function adjustFieldsForDisplay(array $field_map) { $order = $this->getDefaultFieldOrder(); diff --git a/src/applications/search/field/PhabricatorSearchDateControlField.php b/src/applications/search/field/PhabricatorSearchDateControlField.php new file mode 100644 index 0000000000..9ce5c0227d --- /dev/null +++ b/src/applications/search/field/PhabricatorSearchDateControlField.php @@ -0,0 +1,39 @@ +getExists($key.'_d'); + } + + protected function getValueFromRequest(AphrontRequest $request, $key) { + $value = AphrontFormDateControlValue::newFromRequest($request, $key); + $value->setOptional(true); + return $value->getDictionary(); + } + + protected function newControl() { + return id(new AphrontFormDateControl()) + ->setAllowNull(true); + } + + protected function didReadValueFromSavedQuery($value) { + if (!$value) { + return null; + } + + if ($value instanceof AphrontFormDateControlValue && $value->getEpoch()) { + return $value->setOptional(true); + } + + $value = AphrontFormDateControlValue::newFromWild( + $this->getViewer(), + $value); + return $value->setOptional(true); + } + +} diff --git a/src/view/phui/calendar/PHUICalendarDayView.php b/src/view/phui/calendar/PHUICalendarDayView.php index 844cd31222..dd8e5f3fb2 100644 --- a/src/view/phui/calendar/PHUICalendarDayView.php +++ b/src/view/phui/calendar/PHUICalendarDayView.php @@ -208,8 +208,15 @@ final class PHUICalendarDayView extends AphrontView { private function getQueryRangeWarning() { $errors = array(); - $range_start_epoch = $this->rangeStart->getEpoch(); - $range_end_epoch = $this->rangeEnd->getEpoch(); + $range_start_epoch = null; + $range_end_epoch = null; + + if ($this->rangeStart) { + $range_start_epoch = $this->rangeStart->getEpoch(); + } + if ($this->rangeEnd) { + $range_end_epoch = $this->rangeEnd->getEpoch(); + } $day_start = $this->getDateTime(); $day_end = id(clone $day_start)->modify('+1 day'); @@ -226,10 +233,10 @@ final class PHUICalendarDayView extends AphrontView { $errors[] = pht('Part of the day is out of range'); } - if (($this->rangeEnd->getEpoch() != null && - $this->rangeEnd->getEpoch() < $day_start) || - ($this->rangeStart->getEpoch() != null && - $this->rangeStart->getEpoch() > $day_end)) { + if (($range_end_epoch != null && + $range_end_epoch < $day_start) || + ($range_start_epoch != null && + $range_start_epoch > $day_end)) { $errors[] = pht('Day is out of query range'); } return $errors; diff --git a/src/view/phui/calendar/PHUICalendarMonthView.php b/src/view/phui/calendar/PHUICalendarMonthView.php index ed0558fdce..766b47fa89 100644 --- a/src/view/phui/calendar/PHUICalendarMonthView.php +++ b/src/view/phui/calendar/PHUICalendarMonthView.php @@ -434,8 +434,15 @@ final class PHUICalendarMonthView extends AphrontView { private function getQueryRangeWarning() { $errors = array(); - $range_start_epoch = $this->rangeStart->getEpoch(); - $range_end_epoch = $this->rangeEnd->getEpoch(); + $range_start_epoch = null; + $range_end_epoch = null; + + if ($this->rangeStart) { + $range_start_epoch = $this->rangeStart->getEpoch(); + } + if ($this->rangeEnd) { + $range_end_epoch = $this->rangeEnd->getEpoch(); + } $month_start = $this->getDateTime(); $month_end = id(clone $month_start)->modify('+1 month'); @@ -452,10 +459,10 @@ final class PHUICalendarMonthView extends AphrontView { $errors[] = pht('Part of the month is out of range'); } - if (($this->rangeEnd->getEpoch() != null && - $this->rangeEnd->getEpoch() < $month_start) || - ($this->rangeStart->getEpoch() != null && - $this->rangeStart->getEpoch() > $month_end)) { + if (($range_end_epoch != null && + $range_end_epoch < $month_start) || + ($range_start_epoch != null && + $range_start_epoch > $month_end)) { $errors[] = pht('Month is out of query range'); } From ea5462fb600be7aa7d4fe16a3021c82526caabc9 Mon Sep 17 00:00:00 2001 From: Bob Trahan Date: Mon, 22 Jun 2015 13:46:26 -0700 Subject: [PATCH 3/4] MetaMTA - lay some ground work for having an application Summary: Ref T5791. This does a few bits there. Namely: - Adds PHID column to PhabricatorMetaMTAMail - Implements a PhabricatorMetaMTAMailPHIDType - Script to backpopulate them. - Makes PhabricatorMetaMTAMail implement PolicyInterface. - View policy is NOONE and the author and recipients have automatic view capabilities - No edit capability. - Adds a PhabricatorMetaMTAMailQuery for PhabricatorMetaMTAMail. Test Plan: ran `./bin/storage upgrade` successfully. commented on a maniphest task and verifed the metamta mail object in the database was created successfully with a shiny new phid Reviewers: epriestley Reviewed By: epriestley Subscribers: epriestley, Korvin Maniphest Tasks: T5791 Differential Revision: https://secure.phabricator.com/D13394 --- .../20150622.metamta.1.phid-col.sql | 2 + .../20150622.metamta.2.phid-mig.php | 22 +++++++ .../20150622.metamta.3.phid-key.sql | 2 + src/__phutil_library_map__.php | 9 ++- .../phid/PhabricatorMetaMTAMailPHIDType.php | 43 ++++++++++++++ .../query/PhabricatorMetaMTAMailQuery.php | 57 +++++++++++++++++++ .../storage/PhabricatorMetaMTAMail.php | 35 +++++++++++- 7 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 resources/sql/autopatches/20150622.metamta.1.phid-col.sql create mode 100644 resources/sql/autopatches/20150622.metamta.2.phid-mig.php create mode 100644 resources/sql/autopatches/20150622.metamta.3.phid-key.sql create mode 100644 src/applications/metamta/phid/PhabricatorMetaMTAMailPHIDType.php create mode 100644 src/applications/metamta/query/PhabricatorMetaMTAMailQuery.php diff --git a/resources/sql/autopatches/20150622.metamta.1.phid-col.sql b/resources/sql/autopatches/20150622.metamta.1.phid-col.sql new file mode 100644 index 0000000000..9bdd1a005e --- /dev/null +++ b/resources/sql/autopatches/20150622.metamta.1.phid-col.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_metamta.metamta_mail + ADD phid VARBINARY(64) NOT NULL AFTER id; diff --git a/resources/sql/autopatches/20150622.metamta.2.phid-mig.php b/resources/sql/autopatches/20150622.metamta.2.phid-mig.php new file mode 100644 index 0000000000..35932a701b --- /dev/null +++ b/resources/sql/autopatches/20150622.metamta.2.phid-mig.php @@ -0,0 +1,22 @@ +establishConnection('w'); + +echo pht('Assigning PHIDs to mails...')."\n"; +foreach (new LiskMigrationIterator($table) as $mail) { + $id = $mail->getID(); + + echo pht('Updating mail %d...', $id)."\n"; + if ($mail->getPHID()) { + continue; + } + + queryfx( + $conn_w, + 'UPDATE %T SET phid = %s WHERE id = %d', + $table->getTableName(), + $table->generatePHID(), + $id); +} +echo pht('Done.')."\n"; diff --git a/resources/sql/autopatches/20150622.metamta.3.phid-key.sql b/resources/sql/autopatches/20150622.metamta.3.phid-key.sql new file mode 100644 index 0000000000..dae8d7e604 --- /dev/null +++ b/resources/sql/autopatches/20150622.metamta.3.phid-key.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_metamta.metamta_mail + ADD UNIQUE KEY `key_phid` (phid); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index a8162414f3..9c4acc6ec3 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2107,6 +2107,8 @@ phutil_register_library_map(array( 'PhabricatorMetaMTAMail' => 'applications/metamta/storage/PhabricatorMetaMTAMail.php', 'PhabricatorMetaMTAMailBody' => 'applications/metamta/view/PhabricatorMetaMTAMailBody.php', 'PhabricatorMetaMTAMailBodyTestCase' => 'applications/metamta/view/__tests__/PhabricatorMetaMTAMailBodyTestCase.php', + 'PhabricatorMetaMTAMailPHIDType' => 'applications/metamta/phid/PhabricatorMetaMTAMailPHIDType.php', + 'PhabricatorMetaMTAMailQuery' => 'applications/metamta/query/PhabricatorMetaMTAMailQuery.php', 'PhabricatorMetaMTAMailSection' => 'applications/metamta/view/PhabricatorMetaMTAMailSection.php', 'PhabricatorMetaMTAMailTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php', 'PhabricatorMetaMTAMailableDatasource' => 'applications/metamta/typeahead/PhabricatorMetaMTAMailableDatasource.php', @@ -5744,9 +5746,14 @@ phutil_register_library_map(array( 'PhabricatorMetaMTAEmailBodyParser' => 'Phobject', 'PhabricatorMetaMTAEmailBodyParserTestCase' => 'PhabricatorTestCase', 'PhabricatorMetaMTAErrorMailAction' => 'PhabricatorSystemAction', - 'PhabricatorMetaMTAMail' => 'PhabricatorMetaMTADAO', + 'PhabricatorMetaMTAMail' => array( + 'PhabricatorMetaMTADAO', + 'PhabricatorPolicyInterface', + ), 'PhabricatorMetaMTAMailBody' => 'Phobject', 'PhabricatorMetaMTAMailBodyTestCase' => 'PhabricatorTestCase', + 'PhabricatorMetaMTAMailPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorMetaMTAMailQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorMetaMTAMailSection' => 'Phobject', 'PhabricatorMetaMTAMailTestCase' => 'PhabricatorTestCase', 'PhabricatorMetaMTAMailableDatasource' => 'PhabricatorTypeaheadCompositeDatasource', diff --git a/src/applications/metamta/phid/PhabricatorMetaMTAMailPHIDType.php b/src/applications/metamta/phid/PhabricatorMetaMTAMailPHIDType.php new file mode 100644 index 0000000000..8159045dbd --- /dev/null +++ b/src/applications/metamta/phid/PhabricatorMetaMTAMailPHIDType.php @@ -0,0 +1,43 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $mail = $objects[$phid]; + + $id = $mail->getID(); + $name = pht('Mail %d', $id); + + $handle + ->setName($name) + ->setFullName($name); + } + } +} diff --git a/src/applications/metamta/query/PhabricatorMetaMTAMailQuery.php b/src/applications/metamta/query/PhabricatorMetaMTAMailQuery.php new file mode 100644 index 0000000000..d5fd3342ef --- /dev/null +++ b/src/applications/metamta/query/PhabricatorMetaMTAMailQuery.php @@ -0,0 +1,57 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + $where = array(); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn_r, + 'mail.id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn_r, + 'mail.phid IN (%Ls)', + $this->phids); + } + + $where[] = $this->buildPagingClause($conn_r); + + return $this->formatWhereClause($where); + } + + protected function getPrimaryTableAlias() { + return 'mail'; + } + + public function newResultObject() { + return new PhabricatorMetaMTAMail(); + } + + public function getQueryApplicationClass() { + return 'PhabricatorMetaMTAApplication'; + } + +} diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php index af7007cd87..61f376381a 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php @@ -3,7 +3,9 @@ /** * @task recipients Managing Recipients */ -final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO { +final class PhabricatorMetaMTAMail + extends PhabricatorMetaMTADAO + implements PhabricatorPolicyInterface { const STATUS_QUEUE = 'queued'; const STATUS_SENT = 'sent'; @@ -29,6 +31,7 @@ final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO { protected function getConfiguration() { return array( + self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'parameters' => self::SERIALIZATION_JSON, ), @@ -54,6 +57,11 @@ final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO { ) + parent::getConfiguration(); } + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhabricatorMetaMTAMailPHIDType::TYPECONST); + } + protected function setParam($param, $value) { $this->parameters[$param] = $value; return $this; @@ -993,4 +1001,29 @@ final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO { } +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + return PhabricatorPolicies::POLICY_NOONE; + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + $actor_phids = $this->getAllActorPHIDs(); + $actor_phids = $this->expandRecipients($actor_phids); + return in_array($viewer->getPHID(), $actor_phids); + } + + public function describeAutomaticCapability($capability) { + return pht( + 'The mail sender and message recipients can always see the mail.'); + } + + } From 4be568d34611ced7766605a57b568cfe82958170 Mon Sep 17 00:00:00 2001 From: Bob Trahan Date: Mon, 22 Jun 2015 14:14:21 -0700 Subject: [PATCH 4/4] MetaMTA - save actorPHID as its own column Summary: Ref T5791. This should make performance snappy wrt policy checks in some future diff where the Query is updated and in use somewhere in the application. Test Plan: ran `./bin/storage upgrade`. commented on a task and saw actorPHID populated correctly in underlying MetaMTAMail object database entry Reviewers: epriestley Reviewed By: epriestley Subscribers: epriestley, Korvin Maniphest Tasks: T5791 Differential Revision: https://secure.phabricator.com/D13396 --- .../20150622.metamta.4.actor-phid-col.sql | 2 ++ .../20150622.metamta.5.actor-phid-mig.php | 27 +++++++++++++++++++ .../20150622.metamta.6.actor-phid-key.sql | 2 ++ .../storage/PhabricatorMetaMTAMail.php | 10 +++++++ 4 files changed, 41 insertions(+) create mode 100644 resources/sql/autopatches/20150622.metamta.4.actor-phid-col.sql create mode 100644 resources/sql/autopatches/20150622.metamta.5.actor-phid-mig.php create mode 100644 resources/sql/autopatches/20150622.metamta.6.actor-phid-key.sql diff --git a/resources/sql/autopatches/20150622.metamta.4.actor-phid-col.sql b/resources/sql/autopatches/20150622.metamta.4.actor-phid-col.sql new file mode 100644 index 0000000000..cc0bcb221a --- /dev/null +++ b/resources/sql/autopatches/20150622.metamta.4.actor-phid-col.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_metamta.metamta_mail + ADD actorPHID VARBINARY(64) AFTER phid; diff --git a/resources/sql/autopatches/20150622.metamta.5.actor-phid-mig.php b/resources/sql/autopatches/20150622.metamta.5.actor-phid-mig.php new file mode 100644 index 0000000000..d27e54098f --- /dev/null +++ b/resources/sql/autopatches/20150622.metamta.5.actor-phid-mig.php @@ -0,0 +1,27 @@ +establishConnection('w'); + +echo pht('Assigning actorPHIDs to mails...')."\n"; +foreach (new LiskMigrationIterator($table) as $mail) { + $id = $mail->getID(); + + echo pht('Updating mail %d...', $id)."\n"; + if ($mail->getActorPHID()) { + continue; + } + + $actor_phid = $mail->getFrom(); + if ($actor_phid === null) { + continue; + } + + queryfx( + $conn_w, + 'UPDATE %T SET actorPHID = %s WHERE id = %d', + $table->getTableName(), + $actor_phid, + $id); +} +echo pht('Done.')."\n"; diff --git a/resources/sql/autopatches/20150622.metamta.6.actor-phid-key.sql b/resources/sql/autopatches/20150622.metamta.6.actor-phid-key.sql new file mode 100644 index 0000000000..7b0bb0e867 --- /dev/null +++ b/resources/sql/autopatches/20150622.metamta.6.actor-phid-key.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_metamta.metamta_mail + ADD KEY `key_actorPHID` (actorPHID); diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php index 61f376381a..d232f17cb4 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php @@ -14,6 +14,7 @@ final class PhabricatorMetaMTAMail const RETRY_DELAY = 5; + protected $actorPHID; protected $parameters; protected $status; protected $message; @@ -36,6 +37,7 @@ final class PhabricatorMetaMTAMail 'parameters' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( + 'actorPHID' => 'phid?', 'status' => 'text32', 'relatedPHID' => 'phid?', @@ -47,6 +49,9 @@ final class PhabricatorMetaMTAMail 'status' => array( 'columns' => array('status'), ), + 'key_actorPHID' => array( + 'columns' => array('actorPHID'), + ), 'relatedPHID' => array( 'columns' => array('relatedPHID'), ), @@ -219,9 +224,14 @@ final class PhabricatorMetaMTAMail public function setFrom($from) { $this->setParam('from', $from); + $this->setActorPHID($from); return $this; } + public function getFrom() { + return $this->getParam('from'); + } + public function setRawFrom($raw_email, $raw_name) { $this->setParam('raw-from', array($raw_email, $raw_name)); return $this;