VRFS-1491 - create personal/master mix toggle, and beef up user.mods

This commit is contained in:
Seth Call 2014-11-11 16:21:46 -06:00
parent f0b70b1bb7
commit 6277708bb4
17 changed files with 444 additions and 101 deletions

View File

@ -79,6 +79,10 @@ module ValidationMessages
# notification
DIFFERENT_SOURCE_TARGET = 'can\'t be same as the sender'
# mods
MODS_NO_SHOW_MUST_BE_HASH = 'no_show must be a hash'
MODS_UNKNOWN_KEY = 'unknown mod'
# takes either a string/string hash, or a string/array-of-strings|symbols hash,
# and creates a ActiveRecord.errors style object
def createValidationStyleObject(validation_errors)

View File

@ -155,6 +155,7 @@ module JamRuby
validate :validate_avatar_info
validate :email_case_insensitive_uniqueness
validate :update_email_case_insensitive_uniqueness, :if => :updating_email
validate :validate_mods
scope :musicians, where(:musician => true)
scope :fans, where(:musician => false)
@ -185,6 +186,17 @@ module JamRuby
errors.add(:musician_instruments, ValidationMessages::INSTRUMENT_LIMIT_EXCEEDED) if !administratively_created && musician && musician_instruments.length > 5
end
# let's work to stop junk from getting into the mods array; this is essentially the schema
def validate_mods
mods_json.each do |key, value|
if key == "no_show"
errors.add(:mods, ValidationMessages::MODS_NO_SHOW_MUST_BE_HASH) unless value.is_a?(Hash)
else
errors.add(:mods, ValidationMessages::MODS_UNKNOWN_KEY)
end
end
end
def validate_current_password
# checks if the user put in their current password (used when changing your email, for instance)
errors.add(:current_password, ValidationMessages::NOT_YOUR_PASSWORD) if should_confirm_existing_password? && !valid_password?(self.current_password)
@ -355,7 +367,20 @@ module JamRuby
# mods comes back as text; so give ourselves a parsed version
def mods_json
@mods_json ||= mods ? JSON.parse(mods, symbolize_names: true) : {}
@mods_json ||= mods ? JSON.parse(mods) : {}
end
# new_modes should be a regular hash with non-symbolized keys (vs symbolized keys)
def mod_merge(new_mods)
self.mods = (mods_json.merge(new_mods) do |key, old_val, new_val|
if key == "no_show"
# we take the values from previous no_shows, and merge it with the new no_shows
old_val.merge(new_val)
else
raise "unknown in mode_merge key: #{key}"
end
end).to_json
@mods_json = nil # invalidate this since we've updated self.mods
end
def heartbeat_interval_client

View File

@ -77,14 +77,9 @@ describe User do
it { should be_valid }
end
describe "when mods is empty" do
before { @user.mods = 'nil' }
it { should_not be_valid }
end
describe "when mods is json object" do
before { @user.mods = '{"key":"value"}' }
before { @user.mods = '{"no_show":{"value": true}}' }
it { should be_valid }
end
@ -453,25 +448,9 @@ describe User do
describe "mods" do
it "should allow update of JSON" do
@user.mods = {some_field: 5}.to_json
@user.mods = {no_show: {something:1}}.to_json
@user.save!
end
it "should return heartbeart interval" do
@user.heartbeat_interval_client.should be_nil
@user.mods = {heartbeat_interval_client: 5}.to_json
@user.save!
@user = User.find(@user.id) # necessary because mods_json is cached in the model
@user.heartbeat_interval_client.should == 5
end
it "should return connection_expire_time" do
@user.connection_expire_time_client.should be_nil
@user.mods = {connection_expire_time_client: 5}.to_json
@user.save!
@user = User.find(@user.id) # necessary because mods_json is cached in the model
@user.connection_expire_time_client.should == 5
end
end
describe "audio latency" do
@ -587,6 +566,52 @@ describe User do
end
end
describe "mods_merge" do
let(:user) {FactoryGirl.create(:user)}
it "allow empty merge" do
user.mod_merge({})
user.valid?.should be_true
user.mods.should == {}.to_json
end
it "allow no_show set" do
user.mod_merge({"no_show" => {"some_screen" => true}})
user.valid?.should be_true
user.mods.should == {no_show:{some_screen:true}}.to_json
end
it "allow no_show aggregation" do
user.mod_merge({"no_show" => {"some_screen1" => true}})
user.save!
user.reload
user.mod_merge({"no_show" => {"some_screen2" => true}})
user.valid?.should be_true
user.mods.should == {"no_show" => {"some_screen1" => true, "some_screen2" => true}}.to_json
end
it "allow no_show override" do
user.mod_merge({"no_show" => {"some_screen1" => true}})
user.save!
user.reload
user.mod_merge({"no_show" => {"some_screen1" => false}})
user.valid?.should be_true
user.mods.should == {no_show:{some_screen1:false}}.to_json
end
it "does not allow random root keys" do
user.mod_merge({random_root_key:true})
user.valid?.should be_false
user.errors[:mods].should == [ValidationMessages::MODS_UNKNOWN_KEY]
end
it "does not allow non-hash no_show" do
user.mod_merge({no_show:true})
user.valid?.should be_false
user.errors[:mods].should == [ValidationMessages::MODS_NO_SHOW_MUST_BE_HASH]
end
end
=begin
describe "update avatar" do

