From 4d9e8d28472224bc39e6d62b3bb0499affa0ec19 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Wed, 6 May 2020 14:42:52 -0500 Subject: [PATCH] done --- db/up/find_sessions_2020.sql | 18 + .../jam_ruby/models/active_music_session.rb | 13 +- ruby/lib/jam_ruby/models/music_session.rb | 108 ++++- ruby/lib/jam_ruby/models/notification.rb | 27 +- .../jam_ruby/models/music_session_spec.rb | 99 ++++ .../dialog/sessionSettingsDialog.js | 17 +- .../assets/javascripts/notificationPanel.js | 21 +- .../FindSessionFriends.js.jsx.coffee | 198 ++++---- .../FindSessionOpen.js.jsx.coffee | 193 ++++++++ .../FindSessionRow.js.jsx.coffee | 426 +++++++++++++----- .../FindSessionScreen.js.jsx.coffee | 52 ++- .../react-components/HoverUser.js.jsx.coffee | 39 ++ .../actions/FriendActions.js.coffee | 5 + .../actions/SessionsActions.js.coffee | 9 + .../stores/AppStore.js.coffee | 3 + .../stores/FriendStore.js.coffee | 35 ++ .../stores/SessionsStore.js.coffee | 169 +++++++ .../javascripts/scheduled_session.js.erb | 37 +- web/app/assets/javascripts/session_utils.js | 4 + web/app/assets/javascripts/sidebar.js | 3 + web/app/assets/javascripts/utils.js | 16 +- .../stylesheets/client/createSession.scss | 4 +- .../stylesheets/client/findSession.scss | 117 ++++- .../dialogs/sessionSettingsDialog.scss | 4 +- .../api_music_sessions_controller.rb | 9 +- web/app/views/api_music_sessions/show.rabl | 2 +- .../api_music_sessions/show_history.rabl | 2 +- .../views/api_music_sessions/sms_index_2.rabl | 4 + web/app/views/clients/_findSession2.html.erb | 57 --- .../views/clients/_scheduledSession.html.erb | 15 + .../views/clients/_sessionSettings.html.haml | 9 +- web/app/views/clients/index.html.erb | 1 + web/config/environments/development.rb | 4 +- web/config/routes.rb | 4 +- web/lib/music_session_manager.rb | 3 +- 35 files changed, 1388 insertions(+), 339 deletions(-) create mode 100644 web/app/assets/javascripts/react-components/FindSessionOpen.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/HoverUser.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/actions/FriendActions.js.coffee create mode 100644 web/app/assets/javascripts/react-components/actions/SessionsActions.js.coffee create mode 100644 web/app/assets/javascripts/react-components/stores/FriendStore.js.coffee create mode 100644 web/app/assets/javascripts/react-components/stores/SessionsStore.js.coffee create mode 100644 web/app/views/api_music_sessions/sms_index_2.rabl diff --git a/db/up/find_sessions_2020.sql b/db/up/find_sessions_2020.sql index 92f2ccae3..48c559a48 100644 --- a/db/up/find_sessions_2020.sql +++ b/db/up/find_sessions_2020.sql @@ -1,3 +1,21 @@ ALTER TABLE rsvp_requests ADD COLUMN music_session_id VARCHAR(64) REFERENCES music_sessions(id); ALTER TABLE rsvp_requests ADD COLUMN chosen boolean DEFAULT FALSE NOT NULL; + +UPDATE rsvp_requests set CHOSEN = TRUE, music_session_id = b.music_session_id FROM ( + +SELECT music_sessions.id as music_session_id, rsvp_requests.id as rsvp_request_id FROM music_sessions JOIN + rsvp_slots + ON + music_sessions.id = rsvp_slots.music_session_id + JOIN + rsvp_requests_rsvp_slots + ON + rsvp_requests_rsvp_slots.rsvp_slot_id = rsvp_slots.id + JOIN + rsvp_requests + ON rsvp_requests.id = rsvp_requests_rsvp_slots.rsvp_request_id + WHERE rsvp_requests_rsvp_slots.chosen = TRUE + ) b WHERE rsvp_requests.id = rsvp_request_id; + +ALTER TABLE music_sessions ADD COLUMN friends_can_join boolean DEFAULT FALSE NOT NULL; CREATE INDEX rsvp_request_music_session_id ON rsvp_requests USING btree (music_session_id); \ 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 d6e9bbb57..5ff92e3c4 100644 --- a/ruby/lib/jam_ruby/models/active_music_session.rb +++ b/ruby/lib/jam_ruby/models/active_music_session.rb @@ -243,7 +243,6 @@ module JamRuby return query end - # all sessions that are private and active, yet I can see def self.friend_active_index(user, options) @@ -264,7 +263,7 @@ module JamRuby LEFT OUTER JOIN rsvp_requests - ON rsvp_requests.music_session_id = music_sessions.id and rsvp_requests.user_id = '#{user.id}' AND rsvp_requests.chosen = true + ON rsvp_requests.music_session_id = active_music_sessions.id and rsvp_requests.user_id = '#{user.id}' AND rsvp_requests.chosen = true LEFT OUTER JOIN invitations @@ -297,7 +296,8 @@ module JamRuby 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', ?))", ActiveRecord::Base.connection.quote(keyword) + ':*') unless keyword.blank? - query = query.group("music_sessions.id") + query = query.group("music_sessions.id, active_music_sessions.created_at") + query = query.order("active_music_sessions.created_at DESC") query end @@ -331,7 +331,8 @@ module JamRuby 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', ?))", ActiveRecord::Base.connection.quote(keyword) + ':*') unless keyword.blank? - query = query.group("music_sessions.id") + query = query.group("music_sessions.id, active_music_sessions.created_at") + query = query.order("active_music_sessions.created_at DESC") query end @@ -766,6 +767,10 @@ module JamRuby self.save end + def friends_can_join + music_session.friends_can_join + end + def invitations music_session.invitations end diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index 811936873..d3744553c 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -63,6 +63,7 @@ module JamRuby validates :fan_access, :inclusion => {:in => [true, false]} validates :approval_required, :inclusion => {:in => [true, false]} validates :musician_access, :inclusion => {:in => [true, false]} + validates :friends_can_join, :inclusion => {:in => [true, false]} validates :is_unstructured_rsvp, :inclusion => {:in => [true, false]} validates :legal_terms, :inclusion => {:in => [true]}, :on => :create validates :creator, :presence => true @@ -333,6 +334,7 @@ module JamRuby new_session.scheduled_duration = self.scheduled_duration new_session.musician_access = self.musician_access new_session.approval_required = self.approval_required + new_session.friends_can_join = self.friends_can_join new_session.fan_chat = self.fan_chat new_session.genre_id = self.genre_id new_session.legal_policy = self.legal_policy @@ -463,6 +465,13 @@ module JamRuby tracks end + # is the viewer 'friensd with the session?' + # practically, to be a friend with the session, you have to be a friend with the creator + # we could loosen to be friend of friends or friends with anyone in the session + def friends_with_session(user) + self.creator.friends?(user) + end + def can_join? user, as_musician if as_musician unless user.musician @@ -475,10 +484,9 @@ module JamRuby else return true end - else # the creator can always join, and the invited users can join - return self.creator == user || self.invited_musicians.exists?(user.id) || self.approved_rsvps.include?(user) + return self.creator == user || self.invited_musicians.exists?(user.id) || self.approved_rsvps.include?(user) || (self.friends_can_join && self.friends_with_session(user)) end else # it's a fan, and the only way a fan can join is if fan_access is true @@ -490,7 +498,7 @@ module JamRuby if self.musician_access || self.fan_access true else - self.creator == user || self.invited_musicians.exists?(user.id) || self.approved_rsvps.include?(user) || self.has_lesson_access?(user) + self.creator == user || self.invited_musicians.exists?(user.id) || self.approved_rsvps.include?(user) || self.creator.friends?(user) || self.has_lesson_access?(user) end end @@ -637,6 +645,7 @@ module JamRuby ms.description = options[:description] ms.genre_id = (options[:genres].length > 0 ? options[:genres][0] : nil) if options[:genres] ms.musician_access = options[:musician_access] + ms.friends_can_join = options[:friends_can_join] ms.approval_required = options[:approval_required] ms.fan_access = options[:fan_access] ms.fan_chat = options[:fan_chat] @@ -1115,6 +1124,99 @@ SQL [music_sessions, user_scores] end + + def self.scheduled_index(user, options) + session_id = options[:session_id] + genre = options[:genre] + lang = options[:lang] + keyword = options[:keyword] + offset = options[:offset] + limit = options[:limit] + day = options[:day] + timezone_offset = options[:timezone_offset] + + query = MusicSession.select('music_sessions.*') + query = query.where("old = FALSE") + query = query.where("scheduled_start IS NULL OR scheduled_start > (NOW() - (interval '15 minute'))") + query = query.where("music_sessions.canceled = FALSE") + query = query.where("description != 'Jam Track Session'") + query = query.where("music_sessions.id NOT IN (SELECT id FROM active_music_sessions)") + # one flaw in the join below is that we only consider if the creator of the session has asked you to be your friend, but you have not accepted. While this means you may always see a session by someone you haven't friended, the goal is to match up new users more quickly + query = query.joins( + %Q{ + + LEFT OUTER JOIN + rsvp_requests + ON rsvp_requests.music_session_id = music_sessions.id and rsvp_requests.user_id = '#{user.id}' AND rsvp_requests.chosen = true + + LEFT OUTER JOIN + invitations + ON + music_sessions.id = invitations.music_session_id AND invitations.receiver_id = '#{user.id}' + LEFT OUTER JOIN + friendships + ON + music_sessions.user_id = friendships.user_id AND friendships.friend_id = '#{user.id}' + LEFT OUTER JOIN + friendships as friendships_2 + ON + music_sessions.user_id = friendships_2.friend_id AND friendships_2.user_id = '#{user.id}' + } + ) + + # keep only rsvp/invitation/friend results. Nice tailored active list now! + query = query.where("open_rsvps = TRUE OR rsvp_requests.id IS NOT NULL OR invitations.id IS NOT NULL or music_sessions.user_id = '#{user.id}' OR (friendships.id IS NOT NULL AND friendships_2.id IS NOT NULL)") + + + # if not specified, default offset to 0 + offset ||= 0 + offset = offset.to_i + # if not specified, default limit to 20 + limit ||= 20 + limit = limit.to_i + + query = query.offset(offset) + 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', ?))", ActiveRecord::Base.connection.quote(keyword) + ':*') unless keyword.blank? + query = query.group("music_sessions.id, music_sessions.scheduled_start") + query = query.order("music_sessions.scheduled_start DESC") + + if !day.blank? && !timezone_offset.blank? + begin + 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 + 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 + # do nothing. bad date probably + @@log.warn("unable to parse day=#{day}, timezone_offset=#{timezone_offset}, e=#{e}") + end + else + sql =< NOW() - interval '#{UNSTARTED_INTERVAL_DAYS_SKIP} days' OR +music_sessions.scheduled_start > NOW() - interval '#{UNSTARTED_INTERVAL_DAYS_SKIP} days') +SQL + query = query.where(sql) + end + + #FROM music_sessions + # WHERE old = FALSE AND (scheduled_start IS NULL OR scheduled_start > (NOW() - (interval '15 minute'))) + # AND canceled = FALSE AND description != 'Jam Track Session' + # AND id NOT IN (SELECT id FROM active_music_sessions); + + query + 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, include_pending=false) MusicSession.sms_init(current_user, {session_id: music_session_id}, include_pending) diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb index 0f46e36d6..e0bb97fff 100644 --- a/ruby/lib/jam_ruby/models/notification.rb +++ b/ruby/lib/jam_ruby/models/notification.rb @@ -672,9 +672,11 @@ module JamRuby @@mq_router.server_publish_to_session(active_music_session, msg) end + # tell all your friends you joined a session def send_musician_session_join(music_session, user) - if music_session.musician_access || music_session.fan_access + # tell the creator's friends that they joined a session, because we changed Find session to show your friend's sessions + if music_session.creator.id == user.id || music_session.musician_access || music_session.fan_access friends = Friendship.where(:friend_id => user.id) user_followers = user.followers @@ -689,14 +691,14 @@ module JamRuby notification_msg = format_msg(NotificationTypes::MUSICIAN_SESSION_JOIN, {:user => user}) friends_and_followers.each do |ff| - notification = Notification.new - notification.description = NotificationTypes::MUSICIAN_SESSION_JOIN - notification.source_user_id = user.id - notification.target_user_id = ff.id - notification.session_id = music_session.id - notification.save - if ff.online? + notification = Notification.new + notification.description = NotificationTypes::MUSICIAN_SESSION_JOIN + notification.source_user_id = user.id + notification.target_user_id = ff.id + notification.session_id = music_session.id + notification.save + msg = @@message_factory.musician_session_join( ff.id, music_session.id, @@ -710,15 +712,6 @@ module JamRuby ) @@mq_router.publish_to_user(ff.id, msg) - - else - # if APP_CONFIG.send_join_session_email_notifications - # begin - # UserMailer.musician_session_join(ff, notification_msg, music_session.id).deliver_now - # rescue => e - # @@log.error("Unable to send MUSICIAN_SESSION_JOIN email to user #{ff.email} #{e}") - # end - # end 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 502533e4f..464e93c9b 100644 --- a/ruby/spec/jam_ruby/models/music_session_spec.rb +++ b/ruby/spec/jam_ruby/models/music_session_spec.rb @@ -495,6 +495,105 @@ describe MusicSession do end end + describe "sms_index_2" do + describe "simple" do + let(:conn) { FactoryGirl.create(:connection, user: creator, locidispid: creator.last_jam_locidispid) } + let(:searcher) { FactoryGirl.create(:user, last_jam_locidispid: 2) } + let(:searcher_conn) { FactoryGirl.create(:connection, user: searcher, ip_address: '2.2.2.2', locidispid: searcher.last_jam_locidispid) } + let(:default_opts) { {client_id: searcher_conn.client_id} } + let(:network_score) { 20 } + + it "no results" do + music_sessions = MusicSession.scheduled_index(searcher, {}) + end + + it "one session shows/hides based on open_rsvps" do + + music_session = FactoryGirl.create(:music_session, creator: creator, scheduled_start: nil) + music_sessions = MusicSession.scheduled_index(searcher, {}) + music_sessions.length.should == 1 + + music_session.open_rsvps = false + music_session.save! + music_sessions = MusicSession.scheduled_index(searcher, {}) + music_sessions.length.should == 0 + end + + + it "one session, one RSVP (creator)" do + music_session = FactoryGirl.create(:music_session, creator: creator) + music_sessions = MusicSession.scheduled_index(searcher, {}) + music_sessions.length.should == 1 + end + + it "skip session with past due scheduled_start time" do + interval = MusicSession::UNSTARTED_INTERVAL_DAYS_SKIP + dd = Time.now - (interval.to_i + 1).days + Timecop.travel(dd) + msess1 = FactoryGirl.create(:music_session, creator: creator, scheduled_start: dd) + msess2 = FactoryGirl.create(:music_session, creator: creator) + music_sessions = MusicSession.scheduled_index(searcher, {}) + music_sessions.length.should == 1 + expect(music_sessions[0].id).to eq(msess2.id) + end + + it "filters sessions in the past" do + + music_session = FactoryGirl.create(:music_session, creator: creator) + music_sessions = MusicSession.scheduled_index(searcher, {}) + music_sessions.length.should == 1 + + # 15 minutes is the edge of forgiveness + music_session.scheduled_start = 16.minutes.ago + music_session.save! + + music_sessions = MusicSession.scheduled_index(searcher, {}) + music_sessions.length.should == 0 + + # this should still fall in time + music_session.scheduled_start = 14.minutes.ago + music_session.save! + + music_sessions = MusicSession.scheduled_index(searcher, {}) + music_sessions.length.should == 1 + end + + it "one session, one RSVP (creator), one invitation" do + # create an invitee, and friend them with the creator (you have to be friends to send an invite) + invitee = FactoryGirl.create(:user) + FactoryGirl.create(:friendship, user: creator, friend: invitee) + FactoryGirl.create(:friendship, user: invitee, friend: creator) + music_session = FactoryGirl.create(:music_session, creator: creator) + FactoryGirl.create(:invitation, receiver:invitee, sender:creator, music_session: music_session) + + music_sessions = MusicSession.scheduled_index(searcher, default_opts) + music_sessions.length.should == 1 + + #search with the invitee this time. + invitee_conn = FactoryGirl.create(:connection, user: invitee) + music_sessions = MusicSession.scheduled_index(invitee, {}) + music_sessions.length.should == 1 + end + + it "does not show when it goes active" do + # we create a scheduled session--it should return + music_session = FactoryGirl.create(:music_session, creator: creator, scheduled_start: nil) + music_sessions = MusicSession.scheduled_index(searcher, {}) + music_sessions.length.should == 1 + + # but then make an active session for this scheduled session + ams = FactoryGirl.create(:active_music_session, music_session: music_session, creator: creator, musician_access: true) + music_sessions = MusicSession.scheduled_index(searcher, {}) + music_sessions.length.should == 0 + + # finally, delete the active session, and see results go back to one + ams.delete + music_sessions = MusicSession.scheduled_index(searcher, {}) + music_sessions.length.should == 1 + end + end + + end describe "sms_index", no_transaction: true do describe "simple" do diff --git a/web/app/assets/javascripts/dialog/sessionSettingsDialog.js b/web/app/assets/javascripts/dialog/sessionSettingsDialog.js index 92e577ecf..97b1af162 100644 --- a/web/app/assets/javascripts/dialog/sessionSettingsDialog.js +++ b/web/app/assets/javascripts/dialog/sessionSettingsDialog.js @@ -66,6 +66,11 @@ $('#session-settings-fan-access').val('listen-chat-band'); } + // friends can join + + var friendsCanJoinValue = currentSession.friends_can_join ? "true" : "false" + $('#session-settings-friends-can-join').val(friendsCanJoinValue) + var $controllerSelect = $('#session-settings-master-mix-controller') $controllerSelect.empty() @@ -98,12 +103,14 @@ context.JK.dropdown($('#session-settings-language')); context.JK.dropdown($('#session-settings-musician-access')); context.JK.dropdown($('#session-settings-fan-access')); + context.JK.dropdown($('#session-settings-friends-can-join')); context.JK.dropdown($('#session-settings-master-mix-controller')); var easyDropDownState = canPlayWithOthers.canPlay ? 'enable' : 'disable' $('#session-settings-musician-access').easyDropDown(easyDropDownState) $('#session-settings-fan-access').easyDropDown(easyDropDownState) + } function addNotation(notation) { @@ -164,7 +171,15 @@ data.fan_access = true; data.fan_chat = true; } - + + var friendsCanJoin = $('#session-settings-friends-can-join').val(); + if (friendsCanJoin == 'true') { + data.friends_can_join = true + } + else { + data.friends_can_join = false + } + rest.updateSession($('#session-settings-id').val(), data).done(settingsSaved) .done(function(response) { context.SessionActions.updateSession.trigger(response); diff --git a/web/app/assets/javascripts/notificationPanel.js b/web/app/assets/javascripts/notificationPanel.js index aae85ffef..8a1cc8078 100644 --- a/web/app/assets/javascripts/notificationPanel.js +++ b/web/app/assets/javascripts/notificationPanel.js @@ -346,6 +346,7 @@ $action_btn.click(function() { openTerms(payload); }); + context.SessionsActions.updateSession(payload.session_id) } else if (type === context.JK.MessageType.JOIN_REQUEST_REJECTED) { @@ -740,6 +741,8 @@ handleNotification(payload, header.type); + context.SessionsActions.updateSession(payload.session_id) + app.notify({ "title": "Session Invitation", "text": payload.msg @@ -810,18 +813,12 @@ var showNotification = false; var callback; if (context.JK.currentUserMusician) { - // user is MUSICIAN; musician_access = TRUE - if (payload.musician_access) { - showNotification = true; - okText = "JOIN"; - callback = joinSession; - } - // user is MUSICIAN; fan_access = TRUE - else if (payload.fan_access) { - showNotification = true; - okText = "LISTEN"; - callback = listenToSession; - } + + context.SessionsActions.updateSession(payload.session_id) + + showNotification = true; + okText = "JOIN"; + callback = joinSession; } else { // user is FAN; fan_access = TRUE diff --git a/web/app/assets/javascripts/react-components/FindSessionFriends.js.jsx.coffee b/web/app/assets/javascripts/react-components/FindSessionFriends.js.jsx.coffee index 413e0c9a9..8a77e31db 100644 --- a/web/app/assets/javascripts/react-components/FindSessionFriends.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/FindSessionFriends.js.jsx.coffee @@ -1,37 +1,18 @@ context = window +SessionsActions = @SessionsActions + @FindSessionFriends = React.createClass({ - mixins: [Reflux.listenTo(@AppStore, "onAppInit")] + mixins: [Reflux.listenTo(@AppStore, "onAppInit") + Reflux.listenTo(@SessionsStore, "onSessionsChanged")] - LIMIT: 50 + TYPE_SESSION: 'my' + searched_ever: false + registeredInfiniteScroll: false getInitialState: () -> - {sessions: [], currentPage: 0, next: null, searching: false, count: 0} - - - search: () -> - return if @state.searching - - $root = $(@getDOMNode()) - # disable scroll watching now that we've started a new search - $root.find('.content-body-scroller').off('scroll') - $root.find('.end-of-sessions-list').hide() - - - # we have to make sure the query starts from page 1, and no 'next' from previous causes a 'since' to show up - query = @defaultQuery({page: 0}) - delete query.since - @rest.findFriendSessions(query) - .done((response) => - @setState({sessions: response.sessions, searching: false, first_search: false, currentPage: 1}) - ) - .fail((jqXHR) => - @app.notifyServerError jqXHR, 'Search Unavailable' - @setState({searching: false, first_search: false}) - ) - - @setState({currentPage: 0, sessions:[], searching: true}) + {active: false, sessions: [], searching: false, count: 0, currentPage: 0, end:false} sessionResults: () -> results = [] @@ -39,110 +20,143 @@ context = window results.push(``) results + help: () -> + `
+

