diff --git a/db/manifest b/db/manifest index 36e365013..5bad3d1e0 100755 --- a/db/manifest +++ b/db/manifest @@ -182,3 +182,4 @@ rsvp_slots_prof_level.sql add_file_name_music_notation.sql change_scheduled_start_music_session.sql music_sessions_iso_639_3.sql +discardable_claimed_recordings.sql diff --git a/db/up/discardable_claimed_recordings.sql b/db/up/discardable_claimed_recordings.sql new file mode 100644 index 000000000..d082ff770 --- /dev/null +++ b/db/up/discardable_claimed_recordings.sql @@ -0,0 +1 @@ +ALTER TABLE claimed_recordings ADD COLUMN discarded BOOLEAN DEFAULT FALSE; \ No newline at end of file diff --git a/ruby/lib/jam_ruby/lib/nav.rb b/ruby/lib/jam_ruby/lib/nav.rb index 56a7a09af..1bce0f133 100644 --- a/ruby/lib/jam_ruby/lib/nav.rb +++ b/ruby/lib/jam_ruby/lib/nav.rb @@ -1,9 +1,18 @@ module JamRuby + # To use this, be sure to add this in before block: stub_const("APP_CONFIG", web_config) class Nav def self.home(options ={}) - "#{APP_CONFIG.external_root_url}/client#/home#{dialog(options)}" + "#{base_url}/home#{dialog(options)}" + end + + def self.profile(user) + "#{base_url}/profile/#{user.id}" + end + + def self.feed + "#{base_url}/feed" end def self.accept_friend_request_dialog(friend_request_id) @@ -12,6 +21,10 @@ module JamRuby private + def self.base_url + "#{APP_CONFIG.external_root_url}/client#" + end + def self.dialog(options) dialog = '' if options[:dialog] diff --git a/ruby/lib/jam_ruby/models/active_music_session.rb b/ruby/lib/jam_ruby/models/active_music_session.rb index 9d0de3493..a4bf834ce 100644 --- a/ruby/lib/jam_ruby/models/active_music_session.rb +++ b/ruby/lib/jam_ruby/models/active_music_session.rb @@ -334,6 +334,7 @@ module JamRuby # same, sorted by score. date seems irrelevant as these are active sessions. ams_init must be called # first. def self.ams_query(current_user, options = {}) + session_id = options[:session_id] client_id = options[:client_id] genre = options[:genre] lang = options[:lang] @@ -392,6 +393,7 @@ module JamRuby query = query.limit(limit) query = query.where("music_sessions.genre_id = ?", genre) unless genre.blank? query = query.where('music_sessions.language = ?', lang) unless lang.blank? + query = query.where('music_sessions.id = ?', session_id) unless session_id.blank? query = query.where("(description_tsv @@ to_tsquery('jamenglish', ?))", keyword + ':*') unless keyword.blank? if !day.blank? && !timezone_offset.blank? @@ -399,6 +401,9 @@ module JamRuby day = Date.parse(day) next_day = day + 1 timezone_offset = timezone_offset.to_i + if timezone_offset > 0 + timezone_offset = "+#{timezone_offset}" + end query = query.where("scheduled_start BETWEEN TIMESTAMP WITH TIME ZONE '#{day} 00:00:00#{timezone_offset}' AND TIMESTAMP WITH TIME ZONE '#{next_day} 00:00:00#{timezone_offset}'") rescue Exception => e @@ -678,6 +683,10 @@ module JamRuby music_session.approval_required end + def music_notations + music_session.music_notations + end + def tick_track_changes self.track_changes_counter += 1 self.save!(:validate => false) diff --git a/ruby/lib/jam_ruby/models/claimed_recording.rb b/ruby/lib/jam_ruby/models/claimed_recording.rb index 573d5a390..d984d6a3c 100644 --- a/ruby/lib/jam_ruby/models/claimed_recording.rb +++ b/ruby/lib/jam_ruby/models/claimed_recording.rb @@ -8,9 +8,9 @@ module JamRuby belongs_to :genre, :class_name => "JamRuby::Genre" has_many :recorded_tracks, :through => :recording, :class_name => "JamRuby::RecordedTrack" has_many :playing_sessions, :class_name => "JamRuby::ActiveMusicSession" - has_many :likes, :class_name => "JamRuby::RecordingLiker", :foreign_key => "claimed_recording_id" + has_many :likes, :class_name => "JamRuby::RecordingLiker", :foreign_key => "claimed_recording_id", :dependent => :destroy has_many :plays, :class_name => "JamRuby::PlayablePlay", :foreign_key => "claimed_recording_id", :dependent => :destroy - has_one :share_token, :class_name => "JamRuby::ShareToken", :inverse_of => :shareable, :foreign_key => 'shareable_id' + has_one :share_token, :class_name => "JamRuby::ShareToken", :inverse_of => :shareable, :foreign_key => 'shareable_id', :dependent => :destroy validates :name, no_profanity: true, length: {minimum: 3, maximum: 64}, presence: true validates :description, no_profanity: true, length: {maximum: 8000} @@ -39,9 +39,9 @@ module JamRuby raise PermissionError, "user doesn't own claimed_recording" end - self.name = params[:name] unless params[:name].nil? - self.description = params[:description] unless params[:description].nil? - self.genre = Genre.find(params[:genre]) unless params[:genre].nil? + self.name = params[:name] + self.description = params[:description] + self.genre = Genre.find_by_id(params[:genre]) unless params[:genre].nil? self.is_public = params[:is_public] unless params[:is_public].nil? save end @@ -50,13 +50,10 @@ module JamRuby if user != self.user raise PermissionError, "user doesn't own claimed_recording" end - - # If this is the only copy, destroy the entire recording. Otherwise, just destroy this claimed_recording - if recording.claimed_recordings.count == 1 - recording.destroy - else - self.destroy - end + + ClaimedRecording.where(:id => id).update_all(:discarded => true ) + + recording.discard(user) end diff --git a/ruby/lib/jam_ruby/models/country.rb b/ruby/lib/jam_ruby/models/country.rb index ca64b222c..0cf5486a6 100644 --- a/ruby/lib/jam_ruby/models/country.rb +++ b/ruby/lib/jam_ruby/models/country.rb @@ -8,46 +8,16 @@ module JamRuby end def self.import_from_iso3166(file) - - # File iso3166.csv - # Format: - # countrycode,countryname - - # what this does is not replace the contents of the table, but rather update the specified rows with the names. - # any rows not specified have the countryname reset to be the same as the countrycode. - - self.transaction do - self.connection.execute "update #{self.table_name} set countryname = countrycode" - - File.open(file, 'r:ISO-8859-1') do |io| - saved_level = ActiveRecord::Base.logger ? ActiveRecord::Base.logger.level : 0 - count = 0 - - ncols = 2 - - csv = ::CSV.new(io, {encoding: 'ISO-8859-1', headers: false}) - csv.each do |row| - raise "file does not have expected number of columns (#{ncols}): #{row.length}" unless row.length == ncols - - countrycode = row[0] - countryname = row[1] - - stmt = "UPDATE #{self.table_name} SET countryname = #{MaxMindIsp.quote_value(countryname)} WHERE countrycode = #{MaxMindIsp.quote_value(countrycode)}" - self.connection.execute stmt - count += 1 - - if ActiveRecord::Base.logger and ActiveRecord::Base.logger.level < Logger::INFO - ActiveRecord::Base.logger.debug "... logging updates to #{self.table_name} suspended ..." - ActiveRecord::Base.logger.level = Logger::INFO - end - end - - if ActiveRecord::Base.logger - ActiveRecord::Base.logger.level = saved_level - ActiveRecord::Base.logger.debug "updated #{count} records in #{self.table_name}" - end - end # file - end # transaction + self.delete_all + File.open(file, 'r:ISO-8859-1') do |io| + csv = ::CSV.new(io, {encoding: 'ISO-8859-1', headers: false}) + csv.each do |row| + cc = self.new + cc.countrycode = row[0] + cc.countryname = row[1] + cc.save + end + end # file end end end diff --git a/ruby/lib/jam_ruby/models/feed.rb b/ruby/lib/jam_ruby/models/feed.rb index 7c118d364..0802e3085 100644 --- a/ruby/lib/jam_ruby/models/feed.rb +++ b/ruby/lib/jam_ruby/models/feed.rb @@ -43,6 +43,7 @@ module JamRuby query = Feed.joins("LEFT OUTER JOIN recordings ON recordings.id = feeds.recording_id") .joins("LEFT OUTER JOIN music_sessions ON music_sessions.id = feeds.music_session_id") .limit(limit) + .where('recordings is NULL OR recordings.all_discarded = false') # remove any 'all_discarded recordings from the search results' # handle sort if sort == 'date' @@ -72,14 +73,14 @@ module JamRuby end - if target_user + if target_user if target_user != user.id require_public_recordings = "claimed_recordings.is_public = TRUE AND" require_public_sessions = "music_sessions.fan_access = TRUE AND" end - query = query.joins("LEFT OUTER JOIN claimed_recordings ON recordings.id = claimed_recordings.recording_id AND #{require_public_recordings} (claimed_recordings.user_id = '#{target_user}' OR (recordings.band_id IN (SELECT band_id FROM bands_musicians where user_id='#{target_user}')))") + query = query.joins("LEFT OUTER JOIN claimed_recordings ON recordings.id = claimed_recordings.recording_id AND claimed_recordings.discarded = FALSE AND #{require_public_recordings} (claimed_recordings.user_id = '#{target_user}' OR (recordings.band_id IN (SELECT band_id FROM bands_musicians where user_id='#{target_user}')))") query = query.joins("LEFT OUTER JOIN music_sessions_user_history ON music_sessions.id = music_sessions_user_history.music_session_id AND #{require_public_sessions} music_sessions_user_history.user_id = '#{target_user}'") query = query.group("feeds.id, feeds.recording_id, feeds.music_session_id, feeds.created_at, feeds.updated_at, recordings.id, music_sessions.id") if sort == 'plays' @@ -97,7 +98,7 @@ module JamRuby require_public_sessions = "music_sessions.fan_access = TRUE AND" end - query = query.joins("LEFT OUTER JOIN claimed_recordings ON recordings.id = claimed_recordings.recording_id AND #{require_public_recordings} recordings.band_id = '#{target_band}'") + query = query.joins("LEFT OUTER JOIN claimed_recordings ON recordings.id = claimed_recordings.recording_id AND claimed_recordings.discarded = FALSE AND #{require_public_recordings} recordings.band_id = '#{target_band}'") query = query.where("music_sessions IS NULL OR #{require_public_sessions} music_sessions.band_id = '#{target_band}'") query = query.group("feeds.id, feeds.recording_id, feeds.music_session_id, feeds.created_at, feeds.updated_at, recordings.id, music_sessions.id") if sort == 'plays' @@ -108,7 +109,7 @@ module JamRuby query = query.where('recordings.id is NULL OR claimed_recordings.id IS NOT NULL') #query = query.where('music_sessions.id is NULL OR music_sessions_user_history.id IS NOT NULL') else - query = query.joins('LEFT OUTER JOIN claimed_recordings ON recordings.id = claimed_recordings.recording_id AND claimed_recordings.is_public = TRUE') + query = query.joins('LEFT OUTER JOIN claimed_recordings ON recordings.id = claimed_recordings.recording_id AND claimed_recordings.discarded = FALSE AND claimed_recordings.is_public = TRUE') query = query.joins("LEFT OUTER JOIN music_sessions_user_history ON music_sessions.id = music_sessions_user_history.music_session_id AND music_sessions.fan_access = TRUE") query = query.group("feeds.id, feeds.recording_id, feeds.music_session_id, feeds.created_at, feeds.updated_at, recordings.id, music_sessions.id") if sort == 'plays' diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index 39bb9c932..46c4200b5 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -411,7 +411,7 @@ module JamRuby def language_description if self.language.blank? - self.language = "en" + self.language = "eng" # iso-639-3 end iso639Details = ISO_639.find_by_code(self.language) @@ -605,6 +605,7 @@ module JamRuby # same, sorted by score. date seems irrelevant as these are active sessions. sms_init must be called # first. def self.sms_query(current_user, options = {}) + session_id = options[:session_id] client_id = options[:client_id] genre = options[:genre] lang = options[:lang] @@ -663,6 +664,7 @@ module JamRuby query = query.limit(limit) query = query.where("music_sessions.genre_id = ?", genre) unless genre.blank? query = query.where('music_sessions.language = ?', lang) unless lang.blank? + query = query.where('music_sessions.id = ?', session_id) unless session_id.blank? query = query.where("(description_tsv @@ to_tsquery('jamenglish', ?))", keyword + ':*') unless keyword.blank? if !day.blank? && !timezone_offset.blank? @@ -670,9 +672,7 @@ module JamRuby day = Date.parse(day) next_day = day + 1 timezone_offset = timezone_offset.to_i - if timezone_offset == 0 - timezone_offset = '' # no offset to specify in this case - elsif timezone_offset > 0 + if timezone_offset > 0 timezone_offset = "+#{timezone_offset}" end query = query.where("scheduled_start BETWEEN TIMESTAMP WITH TIME ZONE '#{day} 00:00:00#{timezone_offset}' diff --git a/ruby/lib/jam_ruby/models/recording.rb b/ruby/lib/jam_ruby/models/recording.rb index 143aed8f6..c4d5fc08d 100644 --- a/ruby/lib/jam_ruby/models/recording.rb +++ b/ruby/lib/jam_ruby/models/recording.rb @@ -5,12 +5,12 @@ module JamRuby attr_accessible :owner, :owner_id, :band, :band_id, :recorded_tracks_attributes, :mixes_attributes, :claimed_recordings_attributes, :name, :description, :genre, :is_public, :duration, as: :admin - has_many :claimed_recordings, :class_name => "JamRuby::ClaimedRecording", :inverse_of => :recording, :foreign_key => 'recording_id', :dependent => :destroy has_many :users, :through => :recorded_tracks, :class_name => "JamRuby::User" + has_many :claimed_recordings, :class_name => "JamRuby::ClaimedRecording", :inverse_of => :recording, :foreign_key => 'recording_id', :dependent => :destroy has_many :mixes, :class_name => "JamRuby::Mix", :inverse_of => :recording, :foreign_key => 'recording_id', :dependent => :destroy has_many :recorded_tracks, :class_name => "JamRuby::RecordedTrack", :foreign_key => :recording_id, :dependent => :destroy - has_many :comments, :class_name => "JamRuby::RecordingComment", :foreign_key => "recording_id" - has_many :likes, :class_name => "JamRuby::RecordingLiker", :foreign_key => "recording_id" + has_many :comments, :class_name => "JamRuby::RecordingComment", :foreign_key => "recording_id", :dependent => :destroy + has_many :likes, :class_name => "JamRuby::RecordingLiker", :foreign_key => "recording_id", :dependent => :destroy has_many :plays, :class_name => "JamRuby::PlayablePlay", :as => :playable, :dependent => :destroy has_one :feed, :class_name => "JamRuby::Feed", :inverse_of => :recording, :foreign_key => 'recording_id', :dependent => :destroy @@ -128,7 +128,7 @@ module JamRuby unless self.users.exists?(user) raise PermissionError, "user was not in this session" end - recorded_tracks.where(:user_id=> user.id) + recorded_tracks.where(:user_id => user.id) end def has_access?(user) @@ -211,7 +211,7 @@ module JamRuby # check if all recorded_tracks for this recording are discarded if recorded_tracks.where('discard = false or discard is NULL').length == 0 - self.all_discarded = true + self.all_discarded = true # the feed won't pick this up; also background cleanup will find these and whack them later self.save(:validate => false) end @@ -238,7 +238,7 @@ module JamRuby .order('recorded_tracks.id') .where('recorded_tracks.fully_uploaded = TRUE') .where('recorded_tracks.id > ?', since) - .where('claimed_recordings.user_id = ?', user).limit(limit).each do |recorded_track| + .where('claimed_recordings.user_id = ? AND claimed_recordings.discarded = FALSE', user).limit(limit).each do |recorded_track| downloads.push( { :type => "recorded_track", @@ -258,7 +258,7 @@ module JamRuby .order('mixes.id') .where('mixes.completed_at IS NOT NULL') .where('mixes.id > ?', since) - .where('claimed_recordings.user_id = ?', user) + .where('claimed_recordings.user_id = ? AND claimed_recordings.discarded = FALSE', user) .limit(limit).each do |mix| downloads.push( { diff --git a/ruby/lib/jam_ruby/models/region.rb b/ruby/lib/jam_ruby/models/region.rb index 8a44a4f6d..0aa27ea87 100644 --- a/ruby/lib/jam_ruby/models/region.rb +++ b/ruby/lib/jam_ruby/models/region.rb @@ -8,52 +8,17 @@ module JamRuby end def self.import_from_region_codes(file) - - # File region_codes.csv - # Format: - # countrycode,region,regionname - - # what this does is replace the contents of the table with the new data. - - self.transaction do - self.connection.execute "delete from #{self.table_name}" - - File.open(file, 'r:ISO-8859-1') do |io| - saved_level = ActiveRecord::Base.logger ? ActiveRecord::Base.logger.level : -1 - count = 0 - errors = 0 - ncols = 3 - - csv = ::CSV.new(io, {encoding: 'ISO-8859-1', headers: false}) - csv.each do |row| - raise "file does not have expected number of columns (#{ncols}): #{row.length}" unless row.length == ncols - - countrycode = row[0] - region = row[1] - regionname = row[2] - - if countrycode.length == 2 and region.length == 2 and regionname.length >= 2 and regionname.length <= 64 - - stmt = "INSERT INTO #{self.table_name} (countrycode, region, regionname) VALUES (#{self.connection.quote(countrycode)}, #{self.connection.quote(region)}, #{self.connection.quote(regionname)})" - self.connection.execute stmt - count += 1 - - if ActiveRecord::Base.logger and ActiveRecord::Base.logger.level < Logger::INFO - ActiveRecord::Base.logger.debug "... logging updates to #{self.table_name} suspended ..." - ActiveRecord::Base.logger.level = Logger::INFO - end - else - ActiveRecord::Base.logger.warn("bogus region_codes record '#{countrycode}', '#{region}', '#{regionname}'") if ActiveRecord::Base.logger - errors += 1 - end - end - - if ActiveRecord::Base.logger - ActiveRecord::Base.logger.level = saved_level - ActiveRecord::Base.logger.debug "inserted #{count} records into #{self.table_name}, #{errors} errors" - end - end # file - end # transaction + self.delete_all + File.open(file, 'r:ISO-8859-1') do |io| + csv = ::CSV.new(io, {encoding: 'ISO-8859-1', headers: false}) + csv.each do |row| + rr = Region.new + rr.countrycode = row[0] + rr.region = row[1] + rr.regionname = row[2] + rr.save + end + end end end end diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 0513e8f68..7ee7c6137 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -58,7 +58,7 @@ FactoryGirl.define do legal_terms true genre JamRuby::Genre.first band nil - language 'en' + language 'eng' end diff --git a/ruby/spec/jam_ruby/models/active_music_session_spec.rb b/ruby/spec/jam_ruby/models/active_music_session_spec.rb index 79a0992d6..c7d4d276c 100644 --- a/ruby/spec/jam_ruby/models/active_music_session_spec.rb +++ b/ruby/spec/jam_ruby/models/active_music_session_spec.rb @@ -403,8 +403,8 @@ describe ActiveMusicSession do let(:searcher_2) { FactoryGirl.create(:user, last_jam_locidispid: 3, last_jam_audio_latency: 14) } let(:searcher_conn_2) { FactoryGirl.create(:connection, user: searcher_2, ip_address: '9.9.9.9', locidispid: 3, addr:3) } - let!(:music_session_1) { FactoryGirl.create(:active_music_session, :creator => creator_1, genre: Genre.find('african'), language: 'en', description: "Bunny Jumps" ) } - let!(:music_session_2) { FactoryGirl.create(:active_music_session, :creator => creator_2, genre: Genre.find('ambient'), language: 'es', description: "Play with us as we jam to beatles and bunnies") } + let!(:music_session_1) { FactoryGirl.create(:active_music_session, :creator => creator_1, genre: Genre.find('african'), language: 'eng', description: "Bunny Jumps" ) } + let!(:music_session_2) { FactoryGirl.create(:active_music_session, :creator => creator_2, genre: Genre.find('ambient'), language: 'spa', description: "Play with us as we jam to beatles and bunnies") } let(:good_network_score) { 20 } let(:fair_network_score) { 30 } @@ -459,14 +459,14 @@ describe ActiveMusicSession do music_sessions.length.should == 2 # get only english - music_sessions, user_search = ams(searcher_1, client_id: searcher_conn_1.client_id, lang: 'en') + music_sessions, user_search = ams(searcher_1, client_id: searcher_conn_1.client_id, lang: 'eng') music_sessions.length.should == 1 - music_sessions[0].language.should == 'en' + music_sessions[0].language.should == 'eng' # get only ambient - music_sessions, user_search = ams(searcher_1, client_id: searcher_conn_1.client_id, lang: 'es') + music_sessions, user_search = ams(searcher_1, client_id: searcher_conn_1.client_id, lang: 'spa') music_sessions.length.should == 1 - music_sessions[0].language.should == 'es' + music_sessions[0].language.should == 'spa' end it "keyword" do diff --git a/ruby/spec/jam_ruby/models/music_session_spec.rb b/ruby/spec/jam_ruby/models/music_session_spec.rb index 89c379190..9176ed777 100644 --- a/ruby/spec/jam_ruby/models/music_session_spec.rb +++ b/ruby/spec/jam_ruby/models/music_session_spec.rb @@ -314,8 +314,8 @@ describe MusicSession do let!(:searcher_2) { FactoryGirl.create(:user, last_jam_locidispid: 3, last_jam_audio_latency: 14) } let!(:searcher_conn_2) { FactoryGirl.create(:connection, user: searcher_2, ip_address: '9.9.9.9', locidispid: 3, addr:3) } - let!(:music_session_1) { FactoryGirl.create(:music_session, creator: creator_1, genre: Genre.find('african'), language: 'en', description: "Bunny Jumps") } - let!(:music_session_2) { FactoryGirl.create(:music_session, creator: creator_2, genre: Genre.find('ambient'), language: 'es', description: "Play with us as we jam to beatles and bunnies") } + let!(:music_session_1) { FactoryGirl.create(:music_session, creator: creator_1, genre: Genre.find('african'), language: 'eng', description: "Bunny Jumps") } + let!(:music_session_2) { FactoryGirl.create(:music_session, creator: creator_2, genre: Genre.find('ambient'), language: 'spa', description: "Play with us as we jam to beatles and bunnies") } let!(:music_session_3) { FactoryGirl.create(:music_session, creator: creator_3) } let(:good_network_score) { 20 } @@ -399,8 +399,8 @@ describe MusicSession do let(:searcher_2) { FactoryGirl.create(:user, last_jam_locidispid: 3, last_jam_audio_latency: 14) } let(:searcher_conn_2) { FactoryGirl.create(:connection, user: searcher_2, ip_address: '9.9.9.9', locidispid: 3, addr:3) } - let!(:music_session_1) { FactoryGirl.create(:music_session, :creator => creator_1, genre: Genre.find('african'), language: 'en', description: "Bunny Jumps" ) } - let!(:music_session_2) { FactoryGirl.create(:music_session, :creator => creator_2, genre: Genre.find('ambient'), language: 'es', description: "Play with us as we jam to beatles and bunnies") } + let!(:music_session_1) { FactoryGirl.create(:music_session, :creator => creator_1, genre: Genre.find('african'), language: 'eng', description: "Bunny Jumps" ) } + let!(:music_session_2) { FactoryGirl.create(:music_session, :creator => creator_2, genre: Genre.find('ambient'), language: 'spa', description: "Play with us as we jam to beatles and bunnies") } let(:good_network_score) { 20 } let(:fair_network_score) { 30 } @@ -448,14 +448,14 @@ describe MusicSession do music_sessions.length.should == 2 # get only english - music_sessions, user_search = sms(searcher_1, client_id: searcher_conn_1.client_id, lang: 'en') + music_sessions, user_search = sms(searcher_1, client_id: searcher_conn_1.client_id, lang: 'eng') music_sessions.length.should == 1 - music_sessions[0].language.should == 'en' + music_sessions[0].language.should == 'eng' # get only ambient - music_sessions, user_search = sms(searcher_1, client_id: searcher_conn_1.client_id, lang: 'es') + music_sessions, user_search = sms(searcher_1, client_id: searcher_conn_1.client_id, lang: 'spa') music_sessions.length.should == 1 - music_sessions[0].language.should == 'es' + music_sessions[0].language.should == 'spa' end it "keyword" do @@ -498,7 +498,6 @@ describe MusicSession do music_sessions, user_search = sms(searcher_1, client_id: searcher_conn_1.client_id, day: (Date.today + 1).to_s, timezone_offset: DateTime.now.offset.numerator) music_sessions.length.should == 1 music_sessions[0].should == music_session_1 - end end end diff --git a/ruby/spec/jam_ruby/models/recording_spec.rb b/ruby/spec/jam_ruby/models/recording_spec.rb index d61a1eecd..0bc9702e0 100644 --- a/ruby/spec/jam_ruby/models/recording_spec.rb +++ b/ruby/spec/jam_ruby/models/recording_spec.rb @@ -188,8 +188,12 @@ describe Recording do expect { @claimed_recordign.discard(@user2) }.to raise_error @claimed_recording = @recording.claim(@user2, "name2", "description2", @genre, true) @claimed_recording.discard(@user2) + @claimed_recording.reload + @claimed_recording.discarded.should == true + @recording.recorded_tracks_for_user(@user2)[0].discard.should == true @recording.reload - @recording.claimed_recordings.length.should == 1 + @recording.claimed_recordings.length.should == 2 + @recording.all_discarded.should == false end it "should destroy the entire recording if there was only one claimed_recording which is discarded" do @@ -199,8 +203,10 @@ describe Recording do @genre = FactoryGirl.create(:genre) @claimed_recording = @recording.claim(@user, "name", "description", @genre, true) @claimed_recording.discard(@user) - expect { Recording.find(@recording.id) }.to raise_error - expect { ClaimedRecording.find(@claimed_recording.id) }.to raise_error + @claimed_recording.reload + @claimed_recording.discarded.should == true + @claimed_recording.recording.all_discarded.should == true + @recording.recorded_tracks_for_user(@user)[0].discard.should == true end it "should use the since parameter when restricting uploads" do diff --git a/web/app/assets/javascripts/accounts_profile.js b/web/app/assets/javascripts/accounts_profile.js index 32b72de65..bb5a0f48b 100644 --- a/web/app/assets/javascripts/accounts_profile.js +++ b/web/app/assets/javascripts/accounts_profile.js @@ -206,8 +206,8 @@ if(!region) return; var option = $(nilOptionStr) - option.text(region) - option.attr("value", region) + option.text(region['name']) + option.attr("value", region['region']) regionSelect.append(option) }) @@ -431,6 +431,7 @@ var cityElement = getCityElement(); updateRegionList(selectedCountry, getRegionElement()); + updateCityList(selectedCountry, null, cityElement); } function updateRegionList(selectedCountry, regionElement) { @@ -445,7 +446,10 @@ api.getRegions({ country: selectedCountry }) .done(getRegionsDone) - .fail(app.ajaxError) + .error(function(err) { + regionElement.children().remove() + regionElement.append($(nilOptionStr).text(nilOptionText)) + }) .always(function () { loadingRegionsData = false; }) @@ -470,14 +474,18 @@ api.getCities({ country: selectedCountry, region: selectedRegion }) .done(getCitiesDone) - .fail(app.ajaxError) + .error(function(err) { + cityElement.children().remove() + cityElement.append($(nilOptionStr).text(nilOptionText)) + }) .always(function () { loadingCitiesData = false; }) } else { - cityElement.children().remove() - cityElement.append($(nilOptionStr).text(nilOptionText)) + cityElement.children().remove(); + cityElement.append($(nilOptionStr).text(nilOptionText)); + context.JK.dropdown(cityElement); } } diff --git a/web/app/assets/javascripts/band_setup.js b/web/app/assets/javascripts/band_setup.js index 2860c3ed1..a5120f5cb 100644 --- a/web/app/assets/javascripts/band_setup.js +++ b/web/app/assets/javascripts/band_setup.js @@ -309,19 +309,22 @@ $.each(response["regions"], function (index, region) { if (!region) return; var option = $(nilOptionStr); - option.text(region); - option.attr("value", region); + option.text(region['name']); + option.attr("value", region['region']); - if (initialRegion === region) { + if (initialRegion === region['region']) { option.attr("selected", "selected"); } $region.append(option); }); - context.JK.dropdown($region); - + if (onRegionsLoaded) { + onRegionsLoaded(); + } + }).error(function(err) { + context.JK.dropdown($region); if (onRegionsLoaded) { onRegionsLoaded(); } @@ -338,6 +341,7 @@ var nilOption = $(nilOptionStr); nilOption.text(nilOptionText); $city.append(nilOption); + nilOption.attr('selected','selected'); if (selectedCountry && selectedRegion) { rest.getCities({'country': selectedCountry, 'region': selectedRegion}).done(function (response) { @@ -355,9 +359,13 @@ }); context.JK.dropdown($city); + }).error(function(err) { + context.JK.dropdown($city); }); + } else { + context.JK.dropdown($city); } - } + } function addInvitation(value, data) { if ($('#selected-band-invitees div[user-id=' + data + ']').length === 0) { diff --git a/web/app/assets/javascripts/banner.js b/web/app/assets/javascripts/banner.js index 7eda9fa0f..99e0ddfbd 100644 --- a/web/app/assets/javascripts/banner.js +++ b/web/app/assets/javascripts/banner.js @@ -9,6 +9,9 @@ var self = this; var logger = context.JK.logger; var $banner = null; + var $closeBtn = null; + var $yesBtn = null; + var $noBtn = null; // you can also do // * showAlert('title', 'text') @@ -26,6 +29,19 @@ return show(options); } + function showYesNo(options) { + if (typeof options == 'string' || options instanceof String) { + if(arguments.length == 2) { + options = {title: options, html:arguments[1]} + } + else { + options = {html:options}; + } + } + options.type = 'yes_no' + return show(options); + } + // responsible for updating the contents of the update dialog // as well as registering for any event handlers function show(options) { @@ -33,7 +49,12 @@ var html = options.html; if(!options.title) { - options.title = 'alert' + if(options.type == 'alert') { + options.title = 'alert' + } + else if(options.type == 'yes_no') { + options.title = 'please confirm'; + } } var $h1 = $banner.find('h1'); @@ -50,11 +71,18 @@ throw "unable to show banner for empty message"; } - var $closeBtn = $banner.find('.close-btn'); if((options.type == "alert" && !options.buttons) || options.close) { - $closeBtn.show().click(function() { + var closeButtonText = 'CLOSE'; + if(options.close !== null && typeof options.close == 'object') { + // extra styling options for close button + if(options.close.name) { + closeButtonText = options.close.name; + } + } + + $closeBtn.show().text(closeButtonText).unbind('click').click(function() { hide(); return false; }); @@ -63,6 +91,27 @@ $closeBtn.hide(); } + if(options.type == "yes_no") { + $yesBtn.show().unbind('click').click(function() { + if(options.yes) { + options.yes(); + } + hide(); + return false; + }) + $noBtn.show().unbind('click').click(function() { + if(options.no) { + options.no(); + } + hide(); + return false; + }) + } + else { + $yesBtn.hide(); + $noBtn.hide(); + } + if(options.buttons) { var $buttons = $banner.find('.buttons') context._.each(options.buttons, function(button) { @@ -86,6 +135,7 @@ return newContent; } + function hide() { $banner.hide(); $banner.find('.user-btn').remove(); @@ -96,6 +146,10 @@ function initialize() { $banner = $('#banner'); + + $closeBtn = $banner.find('.close-btn'); + $yesBtn = $banner.find('.yes-btn'); + $noBtn = $banner.find('.no-btn'); return self; } @@ -104,6 +158,7 @@ initialize: initialize, show: show, showAlert: showAlert, + showYesNo: showYesNo,// shows Yes and Cancel button (confirmation dialog) hide: hide } diff --git a/web/app/assets/javascripts/commentDialog.js b/web/app/assets/javascripts/commentDialog.js index 263ea7d6d..794ee141b 100644 --- a/web/app/assets/javascripts/commentDialog.js +++ b/web/app/assets/javascripts/commentDialog.js @@ -122,7 +122,7 @@ } function showDialog() { - app.layout.showDialog('comment-dialog'); + return app.layout.showDialog('comment-dialog'); } function initialize() { diff --git a/web/app/assets/javascripts/editRecordingDialog.js b/web/app/assets/javascripts/editRecordingDialog.js new file mode 100644 index 000000000..23c1d4919 --- /dev/null +++ b/web/app/assets/javascripts/editRecordingDialog.js @@ -0,0 +1,170 @@ +(function (context, $) { + + "use strict"; + context.JK = context.JK || {}; + context.JK.EditRecordingDialog = function (app) { + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var claimedRecordingId = null; + var $dialog = null; + var $form = null; + var $name = null; + var $description = null; + var $genre = null; + var $isPublic = null; + var $cancelBtn = null; + var $saveBtn = null; + var $deleteBtn = null; + + var updating = false; + var deleting = false; + + function resetForm() { + + // remove all display errors + $dialog.find('.error-text').remove() + $dialog.find('.error').removeClass("error") + } + + function beforeShow(args) { + + claimedRecordingId = args.d1; + + if(!claimedRecordingId) throw "claimedRecordingId must be specified"; + + resetForm(); + + rest.getClaimedRecording(claimedRecordingId) + .done(function(data) { + var name = data.name; + var description = data.description; + var is_public = data.is_public; + var genre_id = data.genre_id; + + context.JK.GenreSelectorHelper.setSelectedGenres($genre.parent(), [genre_id]); + $name.val(name); + $description.val(description); + if(is_public) { + $isPublic.attr('checked', 'checked').iCheck('check') + } + else { + $isPublic.removeAttr('checked').iCheck('uncheck') + } + }) + .fail(app.ajaxError) + } + + function afterHide() { + + } + + function attemptUpdate() { + if(updating) return; + + updating = true; + var name = $name.val(); + var description = $description.val(); + var genre = $genre.val(); + var is_public = $isPublic.is(':checked'); + + rest.updateClaimedRecording({id: claimedRecordingId, name: name, description: description, is_public: is_public, genre: genre }) + .done(function(updated) { + resetForm(); + $dialog.triggerHandler('recording_updated', {id: claimedRecordingId, name: name, description: description, is_public: is_public, genre: genre}) + app.layout.closeDialog('edit-recording'); + }) + .fail(function(jqXHR) { + + if(jqXHR.status = 422) { + // highlight fields in error + + resetForm(); + + var errors = JSON.parse(jqXHR.responseText); + + var $name_errors = context.JK.format_errors('name', errors); + if ($name_errors) $name.closest('div.field').addClass('error').end().after($name_errors); + + var $description_errors = context.JK.format_errors('description', errors); + if ($description_errors) $description.closest('div.field').addClass('error').end().after($description_errors); + + var $genre_errors = context.JK.format_errors('genre', errors); + if ($genre_errors) $genre.closest('div.field').addClass('error').end().after($genre_errors); + + var $is_public_errors = context.JK.format_errors('is_public', errors); + if ($is_public_errors) $isPublic.closest('div.field').addClass('error').end().after($is_public_errors); + + } + else { + app.ajaxError(arguments); + } + }) + .always(function() { + updating = false; + }) + } + + function attemptDelete() { + if(deleting) return; + + deleting = true; + context.JK.Banner.showYesNo({ + title: "Confirm Deletion", + html: "Are you sure you want to delete this recording?", + yes: function() { + rest.deleteClaimedRecording(claimedRecordingId) + .done(function() { + $dialog.triggerHandler('recording_deleted', {id: claimedRecordingId}); + app.layout.closeDialog('edit-recording'); + }) + .fail(app.ajaxError) + .always(function() { + deleting = false; + }) + }, + no : function() { + context.JK.Banner.hide(); + deleting = false; + } + }) + } + + function cancel() { + app.layout.closeDialog('edit-recording'); + } + + function events() { + $saveBtn.click(attemptUpdate); + $deleteBtn.click(attemptDelete); + $cancelBtn.click(cancel) + $form.submit(false); + } + + + function initialize() { + var dialogBindings = { + 'beforeShow': beforeShow, + 'afterHide': afterHide + }; + + app.bindDialog('edit-recording', dialogBindings); + + $dialog = $('#edit-recording-dialog'); + $form = $dialog.find('form'); + $cancelBtn = $dialog.find('.cancel-btn'); + $saveBtn = $dialog.find('.save-btn'); + $deleteBtn = $dialog.find('.delete-btn'); + $name = $dialog.find('input[name="name"]'); + $description = $dialog.find('textarea[name="description"]'); + $genre = $dialog.find('select[name=genre]'); + $isPublic = $dialog.find('input[name=is_public]'); + + events(); + + context.JK.GenreSelectorHelper.render($genre.parent()); + context.JK.checkbox($isPublic); + }; + + this.initialize = initialize; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/feed.js b/web/app/assets/javascripts/feed.js index da4c107cb..41027b898 100644 --- a/web/app/assets/javascripts/feed.js +++ b/web/app/assets/javascripts/feed.js @@ -5,407 +5,15 @@ context.JK.FeedScreen = function(app) { var logger = context.JK.logger; - var rest = new context.JK.Rest(); - var ui = new context.JK.UIHelper(JK.app); - var currentQuery = null; - var currentPage = 0; - var LIMIT = 20; - var $screen = null; - var $next = null; - var $scroller = null; - var $content = null; - var $noMoreFeeds = null; - var $refresh = null; - var $sortFeedBy = null; - var $includeDate = null; - var $includeType = null; - var next = null; - function defaultQuery() { - var query = { limit:LIMIT, page:currentPage}; - - if(next) { - query.since = next; - } - - return query; - } - - function buildQuery() { - currentQuery = defaultQuery(); - - // specify search criteria based on form - currentQuery.sort = $sortFeedBy.val(); - currentQuery.time_range = $includeDate.val(); - currentQuery.type = $includeType.val(); - - return currentQuery; - } + var feed = null; function beforeShow(data) { } function afterShow(data) { - refresh(); - } - - function clearResults() { - currentPage = 0; - $content.empty(); // TODO: do we need to delete audio elements? - $noMoreFeeds.hide(); - next = null; - } - - function handleFeedResponse(response) { - next = response.next; - - renderFeeds(response); - - if(response.next == null) { - // if we less results than asked for, end searching - $scroller.infinitescroll('pause'); - logger.debug("end of feeds") - - if(currentPage > 0) { - $noMoreFeeds.show(); - // there are bugs with infinitescroll not removing the 'loading'. - // it's most noticeable at the end of the list, so whack all such entries - $('.infinite-scroll-loader').remove(); - } - } - else { - currentPage++; - buildQuery(); - registerInfiniteScroll(); - } - } - - function refresh() { - - clearResults(); - - currentQuery = buildQuery(); - rest.getFeeds(currentQuery) - .done(function(response) { - handleFeedResponse(response); - }) - .fail(function(jqXHR) { - app.notifyServerError(jqXHR, 'Feed Unavailable') - }) - } - - function registerInfiniteScroll() { - - $scroller.infinitescroll({ - behavior: 'local', - navSelector: '#feedScreen .btn-next-pager', - nextSelector: '#feedScreen .btn-next-pager', - binder: $scroller, - dataType: 'json', - appendCallback: false, - prefill: false, - bufferPx:100, - loading: { - msg: $('
Loading ...
'), - img: '/assets/shared/spinner.gif' - }, - path: function(page) { - return '/api/feeds?' + $.param(buildQuery()); - } - },function(json, opts) { - handleFeedResponse(json); - }); - $scroller.infinitescroll('resume'); - } - - - function toggleSessionDetails() { - var $detailsLink = $(this); - var $feedItem = $detailsLink.closest('.feed-entry'); - var $musicians = $feedItem.find('.musician-detail'); - var $description = $feedItem.find('.description'); - var toggledOpen = $detailsLink.data('toggledOpen'); - - if(toggledOpen) { - $feedItem.css('height', $feedItem.height() + 'px') - $feedItem.animate({'height': $feedItem.data('original-max-height')}).promise().done(function() { - $feedItem.css('height', 'auto').css('max-height', $feedItem.data('original-max-height')); - - $musicians.hide(); - $description.css('height', $description.data('original-height')); - $description.dotdotdot(); - }); - } - else { - $description.trigger('destroy.dot'); - $description.data('original-height', $description.css('height')).css('height', 'auto'); - $musicians.show(); - $feedItem.animate({'max-height': '1000px'}); - } - - toggledOpen = !toggledOpen; - $detailsLink.data('toggledOpen', toggledOpen); - return false; - } - - function startSessionPlay($feedItem) { - var img = $('.play-icon', $feedItem); - var $controls = $feedItem.find('.session-controls'); - img.attr('src', '/assets/content/icon_pausebutton.png'); - $controls.trigger('play.listenBroadcast'); - $feedItem.data('playing', true); - } - - function stopSessionPlay($feedItem) { - var img = $('.play-icon', $feedItem); - var $controls = $feedItem.find('.session-controls'); - img.attr('src', '/assets/content/icon_playbutton.png'); - $controls.trigger('pause.listenBroadcast'); - $feedItem.data('playing', false); - } - - function toggleSessionPlay() { - var $playLink = $(this); - var $feedItem = $playLink.closest('.feed-entry'); - - var $status = $feedItem.find('.session-status') - var playing = $feedItem.data('playing'); - - if(playing) { - $status.text('SESSION IN PROGRESS'); - stopSessionPlay($feedItem); - } - else { - startSessionPlay($feedItem); - } - return false; - } - - function stateChangeSession(e, data) { - var $controls = data.element; - var $feedItem = $controls.closest('.feed-entry'); - var $status = $feedItem.find('.session-status'); - - if(data.displayText) $status.text(data.displayText); - - if(data.isEnd) stopSessionPlay(); - - if(data.isSessionOver) { - $controls.removeClass('inprogress').addClass('ended') - } - } - - function startRecordingPlay($feedItem) { - var img = $('.play-icon', $feedItem); - var $controls = $feedItem.find('.recording-controls'); - img.attr('src', '/assets/content/icon_pausebutton.png'); - $controls.trigger('play.listenRecording'); - $feedItem.data('playing', true); - } - - function stopRecordingPlay($feedItem) { - var img = $('.play-icon', $feedItem); - var $controls = $feedItem.find('.recording-controls'); - img.attr('src', '/assets/content/icon_playbutton.png'); - $controls.trigger('pause.listenRecording'); - $feedItem.data('playing', false); - } - - function toggleRecordingPlay() { - - var $playLink = $(this); - var $feedItem = $playLink.closest('.feed-entry'); - var playing = $feedItem.data('playing'); - - if(playing) { - stopRecordingPlay($feedItem); - } - else { - startRecordingPlay($feedItem); - } - return false; - } - - - function stateChangeRecording(e, data) { - var $controls = data.element; - var $feedItem = $controls.closest('.feed-entry'); - - var $sliderBar = $('.recording-position', $feedItem); - var $statusBar = $('.recording-status', $feedItem); - var $currentTime = $('.recording-current', $feedItem); - var $status = $('.status-text', $feedItem); - var $playButton = $('.play-button', $feedItem); - - if(data.isEnd) stopRecordingPlay($feedItem); - if(data.isError) { - $sliderBar.hide(); - $playButton.hide(); - $currentTime.hide(); - $statusBar.show(); - $status.text(data.displayText); - } - } - - function toggleRecordingDetails() { - var $detailsLink = $(this); - var $feedItem = $detailsLink.closest('.feed-entry'); - var $musicians = $feedItem.find('.musician-detail'); - var $description = $feedItem.find('.description'); - var $name = $feedItem.find('.name'); - var toggledOpen = $detailsLink.data('toggledOpen'); - - if(toggledOpen) { - $feedItem.css('height', $feedItem.height() + 'px') - $feedItem.animate({'height': $feedItem.data('original-max-height')}).promise().done(function() { - $feedItem.css('height', 'auto').css('max-height', $feedItem.data('original-max-height')); - - $musicians.hide(); - $description.css('height', $description.data('original-height')); - $description.dotdotdot(); - $name.css('height', $name.data('original-height')); - $name.dotdotdot(); - }); - } - else { - $description.trigger('destroy.dot'); - $description.data('original-height', $description.css('height')).css('height', 'auto'); - $name.trigger('destroy.dot'); - $name.data('original-height', $name.css('height')).css('height', 'auto'); - $musicians.show(); - $feedItem.animate({'max-height': '1000px'}); - } - - toggledOpen = !toggledOpen; - $detailsLink.data('toggledOpen', toggledOpen); - - return false; - } - - function renderFeeds(feeds) { - - $.each(feeds.entries, function(i, feed) { - if(feed.type == 'music_session') { - var options = { - feed_item: feed, - status_class: feed['is_over?'] ? 'ended' : 'inprogress', - mount_class: feed['has_mount?'] ? 'has-mount' : 'no-mount' - } - var $feedItem = $(context._.template($('#template-feed-music-session').html(), options, {variable: 'data'})); - var $controls = $feedItem.find('.session-controls'); - - // do everything we can before we attach the item to the page - $('.timeago', $feedItem).timeago(); - context.JK.prettyPrintElements($('.duration', $feedItem).show()); - context.JK.setInstrumentAssetPath($('.instrument-icon', $feedItem)); - $('.details', $feedItem).click(toggleSessionDetails); - $('.details-arrow', $feedItem).click(toggleSessionDetails); - $('.play-button', $feedItem).click(toggleSessionPlay); - - if (!feed.session_removed_at) - { - $('.btn-share', $feedItem).click(function() { - ui.launchShareDialog(feed.id, 'session'); - }); - } - else { - $('.btn-share', $feedItem).hide(); - } - - $('.btn-comment', $feedItem).click(function() { - ui.launchCommentDialog({ - session_id: feed.id, - entity_type: 'session' - }); - }); - - $('.btn-like', $feedItem).click(function() { - ui.addSessionLike(feed.id, JK.currentUserId, $('.likes', $feedItem), $('.btn-like', $feedItem)) - }); - - // put the feed item on the page - renderFeed($feedItem); - - // these routines need the item to have height to work (must be after renderFeed) - $controls.listenBroadcast(); - $controls.bind('statechange.listenBroadcast', stateChangeSession); - $('.dotdotdot', $feedItem).dotdotdot(); - $feedItem.data('original-max-height', $feedItem.css('height')); - context.JK.bindHoverEvents($feedItem); - context.JK.bindProfileClickEvents($feedItem); - } - else if(feed.type == 'recording') { - if(feed.claimed_recordings.length == 0) { - logger.error("a recording in the feed should always have one claimed_recording") - return; - } - var options = { - feed_item: feed, - candidate_claimed_recording: feed.claimed_recordings[0], - mix_class: feed['has_mix?'] ? 'has-mix' : 'no-mix', - } - - var $feedItem = $(context._.template($('#template-feed-recording').html(), options, {variable: 'data'})); - var $controls = $feedItem.find('.recording-controls'); - - $('.timeago', $feedItem).timeago(); - context.JK.prettyPrintElements($('.duration', $feedItem)); - context.JK.setInstrumentAssetPath($('.instrument-icon', $feedItem)); - $('.details', $feedItem).click(toggleRecordingDetails); - $('.details-arrow', $feedItem).click(toggleRecordingDetails); - $('.play-button', $feedItem).click(toggleRecordingPlay); - - $('.btn-share', $feedItem).click(function() { - ui.launchShareDialog(options.candidate_claimed_recording.id, 'recording'); - }); - - $('.btn-comment', $feedItem).click(function() { - ui.launchCommentDialog({ - recording_id: feed.id, - claimed_recording_id: options.candidate_claimed_recording.id, - entity_type: 'recording' - }); - }); - - $('.btn-like', $feedItem).click(function() { - ui.addRecordingLike(feed.id, options.candidate_claimed_recording.id, JK.currentUserId, $('.likes', $feedItem), $('.btn-like', $feedItem)); - }); - - // put the feed item on the page - renderFeed($feedItem); - - // these routines need the item to have height to work (must be after renderFeed) - $controls.listenRecording({recordingId: feed.id, claimedRecordingId: options.candidate_claimed_recording.id, sliderSelector:'.recording-slider', sliderBarSelector: '.recording-playback', currentTimeSelector:'.recording-current'}); - $controls.bind('statechange.listenRecording', stateChangeRecording); - $('.dotdotdot', $feedItem).dotdotdot(); - $feedItem.data('original-max-height', $feedItem.css('height')); - context.JK.bindHoverEvents($feedItem); - context.JK.bindProfileClickEvents($feedItem); - } - else { - logger.warn("skipping feed type: " + feed.type); - } - - context.JK.bindProfileClickEvents(); - }); - } - - function renderFeed(feed) { - $content.append(feed); - } - - function search() { - logger.debug("Searching for feeds..."); - refresh(); - return false; - } - - function events() { - $refresh.on("click", search); - $sortFeedBy.on('change', search); - $includeDate.on('change', search); - $includeType.on('change', search); + feed.refresh(); } function initialize() { @@ -415,21 +23,17 @@ }; app.bindScreen('feed', screenBindings); - $screen = $('[layout-id="feed"]'); - $scroller = $screen.find('.content-body-scroller'); - $content = $screen.find('.feed-content'); - $noMoreFeeds = $('#end-of-feeds-list'); - $refresh = $screen.find('#btn-refresh-feed'); - $sortFeedBy = $screen.find('#feed_order_by'); - $includeDate = $screen.find('#feed_date'); - $includeType = $screen.find('#feed_show'); + var $screen = $('[layout-id="feed"]'); + var $scroller = $screen.find('.content-body-scroller'); + var $content = $screen.find('.feed-content'); + var $noMoreFeeds = $('#end-of-feeds-list'); + var $refresh = $screen.find('.btn-refresh-entries'); + var $sortFeedBy = $screen.find('#feed_order_by'); + var $includeDate = $screen.find('#feed_date'); + var $includeType = $screen.find('#feed_show'); - // set default search criteria - $sortFeedBy.val('date') - $includeDate.val('month') - $includeType.val('all') - - events(); + feed = new context.JK.Feed(app); + feed.initialize($screen, $scroller, $content, $noMoreFeeds, $refresh, $sortFeedBy, $includeDate, $includeType); } this.initialize = initialize; diff --git a/web/app/assets/javascripts/feedHelper.js b/web/app/assets/javascripts/feedHelper.js new file mode 100644 index 000000000..fd7b3eb73 --- /dev/null +++ b/web/app/assets/javascripts/feedHelper.js @@ -0,0 +1,586 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.Feed = function (app) { + + var logger = context.JK.logger; + var rest = new context.JK.Rest(); + var EVENTS = context.JK.EVENTS; + var ui = new context.JK.UIHelper(JK.app); + var userId = null; + var currentQuery = null; + var currentPage = 0; + var LIMIT = 20; + var $next = null; + var $screen = null; + var $scroller = null; + var $content = null; + var $noMoreFeeds = null; + var $refresh = null; + var $sortFeedBy = null; + var $includeDate = null; + var $includeType = null; + var next = null; + + function defaultQuery() { + var query = { limit:LIMIT, page:currentPage}; + + if(next) { + query.since = next; + } + + if(userId) { + query.user = userId; + } + + return query; + } + + function buildQuery() { + currentQuery = defaultQuery(); + + // specify search criteria based on form + currentQuery.sort = $sortFeedBy.val(); + currentQuery.time_range = $includeDate.val(); + currentQuery.type = $includeType.val(); + + return currentQuery; + } + + + function clearResults() { + currentPage = 0; + $content.empty(); // TODO: do we need to delete audio elements? + $noMoreFeeds.hide(); + next = null; + } + + function handleFeedResponse(response) { + next = response.next; + + renderFeeds(response); + + if(response.next == null) { + // if we less results than asked for, end searching + $scroller.infinitescroll('pause'); + logger.debug("end of feeds") + + if(currentPage == 0 && response.entries.length == 0) { + $content.append("
This user has no history.
") ; + } + + if(currentPage > 0) { + $noMoreFeeds.show(); + // there are bugs with infinitescroll not removing the 'loading'. + // it's most noticeable at the end of the list, so whack all such entries + $('.infinite-scroll-loader').remove(); + } + } + else { + currentPage++; + buildQuery(); + registerInfiniteScroll(); + } + } + + function setUser(_userId) { + userId = _userId; + } + + function refresh() { + + clearResults(); + + currentQuery = buildQuery(); + rest.getFeeds(currentQuery) + .done(function(response) { + handleFeedResponse(response); + }) + .fail(function(jqXHR) { + app.notifyServerError(jqXHR, 'Feed Unavailable') + }) + } + + function registerInfiniteScroll() { + + $scroller.infinitescroll({ + behavior: 'local', + navSelector: '#feedScreen .btn-next-pager', + nextSelector: '#feedScreen .btn-next-pager', + binder: $scroller, + dataType: 'json', + appendCallback: false, + prefill: false, + bufferPx:100, + loading: { + msg: $('
Loading ...
'), + img: '/assets/shared/spinner.gif' + }, + path: function(page) { + return '/api/feeds?' + $.param(buildQuery()); + } + },function(json, opts) { + handleFeedResponse(json); + }); + $scroller.infinitescroll('resume'); + } + + + function toggleSessionDetails() { + var $detailsLink = $(this); + var $feedItem = $detailsLink.closest('.feed-entry'); + var $musicians = $feedItem.find('.musician-detail'); + var $description = $feedItem.find('.description'); + var toggledOpen = $detailsLink.data('toggledOpen'); + + if(toggledOpen) { + $feedItem.css('height', $feedItem.height() + 'px') + $feedItem.animate({'height': $feedItem.data('original-max-height')}).promise().done(function() { + $feedItem.css('height', 'auto').css('max-height', $feedItem.data('original-max-height')); + + $musicians.hide(); + $description.css('height', $description.data('original-height')); + $description.dotdotdot(); + }); + } + else { + $description.trigger('destroy.dot'); + $description.data('original-height', $description.css('height')).css('height', 'auto'); + $musicians.show(); + $feedItem.animate({'max-height': '1000px'}); + } + + toggledOpen = !toggledOpen; + $detailsLink.data('toggledOpen', toggledOpen); + return false; + } + + function startSessionPlay($feedItem) { + var img = $('.play-icon', $feedItem); + var $controls = $feedItem.find('.session-controls'); + img.attr('src', '/assets/content/icon_pausebutton.png'); + $controls.trigger('play.listenBroadcast'); + $feedItem.data('playing', true); + } + + function stopSessionPlay($feedItem) { + var img = $('.play-icon', $feedItem); + var $controls = $feedItem.find('.session-controls'); + img.attr('src', '/assets/content/icon_playbutton.png'); + $controls.trigger('pause.listenBroadcast'); + $feedItem.data('playing', false); + } + + function toggleSessionPlay() { + var $playLink = $(this); + var $feedItem = $playLink.closest('.feed-entry'); + + var $status = $feedItem.find('.session-status') + var playing = $feedItem.data('playing'); + + if(playing) { + $status.text('SESSION IN PROGRESS'); + stopSessionPlay($feedItem); + } + else { + startSessionPlay($feedItem); + } + return false; + } + + function stateChangeSession(e, data) { + var $controls = data.element; + var $feedItem = $controls.closest('.feed-entry'); + var $status = $feedItem.find('.session-status'); + + if(data.displayText) $status.text(data.displayText); + + if(data.isEnd) stopSessionPlay(); + + if(data.isSessionOver) { + $controls.removeClass('inprogress').addClass('ended') + } + } + + function startRecordingPlay($feedItem) { + var img = $('.play-icon', $feedItem); + var $controls = $feedItem.find('.recording-controls'); + img.attr('src', '/assets/content/icon_pausebutton.png'); + $controls.trigger('play.listenRecording'); + $feedItem.data('playing', true); + } + + function stopRecordingPlay($feedItem) { + var img = $('.play-icon', $feedItem); + var $controls = $feedItem.find('.recording-controls'); + img.attr('src', '/assets/content/icon_playbutton.png'); + $controls.trigger('pause.listenRecording'); + $feedItem.data('playing', false); + } + + function toggleRecordingPlay() { + + var $playLink = $(this); + var $feedItem = $playLink.closest('.feed-entry'); + var playing = $feedItem.data('playing'); + + if(playing) { + stopRecordingPlay($feedItem); + } + else { + startRecordingPlay($feedItem); + } + return false; + } + + function isOwner() { + return userId == context.JK.currentUserId; + } + + function obtainCandidate(recording) { + if(isOwner()) { + var candidate = null; + context._.each(recording.claimed_recordings, function(claimedRecording) { + if(claimedRecording.user_id == context.JK.currentUserId) { + candidate = claimedRecording; + return false; + } + }) + + if(!candidate) throw "unable to find candidate claimed recording, yet we can see this recording. server error..." + return candidate; + } + else { + return recording.claimed_recordings[0] + } + } + + function toggleOpen($feedItem, $name, $description, $musicians) { + $description.trigger('destroy.dot'); + $description.data('original-height', $description.css('height')).css('height', 'auto'); + $name.trigger('destroy.dot'); + $name.data('original-height', $name.css('height')).css('height', 'auto'); + $musicians.show(); + $feedItem.animate({'max-height': '1000px'}); + } + + function toggleClose($feedItem, $name, $description, $musicians, immediate) { + $feedItem.css('height', $feedItem.height() + 'px') + $feedItem.animate({'height': $feedItem.data('original-max-height')}, immediate ? 0 : 400).promise().done(function() { + $feedItem.css('height', 'auto').css('max-height', $feedItem.data('original-max-height')); + + $musicians.hide(); + $description.css('height', $description.data('original-height')); + $description.dotdotdot(); + $name.css('height', $name.data('original-height')); + $name.dotdotdot(); + }); + } + + function stateChangeRecording(e, data) { + var $controls = data.element; + var $feedItem = $controls.closest('.feed-entry'); + + var $sliderBar = $('.recording-position', $feedItem); + var $statusBar = $('.recording-status', $feedItem); + var $currentTime = $('.recording-current', $feedItem); + var $status = $('.status-text', $feedItem); + var $playButton = $('.play-button', $feedItem); + + if(data.isEnd) stopRecordingPlay($feedItem); + if(data.isError) { + $sliderBar.hide(); + $playButton.hide(); + $currentTime.hide(); + $statusBar.show(); + $status.text(data.displayText); + } + } + + function toggleRecordingDetails() { + var $detailsLink = $(this); + var $feedItem = $detailsLink.closest('.feed-entry'); + var $musicians = $feedItem.find('.musician-detail'); + var $description = $feedItem.find('.description'); + var $name = $feedItem.find('.name'); + var toggledOpen = $detailsLink.data('toggledOpen'); + + if(toggledOpen) { + toggleClose($feedItem, $name, $description, $musicians) + } + else { + toggleOpen($feedItem, $name, $description, $musicians) + } + + toggledOpen = !toggledOpen; + $detailsLink.data('toggledOpen', toggledOpen); + + return false; + } + + function updateRecordingName($feedEntry, name) { + $feedEntry.find('.name-text').text(name); + } + + function updateRecordingDescription($feedEntry, description) { + $feedEntry.find('.description').text(description); + } + + function updateIsPublic($feedEntry, isPublic) { + var $isPrivate = $feedEntry.find('.is_private') + if(isPublic) { + $isPrivate.removeClass('enabled') + } + else { + $isPrivate.addClass('enabled') + } + } + + function updateComments($feedEntry, comments) { + $feedEntry.find('.comments').html(comments) + } + + function updatePlays($feedEntry, plays) { + $feedEntry.find('.plays').html(plays); + } + + function updateLikes($feedEntry, likes) { + $feedEntry.find('.likes').html(likes); + } + + function updateStats($feedEntry) { + if($feedEntry.is('.recording-entry')) { + var id = $feedEntry.attr('data-claimed-recording-id'); + rest.getClaimedRecording(id) + .done(function(claimedRecording) { + updateComments($feedEntry, claimedRecording.recording.comment_count); + updateLikes($feedEntry, claimedRecording.recording.like_count); + updatePlays($feedEntry, claimedRecording.recording.play_count); + + }) + .fail(app.ajaxError) + } + else { + var id = $feedEntry.attr('data-music-session'); + rest.getSessionHistory(id) + .done(function(music_session) { + updateComments($feedEntry, music_session.comment_count); + updateLikes($feedEntry, music_session.like_count); + updatePlays($feedEntry, music_session.play_count); + }) + .fail(app.ajaxError) + } + } + + function updateGenre($feedEntry, genre) { + $feedEntry.find('.genre').text(context.JK.GenreSelectorHelper.getNameForId(genre)); + } + + function renderFeeds(feeds) { + + $.each(feeds.entries, function(i, feed) { + if(feed.type == 'music_session') { + var options = { + feed_item: feed, + status_class: feed['is_over?'] ? 'ended' : 'inprogress', + mount_class: feed['has_mount?'] ? 'has-mount' : 'no-mount' + } + var $feedItem = $(context._.template($('#template-feed-music-session').html(), options, {variable: 'data'})); + var $controls = $feedItem.find('.session-controls'); + + // do everything we can before we attach the item to the page + $('.timeago', $feedItem).timeago(); + context.JK.prettyPrintElements($('.duration', $feedItem).show()); + context.JK.setInstrumentAssetPath($('.instrument-icon', $feedItem)); + $('.details', $feedItem).click(toggleSessionDetails); + $('.details-arrow', $feedItem).click(toggleSessionDetails); + $('.play-button', $feedItem).click(toggleSessionPlay); + + if (!feed.session_removed_at) + { + $('.btn-share', $feedItem).click(function() { + ui.launchShareDialog(feed.id, 'session'); + }); + } + else { + $('.btn-share', $feedItem).hide(); + } + + $('.btn-comment', $feedItem).click(function() { + var result = ui.launchCommentDialog({ + session_id: feed.id, + entity_type: 'session' + }).one(EVENTS.DIALOG_CLOSED, function() { + updateStats($feedItem); + }) + }); + + $('.btn-like', $feedItem).click(function() { + ui.addSessionLike(feed.id, JK.currentUserId, $('.likes', $feedItem), $('.btn-like', $feedItem)) + }); + + // put the feed item on the page + renderFeed($feedItem); + + // these routines need the item to have height to work (must be after renderFeed) + $controls.listenBroadcast(); + $controls.bind('statechange.listenBroadcast', stateChangeSession); + $('.dotdotdot', $feedItem).dotdotdot(); + $feedItem.data('original-max-height', $feedItem.css('height')); + context.JK.bindHoverEvents($feedItem); + context.JK.bindProfileClickEvents($feedItem); + } + else if(feed.type == 'recording') { + if(feed.claimed_recordings.length == 0) { + logger.error("a recording in the feed should always have one claimed_recording") + return; + } + var options = { + feed_item: feed, + candidate_claimed_recording: obtainCandidate(feed), + mix_class: feed['has_mix?'] ? 'has-mix' : 'no-mix', + } + + var $feedItem = $(context._.template($('#template-feed-recording').html(), options, {variable: 'data'})); + var $controls = $feedItem.find('.recording-controls'); + + $('.timeago', $feedItem).timeago(); + context.JK.prettyPrintElements($('.duration', $feedItem)); + context.JK.setInstrumentAssetPath($('.instrument-icon', $feedItem)); + $('.details', $feedItem).click(toggleRecordingDetails); + $('.details-arrow', $feedItem).click(toggleRecordingDetails); + $('.play-button', $feedItem).click(toggleRecordingPlay); + updateIsPublic($feedItem, options.candidate_claimed_recording.is_public); + + $('.btn-share', $feedItem).click(function() { + ui.launchShareDialog(options.candidate_claimed_recording.id, 'recording'); + }); + + $('.btn-comment', $feedItem).click(function() { + ui.launchCommentDialog({ + recording_id: feed.id, + claimed_recording_id: options.candidate_claimed_recording.id, + entity_type: 'recording' + }) + .one(EVENTS.DIALOG_CLOSED, function() { + updateStats($feedItem); + }); + }); + + $('.btn-like', $feedItem).click(function() { + ui.addRecordingLike(feed.id, options.candidate_claimed_recording.id, JK.currentUserId, $('.likes', $feedItem), $('.btn-like', $feedItem)); + }); + + if(isOwner()) { + $('.edit-recording-dialog', $feedItem).data('claimed_recording_id', options.candidate_claimed_recording.id).click(function() { + app.layout.showDialog('edit-recording', {d1: $(this).data('claimed_recording_id')}) + .one(EVENTS.DIALOG_CLOSED, function() { + $(this).unbind('recording_updated').unbind('recording_deleted'); + }) + .one('recording_updated', function(e, data) { + // find recording by claimed recording id + var $feedEntry = $screen.find('.feed-entry.recording-entry[data-claimed-recording-id="'+ data.id +'"]'); + var $musicians = $feedEntry.find('.musician-detail'); + var $description = $feedEntry.find('.description'); + var $name = $feedEntry.find('.name'); + var $detailsLink = $feedEntry.find('.details'); + var toggledOpen = $detailsLink.data('toggledOpen'); + + if(toggledOpen) { + toggleClose($feedEntry, $name, $description, $musicians, true); + } + + $description.trigger('destroy.dot'); + $name.trigger('destroy.dot'); + + updateRecordingName($feedEntry, data.name); + updateRecordingDescription($feedEntry, data.description); + updateIsPublic($feedEntry, data.is_public); + updateGenre($feedEntry, data.genre); + + $name.dotdotdot(); + $description.dotdotdot(); + $feedItem.data('original-max-height', $feedEntry.css('height')); + + $detailsLink.data('toggledOpen', false); + }) + .one('recording_deleted', function(e, data) { + var $feedEntry = $screen.find('.feed-entry.recording-entry[data-claimed-recording-id="'+ data.id +'"]'); + $feedEntry.remove(); + }) + return false; + }).show(); + } + + // put the feed item on the page + renderFeed($feedItem); + + // these routines need the item to have height to work (must be after renderFeed) + $controls.listenRecording({recordingId: feed.id, claimedRecordingId: options.candidate_claimed_recording.id, sliderSelector:'.recording-slider', sliderBarSelector: '.recording-playback', currentTimeSelector:'.recording-current'}); + $controls.bind('statechange.listenRecording', stateChangeRecording); + $('.dotdotdot', $feedItem).dotdotdot(); + $feedItem.data('original-max-height', $feedItem.css('height')); + context.JK.bindHoverEvents($feedItem); + context.JK.bindProfileClickEvents($feedItem); + } + else { + logger.warn("skipping feed type: " + feed.type); + } + + context.JK.bindProfileClickEvents(); + }); + } + + function renderFeed(feed) { + $content.append(feed); + } + + function search() { + logger.debug("Searching for feeds..."); + refresh(); + return false; + } + + function events() { + $refresh.on("click", search); + $sortFeedBy.on('change', search); + $includeDate.on('change', search); + $includeType.on('change', search); + } + + function initialize(_$parent, _$scroller, _$content, _$noMorefeeds, _$refresh, _$sortFeedBy, _$includeDate, _$includeType) { + $screen = _$parent; + $scroller = _$scroller; + $content = _$content; + $noMoreFeeds = _$noMorefeeds; + $refresh = _$refresh; + $sortFeedBy = _$sortFeedBy; + $includeDate = _$includeDate; + $includeType = _$includeType; + + if($screen.length == 0) throw "$screen must be specified"; + if($scroller.length == 0) throw "$scroller must be specified"; + if($content.length == 0) throw "$content must be specified"; + if($noMoreFeeds.length == 0) throw "$noMoreFeeds must be specified"; + if($refresh.length == 0) throw "$refresh must be specified"; + if($sortFeedBy.length == 0) throw "$sortFeedBy must be specified"; + if($includeDate.length == 0) throw "$includeDate must be specified"; + if($includeType.length ==0) throw "$includeType must be specified"; + + // set default search criteria + $sortFeedBy.val('date') + $includeDate.val('month') + $includeType.val('all') + + events(); + } + + this.initialize = initialize; + this.refresh = refresh; + this.setUser = setUser; + + return this; + } +})(window, jQuery) \ No newline at end of file diff --git a/web/app/assets/javascripts/findSession.js b/web/app/assets/javascripts/findSession.js index c246e5fd2..396c0b813 100644 --- a/web/app/assets/javascripts/findSession.js +++ b/web/app/assets/javascripts/findSession.js @@ -99,10 +99,8 @@ // date filter var date = $('#session-date-filter').val(); if (date !== null && date.length > 0) { - console.log(date); currentQuery.day = context.JK.formatDateYYYYMMDD(date); - // console.log("currentQuery.day=%o", currentQuery.day); - currentQuery.timezone_offset = new Date().getTimezoneOffset(); + currentQuery.timezone_offset = (new Date().getTimezoneOffset()) / 60; } // language filter @@ -160,7 +158,6 @@ clearResults(); buildQuery(); loadSessions(); - context.JK.guardAgainstBrowser(app); } function clearResults() { diff --git a/web/app/assets/javascripts/genreSelector.js b/web/app/assets/javascripts/genreSelector.js index 7dabdf2b0..42c2ca442 100644 --- a/web/app/assets/javascripts/genreSelector.js +++ b/web/app/assets/javascripts/genreSelector.js @@ -7,21 +7,12 @@ "use strict"; context.JK = context.JK || {}; + context.JK.GenreSelectorDeferred = null; context.JK.GenreSelectorHelper = (function() { var logger = context.JK.logger; var _genres = []; // will be list of structs: [ {label:xxx, value:yyy}, {...}, ... ] - function loadGenres() { - var url = "/api/genres"; - $.ajax({ - type: "GET", - url: url, - async: false, // do this synchronously so the event handlers in events() can be wired up - success: genresLoaded - }); - } - function reset(parentSelector, defaultGenre) { defaultGenre = typeof(defaultGenre) == 'undefined' ? '' : defaultGenre; $('select', parentSelector).val(defaultGenre); @@ -38,7 +29,7 @@ function render(parentSelector) { $('select', parentSelector).empty(); - $('select', parentSelector).append(''); + $('select', parentSelector).append(''); var template = $('#template-genre-option').html(); $.each(_genres, function(index, value) { // value will be a dictionary entry from _genres: @@ -67,6 +58,21 @@ return selectedGenres; } + function getNameForId(genreId) { + var name = null; + context._.each(_genres, function(genre) { + if(genreId == genre.value) { + name = genre.label; + return false; + } + }); + + if(!name) { + logger.warn("no genre found for genreId: " + genreId); + } + return name; + } + function setSelectedGenres(parentSelector, genreList) { if (!genreList) { return; @@ -79,18 +85,31 @@ $('select', parentSelector).val(values[0]); } - function initialize() { - loadGenres(); + function initialize(app) { + // XXX; _instruments should be populated in a template, rather than round-trip to server + if(!context.JK.GenreSelectorDeferred) { + // this dance is to make sure there is only one server request instead of InstrumentSelector instances * + context.JK.GenreSelectorDeferred = rest.getGenres() + } + + context.JK.GenreSelectorDeferred + .done(function(response) {genresLoaded(response)}) + .fail(app.ajaxError) + + return this; } var me = { // This will be our singleton. initialize: initialize, getSelectedGenres: getSelectedGenres, setSelectedGenres: setSelectedGenres, + getNameForId : getNameForId, getSelectedGenresValues: getSelectedGenresValues, reset: reset, - render: render, - loadGenres: loadGenres + render: function() { + var _args = arguments; + context.JK.GenreSelectorDeferred.done(function(){render.apply(self, _args)}) + } }; return me; diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index d7749f353..8243324b5 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -139,7 +139,8 @@ } function updateSession(id, newSession) { - return $.ajax('/api/sessions/' + id, { + return $.ajax({ + url: '/api/sessions/' + id, type: "PUT", data : newSession, dataType : 'json' @@ -954,6 +955,27 @@ }); } + function updateClaimedRecording(options) { + var claimedRecordingId = options["id"]; + return $.ajax({ + type: "PUT", + dataType: "json", + url: '/api/claimed_recordings/' + claimedRecordingId, + contentType: 'application/json', + processData: false, + data: JSON.stringify(options) + }); + } + + function deleteClaimedRecording(id) { + return $.ajax({ + type: "DELETE", + dataType: "json", + contentType: 'application/json', + url: "/api/claimed_recordings/" + id + }); + } + function claimRecording(options) { var recordingId = options["id"]; @@ -1220,6 +1242,8 @@ this.getRecording = getRecording; this.getClaimedRecordings = getClaimedRecordings; this.getClaimedRecording = getClaimedRecording; + this.updateClaimedRecording = updateClaimedRecording; + this.deleteClaimedRecording = deleteClaimedRecording; this.claimRecording = claimRecording; this.startPlayClaimedRecording = startPlayClaimedRecording; this.stopPlayClaimedRecording = stopPlayClaimedRecording; diff --git a/web/app/assets/javascripts/layout.js b/web/app/assets/javascripts/layout.js index e087a2820..43aaf0d7b 100644 --- a/web/app/assets/javascripts/layout.js +++ b/web/app/assets/javascripts/layout.js @@ -644,8 +644,9 @@ function showDialog(dialog, options) { if (dialogEvent(dialog, 'beforeShow', options) === false) { - return; + return null; } + logger.debug("opening dialog: " + dialog) var $overlay = $('.dialog-overlay') if (opts.sizeOverlayToContent) { diff --git a/web/app/assets/javascripts/networkTestHelper.js b/web/app/assets/javascripts/networkTestHelper.js index c93b5dce4..098764b4e 100644 --- a/web/app/assets/javascripts/networkTestHelper.js +++ b/web/app/assets/javascripts/networkTestHelper.js @@ -17,6 +17,7 @@ var PAYLOAD_SIZE = 100; var MINIMUM_ACCEPTABLE_SESSION_SIZE = 2; + var gearUtils = context.JK.GearUtils; var rest = context.JK.Rest(); var logger = context.JK.logger; var $step = null; @@ -201,6 +202,7 @@ storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); } else if(reason == "server_comm_timeout") { + gearUtils.skipNetworkTest(); context.JK.alertSupportedNeeded("Communication with the JamKazam network service has timed out." + appendContextualStatement()); renderStopTest('', ''); storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); @@ -211,11 +213,13 @@ storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); } else if(reason == "invalid_response") { + gearUtils.skipNetworkTest(); context.JK.alertSupportedNeeded("The JamKazam client software had an unexpected problem while scoring your Internet connection.