View File

@ -6,12 +6,16 @@
context.JK = context.JK || {};
context.JK.Banner = (function () {
var modUtils = context.JK.ModUtils;
var self = this;
var logger = context.JK.logger;
var $banner = null;
var $closeBtn = null;
var $yesBtn = null;
var $noBtn = null;
var $noShow = null;
var $noShowCheckbox = null;
var $buttons = null;
// you can also do
// * showAlert('title', 'text')
@ -77,7 +81,7 @@
if((options.type == "alert" && !options.buttons) || options.close) {
if((options.type == "alert" && !options.buttons) || options.close || options.no_show) {
var closeButtonText = 'CLOSE';
if(options.close !== null && typeof options.close == 'object') {
@ -87,13 +91,22 @@
}
}
if(options.no_show) {
$buttons.addClass('center')
$noShowCheckbox.data('no_show', options.no_show)
$noShow.show()
}
$closeBtn.show().text(closeButtonText).unbind('click').click(function() {
if ($noShowCheckbox.is(':visible') && $noShowCheckbox.is(':checked')) {
modUtils.updateNoShow($noShowCheckbox.data('no_show'))
}
hide();
return false;
});
}
else {
$closeBtn.hide();
$noShow.hide();
}
if(options.type == "yes_no") {
@ -118,7 +131,6 @@
}
if(options.buttons) {
var $buttons = $banner.find('.buttons')
context._.each(options.buttons, function(button, i) {
if(!button.name) throw "button.name must be specified";
if(!button.click) throw "button.click must be specified";
@ -148,6 +160,9 @@
$banner.find('.user-btn').remove();
$('#banner_overlay .dialog-inner').html("");
$('#banner_overlay').hide();
$buttons.removeClass('center')
$noShowCheckbox.data('no_show', null).iCheck('uncheck').attr('checked', false)
$buttons.children().hide();
}
function initialize() {
@ -157,6 +172,11 @@
$closeBtn = $banner.find('.close-btn');
$yesBtn = $banner.find('.yes-btn');
$noBtn = $banner.find('.no-btn');
$noShowCheckbox = $banner.find('.no-more-show-checkbox')
$noShow = $banner.find('.no-more-show')
$buttons = $banner.find('.buttons')
context.JK.checkbox($noShowCheckbox);
return self;
}

View File

@ -427,7 +427,8 @@
stereo: true,
volume_left: -40,
volume_right:-40,
instrument_id:50 // see globals.js
instrument_id:50, // see globals.js
mode: false
});
}
return response;
@ -489,6 +490,7 @@
function SessionGetDeviceLatency() { return 10.0; }
function SessionPageEnter() {logger.debug("FakeJamClient: SessionPageEnter"); return {}}
function SessionPageLeave() {logger.debug("FakeJamClient: SessionPageLeave")}
function SetMixerMode(mode) {}
function SessionGetMasterLocalMix() {
logger.debug('SessionGetMasterLocalMix. Returning: ' + _mix);
return _mix;
@ -929,6 +931,7 @@
this.SessionGetTracksPlayDurationMs = SessionGetTracksPlayDurationMs;
this.SessionPageEnter = SessionPageEnter;
this.SessionPageLeave = SessionPageLeave;
this.SetMixerMode = SetMixerMode;
this.SetVURefreshRate = SetVURefreshRate;
this.SessionGetMasterLocalMix = SessionGetMasterLocalMix;

View File

@ -39,7 +39,8 @@
FILE_MANAGER_CMD_START : 'file_manager_cmd_start',
FILE_MANAGER_CMD_STOP : 'file_manager_cmd_stop',
FILE_MANAGER_CMD_PROGRESS : 'file_manager_cmd_progress',
FILE_MANAGER_CMD_ASAP_UPDATE : 'file_manager_cmd_asap_update'
FILE_MANAGER_CMD_ASAP_UPDATE : 'file_manager_cmd_asap_update',
MIXER_MODE_CHANGED : 'mixer_mode_changed'
};
context.JK.ALERT_NAMES = {
@ -284,4 +285,14 @@
showASIO: false
}
}
context.JK.MIX_MODES = {
MASTER: true,
PERSONAL: false
}
/** NAMED_MESSAGES means messages that we show to the user (dialogs/banners/whatever), that we have formally named */
context.JK.NAMED_MESSAGES = {
MASTER_VS_PERSONAL_MIX : 'master_vs_personal_mix'
}
})(window,jQuery);

