1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-26 00:32:42 +01:00

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
This commit is contained in:
epriestley 2019-03-21 15:22:09 -07:00
parent 66c1d623c3
commit bfa5ffe8a1
12 changed files with 267 additions and 38 deletions

View file

@ -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',

View file

@ -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',

View file

@ -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);

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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())

View file

@ -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 )------------------------- */

View file

@ -0,0 +1,122 @@
<?php
final class PhabricatorProjectTriggerPlaySoundRule
extends PhabricatorProjectTriggerRule {
const TRIGGERTYPE = 'sound';
public function getSelectControlName() {
return pht('Play sound');
}
protected function assertValidRuleValue($value) {
if (!is_string($value)) {
throw new Exception(
pht(
'Status rule value should be a string, but is not (value is "%s").',
phutil_describe_type($value)));
}
$map = self::getSoundMap();
if (!isset($map[$value])) {
throw new Exception(
pht(
'Rule value ("%s") is not a valid sound.',
$value));
}
}
protected function newDropTransactions($object, $value) {
return array();
}
protected function newDropEffects($value) {
$sound_icon = 'fa-volume-up';
$sound_color = 'blue';
$sound_name = self::getSoundName($value);
$content = pht(
'Play sound %s.',
phutil_tag('strong', array(), $sound_name));
return array(
$this->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;
}
}

View file

@ -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);
}

View file

@ -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;
}
}
}
});

View file

@ -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();
},

View file

@ -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();
});
});