Reason: " + attempt.backend_data.reason + '.'); renderStopTest('', ''); storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); } else if(reason == 'no_servers') { + gearUtils.skipNetworkTest(); context.JK.alertSupportedNeeded("No network test servers are available." + appendContextualStatement()); renderStopTest('', ''); testedSuccessfully = true; @@ -227,18 +231,21 @@ storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.noNetwork); } else if(reason == "rest_api_error") { + gearUtils.skipNetworkTest(); context.JK.alertSupportedNeeded("Unable to acquire a network test server." + appendContextualStatement()); testedSuccessfully = true; renderStopTest('', ''); storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); } else if(reason == "timeout") { + gearUtils.skipNetworkTest(); context.JK.alertSupportedNeeded("Communication with the JamKazam network service timed out." + appendContextualStatement()); testedSuccessfully = true; renderStopTest('', ''); storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); } else { + gearUtils.skipNetworkTest(); context.JK.alertSupportedNeeded("The JamKazam client software had a logic error while scoring your Internet connection."); renderStopTest('', ''); storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); diff --git a/web/app/assets/javascripts/profile.js b/web/app/assets/javascripts/profile.js index b8e2a5810..bc6277302 100644 --- a/web/app/assets/javascripts/profile.js +++ b/web/app/assets/javascripts/profile.js @@ -13,6 +13,7 @@ var sentFriendRequest = false; var profileScreen = null; var textMessageDialog = null; + var feed = null; var instrument_logo_map = context.JK.getInstrumentIconMap24(); @@ -30,6 +31,7 @@ function beforeShow(data) { userId = data.id; + feed.setUser(userId); } function afterShow(data) { @@ -37,6 +39,10 @@ resetForm(); } + function beforeHide(data) { + feed.setUser(null); + } + function resetForm() { $('#profile-instruments').empty(); @@ -571,7 +577,7 @@ } function bindHistory() { - + feed.refresh(); } /****************** BANDS TAB *****************/ @@ -752,15 +758,31 @@ function bindFavorites() { } + function initializeFeed() { + + var $scroller = profileScreen.find('.content-body-scroller'); + var $content = profileScreen.find('.feed-content'); + var $noMoreFeeds = $('#end-of-feeds-list'); + var $refresh = profileScreen.find('.btn-refresh-entries'); + var $sortFeedBy = profileScreen.find('#feed_order_by'); + var $includeDate = profileScreen.find('#feed_date'); + var $includeType = profileScreen.find('#feed_show'); + + feed = new context.JK.Feed(app); + feed.initialize(profileScreen, $scroller, $content, $noMoreFeeds, $refresh, $sortFeedBy, $includeDate, $includeType); + } + function initialize(textMessageDialogInstance) { textMessageDialog = textMessageDialogInstance; var screenBindings = { 'beforeShow': beforeShow, - 'afterShow': afterShow + 'afterShow': afterShow, + 'beforeHide' : beforeHide }; app.bindScreen('profile', screenBindings); profileScreen = $('#user-profile'); events(); + initializeFeed(); } this.initialize = initialize; diff --git a/web/app/assets/javascripts/recordingFinishedDialog.js b/web/app/assets/javascripts/recordingFinishedDialog.js index 90061b63e..972badf3e 100644 --- a/web/app/assets/javascripts/recordingFinishedDialog.js +++ b/web/app/assets/javascripts/recordingFinishedDialog.js @@ -98,10 +98,10 @@ id: recording.id }) .done(function () { - console.error("recording discarded by user. recordingId=%o", recording.id); + logger.debug("recording discarded by user. recordingId=%o", recording.id); }) .fail(function (jqXHR) { - console.error("recording discard by user failed. recordingId=%o. reason: %o", recording.id, jqXHR.responseText); + logger.error("recording discard by user failed. recordingId=%o. reason: %o", recording.id, jqXHR.responseText); }) .always(function () { app.layout.closeDialog('recordingFinished') diff --git a/web/app/assets/javascripts/rsvpCancelDialog.js b/web/app/assets/javascripts/rsvpCancelDialog.js index a0a610cb5..2156cec2e 100644 --- a/web/app/assets/javascripts/rsvpCancelDialog.js +++ b/web/app/assets/javascripts/rsvpCancelDialog.js @@ -34,7 +34,7 @@ } function showDialog() { - app.layout.showDialog('rsvp-cancel-dialog'); + return app.layout.showDialog('rsvp-cancel-dialog'); } function events() { diff --git a/web/app/assets/javascripts/rsvpSubmitDialog.js b/web/app/assets/javascripts/rsvpSubmitDialog.js index 281b8120b..9f7ea2e59 100644 --- a/web/app/assets/javascripts/rsvpSubmitDialog.js +++ b/web/app/assets/javascripts/rsvpSubmitDialog.js @@ -55,7 +55,7 @@ } function showDialog() { - app.layout.showDialog(dialogId); + return app.layout.showDialog(dialogId); } function events() { diff --git a/web/app/assets/javascripts/scheduled_session.js b/web/app/assets/javascripts/scheduled_session.js index 6b17e425a..6766b51cf 100644 --- a/web/app/assets/javascripts/scheduled_session.js +++ b/web/app/assets/javascripts/scheduled_session.js @@ -5,6 +5,7 @@ context.JK = context.JK || {}; context.JK.CreateScheduledSession = function(app) { + var gearUtils = context.JK.GearUtils; var logger = context.JK.logger; var rest = JK.Rest(); var invitationDialog = null; @@ -188,9 +189,7 @@ function beforeShowStep5() { var startType = null; - if (createSessionSettings.createType == 'start-scheduled' || - createSessionSettings.createType == 'immediately' || - createSessionSettings.createType == 'quick-start') { + if (willOptionStartSession()) { startType = 'Now!'; createSessionSettings.startType = "START SESSION"; } @@ -305,6 +304,10 @@ if (createSessionSettings.createType == 'start-scheduled') { var session = scheduledSessions[createSessionSettings.selectedSessionId]; + if(session == null) { + // TODO: notify user they need to pick session? Or maybe it should be grayed out. + return false; + } var moveToFinish = function() { app.layout.closeDialog('confirm'); createSessionSettings.startDate = new Date(session.scheduled_start).toDateString(); @@ -488,7 +491,27 @@ function beforeMoveStep5() { } + function startSessionClicked() { + + if(willOptionStartSession()) { + gearUtils.guardAgainstInvalidConfiguration(app) + .fail(function() { + app.notify( + { title: "Unable to Start New Session", + text: "You can only start a session once you have working audio gear and a tested internet connection." + }) + }) + .done(function(){ + startSession(); + }); + } + else { + startSession(); + } + } + function startSession() { + var data = {}; if (createSessionSettings.createType == 'start-scheduled') { @@ -563,22 +586,15 @@ }); } - var tracks = context.JK.TrackHelpers.getUserTracks(context.jamClient); - if(tracks.length == 0) { - logger.error("we should never have 0 tracks and have gotten this far. Launch FTUE is the best we can do right now") - // If user hasn't completed FTUE - do so now. - app.afterFtue = function() { startSession(); }; - app.layout.startNewFtue(); - return false; - } - var joinSession = function(sessionId) { + var tracks = context.JK.TrackHelpers.getUserTracks(context.jamClient); + var options = {}; options.client_id = app.clientId; options.session_id = sessionId; options.as_musician = true; options.tracks = tracks; - rest.joinSession(options) + rest.joinSession(options) .done(function(response) { var invitationCount = data.invitations.length; @@ -600,16 +616,19 @@ app.notifyServerError(jqXHR, "Unable to Create Session"); } }) + }; if (createSessionSettings.createType == 'start-scheduled') { joinSession(createSessionSettings.selectedSessionId); + $('#create-session-buttons .btn-next').off('click'); } else { rest.createScheduledSession(data) .done(function(response) { + logger.debug("created session on server"); + $('#create-session-buttons .btn-next').off('click'); var newSessionId = response.id; - $(".btn-next").off('click'); if (createSessionSettings.createType == 'quick-start' || createSessionSettings.createType == "immediately") { joinSession(newSessionId); @@ -620,6 +639,7 @@ } }) .fail(function(jqXHR){ + logger.debug("unable to schedule a session") app.notifyServerError(jqXHR, "Unable to schedule a session"); }); } @@ -734,7 +754,7 @@ if (step == STEP_SELECT_CONFIRM) { $btnNext.html(createSessionSettings.startType); - $btnNext.on('click', startSession); + $btnNext.on('click', startSessionClicked); } else $btnNext.on('click', next); @@ -764,7 +784,20 @@ return false; } + // will this option result in a session being started? + function willOptionStartSession() { + return createSessionSettings.createType == 'start-scheduled' || + createSessionSettings.createType == 'immediately' || + createSessionSettings.createType == 'quick-start'; + } + function next(event) { + if(willOptionStartSession()) { + if(!context.JK.guardAgainstBrowser(app)) { + return false; + } + } + var valid = beforeMoveStep(); if (!valid) { return false; @@ -820,7 +853,7 @@ } function afterShow() { - context.JK.guardAgainstBrowser(app); + } function getFormattedTime(date, change) { diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index fd5bc3bee..1a1b0bb77 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -4,6 +4,7 @@ context.JK = context.JK || {}; context.JK.SessionScreen = function(app) { + var gearUtils = context.JK.GearUtils; var logger = context.JK.logger; var self = this; var sessionModel = null; @@ -114,23 +115,25 @@ checkForCurrentUser(); } + + function afterShow(data) { - if(!context.JK.JamServer.connected) { - promptLeave = false; - app.notifyAlert("Not Connected", 'To create or join a session, you must be connected to the server.'); - window.location = '/client#/home' - return; - } + if(!context.JK.JamServer.connected) { + promptLeave = false; + app.notifyAlert("Not Connected", 'To create or join a session, you must be connected to the server.'); + window.location = '/client#/home' + return; + } - if (!context.JK.hasOneConfiguredDevice() || context.JK.TrackHelpers.getUserTracks(context.jamClient).length == 0) { - app.afterFtue = function() { initializeSession(); }; - app.cancelFtue = function() { promptLeave = false; window.location = '/client#/home' }; - app.layout.startNewFtue(); - } - else { + gearUtils.guardAgainstInvalidConfiguration(app) + .fail(function() { + promptLeave = false; + window.location = '/client#/home' + }) + .done(function(){ initializeSession(); - } + }) } function notifyWithUserInfo(title , text, clientId) { diff --git a/web/app/assets/javascripts/sessionList.js b/web/app/assets/javascripts/sessionList.js index c02313117..f0cb814c0 100644 --- a/web/app/assets/javascripts/sessionList.js +++ b/web/app/assets/javascripts/sessionList.js @@ -4,6 +4,7 @@ context.JK = context.JK || {}; context.JK.SessionList = function(app) { + var gearUtils = context.JK.GearUtils; var logger = context.JK.logger; var rest = context.JK.Rest(); var ui = new context.JK.UIHelper(app); @@ -11,18 +12,17 @@ var $inactiveSessionTemplate = $('#template-inactive-session-row'); var $notationFileTemplate = $('#template-notation-files'); var $openSlotsTemplate = $('#template-open-slots'); - var $pendingInvitationsTemplate = $('#template-pending-invitations'); var $latencyTemplate = $('#template-latency'); var $musicianTemplate = $('#template-musician-info'); var showJoinLink = true; var showRsvpLink = true; var LATENCY = { - GOOD : {description: "GOOD", style: "latency-green", min: 0.0, max: 20.0}, - MEDIUM : {description: "MEDIUM", style: "latency-yellow", min: 20.0, max: 40.0}, - POOR : {description: "POOR", style: "latency-red", min: 40.0, max: 10000000000.0}, - UNREACHABLE: {description: "UNREACHABLE", style: "latency-grey", min: -1, max: -1}, - UNKNOWN: {description: "UNKNOWN", style: "latency-grey", min: -2, max: -2} + GOOD : {description: "GOOD", style: "latency-green", min: 0.0, max: 20.0}, + MEDIUM : {description: "MEDIUM", style: "latency-yellow", min: 20.0, max: 40.0}, + POOR : {description: "POOR", style: "latency-red", min: 40.0, max: 10000000000.0}, + UNREACHABLE: {description: "UNREACHABLE", style: "latency-grey", min: -1, max: -1}, + UNKNOWN: {description: "UNKNOWN", style: "latency-grey", min: -2, max: -2} }; var instrument_logo_map = context.JK.getInstrumentIconMap24(); @@ -41,7 +41,7 @@ showJoinLink = session.musician_access; // render musicians who are already in the session - if (session.active_music_session && "participants" in session.active_music_session) { + if (session.active_music_session && "participants" in session.active_music_session && session.active_music_session.participants.length > 0) { for (i=0; i < session.active_music_session.participants.length; i++) { inSessionUsers.push(session.active_music_session.participants[i].user.id); var inSessionUserInfo = createInSessionUser(session.active_music_session.participants[i]); @@ -49,11 +49,16 @@ latencyHtml += inSessionUserInfo[1]; } } + // this provides a buffer at the top to shift the first latency tag down in the event there are NO in-session musicians + else { + latencyHtml += "
 
