diff --git a/db/manifest b/db/manifest index 6f303be21..1250059b1 100755 --- a/db/manifest +++ b/db/manifest @@ -326,4 +326,5 @@ profile_teacher.sql populate_languages.sql populate_subjects.sql reviews.sql -download_tracker_fingerprints.sql \ No newline at end of file +download_tracker_fingerprints.sql +connection_active.sql \ No newline at end of file diff --git a/db/up/connection_active.sql b/db/up/connection_active.sql new file mode 100644 index 000000000..085021aaf --- /dev/null +++ b/db/up/connection_active.sql @@ -0,0 +1 @@ +ALTER TABLE connections ADD COLUMN user_active BOOLEAN DEFAULT TRUE; \ No newline at end of file diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto index aa9fe6a12..250bede8e 100644 --- a/pb/src/client_container.proto +++ b/pb/src/client_container.proto @@ -21,6 +21,7 @@ message ClientMessage { UNSUBSCRIBE = 137; SUBSCRIPTION_MESSAGE = 138; SUBSCRIBE_BULK = 139; + USER_STATUS = 141; // friend notifications FRIEND_UPDATE = 140; @@ -67,6 +68,7 @@ message ClientMessage { // text message TEXT_MESSAGE = 236; CHAT_MESSAGE = 237; + SEND_CHAT_MESSAGE = 238; MUSICIAN_SESSION_FRESH = 240; MUSICIAN_SESSION_STALE = 245; @@ -131,6 +133,7 @@ message ClientMessage { optional Unsubscribe unsubscribe = 137; optional SubscriptionMessage subscription_message = 138; optional SubscribeBulk subscribe_bulk = 139; + optional UserStatus user_status = 141; // friend notifications optional FriendUpdate friend_update = 140; // from server to all friends of user @@ -178,6 +181,7 @@ message ClientMessage { // text message optional TextMessage text_message = 236; optional ChatMessage chat_message = 237; + optional SendChatMessage send_chat_message = 238; optional MusicianSessionFresh musician_session_fresh = 240; optional MusicianSessionStale musician_session_stale = 245; @@ -569,6 +573,12 @@ message ChatMessage { optional string msg = 3; optional string msg_id = 4; optional string created_at = 5; + optional string channel = 6; +} + +message SendChatMessage { + optional string msg = 1; + optional string channel = 2; } // route_to: client: @@ -657,6 +667,11 @@ message SubscribeBulk { //repeated Subscription subscriptions = 1; # the ruby protocol buffer library chokes on this. so we have to do the above } +message UserStatus { + optional bool active = 1; // same as heartbeat 'active'... does the user appear present + optional string status = 2; +} + // route_to: session // a test message used by ruby-client currently. just gives way to send out to rest of session message TestSessionMessage { @@ -693,6 +708,7 @@ message TestClientMessage { message Heartbeat { optional string notification_seen = 1; optional string notification_seen_at = 2; + optional bool active = 3; // is the user active? } // target: client diff --git a/ruby/lib/jam_ruby/jam_track_importer.rb b/ruby/lib/jam_ruby/jam_track_importer.rb index c6a6549b3..87637ddc8 100644 --- a/ruby/lib/jam_ruby/jam_track_importer.rb +++ b/ruby/lib/jam_ruby/jam_track_importer.rb @@ -1650,11 +1650,11 @@ module JamRuby output = cmd("soxi -r \"#{track.wav_file}\"", "get_sample_rate") sample_rate = output.to_i - create_silence(tmp_dir, "padded_silence#{track.id}", amount, sample_rate, channels) + padding_file = create_silence(tmp_dir, "padded_silence#{track.id}", amount, sample_rate, channels) output_file = File.join(tmp_dir, "with_padding_#{track.id}.wav") - cmd("sox \"#{track.wav_file}\" \"#{output_file}\"", "same_lengthening") + cmd("sox \"#{track.wav_file}\" \"#{padding_file}\" \"#{output_file}\"", "same_lengthening") track.wav_file = output_file end diff --git a/ruby/lib/jam_ruby/message_factory.rb b/ruby/lib/jam_ruby/message_factory.rb index 94e86af4f..dff7e726b 100644 --- a/ruby/lib/jam_ruby/message_factory.rb +++ b/ruby/lib/jam_ruby/message_factory.rb @@ -897,19 +897,26 @@ module JamRuby ) end - # creates the session chat message - def chat_message(session_id, sender_name, sender_id, msg, msg_id, created_at) + # creates the chat message + def chat_message(session_id, sender_name, sender_id, msg, msg_id, created_at, channel) chat_message = Jampb::ChatMessage.new( :sender_id => sender_id, :sender_name => sender_name, :msg => msg, :msg_id => msg_id, - :created_at => created_at + :created_at => created_at, + :channel => channel ) + if session_id + route_to = SESSION_TARGET_PREFIX + session_id + else + route_to = ALL_ACTIVE_CLIENTS + end + Jampb::ClientMessage.new( :type => ClientMessage::Type::CHAT_MESSAGE, - :route_to => SESSION_TARGET_PREFIX + session_id, + :route_to => route_to, :chat_message => chat_message ) end diff --git a/ruby/lib/jam_ruby/models/chat_message.rb b/ruby/lib/jam_ruby/models/chat_message.rb index 52046d7b3..005187415 100644 --- a/ruby/lib/jam_ruby/models/chat_message.rb +++ b/ruby/lib/jam_ruby/models/chat_message.rb @@ -50,7 +50,8 @@ module JamRuby user.id, chat_msg.message, chat_msg.id, - chat_msg.created_at.utc.iso8601 + chat_msg.created_at.utc.iso8601, + 'session' ) @@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => client_id}) diff --git a/web/app/assets/javascripts/AAB_message_factory.js b/web/app/assets/javascripts/AAB_message_factory.js index 2a502f43e..cc75ebbb4 100644 --- a/web/app/assets/javascripts/AAB_message_factory.js +++ b/web/app/assets/javascripts/AAB_message_factory.js @@ -20,6 +20,7 @@ UNSUBSCRIBE : "UNSUBSCRIBE", SUBSCRIPTION_MESSAGE : "SUBSCRIPTION_MESSAGE", SUBSCRIBE_BULK : "SUBSCRIBE_BULK", + USER_STATUS : "USER_STATUS", // friend notifications FRIEND_UPDATE : "FRIEND_UPDATE", @@ -66,6 +67,7 @@ // text message TEXT_MESSAGE : "TEXT_MESSAGE", CHAT_MESSAGE : "CHAT_MESSAGE", + SEND_CHAT_MESSAGE : "SEND_CHAT_MESSAGE", // broadcast notifications SOURCE_UP_REQUESTED : "SOURCE_UP_REQUESTED", @@ -120,13 +122,30 @@ }; // Heartbeat message - factory.heartbeat = function(lastNotificationSeen, lastNotificationSeenAt) { + factory.heartbeat = function(lastNotificationSeen, lastNotificationSeenAt, active) { var data = {}; data.notification_seen = lastNotificationSeen; data.notification_seen_at = lastNotificationSeenAt; + data.active = active; return client_container(msg.HEARTBEAT, route_to.SERVER, data); }; + // User Status update message + factory.userStatus = function(active, status) { + var data = {}; + data.active = active; + data.status = status; + return client_container(msg.USER_STATUS, route_to.SERVER, data); + }; + + factory.chatMessage = function(channel, msg) { + var data = {} + data.channel = {} + data.msg = {} + + return client_container(msg.SEND_CHAT_MESSAGE, route_to.SERVER, data) + } + // create a login message using user/pass factory.login_with_user_pass = function(username, password) { var login = { username : username , password : password }; diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js index b893ef7c7..7b523dbb5 100644 --- a/web/app/assets/javascripts/JamServer.js +++ b/web/app/assets/javascripts/JamServer.js @@ -36,6 +36,7 @@ var notificationLastSeen = undefined; var clientClosedConnection = false; var initialConnectAttempt = true; + var active = true; // reconnection logic var connectDeferred = null; @@ -46,6 +47,7 @@ var reconnectingWaitPeriodStart = null; var reconnectDueTime = null; var connectTimeout = null; + var activityTimeout; // elements var $inSituBanner = null; @@ -80,7 +82,6 @@ function initiateReconnect(activeElementVotes, in_error) { var initialConnect = !!activeElementVotes; - console.log("activeElementVotes", activeElementVotes) freezeInteraction = activeElementVotes && ((activeElementVotes.dialog && activeElementVotes.dialog.freezeInteraction === true) || (activeElementVotes.screen && activeElementVotes.screen.freezeInteraction === true)); if (in_error) { @@ -174,7 +175,8 @@ function _heartbeat() { if (app.heartbeatActive) { - var message = context.JK.MessageFactory.heartbeat(notificationLastSeen, notificationLastSeenAt); + console.log("heartbeat active?: " + active) + var message = context.JK.MessageFactory.heartbeat(notificationLastSeen, notificationLastSeenAt, active); notificationLastSeenAt = undefined; notificationLastSeen = undefined; // for debugging purposes, see if the last time we've sent a heartbeat is way off (500ms) of the target interval @@ -290,6 +292,35 @@ }, 0) } + function markAway() { + logger.debug("sleep again!") + active = false; + } + + function activityCheck() { + var timeoutTime = 300000; // 5 * 1000 * 60 , 5 minutes + active = true; + activityTimeout = setTimeout(markAway, timeoutTime); + $(document).ready(function() { + $('body').bind('mousedown keydown touchstart focus', function(event) { + if (activityTimeout) { + clearTimeout(activityTimeout); + activityTimeout = null; + } + + if (!active) { + if(server && server.connected) { + logger.debug("awake again!") + var userStatus = msg_factory.userStatus(true, null); + server.send(userStatus); + } + } + active = true; + activityTimeout = setTimeout(markAway, timeoutTime); + }); + }); + } + function heartbeatAck(header, payload) { lastHeartbeatAckTime = new Date(); } @@ -771,6 +802,19 @@ //console.timeEnd('sendP2PMessage'); }; + server.sendChatMessage = function(message) { + + if (server.connected) { + + var chatMsg = msg_factory.chatMessage(channel, message) + server.send(chatMsg) + return true; + } + else { + return false; + } + } + server.updateNotificationSeen = function (notificationId, notificationCreatedAt) { var time = new Date(notificationCreatedAt); @@ -816,6 +860,7 @@ registerHeartbeatAck(); registerServerRejection(); registerSocketClosed(); + activityCheck(); $inSituBanner = $('.server-connection'); $inSituBannerHolder = $('.no-websocket-connection'); diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js index b99487546..019ddfcda 100644 --- a/web/app/assets/javascripts/application.js +++ b/web/app/assets/javascripts/application.js @@ -40,6 +40,7 @@ //= require jquery.visible //= require jquery.jstarbox //= require fingerprint2.min +//= require ResizeSensor //= require classnames //= require reflux //= require howler.core.js diff --git a/web/app/assets/javascripts/dialog/recordingSelectorDialog.js b/web/app/assets/javascripts/dialog/recordingSelectorDialog.js index 10e76595b..d34a338f5 100644 --- a/web/app/assets/javascripts/dialog/recordingSelectorDialog.js +++ b/web/app/assets/javascripts/dialog/recordingSelectorDialog.js @@ -15,9 +15,10 @@ var feedHelper = new context.JK.Feed(app); var $scroller = $recordings; var $content = $recordings; + var $recordsHolder = $screen.find('.recordings-content'); var $noMoreFeeds = $screen.find('.end-of-list'); var $empty = $(); - feedHelper.initialize($screen, $scroller, $content, $noMoreFeeds, $empty, $empty, $empty, $empty, {sort: 'date', time_range: 'all', type: 'recording', show_checkbox: true, hide_avatar: true}); + feedHelper.initialize($screen, $scroller, $recordsHolder, $noMoreFeeds, $empty, $empty, $empty, $empty, {sort: 'date', time_range: 'all', type: 'recording', show_checkbox: true, hide_avatar: true}); function beforeShow(data) { diff --git a/web/app/assets/javascripts/everywhere/everywhere.js b/web/app/assets/javascripts/everywhere/everywhere.js index 4adfb59d3..d40ad9186 100644 --- a/web/app/assets/javascripts/everywhere/everywhere.js +++ b/web/app/assets/javascripts/everywhere/everywhere.js @@ -41,6 +41,7 @@ trackNewUser(); trackScreenChanges(); + }) $(document).on('JAMKAZAM_READY', function() { diff --git a/web/app/assets/javascripts/feedHelper.js b/web/app/assets/javascripts/feedHelper.js index d7ce6765f..ce7196160 100644 --- a/web/app/assets/javascripts/feedHelper.js +++ b/web/app/assets/javascripts/feedHelper.js @@ -13,7 +13,7 @@ var userId = null; var bandId = null; var currentFeedPage = 0; - var feedBatchSize = 10; + var feedBatchSize = 4; var $screen = null; var $scroller = null; var $content = null; @@ -454,7 +454,6 @@ mix_class: feed['has_mix?'] ? 'has-mix' : 'no-mix', } - console.log("OPTIONS", options) var $feedItem = $(context._.template($('#template-feed-recording').html(), options, {variable: 'data'})); var $controls = $feedItem.find('.recording-controls'); diff --git a/web/app/assets/javascripts/layout.js b/web/app/assets/javascripts/layout.js index fd39f6b9f..3ea51c8be 100644 --- a/web/app/assets/javascripts/layout.js +++ b/web/app/assets/javascripts/layout.js @@ -739,6 +739,13 @@ stackDialogs($dialog, $overlay); addScreenContextToDialog($dialog) $dialog.show(); + + // maintain center (un-attach previous sensor if applicable, then re-add always) + window.ResizeSensor.detach($dialog.get(0)) + new window.ResizeSensor($dialog, function(){ + centerDialog(dialog); + }); + dialogEvent(dialog, 'afterShow', options); $.btOffAll(); // add any prod bubbles if you open a dailog return $dialog; diff --git a/web/app/assets/javascripts/react-components.js b/web/app/assets/javascripts/react-components.js index d02622469..9e82ac707 100644 --- a/web/app/assets/javascripts/react-components.js +++ b/web/app/assets/javascripts/react-components.js @@ -9,6 +9,7 @@ //= require ./react-components/stores/SubjectStore //= require ./react-components/stores/ProfileStore //= require ./react-components/stores/PlatformStore +//= require ./react-components/stores/ChatStore //= require ./react-components/stores/BrowserMediaStore //= require ./react-components/stores/RecordingStore //= require ./react-components/stores/VideoStore diff --git a/web/app/assets/javascripts/react-components/ChatWindow.js.jsx.coffee b/web/app/assets/javascripts/react-components/ChatWindow.js.jsx.coffee new file mode 100644 index 000000000..415eaae52 --- /dev/null +++ b/web/app/assets/javascripts/react-components/ChatWindow.js.jsx.coffee @@ -0,0 +1,16 @@ +context = window +MIX_MODES = context.JK.MIX_MODES + + +@ChatWindow = React.createClass({ + mixins: [Reflux.listenTo(@AppStore, "onAppInit"), Reflux.listenTo(@UserStore, "onUserChanged"), Reflux.listenTo(@ChatStore, "onChatChanged")] + + getInitialState: () -> + {channels:['global', 'session']} + + render: () -> + `
+
+
+
` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/EditableList.js.jsx.coffee b/web/app/assets/javascripts/react-components/EditableList.js.jsx.coffee index 6b9b00b71..4844fc15b 100644 --- a/web/app/assets/javascripts/react-components/EditableList.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/EditableList.js.jsx.coffee @@ -21,7 +21,6 @@ logger = context.JK.logger render: () -> object_options = [] - logger.debug("Rendering EditableList", this.props, this.props.listItems) if this.props.listItems? && this.props.listItems.length > 0 for object,i in this.props.listItems diff --git a/web/app/assets/javascripts/react-components/TeacherExperienceEditableList.js.jsx.coffee b/web/app/assets/javascripts/react-components/TeacherExperienceEditableList.js.jsx.coffee index 3ac17dd47..ebebdd62f 100644 --- a/web/app/assets/javascripts/react-components/TeacherExperienceEditableList.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TeacherExperienceEditableList.js.jsx.coffee @@ -11,8 +11,14 @@ logger = context.JK.logger @root.off("submit", ".teacher-experience-teaching-form").on("submit", ".teacher-experience-teaching-form", @addExperience) formatListItem: (obj) -> + + if obj.end_year? + endYear = obj.end_year + else + endYear = 'Present' + t = "#{obj.name}/#{obj.organization} (#{obj.start_year}" - t += "-#{obj.end_year}" if this.props.showEndDate + t += "-#{endYear}" if this.props.showEndDate t += ")" getInitialProps: () -> @@ -24,7 +30,6 @@ logger = context.JK.logger addExperience: (e) -> e.preventDefault() - logger.debug("addExperience", this.props.listItems, this.props) $form = e.target start_year = $("[name='start_year']", $form).val() diff --git a/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee b/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee index c94a7c7ef..90de1c5d5 100644 --- a/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee @@ -152,21 +152,23 @@ proficiencyDescriptionMap = { ` + getYoutubeId:(url) -> + regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/ + match = url.match(regExp) + + if match && match[2].length >= 9 + return match[2] + else + return 'unknown' + + sampleVideo: (user, teacher) -> - if teacher.introductory_video? + if teacher.introductory_video? && teacher.introductory_video.length > 0 videoUrl = teacher.introductory_video - if videoUrl.indexOf(window.location.protocol) != 0 - console.log("replacing video") - if window.location.protocol == 'http:' - console.log("replacing https: " + videoUrl) - videoUrl = videoUrl.replace('https://', 'http://') - console.log("replaced : " + videoUrl) - else - videoUrl = videoUrl.replace('http://', 'https://') - - videoUrl = videoUrl.replace("watch?v=", "v/") - + # http://stackoverflow.com/a/21607897 + embedId = @getYoutubeId(videoUrl) + embedUrl = "//www.youtube.com/embed/#{embedId}" return `

Intro Video

@@ -174,7 +176,7 @@ proficiencyDescriptionMap = {
-