This commit is contained in:
Seth Call 2015-01-23 13:56:10 -06:00
commit 9c9a42c2a5
53 changed files with 867 additions and 167 deletions

View File

@ -241,3 +241,5 @@ jam_track_available.sql
active_jam_track.sql
bpms_on_tap_in.sql
jamtracks_job.sql
text_messages.sql
text_message_migration.sql

View File

@ -0,0 +1,5 @@
insert into text_messages(source_user_id, target_user_id, message, created_at, updated_at) (
select source_user_id, target_user_id, message, created_at, updated_at
from notifications
where description='TEXT_MESSAGE'
);

8
db/up/text_messages.sql Normal file
View File

@ -0,0 +1,8 @@
CREATE TABLE text_messages (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
source_user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE,
target_user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE,
message TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

View File

@ -197,6 +197,7 @@ require "jam_ruby/models/jam_company"
require "jam_ruby/models/user_sync"
require "jam_ruby/models/video_source"
require "jam_ruby/models/recorded_video"
require "jam_ruby/models/text_message"
require "jam_ruby/jam_tracks_manager"
include Jampb

View File

@ -64,7 +64,7 @@ module JamRuby
def on_recovery(conn, settings)
@connected = true
@log.debug "reconnected #{conn} #{settings}"
@log.warn "reconnected #{conn} #{settings}"
#puts "#channel before #{@channel}"
#puts "recovered channel: #{@channel.reuse}"

View File

@ -11,13 +11,13 @@
<p>
Getting Started Video<br/>
We recommend watching this video before you jump into the service just to get oriented. It will really help you hit the ground running:
<a style="color: #ffcc00;" href="https://www.youtube.com/watch?v=VexH4834o9I">https://www.youtube.com/watch?v=VexH4834o9I</a>
<a style="color: #ffcc00;" href="https://www.youtube.com/watch?v=DBo--aj_P1w">https://www.youtube.com/watch?v=DBo--aj_P1w</a>
</p>
<p>
Other Great Tutorial Videos<br />
There are several other very great videos that will help you understand how to find and connect with other musicians on the service, create your own sessions or find and join other musicians sessions, play in sessions, record and share your performances, and even live broadcast your sessions to family, friends, and fans. Check these helpful videos out here:
<a style="color: #ffcc00;" href="https://jamkazam.desk.com/customer/portal/articles/1304097-tutorial-videos">https://jamkazam.desk.com/customer/portal/articles/1304097-tutorial-videos</a>
<a style="color: #ffcc00;" href="https://jamkazam.desk.com/customer/portal/topics/673198-tutorials-on-major-features/articles">https://jamkazam.desk.com/customer/portal/topics/673198-tutorials-on-major-features/articles</a>
</p>
<p>

View File

@ -3,12 +3,12 @@ Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --
We're delighted that you have decided to try the JamKazam service, and we hope that you will enjoy using JamKazam to play music with others. Following are links to some resources that can help to get you up and running quickly.
Getting Started Video
We recommend watching this video before you jump into the service just to get oriented. It will really help you hit the ground running:
https://www.youtube.com/watch?v=VexH4834o9I
We recommend watching this video before you jump into the service just to get oriented. It will really help you hit the ground running:
https://www.youtube.com/watch?v=DBo--aj_P1w
Other Great Tutorial Videos
There are several other very great videos that will help you understand how to find and connect with other musicians on the service, create your own sessions or find and join other musicians sessions, play in sessions, record and share your performances, and even live broadcast your sessions to family, friends, and fans. Check these helpful videos out here:
https://jamkazam.desk.com/customer/portal/articles/1304097-tutorial-videos
https://jamkazam.desk.com/customer/portal/topics/673198-tutorials-on-major-features/articles
Knowledge Base Articles
You can find Getting Started knowledge base articles on things like frequently asked questions (FAQ), minimum system requirements for your Windows or Mac computer, how to troubleshoot audio problems in sessions, and more here:

View File

@ -333,6 +333,8 @@ SQL
connection = Connection.find_by_client_id_and_user_id!(client_id, user.id)
connection.join_the_session(music_session, as_musician, tracks, user, audio_latency, video_sources)
JamRuby::MusicSessionUserHistory.join_music_session(user.id, music_session.id)
# connection.music_session_id = music_session.id
# connection.as_musician = as_musician
# connection.joining_session = true

View File

@ -290,13 +290,8 @@ module JamRuby
}
)
if (offset)
query = query.offset(offset)
end
if (limit)
query = query.limit(limit)
end
query = query.offset(offset) if offset
query = query.limit(limit) if limit
if as_musician
query = query.where(

View File

@ -426,14 +426,19 @@ module JamRuby
# returns one user history per user, with instruments all crammed together, and with total duration
def unique_user_histories
# only get the active users if the session is in progress
user_filter = "music_sessions_user_history.session_removed_at is null" if self.session_removed_at.nil?
MusicSessionUserHistory
.joins(:user)
.select("STRING_AGG(instruments, '|') AS total_instruments,
SUM(date_part('epoch', COALESCE(music_sessions_user_history.session_removed_at, music_sessions_user_history.created_at) - music_sessions_user_history.created_at)) AS total_duration,
music_sessions_user_history.user_id, music_sessions_user_history.music_session_id, users.first_name, users.last_name, users.photo_url")
.group("music_sessions_user_history.user_id, music_sessions_user_history.music_session_id, users.first_name, users.last_name, users.photo_url")
.order("music_sessions_user_history.user_id")
.where(%Q{ music_sessions_user_history.music_session_id = '#{id}'})
.joins(:user)
.select("STRING_AGG(instruments, '|') AS total_instruments,
SUM(date_part('epoch', COALESCE(music_sessions_user_history.session_removed_at, music_sessions_user_history.created_at) - music_sessions_user_history.created_at)) AS total_duration,
music_sessions_user_history.user_id, music_sessions_user_history.music_session_id, users.first_name, users.last_name, users.photo_url")
.group("music_sessions_user_history.user_id, music_sessions_user_history.music_session_id, users.first_name, users.last_name, users.photo_url")
.order("music_sessions_user_history.user_id")
.where(%Q{ music_sessions_user_history.music_session_id = '#{id}'})
.where(user_filter)
end
def duration_minutes

View File

@ -56,6 +56,20 @@ module JamRuby
(end_time - self.created_at) / 60.0
end
def self.join_music_session(user_id, session_id)
hist = self
.where(:user_id => user_id)
.where(:music_session_id => session_id)
.limit(1)
.first
hist.start_history if hist
end
def start_history
self.session_removed_at = nil
self.update_attributes(:session_removed_at => self.session_removed_at, :max_concurrent_connections => determine_max_concurrent)
end
def self.removed_music_session(user_id, session_id)
hist = self
.where(:user_id => user_id)
@ -67,7 +81,6 @@ module JamRuby
def end_history
self.session_removed_at = Time.now if self.session_removed_at.nil?
self.update_attributes(:session_removed_at => self.session_removed_at, :max_concurrent_connections => determine_max_concurrent)
end

View File

@ -42,7 +42,7 @@ module JamRuby
# used for persisted notifications
def formatted_msg
# target_user, band, session, recording, invitation, join_request = nil
source_user, band = nil
source_user, band, session = nil
unless self.source_user_id.nil?
source_user = User.find(self.source_user_id)
@ -53,7 +53,12 @@ module JamRuby
end
unless self.session_id.nil?
session = MusicSession.find(self.session_id)
session = MusicSession.find_by_id(self.session_id)
# remove all notifications related to this session if it's not found
if session.nil?
Notification.delete_all "(session_id = '#{session_id}')"
end
end
self.class.format_msg(self.description, {:user => source_user, :band => band, :session => session})

View File

@ -0,0 +1,52 @@
include Devise::Models
module JamRuby
class TextMessage < ActiveRecord::Base
self.primary_key = 'id'
default_scope order('created_at DESC')
attr_accessible :target_user_id, :source_user_id, :message
belongs_to :target_user, :class_name => "JamRuby::User", :foreign_key => "target_user_id"
belongs_to :source_user, :class_name => "JamRuby::User", :foreign_key => "source_user_id"
validates :message, length: {minimum: 1, maximum: 400}, no_profanity: true
def self.index(target_user_id, source_user_id, options = {})
offset = options[:offset]
limit = options[:limit]
# if not specified, default offset to 0
offset ||= 0
offset = offset.to_i
# if not specified, default limit to 10
limit ||= 10
limit = limit.to_i
TextMessage
.offset(offset)
.limit(limit)
.where('(source_user_id = (?) AND target_user_id = (?)) OR (source_user_id = (?) AND target_user_id = (?))', source_user_id, target_user_id, target_user_id, source_user_id)
end
def self.create(message, target_user_id, source_user_id)
sanitized_text = Sanitize.fragment(message, elements: HtmlSanitize::SAFE)
# create new message
tm = TextMessage.new
tm.message = sanitized_text
tm.target_user_id = target_user_id
tm.source_user_id = source_user_id
tm.save
# send notification
@notification = Notification.send_text_message(sanitized_text, User.find(source_user_id), User.find(target_user_id))
tm
end
end
end

View File

@ -25,6 +25,10 @@ module JamRuby
MOD_NO_SHOW = "no_show"
# MIN/MAX AUDIO LATENCY
MINIMUM_AUDIO_LATENCY = 2
MAXIMUM_AUDIO_LATENCY = 10000
devise :database_authenticatable, :recoverable, :rememberable
acts_as_mappable
@ -79,6 +83,9 @@ module JamRuby
# self.id = followable_id in follows table
has_many :followers, :as => :followable, :class_name => "JamRuby::Follow", :dependent => :destroy
# text messages
has_many :text_messages, :class_name => "JamRuby:TextMessage", :foreign_key => "target_user_id"
# notifications
has_many :notifications, :class_name => "JamRuby::Notification", :foreign_key => "target_user_id"
has_many :inverse_notifications, :through => :notifications, :class_name => "JamRuby::User"
@ -163,7 +170,7 @@ module JamRuby
validates :musician, :inclusion => {:in => [true, false]}
validates :show_whats_next, :inclusion => {:in => [nil, true, false]}
validates :mods, json: true
validates_numericality_of :last_jam_audio_latency, greater_than:0, :allow_nil => true
validates_numericality_of :last_jam_audio_latency, greater_than:MINIMUM_AUDIO_LATENCY, less_than:MAXIMUM_AUDIO_LATENCY, :allow_nil => true
validates :last_jam_updated_reason, :inclusion => {:in => [nil, JAM_REASON_REGISTRATION, JAM_REASON_NETWORK_TEST, JAM_REASON_FTUE, JAM_REASON_JOIN, JAM_REASON_IMPORT, JAM_REASON_LOGIN] }
# custom validators
@ -1017,6 +1024,8 @@ module JamRuby
user.save
user.errors.add("recaptcha", "verification failed") if recaptcha_failed
if user.errors.any?
raise ActiveRecord::Rollback
else
@ -1033,16 +1042,9 @@ module JamRuby
UserMailer.confirm_email(user, signup_confirm_url.nil? ? nil : (signup_confirm_url + "/" + user.signup_token) ).deliver
end
end
if recaptcha_failed
user.errors.add "recaptcha", "verification failed"
raise ActiveRecord::Rollback
end
end
return user
end
user
end # def signup
# this is intended to be development-mode or test-mode only; VRFS-149
# it creates or updates one user per developer, so that we aren't in the business
@ -1379,8 +1381,9 @@ module JamRuby
end
def update_audio_latency(connection, audio_latency)
if audio_latency > 2
# updating the connection is best effort
# the backend sometimes gives tiny numbers, and sometimes very large numbers
if audio_latency > MINIMUM_AUDIO_LATENCY && audio_latency < MAXIMUM_AUDIO_LATENCY
# updating the connection is best effort; if it's not there that's OK
if connection
Connection.where(:id => connection.id).update_all(:last_jam_audio_latency => audio_latency)
end

View File

@ -11,7 +11,10 @@ def shutdown
end
Resque.before_first_fork do
JamWebEventMachine.start
current = Thread.current
Thread.new do
JamWebEventMachine.run_em(current)
end
#ActiveRecord::Base.establish_connection
config = {
@ -41,6 +44,9 @@ Resque.before_fork do
# ActiveRecord::Base.connection.disconnect!
#JamRuby::Stats.destroy!
# reconnect between jobs
ActiveRecord::Base.connection_handler.verify_active_connections!
end
Resque.after_fork do

View File

@ -0,0 +1,46 @@
require 'spec_helper'
describe TextMessage do
before do
TextMessage.delete_all
User.delete_all
@target_user = FactoryGirl.create(:user)
@source_user = FactoryGirl.create(:user)
@msg = TextMessage.new(:target_user_id => @target_user.id, :source_user_id => @source_user.id)
end
describe "index" do
it "should retrieve conversation for both users" do
@msg.message = "Test message"
@msg.save!
messages = TextMessage.index(@target_user.id, @source_user.id)
messages.count.should == 1
messages = TextMessage.index(@source_user.id, @target_user.id)
messages.count.should == 1
end
it "should page records" do
11.times do |n|
message = TextMessage.new(:target_user_id => @target_user.id, :source_user_id => @source_user.id)
message.message = "Message #{n}"
message.save!
end
messages = TextMessage.index(@target_user.id, @source_user.id, {:offset => 0})
messages.count.should == 10
messages = TextMessage.index(@target_user.id, @source_user.id, {:offset => 10})
messages.count.should == 1
end
it "should not allow empty message" do
expect { @msg.save! }.to raise_error(ActiveRecord::RecordInvalid)
end
end
end

View File

@ -489,14 +489,14 @@ describe User do
describe "audio latency" do
it "allow update" do
@user.last_jam_audio_latency = 1
@user.last_jam_audio_latency = 5
@user.save!
end
it "prevent negative or 0" do
@user.last_jam_audio_latency = 0
it "prevent negative" do
@user.last_jam_audio_latency = -1
@user.save.should be_false
@user.errors[:last_jam_audio_latency].should == ['must be greater than 0']
@user.errors[:last_jam_audio_latency].should == ['must be greater than 2']
end
it "prevent non numerical" do
@ -504,6 +504,42 @@ describe User do
@user.save.should be_false
@user.errors[:last_jam_audio_latency].should == ['is not a number']
end
it "prevent 2 (minimum)" do
@user.last_jam_audio_latency = User::MINIMUM_AUDIO_LATENCY
@user.save.should be_false
@user.errors[:last_jam_audio_latency].should == ['must be greater than 2']
end
it "prevent 10000 (maximum)" do
@user.last_jam_audio_latency = User::MAXIMUM_AUDIO_LATENCY
@user.save.should be_false
@user.errors[:last_jam_audio_latency].should == ['must be less than 10000']
end
end
describe "update_audio_latency" do
before(:each) do
@user.last_jam_audio_latency.should be_nil
end
it "ignores low latency" do
@user.update_audio_latency(nil, User::MINIMUM_AUDIO_LATENCY)
@user.last_jam_audio_latency.should be_nil
end
it "ignores high latency" do
@user.last_jam_audio_latency.should be_nil
@user.update_audio_latency(nil, User::MAXIMUM_AUDIO_LATENCY)
@user.last_jam_audio_latency.should be_nil
end
it "accepts normal latency" do
@user.last_jam_audio_latency.should be_nil
@user.update_audio_latency(nil, User::MINIMUM_AUDIO_LATENCY + 1)
@user.last_jam_audio_latency.should == User::MINIMUM_AUDIO_LATENCY + 1
end
end
describe "html_sanitize" do

View File

@ -65,7 +65,6 @@ gem 'postgres-copy'
#end
gem 'geokit-rails'
gem 'postgres_ext'
gem 'recaptcha', '0.3.6'
gem 'resque'
gem 'resque-retry'
gem 'resque-failed-job-mailer'

View File

@ -339,7 +339,14 @@
if ("instrument_list" in pending_rsvp_request && pending_rsvp_request.instrument_list != null) {
$.each(pending_rsvp_request.instrument_list, function (index, instrument) {
var instrumentId = context.JK.getInstrumentId(instrument.id);
var instrumentId;
if (instrument) {
instrumentId = context.JK.getInstrumentId(instrument.id);
}
else {
instrumentId = 'other';
}
var inst = context.JK.getInstrumentIcon24(instrumentId);
instrumentLogoHtml += '<img title="' + instrumentId + '" hoveraction="instrument" data-instrument-id="' + instrumentId + '" src="' + inst + '" width="24" height="24" />&nbsp;';
instrumentDesc.push(instrumentId);
@ -378,7 +385,14 @@
$.each(sessionData.approved_rsvps, function(index, approved_rsvp) {
if ("instrument_list" in approved_rsvp) {
$.each(approved_rsvp.instrument_list, function(index, instrument) {
var instrumentId = context.JK.getInstrumentId(instrument.id);
var instrumentId;
if (instrument) {
instrumentId = context.JK.getInstrumentId(instrument.id);
}
else {
instrumentId = 'other';
}
var inst = context.JK.getInstrumentIcon24(instrumentId);
instrumentLogoHtml += '<img title="' + instrumentId + '" hoveraction="instrument" data-instrument-id="' + instrumentId + '" src="' + inst + '" width="24" height="24" />&nbsp;';
});

View File

@ -0,0 +1,130 @@
(function(context,$) {
"use strict";
context.JK = context.JK || {};
context.JK.SessionStartDialog = function(app, session) {
var logger = context.JK.logger;
var sessionUtils = context.JK.SessionUtils;
var $dialog = null;
var dialogId = 'session-start-dialog';
var $btnStartSession = null;
function beforeShow(data) {
}
function afterShow(data) {
}
function afterHide() {
}
function showDialog() {
return app.layout.showDialog(dialogId);
}
function events() {
$btnStartSession.unbind('click');
$btnStartSession.click(function(e) {
context.location = '/client#/session/' + session.id;
app.layout.closeDialog(dialogId);
});
}
function initializeSessionDetails() {
$dialog.find('#session-start-type-disp').html('Now!');
$dialog.find('#session-name-disp').html(session.name);
$dialog.find('#session-description-disp').html(session.description);
if (session.music_notations && session.music_notations.length > 0) {
$dialog.find('#session-notations-disp').html("Notations: " + session.music_notations.join(', '));
}
if (session.band) {
$dialog.find('#session-band-disp').html(band.name);
}
else {
$dialog.find('#session-band-disp').html('N/A');
}
$dialog.find('#session-language-disp').html(session.language_description);
var invitedFriends = session.invitations;
var sessionInvited = [];
$.each(invitedFriends, function(index, invitation) {
sessionInvited.push(invitation.receiver_name);
});
var sessionInvitedString = sessionInvited.join(', ');
if (session.musician_access && session.approval_required) {
if (session.open_rsvps) {
if (invitedFriends.length == 0)
sessionInvitedString = "Any interested JamKazam musicians that I approve";
else
sessionInvitedString += ", plus any interested JamKazam musicians that I approve";
}
else {
if (invitedFriends.length == 0) {
sessionInvitedString = "No open RSVPs";
}
else {
sessionInvitedString += " (No open RSVPs)";
}
}
}
else if (session.musician_access && !session.approval_required) {
if (invitedFriends.length == 0)
sessionInvitedString = "Any interested JamKazam musicians who want to join us";
else
sessionInvitedString += ", plus any interested JamKazam musicians who want to join us";
}
$dialog.find('#session-invited-disp').html(sessionInvitedString);
var instrumentsMe = [], instrumentsOthers = [];
$.each(session.approved_rsvps, function(index, rsvp) {
if (rsvp.id === context.JK.currentUserId) {
$.each(rsvp.instrument_list, function(index, instrument) {
instrumentsMe.push(instrument.desc);
});
}
else {
$.each(rsvp.instrument_list, function(index, instrument) {
instrumentsOthers.push(instrument.desc);
});
}
});
$dialog.find('#session-instruments-me-disp').html(instrumentsMe.join(', '));
$dialog.find('#session-instruments-rsvp-disp').html(instrumentsOthers.join(', '));
$dialog.find('#session-musician-access-disp').html('Musicians: ' + session.musician_access_description);
$dialog.find('#session-fans-access-disp').html('Fans: ' + session.fan_access_description);
$dialog.find('#session-policy-disp').html(session.legal_policy);
}
function initialize() {
var dialogBindings = {
'beforeShow' : beforeShow,
'afterShow' : afterShow,
'afterHide': afterHide
};
app.bindDialog(dialogId, dialogBindings);
$dialog = $('[layout-id="' + dialogId + '"]');
$btnStartSession = $dialog.find('.btnStartSession');
initializeSessionDetails();
events();
}
this.initialize = initialize;
this.showDialog = showDialog;
}
return this;
})(window,jQuery);

View File

@ -37,14 +37,14 @@
}
function buildParams() {
return { type: 'TEXT_MESSAGE', receiver: otherId, offset: offset, limit: LIMIT};
return { target_user_id: otherId, offset: offset, limit: LIMIT};
}
function buildMessage() {
var message = {};
message['message'] = $textBox.val();
message['receiver'] = otherId;
message['target_user_id'] = otherId;
return message;
}
@ -66,17 +66,17 @@
$sendTextMessage.text('SENDING...')
rest.createTextMessage(buildMessage())
.done(function() {
$textBox.val('');
renderMessage(msg, user.id, user.name, new Date().toISOString(), true);
})
.fail(function(jqXHR) {
app.notifyServerError(jqXHR, 'Unable to Send Message');
})
.always(function() {
sendingMessage = false;
$sendTextMessage.text('SEND');
})
.done(function() {
$textBox.val('');
renderMessage(msg, user.id, user.name, new Date().toISOString(), true);
})
.fail(function(jqXHR) {
app.notifyServerError(jqXHR, 'Unable to Send Message');
})
.always(function() {
sendingMessage = false;
$sendTextMessage.text('SEND');
});
}
return false;
@ -163,7 +163,7 @@
}
$sendTextMessage.click(sendMessage);
rest.getNotifications(buildParams())
rest.getTextMessages(buildParams())
.done(function (response) {
context._.each(response, function (textMessage) {
renderMessage(textMessage.message, textMessage.source_user_id, userLookup[textMessage.source_user_id].name, textMessage.created_at);

View File

@ -1262,13 +1262,24 @@
var id = getId(options);
return $.ajax({
type: "POST",
url: '/api/users/' + id + '/notifications',
url: '/api/text_messages',
dataType: "json",
contentType: 'application/json',
data: JSON.stringify(options)
});
}
function getTextMessages(options) {
if(!options) options = {};
var id = getId(options);
return $.ajax({
type: "GET",
url: '/api/text_messages?' + $.param(options),
dataType: "json",
contentType: 'application/json'
});
}
function getNotifications(options) {
if(!options) options = {};
var id = getId(options);
@ -1597,6 +1608,7 @@
this.tweet = tweet;
this.createFbInviteUrl = createFbInviteUrl;
this.createTextMessage = createTextMessage;
this.getTextMessages = getTextMessages;
this.getNotifications = getNotifications;
this.createChatMessage = createChatMessage;
this.getChatMessages = getChatMessages;

View File

@ -207,20 +207,25 @@
isLoading = true;
// retrieve pending notifications for this user
rest.getNotifications(buildParams())
.done(function(response) {
updateNotificationList(response);
isLoading = false;
})
.fail(function() {
isLoading = false;
app.ajaxError();
})
.done(function(response) {
updateNotificationList(response);
isLoading = false;
})
.fail(function() {
isLoading = false;
app.ajaxError();
})
}
function updateNotificationList(response) {
$.each(response, function(index, val) {
if(val.description == 'TEXT_MESSAGE') {
// this means the session no longer exists
if (response.fan_access == null && response.musician_access == null) {
return;
}
if(val.description == context.JK.MessageType.TEXT_MESSAGE) {
val.formatted_msg = textMessageDialog.formatTextMessage(val.message.substring(0, 200), val.source_user_id, val.source_user.name, val.message.length > 200).html();
}

View File

@ -200,14 +200,14 @@
createSessionSettings.startDate = createSessionSettings.startDate || (new Date().toDateString());
$("#session-start-date").val(createSessionSettings.startDate);
$screen.find("#session-start-date").val(createSessionSettings.startDate);
toggleDate(true);
toggleStartTime();
toggleStepStatus();
sessionUtils.defaultTimezone($timezoneList);
if(firstTimeShown) {
firstTimeShown = false;
$('#session-when-start-scheduled').iCheck('check');
$screen.find('#session-when-start-scheduled').iCheck('check');
}
}
@ -217,7 +217,7 @@
function beforeShowStep3() {
rest.getBands(context.JK.currentUserId)
.done(function(result) {
var options = $("#session-band-list");
var options = $screen.find("#session-band-list");
options.empty();
options.append($("<option />").val('').text('No'));
$.each(result, function(idx, item) {
@ -251,10 +251,10 @@
var sessionName = createSessionSettings.name;
sessionName += ' (' + createSessionSettings.genresValues[0] + ')';
$('#session-name-disp').html(sessionName);
$screen.find('#session-name-disp').html(sessionName);
var sessionDescription = createSessionSettings.description;
$('#session-description-disp').html(sessionDescription);
$screen.find('#session-description-disp').html(sessionDescription);
var sessionNotations = [];
for (var i = 0; i < createSessionSettings.notations.length; i++) {
@ -262,16 +262,16 @@
sessionNotations.push(name);
}
if(sessionNotations.length > 0) {
$('#session-notations-disp').html("Notations: " + sessionNotations.join(', '));
$screen.find('#session-notations-disp').html("Notations: " + sessionNotations.join(', '));
}
else {
$('#session-notations-disp').html('');
$screen.find('#session-notations-disp').html('');
}
$('#session-language-disp').html(createSessionSettings.language.label);
$('#session-band-disp').html(createSessionSettings.band.label);
$screen.find('#session-language-disp').html(createSessionSettings.language.label);
$screen.find('#session-band-disp').html(createSessionSettings.band.label);
var plusMusicians = $('#session-plus-musicians')[0].checked;
var plusMusicians = $screen.find('#session-plus-musicians')[0].checked;
var sessionInvited = [];
var invitedFriends = inviteMusiciansUtil.getInvitedFriendNames();
@ -302,7 +302,7 @@
else
sessionInvitedString += ", plus any interested JamKazam musicians who want to join us";
}
$('#session-invited-disp').html(sessionInvitedString);
$screen.find('#session-invited-disp').html(sessionInvitedString);
if (createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_START_SCHEDULED%>') {
var session = scheduledSessions[createSessionSettings.selectedSessionId];
@ -315,7 +315,7 @@
});
}
});
$('#session-instruments-me-disp').html(instruments_me.join(', '));
$screen.find('#session-instruments-me-disp').html(instruments_me.join(', '));
}
if (session.open_slots.length > 0) {
@ -334,7 +334,7 @@
$.map(instruments_rsvp_arr, function(val, i) {
instruments_str_arr.push(i + ' (' + val.count + ') (' + val.level + ')');
})
$('#session-instruments-rsvp-disp').html(instruments_str_arr.join(', '));
$screen.find('#session-instruments-rsvp-disp').html(instruments_str_arr.join(', '));
}
}
else {
@ -342,7 +342,7 @@
$.each(getCreatorInstruments(), function(index, instrument) {
instruments_me.push(instrument.name);
});
$('#session-instruments-me-disp').html(instruments_me.join(', '));
$screen.find('#session-instruments-me-disp').html(instruments_me.join(', '));
var instruments_rsvp = [];
var otherInstruments = instrumentRSVP.getSelectedInstruments();
@ -353,13 +353,13 @@
$.each(otherInstruments, function(index, instrument) {
instruments_rsvp.push(instrument.name + ' (' + instrument.count + ') (' + proficiencyDescriptionMap[instrument.level] + ')');
});
$('#session-instruments-rsvp-disp').html(instruments_rsvp.join(', '));
$screen.find('#session-instruments-rsvp-disp').html(instruments_rsvp.join(', '));
}
$('#session-musician-access-disp').html('Musicians: ' + createSessionSettings.musician_access.label);
$('#session-fans-access-disp').html('Fans: ' + createSessionSettings.fans_access.label);
$screen.find('#session-musician-access-disp').html('Musicians: ' + createSessionSettings.musician_access.label);
$screen.find('#session-fans-access-disp').html('Fans: ' + createSessionSettings.fans_access.label);
$('#session-policy-disp').html(createSessionSettings.session_policy);
$screen.find('#session-policy-disp').html(createSessionSettings.session_policy);
}
function beforeMoveStep1() {
@ -428,7 +428,7 @@
createSessionSettings.recurring_mode.value = 'once';
}
else {
createSessionSettings.startDate = $('#session-start-date').val();
createSessionSettings.startDate = $screen.find('#session-start-date').val();
createSessionSettings.startTime = $startTimeList.val();
createSessionSettings.endTime = $endTimeList.val();
createSessionSettings.notations = [];
@ -437,7 +437,7 @@
createSessionSettings.timezone.label = $timezoneList.get(0).options[$timezoneList.get(0).selectedIndex].text;
createSessionSettings.recurring_mode.label = $recurringModeList.get(0).options[$recurringModeList.get(0).selectedIndex].text;
createSessionSettings.recurring_mode.value = $recurringModeList.val();
createSessionSettings.open_rsvps = $('#session-plus-musicians')[0].checked;
createSessionSettings.open_rsvps = $screen.find('#session-plus-musicians')[0].checked;
}
return true;
@ -503,22 +503,22 @@
function beforeMoveStep2() {
var isValid = true;
var name = $('#session-name').val();
var name = $screen.find('#session-name').val();
if (!name) {
$('#divSessionName .error-text').remove();
$('#divSessionName').addClass("error");
$('#session-name').after("<ul class='error-text'><li>Name is required</li></ul>");
$screen.find('#session-name').after("<ul class='error-text'><li>Name is required</li></ul>");
isValid = false;
}
else {
$('#divSessionName').removeClass("error");
}
var description = $('#session-description').val();
var description = $screen.find('#session-description').val();
if (!description) {
$('#divSessionDescription .error-text').remove();
$('#divSessionDescription').addClass("error");
$('#session-description').after("<ul class='error-text'><li>Description is required</li></ul>");
$screen.find('#session-description').after("<ul class='error-text'><li>Description is required</li></ul>");
isValid = false;
}
else {
@ -558,13 +558,13 @@
else
createSessionSettings.band.label = $bandList.get(0).options[$bandList.get(0).selectedIndex].text;
createSessionSettings.open_rsvps = $('#session-plus-musicians')[0].checked;
createSessionSettings.open_rsvps = $screen.find('#session-plus-musicians')[0].checked;
return true;
}
function beforeMoveStep4() {
var isValid = true;
var sessionPolicyChecked = $('#session-policy-confirm').is(':checked');
var sessionPolicyChecked = $screen.find('#session-policy-confirm').is(':checked');
if (!sessionPolicyChecked) {
$('#divSessionPolicy .error-text').remove();
$('#divSessionPolicy').addClass("error");
@ -576,11 +576,11 @@
}
createSessionSettings.session_policy = $('input[name="session-policy-type"][checked="checked"]').attr('policy-id');
var $musicianAccess = $('#session-musician-access');
var $musicianAccess = $screen.find('#session-musician-access');
createSessionSettings.musician_access.value = $musicianAccess.val();
createSessionSettings.musician_access.label = $musicianAccess.get(0).options[$musicianAccess.get(0).selectedIndex].text;
var $fansAccess = $('#session-fans-access');
var $fansAccess = $screen.find('#session-fans-access');
createSessionSettings.fans_access.value = $fansAccess.val();
createSessionSettings.fans_access.label = $fansAccess.get(0).options[$fansAccess.get(0).selectedIndex].text;
@ -975,7 +975,7 @@
}
function toggleDate(dontRebuildDropdowns) {
var selectedDate = new Date($('#session-start-date').val());
var selectedDate = new Date($screen.find('#session-start-date').val());
var currentDate = new Date();
var startIndex = 0;
@ -1046,7 +1046,7 @@
radioClass: 'iradio_minimal',
inheritClass: true
});
$("#session-start-date").datepicker({
$screen.find("#session-start-date").datepicker({
dateFormat: "D d MM yy",
onSelect: function() { toggleDate(); }
}

View File

@ -836,7 +836,6 @@
var metronomeTrackMixers = [];
var adhocTrackMixers = [];
console.log("_renderLocalMediaTracks", localMediaMixers)
function groupByType(mixers) {
context._.each(mixers, function(mixer) {
var mediaType = mixer.media_type;
@ -1254,6 +1253,11 @@
if (mixer && oppositeMixer) {
myTrack = (mixer.group_id === ChannelGroupIds.AudioInputMusicGroup);
if(!myTrack) {
// it only makes sense to track 'audio established' for tracks that don't belong to you
sessionModel.setAudioEstablished(participant.client_id, true);
}
var gainPercent = percentFromMixerValue(
mixer.range_low, mixer.range_high, mixer.volume_left);
var muteClass = "enabled";
@ -1342,6 +1346,10 @@
var muteMixer = mixerData.muteMixer;
if (mixer && oppositeMixer) {
if(!myTrack) {
// it only makes sense to track 'audio established' for tracks that don't belong to you
sessionModel.setAudioEstablished(clientId, true);
}
var participant = (sessionModel.getParticipant(clientId) || {name:'unknown'}).name;
logger.debug("found mixer=" + mixer.id + ", participant=" + participant)
usedMixers[mixer.id] = true;
@ -1369,6 +1377,13 @@
$('.disabled-track-overlay', $track).show();
$('.track-connection', $track).removeClass('red yellow green').addClass('red');
}
// if 5 seconds have gone by and no mixer, then we tell the server failed to establish audio
else if(lookingForMixersCount == 10) {
if(!myTrack) {
// it only makes sense to track 'audio established' for tracks that don't belong to you
sessionModel.setAudioEstablished(clientId, false);
}
}
var participant = (sessionModel.getParticipant(clientId) || { user: {name: 'unknown'}}).user.name;
logger.debug("still looking for mixer for participant=" + participant + ", clientId=" + clientId)
}

View File

@ -19,7 +19,7 @@
var $musicianTemplate = $('#template-musician-info');
var showJoinLink = true;
var showListenLink = true;
var showRsvpLink = true;
var MAX_MINUTES_SHOW_START = 15;
// related to listen
function stateChange(e, data) {
@ -88,7 +88,6 @@
var inSessionUsers = [];
showJoinLink = session.musician_access;
console.log('session', session)
showListenLink = session.fan_access && session.active_music_session && session.active_music_session.mount;
// render musicians who are already in the session
@ -159,6 +158,17 @@
sessionVals.in_session_musicians = inSessionUsersHtml.length > 0 ? inSessionUsersHtml : 'N/A';
sessionVals.join_link_display_style = showJoinLink ? "block" : "none";
sessionVals.listen_link_display_style = showListenLink ? "inline-block" : "none";
if (!session.fan_access) {
sessionVals.listen_link_text = '';
}
else if (session.active_music_session && session.active_music_session.mount) {
sessionVals.listen_link_text = 'Listen';
}
else {
sessionVals.listen_link_text = '';
}
var $row = $(context.JK.fillTemplate($activeSessionTemplate.html(), sessionVals));
var $offsetParent = $(tbGroup).closest('.content');
@ -359,11 +369,39 @@
var showRsvpLink = true;
var noLinkText = '';
$('.rsvp-link-text', $parentRow).hide();
if (approvedRsvpId) {
function showStartSessionButton(scheduledStart) {
var now = new Date();
var scheduledDate = new Date(scheduledStart);
var minutesFromStart = (scheduledDate.getTime() - now.getTime()) / (1000 * 60);
return minutesFromStart <= MAX_MINUTES_SHOW_START;
};
if (session.creator.id === context.JK.currentUserId) {
showRsvpLink = false;
noLinkText = $('<span class="text">You have been confirmed for this session. <a href="#" style="color: #fc0">Cancel</a></span>');
noLinkText = $('<span class="text"><a class="start" style="color: #fc0">Start session now?</a></span>');
noLinkText.find('a').click(function() {
ui.launchSessionStartDialog(session);
return false;
});
}
else if (approvedRsvpId) {
showRsvpLink = false;
if (session.scheduled_start && showStartSessionButton(session.scheduled_start)) {
noLinkText = $('<span class="text"><a class="start" style="color: #fc0">Start session now?</a>&nbsp;|&nbsp;<a class="cancel" style="color: #fc0">Cancel RSVP</a></span>');
noLinkText.find('a.start').click(function() {
ui.launchSessionStartDialog(session);
return false;
});
}
else {
noLinkText = $('<span class="text"><a class="cancel" style="color: #fc0">Cancel RSVP</a></span>');
}
// wire cancel link
noLinkText.find('a.cancel').click(function() {
ui.launchRsvpCancelDialog(session.id, approvedRsvpId)
.one(EVENTS.RSVP_CANCELED, function() {
rest.getSessionHistory(session.id)
@ -377,9 +415,20 @@
return false;
});
}
else if (hasInvitation) {
showRsvpLink = false;
if (session.scheduled_start && showStartSessionButton(session.scheduled_start)) {
noLinkText = $('<span class="text"><a class="start" style="color: #fc0">Start session now?</a></span>');
noLinkText.find('a').click(function() {
ui.launchSessionStartDialog(session);
return false;
});
}
}
else if (pendingRsvpId) {
showRsvpLink = false;
noLinkText = $('<span class="text">You have RSVP\'ed to this session. <a href="#" style="color: #fc0">Cancel</a></span>');
noLinkText = $('<span class="text"><a class="cancel" style="color: #fc0">Cancel RSVP</a></span>');
noLinkText.find('a').click(function() {
ui.launchRsvpCancelDialog(session.id, pendingRsvpId)
.one(EVENTS.RSVP_CANCELED, function() {
@ -406,6 +455,7 @@
if (showRsvpLink) {
$('.rsvp-msg', $parentRow).hide();
$('.rsvp-link', $parentRow).show();
$('.rsvp-link-text', $parentRow).show();
$('.rsvp-link', $parentRow).click(function(evt) {
ui.launchRsvpSubmitDialog(session.id)

View File

@ -28,6 +28,7 @@
var backendMixerAlertThrottleTimer = null;
// we track all the clientIDs of all the participants ever seen by this session, so that we can reliably convert a clientId from the backend into a username/avatar
var participantsEverSeen = {};
var currentParticipants = {};
var $self = $(this);
var sessionPageEnterDeferred = null;
var sessionPageEnterTimeout = null;
@ -301,6 +302,7 @@
$(document).trigger(EVENTS.SESSION_ENDED, {session: {id: currentSessionId}});
}
currentSessionId = null;
currentParticipants = {}
}
// you should only update currentSession with this function
@ -391,6 +393,16 @@
};
}
function ParticipantJoined(newSession, participant) {
client.ParticipantJoined(newSession, _toJamClientParticipant(participant));
currentParticipants[participant.client_id] = {server:participant, client: {audio_established:null}}
}
function ParticipantLeft(newSession, participant) {
client.ParticipantLeft(newSession, _toJamClientParticipant(participant));
delete currentParticipants[participant.client_id]
}
function sendClientParticipantChanges(oldSession, newSession) {
var joins = [], leaves = [], leaveJoins = []; // Will hold JamClientParticipants
@ -422,40 +434,41 @@
// if the participant is here now, and here before, there is still a chance we missed a
// very fast leave/join. So check if joined_session_at is different
if(oldParticipantIds[client_id].joined_session_at != participant.joined_session_at) {
leaveJoins.push(_toJamClientParticipant(participant))
leaveJoins.push(participant)
}
}
else {
// new participant id that's not in old participant ids: Join
joins.push(_toJamClientParticipant(participant));
joins.push(participant);
}
});
$.each(oldParticipantIds, function(client_id, participant) {
if (!(client_id in newParticipantIds)) {
// old participant id that's not in new participant ids: Leave
leaves.push(_toJamClientParticipant(participant));
leaves.push(participant);
}
});
$.each(joins, function(i,v) {
if (v.client_id != clientId) {
logger.debug("jamClient.ParticipantJoined", v.clientID)
client.ParticipantJoined(newSession, v);
logger.debug("jamClient.ParticipantJoined", v.client_id)
ParticipantJoined(newSession, v)
}
});
$.each(leaves, function(i,v) {
if (v.client_id != clientId) {
logger.debug("jamClient.ParticipantLeft", v.clientID)
client.ParticipantLeft(newSession, v);
logger.debug("jamClient.ParticipantLeft", v.client_id)
ParticipantLeft(newSession, v)
}
});
$.each(leaveJoins, function(i,v) {
if (v.client_id != clientId) {
logger.debug("participant had a rapid leave/join")
logger.debug("jamClient.ParticipantLeft", v.clientID)
client.ParticipantLeft(newSession, v);
logger.debug("jamClient.ParticipantJoined", v.clientID)
client.ParticipantJoined(newSession, v);
logger.debug("jamClient.ParticipantLeft", v.client_id)
ParticipantLeft(newSession, v)
logger.debug("jamClient.ParticipantJoined", v.client_id)
ParticipantJoined(newSession, v)
}
});
}
@ -754,6 +767,23 @@
this.getParticipant = function(clientId) {
return participantsEverSeen[clientId]
};
// call to report if the current user was able to establish audio with the specified clientID
this.setAudioEstablished = function(clientId, audioEstablished) {
var participant = currentParticipants[clientId]
if(participant) {
if(participant.client.audio_established === null) {
participant.client.audio_established = audioEstablished;
context.stats.write('client.audio_established.' + (audioEstablished ? 'success': 'failure'), {user_id: context.JK.currentUserId, name: context.JK.currentUserName, remote_user_id: participant.server.user.id, remote_name: participant.server.user.name, value: 1})
}
else if(participant.client.audio_established === false && audioEstablished === true) {
// this means the frontend had declared this audio failed, but later says it works.
// this could mean our threshold of time to wait before audio is considered failed is too low, and needs tweaking
context.stats.write('client.audio_established.delayed', {user_id: context.JK.currentUserId, name: context.JK.currentUserName, remote_user_id: participant.server.user.id, remote_name: participant.server.user.name, value: 1})
}
}
}
this.ensureEnded = function() {
updateCurrentSession(null);
}

View File

@ -53,6 +53,12 @@
return rsvpDialog.showDialog();
}
function launchSessionStartDialog(session) {
var sessionStartDialog = new JK.SessionStartDialog(JK.app, session);
sessionStartDialog.initialize();
return sessionStartDialog.showDialog();
}
this.addSessionLike = addSessionLike;
this.addRecordingLike = addRecordingLike;
this.launchCommentDialog = launchCommentDialog;
@ -60,6 +66,7 @@
this.launchRsvpSubmitDialog = launchRsvpSubmitDialog;
this.launchRsvpCancelDialog = launchRsvpCancelDialog;
this.launchRsvpCreateSlotDialog = launchRsvpCreateSlotDialog;
this.launchSessionStartDialog = launchSessionStartDialog;
return this;
};

View File

@ -127,6 +127,10 @@ table.findsession-table, table.local-recordings, table.open-jam-tracks {
vertical-align:top;
}
.musician-groups td.nowrap {
white-space: nowrap;
}
a {
color:#fff;
text-decoration:none;

View File

@ -70,8 +70,8 @@
min-height: 600px;
}
.dialog-overlay-sm {
width: 600px;
.dialog-overlay-lg {
width: 800px;
height: auto;
position: fixed;
left: 50%;
@ -90,6 +90,18 @@
color: #aaa;
}
.dialog-overlay-sm {
width: 600px;
height: auto;
position: fixed;
left: 50%;
top: 20%;
margin-left: -300px;
background-color: #333;
border: 1px solid #ed3618;
z-index: 1000;
}
.dialog-overlay-sm .dialog-inner {
width: 550px;
height: auto;

View File

@ -0,0 +1,17 @@
.session-wrapper {
padding: 10px 35px 0 0px;
white-space: initial;
h3 {
font-weight: bold;
color:#dedede;
}
> div.session {
width: 50%;
&.right {
font-size: 13px;
}
}
}

View File

@ -34,8 +34,8 @@ class ApiJamTracksController < ApiController
def download
if @jam_track_right.valid?
if (@jam_track_right && @jam_track_right.signed && @jam_track_right.url.present? &&@jam_track_right.url.file.exists?)
JamTrackRight.where(:id => @jam_track_right.id).update_all(:last_downloaded_at => Time.now)
@jam_track_right.update_download_count
@jam_track_right.update_download_count
@jam_track_right.last_downloaded_at = Time.now
@jam_track_right.save!
redirect_to @jam_track_right.sign_url
else

View File

@ -230,13 +230,13 @@ class ApiRecordingsController < ApiController
# POST /api/recordings/:id/videos/:video_id/upload_sign
def video_upload_sign
length = params[:length]
@youtube_client.upload_sign(current_user, @recorded_video.url, length)
@youtube_client.sign_youtube_upload(current_user, @recorded_video.url, length)
end
# POST /api/recordings/:id/videos/:video_id/upload_complete
def video_upload_start
length = params[:length]
@youtube_client.get_upload_status(current_user, @recorded_video.url, length)
@youtube_client.youtube_upload_status(current_user, @recorded_video.url, length)
end
# POST /api/recordings/:id/videos/:video_id/upload_complete

View File

@ -0,0 +1,18 @@
require 'sanitize'
class ApiTextMessagesController < ApiController
before_filter :api_signed_in_user
respond_to :json
def index
@text_messages = TextMessage.index(params[:target_user_id], current_user.id, {:offset => params[:offset]})
respond_with @text_messages, responder: ApiResponder, :status => 200
end
def create
@text_message = TextMessage.create(params[:message], params[:target_user_id], current_user.id)
respond_with_model(@text_message, new: true)
end
end

View File

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
require 'builder'
require 'recaptcha/rails'
class Slide
attr_accessor :img_url, :header, :vid_url
@ -142,10 +141,6 @@ class UsersController < ApplicationController
terms_of_service = params[:jam_ruby_user][:terms_of_service].nil? || params[:jam_ruby_user][:terms_of_service] == "0"? false : true
musician = params[:jam_ruby_user][:musician]
if Rails.application.config.recaptcha_enable
recaptcha_failed = verify_recaptcha(:private_key=>Rails.application.config.recaptcha_private_key, :timeout=>10)
end
@user = UserManager.new.signup(remote_ip: request.remote_ip,
first_name: params[:jam_ruby_user][:first_name],
last_name: params[:jam_ruby_user][:last_name],
@ -157,7 +152,7 @@ class UsersController < ApplicationController
birth_date: birth_date,
location: location,
musician: musician,
recaptcha_failed: recaptcha_failed,
recaptcha_response: params['g-recaptcha-response'],
invited_user: @invited_user,
fb_signup: @fb_signup,
signup_confirm_url: ApplicationHelper.base_uri(request) + "/confirm",

View File

@ -98,7 +98,9 @@ else
node do |invitation|
user_score(invitation.receiver.id).merge({
receiver_avatar_url: invitation.receiver.resolved_photo_url, audio_latency: last_jam_audio_latency(invitation.receiver)
receiver_name: invitation.receiver.name,
receiver_avatar_url: invitation.receiver.resolved_photo_url,
audio_latency: last_jam_audio_latency(invitation.receiver)
})
end
}

View File

@ -0,0 +1,11 @@
collection @text_messages
attributes :id, :source_user_id, :target_user_id, :message, :created_at
node :source_user do |msg|
attributes :id, :name
end
node :target_user do |msg|
attributes :id, :name
end

View File

@ -97,7 +97,7 @@
<%= image_tag "content/listen-icon.jpg", :size => "40x40" %>
</a>
<br/>
<span class="listen-link-details"><span class="listen-link-text">Listen</span><a href="#" class="listen-detail-hover">?</a></span>
<span class="listen-link-details"><span class="listen-link-text">{listen_link_text}</span><a href="#" class="listen-detail-hover">?</a></span>
</td>
<td>
<a class="join-link" style="display:{join_link_display_style};">
@ -196,7 +196,7 @@
<tr>
<td>
</td>
<td>
<td class="nowrap">
<span class="rsvp-msg" style="display:none;">You cannot RSVP to this session.</span>
<a class="rsvp-link">
<%= image_tag "content/rsvp-icon.jpg", :size => "40x40" %>

View File

@ -402,9 +402,9 @@
<h3 class="mt20">What instruments/parts do you need?</h3>
<div class="left ib w20 mt5">Me:</div>
<div class="left ib w75 mt5" id="session-instruments-me-disp" class="mt5"></div>
<div class="left ib w75 mt5" id="session-instruments-me-disp"></div>
<div class="clearall left ib w20">Others:</div>
<div class="left ib w75" id="session-instruments-rsvp-disp" class="mt5"></div><br clear="all">
<div class="left ib w75 mt5" id="session-instruments-rsvp-disp"></div><br clear="all">
<h3 class="mt20">What access policies are in effect?</h3>
<div id="session-musician-access-disp" class="mt5"></div>

View File

@ -4,7 +4,7 @@
<h1>Features &amp; Benefits</h1>
<p>
Musicians can enjoy four major features of the new JamKazam service, as described below. Also, several <a href="https://jamkazam.desk.com/customer/portal/articles/1304097-tutorial-videos" target="_blank">Tutorial Videos</a> are available to see how various features work in more detail.
Musicians can enjoy four major features of the new JamKazam service, as described below. Also, several <a href="https://jamkazam.desk.com/customer/portal/topics/673198-tutorials-on-major-features/articles" target="_blank">Tutorial Videos</a> are available to see how various features work in more detail.
</p>
<br/>

View File

@ -23,5 +23,5 @@
</p>
<p>
Also, several <a href="https://jamkazam.desk.com/customer/portal/articles/1304097-tutorial-videos" target="_blank">Tutorial Videos</a> are available to see how various features work in more detail.
Also, several <a href="https://jamkazam.desk.com/customer/portal/topics/673198-tutorials-on-major-features/articles" target="_blank">Tutorial Videos</a> are available to see how various features work in more detail.
</p>

View File

@ -18,6 +18,7 @@
= render 'dialogs/rsvpSubmitDialog'
= render 'dialogs/rsvpCancelDialog'
= render 'dialogs/rsvpCreateSlotDialog'
= render 'dialogs/sessionStartDialog'
= render 'dialogs/sessionCancelDialog'
= render 'dialogs/signinDialog'
= render 'dialogs/signupDialog'

View File

@ -0,0 +1,44 @@
.dialog.dialog-overlay-lg layout='dialog' layout-id='session-start-dialog' id='session-start-dialog'
.content-head
= image_tag "content/icon_add.png", {:width => 24, :height => 24, :class => 'content-icon' }
h1 Start Session
.dialog-inner
.session-wrapper
.left.w55
h3 When are you starting your session?
.mt5 id='session-start-type-disp'
h3.mb5.mt20 What are you playing?
em id='session-name-disp'
#session-description-disp.mt5
#session-notations-disp.mt5
h3.mt20 Which band is playing?
#session-band-disp.mt5
h3.mt20 What language will be spoken?
#session-language-disp.mt5
.right.w40
h3 Who is invited?
#session-invited-disp.mt5
h3.mt20 What instruments/parts do you need?
.left.ib.w20.mt5 Me:
#session-instruments-me-disp.left.ib.w75.mt5
.clearall.left.ib.w20.mt5 Others:
#session-instruments-rsvp-disp.left.ib.w75.mt5
br clear='all'
h3.mt20 What access policies are in effect?
#session-musician-access-disp.mt5
#session-fans-access-disp
h3.mt20 What legal policy is in effect?
#session-policy-disp.mt5
.clearall
br
br
.buttons
.right
a.button-grey class='btnCancel' layout-action='cancel' CANCEL
a.button-orange class='btnStartSession' START SESSION

View File

@ -295,6 +295,10 @@ SampleApp::Application.routes.draw do
match '/users/:id/friends/:friend_id' => 'api_users#friend_show', :via => :get, :as => 'api_friend_detail'
match '/users/:id/friends/:friend_id' => 'api_users#friend_destroy', :via => :delete
# text messages
match '/text_messages' => 'api_text_messages#index', :via => :get
match '/text_messages' => 'api_text_messages#create', :via => :post
# notifications
match '/users/:id/notifications' => 'api_users#notification_index', :via => :get
match '/users/:id/notifications/:notification_id' => 'api_users#notification_destroy', :via => :delete

View File

@ -9,7 +9,7 @@ require 'socket'
# Youtube OAuth and API functionality:
module JamRuby
class YouTubeClient
class GoogleClient
attr_accessor :client
attr_accessor :api
attr_accessor :request
@ -27,7 +27,7 @@ module JamRuby
:application_version => '1.0.0'
)
youtube = client.discovered_api('youtube', 'v3')
#youtube = client.discovered_api('youtube', 'v3')
end
# Return a login URL that will show a web page with
@ -52,7 +52,7 @@ module JamRuby
# https://developers.google.com/youtube/v3/docs/videos/insert
# https://developers.google.com/youtube/v3/guides/using_resumable_upload_protocol
def upload_sign(user, filename, length)
def sign_youtube_upload(user, filename, length)
raise ArgumentError, "Length is required and should be > 0" if length.to_i.zero?
# Something like this:
@ -140,7 +140,7 @@ module JamRuby
end
# https://developers.google.com/youtube/v3/guides/using_resumable_upload_protocol#Check_Upload_Status
def get_upload_status(user, upload_url, length)
def youtube_upload_status(user, upload_url, length)
auth = UserAuthorization.google_auth(user).first
if auth.nil? || auth.token.nil?
raise SecurityError, "No current google token found for user #{user}"
@ -187,21 +187,50 @@ module JamRuby
end
# @return true if file specified by URL uploaded, false otherwise
def verify_upload(user, upload_url, length)
status_hash=get_upload_status(user, upload_url, length)
def verify_youtube_upload(user, upload_url, length)
status_hash=youtube_upload_status(user, upload_url, length)
(status_hash['status']>=200 && status_hash['status']<300)
end
# Set fully_uploaded if the upload can be verified.
# @return true if verified; false otherwise:
def complete_upload(recorded_video)
if (verify_upload(recorded_video.user, recorded_video.url, recorded_video.length))
if (verify_youtube_upload(recorded_video.user, recorded_video.url, recorded_video.length))
recorded_video.update_attribute(:fully_uploaded, true)
else
false
end
end
def verify_recaptcha(recaptcha_response)
success = false
if !Rails.application.config.recaptcha_enable
success = true
else
Rails.logger.info "Login with: #{recaptcha_response}"
RestClient.get("https://www.google.com/recaptcha/api/siteverify",
params: {
secret: Rails.application.config.recaptcha_private_key,
response: recaptcha_response
}
) do |response, request, result|
Rails.logger.info "response: #{response.inspect}"
case(response.code)
when 200..207
json = JSON.parse(response.to_str)
if json['success']
success = true
else
Rails.logger.info("Error verifying recaptcha: #{json['error-codes'].inspect}")
end
else
Rails.logger.info("Unexpected status from google_recaptcha: [#{response.code}] with headers: #{response.headers.inspect}")
end #case
end #do
end # if
success
end #def
# This will also sign in and prompt for login as necessary;
# currently requires the server to be running at localhost:3000
def signin_flow()

View File

@ -1,8 +1,10 @@
require 'google_client'
class UserManager < BaseManager
def initialize(options={})
super(options)
@log = Logging.logger[self]
@google_client = GoogleClient.new()
end
# Note that almost everything can be nil here. This is because when users sign up via social media,
@ -24,7 +26,8 @@ class UserManager < BaseManager
fb_signup = options[:fb_signup]
signup_confirm_url = options[:signup_confirm_url]
affiliate_referral_id = options[:affiliate_referral_id]
recaptcha_failed = Rails.application.config.recaptcha_enable && options[:recaptcha_failed]
recaptcha_failed=(fb_signup) ? false : !@google_client.verify_recaptcha(options[:recaptcha_response])
user = User.new
# check if we have disabled open signup for this site. open == invited users can still get in

View File

@ -192,6 +192,120 @@ describe "Find Session", :js => true, :type => :feature, :capybara_feature => tr
end
end
describe "start session behavior" do
before(:each) do
@music_session = FactoryGirl.create(:music_session, creator: user)
@invited_user = FactoryGirl.create(:user)
FactoryGirl.create(:friendship, :user => @invited_user, :friend => user)
FactoryGirl.create(:friendship, :user => user, :friend => @invited_user)
@invitation = FactoryGirl.create(:invitation, :sender => user, :receiver => @invited_user, :music_session => @music_session)
@rsvp_user = FactoryGirl.create(:user)
@rsvp_slot = FactoryGirl.create(:rsvp_slot, music_session: @music_session)
@rsvp_request = FactoryGirl.create(:rsvp_request_for_slots, chosen: nil, user: @rsvp_user, slots: [@rsvp_slot])
end
it "should always show start session link for session creator" do
pending
fast_signin(user, Nav.find_session)
find("#sessions-scheduled .rsvp-msg span.text a.start", text: "Start session now?")
fast_signout
end
it "should not show start session link for anyone who is not creator, invitee, or RSVP user" do
pending
random_user = FactoryGirl.create(:user)
fast_signin(random_user, Nav.find_session)
page.should have_no_selector("#sessions-scheduled .rsvp-msg span.text a.start")
fast_signout
end
it "should show start session link for invited or RSVP users" do
pending
# make session date/time TBD
@music_session.scheduled_start = Time.now + 5.minutes
@music_session.save!
# invited user
fast_signin(@invited_user, Nav.find_session)
page.should have_selector("#sessions-scheduled .rsvp-msg span.text a.start", text: "Start session now?")
fast_signout
# RSVP user
fast_signin(@rsvp_user, Nav.find_session)
page.should have_no_selector("#sessions-scheduled .rsvp-msg span.text a.start", text: "Start session now?")
page.should have_selector("#sessions-scheduled .rsvp-msg span.text a.cancel", text: "Cancel RSVP")
fast_signout
# now approve the RSVP
@rsvp_request.rsvp_requests_rsvp_slots[0].chosen = true
@rsvp_request.rsvp_requests_rsvp_slots[0].save!
fast_signin(@rsvp_user, Nav.find_session)
page.should have_selector("#sessions-scheduled .rsvp-msg span.text a.start", text: "Start session now?")
page.should have_selector("#sessions-scheduled .rsvp-msg span.text a.cancel", text: "Cancel RSVP")
fast_signout
end
it "should not show start session link for invited or RSVP users when date/time is TBD" do
pending
# make session date/time TBD
@music_session.scheduled_start = nil
@music_session.save!
# invited user
fast_signin(@invited_user, Nav.find_session)
page.should have_no_selector("#sessions-scheduled .rsvp-msg span.text a.start", text: "Start session now?")
fast_signout
# RSVP user
fast_signin(@rsvp_user, Nav.find_session)
page.should have_no_selector("#sessions-scheduled .rsvp-msg span.text a.start", text: "Start session now?")
page.should have_selector("#sessions-scheduled .rsvp-msg span.text a.cancel", text: "Cancel RSVP")
fast_signout
# now approve the RSVP
@rsvp_request.rsvp_requests_rsvp_slots[0].chosen = true
@rsvp_request.rsvp_requests_rsvp_slots[0].save!
# "start session" should still be hidden
fast_signin(@rsvp_user, Nav.find_session)
page.should have_no_selector("#sessions-scheduled .rsvp-msg span.text a.start", text: "Start session now?")
page.should have_selector("#sessions-scheduled .rsvp-msg span.text a.cancel", text: "Cancel RSVP")
fast_signout
end
it "should not show start session link for invited or RSVP users when more than 15 minutes remain to start time" do
pending
# make session date/time more than 15 min away
@music_session.scheduled_start = Time.now + 60.minutes
@music_session.save!
# invited user
fast_signin(@invited_user, Nav.find_session)
page.should have_no_selector("#sessions-scheduled .rsvp-msg span.text a.start", text: "Start session now?")
fast_signout
# RSVP user
fast_signin(@rsvp_user, Nav.find_session)
page.should have_no_selector("#sessions-scheduled .rsvp-msg span.text a.start", text: "Start session now?")
page.should have_selector("#sessions-scheduled .rsvp-msg span.text a.cancel", text: "Cancel RSVP")
fast_signout
# now approve the RSVP
@rsvp_request.rsvp_requests_rsvp_slots[0].chosen = true
@rsvp_request.rsvp_requests_rsvp_slots[0].save!
# "start session" should still be hidden
fast_signin(@rsvp_user, Nav.find_session)
page.should have_no_selector("#sessions-scheduled .rsvp-msg span.text a.start", text: "Start session now?")
page.should have_selector("#sessions-scheduled .rsvp-msg span.text a.cancel", text: "Cancel RSVP")
fast_signout
end
end
describe "rsvp behavior" do
before(:each) do
stub_const("APP_CONFIG", web_config)
@ -222,10 +336,9 @@ describe "Find Session", :js => true, :type => :feature, :capybara_feature => tr
it "RSVP text shows correctly" do
music_session = FactoryGirl.create(:music_session, creator: user)
# session creator cannot cancel
fast_signin(user, Nav.find_session)
find("#sessions-scheduled .rsvp-msg span.text", text: "You have been confirmed for this session. ")
page.should have_no_selector("#sessions-scheduled .rsvp-msg span.text a.cancel", text: "Cancel RSVP")
sign_out
# create a slot so the session can be joined
@ -241,15 +354,15 @@ describe "Find Session", :js => true, :type => :feature, :capybara_feature => tr
# first state: an unconfirmed RSVP
go_to_root
fast_signin(finder, Nav.find_session)
find("#sessions-scheduled .rsvp-msg span.text", text: "You have RSVP'ed to this session. ")
find("#sessions-scheduled .rsvp-msg span.text a.cancel", text: "Cancel RSVP")
rsvp_request.rsvp_requests_rsvp_slots[0].chosen = true
rsvp_request.rsvp_requests_rsvp_slots[0].save!
# second state: a connfirmed RSVP
# second state: a confirmed RSVP
go_to_root
fast_signin(finder, Nav.find_session)
find("#sessions-scheduled .rsvp-msg span.text", text: "You have been confirmed for this session. ")
find("#sessions-scheduled a.cancel", text: "Cancel RSVP")
# need to now CANCEL, and check what it says: // VRFS-1891

View File

@ -1,5 +1,5 @@
require 'spec_helper'
require 'you_tube_client'
require 'google_client'
describe "OAuth", :slow=>true, :js=>true, :type=>:feature, :capybara_feature=>true do
@ -15,7 +15,7 @@ describe "OAuth", :slow=>true, :js=>true, :type=>:feature, :capybara_feature=>tr
end
before(:each) do
@youtube_client = YouTubeClient.new()
@youtube_client = GoogleClient.new()
end
after(:each) do

View File

@ -1,5 +1,5 @@
require 'spec_helper'
require 'you_tube_client'
require 'google_client'
require 'rest_client'
describe "YouTube", :slow=>true, :js=>true, :type => :feature, :capybara_feature => true do
@ -12,7 +12,7 @@ describe "YouTube", :slow=>true, :js=>true, :type => :feature, :capybara_feature
@previous_run_server = Capybara.run_server
Capybara.run_server = false
@user=FactoryGirl.create(:user, :email => "jamkazamtest@gmail.com")
@youtube_client = YouTubeClient.new()
@youtube_client = GoogleClient.new()
authorize_google_user(@youtube_client, @user, "stinkyblueberryjam")
google_auth = UserAuthorization.google_auth(@user).first # Consider returning this from above now that it is reliable
end
@ -25,7 +25,7 @@ describe "YouTube", :slow=>true, :js=>true, :type => :feature, :capybara_feature
it "should retrieve upload url" do
length = 3276
upload_hash=@youtube_client.upload_sign(@user, "test_video.mp4", length)
upload_hash=@youtube_client.sign_youtube_upload(@user, "test_video.mp4", length)
upload_hash.should_not be_nil
upload_hash.length.should be >=1
upload_hash['method'].should eq("PUT")
@ -35,13 +35,13 @@ describe "YouTube", :slow=>true, :js=>true, :type => :feature, :capybara_feature
upload_hash['Content-Length'].should eq(length)
upload_hash['Content-Type'].should_not be_nil
@youtube_client.verify_upload(@user, upload_hash['url'], length).should be_false
@youtube_client.verify_youtube_upload(@user, upload_hash['url'], length).should be_false
end
it "upload url should allow uploading" do
vid_path = Rails.root.join('spec', 'files', 'test_video.mp4')
length = File.size?(vid_path)
upload_hash=@youtube_client.upload_sign(@user, "test_video.mp4", length)
upload_hash=@youtube_client.sign_youtube_upload(@user, "test_video.mp4", length)
#puts upload_hash.inspect
upload_hash.should_not be_nil
upload_hash.length.should be >=1
@ -54,8 +54,8 @@ describe "YouTube", :slow=>true, :js=>true, :type => :feature, :capybara_feature
# Upload this file as the client would:
RestClient.put(upload_hash['url'], File.read(vid_path))
@youtube_client.verify_upload(@user, upload_hash['url'], length).should be_true
#@youtube_client.get_upload_status(@user, upload_hash['url'], length)
@youtube_client.verify_youtube_upload(@user, upload_hash['url'], length).should be_true
#@youtube_client.youtube_upload_status(@user, upload_hash['url'], length)
end
it "sets upload flag when complete" do
@ -67,7 +67,7 @@ describe "YouTube", :slow=>true, :js=>true, :type => :feature, :capybara_feature
vid_path = Rails.root.join('spec', 'files', 'test_video.mp4')
length = File.size?(vid_path)
upload_hash=@youtube_client.upload_sign(@user, "test_video.mp4", length)
upload_hash=@youtube_client.sign_youtube_upload(@user, "test_video.mp4", length)
upload_hash.should_not be_nil
upload_hash['url'].should_not be_nil
RestClient.put(upload_hash['url'], File.read(vid_path))
@ -82,7 +82,7 @@ describe "YouTube", :slow=>true, :js=>true, :type => :feature, :capybara_feature
@recording.recorded_videos << recorded_video
@youtube_client.verify_upload(@user, upload_hash['url'], length).should be_true
@youtube_client.verify_youtube_upload(@user, upload_hash['url'], length).should be_true
@youtube_client.complete_upload(recorded_video).should be_true
recorded_video.fully_uploaded.should be_true
end

View File

@ -4,6 +4,7 @@ require 'spec_helper'
describe UserManager do
before(:each) do
Rails.application.config.recaptcha_enable=false
@user_manager = UserManager.new(:conn => @conn)
UserMailer.deliveries.clear
@location = { :country => "US", :state => "Arkansas", :city => "Little Rock" }
@ -662,12 +663,13 @@ describe UserManager do
end # describe "without nocaptcha"
describe "with nocaptcha" do
before(:all) do
before(:each) do
@old_recaptcha=Rails.application.config.recaptcha_enable
Rails.application.config.recaptcha_enable=true
UserMailer.deliveries.clear
end
after(:all) do
after(:each) do
Rails.application.config.recaptcha_enable=@old_recaptcha
end
@ -682,11 +684,14 @@ describe UserManager do
instruments: @instruments,
musician: true,
location: @loca,
recaptcha_failed: true,
recaptcha_response: nil,
signup_confirm_url: "http://localhost:3000/confirm")
user.errors.any?.should be_true
UserMailer.deliveries.should have(0).items
end
it "passes when facebook signup" do
user = @user_manager.signup(remote_ip: "1.2.3.4",
first_name: "bob",
last_name: "smith",
@ -694,13 +699,14 @@ describe UserManager do
password: "foobar",
password_confirmation: "foobar",
terms_of_service: true,
recaptcha_failed: false,
fb_signup: FactoryGirl.create(:facebook_signup),
recaptcha_response: nil,
instruments: @instruments,
musician: true,
location: @loca,
signup_confirm_url: "http://localhost:3000/confirm")
user.errors.any?.should be_false
end # it "fails when nocaptcha fails"
end # it "passes when facebook signup"
end # describe "with nocaptcha"
end # test

View File

@ -4,7 +4,7 @@ To obtain an access token, one must actually log into google using a browser run
Getting an access token for the purposes of automated testing is tricky, but possible using Capybara with a javascript-enabled driver. (Note, web/spec/support/utilities.rb utilizes the JK youtube client to perform the intricate bits):
1) Obtain the login URL. It's ugly, but we can get it from the YouTubeClient. It contains the callback URL, as well as a "hint" that will fill in the username for us.
1) Obtain the login URL. It's ugly, but we can get it from the GoogleClient. It contains the callback URL, as well as a "hint" that will fill in the username for us.
2) Start a web server on an enabled callback server, such as localhost:3000
3) Obtain the URL using a known test user
4) Visit the URL in a capybara test
@ -12,7 +12,7 @@ Getting an access token for the purposes of automated testing is tricky, but pos
4b) Click the login button
4c) The approve page should load. Wait for the approve button to be enabled. This is usually a second or two after the page loads, but not immediately.
4d) Click the approve button
5) After google approves, some javascript will redirect to our test web server, which contains a code. This is not the access_token, but a one-time code that can be exchanged for an access_token, again POSTing to google's auth server. You can see it in gory detail in YouTubeClient.exchange_for_token.
5) After google approves, some javascript will redirect to our test web server, which contains a code. This is not the access_token, but a one-time code that can be exchanged for an access_token, again POSTing to google's auth server. You can see it in gory detail in GoogleClient.exchange_for_token.
6) If all goes well, the test web server will call back the invoker with a real access token.
7) For testing purposes, stick the access token in the user.user_authorizations table for the user for which we are testing.

View File

@ -804,7 +804,7 @@ module JamWebsockets
id = subscribe.id
type = subscribe.type
if id && id.length > 0 && type && type.length > 0
register_subscription(client, type, id)
#register_subscription(client, type, id)
else
@log.error("handle_subscribe: empty data #{subscribe}")
end
@ -814,7 +814,7 @@ module JamWebsockets
id = unsubscribe.id
type = unsubscribe.type
if id && id.length > 0 && type && type.length > 0
unregister_subscription(client, type, id)
#unregister_subscription(client, type, id)
else
@log.error("handle_subscribe: empty data #{unsubscribe}")
end