From 0c3a43a1776f2d2fd32ade920daf6f9e3ebd6c96 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Mon, 18 Aug 2014 10:37:55 -0500 Subject: [PATCH 1/6] * wip --- db/manifest | 3 +- db/up/sms_index_single_session.sql | 157 ++++++++++++++++++ .../jam_ruby/models/active_music_session.rb | 11 +- ruby/lib/jam_ruby/models/generic_state.rb | 4 + ruby/lib/jam_ruby/models/geo_ip_locations.rb | 2 +- ruby/lib/jam_ruby/models/music_session.rb | 30 +++- ruby/lib/jam_ruby/models/search.rb | 4 +- ruby/lib/jam_ruby/models/user.rb | 35 ++-- .../models/active_music_session_spec.rb | 16 +- .../jam_ruby/models/music_session_spec.rb | 55 ++++-- ruby/spec/support/maxmind.rb | 12 +- .../javascripts/accounts_session_detail.js | 103 +++++------- web/app/assets/javascripts/findMusician.js | 7 +- web/app/assets/javascripts/findSession.js | 12 +- .../assets/javascripts/helpBubbleHelper.js | 29 ++++ web/app/assets/javascripts/sessionList.js | 94 ++++------- web/app/assets/javascripts/session_utils.js | 50 ++++++ .../assets/javascripts/web/session_info.js | 106 +++++------- web/app/assets/javascripts/web/web.js | 3 + .../stylesheets/client/account.css.scss | 3 +- .../assets/stylesheets/client/common.css.scss | 45 +++++ .../stylesheets/client/findSession.css.scss | 6 + .../assets/stylesheets/client/help.css.scss | 7 +- .../stylesheets/client/sessionList.css.scss | 38 ++++- .../stylesheets/web/session_info.css.scss | 56 +++++++ .../assets/stylesheets/web/sessions.css.scss | 51 ------ web/app/assets/stylesheets/web/web.css | 2 + .../api_music_sessions_controller.rb | 8 +- web/app/helpers/score_helper.rb | 10 +- .../views/api_music_sessions/ams_index.rabl | 9 +- .../api_music_sessions/show_history.rabl | 10 +- .../views/api_music_sessions/sms_index.rabl | 9 +- web/app/views/api_search/index.rabl | 2 +- web/app/views/api_users/show.rabl | 4 + .../clients/_account_session_detail.html.haml | 2 +- web/app/views/clients/_findSession.html.erb | 4 +- web/app/views/clients/_help.html.erb | 9 + web/app/views/clients/_musicians.html.erb | 9 - web/app/views/layouts/web.html.erb | 2 + .../music_sessions/session_info.html.haml | 5 +- web/lib/tasks/password.rake | 17 ++ .../api_music_sessions_controller_spec.rb | 58 +++---- .../controllers/api_users_controller_spec.rb | 23 ++- web/spec/features/find_sessions_spec.rb | 23 ++- web/spec/support/maxmind.rb | 12 +- .../lib/jam_websockets/router.rb | 2 +- 46 files changed, 780 insertions(+), 379 deletions(-) create mode 100644 db/up/sms_index_single_session.sql create mode 100644 web/app/assets/javascripts/helpBubbleHelper.js create mode 100644 web/app/assets/stylesheets/web/session_info.css.scss create mode 100644 web/lib/tasks/password.rake diff --git a/db/manifest b/db/manifest index 004eb74e6..ad2c35ab5 100755 --- a/db/manifest +++ b/db/manifest @@ -201,4 +201,5 @@ current_scores_use_median.sql current_scores_ams_index_sms_index_use_user_instrument.sql locidispid_in_score_histories.sql define_environment_in_db.sql -drop_session_invite_constraint.sql \ No newline at end of file +drop_session_invite_constraint.sql +sms_index_single_session.sql \ No newline at end of file diff --git a/db/up/sms_index_single_session.sql b/db/up/sms_index_single_session.sql new file mode 100644 index 000000000..07c56b4d1 --- /dev/null +++ b/db/up/sms_index_single_session.sql @@ -0,0 +1,157 @@ +-- changelog: +-- * allow session_id to be passed to sms_index, which skips all the access questions, and just makes sure you'll get back that session, with user scores +-- * in both sms_index and ams_index, in the user tmp table, return also internet score and the other user's audio latency + +-- check that the music_sessions does not currently have an active_music_sessions +CREATE OR REPLACE FUNCTION sms_index (my_user_id VARCHAR, my_locidispid BIGINT, my_audio_latency INTEGER, session_id VARCHAR) RETURNS VOID STRICT VOLATILE AS $$ + BEGIN + -- output table to hold tagged music sessions with latency + CREATE TEMPORARY TABLE sms_music_session_tmp (music_session_id VARCHAR(64) NOT NULL, tag INTEGER, latency INTEGER) ON COMMIT DROP; + + IF session_id = 'any' THEN + -- populate sms_music_session_tmp as all music sessions + -- XXX: we should pass in enough info to match pagination/query to reduce the impact of this step + INSERT INTO sms_music_session_tmp SELECT DISTINCT id, NULL::INTEGER AS tag, NULL::INTEGER AS latency + FROM music_sessions + WHERE (scheduled_start IS NULL OR scheduled_start > (NOW() - (interval '15 minute'))) + AND canceled = FALSE + AND id NOT IN (SELECT id FROM active_music_sessions); + + -- tag accepted rsvp as 1 + UPDATE sms_music_session_tmp q SET tag = 1 FROM rsvp_slots s, rsvp_requests_rsvp_slots rrs, rsvp_requests r WHERE + q.music_session_id = s.music_session_id AND + s.id = rrs.rsvp_slot_id AND + rrs.rsvp_request_id = r.id AND + r.user_id = my_user_id AND + rrs.chosen = TRUE AND + q.tag is NULL; + + -- tag invitation as 2 + UPDATE sms_music_session_tmp q SET tag = 2 FROM invitations i WHERE + q.music_session_id = i.music_session_id AND + i.receiver_id = my_user_id AND + q.tag IS NULL; + + -- musician access as 3 + UPDATE sms_music_session_tmp q SET tag = 3 FROM music_sessions m WHERE + q.music_session_id = m.id AND + m.open_rsvps = TRUE AND + q.tag IS NULL; + + -- delete anything not tagged + DELETE FROM sms_music_session_tmp WHERE tag IS NULL; + + ELSE + INSERT INTO sms_music_session_tmp SELECT DISTINCT id, NULL::INTEGER AS tag, NULL::INTEGER AS latency + FROM music_sessions + WHERE music_sessions.id = session_id; + END IF; + + -- output table to hold users involved in the sms_music_session_tmp sessions and their latency + CREATE TEMPORARY TABLE sms_users_tmp (music_session_id VARCHAR(64), user_id VARCHAR(64) NOT NULL, full_score INTEGER, audio_latency INTEGER, internet_score INTEGER) ON COMMIT DROP; + + IF my_audio_latency > -1 THEN + -- populate sms_users_tmp with users that have an approved RSVP for sessions in the sms_music_session_tmp table, accompanied with full latency and music session + INSERT INTO sms_users_tmp SELECT q.music_session_id, users.id, s.full_score AS full_score, s.a_audio_latency, s.score + FROM sms_music_session_tmp q + INNER JOIN rsvp_slots ON rsvp_slots.music_session_id = q.music_session_id + INNER JOIN rsvp_requests_rsvp_slots ON rsvp_requests_rsvp_slots.rsvp_slot_id = rsvp_slots.id + INNER JOIN rsvp_requests ON rsvp_requests.id = rsvp_requests_rsvp_slots.rsvp_request_id + INNER JOIN users ON rsvp_requests.user_id = users.id + LEFT OUTER JOIN current_scores s ON s.alocidispid = users.last_jam_locidispid + WHERE + s.blocidispid = my_locidispid; + + -- populate sms_users_tmp with invited users for session in the sms_music_session_tmp table, accompanied with full latency and music session + -- specify NULL for music_session_id, because we don't want RSVP users to affect the AVG computed for each session later + INSERT INTO sms_users_tmp SELECT NULL, users.id, s.full_score AS full_score, s.a_audio_latency, s.score + FROM sms_music_session_tmp q + INNER JOIN invitations ON invitations.music_session_id = q.music_session_id + INNER JOIN users ON invitations.receiver_id = users.id + LEFT OUTER JOIN current_scores s ON s.alocidispid = users.last_jam_locidispid + WHERE + s.blocidispid = my_locidispid AND + users.id NOT IN (SELECT user_id FROM sms_users_tmp); + END IF; + + -- calculate the average latency + UPDATE sms_music_session_tmp q SET latency = (select AVG(u.full_score) FROM sms_users_tmp u WHERE + q.music_session_id = u.music_session_id); + + RETURN; + END; +$$ LANGUAGE plpgsql; + + +-- my_audio_latency can have a special value of -1, which means 'unknown'. +CREATE OR REPLACE FUNCTION ams_index (my_user_id VARCHAR, my_locidispid BIGINT, my_audio_latency INTEGER) RETURNS VOID STRICT VOLATILE AS $$ + BEGIN + -- output table to hold tagged music sessions with latency + CREATE TEMPORARY TABLE ams_music_session_tmp (music_session_id VARCHAR(64) NOT NULL, tag INTEGER, latency INTEGER) ON COMMIT DROP; + + -- populate ams_music_session_tmp as all music sessions + INSERT INTO ams_music_session_tmp SELECT DISTINCT id, NULL::INTEGER AS tag, NULL::INTEGER AS latency + FROM active_music_sessions; + + -- TODO worry about active music session where my_user_id is the creator? + -- eh, maybe, but if the music session is active and you're the creator wouldn't you already be in it? + -- so maybe you're on another computer, so why care? plus seth is talking about auto rsvp'ing the session + -- for you, so maybe not a problem. + + -- tag accepted rsvp as 1 + UPDATE ams_music_session_tmp q SET tag = 1 FROM rsvp_slots s, rsvp_requests_rsvp_slots rrs, rsvp_requests r WHERE + q.music_session_id = s.music_session_id AND + s.id = rrs.rsvp_slot_id AND + rrs.rsvp_request_id = r.id AND + r.user_id = my_user_id AND + rrs.chosen = TRUE AND + q.tag is NULL; + + -- tag invitation as 2 + UPDATE ams_music_session_tmp q SET tag = 2 FROM invitations i WHERE + q.music_session_id = i.music_session_id AND + i.receiver_id = my_user_id AND + q.tag IS NULL; + + -- musician access as 3 + UPDATE ams_music_session_tmp q SET tag = 3 FROM music_sessions m WHERE + q.music_session_id = m.id AND + m.musician_access = TRUE AND + q.tag IS NULL; + + -- delete anything not tagged + DELETE FROM ams_music_session_tmp WHERE tag IS NULL; + + -- output table to hold users involved in the ams_music_session_tmp sessions and their latency + CREATE TEMPORARY TABLE ams_users_tmp (music_session_id VARCHAR(64), user_id VARCHAR(64) NOT NULL, full_score INTEGER, audio_latency INTEGER, internet_score INTEGER) ON COMMIT DROP; + + IF my_audio_latency > -1 THEN + -- populate ams_users_tmp with users that have a connection for sessions in the ams_music_session_tmp table, accompanied with full latency and music session + INSERT INTO ams_users_tmp SELECT c.music_session_id, c.user_id, s.full_score AS full_score, s.a_audio_latency, s.score + FROM ams_music_session_tmp q + INNER JOIN connections c ON c.music_session_id = q.music_session_id + LEFT OUTER JOIN current_scores s ON s.alocidispid = c.locidispid + WHERE s.blocidispid = my_locidispid; + + -- populate ams_users_tmp with users that have an approved RSVP for sessions inthe ams_music_session_tmp table, accompanied with full latency and music session + -- specify NULL for music_session_id, because we don't want RSVP users to affect the AVG computed for each session later + INSERT INTO ams_users_tmp SELECT NULL, users.id, s.full_score AS full_score, s.a_audio_latency, s.score + FROM ams_music_session_tmp q + INNER JOIN rsvp_slots ON rsvp_slots.music_session_id = q.music_session_id + INNER JOIN rsvp_requests_rsvp_slots ON rsvp_requests_rsvp_slots.rsvp_slot_id = rsvp_slots.id + INNER JOIN rsvp_requests ON rsvp_requests.id = rsvp_requests_rsvp_slots.rsvp_request_id + INNER JOIN users ON rsvp_requests.user_id = users.id + LEFT OUTER JOIN current_scores s ON s.alocidispid = users.last_jam_locidispid + WHERE + s.blocidispid = my_locidispid AND + rsvp_requests_rsvp_slots.chosen = TRUE AND + users.id NOT IN (SELECT user_id FROM ams_users_tmp); + END IF; + + -- calculate the average latency + UPDATE ams_music_session_tmp q SET latency = (select AVG(u.full_score) FROM ams_users_tmp u WHERE + q.music_session_id = u.music_session_id); + + RETURN; + END; +$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/active_music_session.rb b/ruby/lib/jam_ruby/models/active_music_session.rb index 9c9a5a07f..662512572 100644 --- a/ruby/lib/jam_ruby/models/active_music_session.rb +++ b/ruby/lib/jam_ruby/models/active_music_session.rb @@ -319,12 +319,9 @@ module JamRuby # initialize the two temporary tables we use to drive ams_index and ams_users def self.ams_init(current_user, options = {}) - client_id = options[:client_id] - - connection = Connection.where(user_id: current_user.id, client_id: client_id).first! - my_locidispid = connection.locidispid + my_locidispid = current_user.last_jam_locidispid # 13 is an average audio gear value we use if they have not qualified any gear - my_audio_latency = connection.last_jam_audio_latency || current_user.last_jam_audio_latency || 13 + my_audio_latency = current_user.last_jam_audio_latency || 13 locidispid_expr = my_locidispid ? "#{my_locidispid}::bigint" : '0::bigint' self.connection.execute("select ams_index('#{current_user.id}'::varchar, #{locidispid_expr}, #{my_audio_latency}::integer)").check @@ -420,7 +417,7 @@ module JamRuby # ams_init must be called first. # user.audio_latency / 2 , + other_user.audio_latency of them / 2, + network latency /2 def self.ams_users - return User.select('users.*, ams_users_tmp.music_session_id, ams_users_tmp.latency') + return User.select('users.*, ams_users_tmp.music_session_id, ams_users_tmp.full_score, ams_users_tmp.audio_latency, ams_users_tmp.internet_score') .joins( %Q{ INNER JOIN @@ -444,7 +441,7 @@ module JamRuby user_scores = {} music_session_users.each do |user| - user_scores[user.id] = {latency: user.latency} + user_scores[user.id] = {full_score: user.full_score, audio_latency: user.audio_latency, internet_score: user.internet_score} end [music_sessions, user_scores] diff --git a/ruby/lib/jam_ruby/models/generic_state.rb b/ruby/lib/jam_ruby/models/generic_state.rb index 546006794..d6c0bc687 100644 --- a/ruby/lib/jam_ruby/models/generic_state.rb +++ b/ruby/lib/jam_ruby/models/generic_state.rb @@ -6,6 +6,10 @@ module JamRuby validates :env, :inclusion => {:in => ['development', 'staging', 'production', 'test']} + def self.env + GenericState.singleton.env + end + def self.allow_emails? database_environment = singleton.env diff --git a/ruby/lib/jam_ruby/models/geo_ip_locations.rb b/ruby/lib/jam_ruby/models/geo_ip_locations.rb index 490ad79ca..1f8caeb50 100644 --- a/ruby/lib/jam_ruby/models/geo_ip_locations.rb +++ b/ruby/lib/jam_ruby/models/geo_ip_locations.rb @@ -51,7 +51,7 @@ module JamRuby end end - {city: city, state: state, country: country, addr: addr, locidispid: (locid.nil? || ispid.nil?) ? nil : locid*1000000+ispid} + {city: city, state: state, country: country, addr: addr, locidispid: (locid.nil? || ispid.nil?) ? nil : Score.compute_locidispid(locid, ispid) } end def self.createx(locid, countrycode, region, city, postalcode, latitude, longitude, metrocode, areacode) diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index 6ea00de1d..f522889ad 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -588,15 +588,14 @@ module JamRuby # initialize the two temporary tables we use to drive sms_index and sms_users def self.sms_init(current_user, options = {}) - client_id = options[:client_id] + session_id = options[:session_id] || 'any' - connection = Connection.where(user_id: current_user.id, client_id: client_id).first! - my_locidispid = connection.locidispid + my_locidispid = current_user.last_jam_locidispid # 13 is an average audio gear value we use if they have not qualified any gear - my_audio_latency = connection.last_jam_audio_latency || current_user.last_jam_audio_latency || 13 + my_audio_latency = current_user.last_jam_audio_latency || 13 locidispid_expr = my_locidispid ? "#{my_locidispid}::bigint" : '0::bigint' # Have to pass in zero; NULL fails silently in the stored proc - self.connection.execute("SELECT sms_index('#{current_user.id}'::varchar, #{locidispid_expr}, #{my_audio_latency}::integer)").check + self.connection.execute("SELECT sms_index('#{current_user.id}'::varchar, #{locidispid_expr}, #{my_audio_latency}::integer, #{ActiveRecord::Base.connection.quote(session_id)}::varchar)").check end # Generate a list of music sessions (that are active) filtered by genre, language, keyword, and sorted @@ -689,7 +688,7 @@ module JamRuby # sms_init must be called first. # user.audio_latency / 2 , + other_user.audio_latency of them / 2, + network latency /2 def self.sms_users - return User.select('users.*, sms_users_tmp.music_session_id, sms_users_tmp.latency') + return User.select('users.*, sms_users_tmp.music_session_id, sms_users_tmp.full_score, sms_users_tmp.audio_latency, sms_users_tmp.internet_score') .joins( %Q{ INNER JOIN @@ -713,12 +712,29 @@ module JamRuby user_scores = {} music_session_users.each do |user| - user_scores[user.id] = {latency: user.latency} + user_scores[user.id] = {full_score: user.full_score, audio_latency: user.audio_latency, internet_score: user.internet_score} end + [music_sessions, user_scores] end + # returns a single session, but populates any other user info with latency scores, so that show_history.rabl can do it's business + def self.session_with_scores(current_user, music_session_id) + MusicSession.sms_init(current_user, {session_id: music_session_id}) + + music_session = MusicSession.find(music_session_id) + + music_session_users = MusicSession.sms_users.all + + user_scores = {} + music_session_users.each do |user| + user_scores[user.id] = {full_score: user.full_score, audio_latency: user.audio_latency, internet_score: user.internet_score} + end + + [music_session, user_scores] + end + def self.upcoming_sessions end diff --git a/ruby/lib/jam_ruby/models/search.rb b/ruby/lib/jam_ruby/models/search.rb index 761660c84..cdfd3e1fb 100644 --- a/ruby/lib/jam_ruby/models/search.rb +++ b/ruby/lib/jam_ruby/models/search.rb @@ -225,8 +225,8 @@ module JamRuby rel = rel.where(['current_scores.full_score > ?', score_min]) unless score_min.nil? rel = rel.where(['current_scores.full_score <= ?', score_max]) unless score_max.nil? - rel = rel.select('current_scores.full_score, current_scores.score, current_scores.b_audio_latency as audio_latency, regions.regionname') - rel = rel.group('current_scores.full_score, current_scores.score, current_scores.b_audio_latency, regions.regionname') + rel = rel.select('current_scores.full_score, current_scores.score, round(current_scores.a_audio_latency) as audio_latency, regions.regionname') + rel = rel.group('current_scores.full_score, current_scores.score, current_scores.a_audio_latency, regions.regionname') end ordering = self.order_param(params) diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 06d7d97e4..15eb46d2f 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -320,12 +320,26 @@ module JamRuby read_attribute(:music_session_id) end - def latency - return nil unless has_attribute?(:latency) - a = read_attribute(:latency) + # ===== ARTIFICIAL ATTRIBUTES CREATED BY ActiveMusicSession.ams_users, MusicSession.sms_uses + def full_score + return nil unless has_attribute?(:full_score) + a = read_attribute(:full_score) a.nil? ? nil : a.to_i end + def internet_score + return nil unless has_attribute?(:internet_score) + a = read_attribute(:internet_score) + a.nil? ? nil : a.to_i + end + + def audio_latency + return nil unless has_attribute?(:audio_latency) + a = read_attribute(:audio_latency) + a.nil? ? nil : a.to_i + end + # ====== END ARTIFICAL ATTRIBUTES + # mods comes back as text; so give ourselves a parsed version def mods_json @mods_json ||= mods ? JSON.parse(mods, symbolize_names: true) : {} @@ -1239,15 +1253,16 @@ module JamRuby end def update_audio_latency(connection, audio_latency) + if audio_latency > 2 + # updating the connection is best effort + if connection + connection.last_jam_audio_latency = audio_latency + connection.save + end - # updating the connection is best effort - if connection - connection.last_jam_audio_latency = audio_latency - connection.save + self.last_jam_audio_latency = audio_latency + self.save end - - self.last_jam_audio_latency = audio_latency - self.save end def top_followings 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 03f1a0bac..4297796a3 100644 --- a/ruby/spec/jam_ruby/models/active_music_session_spec.rb +++ b/ruby/spec/jam_ruby/models/active_music_session_spec.rb @@ -376,17 +376,25 @@ describe ActiveMusicSession do users.length.should == 2 if users[0].music_session_id == earlier_session.id users[0].id.should == creator.id - users[0].latency.should == 15 # (5 + 20 + 5) / 2 + users[0].full_score.should == 30 # (5 + 20 + 5) + users[0].audio_latency = 5 + users[0].internet_score = 20 users[1].music_session_id == later_session.id users[1].id.should == creator2.id - users[1].latency.should == 22 # (5 + 30 + 10) / 2 + users[1].full_score.should == 45 # (5 + 30 + 10) + users[1].audio_latency.should == 10 + users[1].internet_score.should == 30 else users[0].music_session_id.should == later_session.id users[0].id.should == creator2.id - users[0].latency.should == 22 # (5 + 30 + 10) / 2 + users[0].full_score.should == 45 # (5 + 30 + 10) + users[0].audio_latency.should == 10 + users[0].internet_score.should == 30 users[1].music_session_id == earlier_session.id users[1].id.should == creator.id - users[1].latency.should == 15 # (5 + 20 + 5) / 2 + users[1].full_score.should == 30 # (5 + 20 + 5) + users[1].audio_latency.should == 5 + users[1].internet_score.should == 20 end end end diff --git a/ruby/spec/jam_ruby/models/music_session_spec.rb b/ruby/spec/jam_ruby/models/music_session_spec.rb index eb4857a9e..07d2521dd 100644 --- a/ruby/spec/jam_ruby/models/music_session_spec.rb +++ b/ruby/spec/jam_ruby/models/music_session_spec.rb @@ -411,6 +411,39 @@ describe MusicSession do end end + def session_with_scores(user, music_session_id) + ActiveRecord::Base.transaction do + return MusicSession.session_with_scores(user, music_session_id) + end + end + + describe "session_with_scores", no_transaction: true do + let(:conn) { FactoryGirl.create(:connection, user: creator) } + let(:searcher) { FactoryGirl.create(:user) } + let(:searcher_conn) { FactoryGirl.create(:connection, user: searcher, ip_address: '2.2.2.2') } + let(:default_opts) { {client_id: searcher_conn.client_id} } + let(:network_score) { 20 } + + before(:each) do + Score.createx(conn.locidispid, conn.client_id, conn.addr, searcher_conn.locidispid, searcher_conn.client_id, searcher_conn.addr, network_score, nil, nil, {auserid: creator.id, buserid: searcher.id}) + end + + it "invalid session ID" do + expect {session_with_scores(searcher, 'blah')}.to raise_error(ActiveRecord::RecordNotFound) + end + + it "one session with scores" do + creator.last_jam_locidispid = conn.locidispid + creator.save! + + session = FactoryGirl.create(:music_session, creator: creator) + music_session, user_scores = session_with_scores(searcher, session.id) + music_session.latency.should be_nil # we don't return music_session.latency with session_with_scores, because it's used for sorting among many sessions + user_scores.length.should == 1 + user_scores[creator.id][:full_score].should == (network_score + searcher.last_jam_audio_latency + creator.last_jam_audio_latency ) + end + end + describe "sms_index", no_transaction: true do describe "simple" do @@ -461,9 +494,9 @@ describe MusicSession do music_sessions, user_scores = sms(searcher, default_opts) music_sessions.length.should == 1 music_sessions[0].tag.should == 3 # open session sort - music_sessions[0].latency.should == (network_score + searcher.last_jam_audio_latency + creator.last_jam_audio_latency ) / 2 + music_sessions[0].latency.should == (network_score + searcher.last_jam_audio_latency + creator.last_jam_audio_latency ) user_scores.length.should == 1 - user_scores[creator.id][:latency].should == (network_score + searcher.last_jam_audio_latency + creator.last_jam_audio_latency ) / 2 + user_scores[creator.id][:full_score].should == (network_score + searcher.last_jam_audio_latency + creator.last_jam_audio_latency ) end it "filters sessions in the past" do @@ -504,19 +537,19 @@ describe MusicSession do music_sessions, user_scores = sms(searcher, default_opts) music_sessions.length.should == 1 music_sessions[0].tag.should == 3 # open session sort - music_sessions[0].latency.should == (network_score + searcher.last_jam_audio_latency + creator.last_jam_audio_latency ) / 2 + music_sessions[0].latency.should == (network_score + searcher.last_jam_audio_latency + creator.last_jam_audio_latency ) user_scores.length.should == 2 # the creator, and the invitee - user_scores[creator.id][:latency].should == (network_score + searcher.last_jam_audio_latency + creator.last_jam_audio_latency ) / 2 - user_scores[invitee.id][:latency].should == ((network_score + searcher.last_jam_audio_latency + invitee.last_jam_audio_latency ) / 2).ceil + user_scores[creator.id][:full_score].should == (network_score + searcher.last_jam_audio_latency + creator.last_jam_audio_latency ) + user_scores[invitee.id][:full_score].should == ((network_score + searcher.last_jam_audio_latency + invitee.last_jam_audio_latency )).ceil #search with the invitee this time. invitee_conn = FactoryGirl.create(:connection, user: invitee, ip_address: '3.3.3.3', locidispid: invitee.last_jam_locidispid) music_sessions, user_scores = sms(invitee, {client_id: invitee_conn.client_id}) music_sessions.length.should == 1 music_sessions[0].tag.should == 2 # invited sort - music_sessions[0].latency.should == ((network_score + invitee.last_jam_audio_latency + creator.last_jam_audio_latency ) / 2).ceil + music_sessions[0].latency.should == ((network_score + invitee.last_jam_audio_latency + creator.last_jam_audio_latency )).ceil user_scores.length.should == 1 # the creator, and the invitee - user_scores[creator.id][:latency].should == ((network_score + invitee.last_jam_audio_latency + creator.last_jam_audio_latency ) / 2).ceil + user_scores[creator.id][:full_score].should == ((network_score + invitee.last_jam_audio_latency + creator.last_jam_audio_latency )).ceil end it "does not show when it goes active" do @@ -631,20 +664,20 @@ describe MusicSession do music_session = music_sessions[0] music_session.should == music_session_1 music_session.tag.should == 1 # RSVP - music_session.latency.should == (bad_network_score + searcher_1.last_jam_audio_latency + creator_1.last_jam_audio_latency ) / 2 + music_session.latency.should == (bad_network_score + searcher_1.last_jam_audio_latency + creator_1.last_jam_audio_latency ) music_session = music_sessions[1] music_session.should == music_session_2 music_session.tag.should == 2 # INVITE - music_session.latency.should == (fair_network_score + searcher_1.last_jam_audio_latency + creator_2.last_jam_audio_latency ) / 2 + music_session.latency.should == (fair_network_score + searcher_1.last_jam_audio_latency + creator_2.last_jam_audio_latency ) music_session = music_sessions[2] music_session.should == music_session_3 music_session.tag.should == 3 # OPEN - music_session.latency.should == (good_network_score + searcher_1.last_jam_audio_latency + creator_3.last_jam_audio_latency ) / 2 + music_session.latency.should == (good_network_score + searcher_1.last_jam_audio_latency + creator_3.last_jam_audio_latency ) user_scores.length.should == 3 # the creator, and the invitee - user_scores[creator_1.id][:latency].should == (bad_network_score + searcher_1.last_jam_audio_latency + creator_1.last_jam_audio_latency ) / 2 + user_scores[creator_1.id][:full_score].should == (bad_network_score + searcher_1.last_jam_audio_latency + creator_1.last_jam_audio_latency ) # let's make music_session_3 invisible, and verify the count goes to 2 music_session_3.open_rsvps = false diff --git a/ruby/spec/support/maxmind.rb b/ruby/spec/support/maxmind.rb index d48a550e5..75cf0c542 100644 --- a/ruby/spec/support/maxmind.rb +++ b/ruby/spec/support/maxmind.rb @@ -159,12 +159,20 @@ def create_phony_database GeoIpBlocks.connection.execute("select generate_scores_dataset()").check end +def austin_ip + IPAddr.new(0x0FFFFFFF, Socket::AF_INET).to_s +end + +def dallas_ip + IPAddr.new(0x1FFFFFFF, Socket::AF_INET).to_s +end + # gets related models for an IP in the 1st block from the scores_better_test_data.sql def austin_geoip geoiplocation = GeoIpLocations.find_by_locid(17192) geoipblock = GeoIpBlocks.find_by_locid(17192) jamisp = JamIsp.find_by_beginip(geoipblock.beginip) - {jamisp: jamisp, geoiplocation: geoiplocation, geoipblock: geoipblock } + {jamisp: jamisp, geoiplocation: geoiplocation, geoipblock: geoipblock, locidispid: Score.compute_locidispid(geoiplocation.locid, geoipblock.coid)} end # gets related models for an IP in the 1st block from the scores_better_test_data.sql @@ -172,7 +180,7 @@ def dallas_geoip geoiplocation = GeoIpLocations.find_by_locid(667) geoipblock = GeoIpBlocks.find_by_locid(667) jamisp = JamIsp.find_by_beginip(geoipblock.beginip) - {jamisp: jamisp, geoiplocation: geoiplocation, geoipblock: geoipblock} + {jamisp: jamisp, geoiplocation: geoiplocation, geoipblock: geoipblock, locidispid: Score.compute_locidispid(geoiplocation.locid, geoipblock.coid)} end # attempts to make the creation of a score more straightforward. diff --git a/web/app/assets/javascripts/accounts_session_detail.js b/web/app/assets/javascripts/accounts_session_detail.js index a148f4241..e5062eb54 100644 --- a/web/app/assets/javascripts/accounts_session_detail.js +++ b/web/app/assets/javascripts/accounts_session_detail.js @@ -9,6 +9,7 @@ var logger = context.JK.logger; var rest = context.JK.Rest(); var sessionUtils = context.JK.SessionUtils; + var helpBubble = context.JK.HelpBubbleHelper; var sessionId = null; var sessionData = null; var rsvpData = null; @@ -24,15 +25,6 @@ var inviteMusiciansUtil = null; var friendInput=null; - - 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 beforeShow(data) { sessionId = data.id; loadSessionData(); @@ -202,65 +194,44 @@ var sessionInvitedHtml = generateSessionInvited(); var sessionPropertiesHtml = generateSessionProperties(); - var template = context._.template( + var $template = $(context._.template( $('#template-account-session-detail').html(), { has_pending: hasPending, is_owner: isOwner, notification_msg: sessionData.notification_msg, pending_rsvps: pendingRsvpHtml, session_rsvps: sessionRsvpsHtml, still_needed: sessionNeededHtml, invited_users: sessionInvitedHtml, session_properties: sessionPropertiesHtml, id: sessionData.id}, {variable: 'data'} - ); + )); - $sessionDetail.html(template); + var $offsetParent = $($sessionDetail).closest('.content-body'); + + $.each($template.find('.latency'), function(index, latencyBadge) { + var $latencyBadge = $(latencyBadge); + var full_score = $latencyBadge.attr('data-full-score') || null; + var internet_score = $latencyBadge.attr('data-internet-score') || null; + var audio_latency = $latencyBadge.attr('data-audio-latency') || null; + var latencyBadgeUserId = $latencyBadge.attr('data-user-id'); + var scoreOptions = {offsetParent: $offsetParent}; + app.user() + .done(function(userMe) { + helpBubble.scoreBreakdown($latencyBadge, context.JK.currentUserId == latencyBadgeUserId, full_score, userMe.last_jam_audio_latency, audio_latency, internet_score, scoreOptions); + }) + }); + + + $sessionDetail.html($template); events(); } - function createLatency(user) { - var latencyStyle = LATENCY.UNREACHABLE.style, latencyDescription = LATENCY.UNREACHABLE.description - if (user.id === context.JK.currentUserId) { - latencyStyle = LATENCY.GOOD.style, latencyDescription = LATENCY.GOOD.description; - } - - else { - var latency = user.latency; - - if (!latency || latency === 1000) { - // 1000 is a magical number returned by new scoring API to indicate one or more people in the session have an unknown score - latencyDescription = LATENCY.UNKNOWN.description; - latencyStyle = LATENCY.UNKNOWN.style; - } - else { - if (latency <= LATENCY.GOOD.max) { - latencyDescription = LATENCY.GOOD.description; - latencyStyle = LATENCY.GOOD.style; - } - else if (latency > LATENCY.MEDIUM.min && latency <= LATENCY.MEDIUM.max) { - latencyDescription = LATENCY.MEDIUM.description; - latencyStyle = LATENCY.MEDIUM.style; - } - else { - latencyDescription = LATENCY.POOR.description; - latencyStyle = LATENCY.POOR.style; - } - } - } - - return { - latency_style: latencyStyle, - latency_text: latencyDescription - }; - } - function generatePendingRsvps() { var resultHtml = ""; var rsvpHtml = ""; var instrumentLogoHtml = ""; var latencyHtml = ""; - $.each(sessionData.pending_rsvp_requests, function(index, request) { - if (request.user_id != context.JK.currentUserId) { - if ("instrument_list" in request && request.instrument_list != null) { - console.log(request.instrument_list) - $.each(request.instrument_list, function (index, instrument) { + $.each(sessionData.pending_rsvp_requests, function(index, pending_rsvp_request) { + if (pending_rsvp_request.user_id != context.JK.currentUserId) { + if ("instrument_list" in pending_rsvp_request && pending_rsvp_request.instrument_list != null) { + $.each(pending_rsvp_request.instrument_list, function (index, instrument) { var instrumentId = instrument == null ? null : instrument.id; var inst = context.JK.getInstrumentIcon24(instrumentId); instrumentLogoHtml += ' '; @@ -269,17 +240,17 @@ latencyHtml = context._.template( $("#template-account-session-latency").html(), - createLatency(request.user), + $.extend(sessionUtils.createLatency(pending_rsvp_request.user), pending_rsvp_request.user), {variable: 'data'} ); - var avatar_url = context.JK.resolveAvatarUrl(request.user.photo_url); + var avatar_url = context.JK.resolveAvatarUrl(pending_rsvp_request.user.photo_url); rsvpHtml = context._.template( $("#template-account-pending-rsvp").html(), - {user_id: request.user_id, avatar_url: avatar_url, - user_name: request.user.name, instruments: instrumentLogoHtml, - latency: latencyHtml, request_id: request.id}, + {user_id: pending_rsvp_request.user_id, avatar_url: avatar_url, + user_name: pending_rsvp_request.user.name, instruments: instrumentLogoHtml, + latency: latencyHtml, request_id: pending_rsvp_request.id}, {variable: 'data'} ); @@ -296,9 +267,9 @@ var rsvpHtml = ""; var instrumentLogoHtml = ""; var latencyHtml = ""; - $.each(sessionData.approved_rsvps, function(index, request) { - if ("instrument_list" in request) { - $.each(request.instrument_list, function(index, instrument) { + $.each(sessionData.approved_rsvps, function(index, approved_rsvp) { + if ("instrument_list" in approved_rsvp) { + $.each(approved_rsvp.instrument_list, function(index, instrument) { var instrumentId = instrument == null ? null : instrument.id; var inst = context.JK.getInstrumentIcon24(instrumentId); instrumentLogoHtml += ' '; @@ -307,16 +278,16 @@ latencyHtml = context._.template( $("#template-account-session-latency").html(), - createLatency(request), + $.extend(sessionUtils.createLatency(approved_rsvp), approved_rsvp), {variable: 'data'} ); - var avatar_url = request.resolved_photo_url; + var avatar_url = approved_rsvp.resolved_photo_url; var request_id = null; $.each(rsvpData, function(index, rsvp) { - if (rsvp.user_id == request.id) { + if (rsvp.user_id == approved_rsvp.id) { var approved = true; $.each(rsvp, function(index, rsvp_slot) { if (rsvp_slot.approved == false) { @@ -332,8 +303,8 @@ rsvpHtml = context._.template( $("#template-account-session-rsvp").html(), - {id: request.id, avatar_url: avatar_url, - user_name: request.name, instruments: instrumentLogoHtml, + {id: approved_rsvp.id, avatar_url: avatar_url, + user_name: approved_rsvp.name, instruments: instrumentLogoHtml, latency: latencyHtml, is_owner: sessionData.isOwner, request_id: request_id}, {variable: 'data'} ); diff --git a/web/app/assets/javascripts/findMusician.js b/web/app/assets/javascripts/findMusician.js index ea8b9f564..77b9600b6 100644 --- a/web/app/assets/javascripts/findMusician.js +++ b/web/app/assets/javascripts/findMusician.js @@ -13,6 +13,7 @@ var page_num=1, page_count=0; var textMessageDialog = null; var $results = null; + var helpBubble = context.JK.HelpBubbleHelper; function loadMusicians(queryString) { // squelch nulls and undefines @@ -190,14 +191,12 @@ var $offsetParent = $results.closest('.content'); var options = {positions: ['top', 'bottom', 'right', 'left'], offsetParent: $offsetParent}; - var scoreOptions = {positions: ['right', 'top', 'bottom', 'left'], offsetParent: $offsetParent, width:'600px'}; + var scoreOptions = {offsetParent: $offsetParent}; context.JK.helpBubble($('.follower-count', $rendering), 'musician-follower-count', {}, options); context.JK.helpBubble($('.friend-count', $rendering), 'musician-friend-count', {}, options); context.JK.helpBubble($('.recording-count', $rendering), 'musician-recording-count', {}, options); context.JK.helpBubble($('.session-count', $rendering), 'musician-session-count', {}, options); - context.JK.helpBubble($('.score-count', $rendering), 'musician-score-count', - {full_score: full_score ? Math.round(full_score / 2) : null, my_gear_latency: myAudioLatency, their_gear_latency:musician['audio_latency'], internet_latency: musician['score']}, - scoreOptions) + helpBubble.scoreBreakdown($('.score-count', $rendering), false, full_score, myAudioLatency, musician['audio_latency'], musician['score'], scoreOptions); $results.append($rendering); } diff --git a/web/app/assets/javascripts/findSession.js b/web/app/assets/javascripts/findSession.js index 80e0fa67f..de5c795ea 100644 --- a/web/app/assets/javascripts/findSession.js +++ b/web/app/assets/javascripts/findSession.js @@ -94,11 +94,11 @@ } function renderActiveSessions(sessions) { - $.each(sessions, function(i, session) { - sessionList.renderActiveSession(session, $(CATEGORY.ACTIVE.id)); + $.each(sessions.sessions, function(i, session) { + sessionList.renderActiveSession(session, $(CATEGORY.ACTIVE.id), sessions.my_audio_latency); }); - afterLoadActiveSessions(sessions); + afterLoadActiveSessions(sessions.sessions); } function buildActiveSessionsQuery() { @@ -162,10 +162,10 @@ } function renderScheduledSessions(sessions) { - $.each(sessions, function(i, session) { - sessionList.renderInactiveSession(session, $(CATEGORY.SCHEDULED.id)); + $.each(sessions.sessions, function(i, session) { + sessionList.renderInactiveSession(session, $(CATEGORY.SCHEDULED.id), undefined, sessions.my_audio_latency); }); - afterLoadScheduledSessions(sessions); + afterLoadScheduledSessions(sessions.sessions); } function buildScheduledSessionsQuery() { diff --git a/web/app/assets/javascripts/helpBubbleHelper.js b/web/app/assets/javascripts/helpBubbleHelper.js new file mode 100644 index 000000000..b1e842e50 --- /dev/null +++ b/web/app/assets/javascripts/helpBubbleHelper.js @@ -0,0 +1,29 @@ +/** + * HelpBubble helper functions, for help bubbles with lots of arguments or logic + */ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + var helpBubble = {}; + var rest = new context.JK.Rest(); + context.JK.HelpBubbleHelper = helpBubble; + var logger = context.JK.logger; + + var defaultScoreBreakDownOptions = {positions: ['right', 'top', 'bottom', 'left'], width:'600px' }; + helpBubble.scoreBreakdown = function($element, isCurrentUser, full_score, myAudioLatency, otherAudioLatency, internetScore, options) { + options = options || {}; + options = $.extend({}, defaultScoreBreakDownOptions, options) + if(isCurrentUser) { + context.JK.helpBubble($element, 'musician-score-self', + {full_score: full_score ? Math.round(full_score / 2) : null, my_gear_latency: myAudioLatency, their_gear_latency: otherAudioLatency, internet_latency: internetScore}, + options); + } else { + context.JK.helpBubble($element, 'musician-score-count', + {full_score: full_score ? Math.round(full_score / 2) : null, my_gear_latency: myAudioLatency, their_gear_latency: otherAudioLatency, internet_latency: internetScore}, + options); + } + } + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/sessionList.js b/web/app/assets/javascripts/sessionList.js index 705ccafae..21beca5eb 100644 --- a/web/app/assets/javascripts/sessionList.js +++ b/web/app/assets/javascripts/sessionList.js @@ -6,10 +6,11 @@ context.JK.SessionList = function(app) { var EVENTS = context.JK.EVENTS; var gearUtils = context.JK.GearUtils; + var sessionUtils = context.JK.SessionUtils; + var helpBubble = context.JK.HelpBubbleHelper; var logger = context.JK.logger; var rest = context.JK.Rest(); var ui = new context.JK.UIHelper(app); - var sessionUtils = context.JK.SessionUtils; var $activeSessionTemplate = $('#template-active-session-row'); var $inactiveSessionTemplate = $('#template-inactive-session-row'); var $notationFileTemplate = $('#template-notation-files'); @@ -19,15 +20,7 @@ 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: 35.0}, - POOR : {description: "POOR", style: "latency-red", min: 35.0, max: 50}, - UNACCEPTABLE: {description: "UNACCEPTABLE", style: "latency-grey", min: 50, max: 10000000}, - UNKNOWN: {description: "UNKNOWN", style: "latency-grey", min: -2, max: -2} - }; - - function renderActiveSession(session, tbGroup) { + function renderActiveSession(session, tbGroup, myAudioLatency) { $('#actionHeader', tbGroup).html('JOIN'); @@ -95,8 +88,17 @@ sessionVals.in_session_musicians = inSessionUsersHtml.length > 0 ? inSessionUsersHtml : 'N/A'; sessionVals.join_link_display_style = showJoinLink ? "block" : "none"; - var row = context.JK.fillTemplate($activeSessionTemplate.html(), sessionVals); - $(tbGroup).append(row); + var $row = $(context.JK.fillTemplate($activeSessionTemplate.html(), sessionVals)); + var $offsetParent = $(tbGroup).closest('.content'); + var $latencyBadge = $row.find('.latency-value'); + var full_score = $latencyBadge.attr('data-full-score') || null; + var internet_score = $latencyBadge.attr('data-internet-score') || null; + var audio_latency = $latencyBadge.attr('data-audio-latency') || null; + var latencyBadgeUserId = $latencyBadge.attr('data-user-id'); + var scoreOptions = {offsetParent: $offsetParent}; + helpBubble.scoreBreakdown($latencyBadge, context.JK.currentUserId == latencyBadgeUserId, full_score, myAudioLatency, audio_latency, internet_score, scoreOptions); + + $(tbGroup).append($row); if (showJoinLink) { // wire up the Join Link to the T&Cs dialog @@ -127,7 +129,7 @@ } } - function renderInactiveSession(session, tbGroup, $rowToUpdate) { + function renderInactiveSession(session, tbGroup, $rowToUpdate, myAudioLatency) { var openSlots = false; var hasInvitation = false; @@ -195,15 +197,25 @@ var sessionVals = buildSessionObject(session, notationFileHtml, rsvpUsersHtml, openSlotsHtml, latencyHtml); sessionVals.scheduled_start = session.pretty_scheduled_start_with_timezone; - var row = context.JK.fillTemplate($inactiveSessionTemplate.html(), sessionVals); + var $row = $(context.JK.fillTemplate($inactiveSessionTemplate.html(), sessionVals)); + + var $offsetParent = $(tbGroup).closest('.content'); + var $latencyBadge = $row.find('.latency-value'); + var full_score = $latencyBadge.attr('data-full-score') || null; + var internet_score = $latencyBadge.attr('data-internet-score') || null; + var audio_latency = $latencyBadge.attr('data-audio-latency') || null; + var latencyBadgeUserId = $latencyBadge.attr('data-user-id'); + var scoreOptions = {offsetParent: $offsetParent}; + helpBubble.scoreBreakdown($latencyBadge, context.JK.currentUserId == latencyBadgeUserId, full_score, myAudioLatency, audio_latency, internet_score, scoreOptions); + // initial page load if (!$rowToUpdate) { - $(tbGroup).append(row); + $(tbGroup).append($row); } // inline update after an RSVP submission / cancellation else { - $rowToUpdate.replaceWith(row); + $rowToUpdate.replaceWith($row); } var $parentRow = $('tr[data-session-id=' + session.id + ']', tbGroup); @@ -219,7 +231,7 @@ .one(EVENTS.RSVP_CANCELED, function() { rest.getSessionHistory(session.id) .done(function(response) { - renderInactiveSession(response, tbGroup, $parentRow); + renderInactiveSession(response, tbGroup, $parentRow, myAudioLatency); }); }) .one(EVENTS.DIALOG_CLOSED, function() { @@ -236,7 +248,7 @@ .one(EVENTS.RSVP_CANCELED, function() { rest.getSessionHistory(session.id) .done(function(response) { - renderInactiveSession(response, tbGroup, $parentRow); + renderInactiveSession(response, tbGroup, $parentRow, myAudioLatency); }); }) .one(EVENTS.DIALOG_CLOSED, function() { @@ -263,7 +275,7 @@ .one(EVENTS.RSVP_SUBMITTED, function() { rest.getSessionHistory(session.id) .done(function(response) { - renderInactiveSession(response, tbGroup, $parentRow); + renderInactiveSession(response, tbGroup, $parentRow, myAudioLatency); }); }) .one(EVENTS.DIALOG_CLOSED, function() { @@ -318,7 +330,7 @@ }; var musicianHtml = context.JK.fillTemplate($musicianTemplate.html(), musicianVals); - var latencyHtml = context.JK.fillTemplate($latencyTemplate.html(), createLatency(participant.user)); + var latencyHtml = context._.template($latencyTemplate.html(), $.extend(sessionUtils.createLatency(participant.user), participant.user), { variable: 'data' }); return [musicianHtml, latencyHtml]; } @@ -349,51 +361,11 @@ }; var musicianHtml = context.JK.fillTemplate($musicianTemplate.html(), musicianVals); - var latencyHtml = context.JK.fillTemplate($latencyTemplate.html(), createLatency(user)); + var latencyHtml = context._.template($latencyTemplate.html(), $.extend(sessionUtils.createLatency(user), user), { variable: 'data' }); return [musicianHtml, latencyHtml]; } - function createLatency(user) { - - var latencyStyle; - var latencyDescription; - - if (user.id === context.JK.currentUserId) { - latencyStyle = LATENCY.GOOD.style, latencyDescription = LATENCY.GOOD.description; - } - - else { - var latency = user.latency; - - if (!latency) { - latencyDescription = LATENCY.UNKNOWN.description; - latencyStyle = LATENCY.UNKNOWN.style; - } - else if (latency <= LATENCY.GOOD.max) { - latencyDescription = LATENCY.GOOD.description; - latencyStyle = LATENCY.GOOD.style; - } - else if (latency > LATENCY.MEDIUM.min && latency <= LATENCY.MEDIUM.max) { - latencyDescription = LATENCY.MEDIUM.description; - latencyStyle = LATENCY.MEDIUM.style; - } - else if (latency > LATENCY.POOR.min && latency <= LATENCY.UNACCEPTABLE.max) { - latencyDescription = LATENCY.POOR.description; - latencyStyle = LATENCY.POOR.style; - } - else { - latencyStyle = LATENCY.UNREACHABLE.style - latencyDescription = LATENCY.UNREACHABLE.description - } - } - - return { - latency_style: latencyStyle, - latency_text: latencyDescription - }; - } - function createNotationFile(notation) { var notationVals = { file_url: notation.file_url, diff --git a/web/app/assets/javascripts/session_utils.js b/web/app/assets/javascripts/session_utils.js index a80aae99e..ed9b6c47d 100644 --- a/web/app/assets/javascripts/session_utils.js +++ b/web/app/assets/javascripts/session_utils.js @@ -11,6 +11,16 @@ context.JK.SessionUtils = sessionUtils; var logger = context.JK.logger; + + var LATENCY = { + ME : {description: "ME", style: "latency-me", min: -1, max: -1}, + GOOD : {description: "GOOD", style: "latency-good", min: 0.0, max: 20.0}, + MEDIUM : {description: "FAIR", style: "latency-fair", min: 40.0, max: 70.0}, + POOR : {description: "POOR", style: "latency-poor", min: 70.0, max: 100}, + UNACCEPTABLE: {description: "UNACCEPTABLE", style: "latency-unacceptable", min: 100, max: 10000000}, + UNKNOWN: {description: "UNKNOWN", style: "latency-unknown", min: -2, max: -2} + }; + sessionUtils.createOpenSlot = function($openSlotsTemplate, slot) { var inst = context.JK.getInstrumentIcon24(slot.instrument_id); @@ -52,4 +62,44 @@ } } + sessionUtils.createLatency = function(user) { + + var latencyStyle; + var latencyDescription; + + if (user.id === context.JK.currentUserId) { + latencyStyle = LATENCY.ME.style, latencyDescription = LATENCY.ME.description; + } + + else { + var latency = user.full_score; + + if (!latency) { + latencyDescription = LATENCY.UNKNOWN.description; + latencyStyle = LATENCY.UNKNOWN.style; + } + else if (latency <= LATENCY.GOOD.max) { + latencyDescription = LATENCY.GOOD.description; + latencyStyle = LATENCY.GOOD.style; + } + else if (latency > LATENCY.MEDIUM.min && latency <= LATENCY.MEDIUM.max) { + latencyDescription = LATENCY.MEDIUM.description; + latencyStyle = LATENCY.MEDIUM.style; + } + else if (latency > LATENCY.POOR.min && latency <= LATENCY.UNACCEPTABLE.max) { + latencyDescription = LATENCY.POOR.description; + latencyStyle = LATENCY.POOR.style; + } + else { + latencyStyle = LATENCY.UNACCEPTABLE.style + latencyDescription = LATENCY.UNACCEPTABLE.description + } + } + + return { + latency_style: latencyStyle, + latency_text: latencyDescription + }; + } + })(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/web/session_info.js b/web/app/assets/javascripts/web/session_info.js index 3886a6068..7e8e0514d 100644 --- a/web/app/assets/javascripts/web/session_info.js +++ b/web/app/assets/javascripts/web/session_info.js @@ -9,15 +9,11 @@ var logger = context.JK.logger; var rest = JK.Rest(); var ui = new context.JK.UIHelper(app); + var sessionUtils = context.JK.SessionUtils; + var helpBubble = context.JK.HelpBubbleHelper; 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} - }; + var $templateLatencyDetail = null; + var $landingSidebar = null; function addComment() { var comment = $("#txtSessionInfoComment").val(); @@ -66,70 +62,48 @@ } // this is done post-page load to allow websocket connection to be established - function addLatencyDetails() { - rest.getSessionHistory(musicSessionId) - .done(function(session) { - if (session.approved_rsvps && session.approved_rsvps.length > 0) { - // loop through each record in RSVPs, which has already been rendered, and extract - // the user ID - $('.rsvp-details').each(function(index) { - var $user = $(this).find('div[hoveraction="musician"]'); - var userId = $user.attr('user-id'); - var latency = null; - var latencyStyle = LATENCY.UNKNOWN.style; - var latencyDescription = LATENCY.UNKNOWN.description; + function addLatencyDetails(session) { + if (session.approved_rsvps && session.approved_rsvps.length > 0) { + // loop through each record in RSVPs, which has already been rendered, and extract + // the user ID + $('.rsvp-details').each(function(index) { + var $user = $(this).find('div[hoveraction="musician"]'); + var userId = $user.attr('user-id'); - // default current user to GOOD - if (userId === context.JK.currentUserId) { - latencyStyle = LATENCY.GOOD.style; - latencyDescription = LATENCY.GOOD.description; - } - else { - // find the corresponding user in the API response payload - for (var i=0; i < session.approved_rsvps.length; i++) { - if (session.approved_rsvps.id === userId) { - latency = session.approved_rsvps[i].latency; - break; - } - } - if (latency) { - if (latency <= LATENCY.GOOD.max) { - latencyDescription = LATENCY.GOOD.description; - latencyStyle = LATENCY.GOOD.style; - } - else if (latency > LATENCY.MEDIUM.min && latency <= LATENCY.MEDIUM.max) { - latencyDescription = LATENCY.MEDIUM.description; - latencyStyle = LATENCY.MEDIUM.style; - } - else { - latencyDescription = LATENCY.POOR.description; - latencyStyle = LATENCY.POOR.style; - } - } - else { - latencyStyle = LATENCY.UNKNOWN.style; - latencyDescription = LATENCY.UNKNOWN.description; - } - } + var user = null; + for (var i=0; i < session.approved_rsvps.length; i++) { + if (session.approved_rsvps[i].id === userId) { + user = session.approved_rsvps[i]; + break; + } + } + + if(user) { // add the latency details for this user to the UI - var latencyHtml = context.JK.fillTemplate($('#template-latency-detail').html(), { - latencyStyle: latencyStyle, - latencyDescription: latencyDescription - }); - - $(this).find('.latency-tags').append(latencyHtml); - }); - } - }); + var $latencyHtml = $(context.JK.fillTemplate($templateLatencyDetail.html(), + sessionUtils.createLatency(user))); + var $latencyBadge = $latencyHtml.find('.latency'); + app.user().done(function(userMe) { + helpBubble.scoreBreakdown($latencyBadge, context.JK.currentUserId == user.id, user.full_score, userMe.last_jam_audio_latency, user.audio_latency, user.internet_score, {offsetParent: $landingSidebar}); + }) + $(this).find('.latency-tags').append($latencyHtml); + } + else { + logger.warn("unable to find user in approved_rsvps"); + } + }); + } } function initialize() { registerScheduledSessionComment(); - var $parent = $('.landing-sidebar'); - context.JK.bindHoverEvents($parent); - context.JK.setInstrumentAssetPath($('.instrument-icon', $parent)); + $landingSidebar = $('.landing-sidebar'); + $templateLatencyDetail = $('#template-latency-detail'); + + context.JK.bindHoverEvents($landingSidebar); + context.JK.setInstrumentAssetPath($('.instrument-icon', $landingSidebar)); // render comments $(".landing-comment-scroller").empty(); @@ -141,6 +115,8 @@ context.JK.resolveAvatarUrl(val.creator.photo_url), $.timeago(val.created_at), true); }); } + + addLatencyDetails(response); }) .fail(function(xhr) { @@ -212,8 +188,6 @@ $("#txtSessionComment").blur(); } }); - - addLatencyDetails(); } this.initialize = initialize; diff --git a/web/app/assets/javascripts/web/web.js b/web/app/assets/javascripts/web/web.js index 86f8d3bf3..62a9e8d40 100644 --- a/web/app/assets/javascripts/web/web.js +++ b/web/app/assets/javascripts/web/web.js @@ -17,6 +17,7 @@ //= require jquery.custom-protocol //= require jquery.ba-bbq //= require jquery.icheck +//= require jquery.bt //= require AAA_Log //= require AAC_underscore //= require globals @@ -43,6 +44,8 @@ //= require custom_controls //= require ga //= require jam_rest +//= require session_utils +//= require helpBubbleHelper //= require facebook_rest //= require landing/init //= require landing/signup diff --git a/web/app/assets/stylesheets/client/account.css.scss b/web/app/assets/stylesheets/client/account.css.scss index f80ef3f02..a26e99251 100644 --- a/web/app/assets/stylesheets/client/account.css.scss +++ b/web/app/assets/stylesheets/client/account.css.scss @@ -294,8 +294,9 @@ .latency { font-size: 15px; height: 16px; - padding-left: 4px; + padding: 3px; width: 100%; + @include border-radius(2px); } #session-rsvps, #still-needed, #invited-users { diff --git a/web/app/assets/stylesheets/client/common.css.scss b/web/app/assets/stylesheets/client/common.css.scss index 08bb7e86e..fdd5027de 100644 --- a/web/app/assets/stylesheets/client/common.css.scss +++ b/web/app/assets/stylesheets/client/common.css.scss @@ -37,6 +37,13 @@ $border: hsl(210, 50%, 45%); $narrow-screen: 1000px; // 990 ? 1000 ? $short-screen: 600px; // toolbars / chrome for x768 +$latencyBadgeMe: #dddddd; +$latencyBadgeGood: #71a43b; +$latencyBadgeFair: #cc9900; +$latencyBadgePoor: #980006; +$latencyBadgeUnacceptable: #868686; +$latencyBadgeUnknown: #868686; + @mixin border_box_sizing { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; @@ -51,6 +58,44 @@ $short-screen: 600px; // toolbars / chrome for x768 box-sizing: content-box; } +@mixin border-radius($radius) { + -moz-border-radius:$radius; + -webkit-border-radius: $radius; + border-radius: $radius; + background-clip: padding-box; /* stops bg color from leaking outside the border: */ +} + +// Single side border-radius + +@mixin border-top-radius($radius) { + -webkit-border-top-right-radius: $radius; + border-top-right-radius: $radius; + -webkit-border-top-left-radius: $radius; + border-top-left-radius: $radius; + background-clip: padding-box; +} +@mixin border-right-radius($radius) { + -webkit-border-bottom-right-radius: $radius; + border-bottom-right-radius: $radius; + -webkit-border-top-right-radius: $radius; + border-top-right-radius: $radius; + background-clip: padding-box; +} +@mixin border-bottom-radius($radius) { + -webkit-border-bottom-right-radius: $radius; + border-bottom-right-radius: $radius; + -webkit-border-bottom-left-radius: $radius; + border-bottom-left-radius: $radius; + background-clip: padding-box; +} +@mixin border-left-radius($radius) { + -webkit-border-bottom-left-radius: $radius; + border-bottom-left-radius: $radius; + -webkit-border-top-left-radius: $radius; + border-top-left-radius: $radius; + background-clip: padding-box; +} + @mixin flat_dropdown { box-shadow: none !important; color: #666666; diff --git a/web/app/assets/stylesheets/client/findSession.css.scss b/web/app/assets/stylesheets/client/findSession.css.scss index 5f47c6d58..627b28192 100644 --- a/web/app/assets/stylesheets/client/findSession.css.scss +++ b/web/app/assets/stylesheets/client/findSession.css.scss @@ -1,3 +1,5 @@ +@import 'common.css.scss'; + #findSession { th, td { margin: 4px; padding:4px; } @@ -68,4 +70,8 @@ .end-of-list { margin-top:0; } + + .latency-value { + @include border-radius(2px); + } } \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/help.css.scss b/web/app/assets/stylesheets/client/help.css.scss index 691bc7c74..32074d802 100644 --- a/web/app/assets/stylesheets/client/help.css.scss +++ b/web/app/assets/stylesheets/client/help.css.scss @@ -1,4 +1,4 @@ -.screen { +.screen, body.web { .bt-wrapper { .bt-content { @@ -7,10 +7,15 @@ font-size:14px; p { + margin:1em; + line-height:150%; font-size:14px; } ul { font-size:14px; + margin-left: 2em; + list-style: disc; + color:#CCCCCC; } li { diff --git a/web/app/assets/stylesheets/client/sessionList.css.scss b/web/app/assets/stylesheets/client/sessionList.css.scss index 9e4884a5b..6dbe804df 100644 --- a/web/app/assets/stylesheets/client/sessionList.css.scss +++ b/web/app/assets/stylesheets/client/sessionList.css.scss @@ -80,43 +80,65 @@ table.findsession-table, table.local-recordings { } } -.latency-grey { + +.latency-unacceptable { + width: 50px; + height: 10px; + font-family:Arial, Helvetica, sans-serif; + font-weight:200; + font-size:11px; + background-color:$latencyBadgeUnacceptable; + text-align:center; +} + +.latency-unknown { width: 50px; height: 10px; font-family:Arial, Helvetica, sans-serif; font-weight:200; font-size:11px; - background-color:#868686; + background-color:$latencyBadgeUnacceptable; text-align:center; } -.latency-green { +.latency-good { width: 50px; height: 10px; font-family:Arial, Helvetica, sans-serif; font-weight:200; font-size:11px; - background-color:#71a43b; + background-color:$latencyBadgeGood; text-align:center; } -.latency-yellow { +.latency-me { + width: 50px; + height: 10px; + font-family:Arial, Helvetica, sans-serif; + font-weight:200; + font-size:11px; + text-align:center; + background-color:$latencyBadgeMe; + color:black; +} + +.latency-fair { width: 50px; height: 10px; font-family:Arial, Helvetica, sans-serif; font-weight:200; font-size:11px; - background-color:#cc9900; + background-color:$latencyBadgeFair; text-align:center; } -.latency-red { +.latency-poor{ width: 40px; height: 10px; font-family:Arial, Helvetica, sans-serif; font-weight:200; font-size:11px; - background-color:#980006; + background-color:$latencyBadgePoor; text-align:center; } diff --git a/web/app/assets/stylesheets/web/session_info.css.scss b/web/app/assets/stylesheets/web/session_info.css.scss new file mode 100644 index 000000000..975d9db0c --- /dev/null +++ b/web/app/assets/stylesheets/web/session_info.css.scss @@ -0,0 +1,56 @@ +@import "client/common"; + +body.web.session_info { + + table.musicians { + margin-left:4px; + } + + table.musicians td { + border-right:none; + border-top:none; + padding:2px; + vertical-align:middle !important; + } + + .latency { + width: 50px; + height: 10px; + font-family:Arial, Helvetica, sans-serif; + font-weight:200; + font-size:11px; + text-align:center; + @include border-radius(2px); + } + + .latency-unknown { + background-color:$latencyBadgeUnknown; + } + + .latency-unacceptable { + background-color:$latencyBadgeUnacceptable; + } + + .latency-good { + background-color:$latencyBadgeGood; + } + + .latency-fair{ + background-color:$latencyBadgeFair; + } + + .latency-poor { + background-color:$latencyBadgePoor; + } + + .latency-me { + width: 40px; + height: 10px; + font-family:Arial, Helvetica, sans-serif; + font-weight:200; + font-size:11px; + background-color:$latencyBadgeMe; + text-align:center; + color:black; + } +} \ 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 f0a39eb7c..8a30c08e0 100644 --- a/web/app/assets/stylesheets/web/sessions.css.scss +++ b/web/app/assets/stylesheets/web/sessions.css.scss @@ -1,54 +1,3 @@ #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/assets/stylesheets/web/web.css b/web/app/assets/stylesheets/web/web.css index 37e75c4ac..41d1bea03 100644 --- a/web/app/assets/stylesheets/web/web.css +++ b/web/app/assets/stylesheets/web/web.css @@ -11,12 +11,14 @@ *= require client/ftue *= require client/user_dropdown *= require client/hoverBubble +*= require client/help *= require web/main *= require web/footer *= require web/recordings *= require web/welcome #= require web/sessions *= require web/events +*= require web/session_info *= require users/signinCommon *= require dialogs/dialog *= require landings/landing_page diff --git a/web/app/controllers/api_music_sessions_controller.rb b/web/app/controllers/api_music_sessions_controller.rb index f3f460b50..afa0eb29f 100644 --- a/web/app/controllers/api_music_sessions_controller.rb +++ b/web/app/controllers/api_music_sessions_controller.rb @@ -532,7 +532,13 @@ class ApiMusicSessionsController < ApiController end def show_history - @history = MusicSession.find(params[:id]) + if current_user + ActiveRecord::Base.transaction do + @history, @user_scores = MusicSession.session_with_scores(current_user, params[:id]) + end + else + @history = MusicSession.find(params[:id]) + end end def claimed_recording_start diff --git a/web/app/helpers/score_helper.rb b/web/app/helpers/score_helper.rb index 1609e2378..b77d7c323 100644 --- a/web/app/helpers/score_helper.rb +++ b/web/app/helpers/score_helper.rb @@ -1,9 +1,15 @@ module ScoreHelper # helper method to make finding a user's score fault-tolerant - def user_score(user_id) + def user_score_old(user_id) @user_scores ||= {} user = @user_scores[user_id] - user ? user[:latency] : nil + user ? user[:full_score] : nil + end + + def user_score(user_id) + @user_scores ||= {} + user = @user_scores[user_id] || {} + { full_score: user[:full_score], audio_latency: user[:audio_latency], internet_score: user[:internet_score] } end end diff --git a/web/app/views/api_music_sessions/ams_index.rabl b/web/app/views/api_music_sessions/ams_index.rabl index 05e15a27f..f602faffa 100644 --- a/web/app/views/api_music_sessions/ams_index.rabl +++ b/web/app/views/api_music_sessions/ams_index.rabl @@ -1,3 +1,8 @@ -object @music_sessions -extends "api_music_sessions/show_history" +node :my_audio_latency do |user| + current_user.last_jam_audio_latency.round if current_user.last_jam_audio_latency +end + +child @music_sessions => :sessions do + extends "api_music_sessions/show_history" +end diff --git a/web/app/views/api_music_sessions/show_history.rabl b/web/app/views/api_music_sessions/show_history.rabl index 9d9d42bca..f1fa3a1a0 100644 --- a/web/app/views/api_music_sessions/show_history.rabl +++ b/web/app/views/api_music_sessions/show_history.rabl @@ -94,9 +94,8 @@ else node do |invitation| { - latency: user_score(invitation.receiver.id), receiver_avatar_url: invitation.receiver.resolved_photo_url - } + }.merge(user_score(invitation.receiver.id)) end } @@ -105,10 +104,9 @@ else node do |user| { - latency: user_score(user.id), instrument_list: process_approved_rsvps(user), rsvp_request_id: JSON.parse(user.rsvp_request_ids)[0] # there must always be a rsvp_request_id; and they should all be the same - } + }.merge(user_score(user.id)) end } @@ -127,7 +125,7 @@ else attributes :id, :photo_url, :name, :first_name, :last_name node do |user| - { latency: user_score(user.id), name: user.name } + { name: user.name }.merge(user_score(user.id)) end } } @@ -156,7 +154,7 @@ else attributes :ip_address, :client_id, :joined_session_at node :user do |connection| - { :id => connection.user.id, :photo_url => connection.user.photo_url, :name => connection.user.name, :is_friend => connection.user.friends?(current_user), :connection_state => connection.aasm_state, latency: user_score(connection.user.id) } + { :id => connection.user.id, :photo_url => connection.user.photo_url, :name => connection.user.name, :is_friend => connection.user.friends?(current_user), :connection_state => connection.aasm_state }.merge(user_score(connection.user.id)) end child(:tracks => :tracks) { diff --git a/web/app/views/api_music_sessions/sms_index.rabl b/web/app/views/api_music_sessions/sms_index.rabl index 05e15a27f..f17cdc21f 100644 --- a/web/app/views/api_music_sessions/sms_index.rabl +++ b/web/app/views/api_music_sessions/sms_index.rabl @@ -1,3 +1,8 @@ -object @music_sessions -extends "api_music_sessions/show_history" +node :my_audio_latency do |user| + current_user.last_jam_audio_latency.round if current_user.last_jam_audio_latency +end + +child @music_sessions => :sessions do + extends "api_music_sessions/show_history" +end \ No newline at end of file diff --git a/web/app/views/api_search/index.rabl b/web/app/views/api_search/index.rabl index e7120fca6..90fca5c5d 100644 --- a/web/app/views/api_search/index.rabl +++ b/web/app/views/api_search/index.rabl @@ -43,7 +43,7 @@ if @search.musicians_filter_search? end node :my_audio_latency do |user| - current_user.last_jam_audio_latency + current_user.last_jam_audio_latency.round if current_user.last_jam_audio_latency end child(:results => :musicians) { diff --git a/web/app/views/api_users/show.rabl b/web/app/views/api_users/show.rabl index eba9115d9..5dc6f0d02 100644 --- a/web/app/views/api_users/show.rabl +++ b/web/app/views/api_users/show.rabl @@ -68,3 +68,7 @@ end child :music_sessions => :sessions do attributes :id, :description, :musician_access, :approval_required, :fan_access end + +node :last_jam_audio_latency do |user| + user.last_jam_audio_latency.round if user.last_jam_audio_latency +end \ No newline at end of file diff --git a/web/app/views/clients/_account_session_detail.html.haml b/web/app/views/clients/_account_session_detail.html.haml index 7057069b8..ed08fbc17 100644 --- a/web/app/views/clients/_account_session_detail.html.haml +++ b/web/app/views/clients/_account_session_detail.html.haml @@ -159,5 +159,5 @@ %img{src: "{{data.avatar_url}}"} %script{type: 'text/template', id: 'template-account-session-latency'} - .latency{class: "{{data.latency_style}}"} + .latency{class: "{{data.latency_style}}", 'data-user-id' => "{{data.id}}", 'data-audio-latency' => "{{data.audio_latency || ''}}", 'data-full-score' => "{{data.full_score || ''}}", 'data-internet-score' => "{{data.internet_score || ''}}"} {{data.latency_text}} diff --git a/web/app/views/clients/_findSession.html.erb b/web/app/views/clients/_findSession.html.erb index 5b642c0f2..ec21dc8f4 100644 --- a/web/app/views/clients/_findSession.html.erb +++ b/web/app/views/clients/_findSession.html.erb @@ -242,8 +242,8 @@ + + \ No newline at end of file diff --git a/web/app/views/clients/_musicians.html.erb b/web/app/views/clients/_musicians.html.erb index df1b044b4..290d8422c 100644 --- a/web/app/views/clients/_musicians.html.erb +++ b/web/app/views/clients/_musicians.html.erb @@ -30,15 +30,6 @@
- -
FOLLOWING:
{musician_follow_template}
diff --git a/web/app/views/layouts/web.html.erb b/web/app/views/layouts/web.html.erb index ae0afc893..43debd078 100644 --- a/web/app/views/layouts/web.html.erb +++ b/web/app/views/layouts/web.html.erb @@ -77,8 +77,10 @@ <%= render "clients/hoverBand" %> <%= render "clients/hoverSession" %> <%= render "clients/hoverRecording" %> + <%= render "clients/help" %> <%= render 'dialogs/dialogs' %> +