"; + } // render users who have approved RSVPs if (session.approved_rsvps) { for (i=0; i < session.approved_rsvps.length; i++) { - if (!(session.approved_rsvps[i].id in inSessionUsers)) { + // do not show the user in this section if he is already in the session + if ($.inArray(session.approved_rsvps[i].id, inSessionUsers) === -1) { if (session.approved_rsvps[i].id === context.JK.currentUserId) { showJoinLink = true; } @@ -61,6 +66,9 @@ rsvpUsersHtml += rsvpUserInfo[0]; latencyHtml += rsvpUserInfo[1]; } + else { + showJoinLink = true; + } } } @@ -89,18 +97,25 @@ // wire up the Join Link to the T&Cs dialog var $parentRow = $('tr[id=' + session.id + ']', tbGroup); $('.join-link', $parentRow).click(function(evt) { + if(!context.JK.guardAgainstBrowser(app)) { + return false; + } + if (!context.JK.JamServer.connected) { app.notifyAlert("Not Connected", 'To create or join a session, you must be connected to the server.'); return false; } - // If no FTUE, show that first. - if (!context.JK.hasOneConfiguredDevice() || context.JK.TrackHelpers.getUserTracks(context.jamClient).length == 0) { - app.afterFtue = function() { joinClick(session.id); }; - app.layout.startNewFtue(); - } - else { - joinClick(session.id); - } + + gearUtils.guardAgainstInvalidConfiguration(app) + .fail(function() { + app.notify( + { title: "Unable to Join Session", + text: "You can only join a session once you have working audio gear and a tested internet connection." + }) + }) + .done(function(){ + joinClick(session.id); + }) return false; }); @@ -119,10 +134,10 @@ $('#actionHeader', tbGroup).html('RSVP'); var i = 0; - var rsvpUsersHtml = '', openSlotsHtml = '', pendingInvitationsHtml = '', latencyHtml = '', notationFileHtml = ''; + var rsvpUsersHtml = '', openSlotsHtml = '', latencyHtml = '', notationFileHtml = ''; // render users who have approved RSVPs - if (session.approved_rsvps) { + if (session.approved_rsvps && session.approved_rsvps.length > 0) { for (i=0; i < session.approved_rsvps.length; i++) { if (session.approved_rsvps[i].id === context.JK.currentUserId) { hasApprovedRsvp = true; @@ -132,6 +147,10 @@ latencyHtml += rsvpUserInfo[1]; } } + // this provides a buffer at the top to shift the first latency tag down in the event there are NO RSVP musicians + else { + latencyHtml += "
 
