@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add a "Play Sound" workboard trigger rule

Summary:
Ref T5474. Allow columns to play a sound when tasks are dropped.

This is a little tricky because Safari has changed somewhat recently to require some gymnastics to play sounds when the user didn't explicitly click something. Preloading the sound on the first mouse interaction, then playing and immediately pausing it seems to work, though.

Test Plan: Added a trigger with 5 sounds. In Safari, Chrome, and Firefox, dropped a card into the column. In all browsers, heard a nice sequence of 5 sounds played one after the other.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T5474

Differential Revision: https://secure.phabricator.com/D20306

+267 -38
+32 -32
resources/celerity/map.php
··· 10 10 'conpherence.pkg.css' => '3c8a0668', 11 11 'conpherence.pkg.js' => '020aebcf', 12 12 'core.pkg.css' => 'b797945d', 13 - 'core.pkg.js' => 'eaca003c', 13 + 'core.pkg.js' => 'eb53fc5b', 14 14 'differential.pkg.css' => '8d8360fb', 15 15 'differential.pkg.js' => '67e02996', 16 16 'diffusion.pkg.css' => '42c75c37', ··· 249 249 'rsrc/externals/javelin/lib/Routable.js' => '6a18c42e', 250 250 'rsrc/externals/javelin/lib/Router.js' => '32755edb', 251 251 'rsrc/externals/javelin/lib/Scrollbar.js' => 'a43ae2ae', 252 - 'rsrc/externals/javelin/lib/Sound.js' => 'e562708c', 252 + 'rsrc/externals/javelin/lib/Sound.js' => 'd4cc2d2a', 253 253 'rsrc/externals/javelin/lib/URI.js' => '2e255291', 254 254 'rsrc/externals/javelin/lib/Vector.js' => 'e9c80beb', 255 255 'rsrc/externals/javelin/lib/WebSocket.js' => 'fdc13e4e', ··· 409 409 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 410 410 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 411 411 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', 412 - 'rsrc/js/application/projects/WorkboardBoard.js' => '65afb173', 412 + 'rsrc/js/application/projects/WorkboardBoard.js' => '3ba8e6ad', 413 413 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', 414 414 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4', 415 415 'rsrc/js/application/projects/WorkboardColumn.js' => 'c3d24e63', ··· 418 418 'rsrc/js/application/projects/WorkboardHeader.js' => '111bfd2d', 419 419 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => 'ebe83a6b', 420 420 'rsrc/js/application/projects/WorkboardOrderTemplate.js' => '03e8891f', 421 - 'rsrc/js/application/projects/behavior-project-boards.js' => '8512e4ea', 421 + 'rsrc/js/application/projects/behavior-project-boards.js' => 'aad45445', 422 422 'rsrc/js/application/projects/behavior-project-create.js' => '34c53422', 423 423 'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9', 424 424 'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68', ··· 664 664 'javelin-behavior-phuix-example' => 'c2c500a7', 665 665 'javelin-behavior-policy-control' => '0eaa33a9', 666 666 'javelin-behavior-policy-rule-editor' => '9347f172', 667 - 'javelin-behavior-project-boards' => '8512e4ea', 667 + 'javelin-behavior-project-boards' => 'aad45445', 668 668 'javelin-behavior-project-create' => '34c53422', 669 669 'javelin-behavior-quicksand-blacklist' => '5a6f6a06', 670 670 'javelin-behavior-read-only-warning' => 'b9109f8f', ··· 718 718 'javelin-routable' => '6a18c42e', 719 719 'javelin-router' => '32755edb', 720 720 'javelin-scrollbar' => 'a43ae2ae', 721 - 'javelin-sound' => 'e562708c', 721 + 'javelin-sound' => 'd4cc2d2a', 722 722 'javelin-stratcom' => '0889b835', 723 723 'javelin-tokenizer' => '89a1ae3a', 724 724 'javelin-typeahead' => 'a4356cde', ··· 737 737 'javelin-view-renderer' => '9aae2b66', 738 738 'javelin-view-visitor' => '308f9fe4', 739 739 'javelin-websocket' => 'fdc13e4e', 740 - 'javelin-workboard-board' => '65afb173', 740 + 'javelin-workboard-board' => '3ba8e6ad', 741 741 'javelin-workboard-card' => '0392a5d8', 742 742 'javelin-workboard-card-template' => '2a61f8d4', 743 743 'javelin-workboard-column' => 'c3d24e63', ··· 1227 1227 'javelin-behavior', 1228 1228 'phabricator-prefab', 1229 1229 ), 1230 + '3ba8e6ad' => array( 1231 + 'javelin-install', 1232 + 'javelin-dom', 1233 + 'javelin-util', 1234 + 'javelin-stratcom', 1235 + 'javelin-workflow', 1236 + 'phabricator-draggable-list', 1237 + 'javelin-workboard-column', 1238 + 'javelin-workboard-header-template', 1239 + 'javelin-workboard-card-template', 1240 + 'javelin-workboard-order-template', 1241 + ), 1230 1242 '3dc5ad43' => array( 1231 1243 'javelin-behavior', 1232 1244 'javelin-stratcom', ··· 1456 1468 '60cd9241' => array( 1457 1469 'javelin-behavior', 1458 1470 ), 1459 - '65afb173' => array( 1460 - 'javelin-install', 1461 - 'javelin-dom', 1462 - 'javelin-util', 1463 - 'javelin-stratcom', 1464 - 'javelin-workflow', 1465 - 'phabricator-draggable-list', 1466 - 'javelin-workboard-column', 1467 - 'javelin-workboard-header-template', 1468 - 'javelin-workboard-card-template', 1469 - 'javelin-workboard-order-template', 1470 - ), 1471 1471 '65bb0011' => array( 1472 1472 'javelin-behavior', 1473 1473 'javelin-dom', ··· 1593 1593 'javelin-util', 1594 1594 'javelin-dom', 1595 1595 'javelin-vector', 1596 - ), 1597 - '8512e4ea' => array( 1598 - 'javelin-behavior', 1599 - 'javelin-dom', 1600 - 'javelin-util', 1601 - 'javelin-vector', 1602 - 'javelin-stratcom', 1603 - 'javelin-workflow', 1604 - 'javelin-workboard-controller', 1605 - 'javelin-workboard-drop-effect', 1606 1596 ), 1607 1597 '87428eb2' => array( 1608 1598 'javelin-behavior', ··· 1848 1838 'javelin-dom', 1849 1839 'javelin-util', 1850 1840 ), 1841 + 'aad45445' => array( 1842 + 'javelin-behavior', 1843 + 'javelin-dom', 1844 + 'javelin-util', 1845 + 'javelin-vector', 1846 + 'javelin-stratcom', 1847 + 'javelin-workflow', 1848 + 'javelin-workboard-controller', 1849 + 'javelin-workboard-drop-effect', 1850 + ), 1851 1851 'ab85e184' => array( 1852 1852 'javelin-install', 1853 1853 'javelin-dom', ··· 2041 2041 'd3799cb4' => array( 2042 2042 'javelin-install', 2043 2043 ), 2044 + 'd4cc2d2a' => array( 2045 + 'javelin-install', 2046 + ), 2044 2047 'd8a86cfb' => array( 2045 2048 'javelin-behavior', 2046 2049 'javelin-dom', ··· 2074 2077 'javelin-stratcom', 2075 2078 'javelin-dom', 2076 2079 'javelin-history', 2077 - ), 2078 - 'e562708c' => array( 2079 - 'javelin-install', 2080 2080 ), 2081 2081 'e5bdb730' => array( 2082 2082 'javelin-behavior',
+2
src/__phutil_library_map__.php
··· 4183 4183 'PhabricatorProjectTriggerManiphestStatusRule' => 'applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php', 4184 4184 'PhabricatorProjectTriggerNameTransaction' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerNameTransaction.php', 4185 4185 'PhabricatorProjectTriggerPHIDType' => 'applications/project/phid/PhabricatorProjectTriggerPHIDType.php', 4186 + 'PhabricatorProjectTriggerPlaySoundRule' => 'applications/project/trigger/PhabricatorProjectTriggerPlaySoundRule.php', 4186 4187 'PhabricatorProjectTriggerQuery' => 'applications/project/query/PhabricatorProjectTriggerQuery.php', 4187 4188 'PhabricatorProjectTriggerRule' => 'applications/project/trigger/PhabricatorProjectTriggerRule.php', 4188 4189 'PhabricatorProjectTriggerRuleRecord' => 'applications/project/trigger/PhabricatorProjectTriggerRuleRecord.php', ··· 10317 10318 'PhabricatorProjectTriggerManiphestStatusRule' => 'PhabricatorProjectTriggerRule', 10318 10319 'PhabricatorProjectTriggerNameTransaction' => 'PhabricatorProjectTriggerTransactionType', 10319 10320 'PhabricatorProjectTriggerPHIDType' => 'PhabricatorPHIDType', 10321 + 'PhabricatorProjectTriggerPlaySoundRule' => 'PhabricatorProjectTriggerRule', 10320 10322 'PhabricatorProjectTriggerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 10321 10323 'PhabricatorProjectTriggerRule' => 'Phobject', 10322 10324 'PhabricatorProjectTriggerRuleRecord' => 'Phobject',
+6
src/applications/project/controller/PhabricatorProjectBoardViewController.php
··· 542 542 $templates = array(); 543 543 $all_tasks = array(); 544 544 $column_templates = array(); 545 + $sounds = array(); 545 546 foreach ($visible_columns as $column_phid => $column) { 546 547 $column_tasks = $column_phids[$column_phid]; 547 548 ··· 629 630 if ($trigger) { 630 631 $preview_effect = $trigger->getPreviewEffect() 631 632 ->toDictionary(); 633 + 634 + foreach ($trigger->getSoundEffects() as $sound) { 635 + $sounds[] = $sound; 636 + } 632 637 } 633 638 } 634 639 ··· 685 690 686 691 'boardID' => $board_id, 687 692 'projectPHID' => $project->getPHID(), 693 + 'preloadSounds' => $sounds, 688 694 ); 689 695 $this->initBehavior('project-boards', $behavior_config); 690 696
+4 -2
src/applications/project/controller/PhabricatorProjectController.php
··· 152 152 protected function newCardResponse( 153 153 $board_phid, 154 154 $object_phid, 155 - PhabricatorProjectColumnOrder $ordering = null) { 155 + PhabricatorProjectColumnOrder $ordering = null, 156 + $sounds = array()) { 156 157 157 158 $viewer = $this->getViewer(); 158 159 ··· 166 167 ->setViewer($viewer) 167 168 ->setBoardPHID($board_phid) 168 169 ->setObjectPHID($object_phid) 169 - ->setVisiblePHIDs($visible_phids); 170 + ->setVisiblePHIDs($visible_phids) 171 + ->setSounds($sounds); 170 172 171 173 if ($ordering) { 172 174 $engine->setOrdering($ordering);
+10 -1
src/applications/project/controller/PhabricatorProjectMoveController.php
··· 111 111 $xactions[] = $header_xaction; 112 112 } 113 113 114 + $sounds = array(); 114 115 if ($column->canHaveTrigger()) { 115 116 $trigger = $column->getTrigger(); 116 117 if ($trigger) { ··· 121 122 foreach ($trigger_xactions as $trigger_xaction) { 122 123 $xactions[] = $trigger_xaction; 123 124 } 125 + 126 + foreach ($trigger->getSoundEffects() as $effect) { 127 + $sounds[] = $effect; 128 + } 124 129 } 125 130 } 126 131 ··· 133 138 134 139 $editor->applyTransactions($object, $xactions); 135 140 136 - return $this->newCardResponse($board_phid, $object_phid, $ordering); 141 + return $this->newCardResponse( 142 + $board_phid, 143 + $object_phid, 144 + $ordering, 145 + $sounds); 137 146 } 138 147 139 148 }
+11
src/applications/project/engine/PhabricatorBoardResponseEngine.php
··· 7 7 private $objectPHID; 8 8 private $visiblePHIDs; 9 9 private $ordering; 10 + private $sounds; 10 11 11 12 public function setViewer(PhabricatorUser $viewer) { 12 13 $this->viewer = $viewer; ··· 51 52 52 53 public function getOrdering() { 53 54 return $this->ordering; 55 + } 56 + 57 + public function setSounds(array $sounds) { 58 + $this->sounds = $sounds; 59 + return $this; 60 + } 61 + 62 + public function getSounds() { 63 + return $this->sounds; 54 64 } 55 65 56 66 public function buildResponse() { ··· 150 160 'columnMaps' => $natural, 151 161 'cards' => $cards, 152 162 'headers' => $headers, 163 + 'sounds' => $this->getSounds(), 153 164 ); 154 165 155 166 return id(new AphrontAjaxResponse())
+12
src/applications/project/storage/PhabricatorProjectTrigger.php
··· 245 245 ->setContent($header); 246 246 } 247 247 248 + public function getSoundEffects() { 249 + $sounds = array(); 250 + 251 + foreach ($this->getTriggerRules() as $rule) { 252 + foreach ($rule->getSoundEffects() as $effect) { 253 + $sounds[] = $effect; 254 + } 255 + } 256 + 257 + return $sounds; 258 + } 259 + 248 260 249 261 /* -( PhabricatorApplicationTransactionInterface )------------------------- */ 250 262
+122
src/applications/project/trigger/PhabricatorProjectTriggerPlaySoundRule.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectTriggerPlaySoundRule 4 + extends PhabricatorProjectTriggerRule { 5 + 6 + const TRIGGERTYPE = 'sound'; 7 + 8 + public function getSelectControlName() { 9 + return pht('Play sound'); 10 + } 11 + 12 + protected function assertValidRuleValue($value) { 13 + if (!is_string($value)) { 14 + throw new Exception( 15 + pht( 16 + 'Status rule value should be a string, but is not (value is "%s").', 17 + phutil_describe_type($value))); 18 + } 19 + 20 + $map = self::getSoundMap(); 21 + 22 + if (!isset($map[$value])) { 23 + throw new Exception( 24 + pht( 25 + 'Rule value ("%s") is not a valid sound.', 26 + $value)); 27 + } 28 + } 29 + 30 + protected function newDropTransactions($object, $value) { 31 + return array(); 32 + } 33 + 34 + protected function newDropEffects($value) { 35 + $sound_icon = 'fa-volume-up'; 36 + $sound_color = 'blue'; 37 + $sound_name = self::getSoundName($value); 38 + 39 + $content = pht( 40 + 'Play sound %s.', 41 + phutil_tag('strong', array(), $sound_name)); 42 + 43 + return array( 44 + $this->newEffect() 45 + ->setIcon($sound_icon) 46 + ->setColor($sound_color) 47 + ->setContent($content), 48 + ); 49 + } 50 + 51 + protected function getDefaultValue() { 52 + return head_key(self::getSoundMap()); 53 + } 54 + 55 + protected function getPHUIXControlType() { 56 + return 'select'; 57 + } 58 + 59 + protected function getPHUIXControlSpecification() { 60 + $map = self::getSoundMap(); 61 + $map = ipull($map, 'name'); 62 + 63 + return array( 64 + 'options' => $map, 65 + 'order' => array_keys($map), 66 + ); 67 + } 68 + 69 + public function getRuleViewLabel() { 70 + return pht('Play Sound'); 71 + } 72 + 73 + public function getRuleViewDescription($value) { 74 + $sound_name = self::getSoundName($value); 75 + 76 + return pht( 77 + 'Play sound %s.', 78 + phutil_tag('strong', array(), $sound_name)); 79 + } 80 + 81 + public function getRuleViewIcon($value) { 82 + $sound_icon = 'fa-volume-up'; 83 + $sound_color = 'blue'; 84 + 85 + return id(new PHUIIconView()) 86 + ->setIcon($sound_icon, $sound_color); 87 + } 88 + 89 + private static function getSoundName($value) { 90 + $map = self::getSoundMap(); 91 + $spec = idx($map, $value, array()); 92 + return idx($spec, 'name', $value); 93 + } 94 + 95 + private static function getSoundMap() { 96 + return array( 97 + 'bing' => array( 98 + 'name' => pht('Bing'), 99 + 'uri' => celerity_get_resource_uri('/rsrc/audio/basic/bing.mp3'), 100 + ), 101 + 'glass' => array( 102 + 'name' => pht('Glass'), 103 + 'uri' => celerity_get_resource_uri('/rsrc/audio/basic/ting.mp3'), 104 + ), 105 + ); 106 + } 107 + 108 + public function getSoundEffects() { 109 + $value = $this->getValue(); 110 + 111 + $map = self::getSoundMap(); 112 + $spec = idx($map, $value, array()); 113 + 114 + $uris = array(); 115 + if (isset($spec['uri'])) { 116 + $uris[] = $spec['uri']; 117 + } 118 + 119 + return $uris; 120 + } 121 + 122 + }
+4
src/applications/project/trigger/PhabricatorProjectTriggerRule.php
··· 60 60 return null; 61 61 } 62 62 63 + public function getSoundEffects() { 64 + return array(); 65 + } 66 + 63 67 final public function getDropTransactions($object, $value) { 64 68 return $this->newDropTransactions($object, $value); 65 69 }
+47 -3
webroot/rsrc/externals/javelin/lib/Sound.js
··· 8 8 JX.install('Sound', { 9 9 statics: { 10 10 _sounds: {}, 11 + _queue: [], 12 + _playingQueue: false, 11 13 12 14 load: function(uri) { 13 15 var self = JX.Sound; 14 16 15 17 if (!(uri in self._sounds)) { 16 - self._sounds[uri] = JX.$N( 18 + var audio = JX.$N( 17 19 'audio', 18 20 { 19 21 src: uri, 20 22 preload: 'auto' 21 23 }); 24 + 25 + // In Safari, it isn't good enough to just load a sound in response 26 + // to a click: we must also play it. Once we've played it once, we 27 + // can continue to play it freely. 28 + 29 + // Play the sound, then immediately pause it. This rejects the "play()" 30 + // promise but marks the audio as playable, so our "play()" method will 31 + // work correctly later. 32 + if (window.webkitAudioContext) { 33 + audio.play().then(JX.bag, JX.bag); 34 + audio.pause(); 35 + } 36 + 37 + self._sounds[uri] = audio; 22 38 } 23 39 }, 24 40 25 - play: function(uri) { 41 + play: function(uri, callback) { 26 42 var self = JX.Sound; 27 43 self.load(uri); 28 44 29 45 var sound = self._sounds[uri]; 30 46 31 47 try { 32 - sound.play(); 48 + sound.onended = callback || JX.bag; 49 + sound.play().then(JX.bag, callback || JX.bag); 33 50 } catch (ex) { 34 51 JX.log(ex); 35 52 } 53 + }, 54 + 55 + queue: function(uri) { 56 + var self = JX.Sound; 57 + self._queue.push(uri); 58 + self._playQueue(); 59 + }, 60 + 61 + _playQueue: function() { 62 + var self = JX.Sound; 63 + if (self._playingQueue) { 64 + return; 65 + } 66 + self._playingQueue = true; 67 + self._nextQueue(); 68 + }, 69 + 70 + _nextQueue: function() { 71 + var self = JX.Sound; 72 + if (self._queue.length) { 73 + var next = self._queue[0]; 74 + self._queue.splice(0, 1); 75 + self.play(next, self._nextQueue); 76 + } else { 77 + self._playingQueue = false; 78 + } 36 79 } 80 + 37 81 } 38 82 });
+5
webroot/rsrc/js/application/projects/WorkboardBoard.js
··· 529 529 530 530 this.updateCard(response); 531 531 532 + var sounds = response.sounds || []; 533 + for (var ii = 0; ii < sounds.length; ii++) { 534 + JX.Sound.queue(sounds[ii]); 535 + } 536 + 532 537 list.unlock(); 533 538 }, 534 539
+12
webroot/rsrc/js/application/projects/behavior-project-boards.js
··· 166 166 167 167 board.start(); 168 168 169 + // In Safari, we can only play sounds that we've already loaded, and we can 170 + // only load them in response to an explicit user interaction like a click. 171 + var sounds = config.preloadSounds; 172 + var listener = JX.Stratcom.listen('mousedown', null, function() { 173 + for (var ii = 0; ii < sounds.length; ii++) { 174 + JX.Sound.load(sounds[ii]); 175 + } 176 + 177 + // Remove this callback once it has run once. 178 + listener.remove(); 179 + }); 180 + 169 181 });