diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 6ec1262989..199b903c34 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -8,7 +8,7 @@ return array( 'names' => array( 'core.pkg.css' => '61e69662', - 'core.pkg.js' => 'f1e8abd7', + 'core.pkg.js' => 'a590b451', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => 'fe951924', 'differential.pkg.js' => 'ebef29b1', @@ -327,8 +327,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' => '031cee25', - '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', @@ -428,7 +429,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', @@ -532,7 +533,7 @@ return array( 'javelin-aphlict' => '5359e785', 'javelin-behavior' => '61cbc29a', 'javelin-behavior-aphlict-dropdown' => '031cee25', - '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', @@ -554,6 +555,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', @@ -724,7 +726,7 @@ return array( 'phabricator-keyboard-shortcut-manager' => 'c1700f6f', 'phabricator-main-menu-view' => '3cd48671', 'phabricator-nav-view-css' => '0ecd30a1', - 'phabricator-notification' => '0c6946e7', + 'phabricator-notification' => 'ccf1cbf8', 'phabricator-notification-css' => '9c279160', 'phabricator-notification-menu-css' => 'f31c0bde', 'phabricator-object-selector-css' => '029a133d', @@ -903,13 +905,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', @@ -1650,20 +1645,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', @@ -1798,6 +1779,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', @@ -1942,6 +1930,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', @@ -2017,6 +2012,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/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/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/__phutil_library_map__.php b/src/__phutil_library_map__.php index 2c35d4a92a..f1a5d85bfa 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1792,6 +1792,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', @@ -2109,6 +2110,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', @@ -2136,7 +2139,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', @@ -2150,6 +2152,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', @@ -2550,6 +2553,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', @@ -5090,6 +5094,7 @@ phutil_register_library_map(array( 'PhabricatorCalendarEvent' => array( 'PhabricatorCalendarDAO', 'PhabricatorPolicyInterface', + 'PhabricatorProjectInterface', 'PhabricatorMarkupInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorSubscribableInterface', @@ -5390,6 +5395,7 @@ phutil_register_library_map(array( 'PhabricatorDatabaseSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorDateTimeSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorDebugController' => 'PhabricatorController', + 'PhabricatorDesktopNotificationsSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorDestructionEngine' => 'Phobject', 'PhabricatorDeveloperConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorDeveloperPreferencesSettingsPanel' => 'PhabricatorSettingsPanel', @@ -5745,9 +5751,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', @@ -5778,7 +5789,6 @@ phutil_register_library_map(array( 'PhabricatorNamedQueryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorNavigationRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorNeverTriggerClock' => 'PhabricatorTriggerClock', - 'PhabricatorNotificationAdHocFeedStory' => 'PhabricatorFeedStory', 'PhabricatorNotificationBuilder' => 'Phobject', 'PhabricatorNotificationClearController' => 'PhabricatorNotificationController', 'PhabricatorNotificationClient' => 'Phobject', @@ -5792,6 +5802,7 @@ phutil_register_library_map(array( 'PhabricatorNotificationStatusController' => 'PhabricatorNotificationController', 'PhabricatorNotificationStatusView' => 'AphrontTagView', 'PhabricatorNotificationTestController' => 'PhabricatorNotificationController', + 'PhabricatorNotificationTestFeedStory' => 'PhabricatorFeedStory', 'PhabricatorNotificationUIExample' => 'PhabricatorUIExample', 'PhabricatorNotificationsApplication' => 'PhabricatorApplication', 'PhabricatorNuanceApplication' => 'PhabricatorApplication', @@ -6288,6 +6299,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 9a6d58f3ee..b557acb4f3 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 = ''; @@ -353,11 +305,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'); @@ -365,15 +321,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); } @@ -414,13 +370,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)) @@ -465,21 +426,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'); @@ -499,20 +465,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/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..d232f17cb4 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'; @@ -12,6 +14,7 @@ final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO { const RETRY_DELAY = 5; + protected $actorPHID; protected $parameters; protected $status; protected $message; @@ -29,10 +32,12 @@ final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO { protected function getConfiguration() { return array( + self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'parameters' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( + 'actorPHID' => 'phid?', 'status' => 'text32', 'relatedPHID' => 'phid?', @@ -44,6 +49,9 @@ final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO { 'status' => array( 'columns' => array('status'), ), + 'key_actorPHID' => array( + 'columns' => array('actorPHID'), + ), 'relatedPHID' => array( 'columns' => array('relatedPHID'), ), @@ -54,6 +62,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; @@ -211,9 +224,14 @@ final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO { 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; @@ -993,4 +1011,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.'); + } + + } 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/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php index 46de711348..ddfddd3c5f 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/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 51fc2ad5a7..1527f2844d 100644 --- a/src/applications/settings/storage/PhabricatorUserPreferences.php +++ b/src/applications/settings/storage/PhabricatorUserPreferences.php @@ -39,6 +39,7 @@ final class PhabricatorUserPreferences extends PhabricatorUserDAO { const PREFERENCE_CONPHERENCE_COLUMN = 'conpherence-column'; const PREFERENCE_RESOURCE_POSTPROCESSOR = 'resource-postprocessor'; + const PREFERENCE_DESKTOP_NOTIFICATIONS = 'desktop-notifications'; // These are in an unusual order for historic reasons. const MAILTAG_PREFERENCE_NOTIFY = 0; 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 3e9e1ac4a3..3c6080aa34 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/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'); } 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 : [],