How This Page Works

+

This page will show sessions tailored for you:

+
    +
  • sessions created by a friend (invite others to JamKazam!)
  • +
  • sessions for which you have an invitation
  • +
  • sessions for which you have a RSVP
  • +
+
+

Sit Back and Relax

+

This page will automatically update when a friend creates a session, or you are invited to one.

+

So if your friend is creating a session soon, just sit tight and watch for sessions to show up!

+
` + + refresh: () -> + if @state.searching + return + + SessionsActions.clearSessions.trigger(@TYPE_SESSION, @query()) + render: () -> results = @sessionResults() - className = "sessions-for-me" + className = "content-body-scroller sessions-for-me" if(@props.active) className = className + " active" + firstTime = null + if results.length == 0 + if @state.searching + results = `... Searching ...` + else + # help = @help() + results = `None Found` + + help = @help() + + refreshButtonClasses = "button-grey btn-refresh" + + if @state.searching + refreshButtonClasses = refreshButtonClasses += " disabled" + `
-
-
-
-
-

sessions for me

+
+
+
+

sessions for me

-
- REFRESH -
-
- -
-
- -
- -
- - - - - - - {results} -
SESSIONMUSICIANSACTIONS
+ +
+ +
+ +
+ +
+ + + + + + + + + + {results} + +
SESSIONMUSICIANSACTIONS
+
+ {help}
` + query: () -> - defaultQuery: (extra) -> + $root = $(this.getDOMNode()) + keyword = $root.find('input.session-keyword-srch').val() + if keyword == '' + keyword = undefined query = - limit: @LIMIT - offset: @state.currentPage * @LIMIT + keyword: keyword + + query - $.extend(query, extra) componentDidMount: () -> @EVENTS = context.JK.EVENTS @rest = context.JK.Rest() @logger = context.JK.logger - @search() - componentDidUpdate: () -> + componentDidUpdate: (prevProps, prevState) -> + + if @props.active && !prevProps.active && !@searched_ever + SessionsActions.updateSessions.trigger('my') + @searched_ever = true + $root = $(this.getDOMNode()) - $scroller = $root.find('.content-body-scroller') + $scroller = $root - if @state.next == null - $scroller = $root.find('.content-body-scroller') - # if we less results than asked for, end searching - #$scroller.infinitescroll 'pause' - $scroller.off('scroll') - if @state.currentPage == 1 and @state.sessions.length == 0 - $root.find('.end-of-sessions-list').text('No friend sessions found').show() - @logger.debug("FindSessions: empty search") - else if @state.currentPage > 0 - $noMoreSessions = $root.find('.end-of-sessions-list').text('No more sessions').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 - else + if !@registeredInfiniteScroll @registerInfiniteScroll($scroller) registerInfiniteScroll: ($scroller) -> + @registeredInfiniteScroll = true + $scroller.off('scroll') $scroller.on('scroll', () => # be sure to not fire off many refreshes when user hits the bottom - return if @refreshing + return if @state.searching + + if @state.end + return if $scroller.scrollTop() + $scroller.innerHeight() + 100 >= $scroller[0].scrollHeight - $scroller.append('
... Loading more Sessions ...
') - @refreshing = true - @logger.debug("refreshing more sessions for infinite scroll") - @setState({searching: true}) - @rest.findFriendSessions(@defaultQuery()) - .done((json) => - @setState({ - sessions: @state.sessions.concat(json.sessions), - currentPage: @state.currentPage + 1 - }) - ) - .always(() => - $scroller.find('.infinite-scroll-loader-2').remove() - @refreshing = false - @setState({searching: false}) - ) + SessionsActions.updateSessions.trigger(@TYPE_SESSION, @query()) ) + inviteFriends:() -> + JK.InvitationDialogInstance.showEmailDialog(); + onAppInit: (@app) -> return + + onSessionsChanged: (sessionsChanged) -> + if sessionsChanged.type == @TYPE_SESSION + @setState(sessionsChanged) + }) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/FindSessionOpen.js.jsx.coffee b/web/app/assets/javascripts/react-components/FindSessionOpen.js.jsx.coffee new file mode 100644 index 000000000..b0948c61f --- /dev/null +++ b/web/app/assets/javascripts/react-components/FindSessionOpen.js.jsx.coffee @@ -0,0 +1,193 @@ +context = window + +SessionsActions = @SessionsActions + +@FindSessionOpen = React.createClass({ + + mixins: [Reflux.listenTo(@AppStore, "onAppInit") + Reflux.listenTo(@SessionsStore, "onSessionsChanged")] + + searched_ever: false + registeredInfiniteScroll: false + + getInitialState: () -> + {active: false, sessions: [], searching: false, count: 0, currentPage: 0, end: false} + + sessionResults: () -> + results = [] + for session in @state.sessions + results.push(``) + results + + help: () -> + if this.props.mode == 'open' + `
+