"; + } // render open slots if (session.open_slots) { @@ -147,7 +166,6 @@ if (session.pending_invitations[i].id === context.JK.currentUserId) { hasInvitation = true; } - pendingInvitationsHtml += createPendingInvitation(session.pending_invitations[i]); } } @@ -166,7 +184,6 @@ } var sessionVals = buildSessionObject(session, notationFileHtml, rsvpUsersHtml, openSlotsHtml, latencyHtml); - sessionVals.pending_invitations = pendingInvitationsHtml.length > 0 ? pendingInvitationsHtml : 'N/A'; sessionVals.rsvp_link_display_style = showRsvpLink ? "block" : "none"; var row = context.JK.fillTemplate($inactiveSessionTemplate.html(), sessionVals); @@ -319,15 +336,6 @@ return context.JK.fillTemplate($openSlotsTemplate.html(), slot); } - function createPendingInvitation(user) { - - var invitationVals = { - avatar_url: context.JK.resolveAvatarUrl(user.photo_url) - }; - - return context.JK.fillTemplate($pendingInvitationsTemplate.html(), invitationVals); - } - function createNotationFile(notation) { var notationVals = { file_url: notation.file_url, diff --git a/web/app/assets/javascripts/sessionSettingsDialog.js b/web/app/assets/javascripts/sessionSettingsDialog.js index 603704484..041ee32dd 100644 --- a/web/app/assets/javascripts/sessionSettingsDialog.js +++ b/web/app/assets/javascripts/sessionSettingsDialog.js @@ -4,6 +4,12 @@ context.JK.SessionSettingsDialog = function(app, sessionScreen) { var logger = context.JK.logger; var $dialog; + var $screen = $('#session-settings'); + var $selectedFilenames = $screen.find('#selected-filenames'); + var $uploadSpinner = $screen.find($('.upload-spinner')); + var $selectedFilenames = $('#settings-selected-filenames'); + var $inputFiles = $('#settings-select-files'); + var $btnSelectFiles = $screen.find('.btn-select-files'); var rest = new JK.Rest(); function beforeShow(data) { @@ -19,7 +25,6 @@ // genre context.JK.GenreSelectorHelper.setSelectedGenres('#session-settings-genre', currentSession.genres); - // name $('#session-settings-name').val(currentSession.name); @@ -56,7 +61,12 @@ } // notation files - + $selectedFilenames.empty(); + for (var i=0; i < currentSession.music_notations.length; i++) { + var notation = currentSession.music_notations[i]; + console.log('notation.file_name %o', notation.file_name); + $selectedFilenames.append('' + notation.file_name + ' '); + } context.JK.dropdown($('#session-settings-language')); context.JK.dropdown($('#session-settings-musician-access')); @@ -67,7 +77,7 @@ var data = {}; - data.genre = $('#session-settings-genre').val(); + data.genre = context.JK.GenreSelectorHelper.getSelectedGenres('#session-settings-genre')[0]; data.name = $('#session-settings-name').val(); data.description = $('#session-settings-description').val(); data.language = $('#session-settings-language').val(); @@ -105,6 +115,78 @@ rest.updateSession($('#session-settings-id').val(), data).done(settingsSaved); } + function changeSelectedFiles() { + var fileNames = []; + var files = $inputFiles.get(0).files; + var error = false; + for (var i = 0; i < files.length; ++i) { + var name = files.item(i).name; + var ext = name.split('.').pop(); + if ($.inArray(ext, ["pdf", "png", "jpg", "jpeg", "gif", "xml", "mxl", "txt"]) == -1) { + error = true; + break; + } + fileNames.push(name); + } + + if (error) { + app.notifyAlert("Error", "We're sorry, but we do not allow upload of that file type. Please upload only the file types listed in the Upload dialog box."); + $inputFiles.replaceWith($inputFiles.clone(true)); + } + else { + } + + // upload as soon as user picks their files. + uploadNotations($inputFiles.get(0).files) + .done(function() { + context._.each(fileNames, function(fileName) { + $selectedFilenames.append(fileName); + }) + }) + } + + function uploadNotations(notations) { + var formData = new FormData(); + $.each(notations, function(i, file) { + formData.append('files[]', file); + }); + + formData.append('client_id', app.clientId); + + $btnSelectFiles.text('UPLOADING...').data('uploading', true) + $uploadSpinner.show(); + return rest.uploadMusicNotations(formData) + .done(function(response) { + var error_files = []; + $.each(response, function(i, music_notation) { + if (music_notation.errors) { + //error_files.push(createSessionSettings.notations[i].name); + } + }) + if (error_files.length > 0) { + app.notifyAlert("Failed to upload notations.", error_files.join(', ')); + } + }) + .fail(function(jqXHR) { + app.notifyServerError(jqXHR, "Unable to upload music notations"); + }) + .always(function() { + $btnSelectFiles.text('SELECT FILES...').data('uploading', null) + $uploadSpinner.hide(); + }) + } + + function toggleSelectFiles(event) { + if($btnSelectFiles.data('uploading')) { + logger.debug("ignoring click of SELECT FILES... while uploading") + return false; + } + + event.preventDefault(); + $('#session-select-files').trigger('click'); + return false; + } + function settingsSaved(response) { // No response returned from this call. 204. sessionScreen.refreshCurrentSession(true); @@ -113,6 +195,9 @@ function events() { $('#session-settings-dialog-submit').on('click', saveSettings); + + $inputFiles.on('change', changeSelectedFiles); + $btnSelectFiles.on('click', toggleSelectFiles); } this.initialize = function() { diff --git a/web/app/assets/javascripts/shareDialog.js b/web/app/assets/javascripts/shareDialog.js index dc3bca311..0d3fbcfda 100644 --- a/web/app/assets/javascripts/shareDialog.js +++ b/web/app/assets/javascripts/shareDialog.js @@ -336,7 +336,7 @@ } function showDialog() { - app.layout.showDialog('share-dialog'); + return app.layout.showDialog('share-dialog'); } // function initDialog() { diff --git a/web/app/assets/javascripts/ui_helper.js b/web/app/assets/javascripts/ui_helper.js index 9bff51a7d..0ed51e8d0 100644 --- a/web/app/assets/javascripts/ui_helper.js +++ b/web/app/assets/javascripts/ui_helper.js @@ -26,25 +26,25 @@ function launchCommentDialog(options) { var commentDialog = new JK.CommentDialog(JK.app, options); commentDialog.initialize(); - commentDialog.showDialog(); + return commentDialog.showDialog(); } function launchShareDialog(entityId, entityType) { var shareDialog = new JK.ShareDialog(JK.app, entityId, entityType); shareDialog.initialize(JK.FacebookHelperInstance); - shareDialog.showDialog(); + return shareDialog.showDialog(); } function launchRsvpSubmitDialog(sessionId) { var rsvpDialog = new JK.RsvpSubmitDialog(JK.app, sessionId); rsvpDialog.initialize(); - rsvpDialog.showDialog(); + return rsvpDialog.showDialog(); } function launchRsvpCancelDialog(sessionId, rsvpRequestId) { var rsvpDialog = new JK.RsvpCancelDialog(JK.app, sessionId, rsvpRequestId); rsvpDialog.initialize(); - rsvpDialog.showDialog(); + return rsvpDialog.showDialog(); } this.addSessionLike = addSessionLike; diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index 586186111..592c06b06 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -936,8 +936,11 @@ context.JK.guardAgainstBrowser = function(app, args) { if(!gon.isNativeClient) { + logger.debug("guarding against normal browser on screen thaht requires native client") app.layout.showDialog('launch-app-dialog', args) + return false; } + return true; } /* diff --git a/web/app/assets/javascripts/web/session_info.js b/web/app/assets/javascripts/web/session_info.js index 0426fedf0..fba2b7d55 100644 --- a/web/app/assets/javascripts/web/session_info.js +++ b/web/app/assets/javascripts/web/session_info.js @@ -10,8 +10,15 @@ var ui = new context.JK.UIHelper(app); var $btnAction = $("#btn-action"); + var LATENCY = { + GOOD : {description: "GOOD", style: "latency-green", min: 0.0, max: 20.0}, + MEDIUM : {description: "MEDIUM", style: "latency-yellow", min: 20.0, max: 40.0}, + POOR : {description: "POOR", style: "latency-red", min: 40.0, max: 10000000000.0}, + UNREACHABLE: {description: "UNREACHABLE", style: "latency-grey", min: -1, max: -1}, + UNKNOWN: {description: "UNKNOWN", style: "latency-grey", min: -2, max: -2} + }; + function addComment(musicSessionId) { - console.log("here"); var comment = $("#txtSessionInfoComment").val(); if ($.trim(comment).length > 0) { rest.addSessionInfoComment(musicSessionId, comment) diff --git a/web/app/assets/javascripts/wizard/gear_utils.js b/web/app/assets/javascripts/wizard/gear_utils.js index 30ba2e686..6eb7aa2eb 100644 --- a/web/app/assets/javascripts/wizard/gear_utils.js +++ b/web/app/assets/javascripts/wizard/gear_utils.js @@ -13,6 +13,11 @@ var ASSIGNMENT = context.JK.ASSIGNMENT; var VOICE_CHAT = context.JK.VOICE_CHAT; var AUDIO_DEVICE_BEHAVIOR = context.JK.AUDIO_DEVICE_BEHAVIOR; + var EVENTS = context.JK.EVENTS; + + gearUtils.SKIPPED_NETWORK_TEST = -1; // we store a negative 1 to mean that we let the user skip. + + gearUtils.skippedNetworkTest = false; // we allow someone to play in session (for one client run) if it's our fault they can't network test score // checks if it's an assigned OUTPUT or ASSIGNED CHAT gearUtils.isChannelAssigned = function (channel) { @@ -271,4 +276,88 @@ app.notifyServerError(jqXHR, "Unable to sync audio latency") }); } + + // if the user has a good user network score, immediately returns with a resolved deferred object. + // if not, the user will have the network test dialog prompted... once it's closed, then you'll be told reject() if score is still bad, or resolve() if now good + gearUtils.guardAgainstBadNetworkScore = function(app) { + var deferred = new $.Deferred(); + + if (!gearUtils.validNetworkScore()) { + // invalid network test score. They have to score to move on + app.layout.showDialog('network-test').one(EVENTS.DIALOG_CLOSED, function() { + if(gearUtils.validNetworkScore()) { + deferred.resolve(); + } + else { + deferred.reject(); + } + }); + } + else { + deferred.resolve(); + } + return deferred; + } + + // XXX this isn't quite right... it needs to check if a good device is *active* + // but seen too many problems so far with the backend not reporting any profile active + gearUtils.hasGoodActiveProfile = function(app) { + return context.JK.hasOneConfiguredDevice() && context.JK.TrackHelpers.getUserTracks(context.jamClient).length > 0 + } + + // if the user does not have a currently active, good profile, then they are made to deal with it + gearUtils.guardAgainstInvalidGearConfiguration = function(app) { + var deferred = new $.Deferred(); + + if (!gearUtils.hasGoodActiveProfile()) { + app.layout.showDialog('gear-wizard').one(EVENTS.DIALOG_CLOSED, function() { + if(gearUtils.hasGoodActiveProfile() && gearUtils.validNetworkScore()) { + deferred.resolve(); + } + else { + deferred.reject(); + } + }); + } + else { + deferred.resolve(); + } + + return deferred; + } + + // tests both device config, and network score + gearUtils.guardAgainstInvalidConfiguration = function(app) { + var deferred = new $.Deferred(); + gearUtils.guardAgainstInvalidGearConfiguration(app) + .fail(function() { + deferred.reject(); + }) + .done(function() { + gearUtils.guardAgainstBadNetworkScore(app) + .fail(function() { + deferred.reject(); + }) + .done(function() { + deferred.resolve(); + }) + }) + + return deferred; + } + + gearUtils.skipNetworkTest = function() { + context.jamClient.SetNetworkTestScore(gearUtils.SKIPPED_NETWORK_TEST); + gearUtils.skippedNetworkTest = true; + } + + gearUtils.isNetworkTestSkipped = function() { + return gearUtils.skippedNetworkTest; + } + + gearUtils.validNetworkScore = function() { + return gearUtils.skippedNetworkTest || context.jamClient.GetNetworkTestScore() >= 2; + } + + })(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/client.css b/web/app/assets/stylesheets/client/client.css index 7dba0e458..0fe1feba3 100644 --- a/web/app/assets/stylesheets/client/client.css +++ b/web/app/assets/stylesheets/client/client.css @@ -54,6 +54,7 @@ *= require ./textMessageDialog *= require ./acceptFriendRequestDialog *= require ./launchAppDialog + *= require ./editRecordingDialog *= require ./iconInstrumentSelect *= require ./terms *= require ./createSession diff --git a/web/app/assets/stylesheets/client/content.css.scss b/web/app/assets/stylesheets/client/content.css.scss index b74d26068..d5e335f43 100644 --- a/web/app/assets/stylesheets/client/content.css.scss +++ b/web/app/assets/stylesheets/client/content.css.scss @@ -340,6 +340,41 @@ a.arrow-down { width:200px; } +.btn-select-files { + margin-top: 10px; + margin-left:0; + width:110px; + @include border_box_sizing; +} + +.spinner-small.upload-spinner { + display:none; + position: absolute; + left: 0px; + margin-top: 4px; +} + +.select-files-section { + position:absolute; +} + +#settings-selected-filenames { + font-size:12px; + + span { + white-space:nowrap; + text-overflow:ellipsis; + overflow:hidden; + display:block; + } +} + +.selected-files-section { + overflow: hidden; + width: 100%; + @include border_box_sizing; +} + #session-controls { width:100%; padding:6px 0px 11px 0px; diff --git a/web/app/assets/stylesheets/client/editRecordingDialog.css.scss b/web/app/assets/stylesheets/client/editRecordingDialog.css.scss new file mode 100644 index 000000000..5a38282a4 --- /dev/null +++ b/web/app/assets/stylesheets/client/editRecordingDialog.css.scss @@ -0,0 +1,79 @@ +@import "client/common"; + +#edit-recording-dialog { + + min-height:330px; + + input, textarea { + @include border_box_sizing; + } + + .field { + margin-top:20px; + + &:nth-of-type(1) { + margin-top:0; + } + } + + .buttons { + float:right; + clear:both; + margin-top:20px; + } + + label[for="name"] { + + } + input[name="name"] { + margin-top:5px; + width:100%; + } + + label[for="description"] { + + } + textarea[name="description"] { + margin-top:5px; + width:100%; + } + + label[for="genre"] { + display:inline; + float:left; + line-height:26px; + vertical-align:middle; + } + select[name="genre"] { + float:left; + margin-left:5px; + } + + .genre-selector { + float:left; + .dropdown-wrapper { + margin-left:5px; + } + } + + label[for="is_public"] { + display: inline; + float:right; + line-height: 26px; + padding-right: 5px; + vertical-align: middle; + } + select[name="is_public"] { + float:right; + } + div[purpose="is_public"] { + float:right; + .icheckbox_minimal { + float:right; + margin-top:4px; + } + } + + + +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/feed.css.scss b/web/app/assets/stylesheets/client/feed.css.scss index 4dbd60d4f..a69727e00 100644 --- a/web/app/assets/stylesheets/client/feed.css.scss +++ b/web/app/assets/stylesheets/client/feed.css.scss @@ -2,4 +2,9 @@ .recording-current { position:absolute; // solves a problem with duration wrapping--only in firefox } + + .btn-refresh-holder { + float:right; + margin-right:10px; + } } \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/jamServer.css.scss b/web/app/assets/stylesheets/client/jamServer.css.scss index 44955e99f..6eab98e23 100644 --- a/web/app/assets/stylesheets/client/jamServer.css.scss +++ b/web/app/assets/stylesheets/client/jamServer.css.scss @@ -66,4 +66,3 @@ } } - diff --git a/web/app/assets/stylesheets/client/jamkazam.css.scss b/web/app/assets/stylesheets/client/jamkazam.css.scss index e4256229f..16d357c82 100644 --- a/web/app/assets/stylesheets/client/jamkazam.css.scss +++ b/web/app/assets/stylesheets/client/jamkazam.css.scss @@ -328,6 +328,7 @@ input[type="text"], input[type="password"]{ textarea { font-size:15px; + padding:3px; } diff --git a/web/app/assets/stylesheets/client/profile.css.scss b/web/app/assets/stylesheets/client/profile.css.scss index 746f783e3..ff45db852 100644 --- a/web/app/assets/stylesheets/client/profile.css.scss +++ b/web/app/assets/stylesheets/client/profile.css.scss @@ -317,4 +317,60 @@ #btn-add-friend { display:none; +} + +#profile-history { + padding:0 10px 0 20px; + width:100%; + position:relative; + height:100%; + @include border_box_sizing; + + #user-feed-controls { + width:100%; + @include border_box_sizing; + position:relative; + display:none; + } + + .btn-refresh-holder { + left: 95%; // 5 * 19% to right-align 5 user blocks (in conjunction with the margin-left + margin-left: -65px; + position: absolute; + } + + .filter-body { + bottom: 0; + right: 0; + top: 0; + left: 20px; + position: absolute; + width: 95%; + padding-top:0; + margin-top:10px; + height:auto; + } + + .profile-wrapper { + padding: 10px 0; + } + + .feed-entry .feed-details { + margin-right:5px; + } + + .recording-current { + position:absolute; // solves a problem with duration wrapping--only in firefox + } + + .content-body-scroller { + height: 100%; + width: 100%; + position: absolute; + } + + + .no-feed-msg { + text-align:center + } } \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/sessionList.css.scss b/web/app/assets/stylesheets/client/sessionList.css.scss index db441a3ae..f6bf67216 100644 --- a/web/app/assets/stylesheets/client/sessionList.css.scss +++ b/web/app/assets/stylesheets/client/sessionList.css.scss @@ -22,9 +22,15 @@ table.findsession-table, table.local-recordings { } td.latency { + padding-top: 15px !important; text-align:center !important; } + td.latency div.center { + display: inline-block; + margin: auto; + } + .noborder { border-right:none; } @@ -65,6 +71,7 @@ table.findsession-table, table.local-recordings { font-weight:200; font-size:11px; background-color:#868686; + text-align:center; } .latency-green { @@ -74,6 +81,7 @@ table.findsession-table, table.local-recordings { font-weight:200; font-size:11px; background-color:#71a43b; + text-align:center; } .latency-yellow { @@ -83,6 +91,7 @@ table.findsession-table, table.local-recordings { font-weight:200; font-size:11px; background-color:#cc9900; + text-align:center; } .latency-red { @@ -92,6 +101,7 @@ table.findsession-table, table.local-recordings { font-weight:200; font-size:11px; background-color:#980006; + text-align:center; } .avatar-tiny { diff --git a/web/app/assets/stylesheets/web/audioWidgets.css.scss b/web/app/assets/stylesheets/web/audioWidgets.css.scss index da7a933f1..1fcd93f85 100644 --- a/web/app/assets/stylesheets/web/audioWidgets.css.scss +++ b/web/app/assets/stylesheets/web/audioWidgets.css.scss @@ -272,6 +272,20 @@ } } + a.edit-recording-dialog { + font-size:12px; + display:none; + } + + .is_private { + display:none; + font-size:12px; + font-style:italic; + &.enabled { + display:inline; + } + } + .play-count { margin-right:10px; } diff --git a/web/app/assets/stylesheets/web/main.css.scss b/web/app/assets/stylesheets/web/main.css.scss index 3cd310634..c0ec227e9 100644 --- a/web/app/assets/stylesheets/web/main.css.scss +++ b/web/app/assets/stylesheets/web/main.css.scss @@ -589,4 +589,8 @@ strong { fieldset.login-error .login-error-msg { display:block; } +} + +body.jam.web.welcome .no-websocket-connection { + display:none; } \ No newline at end of file diff --git a/web/app/assets/stylesheets/web/sessions.css.scss b/web/app/assets/stylesheets/web/sessions.css.scss index ef513c304..f0a39eb7c 100644 --- a/web/app/assets/stylesheets/web/sessions.css.scss +++ b/web/app/assets/stylesheets/web/sessions.css.scss @@ -1,23 +1,54 @@ -/*.session-controls { - background-color:#471f18; -} - -.session-controls.inprogress { - background-color:#4C742E; -} - -.session-status-ended, .session-status { - float:left; - font-size:18px; -} - -.session-status-inprogress { - float:left; - font-size:15px; - color:#cccc00; - margin-left:20px; -}*/ - #btnPlayPause { position: relative; +} + +table.musicians { + margin-top:-3px; +} + +table.musicians td { + border-right:none; + border-top:none; + padding:2px; + vertical-align:middle !important; + } + +.latency-grey { + width: 50px; + height: 10px; + font-family:Arial, Helvetica, sans-serif; + font-weight:200; + font-size:11px; + background-color:#868686; + text-align:center; +} + +.latency-green { + width: 50px; + height: 10px; + font-family:Arial, Helvetica, sans-serif; + font-weight:200; + font-size:11px; + background-color:#71a43b; + text-align:center; +} + +.latency-yellow { + width: 50px; + height: 10px; + font-family:Arial, Helvetica, sans-serif; + font-weight:200; + font-size:11px; + background-color:#cc9900; + text-align:center; +} + +.latency-red { + width: 40px; + height: 10px; + font-family:Arial, Helvetica, sans-serif; + font-weight:200; + font-size:11px; + background-color:#980006; + text-align:center; } \ No newline at end of file diff --git a/web/app/controllers/api_claimed_recordings_controller.rb b/web/app/controllers/api_claimed_recordings_controller.rb index 61eaa9de2..04cef8d3f 100644 --- a/web/app/controllers/api_claimed_recordings_controller.rb +++ b/web/app/controllers/api_claimed_recordings_controller.rb @@ -19,30 +19,20 @@ class ApiClaimedRecordingsController < ApiController end def update - if @claimed_recording.user_id != current_user.id raise PermissionError, 'only owner of claimed_recording can update it' end - begin - @claimed_recording.update_fields(current_user, params) - respond_with responder: ApiResponder, :status => 204 - rescue - render :json => { :message => "claimed_recording could not be updated" }, :status => 403 - end + @claimed_recording.update_fields(current_user, params) + respond_with @claimed_recording end def delete if @claimed_recording.user_id != current_user.id raise PermissionError, 'only owner of claimed_recording can update it' end - #begin - #@claimed_recording.discard(current_user) - #render :json => {}, :status => 204 -# respond_with responder: ApiResponder, :status => 204 - #rescue - #render :json => { :message => "claimed_recording could not be deleted" }, :status => 403 - #end + @claimed_recording.discard(current_user) + render :json => {}, :status => 200 end def download diff --git a/web/app/controllers/api_music_sessions_controller.rb b/web/app/controllers/api_music_sessions_controller.rb index aa2f37331..501edc025 100644 --- a/web/app/controllers/api_music_sessions_controller.rb +++ b/web/app/controllers/api_music_sessions_controller.rb @@ -82,7 +82,6 @@ class ApiMusicSessionsController < ApiController end end - def scheduled @music_sessions = MusicSession.scheduled(current_user) end @@ -175,7 +174,7 @@ class ApiMusicSessionsController < ApiController @music_session.music_session, params[:name], params[:description], - params[:genre], + params[:genre] ? Genre.find(params[:genre]) : nil, params[:language], params[:musician_access], params[:approval_required], diff --git a/web/app/controllers/api_recordings_controller.rb b/web/app/controllers/api_recordings_controller.rb index 478a3aeee..75e3ea6f4 100644 --- a/web/app/controllers/api_recordings_controller.rb +++ b/web/app/controllers/api_recordings_controller.rb @@ -205,6 +205,7 @@ class ApiRecordingsController < ApiController end end + private def parse_filename @recorded_track = RecordedTrack.find_by_recording_id_and_client_track_id!(params[:id], params[:track_id]) diff --git a/web/app/controllers/music_sessions_controller.rb b/web/app/controllers/music_sessions_controller.rb index 226142a58..19f37c7c2 100644 --- a/web/app/controllers/music_sessions_controller.rb +++ b/web/app/controllers/music_sessions_controller.rb @@ -47,9 +47,15 @@ class MusicSessionsController < ApplicationController # run these 3 queries only if the user has access to the page if @can_view - @approved_rsvps = @music_session.approved_rsvps - @open_slots = @music_session.open_slots - @pending_invitations = @music_session.pending_invitations + ActiveRecord::Base.transaction do + @music_sessions, @user_scores = MusicSession.sms_index(current_user, {:session_id => params[:id], :client_id => cookies[:client_id]}) + unless @music_sessions.blank? + ms = @music_sessions[0] + @approved_rsvps = ms.approved_rsvps + @open_slots = ms.open_slots + @pending_invitations = ms.pending_invitations + end + end end render :layout => "web" diff --git a/web/app/views/api_feeds/show.rabl b/web/app/views/api_feeds/show.rabl index 03a8ad2c5..140828833 100644 --- a/web/app/views/api_feeds/show.rabl +++ b/web/app/views/api_feeds/show.rabl @@ -140,7 +140,7 @@ glue :recording do child(:claimed_recordings => :claimed_recordings) { - attributes :id, :name, :description, :is_public, :genre_id, :has_mix? + attributes :id, :name, :description, :is_public, :genre_id, :has_mix?, :user_id child(:user => :creator) { attributes :id, :first_name, :last_name, :photo_url diff --git a/web/app/views/api_music_sessions/show.rabl b/web/app/views/api_music_sessions/show.rabl index 1eebd1728..15e1ef234 100644 --- a/web/app/views/api_music_sessions/show.rabl +++ b/web/app/views/api_music_sessions/show.rabl @@ -19,6 +19,14 @@ else [item.genre.description] # XXX: need to return single genre; not array end + child(:music_notations => :music_notations) { + attributes :id, :file_name + + node do |music_notation| + { file_url: music_notation["file_url"] } + end + } + if :is_recording? node do |music_session| { :recording => partial("api_recordings/show", :object => music_session.recording) } @@ -44,7 +52,6 @@ else } } - child({:invitations => :invitations}) { attributes :id, :sender_id, :receiver_id } diff --git a/web/app/views/api_music_sessions/show_history.rabl b/web/app/views/api_music_sessions/show_history.rabl index 1da5fc3ab..0ff523c36 100644 --- a/web/app/views/api_music_sessions/show_history.rabl +++ b/web/app/views/api_music_sessions/show_history.rabl @@ -17,7 +17,7 @@ if !current_user else attributes :id, :music_session_id, :name, :description, :musician_access, :approval_required, :fan_access, :fan_chat, - :band_id, :user_id, :genre_id, :created_at, :like_count, :comment_count, :scheduled_start, :scheduled_duration, + :band_id, :user_id, :genre_id, :created_at, :like_count, :comment_count, :play_count, :scheduled_start, :scheduled_duration, :language, :recurring_mode, :language_description, :scheduled_start_time, :access_description, :timezone, :timezone_description, :musician_access_description, :fan_access_description, :session_removed_at, :legal_policy, :open_rsvps diff --git a/web/app/views/api_recordings/show.rabl b/web/app/views/api_recordings/show.rabl index 633816ba0..8b75f0384 100644 --- a/web/app/views/api_recordings/show.rabl +++ b/web/app/views/api_recordings/show.rabl @@ -28,7 +28,7 @@ child(:comments => :comments) { child(:claimed_recordings => :claimed_recordings) { - attributes :id, :name, :description, :is_public, :genre_id + attributes :id, :name, :description, :is_public, :genre_id, :has_mix?, :user_id node :share_url do |claimed_recording| unless claimed_recording.share_token.nil? diff --git a/web/app/views/clients/_banner.html.erb b/web/app/views/clients/_banner.html.erb deleted file mode 100644 index 1bcde7cbd..000000000 --- a/web/app/views/clients/_banner.html.erb +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/web/app/views/clients/_banner.html.haml b/web/app/views/clients/_banner.html.haml new file mode 100644 index 000000000..9e53fb0e1 --- /dev/null +++ b/web/app/views/clients/_banner.html.haml @@ -0,0 +1,33 @@ +#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"}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 it. + %li If you are still running the JamKazam application, you will need to stop it before executing the last step. + %li Find the JamKazam application in the Applications folder, and double-click the icon to launch it! \ No newline at end of file diff --git a/web/app/views/clients/_edit_recording_dialog.html.haml b/web/app/views/clients/_edit_recording_dialog.html.haml new file mode 100644 index 000000000..b1b944c4d --- /dev/null +++ b/web/app/views/clients/_edit_recording_dialog.html.haml @@ -0,0 +1,24 @@ +.dialog.configure-tracks{ layout: 'dialog', 'layout-id' => 'edit-recording', id: 'edit-recording-dialog'} + .content-head + = image_tag "content/icon_add.png", {:width => 19, :height => 19, :class => 'content-icon' } + %h1 Edit Recording + .dialog-inner + %form + .field + %label{for: 'name'} Recording name: + %input{type: 'text', name: 'name'} + .field + %label{for: 'description'} Description: + %textarea{name: 'description', rows: '4'} + .field.genre-selector + %label{for: 'genre'} Genre: + %select{name:'genre'} + .field{purpose: 'is_public'} + %input{type: 'checkbox', name: 'is_public'} + %label{for: 'is_public'} Public Recording + + .buttons + %a.button-grey.cancel-btn CANCEL + %a.button-orange.delete-btn DELETE + %a.button-orange.save-btn UPDATE + %br{clear: 'all'} \ No newline at end of file diff --git a/web/app/views/clients/_findSession.html.erb b/web/app/views/clients/_findSession.html.erb index 050f5658c..49c0110b1 100644 --- a/web/app/views/clients/_findSession.html.erb +++ b/web/app/views/clients/_findSession.html.erb @@ -81,14 +81,14 @@ - + - +
{name}{name} ({genres})
{description}
Notation Files:Notation Files: {notation_files}
@@ -121,21 +121,19 @@ - - - {latency} -
+ +
+ + {latency} +
+
- - - - - - - - + + + +
Chat Language:
{language}
Musician Access:
{musician_access}
Fan Access:
{fan_access}
Legal Policy:
{legal_policy}
Chat Language:
{language}
Musician Access:
{musician_access}
Fan Access:
{fan_access}
Legal Policy:
{legal_policy}
@@ -152,7 +150,7 @@ - + @@ -182,31 +180,21 @@
{name}{name} ({genres})
- - Invited: - - - {pending_invitations} -
- - - - - {latency} -
+ +
+ + {latency} +
+
- - - - - - - - + + + +
Chat Language:
{language}
Musician Access:
{musician_access}
Fan Access:
{fan_access}
Legal Policy:
{legal_policy}
Chat Language:
{language}
Musician Access:
{musician_access}
Fan Access:
{fan_access}
Legal Policy:
{legal_policy}
@@ -249,10 +237,6 @@ - -