diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 62cdf2f1e3..5dd506784a 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -10,7 +10,7 @@ return array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', 'core.pkg.css' => 'b797945d', - 'core.pkg.js' => 'eaca003c', + 'core.pkg.js' => 'eb53fc5b', 'differential.pkg.css' => '8d8360fb', 'differential.pkg.js' => '67e02996', 'diffusion.pkg.css' => '42c75c37', @@ -249,7 +249,7 @@ return array( 'rsrc/externals/javelin/lib/Routable.js' => '6a18c42e', 'rsrc/externals/javelin/lib/Router.js' => '32755edb', 'rsrc/externals/javelin/lib/Scrollbar.js' => 'a43ae2ae', - 'rsrc/externals/javelin/lib/Sound.js' => 'e562708c', + 'rsrc/externals/javelin/lib/Sound.js' => 'd4cc2d2a', 'rsrc/externals/javelin/lib/URI.js' => '2e255291', 'rsrc/externals/javelin/lib/Vector.js' => 'e9c80beb', 'rsrc/externals/javelin/lib/WebSocket.js' => 'fdc13e4e', @@ -409,7 +409,7 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => '65afb173', + 'rsrc/js/application/projects/WorkboardBoard.js' => '3ba8e6ad', 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4', 'rsrc/js/application/projects/WorkboardColumn.js' => 'c3d24e63', @@ -418,7 +418,7 @@ return array( 'rsrc/js/application/projects/WorkboardHeader.js' => '111bfd2d', 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => 'ebe83a6b', 'rsrc/js/application/projects/WorkboardOrderTemplate.js' => '03e8891f', - 'rsrc/js/application/projects/behavior-project-boards.js' => '8512e4ea', + 'rsrc/js/application/projects/behavior-project-boards.js' => 'aad45445', 'rsrc/js/application/projects/behavior-project-create.js' => '34c53422', 'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9', 'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68', @@ -664,7 +664,7 @@ return array( 'javelin-behavior-phuix-example' => 'c2c500a7', 'javelin-behavior-policy-control' => '0eaa33a9', 'javelin-behavior-policy-rule-editor' => '9347f172', - 'javelin-behavior-project-boards' => '8512e4ea', + 'javelin-behavior-project-boards' => 'aad45445', 'javelin-behavior-project-create' => '34c53422', 'javelin-behavior-quicksand-blacklist' => '5a6f6a06', 'javelin-behavior-read-only-warning' => 'b9109f8f', @@ -718,7 +718,7 @@ return array( 'javelin-routable' => '6a18c42e', 'javelin-router' => '32755edb', 'javelin-scrollbar' => 'a43ae2ae', - 'javelin-sound' => 'e562708c', + 'javelin-sound' => 'd4cc2d2a', 'javelin-stratcom' => '0889b835', 'javelin-tokenizer' => '89a1ae3a', 'javelin-typeahead' => 'a4356cde', @@ -737,7 +737,7 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => '65afb173', + 'javelin-workboard-board' => '3ba8e6ad', 'javelin-workboard-card' => '0392a5d8', 'javelin-workboard-card-template' => '2a61f8d4', 'javelin-workboard-column' => 'c3d24e63', @@ -1227,6 +1227,18 @@ return array( 'javelin-behavior', 'phabricator-prefab', ), + '3ba8e6ad' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + 'javelin-workboard-card-template', + 'javelin-workboard-order-template', + ), '3dc5ad43' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1456,18 +1468,6 @@ return array( '60cd9241' => array( 'javelin-behavior', ), - '65afb173' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - 'javelin-workboard-header-template', - 'javelin-workboard-card-template', - 'javelin-workboard-order-template', - ), '65bb0011' => array( 'javelin-behavior', 'javelin-dom', @@ -1594,16 +1594,6 @@ return array( 'javelin-dom', 'javelin-vector', ), - '8512e4ea' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'javelin-workboard-controller', - 'javelin-workboard-drop-effect', - ), '87428eb2' => array( 'javelin-behavior', 'javelin-diffusion-locate-file-source', @@ -1848,6 +1838,16 @@ return array( 'javelin-dom', 'javelin-util', ), + 'aad45445' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-workboard-controller', + 'javelin-workboard-drop-effect', + ), 'ab85e184' => array( 'javelin-install', 'javelin-dom', @@ -2041,6 +2041,9 @@ return array( 'd3799cb4' => array( 'javelin-install', ), + 'd4cc2d2a' => array( + 'javelin-install', + ), 'd8a86cfb' => array( 'javelin-behavior', 'javelin-dom', @@ -2075,9 +2078,6 @@ return array( 'javelin-dom', 'javelin-history', ), - 'e562708c' => array( - 'javelin-install', - ), 'e5bdb730' => array( 'javelin-behavior', 'javelin-stratcom', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index a4edf3d246..f56c616545 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4183,6 +4183,7 @@ phutil_register_library_map(array( 'PhabricatorProjectTriggerManiphestStatusRule' => 'applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php', 'PhabricatorProjectTriggerNameTransaction' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerNameTransaction.php', 'PhabricatorProjectTriggerPHIDType' => 'applications/project/phid/PhabricatorProjectTriggerPHIDType.php', + 'PhabricatorProjectTriggerPlaySoundRule' => 'applications/project/trigger/PhabricatorProjectTriggerPlaySoundRule.php', 'PhabricatorProjectTriggerQuery' => 'applications/project/query/PhabricatorProjectTriggerQuery.php', 'PhabricatorProjectTriggerRule' => 'applications/project/trigger/PhabricatorProjectTriggerRule.php', 'PhabricatorProjectTriggerRuleRecord' => 'applications/project/trigger/PhabricatorProjectTriggerRuleRecord.php', @@ -10317,6 +10318,7 @@ phutil_register_library_map(array( 'PhabricatorProjectTriggerManiphestStatusRule' => 'PhabricatorProjectTriggerRule', 'PhabricatorProjectTriggerNameTransaction' => 'PhabricatorProjectTriggerTransactionType', 'PhabricatorProjectTriggerPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorProjectTriggerPlaySoundRule' => 'PhabricatorProjectTriggerRule', 'PhabricatorProjectTriggerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorProjectTriggerRule' => 'Phobject', 'PhabricatorProjectTriggerRuleRecord' => 'Phobject', diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 4e4ff81b4e..e8a47d362a 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -542,6 +542,7 @@ final class PhabricatorProjectBoardViewController $templates = array(); $all_tasks = array(); $column_templates = array(); + $sounds = array(); foreach ($visible_columns as $column_phid => $column) { $column_tasks = $column_phids[$column_phid]; @@ -629,6 +630,10 @@ final class PhabricatorProjectBoardViewController if ($trigger) { $preview_effect = $trigger->getPreviewEffect() ->toDictionary(); + + foreach ($trigger->getSoundEffects() as $sound) { + $sounds[] = $sound; + } } } @@ -685,6 +690,7 @@ final class PhabricatorProjectBoardViewController 'boardID' => $board_id, 'projectPHID' => $project->getPHID(), + 'preloadSounds' => $sounds, ); $this->initBehavior('project-boards', $behavior_config); diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php index 850dfa2268..c28ace305c 100644 --- a/src/applications/project/controller/PhabricatorProjectController.php +++ b/src/applications/project/controller/PhabricatorProjectController.php @@ -152,7 +152,8 @@ abstract class PhabricatorProjectController extends PhabricatorController { protected function newCardResponse( $board_phid, $object_phid, - PhabricatorProjectColumnOrder $ordering = null) { + PhabricatorProjectColumnOrder $ordering = null, + $sounds = array()) { $viewer = $this->getViewer(); @@ -166,7 +167,8 @@ abstract class PhabricatorProjectController extends PhabricatorController { ->setViewer($viewer) ->setBoardPHID($board_phid) ->setObjectPHID($object_phid) - ->setVisiblePHIDs($visible_phids); + ->setVisiblePHIDs($visible_phids) + ->setSounds($sounds); if ($ordering) { $engine->setOrdering($ordering); diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php index 71588754c0..950b1e90cd 100644 --- a/src/applications/project/controller/PhabricatorProjectMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectMoveController.php @@ -111,6 +111,7 @@ final class PhabricatorProjectMoveController $xactions[] = $header_xaction; } + $sounds = array(); if ($column->canHaveTrigger()) { $trigger = $column->getTrigger(); if ($trigger) { @@ -121,6 +122,10 @@ final class PhabricatorProjectMoveController foreach ($trigger_xactions as $trigger_xaction) { $xactions[] = $trigger_xaction; } + + foreach ($trigger->getSoundEffects() as $effect) { + $sounds[] = $effect; + } } } @@ -133,7 +138,11 @@ final class PhabricatorProjectMoveController $editor->applyTransactions($object, $xactions); - return $this->newCardResponse($board_phid, $object_phid, $ordering); + return $this->newCardResponse( + $board_phid, + $object_phid, + $ordering, + $sounds); } } diff --git a/src/applications/project/engine/PhabricatorBoardResponseEngine.php b/src/applications/project/engine/PhabricatorBoardResponseEngine.php index fb5299a857..f22254e43a 100644 --- a/src/applications/project/engine/PhabricatorBoardResponseEngine.php +++ b/src/applications/project/engine/PhabricatorBoardResponseEngine.php @@ -7,6 +7,7 @@ final class PhabricatorBoardResponseEngine extends Phobject { private $objectPHID; private $visiblePHIDs; private $ordering; + private $sounds; public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; @@ -53,6 +54,15 @@ final class PhabricatorBoardResponseEngine extends Phobject { return $this->ordering; } + public function setSounds(array $sounds) { + $this->sounds = $sounds; + return $this; + } + + public function getSounds() { + return $this->sounds; + } + public function buildResponse() { $viewer = $this->getViewer(); $object_phid = $this->getObjectPHID(); @@ -150,6 +160,7 @@ final class PhabricatorBoardResponseEngine extends Phobject { 'columnMaps' => $natural, 'cards' => $cards, 'headers' => $headers, + 'sounds' => $this->getSounds(), ); return id(new AphrontAjaxResponse()) diff --git a/src/applications/project/storage/PhabricatorProjectTrigger.php b/src/applications/project/storage/PhabricatorProjectTrigger.php index a499f0bcf8..bac3927b74 100644 --- a/src/applications/project/storage/PhabricatorProjectTrigger.php +++ b/src/applications/project/storage/PhabricatorProjectTrigger.php @@ -245,6 +245,18 @@ final class PhabricatorProjectTrigger ->setContent($header); } + public function getSoundEffects() { + $sounds = array(); + + foreach ($this->getTriggerRules() as $rule) { + foreach ($rule->getSoundEffects() as $effect) { + $sounds[] = $effect; + } + } + + return $sounds; + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerPlaySoundRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerPlaySoundRule.php new file mode 100644 index 0000000000..ef19b504ef --- /dev/null +++ b/src/applications/project/trigger/PhabricatorProjectTriggerPlaySoundRule.php @@ -0,0 +1,122 @@ +newEffect() + ->setIcon($sound_icon) + ->setColor($sound_color) + ->setContent($content), + ); + } + + protected function getDefaultValue() { + return head_key(self::getSoundMap()); + } + + protected function getPHUIXControlType() { + return 'select'; + } + + protected function getPHUIXControlSpecification() { + $map = self::getSoundMap(); + $map = ipull($map, 'name'); + + return array( + 'options' => $map, + 'order' => array_keys($map), + ); + } + + public function getRuleViewLabel() { + return pht('Play Sound'); + } + + public function getRuleViewDescription($value) { + $sound_name = self::getSoundName($value); + + return pht( + 'Play sound %s.', + phutil_tag('strong', array(), $sound_name)); + } + + public function getRuleViewIcon($value) { + $sound_icon = 'fa-volume-up'; + $sound_color = 'blue'; + + return id(new PHUIIconView()) + ->setIcon($sound_icon, $sound_color); + } + + private static function getSoundName($value) { + $map = self::getSoundMap(); + $spec = idx($map, $value, array()); + return idx($spec, 'name', $value); + } + + private static function getSoundMap() { + return array( + 'bing' => array( + 'name' => pht('Bing'), + 'uri' => celerity_get_resource_uri('/rsrc/audio/basic/bing.mp3'), + ), + 'glass' => array( + 'name' => pht('Glass'), + 'uri' => celerity_get_resource_uri('/rsrc/audio/basic/ting.mp3'), + ), + ); + } + + public function getSoundEffects() { + $value = $this->getValue(); + + $map = self::getSoundMap(); + $spec = idx($map, $value, array()); + + $uris = array(); + if (isset($spec['uri'])) { + $uris[] = $spec['uri']; + } + + return $uris; + } + +} diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerRule.php index c75c15a1ab..ae2b3ee092 100644 --- a/src/applications/project/trigger/PhabricatorProjectTriggerRule.php +++ b/src/applications/project/trigger/PhabricatorProjectTriggerRule.php @@ -60,6 +60,10 @@ abstract class PhabricatorProjectTriggerRule return null; } + public function getSoundEffects() { + return array(); + } + final public function getDropTransactions($object, $value) { return $this->newDropTransactions($object, $value); } diff --git a/webroot/rsrc/externals/javelin/lib/Sound.js b/webroot/rsrc/externals/javelin/lib/Sound.js index accbe3d29b..68181560ff 100644 --- a/webroot/rsrc/externals/javelin/lib/Sound.js +++ b/webroot/rsrc/externals/javelin/lib/Sound.js @@ -8,31 +8,75 @@ JX.install('Sound', { statics: { _sounds: {}, + _queue: [], + _playingQueue: false, load: function(uri) { var self = JX.Sound; if (!(uri in self._sounds)) { - self._sounds[uri] = JX.$N( + var audio = JX.$N( 'audio', { src: uri, preload: 'auto' }); + + // In Safari, it isn't good enough to just load a sound in response + // to a click: we must also play it. Once we've played it once, we + // can continue to play it freely. + + // Play the sound, then immediately pause it. This rejects the "play()" + // promise but marks the audio as playable, so our "play()" method will + // work correctly later. + if (window.webkitAudioContext) { + audio.play().then(JX.bag, JX.bag); + audio.pause(); + } + + self._sounds[uri] = audio; } }, - play: function(uri) { + play: function(uri, callback) { var self = JX.Sound; self.load(uri); var sound = self._sounds[uri]; try { - sound.play(); + sound.onended = callback || JX.bag; + sound.play().then(JX.bag, callback || JX.bag); } catch (ex) { JX.log(ex); } + }, + + queue: function(uri) { + var self = JX.Sound; + self._queue.push(uri); + self._playQueue(); + }, + + _playQueue: function() { + var self = JX.Sound; + if (self._playingQueue) { + return; + } + self._playingQueue = true; + self._nextQueue(); + }, + + _nextQueue: function() { + var self = JX.Sound; + if (self._queue.length) { + var next = self._queue[0]; + self._queue.splice(0, 1); + self.play(next, self._nextQueue); + } else { + self._playingQueue = false; + } } + } }); diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index a7786a86f4..6add658259 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -529,6 +529,11 @@ JX.install('WorkboardBoard', { this.updateCard(response); + var sounds = response.sounds || []; + for (var ii = 0; ii < sounds.length; ii++) { + JX.Sound.queue(sounds[ii]); + } + list.unlock(); }, diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index daec59155f..bba6db7a49 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -166,4 +166,16 @@ JX.behavior('project-boards', function(config, statics) { board.start(); + // In Safari, we can only play sounds that we've already loaded, and we can + // only load them in response to an explicit user interaction like a click. + var sounds = config.preloadSounds; + var listener = JX.Stratcom.listen('mousedown', null, function() { + for (var ii = 0; ii < sounds.length; ii++) { + JX.Sound.load(sounds[ii]); + } + + // Remove this callback once it has run once. + listener.remove(); + }); + });