How This Page Works

+

This page will show only public jam sessions.

+

Most recent sessions are shown an the top.

+

This list of public sessions does not auto-update. You will have to push the REFRESH button to see new sessions.

+
` + else if this.props.mode == 'my' + `
+

How This Page Works

+

This page will show sessions tailored for you:

+
    +
  • sessions created by a friend (invite others to JamKazam)
  • +
  • sessions for which you have an invitation
  • +
  • sessions for which you have a RSVP
  • +
+
+

Sit Back and Relax

+

This page will automatically update when a friend creates a session, or you are invited to one.

+

So if your friend is creating a session soon, just sit tight and watch for sessions to show up!

+
` + else + `
+

How This Page Works

+

This page will show these scheduled sessions:

+
    +
  • public jam sessions
  • +
  • sessions created by a friend (invite others to JamKazam)
  • +
  • sessions for which you have an invitation
  • +
  • sessions for which you have a RSVP
  • +
+
+

Reserve a spot

+

If you find a session you are interested in attending later, then click the RSVP icon to reserve your spot.

An email will be sent to the creator of the session, and they can then approve your request.

+

Schedule your Own

+

Don't see a session you like? You can schedule your own session for others to RSVP to at the create session screen. You will receive emails when others RSVP.

+
` + + + refresh: () -> + if @state.searching + return + + SessionsActions.clearSessions.trigger(@props.mode, @query()) + + render: () -> + results = @sessionResults() + + className = "content-body-scroller sessions-for-open" + + if(@props.active) + className = className + " active" + + firstTime = null + if results.length == 0 + if @state.searching + results = `... Searching ...` + else + # help = @help() + results = `None Found` + + if @state.end && results.length > 5 + results.push(`End of Search Results
`) + help = @help() + + refreshButtonClasses = "button-grey btn-refresh" + + if @state.searching + refreshButtonClasses = refreshButtonClasses += " disabled" + + + `
+
+
+
+

