diff --git a/ruby/lib/jam_ruby/constants/validation_messages.rb b/ruby/lib/jam_ruby/constants/validation_messages.rb index 724eb5680..ea914a4c9 100644 --- a/ruby/lib/jam_ruby/constants/validation_messages.rb +++ b/ruby/lib/jam_ruby/constants/validation_messages.rb @@ -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) diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index a9c278566..af931a0ac 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -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 diff --git a/ruby/spec/jam_ruby/models/user_spec.rb b/ruby/spec/jam_ruby/models/user_spec.rb index 29a39fa48..36425e495 100644 --- a/ruby/spec/jam_ruby/models/user_spec.rb +++ b/ruby/spec/jam_ruby/models/user_spec.rb @@ -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 diff --git a/web/app/assets/javascripts/dialog/banner.js b/web/app/assets/javascripts/dialog/banner.js index b34134129..991d14d67 100644 --- a/web/app/assets/javascripts/dialog/banner.js +++ b/web/app/assets/javascripts/dialog/banner.js @@ -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; } diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index 294580721..899ca4274 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -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; diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js index eb6fd0b27..0e6e05e99 100644 --- a/web/app/assets/javascripts/globals.js +++ b/web/app/assets/javascripts/globals.js @@ -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); \ No newline at end of file diff --git a/web/app/assets/javascripts/mods_utils.js.coffee b/web/app/assets/javascripts/mods_utils.js.coffee new file mode 100644 index 000000000..b10250c32 --- /dev/null +++ b/web/app/assets/javascripts/mods_utils.js.coffee @@ -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() \ No newline at end of file diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index 9a8e1dd6a..8e3a2a4a6 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -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 diff --git a/web/app/assets/javascripts/sessionModel.js b/web/app/assets/javascripts/sessionModel.js index 60d8862b8..719fc650f 100644 --- a/web/app/assets/javascripts/sessionModel.js +++ b/web/app/assets/javascripts/sessionModel.js @@ -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; diff --git a/web/app/assets/stylesheets/client/session.css.scss b/web/app/assets/stylesheets/client/session.css.scss index 0a7e00e86..2fb6c24b2 100644 --- a/web/app/assets/stylesheets/client/session.css.scss +++ b/web/app/assets/stylesheets/client/session.css.scss @@ -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; + } } diff --git a/web/app/assets/stylesheets/dialogs/banner.css.scss b/web/app/assets/stylesheets/dialogs/banner.css.scss index fbad88349..7a84be027 100644 --- a/web/app/assets/stylesheets/dialogs/banner.css.scss +++ b/web/app/assets/stylesheets/dialogs/banner.css.scss @@ -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; diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index 06d9d6b03..d9b473307 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -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) diff --git a/web/app/views/api_users/show.rabl b/web/app/views/api_users/show.rabl index fd8b9b87e..59d72f304 100644 --- a/web/app/views/api_users/show.rabl +++ b/web/app/views/api_users/show.rabl @@ -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) diff --git a/web/app/views/clients/_session.html.erb b/web/app/views/clients/_session.html.erb index 6865d987f..dc0f6a81e 100644 --- a/web/app/views/clients/_session.html.erb +++ b/web/app/views/clients/_session.html.erb @@ -30,12 +30,22 @@ + +
+
MIX:
+ +
+ X  LEAVE diff --git a/web/app/views/dialogs/_banner.html.haml b/web/app/views/dialogs/_banner.html.haml deleted file mode 100644 index 4e8b81562..000000000 --- a/web/app/views/dialogs/_banner.html.haml +++ /dev/null @@ -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! diff --git a/web/app/views/dialogs/_banner.html.slim b/web/app/views/dialogs/_banner.html.slim new file mode 100644 index 000000000..d79584383 --- /dev/null +++ b/web/app/views/dialogs/_banner.html.slim @@ -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  + 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 \ No newline at end of file diff --git a/web/spec/controllers/api_users_controller_spec.rb b/web/spec/controllers/api_users_controller_spec.rb index 3ee0c43d5..b66d3c3ea 100644 --- a/web/spec/controllers/api_users_controller_spec.rb +++ b/web/spec/controllers/api_users_controller_spec.rb @@ -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