From cc3576f70f47d3e6b06ac78664f48c1ec9aec2d1 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 26 May 2016 16:25:51 -0500 Subject: [PATCH] attach notations, audio, and recordings done --- db/manifest | 3 +- db/up/audio_in_music_notations.sql | 5 + pb/src/client_container.proto | 4 + ruby/lib/jam_ruby/app/mailers/user_mailer.rb | 2 +- .../app/uploaders/music_notation_uploader.rb | 2 +- ...udent_test_drive_lesson_completed.html.erb | 2 +- ...udent_test_drive_lesson_completed.text.erb | 2 +- ruby/lib/jam_ruby/message_factory.rb | 8 +- ruby/lib/jam_ruby/models/chat_message.rb | 34 +++++- ruby/lib/jam_ruby/models/music_notation.rb | 8 +- ruby/lib/jam_ruby/models/recording.rb | 10 +- ruby/lib/jam_ruby/models/teacher.rb | 2 +- web/app/assets/javascripts/chatPanel.js | 7 +- web/app/assets/javascripts/jam_rest.js | 13 +++ .../jquery.lessonSessionActions.js | 21 +++- .../AttachmentStatus.js.jsx.coffee | 22 +++- .../react-components/ChatDialog.js.jsx.coffee | 1 + .../react-components/ChatWindow.js.jsx.coffee | 110 ++++++++++++++---- .../LessonBooking.js.jsx.coffee | 14 +++ .../LessonBookingDecision.js.jsx.coffee | 4 +- .../TeacherProfile.js.jsx.coffee | 1 + .../TeacherSearchScreen.js.jsx.coffee | 2 + .../stores/AttachmentStore.js.coffee | 42 ++++--- .../stores/ChatStore.js.coffee | 38 ++++-- .../stores/SessionStore.js.coffee | 3 +- .../javascripts/scheduled_session.js.erb | 1 + web/app/assets/javascripts/sessionModel.js | 2 +- web/app/assets/javascripts/utils.js | 17 +++ .../client/lessonSessionActions.css.scss | 6 + .../react-components/ChatWindow.css.scss | 65 ++++++++--- .../api_lesson_bookings_controller.rb | 2 +- .../api_lesson_sessions_controller.rb | 26 +++++ .../api_music_notations_controller.rb | 40 +++++-- web/app/views/api_chats/show.rabl | 10 +- .../api_music_sessions/show_history.rabl | 4 + .../clients/_lessonSessionActions.html.slim | 13 ++- web/config/routes.rb | 1 + 37 files changed, 451 insertions(+), 96 deletions(-) create mode 100644 db/up/audio_in_music_notations.sql diff --git a/db/manifest b/db/manifest index 3afb06265..38aa9b087 100755 --- a/db/manifest +++ b/db/manifest @@ -353,4 +353,5 @@ phantom_accounts.sql lesson_booking_success.sql user_origin.sql remove_stripe_acct_id.sql -track_user_on_lesson.sql \ No newline at end of file +track_user_on_lesson.sql +audio_in_music_notations.sql \ No newline at end of file diff --git a/db/up/audio_in_music_notations.sql b/db/up/audio_in_music_notations.sql new file mode 100644 index 000000000..5774b3c44 --- /dev/null +++ b/db/up/audio_in_music_notations.sql @@ -0,0 +1,5 @@ +ALTER TABLE music_notations ADD COLUMN attachment_type VARCHAR NOT NULL DEFAULT 'notation'; +ALTER TABLE chat_messages ADD PRIMARY KEY (id); +ALTER TABLE music_notations ADD PRIMARY KEY (id); +ALTER TABLE chat_messages ADD COLUMN music_notation_id VARCHAR(64) REFERENCES music_notations(id); +ALTER TABLE chat_messages ADD COLUMN claimed_recording_id VARCHAR(64) REFERENCES claimed_recordings(id); \ No newline at end of file diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto index 207d61e5d..1db3e557a 100644 --- a/pb/src/client_container.proto +++ b/pb/src/client_container.proto @@ -621,6 +621,10 @@ message ChatMessage { optional string channel = 6; optional string lesson_session_id = 7; optional string purpose = 8; + optional string attachment_id = 9; + optional string attachment_type = 10; + optional string attachment_name = 11; + } message SendChatMessage { diff --git a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb index 4ff5f331c..25860e100 100644 --- a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb @@ -934,7 +934,7 @@ module JamRuby @lesson_session = lesson_session email = @student.email - subject = "You have used #{@student.remaining_test_drives} of #{@student.total_test_drives} TestDrive lesson credits" + subject = "You have used #{@student.used_test_drives} of #{@student.total_test_drives} TestDrive lesson credits" unique_args = {:type => "student_test_drive_success"} sendgrid_category "Notification" diff --git a/ruby/lib/jam_ruby/app/uploaders/music_notation_uploader.rb b/ruby/lib/jam_ruby/app/uploaders/music_notation_uploader.rb index 3f2f49e4f..d098e76cc 100644 --- a/ruby/lib/jam_ruby/app/uploaders/music_notation_uploader.rb +++ b/ruby/lib/jam_ruby/app/uploaders/music_notation_uploader.rb @@ -20,6 +20,6 @@ class MusicNotationUploader < CarrierWave::Uploader::Base end def extension_white_list - %w(pdf png jpg jpeg gif xml mxl txt) + %w(pdf png jpg jpeg gif xml mxl txt wav flac ogg aiff aifc au) end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.html.erb index 40ff80156..56e8750e5 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.html.erb @@ -1,4 +1,4 @@ -<% provide(:title, "You have used #{@student.remaining_test_drives} of #{@student.total_test_drives} TestDrive lesson credits") %> +<% provide(:title, "You have used #{@student.used_test_drives} of #{@student.total_test_drives} TestDrive lesson credits") %> <% provide(:photo_url, @teacher.resolved_photo_url) %> <% content_for :note do %> diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.text.erb index 3e3b6ea4f..34db7e7bc 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.text.erb @@ -1,4 +1,4 @@ -You have used <%= @student.remaining_test_drives %> of <%= @student.total_test_drives %> TestDrive lesson credits. +You have used <%= @student.used_test_drives %> of <%= @student.total_test_drives %> TestDrive lesson credits. <% if @student.has_rated_teacher(@teacher) %> Also, please rate your teacher at <%= @teacher.ratings_url %> now for today’s lesson to help other students in the community find the best instructors. diff --git a/ruby/lib/jam_ruby/message_factory.rb b/ruby/lib/jam_ruby/message_factory.rb index 53e63e9e3..87cd8525b 100644 --- a/ruby/lib/jam_ruby/message_factory.rb +++ b/ruby/lib/jam_ruby/message_factory.rb @@ -993,7 +993,8 @@ module JamRuby end # creates the chat message - def chat_message(session_id, sender_name, sender_id, msg, msg_id, created_at, channel, lesson_session_id, purpose) + def chat_message(session_id, sender_name, sender_id, msg, msg_id, created_at, channel, lesson_session_id, purpose, + attachment_id, attachment_type, attachment_name) chat_message = Jampb::ChatMessage.new( :sender_id => sender_id, :sender_name => sender_name, @@ -1002,7 +1003,10 @@ module JamRuby :created_at => created_at, :channel => channel, :lesson_session_id => lesson_session_id, - :purpose => purpose + :purpose => purpose, + :attachment_id => attachment_id, + :attachment_type => attachment_type, + :attachment_name => attachment_name ) if session_id diff --git a/ruby/lib/jam_ruby/models/chat_message.rb b/ruby/lib/jam_ruby/models/chat_message.rb index a530c9d66..60ef217c7 100644 --- a/ruby/lib/jam_ruby/models/chat_message.rb +++ b/ruby/lib/jam_ruby/models/chat_message.rb @@ -17,11 +17,13 @@ module JamRuby belongs_to :music_session belongs_to :target_user, class_name: "JamRuby::User" belongs_to :lesson_session, class_name: "JamRuby::LessonSession" + belongs_to :music_notation, class_name: "JamRuby::MusicNotation" + belongs_to :claimed_recording, class_name: "JamRuby::ClaimedRecording" validates :user, presence: true validates :message, length: {minimum: 1, maximum: 255}, no_profanity: true, unless: :ignore_message_checks - def self.create(user, music_session, message, channel, client_id, target_user = nil, lesson_session = nil, purpose = nil) + def self.create(user, music_session, message, channel, client_id, target_user = nil, lesson_session = nil, purpose = nil, music_notation = nil, recording = nil) chat_msg = ChatMessage.new chat_msg.user_id = user.id chat_msg.music_session_id = music_session.id if music_session @@ -30,6 +32,8 @@ module JamRuby chat_msg.target_user = target_user chat_msg.lesson_session = lesson_session chat_msg.purpose = purpose + chat_msg.music_notation = music_notation + chat_msg.claimed_recording = recording if lesson_session @@ -37,23 +41,25 @@ module JamRuby if user.id == lesson_session.student.id lesson_session.teacher_unread_messages = true + target = lesson_session.teacher Notification.send_lesson_message('chat', lesson_session, false, message) else lesson_session.student_unread_messages = true + target = lesson_session.student Notification.send_lesson_message('chat', lesson_session, true, message) end lesson_session.save(validate: false) # a nil purpose means 'normal chat', which is the only time we should send an email - if !target_user.online? && purpose.nil? && message.present? + if !target.online? && purpose.nil? && message.present? UserMailer.lesson_chat(chat_msg).deliver! end end if chat_msg.save - ChatMessage.send_chat_msg music_session, chat_msg, user, client_id, channel, lesson_session, purpose, target_user + ChatMessage.send_chat_msg music_session, chat_msg, user, client_id, channel, lesson_session, purpose, target_user, music_notation, recording end chat_msg end @@ -94,10 +100,24 @@ module JamRuby end end - def send_chat_msg(music_session, chat_msg, user, client_id, channel, lesson_session, purpose, target_user) + def send_chat_msg(music_session, chat_msg, user, client_id, channel, lesson_session, purpose, target_user, music_notation, claimed_recording) music_session_id = music_session.id if music_session lesson_session_id = lesson_session.id if lesson_session + if music_notation + puts "IS MUSIC NOTATION" + attachment_id = music_notation.id + attachment_type = music_notation.attachment_type + attachment_name = music_notation.file_name + elsif claimed_recording + attachment_id = claimed_recording.id + attachment_type = 'recording' + attachment_name = claimed_recording.name + end + + puts "ATTACMENT #{}" + + msg = @@message_factory.chat_message( music_session_id, user.name, @@ -107,7 +127,10 @@ module JamRuby chat_msg.created_at.utc.iso8601, channel, lesson_session_id, - purpose + purpose, + attachment_id, + attachment_type, + attachment_name ) if channel == 'session' @@ -116,6 +139,7 @@ module JamRuby @@mq_router.publish_to_active_clients(msg) elsif channel == 'lesson' @@mq_router.publish_to_user(target_user.id, msg, sender = {:client_id => client_id}) + @@mq_router.publish_to_user(user.id, msg, sender = {:client_id => client_id}) end end diff --git a/ruby/lib/jam_ruby/models/music_notation.rb b/ruby/lib/jam_ruby/models/music_notation.rb index fc2af0880..767453d14 100644 --- a/ruby/lib/jam_ruby/models/music_notation.rb +++ b/ruby/lib/jam_ruby/models/music_notation.rb @@ -4,6 +4,10 @@ module JamRuby NOTATION_FILE_DIR = "music_session_notations" + TYPE_NOTATION = 'notation' + TYPE_AUDIO = 'audio' + + ATTACHMENT_TYPES = [TYPE_NOTATION, TYPE_AUDIO] self.primary_key = 'id' attr_accessible :file_url, :size, :file_name @@ -16,10 +20,12 @@ module JamRuby before_destroy :delete_s3_files #validates :file_url, :presence => true + validates :attachment_type, :presence => true, inclusion: {in: ATTACHMENT_TYPES} validates :size, :presence => true - def self.create(session_id, file, current_user) + def self.create(session_id, type, file, current_user) music_notation = MusicNotation.new + music_notation.attachment_type = type music_notation.file_name = file.original_filename music_notation.music_session_id = session_id music_notation.user = current_user diff --git a/ruby/lib/jam_ruby/models/recording.rb b/ruby/lib/jam_ruby/models/recording.rb index 0fbf186e8..67585f678 100644 --- a/ruby/lib/jam_ruby/models/recording.rb +++ b/ruby/lib/jam_ruby/models/recording.rb @@ -201,7 +201,15 @@ module JamRuby def has_access?(user) - users.exists?(user) || plays.where("player_id=?", user).count != 0 + return false if user.nil? + + users.exists?(user) || attached_with_lesson(user) #|| plays.where("player_id=?", user).count != 0 + end + + def attached_with_lesson(user) + + ChatMessage.joins(:claimed_recording => [:recording]).where('recordings.id = ?', self.id).where('chat_messages.user_id = ?', user.id).count > 0 || + ChatMessage.joins(:claimed_recording => [:recording]).where('recordings.id = ?', self.id).where('chat_messages.target_user_id = ?', user.id).count > 0 end # Start recording a session. diff --git a/ruby/lib/jam_ruby/models/teacher.rb b/ruby/lib/jam_ruby/models/teacher.rb index f8f0f729d..b5e5d7b09 100644 --- a/ruby/lib/jam_ruby/models/teacher.rb +++ b/ruby/lib/jam_ruby/models/teacher.rb @@ -205,7 +205,7 @@ module JamRuby teacher.price_per_month_120_cents = params[:price_per_month_120_cents] if params.key?(:price_per_month_120_cents) teacher.teaches_test_drive = params[:teaches_test_drive] if params.key?(:teaches_test_drive) teacher.test_drives_per_week = params[:test_drives_per_week] if params.key?(:test_drives_per_week) - teacher.test_drives_per_week ||= 10 # default to 10 in absence of others + teacher.test_drives_per_week = 10 if !params.key?(:test_drives_per_week) # default to 10 in absence of others teacher.school_id = params[:school_id] if params.key?(:school_id) diff --git a/web/app/assets/javascripts/chatPanel.js b/web/app/assets/javascripts/chatPanel.js index 32aaf043c..995b636c0 100644 --- a/web/app/assets/javascripts/chatPanel.js +++ b/web/app/assets/javascripts/chatPanel.js @@ -189,7 +189,10 @@ function sessionStarted(e, data) { $sessionId = data.session.id; - + var lessonId = null; + if (data.session.lesson_session) { + lessonId = data.session.lesson_session.id + } // open chat panel //$chatSender.show(); //$chatMessagesScroller.show(); @@ -199,7 +202,7 @@ reset(); - context.ChatActions.sessionStarted($sessionId); + context.ChatActions.sessionStarted($sessionId, lessonId); showing = true fullyInitialized = true; drainQueue(); diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 01db700d4..0a85887cf 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -2111,6 +2111,18 @@ }) } + + function attachRecordingToLesson(data) { + return $.ajax({ + type: "POST", + url: '/api/lesson_sessions/' + data.id + '/attach_recording', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(data) + }) + } + + function bookLesson(data) { return $.ajax({ type: "POST", @@ -2695,6 +2707,7 @@ this.signup = signup; this.portOverCarts = portOverCarts; this.bookLesson = bookLesson; + this.attachRecordingToLesson = attachRecordingToLesson; this.getLessonBooking = getLessonBooking; this.getUnprocessedLesson = getUnprocessedLesson; this.getUnprocessedLessonOrIntent = getUnprocessedLessonOrIntent; diff --git a/web/app/assets/javascripts/jquery.lessonSessionActions.js b/web/app/assets/javascripts/jquery.lessonSessionActions.js index bff821074..85db4c285 100644 --- a/web/app/assets/javascripts/jquery.lessonSessionActions.js +++ b/web/app/assets/javascripts/jquery.lessonSessionActions.js @@ -12,7 +12,7 @@ return this.each(function(index) { function close() { - $parent.btOff(); + //$parent.btOff(); $parent.focus(); } @@ -41,7 +41,7 @@ } }, out: function() { - $parent.btOff(); + //$parent.btOff(); }}); } @@ -53,6 +53,10 @@ var width = 100; var otherOverlap = 22; + if (options.attachments_only) { + extraClasses += 'attachments-only' + width = 120; + } if(options.isRequested) { extraClasses += 'is-requested ' width = 100; @@ -69,6 +73,15 @@ extraClasses += 'is-admin ' width = 135; } + if (options.chat_dialog) { + var $sidebar = $parent.closest('.dialog') + } + else if (options.attachments_only) { + var $sidebar = $parent.closest('#sidebar-div'); + } + else { + var $sidebar = $parent.closest('.screen') + } context.JK.hoverBubble($parent, html, { trigger:'none', cssClass: 'lesson-action-popup' + extraClasses, @@ -78,7 +91,7 @@ overlap: -10, width:width, closeWhenOthersOpen: true, - offsetParent: $parent.closest('.screen'), + offsetParent: $sidebar, positions:['bottom'], preShow: function() { @@ -90,7 +103,7 @@ timeout = null; } waitForBubbleHover($(container)) - timeout = setTimeout(function() {$parent.btOff()}, 6000) + //timeout = setTimeout(function() {$parent.btOff()}, 6000) } }); }); diff --git a/web/app/assets/javascripts/react-components/AttachmentStatus.js.jsx.coffee b/web/app/assets/javascripts/react-components/AttachmentStatus.js.jsx.coffee index 9dbd8d24a..733cd77b9 100644 --- a/web/app/assets/javascripts/react-components/AttachmentStatus.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/AttachmentStatus.js.jsx.coffee @@ -19,12 +19,28 @@ AttachmentStore = context.AttachmentStore notationSelected: (e) -> files = $(e.target).get(0).files - logger.debug("notation files selected: " + files) - window.AttachmentActions.uploadNotations(files) + logger.debug("notation files selected: ", files) + window.AttachmentActions.uploadNotations.trigger(files, @notationUploadDone, @notationUploadFail) + + notationUploadDone: () -> + logger.debug("AttachmentStatus: notationUploadDone") + context.JK.Banner.showNotice('Notation Uploaded', 'The music notation file has been uploaded, and can be accessed from the Messages window for this lesson.') + + notationUploadFail: () -> + logger.debug("AttachmentStatus: notationUploadFail") audioSelected: (e) -> files = $(e.target).get(0).files - logger.debug("audio files selected: " + files) + logger.debug("audio files selected: ", files) + window.AttachmentActions.uploadAudio.trigger(files, @notationUploadDone, @notationUploadFail) + + audioUploadDone: () -> + logger.debug("AttachmentStatus: audioUploadDone") + context.JK.Banner.showNotice('Audio file Uploaded', 'The audio file has been uploaded, and can be accessed from the Messages window for this lesson.') + + audioUploadFail: () -> + logger.debug("AttachmentStatus: audioUploadFail") + render: () -> `
diff --git a/web/app/assets/javascripts/react-components/ChatDialog.js.jsx.coffee b/web/app/assets/javascripts/react-components/ChatDialog.js.jsx.coffee index 3e6304655..3b012781b 100644 --- a/web/app/assets/javascripts/react-components/ChatDialog.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/ChatDialog.js.jsx.coffee @@ -27,6 +27,7 @@ context = window @app.ajaxError(jqXHR) afterHide: () -> + window.ChatActions.activateChannel('global') parseId:(id) -> if !id? diff --git a/web/app/assets/javascripts/react-components/ChatWindow.js.jsx.coffee b/web/app/assets/javascripts/react-components/ChatWindow.js.jsx.coffee index 7120c38ad..89019ceaa 100644 --- a/web/app/assets/javascripts/react-components/ChatWindow.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/ChatWindow.js.jsx.coffee @@ -13,6 +13,9 @@ ChatActions = @ChatActions state = context.ChatStore.getState() state + getInitialProps: () -> + {newFormat:true} + onChatChanged: (chatState) -> @setState(chatState) @@ -38,11 +41,41 @@ ChatActions = @ChatActions when 'Lesson Updated Time Approved' then 'updated lesson time' when 'New Time Proposed' then 'proposed new time' when 'Lesson Declined' then 'declined lesson' + when 'Notation File' then 'attached a notation file' + when 'Audio File' then 'attached an audio file' + when 'JamKazam Recording' then 'attached a recording' else purpose + notationClicked: (music_notation, e) -> + e.preventDefault() + + context.JK.popExternalLink("/api/music_notations/#{music_notation.id}?target=_blank") + + audioClicked: (music_notation, e) -> + e.preventDefault() + + context.JK.popExternalLink("/api/music_notations/#{music_notation.id}?target=_blank") + + recordingClicked: (recording, e) -> + e.preventDefault() + context.JK.popExternalLink("/recordings/#{recording.id}") + + openMenu: (lesson, e) -> + $this = $(e.target) + if !$this.is('.lesson-session-actions-btn') + $this = $this.closest('.lesson-session-actions-btn') + $this.btOn() + render: () -> - if !this.props.hideHeader + if @state.channel == 'lesson' + chatTabs = `
+ attach file +
+ +
` + else if !this.props.hideHeader tabs = [] for channel of @state.msgs classes = {} @@ -71,8 +104,8 @@ ChatActions = @ChatActions msgs = [] - if this.props?.channel? - activeChannel = this.props.channel + if @activeChannelType() == 'lesson' + activeChannel = @state.lessonSessionId else activeChannel = @state.channel @@ -86,23 +119,25 @@ ChatActions = @ChatActions else sender = msg.sender_name - if this.props.newFormat - if msg.purpose - purpose = `
{this.convertPurpose(msg.purpose)}
` - else - purpose = null - msgs.push(`
- {sender}{purpose} - {msg.msg} -
`) - + if msg.purpose + purpose = `
{this.convertPurpose(msg.purpose)}
` else + purpose = null + + if msg.purpose == 'Notation File' + additional = `{msg.music_notation.file_name}` + else if msg.purpose == 'Audio File' + additional = `{msg.music_notation.file_name}` + else if msg.purpose == 'JamKazam Recording' + additional = `{msg.claimed_recording.name}` + + msgs.push(`
+ {sender}{purpose} + {msg.msg} + {additional} +
`) + - msgs.push(`
- {sender} - {msg.msg} - -
`) if this.props?.showEmailNotice otherName = this.props?.other?.name @@ -114,6 +149,12 @@ ChatActions = @ChatActions if this.props?.rootClass? topClasses[this.props.rootClass] = true + if this.props.rootClass == 'ChatDialog' + attachFiles = `attach file +
+ ` + `
{chatTabs}
@@ -128,16 +169,14 @@ ChatActions = @ChatActions SEND {closeBtn} {emailSentNotice} + {attachFiles}
` activeChannelType: () -> - if this.props?.channelType? - this.props.channelType - else - @state.channel + @state.channelType sendMessage:()-> if !context.JK.JamServer.connected @@ -187,6 +226,33 @@ ChatActions = @ChatActions $scroller = @root.find('.chat-list-scroller') $scroller.animate({scrollTop: $scroller[0].scrollHeight}, speed) + items = @root.find('.chat-tabs .lesson-session-actions-btn') + @hookupMenu(items) + + items = @root.find('#attach-files-chat-dialog') + @hookupMenu(items) + + + hookupMenu: (items) -> + $.each(items, (i, node) => ( + $node = $(node) + + chat_dialog = this.props.rootClass == 'ChatDialog' + lesson = {id: $node.attr('data-lesson-id'), attachments_only: true, chat_dialog: chat_dialog} + + $node.lessonSessionActions(lesson).off(context.JK.EVENTS.LESSON_SESSION_ACTION).on(context.JK.EVENTS.LESSON_SESSION_ACTION, @lessonSessionActionSelected) + )) + + lessonSessionActionSelected: (e, data) -> + lessonId = data.options.id + + if data.lessonAction == 'attach-recording' + window.AttachmentActions.startAttachRecording(lessonId) + else if data.lessonAction == 'attach-notation' + window.AttachmentActions.startAttachNotation(lessonId) + else if data.lessonAction == 'attach-audio' + window.AttachmentActions.startAttachAudio(lessonId) + pasteIntoInput: (el, text) -> el.focus(); if typeof el.selectionStart == "number" && typeof el.selectionEnd == "number" diff --git a/web/app/assets/javascripts/react-components/LessonBooking.js.jsx.coffee b/web/app/assets/javascripts/react-components/LessonBooking.js.jsx.coffee index d86dc7d12..b80a92908 100644 --- a/web/app/assets/javascripts/react-components/LessonBooking.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/LessonBooking.js.jsx.coffee @@ -869,6 +869,20 @@ UserStore = context.UserStore else if @isPast() else + if @studentMadeDefaultSlot() + message = this.slotMessage(this.state.booking.default_slot, 'accept') + + if @isRecurring() + detail = `

Your {this.lessonDesc()} will take place each {this.slotTime(this.state.booking.default_slot)}

` + else + detail = `

Your {this.lessonDesc()} will take place this {this.slotTime(this.state.booking.default_slot)}

` + + summary = `
+ {this.userHeader(this.teacher())} +

Has accepted your lesson request.

+ {detail} + {message} +
` decision = `` `
diff --git a/web/app/assets/javascripts/react-components/LessonBookingDecision.js.jsx.coffee b/web/app/assets/javascripts/react-components/LessonBookingDecision.js.jsx.coffee index a2818fafe..49fcfab49 100644 --- a/web/app/assets/javascripts/react-components/LessonBookingDecision.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/LessonBookingDecision.js.jsx.coffee @@ -177,7 +177,7 @@ :
- * Time will be local to {window.jstz.determine().name()} + * Time will be local to {context.JK.currentTimezone()} {errorText} @@ -199,7 +199,7 @@ :
- *Time will be local to {window.jstz.determine().name()} + *Time will be local to {context.JK.currentTimezone()} {errorText}
` 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 76fb3f42b..9b60eb7d1 100644 --- a/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TeacherProfile.js.jsx.coffee @@ -118,6 +118,7 @@ proficiencyDescriptionMap = { afterShow: (e) -> + UserActions.refresh() @visible = true logger.debug("TeacherProfile: afterShow") @setState({userId: e.id, user: null}) diff --git a/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee index 6ddee4fb8..904b517c0 100644 --- a/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TeacherSearchScreen.js.jsx.coffee @@ -37,6 +37,8 @@ ProfileActions = @ProfileActions @visible = true afterShow: (e) -> + UserActions.refresh() + #@setState(TeacherSearchStore.getState()) #if @state.results.length == 0 # don't issue a new search every time someone comes to the screen, to preserve location from previous browsing diff --git a/web/app/assets/javascripts/react-components/stores/AttachmentStore.js.coffee b/web/app/assets/javascripts/react-components/stores/AttachmentStore.js.coffee index a94595513..a24cdcd85 100644 --- a/web/app/assets/javascripts/react-components/stores/AttachmentStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/AttachmentStore.js.coffee @@ -23,8 +23,18 @@ AttachmentActions = @AttachmentActions recordingsSelected: (recordings) -> logger.debug("recording selected", recordings) + options = {id: @lessonId} + options.recordings = recordings + rest.attachRecordingToLesson(options).done((response) => @attachedRecordingsToLesson(response)).fail((jqXHR) => @attachedRecordingsFail(jqXHR)) + + attachedRecordingsToLesson: (response) -> + context.JK.Banner.showNotice('Recording Attached', 'Your recording has been associated with this lesson, and can be accessed from the Messages window for this lesson.') + + attachedRecordingsFail: (jqXHR) -> + @app.ajaxError(jqXHR) + onStartAttachRecording: (lessonId) -> - if @lessonId? + if @uploading logger.warn("rejecting startAttachRecording attempt as currently busy") return @lessonId = lessonId @@ -32,25 +42,25 @@ AttachmentActions = @AttachmentActions @ui.launchRecordingSelectorDialog([], (recordings) => @recordingsSelected(recordings) ) - @change() + @changed() onStartAttachNotation: (lessonId) -> - if @lessonId? + if @uploading logger.warn("rejecting onStartAttachNotation attempt as currently busy") return @lessonId = lessonId - logger.debug("notation upload started") + logger.debug("notation upload started for lesson: " + lessonId) @triggerNotation() - @change() + @changed() onStartAttachAudio: (lessonId) -> - if @lessonId? + if @uploading logger.warn("rejecting onStartAttachAudio attempt as currently busy") return @lessonId = lessonId - logger.debug("audio upload started") + logger.debug("audio upload started for lesson: " + lessonId) @triggerAudio() @changed() @@ -66,7 +76,7 @@ AttachmentActions = @AttachmentActions onUploadNotations: (notations, doneCallback, failCallback) -> - logger.debug("beginning upload of notations") + logger.debug("beginning upload of notations", notations) @uploading = true @changed() @@ -92,14 +102,16 @@ AttachmentActions = @AttachmentActions return - formData.append('client_id', app.clientId) - formData.append('lesson_session_id', @lessonid); + formData.append('lesson_session_id', @lessonId); + formData.append('attachment_type', 'notation') rest.uploadMusicNotations(formData) - .done((response) => @doneUploadingNotatations(notations, response)) - .fail((jqXHR) => @failUploadingNotations(jqXHR)) + .done((response) => @doneUploadingNotatations(notations, response, doneCallback, failCallback)) + .fail((jqXHR) => @failUploadingNotations(jqXHR, failCallback)) - doneUploadingNotatations: (notations, response) -> + doneUploadingNotatations: (notations, response, doneCallback, failCallback) -> + @uploading = false + @changed() error_files = []; $.each(response, (i, music_notation) => ( if music_notation.errors @@ -112,7 +124,9 @@ AttachmentActions = @AttachmentActions else doneCallback() - failUploadingNotations: (jqXHR) -> + failUploadingNotations: (jqXHR, failCallback) -> + @uploading = false + @changed() if jqXHR.status == 413 # the file is too big. Let the user know. # This should happen when they select the file, but a misconfiguration on the server could cause this. diff --git a/web/app/assets/javascripts/react-components/stores/ChatStore.js.coffee b/web/app/assets/javascripts/react-components/stores/ChatStore.js.coffee index c6f175967..959b6b5e6 100644 --- a/web/app/assets/javascripts/react-components/stores/ChatStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/ChatStore.js.coffee @@ -14,6 +14,7 @@ SessionStore = context.SessionStore systemMsgId: 0 msgs: {global:[], session:[]} max_global_msgs: 100 + channelType: null init: () -> # Register with the app store to get @app @@ -46,11 +47,22 @@ SessionStore = context.SessionStore @changed() # called from ChatPanel - onSessionStarted: () -> - @msgs['session'] = [] - @channel = 'session' - @fetchHistory() - @onEmptyChannel(@channel) + onSessionStarted: (sessionId, lessonId) -> + logger.debug("ChatStore.sessionStarted sessionId: #{sessionId} lessonId: #{lessonId}") + if lessonId? + @lessonSessionId = lessonId + #@msgs['session'] = [] + @channel = 'lesson' + @channelType = 'lesson' + @fetchHistory() + @onEmptyChannel(@channel) + else + @msgs['session'] = [] + @channel = 'session' + @channelType = null + @fetchHistory() + @onEmptyChannel(@channel) + buildQuery: (channel = null) -> if !channel? @@ -71,6 +83,7 @@ SessionStore = context.SessionStore onInitializeLesson: (lessonSessionId) -> @lessonSessionId = lessonSessionId + @channelType = 'lesson' @fetchHistory('lesson') @@ -102,6 +115,8 @@ SessionStore = context.SessionStore convert.created_at = chat.created_at convert.channel = chat.channel convert.purpose = chat.purpose + convert.music_notation = chat.music_notation + convert.claimed_recording = chat.claimed_recording converted.push(convert) converted @@ -146,7 +161,13 @@ SessionStore = context.SessionStore if msg.channel == 'lesson' effectiveChannel = msg.lesson_session_id - console.log("effective channel", effectiveChannel, @msgs) + if msg.attachment_type? + console.log("attachment type seen") + if msg.attachment_type == 'notation' || msg.attachment_type == 'audio' + msg.music_notation = {id: msg.attachment_id, file_name: msg.attachment_name, attachment_type: msg.attachment_type} + else + msg.claimed_recording = {id: msg.attachment_id, name: msg.attachment_name} + channelMsgs = @msgs[effectiveChannel] if !channelMsgs? @@ -174,7 +195,10 @@ SessionStore = context.SessionStore payload onActivateChannel: (channel) -> + logger.debug("onActivateChannel: " + channel) @channel = channel + if @channel != 'lesson' + @channelType = null @fetchHistory() @changed() @@ -218,7 +242,7 @@ SessionStore = context.SessionStore window.JK.JamServer.sendChatMessage(channel, msg) getState: () -> - return {msgs: @msgs, channel: @channel} + return {msgs: @msgs, channel: @channel, channelType: @channelType, lessonSessionId: @lessonSessionId} changed: () -> @trigger(@getState()) diff --git a/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee index 49c7d01b3..bd2447424 100644 --- a/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee @@ -774,7 +774,8 @@ ConfigureTracksActions = @ConfigureTracksActions context.JK.JamServer.registerMessageCallback(context.JK.MessageType.TRACKS_CHANGED, @trackChanges); context.JK.JamServer.registerMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, @trackChanges); - $(document).trigger(EVENTS.SESSION_STARTED, {session: {id: @currentSessionId}}) if document + console.log("SESSION STARTED EVENT") + $(document).trigger(EVENTS.SESSION_STARTED, {session: {id: @currentSessionId, lesson_session: response.lesson_session}}) if document @handleAutoOpenJamTrack() diff --git a/web/app/assets/javascripts/scheduled_session.js.erb b/web/app/assets/javascripts/scheduled_session.js.erb index 3296d9275..5e3ec99e8 100644 --- a/web/app/assets/javascripts/scheduled_session.js.erb +++ b/web/app/assets/javascripts/scheduled_session.js.erb @@ -605,6 +605,7 @@ return deferred; } + formData.append('attachment_type', 'notation') formData.append('client_id', app.clientId); $btnSelectFiles.text('UPLOADING...').data('uploading', true) $uploadSpinner.show(); diff --git a/web/app/assets/javascripts/sessionModel.js b/web/app/assets/javascripts/sessionModel.js index cd7edb8ae..d92e1f065 100644 --- a/web/app/assets/javascripts/sessionModel.js +++ b/web/app/assets/javascripts/sessionModel.js @@ -274,7 +274,7 @@ server.registerMessageCallback(context.JK.MessageType.TRACKS_CHANGED, trackChanges); server.registerMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, trackChanges); - $(document).trigger(EVENTS.SESSION_STARTED, {session: {id: sessionId}}); + $(document).trigger(EVENTS.SESSION_STARTED, {session: {id: sessionId, lesson_session: session.lesson_session}}); }) .fail(function() { updateCurrentSession(null); diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index 8dd55d654..9a00e241d 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -1218,6 +1218,23 @@ }) } + context.JK.currentTimezone = function() { + var tz = window.jstz.determine().name() + + if (tz == 'America/Chicago') { + tz = 'US Central Time' + } + else if(tz == 'America/Los_Angeles' || tz == 'America/Los Angeles') { + tz = 'US Pacific Time' + } + else if(tz == 'America/New_York' || tz == 'America/New York') { + tz = 'US Eastern Time' + } + else if (tz == 'America/Arizona') { + tz = 'US Mountain Time' + } + return tz; + } context.JK.flash = function(msg, options) { options = options || {} diff --git a/web/app/assets/stylesheets/client/lessonSessionActions.css.scss b/web/app/assets/stylesheets/client/lessonSessionActions.css.scss index f8d559669..c1bbc22c8 100644 --- a/web/app/assets/stylesheets/client/lessonSessionActions.css.scss +++ b/web/app/assets/stylesheets/client/lessonSessionActions.css.scss @@ -2,6 +2,12 @@ .lesson-action-popup { + a{ + color:#fc0 !important; + } + li { + border-bottom:0 !important; + } &.not-card-ok .bt-content{ height:20px !important; width:90px !important; diff --git a/web/app/assets/stylesheets/client/react-components/ChatWindow.css.scss b/web/app/assets/stylesheets/client/react-components/ChatWindow.css.scss index 4cc84323a..c574014f1 100644 --- a/web/app/assets/stylesheets/client/react-components/ChatWindow.css.scss +++ b/web/app/assets/stylesheets/client/react-components/ChatWindow.css.scss @@ -6,6 +6,28 @@ div[data-react-class="ChatWindow"] { .ChatWindow { + .chat-message-purpose { + display:inline; + color: $ColorTextTypical; + margin: 0 10px 0 0; + font-size:11px; + } + + .chat-message-text { + margin-top:8px; + display:block; + } + + .chat-message-timestamp { + display:inline-block; + } + + .additional { + margin-left:4px; + display: block; + margin-top:4px; + } + &.ChatDialog { .active-tab { top: 0; @@ -30,13 +52,7 @@ div[data-react-class="ChatWindow"] { } .chat-message { margin:0 0 15px 0; - .chat-message-text { - margin-top:8px; - display:block; - } - .chat-message-timestamp { - display:inline-block; - } + .chat-message-sender { &:after { content: ''; @@ -65,12 +81,6 @@ div[data-react-class="ChatWindow"] { margin: 8px -4px 0 0; width:50px; } - .chat-message-purpose { - display:inline; - color: $ColorTextTypical; - margin: 0 10px 0 0; - font-size:11px; - } } height: 100%; @@ -166,7 +176,6 @@ div[data-react-class="ChatWindow"] { .chat-message-timestamp { margin-top: 4px; color: #AAA; - display: block; margin-left:4px; } } @@ -181,4 +190,32 @@ div[data-react-class="ChatWindow"] { box-sizing: border-box; } + .arrow-down { + float:none; + margin-left:5px; + margin-top:0; + margin-right:0; + border-top: 4px solid #fc0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + display:inline-block; + } + .arrow-up { + float:none; + margin-right:0; + margin-left:5px; + margin-bottom:2px; + border-bottom: 4px solid #fc0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + display:inline-block; + } + .lesson-session-actions-btn { + margin-bottom:5px; + } + #attach-files-chat-dialog { + font-size:12px; + float:left; + margin-top:6px; + } } diff --git a/web/app/controllers/api_lesson_bookings_controller.rb b/web/app/controllers/api_lesson_bookings_controller.rb index da570b372..ef65f1459 100644 --- a/web/app/controllers/api_lesson_bookings_controller.rb +++ b/web/app/controllers/api_lesson_bookings_controller.rb @@ -150,7 +150,7 @@ class ApiLessonBookingsController < ApiController def counter if params[:lesson_session_id] - target_lesson = @lesson_session.find(params[:lesson_session_id]) + target_lesson = LessonSession.find(params[:lesson_session_id]) else target_lesson = @lesson_booking.next_lesson end diff --git a/web/app/controllers/api_lesson_sessions_controller.rb b/web/app/controllers/api_lesson_sessions_controller.rb index b2c19bf01..8acc3ff57 100644 --- a/web/app/controllers/api_lesson_sessions_controller.rb +++ b/web/app/controllers/api_lesson_sessions_controller.rb @@ -115,6 +115,32 @@ class ApiLessonSessionsController < ApiController @lesson_payment_charges = current_user.uncollectables end + def attach_recording + + if @lesson_session.student.id == current_user.id + me = @lesson_session.student + other = @lesson_session.teacher + else + me = @lesson_session.teacher + other = @lesson_session.student + end + + + recordings = params[:recordings] + recordings.each do |recording_data| + #recording = Recording.find(recording_data[:id]) + claimed_recording = ClaimedRecording.find_by_id(recording_data[:id]) + if claimed_recording.nil? || claimed_recording.user != current_user + raise JamPermissionError, 'only owner of claimed_recording can associated it with a lesson' + end + + msg = ChatMessage.create(me, nil, '', ChatMessage::CHANNEL_LESSON, nil, other, @lesson_session, 'JamKazam Recording', nil, claimed_recording) + end + + render :json => {}, :status => 200 + + end + private def lookup_lesson diff --git a/web/app/controllers/api_music_notations_controller.rb b/web/app/controllers/api_music_notations_controller.rb index 90e64435c..c1c6822f1 100644 --- a/web/app/controllers/api_music_notations_controller.rb +++ b/web/app/controllers/api_music_notations_controller.rb @@ -6,17 +6,43 @@ class ApiMusicNotationsController < ApiController respond_to :json def create - client_id = params[:client_id] - - if client_id.nil? - raise JamArgumentError, "client_id must be specified" - end - @music_notations = [] + lesson_session = LessonSession.find_by_id(params[:lesson_session_id]) + + if lesson_session + session_id = lesson_session.music_session.id + else + session_id = params[:session_id] + end + + params[:files].each do |file| - music_notation = MusicNotation.create(params[:session_id], file, current_user) + music_notation = MusicNotation.create(session_id, params[:attachment_type], file, current_user) @music_notations.push music_notation + + if lesson_session + if lesson_session.student.id == current_user.id + me = lesson_session.student + other = lesson_session.teacher + else + me = lesson_session.teacher + other = lesson_session.student + end + + if !music_notation.errors.any? + # if no error and it's a lesson, then make a chat about it + + if params[:attachment_type] == MusicNotation::TYPE_NOTATION + purpose = "Notation File" + else + purpose = "Audio File" + end + + msg = ChatMessage.create(me, nil, '', ChatMessage::CHANNEL_LESSON, nil, other, lesson_session, purpose, music_notation) + end + end + end if params[:files] respond_with @music_notations, responder: ApiResponder, :status => 201 diff --git a/web/app/views/api_chats/show.rabl b/web/app/views/api_chats/show.rabl index 3b6c57e72..ec147a8dc 100644 --- a/web/app/views/api_chats/show.rabl +++ b/web/app/views/api_chats/show.rabl @@ -8,4 +8,12 @@ node :user do |c| user_data[:name] = c.user.name end user_data -end \ No newline at end of file +end + +child :music_notation do + attributes :id, :file_name, :attachment_type +end + +child :claimed_recording do + attributes :id, :name +end diff --git a/web/app/views/api_music_sessions/show_history.rabl b/web/app/views/api_music_sessions/show_history.rabl index 19d75ee64..c244ae794 100644 --- a/web/app/views/api_music_sessions/show_history.rabl +++ b/web/app/views/api_music_sessions/show_history.rabl @@ -140,6 +140,10 @@ else } } + child(lesson_session: :lesson_session) { + attributes :id, :scheduled_start, :status, :teacher_id, :success, :duration, :student_id + } + child(:active_music_session => :active_music_session) { attributes :claimed_recording_initiator_id, :track_changes_counter diff --git a/web/app/views/clients/_lessonSessionActions.html.slim b/web/app/views/clients/_lessonSessionActions.html.slim index 26ebca9a7..c42b76c35 100644 --- a/web/app/views/clients/_lessonSessionActions.html.slim +++ b/web/app/views/clients/_lessonSessionActions.html.slim @@ -1,10 +1,19 @@ script type='text/template' id='template-lesson-session-actions' = '{% if (data.cardNotOk) { %}' + ul + li data-lesson-option="enter-payment" + a href='#' Enter Payment + = '{% } else if (data.attachments_only) { %}' + ul + li data-lesson-option="attach-recording" + a href='#' Attach Recording - li data-lesson-option="enter-payment" - a href='#' Enter Payment + li data-lesson-option="attach-notation" + a href='#' Attach Notation File + li data-lesson-option="attach-audio" + a href='#' Attach Audio File = '{% } else if (data.isRequested) { %}' ul li data-lesson-option="status" diff --git a/web/config/routes.rb b/web/config/routes.rb index fb67e9e5a..c05812a9c 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -696,6 +696,7 @@ SampleApp::Application.routes.draw do match '/lesson_sessions/uncollectable' => 'api_lesson_sessions#uncollectable', :via => :get match '/lesson_sessions/:id' => 'api_lesson_sessions#show', :via => :get match '/lesson_sessions/:id/update_unread_messages' => 'api_lesson_sessions#update_unread_messages', :via => :post + match '/lesson_sessions/:id/attach_recording' => 'api_lesson_sessions#attach_recording', :via => :post match '/lesson_sessions/:id/start_time' => 'api_lesson_sessions#start_time', :via => :post match '/lesson_sessions/:id/reschedule_check' => 'api_lesson_sessions#reschedule_check', :via => :post match '/lesson_sessions/:id/cancel_check' => 'api_lesson_sessions#cancel_check', :via => :post