sessions for me

+ + +
+ REFRESH +
+
+ +
+
+ +
+ +
+ + + + + + + + + + {results} + +
SESSIONMUSICIANSACTIONS
+
+ {help} +
+
+
` + + + query: () -> + + $root = $(this.getDOMNode()) + keyword = $root.find('input.session-keyword-srch').val() + if keyword == '' + keyword = undefined + query = + keyword: keyword + + query + + componentDidMount: () -> + @EVENTS = context.JK.EVENTS + @rest = context.JK.Rest() + @logger = context.JK.logger + + + componentDidUpdate: (prevProps, prevState) -> + + if !@props.screenActive && prevProps.screenActive + @searched_ever = false + + if @props.screenActive && @props.active && !@searched_ever + SessionsActions.updateSessions.trigger(@props.mode) + @searched_ever = true + + #if @props.active && !prevProps.active && !@searched_ever + + $root = $(this.getDOMNode()) + $scroller = $root + + if !@registeredInfiniteScroll + @registerInfiniteScroll($scroller) + + registerInfiniteScroll: ($scroller) -> + @registeredInfiniteScroll = true + + $scroller.off('scroll') + $scroller.on('scroll', () => + + # be sure to not fire off many refreshes when user hits the bottom + return if @state.searching + + return if @state.end + + if $scroller.scrollTop() + $scroller.innerHeight() + 400 >= $scroller[0].scrollHeight + SessionsActions.updateSessions.trigger(@props.mode, @query()) + ) + + inviteFriends:() -> + JK.InvitationDialogInstance.showEmailDialog(); + + onAppInit: (@app) -> + return + + onSessionsChanged: (sessionsChanged) -> + if sessionsChanged.type == @props.mode + @setState(sessionsChanged) + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/FindSessionRow.js.jsx.coffee b/web/app/assets/javascripts/react-components/FindSessionRow.js.jsx.coffee index e0a582cad..05cb6b0cc 100644 --- a/web/app/assets/javascripts/react-components/FindSessionRow.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/FindSessionRow.js.jsx.coffee @@ -1,14 +1,24 @@ context = window rest = window.JK.Rest() logger = context.JK.logger +EVENTS = context.JK.EVENTS +SessionsActions = context.SessionsActions +AppStore = context.AppStore +MAX_MINUTES_SHOW_START = 15 @FindSessionRow = React.createClass({ + mixins: [Reflux.listenTo(AppStore, "onAppInit")] + + ui: null + + getInitialState: () -> + {rsvpToggle: false, openSlotToggle: false} + createInstrument: (participant) -> instruments = [] existingTracks = [] - logger.debug("Find:Finding instruments. Participant tracks:", participant.tracks) for track in participant.tracks if existingTracks.indexOf(track.instrument_id) < 0 @@ -22,31 +32,10 @@ logger = context.JK.logger createInSessionUser: (participant) -> instruments = @createInstrument(participant) - id = participant.user.id; - name = participant.user.name; - userId = id - avatar_url = context.JK.resolveAvatarUrl(participant.user.photo_url) - profile_url = "/client#/profile/" + id - musician_name = name - more_link = '' - ` - - - - - - - {musician_name} - - -
{instruments}
- - {more_link}  - ` + `` - - createOpenSlot:(slot) -> + createOpenSlot:(slot, isLast) -> inst = context.JK.getInstrumentIcon24(slot.instrument_id); @@ -55,9 +44,20 @@ logger = context.JK.logger # this is to allow unstructured RSVPs to not specify proficiency_desc proficiency_desc = "Any Skill Level" - moreLinkHtml = ''; + toggle = @state.openSlotToggle + if isLast - moreLinkHtml = `more` + remainingStyles = {} + text = null + computedClass = "details-arrow" + if toggle + text = 'less' + computedClass += " arrow-up-orange" + else + text = 'more' + computedClass += " arrow-down-orange" + + moreLinkHtml = `{text}` instrument_url = inst instrument = slot.description @@ -65,9 +65,9 @@ logger = context.JK.logger more_link = moreLinkHtml - ` + ` - +
{instrument} ({proficiency})
@@ -86,31 +86,23 @@ logger = context.JK.logger moreLinkHtml = ''; if isLast - moreLinkHtml = `more` + # false means hide, true means show + toggle = @state.rsvpToggle - #instruments = @createInstrument(user) - id = user.id; - name = user.name; - userId = id - avatar_url = context.JK.resolveAvatarUrl(user.photo_url) - profile_url = "/client#/profile/" + id - musician_name = name - more_link = '' + remainingStyles = {} + text = null + computedClass = "details-arrow" + if toggle + text = 'less' + computedClass += " arrow-up-orange" + else + text = 'more' + computedClass += " arrow-down-orange" - ` - - - - - - - {musician_name} - - -
{instruments}
- - {more_link}  - ` + moreLinkHtml = `{text}` + + + `` @@ -118,12 +110,17 @@ logger = context.JK.logger inSessionUsers = [] result = [] - console.log("render active", session) if session.active_music_session && session.active_music_session.participants && session.active_music_session.participants.length > 0 for participant in session.active_music_session.participants inSessionUsers.push(participant.user.id); result.push(@createInSessionUser(participant)) + if result.length == 0 + result = `Abandoned` + else + result = ` + {result} +
` return [result, inSessionUsers] @@ -139,8 +136,12 @@ logger = context.JK.logger last = session.approved_rsvps.slice(3) for approved_rsvp in first - firstResults.push(@createRsvpUser(approved_rsvp, session, approvedRsvpCount > 3 && i == 2)) + if approved_rsvp.id == session.user_id + continue + firstResults.push(@createRsvpUser(approved_rsvp, session, approvedRsvpCount > 3 && i == 0)) for approved_rsvp in last + if approved_rsvp.id == session.user_id + continue lastResults.push(@createRsvpUser(approved_rsvp, session, false)) [firstResults, lastResults] @@ -155,48 +156,248 @@ logger = context.JK.logger if session['is_unstructured_rsvp?'] firstResults.push(@createOpenSlot({description: 'Any Instrument'})) + i = 0 if session.open_slots + openSlotCount = session.open_slots.length for openSlot in session.open_slots if i < 3 firstResults.push(@createOpenSlot(openSlot, openSlotCount > 3 && i == 2)) else remainingResults.push(@createOpenSlot(openSlot, false)) + i++ + return [firstResults, remainingResults] - showJoinLink: (session, inSessionUsers) -> - showJoinLink = session.musician_access - if session.approved_rsvps - for approved_rsvps in session.approved_rsvps - # do not show the user in this section if he is already in the session - if $.inArray(approved_rsvps.id, inSessionUsers) == -1 - if approved_rsvps.id == context.JK.currentUserId - showJoinLink = true - else - showJoinLink = true + joinLink: (session, inSessionUsers) -> + #showJoinLink = session.musician_access + #if session.approved_rsvps + # for approved_rsvps in session.approved_rsvps + # # do not show the user in this section if he is already in the session + # if $.inArray(approved_rsvps.id, inSessionUsers) == -1 + # if approved_rsvps.id == context.JK.currentUserId + # showJoinLink = true + # else + # showJoinLink = true - showJoinLink - - rsvps: (session, rsvp_musicians_first_3, rsvp_musicians_remaining) -> - - if session.create_type == 'quick-start' || !rsvp_musicians_first_3 || rsvp_musicians_first_3.length == 0 + if @props.mode == 'upcoming' return null - ` - RSVPs: + joinText = 'Join' + if session.highlight + highlight = session.highlight + if highlight.updated + joinText = 'Ready!' + + `
+ +
+
+
{joinText}
+
` + + + rsvpLink: (session) -> + + pendingRsvpId = null + approvedRsvpId = null + hasInvitation = false + + for pending_rsvp_request in session.pending_rsvp_requests + if pending_rsvp_request.user_id == context.JK.currentUserId + pendingRsvpId = pending_rsvp_request.id + break + + for approved_rsvp in session.approved_rsvps + if approved_rsvp.id == context.JK.currentUserId + approvedRsvpId = approved_rsvp.rsvp_request_id + break + + if session.invitations + for pending_invitation in session.invitations + if context.JK.currentUserId == pending_invitation.receiver_id + hasInvitation = true + break + + + errorMsg = null + error = false + + if error + errorMsg = `You cannot RSVP to this session.` + + # if this is your own session, let you start it immediately + if context.JK.currentUserId == session.user_id + result = `
Start session now?