View File

@ -0,0 +1,35 @@
#
# Common utility functions.
#
$ = jQuery
context = window
context.JK ||= {};
class ModUtils
constructor: () ->
@logger = context.JK.logger
init: () =>
# creates a new show structure suitable for applying to a user update
noShow: (noShowName) =>
noShowValue = {}
noShowValue[noShowName] = true
{no_show: noShowValue}
updateNoShow: (noShowName) =>
context.JK.app.updateUserModel({mods: this.noShow(noShowName)})
# returns a deferred, so use .done
shouldShow: (noShowName) =>
deferred = new $.Deferred();
context.JK.app.user()
.done((user) => (
noShows = user.mods?.no_show
shouldShowForName = if noShows? then !noShows[noShowName] else true
deferred.resolve(shouldShowForName)
))
return deferred;
# global instance
context.JK.ModUtils = new ModUtils()

View File

@ -5,8 +5,11 @@
context.JK = context.JK || {};
context.JK.SessionScreen = function(app) {
var EVENTS = context.JK.EVENTS;
var MIX_MODES = context.JK.MIX_MODES;
var NAMED_MESSAGES = context.JK.NAMED_MESSAGES;
var gearUtils = context.JK.GearUtils;
var sessionUtils = context.JK.SessionUtils;
var modUtils = context.JK.ModUtils;
var logger = context.JK.logger;
var self = this;
var sessionModel = null;
@ -38,8 +41,10 @@
var sessionPageDone = null;
var $recordingManagerViewer = null;
var $screen = null;
var rest = context.JK.Rest();
var $mixModeDropdown = null;
var $templateMixerModeChange = null;
var rest = context.JK.Rest();
var RENDER_SESSION_DELAY = 750; // When I need to render a session, I have to wait a bit for the mixers to be there.
var defaultParticipant = {
@ -490,14 +495,35 @@
var mixerIds = context.jamClient.SessionGetIDs();
var holder = $.extend(true, {}, {mixers: context.jamClient.SessionGetControlState(mixerIds)});
mixers = holder.mixers;
console.log("mixers", mixers)
// grab the first mixer, and check the mode
var newMixerMode;;
if(mixers.length > 0) {
newMixerMode = mixers[0]["mode"]
if(newMixerMode === undefined) {
logger.error("mixer mode not present. defaulting to personal")
newMixerMode = MIX_MODES.PERSONAL;
}
}
else {
logger.error("no mixers present. defaulting mixer mode to personal")
newMixerMode = MIX_MODES.PERSONAL;
}
sessionModel.setMixerMode(newMixerMode)
// Always add a hard-coded simplified 'mixer' for the L2M mix
/**
var l2m_mixer = {
id: '__L2M__',
range_low: -80,
range_high: 20,
volume_left: context.jamClient.SessionGetMasterLocalMix()
};
mixers.push(l2m_mixer);
id: '__L2M__',
range_low: -80,
range_high: 20,
volume_left: context.jamClient.SessionGetMasterLocalMix()
};
mixers.push(l2m_mixer);*/
}
function _mixersForGroupId(groupId) {
@ -558,13 +584,15 @@
var gainPercent = 0;
var mixerIds = [];
$.each(mixers, function(index, mixer) {
if (mixer.group_id === ChannelGroupIds.MasterGroup) {
if (sessionModel.isMasterMixMode() && mixer.group_id === ChannelGroupIds.MasterGroup) {
mixerIds.push(mixer.id);
gainPercent = percentFromMixerValue(
mixer.range_low, mixer.range_high, mixer.volume_left);
}
if (mixer.group_id === ChannelGroupIds.MonitorGroup) {
else if (!sessionModel.isMasterMixMode() && mixer.group_id === ChannelGroupIds.MonitorGroup) {
mixerIds.push(mixer.id);
gainPercent = percentFromMixerValue(
mixer.range_low, mixer.range_high, mixer.volume_left);
}
});
var faderId = mixerIds.join(',');
@ -808,13 +836,28 @@
// With multiple tracks, there will be more than one mixer for a
// particular client, in a particular group, and I'll need to further
// identify by track id or something similar.
var mixer = _mixerForClientId(
var mixer = null;
if(sessionModel.isMasterMixMode()) {
mixer = _mixerForClientId(
participant.client_id,
[
ChannelGroupIds.AudioInputMusicGroup,
ChannelGroupIds.PeerAudioInputMusicGroup
ChannelGroupIds.AudioInputMusicGroup,
ChannelGroupIds.PeerAudioInputMusicGroup
],
usedMixers);
}
else {
// don't pass in used mixers; we need to associate multiple tracks with the same mixer
mixer = _mixerForClientId(
participant.client_id,
[
ChannelGroupIds.AudioInputMusicGroup,
ChannelGroupIds.UserMusicInputGroup
],
{});
}
if (mixer) {
usedMixers[mixer.id] = true;
myTrack = (mixer.group_id === ChannelGroupIds.AudioInputMusicGroup);
@ -829,6 +872,7 @@
trackData.muteClass = muteClass;
trackData.mixerId = mixer.id;
trackData.noaudio = false;
trackData.group_id = mixer.group_id;
context.jamClient.SessionSetUserName(participant.client_id,name);
} else { // No mixer to match, yet
@ -856,20 +900,20 @@
addNewGearDialog = new context.JK.AddNewGearDialog(app, self);
}
function connectTrackToMixer(trackSelector, clientId, mixerId, gainPercent) {
function connectTrackToMixer(trackSelector, clientId, mixerId, gainPercent, groupId) {
var vuOpts = $.extend({}, trackVuOpts);
var faderOpts = $.extend({}, trackFaderOpts);
faderOpts.faderId = mixerId;
var vuLeftSelector = trackSelector + " .track-vu-left";
var vuRightSelector = trackSelector + " .track-vu-right";
var faderSelector = trackSelector + " .track-gain";
var $fader = $(faderSelector).attr('mixer-id', mixerId);
var $fader = $(faderSelector).attr('mixer-id', mixerId).data('groupId', groupId)
var $track = $(trackSelector);
// Set mixer-id attributes and render VU/Fader
context.JK.VuHelpers.renderVU(vuLeftSelector, vuOpts);
$track.find('.track-vu-left').attr('mixer-id', mixerId + '_vul');
$track.find('.track-vu-left').attr('mixer-id', mixerId + '_vul').data('groupId', groupId)
context.JK.VuHelpers.renderVU(vuRightSelector, vuOpts);
$track.find('.track-vu-right').attr('mixer-id', mixerId + '_vur');
$track.find('.track-vu-right').attr('mixer-id', mixerId + '_vur').data('groupId', groupId)
context.JK.FaderHelpers.renderFader($fader, faderOpts);
// Set gain position
context.JK.FaderHelpers.setFaderValue(mixerId, gainPercent);
@ -887,13 +931,26 @@
var keysToDelete = [];
for (var key in lookingForMixers) {
var clientId = lookingForMixers[key];
var mixer = _mixerForClientId(
var mixer = null;
if(sessionModel.isMasterMixMode()) {
mixer = _mixerForClientId(
clientId,
[
ChannelGroupIds.AudioInputMusicGroup,
ChannelGroupIds.PeerAudioInputMusicGroup
ChannelGroupIds.AudioInputMusicGroup,
ChannelGroupIds.PeerAudioInputMusicGroup
],
usedMixers);
}
else {
// don't pass in used mixers; we need to associate multiple tracks with the same mixer
mixer = _mixerForClientId(
clientId,
[
ChannelGroupIds.AudioInputMusicGroup,
ChannelGroupIds.UserMusicInputGroup
],
{});
}
if (mixer) {
var participant = (sessionModel.getParticipant(clientId) || {name:'unknown'}).name;
logger.debug("found mixer=" + mixer.id + ", participant=" + participant)
@ -903,7 +960,7 @@
mixer.range_low, mixer.range_high, mixer.volume_left);
var trackSelector = 'div.track[track-id="' + key + '"]';
connectTrackToMixer(trackSelector, key, mixer.id, gainPercent);
connectTrackToMixer(trackSelector, key, mixer.id, gainPercent, mixer.group_id);
var $track = $('div.track[client-id="' + clientId + '"]');
$track.find('.track-icon-mute').attr('mixer-id', mixer.id);
// hide overlay for all tracks associated with this client id (if one mixer is present, then all tracks are valid)
@ -983,7 +1040,7 @@
// Render VU meters and gain fader
var trackSelector = parentSelector + ' .session-track[track-id="' + trackData.trackId + '"]';
var gainPercent = trackData.gainPercent || 0;
connectTrackToMixer(trackSelector, trackData.clientId, trackData.mixerId, gainPercent);
connectTrackToMixer(trackSelector, trackData.clientId, trackData.mixerId, gainPercent, trackData.group_id);
var $closeButton = $('#div-track-close', 'div[track-id="' + trackData.trackId + '"]');
if (!allowDelete) {
@ -1015,7 +1072,7 @@
// Render VU meters and gain fader
var trackSelector = parentSelector + ' .session-track[track-id="' + trackData.trackId + '"]';
var gainPercent = trackData.gainPercent || 0;
connectTrackToMixer(trackSelector, trackData.clientId, trackData.mixerId, gainPercent);
connectTrackToMixer(trackSelector, trackData.clientId, trackData.mixerId, gainPercent, null);
tracks[trackData.trackId] = new context.JK.SessionTrack(trackData.clientId);
}
@ -1028,11 +1085,17 @@
function faderChanged(e, data) {
var $target = $(this);
var faderId = $target.attr('mixer-id');
var groupId = $target.data('groupId');
var mixerIds = faderId.split(',');
$.each(mixerIds, function(i,v) {
var broadcast = !(data.dragging); // If fader is still dragging, don't broadcast
fillTrackVolumeObject(v, broadcast);
setMixerVolume(v, data.percentage);
if(groupId == ChannelGroupIds.UserMusicInputGroup) {
// there may be other mixers with this same ID in the case of a Peer Music Stream, so update them as well
context.JK.FaderHelpers.setFaderValue(v, data.percentage);
}
});
}
@ -1437,6 +1500,28 @@
sessionId);
inviteMusiciansUtil.loadFriends();
$(friendInput).show();
}
function onMixerModeChanged(e, data)
{
$mixModeDropdown.easyDropDown('select', data.mode, true);
setTimeout(renderSession, 1);
}
function onUserChangeMixMode(e) {
var mode = $mixModeDropdown.val() == "master" ? MIX_MODES.MASTER : MIX_MODES.PERSONAL;
context.jamClient.SetMixerMode(mode)
modUtils.shouldShow(NAMED_MESSAGES.MASTER_VS_PERSONAL_MIX).done(function(shouldShow) {
if(shouldShow) {
var modeChangeHtml = $($templateMixerModeChange.html());
context.JK.Banner.show({title: 'Master vs. Personal Mix', text: modeChangeHtml, no_show: NAMED_MESSAGES.MASTER_VS_PERSONAL_MIX});
}
})
return true;
}
function events() {
@ -1460,6 +1545,8 @@
.on('play', onPlay)
.on('change-position', onChangePlayPosition);
$(friendInput).focus(function() { $(this).val(''); })
$(document).on(EVENTS.MIXER_MODE_CHANGED, onMixerModeChanged)
$mixModeDropdown.change(onUserChangeMixMode)
}
this.initialize = function(localRecordingsDialogInstance, recordingFinishedDialogInstance, friendSelectorDialog) {
@ -1470,7 +1557,6 @@
context.jamClient.SetVURefreshRate(150);
context.jamClient.RegisterVolChangeCallBack("JK.HandleVolumeChangeCallback");
playbackControls = new context.JK.PlaybackControls($('.session-recordings .recording-controls'));
events();
var screenBindings = {
'beforeShow': beforeShow,
@ -1483,7 +1569,12 @@
$recordingManagerViewer = $('#recording-manager-viewer');
$screen = $('#session-screen');
// make sure no previous plays are still going on by accident
$mixModeDropdown = $screen.find('select.monitor-mode')
$templateMixerModeChange = $('#template-mixer-mode-change');
events();
// make sure no previous plays are still going on by accident
context.jamClient.SessionStopPlay();
if(context.jamClient.SessionRemoveAllPlayTracks) {
// upgrade guard

View File

@ -11,6 +11,7 @@
context.JK.SessionModel = function(app, server, client, sessionScreen) {
var ALERT_TYPES = context.JK.ALERT_TYPES;
var EVENTS = context.JK.EVENTS;
var MIX_MODES = context.JK.MIX_MODES;
var userTracks = null; // comes from the backend
var clientId = client.clientID;
@ -33,6 +34,8 @@
var startTime = null;
var joinDeferred = null;
var mixerMode = MIX_MODES.GLOBAL;
server.registerOnSocketClosed(onWebsocketDisconnected);
function id() {
@ -93,6 +96,13 @@
return inSession;
}
function onMixerModeChanged(newMixerMode)
{
mixerMode = newMixerMode;
var mode = newMixerMode == MIX_MODES.MASTER ? "master" : "personal";
logger.debug("onMixerModeChanged:" + mode);
$(document).triggerHandler(EVENTS.MIXER_MODE_CHANGED, {mode:mode});
}
function waitForSessionPageEnterDone() {
sessionPageEnterDeferred = $.Deferred();
@ -277,6 +287,7 @@
userTracks = null;
startTime = null;
joinDeferred = null;
mixerMode = MIX_MODES.PERSONAL;
if(fullyJoined) {
$(document).trigger(EVENTS.SESSION_ENDED, {session: {id: currentSessionId}});
}
@ -485,6 +496,16 @@
});
}
function setMixerMode(newMixerMode) {
if(mixerMode != newMixerMode) {
onMixerModeChanged(newMixerMode);
}
}
function isMasterMixMode() {
return mixerMode == MIX_MODES.MASTER;
}
function onWebsocketDisconnected(in_error) {
// kill the streaming of the session immediately
if(currentSessionId) {
@ -616,6 +637,12 @@
else if(inSession() && (text == 'RebuildMediaControl' || text == 'RebuildRemoteUserControl')) {
refreshCurrentSession(true);
}
else if(inSession() && (text == 'Global Peer Input Mixer Mode')) {
setMixerMode(MIX_MODES.MASTER);
}
else if(inSession() && (text == 'Local Peer Stream Mixer Mode')) {
setMixerMode(MIX_MODES.PERSONAL);
}
}
// Public interface
@ -635,6 +662,8 @@
this.recordingModel = recordingModel;
this.findUserBy = findUserBy;
this.inSession = inSession;
this.setMixerMode = setMixerMode;
this.isMasterMixMode = isMasterMixMode;
// ALERT HANDLERS
this.onBackendMixerChanged = onBackendMixerChanged;

View File

@ -12,7 +12,6 @@
padding:11px 0px 11px 0px;
background-color:#4c4c4c;
min-height:20px;
overflow-x:hidden;
position:relative;
}
@ -69,6 +68,11 @@
.playback-mode-buttons {
display:none;
}
.monitor-mode-holder .easydropdown-wrapper{
top:-7px;
left:5px;
}
}

View File

@ -8,6 +8,11 @@
height:240px;
}
.dialog-inner {
padding-bottom:0;
margin-bottom:25px;
}
h2 {
font-weight:bold;
font-size:x-large;
@ -21,12 +26,40 @@
.buttons {
margin:0 20px 20px 0;
position:relative;
&.center {
text-align:center;
float:none;
}
}
.close-btn {
display:none;
}
.no-more-show {
display:none;
position:absolute;
margin-left:50px;
left:50%;
top:-4px;
color:rgb(170, 170, 170);
span {
font-size:15px;
}
.icheckbox_minimal {
display:inline-block;
position:relative;
top:3px;
margin-right:3px;
}
}
ul {
list-style:disc;
margin-left:20px;
@ -40,6 +73,10 @@
margin: 15px 12px 15px 36px;
}
.definition {
font-weight:bold;
}
.end-content {
height: 0;
line-height: 0;

View File

@ -46,6 +46,7 @@ class ApiUsersController < ApiController
@user.show_whats_next = params[:show_whats_next] if params.has_key?(:show_whats_next)
@user.subscribe_email = params[:subscribe_email] if params.has_key?(:subscribe_email)
@user.biography = params[:biography] if params.has_key?(:biography)
@user.mod_merge(params[:mods]) if params[:mods]
# allow keyword of 'LATEST' to mean set the notification_seen_at to the most recent notification for this user
if params.has_key?(:notification_seen_at)

View File

@ -17,6 +17,10 @@ if @user == current_user
geoiplocation.info if geoiplocation
end
node :mods do |user|
user.mods_json
end
elsif current_user
node :is_friend do |uu|
current_user.friends?(@user)

View File

@ -30,12 +30,22 @@
</div>
<!-- Mix: Me versus Others -->
<div class="block monitor-mode-holder">
<div class="label">MIX:</div>
<select class="monitor-mode easydropdown">
<option value="personal" class="label">Personal</option>
<option value="master">Master</option>
</select>
</div>
<!--
<div class="block">
<div class="label">MONITOR:</div>
<div class="label"><small>others</small></div>
<div id="l2m" class="fader flat" mixer-id="__L2M__"></div>
<div class="label"><small>me</small></div>
</div>
-->
<!-- Leave Button -->
<a class="button-grey right leave" href="/client#/home" id="session-leave">X&nbsp;&nbsp;LEAVE</a>

View File

@ -1,47 +0,0 @@
#banner_overlay.overlay
#banner.dialog-overlay-sm{ 'data-type' => '' }
.content-head
= image_tag("content/icon_alert.png", :height => '24', :width => '24', :class => "content-icon")
%h1
.dialog-inner
%br.end-content{ clear: 'all'}
.right.buttons
%a.button-orange.close-btn CLOSE
%a.button-orange.yes-btn YES
%a.button-grey.no-btn CANCEL
%script{type: 'text/template', id: 'template-app-in-read-only-volume'}
.template-app-in-read-only-volume
%p The JamKazam application is running in a read-only volume. This stops the automatic update feature from working, and may cause other issues because it is not a supported configuration.
%p So let's fix it. Don't worry--it's easy to do--please read on.
%p First, here's almost certainly what happened to cause this problem: after JamKazam.dmg was downloaded, it was then double-clicked and a window opened showing the contents of the dmg. The JamKazam application icon was double-clicked inside that opened window. Unfortunately, that isn't OK.
%p Instead, do this to move JamKazam to a good location, and run it from there:
%ol
%li.download-dmg
Download the latest mac installer from the
%a{href:"/downloads", rel: 'external'}Downloads
page.
%br
%em (the download will have a filename ending in .dmg)
%li Double-click the downloaded dmg file to open it.
%li In the resulting screen, drag the JamKazam icon to the Applications folder. It will show a progress bar as it copies.
%li Double-click the Applications folder to go into the folder.
%li If you are still running the JamKazam application at this point, you will need to stop it before executing the next step.
%li Find the JamKazam application in the Applications folder, and double-click the icon to launch it!
%script{type: 'text/template', id: 'template-shutdown-prompt'}
.shutdown-prompt
We strongly recommend that you leave the JamKazam application running in the background.
This is a very lightweight app that will not disrupt your use of your computer and other applications, and leaving this app running will deliver the following benefits to you:
%ul
%li
%span.definition Scoring Service
= '- If you leave the app running, there is a service that can check your Internet latency to other JamKazam users. This is critical data that will guide you on which musicians and which sessions will offer a good online play experience.'
%li
%span.definition Recordings
= '- If you leave the app running, any recordings that you and others have made during sessions can be mastered - i.e. uploaded, mixed on our servers, and downloaded back to your computer - so that you have high quality versions of your recordings available.'
Please consider leaving this lightweight app running in the background for your own benefit, thanks!

View File

@ -0,0 +1,68 @@
#banner_overlay.overlay
#banner.dialog-overlay-sm data-type=''
.content-head
= image_tag("content/icon_alert.png", :height => '24', :width => '24', :class => "content-icon")
h1
.dialog-inner
br.end-content clear='all'
.right.buttons
a.button-orange.close-btn CLOSE
.no-more-show
input.no-more-show-checkbox type="checkbox"
span Don't show this again
a.button-orange.yes-btn YES
a.button-grey.no-btn CANCEL
script type='text/template' id='template-app-in-read-only-volume'
.template-app-in-read-only-volume
p The JamKazam application is running in a read-only volume. This stops the automatic update feature from working, and may cause other issues because it is not a supported configuration.
p So let's fix it. Don't worry--it's easy to do--please read on.
p First, here's almost certainly what happened to cause this problem: after JamKazam.dmg was downloaded, it was then double-clicked and a window opened showing the contents of the dmg. The JamKazam application icon was double-clicked inside that opened window. Unfortunately, that isn't OK.
p Instead, do this to move JamKazam to a good location, and run it from there:
ol
li.download-dmg
| Download the latest mac installer from the
a href="/downloads" rel='external' Downloads
| page.
br
em
| (the download will have a filename ending in .dmg)
li Double-click the downloaded dmg file to open it.
li In the resulting screen, drag the JamKazam icon to the Applications folder. It will show a progress bar as it copies.
li Double-click the Applications folder to go into the folder.
li If you are still running the JamKazam application at this point, you will need to stop it before executing the next step.
li Find the JamKazam application in the Applications folder, and double-click the icon to launch it!
script type='text/template' id='template-shutdown-prompt'
.shutdown-prompt
| We strongly recommend that you leave the JamKazam application running in the background.
| This is a very lightweight app that will not disrupt your use of your computer and other applications, and leaving this app running will deliver the following benefits to you:
ul
li
span.definition Scoring Service
= '- If you leave the app running, there is a service that can check your Internet latency to other JamKazam users. This is critical data that will guide you on which musicians and which sessions will offer a good online play experience.'
li
span.definition Recordings
= '- If you leave the app running, any recordings that you and others have made during sessions can be mastered - i.e. uploaded, mixed on our servers, and downloaded back to your computer - so that you have high quality versions of your recordings available.'
| Please consider leaving this lightweight app running in the background for your own benefit, thanks!
script type='text/template' id='template-mixer-mode-change'
.mixer-mode-change
| JamKazam gives you control over both a personal and a master mix in each session:
ul
li
span.definition Master Mix
div The master mix controls the audio mix that will be used for any recordings you make while in sessions, and also the audio mix that will be broadcast to fans listening to your live session performances. With master mix selected, when you adjust the faders on the session screen up or down, it changes the master mix for all musicians and tracks in the session globally.
li
span.definition Personal Mix
div The personal mix controls the audio mix that you individually hear while playing in the session, and you can customize this mix to hear more or less of the music stream from each other musician playing in the session. This does not affect the master mix used for recordings or broadcasts. With personal mix selected, when you adjust the faders on the session screen up or down, it changes the personal mix only for you locally.
br
div
| For more detailed information on this topic, read our knowledge base article on&nbsp;
a rel="external" href="https://jamkazam.desk.com/customer/portal/articles/1757233-using-personal-vs-master-mix-controls" Configuring Master and Personal Mixes in a Session
| .
br

View File

@ -3,7 +3,7 @@ require 'spec_helper'
describe ApiUsersController do
render_views
let(:user) { FactoryGirl.create(:user) }
let (:user) { FactoryGirl.create(:user) }
let (:conn) { FactoryGirl.create(:connection, user: user, last_jam_audio_latency: 5) }
@ -11,6 +11,29 @@ describe ApiUsersController do
controller.current_user = user
end
describe "update mod" do
it "empty mod" do
post :update, id:user.id, mods: {}, :format=>'json'
response.should be_success
user.mods_json.should == {}
end
it "no_show mod" do
user_id = user.id
mods = {"no_show" => {"something1" => true}}
post :update, id:user.id, mods: mods, :format=>'json'
response.should be_success
# verify that the user object has the mods data
user_again = User.find(user_id)
user_again.mods_json.should == mods
# verify that the response shows the mods structure
json = JSON.parse(response.body)
json["mods"].should == mods
end
end
describe "audio_latency" do
it "updates both connection and user" do