This is your session.
` + return result + + # if you are approved RSVP person, let you cancel it + if approvedRsvpId + + if session.scheduled_start && @showStartSessionButton(session.scheduled_start) + # give user both option to start session, and also cancel RSVP + result = `` + return result + else + # user can just cancel their RSVP + result = `` + return result + + else if hasInvitation + if session.scheduled_start && @showStartSessionButton(session.scheduled_start) + # give user both option to start session, and also cancel RSVP + result = `
Start session now? | You have an invite to this session.

You can join it when it starts.
` + return result + else + # user can just cancel their RSVP + result = `
You have an invite to this session.

You can join it when it starts.
` + return result + + else if pendingRsvpId + result = `` + return result + + else if !session['is_unstructured_rsvp?'] && session.open_slots.length == 0 + result = `
No more open positions.
` + return result + + + else if !session.open_rsvps && !hasInvitation + result = `
You need an invitation to RSVP to this session.
` + return result + + + `
+ {errorMsg} + +
+
+
RSVP
+
` + + openSlots: (session, open_slots_first_3, open_slots_remaining) -> + + # false means hide, true means show + openSlotToggle = @state.openSlotToggle + + remainingStyles = {} + if openSlotToggle + remainingStyles.display = 'block' + else + remainingStyles.display = 'none' + ` + Still Needed: - - {rsvp_musicians_first_3} +
+ + {open_slots_first_3} +
-
- - {rsvp_musicians_remaining} +
+
+ + {open_slots_remaining} +
` + rsvps: (session, rsvp_musicians_first_3, rsvp_musicians_remaining, open_slots_first_3) -> + + if session.create_type == 'quick-start' || ((!rsvp_musicians_first_3 || rsvp_musicians_first_3.length == 0) && (!open_slots_first_3 || open_slots_first_3.length == 0)) + return null + + # if no rsvps yet some open slots + if (!rsvp_musicians_first_3 || rsvp_musicians_first_3.length == 0) && (open_slots_first_3 && open_slots_first_3.length > 0) + return ` + RSVPs: + +
+ None yet +
+ + ` + + + # false means hide, true means show + rsvpToggle = @state.rsvpToggle + + remainingStyles = {} + if rsvpToggle + remainingStyles.display = 'block' + else + remainingStyles.display = 'none' + ` + RSVPs: + + + + {rsvp_musicians_first_3} + +
+
+ + + {rsvp_musicians_remaining} + +
+
+ + ` + + componentDidMount: () -> + @ui = new context.JK.UIHelper(AppStore.app) + + ensuredCallback: (sessionId) -> + context.JK.SessionUtils.joinSession(sessionId) + + joinLinkClicked: (session) -> + context.JK.SessionUtils.ensureValidClient(@app, context.JK.GearUtils, @ensuredCallback.bind(this, session.id)) + + rsvpLinkClicked: (session) -> + @ui.launchRsvpSubmitDialog(session.id) + .one(EVENTS.RSVP_SUBMITTED, () -> SessionsActions.updateSession.trigger(session.id)) + .one(EVENTS.DIALOG_CLOSED, () -> + $(this).unbind(EVENTS.RSVP_SUBMITTED); + ) + return false + + toggleRsvp: () -> + @setState(rsvpToggle: !@state.rsvpToggle) + + toggleOpenSlot: (sessionId) -> + @setState(openSlotToggle: !@state.openSlotToggle) + + startSessionNow: (session) -> + @ui.launchSessionStartDialog(session) + + showStartSessionButton: (scheduledStart) -> + now = new Date() + scheduledDate = new Date(scheduledStart) + minutesFromStart = (scheduledDate.getTime() - now.getTime()) / (1000 * 60) + minutesFromStart <= MAX_MINUTES_SHOW_START + + cancelRsvpClicked: (session, approvedRsvpId) -> + @ui.launchRsvpCancelDialog(session.id, approvedRsvpId) + .one(EVENTS.RSVP_CANCELED, () -> SessionsActions.updateSession.trigger(session.id)) + .one(EVENTS.DIALOG_CLOSED, () -> + $(this).unbind(EVENTS.RSVP_CANCELED); + ) + return false + + + inSessionMusicians: (in_session_musicians) -> + if @props.mode == 'upcoming' + return null + + ` + In Session: + + {in_session_musicians} + + ` render: () -> session = @props.session @@ -209,12 +410,29 @@ logger = context.JK.logger [in_session_musicians, inSessionUsers] = @inSessionUsersHtml(session) [rsvp_musicians_first_3, rsvp_musicians_remaining] = @createRsvpUsers(session) [open_slots_first_3, open_slots_remaining] = @createOpenSlots(session) - showJoinLink = @showJoinLink(session, inSessionUsers) + rsvps = @rsvps(session, rsvp_musicians_first_3, rsvp_musicians_remaining, open_slots_first_3) + + + joinLink = @joinLink(session, inSessionUsers) showListenLink = session.fan_access && session.active_music_session && session.active_music_session.mount - join_link_display_style = {display: "none"} + showListenLink = false # for now... XXX + + openSlots = null + + scheduled_start = null + rsvpLink = null + if @props.mode == 'upcoming' + + openSlots = @openSlots(session, open_slots_first_3, open_slots_remaining) + + scheduled_start = ` + {session.pretty_scheduled_start_with_timezone} + ` + + rsvpLink = @rsvpLink(session) + + listen_link_display_style = {display: "none"} - if showJoinLink - join_link_display_style = {display: "block"} if showListenLink listen_link_display_style = {display: "inline-block"} listen_link_text = '' @@ -226,32 +444,42 @@ logger = context.JK.logger else listen_link_text = ''; - rsvps = @rsvps(rsvp_musicians_first_3, rsvp_musicians_remaining) + remark = null + if session.highlight + highlight = session.highlight + if highlight.new + remark = `
NEW!
` + + + inSessionMusicians = @inSessionMusicians(in_session_musicians) + ` + {remark} - - - - - - - + + + + + + + + + + {scheduled_start} + +
{name}{genres}
{description}
{name}{genres}
{description}
- - - - + + {inSessionMusicians} {rsvps} + {openSlots} +
In Session: - - {in_session_musicians} -
-
@@ -263,12 +491,8 @@ logger = context.JK.logger
{listen_link_text}?
-
- -
-
- Join -
+ {joinLink} + {rsvpLink}
diff --git a/web/app/assets/javascripts/react-components/FindSessionScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/FindSessionScreen.js.jsx.coffee index ae12b9c2e..0edba8f2f 100644 --- a/web/app/assets/javascripts/react-components/FindSessionScreen.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/FindSessionScreen.js.jsx.coffee @@ -1,6 +1,7 @@ context = window MIX_MODES = context.JK.MIX_MODES +SessionsActions = @SessionsActions @FindSessionScreen = React.createClass({ @@ -10,7 +11,7 @@ MIX_MODES = context.JK.MIX_MODES instrument_logo_map: context.JK.getInstrumentIconMap24() getInitialState: () -> - {activeTab: 'friends', search: '', type: 'user-input'} + {activeTab: '', search: '', type: 'user-input', screenActive: false} generateProperties: (tab) -> @@ -28,33 +29,35 @@ MIX_MODES = context.JK.MIX_MODES classNames(classes) + tabActivate: (tab) -> + @setState({activeTab: tab}) + render: () -> - friendTabClasses = @generateTabClasses('friends') - publicTabClasses = @generateTabClasses('public') - scheduledTabClasses = @generateTabClasses('scheduled') + myTabClasses = @generateTabClasses('my') + openTabClasses = @generateTabClasses('open') + upcomingTabClasses = @generateTabClasses('upcoming') - friendProperties = @generateProperties('friends') - publicProperties = @generateProperties('public') - scheduledProperties = @generateProperties('scheduled') + myProperties = @generateProperties('my') + openProperties = @generateProperties('open') + upcomingProperties = @generateProperties('upcoming') search = '' - `
-
+ `
@@ -62,23 +65,33 @@ MIX_MODES = context.JK.MIX_MODES
- -
-
` + + + +
` componentDidMount: () -> return - componentDidUpdate: () -> + componentDidUpdate: (prevProps, prevState) -> return beforeShow: () -> return afterShow: () -> - return + SessionsActions.watching.trigger(true) + + if @state.activeTab == '' + @setState({activeTab: 'my'}) + + @setState({screenActive:true}) + beforeHide: () -> + SessionsActions.watching.trigger(false) + SessionsActions.resetSessions.trigger() + @setState({screenActive:false}) onAppInit: (@app) -> @EVENTS = context.JK.EVENTS @@ -88,6 +101,7 @@ MIX_MODES = context.JK.MIX_MODES screenBindings = 'beforeShow': @beforeShow 'afterShow': @afterShow + 'beforeHide': @beforeHide @app.bindScreen('findSession', screenBindings) diff --git a/web/app/assets/javascripts/react-components/HoverUser.js.jsx.coffee b/web/app/assets/javascripts/react-components/HoverUser.js.jsx.coffee new file mode 100644 index 000000000..7dbd0ccf2 --- /dev/null +++ b/web/app/assets/javascripts/react-components/HoverUser.js.jsx.coffee @@ -0,0 +1,39 @@ +context = window +rest = window.JK.Rest() +logger = context.JK.logger +EVENTS = context.JK.EVENTS +SessionsActions = context.SessionsActions +AppStore = context.AppStore +MAX_MINUTES_SHOW_START = 15 + +@HoverUser = React.createClass({ + + render: () -> + + user = this.props.user + + userId = user.id + name = user.name + avatar_url = context.JK.resolveAvatarUrl(user.photo_url) + profile_url = "/client#/profile/" + userId + musician_name = name + + ` + + + + + + + {musician_name} + + +
{this.props.instruments}
+ + {this.props.more} + ` + + componentDidMount: () -> + $root = $(this.getDOMNode()) + context.JK.bindHoverEvents($root, "data-hoveraction") +}) diff --git a/web/app/assets/javascripts/react-components/actions/FriendActions.js.coffee b/web/app/assets/javascripts/react-components/actions/FriendActions.js.coffee new file mode 100644 index 000000000..6916a9c14 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/FriendActions.js.coffee @@ -0,0 +1,5 @@ +context = window + +@FriendActions = Reflux.createActions({ + updateFriends: {} +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/actions/SessionsActions.js.coffee b/web/app/assets/javascripts/react-components/actions/SessionsActions.js.coffee new file mode 100644 index 000000000..448c97ea3 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/SessionsActions.js.coffee @@ -0,0 +1,9 @@ +context = window + +@SessionsActions = Reflux.createActions({ + updateSession: {} + updateSessions: {} + resetSessions: {} + clearSessions: {} + watching: {} +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee b/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee index 223fe0fcf..157e0998e 100644 --- a/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee @@ -6,9 +6,12 @@ logger = context.JK.logger { listenables: @AppActions + app: null onAppInit: (app) -> + @app = app @trigger(app) + onOpenExternalUrl: (href) -> logger.debug("opening external url #{href}") diff --git a/web/app/assets/javascripts/react-components/stores/FriendStore.js.coffee b/web/app/assets/javascripts/react-components/stores/FriendStore.js.coffee new file mode 100644 index 000000000..32d00e891 --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/FriendStore.js.coffee @@ -0,0 +1,35 @@ +$ = jQuery +context = window +logger = context.JK.logger +rest = context.JK.Rest() +FriendActions = @FriendActions + +@FriendStore = Reflux.createStore( + { + listenables: FriendActions + + friends: [] + + init: -> + # Register with the app store to get @app + this.listenTo(context.AppStore, this.onAppInit) + + onAppInit: (@app) -> + return + + updateFriends: (friends) -> + @friends = friends + + issueChange: (type) -> + @trigger({friends: @friends}) + + isFriend: (userId) -> + + found = false + for friend in @friends + if friend.id == userId + found = true + break + found + } +) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/stores/SessionsStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SessionsStore.js.coffee new file mode 100644 index 000000000..ee3c8fefb --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/SessionsStore.js.coffee @@ -0,0 +1,169 @@ +$ = jQuery +context = window +logger = context.JK.logger +rest = context.JK.Rest() +SessionActions = @SessionActions + +@SessionsStore = Reflux.createStore( + { + listenables: SessionsActions + + my: {sessions: [], currentPage: 0, next: null, searching: false, count: 0} + open: {sessions: [], currentPage: 0, next: null, searching: false, count: 0} + upcoming: {sessions: [], currentPage: 0, next: null, searching: false, count: 0} + + highlight: null + + LIMITS: {my: 50, open: 50, upcoming: 20} + + TYPE_MY: 'my' + TYPE_OPEN: 'open' + TYPE_UPCOMING: 'upcoming' + + # if the Find Session screen isn't active, don't 'watch' automatically + watchingState: false + + init: -> + # Register with the app store to get @app + this.listenTo(context.AppStore, this.onAppInit) + + onAppInit: (@app) -> + return + + defaultQuery: (extra, type) -> + query = + limit: @LIMITS[type], + offset: this[type].currentPage * @LIMITS[type] + + $.extend(query, extra) + + updateSessionState:(session, type) -> + state = this[type] + + # so callers can realize this is a pinpointed session update + session.highlight = {new: false, updated: false} + @highlight = session + + + foundIndex = -1 + + console.log("STATE", state) + if state.sessions.length > 0 + for x in [0..state.sessions.length - 1] + if state.sessions[x].id == session.id + foundIndex = x + break + + # session did not exist already. Add it to front of list because that's where interesting stuff should be? + if foundIndex == -1 + session.highlight.new = true + console.log("SessionStore.updateSession: adding a new one") + state.sessions.unshift(session) + else + session.highlight.updated = true + console.log("SessionStore.updateSession: updating existing") + state.sessions[x] = session + + + updateSession: (sessionId) -> + if !@watchingState + return + + rest.getSessionHistory(sessionId) + .done((response) => + determined_type = null + if response.active_music_session + if response.can_join || context.FriendStore.isFriend(response.user_id) + determined_type = @TYPE_MY + else + determined_type = @TYPE_OPEN + else + determined_type = @TYPE_UPCOMING + + @updateSessionState(response, determined_type) + @issueChange(determined_type) + ) + .fail((jqXHR) => + @app.notifyServerError jqXHR, 'SessionsStore: Unable to fetch session information' + ) + + resetSessions:() -> + @my.sessions = [] + @my.currentPage = 0 + @my.end = false + @open.sessions = [] + @open.currentPage = 0 + @open.end = false + @upcoming.sessions = [] + @upcoming.currentPage = 0 + @upcoming.end = false + @issueChange(@TYPE_MY) + @issueChange(@TYPE_OPEN) + @issueChange(@TYPE_UPCOMING) + + clearSessions: (type, query) -> + this[type].sessions = [] + this[type].currentPage = 0 + this[type].end = false + @updateSessions(type, query) + + handleSessionResponse: (type, response) -> + state = this[type] + state.sessions = state.sessions.concat(response.sessions) + state.searching = false + state.currentPage = state.currentPage + 1 + state.first_search = false + state.error = null + state.end = response.sessions.length < @LIMITS[type] + @issueChange(type) + + handleSessionError: (type, response) -> + #@app.notifyServerError jqXHR, 'Search Unavailable' + state = this[type] + state.searching = false + state.error = jqXHR + @issueChange(type) + + updateSessions: (type, query) -> + + query = @defaultQuery(query, type) + + this[type].searching = true + @issueChange(type) + + if type == 'my' + rest.findFriendSessions(query) + .done((response) => + @handleSessionResponse(type, response) + ) + .fail((jqXHR) => + @handleSessionError(type, jqXHR) + ) + + else if type == 'open' + rest.findPublicSessions(query) + .done((response) => + @handleSessionResponse(type, response) + ) + .fail((jqXHR) => + @handleSessionError(type, jqXHR) + ) + else if type == 'upcoming' + rest.findInactiveSessions(query) + .done((response) => + @handleSessionResponse(type, response) + ) + .fail((jqXHR) => + @handleSessionError(type, jqXHR) + ) + + watching:(watching) -> + @watchingState = watching + + issueChange: (type) -> + sessions = this[type] + @trigger({type: type, sessions: sessions.sessions, highlight: @highlight, searching: sessions.searching, currentPage: sessions.currentPage, end: sessions.end}) + @highlight = null + + } +) \ No newline at end of file diff --git a/web/app/assets/javascripts/scheduled_session.js.erb b/web/app/assets/javascripts/scheduled_session.js.erb index 5e3ec99e8..0d4bc9093 100644 --- a/web/app/assets/javascripts/scheduled_session.js.erb +++ b/web/app/assets/javascripts/scheduled_session.js.erb @@ -24,7 +24,9 @@ band: {}, musician_access: {}, fans_access: {}, - open_rsvps: false + open_rsvps: false, + friends_can_join: false + }; var friendInput = null; @@ -49,6 +51,7 @@ var $selectedFilenames = null; var $uploadSpinner = null; var $quickStartSoloBtn = null; + var $quickStartFriendsBtn = null; var $quickStartOpenBtn = null; var $startOrScheduledBtn = null; var $featureSessions = null; @@ -245,6 +248,16 @@ logger.debug("user clicked quick start solo") createSessionSettings.createType = '<%= MusicSession::CREATE_TYPE_QUICK_START %>' + createSessionSettings.friends_can_join = false + next(); + return false; + } + + function clickQuickStartFriends () { + + logger.debug("user clicked quick start solo") + createSessionSettings.createType = '<%= MusicSession::CREATE_TYPE_QUICK_START %>' + createSessionSettings.friends_can_join = true next(); return false; } @@ -253,6 +266,7 @@ logger.debug("user clicked quick start public") createSessionSettings.createType = '<%= MusicSession::CREATE_TYPE_QUICK_PUBLIC %>' + createSessionSettings.friends_can_join = false next(); return false; } @@ -725,6 +739,10 @@ createSessionSettings.fans_access.value = $fansAccess.val(); createSessionSettings.fans_access.label = $fansAccess.get(0).options[$fansAccess.get(0).selectedIndex].text; + var $friendsCanJoin = $screen.find('#session-friends-can-join'); + createSessionSettings.friends_can_join = $friendsCanJoin.val() == 'true' + //createSessionSettings.friends_can_join.label = $friendsCanJoin.get(0).options[$friendsCanJoin.get(0).selectedIndex].text; + return isValid; } @@ -832,6 +850,7 @@ data.timezone = createSessionSettings.timezone.value; data.open_rsvps = createSessionSettings.open_rsvps; data.create_type = createSessionSettings.createType; + data.friends_can_join = createSessionSettings.friends_can_join; data.rsvp_slots = []; $.each(getCreatorInstruments(), function(index, instrument) { @@ -893,7 +912,7 @@ joinSession(newSessionId); } else { - app.notifyAlert("Session is successfully published."); + app.notifyAlert("Session published! It can be now found on the Find Session screen."); context.location = '/client#/home'; } }) @@ -1244,6 +1263,7 @@ context.JK.dropdown($screen.find('#session-musician-access')); context.JK.dropdown($screen.find('#session-fans-access')); + context.JK.dropdown($screen.find('#session-friends-can-join')); context.JK.dropdown($timezoneList); context.JK.dropdown($recurringModeList); context.JK.dropdown($languageList); @@ -1398,6 +1418,11 @@ $('#session-fans-access-info .info-box[fans-access-type="' + $(event.target).val() + '"]').removeClass('hidden'); } + function toggleFriendsCanJoinTypes(event) { + $('#session-friends-can-join-info .info-box').addClass('hidden'); + $('#session-friends-can-join-info .info-box[friends-can-join-type="' + $(event.target).val() + '"]').removeClass('hidden'); + } + function selectBand() { var bandId = $bandList.get(0).options[$bandList.get(0).selectedIndex].value; if (bandId != '') { @@ -1435,8 +1460,10 @@ $createTypes.on("ifChanged", toggleCreateType); $startTimeList.on('change', function() { toggleStartTime(); }); $policyTypes.on("ifChanged", togglePolicyTypeChanged); - $('#session-step-4 #session-musician-access').on('change', toggleMusicianAccessTypes); - $('#session-step-4 #session-fans-access').on('change', toggleFanAccessTypes); + $('#session-step-5 #session-musician-access').on('change', toggleMusicianAccessTypes); + $('#session-step-5 #session-fans-access').on('change', toggleFanAccessTypes); + $('#session-step-5 #session-friends-can-join').on('change', toggleFriendsCanJoinTypes); + $('div[layout-id="createSession"] .btn-email-invitation').click(function() { invitationDialog.showEmailDialog(); @@ -1457,6 +1484,7 @@ $btnSelectFiles.on('click', toggleSelectFiles); $quickStartSoloBtn.on('click', clickQuickStartSolo) + $quickStartFriendsBtn.on('click', clickQuickStartFriends) $quickStartOpenBtn.on('click', clickQuickStartPublic) $startOrScheduledBtn.on('click', clickStartOrSchedule) } @@ -1500,6 +1528,7 @@ $noSessionFound = $screen.find("#scheduled-session-not-found"); $sessionHeader = $screen.find('.session-header') $quickStartSoloBtn = $screen.find('.quick-start-solo') + $quickStartFriendsBtn = $screen.find('.quick-start-friends') $quickStartOpenBtn = $screen.find('.quick-start-open') $startOrScheduledBtn = $screen.find('.start-or-schedule') $featureSessions = $screen.find('.featured-sessions tbody') diff --git a/web/app/assets/javascripts/session_utils.js b/web/app/assets/javascripts/session_utils.js index 7e8de1acb..6daf85904 100644 --- a/web/app/assets/javascripts/session_utils.js +++ b/web/app/assets/javascripts/session_utils.js @@ -196,6 +196,10 @@ openJoinSessionTerms(sessionId); } } + else { + // if it's private, yet we can see it, presumably we are friends (better there was a front-end check here) + openJoinRequestAlert(sessionId); + } } } }) diff --git a/web/app/assets/javascripts/sidebar.js b/web/app/assets/javascripts/sidebar.js index 650d6efa8..8d5e5a2ac 100644 --- a/web/app/assets/javascripts/sidebar.js +++ b/web/app/assets/javascripts/sidebar.js @@ -73,6 +73,9 @@ return 0; }); + context.FriendActions.updateFriends.trigger(response) + + $.each(response, function(index, val) { var css = val.online ? '' : 'offline'; diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index cf800db15..ebccfcc2a 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -466,12 +466,16 @@ return css; } - context.JK.bindHoverEvents = function ($parent) { + context.JK.bindHoverEvents = function ($parent, hoveractionAttr) { var timeout = 300; var fadeoutValue = 100; var sensitivity = 3; var interval = 500; + if(!hoveractionAttr) { + hoveractionAttr = 'hoveraction' + } + if (!$parent) { $parent = $('body'); } @@ -505,7 +509,7 @@ } // MUSICIAN - $("[hoveraction='musician']", $parent).hoverIntent({ + $("[" + hoveractionAttr + "='musician']", $parent).hoverIntent({ over: function(e) { var bubble = new JK.MusicianHoverBubble($(this).attr('user-id'), e.pageX, e.pageY); @@ -522,7 +526,7 @@ }); // FAN - $("[hoveraction='fan']", $parent).hoverIntent({ + $("[" + hoveractionAttr + "='fan']", $parent).hoverIntent({ over: function(e) { var bubble = new JK.FanHoverBubble($(this).attr('user-id'), e.pageX, e.pageY); showBubble(bubble, $(this)); @@ -536,7 +540,7 @@ }); // BAND - $("[hoveraction='band']", $parent).hoverIntent({ + $("[" + hoveractionAttr +"='band']", $parent).hoverIntent({ over: function(e) { var bubble = new JK.BandHoverBubble($(this).attr('band-id'), e.pageX, e.pageY); showBubble(bubble, $(this)); @@ -550,7 +554,7 @@ }); // SESSION - $("[hoveraction='session']", $parent).hoverIntent({ + $("[" + hoveractionAttr + "='session']", $parent).hoverIntent({ over: function(e) { var bubble = new JK.SessionHoverBubble($(this).attr('session-id'), e.pageX, e.pageY); showBubble(bubble, $(this)); @@ -564,7 +568,7 @@ }); // RECORDING - $("[hoveraction='recording']", $parent).hoverIntent({ + $("[" + hoveractionAttr + "='recording']", $parent).hoverIntent({ over: function(e) { var bubble = new JK.RecordingHoverBubble($(this).attr('recording-id'), e.pageX, e.pageY); showBubble(bubble, $(this)); diff --git a/web/app/assets/stylesheets/client/createSession.scss b/web/app/assets/stylesheets/client/createSession.scss index 611816d03..9e1a33cfd 100644 --- a/web/app/assets/stylesheets/client/createSession.scss +++ b/web/app/assets/stylesheets/client/createSession.scss @@ -62,7 +62,7 @@ .quick-options { margin: 0 0 35px 0; a { - width:130px; + width:135px; margin-left:0px; position:absolute; top:5px; @@ -385,7 +385,7 @@ } } - .session-musician-access-header, .session-fans-access-header { + .session-musician-access-header, .session-fans-access-header, .session-friends-can-join-header { margin: 0 0 5px 0; } diff --git a/web/app/assets/stylesheets/client/findSession.scss b/web/app/assets/stylesheets/client/findSession.scss index e10187713..7a2698741 100644 --- a/web/app/assets/stylesheets/client/findSession.scss +++ b/web/app/assets/stylesheets/client/findSession.scss @@ -3,12 +3,92 @@ #findSession { + .FindSessionScreen { + height:100%; + } + + [data-react-class="FindSessionScreen"] { + height:100%; + } + .session-keyword-srch { } + .musicians-category { + + } + + .musicians-detail > td { + padding-bottom:20px; + } + .musicians-header { + span { + display:block; + height:100%; + margin:10px; + + } + } + .find-session-help { + background-color: #262626; + max-width: 66%; + border: 2px solid #333; + color: white; + font-size: 14px; + text-align: left; + /* margin-top: 20px; */ + margin: 30px auto; + padding: 10px; + width: 400px; + + a { + } + li { + margin-bottom: 7px; + } + h3 { + text-align:center; + font-weight:bold; + margin: 0 0 10px 0; + text-decoration: underline; + } + + p { + line-height:125%; + margin: 0 0 10px 0; + font-size:14px; + + } + p + h3 { + margin-top:20px; + } + } + + .none-found { + text-align:center; + padding:10px; + } + .tabs { - padding-left: 25px; + padding-left: 35px; + } + + .highlight { + writing-mode: vertical-rl; + text-orientation: upright; + position: absolute; + height: calc(100% - 10px); // 10px comes from spacer height + width: 20px; + left: -21px; + background-color: #ed3618; + top: 0; + border: 0px solid black; + font-size: 11px; + text-align: center; + padding: 0 5px; + margin: 0; + @include border_box_sizing; } .header-zone { @@ -65,7 +145,16 @@ padding-bottom:20px; } - .sessions-for-me { + .actions { + vertical-align:middle; + } + + .end-of-results { + position:relative; + padding-bottom:20px; + text-align:center; + } + .sessions-for-me, .sessions-for-open { clear:both; display:none; &.active { @@ -73,6 +162,12 @@ } } + .rsvp-icon { + display: inline-block; + background: url('/assets/content/rsvp-icon.jpg') no-repeat; + height: 40px; + width: 40px; + } .join-icon { display: inline-block; background: url('/assets/content/join-icon.jpg') no-repeat; @@ -123,8 +218,8 @@ .content-body-scroller { height:inherit; position:relative; - display:block; - overflow:auto + height:calc(100% - 123px) ! important; + overflow:auto; } .session-filter { @@ -173,6 +268,10 @@ margin-top:0; } + .none-yet { + margin-left:36px; + } + .latency-value { @include border-radius(2px); } @@ -190,6 +289,10 @@ } } + a.start, a.cancel { + color:#fc0; + } + .listen-detail-hover { margin-left:5px; margin-top:0 !important; @@ -203,17 +306,11 @@ } .join-link-text { font-weight:bold; - #position:relative; - #left:6px; } .rsvp-link { - #position:relative; - #left:0px; } .rsvp-link-text { font-weight:bold; - #position:relative; - #left:6px; } } \ No newline at end of file diff --git a/web/app/assets/stylesheets/dialogs/sessionSettingsDialog.scss b/web/app/assets/stylesheets/dialogs/sessionSettingsDialog.scss index 13395483d..889b19a00 100644 --- a/web/app/assets/stylesheets/dialogs/sessionSettingsDialog.scss +++ b/web/app/assets/stylesheets/dialogs/sessionSettingsDialog.scss @@ -2,7 +2,7 @@ #session-settings { - width:500px; + width:550px; .dropdown-wrapper { width:100%; @@ -38,7 +38,7 @@ } .input-holder { - width:350px; + width:400px; } .notation-entry { diff --git a/web/app/controllers/api_music_sessions_controller.rb b/web/app/controllers/api_music_sessions_controller.rb index da6440c04..95999c8c6 100644 --- a/web/app/controllers/api_music_sessions_controller.rb +++ b/web/app/controllers/api_music_sessions_controller.rb @@ -93,6 +93,11 @@ class ApiMusicSessionsController < ApiController end end + def sms_index_2 + @music_sessions = MusicSession.scheduled_index(current_user, params) + @user_scores = {} + end + def scheduled @music_sessions = MusicSession.scheduled(current_user) end @@ -192,7 +197,8 @@ class ApiMusicSessionsController < ApiController params[:approval_required], params[:fan_chat], params[:fan_access], - params[:session_controller]) + params[:session_controller], + params[:friends_can_join]) if @music_session.errors.any? # we have to do this because api_session_detail_url will fail with a bad @music_session @@ -234,6 +240,7 @@ class ApiMusicSessionsController < ApiController @music_session.approval_required = params[:approval_required] if params.include? :approval_required @music_session.fan_chat = params[:fan_chat] if params.include? :fan_chat @music_session.fan_access = params[:fan_access] if params.include? :fan_access + @music_session.friends_can_join = params[:friends_can_join] if params.include? :friends_can_join @music_session.genre = Genre.find_by_id(params[:genres][0]) if params.include?(:genres) && params[:genres] && params[:genres].length > 0 @music_session.legal_policy = params[:legal_policy] if params.include? :legal_policy @music_session.language = params[:language] if params.include? :language diff --git a/web/app/views/api_music_sessions/show.rabl b/web/app/views/api_music_sessions/show.rabl index 2b931e40e..f6868fc84 100644 --- a/web/app/views/api_music_sessions/show.rabl +++ b/web/app/views/api_music_sessions/show.rabl @@ -13,7 +13,7 @@ if !current_user } else - attributes :id, :name, :description, :musician_access, :approval_required, :fan_access, :fan_chat, :band_id, :user_id, :claimed_recording_initiator_id, :track_changes_counter, :max_score, :backing_track_path, :metronome_active, :jam_track_initiator_id, :jam_track_id + attributes :id, :name, :description, :musician_access, :approval_required, :friends_can_join, :fan_access, :fan_chat, :band_id, :user_id, :claimed_recording_initiator_id, :track_changes_counter, :max_score, :backing_track_path, :metronome_active, :jam_track_initiator_id, :jam_track_id node :can_join do |session| session.can_join?(current_user, true) diff --git a/web/app/views/api_music_sessions/show_history.rabl b/web/app/views/api_music_sessions/show_history.rabl index b17469bd6..2b5ce02cb 100644 --- a/web/app/views/api_music_sessions/show_history.rabl +++ b/web/app/views/api_music_sessions/show_history.rabl @@ -19,7 +19,7 @@ else attributes :id, :music_session_id, :name, :description, :musician_access, :approval_required, :fan_access, :fan_chat, :create_type, :band_id, :user_id, :genre_id, :created_at, :like_count, :comment_count, :play_count, :scheduled_duration, :language, :recurring_mode, :language_description, :scheduled_start_date, :access_description, :timezone, :timezone_id, :timezone_description, - :musician_access_description, :fan_access_description, :session_removed_at, :legal_policy, :open_rsvps, :is_unstructured_rsvp? + :musician_access_description, :fan_access_description, :session_removed_at, :legal_policy, :open_rsvps, :is_unstructured_rsvp?, :friends_can_join node :can_join do |session| session.can_join?(current_user, true) diff --git a/web/app/views/api_music_sessions/sms_index_2.rabl b/web/app/views/api_music_sessions/sms_index_2.rabl new file mode 100644 index 000000000..7c3566a6d --- /dev/null +++ b/web/app/views/api_music_sessions/sms_index_2.rabl @@ -0,0 +1,4 @@ + +child @music_sessions => :sessions do + extends "api_music_sessions/show_history" +end \ No newline at end of file diff --git a/web/app/views/clients/_findSession2.html.erb b/web/app/views/clients/_findSession2.html.erb index eab6bb8b1..65f3dbcd0 100644 --- a/web/app/views/clients/_findSession2.html.erb +++ b/web/app/views/clients/_findSession2.html.erb @@ -1,60 +1,3 @@ - -
-
-
-
- <%= image_tag "content/icon_search.png", :size => "19x19" %> -
- -

find a session

- - <%= render "screen_navigation" %> -
-
-
-
-
-
-
Filter Session List:
- - - - - -
- REFRESH -
-
-
-
-
-
- <%= render :partial => "sessionList", :locals => {:title => "current, active sessions", :category => "sessions-active"} %> -
-
Fetching results...
-
- End of list. -
-
-
-
- <%= render :partial => "sessionList", :locals => {:title => "future, scheduled sessions", :category => "sessions-scheduled"} %> -
-
Fetching results...
-
- End of list. -
- Next -
-
-
-
-
-
-
-