+ <% end %>
+
+ <%= f.actions %>
+ <% end %>
+<% end %>
\ No newline at end of file
diff --git a/admin/app/views/admin/test_drive_packages/_test_drive_package_teacher_fields.html.slim b/admin/app/views/admin/test_drive_packages/_test_drive_package_teacher_fields.html.slim
new file mode 100644
index 000000000..fdb47b3ba
--- /dev/null
+++ b/admin/app/views/admin/test_drive_packages/_test_drive_package_teacher_fields.html.slim
@@ -0,0 +1,8 @@
+= f.inputs name: 'Teachers' do
+
+ ol.nested-fields
+ //= f.input :test_drive_package, :required=>true, value: @test_drive_package, include_blank: true
+ = f.input :user, :required=>true, collection: User.where(is_a_teacher: true, phantom: false), include_blank: true
+ = f.input :short_bio_temp
+
+ = link_to_remove_association "Delete Teacher", f, class: 'button', style: 'margin-left:10px'
diff --git a/admin/app/views/email/dump_emailables.csv.erb b/admin/app/views/email/dump_emailables.csv.erb
index efa44b0f8..f226e68fa 100644
--- a/admin/app/views/email/dump_emailables.csv.erb
+++ b/admin/app/views/email/dump_emailables.csv.erb
@@ -1,2 +1,2 @@
<%- headers = ['email', 'name', 'unsubscribe_token'] -%>
-<%= CSV.generate_line headers %><%- @users.each do |user| -%><%= CSV.generate_line([user.email, user.first_name, user.unsubscribe_token]) %><%- end -%>
\ No newline at end of file
+<%= CSV.generate_line headers %><%- @users.each do |user| -%><%= CSV.generate_line([user.email, user.anonymous? ? '-' : user.first_name, user.unsubscribe_token]) %><%- end -%>
\ No newline at end of file
diff --git a/admin/app/views/jam_track/dump_top_selling.html.erb b/admin/app/views/jam_track/dump_top_selling.html.erb
new file mode 100644
index 000000000..a8d59eab0
--- /dev/null
+++ b/admin/app/views/jam_track/dump_top_selling.html.erb
@@ -0,0 +1,2 @@
+<%- headers = ['Artist Name', 'Song Name', 'ID', 'Count', 'Has Count-in'] -%>
+<%= CSV.generate_line headers %><%- @jam_tracks.each do |jam_track| -%><%= CSV.generate_line([jam_track.original_artist, jam_track.name, jam_track.id, jam_track['count'], jam_track['has_tap_in']]) %><%- end -%>
\ No newline at end of file
diff --git a/admin/config/application.rb b/admin/config/application.rb
index 8b349a913..04ac0ef94 100644
--- a/admin/config/application.rb
+++ b/admin/config/application.rb
@@ -111,6 +111,7 @@ module JamAdmin
config.redis_host = "localhost:6379"
+ config.email_social_alias = 'social@jamkazam.com'
config.email_alerts_alias = 'alerts@jamkazam.com' # should be used for 'oh no' server down/service down sorts of emails
config.email_generic_from = 'nobody@jamkazam.com'
config.email_smtp_address = 'smtp.sendgrid.net'
@@ -154,5 +155,13 @@ module JamAdmin
config.jmep_dir = ENV['JMEP_DIR'] || File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "jmep"))
config.email_dump_code = 'rcAUyC3TZCbgGx4YQpznBRbNnQMXW5iKTzf9NSBfzMLsnw9dRQ'
+ config.data_dump_code = 'rcAUyC3TZCbgGx4Y3321eudbNnQMXW5iKTzf9NSBfzMLsnw9dRQ'
+
+ config.admin_port = ENV['ADMIN_PORT'] || 3333
+ config.admin_root_url = "#{config.external_protocol}#{config.external_hostname}#{(config.admin_port == 80 || config.admin_port == 443) ? '' : ':' + config.admin_port.to_s}"
+
+ config.download_tracker_day_range = 30
+ config.max_user_ip_address = 10
+ config.max_multiple_users_same_ip = 2
end
end
diff --git a/admin/config/environments/development.rb b/admin/config/environments/development.rb
index 59bfa2e8a..7950d3194 100644
--- a/admin/config/environments/development.rb
+++ b/admin/config/environments/development.rb
@@ -43,4 +43,5 @@ JamAdmin::Application.configure do
config.email_generic_from = 'nobody-dev@jamkazam.com'
config.email_alerts_alias = 'alerts-dev@jamkazam.com'
+ config.email_social_alias = 'social-dev@jamkazam.com'
end
diff --git a/admin/config/initializers/gift_cards.rb b/admin/config/initializers/gift_cards.rb
new file mode 100644
index 000000000..8c967ccb4
--- /dev/null
+++ b/admin/config/initializers/gift_cards.rb
@@ -0,0 +1,9 @@
+class JamRuby::GiftCard
+
+ attr_accessor :csv
+
+
+ def process_csv
+
+ end
+end
diff --git a/admin/config/initializers/jam_ruby_teacher.rb b/admin/config/initializers/jam_ruby_teacher.rb
new file mode 100644
index 000000000..f855ae238
--- /dev/null
+++ b/admin/config/initializers/jam_ruby_teacher.rb
@@ -0,0 +1,5 @@
+ class JamRuby::Teacher
+
+ attr_accessible :short_bio, as: :admin
+
+end
diff --git a/admin/config/initializers/jam_ruby_user.rb b/admin/config/initializers/jam_ruby_user.rb
index 57435d83f..5effb5420 100644
--- a/admin/config/initializers/jam_ruby_user.rb
+++ b/admin/config/initializers/jam_ruby_user.rb
@@ -1,6 +1,8 @@
class JamRuby::User
- attr_accessible :admin, :raw_password, :musician, :can_invite, :photo_url, :session_settings, :confirm_url, :email_template # :invite_email
+ attr_accessible :admin, :raw_password, :musician, :can_invite, :photo_url, :session_settings, :confirm_url, :teacher_attributes, :email_template # :invite_email
+
+ accepts_nested_attributes_for :teacher, allow_destroy: true
def raw_password
''
diff --git a/admin/config/initializers/jam_tracks.rb b/admin/config/initializers/jam_tracks.rb
index 33efd995c..60537c467 100644
--- a/admin/config/initializers/jam_tracks.rb
+++ b/admin/config/initializers/jam_tracks.rb
@@ -2,28 +2,5 @@ class JamRuby::JamTrack
# add a custom validation
- attr_accessor :preview_generate_error
- before_save :jmep_json_generate
- validate :jmep_text_validate
-
- def jmep_text_validate
- begin
- JmepManager.execute(self.jmep_text)
- rescue ArgumentError => err
- errors.add(:jmep_text, err.to_s)
- end
- end
-
- def jmep_json_generate
- self.licensor_id = nil if self.licensor_id == ''
- self.jmep_json = nil if self.jmep_json == ''
- self.time_signature = nil if self.time_signature == ''
-
- begin
- self[:jmep_json] = JmepManager.execute(self.jmep_text)
- rescue ArgumentError => err
- #errors.add(:jmep_text, err.to_s)
- end
- end
end
diff --git a/admin/config/routes.rb b/admin/config/routes.rb
index 2bbf1b3fa..7c671b018 100644
--- a/admin/config/routes.rb
+++ b/admin/config/routes.rb
@@ -30,6 +30,8 @@ JamAdmin::Application.routes.draw do
match '/api/checks/latency_tester' => 'checks#check_latency_tester', :via => :get
match '/api/users/emailables/:code' => 'email#dump_emailables', :via => :get
+ match '/api/teachers/:code' => 'email#dump_teachers', :via => :get
+ match '/jam_tracks/top/:code' => 'jam_track#dump_top_selling', :via => :get
match '/api/jam_tracks/released' => 'jam_track#dump_released', :via => :get, as: 'released_jamtracks_csv'
mount Resque::Server.new, :at => "/resque"
diff --git a/db/Gemfile b/db/Gemfile
index 79ec5bca4..1908dd61f 100644
--- a/db/Gemfile
+++ b/db/Gemfile
@@ -3,4 +3,4 @@ source 'http://rubygems.org'
# Assumes you have already cloned pg_migrate_ruby in your workspace
# $ cd [workspace]
# $ git clone https://github.com/sethcall/pg_migrate_ruby
-gem 'pg_migrate', '0.1.13', :source => 'http://rubygems.org/'
+gem 'pg_migrate', '0.1.14', :source => 'http://rubygems.org/'
diff --git a/db/Gemfile.lock b/db/Gemfile.lock
index 88080fb81..649f3aaaf 100644
--- a/db/Gemfile.lock
+++ b/db/Gemfile.lock
@@ -1,21 +1,21 @@
GEM
remote: http://rubygems.org/
specs:
- little-plugger (1.1.3)
+ little-plugger (1.1.4)
logging (1.7.2)
little-plugger (>= 1.1.3)
pg (0.17.1)
- pg_migrate (0.1.13)
+ pg_migrate (0.1.14)
logging (= 1.7.2)
pg (= 0.17.1)
thor
- thor (0.18.1)
+ thor (0.19.1)
PLATFORMS
ruby
DEPENDENCIES
- pg_migrate (= 0.1.13)!
+ pg_migrate (= 0.1.14)!
BUNDLED WITH
- 1.10.5
+ 1.11.2
diff --git a/db/manifest b/db/manifest
index 9f84ea0a2..0130037cb 100755
--- a/db/manifest
+++ b/db/manifest
@@ -303,4 +303,61 @@ jam_track_name_drop_unique.sql
jam_track_searchability.sql
harry_fox_agency.sql
jam_track_slug.sql
-rails4_migration.sql
\ No newline at end of file
+mixdown.sql
+aac_master.sql
+video_recording.sql
+web_playable_jamtracks.sql
+affiliate_partner_rate.sql
+track_downloads.sql
+jam_track_lang_idx.sql
+giftcard.sql
+add_description_to_crash_dumps.sql
+acappella.sql
+purchasable_gift_cards.sql
+versionable_jamtracks.sql
+session_controller.sql
+jam_tracks_bpm.sql
+jam_track_sessions.sql
+jam_track_sessions_v2.sql
+email_screening.sql
+bounced_email_cleanup.sql
+news.sql
+profile_teacher.sql
+populate_languages.sql
+populate_subjects.sql
+reviews.sql
+download_tracker_fingerprints.sql
+connection_active.sql
+chat_channel.sql
+jamblaster.sql
+test_drive_lessons.sql
+whitelist.sql
+teacher_student_flags.sql
+add_sale_source_col.sql
+jamblaster_v2.sql
+acapella_rename.sql
+jamblaster_pairing_active.sql
+email_blacklist.sql
+jamblaster_connection.sql
+teacher_progression.sql
+teacher_complete.sql
+lessons.sql
+lessons_unread_messages.sql
+track_school_signups.sql
+add_test_drive_types.sql
+updated_subjects.sql
+update_payment_history.sql
+lesson_booking_schools.sql
+lesson_booking_schools_2.sql
+phantom_accounts.sql
+lesson_booking_success.sql
+user_origin.sql
+remove_stripe_acct_id.sql
+track_user_on_lesson.sql
+audio_in_music_notations.sql
+lesson_time_tracking.sql
+packaged_test_drive.sql
+packaged_test_drive2.sql
+jamclass_report.sql
+jamblasters_network.sql
+rails4_migration.sql
diff --git a/db/up/aac_master.sql b/db/up/aac_master.sql
new file mode 100644
index 000000000..28f67f81d
--- /dev/null
+++ b/db/up/aac_master.sql
@@ -0,0 +1,3 @@
+ALTER TABLE jam_track_tracks ADD COLUMN preview_aac_url VARCHAR;
+ALTER TABLE jam_track_tracks ADD COLUMN preview_aac_md5 VARCHAR;
+ALTER TABLE jam_track_tracks ADD COLUMN preview_aac_length bigint;
\ No newline at end of file
diff --git a/db/up/acapella_rename.sql b/db/up/acapella_rename.sql
new file mode 100644
index 000000000..df474501f
--- /dev/null
+++ b/db/up/acapella_rename.sql
@@ -0,0 +1,2 @@
+UPDATE genres set description = 'A Cappella' where id = 'acapella';
+
diff --git a/db/up/acappella.sql b/db/up/acappella.sql
new file mode 100644
index 000000000..cf17e052d
--- /dev/null
+++ b/db/up/acappella.sql
@@ -0,0 +1,2 @@
+INSERT INTO genres (id, description) values ('acapella', 'A Capella');
+ALTER TABLE jam_track_licensors ADD COLUMN slug VARCHAR UNIQUE;
diff --git a/db/up/add_description_to_crash_dumps.sql b/db/up/add_description_to_crash_dumps.sql
new file mode 100644
index 000000000..e08540d85
--- /dev/null
+++ b/db/up/add_description_to_crash_dumps.sql
@@ -0,0 +1 @@
+ALTER TABLE crash_dumps ADD COLUMN description VARCHAR(20000);
\ No newline at end of file
diff --git a/db/up/add_sale_source_col.sql b/db/up/add_sale_source_col.sql
new file mode 100644
index 000000000..5b1607e97
--- /dev/null
+++ b/db/up/add_sale_source_col.sql
@@ -0,0 +1 @@
+ALTER TABLE sales ADD COLUMN source VARCHAR NOT NULL DEFAULT 'recurly';
diff --git a/db/up/add_test_drive_types.sql b/db/up/add_test_drive_types.sql
new file mode 100644
index 000000000..01584db3d
--- /dev/null
+++ b/db/up/add_test_drive_types.sql
@@ -0,0 +1,4 @@
+INSERT INTO lesson_package_types (id, name, description, package_type, price) VALUES ('test-drive-2', 'Test Drive (2)', 'Two reduced-price lessons which you can use to find that ideal teacher.', 'test-drive-2', 29.99);
+INSERT INTO lesson_package_types (id, name, description, package_type, price) VALUES ('test-drive-1', 'Test Drive (1)', 'One reduced-price lessons which you can use to find that ideal teacher.', 'test-drive-1', 15.99);
+UPDATE lesson_package_types set name = 'Test Drive (4)', package_type = 'test-drive-4' WHERE id = 'test-drive';
+ALTER TABLE users ADD COLUMN lesson_package_type_id VARCHAR(64) REFERENCES lesson_package_types(id);
\ No newline at end of file
diff --git a/db/up/admin_users.sql b/db/up/admin_users.sql
index 8933ceaeb..93a12d91a 100644
--- a/db/up/admin_users.sql
+++ b/db/up/admin_users.sql
@@ -15,7 +15,6 @@ CREATE TABLE admin_users (
updated_at timestamp without time zone NOT NULL
);
-ALTER TABLE public.admin_users OWNER TO postgres;
CREATE SEQUENCE admin_users_id_seq
START WITH 1
INCREMENT BY 1
@@ -23,7 +22,6 @@ CREATE SEQUENCE admin_users_id_seq
NO MAXVALUE
CACHE 1;
-ALTER TABLE public.admin_users_id_seq OWNER TO postgres;
ALTER SEQUENCE admin_users_id_seq OWNED BY admin_users.id;
SELECT pg_catalog.setval('admin_users_id_seq', 2, true);
ALTER TABLE ONLY admin_users ALTER COLUMN id SET DEFAULT nextval('admin_users_id_seq'::regclass);
@@ -45,7 +43,6 @@ CREATE TABLE active_admin_comments (
updated_at timestamp without time zone NOT NULL,
namespace character varying(255)
);
-ALTER TABLE public.active_admin_comments OWNER TO postgres;
CREATE SEQUENCE active_admin_comments_id_seq
START WITH 1
INCREMENT BY 1
@@ -53,7 +50,6 @@ CREATE SEQUENCE active_admin_comments_id_seq
NO MAXVALUE
CACHE 1;
-ALTER TABLE public.active_admin_comments_id_seq OWNER TO postgres;
ALTER SEQUENCE active_admin_comments_id_seq OWNED BY active_admin_comments.id;
SELECT pg_catalog.setval('active_admin_comments_id_seq', 1, false);
ALTER TABLE ONLY active_admin_comments ALTER COLUMN id SET DEFAULT nextval('active_admin_comments_id_seq'::regclass);
diff --git a/db/up/affiliate_partner_rate.sql b/db/up/affiliate_partner_rate.sql
new file mode 100644
index 000000000..d36c80c81
--- /dev/null
+++ b/db/up/affiliate_partner_rate.sql
@@ -0,0 +1 @@
+ALTER TABLE affiliate_partners ADD COLUMN rate NUMERIC(8,2) DEFAULT 0.10;
\ 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/db/up/bounced_email_cleanup.sql b/db/up/bounced_email_cleanup.sql
new file mode 100644
index 000000000..0f18b688a
--- /dev/null
+++ b/db/up/bounced_email_cleanup.sql
@@ -0,0 +1,3 @@
+ALTER TABLE generic_state ADD COLUMN bounce_check_at DATE;
+UPDATE generic_state SET bounce_check_at = NOW();
+ALTER TABLE users ADD COLUMN bounced BOOLEAN DEFAULT FALSE;
\ No newline at end of file
diff --git a/db/up/chat_channel.sql b/db/up/chat_channel.sql
new file mode 100644
index 000000000..ea0cbba75
--- /dev/null
+++ b/db/up/chat_channel.sql
@@ -0,0 +1,4 @@
+ALTER TABLE chat_messages ADD COLUMN channel VARCHAR(128) NOT NULL DEFAULT 'session';
+CREATE INDEX chat_messages_idx_channels ON chat_messages(channel);
+CREATE INDEX chat_messages_idx_created_at ON chat_messages(created_at);
+CREATE INDEX chat_messages_idx_music_session_id ON chat_messages(music_session_id);
\ No newline at end of file
diff --git a/db/up/connection_active.sql b/db/up/connection_active.sql
new file mode 100644
index 000000000..085021aaf
--- /dev/null
+++ b/db/up/connection_active.sql
@@ -0,0 +1 @@
+ALTER TABLE connections ADD COLUMN user_active BOOLEAN DEFAULT TRUE;
\ No newline at end of file
diff --git a/db/up/crash_dumps_2.sql b/db/up/crash_dumps_2.sql
new file mode 100644
index 000000000..96a5374b1
--- /dev/null
+++ b/db/up/crash_dumps_2.sql
@@ -0,0 +1,6 @@
+ALTER TABLE crash_dumps ADD COLUMN email VARCHAR(255);
+ALTER TABLE crash_dumps ADD COLUMN description VARCHAR(10000);
+ALTER TABLE crash_dumps ADD COLUMN os VARCHAR(100);
+ALTER TABLE crash_dumps ADD COLUMN os_version VARCHAR(100);
+ALTER TABLE crash_dumps DROP CONSTRAINT crash_dumps_user_id_fkey;
+
diff --git a/db/up/download_tracker_fingerprints.sql b/db/up/download_tracker_fingerprints.sql
new file mode 100644
index 000000000..1163cfff2
--- /dev/null
+++ b/db/up/download_tracker_fingerprints.sql
@@ -0,0 +1,3 @@
+ALTER TABLE download_trackers ADD COLUMN fingerprint VARCHAR(1000);
+CREATE INDEX index_download_trackers_on_fingerprint ON download_trackers USING btree (fingerprint);
+ALTER TABLE download_trackers ADD COLUMN is_client BOOLEAN DEFAULT FALSE;
\ No newline at end of file
diff --git a/db/up/email_blacklist.sql b/db/up/email_blacklist.sql
new file mode 100644
index 000000000..d1b7c30f0
--- /dev/null
+++ b/db/up/email_blacklist.sql
@@ -0,0 +1,10 @@
+CREATE TABLE email_blacklists (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL,
+ email VARCHAR(1000) UNIQUE NOT NULL,
+ source VARCHAR(1000),
+ notes VARCHAR(1000),
+ created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL,
+ updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL
+);
+
+ALTER TABLE jamblasters DROP COLUMN vtoken;
\ No newline at end of file
diff --git a/db/up/email_screening.sql b/db/up/email_screening.sql
new file mode 100644
index 000000000..7f64c0123
--- /dev/null
+++ b/db/up/email_screening.sql
@@ -0,0 +1,2 @@
+ALTER TABLE users ADD COLUMN email_needs_verification BOOLEAN DEFAULT FALSE;
+ALTER TABLE users ADD COLUMN kickbox_response JSON;
\ No newline at end of file
diff --git a/db/up/giftcard.sql b/db/up/giftcard.sql
new file mode 100644
index 000000000..eae33c13e
--- /dev/null
+++ b/db/up/giftcard.sql
@@ -0,0 +1,13 @@
+CREATE TABLE gift_cards (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ code VARCHAR(64) UNIQUE NOT NULL,
+ user_id VARCHAR (64) REFERENCES users(id) ON DELETE CASCADE,
+ card_type VARCHAR(64) NOT NULL,
+ origin VARCHAR(200),
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE INDEX gift_card_user_id_idx ON gift_cards(user_id);
+
+ALTER TABLE users ADD COLUMN gifted_jamtracks INTEGER DEFAULT 0;
diff --git a/db/up/jam_track_lang_idx.sql b/db/up/jam_track_lang_idx.sql
new file mode 100644
index 000000000..aa5c84c26
--- /dev/null
+++ b/db/up/jam_track_lang_idx.sql
@@ -0,0 +1 @@
+CREATE INDEX ON jam_tracks(language);
diff --git a/db/up/jam_track_sessions.sql b/db/up/jam_track_sessions.sql
new file mode 100644
index 000000000..7936b5031
--- /dev/null
+++ b/db/up/jam_track_sessions.sql
@@ -0,0 +1,9 @@
+CREATE TABLE jam_track_sessions (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ jam_track_id VARCHAR(64) NOT NULL REFERENCES jam_tracks(id) ON DELETE CASCADE,
+ session_type VARCHAR(10) NOT NULL,
+ music_session_id VARCHAR(64) REFERENCES music_sessions(id) ON DELETE SET NULL,
+ user_id VARCHAR(64) NOT NULL REFERENCES users(id),
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
\ No newline at end of file
diff --git a/db/up/jam_track_sessions_v2.sql b/db/up/jam_track_sessions_v2.sql
new file mode 100644
index 000000000..cf1c6dab6
--- /dev/null
+++ b/db/up/jam_track_sessions_v2.sql
@@ -0,0 +1,2 @@
+ALTER TABLE jam_track_sessions ALTER COLUMN user_id DROP NOT NULL;
+ALTER TABLE crash_dumps ALTER COLUMN user_id DROP NOT NULL;
\ No newline at end of file
diff --git a/db/up/jam_tracks_bpm.sql b/db/up/jam_tracks_bpm.sql
new file mode 100644
index 000000000..0fce5154d
--- /dev/null
+++ b/db/up/jam_tracks_bpm.sql
@@ -0,0 +1,2 @@
+ALTER TABLE jam_tracks ADD COLUMN bpm numeric(8,3);
+INSERT INTO instruments (id, description) VALUES ('percussion', 'Percussion');
\ No newline at end of file
diff --git a/db/up/jamblaster.sql b/db/up/jamblaster.sql
new file mode 100644
index 000000000..56760576a
--- /dev/null
+++ b/db/up/jamblaster.sql
@@ -0,0 +1,28 @@
+CREATE TABLE jamblasters (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL,
+ user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE SET NULL,
+ serial_no VARCHAR(1000) UNIQUE,
+ vtoken VARCHAR(1000) UNIQUE,
+ client_id VARCHAR(64) UNIQUE,
+ created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL,
+ updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL
+);
+
+CREATE TABLE jamblasters_users (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL,
+ user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ jamblaster_id VARCHAR(64) NOT NULL REFERENCES jamblasters(id) ON DELETE CASCADE,
+ created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL,
+ updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL
+);
+
+CREATE TABLE jamblaster_pairing_requests (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL,
+ user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ jamblaster_id VARCHAR(64) NOT NULL REFERENCES jamblasters(id) ON DELETE CASCADE,
+ jamblaster_client_id VARCHAR(64) NOT NULL,
+ sibling_client_id VARCHAR(64) NOT NULL,
+ sibling_key VARCHAR(1000) NOT NULL,
+ created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL,
+ updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL
+);
diff --git a/db/up/jamblaster_connection.sql b/db/up/jamblaster_connection.sql
new file mode 100644
index 000000000..3597af2fd
--- /dev/null
+++ b/db/up/jamblaster_connection.sql
@@ -0,0 +1 @@
+ALTER TABLE connections ADD COLUMN is_jamblaster BOOLEAN DEFAULT FALSE;
\ No newline at end of file
diff --git a/db/up/jamblaster_pairing_active.sql b/db/up/jamblaster_pairing_active.sql
new file mode 100644
index 000000000..057fd895d
--- /dev/null
+++ b/db/up/jamblaster_pairing_active.sql
@@ -0,0 +1 @@
+ALTER TABLE jamblaster_pairing_requests ADD COLUMN active BOOLEAN NOT NULL DEFAULT FALSE;
\ No newline at end of file
diff --git a/db/up/jamblaster_v2.sql b/db/up/jamblaster_v2.sql
new file mode 100644
index 000000000..6f29d461a
--- /dev/null
+++ b/db/up/jamblaster_v2.sql
@@ -0,0 +1,3 @@
+ALTER TABLE jamblaster_pairing_requests ALTER COLUMN sibling_key DROP NOT NULL;
+ALTER TABLE jamblaster_pairing_requests ADD COLUMN vtoken VARCHAR(400) NOT NULL;
+ALTER TABLE jamblaster_pairing_requests DROP COLUMN sibling_client_id;
\ No newline at end of file
diff --git a/db/up/jamblasters_network.sql b/db/up/jamblasters_network.sql
new file mode 100644
index 000000000..e986360cd
--- /dev/null
+++ b/db/up/jamblasters_network.sql
@@ -0,0 +1,3 @@
+ALTER TABLE jamblasters ADD COLUMN ipv6_link_local VARCHAR;
+ALTER TABLE jamblasters ADD COLUMN ipv4_link_local VARCHAR;
+ALTER TABLE jamblasters ADD COLUMN display_name VARCHAR;
\ No newline at end of file
diff --git a/db/up/jamclass_report.sql b/db/up/jamclass_report.sql
new file mode 100644
index 000000000..427b9ab0b
--- /dev/null
+++ b/db/up/jamclass_report.sql
@@ -0,0 +1,33 @@
+CREATE TABLE campaign_spends (
+ id VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
+ campaign VARCHAR NOT NULL,
+ spend NUMERIC(8,2) NOT NULL,
+ month INTEGER NOT NULL,
+ year INTEGER NOT NULL,
+ created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE jam_class_reports (
+ cohort DATE,
+ campaign VARCHAR,
+ spend NUMERIC (8,2),
+ registrations INTEGER,
+ td_customers INTEGER,
+ jamclass_rev NUMERIC (8,2),
+ td4 INTEGER,
+ td2 INTEGER,
+ td1 INTEGER,
+ spend_td NUMERIC (8,2),
+ purchases0 NUMERIC (8,2),
+ purchases1 NUMERIC (8,2),
+ purchases2 NUMERIC (8,2),
+ purchases3 NUMERIC (8,2),
+ purchases_rest NUMERIC (8,2),
+ purchases0_count INTEGER,
+ purchases1_count INTEGER,
+ purchases2_count INTEGER,
+ purchases3_count INTEGER,
+ purchases_rest_count INTEGER,
+ purchases_count INTEGER
+);
\ No newline at end of file
diff --git a/db/up/lesson_booking_schools.sql b/db/up/lesson_booking_schools.sql
new file mode 100644
index 000000000..265e731b7
--- /dev/null
+++ b/db/up/lesson_booking_schools.sql
@@ -0,0 +1,3 @@
+ALTER TABLE lesson_bookings ADD COLUMN school_id INTEGER REFERENCES schools(id);
+ALTER TABLE teacher_payments ADD COLUMN school_id INTEGER REFERENCES schools(id);
+ALTER TABLE teacher_distributions ADD COLUMN school_id INTEGER REFERENCES schools(id);
\ No newline at end of file
diff --git a/db/up/lesson_booking_schools_2.sql b/db/up/lesson_booking_schools_2.sql
new file mode 100644
index 000000000..7888cbeb8
--- /dev/null
+++ b/db/up/lesson_booking_schools_2.sql
@@ -0,0 +1,2 @@
+ALTER TABLE lesson_bookings ADD COLUMN same_school BOOLEAN DEFAULT FALSE NOT NULL;
+ALTER TABLE schools ADD COLUMN affiliate_partner_id INTEGER REFERENCES affiliate_partners(id);
\ No newline at end of file
diff --git a/db/up/lesson_booking_success.sql b/db/up/lesson_booking_success.sql
new file mode 100644
index 000000000..3dae1ae2f
--- /dev/null
+++ b/db/up/lesson_booking_success.sql
@@ -0,0 +1 @@
+ALTER TABLE lesson_bookings ADD COLUMN success BOOLEAN;
\ No newline at end of file
diff --git a/db/up/lesson_time_tracking.sql b/db/up/lesson_time_tracking.sql
new file mode 100644
index 000000000..9141b0bd8
--- /dev/null
+++ b/db/up/lesson_time_tracking.sql
@@ -0,0 +1,6 @@
+ALTER TABLE lesson_bookings ADD COLUMN sent_notices_at timestamp without time zone;
+ALTER TABLE lesson_bookings ADD COLUMN countered_at timestamp without time zone;
+ALTER TABLE lesson_sessions ADD COLUMN countered_at timestamp without time zone;
+ALTER TABLE lesson_bookings ADD COLUMN counterer_id VARCHAR(64) REFERENCES users(id);
+ALTER TABLE lesson_sessions ADD COLUMN counterer_id VARCHAR(64) REFERENCES users(id);
+ALTER TABLE lesson_bookings ADD COLUMN sent_counter_reminder BOOLEAN NOT NULL DEFAULT FALSE;
\ No newline at end of file
diff --git a/db/up/lessons.sql b/db/up/lessons.sql
new file mode 100644
index 000000000..bc6ff6daf
--- /dev/null
+++ b/db/up/lessons.sql
@@ -0,0 +1,261 @@
+
+CREATE TABLE lesson_package_types (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ name VARCHAR NOT NULL,
+ description VARCHAR NOT NULL,
+ package_type VARCHAR(64) NOT NULL,
+ price NUMERIC(8,2),
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE lesson_bookings (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id VARCHAR(64) REFERENCES users(id) NOT NULL,
+ active BOOLEAN NOT NULL DEFAULT FALSE,
+ accepter_id VARCHAR(64) REFERENCES users(id),
+ canceler_id VARCHAR(64) REFERENCES users(id),
+ lesson_type VARCHAR(64) NOT NULL,
+ recurring BOOLEAN NOT NULL,
+ lesson_length INTEGER NOT NULL,
+ payment_style VARCHAR(64) NOT NULL,
+ description VARCHAR,
+ booked_price NUMERIC(8,2) NOT NULL,
+ teacher_id VARCHAR(64) REFERENCES users(id) NOT NULL,
+ card_presumed_ok BOOLEAN NOT NULL DEFAULT FALSE,
+ sent_notices BOOLEAN NOT NULL DEFAULT FALSE,
+ status VARCHAR,
+ cancel_message VARCHAR,
+ user_decremented BOOLEAN NOT NULL DEFAULT FALSE,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE charges (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ amount_in_cents INTEGER NOT NULL,
+ fee_in_cents INTEGER NOT NULL DEFAULT 0,
+ type VARCHAR(64) NOT NULL,
+ sent_billing_notices BOOLEAN NOT NULL DEFAULT FALSE,
+ sent_billing_notices_at TIMESTAMP,
+ last_billing_attempt_at TIMESTAMP,
+ billed BOOLEAN NOT NULL DEFAULT FALSE,
+ billed_at TIMESTAMP,
+ post_processed BOOLEAN NOT NULL DEFAULT FALSE,
+ post_processed_at TIMESTAMP,
+ billing_error_reason VARCHAR,
+ billing_error_detail VARCHAR,
+ billing_should_retry BOOLEAN NOT NULL DEFAULT TRUE ,
+ billing_attempts INTEGER NOT NULL DEFAULT 0,
+ stripe_charge_id VARCHAR(200),
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE lesson_package_purchases (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ lesson_package_type_id VARCHAR(64) REFERENCES lesson_package_types(id) NOT NULL,
+ user_id VARCHAR(64) REFERENCES users(id) NOT NULL,
+ teacher_id VARCHAR(64) REFERENCES users(id),
+ price NUMERIC(8,2),
+ recurring BOOLEAN NOT NULL DEFAULT FALSE,
+ year INTEGER,
+ month INTEGER,
+ charge_id VARCHAR(64) REFERENCES charges(id),
+ lesson_booking_id VARCHAR(64) REFERENCES lesson_bookings(id),
+ sent_notices BOOLEAN NOT NULL DEFAULT FALSE,
+ sent_notices_at TIMESTAMP,
+ post_processed BOOLEAN NOT NULL DEFAULT FALSE,
+ post_processed_at TIMESTAMP,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+
+CREATE TABLE lesson_sessions (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ lesson_type VARCHAR(64) NOT NULL,
+ teacher_id VARCHAR(64) REFERENCES users(id) NOT NULL,
+ lesson_package_purchase_id VARCHAR(64) REFERENCES lesson_package_purchases(id),
+ lesson_booking_id VARCHAR(64) REFERENCES lesson_bookings(id),
+ duration INTEGER NOT NULL,
+ booked_price NUMERIC(8,2) NOT NULL,
+ teacher_complete BOOLEAN DEFAULT FALSE NOT NULL,
+ student_complete BOOLEAN DEFAULT FALSE NOT NULL,
+ student_canceled BOOLEAN DEFAULT FALSE NOT NULL,
+ teacher_canceled BOOLEAN DEFAULT FALSE NOT NULL,
+ student_canceled_at TIMESTAMP,
+ teacher_canceled_at TIMESTAMP,
+ student_canceled_reason VARCHAR,
+ teacher_canceled_reason VARCHAR,
+ status VARCHAR,
+ analysed BOOLEAN NOT NULL DEFAULT FALSE,
+ analysis JSON,
+ analysed_at TIMESTAMP,
+ cancel_message VARCHAR,
+ canceler_id VARCHAR(64) REFERENCES users(id),
+ charge_id VARCHAR(64) REFERENCES charges(id),
+ success BOOLEAN NOT NULL DEFAULT FALSE,
+ sent_notices BOOLEAN NOT NULL DEFAULT FALSE,
+ sent_notices_at TIMESTAMP,
+ post_processed BOOLEAN NOT NULL DEFAULT FALSE,
+ post_processed_at TIMESTAMP,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+
+ALTER TABLE music_sessions ADD COLUMN lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id);
+ALTER TABLE notifications ADD COLUMN lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id);
+ALTER TABLE notifications ADD COLUMN purpose VARCHAR(200);
+ALTER TABLE notifications ADD COLUMN student_directed BOOLEAN;
+
+INSERT INTO lesson_package_types (id, name, description, package_type, price) VALUES ('single', 'Single Lesson', 'A single lesson purchased at the teacher''s price.', 'single', 0.00);
+INSERT INTO lesson_package_types (id, name, description, package_type, price) VALUES ('single-free', 'Free Lesson', 'A free, single lesson.', 'single-free', 0.00);
+INSERT INTO lesson_package_types (id, name, description, package_type, price) VALUES ('test-drive', 'Test Drive', 'Four reduced-price lessons which you can use to find that ideal teacher.', 'test-drive', 49.99);
+
+
+CREATE TABLE lesson_booking_slots (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ lesson_booking_id VARCHAR(64) REFERENCES lesson_bookings(id),
+ lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id),
+ slot_type VARCHAR(64) NOT NULL,
+ preferred_day DATE,
+ day_of_week INTEGER,
+ hour INTEGER,
+ minute INTEGER,
+ timezone VARCHAR NOT NULL,
+ message VARCHAR,
+ accept_message VARCHAR,
+ update_all BOOLEAN NOT NULL DEFAULT FALSE,
+ proposer_id VARCHAR(64) REFERENCES users(id) NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+ALTER TABLE lesson_bookings ADD COLUMN default_slot_id VARCHAR(64) REFERENCES lesson_booking_slots(id);
+ALTER TABLE lesson_bookings ADD COLUMN counter_slot_id VARCHAR(64) REFERENCES lesson_booking_slots(id);
+ALTER TABLE lesson_sessions ADD COLUMN counter_slot_id VARCHAR(64) REFERENCES lesson_booking_slots(id);
+ALTER TABLE lesson_sessions ADD COLUMN slot_id VARCHAR(64) REFERENCES lesson_booking_slots(id);
+
+ALTER TABLE chat_messages ADD COLUMN target_user_id VARCHAR(64) REFERENCES users(id);
+ALTER TABLE chat_messages ADD COLUMN lesson_booking_id VARCHAR(64) REFERENCES lesson_bookings(id);
+ALTER TABLE users ADD COLUMN remaining_free_lessons INTEGER NOT NULL DEFAULT 1;
+ALTER TABLE users ADD COLUMN stored_credit_card BOOLEAN NOT NULL DEFAULT FALSE;
+ALTER TABLE users ADD COLUMN remaining_test_drives INTEGER NOT NULL DEFAULT 0;
+ALTER TABLE users ADD COLUMN stripe_token VARCHAR(200);
+ALTER TABLE users ADD COLUMN stripe_customer_id VARCHAR(200);
+ALTER TABLE users ADD COLUMN stripe_zip_code VARCHAR(200);
+ALTER TABLE sales ADD COLUMN stripe_charge_id VARCHAR(200);
+ALTER TABLE teachers ADD COLUMN stripe_account_id VARCHAR(200);
+ALTER TABLE sale_line_items ADD COLUMN lesson_package_purchase_id VARCHAR(64) REFERENCES lesson_package_purchases(id);
+
+
+-- one is created every time the teacher is paid. N teacher_distributions point to this
+CREATE TABLE teacher_payments (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ teacher_id VARCHAR(64) REFERENCES users(id) NOT NULL,
+ charge_id VARCHAR(64) REFERENCES charges(id) NOT NULL,
+ amount_in_cents INTEGER NOT NULL,
+ fee_in_cents INTEGER NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+-- one is created for every bit of money the teacher is due
+CREATE TABLE teacher_distributions (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ teacher_id VARCHAR(64) REFERENCES users(id) NOT NULL,
+ teacher_payment_id VARCHAR(64) REFERENCES teacher_payments(id),
+ lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id),
+ lesson_package_purchase_id VARCHAR(64) REFERENCES lesson_package_purchases(id),
+ amount_in_cents INTEGER NOT NULL,
+ ready BOOLEAN NOT NULL DEFAULT FALSE,
+ distributed BOOLEAN NOT NULL DEFAULT FALSE,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE affiliate_distributions (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ affiliate_referral_id INTEGER REFERENCES affiliate_partners(id) NOT NULL,
+ affiliate_referral_fee_in_cents INTEGER NOT NULL,
+ sale_line_item_id VARCHAR(64) REFERENCES sale_line_items(id) NOT NULL,
+ affiliate_refunded BOOLEAN NOT NULL DEFAULT FALSE,
+ affiliate_refunded_at TIMESTAMP WITHOUT TIME ZONE,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+ALTER TABLE affiliate_partners ADD COLUMN lesson_rate NUMERIC (8,2) NOT NULL DEFAULT 0.20;
+
+-- move over all sale_line_item affiliate info
+INSERT INTO affiliate_distributions (
+ SELECT
+ sale_line_items.id,
+ sale_line_items.affiliate_referral_id,
+ sale_line_items.affiliate_referral_fee_in_cents,
+ sale_line_items.id,
+ sale_line_items.affiliate_refunded,
+ sale_line_items.affiliate_refunded_at,
+ sale_line_items.created_at,
+ sale_line_items.updated_at
+ FROM sale_line_items
+ WHERE sale_line_items.affiliate_referral_id IS NOT NULL
+);
+
+CREATE TABLE teacher_intents (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id VARCHAR(64) REFERENCES users(id) NOT NULL,
+ teacher_id VARCHAR(64) REFERENCES teachers(id) NOT NULL,
+ intent VARCHAR(64),
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+CREATE INDEX teacher_intents_intent_idx ON teacher_intents(teacher_id, intent);
+
+CREATE TABLE schools (
+ id INTEGER PRIMARY KEY,
+ user_id VARCHAR(64) REFERENCES users(id) NOT NULL,
+ name VARCHAR,
+ enabled BOOLEAN DEFAULT TRUE,
+ scheduling_communication VARCHAR NOT NULL DEFAULT 'teacher',
+ correspondence_email VARCHAR,
+ photo_url VARCHAR(2048),
+ original_fpfile VARCHAR(8000),
+ cropped_fpfile VARCHAR(8000),
+ cropped_s3_path VARCHAR(8000),
+ crop_selection VARCHAR(256),
+ large_photo_url VARCHAR(512),
+ cropped_large_s3_path VARCHAR(512),
+ cropped_large_fpfile VARCHAR(8000),
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE SEQUENCE school_key_sequence;
+ALTER SEQUENCE school_key_sequence RESTART WITH 10000;
+ALTER TABLE schools ALTER COLUMN id SET DEFAULT nextval('school_key_sequence');
+
+ALTER TABLE users ADD COLUMN school_id INTEGER REFERENCES schools(id);
+ALTER TABLE users ADD COLUMN joined_school_at TIMESTAMP;
+ALTER TABLE teachers ADD COLUMN school_id INTEGER REFERENCES schools(id);
+ALTER TABLE teachers ADD COLUMN joined_school_at TIMESTAMP;
+
+CREATE TABLE school_invitations (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id VARCHAR(64) REFERENCES users(id),
+ school_id INTEGER REFERENCES schools(id) NOT NULL,
+ invitation_code VARCHAR(256) NOT NULL UNIQUE,
+ note VARCHAR,
+ as_teacher BOOLEAN NOT NULL,
+ email VARCHAR NOT NULL,
+ first_name VARCHAR,
+ last_name VARCHAR,
+ accepted BOOLEAN NOT NULL DEFAULT FALSE,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+ALTER TABLE teachers ADD jamkazam_rate NUMERIC (8, 2) DEFAULT 0.25;
+ALTER TABLE schools ADD jamkazam_rate NUMERIC (8, 2) DEFAULT 0.25;
\ No newline at end of file
diff --git a/db/up/lessons_unread_messages.sql b/db/up/lessons_unread_messages.sql
new file mode 100644
index 000000000..1b2d65f26
--- /dev/null
+++ b/db/up/lessons_unread_messages.sql
@@ -0,0 +1,34 @@
+ALTER TABLE chat_messages DROP COLUMN lesson_booking_id;
+ALTER TABLE chat_messages ADD COLUMN lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id);
+ALTER TABLE lesson_sessions ADD COLUMN teacher_unread_messages BOOLEAN DEFAULT FALSE NOT NULL;
+ALTER TABLE lesson_sessions ADD COLUMN student_unread_messages BOOLEAN DEFAULT FALSE NOT NULL;
+ALTER TABLE chat_messages ADD COLUMN purpose VARCHAR(200);
+ALTER TABLE lesson_sessions ADD COLUMN student_short_canceled BOOLEAN DEFAULT FALSE NOT NULL;
+ALTER TABLE lesson_sessions ADD COLUMN teacher_short_canceled BOOLEAN DEFAULT FALSE NOT NULL;
+ALTER TABLE lesson_sessions ADD COLUMN sent_starting_notice BOOLEAN DEFAULT FALSE NOT NULL;
+
+ALTER TABLE lesson_bookings DROP CONSTRAINT lesson_bookings_counter_slot_id_fkey;
+ALTER TABLE lesson_bookings ADD CONSTRAINT lesson_bookings_counter_slot_id_fkey FOREIGN KEY (counter_slot_id) REFERENCES lesson_booking_slots(id) ON DELETE CASCADE;
+
+ALTER TABLE lesson_bookings DROP CONSTRAINT lesson_bookings_default_slot_id_fkey;
+ALTER TABLE lesson_bookings ADD CONSTRAINT lesson_bookings_default_slot_id_fkey FOREIGN KEY (default_slot_id) REFERENCES lesson_booking_slots(id) ON DELETE CASCADE;
+
+
+ALTER TABLE lesson_sessions DROP CONSTRAINT lesson_sessions_slot_id_fkey;
+ALTER TABLE lesson_sessions ADD CONSTRAINT lesson_sessions_slot_id_fkey FOREIGN KEY (slot_id) REFERENCES lesson_booking_slots(id) ON DELETE CASCADE;
+
+ALTER TABLE users DROP CONSTRAINT users_teacher_id_fkey;
+ALTER TABLE users ADD CONSTRAINT users_teacher_id_fkey FOREIGN KEY (teacher_id) REFERENCES teachers(id) ON DELETE CASCADE;
+
+ALTER TABLE music_sessions DROP CONSTRAINT music_sessions_lesson_session_id_fkey;
+ALTER TABLE music_sessions ADD CONSTRAINT music_sessions_lesson_session_id_fkey FOREIGN KEY (lesson_session_id) REFERENCES lesson_sessions(id) ON DELETE SET NULL;
+
+ALTER TABLE notifications DROP CONSTRAINT notifications_lesson_session_id_fkey;
+ALTER TABLE notifications ADD CONSTRAINT notifications_lesson_session_id_fkey FOREIGN KEY (lesson_session_id) REFERENCES lesson_sessions(id) ON DELETE CASCADE;
+
+ALTER TABLE chat_messages DROP CONSTRAINT chat_messages_lesson_session_id_fkey;
+ALTER TABLE chat_messages ADD CONSTRAINT chat_messages_lesson_session_id_fkey FOREIGN KEY (lesson_session_id) REFERENCES lesson_sessions(id) ON DELETE CASCADE;
+
+ALTER TABLE chat_messages DROP CONSTRAINT chat_messages_target_user_id_fkey;
+ALTER TABLE chat_messages ADD CONSTRAINT chat_messages_target_user_id_fkey FOREIGN KEY (lesson_session_id) REFERENCES lesson_sessions(id) ON DELETE SET NULL;
+
diff --git a/db/up/mixdown.sql b/db/up/mixdown.sql
new file mode 100644
index 000000000..24295cdae
--- /dev/null
+++ b/db/up/mixdown.sql
@@ -0,0 +1,61 @@
+CREATE TABLE jam_track_mixdowns (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ jam_track_id VARCHAR(64) NOT NULL REFERENCES jam_tracks(id) ON DELETE CASCADE,
+ user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ settings JSON NOT NULL,
+ name VARCHAR(1000) NOT NULL,
+ description VARCHAR(1000),
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE jam_track_mixdown_packages (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ jam_track_mixdown_id VARCHAR(64) NOT NULL REFERENCES jam_track_mixdowns(id) ON DELETE CASCADE,
+ file_type VARCHAR NOT NULL ,
+ sample_rate INTEGER NOT NULL,
+ url VARCHAR(2048),
+ md5 VARCHAR,
+ length INTEGER,
+ downloaded_since_sign BOOLEAN NOT NULL DEFAULT FALSE,
+ last_step_at TIMESTAMP,
+ last_signed_at TIMESTAMP,
+ download_count INTEGER NOT NULL DEFAULT 0,
+ signed_at TIMESTAMP,
+ downloaded_at TIMESTAMP,
+ signing_queued_at TIMESTAMP,
+ error_count INTEGER NOT NULL DEFAULT 0,
+ error_reason VARCHAR,
+ error_detail VARCHAR,
+ should_retry BOOLEAN NOT NULL DEFAULT FALSE,
+ packaging_steps INTEGER,
+ current_packaging_step INTEGER,
+ private_key VARCHAR,
+ signed BOOLEAN,
+ signing_started_at TIMESTAMP,
+ first_downloaded TIMESTAMP,
+ signing BOOLEAN NOT NULL DEFAULT FALSE,
+ encrypt_type VARCHAR,
+ first_downloaded_at TIMESTAMP,
+ last_downloaded_at TIMESTAMP,
+ version VARCHAR NOT NULL DEFAULT '1',
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+ALTER TABLE jam_track_rights ADD COLUMN last_mixdown_id VARCHAR(64) REFERENCES jam_track_mixdowns(id) ON DELETE SET NULL;
+
+ALTER TABLE notifications ADD COLUMN jam_track_mixdown_package_id VARCHAR(64) REFERENCES jam_track_mixdown_packages(id) ON DELETE CASCADE;
+
+ALTER TABLE jam_track_mixdown_packages ADD COLUMN last_errored_at TIMESTAMP;
+ALTER TABLE jam_track_mixdown_packages ADD COLUMN queued BOOLEAN DEFAULT FALSE;
+ALTER TABLE jam_track_mixdown_packages ADD COLUMN speed_pitched BOOLEAN DEFAULT FALSE;
+ALTER TABLE jam_track_rights ADD COLUMN queued BOOLEAN DEFAULT FALSE;
+
+CREATE INDEX jam_track_rights_queued ON jam_track_rights(queued);
+CREATE INDEX jam_track_rights_signing_queued ON jam_track_rights(signing_queued_at);
+CREATE INDEX jam_track_rights_updated ON jam_track_rights(updated_at);
+
+CREATE INDEX jam_track_mixdown_packages_queued ON jam_track_mixdown_packages(queued);
+CREATE INDEX jam_track_mixdown_packages_signing_queued ON jam_track_mixdown_packages(signing_queued_at);
+CREATE INDEX jam_track_mixdown_packages_updated ON jam_track_mixdown_packages(updated_at);
diff --git a/db/up/news.sql b/db/up/news.sql
new file mode 100644
index 000000000..2eb981de8
--- /dev/null
+++ b/db/up/news.sql
@@ -0,0 +1,8 @@
+CREATE TABLE news (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ title VARCHAR NOT NULL,
+ body VARCHAR NOT NULL,
+ position INTEGER NOT NULL UNIQUE,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
\ No newline at end of file
diff --git a/db/up/packaged_test_drive.sql b/db/up/packaged_test_drive.sql
new file mode 100644
index 000000000..fb1796448
--- /dev/null
+++ b/db/up/packaged_test_drive.sql
@@ -0,0 +1,38 @@
+CREATE TABLE test_drive_packages (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ name VARCHAR UNIQUE NOT NULL,
+ package_type VARCHAR NOT NULL,
+ description VARCHAR,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE test_drive_package_teachers (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE,
+ test_drive_package_id VARCHAR(64) REFERENCES test_drive_packages(id) ON DELETE CASCADE,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+ALTER TABLE lesson_bookings ADD COLUMN test_drive_package_id VARCHAR(64) REFERENCES test_drive_packages(id);
+
+CREATE TABLE test_drive_package_choices (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ test_drive_package_id VARCHAR(64) REFERENCES test_drive_packages(id) ON DELETE CASCADE,
+ user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+
+CREATE TABLE test_drive_package_choice_teachers (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ test_drive_package_choice_id VARCHAR(64) REFERENCES test_drive_package_choices(id) ON DELETE CASCADE,
+ teacher_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+ALTER TABLE teachers ADD COLUMN short_bio VARCHAR;
+ALTER TABLE test_drive_package_teachers ADD COLUMN short_bio VARCHAR;
\ No newline at end of file
diff --git a/db/up/packaged_test_drive2.sql b/db/up/packaged_test_drive2.sql
new file mode 100644
index 000000000..c5c7209c9
--- /dev/null
+++ b/db/up/packaged_test_drive2.sql
@@ -0,0 +1,2 @@
+ALTER TABLE lesson_booking_slots ADD COLUMN from_package BOOL DEFAULT FALSE;
+ALTER TABLE lesson_bookings ADD COLUMN test_drive_package_choice_id VARCHAR(64) REFERENCES test_drive_package_choices(id);
diff --git a/db/up/phantom_accounts.sql b/db/up/phantom_accounts.sql
new file mode 100644
index 000000000..696c57370
--- /dev/null
+++ b/db/up/phantom_accounts.sql
@@ -0,0 +1,16 @@
+ALTER TABLE users ADD COLUMN phantom BOOLEAN DEFAULT FALSE NOT NULL;
+
+CREATE OR REPLACE FUNCTION phantom_check() RETURNS TRIGGER
+STRICT VOLATILE AS $$
+ BEGIN
+
+ -- Remember who changed the payroll when
+ NEW.phantom := (SELECT NEW.email ilike 'phantom+%@jamkazam.com');
+ RETURN NEW;
+ END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER phantom_update BEFORE INSERT OR UPDATE
+ON users FOR EACH ROW EXECUTE PROCEDURE phantom_check(id);
+
+UPDATE users set updated_at = NOW();
diff --git a/db/up/populate_languages.sql b/db/up/populate_languages.sql
new file mode 100644
index 000000000..885995b69
--- /dev/null
+++ b/db/up/populate_languages.sql
@@ -0,0 +1,72 @@
+insert into languages(description, id) values ('English','EN');
+insert into languages(description, id) values ('Afrikanns','AF');
+insert into languages(description, id) values ('Albanian','SQ');
+insert into languages(description, id) values ('Arabic','AR');
+insert into languages(description, id) values ('Armenian','HY');
+insert into languages(description, id) values ('Basque','EU');
+insert into languages(description, id) values ('Bengali','BN');
+insert into languages(description, id) values ('Bulgarian','BG');
+insert into languages(description, id) values ('Catalan','CA');
+insert into languages(description, id) values ('Cambodian','KM');
+insert into languages(description, id) values ('Chinese (Mandarin)','ZH');
+insert into languages(description, id) values ('Croation','HR');
+insert into languages(description, id) values ('Czech','CS');
+insert into languages(description, id) values ('Danish','DA');
+insert into languages(description, id) values ('Dutch','NL');
+insert into languages(description, id) values ('Estonian','ET');
+insert into languages(description, id) values ('Fiji','FJ');
+insert into languages(description, id) values ('Finnish','FI');
+insert into languages(description, id) values ('French','FR');
+insert into languages(description, id) values ('Georgian','KA');
+insert into languages(description, id) values ('German','DE');
+insert into languages(description, id) values ('Greek','EL');
+insert into languages(description, id) values ('Gujarati','GU');
+insert into languages(description, id) values ('Hebrew','HE');
+insert into languages(description, id) values ('Hindi','HI');
+insert into languages(description, id) values ('Hungarian','HU');
+insert into languages(description, id) values ('Icelandic','IS');
+insert into languages(description, id) values ('Indonesian','ID');
+insert into languages(description, id) values ('Irish','GA');
+insert into languages(description, id) values ('Italian','IT');
+insert into languages(description, id) values ('Japanese','JA');
+insert into languages(description, id) values ('Javanese','JW');
+insert into languages(description, id) values ('Korean','KO');
+insert into languages(description, id) values ('Latin','LA');
+insert into languages(description, id) values ('Latvian','LV');
+insert into languages(description, id) values ('Lithuanian','LT');
+insert into languages(description, id) values ('Macedonian','MK');
+insert into languages(description, id) values ('Malay','MS');
+insert into languages(description, id) values ('Malayalam','ML');
+insert into languages(description, id) values ('Maltese','MT');
+insert into languages(description, id) values ('Maori','MI');
+insert into languages(description, id) values ('Marathi','MR');
+insert into languages(description, id) values ('Mongolian','MN');
+insert into languages(description, id) values ('Nepali','NE');
+insert into languages(description, id) values ('Norwegian','NO');
+insert into languages(description, id) values ('Persian','FA');
+insert into languages(description, id) values ('Polish','PL');
+insert into languages(description, id) values ('Portuguese','PT');
+insert into languages(description, id) values ('Punjabi','PA');
+insert into languages(description, id) values ('Quechua','QU');
+insert into languages(description, id) values ('Romanian','RO');
+insert into languages(description, id) values ('Russian','RU');
+insert into languages(description, id) values ('Samoan','SM');
+insert into languages(description, id) values ('Serbian','SR');
+insert into languages(description, id) values ('Slovak','SK');
+insert into languages(description, id) values ('Slovenian','SL');
+insert into languages(description, id) values ('Spanish','ES');
+insert into languages(description, id) values ('Swahili','SW');
+insert into languages(description, id) values ('Swedish ','SV');
+insert into languages(description, id) values ('Tamil','TA');
+insert into languages(description, id) values ('Tatar','TT');
+insert into languages(description, id) values ('Telugu','TE');
+insert into languages(description, id) values ('Thai','TH');
+insert into languages(description, id) values ('Tibetan','BO');
+insert into languages(description, id) values ('Tonga','TO');
+insert into languages(description, id) values ('Turkish','TR');
+insert into languages(description, id) values ('Ukranian','UK');
+insert into languages(description, id) values ('Urdu','UR');
+insert into languages(description, id) values ('Uzbek','UZ');
+insert into languages(description, id) values ('Vietnamese','VI');
+insert into languages(description, id) values ('Welsh','CY');
+insert into languages(description, id) values ('Xhosa','XH');
diff --git a/db/up/populate_subjects.sql b/db/up/populate_subjects.sql
new file mode 100644
index 000000000..fa433387a
--- /dev/null
+++ b/db/up/populate_subjects.sql
@@ -0,0 +1,6 @@
+insert into subjects(id, description) values ('arranging', 'Arranging');
+insert into subjects(id, description) values ('composing', 'Composing');
+insert into subjects(id, description) values ('music-business', 'Music Business');
+insert into subjects(id, description) values ('music-theory', 'Music Theory');
+insert into subjects(id, description) values ('recording', 'Recording');
+insert into subjects(id, description) values ('site-reading', 'Site Reading');
diff --git a/db/up/profile_teacher.sql b/db/up/profile_teacher.sql
new file mode 100644
index 000000000..e4d0bd0b1
--- /dev/null
+++ b/db/up/profile_teacher.sql
@@ -0,0 +1,75 @@
+CREATE TABLE teachers (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ introductory_video VARCHAR(1024) NULL,
+ years_teaching SMALLINT NOT NULL DEFAULT 0,
+ years_playing SMALLINT NOT NULL DEFAULT 0,
+ teaches_age_lower SMALLINT NOT NULL DEFAULT 0,
+ teaches_age_upper SMALLINT NOT NULL DEFAULT 0,
+ teaches_beginner BOOLEAN NOT NULL DEFAULT FALSE,
+ teaches_intermediate BOOLEAN NOT NULL DEFAULT FALSE,
+ teaches_advanced BOOLEAN NOT NULL DEFAULT FALSE,
+ website VARCHAR(1024) NULL,
+ biography VARCHAR(4096) NULL,
+ prices_per_lesson BOOLEAN NOT NULL DEFAULT FALSE,
+ prices_per_month BOOLEAN NOT NULL DEFAULT FALSE,
+ lesson_duration_30 BOOLEAN NOT NULL DEFAULT FALSE,
+ lesson_duration_45 BOOLEAN NOT NULL DEFAULT FALSE,
+ lesson_duration_60 BOOLEAN NOT NULL DEFAULT FALSE,
+ lesson_duration_90 BOOLEAN NOT NULL DEFAULT FALSE,
+ lesson_duration_120 BOOLEAN NOT NULL DEFAULT FALSE,
+ price_per_lesson_30_cents INT NULL,
+ price_per_lesson_45_cents INT NULL,
+ price_per_lesson_60_cents INT NULL,
+ price_per_lesson_90_cents INT NULL,
+ price_per_lesson_120_cents INT NULL,
+ price_per_month_30_cents INT NULL,
+ price_per_month_45_cents INT NULL,
+ price_per_month_60_cents INT NULL,
+ price_per_month_90_cents INT NULL,
+ price_per_month_120_cents INT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+ALTER TABLE users ADD COLUMN teacher_id VARCHAR(64) REFERENCES teachers(id) ON DELETE SET NULL;
+
+CREATE TABLE subjects(
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ description VARCHAR(1024) NULL
+);
+
+CREATE TABLE languages(
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ description VARCHAR(1024) NULL
+);
+
+-- Has many:
+CREATE TABLE teacher_experiences(
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ teacher_id VARCHAR(64) REFERENCES teachers(id) ON DELETE CASCADE,
+ -- experience type: teaching, education, award:
+ experience_type VARCHAR(32) NOT NULL,
+ name VARCHAR(200) NOT NULL,
+ organization VARCHAR(200) NOT NULL,
+ start_year SMALLINT NOT NULL DEFAULT 0,
+ end_year SMALLINT NULL
+);
+
+-- Has many/through tables:
+CREATE TABLE teachers_genres(
+ teacher_id VARCHAR(64) REFERENCES teachers(id) ON DELETE CASCADE,
+ genre_id VARCHAR(64) REFERENCES genres(id) ON DELETE CASCADE
+);
+CREATE TABLE teachers_instruments(
+ teacher_id VARCHAR(64) REFERENCES teachers(id) ON DELETE CASCADE,
+ instrument_id VARCHAR(64) REFERENCES instruments(id) ON DELETE CASCADE
+);
+CREATE TABLE teachers_subjects(
+ teacher_id VARCHAR(64) REFERENCES teachers(id) ON DELETE CASCADE,
+ subject_id VARCHAR(64) REFERENCES subjects(id) ON DELETE CASCADE
+);
+CREATE TABLE teachers_languages(
+ teacher_id VARCHAR(64) REFERENCES teachers(id) ON DELETE CASCADE,
+ language_id VARCHAR(64) REFERENCES languages(id) ON DELETE CASCADE
+);
+
diff --git a/db/up/purchasable_gift_cards.sql b/db/up/purchasable_gift_cards.sql
new file mode 100644
index 000000000..9ef8d29fa
--- /dev/null
+++ b/db/up/purchasable_gift_cards.sql
@@ -0,0 +1,24 @@
+
+
+CREATE TABLE gift_card_types (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ card_type VARCHAR(64) NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+INSERT INTO gift_card_types (id, card_type) VALUES ('jam_tracks_5', 'jam_tracks_5');
+INSERT INTO gift_card_types (id, card_type) VALUES ('jam_tracks_10', 'jam_tracks_10');
+
+CREATE TABLE gift_card_purchases (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE SET NULL,
+ gift_card_type_id VARCHAR(64) REFERENCES gift_card_types(id) ON DELETE SET NULL,
+ recurly_adjustment_uuid VARCHAR(500),
+ recurly_adjustment_credit_uuid VARCHAR(500),
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+
+ALTER TABLE sale_line_items ADD COLUMN gift_card_purchase_id VARCHAR(64) REFERENCES gift_card_purchases(id);
diff --git a/db/up/remove_stripe_acct_id.sql b/db/up/remove_stripe_acct_id.sql
new file mode 100644
index 000000000..5f44e52ba
--- /dev/null
+++ b/db/up/remove_stripe_acct_id.sql
@@ -0,0 +1 @@
+ALTER TABLE teachers DROP COLUMN stripe_account_id;
\ No newline at end of file
diff --git a/db/up/reviews.sql b/db/up/reviews.sql
new file mode 100644
index 000000000..89a30ee32
--- /dev/null
+++ b/db/up/reviews.sql
@@ -0,0 +1,23 @@
+CREATE TABLE reviews (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL,
+ user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ target_id VARCHAR(64) NOT NULL,
+ target_type VARCHAR(32) NOT NULL,
+ description VARCHAR,
+ rating INT NOT NULL,
+ deleted_by_user_id VARCHAR(64) REFERENCES users(id) ON DELETE SET NULL,
+ deleted_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NULL,
+ created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL,
+ updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL
+);
+
+CREATE TABLE review_summaries (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL,
+ target_id VARCHAR(64) NOT NULL,
+ target_type VARCHAR(32) NOT NULL,
+ avg_rating FLOAT NOT NULL,
+ wilson_score FLOAT NOT NULL,
+ review_count INT NOT NULL,
+ created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL,
+ updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL
+);
\ No newline at end of file
diff --git a/db/up/session_controller.sql b/db/up/session_controller.sql
new file mode 100644
index 000000000..d146481b6
--- /dev/null
+++ b/db/up/session_controller.sql
@@ -0,0 +1 @@
+ALTER TABLE music_sessions ADD COLUMN session_controller_id VARCHAR(64) REFERENCES users(id);
\ No newline at end of file
diff --git a/db/up/teacher_complete.sql b/db/up/teacher_complete.sql
new file mode 100644
index 000000000..7e67f1a20
--- /dev/null
+++ b/db/up/teacher_complete.sql
@@ -0,0 +1,2 @@
+ALTER TABLE teachers ADD COLUMN profile_pct NUMERIC(8,2) ;
+ALTER TABLE teachers ADD COLUMN profile_pct_summary JSON;
\ No newline at end of file
diff --git a/db/up/teacher_progression.sql b/db/up/teacher_progression.sql
new file mode 100644
index 000000000..111a453ee
--- /dev/null
+++ b/db/up/teacher_progression.sql
@@ -0,0 +1,4 @@
+ALTER TABLE teachers ADD COLUMN background_check_at TIMESTAMP WITHOUT TIME ZONE;
+ALTER TABLE teachers ADD COLUMN ready_for_session_at TIMESTAMP WITHOUT TIME ZONE;
+ALTER TABLE users ADD COLUMN ready_for_session_at TIMESTAMP WITHOUT TIME ZONE;
+ALTER TABLE teachers ADD COLUMN top_rated BOOLEAN NOT NULL DEFAULT FALSE;
\ No newline at end of file
diff --git a/db/up/teacher_student_flags.sql b/db/up/teacher_student_flags.sql
new file mode 100644
index 000000000..c94290d2f
--- /dev/null
+++ b/db/up/teacher_student_flags.sql
@@ -0,0 +1,2 @@
+ALTER TABLE users ADD COLUMN is_a_student BOOLEAN NOT NULL DEFAULT FALSE;
+ALTER TABLE users ADD COLUMN is_a_teacher BOOLEAN NOT NULL DEFAULT FALSE;
\ No newline at end of file
diff --git a/db/up/test_drive_lessons.sql b/db/up/test_drive_lessons.sql
new file mode 100644
index 000000000..bac4f76eb
--- /dev/null
+++ b/db/up/test_drive_lessons.sql
@@ -0,0 +1,2 @@
+ALTER TABLE teachers ADD COLUMN test_drives_per_week INTEGER NOT NULL DEFAULT 2;
+ALTER TABLE teachers ADD COLUMN teaches_test_drive BOOLEAN NOT NULL DEFAULT TRUE;
\ No newline at end of file
diff --git a/db/up/track_downloads.sql b/db/up/track_downloads.sql
new file mode 100644
index 000000000..f328a9579
--- /dev/null
+++ b/db/up/track_downloads.sql
@@ -0,0 +1,30 @@
+CREATE TABLE download_trackers (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE,
+ remote_ip VARCHAR(400) NOT NULL,
+ jam_track_id VARCHAR (64) NOT NULL REFERENCES jam_tracks(id) ON DELETE CASCADE,
+ paid BOOLEAN NOT NULL DEFAULT FALSE,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE INDEX index_download_trackers_on_user_id ON download_trackers USING btree (user_id);
+CREATE INDEX index_download_trackers_on_remote_ip ON download_trackers USING btree (remote_ip);
+CREATE INDEX index_download_trackers_on_created_at ON download_trackers USING btree (created_at, paid);
+
+
+CREATE TABLE ip_blacklists (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ remote_ip VARCHAR(400) UNIQUE NOT NULL,
+ notes VARCHAR,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+
+CREATE TABLE user_blacklists (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id VARCHAR(64) UNIQUE NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ notes VARCHAR,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
\ No newline at end of file
diff --git a/db/up/track_school_signups.sql b/db/up/track_school_signups.sql
new file mode 100644
index 000000000..7e66f48fc
--- /dev/null
+++ b/db/up/track_school_signups.sql
@@ -0,0 +1 @@
+ALTER TABLE USERS ADD COLUMN school_interest BOOLEAN DEFAULT FALSE;
\ No newline at end of file
diff --git a/db/up/track_user_on_lesson.sql b/db/up/track_user_on_lesson.sql
new file mode 100644
index 000000000..a5e381d96
--- /dev/null
+++ b/db/up/track_user_on_lesson.sql
@@ -0,0 +1,4 @@
+ALTER TABLE lesson_sessions ADD COLUMN user_id VARCHAR(64) REFERENCES users(id);
+UPDATE lesson_sessions SET user_id = (select user_id from music_sessions where lesson_sessions.id = music_sessions.lesson_session_id) where user_id is NULL;
+ALTER TABLE lesson_sessions ALTER COLUMN user_id SET NOT NULL;
+CREATE INDEX msuh_client_id ON music_sessions_user_history USING btree (client_id);
\ No newline at end of file
diff --git a/db/up/update_payment_history.sql b/db/up/update_payment_history.sql
new file mode 100644
index 000000000..0e3e345ca
--- /dev/null
+++ b/db/up/update_payment_history.sql
@@ -0,0 +1 @@
+ALTER TABLE charges ADD COLUMN user_id VARCHAR(64) REFERENCES users(id);
\ No newline at end of file
diff --git a/db/up/updated_subjects.sql b/db/up/updated_subjects.sql
new file mode 100644
index 000000000..4bd7873e1
--- /dev/null
+++ b/db/up/updated_subjects.sql
@@ -0,0 +1,27 @@
+-- https://jamkazam.atlassian.net/browse/VRFS-3407
+UPDATE subjects SET description = 'Composition' WHERE id = 'composing';
+UPDATE subjects SET description = 'Recording & Production' WHERE id = 'recording';
+UPDATE subjects SET description = 'Sight Reading' WHERE id = 'site-reading';
+
+INSERT INTO subjects(id, description) VALUES ('film-scoring', 'Film Scoring');
+INSERT INTO subjects(id, description) VALUES ('video-game-scoring', 'Video Game Scoring');
+INSERT INTO subjects(id, description) VALUES ('ear-training', 'Ear Training');
+INSERT INTO subjects(id, description) VALUES ('harmony', 'Harmony');
+INSERT INTO subjects(id, description) VALUES ('music-therapy', 'Music Therapy');
+INSERT INTO subjects(id, description) VALUES ('songwriting', 'Songwriting');
+INSERT INTO subjects(id, description) VALUES ('conducting', 'Conducting');
+INSERT INTO subjects(id, description) VALUES ('instrument-repair', 'Instrument Repair');
+INSERT INTO subjects(id, description) VALUES ('improvisation', 'Improvisation');
+INSERT INTO subjects(id, description) VALUES ('pro-tools', 'Pro Tools');
+INSERT INTO subjects(id, description) VALUES ('ableton-live', 'Ableton Live');
+INSERT INTO subjects(id, description) VALUES ('fl-studio', 'FL Studio');
+INSERT INTO subjects(id, description) VALUES ('garageband', 'GarageBand');
+INSERT INTO subjects(id, description) VALUES ('apple-logic-pro', 'Apple Logic Pro');
+INSERT INTO subjects(id, description) VALUES ('presonus-studio-one', 'PreSonus Studio One');
+INSERT INTO subjects(id, description) VALUES ('reaper', 'Reaper');
+INSERT INTO subjects(id, description) VALUES ('cubase', 'Cubase');
+INSERT INTO subjects(id, description) VALUES ('sonar', 'Sonar');
+INSERT INTO subjects(id, description) VALUES ('reason', 'Reason');
+INSERT INTO subjects(id, description) VALUES ('amplitube', 'AmpliTube');
+INSERT INTO subjects(id, description) VALUES ('line-6-pod', 'Line 6 Pod');
+INSERT INTO subjects(id, description) VALUES ('guitar-ring', 'Guitar Rig');
\ No newline at end of file
diff --git a/db/up/user_origin.sql b/db/up/user_origin.sql
new file mode 100644
index 000000000..f778b731f
--- /dev/null
+++ b/db/up/user_origin.sql
@@ -0,0 +1,4 @@
+ALTER TABLE users ADD COLUMN origin_utm_source VARCHAR DEFAULT 'legacy';
+ALTER TABLE users ADD COLUMN origin_utm_medium VARCHAR;
+ALTER TABLE users ADD COLUMN origin_utm_campaign VARCHAR;
+ALTER TABLE users ADD COLUMN origin_referrer VARCHAR;
diff --git a/db/up/versionable_jamtracks.sql b/db/up/versionable_jamtracks.sql
new file mode 100644
index 000000000..9751bb940
--- /dev/null
+++ b/db/up/versionable_jamtracks.sql
@@ -0,0 +1 @@
+ALTER TABLE jam_track_rights ADD COLUMN version VARCHAR NOT NULL DEFAULT '0';
\ No newline at end of file
diff --git a/db/up/video_recording.sql b/db/up/video_recording.sql
new file mode 100644
index 000000000..46502b104
--- /dev/null
+++ b/db/up/video_recording.sql
@@ -0,0 +1,3 @@
+ALTER TABLE recordings ADD video BOOLEAN NOT NULL DEFAULT FALSE;
+ALTER TABLE user_authorizations ADD refresh_token VARCHAR;
+ALTER TABLE recordings ADD external_video_id VARCHAR;
\ No newline at end of file
diff --git a/db/up/web_playable_jamtracks.sql b/db/up/web_playable_jamtracks.sql
new file mode 100644
index 000000000..b3d6e8a4e
--- /dev/null
+++ b/db/up/web_playable_jamtracks.sql
@@ -0,0 +1,16 @@
+ALTER TABLE users ADD COLUMN first_opened_jamtrack_web_player TIMESTAMP;
+ALTER TABLE jam_track_rights ADD COLUMN last_stem_id VARCHAR(64) REFERENCES jam_track_tracks(id) ON DELETE SET NULL;
+ALTER TABLE jam_track_tracks ADD COLUMN url_mp3_48 VARCHAR;
+ALTER TABLE jam_track_tracks ADD COLUMN md5_mp3_48 VARCHAR;
+ALTER TABLE jam_track_tracks ADD COLUMN length_mp3_48 BIGINT;
+ALTER TABLE jam_track_tracks ADD COLUMN url_aac_48 VARCHAR;
+ALTER TABLE jam_track_tracks ADD COLUMN md5_aac_48 VARCHAR;
+ALTER TABLE jam_track_tracks ADD COLUMN length_aac_48 BIGINT;
+
+CREATE TABLE user_events (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id VARCHAR(64) REFERENCES users(id) ON DELETE SET NULL,
+ name VARCHAR(100) NOT NULL,
+ detail JSON,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
\ No newline at end of file
diff --git a/db/up/whitelist.sql b/db/up/whitelist.sql
new file mode 100644
index 000000000..9bb1eb97e
--- /dev/null
+++ b/db/up/whitelist.sql
@@ -0,0 +1,18 @@
+
+
+CREATE TABLE ip_whitelists (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ remote_ip VARCHAR(400) UNIQUE NOT NULL,
+ notes VARCHAR,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+
+CREATE TABLE user_whitelists (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id VARCHAR(64) UNIQUE NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ notes VARCHAR,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
\ No newline at end of file
diff --git a/pb/build_rprotoc b/pb/build_rprotoc
index e019cc519..f4c5555b1 100755
--- a/pb/build_rprotoc
+++ b/pb/build_rprotoc
@@ -5,10 +5,10 @@
RUBY_OUT=$TARGET/ruby
# we exit with 0; treat ruby as optional at the moment
-command -v "bundle" >/dev/null 2>&1 || { echo >&2 "bundle is required but not installed. Skipping ruby protocol buffers."; exit 0; }
+command -v "bundle" || { echo >&2 "bundle is required but not installed. Skipping ruby protocol buffers."; exit 0; }
# creates a bin folder with 'rprotoc' command inside
-bundle install --binstubs > /dev/null
+bundle install --binstubs
# die on error at this point
set -e
diff --git a/pb/package_ruby b/pb/package_ruby
index 46de2891d..282f4df38 100755
--- a/pb/package_ruby
+++ b/pb/package_ruby
@@ -5,7 +5,9 @@ pushd target/ruby > /dev/null
rm -rf jampb
-bundle gem jampb > /dev/null
+#bundle gem jampb > /dev/null
+echo "build gem jampb"
+bundle gem jampb
# copy over built ruby code
cp src/*.rb jampb/lib/jampb
@@ -57,7 +59,9 @@ end
EOF
+echo "gem build jampb.gemspec"
gem build jampb.gemspec > /dev/null
+#gem build jampb.gemspec > /dev/null
popd > /dev/null
popd > /dev/null
diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto
index 5d5ecf100..1db3e557a 100644
--- a/pb/src/client_container.proto
+++ b/pb/src/client_container.proto
@@ -11,6 +11,9 @@ message ClientMessage {
enum Type {
LOGIN = 100;
LOGIN_ACK = 105;
+ LOGOUT = 106;
+ LOGOUT_ACK = 107;
+ CONNECT_ACK = 108;
LOGIN_MUSIC_SESSION = 110;
LOGIN_MUSIC_SESSION_ACK = 115;
LEAVE_MUSIC_SESSION = 120;
@@ -21,6 +24,8 @@ message ClientMessage {
UNSUBSCRIBE = 137;
SUBSCRIPTION_MESSAGE = 138;
SUBSCRIBE_BULK = 139;
+ USER_STATUS = 141;
+ DIAGNOSTIC = 142;
// friend notifications
FRIEND_UPDATE = 140;
@@ -67,6 +72,7 @@ message ClientMessage {
// text message
TEXT_MESSAGE = 236;
CHAT_MESSAGE = 237;
+ SEND_CHAT_MESSAGE = 238;
MUSICIAN_SESSION_FRESH = 240;
MUSICIAN_SESSION_STALE = 245;
@@ -82,6 +88,13 @@ message ClientMessage {
JAM_TRACK_SIGN_COMPLETE = 260;
JAM_TRACK_SIGN_FAILED = 261;
+ // jamtracks mixdown notifications
+ MIXDOWN_SIGN_COMPLETE = 270;
+ MIXDOWN_SIGN_FAILED = 271;
+
+ LESSON_MESSAGE = 280;
+ SCHEDULED_JAMCLASS_INVITATION = 281;
+
TEST_SESSION_MESSAGE = 295;
PING_REQUEST = 300;
@@ -96,6 +109,9 @@ message ClientMessage {
RESTART_APPLICATION = 403;
STOP_APPLICATION = 404;
+ // jamblaster messages
+ PAIR_ATTEMPT = 500;
+
SERVER_BAD_STATE_RECOVERED = 900;
SERVER_GENERIC_ERROR = 1000;
@@ -117,6 +133,9 @@ message ClientMessage {
// Client-Server messages (to/from)
optional Login login = 100; // to server
optional LoginAck login_ack = 105; // from server
+ optional Logout logout = 106; // to server
+ optional LogoutAck logout_ack = 107; // from server
+ optional ConnectAck connect_ack = 108; // from server
optional LoginMusicSession login_music_session = 110; // to server
optional LoginMusicSessionAck login_music_session_ack = 115; // from server
optional LeaveMusicSession leave_music_session = 120;
@@ -127,6 +146,8 @@ message ClientMessage {
optional Unsubscribe unsubscribe = 137;
optional SubscriptionMessage subscription_message = 138;
optional SubscribeBulk subscribe_bulk = 139;
+ optional UserStatus user_status = 141;
+ optional Diagnostic diagnostic = 142;
// friend notifications
optional FriendUpdate friend_update = 140; // from server to all friends of user
@@ -174,6 +195,7 @@ message ClientMessage {
// text message
optional TextMessage text_message = 236;
optional ChatMessage chat_message = 237;
+ optional SendChatMessage send_chat_message = 238;
optional MusicianSessionFresh musician_session_fresh = 240;
optional MusicianSessionStale musician_session_stale = 245;
@@ -188,6 +210,13 @@ message ClientMessage {
optional JamTrackSignComplete jam_track_sign_complete = 260;
optional JamTrackSignFailed jam_track_sign_failed = 261;
+ // jamtrack mixdown notification
+ optional MixdownSignComplete mixdown_sign_complete = 270;
+ optional MixdownSignFailed mixdown_sign_failed = 271;
+
+ // lesson notifications
+ optional LessonMessage lesson_message = 280;
+ optional ScheduledJamclassInvitation scheduled_jamclass_invitation = 281;
// Client-Session messages (to/from)
optional TestSessionMessage test_session_message = 295;
@@ -205,6 +234,9 @@ message ClientMessage {
optional RestartApplication restart_application = 403;
optional StopApplication stop_application = 404;
+ // JamBlaster messages
+ optional PairAttempt pair_attempt = 500;
+
// Server-to-Client special messages
optional ServerBadStateRecovered server_bad_state_recovered = 900;
@@ -243,8 +275,31 @@ message LoginAck {
optional string user_id = 7; // the database user id
optional int32 connection_expire_time = 8; // this is how long the server gives you before killing your connection entirely after missing heartbeats
optional ClientUpdate client_update = 9;
+ optional string username = 10;
}
+message ConnectAck {
+ optional string public_ip = 1;
+ optional string client_id = 2; // a new client_id if none is supplied in Login, or just the original client_id echoed back
+ optional int32 heartbeat_interval = 3; // set your heartbeat interval to this value
+ optional int32 connection_expire_time = 4; // this is how long the server gives you before killing your connection entirely after missing heartbeats
+ optional ClientUpdate client_update = 5;
+}
+
+// route_to: server
+// a logout ack is always sent
+message Logout {
+
+}
+
+// route_to: client
+// sent from server to client to let the client know the login was successful,
+// and to also show the IP address of the client as seen by the server.
+message LogoutAck {
+ optional bool success = 1;
+}
+
+
// route_to: server
// send from client to server to log in to a music session and be 'present'.
// if successful, a LoginMusicSessionAck is sent with error = false, and Client-Session messages will be passed
@@ -355,6 +410,7 @@ message SessionJoin {
optional string msg = 3;
optional int32 track_changes_counter = 4;
optional string source_user_id = 5;
+ optional string client_id = 6;
}
message SessionDepart {
@@ -363,6 +419,8 @@ message SessionDepart {
optional string msg = 3;
optional string recording_id = 4;
optional int32 track_changes_counter = 5;
+ optional string client_id = 6;
+ optional string source_user_id = 7;
}
message TracksChanged {
@@ -560,6 +618,18 @@ message ChatMessage {
optional string msg = 3;
optional string msg_id = 4;
optional string created_at = 5;
+ 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 {
+ optional string msg = 1;
+ optional string channel = 2;
}
// route_to: client:
@@ -612,6 +682,40 @@ message JamTrackSignFailed {
required int32 jam_track_right_id = 1; // jam track right id
}
+message MixdownSignComplete {
+ required string mixdown_package_id = 1; // jam track mixdown package id
+}
+
+message MixdownSignFailed {
+ required string mixdown_package_id = 1; // jam track mixdown package id
+}
+
+message LessonMessage {
+ optional string music_session_id = 1;
+ optional string photo_url = 2;
+ optional string msg = 3;
+ optional string notification_id = 4;
+ optional string created_at = 5;
+ optional string sender_id = 6;
+ optional string receiver_id = 7;
+ optional bool student_directed = 8;
+ optional string purpose = 9;
+ optional string sender_name = 10;
+ optional string lesson_session_id = 11;
+}
+
+message ScheduledJamclassInvitation {
+ optional string session_id = 1;
+ optional string photo_url = 2;
+ optional string msg = 3;
+ optional string session_name = 4;
+ optional string session_date = 5;
+ optional string notification_id = 6;
+ optional string created_at = 7;
+ optional string lesson_session_id = 8;
+}
+
+
message SubscriptionMessage {
optional string type = 1; // the type of the subscription
optional string id = 2; // data about what to subscribe to, specifically
@@ -639,6 +743,16 @@ message SubscribeBulk {
//repeated Subscription subscriptions = 1; # the ruby protocol buffer library chokes on this. so we have to do the above
}
+message UserStatus {
+ optional bool active = 1; // same as heartbeat 'active'... does the user appear present
+ optional string status = 2;
+}
+
+
+message Diagnostic {
+ optional string message = 1;
+}
+
// route_to: session
// a test message used by ruby-client currently. just gives way to send out to rest of session
message TestSessionMessage {
@@ -675,6 +789,7 @@ message TestClientMessage {
message Heartbeat {
optional string notification_seen = 1;
optional string notification_seen_at = 2;
+ optional bool active = 3; // is the user active?
}
// target: client
@@ -716,6 +831,11 @@ message StopApplication {
}
+message PairAttempt {
+ optional string scid = 1;
+ optional string vtoken = 2;
+}
+
// route_to: client
// this should follow a ServerBadStateError in the case that the
// websocket gateway recovers from whatever ailed it
diff --git a/ruby/Gemfile b/ruby/Gemfile
index b5c229e8e..e1b13151c 100644
--- a/ruby/Gemfile
+++ b/ruby/Gemfile
@@ -32,6 +32,7 @@ gem 'bcrypt-ruby', '3.0.1'
gem 'ruby-protocol-buffers', '1.2.2'
gem 'eventmachine', '1.0.4'
gem 'amqp', '1.0.2'
+gem 'kickbox'
gem 'will_paginate'
gem 'sendgrid', '1.2.0'
gem 'aws-sdk', '~> 1'
@@ -54,8 +55,13 @@ gem 'rest-client'
gem 'iso-639'
gem 'rubyzip'
gem 'sanitize'
-gem 'influxdb', '0.1.8'
+#gem 'influxdb'
gem 'recurly'
+gem 'sendgrid_toolkit', '>= 1.1.1'
+gem 'stripe'
+gem 'zip-codes'
+gem 'icalendar'
+gem 'email_validator'
group :test do
gem 'simplecov', '~> 0.7.1'
@@ -70,7 +76,8 @@ group :test do
gem 'rspec-prof'
gem 'time_difference'
gem 'byebug'
- gem 'icalendar'
+ gem 'stripe-ruby-mock'
+
end
# Specify your gem's dependencies in jam_ruby.gemspec
diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb
index 4e21009ce..45fc7b857 100755
--- a/ruby/lib/jam_ruby.rb
+++ b/ruby/lib/jam_ruby.rb
@@ -26,15 +26,19 @@ require 'rest-client'
require 'zip'
require 'csv'
require 'tzinfo'
+require 'stripe'
+require 'zip-codes'
+require 'email_validator'
ActiveRecord::Base.raise_in_transactional_callbacks = true
-
+require "jam_ruby/lib/timezone"
require "jam_ruby/constants/limits"
require "jam_ruby/constants/notification_types"
require "jam_ruby/constants/validation_messages"
require "jam_ruby/errors/jam_permission_error"
require "jam_ruby/errors/state_error"
require "jam_ruby/errors/jam_argument_error"
+require "jam_ruby/errors/jam_record_not_found"
require "jam_ruby/errors/conflict_error"
require "jam_ruby/lib/app_config"
require "jam_ruby/lib/s3_manager_mixin"
@@ -59,6 +63,8 @@ require "jam_ruby/resque/scheduled/cleanup_facebook_signup"
require "jam_ruby/resque/scheduled/unused_music_notation_cleaner"
require "jam_ruby/resque/scheduled/user_progress_emailer"
require "jam_ruby/resque/scheduled/daily_job"
+require "jam_ruby/resque/scheduled/hourly_job"
+require "jam_ruby/resque/scheduled/minutely_job"
require "jam_ruby/resque/scheduled/daily_session_emailer"
require "jam_ruby/resque/scheduled/new_musician_emailer"
require "jam_ruby/resque/scheduled/music_session_reminder"
@@ -71,6 +77,7 @@ require "jam_ruby/resque/scheduled/jam_tracks_cleaner"
require "jam_ruby/resque/scheduled/stats_maker"
require "jam_ruby/resque/scheduled/tally_affiliates"
require "jam_ruby/resque/jam_tracks_builder"
+require "jam_ruby/resque/jam_track_mixdown_packager"
require "jam_ruby/resque/google_analytics_event"
require "jam_ruby/resque/batch_email_job"
require "jam_ruby/resque/long_running"
@@ -112,12 +119,21 @@ require "jam_ruby/models/max_mind_release"
require "jam_ruby/models/genre_player"
require "jam_ruby/models/genre"
require "jam_ruby/models/user"
+require "jam_ruby/models/user_event"
require "jam_ruby/models/anonymous_user"
require "jam_ruby/models/signup_hint"
require "jam_ruby/models/machine_fingerprint"
require "jam_ruby/models/machine_extra"
+require "jam_ruby/models/download_tracker"
+require "jam_ruby/models/ip_blacklist"
+require "jam_ruby/models/user_blacklist"
+require "jam_ruby/models/email_blacklist"
+require "jam_ruby/models/ip_whitelist"
+require "jam_ruby/models/user_whitelist"
require "jam_ruby/models/fraud_alert"
require "jam_ruby/models/fingerprint_whitelist"
+require "jam_ruby/models/review"
+require "jam_ruby/models/review_summary"
require "jam_ruby/models/rsvp_request"
require "jam_ruby/models/rsvp_slot"
require "jam_ruby/models/rsvp_request_rsvp_slot"
@@ -216,6 +232,8 @@ require "jam_ruby/models/jam_track_track"
require "jam_ruby/models/jam_track_right"
#require "jam_ruby/models/jam_track_tap_in" # consider deletion
require "jam_ruby/models/jam_track_file"
+require "jam_ruby/models/jam_track_mixdown"
+require "jam_ruby/models/jam_track_mixdown_package"
require "jam_ruby/models/genre_jam_track"
require "jam_ruby/app/mailers/async_mailer"
require "jam_ruby/app/mailers/batch_mailer"
@@ -236,11 +254,16 @@ require "jam_ruby/models/user_sync"
require "jam_ruby/models/payment_history"
require "jam_ruby/models/video_source"
require "jam_ruby/models/text_message"
+require "jam_ruby/models/news"
require "jam_ruby/models/sale"
require "jam_ruby/models/sale_line_item"
require "jam_ruby/models/recurly_transaction_web_hook"
require "jam_ruby/models/broadcast_notification"
require "jam_ruby/models/broadcast_notification_view"
+require "jam_ruby/models/test_drive_package"
+require "jam_ruby/models/test_drive_package_teacher"
+require "jam_ruby/models/test_drive_package_choice"
+require "jam_ruby/models/test_drive_package_choice_teacher"
require "jam_ruby/calendar_manager"
require "jam_ruby/jam_tracks_manager"
require "jam_ruby/jam_track_importer"
@@ -250,9 +273,44 @@ require "jam_ruby/models/online_presence"
require "jam_ruby/models/json_store"
require "jam_ruby/models/base_search"
require "jam_ruby/models/musician_search"
+require "jam_ruby/models/teacher"
+require "jam_ruby/models/teacher_experience"
+require "jam_ruby/models/language"
+require "jam_ruby/models/subject"
require "jam_ruby/models/band_search"
require "jam_ruby/import/tency_stem_mapping"
-
+require "jam_ruby/models/jam_track_search"
+require "jam_ruby/models/gift_card"
+require "jam_ruby/models/gift_card_purchase"
+require "jam_ruby/models/gift_card_type"
+require "jam_ruby/models/jam_track_session"
+require "jam_ruby/models/lesson_package_type"
+require "jam_ruby/models/lesson_package_purchase"
+require "jam_ruby/models/lesson_session"
+require "jam_ruby/models/lesson_booking"
+require "jam_ruby/models/lesson_booking_slot"
+require "jam_ruby/models/jamblaster"
+require "jam_ruby/models/jamblaster_user"
+require "jam_ruby/models/jamblaster_pairing_request"
+require "jam_ruby/models/sale_receipt_ios"
+require "jam_ruby/models/lesson_session_analyser"
+require "jam_ruby/models/lesson_session_monthly_price"
+require "jam_ruby/models/teacher_distribution"
+require "jam_ruby/models/teacher_payment"
+require "jam_ruby/models/charge"
+require "jam_ruby/models/teacher_payment_charge"
+require "jam_ruby/models/affiliate_payment_charge"
+require "jam_ruby/models/lesson_payment_charge"
+require "jam_ruby/models/affiliate_distribution"
+require "jam_ruby/models/teacher_intent"
+require "jam_ruby/models/school"
+require "jam_ruby/models/school_invitation"
+require "jam_ruby/models/teacher_instrument"
+require "jam_ruby/models/teacher_subject"
+require "jam_ruby/models/teacher_language"
+require "jam_ruby/models/teacher_genre"
+require "jam_ruby/models/jam_class_report"
+require "jam_ruby/models/campaign_spend"
include Jampb
module JamRuby
diff --git a/ruby/lib/jam_ruby/app/assets/sounds/knock44.wav b/ruby/lib/jam_ruby/app/assets/sounds/knock44.wav
new file mode 100644
index 000000000..33c1aea5f
Binary files /dev/null and b/ruby/lib/jam_ruby/app/assets/sounds/knock44.wav differ
diff --git a/ruby/lib/jam_ruby/app/assets/sounds/knock48.wav b/ruby/lib/jam_ruby/app/assets/sounds/knock48.wav
new file mode 100644
index 000000000..3a3fd3d68
Binary files /dev/null and b/ruby/lib/jam_ruby/app/assets/sounds/knock48.wav differ
diff --git a/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb b/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb
index f14d8e424..993b9f81b 100644
--- a/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb
+++ b/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb
@@ -1,6 +1,6 @@
module JamRuby
# sends out a boring ale
- class AdminMailer < ActionMailer::Base
+ class AdminMailer < ActionMailer::Base
include SendGrid
@@ -20,6 +20,30 @@ module JamRuby
subject: options[:subject])
end
+ def crash_alert(options)
+ mail(to: APP_CONFIG.email_crashes_alias,
+ from: APP_CONFIG.email_generic_from,
+ body: options[:body],
+ content_type: "text/plain",
+ subject: options[:subject])
+ end
+
+ def social(options)
+ mail(to: APP_CONFIG.email_social_alias,
+ from: options[:from] || APP_CONFIG.email_generic_from,
+ body: options[:body],
+ content_type: "text/plain",
+ subject: options[:subject])
+ end
+
+ def partner(options)
+ mail(to: APP_CONFIG.email_partners_alias,
+ from: APP_CONFIG.email_generic_from,
+ body: options[:body],
+ content_type: "text/plain",
+ subject: options[:subject])
+ end
+
def recurly_alerts(user, options)
body = options[:body]
diff --git a/ruby/lib/jam_ruby/app/mailers/auto_mailer.rb b/ruby/lib/jam_ruby/app/mailers/auto_mailer.rb
new file mode 100644
index 000000000..05f144821
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/mailers/auto_mailer.rb
@@ -0,0 +1,23 @@
+module JamRuby
+ # UserMailer must be configured to work
+ # Some common configs occur in jam_ruby/init.rb
+ # Environment specific configs occur in spec_helper.rb in jam-ruby and jam-web (to put it into test mode),
+ # and in config/initializers/email.rb in rails to configure sendmail account settings
+ # If UserMailer were to be used in another project, it would need to be configured there, as well.
+
+ # Templates for UserMailer can be found in jam_ruby/app/views/jam_ruby/user_mailer
+ class UserMailer < ActionMailer::Base
+ include SendGrid
+
+ layout "auto_mailer"
+
+ DEFAULT_SENDER = "JamKazam "
+
+ default :from => DEFAULT_SENDER
+
+ sendgrid_category :use_subject_lines
+ #sendgrid_enable :opentrack, :clicktrack # this makes our emails creepy, imo (seth)
+ sendgrid_unique_args :env => Environment.mode
+
+ end
+end
diff --git a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb
index 40d5480f0..b5f055419 100644
--- a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb
+++ b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb
@@ -1,606 +1,1797 @@
- module JamRuby
- # UserMailer must be configured to work
- # Some common configs occur in jam_ruby/init.rb
- # Environment specific configs occur in spec_helper.rb in jam-ruby and jam-web (to put it into test mode),
- # and in config/initializers/email.rb in rails to configure sendmail account settings
- # If UserMailer were to be used in another project, it would need to be configured there, as well.
+module JamRuby
+ # UserMailer must be configured to work
+ # Some common configs occur in jam_ruby/init.rb
+ # Environment specific configs occur in spec_helper.rb in jam-ruby and jam-web (to put it into test mode),
+ # and in config/initializers/email.rb in rails to configure sendmail account settings
+ # If UserMailer were to be used in another project, it would need to be configured there, as well.
- # Templates for UserMailer can be found in jam_ruby/app/views/jam_ruby/user_mailer
- class UserMailer < ActionMailer::Base
- include SendGrid
+ # Templates for UserMailer can be found in jam_ruby/app/views/jam_ruby/user_mailer
+ class UserMailer < ActionMailer::Base
+ include SendGrid
- layout "user_mailer"
+ layout "user_mailer"
- DEFAULT_SENDER = "JamKazam "
+ DEFAULT_SENDER = "JamKazam "
- default :from => DEFAULT_SENDER
+ default :from => DEFAULT_SENDER
- sendgrid_category :use_subject_lines
- #sendgrid_enable :opentrack, :clicktrack # this makes our emails creepy, imo (seth)
- sendgrid_unique_args :env => Environment.mode
+ sendgrid_category :use_subject_lines
+ #sendgrid_enable :opentrack, :clicktrack # this makes our emails creepy, imo (seth)
+ sendgrid_unique_args :env => Environment.mode
- def confirm_email(user, signup_confirm_url)
- @user = user
- @signup_confirm_url = signup_confirm_url
- sendgrid_category "Confirm Email"
- sendgrid_unique_args :type => "confirm_email"
+ def confirm_email(user, signup_confirm_url)
+ @user = user
+ @signup_confirm_url = signup_confirm_url
+ sendgrid_category "Confirm Email"
+ sendgrid_unique_args :type => "confirm_email"
- sendgrid_recipients([user.email])
- sendgrid_substitute('@USERID', [user.id])
+ sendgrid_recipients([user.email])
+ sendgrid_substitute('@USERID', [user.id])
- mail(:to => user.email, :subject => "Please confirm your JamKazam email") do |format|
- format.text
- format.html
+ mail(:to => user.email, :subject => "Please confirm your JamKazam email") do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def welcome_message(user)
+ @user = user
+ sendgrid_category "Welcome"
+ sendgrid_unique_args :type => "welcome_message"
+
+ sendgrid_recipients([user.email])
+ sendgrid_substitute('@USERID', [user.id])
+ sendgrid_substitute(EmailBatchProgression::VAR_FIRST_NAME, [user.first_name])
+
+ mail(:to => user.email, :subject => "Welcome to JamKazam") do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def student_welcome_message(user)
+ @user = user
+ @subject = "Welcome to JamKazam and JamClass online lessons!"
+ sendgrid_category "Welcome"
+ sendgrid_unique_args :type => "welcome_message"
+
+ sendgrid_recipients([user.email])
+ sendgrid_substitute('@USERID', [user.id])
+ sendgrid_substitute(EmailBatchProgression::VAR_FIRST_NAME, [user.first_name])
+
+ mail(:to => user.email, :subject => @subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def teacher_welcome_message(user)
+ @user = user
+ @subject= "Welcome to JamKazam and JamClass online lessons!"
+ sendgrid_category "Welcome"
+ sendgrid_unique_args :type => "welcome_message"
+
+ sendgrid_recipients([user.email])
+ sendgrid_substitute('@USERID', [user.id])
+ sendgrid_substitute(EmailBatchProgression::VAR_FIRST_NAME, [user.first_name])
+
+ mail(:to => user.email, :subject => @subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def school_owner_welcome_message(user)
+ @user = user
+ @subject= "Welcome to JamKazam and JamClass online lessons!"
+ sendgrid_category "Welcome"
+ sendgrid_unique_args :type => "welcome_message"
+
+ sendgrid_recipients([user.email])
+ sendgrid_substitute('@USERID', [user.id])
+ sendgrid_substitute(EmailBatchProgression::VAR_FIRST_NAME, [user.first_name])
+
+ mail(:to => user.email, :subject => @subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def password_changed(user)
+ @user = user
+
+ sendgrid_recipients([user.email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ sendgrid_unique_args :type => "password_changed"
+ mail(:to => user.email, :subject => "JamKazam Password Changed") do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def password_reset(user, password_reset_url)
+ @user = user
+
+ sendgrid_recipients([user.email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ @password_reset_url = password_reset_url
+ sendgrid_unique_args :type => "password_reset"
+ mail(:to => user.email, :subject => "JamKazam Password Reset") do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def updating_email(user)
+ @user = user
+
+ sendgrid_recipients([user.email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ sendgrid_unique_args :type => "updating_email"
+ mail(:to => user.update_email, :subject => "JamKazam Email Change Confirmation") do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def updated_email(user)
+ @user = user
+
+ sendgrid_recipients([user.email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ sendgrid_unique_args :type => "updated_email"
+ mail(:to => user.email, :subject => "JamKazam Email Changed") do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def new_musicians(user, new_musicians, host='www.jamkazam.com')
+ @user, @new_musicians, @host = user, new_musicians, host
+ sendgrid_recipients([user.email])
+ sendgrid_substitute('@USERID', [user.id])
+ sendgrid_unique_args :type => "new_musicians"
+
+ mail(:to => user.email, :subject => EmailBatchNewMusician.subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ #################################### NOTIFICATION EMAILS ####################################
+ def friend_request(user, msg, friend_request_id)
+ return if !user.subscribe_email
+
+ email = user.email
+ subject = "You have a new friend request on JamKazam"
+ unique_args = {:type => "friend_request"}
+
+ @url = Nav.accept_friend_request_dialog(friend_request_id)
+ @body = msg
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def friend_request_accepted(user, msg)
+ return if !user.subscribe_email
+
+ email = user.email
+ subject = "You have a new friend on JamKazam"
+ unique_args = {:type => "friend_request_accepted"}
+
+ @body = msg
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def new_user_follower(user, msg)
+ return if !user.subscribe_email
+
+ email = user.email
+ subject = "You have a new follower on JamKazam"
+ unique_args = {:type => "new_user_follower"}
+
+ @body = msg
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def new_band_follower(user, msg)
+ return if !user.subscribe_email
+
+ email = user.email
+ subject = "Your band has a new follower on JamKazam"
+ unique_args = {:type => "new_band_follower"}
+
+ @body = msg
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def session_invitation(user, msg)
+ return if !user.subscribe_email
+
+ email = user.email
+ subject = "You have been invited to a session on JamKazam"
+ unique_args = {:type => "session_invitation"}
+
+ @body = msg
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def musician_session_join(user, msg, session_id)
+ return if !user.subscribe_email
+
+ email = user.email
+ subject = "Someone you know is in a session on JamKazam"
+ unique_args = {:type => "musician_session_join"}
+ @body = msg
+ @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session_id}"
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def scheduled_session_invitation(user, msg, session)
+ return if !user.subscribe_email
+
+ email = user.email
+ subject = "Session Invitation"
+ unique_args = {:type => "scheduled_session_invitation"}
+ @body = msg
+ @session_name = session.name
+ @session_date = session.pretty_scheduled_start(true)
+ @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details"
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def scheduled_session_rsvp(user, msg, session)
+ return if !user.subscribe_email
+
+ email = user.email
+ subject = "Session RSVP"
+ unique_args = {:type => "scheduled_session_rsvp"}
+ @body = msg
+ @session_name = session.name
+ @session_date = session.pretty_scheduled_start(true)
+ @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details"
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def scheduled_session_rsvp_approved(user, msg, session)
+ return if !user.subscribe_email
+
+ email = user.email
+ subject = "Session RSVP Approved"
+ unique_args = {:type => "scheduled_session_rsvp_approved"}
+ @body = msg
+ @session_name = session.name
+ @session_date = session.pretty_scheduled_start(true)
+ @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details"
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def scheduled_session_rsvp_cancelled(user, msg, session)
+ return if !user.subscribe_email
+
+ email = user.email
+ subject = "Session RSVP Cancelled"
+ unique_args = {:type => "scheduled_session_rsvp_cancelled"}
+ @body = msg
+ @session_name = session.name
+ @session_date = session.pretty_scheduled_start(true)
+ @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details"
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def scheduled_session_rsvp_cancelled_org(user, msg, session)
+ return if !user.subscribe_email
+
+ email = user.email
+ subject = "Your Session RSVP Cancelled"
+ unique_args = {:type => "scheduled_session_rsvp_cancelled_org"}
+ @body = msg
+ @session_name = session.name
+ @session_date = session.pretty_scheduled_start(true)
+ @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details"
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def scheduled_session_cancelled(user, msg, session)
+ return if !user.subscribe_email
+
+ email = user.email
+ subject = "Session Cancelled"
+ unique_args = {:type => "scheduled_session_cancelled"}
+ @body = msg
+ @session_name = session.name
+ @session_date = session.pretty_scheduled_start(true)
+ @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details"
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def scheduled_session_rescheduled(user, msg, session)
+ return if !user.subscribe_email
+
+ email = user.email
+ subject = "Session Rescheduled"
+ unique_args = {:type => "scheduled_session_rescheduled"}
+ @body = msg
+ @session_name = session.name
+ @session_date = session.pretty_scheduled_start(true)
+ @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details"
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def scheduled_session_reminder_upcoming(user, session)
+ subject = "Your JamKazam session starts in 1 hour!"
+ unique_args = {:type => "scheduled_session_reminder_upcoming"}
+ send_scheduled_session_reminder(user, session, subject, unique_args)
+ end
+
+ def scheduled_session_reminder_day(user, session)
+ subject = "JamKazam Session Reminder"
+ unique_args = {:type => "scheduled_session_reminder_day"}
+ send_scheduled_session_reminder(user, session, subject, unique_args)
+ end
+
+ def send_scheduled_session_reminder(user, session, subject, unique_args)
+ return if !user.subscribe_email
+
+ email = user.email
+ @user = user
+ @session_name = session.name
+ @session_date = session.pretty_scheduled_start(true)
+ @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details"
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def scheduled_session_comment(target_user, sender, msg, comment, session)
+ return if !target_user.subscribe_email
+
+ email = target_user.email
+ subject = "New Session Comment"
+ unique_args = {:type => "scheduled_session_comment"}
+ @body = msg
+ @session_name = session.name
+ @session_date = session.pretty_scheduled_start(true)
+ @comment = comment
+ @sender = sender
+ @suppress_user_has_account_footer = true
+ @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details"
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [target_user.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ def scheduled_session_daily(receiver, sessions_and_latency)
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => "scheduled_session_daily"
+
+ sendgrid_recipients([receiver.email])
+ sendgrid_substitute('@USERID', [receiver.id])
+
+ @user = receiver
+ @sessions_and_latency = sessions_and_latency
+
+ @title = 'New Scheduled Sessions Matched to You'
+ mail(:to => receiver.email,
+ :subject => EmailBatchScheduledSessions.subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def band_session_join(user, msg, session_id)
+ return if !user.subscribe_email
+
+ email = user.email
+ subject = "A band that you follow has joined a session"
+ unique_args = {:type => "band_session_join"}
+
+ @body = msg
+ @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session_id}"
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def musician_recording_saved(user, msg)
+ return if !user.subscribe_email
+
+ email = user.email
+ subject = "A musician has saved a new recording on JamKazam"
+ unique_args = {:type => "musician_recording_saved"}
+
+ @body = msg
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def band_recording_saved(user, msg)
+ return if !user.subscribe_email
+
+ email = user.email
+ subject = "A band has saved a new recording on JamKazam"
+ unique_args = {:type => "band_recording_saved"}
+
+ @body = msg
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def band_invitation(user, msg)
+ return if !user.subscribe_email
+
+ email = user.email
+ subject = "You have been invited to join a band on JamKazam"
+ unique_args = {:type => "band_invitation"}
+
+ @body = msg
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def band_invitation_accepted(user, msg)
+ return if !user.subscribe_email
+
+ email = user.email
+ subject = "Your band invitation was accepted"
+ unique_args = {:type => "band_invitation_accepted"}
+
+ @body = msg
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+
+ def text_message(user, sender_id, sender_name, sender_photo_url, message)
+ return if !user.subscribe_email
+
+ email = user.email
+ subject = "Message from #{sender_name}"
+ unique_args = {:type => "text_message"}
+
+ @note = message
+ @url = Nav.home(dialog: 'text-message', dialog_opts: {d1: sender_id})
+ @sender_id = sender_id
+ @sender_name = sender_name
+ @sender_photo_url = sender_photo_url
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ def student_lesson_request(lesson_booking)
+ email = lesson_booking.user.email
+ subject = "You have sent a lesson request to #{lesson_booking.teacher.name}!"
+ unique_args = {:type => "student_lesson_request"}
+
+ @sender = lesson_booking.teacher
+ @lesson_booking = lesson_booking
+
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [lesson_booking.user.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ # teacher
+ def teacher_lesson_request(lesson_booking)
+ email = lesson_booking.school_over_teacher
+ subject = "You have received a lesson request through JamKazam!"
+ unique_args = {:type => "teacher_lesson_request"}
+
+ @sender = lesson_booking.user
+ @lesson_booking = lesson_booking
+
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients(email)
+ sendgrid_substitute('@USERID', lesson_booking.school_over_teacher_ids)
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ def student_lesson_accepted(lesson_session, message, slot)
+ @slot = slot
+ if slot.is_teacher_approved?
+ @target = lesson_session.student
+ @sender = lesson_session.teacher
+ @subject = "Your lesson request is confirmed!"
+ else
+ @target = lesson_session.teacher
+ @sender = lesson_session.student
+ @subject = "Your have confirmed a lesson!"
+ end
+ @lesson_session = lesson_session
+ @message = message
+ email = lesson_session.student.email
+ unique_args = {:type => "student_lesson_accepted"}
+
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [lesson_session.student.id])
+
+ mail(:to => email, :subject => @subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ def teacher_lesson_accepted(lesson_session, message, slot)
+ @slot = slot
+ if slot.is_teacher_approved?
+ @target = lesson_session.student
+ @sender = lesson_session.teacher
+ @subject = "You have confirmed a lesson!"
+ else
+ @target = lesson_session.teacher
+ @sender = lesson_session.student
+ @subject = "Your lesson time change is confirmed by #{lesson_session.student.name}!"
+ end
+
+ @lesson_session = lesson_session
+ @message = message
+ email = lesson_session.school_and_teacher
+ unique_args = {:type => "teacher_lesson_accepted"}
+
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients(email)
+ sendgrid_substitute('@USERID', lesson_session.school_and_teacher_ids)
+
+ mail(:to => email, :subject => @subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ def student_lesson_update_all(lesson_session, message, slot)
+ @slot = slot
+ if slot.is_teacher_created?
+ @target = lesson_session.teacher
+ @sender = lesson_session.student
+ subject = "All lesson times changed with #{lesson_session.teacher.name}!"
+ else
+ @target = lesson_session.student
+ @sender = lesson_session.teacher
+ subject = "All lesson times changed with #{lesson_session.teacher.name}!"
+ end
+ @lesson_session = lesson_session
+ @message = message
+ email = lesson_session.student.email
+ unique_args = {:type => "student_lesson_accepted"}
+
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [lesson_session.student.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ def teacher_lesson_update_all(lesson_session, message, slot)
+ @slot = slot
+ if slot.is_teacher_created?
+ @target = lesson_session.teacher
+ @sender = lesson_session.student
+ subject = "All lesson times changed with #{lesson_session.student.name}!"
+ else
+ @target = lesson_session.student
+ @sender = lesson_session.teacher
+ subject = "All lesson times changed with #{lesson_session.student.name}!"
+ end
+
+ @lesson_session = lesson_session
+ @message = message
+ email = lesson_session.school_and_teacher
+ unique_args = {:type => "teacher_lesson_update_all"}
+
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients(email)
+ sendgrid_substitute('@USERID', lesson_session.school_and_teacher_ids)
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ def teacher_scheduled_jamclass_invitation(user, msg, session)
+
+ email = user.email
+ @subject = "#{session.lesson_session.lesson_booking.display_type2.capitalize} JamClass Scheduled with #{session.lesson_session.student.name}"
+ unique_args = {:type => "scheduled_jamclass_invitation"}
+ @student = session.lesson_session.student
+ @teacher = session.lesson_session.teacher
+ @body = msg
+ @session_name = session.name
+ @session_description = session.description
+ @session_date = session.pretty_scheduled_start(true)
+ @session_url = session.lesson_session.web_url
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ mail(:to => email, :subject => @subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def student_scheduled_jamclass_invitation(user, msg, session)
+ return if !user.subscribe_email
+
+ email = user.email
+ @subject = "#{session.lesson_session.lesson_booking.display_type2.capitalize} JamClass Scheduled with #{session.lesson_session.teacher.name}"
+ unique_args = {:type => "scheduled_jamclass_invitation"}
+ @student = session.lesson_session.student
+ @teacher = session.lesson_session.teacher
+ @body = msg
+ @session_name = session.name
+ @session_description = session.description
+ @session_date = session.pretty_scheduled_start(true)
+ @session_url = session.lesson_session.web_url
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [user.id])
+
+ mail(:to => email, :subject => @subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ # teacher proposed counter time; so send msg to the student
+ def student_lesson_counter(lesson_session, slot)
+
+ email = lesson_session.student.email
+ subject = "Instructor has proposed a different time for your lesson"
+ unique_args = {:type => "student_lesson_counter"}
+ @student = lesson_session.student
+ @teacher = lesson_session.teacher
+ @session_name = lesson_session.music_session.name
+ @session_description = lesson_session.music_session.description
+ @session_date = slot.pretty_scheduled_start(true)
+ @session_url = lesson_session.web_url
+ @lesson_session = lesson_session
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [@student.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ # student proposed counter time; so send msg to the teacher
+ def teacher_lesson_counter(lesson_session, slot)
+
+ email = lesson_session.school_over_teacher
+ subject = "Student has proposed a different time for their lesson"
+ unique_args = {:type => "teacher_lesson_counter"}
+ @student = lesson_session.student
+ @teacher = lesson_session.teacher
+ @session_name = lesson_session.music_session.name
+ @session_description = lesson_session.music_session.description
+ @session_date = slot.pretty_scheduled_start(true)
+ @session_url = lesson_session.web_url
+ @lesson_session = lesson_session
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients(email)
+ sendgrid_substitute('@USERID', lesson_session.school_over_teacher_ids)
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ def teacher_lesson_completed(lesson_session)
+ @student = lesson_session.student
+ @teacher = lesson_session.teacher
+ @session_name = lesson_session.music_session.name
+ @session_description = lesson_session.music_session.description
+ @session_date = lesson_session.slot.pretty_scheduled_start(true)
+ @session_url = lesson_session.web_url
+ @lesson_session = lesson_session
+ email = lesson_session.school_and_teacher
+ if @lesson_session.student_missed
+ subject = "You will be paid for your lesson with #{@student.name}"
+ else
+ subject = "You successfully completed a lesson with #{@student.name}"
+ end
+
+
+ unique_args = {:type => "teacher_lesson_completed"}
+
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients(email)
+ sendgrid_substitute('@USERID', lesson_session.school_and_teacher_ids)
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ # successfully completed, and has some remaining test drives
+ def student_test_drive_lesson_completed(lesson_session)
+
+ @student = lesson_session.student
+ @teacher = lesson_session.teacher
+ @session_name = lesson_session.music_session.name
+ @session_description = lesson_session.music_session.description
+ @session_date = lesson_session.slot.pretty_scheduled_start(true)
+ @session_url = lesson_session.web_url
+ @lesson_session = lesson_session
+
+ email = @student.email
+ 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"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [@student.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ def teacher_test_drive_no_bill(lesson_session)
+ @student = lesson_session.student
+ @teacher = lesson_session.teacher
+ @session_name = lesson_session.music_session.name
+ @session_description = lesson_session.music_session.description
+ @session_date = lesson_session.slot.pretty_scheduled_start(true)
+ @session_url = lesson_session.web_url
+ @lesson_session = lesson_session
+
+ email = lesson_session.school_and_teacher
+ subject = "Your TestDrive with #{@student.name} was not successful"
+ unique_args = {:type => "teacher_test_drive_no_bill"}
+
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients(email)
+ sendgrid_substitute('@USERID', lesson_session.school_and_teacher_ids)
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ def student_test_drive_no_bill(lesson_session)
+ @student = lesson_session.student
+ @teacher = lesson_session.teacher
+ @session_name = lesson_session.music_session.name
+ @session_description = lesson_session.music_session.description
+ @session_date = lesson_session.slot.pretty_scheduled_start(true)
+ @session_url = lesson_session.web_url
+ @lesson_session = lesson_session
+
+ email = @student.email
+ subject = "Your TestDrive with #{@teacher.name} will not use a credit"
+ unique_args = {:type => "student_test_drive_no_bill"}
+
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [@student.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ # successfully completed, but no more test drives left
+ def student_test_drive_lesson_done(lesson_session)
+
+ @student = lesson_session.student
+ @teacher = lesson_session.teacher
+ @session_name = lesson_session.music_session.name
+ @session_description = lesson_session.music_session.description
+ @session_date = lesson_session.slot.pretty_scheduled_start(true)
+ @session_url = lesson_session.web_url
+ @lesson_session = lesson_session
+
+ email = @student.email
+ subject = "You have used all TestDrive lesson credits"
+ unique_args = {:type => "student_test_drive_success"}
+
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [@student.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html { render :layout => "raw_mailer" }
+ end
+ end
+
+ def student_lesson_normal_no_bill(lesson_session)
+ @student = lesson_session.student
+ @teacher = lesson_session.teacher
+ @session_name = lesson_session.music_session.name
+ @session_description = lesson_session.music_session.description
+ @session_date = lesson_session.slot.pretty_scheduled_start(true)
+ @session_url = lesson_session.web_url
+ @lesson_session = lesson_session
+
+ email = @student.email
+ subject = "Your lesson with #{@teacher.name} will not be billed"
+ unique_args = {:type => "student_lesson_normal_no_bill"}
+
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [@student.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ def teacher_lesson_normal_no_bill(lesson_session)
+ @student = lesson_session.student
+ @teacher = lesson_session.teacher
+ @session_name = lesson_session.music_session.name
+ @session_description = lesson_session.music_session.description
+ @session_date = lesson_session.slot.pretty_scheduled_start(true)
+ @session_url = lesson_session.web_url
+ @lesson_session = lesson_session
+ email = lesson_session.school_and_teacher
+ subject = "Your student #{@student.name} will not be charged for their lesson"
+ unique_args = {:type => "teacher_lesson_normal_no_bill"}
+
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients(email)
+ sendgrid_substitute('@USERID', lesson_session.school_and_teacher_ids)
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ def student_lesson_normal_done(lesson_session)
+ @student = lesson_session.student
+ @teacher = lesson_session.teacher
+ @session_name = lesson_session.music_session.name
+ @session_description = lesson_session.music_session.description
+ @session_date = lesson_session.slot.pretty_scheduled_start(true)
+ @session_url = lesson_session.web_url
+ @lesson_session = lesson_session
+
+ email = @student.email
+ subject = "Your JamClass lesson today with #{@teacher.first_name}"
+ unique_args = {:type => "student_lesson_normal_done"}
+
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [@student.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ def teacher_lesson_normal_done(lesson_session)
+ @student = lesson_session.student
+ @teacher = lesson_session.teacher
+ @session_name = lesson_session.music_session.name
+ @session_description = lesson_session.music_session.description
+ @session_date = lesson_session.slot.pretty_scheduled_start(true)
+ @session_url = lesson_session.web_url
+ @lesson_session = lesson_session
+ email = lesson_session.school_over_teacher
+ subject = "Your JamClass lesson today with #{@student.first_name}"
+ unique_args = {:type => "teacher_lesson_normal_done"}
+
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients(email)
+ sendgrid_substitute('@USERID', lesson_session.school_over_teacher_ids)
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ def student_unable_charge(lesson_session)
+ @student = lesson_session.student
+ @teacher = lesson_session.teacher
+ @session_name = lesson_session.music_session.name
+ @session_description = lesson_session.music_session.description
+ @session_date = lesson_session.slot.pretty_scheduled_start(true)
+ @session_url = lesson_session.web_url
+ @lesson_session = lesson_session
+ @card_declined = lesson_session.is_card_declined?
+ @card_expired = lesson_session.is_card_expired?
+ @bill_date = lesson_session.last_billed_at_date
+
+ email = @student.email
+ subject = "The credit card charge for your lesson today with #{@teacher.name} failed"
+ unique_args = {:type => "student_unable_charge"}
+
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [@student.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ def teacher_unable_charge(lesson_session)
+ @student = lesson_session.student
+ @teacher = lesson_session.teacher
+ @session_name = lesson_session.music_session.name
+ @session_description = lesson_session.music_session.description
+ @session_date = lesson_session.slot.pretty_scheduled_start(true)
+ @session_url = lesson_session.web_url
+ @lesson_session = lesson_session
+ email = lesson_session.teacher.email
+ subject = "The credit card charge for your lesson today with #{@teacher.name} failed"
+ unique_args = {:type => "teacher_lesson_normal_done"}
+
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [@teacher.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ def student_unable_charge_monthly(lesson_package_purchase)
+ lesson_booking = lesson_package_purchase.lesson_booking
+ @student = lesson_booking.student
+ @teacher = lesson_booking.teacher
+ @lesson_package_purchase = lesson_package_purchase
+ @card_declined = lesson_package_purchase.is_card_declined?
+ @card_expired = lesson_package_purchase.is_card_expired?
+ @bill_date = lesson_package_purchase.last_billed_at_date
+ @lesson_booking = lesson_booking
+ @month_name = lesson_package_purchase.month_name
+ email = @student.email
+ if lesson_booking.is_suspended?
+ @subject = "Your weekly lessons with #{@teacher.name} have been suspended."
+ else
+ @subject = "The credit card charge for your #{@month_name} lessons with #{@teacher.name} failed."
+ end
+
+
+ unique_args = {:type => "student_unable_charge_monthly"}
+
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [@student.id])
+
+ mail(:to => email, :subject => @subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ def teacher_unable_charge_monthly(lesson_package_purchase)
+ lesson_booking = lesson_package_purchase.lesson_booking
+ @student = lesson_booking.student
+ @teacher = lesson_booking.teacher
+ @lesson_package_purchase = lesson_package_purchase
+ @card_declined = lesson_package_purchase.is_card_declined?
+ @card_expired = lesson_package_purchase.is_card_expired?
+ @bill_date = lesson_package_purchase.last_billed_at_date
+ @lesson_booking = lesson_booking
+ @month_name = lesson_package_purchase.month_name
+
+ email = lesson_booking.school_over_teacher
+ if lesson_booking.is_suspended?
+ @subject = "Your weekly lessons with #{@student.name} has been suspended."
+ else
+ @subject = "The student #{@student.name} had a failed credit card charge for #{@month_name}."
+ end
+
+
+ unique_args = {:type => "teacher_unable_charge_monthly"}
+
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients(email)
+ sendgrid_substitute('@USERID', lesson_booking.school_over_teacher_ids)
+
+ mail(:to => email, :subject => @subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ def student_lesson_monthly_charged(lesson_package_purchase)
+ lesson_booking = lesson_package_purchase.lesson_booking
+ @student = lesson_booking.student
+ @teacher = lesson_booking.teacher
+ @lesson_package_purchase = lesson_package_purchase
+ @card_declined = lesson_package_purchase.is_card_declined?
+ @card_expired = lesson_package_purchase.is_card_expired?
+ @bill_date = lesson_package_purchase.last_billed_at_date
+ @lesson_booking = lesson_booking
+ @month_name = lesson_package_purchase.month_name
+ email = @student.email
+ @subject = "Your JamClass lessons with #{@teacher.first_name} for #{@month_name}"
+
+ unique_args = {:type => "student_lesson_monthly_charged"}
+
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [@student.id])
+
+ mail(:to => email, :subject => @subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ def teacher_lesson_monthly_charged(lesson_package_purchase)
+ lesson_booking = lesson_package_purchase.lesson_booking
+ @student = lesson_booking.student
+ @teacher = lesson_booking.teacher
+ @lesson_package_purchase = lesson_package_purchase
+ @card_declined = lesson_package_purchase.is_card_declined?
+ @card_expired = lesson_package_purchase.is_card_expired?
+ @bill_date = lesson_package_purchase.last_billed_at_date
+ @lesson_booking = lesson_booking
+ @month_name = lesson_package_purchase.month_name
+
+ email = lesson_booking.school_over_teacher
+ if lesson_booking.is_suspended?
+ @subject = "Your weekly lessons with #{@student.name} has been suspended."
+ else
+ @subject = "The student #{@student.name} had a failed credit card charge for #{@month_name}."
+ end
+
+
+ unique_args = {:type => "student_lesson_monthly_charged"}
+
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients(email)
+ sendgrid_substitute('@USERID', lesson_booking.school_over_teacher_ids)
+
+ mail(:to => email, :subject => @subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
+ end
+ end
+
+ # always goes to the teacher
+ def teacher_distribution_done(teacher_payment)
+ @school = teacher_payment.school
+ @teacher_payment = teacher_payment
+ @distribution = teacher_payment.teacher_distribution
+ @teacher = teacher_payment.teacher
+ @payable_teacher = teacher_payment.payable_teacher
+ @name = @teacher.first_name || 'Anonymous'
+ @student = @distribution.student
+ email = @distribution.target.lesson_booking.school_over_teacher
+
+ if @school
+ if @distribution.is_test_drive?
+ @subject = "Your TestDrive lesson with #{@student.name}"
+ elsif @distribution.is_normal?
+ @subject = "Your lesson with #{@student.name}"
+ elsif @distribution.is_monthly?
+ @subject = "Your #{@distribution.month_name} lessons with #{@student.name}"
+ else
+ @subject = "Your lesson with #{@student.name}"
+ end
+ else
+ if @distribution.is_test_drive?
+ @subject = "You have earned #{@distribution.real_distribution_display} for your TestDrive lesson with #{@student.first_name}"
+ elsif @distribution.is_normal?
+ @subject = "You have earned #{@distribution.real_distribution_display} for your lesson with #{@student.first_name}"
+ elsif @distribution.is_monthly?
+ @subject = "You have earned #{@distribution.real_distribution_display} for your #{@distribution.month_name} lessons with #{@student.first_name}"
+ else
+ @subject = "You have earned #{@distribution.real_distribution_display} for your lesson with #{@student.first_name}"
end
end
- def welcome_message(user)
- @user = user
- sendgrid_category "Welcome"
- sendgrid_unique_args :type => "welcome_message"
+ unique_args = {:type => "teacher_distribution_done"}
- sendgrid_recipients([user.email])
- sendgrid_substitute('@USERID', [user.id])
- sendgrid_substitute(EmailBatchProgression::VAR_FIRST_NAME, [user.first_name])
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
- mail(:to => user.email, :subject => "Welcome to JamKazam") do |format|
- format.text
- format.html
- end
+ sendgrid_recipients(email)
+ sendgrid_substitute('@USERID',@distribution.target.lesson_booking.school_over_teacher_ids )
+
+ mail(:to => email, :subject => @subject) do |format|
+ format.text
+ format.html
end
+ end
- def password_changed(user)
- @user = user
+ # if school, goes to school owner; otherwise goes to teacher
+ def teacher_distribution_fail(teacher_payment)
+ @school = teacher_payment.school
+ @teacher_payment = teacher_payment
+ @distribution = teacher_payment.teacher_distribution
+ @teacher = teacher_payment.teacher
+ @payable_teacher = teacher_payment.payable_teacher
+ @student = @distribution.student
+ @name = @payable_teacher.first_name || 'Anonymous'
+ email = @distribution.target.lesson_booking.school_over_teacher
- sendgrid_recipients([user.email])
- sendgrid_substitute('@USERID', [user.id])
+ @card_declined = teacher_payment.is_card_declined?
+ @card_expired = teacher_payment.is_card_expired?
+ @bill_date = teacher_payment.last_billed_at_date
- sendgrid_unique_args :type => "password_changed"
- mail(:to => user.email, :subject => "JamKazam Password Changed") do |format|
- format.text
- format.html
- end
- end
- def password_reset(user, password_reset_url)
- @user = user
-
- sendgrid_recipients([user.email])
- sendgrid_substitute('@USERID', [user.id])
-
- @password_reset_url = password_reset_url
- sendgrid_unique_args :type => "password_reset"
- mail(:to => user.email, :subject => "JamKazam Password Reset") do |format|
- format.text
- format.html
+ if @school
+ if @distribution.is_test_drive?
+ @subject = "We had a problem paying #{@distribution.real_distribution_display} for #{@teacher.name}'s TestDrive lesson with #{@student.name}"
+ elsif @distribution.is_normal?
+ @subject = "We had a problem paying #{@distribution.real_distribution_display} for #{@teacher.name}'s lesson with #{@student.name}"
+ elsif @distribution.is_monthly?
+ @subject = "We had a problem paying #{@distribution.real_distribution_display} for #{@teacher.name}'s #{@distribution.month_name} lessons with #{@student.name}"
+ else
+ @subject = "We had a problem paying #{@distribution.real_distribution_display} for #{@teacher.name}'s lesson with #{@student.name}"
+ end
+ else
+ if @distribution.is_test_drive?
+ @subject = "We had a problem paying you #{@distribution.real_distribution_display} for your TestDrive lesson with #{@student.first_name}"
+ elsif @distribution.is_normal?
+ @subject = "We had a problem paying you #{@distribution.real_distribution_display} for your lesson with #{@student.first_name}"
+ elsif @distribution.is_monthly?
+ @subject = "We had a problem paying you #{@distribution.real_distribution_display} for your #{@distribution.month_name} lessons with #{@student.first_name}"
+ else
+ @subject = "We had a problem paying you #{@distribution.real_distribution_display} for your lesson with #{@student.first_name}"
end
end
- def updating_email(user)
- @user = user
+ unique_args = {:type => "teacher_distribution_fail"}
- sendgrid_recipients([user.email])
- sendgrid_substitute('@USERID', [user.id])
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
- sendgrid_unique_args :type => "updating_email"
- mail(:to => user.update_email, :subject => "JamKazam Email Change Confirmation") do |format|
- format.text
- format.html
- end
+ sendgrid_recipients(email)
+ sendgrid_substitute('@USERID', @distribution.target.lesson_booking.school_over_teacher_ids)
+
+ mail(:to => email, :subject => @subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ # always goes to the school owner
+ def school_distribution_done(teacher_payment)
+ @school = teacher_payment.school
+ @teacher_payment = teacher_payment
+ @distribution = teacher_payment.teacher_distribution
+ @teacher = teacher_payment.teacher
+ @payable_teacher = @school.owner
+ @name = @payable_teacher.first_name || 'Anonymous'
+ @student = @distribution.student
+ email = @payable_teacher.email
+
+ if @distribution.is_test_drive?
+ @subject = "#{@teacher.name} has earned #{@distribution.real_distribution_display} for TestDrive lesson with #{@student.name}"
+ elsif @distribution.is_normal?
+ @subject = "#{@teacher.name} has earned #{@distribution.real_distribution_display} for a lesson with #{@student.name}"
+ elsif @distribution.is_monthly?
+ @subject = "#{@teacher.name} has earned #{@distribution.real_distribution_display} for #{@distribution.month_name} lessons with #{@student.name}"
+ else
+ @subject = "#{@teacher.name} has earned #{@distribution.real_distribution_display} for a lesson with #{@student.name}"
end
- def updated_email(user)
- @user = user
+ unique_args = {:type => "school_distribution_done"}
- sendgrid_recipients([user.email])
- sendgrid_substitute('@USERID', [user.id])
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
- sendgrid_unique_args :type => "updated_email"
- mail(:to => user.email, :subject => "JamKazam Email Changed") do |format|
- format.text
- format.html
- end
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [@payable_teacher.id])
+
+ mail(:to => email, :subject => @subject) do |format|
+ format.text
+ format.html
end
+ end
- def new_musicians(user, new_musicians, host='www.jamkazam.com')
- @user, @new_musicians, @host = user, new_musicians, host
- sendgrid_recipients([user.email])
- sendgrid_substitute('@USERID', [user.id])
- sendgrid_unique_args :type => "new_musicians"
- mail(:to => user.email, :subject => EmailBatchNewMusician.subject) do |format|
- format.text
- format.html
- end
+ def monthly_recurring_done(lesson_session)
+ @student = lesson_session.student
+ @teacher = lesson_session.teacher
+ @session_name = lesson_session.music_session.name
+ @session_description = lesson_session.music_session.description
+ @session_date = lesson_session.slot.pretty_scheduled_start(true)
+ @session_url = lesson_session.web_url
+ @lesson_session = lesson_session
+
+ email = @student.email
+ subject = "Your JamClass lesson today with #{@teacher.first_name}"
+ unique_args = {:type => "monthly_recurring_done"}
+
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [@student.id])
+
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
end
+ end
- #################################### NOTIFICATION EMAILS ####################################
- def friend_request(user, msg, friend_request_id)
- return if !user.subscribe_email
+ def monthly_recurring_no_bill(lesson_session)
+ @student = lesson_session.student
+ @teacher = lesson_session.teacher
+ @session_name = lesson_session.music_session.name
+ @session_description = lesson_session.music_session.description
+ @session_date = lesson_session.slot.pretty_scheduled_start(true)
+ @session_url = lesson_session.web_url
+ @lesson_session = lesson_session
- email = user.email
- subject = "You have a new friend request on JamKazam"
- unique_args = {:type => "friend_request"}
+ email = @student.email
+ subject = "Your lesson with #{@teacher.name} will not be billed"
+ unique_args = {:type => "student_lesson_normal_done"}
- @url = Nav.accept_friend_request_dialog(friend_request_id)
- @body = msg
- sendgrid_category "Notification"
- sendgrid_unique_args :type => unique_args[:type]
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
- sendgrid_recipients([email])
- sendgrid_substitute('@USERID', [user.id])
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [@student.id])
- mail(:to => email, :subject => subject) do |format|
- format.text
- format.html
- end
+ mail(:to => email, :subject => subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
end
+ end
- def friend_request_accepted(user, msg)
- return if !user.subscribe_email
+ def student_lesson_booking_declined(lesson_booking, message)
+ @lesson_booking = lesson_booking
+ @student = lesson_booking.student
+ @teacher = lesson_booking.teacher
+ @message = message
+ @lesson_session = lesson_booking.next_lesson
+ @session_name = @lesson_session.music_session.name
+ @session_description = @lesson_session.music_session.description
+ @session_date = @lesson_session.slot.pretty_scheduled_start(true)
+ email = @student.email
+ @subject = "We're sorry your lesson request has been declined"
+ unique_args = {:type => "student_lesson_booking_declined"}
- email = user.email
- subject = "You have a new friend on JamKazam"
- unique_args = {:type => "friend_request_accepted"}
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [@student.id])
- @body = msg
- sendgrid_category "Notification"
- sendgrid_unique_args :type => unique_args[:type]
-
- sendgrid_recipients([email])
- sendgrid_substitute('@USERID', [user.id])
-
- mail(:to => email, :subject => subject) do |format|
- format.text
- format.html
- end
+ mail(:to => email, :subject => @subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
end
+ end
- def new_user_follower(user, msg)
- return if !user.subscribe_email
+ def student_lesson_booking_canceled(lesson_booking, message)
+ @lesson_booking = lesson_booking
+ @student = lesson_booking.student
+ @teacher = lesson_booking.teacher
+ @message = message
+ @lesson_session = lesson_booking.next_lesson
+ @session_name = @lesson_session.music_session.name
+ @session_description = @lesson_session.music_session.description
+ @session_date = @lesson_session.slot.pretty_scheduled_start(true)
+ email = @student.email
+ @subject = "Your lesson has been canceled"
+ unique_args = {:type => "student_lesson_booking_canceled"}
- email = user.email
- subject = "You have a new follower on JamKazam"
- unique_args = {:type => "new_user_follower"}
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [@student.id])
- @body = msg
- sendgrid_category "Notification"
- sendgrid_unique_args :type => unique_args[:type]
-
- sendgrid_recipients([email])
- sendgrid_substitute('@USERID', [user.id])
-
- mail(:to => email, :subject => subject) do |format|
- format.text
- format.html
- end
+ mail(:to => email, :subject => @subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
end
+ end
- def new_band_follower(user, msg)
- return if !user.subscribe_email
+ def teacher_lesson_booking_canceled(lesson_booking, message)
+ @lesson_booking = lesson_booking
+ @student = lesson_booking.student
+ @teacher = lesson_booking.teacher
+ @message = message
+ @lesson_session = lesson_booking.next_lesson
+ @session_name = @lesson_session.music_session.name
+ @session_description = @lesson_session.music_session.description
+ @session_date = @lesson_session.slot.pretty_scheduled_start(true)
- email = user.email
- subject = "Your band has a new follower on JamKazam"
- unique_args = {:type => "new_band_follower"}
+ email = @lesson_booking.school_and_teacher
+ @subject = "Your lesson has been canceled"
+ unique_args = {:type => "teacher_lesson_booking_canceled"}
- @body = msg
- sendgrid_category "Notification"
- sendgrid_unique_args :type => unique_args[:type]
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+ sendgrid_recipients(email)
+ sendgrid_substitute('@USERID', @lesson_booking.school_and_teacher_ids)
- sendgrid_recipients([email])
- sendgrid_substitute('@USERID', [user.id])
-
- mail(:to => email, :subject => subject) do |format|
- format.text
- format.html
- end
+ mail(:to => email, :subject => @subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
end
+ end
- def session_invitation(user, msg)
- return if !user.subscribe_email
+ def student_lesson_canceled(lesson_session, message)
+ @lesson_booking = lesson_booking = lesson_session.lesson_booking
+ @student = lesson_booking.student
+ @teacher = lesson_booking.teacher
+ @message = message
+ @lesson_session = lesson_booking.next_lesson
+ @session_name = @lesson_session.music_session.name
+ @session_description = @lesson_session.music_session.description
+ @session_date = @lesson_session.slot.pretty_scheduled_start(true)
+ email = @student.email
+ @subject = "Your lesson has been canceled"
+ unique_args = {:type => "student_lesson_canceled"}
- email = user.email
- subject = "You have been invited to a session on JamKazam"
- unique_args = {:type => "session_invitation"}
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [@student.id])
- @body = msg
- sendgrid_category "Notification"
- sendgrid_unique_args :type => unique_args[:type]
-
- sendgrid_recipients([email])
- sendgrid_substitute('@USERID', [user.id])
-
- mail(:to => email, :subject => subject) do |format|
- format.text
- format.html
- end
+ mail(:to => email, :subject => @subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
end
+ end
- def musician_session_join(user, msg, session_id)
- return if !user.subscribe_email
+ def teacher_lesson_canceled(lesson_session, message)
+ @lesson_booking = lesson_booking = lesson_session.lesson_booking
+ @student = lesson_booking.student
+ @teacher = lesson_booking.teacher
+ @message = message
+ @lesson_session = lesson_booking.next_lesson
+ @session_name = @lesson_session.music_session.name
+ @session_description = @lesson_session.music_session.description
+ @session_date = @lesson_session.slot.pretty_scheduled_start(true)
- email = user.email
- subject = "Someone you know is in a session on JamKazam"
- unique_args = {:type => "musician_session_join"}
- @body = msg
- @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session_id}"
- sendgrid_category "Notification"
- sendgrid_unique_args :type => unique_args[:type]
+ email = @lesson_booking.school_and_teacher
+ @subject = "Your lesson has been canceled"
+ unique_args = {:type => "teacher_lesson_canceled"}
- sendgrid_recipients([email])
- sendgrid_substitute('@USERID', [user.id])
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+ sendgrid_recipients(email)
+ sendgrid_substitute('@USERID', @lesson_booking.school_and_teacher_ids)
- mail(:to => email, :subject => subject) do |format|
- format.text
- format.html
- end
+ mail(:to => email, :subject => @subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
end
+ end
- def scheduled_session_invitation(user, msg, session)
- return if !user.subscribe_email
+ def invite_school_teacher(school_invitation)
+ @school_invitation = school_invitation
+ @school = school_invitation.school
- email = user.email
- subject = "Session Invitation"
- unique_args = {:type => "scheduled_session_invitation"}
- @body = msg
- @session_name = session.name
- @session_date = session.pretty_scheduled_start(true)
- @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details"
- sendgrid_category "Notification"
- sendgrid_unique_args :type => unique_args[:type]
+ email = school_invitation.email
+ @subject = "#{@school.owner.name} has sent you an invitation to join #{@school.name} on JamKazam"
+ unique_args = {:type => "invite_school_teacher"}
- sendgrid_recipients([email])
- sendgrid_substitute('@USERID', [user.id])
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+ sendgrid_recipients([email])
- mail(:to => email, :subject => subject) do |format|
- format.text
- format.html
- end
+ @suppress_user_has_account_footer = true
+
+ mail(:to => email, :subject => @subject) do |format|
+ format.text
+ format.html
end
+ end
- def scheduled_session_rsvp(user, msg, session)
- return if !user.subscribe_email
+ def invite_school_student(school_invitation)
+ @school_invitation = school_invitation
+ @school = school_invitation.school
- email = user.email
- subject = "Session RSVP"
- unique_args = {:type => "scheduled_session_rsvp"}
- @body = msg
- @session_name = session.name
- @session_date = session.pretty_scheduled_start(true)
- @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details"
- sendgrid_category "Notification"
- sendgrid_unique_args :type => unique_args[:type]
+ email = school_invitation.email
+ @subject = "#{@school.name} has sent you an invitation to join JamKazam for lessons"
+ unique_args = {:type => "invite_school_student"}
- sendgrid_recipients([email])
- sendgrid_substitute('@USERID', [user.id])
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+ sendgrid_recipients([email])
- mail(:to => email, :subject => subject) do |format|
- format.text
- format.html
- end
+ @suppress_user_has_account_footer = true
+
+ mail(:to => email, :subject => @subject) do |format|
+ format.text
+ format.html
end
+ end
- def scheduled_session_rsvp_approved(user, msg, session)
- return if !user.subscribe_email
+ def lesson_chat(chat_msg)
+ @target = chat_msg.target_user
+ @sender = chat_msg.user
+ @message = chat_msg.message
+ @lesson_session = chat_msg.lesson_session
+ @session_name = @lesson_session.music_session.name
+ @session_description = @lesson_session.music_session.description
+ @session_date = @lesson_session.slot.pretty_scheduled_start(true)
- email = user.email
- subject = "Session RSVP Approved"
- unique_args = {:type => "scheduled_session_rsvp_approved"}
- @body = msg
- @session_name = session.name
- @session_date = session.pretty_scheduled_start(true)
- @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details"
- sendgrid_category "Notification"
- sendgrid_unique_args :type => unique_args[:type]
+ email = @lesson_session.school_over_teacher
+ @subject = "#{@sender.name} has sent you a message about a lesson"
+ unique_args = {:type => "lesson_chat"}
- sendgrid_recipients([email])
- sendgrid_substitute('@USERID', [user.id])
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+ sendgrid_recipients(email)
+ sendgrid_substitute('@USERID', @lesson_session.school_over_teacher_ids)
- mail(:to => email, :subject => subject) do |format|
- format.text
- format.html
- end
+ mail(:to => email, :subject => @subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
end
+ end
- def scheduled_session_rsvp_cancelled(user, msg, session)
- return if !user.subscribe_email
+ # please respond to outstanding counter!
+ def student_counter_reminder(lesson_session)
+ @student = lesson_session.student
+ @teacher = lesson_session.teacher
+ @session_url = lesson_session.web_url
+ @lesson_session = lesson_session
- email = user.email
- subject = "Session RSVP Cancelled"
- unique_args = {:type => "scheduled_session_rsvp_cancelled"}
- @body = msg
- @session_name = session.name
- @session_date = session.pretty_scheduled_start(true)
- @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details"
- sendgrid_category "Notification"
- sendgrid_unique_args :type => unique_args[:type]
+ email = @student.email
+ @subject = "Instructor's time proposal is still awaiting your response"
+ unique_args = {:type => "student_counter_reminder"}
- sendgrid_recipients([email])
- sendgrid_substitute('@USERID', [user.id])
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
- mail(:to => email, :subject => subject) do |format|
- format.text
- format.html
- end
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [@student.id])
+
+ mail(:to => email, :subject => @subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
end
+ end
- def scheduled_session_rsvp_cancelled_org(user, msg, session)
- return if !user.subscribe_email
+ # please respond to outstanding counter!
+ def teacher_counter_reminder(lesson_session)
+ @student = lesson_session.student
+ @teacher = lesson_session.teacher
+ @session_name = lesson_session.music_session.name
+ @session_description = lesson_session.music_session.description
+ @session_date = lesson_session.slot.pretty_scheduled_start(true)
+ @session_url = lesson_session.web_url
+ @lesson_session = lesson_session
+ email = lesson_session.school_over_teacher
+ @subject = "Student #{@student.name}'s time proposal is still awaiting your response"
+ unique_args = {:type => "teacher_counter_reminder"}
- email = user.email
- subject = "Your Session RSVP Cancelled"
- unique_args = {:type => "scheduled_session_rsvp_cancelled_org"}
- @body = msg
- @session_name = session.name
- @session_date = session.pretty_scheduled_start(true)
- @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details"
- sendgrid_category "Notification"
- sendgrid_unique_args :type => unique_args[:type]
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
- sendgrid_recipients([email])
- sendgrid_substitute('@USERID', [user.id])
+ sendgrid_recipients(email)
+ sendgrid_substitute('@USERID', lesson_session.school_over_teacher_ids)
- mail(:to => email, :subject => subject) do |format|
- format.text
- format.html
- end
+ mail(:to => email, :subject => @subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
end
+ end
- def scheduled_session_cancelled(user, msg, session)
- return if !user.subscribe_email
- email = user.email
- subject = "Session Cancelled"
- unique_args = {:type => "scheduled_session_cancelled"}
- @body = msg
- @session_name = session.name
- @session_date = session.pretty_scheduled_start(true)
- @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details"
- sendgrid_category "Notification"
- sendgrid_unique_args :type => unique_args[:type]
+ def lesson_starting_soon_teacher(lesson_session)
+ @lesson_booking = lesson_booking = lesson_session.lesson_booking
+ @student = lesson_booking.student
+ @teacher = lesson_booking.teacher
+ @lesson_session = lesson_booking.next_lesson
+ @session_name = @lesson_session.music_session.name
+ @session_description = @lesson_session.music_session.description
+ @session_date = @lesson_session.slot.pretty_scheduled_start(true)
- sendgrid_recipients([email])
- sendgrid_substitute('@USERID', [user.id])
+ email = @teacher.email
+ @subject = "Your lesson with #{@student.first_name} on JamKazam is starting soon"
+ unique_args = {:type => "send_starting_notice_teacher"}
- mail(:to => email, :subject => subject) do |format|
- format.text
- format.html
- end
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [@teacher.id])
+
+ mail(:to => email, :subject => @subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
end
+ end
- def scheduled_session_rescheduled(user, msg, session)
- return if !user.subscribe_email
+ def lesson_starting_soon_student(lesson_session)
+ @lesson_booking = lesson_booking = lesson_session.lesson_booking
+ @student = lesson_booking.student
+ @teacher = lesson_booking.teacher
+ @message = message
+ @lesson_session = lesson_booking.next_lesson
+ @session_name = @lesson_session.music_session.name
+ @session_description = @lesson_session.music_session.description
+ @session_date = @lesson_session.slot.pretty_scheduled_start(true)
- email = user.email
- subject = "Session Rescheduled"
- unique_args = {:type => "scheduled_session_rescheduled"}
- @body = msg
- @session_name = session.name
- @session_date = session.pretty_scheduled_start(true)
- @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details"
- sendgrid_category "Notification"
- sendgrid_unique_args :type => unique_args[:type]
+ email = @student.email
+ @subject = "Your lesson with #{@teacher.first_name} on JamKazam is starting soon"
+ unique_args = {:type => "send_starting_notice_student"}
- sendgrid_recipients([email])
- sendgrid_substitute('@USERID', [user.id])
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [@student.id])
- mail(:to => email, :subject => subject) do |format|
- format.text
- format.html
- end
+ mail(:to => email, :subject => @subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
end
+ end
- def scheduled_session_reminder_upcoming(user, session)
- subject = "Your JamKazam session starts in 1 hour!"
- unique_args = {:type => "scheduled_session_reminder_upcoming"}
- send_scheduled_session_reminder(user, session, subject, unique_args)
+ def lesson_attachment(sender, target, lesson_session, attachment)
+ @sender = sender
+ @target = target
+ @lesson_session = lesson_session
+ @attachment = attachment
+
+
+ email = target.email
+ @subject = "An attachment has been added to your lesson by #{sender.name}"
+ unique_args = {:type => "lesson_attachment"}
+
+ sendgrid_category "Notification"
+ sendgrid_unique_args :type => unique_args[:type]
+ sendgrid_recipients([email])
+ sendgrid_substitute('@USERID', [@target.id])
+
+ mail(:to => email, :subject => @subject) do |format|
+ format.text
+ format.html { render :layout => "from_user_mailer" }
end
- def scheduled_session_reminder_day(user, session)
- subject = "JamKazam Session Reminder"
- unique_args = {:type => "scheduled_session_reminder_day"}
- send_scheduled_session_reminder(user, session, subject, unique_args)
- end
-
- def send_scheduled_session_reminder(user, session, subject, unique_args)
- return if !user.subscribe_email
-
- email = user.email
- @user = user
- @session_name = session.name
- @session_date = session.pretty_scheduled_start(true)
- @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details"
- sendgrid_category "Notification"
- sendgrid_unique_args :type => unique_args[:type]
-
- sendgrid_recipients([email])
- sendgrid_substitute('@USERID', [user.id])
-
- mail(:to => email, :subject => subject) do |format|
- format.text
- format.html
- end
- end
-
- def scheduled_session_comment(target_user, sender, msg, comment, session)
- return if !target_user.subscribe_email
-
- email = target_user.email
- subject = "New Session Comment"
- unique_args = {:type => "scheduled_session_comment"}
- @body = msg
- @session_name = session.name
- @session_date = session.pretty_scheduled_start(true)
- @comment = comment
- @sender = sender
- @suppress_user_has_account_footer = true
- @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details"
- sendgrid_category "Notification"
- sendgrid_unique_args :type => unique_args[:type]
-
- sendgrid_recipients([email])
- sendgrid_substitute('@USERID', [target_user.id])
-
- mail(:to => email, :subject => subject) do |format|
- format.text
- format.html { render :layout => "from_user_mailer" }
- end
- end
-
- def scheduled_session_daily(receiver, sessions_and_latency)
- sendgrid_category "Notification"
- sendgrid_unique_args :type => "scheduled_session_daily"
-
- sendgrid_recipients([receiver.email])
- sendgrid_substitute('@USERID', [receiver.id])
-
- @user = receiver
- @sessions_and_latency = sessions_and_latency
-
- @title = 'New Scheduled Sessions Matched to You'
- mail(:to => receiver.email,
- :subject => EmailBatchScheduledSessions.subject) do |format|
- format.text
- format.html
- end
- end
-
- def band_session_join(user, msg, session_id)
- return if !user.subscribe_email
-
- email = user.email
- subject = "A band that you follow has joined a session"
- unique_args = {:type => "band_session_join"}
-
- @body = msg
- @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session_id}"
- sendgrid_category "Notification"
- sendgrid_unique_args :type => unique_args[:type]
-
- sendgrid_recipients([email])
- sendgrid_substitute('@USERID', [user.id])
-
- mail(:to => email, :subject => subject) do |format|
- format.text
- format.html
- end
- end
-
- def musician_recording_saved(user, msg)
- return if !user.subscribe_email
-
- email = user.email
- subject = "A musician has saved a new recording on JamKazam"
- unique_args = {:type => "musician_recording_saved"}
-
- @body = msg
- sendgrid_category "Notification"
- sendgrid_unique_args :type => unique_args[:type]
-
- sendgrid_recipients([email])
- sendgrid_substitute('@USERID', [user.id])
-
- mail(:to => email, :subject => subject) do |format|
- format.text
- format.html
- end
- end
-
- def band_recording_saved(user, msg)
- return if !user.subscribe_email
-
- email = user.email
- subject = "A band has saved a new recording on JamKazam"
- unique_args = {:type => "band_recording_saved"}
-
- @body = msg
- sendgrid_category "Notification"
- sendgrid_unique_args :type => unique_args[:type]
-
- sendgrid_recipients([email])
- sendgrid_substitute('@USERID', [user.id])
-
- mail(:to => email, :subject => subject) do |format|
- format.text
- format.html
- end
- end
-
- def band_invitation(user, msg)
- return if !user.subscribe_email
-
- email = user.email
- subject = "You have been invited to join a band on JamKazam"
- unique_args = {:type => "band_invitation"}
-
- @body = msg
- sendgrid_category "Notification"
- sendgrid_unique_args :type => unique_args[:type]
-
- sendgrid_recipients([email])
- sendgrid_substitute('@USERID', [user.id])
-
- mail(:to => email, :subject => subject) do |format|
- format.text
- format.html
- end
- end
-
- def band_invitation_accepted(user, msg)
- return if !user.subscribe_email
-
- email = user.email
- subject = "Your band invitation was accepted"
- unique_args = {:type => "band_invitation_accepted"}
-
- @body = msg
- sendgrid_category "Notification"
- sendgrid_unique_args :type => unique_args[:type]
-
- sendgrid_recipients([email])
- sendgrid_substitute('@USERID', [user.id])
-
- mail(:to => email, :subject => subject) do |format|
- format.text
- format.html
- end
- end
-
-
- def text_message(user, sender_id, sender_name, sender_photo_url, message)
- return if !user.subscribe_email
-
- email = user.email
- subject = "Message from #{sender_name}"
- unique_args = {:type => "text_message"}
-
- @note = message
- @url = Nav.home(dialog: 'text-message', dialog_opts: {d1: sender_id})
- @sender_id = sender_id
- @sender_name = sender_name
- @sender_photo_url = sender_photo_url
- sendgrid_category "Notification"
- sendgrid_unique_args :type => unique_args[:type]
-
- sendgrid_recipients([email])
- sendgrid_substitute('@USERID', [user.id])
-
- mail(:to => email, :subject => subject) do |format|
- format.text
- format.html { render :layout => "from_user_mailer" }
- end
- end
-
- # def send_notification(email, subject, msg, unique_args)
- # @body = msg
- # sendgrid_category "Notification"
- # sendgrid_unique_args :type => unique_args[:type]
- # mail(:bcc => email, :subject => subject) do |format|
- # format.text
- # format.html
- # end
- # end
- #############################################################################################
-
end
end
+end
diff --git a/ruby/lib/jam_ruby/app/uploaders/artifact_uploader.rb b/ruby/lib/jam_ruby/app/uploaders/artifact_uploader.rb
index 652e877c2..7b2bbab6e 100644
--- a/ruby/lib/jam_ruby/app/uploaders/artifact_uploader.rb
+++ b/ruby/lib/jam_ruby/app/uploaders/artifact_uploader.rb
@@ -52,7 +52,7 @@ class ArtifactUploader < CarrierWave::Uploader::Base
# Add a white list of extensions which are allowed to be uploaded.
# For images you might use something like this:
def extension_white_list
- %w(exe msi dmg)
+ %w(exe msi dmg zip)
end
# Override the filename of the uploaded files:
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/confirm_email.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.html.erb
index 6f760961e..358f0f2b1 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.html.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.html.erb
@@ -1,5 +1,9 @@
-<% provide(:title, 'Confirm Email') %>
+<% provide(:title, 'Welcome to JamKazam!') %>
-
Welcome to JamKazam, <%= @user.first_name %>!
+
We’re delighted you have joined our community of <%= APP_CONFIG.musician_count %> musicians. We’d like to send you an orientation email with information and resource links that will help you get the most out of JamKazam. Please click here to confirm this email has reached you successfully and we will then send the orientation email.
If you have received this email but aren’t familiar with JamKazam or JamTracks, then someone has registered at our website using your email address, and you can just ignore and delete this email.
+
+
Best Regards,
+ Team JamKazam
+
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.text.erb
index d412a7b92..129da288d 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.text.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/confirm_email.text.erb
@@ -1,3 +1,8 @@
-Welcome to JamKazam, <%= @user.first_name %>!
+Welcome to JamKazam!
-To confirm this email address, please go to the signup confirmation page at: <%= @signup_confirm_url %>.
\ No newline at end of file
+We’re delighted you have joined our community of <%= APP_CONFIG.musician_count %> musicians. We’d like to send you an orientation email with information and resource links that will help you get the most out of JamKazam. Please click <%= @signup_confirm_url %> to confirm this email has reached you successfully and we will then send the orientation email.
+
+If you have received this email but aren’t familiar with JamKazam or JamTracks, then someone has registered at our website using your email address, and you can just ignore and delete this email.
+
+Best Regards,
+Team JamKazam
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.html.erb
new file mode 100644
index 000000000..164315809
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.html.erb
@@ -0,0 +1,16 @@
+<% provide(:title, @subject) %>
+
+Hello <%= @school_invitation.first_name %> -
+
<%= @school.owner.first_name %> is using JamKazam to deliver online music lessons, and has sent you this invitation so that you can
+ register to take online music lessons with <%= @school.name %>. To accept this invitation, please click the SIGN UP NOW
+ button below, and follow the instructions on the web page to which you are taken. Thanks, and on behalf of
+ <%= @school.name %>, welcome to JamKazam!
+
+
+Best Regards,
+Team JamKazam
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.text.erb
new file mode 100644
index 000000000..1b1ea765b
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_student.text.erb
@@ -0,0 +1,12 @@
+<% provide(:title, @subject) %>
+
+Hello <%= @school_invitation.first_name %> -
+<%= @school.owner.first_name %> is using JamKazam to deliver online music lessons, and has sent you this invitation so that you can
+register to take online music lessons with <%= @school.name %>. To accept this invitation, please click the link
+below, and follow the instructions on the web page to which you are taken. Thanks, and on behalf of
+<%= @school.name %>, welcome to JamKazam!
+
+<%= @school_invitation.generate_signup_url %>
+
+Best Regards,
+Team JamKazam
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.html.erb
new file mode 100644
index 000000000..6e59f21d8
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.html.erb
@@ -0,0 +1,18 @@
+<% provide(:title, @subject) %>
+
+Hello <%= @school_invitation.first_name %> -
+
+
+ <%= @school.owner.first_name %> has set up <%= @school.name %> on JamKazam, enabling you to deliver online music
+ lessons in an amazing new way that really works. To accept this invitation, please click the SIGN UP NOW button below,
+ and follow the instructions on the web page to which you are taken. Thanks, and welcome to JamKazam!
+
+
+
+Best Regards,
+Team JamKazam
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.text.erb
new file mode 100644
index 000000000..c31475ea8
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/invite_school_teacher.text.erb
@@ -0,0 +1,11 @@
+<% provide(:title, @subject) %>
+
+Hello <%= @school_invitation.first_name %> -
+<%= @school.owner.first_name %> has set up <%= @school.name %> on JamKazam, enabling you to deliver online music
+lessons in an amazing new way that really works. To accept this invitation, please click the link below,
+and follow the instructions on the web page to which you are taken. Thanks, and welcome to JamKazam!
+
+<%= @school_invitation.generate_signup_url %>
+
+Best Regards,
+Team JamKazam
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_attachment.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_attachment.html.erb
new file mode 100644
index 000000000..fc49d2163
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_attachment.html.erb
@@ -0,0 +1,20 @@
+<% provide(:title, @subject) %>
+<% provide(:photo_url, @sender.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ <% if @attachment.is_a?(JamRuby::MusicNotation) %>
+ <% if @attachment.is_notation? %>
+ A music notation has been added to your lesson. You can download "><%= @attachment.file_name %> directly or at any time in the message window for this lesson.
+ <% else %>
+ A audio file has been added to your lesson. You can download "><%= @attachment.file_name %> directly or at any time in the message window for this lesson.
+ <% end %>
+ <% else %>
+ A recording named "<%= @attachment.name %>" has been added to your lesson. It can be viewed ">here or found within the message window for this lesson.
+ <% end %>
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_attachment.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_attachment.text.erb
new file mode 100644
index 000000000..8756d460c
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_attachment.text.erb
@@ -0,0 +1,12 @@
+<% if @attachment.is_a?(JamRuby::MusicNotation) %>
+<% if @attachment.is_notation? %>
+A music notation has been added to your lesson. You can download (<%= APP_CONFIG.external_root_url + "/api/music_notations/#{@attachment.id}" %>">)<%= @attachment.file_name %> directly or at any time in the message window for this lesson.
+<% else %>
+A audio file has been added to your lesson. You can download (<%= APP_CONFIG.external_root_url + "/api/music_notations/#{@attachment.id}" %>">)<%= @attachment.file_name %> directly or at any time in the message window for this lesson.
+<% end %>
+<% else %>
+A recording named "<%= @attachment.name %>" has been added to your lesson. It can be viewed (<%= APP_CONFIG.external_root_url + "/recordings/#{@attachment.id}" %>">) here or found within the message window for this lesson.
+<% end %>
+VIEW LESSON DETAILS (<%= @lesson_session.web_url %>)
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_chat.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_chat.html.erb
new file mode 100644
index 000000000..4b11f3cc0
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_chat.html.erb
@@ -0,0 +1,18 @@
+<% provide(:title, @subject) %>
+<% provide(:photo_url, @sender.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_teacher.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_teacher.text.erb
new file mode 100644
index 000000000..c8f460e25
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/lesson_starting_soon_teacher.text.erb
@@ -0,0 +1,3 @@
+Your lesson with <%= @student.name %> is scheduled to begin on JamKazam in less than 30 minutes.
+JAMCLASS HOME (<%= @lesson_session.home_url %>
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.html.erb
new file mode 100644
index 000000000..bc205e230
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.html.erb
@@ -0,0 +1,28 @@
+<% provide(:title, "Your JamClass lesson today with #{@teacher.first_name}") %>
+<% provide(:photo_url, @teacher.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ Hello <%= @student.name %>,
+
+
+
+ We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>.
+
+
+
+ <% if !@student.has_rated_teacher(@teacher) %>
+ If you haven't already done so, please rate your teacher now to help other students in the community find the best
+ instructors.
+ <% end %>
+
+ If you had technical problems during your lesson, or have questions, or would like to make suggestions
+ on how to improve JamClass, please email us at support@jamkazam.com.
+
+
+
+ Best Regards, Team JamKazam
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.text.erb
new file mode 100644
index 000000000..96bf19889
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_done.text.erb
@@ -0,0 +1,28 @@
+<% provide(:title, "Your JamClass lesson today with #{@teacher.first_name}") %>
+<% provide(:photo_url, @teacher.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ Hello <%= @student.name %>,
+
+
+
+ We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. As just a reminder, you already paid for this lesson in advance.
+
+
+
+ <% if !@student.has_rated_teacher(@teacher) %>
+ If you haven't already done so, please rate your teacher now to help other students in the community find the best
+ instructors.
+ <% end %>
+
+ If you had technical problems during your lesson, or have questions, or would like to make suggestions
+ on how to improve JamClass, please email us at support@jamkazam.com.
+
+
+
+ Best Regards, Team JamKazam
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.html.erb
new file mode 100644
index 000000000..3dc58d8ec
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.html.erb
@@ -0,0 +1,23 @@
+<% provide(:title, "Your lesson with #{@teacher.name} will not be billed") %>
+<% provide(:photo_url, @teacher.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ Hello <%= @student.name %>,
+
+
+
You will not be billed for today's session with <%= @teacher.name %>. However, you already paid for the lesson in advance, so next month's bill will be lower than usual.
+
+
+ Click the button below to see more information about this session.
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.text.erb
new file mode 100644
index 000000000..f21f164b5
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/monthly_recurring_no_bill.text.erb
@@ -0,0 +1,5 @@
+Hello <%= @student.name %>,
+
+You will not be billed for today's session with <%= @teacher.name %>. However, you already paid for the lesson in advance, so next month's bill will be lower than usual.
+
+To see this lesson, click here: <%= @lesson_session.web_url %>
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.html.erb
index be9aa27fb..65bf1295f 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.html.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.html.erb
@@ -1,5 +1,7 @@
<% provide(:title, 'New Musicians You Should Check Out') %>
-Hi <%= @user.first_name %>,
+<% if !@user.anonymous? %>
+
Hi <%= @user.first_name %>,
+<% end %>
The following new musicians have joined JamKazam within the last week, and have Internet connections with low enough latency to you that you can have a good online session together. We'd suggest that you look through the new musicians listed below to see if any match your musical interests, and if so, click through to their profile page on the JamKazam website to send them a message or a request to connect as a JamKazam friend:
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.text.erb
index 05fbbd268..ee100dbff 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.text.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.text.erb
@@ -1,7 +1,8 @@
New Musicians You Should Check Out
+<% if !@user.anonymous? %>
Hi <%= @user.first_name %>,
-
+<% end %>
The following new musicians have joined JamKazam within the last week, and have Internet connections with low enough latency to you that you can have a good online session together. We'd suggest that you look through the new musicians listed below to see if any match your musical interests, and if so, click through to their profile page on the JamKazam website to send them a message or a request to connect as a JamKazam friend:
<% @new_musicians.each do |user| %>
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.html.erb
index 2fdc11256..769b5bcda 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.html.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.html.erb
@@ -1,7 +1,8 @@
<% provide(:title, @title) %>
-
Hello <%= @user.first_name %> --
-
+<% if !@user.anonymous? %>
+
Hi <%= @user.first_name %>,
+<% end %>
The following new sessions have been posted within the last 24 hours, and you have good or acceptable latency to the organizer of each session below. If a session looks interesting, click the Details link to see the session page. You can RSVP to a session from the session page, and you'll be notified if/when the session organizer approves your RSVP.
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.text.erb
index 282c6dd90..3d1e15346 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.text.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.text.erb
@@ -1,7 +1,8 @@
<% provide(:title, @title) %>
-Hello <%= @user.first_name %> --
-
+<% if !@user.anonymous? %>
+Hi <%= @user.first_name %>,
+<% end %>
The following new sessions have been posted within the last 24 hours, and you have good or acceptable latency to the organizer of each session below. If a session looks interesting, click the Details link to see the session page. You can RSVP to a session from the session page, and you'll be notified if/when the session organizer approves your RSVP.
GENRE | NAME | DESCRIPTION | LATENCY
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.html.erb
index b72d3c133..cf4816bf1 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.html.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.html.erb
@@ -1,18 +1,20 @@
<% provide(:title, 'JamKazam Session Reminder') %>
-
+<% if !@user.anonymous? %>
+
Hi <%= @user.first_name %>,
-
+
+<% end %>
-
+
This is a reminder that your JamKazam session<%= @session_name %>is scheduled for tomorrow. We hope you have fun!
-
+
-
+
Best Regards,
Team JamKazam
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.text.erb
index c3f0576bf..333acdb74 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.text.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.text.erb
@@ -1,5 +1,6 @@
+<% if !@user.anonymous? %>
Hi <%= @user.first_name %>,
-
+<% end %>
This is a reminder that your JamKazam session <%=@session_name%> is scheduled for tomorrow. We hope you have fun!
Best Regards,
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.html.erb
index 4fbc59ace..119d57b16 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.html.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.html.erb
@@ -1,17 +1,20 @@
<% provide(:title, 'Your JamKazam session starts in 1 hour!') %>
-
+<% if !@user.anonymous? %>
+
Hi <%= @user.first_name %>,
-
+
+<% end %>
+
-
+
This is a reminder that your JamKazam session<%= @session_name %>starts in 1 hour. We hope you have fun!
-
+
-
+
Best Regards,
Team JamKazam
-
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.text.erb
index 70726a9e6..d4719bd4c 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.text.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.text.erb
@@ -1,4 +1,6 @@
+<% if !@user.anonymous? %>
Hi <%= @user.first_name %>,
+<% end %>
This is a reminder that your JamKazam session
<%=@session_name%>
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_distribution_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_distribution_done.html.erb
new file mode 100644
index 000000000..37f9d2780
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_distribution_done.html.erb
@@ -0,0 +1,20 @@
+<% provide(:title, @subject) %>
+
+
+ Hello <%= @name %>,
+
+
+ <% if @distribution.is_test_drive? %>
+
We have processed a payment to you via your Stripe account for $<%= @distribution.real_distribution_display %> for this lesson.
+ <% elsif @distribution.is_normal? %>
+
We have processed a payment to you via your Stripe account for $<%= @distribution.real_distribution_display %> for this lesson.
+ <% elsif @distribution.is_monthly? %>
+
We have processed a payment to you via your Stripe account for $<%= @distribution.real_distribution_display %> for <%= @distribution.month_name %> lessons.
+ <% else %>
+ Unknown payment type.
+ <% end %>
+
+
+
+Best Regards,
+JamKazam
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_distribution_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_distribution_done.text.erb
new file mode 100644
index 000000000..fcf56ab98
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_distribution_done.text.erb
@@ -0,0 +1,16 @@
+<% provide(:title, @subject) %>
+
+Hello <%= @name %>,
+
+<% if @distribution.is_test_drive? %>
+We have processed a payment to you via your Stripe account for $<%= @distribution.real_distribution_display %> for this lesson.
+<% elsif @distribution.is_normal? %>
+We have processed a payment to you via your Stripe account for $<%= @distribution.real_distribution_display %> for this lesson.
+<% elsif @distribution.is_monthly? %>
+We have processed a payment to you via your Stripe account for $<%= @distribution.real_distribution_display %> for <%= @distribution.month_name %> lessons.
+<% else %>
+Unknown payment type.
+<% end %>
+
+Best Regards,
+JamKazam
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_owner_welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_owner_welcome_message.html.erb
new file mode 100644
index 000000000..7df3b4c61
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_owner_welcome_message.html.erb
@@ -0,0 +1,39 @@
+<% provide(:title, @subject) %>
+
+
+<% if !@user.anonymous? %>
+
+ Thank you for expressing an interest in exploring our music school partner program! A member of our staff will reach out to you shortly to chat with you and answer any/all questions you may have about our partner program, our technologies, and how we can help you continue to build your music school business.
+
+
+
+ We'd also like to provide links to some help articles that explain how many things work, and will likely answer many of your questions in a well-organized manner:
+
+
+
+
Guide for Music School Owners
+ These help articles explain things from the perspective of the school owner - e.g. how you can schedule and book lessons from our marketplace with your teachers, how billing and payments are handled, and so on.
+
+
+
Guide for Music Lesson Teachers
+ These help articles explain how teachers use the features of the platform outside of the online lesson sessions.
+
+ Gear Requirements
+ This help article explains the requirements for your computer, audio and video gear, and Internet service.
+
+
+
+ Thanks again for connecting with us, and we look forward to speaking with you soon!
+
+
Best Regards,
+ Team JamKazam
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_owner_welcome_message.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_owner_welcome_message.text.erb
new file mode 100644
index 000000000..771a82b03
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/school_owner_welcome_message.text.erb
@@ -0,0 +1,24 @@
+<% if !@user.anonymous? %>
+Hello <%= EmailBatchProgression::VAR_FIRST_NAME %>
+<% end %>
+
+Thank you for expressing an interest in exploring our music school partner program! A member of our staff will reach out to you shortly to chat with you and answer any/all questions you may have about our partner program, our technologies, and how we can help you continue to build your music school business.
+
+We'd also like to provide links to some help articles that explain how many things work, and will likely answer many of your questions in a well-organized manner:
+
+-- Guide for Music School Owners (https://jamkazam.desk.com/customer/en/portal/topics/935633-jamclass-online-music-lessons---for-music-schools/articles)
+These help articles explain things from the perspective of the school owner - e.g. how you can schedule and book lessons from our marketplace with your teachers, how billing and payments are handled, and so on.
+
+-- Guide for Music Lesson Teachers (https://jamkazam.desk.com/customer/en/portal/topics/926076-jamclass-online-music-lessons---for-teachers/articles)
+These help articles explain how teachers use the features of the platform outside of the online lesson sessions.
+
+-- Key Features To Use In Online Sessions (https://jamkazam.desk.com/customer/en/portal/topics/673198-key-features-to-use-in-online-sessions/articles)
+These help articles explain the key features instructors can use in online sessions to teach effectively.
+
+-- Gear Requirements (https://jamkazam.desk.com/customer/en/portal/articles/1288274-computer-internet-audio-and-video-requirements)
+This help article explains the requirements for your computer, audio and video gear, and Internet service.
+
+Thanks again for connecting with us, and we look forward to speaking with you soon!
+
+Best Regards,
+Team JamKazam
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_counter_reminder.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_counter_reminder.html.erb
new file mode 100644
index 000000000..f437bd8ad
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_counter_reminder.html.erb
@@ -0,0 +1,17 @@
+<% provide(:title, @subject) %>
+<% provide(:photo_url, @teacher.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ <%= @teacher.name %> proposed a different time 24 hours ago.
+
+
+ Please click the button below to respond.
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_counter_reminder.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_counter_reminder.text.erb
new file mode 100644
index 000000000..e05617102
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_counter_reminder.text.erb
@@ -0,0 +1,3 @@
+<%= @teacher.name %> has proposed a different time 24 hours ago. Please respond.
+
+To see this lesson, click here: <%= @lesson_session.web_url %>
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.html.erb
new file mode 100644
index 000000000..a5b2c826a
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.html.erb
@@ -0,0 +1,24 @@
+<% provide(:title, @subject) %>
+<% provide(:photo_url, @lesson_session.teacher.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ <% if @slot.is_teacher_approved? %>
+ This teacher has accepted your lesson request!
+ <% else %>
+ You have confirmed a lesson request.
+ <% end %>
+
+ <% if @message.present? %>
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.text.erb
new file mode 100644
index 000000000..c7506b0ad
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_accepted.text.erb
@@ -0,0 +1,3 @@
+<%= @subject %>
+
+To see this lesson, click here: <%= @lesson_session.web_url %>
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_canceled.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_canceled.html.erb
new file mode 100644
index 000000000..bc819bb5e
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_booking_canceled.html.erb
@@ -0,0 +1,29 @@
+<% provide(:title, @subject) %>
+<% provide(:photo_url, @lesson_booking.canceler.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ <% if @lesson_booking.recurring %>
+
+ All lessons that were scheduled for <%= @lesson_booking.dayWeekDesc %> with <%= @teacher.name %> have been canceled.
+
+ <% else %>
+ Your lesson with <%= @teacher.name %> has been canceled.
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_canceled.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_canceled.text.erb
new file mode 100644
index 000000000..c7506b0ad
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_canceled.text.erb
@@ -0,0 +1,3 @@
+<%= @subject %>
+
+To see this lesson, click here: <%= @lesson_session.web_url %>
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.html.erb
new file mode 100644
index 000000000..38ec8a404
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.html.erb
@@ -0,0 +1,17 @@
+<% provide(:title, "#{@teacher.name} has proposed a different time for your lesson") %>
+<% provide(:photo_url, @teacher.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ <%= @teacher.name %> has proposed a different time for your lesson request.
+
+
+ Click the button below to get more information and respond.
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.text.erb
new file mode 100644
index 000000000..67972c906
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_counter.text.erb
@@ -0,0 +1,3 @@
+<%= @teacher.name %> has proposed a different time for your lesson request.
+
+To see this lesson, click here: <%= @lesson_session.web_url %>
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_charged.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_charged.html.erb
new file mode 100644
index 000000000..d795f64b4
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_charged.html.erb
@@ -0,0 +1,28 @@
+<% provide(:title, @subject) %>
+<% provide(:photo_url, @teacher.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ Hello <%= @student.name %>,
+
+
+
+ You have been billed $<%= @lesson_package_purchase.amount_charged %> for this month's lessons with <%= @teacher.name %>.
+
+
+
+ <% if !@student.has_rated_teacher(@teacher) %>
+ If you haven't already done so, please rate your teacher now to help other students in the community find the best
+ instructors.
+ <% end %>
+
+ If you had technical problems during your lesson, or have questions, or would like to make suggestions
+ on how to improve JamClass, please email us at support@jamkazam.com.
+
+
+
+ Best Regards, Team JamKazam
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_charged.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_charged.text.erb
new file mode 100644
index 000000000..7328cf9a7
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_charged.text.erb
@@ -0,0 +1,14 @@
+Hello <%= @student.name %>,
+
+You have been billed $<%= @lesson_package_purchase.amount_charged %> for this month's lessons with <%= @teacher.name %>.
+
+<% if !@student.has_rated_teacher(@teacher) %>
+If you haven't already done so, please rate your teachernow to help other students in the community find the best instructors. <%= @teacher.ratings_url %>
+<% end %>
+
+If you had technical problems during your lesson, or have questions, or would like to make suggestions
+on how to improve JamClass, please email us at support@jamkazam.com.
+
+Best Regards,
+Team JamKazam
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_done.html.erb
new file mode 100644
index 000000000..d795f64b4
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_monthly_done.html.erb
@@ -0,0 +1,28 @@
+<% provide(:title, @subject) %>
+<% provide(:photo_url, @teacher.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ Hello <%= @student.name %>,
+
+
+
+ You have been billed $<%= @lesson_package_purchase.amount_charged %> for this month's lessons with <%= @teacher.name %>.
+
+
+
+ <% if !@student.has_rated_teacher(@teacher) %>
+ If you haven't already done so, please rate your teacher now to help other students in the community find the best
+ instructors.
+ <% end %>
+
+ If you had technical problems during your lesson, or have questions, or would like to make suggestions
+ on how to improve JamClass, please email us at support@jamkazam.com.
+
+
+
+ Best Regards, Team JamKazam
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.html.erb
new file mode 100644
index 000000000..a6a2cddc7
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.html.erb
@@ -0,0 +1,28 @@
+<% provide(:title, "Your JamClass lesson today with #{@teacher.first_name}") %>
+<% provide(:photo_url, @teacher.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ Hello <%= @student.name %>,
+
+
+
+ We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. You have been billed $<%= @lesson_session.amount_charged %> for today's lesson.
+
+
+
+ <% if !@student.has_rated_teacher(@teacher) %>
+ If you haven't already done so, please rate your teacher now to help other students in the community find the best
+ instructors.
+ <% end %>
+
+ If you had technical problems during your lesson, or have questions, or would like to make suggestions
+ on how to improve JamClass, please email us at support@jamkazam.com.
+
+
+
+ Best Regards, Team JamKazam
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.text.erb
new file mode 100644
index 000000000..efdccf098
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_done.text.erb
@@ -0,0 +1,16 @@
+Hello <%= @student.name %>,
+
+We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. You have been billed $<%= @lesson_session.amount_charged %> for today's lesson.
+
+<% if !@student.has_rated_teacher(@teacher) %>
+ If you haven't already done so, please rate your teacher now to help other students in the community find the best
+ instructors. You can rate your teacher here: <%= @teacher.ratings_url %>
+<% end %>
+
+If you had technical problems during your lesson, or have questions, or would like to make suggestions
+on how to improve JamClass, please email us at support@jamkazam.com.
+
+Best Regards,
+Team JamKazam
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.html.erb
new file mode 100644
index 000000000..aa263319f
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_normal_no_bill.html.erb
@@ -0,0 +1,23 @@
+<% provide(:title, "Your lesson with #{@teacher.name} will not be billed") %>
+<% provide(:photo_url, @teacher.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ Hello <%= @student.name %>,
+
+
+
You will not be billed for today's session with <%= @teacher.name %>.
+
+
+ Click the button below to see more information about this session.
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_update_all.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_update_all.text.erb
new file mode 100644
index 000000000..6011f7e65
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_lesson_update_all.text.erb
@@ -0,0 +1,3 @@
+All your lessons with <%= @lesson_session.teacher.name%> have been rescheduled.
+
+To see this lesson, click here: <%= @lesson_session.web_url %>
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.html.erb
new file mode 100644
index 000000000..f192b7b9c
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.html.erb
@@ -0,0 +1,11 @@
+<% provide(:title, @subject) %>
+
+
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.text.erb
new file mode 100644
index 000000000..63679c0d2
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_scheduled_jamclass_invitation.text.erb
@@ -0,0 +1,7 @@
+<%= @body %>
+
+<%= @session_name %>
+<%= @session_description %>
+<%= @session_date %>
+
+See session details at <%= @session_url %>.
\ 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
new file mode 100644
index 000000000..56e8750e5
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.html.erb
@@ -0,0 +1,30 @@
+<% 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 %>
+
+ Hello <%= @student.name %>,
+
+
+
We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. You have
+ used <%= @student.used_test_drives %> TestDrive credits, and you have <%= @student.remaining_test_drives %>
+ remaining TestDrive lesson(s) available. If you haven’t booked your next TestDrive lesson,
+ click here to search our teachers and get your next
+ lesson lined up today!
+
+
+ <% if !@student.has_rated_teacher(@teacher) %>
+ Also, please rate your teacher now for today’s lesson
+ to help other students in the community find the best instructors.
+ <% end %>
+ And if you had technical problems during your lesson, or have questions, or would like to make suggestions on how to
+ improve JamClass,
+ please email us at support@jamkazam.com.
+
+
+
+ Best Regards, Team JamKazam
+
+<% end %>
+
+
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
new file mode 100644
index 000000000..34db7e7bc
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_completed.text.erb
@@ -0,0 +1,14 @@
+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.
+<% end %>
+
+If you clicked with one of your TestDrive instructors, here are links to each teacher’s listing. You can use the
+link to your favorite to book single or weekly recurring lessons with the best instructor for you!
+<% @student.recent_test_drive_teachers.each do |teacher| %>
+<%= teacher.name %>: <%= teacher.teacher_profile_url %>
+<% end %>
+
+And if you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass,
+please email us at support@jamkazam.com.
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.html.erb
new file mode 100644
index 000000000..d99b8494a
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.html.erb
@@ -0,0 +1,47 @@
+<% provide(:title, "You have used all TestDrive lesson credits") %>
+
+
+ Hello <%= @student.name %>,
+
+
+
+ We hope you enjoyed your JamClass lesson today with <%= @teacher.name %>. You have now used all your TestDrive credits.
+
+ <% if !@student.has_rated_teacher(@teacher) %>
+ Please rate your teacher now for today’s lesson to
+ help other students in the community find the best instructors.
+ <% end %>
+
+
+ If you clicked with one of your TestDrive instructors, here are links to each teacher’s listing. You can use the
+ link to your favorite to book single or weekly recurring lessons with the best instructor for you!
+
+<% @student.recent_test_drive_teachers.each do |teacher| %>
+
+
+ And if you had technical problems during your lesson, or have questions, or would like to make suggestions on how
+ to improve JamClass,
+ please email us at support@jamkazam.com.
+
+
+
+ Best Regards, Team JamKazam
+
+
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.text.erb
new file mode 100644
index 000000000..f80c5c8cb
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_lesson_done.text.erb
@@ -0,0 +1,8 @@
+You have used all of your 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.
+<% end %>
+
+And if you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass,
+please email us at support@jamkazam.com.
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.html.erb
new file mode 100644
index 000000000..a10a0bc5d
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.html.erb
@@ -0,0 +1,23 @@
+<% provide(:title, "Your TestDrive with #{@teacher.name} will not be billed") %>
+<% provide(:photo_url, @teacher.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ Hello <%= @student.name %>,
+
+
+
You have not used a credit for today's TestDrive with <%= @teacher.name %> because <%= @lesson_session.error_display %>.
+
+
+ Click the button below to see more information about this session.
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.text.erb
new file mode 100644
index 000000000..f3d3b2825
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_test_drive_no_bill.text.erb
@@ -0,0 +1,5 @@
+Hello <%= @student.name %>,
+
+You have not used a credit for today's TestDrive with <%= @teacher.name %>.
+
+To see this lesson, click here: <%= @lesson_session.web_url %>
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.html.erb
new file mode 100644
index 000000000..2da383332
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.html.erb
@@ -0,0 +1,28 @@
+<% provide(:title, "The credit card charge for your lesson today with #{@teacher.first_name} failed") %>
+<% provide(:photo_url, @teacher.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ Hello <%= @student.name %>,
+
+
+
+
+ <% if @card_declined %>
+ When we tried to charge your credit card for your lesson on <%= @bill_date %> with <%= @teacher.name %>, the charge was declined. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you!
+ <% elsif @card_expired %>
+ When we tried to charge your credit card for your lesson on <%= @bill_date %> with <%= @teacher.name %>, the charge failed because the card is expired. To update your credit card information, please use the button below to go to a page where you can change your card info. Thank you!
+ <% else %>
+ For some reason, when we tried to charge your credit card for your lesson on <%= @bill_date %> with <%= @teacher.name %>, the charge failed. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you!
+ <% end %>
+
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.text.erb
new file mode 100644
index 000000000..b4cfb4628
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge.text.erb
@@ -0,0 +1,18 @@
+The credit card charge for your lesson today with <%= @teacher.first_name %> failed.
+Hello <%= @student.name %>,
+
+<% if @card_declined %>
+ When we tried to charge your credit card for your lesson on <%= @bill_date %> with <%= @teacher.name %>, the charge was declined. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you!
+<% elsif @card_expired %>
+ When we tried to charge your credit card for your lesson on <%= @bill_date %> with <%= @teacher.name %>, the charge failed because the card is expired. To update your credit card information, please use the button below to go to a page where you can change your card info. Thank you!
+<% else %>
+ For some reason, when we tried to charge your credit card for your lesson on <%= @bill_date %> with <%= @teacher.name %>, the charge failed. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you!
+<% end %>
+
+
+Update Payment info here: <%= @lesson_session.update_payment_url %>
+
+Best Regards,
+Team JamKazam
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.html.erb
new file mode 100644
index 000000000..f4d5f5402
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.html.erb
@@ -0,0 +1,32 @@
+<% provide(:title, @subject) %>
+<% provide(:photo_url, @teacher.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ Hello <%= @student.name %>,
+
+
+
+ <% if @lesson_booking.is_suspended? %>
+ Your weekly lessons with <%= @teacher.name %> have been suspended because we have tried repeatedly to charge your credit card but have failed.
+
+
+ <% end %>
+
+ <% if @card_declined %>
+ When we tried to charge your credit card for your <%= @month_name %> lessons on <%= @bill_date %> with <%= @teacher.name %>, the charge was declined. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you!
+ <% elsif @card_expired %>
+ When we tried to charge your credit card for your <%= @month_name %> lessons on <%= @bill_date %> with <%= @teacher.name %>, the charge failed because the card is expired. To update your credit card information, please use the button below to go to a page where you can change your card info. Thank you!
+ <% else %>
+ For some reason, when we tried to charge your credit card for your <%= @month_name %> lessons on <%= @bill_date %> with <%= @teacher.name %>, the charge failed. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you!
+ <% end %>
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.text.erb
new file mode 100644
index 000000000..2567d6891
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_unable_charge_monthly.text.erb
@@ -0,0 +1,23 @@
+Hello <%= @student.name %>,
+
+<%= @subject %>
+
+<% if @lesson_booking.is_suspended? %>
+ Your weekly lessons with <%= @teacher.name %> have been suspended because we have tried repeatedly to charge your credit card but have failed.
+<% end %>
+
+<% if @card_declined %>
+ When we tried to charge your credit card for your <%= @month_name %> lessons on <%= @bill_date %> with <%= @teacher.name %>, the charge was declined. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you!
+<% elsif @card_expired %>
+ When we tried to charge your credit card for your <%= @month_name %> lessons on <%= @bill_date %> with <%= @teacher.name %>, the charge failed because the card is expired. To update your credit card information, please use the button below to go to a page where you can change your card info. Thank you!
+<% else %>
+ For some reason, when we tried to charge your credit card for your <%= @month_name %> lessons on <%= @bill_date %> with <%= @teacher.name %>, the charge failed. Can you please call your credit card company to clear up the issue, or if you need to update some of your credit card information, please use the button below to go to a page where you can change your card info. Thank you!
+<% end %>
+
+
+Update Payment info here: <%= @lesson_booking.update_payment_url %>
+
+Best Regards,
+Team JamKazam
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb
new file mode 100644
index 000000000..d2d9a47f2
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.html.erb
@@ -0,0 +1,52 @@
+<% provide(:title, @subject) %>
+
+
+<% if !@user.anonymous? %>
+
+ Thank you for signing up to take online music lessons using the JamClass service by JamKazam. JamKazam technology was
+ built from the ground up for playing music live in sync with high quality audio from different locations over the
+ Internet. Unlike other lesson services, this means we can deliver a massively better online music lesson experience
+ than voice/chat apps like Skype, etc. Our rapidly growing community of <%= APP_CONFIG.musician_count %> musicians will attest to this.
+
+
+
+ To get ready to take JamClass lessons online, here are the things you'll want to do:
+
+
+
1. Find a Teacher & Book Lessons
+
+ If you haven't done so already, use this link to search our teachers, and click to book a TestDrive with a teacher who looks good for you. When you do this, you'll be given the option to take full 30-minute TestDrive lessons:
+
+
+
With 4 different teachers for just $12.50 each
+
With 2 different teachers for just $14.99 each
+
Or with 1 teacher for just $14.99
+
+
+ Pick whichever option you prefer. TestDrive lets you safely and easily try multiple teachers to find the one who is best specifically for you, which is a great way to maximize the benefit from your lessons. And TestDrive lessons are heavily discounted to give you a risk-free way to get started. We'd suggest scheduling your first lesson for about a week in the future to give you plenty of time to get up and running with our free app.
+
+
+
+
2. Set Up Your Gear
+ Please review our help articles on gear recommendations
+ to make sure you have everything you need to get set up properly for best results in your online lessons.
+ If you have everything you need, then you can follow the instructions on our setup help articles to download and install our free app and set it up with your audio gear and webcam. Please email us at support@jamkazam.com or call us at 1-877-376-8742 any time so that we can help you with these steps. We are very happy to help, and we also strongly suggest that you let one of our staff get into an online session with you to make sure everything is working properly and to make sure you're comfortable with the app and ready for your first lesson.
+
+
+
3. Learn About JamClass Features
+ Please review our JamClass user guide for students to familiarize yourself with the features and resources available to you through our JamClass lesson service. This includes how to search for the best teacher for you, how to request/book lessons, how to join your teacher in online lessons, features you can use while in lessons, and much more.
+
+
+
+
+ Again, welcome to JamKazam and our JamClass online music lesson service, and we look forward to helping you learn and grow as a musician!
+
+
+
+
Best Regards,
+ Team JamKazam
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.text.erb
new file mode 100644
index 000000000..b16eef375
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/student_welcome_message.text.erb
@@ -0,0 +1,57 @@
+<% if !@user.anonymous? %>Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --<% end %>
+
+Thank you for signing up to take online music lessons using the JamClass service by JamKazam. JamKazam technology was
+built from the ground up for playing music live in sync with high quality audio from different locations over the
+Internet. Unlike other lesson services, this means we can deliver a massively better online music lesson experience
+than voice/chat apps like Skype, etc. Our rapidly growing community of <%= APP_CONFIG.musician_count %> musicians will attest to this.
+
+
+To get ready to take JamClass lessons online, here are the things you'll want to do:
+
+1. Find a Teacher & Book Lessons
+If you already know the teacher from whom you want to learn, then you can simply
+use this link to search for them (https://www.jamkazam.com/client#/jamclass/searchOptions), and
+click the Book Lesson button to get started. But if you're like most of us, you don't know. In this case, we strongly
+advise signing up for our unique TestDrive service.
+
+TestDrive lets you take 4 full lessons (30 minutes each) from 4 different teachers for just $49.99 to find the best
+teacher for you. Finding the right teacher is the single most important determinant of success in your lessons. Would
+you marry the first person you ever dated? No? Same here. Pick 4 teachers who look great, and then see who you click
+with. It's a phenomenal value, and then you can stick with the best teacher for you.
+Click this link to sign up now for TestDrive (https://www.jamkazam.com/client#/jamclass/test-drive-selection).
+Then you can book 4 TestDrive lessons to get rolling.
+
+2. Set Up Your Gear
+Use this link to a set of
+help articles on how to set up your gear (https://jamkazam.desk.com/customer/en/portal/topics/673197-first-time-setup/articles)
+to be ready to teach online. After you have signed
+up, someone from JamKazam will contact you to schedule a test online session, in which we will make sure your audio
+and video gear are working properly in an online session, and to make sure you feel comfortable with the key features
+you will be using in sessions with teachers.
+
+3. Learn About JamClass Features
+Use this link to a set of help articles for students on JamClass (https://jamkazam.desk.com/customer/en/portal/topics/926073-jamclass-online-music-lessons---for-students/articles)
+to familiarize yourself with the most useful features
+for online lessons. This includes how to search for the best teacher for you, how to request/book lessons, how to join
+your teacher in online lessons, features you can use while in lessons, and much more. There is very important basic
+information, plus some really nifty stuff here, so be sure to look through it at least briefly to see how we can
+turbocharge your online lessons!
+
+4. Play With Other Musicians Online - It's Free!
+With JamKazam, you can use the things you're learning in lessons to play with other amateur musicians in online
+sessions, free! Or just play for fun. Once you've set up your gear for lessons, you can
+create online music sessions (https://jamkazam.desk.com/customer/en/portal/articles/1599977-creating-a-session)
+that others can join, or find other musicians' online music sessions (https://jamkazam.desk.com/customer/en/portal/articles/1599978-finding-a-session)
+and hop into those to play with others. If you
+want to take advantage of this part of the JamKazam platform, we'd advise that you edit your musician profile (https://www.jamkazam.com/client#/account/profile) to make
+it easier to connect with other musicians (https://jamkazam.desk.com/customer/en/portal/articles/1707418-connecting-with-other-musicians) in our community to expand your set of musician friends. It's a ton of fun,
+so give it a try!
+
+As you work through these things, if you ever get stuck or have questions, please don't hesitate to reach out for
+help. You can email us any time at support@jamkazam.com. We are happy to
+help you, and we look forward to helping you
+learn and grow as a musician, and expand your musical universe!
+
+Best Regards,
+Team JamKazam
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_counter_reminder.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_counter_reminder.html.erb
new file mode 100644
index 000000000..5b5efd606
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_counter_reminder.html.erb
@@ -0,0 +1,17 @@
+<% provide(:title, @subject) %>
+<% provide(:photo_url, @student.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ <%= @student.name %> has proposed a different time 24 hours ago.
+
+
+ Please click the button below to respond.
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_counter_reminder.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_counter_reminder.text.erb
new file mode 100644
index 000000000..4a781de93
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_counter_reminder.text.erb
@@ -0,0 +1,3 @@
+<%= @student.name %> has proposed a different time 24 hours ago. Please respond.
+
+To see this lesson, click here: <%= @lesson_session.web_url %>
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.html.erb
new file mode 100644
index 000000000..3c057778d
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.html.erb
@@ -0,0 +1,50 @@
+<% provide(:title, @subject) %>
+
+
+ Hello <%= @name %>,
+
+
+ <% if @distribution.is_test_drive? %>
+ <% if @school %>
+
We hope you enjoyed your TestDrive lesson today with <%= @distribution.student.name %>.
+ <% else %>
+
You have earned <%= @distribution.real_distribution_display %> for your TestDrive lesson with <%= @distribution.student.name %>.
+ <% end %>
+
+ <% if !@teacher_payment.teacher.has_rated_student(@distribution.student) %>
+ If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community.
+ <% end %>
+ If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com.
+
we hope you enjoyed your lesson today with <%= @distribution.student.name %>.
+ <% else %>
+
You have earned <%= @distribution.real_distribution_display %> for your lesson with <%= @distribution.student.name %>.
+ <% end %>
+
+ <% if !@teacher_payment.teacher.has_rated_student(@distribution.student) %>
+ If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community.
+ <% end %>
+ If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com.
+
we hope you enjoyed your <%= @distribution.month_name %> lessons with <%= @distribution.student.name %>.
+ <% else %>
+
You have earned <%= @distribution.real_distribution_display %> for your <%= @distribution.month_name%> lessons with <%= @distribution.student.name %>.
+ <% end %>
+
+ <% if !@teacher_payment.teacher.has_rated_student(@distribution.student) %>
+ If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community.
+ <% end %>
+ If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com.
+
+ <% else %>
+ Unknown payment type.
+ <% end %>
+
+
+
+Best Regards,
+JamKazam
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.text.erb
new file mode 100644
index 000000000..e8ce56e85
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_done.text.erb
@@ -0,0 +1,40 @@
+<% provide(:title, @subject) %>
+
+Hello <%= @name %>,
+
+<% if @distribution.is_test_drive? %>
+<% if @school %>
+We hope you enjoyed your TestDrive lesson today with <%= @distribution.student.name %>.
+<% else %>
+You have earned <%= @distribution.real_distribution_display %> for your TestDrive lesson with <%= @distribution.student.name %>.
+<% end %>
+<% if !@teacher_payment.teacher.has_rated_student(@distribution.student) %>
+If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. <%= @distribution.student.student_ratings_url %>
+<% end%>
+If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com.
+<% elsif @distribution.is_normal? %>
+<% if @school %>
+We hope you enjoyed your lesson today with <%= @distribution.student.name %>.
+<% else %>
+You have earned <%= @distribution.real_distribution_display %>for your lesson with <%= @distribution.student.name %>.
+<% end %>
+<% if !@teacher_payment.teacher.has_rated_student(@distribution.student) %>
+If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. <%= @distribution.student.student_ratings_url %>
+<% end%>
+If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com.
+<% elsif @distribution.is_monthly? %>
+<% if @school %>
+We hope you enjoyed your <%= @distribution.month_name%> lessons with <%= @distribution.student.name %>.
+<% else %>
+You have earned <%= @distribution.real_distribution_display %>for your <%= @distribution.month_name%> lessons with <%= @distribution.student.name %>.
+<% end %>
+<% if !@teacher_payment.teacher.has_rated_student(@distribution.student) %>
+If you haven't already done so, please rate your student now to help us monitor for any issues with students who may cause issues for our instructor community. <%= @distribution.student.student_ratings_url %>
+<% end%>
+If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com.
+<% else %>
+Unknown payment type.
+<% end %>
+
+Best Regards,
+JamKazam
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.html.erb
new file mode 100644
index 000000000..bcd797ffd
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.html.erb
@@ -0,0 +1,23 @@
+<% provide(:title, @subject) %>
+
+
Hello <%= @name %>,
+
+<% if @school %>
+
+ We attempted to process a payment via your Stripe account for <%= @distribution.real_distribution_display %> for this lesson, but the payment failed. Please sign into your Stripe account, and verify that everything there is working properly. We’ll try again to process this payment in about 24 hours.
+
+<% else %>
+
+ <% if @card_declined %>
+ When we tried to distribute a payment to you on <%= @bill_date %>, the charge was declined by stripe. Can you please check your stripe account status? Thank you!
+ <% elsif @card_expired %>
+ When we tried to distribute a payment to you on <%= @bill_date %>, the charge was declined by stripe due to a card expiration. Can you please check your stripe account status? Thank you!
+ <% else %>
+ For some reason, when we tried to distribute a payment to you on <%= @bill_date %>, the charge failed. Can you please check your stripe account status? Thank you!
+ <% end %>
+
+<% end %>
+
+
+Best Regards,
+JamKazam
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.text.erb
new file mode 100644
index 000000000..a0ce26b67
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_distribution_fail.text.erb
@@ -0,0 +1,17 @@
+<% provide(:title, @subject) %>
+Hello <%= @name %>,
+
+<% if @school %>
+ We attempted to process a payment via your Stripe account for <%= @distribution.real_distribution_display %> for this lesson, but the payment failed. Please sign into your Stripe account, and verify that everything there is working properly. We’ll try again to process this payment in about 24 hours.
+<% else %>
+ <% if @card_declined %>
+When we tried to distribute a payment to you on <%= @bill_date %>, the charge was declined by stripe. Can you please check your stripe account status? Thank you!
+ <% elsif @card_expired %>
+When we tried to distribute a payment to you on <%= @bill_date %>, the charge was declined by stripe due to a card expiration. Can you please check your stripe account status? Thank you!
+ <% else %>
+For some reason, when we tried to distribute a payment to you on <%= @bill_date %>, the charge failed. Can you please check your stripe account status? Thank you!
+ <% end %>
+<% end %>
+
+Best Regards,
+JamKazam
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.html.erb
new file mode 100644
index 000000000..ab838c208
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.html.erb
@@ -0,0 +1,18 @@
+<% provide(:title, @subject) %>
+<% provide(:photo_url, @lesson_session.student.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ <% if @slot.is_teacher_approved? %>
+ You have confirmed a lesson request.
+ <% else %>
+ This student has accepted your lesson request!
+ <% end %>
+
+
We strongly suggest adding this to your calendar so you don't forget it.
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.text.erb
new file mode 100644
index 000000000..6823a39f7
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_accepted.text.erb
@@ -0,0 +1,3 @@
+You have confirmed a lesson request for <%= @sender.name %>.
+
+To see this lesson, click here: <%= @lesson_session.web_url %>
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_booking_canceled.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_booking_canceled.html.erb
new file mode 100644
index 000000000..c0095a170
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_booking_canceled.html.erb
@@ -0,0 +1,29 @@
+<% provide(:title, @subject) %>
+<% provide(:photo_url, @lesson_booking.canceler.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ <% if @lesson_booking.recurring %>
+
+ All lessons that were scheduled for <%= @lesson_booking.dayWeekDesc %> with <%= @student.name %> have been canceled.
+
+ <% else %>
+ Your lesson with <%= @student.name %> has been canceled.
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_canceled.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_canceled.text.erb
new file mode 100644
index 000000000..c7506b0ad
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_canceled.text.erb
@@ -0,0 +1,3 @@
+<%= @subject %>
+
+To see this lesson, click here: <%= @lesson_session.web_url %>
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_completed.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_completed.html.erb
new file mode 100644
index 000000000..831a074bf
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_completed.html.erb
@@ -0,0 +1,20 @@
+<% provide(:title, "You successfully completed a lesson with #{@student.name}") %>
+<% provide(:photo_url, @student.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ <% if @lesson_session.student_missed %>
+ Even though the student missed the lesson,
+ <% end %>
+ <%= @student.name %> will first be billed and you should receive your payment in the next 48 hours.
+
+
+ Click the button below to see more information about this session.
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_completed.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_completed.text.erb
new file mode 100644
index 000000000..30e884e71
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_completed.text.erb
@@ -0,0 +1,3 @@
+You successfully completed a lesson with <%= @student.name %>.
+
+To see this lesson, click here: <%= @lesson_session.web_url %>
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.html.erb
new file mode 100644
index 000000000..765f13b99
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.html.erb
@@ -0,0 +1,17 @@
+<% provide(:title, "#{@student.name} has proposed a different time for their lesson") %>
+<% provide(:photo_url, @student.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ <%= @student.name %> has proposed a different time for their lesson request.
+
+
+ Click the button below to get more information and respond.
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.text.erb
new file mode 100644
index 000000000..050917a31
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_counter.text.erb
@@ -0,0 +1,3 @@
+<%= @student.name %> has proposed a different time for their lesson request.
+
+To see this lesson, click here: <%= @lesson_session.web_url %>
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.html.erb
new file mode 100644
index 000000000..5b8bc3891
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.html.erb
@@ -0,0 +1,23 @@
+<% provide(:title, @subject) %>
+<% provide(:photo_url, @student.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ Hello <%= @teacher.name %>,
+
+
+
+ Your student, <%= @student.name %>, has been billed for this month's lessons. You should receive your funds within 48 hours.
+
+
+
+ If you had technical problems during your lesson, or have questions, or would like to make suggestions
+ on how to improve JamClass, please email us at support@jamkazam.com.
+
+
+
+ Best Regards, Team JamKazam
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.text.erb
new file mode 100644
index 000000000..0347dc3be
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_monthly_charged.text.erb
@@ -0,0 +1,9 @@
+Hello <%= @teacher.name %>,
+
+Your student, <%= @student.name %>, has been billed for this month's lessons. You should receive your funds within 48 hours.
+
+If you had technical problems during your lesson, or have questions, or would like to make suggestions on how to improve JamClass, please email us at support@jamkazam.com.
+
+Best Regards,
+Team JamKazam
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.html.erb
new file mode 100644
index 000000000..8ec5b345c
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.html.erb
@@ -0,0 +1,23 @@
+<% provide(:title, "Your JamClass lesson today with #{@student.first_name}") %>
+<% provide(:photo_url, @student.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ Hello <%= @teacher.name %>,
+
+
+
+ Your student <%= @student.name %> will be billed for today's lesson, and you should receive payment within 48 hours.
+
+
+
+ If you had technical problems during your lesson, or have questions, or would like to make suggestions
+ on how to improve JamClass, please email us at support@jamkazam.com.
+
+
+
+ Best Regards, Team JamKazam
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.text.erb
new file mode 100644
index 000000000..08db0f67e
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_done.text.erb
@@ -0,0 +1,11 @@
+Hello <%= @teacher.name %>,
+
+Your student <%= @student.name %> will be billed for today's lesson, and you will receive payment within 24 hours.
+
+If you had technical problems during your lesson, or have questions, or would like to make suggestions
+on how to improve JamClass, please email us at support@jamkazam.com.
+
+Best Regards,
+Team JamKazam
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.html.erb
new file mode 100644
index 000000000..7b24c238e
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.html.erb
@@ -0,0 +1,23 @@
+<% provide(:title, "Your student #{@student.name} will not be charged for their lesson") %>
+<% provide(:photo_url, @student.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ Hello <%= @teacher.name %>,
+
+
+
Your student <%= @student.name %> will not be billed for today's session.
+
+
+ Click the button below to see more information about this session.
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.text.erb
new file mode 100644
index 000000000..9b9f3b91c
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_normal_no_bill.text.erb
@@ -0,0 +1,5 @@
+Hello <%= @teacher.name %>,
+
+Your student <%= @student.name %> will not be billed for today's session.
+
+To see this lesson, click here: <%= @lesson_session.web_url %>
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_request.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_request.html.erb
new file mode 100644
index 000000000..a82e118cb
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_request.html.erb
@@ -0,0 +1,11 @@
+<% provide(:title, "Lesson Request from #{@sender.name}") %>
+<% provide(:photo_url, @sender.resolved_photo_url) %>
+
+<% content_for :note do %>
+
This student has requested to schedule a <%= @lesson_booking.display_type %> lesson.
Click the button below to get more information and to respond to this lesson request. You must respond to this lesson request promptly, or it will be cancelled, thank you!
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_update_all.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_update_all.text.erb
new file mode 100644
index 000000000..5fc1c92a5
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_lesson_update_all.text.erb
@@ -0,0 +1,3 @@
+All your lessons with <%= @lesson_session.student.name %> have been rescheduled.
+
+To see this lesson, click here: <%= @lesson_session.web_url %>
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.html.erb
new file mode 100644
index 000000000..f192b7b9c
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.html.erb
@@ -0,0 +1,11 @@
+<% provide(:title, @subject) %>
+
+
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.text.erb
new file mode 100644
index 000000000..63679c0d2
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_scheduled_jamclass_invitation.text.erb
@@ -0,0 +1,7 @@
+<%= @body %>
+
+<%= @session_name %>
+<%= @session_description %>
+<%= @session_date %>
+
+See session details at <%= @session_url %>.
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_test_drive_no_bill.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_test_drive_no_bill.html.erb
new file mode 100644
index 000000000..63b6ad7cb
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_test_drive_no_bill.html.erb
@@ -0,0 +1,23 @@
+<% provide(:title, "Your TestDrive with #{@student.name} was not successful") %>
+<% provide(:photo_url, @student.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ Hello <%= @teacher.name %>,
+
+
+
Your TestDrive with <%= @student.name %> was not successful because <%= @lesson_session.error_display %>.
+
+
+ Click the button below to see more information about this session.
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_test_drive_no_bill.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_test_drive_no_bill.text.erb
new file mode 100644
index 000000000..59a24b9d2
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_test_drive_no_bill.text.erb
@@ -0,0 +1,5 @@
+Hello <%= @teacher.name %>,
+
+You have not used a credit for today's TestDrive with <%= @teacher.name %>.
+
+To see this lesson, click here: <%= @lesson_session.web_url %>
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.html.erb
new file mode 100644
index 000000000..fbee04816
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.html.erb
@@ -0,0 +1,21 @@
+<% provide(:title, @subject) %>
+<% provide(:photo_url, @student.resolved_photo_url) %>
+
+<% content_for :note do %>
+
+ Hello <%= @teacher.name %>,
+
+
+
+ <% if @lesson_booking.is_suspended? %>
+ Your weekly lessons with <%= @student.name %> have been suspended because we have tried repeatedly to charge their credit card but the charge was declined. They have been asked to re-enter updated credit card info.
+ <% else %>
+ We have tried to charge the credit card of <%= @student.name %> but the charge was declined. They still have time to re-enter their credit card info before the session is suspended, though.
+ <% end %>
+
+
+ Best Regards, Team JamKazam
+
+<% end %>
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.text.erb
new file mode 100644
index 000000000..8807cbbda
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_unable_charge_monthly.text.erb
@@ -0,0 +1,14 @@
+Hello <%= @student.name %>,
+
+<%= @subject %>
+
+<% if @lesson_booking.is_suspended? %>
+ Your weekly lessons with <%= @student.name %> have been suspended because we have tried repeatedly to charge their credit card but the charge was declined. They have been asked to re-enter updated credit card info.
+<% else %>
+ We have tried to charge the credit card of <%= @student.name %> but the charge was declined. They still have time to re-enter their credit card info before the session is suspended, though.
+<% end %>
+
+Best Regards,
+Team JamKazam
+
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.html.erb
new file mode 100644
index 000000000..0590bfa1c
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.html.erb
@@ -0,0 +1,61 @@
+<% provide(:title, @subject) %>
+
+
+<% if !@user.anonymous? %>
+
+ Thank you for signing up to teach online music lessons using the JamClass service by JamKazam. JamKazam technology was
+ built from the ground up for playing music live in sync with high quality audio from different locations over the
+ Internet, so you will find it delivers a massively better online music lesson platform than voice/chat apps like
+ Skype, etc.
+
+
+
+ To get ready to teach JamClass students online, here are the things you'll want to do:
+
+
+
+
1. Set Up Your Teacher Profile
+ As JamKazam brings students into the JamClass marketplace, these students search for teachers. The way they find
+ teachers is by searching on their criteria (e.g. instruments, genres, etc.), and then by browsing through teacher
+ profiles to get a feel for the teachers who match their search criteria. Your teacher profile is critical to being
+ found in searches, and then presenting yourself in more depth to students who are interested in you. So you'll want to
+ take a little time to fill in the information in your teacher profile to present yourself well.
+ Click
+ here for
+ instructions on filling out your teacher profile.
+
+ As you work through these things, if you ever get stuck or have questions, please don't hesitate to reach out for
+ help. You can email us any time at support@jamkazam.com.
+ We are happy to help you, and we look forward to helping you
+ reach and teach more students!
+
+
+
Best Regards,
+ Team JamKazam
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.text.erb
new file mode 100644
index 000000000..9702e24f5
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/teacher_welcome_message.text.erb
@@ -0,0 +1,38 @@
+<% if !@user.anonymous? %>Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --
+<% end %>
+
+Thank you for signing up to teach online music lessons using the JamClass service by JamKazam. JamKazam technology was
+built from the ground up for playing music live in sync with high quality audio from different locations over the
+Internet, so you will find it delivers a massively better online music lesson platform than voice/chat apps like
+Skype, etc.
+
+To get ready to teach JamClass students online, here are the things you'll want to do:
+
+1. Set Up Your Teacher Profile
+As JamKazam brings students into the JamClass marketplace, these students search for teachers. The way they find
+teachers is by searching on their criteria (e.g. instruments, genres, etc.), and then by browsing through teacher
+profiles to get a feel for the teachers who match their search criteria. Your teacher profile is critical to being
+found in searches, and then presenting yourself in more depth to students who are interested in you. So you'll want to
+take a little time to fill in the information in your teacher profile to present yourself well.
+Click here for instructions on filling out your teacher profile. (https://jamkazam.desk.com/customer/en/portal/articles/2405835-creating-your-teacher-profile)
+
+2. Set Up Your Gear
+Click here for information on the gear requirements to effectively teach using the JamClass service (https://jamkazam.desk.com/customer/en/portal/articles/1288274-computer-internet-audio-and-video-requirements).
+When you have everything you need, use
+this set of help articles as a good step-by-step guide to set up your gear for use with the
+JamKazam application (https://jamkazam.desk.com/customer/en/portal/topics/930331-setting-up-your-gear-to-play-in-online-sessions/articles).
+After you have signed up, someone from JamKazam will contact you to schedule a test online
+session, in which we will make sure your audio and video gear are working properly in an online session, and to make
+sure you feel comfortable with the key features you will be using in sessions with students.
+
+3. Learn About JamClass Features
+Click this link for a set of help articles specifically for teachers to learn how to respond to student lesson
+requests, how to join your lessons when they are scheduled to begin, how to get paid, and more (https://jamkazam.desk.com/customer/en/portal/topics/926076-jamclass-online-music-lessons---for-teachers/articles).
+You can also use this link for a set of help articles that explain how to use the key features available to you in online sessions to
+effectively teach students (https://jamkazam.desk.com/customer/en/portal/topics/673198-key-features-to-use-in-online-sessions/articles).
+
+As you work through these things, if you ever get stuck or have questions, please don't hesitate to reach out for help. You can email us any time at support@jamkazam.com. We are happy to help you, and we look forward to helping you reach and teach more students!
+
+Best Regards,
+Team JamKazam
+
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb
index 35e380618..8607f7bd5 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb
@@ -1,63 +1,89 @@
<% provide(:title, 'Welcome to JamKazam!') %>
+<% if !@user.anonymous? %>
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 some resources that can help you get oriented and get the most out of JamKazam.
+
We're delighted to welcome you to the JamKazam community of musicians. Following are
+
+ resources you can use to get the most out of JamKazam.
+ or teach online music lessons through our JamClass marketplace. Students can take lessons
-
Getting Started
- There are basically three kinds of setups you can use to play on JamKazam.
-
-
Built-In Audio on Your Computer - You can use a Windows or Mac computer, and just use the built-in mic and headphone jack to
- handle your audio. This is cheap and easy, but your audio quality will suffer, and it will also process audio very slowly,
- creating problems with latency, or lag, in your sessions. Still, you can at least start experimenting with JamKazam in this way.
-
Computer with External Audio Interface - You can use a Windows or Mac computer with an external audio interface that you
- already own and use for recording, if you happen to have one already. If you are going to do this, or use the built-in mic/headphones on your computer, please refer
- to our Minimum System Requirements
- to make sure your computer will work. These requirements were on the download page for the app, but you may have sped by them. Also, we'd recommend watching our
- Getting Started Video to learn more about your options here.
-
The JamBlaster - JamKazam has designed a new product from the ground up to be the best way to play music online in real time. It's called the JamBlaster.
- It processes audio faster than any of the thousands of combinations of computers and interfaces in use on JamKazam today, which means you can play with musicians
- who are farther away from you, and closer sessions will feel/sound tighter. The JamBlaster is both a computer and an audio interface, so it also eliminates the
- system requirements worries, and it "just works" so you don't have to be an audio and computer genius to get it working. This is a great product - available only
- through a Kickstarter program running during a 30-day window during parts of February and March 2015. You can watch the
- JamBlaster Video to learn more about this amazing new product.
-
+ from the best teacher for you vs. the closest, test drive multiple teachers to see who you click
+
+ with, avoid the time and hassle of travel to/from lessons, enjoy studio quality audio in online
+
+ sessions, play live in sync with your teacher, and record lessons for later reference. Teachers
+
+ can vastly increase your market of reachable students and let JamKazam bring students to you
+
+ to help build your student base and income. Unlike Skype and similar apps, the JamKazam
+
+ platform was built from the ground up for music, and it is the ideal technology for online music
+
+ lessons.
-
JamKazam Features
- JamKazam offers a very robust and exciting set of features for playing online and sharing your performances with others. Here are some videos you can watch
- to easily get up to speed on some of the things you can do with JamKazam:
-
Learn & Play Along With Your Favorite Music
+
+ JamTracks by JamKazam are the best way to play along with your favorite songs. JamTracks are
+
+ complete multi-track professional recordings, with fully isolated tracks for each part of the
+
+ music. Mute any part. Slow down playback for practice. Change pitch/key up or down. Record
+
+ yourself playing along with the rest of the band in audio or video, and more. Get your first
+
+ JamTrack free to try one out! After that they are just $1.99 each. Click here for more
+
+ information on how you can use JamTracks in your browser, in our free Mac or Windows
+
+ desktop app, or in our free iOS app.
+
+
+
Play Live And In Sync With Others from Different Locations
+ JamKazam’s free Mac and Windows desktop apps let musicians play together live and in sync
+
+ with hiqh quality audio from different locations over the Internet. Great for band rehearsals,
+
+ co-writing music, or just hopping into open jams with other musicians for fun. If you’re
+
+ interested in playing with others online, check out our gear recommendations, instructions for
+
+ setting up your gear, and help articles on how to set up your own online sessions or find and
+
+ join others’ sessions, plus key features to use while in online sessions.
-
Getting Help
- If you run into trouble and need help, please reach out to us. We will be glad to do everything we can to answer your questions and get you up and running.
- You can visit our
- Support Portal
- to find knowledge base articles and post questions that have
- not already been answered. You can email us at support@jamkazam.com. And if you just want to chat, share tips and war stories, and hang out with fellow JamKazamers,
- you can visit our Community Forum
- .
+
And More...
+
+ You can also generate income from our affiliate program by sharing info about JamKazam with
+
+ your friends and followers online. Connect and network with other musicians. And more. If you
+
+ run into trouble and need help, please reach out to us. We will be glad to do everything we can
+
+ to answer your questions and get you the help you need. You can visit our support portal to
+
+ find knowledge base articles and post questions that have not already been answered. You can
+
+ email us at support@jamkazam.com. And if you just want to chat, share tips and war stories,
+
+ and hang out with fellow JamKazam musicians, you can visit our community forum.
- Again, welcome to JamKazam, and we look forward to seeing – and hearing – you online soon!
+
+
+ Again, welcome to JamKazam, and we hope you have a great time here!
Best Regards,
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb
index a9f2ed06b..52dc8ab65 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb
@@ -1,4 +1,4 @@
-Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --
+<% if !@user.anonymous? %>Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --<% end %>
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 some resources that can help you get oriented and get the most out of JamKazam.
diff --git a/ruby/lib/jam_ruby/app/views/layouts/from_user_mailer.html.erb b/ruby/lib/jam_ruby/app/views/layouts/from_user_mailer.html.erb
index 5b7ce4aec..c80b0490c 100644
--- a/ruby/lib/jam_ruby/app/views/layouts/from_user_mailer.html.erb
+++ b/ruby/lib/jam_ruby/app/views/layouts/from_user_mailer.html.erb
@@ -5,13 +5,14 @@
A charge for your music lesson with {uncollectable.teacher.name} failed. Please update your credit card information immediately so that we can pay the instructor. If you have called your credit card provider and believe there should be no problem with your card, please email us at support@jamkazam.com so that we can figure out what's gone wrong. Thank you!
`
+
+ if @state.user?['has_stored_credit_card?'] && @state.uncollectables.length == 0
+ if @state.userWantsUpdateCC
+ header = 'Please update your billing address and payment information below.'
+ updateCardAction = `NEVERMIND`
+ actions = `
The agreement between your music school and JamKazam is part of JamKazam's terms of service. You can find the
+ complete terms of service here. And you can find the section that is
+ most specific to the music school terms here.
You are booking a single 30-minute TestDrive session.
+
+
You currently have {testDriveLessons} available. If you need to cancel, you must cancel at least 24 hours before the lesson is scheduled to start, or you will be charged 1 TestDrive lesson credit.
+
+
You are booking a single 30-minute TestDrive session.
+
+
Once payment is entered on the next screen, the teacher will be notified, and this lesson will then use 1 of {testDriveCredits} TestDrive credits. If you need to cancel, you must cancel at least 24 hours before the lesson is scheduled to start, or you will be charged 1 TestDrive lesson credit.
+
+
“If you want to use an audio plugin, click the manage audio plugins link above, and then click the scan for new or updated plugins link in the menu.
`
+
+ for input in @state.configureTracks.musicPorts.inputs
+
+ include = false
+ # we need to see that this input is unassigned, or one of the two selected
+ for unassignedInputs in @state.configureTracks.trackAssignments.inputs.unassigned
+ if unassignedInputs.id == input.id
+ include = true
+ break
+
+ if !include
+ # not see if it's the currently edited track
+ for currentInput in @state.configureTracks.editingTrack
+ if currentInput.id == input.id
+ include = true
+
+ if include
+ item = ``
+ inputOneOptions.push(item)
+ inputTwoOptions.push(item)
+
+
+ for plugin in @state.configureTracks.vstPluginList.vsts
+ if plugin.category == 'NONE'
+ vsts.push(``)
+ else if plugin.isInstrument == false
+ vsts.push(``)
+
+ if @state.configureTracks.editingTrack.vst?
+ vstAssignedThisTrack = true
+ selectedVst = @state.configureTracks.editingTrack.vst.file
+
+ vstSettingBtnClasses = classNames({'button-orange': vstAssignedThisTrack, 'button-grey': !vstAssignedThisTrack})
+ `
+
+
Audio Input Ports
+
Select one or two inputs ports to assign to this track. Note that if you assign a single input port, the app will automatically duplicate this port into a stereo track.
+ Choose your audio device. Drag and drop to assign input ports to tracks, and specify the instrument
+ for each track. Drag and drop to assign a pair of output ports for session stereo audio monitoring.
+
`
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/InLessonBroadcast.js.jsx.coffee b/web/app/assets/javascripts/react-components/InLessonBroadcast.js.jsx.coffee
new file mode 100644
index 000000000..379cb69a1
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/InLessonBroadcast.js.jsx.coffee
@@ -0,0 +1,95 @@
+context = window
+
+@InLessonBroadcast = React.createClass({
+ displayName: 'In Lesson Broadcast'
+
+ getTimeRemaining: (t) ->
+
+ if t < 0
+ t = -t
+
+ seconds = Math.floor( (t/1000) % 60 );
+ minutes = Math.floor( (t/1000/60) % 60 );
+ hours = Math.floor( (t/(1000*60*60)) % 24 );
+ days = Math.floor( t/(1000*60*60*24) );
+
+ return {
+ 'total': t,
+ 'days': days,
+ 'hours': hours,
+ 'minutes': minutes,
+ 'seconds': seconds
+ };
+
+
+ displayTime: () ->
+ if @props.lessonSession.initialWindow
+ # offset time by 10 minutes to get the 'you need to wait message' in
+ untilTime = @getTimeRemaining(@props.lessonSession.until.total + (10 * 60 * 1000))
+ else
+ untilTime = @props.lessonSession.until
+ timeString = ''
+ if untilTime.days != 0
+ timeString += "#{untilTime.days} days, "
+ if untilTime.hours != 0 || timeString.length > 0
+ timeString += "#{untilTime.hours} hours, "
+ if untilTime.minutes != 0 || timeString.length > 0
+ timeString += "#{untilTime.minutes} minutes, "
+ if untilTime.seconds != 0 || timeString.length > 0
+ timeString += "#{untilTime.seconds} seconds"
+
+ if timeString == ''
+ 'now!'
+ timeString
+
+ render: () ->
+ if @props.lessonSession.isStudent
+ role = 'student'
+ otherRole = 'teacher'
+ billingStatement = 'charged for the lesson'
+ else
+ role = 'teacher'
+ otherRole = 'student'
+ billingStatement = 'not receive payment for the lesson'
+ if @props.lessonSession.completed
+ if @props.lessonSession.success
+ content = `
+
This lesson is over.
+
`
+ else
+ content = `
+
This lesson is over, but will not be billed.
+
`
+ else if @props.lessonSession.beforeSession
+ content = `
+
This lesson will start in:
+
{this.displayTime()}
+
`
+ else if @props.lessonSession.initialWindow
+ content = `
+
You need to wait in this session for
+
{this.displayTime()}
+
to allow time for your {otherRole} to join you. If you leave before this timer reaches zero, and your {otherRole} joins this session, you will be marked absent and {billingStatement}.
+
`
+ else if @props.lessonSession.teacherFault
+ if @props.lessonSession.isStudent
+ content = `
+
You may now leave the session.
+
Your teacher will be marked absent and penalized for missing the lesson. You will not be charged for this lesson.
+
We apologize for your inconvenience, and we will work to remedy this situation.
+
`
+ else
+ content = `
+
You may now leave the session.
+
Your student will be marked absent and penalized for missing the lesson. You will still received payment for this lesson.
+
We apologize for your inconvenience, and we will work to remedy this situation.
Send invitations to teachers who teach through your music school. Teachers who accept your invitation
+ will be associated with your music school. Any revenues we collect for lessons delivered by these teachers will
+ be processed such that we remit your school's share of these revenues to you, and you will then be responsible
+ to distribute the teacher's share of these revenues, per the JamKazam terms of service. You will also have the
+ option to manage scheduling of lessons for students sourced to the teacher from the JamKazam marketplace.
`
+ else
+ title = 'invite student'
+ help = `
+ Send invitations to students who you have acquired through your own marketing initiatives (versus students
+ JamKazam has brought to you). We will not bill these students for lessons, or will we withhold portions of such
+ billings. All billing and management of your own students remains yours to manage, per the JamKazam terms of
+ service.
+
`
+
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/JamBlasterNameDialog.js.jsx.coffee b/web/app/assets/javascripts/react-components/JamBlasterNameDialog.js.jsx.coffee
new file mode 100644
index 000000000..1b3e77ad7
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/JamBlasterNameDialog.js.jsx.coffee
@@ -0,0 +1,94 @@
+context = window
+@JamBlasterNameDialog = React.createClass({
+
+ mixins: [Reflux.listenTo(@AppStore, "onAppInit")]
+ teacher: null
+
+ beforeShow: (args) ->
+ logger.debug("JamBlasterNameDialog.beforeShow", args.d1)
+ @setState({name: args.d1})
+
+
+ afterHide: () ->
+
+ onAppInit: (@app) ->
+ dialogBindings = {
+ 'beforeShow': @beforeShow,
+ 'afterHide': @afterHide
+ };
+
+ @app.bindDialog('jamblaster-name-dialog', dialogBindings);
+
+ getInitialState: () ->
+ {
+ name: ''
+ }
+
+
+ componentDidMount: () ->
+ @root = $(@getDOMNode())
+ @dialog = @root.closest('.dialog')
+
+ doCancel: (e) ->
+ e.preventDefault()
+ @app.layout.closeDialog('jamblaster-name-dialog', true);
+
+ onNameChange: (e) ->
+
+ @setState({name: $(e.target).val()})
+
+ updateName: (e) ->
+ e.preventDefault()
+
+ # validate
+
+ name = @root.find('.name').val()
+
+ characterMatch = /^[^a-z0-9,' -]+$/i
+
+ if name.length == 0 || name == ''
+ context.JK.Banner.showAlert('invalid name', 'Please specify a name.')
+ return
+ else if name.length < 2
+ context.JK.Banner.showAlert('invalid name', 'Please specify a name at least 3 characters long.')
+ return
+ else if name.length > 63
+ context.JK.Banner.showAlert('invalid name', 'The name must be less than 64 characters long.')
+ return
+ else if characterMatch.test(name)
+ context.JK.Banner.showAlert('invalid name',
+ 'The can only contain A-Z, 0-9, commas, apostrophes, spaces, or hyphens.')
+ return
+
+ result = context.jamClient.setJBName(name.trim())
+
+ if !result
+ context.JK.Banner.showAlert('unable to set the name',
+ 'Please email support@jamkazam.com and let us know the name you are specifying unsuccessfully, or refresh the page and try again.')
+ else
+ @app.layout.closeDialog('jamblaster-name-dialog')
+ render: () ->
+ `
+
+
+
+
update name of JamBlaster
+
+
+
+
You can change the display name for this JamBlaster. The name can only contain A-Z, 0-9, commas, apostrophes,
+ spaces, or hyphens. A valid example: "John Doe's JamBlaster"
No connection established. You may click the CONNECT button to try again. If you cannot connect, please contact us at support@jamkazam.com.
`
+ else
+
+ `
+
+
+
+
connect to JamBlaster
+
+
+
+
To connect this application/device with the selected JamBlaster, please click the CONNECT button below, and then push the small black plastic button located on the back of the JamBlaster between the USB and power ports to confirm this pairing within 60 seconds of clicking the Connect button below.
+
+ {message}
+
+ {countdown} {actions}
+
+
`
+
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/JamBlasterPortDialog.js.jsx.coffee b/web/app/assets/javascripts/react-components/JamBlasterPortDialog.js.jsx.coffee
new file mode 100644
index 000000000..21a345a1d
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/JamBlasterPortDialog.js.jsx.coffee
@@ -0,0 +1,83 @@
+context = window
+@JamBlasterPortDialog = React.createClass({
+
+ mixins: [Reflux.listenTo(@AppStore, "onAppInit")]
+ teacher: null
+
+ beforeShow: (args) ->
+ logger.debug("JamBlasterPortDialog.beforeShow")
+
+
+ afterHide: () ->
+
+ onAppInit: (@app) ->
+ dialogBindings = {
+ 'beforeShow': @beforeShow,
+ 'afterHide': @afterHide
+ };
+
+ @app.bindDialog('jamblaster-port-dialog', dialogBindings);
+
+ getInitialState: () ->
+ {
+ name: ''
+ }
+
+
+ componentDidMount: () ->
+ @root = $(@getDOMNode())
+ @dialog = @root.closest('.dialog')
+
+ doCancel: (e) ->
+ e.preventDefault()
+ @app.layout.closeDialog('jamblaster-port-dialog', true);
+
+ updatePort: (e) ->
+ e.preventDefault()
+
+ # validate
+
+ staticPort = @root.find('.port').val()
+
+ staticPort = new Number(staticPort);
+
+ console.log("staticPort", staticPort)
+ if context._.isNaN(staticPort)
+ @app.layout.notify({title: 'No Settings Have Been Saved!', text: 'Please enter a number from 1026-49150.'})
+ return
+
+ if staticPort < 1026 || staticPort >= 65525
+ @app.layout.notify({title: 'No Settings Have Been Saved!', text: 'Please pick a port from 1026 to 65524.'})
+ return
+
+ result = context.jamClient.setJbPortBindState({use_static_port: true, static_port: staticPort})
+
+ if !result
+ context.JK.Banner.showAlert('unable to set a static port',
+ 'Please email support@jamkazam.com and let us know the port number you are specifying unsuccessfully, or refresh the page and try again.')
+ else
+ @app.layout.closeDialog('jamblaster-port-dialog')
+ render: () ->
+ `
+
+
+
+
set static port for JamBlaster
+
+
+
+
You can specify any port you like, but we recommend an even number in the range including 1026-49150 to avoid conflicts with other programs. When configuring Port Forwarding in your router, be sure to open this port along with the next 10. For example, if this field is 12000, then in your router, forward ports 12000-12010 to your computer's IP address.
If you don't see your JamBlaster listed above, please check to make sure you have power
+ connected to your JamBlaster,
+ and make sure your JamBlaster is connected via an Ethernet cable to the same router/network as the device on
+ which you are viewing this application.
+
+ You have no paired JamBlaster currently. If you've paired the JamBlaster in the past, be sure it's plugged in
+ and connected to an ethernet cable. If you have not paired a JamBlaster before, please go to the JamBlaster management page.
+
`
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/JamClassPhone.js.jsx.coffee b/web/app/assets/javascripts/react-components/JamClassPhone.js.jsx.coffee
new file mode 100644
index 000000000..92135e55f
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/JamClassPhone.js.jsx.coffee
@@ -0,0 +1,33 @@
+context = window
+
+@JamClassPhone = React.createClass(
+ {
+ displayName: 'JamClassPhone',
+
+ getInitialProps: () ->
+ {
+ customClass: null
+ }
+ render: ->
+ classes = {"jamclass-phone": true}
+
+ if @props.customClass?
+ classes[@props.customClass] = true
+
+ `
+
+
+
+
+ Call
+
+ 877-37-MUSIC
+
+
+
+ Have questions? Call now!
+
+
+
`
+ }
+)
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/JamClassScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/JamClassScreen.js.jsx.coffee
new file mode 100644
index 000000000..dd0a7227d
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/JamClassScreen.js.jsx.coffee
@@ -0,0 +1,630 @@
+context = window
+rest = context.JK.Rest()
+logger = context.JK.logger
+
+SessionActions = context.SessionActions
+UserStore = context.UserStore
+LessonTimerStore = context.LessonTimerStore
+LessonTimerActions = context.LessonTimerActions
+
+@JamClassScreen = React.createClass({
+
+ mixins: [
+ @ICheckMixin,
+ @PostProcessorMixin,
+ Reflux.listenTo(AppStore, "onAppInit"),
+ Reflux.listenTo(UserStore, "onUserChanged"),
+ Reflux.listenTo(LessonTimerStore, "onLessonTimersChanged")
+ ]
+
+ lookup: {
+ name_specified: {name: 'Profile', profile: ''},
+ experiences_teaching: {name: 'Experience', teacher_profile: "experience"},
+ experiences_education: {name: 'Experience', teacher_profile: "experience"},
+ experiences_award: {name: 'Experience', teacher_profile: "experience"},
+ has_stripe_account: {name: 'Stripe', text: "Press the 'Stripe Connect' button in the bottom-left of the page"},
+ has_teacher_bio: {name: 'Introduction', teacher_profile: "introduction"},
+ intro_video: {name: 'Introduction', teacher_profile: "introduction"},
+ years_teaching: {name: 'Introduction', teacher_profile: "introduction"},
+ years_playing: {name: 'Introduction', teacher_profile: "introduction"},
+ instruments_or_subject: {name: 'Basics', teacher_profile: "basics"},
+ genres: {name: 'Basics', teacher_profile: "basics"},
+ languages: {name: 'Basics', teacher_profile: "basics"},
+ teaches_ages_specified: {name: 'Basics', teacher_profile: "basics"},
+ teaching_level_specified: {name: 'Basics', teacher_profile: "basics"},
+ has_pricing_specified: {name: 'Pricing', teacher_profile: "pricing"}
+ }
+
+ needsFetching: false
+
+ fetchLessons: () ->
+ if @needsFetching
+
+ if @state.user?.id?
+ @needsFetching = false
+ rest.getLessonSessions({as_teacher: @viewerTeacher()}).done((response) => @jamClassLoaded(response)).fail((jqXHR) => @failedJamClassLoad(jqXHR))
+
+ onAppInit: (@app) ->
+ @app.bindScreen('jamclass',
+ {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide})
+
+ onUserChanged: (userState) ->
+ @setState({user: userState?.user})
+
+ onLessonTimersChanged: (lessons) ->
+ @setState({lessonTimes: lessons})
+
+ componentDidMount: () ->
+ @root = $(@getDOMNode())
+
+ componentWillUpdate: (nextProps, nextState) ->
+ # associate time info from LessonTimerStore with our lesson info
+ if nextState.lessonTimes? && nextState.lesson_sessions?.entries?
+ for lesson in nextState.lesson_sessions.entries
+ lessonWithTime = nextState.lessonTimes[lesson.id]
+ if lessonWithTime?
+ lesson.times = lessonWithTime.times
+
+ componentDidUpdate: () ->
+ @fetchLessons()
+ items = @root.find('.jamtable tbody td.actionsColumn .lesson-session-actions-btn')
+
+ $.each(items, (i, node) => (
+ $node = $(node)
+
+ lesson = @findLesson($node.attr('data-lesson-id'))
+
+ $node.lessonSessionActions(lesson).off(context.JK.EVENTS.LESSON_SESSION_ACTION).on(context.JK.EVENTS.LESSON_SESSION_ACTION, @lessonSessionActionSelected)
+ ))
+
+ context.JK.popExternalLinks(@root)
+ lessonSessionActionSelected: (e, data) ->
+ lessonId = data.options.id
+ lesson = @findLesson(lessonId)
+
+ if data.lessonAction == 'status'
+ # show the status of the lesson
+ window.location.href = '/client#/jamclass/lesson-booking/' + lessonId
+ else if data.lessonAction == 'messages'
+ @app.layout.showDialog('chat-dialog', {d1: 'lesson_' + lessonId})
+ else if data.lessonAction == 'cancel'
+ @cancelLesson(lesson)
+ # @@app.layout.showDialog('cancel-lesson-dialog', {d1: lessonId})
+ else if data.lessonAction == 'join'
+ SessionActions.enterSession(lesson.music_session.id)
+ else if data.lessonAction == 'reschedule'
+ @rescheduleLesson(lesson)
+ else if data.lessonAction == 'attach-recording'
+ window.AttachmentActions.startAttachRecording(lesson.id)
+ else if data.lessonAction == 'attach-notation'
+ window.AttachmentActions.startAttachNotation(lesson.id)
+ else if data.lessonAction == 'attach-audio'
+ window.AttachmentActions.startAttachAudio(lesson.id)
+ else if data.lessonAction == 'start-5-min'
+ rest.lessonStartTime({id: lessonId, minutes: 5}).done((response) => (@app.layout.notify({
+ title: 'Start Time Set',
+ text: "Start time for session set to 5 mins from now"
+ })))
+ else if data.lessonAction == 'start-65-ago'
+ rest.lessonStartTime({
+ id: lessonId,
+ minutes: -65
+ }).done((response) => (@app.layout.notify({
+ title: 'Start Time Set',
+ text: "Start time for session set to 65 mins ago"
+ })))
+ else if data.lessonAction == 'enter-payment'
+ if lesson.lesson_booking.test_drive_package_choice_id?
+ window.location.href = "/client#/jamclass/lesson-payment/package_#{lesson.lesson_booking.test_drive_package_choice_id}"
+ else
+ window.location.href = "/client#/jamclass/lesson-payment/lesson-booking_#{lessonId}"
+ else
+ context.JK.showAlert('unknown lesson action', 'The option in the menu is unknown')
+
+ findLesson: (lessonId) ->
+ for lesson in @lessons()
+ if lessonId == lesson.id
+ return lesson
+
+ return null
+
+ rescheduleSelected: (lesson, recurring) ->
+ rest.checkLessonReschedule({id: lesson.id, update_all: recurring})
+ .done((response) => (
+ if recurring
+ window.location.href = '/client#/jamclass/lesson-booking/' + lesson.lesson_booking_id + "_rescheduling"
+ else
+ window.location.href = '/client#/jamclass/lesson-booking/' + lesson.id + "_rescheduling"
+ ))
+ .fail((jqXHR) => (
+ if jqXHR.status == 422
+
+ if recurring
+ if @viewerStudent()
+ context.JK.Banner.showAlert("Policy Issue",
+ "
We’re sorry, but you cannot reschedule this recurring lesson right now because it is less than 24 hours before the lesson start time. This is not allowed in the terms of service because the instructor cannot reasonably be expected to be able to backfill the time slot that has been lost. You may reschedule this recurring lesson anytime starting from the end of this next scheduled lesson, so please plan to do it then.
We’re sorry, but you cannot reschedule this recurring lesson right now because it is less than 24 hours before the lesson start time. This is not allowed in the terms of service. You may reschedule this recurring lesson anytime starting from the end of this next scheduled lesson, so please plan to do it then.
We’re sorry, but you cannot reschedule a lesson less than 24 hours before the lesson start time. This is not allowed in the terms of service because the instructor cannot reasonably be expected to be able to backfill the time slot that has been lost.
We’re sorry, but you cannot reschedule a lesson less than 24 hours before the lesson start time. This is not allowed in the terms of service.
")
+ else
+ @app.ajaxError(jqXHR)
+ ))
+
+ refreshLesson: (lessonId) ->
+ rest.getLesson({id: lessonId}).done((response) => @refreshLessonDone(response)).fail((jqXHR) => @refreshLessonFail(jqXHR))
+
+ refreshLessonDone: (lesson_session) ->
+ @updateLessonState(lesson_session)
+
+ refreshLessonFail: (jqXHR) ->
+ @app.ajaxError(jqXHR)
+
+ issueCancelLesson: (lesson, update_all) ->
+ request = {}
+ request.message = ''
+ request.id = lesson.lesson_booking_id
+ request.lesson_session_id = lesson.id
+ request.update_all = update_all
+
+ rest.cancelLessonBooking(request).done((response) => @cancelLessonBookingDone(response,
+ lesson)).fail((response) => @cancelLessonBookingFail(response))
+
+ cancelLessonBookingDone: (booking, lesson) ->
+ if booking.focused_lesson.teacher_short_canceled
+ if @teacherViewing()
+ context.JK.Banner.showAlert('late cancellation warning',
+ 'Cancelling a lesson less than 24 hours before it’s scheduled to start should be avoided, as it’s an inconvenience to the student. Repeated violations of this policy will negatively affect your teacher score.')
+
+ lessonsFromBooking = []
+ for check in @lessons()
+ if check.lesson_booking_id == lesson.lesson_booking_id
+ lessonsFromBooking.push(check)
+
+ for check in lessonsFromBooking
+ @refreshLesson(check.id)
+
+ cancelLessonBookingFail: (jqXHR) ->
+ @app.ajaxError(jqXHR)
+
+ cancelSelected: (lesson, recurring) ->
+ rest.checkLessonCancel({id: lesson.id, update_all: recurring}).done((response) => (@issueCancelLesson(lesson,
+ recurring))).fail((jqXHR) => (@cancelSelectedFail(jqXHR)))
+
+ cancelSelectedFailed: (jqXHR) ->
+ if jqXHR.status == 422
+
+ if recurring
+ if @viewerStudent()
+ buttons = []
+ buttons.push({name: 'CLOSE', buttonStyle: 'button-grey'})
+ buttons.push({
+ name: 'CANCEL RECURRING LESSONS',
+ buttonStyle: 'button-orange',
+ click: (() => (@issueCancelLesson(lesson, true)))
+ })
+ context.JK.Banner.show({
+ title: "Policy Issue",
+ html: "You may cancel this recurring series of lessons, but it is too late to cancel the next scheduled lesson because it is less than 24 hours before the lesson start time. This is not allowed in the terms of service because the instructor cannot reasonably be expected to be able to backfill the time slot that has been lost.",
+ buttons: buttons
+ })
+ else
+ # path should not be taken
+ context.JK.Banner.showAlert("Policy Issue",
+ "
We’re sorry, but you cannot cancel a lesson less than 24 hours before the lesson start time. This is not allowed in the terms of service because the instructor cannot reasonably be expected to be able to backfill the time slot that has been lost.
We’re sorry, but you cannot cancel a lesson less than 24 hours before the lesson start time. This is not allowed in the terms of service because the instructor cannot reasonably be expected to be able to backfill the time slot that has been lost.
")
+ else
+ # path should not be taken
+ context.JK.Banner.showAlert("Policy Issue",
+ "
We’re sorry, but you cannot cancel a lesson less than 24 hours before the lesson start time. This is not allowed in the terms of service.
")
+ else
+ @app.ajaxError(jqXHR)
+
+ rescheduleLesson: (lesson) ->
+ if lesson.recurring
+ buttons = []
+ buttons.push({
+ name: 'THIS LESSON',
+ buttonStyle: 'button-orange',
+ click: (() => (@rescheduleSelected(lesson, false)))
+ })
+ buttons.push({
+ name: 'ALL LESSONS',
+ buttonStyle: 'button-orange',
+ click: (() => (@rescheduleSelected(lesson, true)))
+ })
+ context.JK.Banner.show({
+ title: 'Rescheduling Selection',
+ html: 'Do you wish to all reschedule all lessons or just the one selected?',
+ buttons: buttons
+ })
+ else
+ @rescheduleSelected(lesson, false)
+
+
+ cancelLesson: (lesson) ->
+ if lesson.isRequested && @viewerTeacher()
+ confirmTitle = 'Confirm Decline'
+ verbLower = 'decline'
+ else
+ confirmTitle = 'Confirm Cancelation'
+ verbLower = 'cancel'
+ if !lesson.isRequested || lesson.recurring
+ buttons = []
+ buttons.push({name: 'CLOSE', buttonStyle: 'button-grey'})
+ buttons.push({
+ name: 'CANCEL THIS LESSON',
+ buttonStyle: 'button-orange',
+ click: (() => (@cancelSelected(lesson, false)))
+ })
+ buttons.push({name: 'CANCEL ALL LESSONS', buttonStyle: 'button-orange', click: (() => (@cancelSelected(lesson, true)))})
+ context.JK.Banner.show({
+ title: 'Select One',
+ html: "Do you wish to all #{verbLower} all lessons or just the one selected?",
+ buttons: buttons
+ })
+ else
+ context.JK.Banner.showYesNo({
+ title: confirmTitle,
+ html: "Are you sure you want to #{verbLower} this lesson?",
+ yes: () => (@cancelSelected(lesson, lesson.recurring))
+ })
+
+ getInitialState: () ->
+ {
+ user: null,
+ }
+
+ beforeHide: (e) ->
+
+
+ beforeShow: (e) ->
+
+ afterShow: (e) ->
+ @checkStripeSuccessReturn()
+ @setState({updating: true})
+ @needsFetching = true
+ @fetchLessons()
+
+ checkStripeSuccessReturn: () ->
+ if $.QueryString['stripe-success']?
+ if $.QueryString['stripe-success'] == 'true'
+ context.JK.Banner.showNotice('stripe connected',
+ 'Congratulations, you have successfully connected your Stripe account, so payments for student lessons can now be processed.')
+
+ if window.history.replaceState #ie9 proofing
+ window.history.replaceState({}, "", "/client#/jamclass")
+
+ resetState: () ->
+ @setState({updating: false, lesson: null})
+
+ jamClassLoaded: (response) ->
+ @setState({updating: false})
+
+ @postProcess(response)
+
+ LessonTimerActions.loadLessons(response)
+ @setState({lesson_sessions: response})
+
+ failedJamClassLoad: (jqXHR) ->
+ @setState({updating: false})
+ if jqXHR.status == 404
+ @app.layout.notify({title: "Unable to load JamClass info", text: "Try refreshing the web page"})
+
+ lessons: () ->
+ if @state.lesson_sessions?
+ @state.lesson_sessions.entries
+ else
+ []
+
+ postProcess: (data) ->
+ for lesson in data.entries
+
+ # calculate:
+ # .other (user object),
+ # .me (user object),
+ # other/me.musician_profile (link to musician profile)
+ # other/me.resolved_photo_url
+ # .hasUnreadMessages
+ # .isRequested (doesn't matter if countered or not). but can't be done
+ # .isScheduled (doesn't matter if countered or not).
+ @postProcessLesson(lesson)
+
+ onReadMessage: (lesson_session, e) ->
+ e.preventDefault()
+
+ data = {id: lesson_session.id}
+ data["#{this.myRole()}_unread_messages"] = false
+
+ rest.updateLessonSessionUnreadMessages(data).done((response) => @updatedLessonSessionReadDone(response)).fail((jqXHR) => @updatedLessonSessionReadFail(jqXHR))
+
+ updateLessonState: (lesson_session) ->
+ @postProcessLesson(lesson_session)
+
+ for lesson in @lessons()
+ if lesson.id == lesson_session.id
+ $.extend(lesson, lesson_session)
+ @setState({lesson_sessions: this.state.lesson_sessions})
+
+ updatedLessonSessionReadDone: (lesson_session) ->
+ # update lesson session data in local state
+ @updateLessonState(lesson_session)
+
+ # if this is a not-confirmed lesson, send them to the view statu screen
+ if lesson_session.isRequested
+ window.location.href = '/client#/jamclass/lesson-booking/' + lesson_session.id
+ else
+ @app.layout.showDialog('chat-dialog', {d1: 'lesson_' + lesson_session.id})
+
+
+ updatedLessonSessionReadFail: (jqXHR) ->
+ @app.ajaxError(jqXHR)
+
+
+ openMenu: (lesson, e) ->
+ $this = $(e.target)
+ if !$this.is('.lesson-session-actions-btn')
+ $this = $this.closest('.lesson-session-actions-btn')
+ $this.btOn()
+
+ constructMissingLinks: (user) ->
+ links = []
+
+ matches = {}
+ if user.teacher?
+ for problem, isPresent of user.teacher.profile_pct_summary
+ data = @lookup[problem]
+ if data? && !isPresent
+ matches[data.name] = data
+
+ for name, data of matches
+ if !data.text?
+ links.push(`{data.name}`)
+
+ for name, data of matches
+ if data.text?
+ links.push(`{data.name}`)
+ `
JamClass instructors are each individually screened to ensure that they are highly qualified music
+ teachers,
+ and are equipped with the right gear to teach effectively online.
+
`
+
+ if @state.user?['can_buy_test_drive?']
+ rightContent = `
+
+
+ JamClass is the best way to take online music lessons.
+
+
+
Connect with the best teacher for you vs. the closest
+
TestDrive multiple teachers to find your ideal match
+
Avoid the time and hassle of travel to/from lessons
+
Enjoy studio quality audio in lessons
+
Play live in sync with your teacher in lesson sessions
+
Record lessons to refer back to them any time
+
+
+
TestDrive lets you try:
+
+
4 teachers for just $12.50 each, or
+
+
2 teachers for just $14.99 each, or
+
+
1 teacher for just $14.99
+
+
+
+
To get started, click the Search Teachers button to the left. When you find a teacher you like, click the Book TestDrive Lesson button, and follow the on-screen instructions to choose the 4-, 2-, or 1-teacher offer.
If you have any problems, please email us at support@jamkazam.com.
+ We're here to help make your lessons a great experience.
+
`
+
+ else
+ searchTeachers = `
+
stripe status
+
+
+
+
+
`
+ if this.state.user?
+ teacherProfileUri = "/client#/profile/teacher/#{this.state.user.id}"
+ pct = Math.round(this.state.user.teacher?.profile_pct)
+
+ if !pct?
+ pct = 0
+
+ if pct == 100
+ pctCompleteMsg = `
Your teacher profile is {pct}% complete.
`
+ else
+ pctCompleteMsg = `
Your teacher profile is {pct}% complete. The following sections
+ of your profile are missing information. Click any of these links to view and add missing information:
- To play with your JamTracks, open a JamTrack while in a session in the JamKazam app. Or visit the JamTracks section of your account.
-
+ The fastest way to start playing with your JamTracks is to open them below and use our custom mix feature to play them back in your browser. To access the full set of JamTrack features, install our free app. To learn more about all you can do with JamTracks, check out our JamTracks help docs.
+
`
+ playJamTracks = []
+
+ for jamTrack in @state.purchasedJamTracks
+ playJamTracks.push `
`
+
+ if @state.purchasedJamTracks.length < 5
+ # fill out the table with empty rows
+ for x in [@state.purchasedJamTracks.length...(6 - @state.purchasedJamTracks.length )] by 1
+ playJamTracks.push `
+ Or download the entire JamTracks catalog or to easily browse everything we have.
+
what are jamtracks?
- JamTracks are the best way to play along with your favorite music! Unlike traditional backing tracks, JamTracks are professionally mastered, complete multitrack recordings, with fully isolated tracks for each part of the master mix. Used with the free JamKazam app & Internet service, you can:
+ JamTracks are the best way to play along with your favorite music! Unlike traditional backing tracks, JamTracks are professionally mastered, complete multitrack recordings, with fully isolated tracks for each part of the master mix. Watch the video below to learn more.
-
-
Solo just the part you want to play in order to hear and learn it
-
Mute just the part you want to play and play along with the rest
-
Slow down playback to practice without changing the pitch
-
Change the song key by raising or lowering pitch in half steps
-
Make audio recordings and share them via Facebook or URL
-
Make video recordings and share them via YouTube
-
And even go online to play with others live & in sync
{artistSection}
@@ -219,14 +223,18 @@ MIX_MODES = context.JK.MIX_MODES
clearResults:() ->
- @setState({currentPage: 0, next: null, show_all_artists: false, artists:[], jamtracks:[], type: 'user-input', searching:false, artist: null, song:null, is_free: context.JK.currentUserFreeJamTrack, first_search: true})
+ @setState({currentPage: 0, next: null, show_all_artists: false, artists:[], jamtracks:[], type: 'user-input', searching:false, artist: null, song:null, is_free: @user.show_free_jamtrack, first_search: true})
getInitialState: () ->
{search: '', type: 'user-input', artists:[], jamtracks:[], show_all_artists: false, currentPage: 0, next: null, searching: false, first_search: true, count: 0, is_free: context.JK.currentUserFreeJamTrack}
+ onArtistClick: (artist, e) ->
+ e.preventDefault()
+
+ @search('artist-select', artist)
+
onSelectChange: (val) ->
- #@logger.debug("CHANGE #{val}")
return false unless val?
@@ -435,18 +443,39 @@ MIX_MODES = context.JK.MIX_MODES
isFree = $(e.target).is('.is_free')
@rest.addJamtrackToShoppingCart(params).done((response) =>
- if(isFree)
- if context.JK.currentUserId?
- context.JK.currentUserFreeJamTrack = true # make sure the user sees no more free notices
- context.location = '/client#/redeemComplete'
+ if context.JK.currentUserId?
+ if isFree
+ if @user.has_redeemable_jamtrack
+ # this is the 1st jamtrack; let's user the user to completion
+ context.location = '/client#/redeemComplete'
+ else
+ # this is must be a user's gifted jamtrack, to treat them normally in that they'll go to the shopping cart
+ #context.location = '/client#/shoppingCart'
+ context.location = '/client#/redeemComplete'
else
- # now make a rest call to buy it
- context.location = '/client#/redeemSignup'
-
+ # this user has nothing free; so send them to shopping cart
+ context.location = '/client#/shoppingCart'
else
- context.location = '/client#/shoppingCart'
+ if isFree
+ # user not logged in; make them signup
+ context.location = '/client#/redeemSignup'
+ else
+ # this user has nothing free; so send them to shopping cart
+ context.location = '/client#/shoppingCart'
- ).fail(() => @app.ajaxError)
+
+ ).fail(((jqxhr) =>
+
+ handled = false
+ if jqxhr.status == 422
+ body = JSON.parse(jqxhr.responseText)
+ if body.errors && body.errors.base
+ handled = true
+ context.JK.Banner.showAlert("You can not have a mix of free and non-free items in your shopping cart.
If you want to add this new item to your shopping cart, then clear out all current items by clicking on the shopping cart icon and clicking 'delete' next to each item.")
+ if !handled
+ @app.ajaxError(arguments[0], arguments[1], arguments[2])
+
+ ))
licenseUSWhy:(e) ->
e.preventDefault()
@@ -510,6 +539,9 @@ MIX_MODES = context.JK.MIX_MODES
if search?
performSearch = true
@search(search.searchType, search.searchData)
+ else
+ if !@state.first_search
+ @search(@state.type, window.JamTrackSearchInput)
if performSearch
if window.history.replaceState #ie9 proofing
@@ -517,11 +549,6 @@ MIX_MODES = context.JK.MIX_MODES
beforeShow: () ->
- @setState({is_free: context.JK.currentUserFreeJamTrack})
- if !@state.first_search
- @search(@state.type, window.JamTrackSearchInput)
-
-
onAppInit: (@app) ->
@@ -530,10 +557,14 @@ MIX_MODES = context.JK.MIX_MODES
@rest = context.JK.Rest()
@logger = context.JK.logger
-
screenBindings =
'beforeShow': @beforeShow
'afterShow': @afterShow
@app.bindScreen('jamtrack/search', screenBindings)
+
+ onUserChanged: (userState) ->
+ @user = userState?.user
+ @setState({is_free: @user?.show_free_jamtrack})
+
})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/LanguageCheckBoxList.js.jsx.coffee b/web/app/assets/javascripts/react-components/LanguageCheckBoxList.js.jsx.coffee
new file mode 100644
index 000000000..6f2660678
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/LanguageCheckBoxList.js.jsx.coffee
@@ -0,0 +1,26 @@
+context = window
+rest = window.JK.Rest()
+logger = context.JK.logger
+
+@LanguageCheckBoxList = React.createClass({
+
+ mixins: [Reflux.listenTo(@LanguageStore,"onLanguagesChanged")]
+
+ propTypes: {
+ onItemChanged: React.PropTypes.func.isRequired
+ }
+
+ getDefaultProps: () ->
+ selectedLanguages: []
+
+ getInitialState: () ->
+ {languages: []}
+
+ onLanguagesChanged: (languages) ->
+ @setState({languages: languages})
+
+ render: () ->
+ `
We've taken you back to the JamClass home page.")
+ logger.debug("cancel lesson booking done")
+ @updateBookingState(response)
+ window.location.href = '/client#/jamclass'
+
+ counterLessonBookingDone: (response ) ->
+ context.JK.Banner.showNotice("Lesson Change Requested", "Your request for a time has been sent.
`
+
+ dayOfWeek: (slot) ->
+ switch slot.day_of_week
+ when 0 then "Sunday"
+ when 1 then "Monday"
+ when 2 then "Tuesday"
+ when 3 then "Wednesday"
+ when 4 then "Thursday"
+ when 5 then "Friday"
+ when 6 then "Saturday"
+
+ userHeader: (user) ->
+ photo_url = user?.photo_url
+ if !photo_url?
+ photo_url = '/assets/shared/avatar_generic.png'
+
+ `
+
+
+
+
+ {user.name}
+
+
`
+
+ decisionProps: (slots) ->
+ {
+ onSlotDecision: this.onSlotDecision,
+ onUpdateAllDecision: this.onUpdateAllDecision,
+ initial: this.neverAccepted(),
+ counter: this.isCounter(),
+ is_recurring: this.isRecurring(),
+ slot_decision: this.state.slot_decision,
+ update_all: this.state.update_all,
+ slots: slots,
+ otherRole: this.otherRole(),
+ onUserDecision: this.onAccept,
+ onUserCancel: this.onCancel,
+ disabled: this.state.updatingLesson,
+ selfLastToAct: this.selfLastToAct(),
+ counterErrors: this.state.counterErrors,
+ cancelErrors: this.state.cancelErrors,
+ focusedLesson: this.focusedLesson(),
+ noSlots: this.noSlots()
+ }
+
+ render: () ->
+ if @state.updating
+
+ return @renderLoading()
+
+ else if @teacherViewing()
+
+ return @renderTeacher()
+
+ else if @studentViewing()
+
+ return @renderStudent()
+
+ else
+
+ return @renderLoading()
+
+ completedHeader: () ->
+ if @isNow()
+ header = 'the lesson is scheduled for right now!'
+ else
+ if @isMissed()
+ header = "this lesson was #{this.displayableLesson().displayStatus.toLowerCase()}"
+ else if @isSuspended()
+ header = 'this lesson was suspended due to billing issues'
+ else
+ header = 'this lesson is over'
+
+ approvedHeader: () ->
+ if @isCompleted()
+ if @isMissed()
+ header = "this lesson was #{this.displayableLesson().displayStatus.toLowerCase()}"
+ else if @isSuspended()
+ header = 'this lesson was suspended due to billing issues'
+ else
+ header = 'this lesson is over'
+ else if @isNow()
+ header = 'the lesson is scheduled for right now!'
+ else if @isPast()
+ header = 'this lesson is over'
+ else
+ header = 'this lesson is coming up soon'
+ header
+
+ renderTeacher: () ->
+ if @isRequested()
+ header = 'respond to lesson request'
+ content = @renderTeacherRequested()
+
+ else if @isCounter()
+ if @isTeacherCountered()
+ header = 'your proposed alternate day/time is still pending'
+ else
+ header = 'student has proposed an alternate day/time'
+ content = @renderTeacherCountered()
+
+ else if @isApproved()
+ header = @approvedHeader()
+ content = @renderTeacherApproved()
+
+ else if @isCompleted()
+ header = @completedHeader()
+ content = @renderTeacherComplete()
+
+ else if @isCanceled()
+
+ header = "this lesson was #{this.displayableLesson()?.displayStatus?.toLowerCase()}"
+ content = @renderTeacherCanceled()
+
+ else if @isSuspended()
+
+ header = 'This lesson has been suspended'
+ content = @renderTeacherSuspended()
+
+ return `
+
+
+
{header}
+
+ {content}
+
+
+
`
+
+
+ renderStudent: () ->
+ if @isRequested()
+ header = 'your lesson has been requested'
+ content = @renderStudentRequested()
+
+ else if @isCounter()
+ if @isTeacherCountered()
+ header = 'teacher has proposed an alternate day/time'
+ else
+ header = 'your proposed alternate day/time is still pending'
+ content = @renderTeacherCountered()
+
+ else if @isApproved()
+ header = @approvedHeader()
+ content = @renderStudentApproved()
+
+ else if @isCompleted()
+ header = @completedHeader()
+ content = @renderStudentComplete()
+
+ else if @isCanceled()
+
+ if @neverAccepted() && @studentViewing() && !@studentCanceled()
+ header = "we're sorry, but your lesson request has been declined"
+ else
+ header = "this lesson was #{this.displayableLesson()?.displayStatus?.toLowerCase()}"
+
+ content = @renderStudentCanceled()
+
+ else if @isSuspended()
+
+ header = 'this lesson has been suspended'
+ content = @renderStudentSuspended()
+
+
+ `
+ {this.userHeader(this.myself())}
+ Your request has been sent. You will receive an email when {this.teacher().name} responds.
+ {this.createDetail()}
+
`
+
+ if @studentViewing()
+
+ if @studentCanceled()
+ blurb = `
We're sorry this scheduling attempt did not working out for you. Please search our community of instructors to find someone else who looks like a good fit for you, and submit a new lesson request
`
+ else
+ blurb = `
We're sorry this instructor has declined your request. Please search our community of instructors to find someone else who looks like a good fit for you, and submit a new lesson request
+
+ Date:
+
+
+
+ Time:
+ :
+
+
+ *Time will be local to {window.JK.currentTimezone()}
+ {errorText}
+
+
`
+
+ if @showDeclineVerb()
+ declineVerb = "Decline"
+ else
+ declineVerb = "Cancel"
+
+ cancelClasses = {cancel: true, "button-grey": true, disabled: this.props.disabled}
+ scheduleClasses = {schedule: true, "button-orange": true, disabled: this.props.disabled}
+ slots = []
+
+ if !(this.props.counter && this.props.selfLastToAct)
+ if this.props.noSlots
+ proposeAltLabelText = 'Propose a day/time'
+ else
+ proposeAltLabelText = "Propose alternate day/time"
+ for slot, i in @props.slots
+
+ if this.props.is_recurring
+ slotDetail = `
Each {slot.slotTime}
`
+ else
+ slotDetail = `
{slot.slotTime}
`
+
+ slots.push(`
+
+
+
+ {slotDetail}
+
`)
+ else
+ if this.props.noSlots
+ proposeAltLabelText = 'Propose a day/time'
+ else
+ proposeAltLabelText = "Propose new alternate day/time"
+
+ # if you have issued a counter, you should be able to withdraw it
+ # TODO
+
+ #cancelField = `
`
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee b/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee
new file mode 100644
index 000000000..f687a330d
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee
@@ -0,0 +1,672 @@
+context = window
+rest = context.JK.Rest()
+logger = context.JK.logger
+
+UserStore = context.UserStore
+
+@LessonPayment = React.createClass({
+
+ mixins: [
+ ICheckMixin,
+ Reflux.listenTo(AppStore, "onAppInit"),
+ Reflux.listenTo(UserStore, "onUserChanged")
+ ]
+
+ shouldShowNameSet: false
+
+ onAppInit: (@app) ->
+ @app.bindScreen('jamclass/lesson-payment',
+ {beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide})
+
+ onUserChanged: (userState) ->
+
+ if !@shouldShowNameSet
+ @shouldShowNameSet = true
+ if userState?.user?
+ username = userState.user.name
+ first_name = userState.user.first_name
+ last_name = userState.user.last_name
+ shouldShowName = !username? || username.trim() == '' || username.toLowerCase().indexOf('anonymous') > -1
+ else
+ shouldShowName = @state.shouldShowName
+
+ @setState({user: userState?.user, shouldShowName: shouldShowName})
+
+
+ componentDidMount: () ->
+ @checkboxes = [{selector: 'input.billing-address-in-us', stateKey: 'billingInUS'}]
+
+ @root = $(@getDOMNode())
+ @root.find('input.expiration').payment('formatCardExpiry')
+ @root.find("input.card-number").payment('formatCardNumber')
+ @root.find("input.cvv").payment('formatCardCVC')
+ @iCheckify()
+
+ componentDidUpdate: () ->
+ @iCheckify()
+
+ getInitialState: () ->
+ {
+ user: null,
+ lesson: null,
+ updating: false,
+ billingInUS: true,
+ userWantsUpdateCC: false,
+ "test-drive": false,
+ teacher: null,
+ package: null
+ }
+
+ beforeHide: (e) ->
+ @resetErrors()
+
+ beforeShow: (e) ->
+
+ afterShow: (e) ->
+ @resetState()
+ @resetErrors()
+ parsed = @parseId(e.id)
+ parsed.updating = false
+
+ if parsed['lesson-booking']
+ parsed.updating = true
+ rest.getLessonBooking({id: parsed.lesson_booking_id}).done((response) => @lessonBookingLoaded(response)).fail((jqXHR) => @failedLessonBooking(jqXHR))
+ else if parsed['teacher-intent']
+ parsed.updating = true
+ rest.getUserDetail({id: parsed.teacher_id}).done((response) => @teacherLoaded(response)).fail((jqXHR) => @failedTeacher(jqXHR))
+ else if parsed['test-drive']
+ logger.debug("test-drive lesson payment; no teacher/booking in context")
+ else if parsed['package-choice']
+ logger.debug("TestDrive package selected " + parsed.package_id)
+ rest.getTestDrivePackageChoice({id: parsed.package_id}).done((response) => @packageLoaded(response)).fail((jqXHR) => @failedPackage(jqXHR))
+ else
+ logger.error("unknown state for lesson-payment")
+ window.location.href = '/client#/jamclass'
+
+ @setState(parsed)
+
+ parseId: (id) ->
+ result = {}
+
+ # id can be:
+ # 'test-drive'
+ # or 'lesson-booking_id'
+ # or 'teacher_id
+
+ result['test-drive'] = false
+ result['lesson-booking'] = false
+ result['teacher-intent'] = false
+ result['package-choice'] = false
+
+ bits = id.split('_')
+ if bits.length == 1
+ # should be id=test-drive
+ result[id] = true
+ else if bits.length == 2
+ type = bits[0]
+ if type == 'lesson-booking'
+ result[type] = true
+ result.lesson_booking_id = bits[1]
+ else if type == 'teacher'
+ result['teacher-intent'] = true
+ result.teacher_id = bits[1]
+ else if type == 'package'
+ result['package-choice'] = true
+ result.package_id = bits[1]
+
+ logger.debug("LessonPayment: parseId " + JSON.stringify(result))
+
+ result
+
+ resetErrors: () ->
+ @setState({ccError: null, cvvError: null, expiryError: null, billingInUSError: null, zipCodeError: null, nameError: null})
+
+ checkboxChanged: (e) ->
+ checked = $(e.target).is(':checked')
+
+ @setState({billingInUS: checked})
+
+ resetState: () ->
+ @setState({updating: false, lesson: null, teacher: null, "test-drive": false, "lesson-booking" : false, "teacher-intent": false, package: null, "package-choice": null})
+
+ lessonBookingLoaded: (response) ->
+ @setState({updating: false})
+ logger.debug("lesson booking loaded", response)
+
+ if response.card_presumed_ok
+ context.JK.Banner.showNotice("Lesson Already Requested", "You have already requested this lesson from this teacher.")
+ window.location.href = "/client#/jamclass"
+ @setState({lesson: response, teacher: response.teacher})
+
+ failedLessonBooking: (jqXHR) ->
+ @setState({updating: false})
+ @app.layout.notify({
+ title: 'unable to load lesson info',
+ text: 'Something has gone wrong. Please try refreshing the page.'
+ })
+
+ teacherLoaded: (response) ->
+ @setState({updating: false})
+ logger.debug("teacher loaded", response)
+ @setState({teacher: response})
+
+ failedTeacher: (jqXHR) ->
+ @setState({updating: false})
+ @app.layout.notify({
+ title: 'unable to load teacher info',
+ text: 'Something has gone wrong. Please try refreshing the page.'
+ })
+
+ packageLoaded: (response) ->
+ @setState({updating: false})
+ logger.debug("package loaded", response)
+ @setState({package: response})
+
+ failedPackage: (jqXHR) ->
+ @setState({updating: false})
+ @app.layout.notify({
+ title: 'unable to load package info',
+ text: 'Something has gone wrong. Please try refreshing the page.'
+ })
+
+ onBack: (e) ->
+ e.preventDefault()
+ window.location.href = '/client#/teachers/search'
+
+ onSubmit: (e) ->
+ @resetErrors()
+
+ e.preventDefault()
+
+ if !window.Stripe?
+ logger.error("no window.Stripe")
+ @app.layout.notify({
+ title: 'Payment System Not Loaded',
+ text: "Please refresh this page and try to enter your info again. Sorry for the inconvenience!"
+ })
+ else
+
+ if @reuseStoredCard()
+ @attemptPurchase(null)
+ else
+
+ ccNumber = @root.find('input.card-number').val()
+ expiration = @root.find('input.expiration').val()
+ cvv = @root.find('input.cvv').val()
+ inUS = @root.find('input.billing-address-in-us').is(':checked')
+ zip = @root.find('input.zip').val()
+
+ error = false
+
+
+ if @state.shouldShowName
+ name = @root.find('#set-user-on-card').val()
+
+ if name.indexOf('Anonymous') > -1
+ @setState({nameError: true})
+ error = true
+
+ if !$.payment.validateCardNumber(ccNumber)
+ @setState({ccError: true})
+ error = true
+
+ bits = expiration.split('/')
+
+ if bits.length == 2
+ month = bits[0].trim();
+ year = bits[1].trim()
+
+ month = new Number(month)
+ year = new Number(year)
+
+ if year < 2000
+ year += 2000
+
+ if !$.payment.validateCardExpiry(month, year)
+ @setState({expiryError: true})
+ error = true
+ else
+ @setState({expiryError: true})
+ error = true
+
+
+ cardType = $.payment.cardType(ccNumber)
+
+ if !$.payment.validateCardCVC(cvv, cardType)
+ @setState({cvvError: true})
+ error = true
+
+ if inUS && (!zip? || zip == '')
+ @setState({zipCodeError: true})
+
+ if error
+ return
+
+ data = {
+ number: ccNumber,
+ cvc: cvv,
+ exp_month: month,
+ exp_year: year,
+ }
+
+ @setState({updating: true})
+
+ logger.debug("creating stripe token")
+ window.Stripe.card.createToken(data, (status, response) => (@stripeResponseHandler(status, response)));
+
+ stripeResponseHandler: (status, response) ->
+ console.log("stripe response", JSON.stringify(response))
+
+ if response.error
+ @setState({updating: false})
+
+ if response.error.code == "invalid_number"
+ @setState({ccError: true, cvvError: null, expiryError: null})
+ else if response.error.code == "invalid_cvc"
+ @setState({ccError: null, cvvError: true, expiryError: null})
+ else if response.error.code == "invalid_expiry_year" || response.error.code == "invalid_expiry_month"
+ @setState({ccError: null, cvvError: null, expiryError: true})
+ else
+ @attemptPurchase(response.id)
+
+ isNormal: () ->
+ @state.lesson?.lesson_type == 'paid'
+
+ isTestDrive: () ->
+ @state['test-drive'] == true || @state.lesson?.lesson_type == 'test-drive' || @state['teacher-intent'] || @state['package-choice'] == true
+
+ attemptPurchase: (token) ->
+ if this.state.billingInUS
+ zip = @root.find('input.zip').val()
+
+ data = {
+ token: token,
+ zip: zip,
+ test_drive: @isTestDrive(),
+ booking_id: @state.lesson?.id,
+ test_drive_package_choice_id: @state.package?.id
+ normal: @isNormal()
+ }
+
+ if @state.shouldShowName
+ data.name = @root.find('#set-user-on-card').val()
+
+ @setState({updating: true})
+ logger.debug("submitting purchase info: " + JSON.stringify(data))
+ rest.submitStripe(data).done((response) => @stripeSubmitted(response)).fail((jqXHR) => @stripeSubmitFailure(jqXHR))
+
+ stripeSubmitted: (response) ->
+ @setState({updating: false})
+
+ logger.debug("stripe submitted: " + JSON.stringify(response))
+
+ #if @state.shouldShowName
+ window.UserActions.refresh()
+
+ # if the response has a lesson, take them there
+ if response.test_drive?
+ # ok, they bought a package
+ if response.lesson_package_type?
+ # always of form test-drive-#
+ prefixLength = "test-drive-".length
+ packageLength = response.lesson_package_type.package_type.length
+
+ testDriveCount = response.lesson_package_type.package_type.substring(prefixLength, packageLength)
+
+ logger.debug("testDriveCount: " + testDriveCount)
+
+ testDriveCountInt = parseInt(testDriveCount);
+ if context._.isNaN(testDriveCountInt)
+ testDriveCountInt = 3
+
+ context.JK.GA.trackTestDrivePurchase(testDriveCountInt);
+
+ if response.test_drive?.teacher_id
+ teacher_id = response.test_drive.teacher_id
+ if testDriveCount == '1'
+ text = "You have purchased 1 TestDrive credit and have used it to request a JamClass with #{@state.package.teachers[0].user.name}. The teacher has received your request and should respond shortly."
+ else if response.package?
+ text = "Each teacher has received your request and should respond shortly."
+ else
+ text = "You have purchased #{testDriveCount} TestDrive credits and have used 1 credit to request a JamClass with #{@state.teacher.name}. The teacher has received your request and should respond shortly."
+ location = "/client#/jamclass"
+ else
+ if @state.teacher?.id
+
+ # the user bought the testdrive, and there is a teacher of interest in context (but no booking)
+ if testDriveCount == '1'
+ text = "You now have 1 TestDrive credit.
We've taken you to the lesson booking screen for the teacher you initially showed interest in."
+ location = "/client#/jamclass/book-lesson/test-drive_" + teacher_id
+ else
+ text = "You now have #{testDriveCount} TestDrive credits that you can take with #{testDriveCount} different teachers.
We've taken you to the lesson booking screen for the teacher you initially showed interest in."
+ location = "/client#/jamclass/book-lesson/test-drive_" + teacher_id
+ else
+ # the user bought test drive, but 'cold' , i.e., no teacher in context
+ if testDriveCount == '1'
+ text = "You now have 1 TestDrive credit.
We've taken you to the Teacher Search screen, so you can search for teachers right for you."
+ location = "/client#/teachers/search"
+ else
+ text = "You now have #{testDriveCount} TestDrive credits that you can take with #{testDriveCount} different teachers.
We've taken you to the Teacher Search screen, so you can search for teachers right for you."
+ location = "/client#/teachers/search"
+
+ context.JK.Banner.showNotice("TestDrive Purchased",text)
+ window.location = location
+ else
+ context.JK.Banner.showNotice("Something Went Wrong", "Please email support@jamkazam.com and indicate that your attempt to buy a TestDrive failed")
+ window.location = "/client#/jamclass/"
+
+ else if response.lesson?.id?
+ context.JK.Banner.showNotice("Lesson Requested","The teacher has been notified of your lesson request, and should respond soon.
We've taken you back to the JamClass home page, where you can check the status of this lesson, as well as any other past and future lessons.")
+
+ url = "/client#/jamclass/lesson-booking/" + response.lesson.id
+ url = "/client#/jamclass"
+ window.location.href = url
+
+ else
+ window.location = "/client#/teachers/search"
+
+ stripeSubmitFailure: (jqXHR) ->
+ logger.debug("stripe submission failure", jqXHR.responseText)
+ @setState({updating: false})
+ handled = false
+ if jqXHR.status == 422
+ errors = JSON.parse(jqXHR.responseText)
+ if errors.errors.name?
+ @setState({name: errors.errors.name[0]})
+ handled = true
+ else if errors.errors.user?
+ @app.layout.notify({title: "Can't Purchase Test Drive", text: "You " + errors.errors.user[0] + '.' })
+ handled = true
+
+ if !handled
+ @app.notifyServerError(jqXHR, 'Credit Card Not Stored')
+
+ onUnlockPaymentInfo: (e) ->
+ e.preventDefault()
+ @setState({userWantsUpdateCC: true})
+
+ onLockPaymentInfo: (e) ->
+ e.preventDefault()
+ @setState({userWantsUpdateCC: false})
+
+ reuseStoredCard: () ->
+ !@state.userWantsUpdateCC && @state.user?['has_stored_credit_card?']
+
+ bookedPrice: () ->
+ booked_price = this.state.lesson.booked_price
+
+ if booked_price?
+ if typeof booked_price == "string"
+ booked_price = new Number(booked_price)
+ return booked_price.toFixed(2)
+ else
+ return '??'
+
+ render: () ->
+ disabled = @state.updating || @reuseStoredCard()
+
+ if @state.updating
+ photo_url = '/assets/shared/avatar_generic.png'
+ name = 'Loading ...'
+ teacherDetails = `
+
+
+
+ {name}
+
`
+ else
+ if @state.lesson? || @state['test-drive'] || @state.teacher? || @state['package-choice'] == true
+ if @state.teacher?
+ photo_url = @state.teacher.photo_url
+ name = @state.teacher.name
+
+ if !photo_url?
+ photo_url = '/assets/shared/avatar_generic.png'
+
+ teacherDetails = `
+
+
+
+ {name}
+
`
+ else if @state.package?
+ teachers = []
+ teachersHolder = []
+ count = 0
+ for teacher_choice in @state.package.teachers
+
+ if count == 2
+ teachersHolder.push(
+ `
`
+
+ bookingInfo = ``
+ if this.state['package-choice']
+ if this.state.package?
+
+ if @state.package.teachers.length == 1
+ explanation = `You are purchasing the TestDrive package of JamClass by JamKazam. This purchase entities you to take a private online music lesson from this instructor. The price of this TestDrive is $14.99. If you have scheduling conflicts with this instructors, we will help you choose another teacher as a replacement.`
+ else if @state.package.teachers.length == 2
+ explanation = `You are purchasing the TestDrive package of JamClass by JamKazam. This purchase entities you to take 2 private online music lessons - 1 each from these 2 instructors. The price of this TestDrive is $29.99. If you have scheduling conflicts with any of these instructors, we will help you choose another teacher as a replacement.`
+ else if @state.package.teachers.length == 4
+ explanation = `You are purchasing the TestDrive package of JamClass by JamKazam. This purchase entities you to take 4 private online music lessons - 1 each from these 4 instructors. The price of this TestDrive is $49.99. If you have scheduling conflicts with any of these instructors, we will help you choose another teacher as a replacement.`
+ else
+ alert("unknown package type")
+ else
+ if this.state.user.lesson_package_type_id == 'test-drive'
+ explanation = `You are purchasing the TestDrive package of JamClass by JamKazam. This purchase entitles you to take 4 private online music lessons - 1 each from 4 different instructors in the JamClass instructor community. The price of this TestDrive package is $49.99.`
+ else if this.state.user.lesson_package_type_id == 'test-drive-1'
+ explanation =`You are purchasing the TestDrive package of JamClass by JamKazam. This purchase entitles you to take 1 private online music lesson from an instructor in the JamClass instructor community. The price of this TestDrive package is $14.99.`
+ else if this.state.user.lesson_package_type_id == 'test-drive-2'
+ explanation =`You are purchasing the TestDrive package of JamClass by JamKazam. This purchase entitles you to take 2 private online music lessons - 1 each from 2 different instructors in the JamClass instructor community. The price of this TestDrive package is $29.99.`
+ else
+ alert("You do not have a test drive package selected")
+
+
+ bookingDetail = `
`
+ else if @isNormal()
+ if @reuseStoredCard()
+ header = `
purchase lesson
`
+ else
+ header = `
enter payment info for lesson
`
+
+ if this.state.lesson.recurring
+ if this.state.lesson.payment_style == 'single'
+ bookingInfo = `
You are booking a {lesson_length} minute lesson for
+ ${this.bookedPrice()}
`
+ bookingDetail = `
+ Your card will not be charged until the day of the lesson. You must cancel at least 24 hours before your
+ lesson is scheduled, or you will be charged for the lesson in full.
+
+
+ jamclass
+ policies
+
You are booking a weekly recurring series of {lesson_length}-minute
+ lessons, to be paid individually as each lesson is taken, until cancelled.
`
+ bookingDetail = `
+ Your card will be charged on the day of each lesson. If you need to cancel a lesson, you must do so at
+ least 24 hours before the lesson is scheduled, or you will be charged for the lesson in full.
+
+
+ jamclass
+ policies
+
You are booking a weekly recurring series of {lesson_length}-minute
+ lessons, to be paid for monthly until cancelled.
`
+ bookingDetail = `
+ Your card will be charged on the first day of each month. Canceling individual lessons does not earn a
+ refund when buying monthly. To cancel, you must cancel at least 24 hours before the beginning of the
+ month, or you will be charged for that month in full.
+
+
+ jamclass
+ policies
+
`
+ else
+ bookingInfo = `
You are booking a {lesson_length} minute lesson for
+ ${this.bookedPrice()}
`
+ bookingDetail = `
+ Your card will not be charged until the day of the lesson. You must cancel at least 24 hours before your
+ lesson is scheduled, or you will be charged for the lesson in full.
+
+
+ jamclass
+ policies
+
`
+ else
+ if @reuseStoredCard()
+ header = `
payment info already entered
`
+ else
+ header = `
enter payment info
`
+
+ bookingInfo = `
You are entering your credit card info so that later checkouts go quickly. You can skip this
+ for now.
`
+ bookingDetail = `
+
+ Your card will not be charged until the day of the lesson. You must cancel at least 24 hours before your
+ lesson is scheduled, or you will be charged for the lesson in full.
+
+
+ jamclass policies
+
+ If a scan is not finding the VST or AU plugin you want to use, it’s likely that we aren’t scanning the location where the plugin is installed. Click the ADD SCAN FOLDER button below, and navigate to the folder where the plugin is installed to add that location to the scan list.
+
`
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/PopupConfigureVideoGear.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupConfigureVideoGear.js.jsx.coffee
index 7ffd1e641..924567879 100644
--- a/web/app/assets/javascripts/react-components/PopupConfigureVideoGear.js.jsx.coffee
+++ b/web/app/assets/javascripts/react-components/PopupConfigureVideoGear.js.jsx.coffee
@@ -55,7 +55,7 @@ if accessOpener
- CLOSE
+ CLOSE
`
diff --git a/web/app/assets/javascripts/react-components/PopupJamTrackMixdownDownload.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupJamTrackMixdownDownload.js.jsx.coffee
new file mode 100644
index 000000000..5fc77516c
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/PopupJamTrackMixdownDownload.js.jsx.coffee
@@ -0,0 +1,165 @@
+context = window
+logger = context.JK.logger
+ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
+rest = context.JK.Rest()
+
+mixins = []
+
+
+# make sure this is actually us opening the window, not someone else (by checking for MixerStore)
+# this check ensures we attempt to listen if this component is created in a popup
+reactContext = if window.opener? then window.opener else window
+# make sure this is actually us opening the window, not someone else (by checking for MixerStore)
+if window.opener?
+ try
+ m = window.opener.MixerStore
+ catch e
+ reactContext = window
+
+# temporarily..
+# reactContext = window
+
+AppActions = reactContext.AppActions
+JamTrackPlayerActions = reactContext.JamTrackPlayerActions
+JamTrackPlayerStore = reactContext.JamTrackPlayerStore
+
+
+@PopupJamTrackMixdownDownload = React.createClass({
+
+ checkServerTimeout: null
+ checkTime: 5000
+ sampleRate: 48
+
+ render: () ->
+ if @state.mixdown?
+ header = `
Mute or unmute any tracks you like. You can also use the controls below to adjust the tempo or pitch of the JamTrack. Then give your custom mix a name, and click the Create Mix button.
`
- else if @props.mediaSummary.backingTrackOpen
+ header = `
{mediaType}: {mediaName}
`
+ else if @state.media.mediaSummary.jamTrackOpen || @state.jamTrackState.jamTrack?
+ if @state.media.mediaSummary.isOpener || @state.jamTrackState.jamTrack?
+ # if you opened the JamTrack, then you get all the good info
+ jamTrack = @state.jamTrackState.jamTrack
+ mediaType = "JamTrack"
+ mediaName = jamTrack.name
+ closeLinkText = 'CLOSE JAMTRACK'
+ helpLink = 'https://jamkazam.desk.com/customer/portal/articles/2138903-using-custom-mixes-to-slow-tempo-change-pitch'
+
+ selectedMixdown = jamTrack.activeMixdown
+
+
+ if selectedMixdown?
+ jamTrackTypeHeader = 'Custom Mix'
+
+ disabled = true
+ if selectedMixdown.client_state?
+ switch selectedMixdown.client_state
+ when 'cant_open'
+ customMixName = `
Use the JamTrack controls on the session screen to set levels, mute/unmute, or pan any of the parts of the JamTrack as you like. You can also use the controls below to adjust the tempo or pitch of the JamTrack. Then give your custom mix a name, and click the Create Mix button.
`
+ else
+ if @isRatingTeacher()
+ title = 'Rate Teacher'
+ help = `
Please rate this teacher based on your experience with them:
`
+ descriptionPrompt = `
Please help other students by explaining what you like or don’t like about this teacher:
`
+ choices =
+ `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`
+ else
+ title = 'Rate Student'
+ help = `
Please rate this student based on your experience with them:
`
+ descriptionPrompt = `
Please tell us if you have problems with this student in the form of tardiness, abusiveness, or other inappropriate behaviors. We will not share this information with other teachers or students, but we may use aggregate negative feedback on a student from multiple teachers to block the student from our lesson marketplace.
We’re sorry, but you cannot reschedule a lesson less than 24 hours before the lesson start time. This is not allowed in the terms of service because the instructor cannot reasonably be expected to be able to backfill the time slot that has been lost.
{window.aggregate_latency_calc(stats)}`,
+ warn: (user, stats) -> `{user.possessive} one-way, total latency from him to you is typical.
{window.aggregate_latency_calc(stats)}`,
+ poor: (user, stats) -> `{user.possessive} one-way, total latency from him to you is poor.
{window.aggregate_latency_calc(stats)}`
+ }
+ },
+ system: {
+ cpu: {
+ good: (user, stats) -> "#{user.possessive} computer processor is not overworked by JamKazam or the your system.",
+ warn: (user, stats) -> "#{user.possessive} computer processor is being heavily used. There is little spare capacity, and this means your processor may not be able to handle all of it's tasks, causing audio quality, latency, and other issues.",
+ poor: (user, stats) -> "#{user.possessive} computer processor is being very heavily used. There is little spare capacity, and this means your processor may not be able to handle all of it's tasks, causing audio quality, latency, and other issues."
+ }
+ },
+ network: {
+ wifi: {
+ good: (user, stats) -> "#{user.name} is using a wired connection.",
+ warn: (user, stats) -> "#{user.name} is using Wi-Fi, which will create audio quality issues and additional latency.",
+ poor: (user, stats) -> "#{user.name} is using Wi-Fi, which will create audio quality issues and additional latency.",
+ },
+ audio_bitrate_rx: {
+ good: (user, stats) -> "#{user.name} has enough bandwidth to send you a high quality audio stream.",
+ warn: (user, stats) -> "#{user.name} has enough bandwidth to send you a degraded, but sufficient, audio stream.",
+ poor: (user, stats) -> "#{user.name} has not enough bandwidth to send you a decent quality audio stream.",
+ },
+ audio_bitrate_tx: {
+ good: (user, stats) -> "You have enough bandwidth to send you a high quality audio stream.",
+ warn: (user, stats) -> "You have enough bandwidth to send you a degraded, but sufficient, audio stream.",
+ poor: (user, stats) -> "You have not enough bandwidth to send you a decent quality audio stream.",
+ },
+ video_rtpbw_rx: {
+ good: (user, stats) -> "#{user.name} have enough bandwidth to send you a high quality video stream.",
+ warn: (user, stats) -> "#{user.name} have enough bandwidth to send you a degraded, but sufficient, video stream.",
+ poor: (user, stats) -> "#{user.name} have not enough bandwidth to send you a decent quality video stream.",
+ },
+ video_rtpbw_tx: {
+ good: (user, stats) -> "You have enough bandwidth to send you a high quality video stream.",
+ warn: (user, stats) -> "You have enough bandwidth to send you a degraded, but sufficient, video stream.",
+ poor: (user, stats) -> "You have not enough bandwidth to send you a decent quality video stream.",
+ },
+ ping: {
+ good: (user, stats) -> "The internet connection between you and #{user.name} has very low latency.",
+ warn: (user, stats) -> "The internet connection between you and #{user.name} has average latency, which may affect staying in sync.",
+ poor: (user, stats) -> "The internet connection between you and #{user.name} has high latency, making it very difficult to stay in sync.",
+ },
+ pkt_loss: {
+ good: (user, stats) -> "The internet connection between you and #{user.name} loses a small % of packets. It should not affect your audio quality.",
+ warn: (user, stats) -> "The internet connection between you and #{user.name} loses a significant % of packets. This may result in periodical audio artifacts.",
+ poor: (user, stats) -> "The internet connection between you and #{user.name} loses a high % of packets. This will result in frequent audio artifacts.",
+ },
+ audiojq_median: {
+ good: (user, stats) -> `JamKazam has to maintain a only a small buffer of audio to preserve audio quality, resulting in minimal added latency.
This buffer is adding {(2.5 * stats.network.audiojq_median).toFixed(1)}ms of latency.`,
+ warn: (user, stats) -> `JamKazam has to maintain a significant buffer of audio to preserve audio quality, resulting in potentially noticeable additional latency.
This buffer is adding {(2.5 * stats.network.audiojq_median).toFixed(1)}ms of latency.`,
+ poor: (user, stats) -> `JamKazam has to maintain a large buffer of audio to preserve audio quality, resulting in noticeabley added latency.
This buffer is adding {(2.5 * stats.network.audiojq_median).toFixed(1)}ms of latency.`,
+ }
+ },
+ audio: {
+ framesize: {
+ good: (user, stats) -> "#{user.possessive} gear is reading and writing audio data at a very high rate, keeping gear-added latency low.",
+ warn: (user, stats) -> "#{user.possessive} gear is reading and writing audio at a average rate, causing a few milliseconds extra latency compared to a 2.5 Frame Size.",
+ poor: (user, stats) -> "#{user.possessive} gear is reading and writing audio at a slow rate, causing a decent amount of latency before the internet is involved.",
+ },
+ latency: {
+ good: (user, stats) -> "#{user.possessive} gear has a small amount of latency.",
+ warn: (user, stats) -> "#{user.possessive} gear has a significant amount of latency.",
+ poor: (user, stats) -> "#{user.possessive} gear has a large amount of latency, making it difficult to play in time."
+ },
+ input_jitter: {
+ good: (user, stats) -> "#{user.possessive} gear has a small amount of input jitter, meaning it is keeping good time with JamKazam as it reads in your input signal.",
+ warn: (user, stats) -> "#{user.possessive} gear has a significant amount of input jitter, meaning it might be periodically adding delay or audio artifacts to your inputs.",
+ poor: (user, stats) -> "#{user.possessive} gear has a large amount of input jitter, meaning it likely adding delay and audio artifacts to your inputs.",
+ },
+ output_jitter: {
+ good: (user, stats) -> "#{user.possessive} gear has a small amount of output jitter, meaning it is keeping good time with your JamKazam as writes out your audio output.",
+ warn: (user, stats) -> "#{user.possessive} gear has a significant amount of output jitter, meaning it might be periodically adding delay and audio artifacts to your output.",
+ poor: (user, stats) -> "#{user.possessive} gear has a large amount of output jitter, meaning it likely adding delay and audio artifacts to your output.",
+ },
+ audio_in_type: {
+ good: (user, stats) -> "#{user.name} using an ideal driver type for #{user.possessive.toLowerCase()} gear.",
+ warn: (user, stats) -> "#{user.name} using a problematic driver type for #{user.possessive.toLowerCase()} gear.",
+ poor: (user, stats) -> "#{user.name} using a driver type considered problematic.",
+ }
+ }
+}
+
+@SessionStatsHover = React.createClass({
+
+ propTypes: {
+ clientId: React.PropTypes.string
+ }
+
+ mixins: [Reflux.listenTo(@SessionStatsStore, "onStatsChanged")]
+
+ hover: (type, field) ->
+ logger.debug("hover! #{type} #{field}")
+ @setState({hoverType: type, hoverField:field})
+
+ hoverOut: () ->
+ logger.debug("hover out!")
+ @setState({hoverType: null, hoverField: null})
+
+
+ stat: (properties, type, name, field, value) ->
+ classes = {'status-icon': true}
+ classifier = properties[field + '_level']
+ classes[classifier] = true
+ `
- {name}{value}
`
+
+ render: () ->
+ extraInfo = 'Hover over a stat to learn more.'
+
+ if @state.hoverType?
+ type = @state.hoverType
+ field = @state.hoverField
+
+ extraInfo = `No extra info for this metric.`
+
+ classifier = @state.stats?[type]?[field + '_level']
+
+ if classifier?
+ info = StatsInfo[type]?[field]?[classifier](@props.participant.user, @state.stats)
+ if info?
+ extraInfo = `{info}`
+
+ computerStats = []
+ networkStats = []
+ audioStats = []
+ aggregateStats = []
+
+ aggregate = @state.stats?.aggregate
+ network = @state.stats?.network
+ system = @state.stats?.system
+ audio = @state.stats?.audio
+
+ aggregateTag = null
+ if aggregate?
+ if aggregate.latency
+ aggregateStats.push(@stat(aggregate, 'aggregate', 'Tot Latency', 'latency', Math.round(aggregate.latency)))
+
+ aggregateTag =
+ `
You have successfully connected your Stripe account for payments. If you need to make any changes to your Stripe account, please go to the Stripe website and sign in using your Stripe credentials there to make any changes needed.
TestDrive is the primary means by which JamKazam connects new students to teachers, so if you don't do this, the marketplace will really not help you.
If you feel that you have a compelling reason not to give TestDrive lessons, but still want to participate in our marketplace, then please send us an email at support@jamkazam.com to chat with us about it.")
+
+ navTo = 'rejected'
+ else
+ # We are done:
+ navTo = @teacherSetupSource()
+
+ navTo
+
+ handleNav: (e) ->
+ navTo = this.navDestination(e)
+
+ if navTo == 'rejected'
+ # do nothing...handled elsewhere
+ else
+ teacherActions.change.trigger(this.state, {navTo: navTo, instructions:e})
+
+ handleFocus: (e) ->
+ @pricePerLessonCents=e.target.value
+
+ handleTextChange: (e) ->
+ @pricePerLessonCents=e.target.value
+ this.forceUpdate()
+
+ handleCheckChange: (e) ->
+ if @iCheckIgnore
+ return
+ this.setState({"#{e.target.name}": e.target.checked})
+
+ handleTestDriveCountChange: (e) ->
+ $this = $(e.target)
+ value = $this.val()
+ this.setState({test_drives_per_week: new Number(value)})
+
+ handleLearnMoreAboutTestDrive: (e) ->
+ e.preventDefault()
+ alert("Help documentation coming soon!")
+
+ render: () ->
+ priceRows = []
+ for minutes in [30, 45, 60, 90, 120]
+ pricePerLessonName = "price_per_lesson_#{minutes}_cents"
+ pricePerMonthName = "price_per_month_#{minutes}_cents"
+ priceKey = "lesson_duration_#{minutes}"
+ inputName = "#{priceKey}_input"
+ containerName = "#{priceKey}_container"
+ durationChecked = this.state[priceKey]
+
+ # If we are currently editing, don't format; used cache value:
+ if $("[name='#{pricePerLessonName}']", @root).is(":focus")
+ pricePerLessonCents = @pricePerLessonCents
+ else
+ ppl_fld_name="price_per_lesson_"+minutes+"_cents"
+ pricePerLessonCents = context.JK.ProfileUtils.normalizeMoneyForDisplay(this.state[ppl_fld_name])
+
+
+ # If we are currently editing, don't format; used cache value:
+ if $("[name='#{pricePerMonthName}']", @root).is(":focus")
+ pricePerMonthCents = @pricePerMonthCents
+ else
+ pricePerMonthCents = context.JK.ProfileUtils.normalizeMoneyForDisplay(this.state["price_per_month_"+minutes+"_cents"])
+
+ pricesPerLessonEnabled = this.state.prices_per_lesson
+ pricesPerMonthEnabled = this.state.prices_per_month
+
+ monthlyEnabled = durationChecked && pricesPerMonthEnabled
+ lessonEnabled = durationChecked && pricesPerLessonEnabled
+ perMonthInputStyles = classNames({"per-month-target" : true, disabled: !monthlyEnabled})
+ perLessonInputStyles = classNames({"per-lesson-target": true, disabled: !lessonEnabled})
+
+ test_drive_lessons = []
+ for i in [2..10]
+ test_drive_lessons.push(``)
+
+ priceRows.push `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $
+
+
+
+ $
+
+
+
+
+
+
`
+
+ # Render:
+ `
+
+
Offer Lessons Pricing & Payments:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Please fill in the prices (in US Dollars) for the lessons you have chosen to offer in the boxes below:
+
+
+
+
+
+
Offer Lessons of These Durations:
+
+
+
+
+
Price Per Lesson
+
+
+
Price Per Month
+
+
+
+
+
+ {priceRows}
+
+
+
TestDrive Program:
+
+
+
+
+
+ TestDrive is the primary marketing program JamKazam uses to drive new students through our marketplace to teachers. You will be paid $10 per 30-minute TestDrive lesson that you teach. Teach more TestDrive lessons to acquire more students.
+
+ GET 4 LESSONS WITH 4 DIFFERENT TEACHERS FOR JUST $12.50 EACH
+
+
+ You wouldn't marry the first person you date - right? Choosing the right teacher is the most important
+ thing you can do to ensure success and become a better musician. Try 4 different teachers. Then pick the
+ one
+ who is best for YOU!
+
+ Want to try more than one teacher, but 4 is too many for you? Try two lessons with two different
+ teachers for the price of one lesson.
+ A great value, and a good way to find an excellent teacher!
+
+ Are you confident you've found the best teacher for you? Then book your first lesson at a terrific
+ value, and get your first lesson scheduled to start learning more today!
+
`
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/TryTestDriveDialog.js.jsx.coffee b/web/app/assets/javascripts/react-components/TryTestDriveDialog.js.jsx.coffee
new file mode 100644
index 000000000..bc541ed9d
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/TryTestDriveDialog.js.jsx.coffee
@@ -0,0 +1,99 @@
+context = window
+ConfigureTracksStore = @ConfigureTracksStore
+ConfigureTracksActions = @ConfigureTracksActions
+@TryTestDriveDialog = React.createClass({
+
+ mixins: [Reflux.listenTo(@AppStore, "onAppInit")]
+ teacher: null
+
+ beforeShow: (args) ->
+ # d1 should be a teacher ID (vs a user ID that happens to be a teacher)
+ logger.debug("TryTestDriveDialog.beforeShow", args.d1)
+ @teacher = args.d1
+
+ afterHide: () ->
+
+ onAppInit: (@app) ->
+ dialogBindings = {
+ 'beforeShow': @beforeShow,
+ 'afterHide': @afterHide
+ };
+
+ @app.bindDialog('try-test-drive', dialogBindings);
+
+ getInitialState: () ->
+ {}
+
+ doCancel: (e) ->
+ e.preventDefault()
+ @app.layout.closeDialog('try-test-drive', true);
+
+ doTestDriveNow: (e) ->
+ e.preventDefault()
+
+ rest.createTeacherIntent({id: @teacher, intent: 'book-test-drive'}).done((response) => @marked(response)).fail((jqXHR) => @failedMark(jqXHR))
+
+ marked: () ->
+ @app.layout.closeDialog('try-test-drive')
+ window.location.href = "/client#/jamclass/lesson-payment/test-drive"
+
+ failedMark: (jqXHR, textStatus, errorMessage) ->
+ @app.ajaxError(jqXHR, textStatus, errorMessage)
+
+ render: () ->
+ `
+
+
+
+
TestDrive
+
+
+
+
What Is TestDrive?
+
+
+ Our TestDrive package is the best way to painlessly find the best instructor for you - i.e. a teacher who has
+ the qualifications you're looking for, and with whom you really connect. This is the most critical factor in
+ achieving the results you want from your investment of time and money into lessons.
+
+
With TestDrive you pay just $49.99 to take 4 full lessons - one lesson with each of 4 different teachers you
+ select. Then you can pick the teacher who works best for you. It's an amazing value, highly convenient, and
+ the best way to find the right teacher for you.
+
+
If you're serious about getting started on lessons, this is the way to go.
`
+
+
+ componentDidMount: () ->
+ $root = $(@getDOMNode())
+
+ componentWillUpdate: () ->
+ $root = $(@getDOMNode())
+
+ componentDidUpdate: () ->
+ $root = $(@getDOMNode())
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/YearSelect.js.jsx.coffee b/web/app/assets/javascripts/react-components/YearSelect.js.jsx.coffee
new file mode 100644
index 000000000..b9f2ec61c
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/YearSelect.js.jsx.coffee
@@ -0,0 +1,24 @@
+context = window
+rest = window.JK.Rest()
+logger = context.JK.logger
+
+@YearSelect = React.createClass({
+
+
+ render: () ->
+ options = []
+
+ now = new Date().getFullYear()
+ options.push ``
+ for yr in [now..1916]
+ options.push ``
+
+ if this.props?.defaultPresent
+ defaultValue = '0'
+ else
+ defaultValue = now
+
+ ``
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee b/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee
index 6c054e3ec..dbe55b7e4 100644
--- a/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee
+++ b/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee
@@ -2,4 +2,5 @@ context = window
@AppActions = Reflux.createActions({
appInit: {}
+ openExternalUrl: {}
})
diff --git a/web/app/assets/javascripts/react-components/actions/AttachmentActions.js.coffee b/web/app/assets/javascripts/react-components/actions/AttachmentActions.js.coffee
new file mode 100644
index 000000000..78ff35912
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/AttachmentActions.js.coffee
@@ -0,0 +1,9 @@
+context = window
+
+@AttachmentActions = Reflux.createActions({
+ startAttachRecording: {}
+ startAttachNotation: {}
+ startAttachAudio: {}
+ uploadNotations: {}
+ uploadAudios: {}
+})
diff --git a/web/app/assets/javascripts/react-components/actions/AvatarActions.js.coffee b/web/app/assets/javascripts/react-components/actions/AvatarActions.js.coffee
new file mode 100644
index 000000000..130a2f213
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/AvatarActions.js.coffee
@@ -0,0 +1,10 @@
+context = window
+
+@AvatarActions = Reflux.createActions({
+ start: {}
+ select: {}
+ pick: {}
+ update: {}
+ delete: {}
+})
+
diff --git a/web/app/assets/javascripts/react-components/actions/BroadcastActions.js.coffee b/web/app/assets/javascripts/react-components/actions/BroadcastActions.js.coffee
index e4bc43707..3582049eb 100644
--- a/web/app/assets/javascripts/react-components/actions/BroadcastActions.js.coffee
+++ b/web/app/assets/javascripts/react-components/actions/BroadcastActions.js.coffee
@@ -4,5 +4,3 @@ context = window
load: {asyncResult: true},
hide: {}
})
-
-context.JK.Actions.Broadcast = BroadcastActions
diff --git a/web/app/assets/javascripts/react-components/actions/BrowserMediaActions.js.coffee b/web/app/assets/javascripts/react-components/actions/BrowserMediaActions.js.coffee
new file mode 100644
index 000000000..07a574500
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/BrowserMediaActions.js.coffee
@@ -0,0 +1,11 @@
+context = window
+
+@BrowserMediaActions = Reflux.createActions({
+ load: {}
+ play: {}
+ stop: {}
+ pause: {}
+ seek: {}
+ setVolume: {}
+ getPlayPosition: {}
+})
diff --git a/web/app/assets/javascripts/react-components/actions/BrowserMediaPlaybackActions.js.coffee b/web/app/assets/javascripts/react-components/actions/BrowserMediaPlaybackActions.js.coffee
new file mode 100644
index 000000000..f3634dde0
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/BrowserMediaPlaybackActions.js.coffee
@@ -0,0 +1,11 @@
+context = window
+
+@BrowserMediaPlaybackActions = Reflux.createActions({
+ playbackStateChange: {}
+ positionUpdate:{}
+ mediaStartPlay: {}
+ mediaStopPlay: {}
+ mediaPausePlay: {}
+ mediaChangePosition: {}
+ currentTimeChanged: {}
+})
diff --git a/web/app/assets/javascripts/react-components/actions/CallbackActions.js.coffee b/web/app/assets/javascripts/react-components/actions/CallbackActions.js.coffee
new file mode 100644
index 000000000..92a639e88
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/CallbackActions.js.coffee
@@ -0,0 +1,6 @@
+context = window
+
+@CallbackActions = Reflux.createActions({
+ genericCallback: {}
+})
+
diff --git a/web/app/assets/javascripts/react-components/actions/ChatActions.js.coffee b/web/app/assets/javascripts/react-components/actions/ChatActions.js.coffee
new file mode 100644
index 000000000..4757d6488
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/ChatActions.js.coffee
@@ -0,0 +1,12 @@
+context = window
+
+@ChatActions = Reflux.createActions({
+ msgReceived: {}
+ sendMsg: {}
+ loadMessages: {}
+ emptyChannel: {}
+ sessionStarted: {}
+ activateChannel: {}
+ fullyOpened: {}
+ initializeLesson: {}
+})
diff --git a/web/app/assets/javascripts/react-components/actions/ConfigureTracksActions.js.coffee b/web/app/assets/javascripts/react-components/actions/ConfigureTracksActions.js.coffee
new file mode 100644
index 000000000..15559c5d9
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/ConfigureTracksActions.js.coffee
@@ -0,0 +1,28 @@
+context = window
+
+@ConfigureTracksActions = Reflux.createActions({
+ reset: {}
+ trySave: {}
+ midiScan: {}
+ vstScan: {}
+ vstScanComplete: {}
+ clearVsts: {}
+ cancelEdit: {}
+ deleteTrack: {}
+ updateOutputs: {}
+ showAddNewTrack: {}
+ showEditTrack: {}
+ showEditOutputs: {}
+ showVstSettings: {}
+ associateInputsWithTrack: {}
+ associateInstrumentWithTrack: {}
+ associateVSTWithTrack: {}
+ associateMIDIWithTrack: {}
+ desiredTrackType: {}
+ vstChanged: {}
+ manageVsts: {}
+ enableVst: {}
+ addSearchPath: {}
+ removeSearchPath: {}
+ selectVSTDirectory: {}
+})
diff --git a/web/app/assets/javascripts/react-components/actions/GenreActions.js.coffee b/web/app/assets/javascripts/react-components/actions/GenreActions.js.coffee
new file mode 100644
index 000000000..da4266638
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/GenreActions.js.coffee
@@ -0,0 +1,5 @@
+context = window
+
+@GenreActions = Reflux.createActions({
+
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/actions/InstrumentActions.js.coffee b/web/app/assets/javascripts/react-components/actions/InstrumentActions.js.coffee
new file mode 100644
index 000000000..4cd8c006e
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/InstrumentActions.js.coffee
@@ -0,0 +1,5 @@
+context = window
+
+@InstrumentActions = Reflux.createActions({
+
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/actions/JamBlasterActions.js.coffee b/web/app/assets/javascripts/react-components/actions/JamBlasterActions.js.coffee
new file mode 100644
index 000000000..82215844c
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/JamBlasterActions.js.coffee
@@ -0,0 +1,10 @@
+context = window
+
+@JamBlasterActions = Reflux.createActions({
+ resyncBonjour: {},
+ clearPortBindState: {},
+ saveNetworkSettings: {},
+ pairState: {},
+ setAutoPair: {},
+ updateAudio: {}
+})
diff --git a/web/app/assets/javascripts/react-components/actions/JamTrackActions.js.coffee b/web/app/assets/javascripts/react-components/actions/JamTrackActions.js.coffee
index 0c122c4c6..1b5e6a9ae 100644
--- a/web/app/assets/javascripts/react-components/actions/JamTrackActions.js.coffee
+++ b/web/app/assets/javascripts/react-components/actions/JamTrackActions.js.coffee
@@ -3,6 +3,7 @@ context = window
@JamTrackActions = Reflux.createActions({
open: {}
close: {}
+ activateNoMixdown: {}
requestSearch: {}
requestFilter: {}
})
diff --git a/web/app/assets/javascripts/react-components/actions/JamTrackMixdownActions.js.coffee b/web/app/assets/javascripts/react-components/actions/JamTrackMixdownActions.js.coffee
new file mode 100644
index 000000000..475cefc7b
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/JamTrackMixdownActions.js.coffee
@@ -0,0 +1,14 @@
+context = window
+
+@JamTrackMixdownActions = Reflux.createActions({
+ createMixdown: {}
+ editMixdown: {}
+ refreshMixdown: {}
+ deleteMixdown: {}
+ openMixdown: {}
+ closeMixdown: {}
+ enqueueMixdown: {}
+ downloadMixdown: {}
+ openDownloader: {}
+})
+
diff --git a/web/app/assets/javascripts/react-components/actions/JamTrackPlayerActions.js.coffee b/web/app/assets/javascripts/react-components/actions/JamTrackPlayerActions.js.coffee
new file mode 100644
index 000000000..fc6e3d624
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/JamTrackPlayerActions.js.coffee
@@ -0,0 +1,19 @@
+context = window
+
+@JamTrackPlayerActions = Reflux.createActions({
+ open: {}
+ opened: {}
+ createMixdown: {}
+ editMixdown: {}
+ deleteMixdown: {}
+ openMixdown: {}
+ activateNoMixdown: {}
+ closeMixdown: {}
+ enqueueMixdown: {}
+ downloadMixdown: {}
+ refreshMixdown: {}
+ openStem: {}
+
+ windowUnloaded: {}
+})
+
diff --git a/web/app/assets/javascripts/react-components/actions/LanguageActions.js.coffee b/web/app/assets/javascripts/react-components/actions/LanguageActions.js.coffee
new file mode 100644
index 000000000..4b2d0308d
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/LanguageActions.js.coffee
@@ -0,0 +1,5 @@
+context = window
+
+@LanguageActions = Reflux.createActions({
+
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/actions/LessonTimerActions.js.coffee b/web/app/assets/javascripts/react-components/actions/LessonTimerActions.js.coffee
new file mode 100644
index 000000000..bf812cc97
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/LessonTimerActions.js.coffee
@@ -0,0 +1,6 @@
+context = window
+
+@LessonTimerActions = Reflux.createActions({
+ loadLessons: {}
+})
+
diff --git a/web/app/assets/javascripts/react-components/actions/LocationActions.js.coffee b/web/app/assets/javascripts/react-components/actions/LocationActions.js.coffee
new file mode 100644
index 000000000..21b2953bd
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/LocationActions.js.coffee
@@ -0,0 +1,7 @@
+context = window
+
+@LocationActions = Reflux.createActions({
+
+ load: {}
+ selectCountry: {}
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/actions/NavActions.js.coffee b/web/app/assets/javascripts/react-components/actions/NavActions.js.coffee
new file mode 100644
index 000000000..5e3828859
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/NavActions.js.coffee
@@ -0,0 +1,7 @@
+context = window
+
+@NavActions = Reflux.createActions({
+ setScreenInfo: {}
+ screenChanged: {}
+})
+
diff --git a/web/app/assets/javascripts/react-components/actions/ProfileActions.js.coffee b/web/app/assets/javascripts/react-components/actions/ProfileActions.js.coffee
new file mode 100644
index 000000000..0351ee476
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/ProfileActions.js.coffee
@@ -0,0 +1,14 @@
+context = window
+
+@ProfileActions = Reflux.createActions({
+
+ startTeacherEdit: {}
+ cancelTeacherEdit: {}
+ doneTeacherEdit: {}
+ startProfileEdit: {}
+ cancelProfileEdit: {}
+ doneProfileEdit: {}
+ editProfileNext: {}
+ viewTeacherProfile: {}
+ viewTeacherProfileDone: {}
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/actions/SchoolActions.js.coffee b/web/app/assets/javascripts/react-components/actions/SchoolActions.js.coffee
new file mode 100644
index 000000000..8977445d2
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/SchoolActions.js.coffee
@@ -0,0 +1,9 @@
+context = window
+
+@SchoolActions = Reflux.createActions({
+ refresh: {},
+ addInvitation: {},
+ deleteInvitation: {}
+ updateSchool: {}
+})
+
diff --git a/web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee b/web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee
index 634e7d419..522bd8464 100644
--- a/web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee
+++ b/web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee
@@ -1,6 +1,7 @@
context = window
@SessionActions = Reflux.createActions({
+ enterSession: {}
joinSession: {}
leaveSession: {}
mixersChanged: {}
@@ -19,4 +20,7 @@ context = window
broadcastFailure: {}
broadcastSuccess: {}
broadcastStopped: {}
+ mixdownActive: {}
+ sessionJoinedByOther: {}
+ navToSession: {}
})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/actions/SessionStatsActions.js.coffee b/web/app/assets/javascripts/react-components/actions/SessionStatsActions.js.coffee
new file mode 100644
index 000000000..23dd5ef48
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/SessionStatsActions.js.coffee
@@ -0,0 +1,5 @@
+context = window
+
+@SessionStatsActions = Reflux.createActions({
+ pushStats: {}
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/actions/StripeActions.js.coffee b/web/app/assets/javascripts/react-components/actions/StripeActions.js.coffee
new file mode 100644
index 000000000..33d27d6e1
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/StripeActions.js.coffee
@@ -0,0 +1,6 @@
+context = window
+
+@StripeActions = Reflux.createActions({
+ connect: {}
+})
+
diff --git a/web/app/assets/javascripts/react-components/actions/SubjectActions.js.coffee b/web/app/assets/javascripts/react-components/actions/SubjectActions.js.coffee
new file mode 100644
index 000000000..abc7df045
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/SubjectActions.js.coffee
@@ -0,0 +1,5 @@
+context = window
+
+@SubjectActions = Reflux.createActions({
+
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/actions/TeacherActions.js.coffee b/web/app/assets/javascripts/react-components/actions/TeacherActions.js.coffee
new file mode 100644
index 000000000..e58dbeb3d
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/TeacherActions.js.coffee
@@ -0,0 +1,8 @@
+context = window
+
+@TeacherActions = Reflux.createActions({
+ load: {},
+ change: {}
+})
+
+context.JK.Actions.Teacher = TeacherActions
diff --git a/web/app/assets/javascripts/react-components/actions/TeacherSearchActions.js.coffee b/web/app/assets/javascripts/react-components/actions/TeacherSearchActions.js.coffee
new file mode 100644
index 000000000..b9c8681a8
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/TeacherSearchActions.js.coffee
@@ -0,0 +1,7 @@
+context = window
+
+@TeacherSearchActions = Reflux.createActions({
+ updateOptions: {}
+ search: {}
+})
+
diff --git a/web/app/assets/javascripts/react-components/actions/TeacherSearchResultsActions.js.coffee b/web/app/assets/javascripts/react-components/actions/TeacherSearchResultsActions.js.coffee
new file mode 100644
index 000000000..7b39d44e5
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/TeacherSearchResultsActions.js.coffee
@@ -0,0 +1,7 @@
+context = window
+
+@TeacherSearchResultsActions = Reflux.createActions({
+ reset: {}
+ nextPage: {}
+})
+
diff --git a/web/app/assets/javascripts/react-components/actions/UserActions.js.coffee b/web/app/assets/javascripts/react-components/actions/UserActions.js.coffee
new file mode 100644
index 000000000..815277def
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/UserActions.js.coffee
@@ -0,0 +1,8 @@
+context = window
+
+@UserActions = Reflux.createActions({
+ loaded: {}
+ modify: {}
+ refresh: {}
+})
+
diff --git a/web/app/assets/javascripts/react-components/actions/UserActivityActions.js.coffee b/web/app/assets/javascripts/react-components/actions/UserActivityActions.js.coffee
new file mode 100644
index 000000000..685d39fac
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/UserActivityActions.js.coffee
@@ -0,0 +1,6 @@
+context = window
+
+@UserActivityActions = Reflux.createActions({
+ setActive: {}
+})
+
diff --git a/web/app/assets/javascripts/react-components/actions/VideoActions.js.coffee b/web/app/assets/javascripts/react-components/actions/VideoActions.js.coffee
index 5879f124f..662610fa9 100644
--- a/web/app/assets/javascripts/react-components/actions/VideoActions.js.coffee
+++ b/web/app/assets/javascripts/react-components/actions/VideoActions.js.coffee
@@ -4,13 +4,14 @@ context = window
refresh: {}
stopVideo: {}
startVideo: {}
- setVideoEncodeResolution: {}
- setSendFrameRate: {}
+ setCaptureResolution: {}
selectDevice: {}
videoWindowOpened : {}
videoWindowClosed : {}
howToUseVideoPopupClosed: {}
toggleVideo: {}
+ testVideo: {}
configureVideoPopupClosed: {}
checkPromptConfigureVideo: {}
+ setVideoEnabled: {}
})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/actions/VideoUploaderActions.js.coffee b/web/app/assets/javascripts/react-components/actions/VideoUploaderActions.js.coffee
new file mode 100644
index 000000000..e2fcf6804
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/VideoUploaderActions.js.coffee
@@ -0,0 +1,12 @@
+context = window
+
+@VideoUploaderActions = Reflux.createActions({
+ uploadVideo: {},
+ pause: {},
+ resume: {},
+ cancel: {},
+ newVideo: {},
+ showUploader: {},
+ uploaderClosed: {}
+ delete: {}
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee b/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee
index 4639dc8e2..aa28215d6 100644
--- a/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee
+++ b/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee
@@ -19,7 +19,7 @@ MIX_MODES = context.JK.MIX_MODES;
@mediaSummary = {}
@mediaTrackGroups = [ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup,
ChannelGroupIds.MetronomeGroup]
- @muteBothMasterAndPersonalGroups = [ChannelGroupIds.AudioInputMusicGroup, ChannelGroupIds.MediaTrackGroup,
+ @muteBothMasterAndPersonalGroups = [ChannelGroupIds.AudioInputMusicGroup, ChannelGroupIds.MidiInputMusicGroup, ChannelGroupIds.MediaTrackGroup,
ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup]
@vuStats = {}
@shouldCollectVuStats = false
@@ -69,6 +69,7 @@ MIX_MODES = context.JK.MIX_MODES;
backingTracks = @session.backingTracks()
recordedJamTracks = @session.recordedJamTracks()
jamTracks = @session.jamTracks()
+ jamTrackMixdown = @session.jamTrackMixdown()
###
with mixer info, we use these to decide what kind of tracks are open in the backend
@@ -92,6 +93,7 @@ MIX_MODES = context.JK.MIX_MODES;
@metronomeTrackMixers = []
@adhocTrackMixers = []
+
groupByType = (mixers, isLocalMixer) =>
for mixer in mixers
mediaType = mixer.media_type
@@ -106,7 +108,10 @@ MIX_MODES = context.JK.MIX_MODES;
isJamTrack = false;
- if jamTracks
+ if mixer.id == jamTrackMixdown.id
+ isJamTrack = true;
+
+ if !isJamTrack && jamTracks
# check if the ID matches that of an open jam track
for jamTrack in jamTracks
if mixer.id == jamTrack.id
@@ -186,6 +191,8 @@ MIX_MODES = context.JK.MIX_MODES;
backingTrackOpen: @backingTracks.length > 0
metronomeOpen: @session.isMetronomeOpen()
+
+
# figure out if any media is open
mediaOpenSummary = false
for mediaType, mediaOpen of @mediaSummary
@@ -193,6 +200,11 @@ MIX_MODES = context.JK.MIX_MODES;
@mediaSummary.mediaOpen = mediaOpenSummary
+ # the user needs media controls if any media is open, or, if the user has indicated they want to open a JamTrack
+ @mediaSummary.userNeedsMediaControls = @mediaSummary.mediaOpen || window.JamTrackStore.jamTrack?
+
+ # this defines what the user wants to be open, not what actually is open in the backend and/or session
+ @mediaSummary.jamTrack = window.JamTrackStore.jamTrack
# figure out if we opened any media
isOpener = false
@@ -294,6 +306,7 @@ MIX_MODES = context.JK.MIX_MODES;
jamTrackMixers = @jamTrackMixers.slice();
jamTracks = []
jamTrackName = null;
+ jamTrackMixdown = {id: null}
if @session.isPlayingRecording()
# only return managed mixers for recorded backing tracks
@@ -303,6 +316,7 @@ MIX_MODES = context.JK.MIX_MODES;
# only return un-managed (ad-hoc) mixers for normal backing tracks
jamTracks = @session.jamTracks()
jamTrackName = @session.jamTrackName()
+ jamTrackMixdown = @session.jamTrackMixdown()
# pluck the 1st mixer, and assume that all other mixers in this group are of the same type (between JamTrack vs Peer)
# if it's a locally opened track (JamTrackGroup), then we can say this person is the opener
@@ -310,55 +324,97 @@ MIX_MODES = context.JK.MIX_MODES;
if jamTracks
noCorrespondingTracks = false
- for jamTrack in jamTracks
- mixer = null
- preMasteredClass = ""
- # find the track or tracks that correspond to the mixer
- correspondingTracks = []
- for matchMixer in @jamTrackMixers
- if matchMixer.id == jamTrack.id
- correspondingTracks.push(jamTrack)
- mixer = matchMixer
-
- if correspondingTracks.length == 0
+ # Are we opening a mixdown, or a full track?
+ if jamTrackMixdown.id?
+ logger.debug("MixerHelper: mixdown is active. id: #{jamTrackMixdown.id}")
+ if jamTrackMixers.length == 0
noCorrespondingTracks = true
- logger.error("could not correlate jam tracks", jamTrackMixers, jamTracks)
+ logger.error("could not correlate mixdown tracks", jamTrackMixers, jamTrackMixdown)
@app.notify({
- title: "Unable to Open JamTrack",
+ title: "Unable to Open Custom Mix",
text: "Could not correlate server and client tracks",
icon_url: "/assets/content/icon_alert_big.png"})
return _jamTracks
-
- #jamTracks = $.grep(jamTracks, (value) =>
- # $.inArray(value, correspondingTracks) < 0
- #)
-
- # prune found mixers
- jamTrackMixers.splice(mixer);
-
- oneOfTheTracks = correspondingTracks[0];
- instrumentIcon = context.JK.getInstrumentIcon24(oneOfTheTracks.instrument.id);
-
- part = oneOfTheTracks.part
-
- instrumentName = oneOfTheTracks.instrument.description
-
- if part?
- trackName = "#{instrumentName}: #{part}"
+ else if jamTrackMixers.length > 1
+ logger.warn("ignoring wrong amount of mixers for JamTrack in mixdown mode")
+ return _jamTracks
else
- trackName = instrumentName
- data =
- name: jamTrackName
- trackName: trackName
- part: part
- isOpener: isOpener
- instrumentIcon: instrumentIcon
- track: oneOfTheTracks
- mixers: @mediaMixers(mixer, isOpener)
+ instrumentIcon = context.JK.getInstrumentIcon24('other')
+ part = null
+ instrumentName = 'Custom Mix'
+ trackName = 'Custom Mix'
- _jamTracks.push(data)
+ data =
+ name: jamTrackName
+ trackName: trackName
+ part: part
+ isOpener: isOpener
+ instrumentIcon: instrumentIcon
+ track: jamTrackMixdown
+ mixers: @mediaMixers(jamTrackMixers[0], isOpener)
+
+ _jamTracks.push(data)
+ else
+ logger.debug("MixerHelper: full jamtrack is active")
+
+ if jamTrackMixers.length == 1
+ logger.warn("ignoring wrong amount of mixers for JamTrack in Full Track mode")
+ return _jamTracks
+
+ for jamTrack in jamTracks
+ mixer = null
+ preMasteredClass = ""
+ # find the track or tracks that correspond to the mixer
+ correspondingTracks = []
+
+ for matchMixer in @jamTrackMixers
+ if matchMixer.id == jamTrack.id
+ correspondingTracks.push(jamTrack)
+ mixer = matchMixer
+
+ if correspondingTracks.length == 0
+ noCorrespondingTracks = true
+ logger.error("could not correlate jam tracks", jamTrackMixers, jamTracks)
+ @app.notify({
+ title: "Unable to Open JamTrack",
+ text: "Could not correlate server and client tracks",
+ icon_url: "/assets/content/icon_alert_big.png"})
+ return _jamTracks
+
+ #jamTracks = $.grep(jamTracks, (value) =>
+ # $.inArray(value, correspondingTracks) < 0
+ #)
+
+ # prune found mixers
+ jamTrackMixers.splice(mixer);
+
+ oneOfTheTracks = correspondingTracks[0];
+ instrumentIcon = context.JK.getInstrumentIcon24(oneOfTheTracks.instrument.id);
+
+ part = oneOfTheTracks.part
+
+ instrumentName = oneOfTheTracks.instrument.description
+
+ if part?
+ trackName = "#{instrumentName}: #{part}"
+ else
+ trackName = instrumentName
+
+ if jamTrack.track_type == 'Click'
+ trackName = 'Clicktrack'
+
+ data =
+ name: jamTrackName
+ trackName: trackName
+ part: part
+ isOpener: isOpener
+ instrumentIcon: instrumentIcon
+ track: oneOfTheTracks
+ mixers: @mediaMixers(mixer, isOpener)
+
+ _jamTracks.push(data)
_jamTracks
@@ -580,7 +636,7 @@ MIX_MODES = context.JK.MIX_MODES;
muteMixer = mixer
# sanity checks
- if mixer && mixer.group_id != ChannelGroupIds.AudioInputMusicGroup
+ if mixer && (mixer.group_id != ChannelGroupIds.AudioInputMusicGroup && mixer.group_id != ChannelGroupIds.MidiInputMusicGroup)
logger.error("found local mixer that was not of groupID: AudioInputMusicGroup", mixer)
if mixer
@@ -593,7 +649,7 @@ MIX_MODES = context.JK.MIX_MODES;
# sanity checks
if !oppositeMixer
logger.error("unable to find opposite mixer for local mixer", mixer)
- else if oppositeMixer.group_id != ChannelGroupIds.AudioInputMusicGroup
+ else if oppositeMixer.group_id != ChannelGroupIds.AudioInputMusicGroup && oppositeMixer.group_id != ChannelGroupIds.MidiInputMusicGroup
logger.error("found local mixer in opposite mode that was not of groupID: AudioInputMusicGroup", mixer, oppositeMixer)
else
logger.debug("local track is not present: ", track, @allMixers)
@@ -605,7 +661,7 @@ MIX_MODES = context.JK.MIX_MODES;
mixer = @getMixerByTrackId(track.client_track_id, MIX_MODES.MASTER)
# sanity check
- if mixer && mixer.group_id != ChannelGroupIds.PeerAudioInputMusicGroup
+ if mixer && (mixer.group_id != ChannelGroupIds.PeerAudioInputMusicGroup && mixer.group_id != ChannelGroupIds.PeerMidiInputMusicGroup)
logger.warn("master: found remote mixer that was not of groupID: PeerAudioInputMusicGroup", client_id, track.client_track_id, mixer)
vuMixer = mixer
@@ -618,7 +674,7 @@ MIX_MODES = context.JK.MIX_MODES;
oppositeMixer = oppositeMixers[ChannelGroupIds.UserMusicInputGroup][0]
if !oppositeMixer
- logger.warn("unable to find UserMusicInputGroup corresponding to PeerAudioInputMusicGroup mixer", mixer )
+ logger.warn("unable to find UserMusicInputGroup corresponding to PeerAudioInputMusicGroup mixer", mixer, @personalMixers )
when MIX_MODES.PERSONAL
mixers = @groupedMixersForClientId(client_id, [ ChannelGroupIds.UserMusicInputGroup], {}, MIX_MODES.PERSONAL)
@@ -633,7 +689,7 @@ MIX_MODES = context.JK.MIX_MODES;
oppositeMixer = @getMixerByTrackId(track.client_track_id, MIX_MODES.MASTER)
if !oppositeMixer
logger.debug("personal: unable to find a PeerAudioInputMusicGroup master mixer matching a UserMusicInput", client_id, track.client_track_id)
- else if oppositeMixer.group_id != ChannelGroupIds.PeerAudioInputMusicGroup
+ else if oppositeMixer.group_id != ChannelGroupIds.PeerAudioInputMusicGroup && oppositeMixer.group_id != mixer.group_id != ChannelGroupIds.PeerMidiInputMusicGroup
logger.error("personaol: found remote mixer that was not of groupID: PeerAudioInputMusicGroup", client_id, track.client_track_id, mixer)
#vuMixer = oppositeMixer; # for personal mode, use the PeerAudioInputMusicGroup's VUs
@@ -859,18 +915,26 @@ MIX_MODES = context.JK.MIX_MODES;
getGroupMixer: (categoryId, mode) ->
groupId = if mode == MIX_MODES.MASTER then ChannelGroupIds.MasterCatGroup else ChannelGroupIds.MonitorCatGroup
+ oppositeGroupId = if !mode == MIX_MODES.MASTER then ChannelGroupIds.MasterCatGroup else ChannelGroupIds.MonitorCatGroup
mixers = @mixersForGroupId(groupId, mode)
+ oppositeMixers = @mixersForGroupId(oppositeGroupId, !mode)
if mixers.length == 0
#logger.warn("could not find mixer with group ID: " + groupId + ', mode:' + mode)
return null
found = null
+ oppositeFound = null
for mixer in mixers
if mixer.name == categoryId
found = mixer
break
+ for mixer in oppositeMixers
+ if mixer.name == categoryId
+ oppositeFound = mixer
+ break
+
unless found?
logger.warn("could not find mixer with categoryId: " + categoryId)
return null
@@ -879,7 +943,7 @@ MIX_MODES = context.JK.MIX_MODES;
mixer: found,
muteMixer : found,
vuMixer: found,
- oppositeMixer: found
+ oppositeMixer: oppositeFound
}
prepareSimulatedMixers: () ->
diff --git a/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee b/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee
index 1cb5023c7..51f385a46 100644
--- a/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee
+++ b/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee
@@ -2,12 +2,13 @@ context = window
@SessionHelper = class SessionHelper
- constructor: (app, session, participantsEverSeen, isRecording, downloadingJamTrack) ->
+ constructor: (app, session, participantsEverSeen, isRecording, downloadingJamTrack, preppingVstEnable) ->
@app = app
@session = session
@participantsEverSeen = participantsEverSeen
@isRecording = isRecording
@downloadingJamTrack = downloadingJamTrack
+ @preppingVstEnable = preppingVstEnable
inSession: () ->
@session?
@@ -18,6 +19,24 @@ context = window
else
[]
+ users: () ->
+ found = {}
+
+ for participant in @participants()
+ found[participant.user.id] = participant.user
+
+ found
+
+ findParticipantByUserId: (userId) ->
+ foundParticipant = null
+ for participant in @participants()
+ if participant.user.id == userId
+ foundParticipant = participant
+ break
+
+ foundParticipant
+
+
otherParticipants: () ->
others = []
for participant in @participants()
@@ -26,6 +45,26 @@ context = window
others.push(participant) unless myTrack
others
+ sessionController: () ->
+ info = {}
+
+ # XXX testing:
+ info["can_control"] = false
+ info["session_controller"] = @participants()[0].user
+
+ if @session
+ if @session.session_controller_id == null
+ info['session_controller'] = null
+ info['can_control'] = true
+ else
+ for participant in @participants()
+ if participant.user.id == @session.session_controller_id
+ info['session_controller'] = participant.user
+ info['can_control'] = participant.user.id == context.JK.currentUserId
+ break
+
+ info
+
# if any participant has the metronome open, then we say this session has the metronome open
isMetronomeOpen: () ->
@session? && @session.metronome_active
@@ -70,11 +109,14 @@ context = window
jamTracks: () ->
if @session && @session.jam_track
@session.jam_track.tracks.filter((track)->
- track.track_type == 'Track'
+ track.track_type == 'Track' || track.track_type == 'Click'
)
else
null
+ jamTrackMixdown: () ->
+ { id: @session?.jam_track?.mixdown.id }
+
jamTrackName: () ->
@session?.jam_track?.name
diff --git a/web/app/assets/javascripts/react-components/landing/GiftCardLandingPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/GiftCardLandingPage.js.jsx.coffee
new file mode 100644
index 000000000..ae9e77fbb
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/landing/GiftCardLandingPage.js.jsx.coffee
@@ -0,0 +1,94 @@
+context = window
+rest = context.JK.Rest()
+
+@GiftCardLandingPage = React.createClass({
+
+ render: () ->
+
+ if this.state.done
+ ctaButtonText10 = 'sending you in...'
+ ctaButtonText20 = 'sending you in...'
+ else if this.state.processing
+ ctaButtonText10 = 'hold on...'
+ ctaButtonText20 = 'hold on...'
+ else
+ ctaButtonText10 = `ADD $10 CARD TO CART`
+ ctaButtonText20 = `ADD $20 CARD TO CART`
+
+
+ ctaButtons =
+ `
+
+
+
`
+
+
+ `
+
+
+
+
$10 or $20 JAMTRACKS GIFT CARDS
+
A PERFECT GIFT FOR MUSICIANS
+
+
+
+
+
+
+ Preview A JamTrack
+
"{this.props.jam_track.name}"
+
+
+
Click the play buttons below to preview the master mix and 20-second samples of all the isolated tracks.
+
+
+
+
+ Get a $10 gift card (good for 5 songs) or a $20 gift card (good for 10 songs), and your happy
+ gift card getter can choose their favorites from our catalog of 3,700+ popular songs.
+
+ JamTracks by JamKazam are the best way to play along with your favorite songs. Far better and different than traditional
+ backing tracks, our JamTracks are complete multi-track professional recordings, with fully isolated tracks for each part of the music.
+ And our free app and Internet service are packed with features that give you unmatched creative freedom to learn, practice, record, play with others, and share your performances.
+
+
+
`
+
+ getInitialState: () ->
+ {processing:false}
+
+ componentDidMount:() ->
+ $root = $(this.getDOMNode())
+
+# add item to cart, create the user if necessary, and then place the order to get the free JamTrack.
+ ctaClick: (card_type, e) ->
+ e.preventDefault()
+
+ return if @state.processing
+
+ loggedIn = context.JK.currentUserId?
+
+ rest.addGiftCardToShoppingCart({id: card_type}).done((response) =>
+
+ if loggedIn
+ @setState({done: true})
+ context.location = '/client#/shoppingCart'
+ else
+ @setState({done: true})
+ context.location = '/client#/shoppingCart'
+
+ ).fail((jqXHR, textStatus, errorMessage) =>
+ if jqXHR.status == 422
+ errors = JSON.parse(jqXHR.responseText)
+ cart_errors = errors?.errors?.cart_id
+ context.JK.app.ajaxError(jqXHR, textStatus, errorMessage)
+ @setState({processing:false})
+ )
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/landing/HomePage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/HomePage.js.jsx.coffee
new file mode 100644
index 000000000..338b6398f
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/landing/HomePage.js.jsx.coffee
@@ -0,0 +1,399 @@
+context = window
+
+@HomePage = React.createClass({
+
+ celery: () ->
+ script = document.createElement("script");
+ script.src = "https://www.trycelery.com/js/celery.js";
+ script.async = true;
+ document.body.appendChild(script);
+
+ componentDidMount: () ->
+ @root =$(@getDOMNode())
+ @celery()
+
+ window.modernNavInit();
+
+ jamClassClicked: (e) ->
+ e.preventDefault()
+ alertify.alert("COMING SOON!");
+
+ render: () ->
+
+ items = []
+
+ for item in gon.news
+ items.push(`
+ `
+
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/landing/InvidualJamTrackPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/IndividualJamTrackPage.js.jsx.coffee
similarity index 92%
rename from web/app/assets/javascripts/react-components/landing/InvidualJamTrackPage.js.jsx.coffee
rename to web/app/assets/javascripts/react-components/landing/IndividualJamTrackPage.js.jsx.coffee
index b907c43ae..8be108fae 100644
--- a/web/app/assets/javascripts/react-components/landing/InvidualJamTrackPage.js.jsx.coffee
+++ b/web/app/assets/javascripts/react-components/landing/IndividualJamTrackPage.js.jsx.coffee
@@ -9,7 +9,9 @@ context = window
render: () ->
header = null
- if @props.band
+ if @props.instrument
+ header = "We Have #{@props.instrument_count} JamTracks With #{@props.instrument} Parts - Play Along With Your Favorites!"
+ else if @props.band
header = "#{@props.jam_track.original_artist} Backing Tracks - Complete Multitracks"
else if @props.generic?
header = "Backing Tracks + Free Amazing App = Unmatched Experience"
diff --git a/web/app/assets/javascripts/react-components/landing/JamClassAffiliateLandingBottomPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/JamClassAffiliateLandingBottomPage.js.jsx.coffee
new file mode 100644
index 000000000..204353571
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/landing/JamClassAffiliateLandingBottomPage.js.jsx.coffee
@@ -0,0 +1,394 @@
+context = window
+rest = context.JK.Rest()
+
+@JamClassAffiliateLandingBottomPage = React.createClass({
+
+ render: () ->
+ `
+
+
What Is The JamKazam Affiliate Program?
+
+
If you’re a music store and you sell products like instruments and accessories to musicians,
+
+ JamKazam can add value to every sale you make to customers by enabling you to bundle high-value digital music products and services that are highly complementary to the products you
+
+ are selling – at no additional cost to your customers. For example, you can bundle full
+
+ multitrack recordings from our catalog of 4,000+ popular songs that let your customers play
+
+ along with their favorite songs in incredibly innovative and fun ways, and you can
+ bundle free
+
+ online music lessons that really work (unlike Skype, which is great for voice chat but horrible for
+
+ music lessons) to help your customers connect with great teachers and stay better engaged
+
+ with their instruments, so they’ll keep coming back for more music products from your store.
+
+ Additionally, when your customers love these digital products and services and choose to buy
+
+ more, we pay you a share of all revenues we earn from these sales, boosting your revenue and
+
+ profit per sale, while simultaneously delivering greater value to your customer.
+
+
If you’re a music school, JamKazam represents a platform far superior to Skype that you can use
+
+ freely to teach your existing students online, both in your area and across the country to reach
+
+ new markets and students. Skype’s audio quality for music is very poor. Its latency is so high
+
+ that teacher and student cannot play together, and it suffers from a number of other critical
+
+ limitations for music lessons, as it was built for voice chat – not music. In addition to delivering
+
+ online music lessons that really work, JamKazam can drive more students to your school
+ and
+
+ your teachers from our online lesson marketplace, helping you build your business faster and
+
+ bigger.
+
+
And if you’re both a music store and school, we can help you win in all these ways together. If
+
+ this sounds interesting, read on to learn more about some of the unique things we’ve done and
+
+ how they can help to grow your business.
+
+
+
JamTracks Kudos
+
+
+
+
+
Andy Crowley of AndyGuitar
+
+
+
+
+
+
Ryan Jones of PianoKeyz
+
+
+
+
+
+
Carl Brown of GuitarLessions365
+
+
+
+
+
+
JamTracks – The Best Way To Play With Your Favorite Songs
+
+
+ JamTracks are full multitrack recordings of 4,000+ popular songs that deliver amazing fun and
+
+ flexibility for musicians to play with their favorite artists and tunes. With JamTracks, you can:
+
+
+
Listen to just a single isolated part to learn it
+
Mute the part you want to play, and play along with the rest of the band
+
Slow down playback to practice without changing the pitch
+
Change the song key by raising or lowering pitch in half steps
+
Save custom mixes for easy access, and export them to use anywhere
+
Make audio and video recordings to share via Facebook, YouTube, etc.
+
Play online live and in sync with others from different locations
+
Apply VST & AU audio plugin effects to your live performance
+
Use MIDI with VST & AU instruments for keys, electronic drums, etc.
+
And more…
+
+
+ JamTracks sell for $1.99 each. Musicians love to play with these, and typically buy a few at a
+
+ time. Imagine that you are selling a set of guitar strings to an electric guitar player. As a
+
+ JamKazam affiliate, you can bundle free JamTracks with the strings sale, delighting your
+
+ customer with the added value. And then when he/she buys more JamTracks, we pay you a
+
+ share of that revenue, adding to your revenues, and improving your margin on your sales. This
+
+ can be applied to just about anything you sell, as we have JamTracks with parts for almost every
+
+ instrument, as well as vocals. And it’s easy to bundle, as you don’t have to specify particular
+
+ songs per product that you sell. You can just give your customer a free JamTracks credit with
+
+ each sale, and your customer can then redeem this credit online to choose their favorite songs.
+
+
+
+ Here is a video that shows more about how JamTracks work.
+
+
+
+
+
+
+
+
+
JamClass Kudos
+
+
+
+
Dave Sebree
+
+ Founder of Austin School of Music, Gibson-endorsed guitarist, touring musician
+
+
+
+
+
+
Justin Pierce
+
+ Masters degree in jazz studies, performer in multiple bands, saxophone instructor
+
+
+
+
+
+
Julie Bonk
+
+ Oft-recorded pianist, teacher, mentor to Grammy winner Norah Jones and Scott Hoying of Pentatonix
+
+
+
+
+
+
+
JamClass – Online Music Lessons That Work
+
+
+ Where JamTracks are incredibly fun and versatile, JamClass is flat out amazing and mind
+
+ blowing. We’ve spent the last 3 years building technology specifically to let musicians play
+
+ together live in sync over the Internet from different locations with very high quality audio. And
+
+ now we are using this technology to enable online music lessons in a very powerful way.
+
+
+
+ For the last several years, instructors have been experimenting with using Skype to teach online
+
+ music lessons. But if you have tried this yourself or spoken with someone who has, you know
+
+ that audio quality is extremely poor, and latency is far too high for a teacher and student to
+
+ play together in lessons. The reason is that Skype was built for voice conferencing – i.e. for
+
+ people to talk with each other – not for music. Skype uses a voice “codec” that processes all
+
+ audio as if it were a human voice speaking, which crushes music audio. And voice conferencing
+
+ apps like Skype work fine with 150 to 200 milliseconds of latency (lag), whereas for music you
+
+ need latency far lower, in the 20 to 40 millisecond range.
+
+
+
+ In addition to audio quality and latency, JamKazam’s technology lets teacher and student
+
+ record lessons and performances for later reference, open backing tracks in sessions to play
+
+ with, use an in-session metronome, and other music-specific important features.
+
+
+
+ Here is a video that shows more about how JamKazam’s technology works for lessons.
+
+
+
+
+
+
+
+
+
How Does The Affiliate Program Work?
+
+
The affiliate program for music stores and schools is really simple. It’s free to sign up. Once
+
+ you’ve signed up, you are assigned a JamKazam affiliate ID, which tracks your affiliate activities
+
+ automatically. Today there are 3 ways to benefit from the affiliate program:
+
+
+
+
Bundle Free JamTracks and/or Lessons
+ If you bundle free JamTracks or free lessons with the products you sell, your customers
+
+ redeem these free offers from a web page that is co-branded with your store or school’s
+
+ logo. When they redeem the free offer, they are tagged as having come from your store
+
+ or school. For the next two years from this redemption date, we will pay you 10% of all
+
+ JamTracks revenues and 25% of all JamClass revenues earned by JamKazam that are generated from these referred customers. For purposes of illustration, if you own a store and refer a student
+
+ into our lesson marketplace who takes lessons at $30/lesson for a year with one of our
+
+ instructors, you would earn close to $100 in revenues just from that single referred
+
+ student.
+
+
+
Sign Up Teachers to Deliver Lessons
+ If you are a store or school that delivers music lessons yourself, you can have the
+
+ teachers in your school sign up to teach using the JamKazam platform. This is a smart
+
+ move to grow your online lesson business because the user experience is so superior to
+
+ Skype. If you use JamKazam to teach students who you have attracted through your
+
+ own marketing initiatives, it’s free to use our technology to deliver those lessons. You
+
+ can also opt to enable JamKazam to drive new students to your teachers through our
+
+ own network and marketing investments. If you opt in, JamKazam can help build your
+
+ business, and then we simply agree together how to split the revenue held back from
+
+ the lesson payment for delivering the student to the teacher.
+
+
+
Sign Up Students to Take Lessons
+ If you are a store or school that delivers music lessons, if you deliver online lessons to
+
+ your own students (not sourced from the JamKazam marketplace), then when those
+
+ students sign up to use JamKazam, we’ll also tag these students as having been referred
+
+ by you, so that if they choose to buy any other JamKazam products like JamTracks, we’ll
+
+ pay you a revenue share on these purchases.
+
+
+
+
+
+
What Do You Need To Offer JamClass Lessons From Your School?
+
+
+ In order to teach lessons using JamClass by JamKazam from your school facilities, in each room
+
+ where you want to be able to teach online lessons you will need the following:
+ To have very high quality audio in your sessions, rather than using the built-in microphone on
+
+ your computer to capture your instrumental and/or vocal audio, we strongly recommend using
+
+ an external audio interface. An audio interface is a hardware product that connects to your
+
+ computer and processes audio better than your computer alone. If you already own/use an
+
+ audio interface, you can use the one you have. And if you don't, please read this help article on audio interfaces that can guide you to get what you need. You
+ can pick up a perfectly good
+
+ interface very inexpensively, typically for less than $50. And you can use your new interface not
+
+ just for JamClass, but also to make recordings of performances, etc. So it's a great thing to have
+
+ for any music school in general.
+
+
+
+ Also, if possible, you’ll get the best audio quality and lowest latency if you can connect your
+
+ computer to your Internet service using an Ethernet cable rather than a WiFi connection. In any
+
+ application (including ours), WiFi is not great for low latency media streaming. If you have to
+
+ use WiFi, it will be OK for 1:1 lessons, and still far, far better than Skype. But you’ll get the best
+
+ performance if you can use a wired vs. wireless connection.
+
+
+
+ If you feel worried or confused about getting set up because you're not a "tech genius", we are
+
+ happy to work with you 1:1 to answer your questions, and walk you through picking gear and
+
+ setting it up. We'll even hop into an online test session with you, show you around the key
+
+ features, and make sure you're ready to rock and roll online! Just email us at
+
+ support@jamkazam.com, and tell us you need help getting set up for JamClass. We do this all
+
+ the time.
+
+
+
+ Here is a video that shows more about how JamClass works from a teacher's perspective:
+
+
+
+
+
+
+
+
+
+
What Now?
+
+
If you're ready to sign up to join our affiliate program, just scroll back up to the top of this page,
+
+ and enter your email address and a password to sign up. Once you've done this, we'll provide
+
+ instructions and engage with you to do the following:
+
+
Set up your music store or school as an affiliate, and execute the simple online affiliate
+
+ agreement.
+
+
Learn how to share URLs with your customers and students that help them redeem free
+
+ JamKazam products so that you are credited with the referrals.
+
+
Set up the gear you need, and participate in an online session with JamKazam staff to
+
+ ensure everything is working properly and that you are comfortable with how to use the
+
+ features of the service.
+
+
If your store/school teaches, sign up your teachers to use the service, and schedule a
+
+ teacher training class to get everyone comfortable.
+
+
+
+
+ Our JamKazam staff will give you all the 1:1 help you need to get your gear set up for online
+
+ lessons. Once you’ve started bundling free products and/or teaching, we’ll also need to set you
+
+ up for direct deposit so we can deposit funds as they are earned through the program.
+
+
+
+ All of us here at JamKazam look forward to partnering with you to help both you and your
+
+ customers and students succeed in both your musical and business endeavors!
+
+
+
`
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/landing/JamClassAffiliateLandingPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/JamClassAffiliateLandingPage.js.jsx.coffee
new file mode 100644
index 000000000..3bd61abf5
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/landing/JamClassAffiliateLandingPage.js.jsx.coffee
@@ -0,0 +1,140 @@
+context = window
+rest = context.JK.Rest()
+
+@JamClassAffiliateLandingPage = React.createClass({
+
+ render: () ->
+
+
+ loggedIn = context.JK.currentUserId?
+
+ if this.state.done
+ ctaButtonText = 'sending you in...'
+ else if this.state.processing
+ ctaButtonText = 'hold on...'
+ else
+ if loggedIn
+ ctaButtonText = "SIGN UP, IT'S FREE"
+ else
+ ctaButtonText = "SIGN UP, IT'S FREE"
+
+ if loggedIn
+ register = ``
+ else
+
+ if this.state.loginErrors?
+ for key, value of this.state.loginErrors
+ break
+
+ errorText = context.JK.getFullFirstError(key, this.state.loginErrors, {email: 'Email', password: 'Password', 'terms_of_service' : 'The terms of service'})
+
+ register = `
+
+ {errorText}
+
+
+
`
+
+
+ `
+
+
+
+
FOR MUSIC STORES & SCHOOLS
+
Do you own or operate a music store or school?
+
+
+
+
+
+
+ Sign Up as Affiliate
+
+
+
Sign up to enroll your store or school in our affiliate program.
+
We’ll follow up to answer your questions and give you all the 1:1 help you need to get up and running. It’s easy, and your customers will love the added value.
See for yourself how we can help you deliver greater value to your customers while increasing your revenues.
+
+
+
+
+
+ Founded by a team that has built and sold companies to Google, eBay, GameStop and more,
+
+ JamKazam has developed incredibly unique and engaging digital music technologies, content,
+
+ and marketplaces used by our rapidly growing community of {gon.global.musician_count} musicians. Now we’ve
+
+ crafted an affiliate program specifically for music stores and music schools to increase your
+
+ revenues while delighting your customers. Best of all, it’s free and easy to integrate into your
+
+ business.
+
+
+
`
+
+ getInitialState: () ->
+ {loginErrors: null, processing:false}
+
+ privacyPolicy: (e) ->
+ e.preventDefault()
+
+ context.JK.popExternalLink('/corp/privacy')
+
+ termsClicked: (e) ->
+ e.preventDefault()
+
+ context.JK.popExternalLink('/corp/terms')
+
+ componentDidMount:() ->
+ $root = $(this.getDOMNode())
+ $checkbox = $root.find('.terms-checkbox')
+ context.JK.checkbox($checkbox)
+
+ # add item to cart, create the user if necessary, and then place the order to get the free JamTrack.
+ ctaClick: (e) ->
+ e.preventDefault()
+
+ return if @state.processing
+
+ @setState({loginErrors: null})
+
+ loggedIn = context.JK.currentUserId?
+
+
+
+ createUser: () ->
+ $form = $('.jamtrack-signup-form')
+ email = $form.find('input[name="email"]').val()
+ password = $form.find('input[name="password"]').val()
+ terms = $form.find('input[name="terms"]').is(':checked')
+
+ rest.signup({email: email, password: password, first_name: null, last_name: null, terms:terms})
+ .done((response) =>
+
+
+ ).fail((jqXHR) =>
+ @setState({processing:false})
+ if jqXHR.status == 422
+ response = JSON.parse(jqXHR.responseText)
+ if response.errors
+ @setState({loginErrors: response.errors})
+ else
+ context.JK.app.notify({title: 'Unknown Signup Error', text: jqXHR.responseText})
+ else
+ context.JK.app.notifyServerError(jqXHR, "Unable to Sign Up")
+ )
+
+
+ @setState({processing:true})
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/landing/JamClassSchoolLandingBottomPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/JamClassSchoolLandingBottomPage.js.jsx.coffee
new file mode 100644
index 000000000..4876b2cc1
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/landing/JamClassSchoolLandingBottomPage.js.jsx.coffee
@@ -0,0 +1,431 @@
+context = window
+rest = context.JK.Rest()
+
+@JamClassSchoolLandingBottomPage = React.createClass({
+
+ render: () ->
+ `
+
+
How JamClass by JamKazam Can Help Your Music School
+
+
Online music lessons offer obvious advantages to your school. You can reach students who live
+
+ more than 30 minutes away – even students across the country. Teach during normal “off
+
+ hours”, when students in your area are tied up at school or at work. And students and parents
+
+ are increasingly interested in online lessons to avoid the time and hassle of traveling to/from
+
+ your school.
+
+
Several companies have built online marketplaces of music teachers, but these markets haven’t
+
+ offered a partnering model for existing schools. And even if they had, online music lessons
+
+ haven’t taken off like other online Internet markets. Why? Because every one of these markets
+
+ has relied on Skype or similar apps – built for voice chat – to deliver online music lessons. This is
+
+ a major problem. Voice technology makes music sound awful in online sessions – so bad that
+
+ teachers can’t assess the student’s tone and sometimes even the pitch of what they are
+
+ playing. These apps also have very high latency – a technical term that means that the student
+
+ and teacher cannot play together, another critical requirement for productive lessons. Since
+
+ Skype wasn’t built for music, it also lacks many other basic features to support effective lessons,
+
+ like a metronome, mixers, backing tracks, etc.
+
+
At JamKazam, we’ve spent years designing, patenting, and building technology specifically to
+
+ enable musicians to play online live in sync with studio quality audio. We’ve built a wide variety
+
+ of critical online music performance features into this platform. And now we’ve built a lesson
+
+ marketplace on top of this foundation to match students to teachers, and we’re investing in
+
+ marketing to drive students into the market.
+
+
+ The bottom line is that your school can now effectively offer and teach online lessons to
+
+ students using our incredible technology. If you secure students through your own marketing
+
+ initiatives, it’s free to use JamKazam. And if we bring new students to your school, we keep a
+
+ portion of the lesson income for these students, helping you to grow your business, increasing
+
+ your revenues and profitability.
+
If this sounds interesting to you, read on to learn more about some of the top features of
+
+ JamClass by JamKazam.
+
+
+
JamClass Kudos
+
+
+
+
+
Julie Bonk
+
+ Oft-recorded pianist, teacher, mentor to Grammy winner Norah Jones and Scott Hoying of Pentatonix
+
+
+
+
+
+
+
Carl Brown of GuitarLessions365
+
+
+
+
+
Justin Pierce
+
+ Masters degree in jazz studies, performer in multiple bands, saxophone instructor
+
+
+
+
+
+
Dave Sebree
+
+ Founder of Austin School of Music, Gibson-endorsed guitarist, touring musician
+
+
+
+
+
+
Sara Nelson
+
+ Cellist for Austin Lyric Opera, frequently recorded with major artists
+
+
+
+
+
+
+
+
+
1
+ Play Live In Sync From Different Locations
+
+
+
+
+
+
+
+
Teacher and student need to be able to play together to enable effective lessons. As any teacher who has
+ attempted to teach using Skype will tell you, Skype doesn't let you play together. JamKazam's patented
+ technologies deliver on this requirement at an amazing level. Click the video above to watch 6 bands play
+ together from different locations to see our tech in action. And for an even more impressive feat, watch this video with a band
+ playing together from Austin, Atlanta, Chicago, and Brooklyn using JamKazam tech.
These audio links will open a new tab in your browser. When done listening,
+ close the tab and return to this page.
+
+
Skype was built for voice - for people talking with each other. It uses something called a "voice codec".
+ This just means it processes all audio as a spoken human voice, and the result is that music, whether
+ instrumental or vocal, sounds very bad in Skype, as it has been processed through tech built for talking.
+ JamKazam delivers very high quality audio. You will be amazed at how good it sounds. It sounds like you're
+ sitting next to each other playing. This is also critical for a good lesson. Poor audio is hard to endure
+ in lessons.
+
+
+
+
+
+
+
+
+
+
3
+ Get Student Referrals from the Marketplace
+
+
+
+
You can use JamKazam's superior applications and services to conduct better online music
+
+ lessons with your existing students, and to attract new students through your own marketing
+
+ efforts. In addition, even schools are challenged to invest the resources to attract new students.
+
+ Our JamClass marketplace adds value to your business in this area as well, as JamKazam invests
+
+ every month to drive new students into our marketplace, and delivers these qualified students
+
+ to you, set up and ready to go for online lessons. This is explained in more detail later on this
+
+ page.
+
+
+
+
+
+
+
+
+
+
4
+ Record Lessons & Student Performances
+
+
+
+
+
+
+
watch this sample video recording from a lesson
+
+
Many times a student thinks they've got it during a lesson, but they get home and realize "I don't got it", and then they've wasted a week. In JamClass the instructor can record all or portions of a lesson that the student can easily refer back to later. You can also have students record their performances, and you can review them together.
+
+
+
+
+
+
+
+
+
+
5
+ Use JamTracks to Motivate Students
+
+
+
+
+
+
+
+
Teachers usually apply the techniques taught in lessons to playing songs - ideally songs your student loves. JamKazam makes this better too, as we offer a catalog of 3,700+ songs. Each song is is a complete multi-track recording, with fully isolated tracks for each part of the music - e.g. lead vocal, backing vocals, lead guitar, rhythm guitar, keys, bass, drums, etc. So your student can listen to just the part they're learning in isolation, turn around and mute that one part to play along with the rest of the band, slow down playback for practice, record and share your performances, and more. It's really fun! And a great way to keep your students movitated and engaged.
+
+
+
+
+
+
+
+
+
+
6
+ Broadcast Recitals
+
+
+
+
+
Recitals are an important tool for teachers, but recitals are lost using Skype. With JamClass, you can live broadcast video and audio of student recitals through YouTube. This enables other students, family members, and friends to "tune in" for recital performances. And when students share the link to their recitals with their friends, it can also serve as great exposure for your lessons, attracting friends of your existing students as new students.
+
+
+
+
+
+
+
+
+
+
7
+ Apply VST & AU Audio Plug-In Effects
+
+
+
+
+
The free JamKazam app lets you easily apply VST & AU plugin effects to your live performance in lessons.
+ For example, guitarists can apply popular amp sims like AmpliTube to get any kind of guitar tone without
+ pedal boards or amps, and vocalists can apply effects like reverb, pitch correction, etc.
+
+
+
+
+
+
+
+
+
+
8
+ Use MIDI Instruments
+
+
+
+
+
The free JamKazam app also lets you use MIDI instruments in online lesson sessions. For example, keys
+ players can use MIDI keyboard controllers with VST & AU plugins to generate traditional piano sounds,
+ Rhodes electric piano, Hammond organ, and other classic keys tones. And drummers who use electronic kits
+ can use their favorite plugins to power their percussive audio.
+
+
+
+
+
+
+
+
+
+
9
+ And So Much More...
+
+
+
There are many other features that are specifically useful for online lessons built into JamClass by JamKazam, including a metronome feature, the ability for either teacher or student to open any audio file and use it as a backing track for session acccompaniment, and too many more to list.
+
+
In addition to the lesson features, an awesome bonus is that once your students are set up to
+
+ play with your teachers in online lessons, they can also play completely FREE with anyone else
+
+ in the JamKazam community any time to use the skills they’re learning in lessons to play with
+
+ others, which again reinforces and motivates students to stay engaged, as it’s more fun to play
+
+ with others than alone. If you teach ensembles and rock bands, your students can practice in
+
+ groups between lessons without having to find rehearsal space, pack gear, and travel. Plus
+
+ there are thousands of online sessions played every month on the JamKazam service, including
+
+ open jam sessions set up by our user community, and students can hop into these sessions,
+
+ create their own improptu sessions, etc. It's a vibrant and welcoming community of fellow
+
+ musicians.
+
+
+
+
+
+
+
+
What Does Your School Need to Host JamClass Lessons?
+ To have very high quality audio in your sessions, rather than using the built-in microphone on your computer
+ to capture your instrumental and/or vocal audio, we recommend using an external audio interface. An
+ audio interface is a hardware product that connects to your computer and processes audio better than your
+ computer alone. If you already own/use an audio interface, you can use the one you have. And if you don't,
+ please refer to this set of help articles that recommend the best gear based on your instruments and/or vocals. You can pick up a perfectly good interface very inexpensively, typically for less than $50. And you can
+ use your new interface not just for JamClass, but also to make home recordings of your performances, and also to play in online JamKazam sessions with other musicians. So
+ it's a great thing to have for any musician.
+
+
+
+ If you feel worried or confused about getting set up because you're not a "tech genius", we are happy to work
+ with you 1:1 to answer your questions, and walk you through picking gear and setting it up.
+ We'll even hop into an online test session with you and your school’s teachers, show you
+ around the key features, and make sure all your teachers are ready to rock and roll online!
+
+
+
+
How Do the Business Aspects of JamClass Work?
+
+
You can use JamClass by JamKazam to teach your own existing students if they'd like to take
+
+ online lessons, and it’s free to use in this way for schools, much like Skype.
+
+
If you would like JamKazam to bring new students to your school through the JamClass
+
+ marketplace, we will be making substantial marketing investments in attracting, equipping, and
+
+ delivering these students to you. JamKazam will bill and collect payments directly from these
+
+ referred students. You and your teachers set your prices for lessons just as you do today. We
+
+ retain a minority percentage of the lesson revenue from these referred students, and we will
+
+ transfer the majority balance of these lesson revenues to your school. The school is then
+
+ responsible to distribute lesson payments to its teachers according to the school’s agreements
+
+ with its teachers, whatever those may be. Also, as the school operator, you may choose to have
+
+ all lesson booking requests from the JamClass marketplace come directly to you as the school
+
+ administrator, so that you handle scheduling and booking for all these online lessons, just as
+
+ you already do for lessons on premise at your school. Once you have set the date/time for
+
+ these lessons, they will then appear on each teacher’s JamClass dashboard. So when using
+
+ JamClass you, as the school owner, may continue to manage scheduling and distribution of
+
+ earnings to your school’s teachers.
+
+
Finally, please note that to participate in the JamClass marketplace, each teacher will need to
+
+ opt in to participate in our TestDrive program. TestDrive is a core component of our JamClass
+
+ marketing programs, providing students interested in taking online lessons with discounted
+
+ introductory packages to get started. To participate in the marketplace, each teacher must be
+
+ willing to teach at least 2 TestDrive lessons per week. Your school is paid $10 for each 30-
+
+ minute TestDrive lesson, and many TestDrive students will become long-term students who pay
+
+ your normal rates. Teachers may opt to accept more than 2 TestDrive lessons per week if they
+
+ would like to grow their student base more rapidly.
+
+
+
+
What Now?
+
+
+ If you're ready to sign up your school, or you think this might be good for your school but are
+
+ not sure yet, scroll back up to the top of this page, and enter your email address and a
+
+ password to sign up. Once you've done this, we'll reach out to you to answer any and all
+
+ questions you have. If you find you want to move forward, we’ll work with you directly to help
+
+ you get your school ready to go.
+
+
+
`
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/landing/JamClassSchoolLandingPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/JamClassSchoolLandingPage.js.jsx.coffee
new file mode 100644
index 000000000..fffdf3dda
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/landing/JamClassSchoolLandingPage.js.jsx.coffee
@@ -0,0 +1,162 @@
+context = window
+rest = context.JK.Rest()
+
+@JamClassSchoolLandingPage = React.createClass({
+
+ render: () ->
+
+
+ loggedIn = context.JK.currentUserId?
+
+ if this.state.done
+ ctaButtonText = 'sending you in...'
+ else if this.state.processing
+ ctaButtonText = 'hold on...'
+ else
+ if loggedIn
+ ctaButtonText = "SIGN UP"
+ else
+ ctaButtonText = "SIGN UP"
+
+ if loggedIn
+ register = ``
+ else
+
+ if this.state.loginErrors?
+ for key, value of this.state.loginErrors
+ break
+
+ errorText = context.JK.getFullFirstError(key, this.state.loginErrors, {email: 'Email', password: 'Password', 'terms_of_service' : 'The terms of service'})
+
+ register = `
+
+ {errorText}
+
+
+
`
+
+
+ `
+
+
+
+
GROW YOUR SCHOOL’S REACH & INCOME
+
Do you own/operate a music school?
+
+
+
+
+
+
+ Sign Up Your School
+
+
+
Sign up to let us know you’re interested in partnering, and we’ll follow up to answer your
+
+ questions.
+
If this is a good fit for your school, we’ll give you all the 1:1 help you need to get your school
+
+ and staff up and running.
Learn how we can help you greatly extend your reach to new markets while increasing your
+
+ revenues.
+
+
+
+
+
+ Founded by a team that has built and sold companies to Google, eBay, GameStop and more,
+
+ JamKazam has developed incredibly unique technology that lets musicians play together live in
+
+ sync with studio quality audio from different locations over the Internet. Now JamKazam has
+
+ launched an online music lesson marketplace, and we’ve set up a program specifically to
+
+ partner with music schools to help you attract and engage students across the country,
+
+ extending your school’s reach and generating more income.
+
+
+
`
+
+ getInitialState: () ->
+ {loginErrors: null, processing:false}
+
+ privacyPolicy: (e) ->
+ e.preventDefault()
+
+ context.JK.popExternalLink('/corp/privacy')
+
+ termsClicked: (e) ->
+ e.preventDefault()
+
+ context.JK.popExternalLink('/corp/terms')
+
+ componentDidMount:() ->
+ $root = $(this.getDOMNode())
+ $checkbox = $root.find('.terms-checkbox')
+ context.JK.checkbox($checkbox)
+
+ # add item to cart, create the user if necessary, and then place the order to get the free JamTrack.
+ ctaClick: (e) ->
+ e.preventDefault()
+
+ return if @state.processing
+
+ @setState({loginErrors: null})
+
+ loggedIn = context.JK.currentUserId?
+
+ if loggedIn
+ @markTeacher()
+ else
+ @createUser()
+
+ @setState({processing:true})
+
+
+ markTeacher: () ->
+ rest.updateUser({school_interest: true})
+ .done((response) =>
+ this.setState({done: true})
+ context.location = '/client#/home'
+ )
+ .fail((jqXHR) =>
+ this.setState({processing: false})
+ context.JK.app.notifyServerError(jqXHR, "Unable to Mark As Interested in School")
+ )
+
+ createUser: () ->
+ $form = $('.jamtrack-signup-form')
+ email = $form.find('input[name="email"]').val()
+ password = $form.find('input[name="password"]').val()
+ terms = $form.find('input[name="terms"]').is(':checked')
+
+ rest.signup({email: email, password: password, first_name: null, last_name: null, terms:terms, school_interest: true})
+ .done((response) =>
+ context.location = '/client#/home'
+ ).fail((jqXHR) =>
+ @setState({processing:false})
+ if jqXHR.status == 422
+ response = JSON.parse(jqXHR.responseText)
+ if response.errors
+ @setState({loginErrors: response.errors})
+ else
+ context.JK.app.notify({title: 'Unknown Signup Error', text: jqXHR.responseText})
+ else
+ context.JK.app.notifyServerError(jqXHR, "Unable to Sign Up")
+ )
+
+
+ @setState({processing:true})
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/landing/JamClassStudentLandingBottomPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/JamClassStudentLandingBottomPage.js.jsx.coffee
new file mode 100644
index 000000000..0928947a9
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/landing/JamClassStudentLandingBottomPage.js.jsx.coffee
@@ -0,0 +1,407 @@
+context = window
+rest = context.JK.Rest()
+
+@JamClassStudentLandingBottomPage = React.createClass({
+
+ render: () ->
+ if this.props.free
+ find_teacher_header = "Find the Best Teacher For You - Free"
+ find_teacher_text = "With JamClass, you can search our community of outstanding instructors, find the one who is the best fit for you - regardless of where they live - and then use our free lesson offer to take your first lesson with this instructor at no cost to make sure you click. It's free, easy, and kind of amazing!"
+ what_now =
+ `
+
What Now?
+
+
If you're ready to sign up for your free lesson, just scroll back up to the top of this page, and sign up.
+ Once you've done this, there are three more things to do:
+
+
Search for the instructor(s) who look best for you, and book your lesson with him or her.
+
Enter your credit card information. You will not be charged unless you choose to book more
+ lessons after the first free one. We have to collect credit card info to avoid fraud. Unfortunately we've
+ found that some folks will keep signing up with new accounts to keep getting free lessons.
+
+
Work with a JamKazam staff person, who will give you all the 1:1 help you need to get set up and ready
+ for your online lesson.
+
+
+
While you're getting this done, if you want to learn more about all the nifty features you can access in
+ JamClass and in JamKazam in general, you can check out our online JamClass
+ User Guide.
+
`
+ else
+ find_teacher_header = "TestDrive to Find the Right Teacher"
+ test_drive_point = "You often settle on the first teacher you try as it's too hard and expensive to engage multiple teachers."
+ find_teacher_text = "And whether traditional or online, students often just settle on the first teacher they try, as it's hard and expensive to \"try out\" multiple teachers. Our unique TestDrive program lets you try lessons with 4 different teachers for just $49.99. Then you pick the one with whom you find that you work best. It's inexpensive, easy, and no one gets their feelings hurt."
+ what_now =
+ `
+
What Now?
+
+
If you're ready to sign up for TestDrive, just scroll back up to the top of this page, and sign up. Once
+ you've done this, there are three more things to do:
+
+
Search for the instructor(s) who look best for you, and book your lesson with him or her.
+
Plug in your credit card info to pay for your TestDrive
+
Work with a JamKazam staff person, who will give you all the 1:1 help you need to get set up for online
+ lessons.
+
+
+
While you're getting this done, if you want to learn more about all the nifty features you can access in
+ JamClass and in JamKazam in general, you can check out our online JamClass
+ User Guide.
+
`
+ `
+
+
What Makes JamClass Lessons Awesome?
+
+
Online music lessons offer obvious advantages. Connecting with the right teacher is the most important factor
+ in the value of lessons, yet with traditional lessons, you have to settle for a teacher who lives close to you
+ rather than selecting the best teacher. {test_drive_point} Travel to and from lessons takes far more time than
+ the lessons
+ themselves. You often forget important teachings and demonstrations between lessons. The list goes on, as
+ traditional lessons have many drawbacks.
+
+
Historically, online lessons have had major issues too. Several companies have built online marketplaces of
+ music teachers, but every one of these companies relies on Skype or similar apps – built for voice
+ conferencing – to deliver online lessons. This is a major problem. The voice technology of these apps makes
+ music sound awful in online sessions – so bad that teachers can’t assess the student’s tone and sometimes even
+ the pitch of what they are playing. These apps also
+ have very high latency – a technical term that means that the student and teacher cannot play together,
+ another critical requirement for productive lessons. Since Skype wasn’t built for music, it also lacks many
+ other basic features to support effective lessons, like a metronome, support for backing tracks, etc.
+
+
At JamKazam, we’ve spent years designing, patenting, and building technology specifically to enable musicians
+ to play online live in sync with studio quality audio. We’ve built a wide variety of critical online music
+ performance features into this platform. And we’ve built a lesson marketplace on top of this foundation, and
+ attracted and vetted a terrific set of instructors to teach using our unparalleled technology platform.
+ Following are some of the top features of JamClass by JamKazam.
+
+
+
Testimonials
+
+
+
+
+
Julie Bonk
+
+
+ Oft-recorded pianist, teacher, mentor to Grammy winner Norah Jones and Scott Hoying of Pentatonix
+
+
+
+
+
+
+
Carl Brown of GuitarLessions365
+
+
+
+
+
Justin Pierce
+
+
+ Masters degree in jazz studies, performer in multiple bands, saxophone instructor
+
+
+
+
+
+
Dave Sebree
+
+
+ Founder of Austin School of Music, Gibson-endorsed guitarist, touring musician
+
+
+
+
+
+
Sara Nelson
+
+
+ Cellist for Austin Lyric Opera, frequently recorded with major artists
+
+
+
+
+
+
+
+
+
1
+ Play Live In Sync From Different Locations
+
+
+
+
+
+
+
+
Teacher and student need to be able to play together to enable effective lessons. As any teacher who has
+ attempted to teach using Skype will tell you, Skype doesn't let you play together. JamKazam's patented
+ technologies deliver on this requirement at an amazing level. Click the video above to watch 6 bands play
+ together from different locations to see our tech in action. And for an even more impressive feat, watch this video with a band
+ playing together from Austin, Atlanta, Chicago, and Brooklyn using JamKazam tech.
These audio links will open a new tab in your browser. When done listening,
+ close the tab and return to this page.
+
+
Skype was built for voice - for people talking with each other. It uses something called a "voice codec".
+ This just means it processes all audio as a spoken human voice, and the result is that music, whether
+ instrumental or vocal, sounds very bad in Skype, as it has been processed through tech built for talking.
+ JamKazam delivers very high quality audio. You will be amazed at how good it sounds. It sounds like you're
+ sitting next to each other playing. This is also critical for a good lesson. Poor audio is hard to endure
+ in lessons.
+
+
+
+
+
+
+
+
+
+
3
+ {find_teacher_header}
+
+
+
+
+
+
+
+
+
+
+
+
+
Try more than one instructor. You wouldn’t marry the first person you date, right?
+
+
+
Connecting with the right teacher is the most important factor in the effectiveness of lessons. But with
+ traditional lessons, you are stuck choosing a teacher within a 30-minute drive - choosing the closest vs.
+ the best. Plus students often just settle on the first teacher they try, as it's hard and expensive to try
+ out multiple teachers. Our unique TestDrive program lets you try full lessons with multiple teachers at a
+ discounted rate to find the best teacher for you. It's inexpensive, easy, and it makes a world of
+ difference in the value of your investment in lessons.
+
+
+
+
+
+
+
+
+
+
4
+ Record & Refer Back to Lessons
+
+
+
+
+
+
+
watch this sample video recording from a lesson
+
+
Students rarely take notes during lessons, and often notes won't really capture what's being taught. Many
+ times a student thinks they've got it, but they get home and realize "I don't got it", and then you've
+ wasted a week. In JamClass the instructor can record all or portions of a lesson that the student can
+ easily refer back to later.
+
+
+
+
+
+
+
+
+
+
5
+ Play Along with Your Favorite Songs/Artists
+
+
+
+
+
+
+
+
When taking lessons, you usually apply the techniques you are learning to playing songs - ideally songs
+ you love. JamKazam makes this better too, as we offer a catalog of 3,700+ songs. Each song is is a
+ complete multi-track recording, with fully isolated tracks for each part of the music - e.g. lead vocal,
+ backing vocals, lead guitar, rhythm guitar, keys, bass, drums, etc. So you can listen to just the part
+ you're learning in isolation, turn around and mute that one part to play along with the rest of the band,
+ slow down playback for practice, record and share your performances, and more. It's really fun! And a
+ great way to reward yourself and reinforce what you're learning from your lessons.
+
+
+
+
+
+
+
+
+
+
6
+ Apply VST & AU Audio Plug-In Effects
+
+
+
+
+
The free JamKazam app lets you easily apply VST & AU plugin effects to your live performance in lessons.
+ For example, guitarists can apply popular amp sims like AmpliTube to get any kind of guitar tone without
+ pedal boards or amps, and vocalists can apply effects like reverb, pitch correction, etc.
+
+
+
+
+
+
+
+
+
+
7
+ Use MIDI Instruments
+
+
+
+
+
The free JamKazam app also lets you use MIDI instruments in online lesson sessions. For example, keys
+ players can use MIDI keyboard controllers with VST & AU plugins to generate traditional piano sounds,
+ Rhodes electric piano, Hammond organ, and other classic keys tones. And drummers who use electronic kits
+ can use their favorite plugins to power their percussive audio.
+
+
+
+
+
+
+
+
+
+
8
+ And More…
+
+
+
There are many other features that are specifically useful for online lessons built into JamClass by
+ JamKazam, including a metronome feature, the ability for either teacher or student to open any audio file
+ and use it as a backing track for session acccompaniment, and too many more to list.
+
+
In addition to the lesson features, the awesome bonus is that once a student is set up to play in online
+ lessons, he or she can also play completely FREE with anyone else in the JamKazam community any time to
+ practice skills learned in lessons, or just for fun. There are thousands of online sessions played every
+ month on the JamKazam service, including open jam sessions set up by our user community, and you can hop
+ into these sessions, create your own improptu sessions, schedule sessions with a particular focus or song
+ list, etc. It's a vibrant and welcoming community of fellow musicians, and it's open to you anytime,
+ anywhere.
+ To have very high quality audio in your sessions, rather than using the built-in microphone on your computer
+ to capture your instrumental and/or vocal audio, we recommend using an external audio interface. An
+ audio interface is a hardware product that connects to your computer and processes audio better than your
+ computer alone. If you already own/use an audio interface, you can use the one you have. And if you don't,
+ please refer to this set of help articles that recommend the best gear based on your instruments and/or vocals. You can pick up a perfectly good interface very inexpensively, typically for less than $50. And you can
+ use your new interface not just for JamClass, but also to make home recordings of your performances, and also to play in online JamKazam sessions with other musicians. So
+ it's a great thing to have for any musician.
+
+
+ If you feel worried or confused about getting set up because you're not a "tech genius", we are happy to work
+ with you 1:1 to answer your questions, and walk you through picking gear and setting it up. We'll even hop
+ into an online test session with you, show you around the key features, and make sure you're ready to rock and
+ roll online! Just email us at support@jamkazam.com, and tell us you need help getting set up for JamClass. We
+ do this all the time.
+
+
+
+
We Have Fabulous Teachers, Check Them Out!
+
+
JamClass instructors are a terrific set of teachers from whom you can learn almost anything musically. We review every single teacher individually to
+ ensure they meet high standards in terms of their ability, experience, and desire to teach music.
+
+
If you want to check out teachers before signing up, use the link below to search our community of teachers.
+ If you are looking for someone/something specific that you can't find in our set of instructors, please let us
+ know and our lesson concierge staff will find and add the right teachers. We are constantly reviewing prospective teachers
+ and adding to the community, and we don't mind at all using specific student requests to focus our efforts on
+ filling holes in our coverage.
Please note that clicking this link will open a new tab in your browser.
+ When you're done searching, just close that tab, and you'll return here.
+
+
+
+
+
+
Guidance for Parents of Music Students
+
+
If you are the parent of a child who is under the age of 13, you must give consent for your child to use this
+ service, and this must be done in a specific manner, due to the provisions of the Children's Online Privacy
+ Protection Act (COPPA). If this applies to your child, please email us at support@jamkazam.com so that we can handle this process with you
+ prior to signup, thank you!
+
+
+ {what_now}
+
`
+
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/landing/JamClassStudentLandingMiddlePage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/JamClassStudentLandingMiddlePage.js.jsx.coffee
new file mode 100644
index 000000000..7c9779226
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/landing/JamClassStudentLandingMiddlePage.js.jsx.coffee
@@ -0,0 +1,150 @@
+@JamClassStudentLandingMiddlePage = React.createClass({
+
+ avatar: (name = 'your choice', photo_url = '/assets/shared/avatar_generic.png') ->
+ `
The single most important factor in the success of your music lessons is your teacher. You wouldn't marry
+ the first person you date, right?
+
+
Take full 30-minute lessons from each of these 4 amazing teachers for just $12.50 each - a total of
+ $49.99. Then you can pick the one who is best for you to continue your musical journey, with the
+ confidence that your investment in lessons will deliver maximum growth!
+
`
+
+ options =
+ `
+
Like the TestDrive concept, but 4 teachers is too many for you?
The single most important factor in the success of your music lessons is your teacher. You wouldn't marry
+ the first person you date, right?
+
+
Take full 30-minute lessons from each of these 2 amazing teachers for just $14.99 each - a total of
+ $29.99. Then you can pick the one who is best for you to continue your musical journey, with the
+ confidence that your investment in lessons will deliver maximum growth!
+
`
+
+ options =
+ `
+
Like the TestDrive concept, but prefer not to try 2 different teachers?
Take a full 30-minute lesson from this great teacher for just $14.99 - half the cost of a typical music
+ lesson. You can make sure everything is working great, and then continue your musical journey with the
+ confidence that your investment in lessons will deliver maximum growth!
+
`
+ options =
+ `
+
You can book a TestDrive lesson with this awesome teacher now!
The single most important factor in the success of your music lessons is your teacher. You wouldn't marry the
+ first person you date, right? Our TestDrive program lets you:
+
+
Take a full lesson from 4 different teachers for just $12.49 each, or
+
Take a full lesson from 2 different teachers for just $14.99 each, or
+
Take a full lesson from 1 teacher for just $14.99
+
+
+
Then continue your lessons with the best teacher for you!
+
+
Join 40,000+ other musicians in the JamKazam community. Sign up for TestDrive today, and
+ you'll be eligible for any of the three special offers above!
+
+
Not sure if this is for you? Scroll down to learn more...
+
`
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/landing/JamClassStudentLandingPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/JamClassStudentLandingPage.js.jsx.coffee
new file mode 100644
index 000000000..602836074
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/landing/JamClassStudentLandingPage.js.jsx.coffee
@@ -0,0 +1,222 @@
+context = window
+rest = context.JK.Rest()
+
+@JamClassStudentLandingPage = React.createClass({
+
+ render: () ->
+ loggedIn = context.JK.currentUserId?
+
+ if this.state.done
+ ctaButtonText = 'sending you in...'
+ else if this.state.processing
+ ctaButtonText = 'hold on...'
+ else
+ if loggedIn
+ ctaButtonText = 'TRY TESTDRIVE'
+ else
+ ctaButtonText = 'SIGN UP'
+
+ if loggedIn
+ register = ``
+ else
+ if this.state.loginErrors?
+ for key, value of this.state.loginErrors
+ break
+
+ errorText = context.JK.getFullFirstError(key, this.state.loginErrors,
+ {email: 'Email', password: 'Password', 'terms_of_service': 'The terms of service'})
+
+ register = `
+
+ {errorText}
+
+
+
`
+ if @props.package?
+ ctaBoxContents = `
+
Sign up for this amazing TestDrive offer now!
+
+
When you sign up below, we will ask you to pay for your TestDrive package, and then we'll forward
+ your lesson requests to these teachers for scheduling.
We'll give you 1:1 help to get set up and ready to go with our free app.
+
`
+ else
+
+ ctaBoxContents = `
+
Sign up now. You have no obligation to buy anything. Signing up makes you eligible for our TestDrive
+ offers.
+
+
After signing up, you can search our community of world-class instructors. If you book a TestDrive lesson
+ you can choose to TestDrive 4, 2, or 1 teachers at that time.
Online music lessons offer obvious advantages. You can teach students who live more than 30 minutes away – even students across the country. Teach during normal “off hours”, when students in your area are tied up at school or at work. Avoid the time and cost of travel to and from lessons. And our online lesson marketplace can bring students to you, helping you grow your income without requiring the time, effort, and cash needed to attract new students.
+
+
Several companies have built online marketplaces of music teachers, but these markets – and online lessons in general – haven’t taken off like other online Internet markets. Why? Because every one of these companies relies on Skype or similar apps – built for voice conferencing – to deliver online music lessons. This is a major problem. Voice technology makes music sound awful in online sessions – so bad that teachers can’t assess the student’s tone and sometimes even the pitch of what they are playing, and so bad that it steals away the joy of playing music. These apps also have very high latency – a technical term that means that the student and teacher cannot play together, another critical requirement for productive lessons. Since Skype wasn’t built for music, it also lacks many other basic features to support effective lessons, like a metronome, support for backing tracks, etc.
+
+
At JamKazam, we’ve spent years designing, patenting, and building technology specifically to enable musicians to play online live in sync with studio quality audio. We’ve built a wide variety of critical online music performance features into this platform. We’ve built a lesson marketplace on top of this foundation to match students to teachers, and now we are investing in marketing to drive students into the market.
+
+
If this sounds interesting to you, read on to learn more about some of the top features of JamClass by JamKazam.
+
+
+
JamClass Kudos
+
+
+
+
+
Julie Bonk
+
+ Oft-recorded pianist, teacher, mentor to Grammy winner Norah Jones and Scott Hoying of Pentatonix
+
+
+
+
+
+
+
Carl Brown of GuitarLessions365
+
+
+
+
+
Justin Pierce
+
+ Masters degree in jazz studies, performer in multiple bands, saxophone instructor
+
+
+
+
+
+
Dave Sebree
+
+ Founder of Austin School of Music, Gibson-endorsed guitarist, touring musician
+
+
+
+
+
+
Sara Nelson
+
+ Cellist for Austin Lyric Opera, frequently recorded with major artists
+
+
+
+
+
+
+
+
+
1
+ Play Live In Sync From Different Locations
+
+
+
+
+
+
+
+
Teacher and student need to be able to play together to enable effective lessons. As any teacher who has
+ attempted to teach using Skype will tell you, Skype doesn't let you play together. JamKazam's patented
+ technologies deliver on this requirement at an amazing level. Click the video above to watch 6 bands play
+ together from different locations to see our tech in action. And for an even more impressive feat, watch this video with a band
+ playing together from Austin, Atlanta, Chicago, and Brooklyn using JamKazam tech.
These audio links will open a new tab in your browser. When done listening,
+ close the tab and return to this page.
+
+
Skype was built for voice - for people talking with each other. It uses something called a "voice codec".
+ This just means it processes all audio as a spoken human voice, and the result is that music, whether
+ instrumental or vocal, sounds very bad in Skype, as it has been processed through tech built for talking.
+ JamKazam delivers very high quality audio. You will be amazed at how good it sounds. It sounds like you're
+ sitting next to each other playing. This is also critical for a good lesson. Poor audio is hard to endure
+ in lessons.
+
+
+
+
+
+
+
+
+
+
3
+ Get Student Referrals from the Marketplace
+
+
+
+
You can use JamKazam's superior applications and services to conduct better online music lessons with your existing students, and to attract new students through your own marketing efforts. In addition, most teachers find it challenging to attract new students, as it requires significant time, effort, and cash. JamClass adds value to your business in this area as well, as JamKazam invests every month to drive new students into our marketplace, and delivers these qualified students to you, set up and ready to go for online lessons. This is explained in more detail later on this page.
+
+
+
+
+
+
+
+
+
+
4
+ Record Lessons & Student Performances
+
+
+
+
+
+
+
watch this sample video recording from a lesson
+
+
Many times a student thinks they've got it during a lesson, but they get home and realize "I don't got it", and then they've wasted a week. In JamClass the instructor can record all or portions of a lesson that the student can easily refer back to later. You can also have students record their performances, and you can review them together.
+
+
+
+
+
+
+
+
+
+
5
+ Use JamTracks to Motivate Students
+
+
+
+
+
+
+
+
Teachers usually apply the techniques taught in lessons to playing songs - ideally songs your student loves. JamKazam makes this better too, as we offer a catalog of 3,700+ songs. Each song is is a complete multi-track recording, with fully isolated tracks for each part of the music - e.g. lead vocal, backing vocals, lead guitar, rhythm guitar, keys, bass, drums, etc. So your student can listen to just the part they're learning in isolation, turn around and mute that one part to play along with the rest of the band, slow down playback for practice, record and share your performances, and more. It's really fun! And a great way to keep your students movitated and engaged.
+
+
+
+
+
+
+
+
+
+
6
+ Broadcast Recitals
+
+
+
+
+
Recitals are an important tool for teachers, but recitals are lost using Skype. With JamClass, you can live broadcast video and audio of student recitals through YouTube. This enables other students, family members, and friends to "tune in" for recital performances. And when students share the link to their recitals with their friends, it can also serve as great exposure for your lessons, attracting friends of your existing students as new students.
+
+
+
+
+
+
+
+
+
+
7
+ Apply VST & AU Audio Plug-In Effects
+
+
+
+
+
The free JamKazam app lets you easily apply VST & AU plugin effects to your live performance in lessons.
+ For example, guitarists can apply popular amp sims like AmpliTube to get any kind of guitar tone without
+ pedal boards or amps, and vocalists can apply effects like reverb, pitch correction, etc.
+
+
+
+
+
+
+
+
+
+
8
+ Use MIDI Instruments
+
+
+
+
+
The free JamKazam app also lets you use MIDI instruments in online lesson sessions. For example, keys
+ players can use MIDI keyboard controllers with VST & AU plugins to generate traditional piano sounds,
+ Rhodes electric piano, Hammond organ, and other classic keys tones. And drummers who use electronic kits
+ can use their favorite plugins to power their percussive audio.
+
+
+
+
+
+
+
+
+
+
9
+ And So Much More...
+
+
+
There are many other features that are specifically useful for online lessons built into JamClass by JamKazam, including a metronome feature, the ability for either teacher or student to open any audio file and use it as a backing track for session acccompaniment, and too many more to list.
+
+
In addition to the lesson features, the awesome bonus is that once you are set up as a teacher to play in online lessons, you can also play completely FREE with anyone else in the JamKazam community any time to gain exposure to prospective students, meet other musicians, or just to enjoy playing music with others, without having to pack up gear and find rehearsal space. There are thousands of online sessions played every month on the JamKazam service, including open jam sessions set up by our user community, and you can hop into these sessions, create your own improptu sessions, schedule sessions with a particular focus or song list, etc. It's a vibrant and welcoming community of fellow musicians, and it's open to you anytime, anywhere.
+ To have very high quality audio in your sessions, rather than using the built-in microphone on your computer
+ to capture your instrumental and/or vocal audio, we recommend using an external audio interface. An
+ audio interface is a hardware product that connects to your computer and processes audio better than your
+ computer alone. If you already own/use an audio interface, you can use the one you have. And if you don't,
+ please refer to this set of help articles that recommend the best gear based on your instruments and/or vocals. You can pick up a perfectly good interface very inexpensively, typically for less than $50. And you can
+ use your new interface not just for JamClass, but also to make home recordings of your performances, and also to play in online JamKazam sessions with other musicians. So
+ it's a great thing to have for any musician.
+
+
+
+ If you feel worried or confused about getting set up because you're not a "tech genius", we are happy to work
+ with you 1:1 to answer your questions, and walk you through picking gear and setting it up. We'll even hop
+ into an online test session with you, show you around the key features, and make sure you're ready to rock and
+ roll online! Just email us at support@jamkazam.com, and tell us you need help getting set up for JamClass. We
+ do this all the time.
+
+
+
+
How Do the Business Aspects of JamClass Work?
+
+
You can use JamClass by JamKazam to teach your own existing students if they'd like to take online lessons, and it’s free to use in this way for individual teachers, much like Skype.
+
+
If you would like JamKazam to bring new students to you through the JamClass marketplace, we will be making substantial marketing investments in attracting, equipping, and delivering these students to you. JamKazam will bill and collect payments directly from these referred students. We will retain 25% of lesson revenue from these students, and we will transfer the balance of 75% of these lesson revenues to you.
+
+
Also, please note that to participate in the JamClass marketplace, you will need to opt in to participate in our TestDrive program. TestDrive is a core component of our JamClass marketing programs, providing students interested in taking online lessons with discounted introductory packages to get started. To participate in the marketplace, you must be willing to teach at least 2 TestDrive lessons per week. You are paid $10 for each 30-minute TestDrive lesson, and some TestDrive students will become long-term students who pay your normal rates. You may opt to accept more than 2 TestDrive lessons per week if you would like to grow your student base more rapidly.
+
+
+
+
What Now?
+
+
If you're ready to sign up to join our community and teach through JamClass by JamKazam, just scroll back up to the top of this page, and enter your email address and a password to sign up. Once you've done this, we'll provide instructions and engage with you to do the following:
+
+
You'll fill out your musician & teacher profile to tell prospective students about yourself and to price your lessons.
+
Our JamKazam staff will give you all the 1:1 help you need to get your gear set up for online lessons.
+
Once you’ve started teaching, you’ll need to set up a Stripe account to get paid for the lessons you teach.
+
+
While you're getting this done, if you want to learn more about all the nifty features you can access in JamClass and in JamKazam in general, you can check out our online JamClass User Guide.
+
+
`
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/landing/JamClassTeacherLandingPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/JamClassTeacherLandingPage.js.jsx.coffee
new file mode 100644
index 000000000..657c9bb18
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/landing/JamClassTeacherLandingPage.js.jsx.coffee
@@ -0,0 +1,144 @@
+context = window
+rest = context.JK.Rest()
+
+@JamClassTeacherLandingPage = React.createClass({
+
+ render: () ->
+
+
+ loggedIn = context.JK.currentUserId?
+
+ if this.state.done
+ ctaButtonText = 'sending you in...'
+ else if this.state.processing
+ ctaButtonText = 'hold on...'
+ else
+ if loggedIn
+ ctaButtonText = 'Enter Teacher Profile'
+ else
+ ctaButtonText = 'Sign Up'
+
+ if loggedIn
+ register = ``
+ else
+
+ if this.state.loginErrors?
+ for key, value of this.state.loginErrors
+ break
+
+ errorText = context.JK.getFullFirstError(key, this.state.loginErrors, {email: 'Email', password: 'Password', 'terms_of_service' : 'The terms of service'})
+
+ register = `
+
+ {errorText}
+
+
+
`
+
+
+ `
+
+
+
+
JAMCLASS
+
Finally, online music lessons that really work!
+
+
+
+
+
+
+ Sign Up as Teacher
+
+
+
Sign up and start getting students referred from our marketplace.
+
We'll give you all the 1:1 help you need to get set up and running. We can help if you need tech support with gear, and we'll get you in a session to make sure you're ready to go!
+
Sign up now to join our community. We will not share your email. See our privacy policy
+ {register}
+
We guarantee you will be blown away by how much better this is than Skype for online lessons.
+
+
+
+
+
+ JamClass by JamKazam is by far the best way to teach online music lessons. Using our unique, patented technologies, you can play live in sync with your students over the Internet with incredible audio quality. Teach students who live far away. Teach during normally “off hours”. And let JamKazam bring students to you through our lesson marketplace, increasing your income in multiple dimensions. All while avoiding the time and cost of travel to and from lessons.
+
`
})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/landing/JamTrackLandingBottomPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/JamTrackLandingBottomPage.js.jsx.coffee
new file mode 100644
index 000000000..a01152f63
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/landing/JamTrackLandingBottomPage.js.jsx.coffee
@@ -0,0 +1,180 @@
+context = window
+
+@JamTrackLandingBottomPage = React.createClass({
+
+ render: () ->
+ `
+
+
What Makes JamTracks Awesome?
+
+
JamTracks by JamKazam deliver an unparalleled combination of multitrack pro audio and whiz bang technology -
+ all with an eye toward the things that really matter to musicians who love to play. Below are the top 10 great
+ things about JamTracks.
+
+
+
+
+
+
1
Huge, High Quality Multi-Track Catalog
+
+ JamKazam offers a catalog of 3,700+ songs. Each song is reviewed for quality, and every recording
+ is a complete multi-track, with fully isolated tracks for each part of the music - e.g. lead vocal,
+ backing vocals, lead guitar, rhythm guitar, keys, bass, drums, etc. This gives you complete creative control
+ over every aspect of the music and how you want to use it for learning, practice, recording, and other creative endeavors.
+
+
+
+
+
+
+
2
Solo, Mute, Pan or Set Level on Any Part
+
+ When learning to play a part, it's incredibly valuable to be able to hear just one part in isolation.
+ Once you've learned your part, you can turn around and mute just that one part, and then play along with the rest of the band.
+ Or if you prefer, you can turn that part down low but keep it around as a subtle hint. Or pan the recorded track into
+ your left ear while your live performance is panned into your right ear. Whatever you like! You are in control.
+
+
+
+
+
+
+
3
Make Custom Mixes
+
+ When you've customized the JamTrack mix, you can easily save your custom mixes to use them again later without
+ having to recreate them. Your custom mixes are saved to the JamKazam cloud, so you can access them from almost
+ any Internet-connected device. If you want to use your mixes outside the JamKazam app, you can also export custom mixes
+ as a simple .mp3, .wav, or .ogg audio file to use anywhere.
+
+
+
+
+
+
+
4
Slow Down For Practice
+
+ You can easily slow down playback of your JamTrack by a specific % without changing pitch, so that the song still
+ sounds "right", just slower. This is great for building your technique on tougher sections while gradually increasing tempo.
+ You can also make JamTracks play faster if you want to hit the jets.
+
+
+
+
+
+
+
5
Change Pitch/Key
+
+ If you're a singer and you need to bring the song down into your vocal range, or if you're an instrumentalist
+ and want to change the piece to a different key, the JamKazam app lets you change the pitch of any JamTrack up or down
+ by a specified number of semitones (half steps).
+
+
+
+
+
+
+
6
Apply VST & AU Audio Plug-In Effects
+
+ The free JamKazam app lets you easily apply VST & AU plugin effects to your live performance, mixed together
+ seamlessly with JamTrack playback. For example, guitarists can apply popular amp sims like AmpliTube to get
+ just the right guitar tone to match the song, and vocalists can apply effects like reverb, pitch correction, etc.
+
+
+
+
+
+
+
7
Use MIDI Instruments
+
+ The free JamKazam app also lets you use MIDI instruments, and mix and record this instrumental audio with JamTracks.
+ For example, keys players can use MIDI keyboard controllers with VST & AU plugins to generate traditional piano sounds,
+ Rhodes electric piano, Hammond organ, and other classic keys tones. And drummers who use electronic kits can use their favorite
+ plugins to power their percussive audio.
+
+
+
+
+
+
+
8
Make & Share Recordings
+
+
+
+
+
+
watch this sample video made by one of our users
+
+
Use the JamKazam app to make either audio-only or video + audio recordings. The app captures video from built-in
+ or external webcams and combines this video with the mixed audio from the JamTrack and your live performance
+ into a single integrated video, and will even upload the video to YouTube for you!
+
+
+
+
+
+
+
9
Play Live In Sync With Others Over the Internet
+
+
+
+
+
+
watch this sample video made by three of our users
+
+
Perhaps the most mind-blowing thing you can do with JamKazam is that you can play live in sync with others
+ from different locations over the Internet using the free JamKazam app and Internet service. And you can play
+ with your JamTracks in these sessions, with others playing different parts.
+
+
+
+
+
+
+
10
JamTracks Work With All Your Stuff
+
+ You can use your JamTracks with any device that can run a standard web browser for playback of the JamTracks.
+ If you want to mix your live performance with the JamTrack for recordings, and to access other advanced
+ features, you'll need to use a JamKazam app. Our app is currently available for Mac and Windows computers,
+ and we will have iOS and Android apps coming very soon as well. So you can access your JamTracks on just about any device.
+
+
+
+
+
+
+
+
+
+
+
+
Leading Musicians & Teachers Love JamTracks
+
+
+
+
+
Andy Crowley of AndyGuitar
+
+
+
+
+
+
Ryan Jones of PianoKeyz
+
+
+
+
+
+
Carl Brown of GuitarLessions365
+
+
+
Watch A JamTracks Overview Video
+
+
+
+
+
+
+
+
+
`
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/landing/JamTrackLandingPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/JamTrackLandingPage.js.jsx.coffee
new file mode 100644
index 000000000..889219650
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/landing/JamTrackLandingPage.js.jsx.coffee
@@ -0,0 +1,210 @@
+context = window
+rest = context.JK.Rest()
+
+@JamTrackLandingPage = React.createClass({
+
+ render: () ->
+
+ hasFree = context.JK.currentUserFreeJamTrack
+
+ loggedIn = context.JK.currentUserId?
+
+ if this.state.done
+ ctaButtonText = 'sending you in...'
+ else if this.state.processing
+ ctaButtonText = 'hold on...'
+ else
+ if hasFree
+ ctaButtonText = 'GET IT FREE!'
+ else
+ ctaButtonText = 'Add To Cart'
+
+ if @state.iOS
+ iosBadge = `
+
+ `
+
+ register = `
+
Download our free iOS app now, and get this JamTrack free! See why our JamTracks are so much better than traditional backing tracks - with no risk.
by {this.props.jam_track.original_artist.toUpperCase()}
+ {iosBadge}
+
+
+
+
+
+ JamTracks by JamKazam are the best way to play along with your favorite songs. Far better and different than traditional
+ backing tracks, our JamTracks are complete multi-track professional recordings, with fully isolated tracks for each part of the music.
+ And our free app and Internet service are packed with features that give you unmatched creative freedom to learn, practice, record, play with others, and share your performances.
+
+
+
+
+
+
+ Preview JamTrack
+
+
+
Click the play buttons below to preview the master mix and 20-second samples of all the isolated tracks.
Play Music Live in Sync from Different Locations over the Internet
+
+
+ The ability to play live together from different locations enables many new possibilities including:
+
+
+
Rehearse without needing to pack gear, travel, and find rehearsal space
+
+
Co-write new music interactively as if you're sitting in the same room rather than just sharing
+ files, which limits both creativity and speed
+
+
Join open jam sessions any time to play live with others, make new connections, learn, and just have
+ fun playing more music
+
+
+
+ Latency issues have historically prevented musicians from playing together over the Internet, but the
+ JamBlaster and JamKazam's patent pending software innovations have brought this dream to life.
+
+
+
+ To demonstrate the kind of live distributed performances that are possible with the JamBlaster, we flew
+ the members of the band Big Cat to Austin TX, Atlanta GA, Chicago IL, and Brooklyn NY, and had them play
+ together using a JamBlaster at each location. They played from the homes of friends in those cities
+ using normal consumer Internet connections. We used a headphone splitter to record the audio that
+ Malford (singer) heard in real-time while singing, so you can hear exactly what he heard while
+ performing. Watch the video below to see and hear it!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Make Pro Quality Audio Recordings
+
+ The JamBlaster delivers professional quality audio recording, and enables a number of very handy recording scenarios.
+
+
+
Record Yourself
+
+ Plug your instruments and/or microphones into the JamBlaster, and you are ready to record. The JamBlaster automatically records both a master mix and the individual fully isolated stems/tracks from each audio input port, and these audio recordings are saved to the JamBlaster, so they don't use up precious storage space on your smartphone, tablet, or computer. When your recording is complete, the audio files are automatically synched and stored in the JamKazam cloud, so that you can access these audio files from any device, anytime, anywhere, and you can easily share them with others if desired. You can also grab the audio files to use in your favorite DAW (e.g. ProTools, Ableton, GarageBand, etc.) for further editing.
+
+
+ In addition to audio, you can easily make recordings that combine and sync audio from the JamBlaster with video from your smartphone or tablet or from a webcam on your computer to create great videos, and the JamKazam app can upload these videos automatically to YouTube for you.
+
+
+
+
+
Record an Online JamKazam Session
+
+ You can also easily record JamKazam online multiplayer sessions - for example a band rehearsal. When you do this, the recording feature captures the master mix of the combined performance, and also captures the fully isolated audio stems for each musician in the session. When the recording is complete, the high quality stems from all musicians are automatically uploaded to the JamKazam cloud, which then automatically downloads all stems to all musicians who played on the recording. And again in this case, you can easily record video + audio of the full multiplayer session, and upload this to YouTube for easy sharing. Here is an example of a recorded JamKazam session:
+
+
+
+
+
+
+
+
Record a Remote Musician Laying Down a Track
+
+ Producers and composers can use the JamBlaster to open an audio file of a piece of music (work in progress) and play this audio file to a remote musician, who plays along with this audio file to lay down a new track to add to it. When complete, the remote musician's high quality stem audio file(s) are automatically uploaded and accessible to the producer or composer.
+
+
Works With Your Favorite DAW Too
+
+ You can even use the JamBlaster like a traditional audio interface by connecting the JamBlaster with a USB cable to your computer, and you can then record through the JamBlaster straight into your favorite recording app, like ProTools, Ableton, GarageBand, etc.
+
+
+
+
+
+
+
+
Learn And Play Along With 4,000+ Of Your Favorite Songs
Listen to just the single part you want to play to learn it
+
Mute the part you want to play, and play along with the rest of the band
+
Slow down playback to practice without changing the pitch
+
Change the song key by raising or lowering pitch in half steps
+
Save your custom mixes for easy access, and export them to use anywhere
+
Make audio recordings and share via Facebook or URL
+
Make video recordings and share via YouTube
+
Play JamTracks with other musicians online live and in sync
+
+
Here's an example of a YouTube video recording made with a JamTrack by a JamKazam user:
+
+
+
+
+
+
Your first JamTrack is free, and after that JamTracks are just $1.99 each.
+
+
+
+
+
+
+
Teach Or Take Online Music Lessons
+
There are compelling reasons that many music teachers and students want to teach and take music lessons online. Unfortunately Skype, Google Hangouts, and similar apps are horrible for music lessons. Latency is far too high for teacher and student to play together, and audio quality is very poor. Plus these apps don't offer features important to lessons, such as using backing audio tracks in sessions, making quality video/audio recordings of lesson demonstrations by the teacher or performances by the student, and so on.
+
The JamBlaster delivers the ideal platform for online music lessons, with great audio quality, the lowest possible latency, plug-and-play ease of use, and access to all of the JamKazam features for live music performance. Teachers can reach and effectively teach students across great distances, massively increasing the size of the lesson market opportunity for the teacher. Students can connect with the best teacher for their specific needs, rather than settling for the teacher who lives within a 30-minute drive. And both teacher and student can avoid the wasted time and expense of traveling to and from lessons.
+
+
+ Online Music Lesson
+
+
JamKazam is also currently working to develop an online music lesson marketplace, through which we can connect students to teachers. We plan to launch this marketplace in late Q1 2016.
+
+
+
+
+
+
+
Broadcast Live Performances Through YouTube
+
With the JamBlaster, you can broadcast your musical performances live to family, friends, and fans through YouTube, without understanding codecs, without buying streaming software to integrate with YouTube, and so on. Just click a button to broadcast your session, and the JamBlaster takes care of the rest. It combines the pro quality audio of your performance with video from your smartphone, tablet, or computer webcam, and streams this combined video through YouTube Live. You can even schedule a performance and distribute and promote a URL for the live stream in advance of the event, and you can use multiple webcams to cut between different shots/angles during the live broadcast.
+
+
+ YouTube Live Broadcast
+
+
+
+
+
+
+
+
Why Did We Design and Build the JamBlaster?
+
Speed
+
When we initially built the free JamKazam service to let musicians play together live over the Internet, we started by having musicians use the Mac and Windows computers and audio interfaces they already own. We've signed up {gon.global.musician_count} musicians along the way. We've analyzed data from more than 100,000 online sessions. And we've collected audio processing latency data on thousands of combinations of computers and interfaces, as well as 10 million Internet latency measurements between unique pairs of locations and ISPs. We've learned a lot from all this data.
+
Typically you need to keep total one way latency down to 30 to 35 milliseconds or less in an online session, or the session will get too sloppy and fall apart. We found that the average audio processing latency of industry standard gear is 14 milliseconds (full round trip including analog-to-digital and digital-to-analog conversions). So just processing the audio eats up half of your total latency budget!
+
We designed the JamBlaster from the ground up to be the fastest audio processing device possible, and we have the JamBlaster running at 2.8 milliseconds of latency full round trip - a massive latency savings. Every one millisecond saved on audio processing is worth about 100 miles of range on the Internet backbone. The JamBlaster also reduces something called audio processing jitter, which delivers additional latency savings. The result is that the JamBlaster saves audio latency equivalent to about 1,500 miles of distance compared to today's standard computers and interfaces.
+
Looking at it another way, using JamKazam with standard computers and interfaces, a musician in the U.S. can play effectively with about 10% of the other musicians in the U.S. With the JamBlaster, that same musician can now play with about 35% of the other musicians in the U.S.
+
+
Ease of Use
+
We also found that many users struggled to get their computers and interfaces working properly. Sometimes the operating system was incompatible. Sometimes interface drivers had problems, or the user couldn't figure out how to configure the driver for low latency. And so on and on. We heard from many musicians that they did not love or understand technology very well, and they wanted something that "just worked". The JamBlaster just works. It contains all the hardware, software, drivers, etc. that are needed. You don't have to install any apps or drivers or configure things. You just plug in your instruments and/or microphones and connect it to your network.
+
Post PC Ready
+
Finally, we found that many musicians either have ancient computers, or for younger musicians, no computer at all - often just a smartphone or tablet. The JamBlaster is both a computer and an audio interface in one device, so you don't need a computer to use it. You can control it with your iOS or Android smartphone, or with a standard browser running on a tablet or a Windows, Mac, or Linux computer.
+
+
+
+
+
+
+
How Does the JamBlaster Work?
+
It's important to understand that the JamBlaster is both a computer and an audio interface in one highly optimized device. You simply connect your instruments or microphones into the two input ports, connect the JamBlaster to your network, and plug headphones in to hear the music.
+
To control the JamBlaster, you can run a companion app either on an iPhone or Android smartphone, or you can use the interface at www.jamkazam.com in a standard browser on a tablet or a Windows, Mac, or Linux computer. The companion app or website talks to the JamBlaster over WiFi to tell it what to do. There are no physical connections needed, so you don't have to worry about compatibility problems with your current (or future) mobile devices and computers.
+
+
+
+
+
+
+
+
How Does Latency Work For Online Music Performance?
+
Latency exists in all music performance environments. The speed of sound is so slow (sound travels 1 foot per millisecond) that two musicians sitting 10 feet away from each other will experience 10 milliseconds of delay, or latency, from the time one plays an instrument until the other hears it. The key to enabling online live music performance is to keep latency so low that it doesn't interfere with the ability to play in sync.
+
The Internet backbone is fiber, and data can move across fiber at approximately the speed of light. This is the key to enabling distributed live music performance. With the JamBlaster cutting audio processing latency down so close to zero, all that's left to deal with is Internet latency.
+
In the video above where the band Big Cat played together from Austin, Atlanta, Chicago, and Brooklyn, the latency in that online session was equivalent to having the musicians sit about 25 feet away from each other in a room.
+
Here's a video that explains a little bit more about latency:
+
+
+
+
+
+
+
+
+
+
+
+
What Internet Service Do You Need To Play Online?
+
When you get a JamBlaster, you have a device that you know will "just work". The other thing you need is broadband Internet service. You don't need anything special or unusual. Most home broadband Internet connections today work fine. Here's what to watch for.
+
We've found that both cable broadband and fiber broadband Internet services work well, while low-end DSL service sometimes doesn't work well, and satellite/wireless Internet doesn't work due to high latency.
+
To play in groups of 4 musicians (audio only), you'll want to have Internet service rated at 1Mbps of uplink/upload bandwidth. To play in groups of 4 musicians (video + audio), you'll want to have Internet service rated at 2.5Mbps of uplink/upload bandwidth. If you're not sure what your upload bandwidth is, we'd recommend you go to www.speedtest.net and run the test there.
+
And finally, to play in online, real-time sessions, we recommend avoiding WiFi connections. For the best performance, connect the JamBlaster to your router using an Ethernet cable. If you want to play in a room far away from the room where your home router is located, you can buy a 100-foot Ethernet cable on Amazon for about $10, and then just run it from your router to another room in your house, and then coil it back up and store it when you're done. It's cheap and easy. For recording, live YouTube broadcasts, and JamTracks, WiFi is fine.
+
+
+
+
+
+
+
What Else Do You Need To Use With The JamBlaster?
+
If you sing or play an acoustic instrument like a violin, trumpet, or piano, you'll need to use a microphone to capture the audio from your instrument/voice. If you have an electronic instrument like an electric guitar, bass, or keyboard, you can plug a 1/4" TS or XLR connector into either of the two input ports. The JamBlaster can automatically duplicate mono inputs into stereo signals, so you can use just one port per instrument, and still have stereo audio on each instrument/vocal.
+
If you are a multi-instrumentalist and need more than 2 input ports, we recommend using a simple analog mixer like a Behringer Xenyx 1002B. You can plug all your gear into the mixer, and then connect the audio outputs from the mixer into the inputs on the JamBlaster. Analog mixers don't add any latency.
+
Other than that, you'll need a device that presents the user interface to set up or join sessions, record, and so on. This can either be a smartphone - specifically an iPhone (v8 or later) or Android phone (v4.4 or later). Or you can run a standard browser on a tablet or on a Windows, Mac, or Linux computer. Video services are also provided by your smartphone, tablet, or a computer webcam.
+
+
+
+
+
+
+
The JamBlaster Plugs Into The JamKazam Platform And Community
+
JamKazam has already signed up {gon.global.musician_count} musicians who play in thousands of online sessions per month using their computers and audio interfaces. The JamBlaster interoperates seamlessly with other musicians who are running Mac and Windows PC setups, so you can jump right in and start playing with other musicians in the community using your JamBlaster from day one.
+
+
+
+
+
+
+
+
JamBlaster Specifications
+
Following are tech specs on the JamBlaster:
+
+
JamBlaster Computing Resources
+ The JamBlaster includes a quad-core 1.2GHz ARM processor, 1GB memory, 8GB storage, and GigE Ethernet network connectivity. It runs a Linux operating system with an optimized real-time kernel, and the entire system has been designed, architected, and optimized end-to-end to deliver blazing audio processing speed.
+
+
JamBlaster Audio Quality
+ The JamBlaster uses premium audio components throughout to deliver superb audio quality on par with audio interfaces that were designed only for recording. The JamBlaster delivers up to 24-bit, 96kHz premium audio. Frequency response for mic/line/instrument inputs is 20Hz – 20kHz +/-0.2dB. THD+N is -107dB for mic inputs and <-100 dB for line and instrument inputs.
+
+
Input Ports
+ The JamBlaster features two Neutrik combo ports on the front, so you can plug in either TS 1/4" or XLR style connectors. Each input port can be individually set to accept line level (lo Z) or instrument level (hi Z) inputs, and each input port can be individually set to supply 48V phantom power if needed.
+
+
Headphone Port
+ The headphone port accepts a standard 1/4" TRS connector from your headphones.
+
+
Chat Mic
+ The horizontal cutout labeled "MIC" on the front of the JamBlaster is a built-in chat mic you can use to talk with other musicians while in sessions.
+
+
Ethernet Port
+ The Ethernet port on the rear of the JamBlaster is a GigE Ethernet port, and the JamBlaster can be connected to your home router using an Ethernet cable (not included) for Internet connectivity. We strongly recommend a hardwired Ethernet connection for the best performance in online distributed sessions. However, if you want to use WiFi for things like making recordings, live broadcasting through YouTube, or playing along with JamTracks, that is totally fine, and we'd recommend a cheap Ethernet-to-WiFi adapter to WiFi-enable the JamBlaster if desired for greater mobility.
+
+
USB Ports
+ The JamBlaster includes 8GB of onboard storage, and the Linux operating system and JamKazam application that run on the JamBlaster use less than 1GB of this storage. For musicians who want more storage capacity for recordings, we plan to support a short list of "certified" USB memory sticks that you can buy and plug into a USB port to expand the JamBlaster's storage capacity.
+
+
Reset Button
+ This button is available in case troubleshooting is needed on the JamBlaster. For example, it can be used to hard reset the box to factory defaults.
+
+
Power Supply
+ The JamBlaster's power supply (included with the JamBlaster) is rated for 100-240VAC 50/60Hz, so it works with both North America's standard 120V/60Hz and Europe's standard 220V/50Hz power infrastructure. Please note that the physical power supply plug is a standard US plug type, so if you live outside the US, you will need to use a plug adapter (not included), but not a power converter.
+
+
Compatibility with Recording Software
+ The JamBlaster may be connected to Windows and Mac computers using a USB cable (not included), and will act as a "normal" audio interface when used in this configuration, so you can use it with your favorite recording software (e.g. ProTools, Ableton, Cubase, Reaper, GarageBand, etc.).
+
+
Controls
+ The JamBlaster is controlled using a companion app that can run on iOS (v8 or later) or Android (v4.4 or later) smartphones. Or it may be controlled using a web interface that runs in a standard browser on tablets or Windows, Mac, or Linux computers.
+
+
+
+
+
+
+
+
+
Ready to Order Your JamBlaster?
+
The JamBlaster had a great run on Kickstarter, as we hit 347% of our Kickstarter revenue goal, but the Kickstarter campaign is over now. We don't have enough cash to have JamBlaster inventory sitting around waiting for orders, so we are using pre-orders to fund our next JamBlaster production run. This is very similar to the way Kickstarter works.
+
Simply stated, you can pre-order a JamBlaster now by clicking the white "Pre-order the JamBlaster today!” link on the orange background below and following the on-screen instructions.
+
So if you want a JamBlaster, we'd advise you go ahead and place your order now to reserve your place in the next batch.
+
We are currently able to ship to customers in the United States, Canada, the European Union (EU), and Australia. If you have any other questions about the JamBlaster, please email us at support@jamkazam.com.
+
+
+
+
`
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/landing/ProductJamBlasterPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/ProductJamBlasterPage.js.jsx.coffee
new file mode 100644
index 000000000..0979c61b5
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/landing/ProductJamBlasterPage.js.jsx.coffee
@@ -0,0 +1,63 @@
+context = window
+rest = context.JK.Rest()
+
+@ProductJamBlasterPage = React.createClass({
+
+ render: () ->
+
+ `
+
+
+
JAMBLASTER
+
by JamKazam
+
+
+
+
See What You Can Do With The JamBlaster
+
+
+
+
+
+
+
+
+ Reserve your spot in our next JamBlaster manufacturing run now! Pre-order now and you won’t be charged until 30 days before we ship.
+
+
+
+
+
+
+ With your smartphone and a JamBlaster, you can:
+
+
+
+
Play live in sync with other musicians from different locations over the Internet with the lowest possible latency – great for rehearsals without travel or space, co-writing, or joining open jams for fun
+
Make pro quality audio (and optionally video) recordings of yourself and others – both master mix and fully isolated stems
+
Learn and play along with 4,000+ of your favorite songs – with the ability to solo or mute any part, slow down playback for practice, change pitch/key, and more
+
Teach or take online music lessons that really work – unlike Skype and Google Hangouts, which suffer from very high latency and poor audio quality
+
Broadcast live video performances with pro quality audio through YouTube to family, friends, and fans – either yourself or your band playing in one location, or your online distributed JamKazam sessions
`
+ loggedIn = context.JK.currentUserId? && !this.props.preview
+
+ if this.state.done
+ ctaButtonText = 'sending you in...'
+ else if this.state.processing
+ ctaButtonText = 'hold on...'
+ else
+ if loggedIn
+ ctaButtonText = "GO TO JAMKAZAM"
+ else
+ ctaButtonText = "SIGN UP"
+
+ if loggedIn
+ register =
+ `
+
+
`
+ else
+ if this.state.loginErrors?
+ for key, value of this.state.loginErrors
+ break
+
+ errorText = context.JK.getFullFirstError(key, this.state.loginErrors,
+ {email: 'Email', password: 'Password', 'terms_of_service': 'The terms of service'})
+
+ register = `
+
+ {errorText}
+
+
+
+ We will not share your email. See our privacy policy.
+
+
`
+
+
+ `
+
+
+ {logo}
+
+
+
REGISTER AS A STUDENT
+
+
with {this.props.school.name}
+
+
+
+
+
+
+
+ Please register here if you are currently a student with {this.props.school.name}, and if you plan to take
+ online music lessons from {this.props.school.name} using the JamKazam service. When you have registered, we
+ will
+ email you instructions to set up your profile, and we'll schedule a brief online training session to make sure
+ you are comfortable using the service and ready to go for your first online lesson.
+
+
+
+ {register}
+
`
+
+ getInitialState: () ->
+ {loginErrors: null, processing: false}
+
+ componentDidMount: () ->
+ $root = $(this.getDOMNode())
+ $checkbox = $root.find('.terms-checkbox')
+ context.JK.checkbox($checkbox)
+
+# add item to cart, create the user if necessary, and then place the order to get the free JamTrack.
+ ctaClick: (e) ->
+ e.preventDefault()
+
+ return if @state.processing
+
+ @setState({loginErrors: null})
+
+ loggedIn = context.JK.currentUserId?
+
+ if loggedIn
+ #window.location.href = "/client#/jamclass"
+ window.location.href = "/client#/profile/#{context.JK.currentUserId}"
+ else
+ @createUser()
+
+ @setState({processing:true})
+
+ createUser: () ->
+ $form = $('.school-signup-form')
+ email = $form.find('input[name="email"]').val()
+ password = $form.find('input[name="password"]').val()
+ terms = $form.find('input[name="terms"]').is(':checked')
+
+ rest.signup({
+ email: email,
+ password: password,
+ first_name: null,
+ last_name: null,
+ terms: terms,
+ student: true,
+ school_invitation_code: this.props.invitation_code,
+ school_id: this.props.school.id
+ })
+ .done((response) =>
+ @setState({done: true})
+ #window.location.href = "/client#/jamclass"
+ window.location.href = "/client#/profile/#{response.id}"
+ ).fail((jqXHR) =>
+ @setState({processing: false})
+ if jqXHR.status == 422
+ response = JSON.parse(jqXHR.responseText)
+ if response.errors
+ @setState({loginErrors: response.errors})
+ else
+ context.JK.app.notify({title: 'Unknown Signup Error', text: jqXHR.responseText})
+ else
+ context.JK.app.notifyServerError(jqXHR, "Unable to Sign Up")
+ )
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/landing/SchoolTeacherLandingPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/SchoolTeacherLandingPage.js.jsx.coffee
new file mode 100644
index 000000000..7fbf2bb40
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/landing/SchoolTeacherLandingPage.js.jsx.coffee
@@ -0,0 +1,147 @@
+context = window
+rest = context.JK.Rest()
+
+@SchoolTeacherLandingPage = React.createClass({
+
+ render: () ->
+ if this.props.school.large_photo_url?
+ logo = `
+
+
`
+ loggedIn = context.JK.currentUserId? && !this.props.preview
+
+ if this.state.done
+ ctaButtonText = 'sending you in...'
+ else if this.state.processing
+ ctaButtonText = 'hold on...'
+ else
+ if loggedIn
+ ctaButtonText = "GO TO JAMKAZAM"
+ else
+ ctaButtonText = "SIGN UP"
+
+ if loggedIn
+ register =
+ `
+
+
`
+ else
+ if this.state.loginErrors?
+ for key, value of this.state.loginErrors
+ break
+
+ errorText = context.JK.getFullFirstError(key, this.state.loginErrors,
+ {email: 'Email', password: 'Password', 'terms_of_service': 'The terms of service'})
+
+ register = `
+
+ {errorText}
+
+
+
+ We will not share your email. See our privacy policy.
+
+
`
+
+
+ `
+
+
+ {logo}
+
+
+
REGISTER AS A TEACHER
+
+
with {this.props.school.name}
+
+
+
+
+
+
+
+ Please register here if you are currently a teacher with {this.props.school.name}, and if you plan to teach
+ online music lessons for students of {this.props.school.name} using the JamKazam service. When you have registered, we
+ will
+ email you instructions to set up your online teacher profile, and we'll schedule a brief online training session to make sure
+ you are comfortable using the service and ready to go with students in online lessons.
+
+
+
+ {register}
+
`
+
+ getInitialState: () ->
+ {loginErrors: null, processing: false}
+
+ componentDidMount: () ->
+ $root = $(this.getDOMNode())
+ $checkbox = $root.find('.terms-checkbox')
+ context.JK.checkbox($checkbox)
+
+# add item to cart, create the user if necessary, and then place the order to get the free JamTrack.
+ ctaClick: (e) ->
+ e.preventDefault()
+
+ return if @state.processing
+
+ @setState({loginErrors: null})
+
+ loggedIn = context.JK.currentUserId?
+
+ if loggedIn
+ #window.location.href = "/client#/jamclass"
+ window.location.href = "/client#/profile/#{context.JK.currentUserId}"
+ else
+ @createUser()
+
+ @setState({processing:true})
+
+ createUser: () ->
+ $form = $('.school-signup-form')
+ email = $form.find('input[name="email"]').val()
+ password = $form.find('input[name="password"]').val()
+ terms = $form.find('input[name="terms"]').is(':checked')
+
+ rest.signup({
+ email: email,
+ password: password,
+ first_name: null,
+ last_name: null,
+ terms: terms,
+ teacher: true,
+ school_invitation_code: this.props.invitation_code,
+ school_id: this.props.school.id
+ })
+ .done((response) =>
+ @setState({done: true})
+ #window.location.href = "/client#/jamclass"
+ window.location.href = "/client#/profile/#{response.id}"
+ ).fail((jqXHR) =>
+ @setState({processing: false})
+ if jqXHR.status == 422
+ response = JSON.parse(jqXHR.responseText)
+ if response.errors
+ @setState({loginErrors: response.errors})
+ else
+ context.JK.app.notify({title: 'Unknown Signup Error', text: jqXHR.responseText})
+ else
+ context.JK.app.notifyServerError(jqXHR, "Unable to Sign Up")
+ )
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/mixins/BonjourMixin.js.coffee b/web/app/assets/javascripts/react-components/mixins/BonjourMixin.js.coffee
new file mode 100644
index 000000000..3c8da5f24
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/mixins/BonjourMixin.js.coffee
@@ -0,0 +1,17 @@
+context = window
+teacherActions = window.JK.Actions.Teacher
+
+@BonjourMixin = {
+ findJamBlaster: (oldClient) ->
+ found = null
+ if @state.allJamBlasters?
+ for client in @state.allJamBlasters
+ if oldClient.server_id? && client.server_id == oldClient.server_id
+ found = client
+ break
+ if oldClient.ipv6_addr? && client.ipv6_addr == oldClient.ipv6_addr
+ found = client
+ break
+
+ found
+}
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/mixins/ICheckMixin.js.coffee b/web/app/assets/javascripts/react-components/mixins/ICheckMixin.js.coffee
new file mode 100644
index 000000000..38b7c247b
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/mixins/ICheckMixin.js.coffee
@@ -0,0 +1,75 @@
+context = window
+teacherActions = window.JK.Actions.Teacher
+
+@ICheckMixin = {
+
+ iCheckIgnore: false
+ checkboxes: []
+
+ iCheckify: () ->
+ @setCheckboxState()
+ @enableICheck()
+
+ setSingleCheckbox: (checkbox) ->
+ selector = checkbox.selector
+
+ if checkbox.stateKey?
+ choice = @state[checkbox.stateKey]
+ else
+ choice = @props[checkbox.propsKey]
+
+ $candidate = @root.find(selector)
+
+
+ @iCheckIgnore = true
+
+ if $candidate.attr('type') == 'radio'
+ if choice?
+ $found = @root.find(selector + '[value="' + choice + '"]')
+ $found.iCheck('check').attr('checked', true)
+ else
+ $candidate.iCheck('uncheck').attr('checked', false)
+ else
+ if choice
+ $candidate.iCheck('check').attr('checked', true);
+ else
+ $candidate.iCheck('uncheck').attr('checked', false);
+ @iCheckIgnore = false
+
+ setCheckboxState: () ->
+ for checkbox in this.checkboxes
+ @setSingleCheckbox(checkbox)
+ return
+
+ enableSingle: (checkbox) ->
+ selector = checkbox.selector
+
+ checkBoxes = @root.find(selector + '[type="checkbox"]')
+ if checkBoxes.length > 0
+ context.JK.checkbox(checkBoxes)
+ checkBoxes.on('ifChanged', (e) => @checkIfCanFire(e))
+ radioBoxes = @root.find(selector + '[type="radio"]')
+ if radioBoxes.length > 0
+ context.JK.checkbox(radioBoxes)
+ radioBoxes.on('ifChanged', (e) => @checkIfCanFire(e))
+
+
+ enableICheck: (e) ->
+ if !@root?
+ return
+
+ for checkbox in this.checkboxes
+ @enableSingle(checkbox)
+ return
+
+ true
+
+ checkIfCanFire: (e) ->
+ if @iCheckIgnore
+ return
+
+ if @checkboxChanged?
+ @checkboxChanged(e)
+ else
+ logger.error("no checkbox changed defined")
+}
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/mixins/MasterPersonalMixersMixin.js.coffee b/web/app/assets/javascripts/react-components/mixins/MasterPersonalMixersMixin.js.coffee
index 553aa3a5e..f3f12f886 100644
--- a/web/app/assets/javascripts/react-components/mixins/MasterPersonalMixersMixin.js.coffee
+++ b/web/app/assets/javascripts/react-components/mixins/MasterPersonalMixersMixin.js.coffee
@@ -11,7 +11,11 @@ MIX_MODES = context.JK.MIX_MODES
mixers: () ->
if @props.mode == MIX_MODES.MASTER
- @props.mixers['master']
+ masterMixers = @props.mixers['master']
+ masterMixers.oppositeMixer = @props.mixers['personal']?.mixer
+ masterMixers
else
- @props.mixers['personal']
+ personalMixers = @props.mixers['personal']
+ personalMixers.oppositeMixer = @props.mixers['master']?.mixer
+ personalMixers
}
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/mixins/PostProcessorMixin.js.coffee b/web/app/assets/javascripts/react-components/mixins/PostProcessorMixin.js.coffee
new file mode 100644
index 000000000..811f956ba
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/mixins/PostProcessorMixin.js.coffee
@@ -0,0 +1,125 @@
+context = window
+teacherActions = window.JK.Actions.Teacher
+
+@PostProcessorMixin = {
+
+ postProcessLesson: (lesson) ->
+
+ if lesson.student_id == context.JK.currentUserId
+ me = lesson.student
+ other = lesson.teacher
+ lesson.isStudent = true
+ lesson.isTeacher = false
+ lesson.hasUnreadMessages = lesson['student_unread_messages']
+ else
+ me = lesson.teacher
+ other = lesson.student
+ lesson.isStudent = false
+ lesson.isTeacher = true
+ lesson.hasUnreadMessages = lesson['teacher_unread_messages']
+
+ lesson.me = me
+ lesson.other = other
+ lesson.isAdmin = context.JK.currentUserAdmin
+ lesson.schoolOnSchool = lesson['school_on_school?']
+ lesson.cardNotOk = !lesson.schoolOnSchool && !lesson.lesson_booking.card_presumed_ok
+ lesson.isActive = lesson['is_active?']
+ if (lesson.status == 'requested' || lesson.status == 'countered')
+ lesson.isRequested = true
+ if lesson.cardNotOk
+ lesson.displayStatus = 'No Card'
+ else
+ lesson.displayStatus = 'Requested'
+ if lesson['is_active?'] && (lesson.status == 'approved' )
+ lesson.isScheduled = true
+ lesson.displayStatus = 'Scheduled'
+
+ if !lesson.displayStatus?
+ if lesson.status == 'unconfirmed'
+ lesson.displayStatus = 'Unconfirmed'
+ else if lesson.status == 'canceled'
+ lesson.displayStatus = 'Canceled'
+ if lesson.student_canceled
+ lesson.displayStatus = 'Canceled (Student)'
+ else if lesson.teacher_canceled
+ lesson.displayStatus = 'Canceled (Teacher)'
+
+ else if lesson.status == 'suspended'
+ lesson.displayStatus = 'Suspended'
+ else
+ lesson.missed = true
+ lesson.missedRole = 'the teacher'
+ lesson.missedUser = lesson.teacher
+
+ if lesson.success
+ lesson.missed = false
+ lesson.missedRole = null
+ lesson.displayStatus = 'Completed'
+ else
+ if lesson.analysis?.teacher_analysis?.missed && lesson.analysis?.student_analysis?.missed
+ lesson.missedRole = 'both student and teacher'
+ lesson.missedUser = lesson.teacher
+ lesson.displayStatus = 'Missed (Both)'
+ else if lesson.analysis?.teacher_analysis?.missed
+ lesson.missedRole = 'the teacher'
+ lesson.missedUser = lesson.teacher
+ lesson.displayStatus = 'Missed (Teacher)'
+ else if lesson.analysis?.student_analysis?.missed
+ lesson.missedRole = 'the student'
+ lesson.missedUser = lesson.student
+ lesson.displayStatus = 'Missed (Student)'
+ else
+ lesson.displayStatus = 'Missed'
+
+ @postProcessUser(me)
+ @postProcessUser(other)
+
+ postProcessUser: (user) ->
+ if !user.photo_url?
+ user.resolved_photo_url = '/assets/shared/avatar_generic.png'
+ else
+ user.resolved_photo_url = user.photo_url
+ if user.teacher?
+ user.teacher_profile = '/client#/profile/teacher/' + user.id
+ user.best_profile = user.teacher_profile
+ else
+ user.musician_profile = '/client#/profile/' + user.id
+ user.best_profile = user.musician_profile
+
+ if user.is_a_teacher && user.teacher?
+ cheapest_lesson_stmt = '(no pricing set yet)'
+ lowestPrice = null
+ lowestDuration = null
+ single = true
+ enabledMinutes = []
+ for minutes in [30, 45, 60, 90, 120]
+ duration_enabled = user.teacher["lesson_duration_#{minutes}"]
+
+ if duration_enabled
+ enabledMinutes.push(minutes)
+ if user.teacher.prices_per_lesson
+ for minutes in enabledMinutes
+ lesson_price = user.teacher["price_per_lesson_#{minutes}_cents"]
+ if lesson_price?
+ if !lowestPrice? || lesson_price < lowestPrice
+ lowestPrice = lesson_price
+ single = true
+ lowestDuration = minutes
+ for minutes in enabledMinutes
+ lesson_price = user.teacher["price_per_month_#{minutes}_cents"]
+ if lesson_price?
+ if !lowestPrice? || (lesson_price / 4) < lowestPrice
+ lowestPrice = lesson_price / 4
+ single = false
+ lowestDuration = minutes
+
+ if lowestPrice?
+ if single
+ # lowest price appears to be a single lesson
+ cheapest_lesson_stmt = "$#{lowestPrice / 100} for #{lowestDuration} minutes"
+ else
+ # lowest price appears to be a monthly recurring lesson
+ cheapest_lesson_stmt = "$#{Math.round(lowestPrice * 4) / 100} per month"
+
+ user.cheapest_lesson_stmt = cheapest_lesson_stmt
+}
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/mixins/SessionMediaTracksMixin.js.coffee b/web/app/assets/javascripts/react-components/mixins/SessionMediaTracksMixin.js.coffee
index 92dc025f7..6297c7be0 100644
--- a/web/app/assets/javascripts/react-components/mixins/SessionMediaTracksMixin.js.coffee
+++ b/web/app/assets/javascripts/react-components/mixins/SessionMediaTracksMixin.js.coffee
@@ -31,6 +31,7 @@ logger = context.JK.logger
mediaCategoryMixer: mediaCategoryMixer
recordingName: mixers.recordingName()
jamTrackName: mixers.jamTrackName()
+ jamTrackMixdown: session.jamTrackMixdown()
@inputsChangedProcessed(state) if @inputsChangedProcessed?
diff --git a/web/app/assets/javascripts/react-components/mixins/SessionMyTracksMixin.js.coffee b/web/app/assets/javascripts/react-components/mixins/SessionMyTracksMixin.js.coffee
index 9d808485c..a25950505 100644
--- a/web/app/assets/javascripts/react-components/mixins/SessionMyTracksMixin.js.coffee
+++ b/web/app/assets/javascripts/react-components/mixins/SessionMyTracksMixin.js.coffee
@@ -1,12 +1,27 @@
context = window
+ChannelGroupIds = context.JK.ChannelGroupIds
+MIDI_TRACK = context.JK.MIDI_TRACK
+
@SessionMyTracksMixin = {
onInputsChanged: (sessionMixers) ->
+ @sessionMixers = sessionMixers
- session = sessionMixers.session
- mixers = sessionMixers.mixers
+ @recompute()
+
+ onConfigureTracksChanged: (configureTracks) ->
+
+ @configureTracks = configureTracks
+
+ @recompute()
+
+ recompute: () ->
+ return if !@sessionMixers?
+
+ session = @sessionMixers.session
+ mixers = @sessionMixers.mixers
tracks = []
@@ -38,11 +53,37 @@ context = window
instrumentIcon = context.JK.getInstrumentIcon45(track.instrument_id);
trackName = "#{name}: #{track.instrument}"
- tracks.push({track: track, mixerFinder: mixerFinder, mixers: mixerData, hasMixer:hasMixer, name: name, trackName: trackName, instrumentIcon: instrumentIcon, photoUrl: photoUrl, clientId: participant.client_id})
+ no_pan = false
+ associatedVst = null
+ # find any VST info
+ if hasMixer && @configureTracks?
+
+ # bug in the backend; track is wrong for personal mixers (always 1), but correct for master mix
+ trackAssignment = -1
+ if @props.mode == context.JK.MIX_MODES.MASTER
+ if mixerData.mixer.group_id == ChannelGroupIds.MidiInputMusicGroup
+ trackAssignment = MIDI_TRACK
+ else
+ trackAssignment = mixerData.mixer.track
+ else
+ if mixerData.mixer.group_id == ChannelGroupIds.MidiInputMusicGroup
+ trackAssignment = MIDI_TRACK
+ else
+ trackAssignment = mixerData.oppositeMixer?.track
+
+ for vst in @configureTracks.vstTrackAssignments.vsts
+ if vst.track == trackAssignment - 1 && vst.name != 'NONE'
+ logger.debug("found VST on track", vst, track)
+ associatedVst = vst
+ break
+
+ tracks.push({track: track, mixerFinder: mixerFinder, mixers: mixerData, hasMixer:hasMixer, name: name, trackName: trackName, instrumentIcon: instrumentIcon, photoUrl: photoUrl, clientId: participant.client_id, associatedVst: associatedVst})
else
logger.warn("SessionMyTracks: unable to find participant")
this.setState(tracks: tracks, session:session, chat: chat)
+
+
}
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/mixins/TeacherProfileMixin.js.coffee b/web/app/assets/javascripts/react-components/mixins/TeacherProfileMixin.js.coffee
new file mode 100644
index 000000000..fbda3cfbb
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/mixins/TeacherProfileMixin.js.coffee
@@ -0,0 +1,22 @@
+context = window
+teacherActions = window.JK.Actions.Teacher
+
+@TeacherProfileMixin = {
+ onAppInit: (app) ->
+ logger.debug("TeacherProfile onAppInit", app, document.referrer)
+ screenBindings = {
+ 'beforeShow': @beforeShow
+ }
+
+ logger.debug("Binding setup to: teachers/profile/#{@screenName()}")
+ app.bindScreen("teachers/profile/#{@screenName()}", screenBindings)
+
+ beforeShow: (data) ->
+ logger.debug("TeacherProfile beforeShow", data, data.d)
+
+ if data? && data.d?
+ @teacherId = data.d
+ teacherActions.load.trigger({teacher_id: @teacherId})
+ else
+ teacherActions.load.trigger({})
+}
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/mixins/TeacherSetupMixin.js.coffee b/web/app/assets/javascripts/react-components/mixins/TeacherSetupMixin.js.coffee
new file mode 100644
index 000000000..ecdfd5d54
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/mixins/TeacherSetupMixin.js.coffee
@@ -0,0 +1,53 @@
+context = window
+teacherActions = window.JK.Actions.Teacher
+
+@TeacherSetupMixin = {
+ onAppInit: (app) ->
+ @app=app
+ screenBindings = {
+ 'beforeShow': @beforeShow
+ }
+ @root = jQuery(this.getDOMNode())
+ @app.bindScreen("teachers/setup/#{@screenName()}", screenBindings)
+
+ beforeShow: (data) ->
+ if data? && data.d?
+ @teacherId = data.d
+ teacherActions.load.trigger({teacher_id: @teacherId})
+ else
+ teacherActions.load.trigger({})
+
+ if @myBeforeShow?
+ @myBeforeShow()
+
+ handleErrors: (changes) ->
+ $(".error-text", @root).remove()
+ if changes.errors?
+ @addError(k,v) for k,v of changes.errors
+
+ changes.errors?
+
+ addError: (k,v) ->
+ teacherField = @root.find(".teacher-field[name='#{k}']")
+ teacherField.append("
#{v.join()}
")
+ $("input", teacherField).addClass("input-error")
+
+ getParams:() =>
+ params = {}
+ q = window.location.href.split("?")[1]
+ if q?
+ q = q.split('#')[0]
+ raw_vars = q.split("&")
+ for v in raw_vars
+ [key, val] = v.split("=")
+ params[key] = decodeURIComponent(val)
+ params
+
+ teacherSetupSource:() ->
+ if @postmark? then @postmark else "/client#/account"
+
+ teacherSetupDestination:(phase) ->
+ pm = if @postmark? then "?p=#{encodeURIComponent(@postmark)}" else ""
+ # TODO: encode postmark as part of this URI when available:
+ "/client#/teachers/setup/#{phase}"
+}
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee b/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee
index bb5a11d98..223fe0fcf 100644
--- a/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee
+++ b/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee
@@ -8,5 +8,10 @@ logger = context.JK.logger
onAppInit: (app) ->
@trigger(app)
+
+ onOpenExternalUrl: (href) ->
+
+ logger.debug("opening external url #{href}")
+ context.JK.popExternalLink(href)
}
)
diff --git a/web/app/assets/javascripts/react-components/stores/AttachmentStore.js.coffee b/web/app/assets/javascripts/react-components/stores/AttachmentStore.js.coffee
new file mode 100644
index 000000000..e6d795ab7
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/AttachmentStore.js.coffee
@@ -0,0 +1,210 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+
+rest = context.JK.Rest()
+
+AttachmentActions = @AttachmentActions
+
+
+@AttachmentStore = Reflux.createStore(
+ {
+ listenables: AttachmentActions
+ lessonId: null
+ uploading: false
+
+ init: ->
+ # Register with the app store to get @app
+ this.listenTo(context.AppStore, this.onAppInit)
+
+ onAppInit: (@app) ->
+ @ui = new context.JK.UIHelper(@app);
+
+ 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 @uploading
+ logger.warn("rejecting startAttachRecording attempt as currently busy")
+ return
+ @lessonId = lessonId
+
+ @ui.launchRecordingSelectorDialog([], (recordings) =>
+ @recordingsSelected(recordings)
+ )
+ @changed()
+
+ onStartAttachNotation: (lessonId) ->
+ if @uploading
+ logger.warn("rejecting onStartAttachNotation attempt as currently busy")
+ return
+ @lessonId = lessonId
+
+ logger.debug("notation upload started for lesson: " + lessonId)
+ @triggerNotation()
+ @changed()
+
+ onStartAttachAudio: (lessonId) ->
+ if @uploading
+ logger.warn("rejecting onStartAttachAudio attempt as currently busy")
+ return
+ @lessonId = lessonId
+
+ logger.debug("audio upload started for lesson: " + lessonId)
+ @triggerAudio()
+ @changed()
+
+ triggerNotation: () ->
+ if !@attachNotationBtn?
+ @attachNotationBtn = $('input.attachment-notation').eq(0)
+ @attachNotationBtn.trigger('click')
+
+ triggerAudio: () ->
+ if !@attachAudioBtn?
+ @attachAudioBtn = $('input.attachment-audio').eq(0)
+ @attachAudioBtn.trigger('click')
+
+
+ onUploadNotations: (notations, doneCallback, failCallback) ->
+ logger.debug("beginning upload of notations", notations)
+ @uploading = true
+ @changed()
+
+ formData = new FormData()
+ maxExceeded = false;
+ $.each(notations, (i, file) => (
+ max = 10 * 1024 * 1024;
+ if file.size > max
+ maxExceeded = true
+ return false
+
+ formData.append('files[]', file)
+ ))
+
+ if maxExceeded
+ @app.notify({
+ title: "Maximum Music Notation Size Exceeded",
+ text: "You can only upload files up to 10 megabytes in size."
+ })
+ failCallback()
+ @uploading = false
+ @changed()
+ return
+
+
+ formData.append('lesson_session_id', @lessonId);
+ formData.append('attachment_type', 'notation')
+
+ @app.layout.showDialog('music-notation-upload-dialog')
+
+ rest.uploadMusicNotations(formData)
+ .done((response) => @doneUploadingNotatations(notations, response, doneCallback, failCallback))
+ .fail((jqXHR) => @failUploadingNotations(jqXHR, failCallback))
+
+ doneUploadingNotatations: (notations, response, doneCallback, failCallback) ->
+ @uploading = false
+ @changed()
+ error_files = [];
+ $.each(response, (i, music_notation) => (
+ if music_notation.errors
+ error_files.push(notations[i].name)
+ )
+ )
+ if error_files.length > 0
+ failCallback()
+ @app.notifyAlert("Failed to upload notations.", error_files.join(', '));
+ else
+ doneCallback()
+
+ 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.
+ @app.notify({
+ title: "Maximum Music Notation Size Exceeded",
+ text: "You can only upload files up to 10 megabytes in size."
+ })
+ else
+ @app.notifyServerError(jqXHR, "Unable to upload music notations");
+
+ onUploadAudios: (notations, doneCallback, failCallback) ->
+ logger.debug("beginning upload of audio", notations)
+ @uploading = true
+ @changed()
+
+ formData = new FormData()
+ maxExceeded = false;
+ $.each(notations, (i, file) => (
+ max = 10 * 1024 * 1024;
+ if file.size > max
+ maxExceeded = true
+ return false
+
+ formData.append('files[]', file)
+ ))
+
+ if maxExceeded
+ @app.notify({
+ title: "Maximum Music Audio Size Exceeded",
+ text: "You can only upload files up to 10 megabytes in size."
+ })
+ failCallback()
+ @uploading = false
+ @changed()
+ return
+
+
+ formData.append('lesson_session_id', @lessonId);
+ formData.append('attachment_type', 'audio')
+
+ @app.layout.showDialog('music-notation-upload-dialog')
+
+ rest.uploadMusicNotations(formData)
+ .done((response) => @doneUploadingAudios(notations, response, doneCallback, failCallback))
+ .fail((jqXHR) => @failUploadingAudios(jqXHR, failCallback))
+
+ doneUploadingAudios: (notations, response, doneCallback, failCallback) ->
+ @uploading = false
+ @changed()
+ error_files = [];
+ $.each(response, (i, music_notation) => (
+ if music_notation.errors
+ error_files.push(notations[i].name)
+ )
+ )
+ if error_files.length > 0
+ failCallback()
+ @app.notifyAlert("Failed to upload audio files.", error_files.join(', '));
+ else
+ doneCallback()
+
+ failUploadingAudios: (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.
+ @app.notify({
+ title: "Maximum Music Audio Size Exceeded",
+ text: "You can only upload files up to 10 megabytes in size."
+ })
+ else
+ @app.notifyServerError(jqXHR, "Unable to upload music audio files");
+
+
+ changed: () ->
+ this.trigger({lessonId: @lessonId, uploading: @uploading})
+ }
+)
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/stores/AvatarStore.js.coffee b/web/app/assets/javascripts/react-components/stores/AvatarStore.js.coffee
new file mode 100644
index 000000000..bf3a4d381
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/AvatarStore.js.coffee
@@ -0,0 +1,298 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+rest = new context.JK.Rest()
+
+@AvatarStore = Reflux.createStore(
+ {
+
+ listenables: @AvatarActions
+ selection: null
+ updatingAvatar: false
+ targetCropSize: 88
+ largerCropSize: 200
+ currentFpfile: null
+ signedCurrentFpfile: null
+
+ init: ->
+ this.listenTo(context.AppStore, this.onAppInit)
+
+ onAppInit: (@app) ->
+
+ onStart: (target, type) ->
+ logger.debug("AvatarStore start", target.id, type)
+ @target = target
+ @type = type
+
+ @selection = null
+ @updatingAvatar = false
+
+ @currentFpfile = @determineCurrentFpfile();
+ @currentCropSelection = @determineCurrentSelection(@target);
+ @signedCurrentFpFile = null
+ @changed()
+ @signFpfile()
+
+ @app.layout.showDialog('upload-avatar')
+
+ onSelect: (selection) ->
+ @selection = select
+ @changed()
+
+ onPick: () ->
+
+ rest.generateSchoolFilePickerPolicy({id: @target.id})
+ .done((filepickerPolicy) =>
+ @pickerOpen = true
+ @changed()
+ window.filepicker.setKey(gon.fp_apikey);
+ window.filepicker.pickAndStore({
+ mimetype: 'image/*',
+ maxSize: 10000*1024,
+ policy: filepickerPolicy.policy,
+ signature: filepickerPolicy.signature
+ }, { path: @createStorePath(@target), access: 'public' },
+ (fpfiles) => (
+ @pickerOpen = false
+ @afterImageUpload(fpfiles[0]);
+ ),
+ (fperror) => (
+ @pickerOpen = false
+ @changed()
+
+ if fperror.code != 101 # 101 just means the user closed the dialog
+ alert("unable to upload file: " + JSON.stringify(fperror))
+
+ )
+ )
+ )
+ .fail(@app.ajaxError)
+
+ afterImageUpload: (fpfile) ->
+ logger.debug("afterImageUploaded")
+ $.cookie('original_fpfile', JSON.stringify(fpfile));
+
+ @currentFpfile = fpfile
+ @signedCurrentFpfile = null
+ @currentCropSelection = null
+ @changed()
+ @signFpfile()
+
+ signFpfile: () ->
+ rest.generateSchoolFilePickerPolicy({ id: @target.id})
+ .done((policy) => (
+ @signedCurrentFpfile = @currentFpfile.url + '?signature=' + policy.signature + '&policy=' + policy.policy;
+ @changed()
+ ))
+
+ getState: () ->
+ {
+ target: @target,
+ type: @type,
+ selection: @selection,
+ updatingAvatar: @updatingAvatar,
+ currentFpfile: @currentFpfile,
+ currentCropSelection: @currentCropSelection,
+ signedCurrentFpfile: @signedCurrentFpfile
+ }
+
+ changed: () ->
+ state = @getState()
+ logger.debug("change: ", state)
+ @trigger(state)
+
+ onSelect: (event) ->
+ @selection = event;
+
+
+ delete: () ->
+
+ update: () ->
+
+ getPolicy: () ->
+
+
+ updateAvatarSuccess: (response) ->
+ $.cookie('original_fpfile', null)
+
+ @target = response
+
+ # notify any listeners that the avatar changed
+ # userDropdown.loadMe();
+ # $('.avatar_large img').trigger('avatar_changed', [self.userDetail.photo_url]);
+ # $(document).triggerHandler(EVENTS.USER_UPDATED, response);
+ @app.notify({ title: "Logo Updated", text: "You have updated your avatar successfully." }, null, true);
+
+ if @type == 'school'
+ window.SchoolActions.refresh()
+
+ @app.layout.closeDialog('upload-avatar')
+
+
+ # retrieves a file that has not yet been used as an avatar (uploaded, but not cropped)
+ getWorkingFpfile: () ->
+ return JSON.parse($.cookie('original_fpfile'))
+
+
+ createStorePath: (target) ->
+ gon.fp_upload_dir + '/' + @type + '/' + target.id + '/'
+
+
+ createOriginalFilename: (target) ->
+ # get the s3
+ if target.original_fpfile
+ fpfile = JSON.parse(target.original_fpfile)
+ else
+ fpfile = null
+ return 'original_avatar.jpg'
+
+
+
+ determineCurrentFpfile: () ->
+ #precedence is as follows:
+ # * tempOriginal: if set, then the user is working on a new upload
+ # * storedOriginal: if set, then the user has previously uploaded and cropped an avatar
+ # * null: neither are set above
+
+ tempOriginal = @getWorkingFpfile()
+ if @target.original_fpfile
+ storedOriginal = JSON.parse(@target.original_fpfile)
+ else
+ storedOriginal = null
+
+ if tempOriginal
+ tempOriginal
+ else
+ storedOriginal
+
+ determineCurrentSelection: (target) ->
+ # if the cookie is set, don't use the storage selection, just default to null
+ if $.cookie('original_fpfile') == null
+ result = target.crop_selection
+ else
+ result = null
+
+ if result?
+ JSON.parse(result)
+ else
+ null
+
+ onDelete: () ->
+ if @updatingAvatar
+ return
+
+ @updatingAvatar = true
+ @changed()
+
+ rest.deleteSchoolAvatar({id: @target.id}).done((response) => @deleteDone(response)).fail((jqXHR) => @deleteFail(jqXHR))
+
+ deleteDone: (response) ->
+ @currentFpfile = null
+ @signedCurrentFpfile = null
+ @updatingAvatar = false
+ @selection = null
+ @currentCropSelection = null
+ if @type == 'school'
+ window.SchoolActions.refresh()
+
+ @app.layout.closeDialog('upload-avatar');
+
+ @changed()
+
+ deleteFail: (jqXHR) ->
+ $.cookie('original_fpfile', null)
+ @currentFpfile = null
+ @signedCurrentFpfile = null
+ @selection = null
+ @updatingAvatar = false
+ @currentCropSelection = null
+ @changed()
+ @app.ajaxError(jqXHR)
+ onUpdate: () ->
+ if @updatingAvatar
+ return
+
+ if @selection?
+ @updatingAvatar = true
+ @changed()
+ currentSelection = @selection
+ logger.debug("Converting...");
+ fpfile = @determineCurrentFpfile();
+
+ rest.generateSchoolFilePickerPolicy({ id: @target.id, handle: fpfile.url, convert: true })
+ .done((filepickerPolicy) =>
+ window.filepicker.setKey(gon.fp_apikey)
+ window.filepicker.convert(fpfile, {
+ crop: [
+ Math.round(currentSelection.x),
+ Math.round(currentSelection.y),
+ Math.round(currentSelection.w),
+ Math.round(currentSelection.w)],
+ fit: 'crop',
+ format: 'jpg',
+ quality: 90,
+ policy: filepickerPolicy.policy,
+ signature: filepickerPolicy.signature
+ }, { path: @createStorePath(@target) + 'cropped-' + new Date().getTime() + '.jpg', access: 'public' },
+ (cropped) => (@scale(cropped))
+ )
+ )
+ else
+ @app.notify( { title: "Upload an Avatar First", text: "To update your avatar, first you must upload an image using the UPLOAD button"}, null, true);
+
+ scale: (cropped) ->
+ logger.debug("converting cropped");
+
+ rest.generateSchoolFilePickerPolicy({id: @target.id, handle: cropped.url, convert: true})
+ .done((filepickerPolicy) => (
+ window.filepicker.convert(cropped, {
+ height: @targetCropSize,
+ width: @targetCropSize,
+ fit: 'scale',
+ format: 'jpg',
+ quality: 75,
+ policy: filepickerPolicy.policy,
+ signature: filepickerPolicy.signature
+ }, { path: @createStorePath(@target), access: 'public' },
+ (scaled) => (@scaledLarger(scaled, cropped, filepickerPolicy)),
+ (fperror) => (@handleFpError(fperror)))
+ ))
+ .fail(@app.ajaxError)
+
+ scaledLarger: (scaled, cropped, filepickerPolicy) ->
+ window.filepicker.convert(cropped, {
+ height: @largerCropSize,
+ width: @largerCropSize,
+ fit: 'scale',
+ format: 'jpg',
+ quality: 75,
+ policy: filepickerPolicy.policy,
+ signature: filepickerPolicy.signature
+ }, { path: @createStorePath(@target) + 'large.jpg', access: 'public' },
+ (scaledLarger) => (@updateServer(scaledLarger, scaled, cropped)),
+ (fperror) => (@handleFpError(fperror))
+ )
+
+ updateServer: (scaledLarger, scaled, cropped) ->
+ logger.debug("converted and scaled final image %o", scaled);
+ rest.updateSchoolAvatar({
+ id: @target.id,
+ original_fpfile: @determineCurrentFpfile(),
+ cropped_fpfile: scaled,
+ cropped_large_fpfile: scaledLarger,
+ crop_selection: @selection
+ })
+ .done((response) => @updateAvatarSuccess(response))
+ .fail(@app.ajaxError)
+ .always(() => (
+ @updatingAvatar = false
+ @changed()
+ ))
+
+ handleFpError: (fperror) ->
+ alert("unable to scale larger selection. error code: " + fperror.code);
+ @updatingAvatar = false;
+ @changed()
+
+ }
+)
diff --git a/web/app/assets/javascripts/react-components/stores/BroadcastStore.js.coffee b/web/app/assets/javascripts/react-components/stores/BroadcastStore.js.coffee
index e6b9b297b..1d5968687 100644
--- a/web/app/assets/javascripts/react-components/stores/BroadcastStore.js.coffee
+++ b/web/app/assets/javascripts/react-components/stores/BroadcastStore.js.coffee
@@ -1,7 +1,7 @@
$ = jQuery
context = window
logger = context.JK.logger
-broadcastActions = context.JK.Actions.Broadcast
+broadcastActions = @BroadcastActions
rest = context.JK.Rest()
@@ -11,19 +11,146 @@ BroadcastStore = Reflux.createStore(
{
listenables: broadcastActions
+ currentSession: null
+ currentLesson: null
+ broadcast: null
+ currentLessonTimer: null
+ teacherFault: false
+ isJamClass: false
+ init: ->
+ this.listenTo(context.AppStore, this.onAppInit)
+ this.listenTo(context.SessionStore, this.onSessionChange)
+ this.listenTo(context.NavStore, this.onNavChange)
+
+ onAppInit: (@app) ->
+ @lessonUtils = context.JK.LessonUtils
+
+ lessonTick: () ->
+ @timeManagement()
+ @changed()
+
+ timeManagement: () ->
+ lastCheck = $.extend({}, @currentLesson)
+ lessonSession = @currentLesson
+ lessonSession.until = @lessonUtils.getTimeRemaining(lessonSession.scheduled_start)
+ if lessonSession.until.total < 0
+ # we are past the start time
+ if lessonSession.until.total > -(10 * 60 * 1000) # 10 minutes
+ lessonSession.initialWindow = true
+ else
+ lessonSession.initialWindow = false
+ lessonSession.beforeSession = false
+ else
+ # we are before the due time
+ lessonSession.initialWindow = false
+ lessonSession.beforeSession = true
+
+ # if we've transitioned to a new window
+
+ if !lessonSession.beforeSession && ((lastCheck.initialWindow || !lastCheck.initialWindow?) && !lessonSession.initialWindow)
+ logger.debug("BroadcastStore: lesson session 'initial window' transition")
+ rest.getLessonAnalysis({id: lessonSession.id}).done((response) => @lessonAnalysisDone(response)).fail(@app.ajaxError)
+
+ lessonAnalysisDone: (@analysis) ->
+
+ if !@currentLesson?
+ logger.debug("BroadcastStore: ignoring lessonAnalysisDone")
+ return
+
+ if @analysis.status == 'completed'
+ logger.debug("BroadcastStore: lesson is over")
+ @currentLesson.completed = true
+ @currentLesson.success = @analysis.success
+ @clearTimer()
+ @changed()
+ else if @analysis.analysis.reason != 'teacher_fault'
+ logger.debug("BroadcastStore: not teacher fault; clearing lesson info")
+ @clearLesson()
+ else
+ logger.debug("BroadcastStore: teacher is at fault")
+ @teacherFault = true
+ @clearTimer()
+ @changed()
+
+ clearLesson: () ->
+ if @currentLesson?
+ @currentLesson = null
+ @clearTimer()
+ @teacherFault = false
+ @changed()
+
+ clearTimer: () ->
+ if @currentLessonTimer?
+ clearInterval(@currentLessonTimer)
+ @currentLessonTimer = null
+
+ onNavChange: (nav) ->
+ path = nav.screenPath.toLowerCase()
+ @isJamClass = path.indexOf('jamclass') > -1 || path.indexOf('teacher') > -1
+ @changed()
+
+ onSessionChange: (session) ->
+
+ @session = session
+ currentSession = session.session
+ if currentSession? && currentSession.lesson_session? && session.inSession()
+
+ @currentSession = currentSession
+
+ lessonSession = currentSession.lesson_session
+ # so that receivers can check type of info coming at them via one-way events
+ lessonSession.isLesson = true
+
+ if lessonSession.status == 'completed'
+ lessonSession.completed = true
+ lessonSession.success = lessonSession.success
+ #else
+ # rest.getLessonAnalysis({id: lessonSession.id}).done((response) => @lessonAnalysisDone(response)).fail(@app.ajaxError)
+
+ @currentLesson = lessonSession
+ @timeManagement()
+ logger.debug("BroadcastStore: currentLesson until: ", @currentLesson.until, lessonSession.scheduled_start)
+ if !@currentLessonTimer?
+ @currentLessonTimer = setInterval((() => @lessonTick()), 1000)
+ @changed()
+
+ else
+ @clearLesson()
+
onLoad: () ->
logger.debug("loading broadcast notification...")
onLoadCompleted: (response) ->
- logger.debug("broadcast notification sync completed")
- this.trigger(response)
+ if response.id?
+ logger.debug("broadcast notification sync completed")
+ @broadcast = response
+ @changed()
+
onLoadFailed: (jqXHR) ->
if jqXHR.status != 404
logger.error("broadcast notification sync failed")
onHide: () ->
- this.trigger(null)
+ @broadcast = null
+ @changed()
+
+ changed: () ->
+ if @currentLesson?
+ @currentLesson.isStudent == @currentLesson.student_id == context.JK.currentUserId
+ @currentLesson.isTeacher = !@currentLesson.isStudent
+ @currentLesson.teacherFault = @teacherFault
+ @currentLesson.teacherPresent = @session.findParticipantByUserId(@currentLesson.teacher_id)
+ @currentLesson.studentPresent = @session.findParticipantByUserId(@currentLesson.student_id)
+ if (@currentLesson.teacherPresent? && @currentLesson.isStudent) || (@currentLesson.studentPresent? && @currentLesson.isTeacher)
+ # don't show anything if the other person is there
+ this.trigger(null)
+ else
+ this.trigger(@currentLesson)
+ else if @isJamClass
+ this.trigger({isJamClass: true})
+ else
+ this.trigger(@broadcast)
}
)
diff --git a/web/app/assets/javascripts/react-components/stores/BrowserMediaPlaybackStore.js.coffee b/web/app/assets/javascripts/react-components/stores/BrowserMediaPlaybackStore.js.coffee
new file mode 100644
index 000000000..d46182185
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/BrowserMediaPlaybackStore.js.coffee
@@ -0,0 +1,84 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+PLAYBACK_MONITOR_MODE = context.JK.PLAYBACK_MONITOR_MODE
+RecordingActions = @RecordingActions
+BrowserMediaActions = @BrowserMediaActions
+
+@BrowserMediaPlaybackStore = Reflux.createStore(
+ {
+ listenables: @BrowserMediaPlaybackActions
+
+ playbackStateChanged: false
+ positionUpdateChanged: false
+ currentTimeChanged: false
+ playbackState: null
+ positionMs: 0
+ durationMs: 0
+ isRecording: false
+ sessionHelper: null
+
+ init: () ->
+ this.listenTo(context.BrowserMediaStore, this.onBrowserMediaChange);
+
+ onBrowserMediaChange: (changes) ->
+ @onPositionUpdate(PLAYBACK_MONITOR_MODE.BROWSER_MEDIA)
+
+ onCurrentTimeChanged: (time) ->
+ @time = time
+ @currentTimeChanged = true
+ @issueChange()
+
+ onMediaStartPlay: (data) ->
+ BrowserMediaActions.play.trigger()
+
+ onMediaStopPlay: (data) ->
+
+ if !data.endReached
+ BrowserMediaActions.stop()
+
+ onMediaPausePlay: (data) ->
+ # if a JamTrack is open, and the user hits 'pause' or 'stop', we need to automatically stop the recording
+ if !data.endReached
+ BrowserMediaActions.pause()
+
+ onMediaChangePosition: (data) ->
+ seek = data.positionMs;
+
+ BrowserMediaActions.seek(seek);
+
+ issueChange: () ->
+
+ @state =
+ playbackState: @playbackState
+ playbackStateChanged: @playbackStateChanged
+ positionUpdateChanged: @positionUpdateChanged
+ currentTimeChanged: @currentTimeChanged
+ positionMs: @positionMs
+ durationMs: @durationMs
+ isPlaying: @isPlaying
+ time: @time
+
+ this.trigger(@state)
+ @playbackStateChanged = false
+ @positionUpdateChanged = false
+ @currentTimeChanged = false
+
+ onPlaybackStateChange: (text) ->
+ @playbackState = text
+ @playbackStateChanged = true
+
+ @issueChange()
+
+ onPositionUpdate: (playbackMode) ->
+ if playbackMode == PLAYBACK_MONITOR_MODE.BROWSER_MEDIA
+ @positionMs = BrowserMediaStore.onGetPlayPosition() || 0
+ @durationMs = BrowserMediaStore.onGetPlayDuration() || 0;
+ @isPlaying = BrowserMediaStore.playing;
+ else
+ raise 'Only BROWSER_MEDIA is supported'
+ @positionUpdateChanged = true
+ @issueChange()
+
+ }
+)
diff --git a/web/app/assets/javascripts/react-components/stores/BrowserMediaStore.js.coffee b/web/app/assets/javascripts/react-components/stores/BrowserMediaStore.js.coffee
new file mode 100644
index 000000000..a4fa0fc91
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/BrowserMediaStore.js.coffee
@@ -0,0 +1,206 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+rest = context.JK.Rest()
+EVENTS = context.JK.EVENTS
+
+BrowserMediaActions = @BrowserMediaActions
+
+Howler._enableiOSAudio()
+
+@BrowserMediaStore = Reflux.createStore(
+ {
+ listenables: BrowserMediaActions
+ audio: null
+ loaded: false
+ loading: false
+ playing: false
+ paused: false
+ load_error: false
+ id: null
+ media_type: null
+ cachedAudio: []
+ cache_size: 10
+ preload: true
+
+ playbackState:(position, time) ->
+ state = {}
+ if @playing
+ state.playbackState = 'play_start'
+ else if @paused
+ state.playbackState = 'play_pause'
+ else
+ state.playbackState = 'play_stop'
+
+ state.playbackStateChanged = !@state? || @state.isPlaying != @playing || @state.paused != @paused
+ state.positionUpdateChanged = !@state? || @state.positionMs != position
+ state.currentTimeChanged = !@state? || @state.time != time
+ state
+
+ changed: () ->
+ position = @onGetPlayPosition()
+ time = context.JK.prettyPrintSeconds(parseInt(position / 1000))
+ playbackState = @playbackState(position, time)
+
+ # XXX: how to deal with duration? no mention in Howler API
+
+ target = {id: @id, isPlaying: @playing, loaded: @loaded, paused: @paused, loading: @loading, load_error: @load_error, positionMs: position, time: time, durationMs: @onGetPlayDuration()}
+ $.extend(true, target, playbackState)
+ @state = target
+ @trigger(@state)
+
+ onLoad: (id, urls, media_type) ->
+
+ if @loading
+ logger.error("you can't switch to different audio while loading due to weird errors seen in Howler")
+ window.location.reload()
+ return
+
+ if @audio
+ @audio.stop()
+ @loaded = false
+ @loading = false
+ @playing = false
+ @paused = false
+ @load_error = false
+
+ @audio = null
+ @id = id
+ @media_type = media_type
+ @loading = @preload
+ @playing = false
+ @paused = false
+
+
+ for cacheItem in @cachedAudio
+ if cacheItem.id == id
+ # items in our own cache are only there if we saw that it fully loaded
+ @audio = cacheItem.audio
+ @loaded = true
+ @loading = false
+
+ unless @audio?
+ @audio = new Howl({
+ src: urls,
+ autoplay: false,
+ loop: false,
+ volume: 1,
+ preload: @preload,
+ onend: @onAudioEnded,
+ onload: @onAudioLoaded,
+ onloaderror: @onAudioLoadError,
+ onpause: @onAudioPause,
+ onplay: @onAudioPlay
+ })
+ @audio.jkid = id
+
+ @changed()
+
+ onPlay: () ->
+ logger.debug("BrowerMediaStore:play")
+ if @audio
+ @playing = true
+ @paused = false
+ @audio.play()
+
+ try
+ if !@audio.recorded_play
+ rest.postUserEvent({name: @media_type + '_play'}) if @media_type?
+ context.stats.write('web.' + @media_type + '.play', {
+ value: 1,
+ user_id: context.JK.currentUserId,
+ user_name: context.JK.currentUserName
+ }) if @media_type
+
+ @audio.recorded_play = true
+ catch e
+ logger.warn("BrowserMediaStore: unable to post user event")
+
+ onPause: () ->
+ if @audio
+ @playing = false
+ @paused = true
+ @audio.pause()
+
+ onStop: () ->
+ if @audio
+ @playing = false
+ @paused = false
+ try
+ @audio.pause()
+ catch e
+ @logger.info("unable to pause on stop", e)
+ try
+ @audio.seek(0)
+ catch e
+ @logger.info("unable to seek to beginning on stop", e)
+
+
+ onSeek: (pos) ->
+ if @audio
+ console.log("seek time", pos)
+ @audio.seek(pos / 1000)
+
+ onGetPlayPosition: () ->
+ if @audio
+ try
+ position = @audio.seek()
+ if position == @audio
+ return 0
+ position * 1000
+ catch e
+ return 0
+ else
+ 0
+
+ onGetPlayDuration: () ->
+ if @audio
+ # XXX : how to determine duration?
+ try
+ duration = @audio.duration()
+ return Math.round(duration) * 1000
+ catch e
+ return 0
+ else
+ 0
+
+ onAudioEnded: () ->
+ logger.debug("onAudioEnded")
+ @playing = false
+ @changed()
+
+ onAudioLoaded: () ->
+ logger.debug("onAudioLoaded", arguments)
+ @loaded = true
+ @loading = false
+
+ # add audio to cache, and ageout cached audio items if more than 10 \
+ if @cachedAudio.length >= @cache_size
+ audio = @cachedAudio.shift()
+ try
+ audio.audio.unload()
+ catch e
+ logger.error("unable to unload aged audio", @audio)
+
+ @cachedAudio.push({id: @audio.jkid, audio: @audio})
+ @changed()
+
+ onAudioLoadError: () ->
+ logger.debug("onAudioLoadError", arguments)
+ @load_error = true
+ @loading = false
+ @changed()
+
+ onAudioPause: () ->
+ logger.debug("onAudioPause")
+
+ @changed()
+
+ onAudioPlay: () ->
+ logger.debug("onAudioPlay")
+ @playing = true
+ @paused = false
+ @changed()
+
+ }
+)
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/stores/CallbackStore.js.coffee b/web/app/assets/javascripts/react-components/stores/CallbackStore.js.coffee
new file mode 100644
index 000000000..43f545ecf
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/CallbackStore.js.coffee
@@ -0,0 +1,24 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+SessionActions = @SessionActions
+JamBlasterActions = @JamBlasterActions
+
+@CallbackStore = Reflux.createStore(
+ {
+ init: () ->
+ # Register with the app store to get @app
+ this.listenTo(context.AppStore, this.onAppInit)
+
+ onAppInit: (@app) ->
+ if context.jamClient.RegisterGenericCallBack?
+ context.jamClient.RegisterGenericCallBack('CallbackActions.genericCallback')
+
+ onGenericCallback: (map) ->
+ if map.cmd == 'join_session'
+ SessionActions.joinSession(map['music_session_id'])
+ else if map.cmd == 'client_pair_state'
+ JamBlasterActions.pairState(map)
+
+ }
+)
diff --git a/web/app/assets/javascripts/react-components/stores/ChatStore.js.coffee b/web/app/assets/javascripts/react-components/stores/ChatStore.js.coffee
new file mode 100644
index 000000000..959b6b5e6
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/ChatStore.js.coffee
@@ -0,0 +1,250 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+SessionStore = context.SessionStore
+
+@ChatStore = Reflux.createStore(
+ {
+ listenables: @ChatActions
+
+ limit: 20,
+ currentPage: 0,
+ next: null,
+ channel: 'global',
+ systemMsgId: 0
+ msgs: {global:[], session:[]}
+ max_global_msgs: 100
+ channelType: null
+
+ init: () ->
+ # Register with the app store to get @app
+ this.listenTo(context.AppStore, this.onAppInit)
+ this.listenTo(context.UserActivityStore, this.onUserActivityChanged)
+
+ onAppInit: (@app) ->
+ if context.JK.currentUserId?
+ @fetchHistory()
+
+ onUserActivityChanged: (state) ->
+
+ if !gon.chat_blast
+ systemMsg = {}
+ @systemMsgId = @systemMsgId + 1
+ systemMsg.sender_name = 'System'
+ systemMsg.sender_id = 'system'
+ systemMsg.msg_id = @systemMsgId
+ systemMsg.created_at = new Date().toISOString()
+ systemMsg.channel = 'global'
+ if state.active
+ systemMsg.msg = "You've come back!"
+ else
+ systemMsg.msg = "You've become inactive. Any global chat messages while away will be missed."
+
+ @onMsgReceived(systemMsg)
+
+ # after the animation to open the chat panel is done, do it!
+ onFullyOpened: () ->
+ @changed()
+
+ # called from ChatPanel
+ 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?
+ channel = @channel
+
+
+ query = {type: 'CHAT_MESSAGE', limit:@limit, page: @currentPage, channel: channel};
+
+ if channel == 'session' && SessionStore.currentSessionId?
+ query.music_session = SessionStore.currentSessionId
+
+ if channel == 'lesson' && @lessonSessionId?
+ query.lesson_session = @lessonSessionId
+
+ if @next
+ query.start = next
+ return query
+
+ onInitializeLesson: (lessonSessionId) ->
+ @lessonSessionId = lessonSessionId
+ @channelType = 'lesson'
+
+ @fetchHistory('lesson')
+
+ fetchHistory: (channel = null) ->
+
+ if !channel?
+ channel = @channel
+
+ # load previous chat messages
+ rest.getChatMessages(@buildQuery(channel))
+ .done((response) =>
+ @onLoadMessages(channel, response)
+ ).fail((jqXHR) =>
+ @app.notifyServerError(jqXHR, 'Unable to Load Session Conversations')
+ )
+
+ onEmptyChannel: (channel) ->
+ @msgs[channel] = []
+ @changed()
+
+ convertServerMessages: (chats) ->
+ converted = []
+ for chat in chats
+ convert = {}
+ convert.sender_name = chat.user?.name
+ convert.sender_id = chat.user_id
+ convert.msg = chat.message
+ convert.msg_id = chat.id
+ 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
+
+ # called from ChatPanel
+ onLoadMessages: (channel, msgs) ->
+
+ if channel == 'lesson'
+ channel = @lessonSessionId
+
+ channelMsgs = @msgs[channel]
+
+ if !channelMsgs?
+ channelMsgs = []
+ @msgs[channel] = channelMsgs
+
+ history = @convertServerMessages(msgs.chats)
+
+ for oldMsg in history
+ skip = false
+ for channelMsg in channelMsgs
+ if oldMsg.msg_id == channelMsg.msg_id
+ skip = true
+ break
+
+ if !skip
+ channelMsgs.unshift(oldMsg)
+
+ #totalMsgs = history.concat(channelMsgs)
+ channelMsgs.sort((a, b) =>
+ c = new Date(a.created_at)
+ d = new Date(b.created_at)
+ return (c > d) - (c < d)
+ )
+ @msgs[channel] = channelMsgs
+
+ @changed()
+
+ onMsgReceived: (msg) ->
+
+ effectiveChannel = msg.channel
+
+ if msg.channel == 'lesson'
+ effectiveChannel = msg.lesson_session_id
+
+ 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?
+ channelMsgs = []
+ @msgs[effectiveChannel] = channelMsgs
+
+ channelMsgs.push(msg)
+
+ # don't let the global channel grow indefinitely
+ if effectiveChannel == 'global'
+ while channelMsgs.length > @max_global_msgs
+ channelMsgs.shift()
+
+ @changed()
+
+ buildMessage:(msg, target_user, channel) ->
+ payload = {message: msg}
+ if channel == 'session'
+ payload.music_session = SessionStore.currentSessionId
+ else if channel == 'lesson'
+ payload.lesson_session = @lessonSessionId
+ payload.channel = channel
+ payload.client_id = @app.clientId
+ payload.target_user = target_user
+ payload
+
+ onActivateChannel: (channel) ->
+ logger.debug("onActivateChannel: " + channel)
+ @channel = channel
+ if @channel != 'lesson'
+ @channelType = null
+ @fetchHistory()
+ @changed()
+
+ onSendMsg: (msg, done, fail, target_user = null, channel = null) ->
+ if !channel?
+ channel = @channel
+ rest.createChatMessage(@buildMessage(msg, target_user, channel))
+ .done((response) =>
+
+ done(response)
+
+ if response.channel == 'session'
+ @onMsgReceived({
+ sender_name: "me",
+ sender_id: context.JK.currentUserId,
+ msg: msg,
+ msg_id: response.id,
+ created_at: response.created_at,
+ channel: response.channel
+ })
+ else if response.channel == 'lesson'
+ @onMsgReceived({
+ sender_name: "me",
+ sender_id: context.JK.currentUserId,
+ msg: msg,
+ msg_id: response.id,
+ created_at: response.created_at,
+ channel: response.lesson_session_id
+ })
+ )
+ .fail((jqXHR) =>
+ fail(jqXHR)
+ )
+
+ # unused/untested. send direct to gateway
+ onSendMsgInstant: (msg, channel = null) ->
+ logger.debug("ChatStore.sendMsg", msg)
+
+ if !channel?
+ channel = @channel
+ window.JK.JamServer.sendChatMessage(channel, msg)
+
+ getState: () ->
+ return {msgs: @msgs, channel: @channel, channelType: @channelType, lessonSessionId: @lessonSessionId}
+
+ changed: () ->
+ @trigger(@getState())
+ }
+)
diff --git a/web/app/assets/javascripts/react-components/stores/ConfigureTracksStore.js.coffee b/web/app/assets/javascripts/react-components/stores/ConfigureTracksStore.js.coffee
new file mode 100644
index 000000000..1dd5c7ed3
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/ConfigureTracksStore.js.coffee
@@ -0,0 +1,579 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+ASSIGNMENT = context.JK.ASSIGNMENT
+VOICE_CHAT = context.JK.VOICE_CHAT
+MAX_TRACKS = context.JK.MAX_TRACKS
+MAX_OUTPUTS = context.JK.MAX_OUTPUTS
+gearUtils = context.JK.GearUtils
+AUDIO_UNIT_TYPE_ID = 0
+MIDI_TRACK = context.JK.MIDI_TRACK
+
+###
+
+ QVariantMap scanForPlugins();
+QVariantMap VSTListVsts();
+void VSTClearAll();
+QVariantMap VSTSetTrackAssignment(const QVariantMap vst, const QString& trackId);
+QVariantMap VSTListTrackAssignments();
+void VSTShowHideGui(bool show,const QString& trackId);
+void VST_ScanForMidiDevices();
+QVariantMap VST_GetMidiDeviceList();
+bool VST_EnableMidiForTrack(const QString& trackId, bool enableMidi, int midiDeviceIndex);
+QVariantMap listSearchPaths();
+void addSearchPath(int typeId, QString pathToAdd);
+void removeSearchPath(int typeId, QString pathToRemove);
+###
+
+@ConfigureTracksStore = Reflux.createStore(
+ {
+ listenables: ConfigureTracksActions
+
+ musicPorts: {inputs: [], outputs: []}
+ trackNumber: null
+ editingTrack: null
+ vstPluginList: {vsts: []}
+ vstTrackAssignments: {vsts: []}
+ attachedMidiDevices: {midiDevices: []}
+ midiTrackAssignments: {tracks: []}
+ scanPaths: {paths:[]}
+ scanningVsts: false
+ trackType: 'audio'
+ hasVst: true
+
+ init: () ->
+ this.listenTo(context.AppStore, this.onAppInit)
+ this.listenTo(context.MixerStore, this.onMixersChanged)
+ this.listenTo(context.PlatformStore, this.onPlatformChanged)
+
+ onAppInit: (@app) ->
+
+ editingTrackValid: () ->
+ true
+
+ onMixersChanged: (mixers) ->
+ @loadChannels()
+ @loadTrackInstruments()
+ @changed()
+
+ onPlatformChanged: (platform) ->
+ @platform = platform
+
+ onReset: (loadProfile) ->
+ logger.debug("ConfigureTracksStore:reset", this)
+ @trackNumber = null
+ @editingTrack = null
+
+ # you have to load the current profile in order to see track info, which we need
+ #if loadProfile
+ #currentProfile = context.jamClient.LastUsedProfileName();
+ #result = context.jamClient.FTUELoadAudioConfiguration(currentProfile);
+
+ @loadChannels()
+ @loadTrackInstruments()
+
+ #if force || context.jamClient.hasVstAssignment()
+ # @performVstScan()
+
+ @listVsts()
+ @performMidiScan()
+ @listPaths()
+ @changed()
+
+
+ onEnableVst: () ->
+ logger.debug("enabling VSTs")
+ context.jamClient.VSTLoad()
+
+ setTimeout((() =>
+ @listVsts()
+
+ @changed()
+ ), 250)
+
+ onTrySave: () ->
+ logger.debug("ConfigureTracksStore:trySave")
+ @trySave()
+
+ trySave: () ->
+
+ onVstScan: () ->
+ @performVstScan(true)
+
+ @changed()
+
+ performVstScan: (sendChanged) ->
+ #@hasVst = gon.global.vst_enabled & context.jamClient.hasVstHost()
+ logger.debug("hasVst", @hasVst)
+ if @hasVst
+ logger.debug("vstScan starting")
+ @scanningVsts = true
+ @scannedBefore = true
+ result = context.jamClient.VSTScan("window.ConfigureTracksStore.onVstScanComplete")
+
+ onClearVsts: () ->
+ context.jamClient.VSTClearAll()
+
+ setTimeout((() =>
+ @listVsts()
+
+ @changed()
+ ), 250)
+
+ onManageVsts:() ->
+ logger.debug("manage vst selected")
+ @app.layout.showDialog('manage-vsts-dialog')
+
+ onVstScanComplete: () ->
+ # XXX must wait a long time to get track assignments after scan/
+ logger.debug("vst scan complete")
+ @scanningVsts = false
+ setTimeout((() =>
+ @listVsts()
+ @changed()
+ ), 100 )
+
+ onVstChanged: () ->
+ setTimeout()
+ logger.debug("vst changed")
+
+ setTimeout((() =>
+ @listVsts()
+ @changed()
+ ), 0)
+
+ listPaths: () ->
+ @scanPaths = context.jamClient.VSTListSearchPaths()
+
+ # this comes from the JUCE library behavior
+ vstTypeId: () ->
+ if @platform.isWindows
+ logger.debug("vstTypeId is windows")
+ 0
+ else
+ logger.debug("vstTypeId is not-windows")
+ 1
+
+ onAddSearchPath: (path) ->
+ logger.debug("VSTAddSearchPath: " + path)
+ context.jamClient.VSTAddSearchPath(@vstTypeId(), path)
+ @listPaths()
+ @changed()
+
+ onRemoveSearchPath: (path) ->
+ logger.debug("VSTRemoveSearchPath: " + path)
+ context.jamClient.VSTRemoveSearchPath(@vstTypeId(), path)
+ @listPaths()
+ @changed()
+
+ onSelectVSTDirectory:() ->
+ context.jamClient.ShowSelectVSTScanDialog("window.ConfigureTracksStore.onVSTPathSelected")
+
+ onVSTPathSelected: (result) ->
+ success = result.success
+ path = result.vstPath
+
+ if success
+ logger.debug("vst path selected!", path)
+ @onAddSearchPath(path)
+ else
+ logger.debug("nothing selected")
+
+ listVsts: () ->
+
+ @vstPluginList = context.jamClient.VSTListVsts()
+ @vstTrackAssignments = context.jamClient.VSTListTrackAssignments()
+
+
+ onMidiScan: () ->
+ @performMidiScan()
+ @changed()
+
+ performMidiScan: () ->
+
+ if !@hasVst
+ logger.debug("performMidiScan skipped due to no VST")
+ return
+ context.jamClient.VST_ScanForMidiDevices();
+ @attachedMidiDevices = context.jamClient.VST_GetMidiDeviceList();
+
+ # trackNumber is 0-based, and optional
+ onShowVstSettings: (trackNumber) ->
+ if !@hasVst
+ logger.debug("onShowVstSettings skipped due to no VST")
+ return
+
+ if !trackNumber?
+ trackNumber = @trackNumber - 1 if @trackNumber?
+
+ logger.debug("show VST GUI", trackNumber)
+
+ context.jamClient.VSTShowHideGui(true, trackNumber) if trackNumber?
+
+
+ findMidiTrack: () ->
+ midi = null
+ for assignment in @trackAssignments.inputs.assigned
+ if assignment.assignment == MIDI_TRACK
+ midi = assignment
+ break
+ midi
+
+ removeMidiTrack: () ->
+ removeIndex = -1
+ for assignment, i in @trackAssignments.inputs.assigned
+ if assignment.assignment == MIDI_TRACK
+ # remove this
+ removeIndex = i
+ break
+
+ if removeIndex > -1
+ @trackAssignments.inputs.assigned.splice(removeIndex, 1)
+
+ defaultTrackInstrument: (trackNumber) ->
+ clientInstrument = context.jamClient.TrackGetInstrument(trackNumber)
+ if clientInstrument == 0
+ logger.debug("defaulting midi instrument for assignment #{trackNumber}")
+ # ensure that we always have an instrument set (50 = electric guitar
+ context.jamClient.TrackSetInstrument(trackNumber, 50)
+ clientInstrument = 50
+
+ context.JK.client_to_server_instrument_map[clientInstrument];
+ # the backend does not have a consistent way of tracking assigned inputs for midi.
+ # let's make it seem consistent
+ injectMidiToTrackAssignments: () ->
+ if @vstTrackAssignments?
+ for vst in @vstTrackAssignments.vsts
+ if vst.track == MIDI_TRACK - 1
+ if vst.midiDeviceIndex > -1
+
+ # first see if midi is already there
+ midi = @findMidiTrack()
+
+ if !midi?
+ instrument = @defaultTrackInstrument(MIDI_TRACK)
+
+ midi = [{assignment: MIDI_TRACK}]
+ midi.instrument_id = instrument?.server_id
+ midi.assignment = MIDI_TRACK
+ @trackAssignments.inputs.assigned.push(midi)
+ else
+ @removeMidiTrack()
+
+ changed: () ->
+
+ @injectMidiToTrackAssignments()
+
+ @editingTrack = []
+ @editingTrack.assignment = @trackNumber
+
+ if @trackNumber?
+
+ for inputsForTrack in @trackAssignments.inputs.assigned
+ if inputsForTrack.assignment == @trackNumber
+ @editingTrack = inputsForTrack
+ break
+
+
+ # slap on vst, if any, from list of vst assignments
+ for vst in @vstTrackAssignments.vsts
+ if vst.track == @editingTrack.assignment - 1
+ @editingTrack.vst = vst
+ @editingTrack.midiDeviceIndex = vst.midiDeviceIndex
+ break
+
+ for inputsForTrack in @trackAssignments.inputs.assigned
+ if vst.track == inputsForTrack.assignment - 1
+ inputsForTrack.vst = vst
+
+ if @editingTrack.vst?
+ logger.debug("current track has a VST assigned:" + @editingTrack.vst.file)
+
+
+ unscanned = !@scannedBefore && @vstPluginList.vsts.length <= 1
+
+ @item = {
+ unscanned: unscanned,
+ musicPorts: @musicPorts,
+ trackAssignments: @trackAssignments,
+ trackNumber: @trackNumber,
+ editingTrack: @editingTrack,
+ vstPluginList: @vstPluginList,
+ vstTrackAssignments: @vstTrackAssignments,
+ attachedMidiDevices: @attachedMidiDevices,
+ nextTrackNumber: @nextTrackNumber,
+ newTrack: @newTrack,
+ midiTrackAssignments: @midiTrackAssignments,
+ scanningVsts: @scanningVsts,
+ trackType: @trackType,
+ scanPaths: @scanPaths
+ }
+
+ @trigger(@item)
+
+ loadChannels: (forceInputsToUnassign, inputChannelFilter) ->
+ # inputChannelFilter is an optional argument that is used by the Gear Wizard.
+ # basically, if an input channel isn't in there, it's not going to be displayed
+ @musicPorts = context.jamClient.FTUEGetChannels()
+
+ # let's populate this bad boy
+ @trackAssignments = {inputs: {unassigned: [], assigned: [], chat: []}, outputs: {unassigned: [], assigned: []}}
+
+ nextTrackNumber = 0
+
+ for input in @musicPorts.inputs
+ if input.assignment == ASSIGNMENT.UNASSIGNED
+ @trackAssignments.inputs.unassigned.push(input)
+ else if input.assignment == ASSIGNMENT.CHAT
+ @trackAssignments.inputs.chat.push(input)
+ else
+ nextTrackNumber = input.assignment if input.assignment > nextTrackNumber
+
+ # make sure this assignment isn't already preset (you can have multiple inputs per 'track slot')
+ found = false
+ for assigned in @trackAssignments.inputs.assigned
+ if assigned.assignment == input.assignment
+ assigned.push(input)
+ found = true
+
+ if !found
+ initial = [input]
+ initial.assignment = input.assignment # store the assignment on the array itself, so we don't have to check inside the array for an input's assignment (which will all be the same)
+ @trackAssignments.inputs.assigned.push(initial)
+ for output in @musicPorts.outputs
+ if output.assignment == ASSIGNMENT.OUTPUT
+ @trackAssignments.outputs.assigned.push(output)
+ else
+ @trackAssignments.outputs.unassigned.push(output)
+
+ @nextTrackNumber = nextTrackNumber + 1
+
+
+ loadTrackInstruments: (forceInputsToUnassign) ->
+ for inputsForTrack in @trackAssignments.inputs.assigned
+
+ clientInstrument = context.jamClient.TrackGetInstrument(inputsForTrack.assignment)
+
+ if clientInstrument == 0
+ logger.debug("defaulting track instrument for assignment #{@trackNumber}")
+ # ensure that we always have an instrument set (50 = electric guitar
+ context.jamClient.TrackSetInstrument(inputsForTrack.assignment, 50)
+ clientInstrument = 50
+
+ instrument = context.JK.client_to_server_instrument_map[clientInstrument];
+
+ inputsForTrack.instrument_id = instrument?.server_id
+
+
+ onAssociateInputsWithTrack: (inputId1, inputId2) ->
+ return unless @trackNumber?
+
+ for inputs in @editingTrack
+ context.jamClient.TrackSetAssignment(inputs.id, true, ASSIGNMENT.UNASSIGNED)
+
+ if inputId1?
+ logger.debug("setting input1 #{inputId1} to #{@trackNumber}")
+ context.jamClient.TrackSetAssignment(inputId1, true, @trackNumber)
+
+ if inputId2?
+ logger.debug("setting input2 #{inputId2} to #{@trackNumber}")
+ context.jamClient.TrackSetAssignment(inputId2, true, @trackNumber)
+
+ result = context.jamClient.TrackSaveAssignments();
+
+ if(!result || result.length == 0)
+
+ else
+ context.JK.Banner.showAlert('Unable to save assignments. ' + result);
+
+ onAssociateInstrumentWithTrack: (instrumentId) ->
+ return unless @trackNumber?
+
+ logger.debug("context.jamClient.TrackSetInstrument(trackNumber, track.instrument_id)", @trackNumber, instrumentId)
+
+ clientInstrumentId = null
+ if instrumentId != null && instrumentId != ''
+ clientInstrumentId = context.JK.instrument_id_to_instrument[instrumentId].client_id
+ else
+ clientInstrumentId = 0
+
+ context.jamClient.TrackSetInstrument(@trackNumber, clientInstrumentId)
+
+ if @trackNumber == MIDI_TRACK
+ logger.debug("checking midi track for track instrument synchronization")
+ # keep artificial midi track in sync
+ midi = @findMidiTrack()
+ if midi?
+ logger.debug("synced midi track with #{instrumentId}")
+ midi.instrument_id = instrumentId
+
+
+ if(!result || result.length == 0)
+
+ else
+ context.JK.Banner.showAlert('Unable to save assignments. ' + result);
+
+
+ result = context.jamClient.TrackSaveAssignments()
+
+ if(!result || result.length == 0)
+
+ else
+ context.JK.Banner.showAlert('Unable to save assignments. ' + result);
+
+ onAssociateVSTWithTrack: (vst) ->
+
+ if !@hasVst
+ logger.debug("onAssociateVSTWithTrack skipped due to no VST")
+ return
+
+ if vst?
+ logger.debug("associating track:#{@trackNumber - 1} with VST:#{vst.file}")
+
+ found = null
+ for knownVst in @vstPluginList.vsts
+ if knownVst.file == vst.file
+ found = knownVst
+ break
+ if found?
+ context.jamClient.VSTSetTrackAssignment(found, @trackNumber - 1)
+ else
+ logger.error("unable to locate vst for #{vst}")
+ else
+ logger.debug("unassociated track:#{@trackNumber} with VST")
+ # no way to unset VST assignment yet
+
+ setTimeout((() => (
+ @listVsts()
+
+ @changed()
+ )), 250)
+
+ onCancelEdit: () ->
+ if @newTrack
+ for input in @editingTrack
+ context.jamClient.TrackSetAssignment(input.id, true, ASSIGNMENT.UNASSIGNED)
+ result = context.jamClient.TrackSaveAssignments()
+ if(!result || result.length == 0)
+
+ else
+ context.JK.Banner.showAlert('Unable to save assignments. ' + result);
+ else
+ logger.error("unable to process cancel for an existing track")
+
+ onDeleteTrack: (assignment) ->
+ logger.debug("deleting track with assignment #{assignment}")
+ if assignment != MIDI_TRACK
+ track = null
+ for inputsForTrack in @trackAssignments.inputs.assigned
+ if inputsForTrack.assignment == assignment
+ track = inputsForTrack
+ break
+
+ if track?
+ for input in inputsForTrack
+ context.jamClient.TrackSetAssignment(input.id, true, ASSIGNMENT.UNASSIGNED)
+ result = context.jamClient.TrackSaveAssignments()
+
+ if(!result || result.length == 0)
+
+ else
+ context.JK.Banner.showAlert('Unable to save assignments. ' + result);
+ else
+ logger.error("unable to find track to delete")
+ else
+ logger.debug("deleting midi track")
+ @onAssociateMIDIWithTrack(null)
+ @removeMidiTrack()
+ @changed()
+
+ onShowAddNewTrack: (type) ->
+
+ # check if we have what we need... namely, free ports
+
+ if type == 'audio'
+ if @trackAssignments.inputs.unassigned.length == 0
+ context.JK.Banner.showAlert('You have no more unassigned input ports.
You can free some up by editing an AUDIO track.')
+ return
+ @openLiveTrackDialog(@nextTrackNumber)
+ else
+ if @findMidiTrack()?
+ context.JK.Banner.showAlert('You have only one MIDI input.')
+ return
+ @openLiveTrackDialog(MIDI_TRACK)
+
+ onShowEditTrack: (trackNumber) ->
+ @openLiveTrackDialog(trackNumber)
+
+ openLiveTrackDialog: (trackNumber) ->
+ @trackNumber = trackNumber
+ logger.debug("opening live track dialog for track #{trackNumber}", @trackAssignments.inputs.assigned)
+
+ @newTrack = true
+ for inputsForTrack in @trackAssignments.inputs.assigned
+ logger.debug("inputsForTrack.assignment @trackNumber", inputsForTrack.assignment, @trackNumber )
+ if inputsForTrack.assignment == @trackNumber
+ @newTrack = false
+ break
+
+ if @trackNumber == MIDI_TRACK
+ @trackType = 'midi'
+ else
+ @trackType = 'audio'
+
+ if @newTrack
+ assignment = context.jamClient.TrackGetInstrument(@trackNumber)
+
+ if assignment == 0
+ logger.debug("defaulting track instrument for assignment #{@trackNumber}")
+ # ensure that we always have an instrument set (50 = electric guitar
+ context.jamClient.TrackSetInstrument(@trackNumber, 50)
+
+ #@performVstScan()
+ @performMidiScan()
+
+ @changed()
+
+ @app.layout.showDialog('configure-live-tracks-dialog')
+
+ onDesiredTrackType: (trackType) ->
+ @trackType = trackType
+
+ if @trackType == 'midi'
+ @trackNumber = MIDI_TRACK
+ @changed()
+
+ onUpdateOutputs: (outputId1, outputId2) ->
+
+ context.jamClient.TrackSetAssignment(outputId1, true, ASSIGNMENT.OUTPUT);
+ context.jamClient.TrackSetAssignment(outputId2, true, ASSIGNMENT.OUTPUT);
+
+ result = context.jamClient.TrackSaveAssignments();
+
+ if(!result || result.length == 0)
+
+ else
+ context.JK.Banner.showAlert('Unable to save assignments. ' + result);
+
+ onShowEditOutputs: () ->
+ @app.layout.showDialog('configure-outputs-dialog')
+
+ onAssociateMIDIWithTrack: (midiInterface) ->
+
+ @trackNumber = MIDI_TRACK
+
+ if !midiInterface? || midiInterface == ''
+ logger.debug("disabling midiInterface:#{midiInterface}, track:#{@trackNumber - 1}")
+ context.jamClient.VST_EnableMidiForTrack(@trackNumber - 1, false, 0)
+ else
+ logger.debug("enabling midiInterface:#{midiInterface}, track:#{@trackNumber - 1}")
+ context.jamClient.VST_EnableMidiForTrack(@trackNumber - 1, true, midiInterface)
+
+ setTimeout((() => (
+ @listVsts()
+
+ @changed()
+ )), 250)
+
+ }
+)
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/stores/GenreStore.js.coffee b/web/app/assets/javascripts/react-components/stores/GenreStore.js.coffee
new file mode 100644
index 000000000..0548c3014
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/GenreStore.js.coffee
@@ -0,0 +1,26 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+
+@GenreStore = Reflux.createStore(
+ {
+ listenables: @GenreActions
+ genres: []
+ genresLookup: {}
+
+ init: ->
+ # Register with the app store to get @app
+ this.listenTo(context.AppStore, this.onAppInit)
+
+ onAppInit: (@app) ->
+ rest.getGenres().done (genres) =>
+ @genres = genres
+ for genre in genres
+ @genresLookup[genre.id] = genre.description
+
+ @trigger(@genres)
+
+ display: (id) ->
+ @genresLookup[id]
+ }
+)
diff --git a/web/app/assets/javascripts/react-components/stores/InstrumentStore.js.coffee b/web/app/assets/javascripts/react-components/stores/InstrumentStore.js.coffee
new file mode 100644
index 000000000..64f5e3e91
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/InstrumentStore.js.coffee
@@ -0,0 +1,26 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+
+@InstrumentStore = Reflux.createStore(
+ {
+ listenables: @InstrumentActions
+ instruments: []
+ instrumentLookup: {}
+
+ init: ->
+ # Register with the app store to get @app
+ this.listenTo(context.AppStore, this.onAppInit)
+
+ onAppInit: (@app) ->
+ rest.getInstruments().done (instruments) =>
+ @instruments = instruments
+ for instrument in instruments
+ @instrumentLookup[instrument.id] = instrument.description
+
+ @trigger(@instruments)
+
+ display: (id) ->
+ @instrumentLookup[id]
+ }
+)
diff --git a/web/app/assets/javascripts/react-components/stores/JamBlasterStore.js.coffee b/web/app/assets/javascripts/react-components/stores/JamBlasterStore.js.coffee
new file mode 100644
index 000000000..f242d2794
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/JamBlasterStore.js.coffee
@@ -0,0 +1,347 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+
+@JamBlasterStore = Reflux.createStore(
+ {
+ listenables: @JamBlasterActions
+
+ userJamBlasters: []
+ localJamBlasters: []
+ allJamBlasters: []
+
+ init: () ->
+ # Register with the app store to get @app
+ this.listenTo(context.AppStore, this.onAppInit)
+
+ onAppInit: (@app) ->
+
+ onUpdateAudio: (name, value) ->
+ # input1_linemode
+ # input2_linemode
+ # input1_48V
+ # input2_48V
+ # has_chat
+ # track1 = {left, right, inst, stereo)
+ # track1 = {left, right, inst, stereo)
+
+
+ if @pairedJamBlaster? && @pairedJamBlaster.tracks?
+ logger.debug("onUpdateAudio name=#{name} value=#{value}", @pairedJamBlaster.tracks)
+ audio = $.extend({}, @pairedJamBlaster.tracks)
+ if name == 'inputTypeTrack1'
+ audio.input1_linemode = value
+ else if name == 'inputTypeTrack2'
+ audio.input2_linemode = value
+ else if name == 'track1Phantom'
+ audio.input1_48V = value
+ else if name == 'track2Phantom'
+ audio.input2_48V = value
+ else if name == 'micActive'
+ audio.has_chat = value
+
+ track1Active = @pairedJamBlaster.tracks.track1Active
+ if name == 'track1Active'
+ track1Active = value
+
+ track2Active = @pairedJamBlaster.tracks.track2Active
+ if name == 'track2Active'
+ track2Active = value
+
+ combined = @pairedJamBlaster.tracks.combined
+ if name == 'combined'
+ combined = value
+
+ track1Instrument = @pairedJamBlaster.tracks.track1Instrument
+ track2Instrument = @pairedJamBlaster.tracks.track2Instrument
+ if name == 'track1Instrument'
+ track1Instrument = @convertToClientInstrument(value)
+ if name == 'track2Instrument'
+ track2Instrument = @convertToClientInstrument(value)
+
+
+
+
+ if combined
+ # user has chosen to combine both inputs into one track. stereo=true is the key flag her
+
+ audio.track1 = {stereo: true, left: true, inst: track1Instrument}
+
+ else
+
+ if track1Active && track2Active
+
+ audio.track1 = {stereo: false, left: true, inst: track1Instrument}
+ audio.track2 = {stereo: false, right: true, inst: track2Instrument}
+
+ else if track1Active #(means track)
+
+ audio.track1 = {stereo: false, left: true, inst: track1Instrument}
+
+ else # input2Active
+
+ audio.track2 = {stereo: false, right: true, inst: track2Instrument}
+
+ logger.debug("updating JamBlaster track state", audio)
+ context.jamClient.setJbTrackState(audio);
+ @lastClientTrackState = null
+ #@waitOnTracksDone()
+ else
+ context.JK.Banner.showAlert('no paired JamBlaster', 'it seems your JamBlaster has become disconnected. Please ensure it is powered on and connected via an ethernet cable.')
+
+ waitOnTracksDone: () ->
+ @waitingOnTracksDone = true
+ @waitingOnTracksInterval = setInterval()
+ convertToClientInstrument: (instrumentId) ->
+ clientInstrumentId = null
+ if instrumentId != null && instrumentId != ''
+ clientInstrumentId = context.JK.instrument_id_to_instrument[instrumentId].client_id
+ else
+ clientInstrumentId = 10
+ clientInstrumentId
+
+ onSetAutoPair: (autopair) ->
+
+
+ if !autopair
+ context.jamClient.setJBAutoPair(autopair)
+ @lastClientAutoPair = null
+ JamBlasterActions.resyncBonjour()
+ setTimeout((() => context.JK.Banner.showNotice("autoconnect removed",
+ "To use the JamBlaster in the future, you will need to come to this screen and click the connect link.")), 1)
+ else
+ context.JK.Banner.showYesNo({
+ title: "enable auto-connect",
+ html: "If you would like to automatically connect to your JamBlaster whenever you start this app, click the AUTO CONNECT button below.",
+ yes_text: 'AUTO CONNECT',
+ yes: =>
+ context.jamClient.setJBAutoPair(autopair)
+ @lastClientAutoPair = null
+ JamBlasterActions.resyncBonjour()
+ setTimeout((() => context.JK.Banner.showNotice("autoconnect enabled",
+ "Your desktop JamKazam application will automatically reconnect to the JamBlaster .")), 1)
+
+ })
+
+ onPairState: (state) ->
+ if state.client_pair_state == 10
+ # fully paired
+ logger.debug("backend indicates we are paired with a client")
+ @onResyncBonjour()
+
+ onSaveNetworkSettings: (settings) ->
+ logger.debug("onSaveNetworkSettings", settings)
+
+ result = context.jamClient.setJbNetworkState(settings)
+ if !result
+ context.JK.Banner.showAlert('unable to save network settings', 'Please double-check that your JamBlaster is online and paired.')
+ return
+ else
+ context.JK.Banner.showAlert('network settings updated', 'Please reboot the JamBlaster.')
+ # it will be refreshed by backend
+ @onClearNetworkState()
+ @onResyncBonjour()
+
+ onResyncBonjour: () ->
+
+ if @refreshingBonjour
+ logger.debug("already refreshing bonjour")
+ return
+
+ @refreshingBonjour = true
+ @changed()
+ rest.getUserJamBlasters({client_id: @app.clientId}).done((response) => @getUserJamBlastersDone(response)).fail((response) => @getUserJamBlastersFail(response))
+
+ getUserJamBlastersDone: (response) ->
+ @userJamBlasters = response
+
+ @changed()
+
+ @getLocalClients(response)
+
+
+ findJamBlaster: (oldClient) ->
+ found = null
+ if @clients?
+ for client in @clients
+ if oldClient.server_id? && client.server_id == oldClient.server_id
+ found = client
+ break
+ if oldClient.ipv6_addr? && client.ipv6_addr == oldClient.ipv6_addr
+ found = client
+ break
+
+ found
+
+ getUserJamBlastersFail: (jqXHR) ->
+ @refreshingBonjour = false
+ @changed()
+ @app.layout.ajaxError(jqXHR)
+
+ getAutoPair: () ->
+ if @lastClientAutoPair?
+ return @lastClientAutoPair
+ else
+ return @getJbAutoPair()
+
+ getNetworkState: (client) ->
+ if @lastClientNetworkState? && @lastClientNetworkState.ipv6_addr == client.ipv6_addr
+ return @lastClientNetworkState
+ else
+ return @getJbNetworkState(client)
+
+ getPortState: (client) ->
+ if @lastClientPortState? && @lastClientPortState.ipv6_addr == client.ipv6_addr
+ return @lastClientPortState
+ else
+ return @getJbPortBindState(client)
+
+ getJbPortBindState:(client) ->
+ @lastClientPortState = context.jamClient.getJbPortBindState()
+ console.log("context.jamClient.getJbPortBindState()", @lastClientPortState)
+ @lastClientPortState.ipv6_addr = client.ipv6_addr
+ return @lastClientPortState
+
+ getJbNetworkState:(client) ->
+ @lastClientNetworkState = context.jamClient.getJbNetworkState()
+ console.log("context.jamClient.getJbNetworkState()", @lastClientNetworkState)
+ @lastClientNetworkState.ipv6_addr = client.ipv6_addr
+ return @lastClientNetworkState
+
+ getJbAutoPair:() ->
+ @lastClientAutoPair = context.jamClient.getJBAutoPair()
+ console.log("context.jamClient.getJBAutoPair()", @lastClientAutoPair)
+ return @lastClientAutoPair
+
+ getJbTrackState:(client) ->
+ @lastClientTrackState = context.jamClient.getJbTrackState()
+ console.log("context.jamClient.getJbTrackState()", @lastClientTrackState)
+ @lastClientTrackState.ipv6_addr = client.ipv6_addr
+ return @lastClientTrackState
+
+ onClearPortBindState: () ->
+ @lastClientPortState = null
+
+ onClearNetworkState: () ->
+ @lastClientNetworkState = null
+
+ mergeBonjourClients: (localClients, userJamBlasters) ->
+ console.log("@state.localClients", localClients)
+ console.log("@state.userJamBlasters", userJamBlasters)
+
+ # for localClient in @state.localClients
+
+ autoconnect = @getAutoPair()
+
+ foundPaired = null
+ clients = []
+ for localClient in localClients
+ if localClient.connect_url.indexOf(':30330') > -1 && localClient.is_jb
+ client = {}
+ client.ipv6_addr = localClient.ipv6_addr
+ client.isConnected = localClient.isPaired
+ client.name = localClient.name
+ client.has_local = true
+ client.has_server = false
+ client.id = client.ipv6_addr
+ client.connect_url = localClient.connect_url
+ client.isPaired = localClient.pstate? && localClient.pstate == 10 # ePairingState.Paired
+ client.autoconnect = autoconnect
+
+ if client.isPaired
+ client.portState = @getPortState(client)
+ client.network = @getNetworkState(client)
+ client.tracks = @getJbTrackState(client)
+ client.isDynamicPorts = client.portState?.use_static_port
+ foundPaired = client
+
+
+ if client.tracks?
+ client.tracks.inputTypeTrack1 = client.tracks.input1_linemode
+ client.tracks.inputTypeTrack2 = client.tracks.input2_linemode
+ client.tracks.track1Phantom = client.tracks.input1_48V
+ client.tracks.track2Phantom = client.tracks.input2_48V
+ client.tracks.micActive = client.tracks.has_chat
+
+ # combined
+ track1 = client.tracks.track1
+ track2 = client.tracks.track1
+ if track1?
+
+ client.tracks.combined = track1.stereo
+ if track1.stereo
+ client.tracks.track1Active = true
+ client.tracks.track2Active = true
+ client.tracks.track1Active = track1.left
+ client.tracks.track2Active = track1.right
+ client.tracks.track1Instrument = track1.inst
+
+ if track2?
+ client.tracks.track2Instrument = track2.inst
+ client.tracks.track1Active = track2.left
+ client.tracks.track2Active = track2.right
+ # map["adaptiveframe"] = jbcfg.adaptiveframe();
+
+
+
+
+
+
+ for serverClient in userJamBlasters
+ # see if we can join on ipv6
+ if ipv6_addr == serverClient.ipv6_link_local
+ # ok, matched! augment with server data
+ client.serial_no = serverClient.serial_no
+ client.user_id = serverClient.user_id
+ client.id = serverClient.id
+ client.server_id = serverClient.id
+ client.client_id = serverClient.client_id
+ client.ipv4_link_local = serverClient.ipv4_link_local
+ client.display_name = serverClient.display_name
+ client.has_server = true
+ break
+ clients.push(client)
+
+ for serverClient in userJamBlasters
+
+ foundLocal = false
+ for localClient in localClients
+ if ipv6_addr == serverClient.ipv6_link_local
+ foundLocal = true
+ break
+ if !foundLocal
+ # this server version of the client has not been spoken for in the earlier loop above
+ # so we need to add it in to the client list
+
+ client = {}
+ client.serial_no = serverClient.serial_no
+ client.user_id = serverClient.user_id
+ client.id = serverClient.id
+ client.client_id = serverClient.client_id
+ client.ipv4_link_local = serverClient.ipv4_link_local
+ client.display_name = serverClient.display_name
+ client.has_local = false
+ client.has_server = true
+ client.autoconnect = autoconnect
+ clients.push(client)
+
+ @pairedJamBlaster = foundPaired
+
+ console.log("all client", clients)
+
+ @clients = clients
+ @changed()
+
+ getLocalClients: (userJamBlasters) ->
+ @localClients = context.jamClient.getLocalClients()
+
+ @mergeBonjourClients(@localClients, userJamBlasters)
+
+ @refreshingBonjour = false
+ @changed()
+
+
+ changed: () ->
+ @trigger({userJamBlasters: @userJamBlasters, allJamBlasters: @clients, localJamBlasters: @localClients, refreshingBonjour: @refreshingBonjour, pairedJamBlaster: @pairedJamBlaster})
+ }
+)
diff --git a/web/app/assets/javascripts/react-components/stores/JamTrackMixdownStore.js.coffee b/web/app/assets/javascripts/react-components/stores/JamTrackMixdownStore.js.coffee
new file mode 100644
index 000000000..ae24d1cce
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/JamTrackMixdownStore.js.coffee
@@ -0,0 +1,90 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+rest = context.JK.Rest()
+EVENTS = context.JK.EVENTS
+
+
+JamTrackActions = @JamTrackActions
+
+@JamTrackMixdownStore = Reflux.createStore(
+ {
+ # listenables: JamTrackMixdownActions
+
+ # the jamtrack that contains the mixdowns in question
+ jamTrack: null
+
+ # what mixdowns are being built right now
+ building: []
+
+ # a currently open (loaded) mixdown
+ current: null
+
+ init: () ->
+ this.listenTo(context.AppStore, this.onAppInit);
+ this.listenTo(context.JamTrackStore, this.onJamTrackChanged);
+
+ @changed()
+
+ onAppInit: (@app) ->
+
+ getState: () ->
+ @state
+
+ changed: () ->
+ @state = {jamTrack: @jamTrack, building:@building, current: @current}
+ this.trigger(@state)
+
+ onJamTrackChanged: (@jamTrack) ->
+ # TODO: close out building? current?
+
+ onCreateMixdown: (mixdown, package_settings, done, fail) ->
+ logger.debug("creating mixdown", mixdown, package_settings)
+ rest.createMixdown(mixdown)
+ .done((created) =>
+
+ logger.debug("created mixdown", created)
+
+ package_settings.id = created.id
+
+ # we have to determine sample rate here, in the store, because child windows don't have access to jamClient
+ sampleRate = context.jamClient.GetSampleRate()
+ sampleRate = if sampleRate == 48 then 48 else 44
+ package_settings.sample_rate = sampleRate
+
+ rest.enqueueMixdown(package_settings)
+ .done((enqueued) =>
+ logger.debug("enqueued mixdown package", package_settings)
+ done(enqueued)
+ )
+ .fail((jqxhr) =>
+ @app.layout.notify({title:'Unable to Package Mixdown', text: 'You can push the RETRY button.'})
+ fail(jqxhr)
+ )
+ )
+ .fail((jqxhr) =>
+ fail(jqxhr)
+ )
+
+ onEditMixdown: (mixdown) ->
+ logger.debug("editing mixdown", mixdown)
+
+ onDeleteMixdown: (mixdown) ->
+ logger.debug("deleting mixdown", mixdown)
+
+ onOpenMixdown: (mixdown) ->
+ logger.debug("opening mixdown", mixdown)
+
+ onCloseMixdown: (mixdown) ->
+ logger.debug("closing mixdown", mixdown)
+
+ onEnqueueMixdown: (mixdown) ->
+ logger.debug("enqueuing mixdown", mixdown)
+
+ onDownloadMixdown: (mixdown) ->
+ logger.debug("download mixdown", mixdown)
+
+ onRefreshMixdown: (mixdown) ->
+ logger.debug("refresh mixdown", mixdown)
+ }
+)
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/stores/JamTrackPlayerStore.js.coffee b/web/app/assets/javascripts/react-components/stores/JamTrackPlayerStore.js.coffee
new file mode 100644
index 000000000..b9f1e67c6
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/JamTrackPlayerStore.js.coffee
@@ -0,0 +1,485 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+rest = context.JK.Rest()
+EVENTS = context.JK.EVENTS
+
+JamTrackPlayerActions = @JamTrackPlayerActions
+BrowserMediaStore = @BrowserMediaStore
+BrowserMediaActions = @BrowserMediaActions
+
+@JamTrackPlayerStore = Reflux.createStore(
+ {
+ listenables: JamTrackPlayerActions
+ jamTrack: null
+ previous: null
+ requestedSearch: null
+ requestedFilter: null
+ subscriptions: {}
+ enqueuedMixdowns: {}
+ childWindow: null
+ watchedMixdowns: {}
+
+ init: ->
+ # Register with the app store to get @app
+ this.listenTo(context.AppStore, this.onAppInit)
+ this.listenTo(context.BrowserMediaStore, this.onBrowserMediaChanged)
+ @browserMediaState = {}
+ onAppInit: (app) ->
+ @app = app
+
+ onBrowserMediaChanged: (browserMediaState) ->
+ @browserMediaState = browserMediaState
+
+ @changed()
+
+ getState: () ->
+ @state
+
+ onOpened: (jamTrack, popup=true) ->
+ @jamTrack = jamTrack
+
+ @changed()
+
+ onOpen: (jamTrack, popup=true) ->
+
+
+ if jamTrack.id == @jamTrack?.id && @childWindow?
+ logger.info("JamTrackPlayerStore: request to open already-opened JamTrack; ignoring")
+ return
+
+ @enqueuedMixdowns = {}
+ @jamTrack = jamTrack
+
+ sampleRate = 48
+ @sampleRate = if sampleRate == 48 then 48 else 44
+
+
+ if popup
+ if @childWindow?
+ @childWindow.close()
+
+ logger.debug("opening JamTrackPlayer window")
+ @childWindow = window.open(@createPopupUrl(jamTrack), 'Media Controls', 'scrollbars=yes,toolbar=no,status=no,height=667,width=450')
+
+ @changed()
+
+ createPopupUrl: (jamTrack) ->
+ "/popups/jamtrack-player/" + jamTrack.id
+
+ onWindowUnloaded: () ->
+ BrowserMediaActions.stop()
+ @childWindow = null
+
+ pickMyPackage: () ->
+
+ return unless @jamTrack?
+
+ for mixdown in @jamTrack.mixdowns
+
+ myPackage = null
+ for mixdown_package in mixdown.packages
+ if mixdown_package.file_type == 'mp3' && mixdown_package.encrypt_type == null && mixdown_package.sample_rate == @sampleRate
+ myPackage = mixdown_package
+ break
+
+ mixdown.myPackage = myPackage
+
+ subscriptionKey: (mixdown_package) ->
+ "mixdown-#{mixdown_package.id}"
+
+ subscribe: (mixdown_package) ->
+ key = @subscriptionKey(mixdown_package)
+
+ if !@watchedMixdowns[key]?
+ # we need to register
+ context.JK.SubscriptionUtils.subscribe('mixdown', mixdown_package.id).on(context.JK.EVENTS.SUBSCRIBE_NOTIFICATION, this.onMixdownSubscriptionEvent)
+ @watchedMixdowns[key] = {type:'mixdown', id: mixdown_package.id}
+
+ unsubscribe: (mixdown_package) ->
+ key = @subscriptionKey(mixdown_package)
+ if @watchedMixdowns[key]?
+ context.JK.SubscriptionUtils.unsubscribe('mixdown', mixdown_package.id)
+ delete @watchedMixdowns[key]
+
+ manageWatchedMixdowns: () ->
+
+ if @jamTrack?
+ for mixdown in @jamTrack.mixdowns
+ if mixdown.myPackage
+ if mixdown.myPackage.signing_state == 'SIGNED'
+ @unsubscribe(mixdown.myPackage)
+ else
+ @subscribe(mixdown.myPackage)
+
+ else
+ for key, subscription of @watchedMixdowns
+ logger.debug("unsubscribing bulk", key, subscription)
+ context.JK.SubscriptionUtils.unsubscribe(subscription.type, subscription.id)
+
+ # we cleared them all out; clear out storage
+ @watchedMixdowns = {}
+
+ onMixdownSubscriptionEvent: (e, data) ->
+ logger.debug("JamTrackStore: subscription notification received: type:" + data.type, data)
+
+ return unless @jamTrack?
+
+ mixdown_package_id = data.id
+
+ for mixdown in @jamTrack.mixdowns
+ for mixdown_package in mixdown.packages
+ if mixdown_package.id == mixdown_package_id
+ mixdown_package.signing_state = data.body.signing_state
+ mixdown_package.packaging_steps = data.body.packaging_steps
+ mixdown_package.current_packaging_step = data.body.current_packaging_step
+ logger.debug("updated package with subscription notification event")
+
+ if mixdown_package.signing_state == 'SIGNING_TIMEOUT' || mixdown_package.signing_state == 'QUEUED_TIMEOUT' || mixdown_package.signing_state == 'QUIET_TIMEOUT' || mixdown_package.signing_state == 'ERROR'
+ @reportError(mixdown)
+
+ @changed()
+ break
+
+ # this drives the state engine required to get a Mixdown from 'available on the server' to
+ manageMixdownSynchronization: () ->
+
+
+ if @jamTrack
+ @jamTrack.activeMixdown = null
+ @jamTrack.activeStem = null
+
+ # let's see if we have a mixdown active?
+
+ #if !@jamTrack?.last_mixdown_id?
+ # logger.debug("JamTrackStore: no mixdown active")
+
+ for mixdown in @jamTrack.mixdowns
+ if mixdown.id == @jamTrack.last_mixdown_id
+ @jamTrack.activeMixdown = mixdown
+ #logger.debug("JamTrackStore: mixdown active:", mixdown)
+ break
+
+ for stem in @jamTrack.tracks
+ if stem.id == @jamTrack.last_stem_id
+ @jamTrack.activeStem = stem
+ break
+
+ # let's check and see if we've asked the BrowserMediaStore to load this particular file or not
+
+ if @jamTrack?.activeStem
+
+ if @browserMediaState?.id != @jamTrack.activeStem.id
+ new window.Fingerprint2().get((result, components) => (
+ BrowserMediaActions.load(@jamTrack.activeStem.id, [window.location.protocol + '//' + window.location.host + "/api/jamtracks/#{@jamTrack.id}/stems/#{@jamTrack.activeStem.id}/download.mp3?file_type=mp3&mark=#{result}"], 'jamtrack_web_player')
+ ))
+ @jamTrack.activeStem.client_state = 'downloading'
+ else
+ if @browserMediaState.loaded
+ @jamTrack.activeStem.client_state = 'ready'
+ else if @browserMediaState.load_error
+ @jamTrack.activeStem.client_state = 'download_fail'
+ else
+ @jamTrack.activeStem.client_state = 'downloading'
+
+
+ else if @jamTrack?.activeMixdown
+
+ # if we don't have this on the server yet, don't engage the rest of this logic...
+ return if @jamTrack.activeMixdown?.myPackage?.signing_state != 'SIGNED'
+
+ activePackage = @jamTrack.activeMixdown.myPackage
+
+ if activePackage?
+ if @browserMediaState?.id != activePackage.id
+ new window.Fingerprint2().get((result, components) => (
+ BrowserMediaActions.load(activePackage.id, [window.location.protocol + '//' + window.location.host + "/api/mixdowns/#{@jamTrack.activeMixdown.id}/download.mp3?file_type=mp3&sample_rate=48&mark=#{result}"], 'jamtrack_web_player')
+ ))
+ @jamTrack.activeMixdown.client_state = 'downloading'
+ else
+ if @browserMediaState.loaded
+ @jamTrack.activeMixdown.client_state = 'ready'
+ else if @browserMediaState.load_error
+ @jamTrack.activeMixdown.client_state = 'download_fail'
+ else
+ @jamTrack.activeMixdown.client_state = 'downloading'
+
+ else if @jamTrack?
+
+ masterTrack = null
+ for jamTrackTrack in @jamTrack.tracks
+ if jamTrackTrack.track_type == 'Master'
+ masterTrack = jamTrackTrack
+ break
+
+ if @browserMediaState?.id != @jamTrack.id
+ BrowserMediaActions.load(@jamTrack.id, [masterTrack.preview_mp3_url], 'jamtrack_web_player')
+ @jamTrack.client_state = 'downloading'
+ else
+ if @browserMediaState.loaded
+ @jamTrack.client_state = 'ready'
+ else if @browserMediaState.load_error
+ @jamTrack.client_state = 'download_fail'
+ else
+ @jamTrack.client_state = 'downloading'
+ else
+ logger.error("unknown condition when processing manageMixdownSynchronization")
+
+
+ changed: () ->
+
+ @pickMyPackage()
+ @manageWatchedMixdowns()
+ @manageMixdownSynchronization()
+
+ @state = {
+ jamTrack: @jamTrack,
+ opened: @previous == null && @jamTrack != null,
+ closed: @previous != null && @jamTrack == null,
+ fullTrackActivated: @previousMixdown != null && @jamTrack?.activeMixdown == null}
+ @previous = @jamTrack
+ @previousMixdown = @jamTrack?.activeMixdown
+ this.trigger(@state)
+
+ onCreateMixdown: (mixdown, done, fail) ->
+
+ #volumeSettings = context.jamClient.GetJamTrackSettings();
+
+ #track_settings = []
+
+ #for track in volumeSettings.tracks
+ # track_settings.push({id: track.id, pan: track.pan, vol: track.vol_l, mute: track.mute})
+
+ #mixdown.settings.tracks = track_settings
+
+ logger.debug("creating mixdown", mixdown)
+
+ rest.createMixdown(mixdown)
+ .done((created) =>
+
+ @addMixdown(created)
+
+ logger.debug("created mixdown", created)
+
+ @onEnqueueMixdown({id: created.id}, done, fail)
+ )
+ .fail((jqxhr) =>
+ fail(jqxhr)
+ )
+
+
+ onEditMixdown: (mixdown) ->
+ logger.debug("editing mixdown", mixdown)
+
+ rest.editMixdown(mixdown)
+ .done((updatedMixdown) =>
+ logger.debug("edited mixdown")
+ @updateMixdown(updatedMixdown)
+ ).fail((jqxhr) =>
+ @app.layout.notify({title:'Unable to Edit Custom Mix', text: 'The server was unable to edit this mix.'})
+ )
+
+ onDeleteMixdown: (mixdown) ->
+ logger.debug("deleting mixdown", mixdown)
+
+ rest.deleteMixdown(mixdown)
+ .done(() =>
+ logger.debug("deleted mixdown")
+
+ @deleteMixdown(mixdown)
+ )
+ .fail((jqxhr) =>
+ @app.layout.notify({title:'Unable to Deleted Custom Mix', text: 'The server was unable to delete this mix.'})
+ )
+
+ stopPlaying: () ->
+ alert("stop playing")
+
+ onOpenMixdown: (mixdown) ->
+ if @browserMediaState.loading
+ logger.warn("can not activate mixdown while browser media is loading")
+ return
+
+ logger.debug("opening mixdown", mixdown)
+
+ # check if it's already available in the backend or not
+ rest.markMixdownActive({id: @jamTrack.id, mixdown_id: mixdown.id})
+ .done((edited) =>
+ logger.debug("marked mixdown as active")
+ @jamTrack = edited
+
+ BrowserMediaActions.stop()
+
+ @changed()
+ )
+ .fail((jqxhr) =>
+ @app.layout.notify({title:'Unable to Edit Mixdown', text: 'Unable to mark this mixdown as active.'})
+ )
+
+ onOpenStem: (stem_id) ->
+ if @browserMediaState.loading
+ logger.warn("can not activate stem while browser media is loading")
+ return
+
+ logger.debug("opening stem", stem_id)
+
+ # check if it's already available in the backend or not
+ rest.markMixdownActive({id: @jamTrack.id, mixdown_id: null, stem_id: stem_id})
+ .done((edited) =>
+ logger.debug("marked stem as active")
+ @jamTrack = edited
+
+ BrowserMediaActions.stop()
+
+ @changed()
+ )
+ .fail((jqxhr) =>
+ @app.layout.notify({title:'Unable to Edit Mixdown', text: 'Unable to mark this mixdown as active.'})
+ )
+
+ onActivateNoMixdown: (jamTrack) ->
+
+ if @browserMediaState.loading
+ logger.warn("can not activate JamTrack while browser media is loading")
+ return
+
+ logger.debug("activating no mixdown")
+
+ rest.markMixdownActive({id: @jamTrack.id, mixdown_id: null})
+ .done((edited) =>
+ logger.debug("marked JamTrack as active")
+
+ @jamTrack = edited
+ @changed()
+ )
+ .fail((jqxhr) =>
+ @app.layout.notify({title:'Unable to Edit Mixdown', text: 'Unable to mark this mixdown as active.'})
+ )
+
+
+ onCloseMixdown: (mixdown) ->
+ logger.debug("closing mixdown", mixdown)
+
+ onEnqueueMixdown: (mixdown, done, fail) ->
+ logger.debug("enqueuing mixdown", mixdown)
+
+ package_settings = {file_type: 'mp3', encrypt_type: null, sample_rate: @sampleRate}
+ package_settings.id = mixdown.id
+
+ rest.enqueueMixdown(package_settings)
+ .done((enqueued) =>
+
+ @enqueuedMixdowns[mixdown.id] = {}
+
+ logger.debug("enqueued mixdown package", package_settings)
+ @addOrUpdatePackage(enqueued)
+ done(enqueued) if done
+ )
+ .fail((jqxhr) =>
+ @app.layout.notify({title:'Unable to Create Custom Mix', text: 'Click the error icon to retry.'})
+ fail(jqxhr) if fail?
+ )
+
+ onDownloadMixdown: (mixdown) ->
+ logger.debug("download mixdown", mixdown)
+
+ onRefreshMixdown: (mixdown) ->
+ logger.debug("refresh mixdown", mixdown)
+
+ addMixdown: (mixdown) ->
+ if @jamTrack?
+ logger.debug("adding mixdown to JamTrackStore", mixdown)
+ @jamTrack.mixdowns.splice(0, 0, mixdown)
+ @changed()
+ else
+ logger.warn("no jamtrack to add mixdown to in JamTrackStore", mixdown)
+
+ deleteMixdown: (mixdown) ->
+ if @jamTrack?
+ logger.debug("deleting mixdown from JamTrackStore", mixdown)
+ index = null
+ for matchMixdown, i in @jamTrack.mixdowns
+ if mixdown.id == matchMixdown.id
+ index = i
+ if index?
+ @jamTrack.mixdowns.splice(index, 1)
+
+ if @jamTrack.activeMixdown?.id == mixdown.id
+ @onActivateNoMixdown(@jamTrack)
+
+ @changed()
+ else
+ logger.warn("unable to find mixdown to delete in JamTrackStore", mixdown)
+ else
+ logger.warn("no jamtrack to delete mixdown for in JamTrackStore", mixdown)
+
+ updateMixdown: (mixdown) ->
+ if @jamTrack?
+ logger.debug("editing mixdown from JamTrackStore", mixdown)
+ index = null
+ for matchMixdown, i in @jamTrack.mixdowns
+ if mixdown.id == matchMixdown.id
+ index = i
+ if index?
+ @jamTrack.mixdowns[index] = mixdown
+
+ @changed()
+ else
+ logger.warn("unable to find mixdown to edit in JamTrackStore", mixdown)
+ else
+ logger.warn("no jamtrack to edit mixdown for in JamTrackStore", mixdown)
+
+ addOrUpdatePackage: (mixdown_package) ->
+ if @jamTrack?
+ added = false
+ index = null
+ for mixdown in @jamTrack.mixdowns
+ existing = false
+ if mixdown_package.jam_track_mixdown_id == mixdown.id
+ for possiblePackage, i in mixdown.packages
+ if possiblePackage.id == mixdown_package.id
+ existing = true
+ index = i
+ break
+
+ if existing
+ mixdown.packages[index] = mixdown_package
+ logger.debug("replacing mixdown package in JamTrackStore", mixdown_package)
+ else
+ mixdown.packages.splice(0, 0, mixdown_package)
+ logger.debug("adding mixdown package in JamTrackStore")
+
+ added = true
+ @changed()
+ break
+
+ if !added
+ logger.debug("couldn't find the mixdown associated with package in JamTrackStore", mixdown_package)
+ else
+ logger.warn("no mixdown to add package to in JamTrackStore", mixdown_package)
+
+
+
+ reportError: (mixdown) ->
+
+ enqueued = @enqueuedMixdowns[mixdown?.id]
+
+ # don't double-report
+ if !enqueued? || enqueued.marked
+ return
+
+ enqueued.marked = true
+ data = {
+ value: 1,
+ user_id: context.JK.currentUserId,
+ user_name: context.JK.currentUserName,
+ result: "signing state: #{mixdown.myPackage?.signing_state}, client state: #{mixdown.client_state}",
+ mixdown: mixdown.id,
+ package: mixdown.myPackage?.id
+ detail: mixdown.myPackage?.error_reason
+ }
+ rest.createAlert("Mixdown Sync failed for #{context.JK.currentUserName}", data)
+
+ context.stats.write('web.mixdown.error', data)
+ }
+)
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee b/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee
index 53002cf8f..46a095bb8 100644
--- a/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee
+++ b/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee
@@ -9,10 +9,13 @@ JamTrackActions = @JamTrackActions
@JamTrackStore = Reflux.createStore(
{
- listenables: JamTrackActions
+ listenables: [JamTrackActions, JamTrackMixdownActions]
jamTrack: null
+ previous: null
requestedSearch: null
requestedFilter: null
+ subscriptions: {}
+ enqueuedMixdowns: {}
init: ->
# Register with the app store to get @app
@@ -21,17 +24,261 @@ JamTrackActions = @JamTrackActions
onAppInit: (app) ->
@app = app
+ getState: () ->
+ @state
+
+ pickMyPackage: () ->
+
+ return unless @jamTrack?
+
+
+ for mixdown in @jamTrack.mixdowns
+
+ myPackage = null
+ for mixdown_package in mixdown.packages
+ if mixdown_package.file_type == 'ogg' && mixdown_package.encrypt_type == 'jkz' && mixdown_package.sample_rate == @sampleRate
+ myPackage = mixdown_package
+ break
+
+ mixdown.myPackage = myPackage
+
+ subscriptionKey: (mixdown_package) ->
+ "mixdown-#{mixdown_package.id}"
+
+ subscribe: (mixdown_package) ->
+ key = @subscriptionKey(mixdown_package)
+
+ if !@watchedMixdowns[key]?
+ # we need to register
+ context.JK.SubscriptionUtils.subscribe('mixdown', mixdown_package.id).on(context.JK.EVENTS.SUBSCRIBE_NOTIFICATION, this.onMixdownSubscriptionEvent)
+ @watchedMixdowns[key] = {type:'mixdown', id: mixdown_package.id}
+
+ unsubscribe: (mixdown_package) ->
+ key = @subscriptionKey(mixdown_package)
+ if @watchedMixdowns[key]?
+ context.JK.SubscriptionUtils.unsubscribe('mixdown', mixdown_package.id)
+ delete @watchedMixdowns[key]
+
+ manageWatchedMixdowns: () ->
+
+ if @jamTrack?
+ for mixdown in @jamTrack.mixdowns
+ if mixdown.myPackage
+ if mixdown.myPackage.signing_state == 'SIGNED'
+ @unsubscribe(mixdown.myPackage)
+ else
+ @subscribe(mixdown.myPackage)
+
+ else
+ for key, subscription of @watchedMixdowns
+ logger.debug("unsubscribing bulk", key, subscription)
+ context.JK.SubscriptionUtils.unsubscribe(subscription.type, subscription.id)
+
+ # we cleared them all out; clear out storage
+ @watchedMixdowns = {}
+
+ onMixdownSubscriptionEvent: (e, data) ->
+ logger.debug("JamTrackStore: subscription notification received: type:" + data.type, data)
+
+ return unless @jamTrack?
+
+ mixdown_package_id = data.id
+
+ for mixdown in @jamTrack.mixdowns
+ for mixdown_package in mixdown.packages
+ if mixdown_package.id == mixdown_package_id
+ mixdown_package.signing_state = data.body.signing_state
+ mixdown_package.packaging_steps = data.body.packaging_steps
+ mixdown_package.current_packaging_step = data.body.current_packaging_step
+ logger.debug("updated package with subscription notification event")
+
+ if mixdown_package.signing_state == 'SIGNING_TIMEOUT' || mixdown_package.signing_state == 'QUEUED_TIMEOUT' || mixdown_package.signing_state == 'QUIET_TIMEOUT' || mixdown_package.signing_state == 'ERROR'
+ @reportError(mixdown)
+
+ @changed()
+ break
+
+ # this drives the state engine required to get a Mixdown from 'available on the server' to
+ manageMixdownSynchronization: () ->
+
+ @jamTrack.activeMixdown = null if @jamTrack
+
+ # let's see if we have a mixdown active?
+
+ if !@jamTrack?.last_mixdown_id?
+ logger.debug("JamTrackStore: no mixdown active")
+ @clearMixdownTimers()
+ return
+
+ for mixdown in @jamTrack.mixdowns
+ if mixdown.id == @jamTrack.last_mixdown_id
+ @jamTrack.activeMixdown = mixdown
+ logger.debug("JamTrackStore: mixdown active:", mixdown)
+ break
+
+ if @jamTrack.activeMixdown?
+
+ # if we don't have this on the server yet, don't engage the rest of this logic...
+ return if @jamTrack.activeMixdown?.myPackage?.signing_state != 'SIGNED'
+
+ fqId = "#{@jamTrack.id}_#{@jamTrack.activeMixdown.id}-#{@sampleRate}"
+ @trackDetail = context.jamClient.JamTrackGetTrackDetail (fqId)
+
+ logger.debug("JamTrackStore: JamTrackGetTrackDetail(#{fqId}).key_state: " + @trackDetail.key_state, @trackDetail)
+
+ # first check if the version is not the same; if so, invalidate.
+
+ if @trackDetail.version? && @jamTrack.activeMixdown.myPackage?
+ if @jamTrack.activeMixdown.myPackage.version != @trackDetail.version
+ logger.info("JamTrackStore: JamTrack Mixdown on disk is different version (stored: #{@trackDetail.version}, server: #{@jamTrack.activeMixdown.myPackage.version}. Invalidating")
+ context.jamClient.InvalidateJamTrack(fqId)
+ @trackDetail = context.jamClient.JamTrackGetTrackDetail (fqId)
+
+ if @trackDetail.version?
+ logger.error("after invalidating package, the version is still wrong!", @trackDetail.version)
+ throw "after invalidating package, the version is still wrong!"
+
+ if @jamTrack.activeMixdown.client_state == 'cant_open'
+ logger.debug(" skipping state check because of earlier 'cant_open'. user should hit retry. ")
+ return
+
+ if @jamTrack.activeMixdown.client_state == 'download_fail'
+ logger.debug("skipping state check because of earlier 'download_fail'. user should hit retry. ")
+ return
+
+ if @jamTrack.activeMixdown.client_state == 'downloading'
+ logger.debug("skipping state check because we are downloading")
+
+ switch @trackDetail.key_state
+ when 'pending'
+ @attemptKeying()
+ when 'not authorized'
+ # TODO: if not authorized, do we need to re-initiate a keying attempt?
+ @attemptKeying()
+ when 'ready'
+ if @jamTrack.activeMixdown.client_state != 'ready'
+
+ @clearMixdownTimers()
+ @jamTrack.activeMixdown.client_state = 'ready'
+
+ # now load it:
+ # JamTrackPlay means 'load'
+ logger.debug("JamTrackStore: loading mixdown")
+ context.jamClient.JamTrackStopPlay();
+
+ if @jamTrack.jmep
+
+ if @jamTrack.activeMixdown.settings.speed?
+ @jamTrack.jmep.speed = @jamTrack.activeMixdown.settings.speed
+ else
+ @jamTrack.jmep.speed = 0
+
+ logger.debug("setting jmep data. speed:" + @jamTrack.jmep.speed)
+
+ context.jamClient.JamTrackLoadJmep(fqId, @jamTrack.jmep)
+ else
+ logger.debug("no jmep data for jamtrack")
+
+ result = context.jamClient.JamTrackPlay(fqId);
+ if !result
+ @jamTrack.activeMixdown.client_state = 'cant_open'
+ @reportError(@jamTrack.activeMixdown)
+ @app.notify(
+ {
+ title: "Mixdown Can Not Open",
+ text: "Unable to open your JamTrack Mixdown. Please contact support@jamkazam.com"
+ }
+ , null, true)
+
+ when 'unknown'
+ # we need to check if @keyCheckTimeout exists; because if it does, we don't want to download while keying.
+ # 'unknown' is tricky here because the file probably is actually on disk, but the bridge API can say unknown until you've tried to key at least once
+ if @jamTrack.activeMixdown.client_state != 'downloading' && !@keyCheckTimeout?
+ @jamTrack.activeMixdown.client_state = 'downloading'
+ logger.debug("JamTrackStore: initiating download of mixdown")
+ context.jamClient.JamTrackDownload(@jamTrack.id, @jamTrack.activeMixdown.id, context.JK.currentUserId,
+ this.makeDownloadProgressCallback(),
+ this.makeDownloadSuccessCallback(),
+ this.makeDownloadFailureCallback())
+ else
+ logger.debug("JamTrackStore: already downloading")
+
+ attemptKeying: () ->
+ if @keyCheckTimeout?
+ logger.debug("JamTrackStore: attemptKeying: skipping because already keying")
+ return
+ else if @jamTrack.activeMixdown.client_state == 'keying_timeout'
+ # if we have timed out keying, we shouldn't automatically retry
+ logger.debug("JamTrackStore: attempKeying: skipping because we have timed out before and user hasn't requested RETRY")
+ return
+ else
+ @keyCheckTimeout = setTimeout(@onKeyCheckTimeout, 10000)
+ @keyCheckoutInterval = setInterval(@checkOnKeying, 1000)
+ @jamTrack.activeMixdown.client_state = 'keying'
+ logger.debug("JamTrackStore: initiating keying requested")
+ context.jamClient.JamTrackKeysRequest()
+
+ onKeyCheckTimeout: () ->
+ @keyCheckTimeout = null
+ clearInterval(@keyCheckoutInterval)
+ @keyCheckoutInterval = null
+
+ if @jamTrack?.activeMixdown?
+ @jamTrack.activeMixdown.client_state = 'keying_timeout'
+ @reportError(@jamTrack.activeMixdown)
+
+ @changed()
+
+ checkOnKeying: () ->
+ @manageMixdownSynchronization()
+
+ # if we exit keying state, we can clear our timers and poke state
+ if @jamTrack.activeMixdown.client_state != 'keying'
+ @clearMixdownTimers()
+ @changed()
+
+
+ # clear out any timer/watcher stuff
+ clearMixdownTimers: () ->
+ logger.debug("JamTrackStore: clearing mixdown timers", @keyCheckTimeout, @keyCheckoutInterval)
+ clearTimeout(@keyCheckTimeout) if @keyCheckTimeout?
+ clearInterval(@keyCheckoutInterval) if @keyCheckoutInterval?
+ @keyCheckTimeout = null
+ @keyCheckoutInterval = null
+
+ changed: () ->
+
+ @pickMyPackage()
+ @manageWatchedMixdowns()
+ @manageMixdownSynchronization()
+
+ @state = {
+ jamTrack: @jamTrack,
+ opened: @previous == null && @jamTrack != null,
+ closed: @previous != null && @jamTrack == null,
+ fullTrackActivated: @previousMixdown != null && @jamTrack?.activeMixdown == null}
+ @previous = @jamTrack
+ @previousMixdown = @jamTrack?.activeMixdown
+ this.trigger(@state)
+
+
onOpen: (jamTrack) ->
if @jamTrack?
@app.notify({text: 'Unable to open JamTrack because another one is already open.'})
return
+ @enqueuedMixdowns = {}
@jamTrack = jamTrack
- this.trigger(@jamTrack)
+
+ # we can cache this because you can't switch gear while in a session (and possible change sample rate!)
+ sampleRate = context.jamClient.GetSampleRate()
+ @sampleRate = if sampleRate == 48 then 48 else 44
+
+ @changed()
onClose: () ->
@jamTrack = null
- this.trigger(@jamTrack)
+ @changed()
onRequestSearch:(searchType, searchData) ->
@requestedSearch = {searchType: searchType, searchData: searchData}
@@ -53,5 +300,268 @@ JamTrackActions = @JamTrackActions
@requestedFilter = null
requested
+ onCreateMixdown: (mixdown, done, fail) ->
+
+ volumeSettings = context.jamClient.GetJamTrackSettings();
+
+ track_settings = []
+
+ for track in volumeSettings.tracks
+ track_settings.push({id: track.id, pan: track.pan, vol: track.vol_l, mute: track.mute})
+
+ mixdown.settings.tracks = track_settings
+
+ logger.debug("creating mixdown", mixdown)
+
+ rest.createMixdown(mixdown)
+ .done((created) =>
+
+ @addMixdown(created)
+
+ logger.debug("created mixdown", created)
+
+ @onEnqueueMixdown({id: created.id}, done, fail)
+ )
+ .fail((jqxhr) =>
+ fail(jqxhr)
+ )
+
+
+ onEditMixdown: (mixdown) ->
+ logger.debug("editing mixdown", mixdown)
+
+ rest.editMixdown(mixdown)
+ .done((updatedMixdown) =>
+ logger.debug("edited mixdown")
+ @updateMixdown(updatedMixdown)
+ ).fail((jqxhr) =>
+ @app.layout.notify({title:'Unable to Edit Custom Mix', text: 'The server was unable to edit this mix.'})
+ )
+
+ onDeleteMixdown: (mixdown) ->
+ logger.debug("deleting mixdown", mixdown)
+
+ rest.deleteMixdown(mixdown)
+ .done(() =>
+ logger.debug("deleted mixdown")
+
+ @deleteMixdown(mixdown)
+ )
+ .fail((jqxhr) =>
+ @app.layout.notify({title:'Unable to Deleted Custom Mix', text: 'The server was unable to delete this mix.'})
+ )
+
+ onOpenMixdown: (mixdown) ->
+ logger.debug("opening mixdown", mixdown)
+
+ # check if it's already available in the backend or not
+ rest.markMixdownActive({id: @jamTrack.id, mixdown_id: mixdown.id})
+ .done((edited) =>
+ logger.debug("marked mixdown as active")
+ @jamTrack = edited
+
+ # unload any currently loaded JamTrack
+ context.jamClient.JamTrackStopPlay();
+
+ @changed()
+
+ SessionActions.mixdownActive(mixdown)
+ )
+ .fail((jqxhr) =>
+ @app.layout.notify({title:'Unable to Edit Mixdown', text: 'Unable to mark this mixdown as active.'})
+ )
+
+ onActivateNoMixdown: (jamTrack) ->
+ logger.debug("activating no mixdown")
+
+ rest.markMixdownActive({id: @jamTrack.id, mixdown_id: null})
+ .done((edited) =>
+ logger.debug("marked JamTrack as active")
+
+ @jamTrack = edited
+ @changed()
+
+ SessionActions.mixdownActive({id:null})
+ )
+ .fail((jqxhr) =>
+ @app.layout.notify({title:'Unable to Edit Mixdown', text: 'Unable to mark this mixdown as active.'})
+ )
+
+
+ onCloseMixdown: (mixdown) ->
+ logger.debug("closing mixdown", mixdown)
+
+ onEnqueueMixdown: (mixdown, done, fail) ->
+ logger.debug("enqueuing mixdown", mixdown)
+
+ package_settings = {file_type: 'ogg', encrypt_type: 'jkz', sample_rate: @sampleRate}
+ package_settings.id = mixdown.id
+
+ rest.enqueueMixdown(package_settings)
+ .done((enqueued) =>
+
+ @enqueuedMixdowns[mixdown.id] = {}
+
+ logger.debug("enqueued mixdown package", package_settings)
+ @addOrUpdatePackage(enqueued)
+ done(enqueued) if done
+ )
+ .fail((jqxhr) =>
+ @app.layout.notify({title:'Unable to Create Custom Mix', text: 'Click the error icon to retry.'})
+ fail(jqxhr) if fail?
+ )
+
+ onDownloadMixdown: (mixdown) ->
+ logger.debug("download mixdown", mixdown)
+
+ onOpenDownloader: (mixdown) ->
+ logger.debug("open mixdown dowloader", mixdown)
+
+ window.open("/popups/jamtrack/download/#{mixdown.jam_track_id}/mixdowns/#{mixdown.id}", 'Mixdown Downloader', 'scrollbars=yes,toolbar=no,status=no,height=171,width=340')
+
+ onRefreshMixdown: (mixdown) ->
+ logger.debug("refresh mixdown", mixdown)
+
+ addMixdown: (mixdown) ->
+ if @jamTrack?
+ logger.debug("adding mixdown to JamTrackStore", mixdown)
+ @jamTrack.mixdowns.splice(0, 0, mixdown)
+ @changed()
+ else
+ logger.warn("no jamtrack to add mixdown to in JamTrackStore", mixdown)
+
+ deleteMixdown: (mixdown) ->
+ if @jamTrack?
+ logger.debug("deleting mixdown from JamTrackStore", mixdown)
+ index = null
+ for matchMixdown, i in @jamTrack.mixdowns
+ if mixdown.id == matchMixdown.id
+ index = i
+ if index?
+ @jamTrack.mixdowns.splice(index, 1)
+
+ if @jamTrack.activeMixdown?.id == mixdown.id
+ @onActivateNoMixdown(@jamTrack)
+
+ @changed()
+ else
+ logger.warn("unable to find mixdown to delete in JamTrackStore", mixdown)
+ else
+ logger.warn("no jamtrack to delete mixdown for in JamTrackStore", mixdown)
+
+ updateMixdown: (mixdown) ->
+ if @jamTrack?
+ logger.debug("editing mixdown from JamTrackStore", mixdown)
+ index = null
+ for matchMixdown, i in @jamTrack.mixdowns
+ if mixdown.id == matchMixdown.id
+ index = i
+ if index?
+ @jamTrack.mixdowns[index] = mixdown
+
+ @changed()
+ else
+ logger.warn("unable to find mixdown to edit in JamTrackStore", mixdown)
+ else
+ logger.warn("no jamtrack to edit mixdown for in JamTrackStore", mixdown)
+
+ addOrUpdatePackage: (mixdown_package) ->
+ if @jamTrack?
+ added = false
+ index = null
+ for mixdown in @jamTrack.mixdowns
+ existing = false
+ if mixdown_package.jam_track_mixdown_id == mixdown.id
+ for possiblePackage, i in mixdown.packages
+ if possiblePackage.id == mixdown_package.id
+ existing = true
+ index = i
+ break
+
+ if existing
+ mixdown.packages[index] = mixdown_package
+ logger.debug("replacing mixdown package in JamTrackStore", mixdown_package)
+ else
+ mixdown.packages.splice(0, 0, mixdown_package)
+ logger.debug("adding mixdown package in JamTrackStore")
+
+ added = true
+ @changed()
+ break
+
+ if !added
+ logger.debug("couldn't find the mixdown associated with package in JamTrackStore", mixdown_package)
+ else
+ logger.warn("no mixdown to add package to in JamTrackStore", mixdown_package)
+
+
+ updateDownloadProgress: () ->
+
+ if @bytesReceived? and @bytesTotal?
+ progress = "#{Math.round(@bytesReceived/@bytesTotal * 100)}%"
+ else
+ progress = '0%'
+
+ #@root.find('.state-downloading .progress').text(progress)
+
+ downloadProgressCallback: (bytesReceived, bytesTotal) ->
+ logger.debug("download #{bytesReceived}/#{bytesTotal}")
+
+ @bytesReceived = Number(bytesReceived)
+ @bytesTotal = Number(bytesTotal)
+
+ # the reason this timeout is set is because, without it,
+ # we observe that the client will hang. So, if you remove this timeout, make sure to test with real client
+ setTimeout(this.updateDownloadProgress, 100)
+
+ downloadSuccessCallback: (updateLocation) ->
+ # is the package loadable yet?
+ logger.debug("JamTrackStore: download complete - on to keying")
+ @attemptKeying()
+ @changed()
+
+ downloadFailureCallback: (errorMsg) ->
+
+ logger.debug("mixdown download failed", errorMsg);
+
+ if @jamTrack?.activeMixdown?
+ @jamTrack.activeMixdown.client_state = 'download_fail'
+ @reportError(@jamTrack.activeMixdown)
+ @changed()
+
+ # makes a function name for the backend
+ makeDownloadProgressCallback: () ->
+ "JamTrackStore.downloadProgressCallback"
+
+ # makes a function name for the backend
+ makeDownloadSuccessCallback: () ->
+ "JamTrackStore.downloadSuccessCallback"
+
+ # makes a function name for the backend
+ makeDownloadFailureCallback: () ->
+ "JamTrackStore.downloadFailureCallback"
+
+
+ reportError: (mixdown) ->
+
+ enqueued = @enqueuedMixdowns[mixdown?.id]
+
+ # don't double-report
+ if !enqueued? || enqueued.marked
+ return
+
+ enqueued.marked = true
+ data = {
+ value: 1,
+ user_id: context.JK.currentUserId,
+ user_name: context.JK.currentUserName,
+ result: "signing state: #{mixdown.myPackage?.signing_state}, client state: #{mixdown.client_state}",
+ mixdown: mixdown.id,
+ package: mixdown.myPackage?.id
+ detail: mixdown.myPackage?.error_reason
+ }
+ rest.createAlert("Mixdown Sync failed for #{context.JK.currentUserName}", data)
+
+ context.stats.write('web.mixdown.error', data)
}
)
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/stores/LanguageStore.js.coffee b/web/app/assets/javascripts/react-components/stores/LanguageStore.js.coffee
new file mode 100644
index 000000000..83128838d
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/LanguageStore.js.coffee
@@ -0,0 +1,25 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+
+@LanguageStore = Reflux.createStore(
+ {
+ listenables: @LanguageActions
+ languages: []
+ languageLookup: {}
+
+ init: ->
+ # Register with the app store to get @app
+ this.listenTo(context.AppStore, this.onAppInit)
+
+ onAppInit: (@app) ->
+ rest.getLanguages().done (languages) =>
+ @languages = languages
+ for language in @languages
+ @languageLookup[language.id] = language.description
+ @trigger(@languages)
+
+ display: (id) ->
+ @languageLookup[id]
+ }
+)
diff --git a/web/app/assets/javascripts/react-components/stores/LessonTimerStore.js.coffee b/web/app/assets/javascripts/react-components/stores/LessonTimerStore.js.coffee
new file mode 100644
index 000000000..b30a6870a
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/LessonTimerStore.js.coffee
@@ -0,0 +1,56 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+rest = new context.JK.Rest()
+
+@LessonTimerStore = Reflux.createStore(
+ {
+ listenables: @LessonTimerActions
+ lessons: {}
+ slowTime: 10000
+ fastTime: 1000
+
+ init: ( ) ->
+ @timer()
+ this.listenTo(context.AppStore, this.onAppInit)
+
+ onAppInit: (@app) ->
+ @lessonUtils = context.JK.LessonUtils
+
+ timer: () ->
+ setInterval((() => @timeout()), @fastTime)
+
+ onLoadLessons: (lessons) ->
+ for lesson in lessons?.entries
+ @lessons[lesson.id] = lesson
+
+ timeout: () ->
+
+ for id, lesson of @lessons
+ untilInfo = @lessonUtils.getTimeRemaining(lesson.music_session.scheduled_start)
+
+ initialWindow = false
+ inThePast = false
+ beforeSession = false
+ startingSoon = false
+ lessonWindow = false
+
+ if untilInfo.total < 0
+ # we are past the start time
+ if -untilInfo.total < (10 * 60 * 1000) # 10 minutes
+ initialWindow = true
+ if -untilInfo.total < (lesson.duration*60*1000) # duration
+ inThePast = true
+ else
+ # we are before the due time
+ beforeSession = true
+ startingSoon = untilInfo.total <= (60 * 60 * 1000) # 60 minutes - 1hr until start time
+
+ lesson.times = {until:untilInfo, initialWindow: initialWindow, beforeSession: beforeSession, startingSoon: startingSoon, inThePast: inThePast}
+
+ @changed()
+
+ changed:() ->
+ this.trigger(@lessons)
+ }
+)
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/stores/LocationStore.js.coffee b/web/app/assets/javascripts/react-components/stores/LocationStore.js.coffee
new file mode 100644
index 000000000..de24a2f17
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/LocationStore.js.coffee
@@ -0,0 +1,74 @@
+$ = jQuery
+context = window
+rest = window.JK.Rest()
+logger = context.JK.logger
+
+@LocationStore = Reflux.createStore(
+ {
+ listenables: @LocationActions
+ countries: {}
+
+ init: ->
+ # Register with the app store to get @app
+ this.listenTo(context.AppStore, this.onAppInit)
+
+ changed: () ->
+ @trigger(@countries)
+
+ onSelectCountry: (country) ->
+ @loadRegions(country)
+
+ onLoad: () ->
+
+ # avoid double-loads
+ if Object.keys(@countries).length == 0
+ rest.getCountries().done ((response) =>
+ countries = response.countriesx
+ if Object.keys(@countries).length == 0
+ for country in countries
+ name = country.countryname
+ if !name?
+ name = country.countrycode
+ @countries[country.countrycode] = {name: name, regions:null}
+
+ @loadRegions('US')
+ )
+
+ loadRegions: (loadForCountry) ->
+ if loadForCountry == null
+ return
+
+ country = @countries[loadForCountry]
+
+ if !country?
+ logger.warn("country is null in searching for: " + loadForCountry)
+ return
+
+ regions = country.regions
+
+ # avoid double-loads
+ if regions == null
+ rest.getRegions({country: loadForCountry}).done ((countriesRegions) =>
+ regions = country.regions
+ if regions == null
+ regions = []
+ country.regions = regions
+
+ if regions.length > 0
+ return
+
+ for region in countriesRegions.regions
+
+ id = region.region
+ name = region.name
+ if !name?
+ name = region.region
+
+ regions.push({id: id, name: name})
+
+ @changed()
+ )
+ onAppInit: (@app) ->
+
+ }
+)
diff --git a/web/app/assets/javascripts/react-components/stores/NavStore.js.coffee b/web/app/assets/javascripts/react-components/stores/NavStore.js.coffee
new file mode 100644
index 000000000..651b899d7
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/NavStore.js.coffee
@@ -0,0 +1,46 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+rest = new context.JK.Rest()
+
+@NavStore = Reflux.createStore(
+ {
+
+ screenPath: null
+ currentSection: null
+ currentScreenName: null
+
+ sections: {jamclass: {name: 'JamClass Home', url: '/client#/jamclass'}, jamtrack: {name: 'JamTrack Home', url: '/client#/jamtrack'}}
+
+ listenables: @NavActions
+
+ init: ->
+ this.listenTo(context.AppStore, this.onAppInit)
+
+ onAppInit: (@app) ->
+
+ onScreenChanged: (screenPath, screenName) ->
+ if screenPath == @screenPath
+ return
+
+ @screenPath = screenPath
+ @currentScreenName = screenName
+ @currentSection = null
+ if screenPath?
+ index = screenPath.indexOf('/')
+ if index > -1
+ rootPath = screenPath.substring(0, index)
+ else
+ rootPath = screenPath
+ @currentSection = @sections[rootPath]
+
+ @changed()
+
+ onSetScreenInfo: (currentScreenName) ->
+ @currentScreenName = currentScreenName
+ @changed()
+
+ changed:() ->
+ @trigger({screenPath: @screenPath, currentScreen: @currentScreen, currentSection: @currentSection, currentScreenName: @currentScreenName})
+ }
+)
diff --git a/web/app/assets/javascripts/react-components/stores/PlatformStore.js.coffee b/web/app/assets/javascripts/react-components/stores/PlatformStore.js.coffee
new file mode 100644
index 000000000..84366fc7e
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/PlatformStore.js.coffee
@@ -0,0 +1,34 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+
+@PlatformStore = Reflux.createStore(
+ {
+ logger: context.JK.logger
+ os: null
+ serial_no: null
+
+ init: ->
+ this.listenTo(context.AppStore, this.onAppInit)
+
+ jamblasterSerialNo: () ->
+
+ if @serial_no?
+ return @serial_no
+
+ if context.jamClient && context.jamClient.jamBlasterSerialNo
+ @serial_no = context.jamClient.jamBlasterSerialNo()
+ else
+ @serial_no= false
+
+ @serial_no
+
+ onAppInit: (@app) ->
+ @os = context.jamClient.GetOSAsString()
+ this.trigger({os: @os, isWindows: @isWindows()})
+
+ isWindows: ->
+ @os == 'Win32'
+
+ }
+)
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/stores/ProfileStore.js.coffee b/web/app/assets/javascripts/react-components/stores/ProfileStore.js.coffee
new file mode 100644
index 000000000..6a801e115
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/ProfileStore.js.coffee
@@ -0,0 +1,113 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+
+ProfileActions = @ProfileActions
+
+@ProfileStore = Reflux.createStore(
+ {
+ listenables: ProfileActions
+
+ customBack: null
+ customBackDisplay: null
+ returnNav: null
+ solo: false
+
+ # step can be:
+ # introduction
+ # basics
+ # pricing
+ # experience
+ onStartProfileEdit: (step, solo) ->
+
+ if !step?
+ step = ''
+
+ if step != '' && step != 'samples' && step != 'interests' && step != 'experience'
+ alert("invalid step: " + step)
+ return
+
+ @solo = solo
+ @returnNav = window.location.href
+
+ window.location = '/client#/account/profile/' + step
+
+ onDoneProfileEdit: () ->
+
+ if @returnNav
+ window.location = @returnNav
+ @returnNav = null
+ else
+ window.location = "/client#/profile/" + context.JK.currentUserId;
+
+ @solo = false
+
+ onCancelProfileEdit: () ->
+
+ if @returnNav
+ window.location = @returnNav
+ @returnNav = null
+ else
+ window.location = '/client#/profile/' + context.JK.currentUserId
+ @solo = false
+
+ onStartTeacherEdit: (step, solo) ->
+
+ if !step?
+ step = 'introduction'
+
+ if step != 'introduction' && step != 'basics' && step != 'pricing' && step != 'experience'
+ alert("invalid step: " + step)
+ return
+
+ @solo = solo
+ @returnNav = window.location.href
+
+ window.location = '/client#/teachers/setup/' + step
+
+ onDoneTeacherEdit: () ->
+
+ if @solo
+ if @returnNav
+ window.location = @returnNav
+ @returnNav = null
+ else
+ window.location = '/client#/home'
+
+ @solo = false
+
+ onCancelTeacherEdit: () ->
+
+ if @returnNav
+ window.location = @returnNav
+ @returnNav = null
+ else
+ window.location = '/client#/home'
+
+ @solo = false
+
+ onEditProfileNext: (step) ->
+
+ if @solo
+ if @returnNav
+ window.location = @returnNav
+ @returnNav = null
+ else
+ window.location = '/client#/home'
+ @solo = false
+ else
+ context.location = "/client#/account/profile/" + step
+
+ onViewTeacherProfile: (user, customBack, customBackDisplay) ->
+
+ @customBack = customBack
+ @customBackDisplay = customBackDisplay
+ context.location = "/client#/profile/teacher/#{user.id}"
+
+ onViewTeacherProfileDone: () ->
+ @customBack = null
+ @customBackDisplay = null
+ }
+)
+
+
diff --git a/web/app/assets/javascripts/react-components/stores/RecordingStore.js.jsx.coffee b/web/app/assets/javascripts/react-components/stores/RecordingStore.js.jsx.coffee
index f68e4d091..682d8744e 100644
--- a/web/app/assets/javascripts/react-components/stores/RecordingStore.js.jsx.coffee
+++ b/web/app/assets/javascripts/react-components/stores/RecordingStore.js.jsx.coffee
@@ -2,6 +2,15 @@ $ = jQuery
context = window
logger = context.JK.logger
+BackendToFrontendFPS = {
+
+ 0: 30,
+ 1: 24,
+ 2: 20,
+ 3: 15,
+ 4: 10
+}
+
@RecordingStore = Reflux.createStore(
{
listenables: @RecordingActions
@@ -23,8 +32,17 @@ logger = context.JK.logger
@recordingModel = recordingModel
this.trigger({isRecording: @recordingModel.isRecording()})
- onStartRecording: () ->
- @recordingModel.startRecording()
+ onStartRecording: (recordVideo, recordChat) ->
+
+ frameRate = context.jamClient.GetCurrentVideoFrameRate() || 0;
+
+ frameRate = BackendToFrontendFPS[frameRate]
+
+ NoVideoRecordActive = 0
+ WebCamRecordActive = 1
+ ScreenRecordActive = 2
+ logger.debug("onStartRecording: recordVideo: #{recordVideo}, recordChat: #{recordChat} frameRate: #{frameRate}")
+ @recordingModel.startRecording(recordVideo, recordChat, frameRate)
onStopRecording: () ->
@recordingModel.stopRecording()
@@ -43,14 +61,23 @@ logger = context.JK.logger
onStoppingRecording: (details) ->
details.cause = 'stopping'
+
this.trigger(details)
onStoppedRecording: (details) ->
details.cause = 'stopped'
+
+ if @recordingWindow?
+ @recordingWindow.close()
+
this.trigger(details)
onAbortedRecording: (details) ->
details.cause = 'aborted'
+
+ if @recordingWindow?
+ @recordingWindow.close()
+
this.trigger(details)
onOpenRecordingControls: () ->
@@ -67,7 +94,7 @@ logger = context.JK.logger
popupRecordingControls: () ->
logger.debug("poupRecordingControls")
- @recordingWindow = window.open("/popups/recording-controls", 'Recording', 'scrollbars=yes,toolbar=no,status=no,height=315,width=350')
+ @recordingWindow = window.open("/popups/recording-controls", 'Recording', 'scrollbars=yes,toolbar=no,status=no,height=315,width=340')
@recordingWindow.ParentRecordingStore = context.RecordingStore
@recordingWindow.ParentIsRecording = @recordingModel.isRecording()
diff --git a/web/app/assets/javascripts/react-components/stores/SchoolStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SchoolStore.js.coffee
new file mode 100644
index 000000000..fd5a1785f
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/SchoolStore.js.coffee
@@ -0,0 +1,77 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+rest = new context.JK.Rest()
+
+@SchoolStore = Reflux.createStore(
+ {
+ school: null,
+ teacherInvitations: null
+ studentInvitations: null
+
+ listenables: @SchoolActions
+
+ init: ->
+ this.listenTo(context.AppStore, this.onAppInit)
+
+ onAppInit: (@app) ->
+
+ onLoaded: (response) ->
+ @school = response
+
+ if @school.photo_url?
+ @school.photo_url = @school.photo_url + '?cache-bust=' + new Date().getTime()
+
+ if @school.large_photo_url?
+ @school.large_photo_url = @school.large_photo_url + '?cache-bust=' + new Date().getTime()
+
+ @changed()
+ rest.listSchoolInvitations({id:@school.id, as_teacher: true}).done((response) => @onLoadedTeacherInvitations(response)).fail((jqXHR) => @onSchoolInvitationFail(jqXHR))
+
+ onLoadedTeacherInvitations: (response) ->
+ @teacherInvitations = response.entries
+ @changed()
+ rest.listSchoolInvitations({id:@school.id, as_teacher: false}).done((response) => @onLoadedStudentInvitations(response)).fail((jqXHR) => @onSchoolInvitationFail(jqXHR))
+
+ onLoadedStudentInvitations: (response) ->
+ @studentInvitations = response.entries
+ @changed()
+
+ onAddInvitation: (isTeacher, invitation) ->
+ if isTeacher
+ @teacherInvitations.push(invitation)
+ else
+ @studentInvitations.push(invitation)
+ @changed()
+
+ onDeleteInvitation: (id) ->
+ if @studentInvitations?
+ @studentInvitations = @studentInvitations.filter (invitation) -> invitation.id isnt id
+
+ if @teacherInvitations?
+ @teacherInvitations = @teacherInvitations.filter (invitation) -> invitation.id isnt id
+
+ @changed()
+
+ onRefresh: (schoolId) ->
+ if !schoolId?
+ schoolId = @school?.id
+ rest.getSchool({id: schoolId}).done((response) => @onLoaded(response)).fail((jqXHR) => @onSchoolFail(jqXHR))
+
+ onUpdateSchool: (school) ->
+ @school = school
+ @changed()
+
+ onSchoolFail:(jqXHR) ->
+ @app.layout.notify({title: 'Unable to Request School Info', text: "We recommend you refresh the page."})
+
+ onSchoolInvitationFail:(jqXHR) ->
+ @app.layout.notify({title: 'Unable to Request School InvitationInfo', text: "We recommend you refresh the page."})
+
+ changed:() ->
+ @trigger(@getState())
+
+ getState:() ->
+ {school: @school, studentInvitations: @studentInvitations, teacherInvitations: @teacherInvitations}
+ }
+)
diff --git a/web/app/assets/javascripts/react-components/stores/SessionStatsStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SessionStatsStore.js.coffee
new file mode 100644
index 000000000..00439ff3c
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/SessionStatsStore.js.coffee
@@ -0,0 +1,158 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+rest = context.JK.Rest()
+EVENTS = context.JK.EVENTS
+MIX_MODES = context.JK.MIX_MODES
+
+SessionActions = @SessionActions
+
+SessionStatThresholds = gon.session_stat_thresholds
+NetworkThresholds = SessionStatThresholds.network
+SystemThresholds = SessionStatThresholds.system
+AudioThresholds = SessionStatThresholds.audio
+AggregateThresholds = SessionStatThresholds.aggregate
+
+@SessionStatsStore = Reflux.createStore(
+ {
+ listenables: @SessionStatsActions
+ rawStats: null
+
+ init: ->
+ # Register with the app store to get @app
+ this.listenTo(context.AppStore, this.onAppInit)
+
+ onAppInit: (@app) ->
+
+ onPushStats: (stats) ->
+ @rawStats = stats
+ @changed()
+
+ classify: (holder, field, threshold) ->
+ value = holder[field]
+ fieldLevel = field + '_level'
+ fieldThreshold = threshold[field]
+
+ if value? && fieldThreshold?
+
+ if fieldThreshold.inverse
+ if fieldThreshold.zero_is_good
+ holder[fieldLevel] = 'good'
+ else if value <= fieldThreshold.poor
+ holder[fieldLevel] = 'poor'
+ @participantClassification = 3
+ else if value <= fieldThreshold.warn
+ holder[fieldLevel] = 'warn'
+ @participantClassification = 2 if @participantClassification == 1
+ else
+ holder[fieldLevel] = 'good'
+ else if fieldThreshold.eql
+ if value == fieldThreshold.poor
+ holder[fieldLevel] = 'poor'
+ @participantClassification = 3
+ else if value == fieldThreshold.warn
+ holder[fieldLevel] = 'warn'
+ @participantClassification = 2 if @participantClassification == 1
+ else
+ holder[fieldLevel] = 'good'
+ else
+ if value >= fieldThreshold.poor
+ holder[fieldLevel] = 'poor'
+ @participantClassification = 3
+ else if value >= fieldThreshold.warn
+ holder[fieldLevel] = 'warn'
+ @participantClassification = 2 if @participantClassification == 1
+ else
+ holder[fieldLevel] = 'good'
+
+
+ changed: () ->
+ @stats = {}
+
+ self = null
+ for participant in @rawStats
+ if participant.id == @app.clientId
+ self = participant
+ break
+
+ for participant in @rawStats
+
+ aggregate = {}
+
+ @participantClassification = 1 # 1=good, 2=warn, 3=poor
+
+ total_latency = 0
+
+ if participant.cpu?
+ system = {cpu: participant.cpu}
+
+ @classify(system, 'cpu', SystemThresholds)
+
+ participant.system = system
+
+ network = participant.network
+
+ if network?
+ @classify(network, 'audiojq_median', NetworkThresholds)
+ @classify(network, 'jitter_var', NetworkThresholds)
+ @classify(network, 'audio_bitrate_rx', NetworkThresholds)
+ @classify(network, 'audio_bitrate_tx', NetworkThresholds)
+ @classify(network, 'video_rtpbw_tx', NetworkThresholds)
+ @classify(network, 'video_rtpbw_rx', NetworkThresholds)
+ @classify(network, 'ping', NetworkThresholds)
+ @classify(network, 'pkt_loss', NetworkThresholds)
+ @classify(network, 'wifi', NetworkThresholds)
+
+ total_latency += network.ping / 2
+ total_latency += network.audiojq_median * 2.5
+ aggregate.one_way = network.ping / 2
+ aggregate.jq = network.audiojq_median * 2.5
+ else
+ total_latency = null
+
+ audio = participant.audio
+
+ if audio?
+ if audio.cpu?
+ system = {cpu: audio.cpu}
+ @classify(system, 'cpu', SystemThresholds)
+ participant.system = system
+
+ if audio.in_latency? and audio.out_latency?
+ audio.latency = audio.in_latency + audio.out_latency
+
+
+ @classify(audio, 'framesize', AudioThresholds)
+ @classify(audio, 'latency', AudioThresholds)
+ @classify(audio, 'input_jitter', AudioThresholds)
+ @classify(audio, 'output_jitter', AudioThresholds)
+ @classify(audio, 'audio_in_type', AudioThresholds)
+
+ if total_latency != null
+ total_latency += audio.out_latency
+ total_latency += self.audio.in_latency
+ aggregate.their_out_latency = audio.out_latency
+ aggregate.your_in_latency = self.audio.in_latency
+ else
+ total_latency = null
+
+ if participant.id != @app.clientId
+
+ aggregate.latency = total_latency
+
+ @classify(aggregate, 'latency', AggregateThresholds)
+
+ participant.aggregate = aggregate
+
+ switch @participantClassification
+ when 1 then participant.classification = 'good'
+ when 2 then participant.classification = 'warn'
+ when 3 then participant.classification = 'poor'
+ else
+ participant.classification = 'unknown'
+
+ @stats[participant.id] = participant
+
+ @trigger(@stats)
+ }
+)
\ No newline at end of file
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 c91ce7e0c..d1886455e 100644
--- a/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee
+++ b/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee
@@ -4,11 +4,14 @@ logger = context.JK.logger
rest = context.JK.Rest()
EVENTS = context.JK.EVENTS
MIX_MODES = context.JK.MIX_MODES
+CLIENT_ROLE = context.JK.CLIENT_ROLE
JamTrackActions = @JamTrackActions
SessionActions = @SessionActions
RecordingActions = @RecordingActions
NotificationActions = @NotificationActions
+VideoActions = @VideoActions
+ConfigureTracksActions = @ConfigureTracksActions
@SessionStore = Reflux.createStore(
{
@@ -42,6 +45,7 @@ NotificationActions = @NotificationActions
# Register with the app store to get @app
this.listenTo(context.AppStore, this.onAppInit)
this.listenTo(context.RecordingStore, this.onRecordingChanged)
+ this.listenTo(context.VideoStore, this.onVideoChanged)
onAppInit: (@app) ->
@@ -49,26 +53,42 @@ NotificationActions = @NotificationActions
@sessionUtils = context.JK.SessionUtils
@recordingModel = new context.JK.RecordingModel(@app, rest, context.jamClient);
RecordingActions.initModel(@recordingModel)
- @helper = new context.SessionHelper(@app, @currentSession, @participantsEverSeen, @isRecording, @downloadingJamTrack)
+ @helper = new context.SessionHelper(@app, @currentSession, @participantsEverSeen, @isRecording, @downloadingJamTrack, @enableVstTimeout?)
- if gon.global.video_available && gon.global.video_available!="none" && context.JK.WebcamViewer?
- @webcamViewer = new context.JK.WebcamViewer()
- $sessionLayout = $("#create-session-layout")
- @webcamViewer.init($sessionLayout, false)
- @webcamViewer.setVideoOff()
+ onSessionJoinedByOther: (payload) ->
+ clientId = payload.client_id
+
+ #parentClientId = context.jamClient.getParentClientId()
+ #if parentClientId? && parentClientId != ''
+ #if parentClientId == clientId
+ # auto nav to session
+ if context.jamClient.getClientParentChildRole && context.jamClient.getClientParentChildRole() == CLIENT_ROLE.CHILD && payload.source_user_id == context.JK.currentUserId
+ logger.debug("autonav to session #{payload.session_id}")
+ context.SessionActions.navToSession(payload.session_id)
+
+ onNavToSession: (sessionId) ->
+ context.location = '/client#/session/' + sessionId
+
+ onMixdownActive: (mixdown) ->
+ if @currentSession?.jam_track?
+ @currentSession.jam_track.mixdown = mixdown
+ @issueChange()
+
+
+ onVideoChanged: (@videoState) ->
issueChange: () ->
- @helper = new context.SessionHelper(@app, @currentSession, @participantsEverSeen, @isRecording, @downloadingJamTrack)
+ @helper = new context.SessionHelper(@app, @currentSession, @participantsEverSeen, @isRecording, @downloadingJamTrack, @enableVstTimeout?)
this.trigger(@helper)
onWindowBackgrounded: () ->
- @app.user()
- .done((userProfile) =>
- if userProfile.show_whats_next &&
- window.location.pathname.indexOf(gon.client_path) == 0 &&
- !@app.layout.isDialogShowing('getting-started')
- @app.layout.showDialog('getting-started')
- )
+ #@app.user()
+ #.done((userProfile) =>
+ #if userProfile.show_whats_next &&
+ # window.location.pathname.indexOf(gon.client_path) == 0 &&
+ # !@app.layout.isDialogShowing('getting-started')
+ # @app.layout.showDialog('getting-started')
+ #)
return unless @inSession()
@@ -182,8 +202,16 @@ NotificationActions = @NotificationActions
@issueChange()
onToggleSessionVideo: () ->
- logger.debug("toggle session video")
- @webcamViewer.toggleWebcam() if @webcamViewer?
+
+ if @videoState?.videoEnabled
+ logger.debug("toggle session video")
+ VideoActions.toggleVideo()
+ else
+ context.JK.Banner.showAlert({
+ title: "Video Is Disabled",
+ html: "To re-enable video, you must go your video settings in your account settings and enable video.",
+ })
+
onAudioResync: () ->
logger.debug("audio resyncing")
@@ -234,6 +262,7 @@ NotificationActions = @NotificationActions
rest.closeJamTrack({id: @currentSessionId})
.done(() =>
+ @downloadingJamTrack = false
@refreshCurrentSession(true)
)
.fail((jqXHR) =>
@@ -362,6 +391,9 @@ NotificationActions = @NotificationActions
MixerActions.syncTracks()
)
, 100)
+ else if text == 'Midi-Track Update'
+ logger.debug('midi track sync')
+ MixerActions.syncTracks()
else if text == 'RebuildMediaControl' || text == 'RebuildRemoteUserControl'
backingTracks = trackInfo.backingTracks
@@ -520,6 +552,15 @@ NotificationActions = @NotificationActions
# TODO: find it via some REST API if not found?
return $.Deferred().reject().promise();
+ findParticipantByUserId: (userId) ->
+ foundParticipant = null
+ for participant in @participants()
+ if participant.user.id == userId
+ foundParticipant = participant
+ break
+
+ foundParticipant
+
displayWhoCreatedRecording: (clientId) ->
if @app.clientId != clientId # don't show to creator
@findUserBy({clientId: clientId})
@@ -552,6 +593,12 @@ NotificationActions = @NotificationActions
)
.fail(@app.ajaxError)
+ onEnterSession: (sessionId) ->
+ if !context.JK.guardAgainstBrowser(@app)
+ return false;
+
+ window.location.href = '/client#/session/' + sessionId
+
onJoinSession: (sessionId) ->
# poke ShareDialog
@@ -559,8 +606,7 @@ NotificationActions = @NotificationActions
shareDialog.initialize(context.JK.FacebookHelperInstance);
# initialize webcamViewer
- if gon.global.video_available && gon.global.video_available != "none"
- @webcamViewer.beforeShow()
+ VideoActions.stopVideo();
# double-check that we are connected to the server via websocket
@@ -584,6 +630,18 @@ NotificationActions = @NotificationActions
shouldVerifyNetwork = musicSession.musician_access;
+ # old client protection
+ if !context.jamClient.getClientParentChildRole?
+ clientRole = CLIENT_ROLE.PARENT
+ else
+ clientRole = context.jamClient.getClientParentChildRole()
+
+ if clientRole == CLIENT_ROLE.CHILD
+ logger.debug("client is configured to act as child. skipping all checks. assuming 0 tracks")
+ @userTracks = []
+ @joinSession()
+ return
+
@gearUtils.guardAgainstInvalidConfiguration(@app, shouldVerifyNetwork).fail(() =>
SessionActions.leaveSession.trigger({location: '/client#/home'})
).done(() =>
@@ -716,9 +774,14 @@ NotificationActions = @NotificationActions
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
+ $(document).trigger(EVENTS.SESSION_STARTED, {session: {id: @currentSessionId, lesson_session: response.lesson_session}}) if document
@handleAutoOpenJamTrack()
+
+ @watchBackendStats()
+
+ ConfigureTracksActions.reset(true)
+ @delayEnableVst()
)
.fail((xhr) =>
@updateCurrentSession(null)
@@ -750,6 +813,36 @@ NotificationActions = @NotificationActions
@app.notifyServerError(xhr, 'Unable to Join Session');
)
+ delayEnableVst: () ->
+ if @enableVstTimeout?
+ clearTimeout(@enableVstTimeout)
+ @enableVstTimeout = null
+
+ isVstLoaded = context.jamClient.IsVstLoaded()
+ hasVstAssignment = context.jamClient.hasVstAssignment()
+
+ if hasVstAssignment && !isVstLoaded
+ @enableVstTimeout = setTimeout((() =>
+ @enableVst()
+ ), 5000)
+ @issueChange()
+
+ enableVst: () ->
+ @enableVstTimeout = null
+
+ if @inSession()
+ ConfigureTracksActions.enableVst()
+ else
+ logger.debug("no longer in session; not enabling VSTs at this time")
+ @issueChange()
+
+ watchBackendStats: () ->
+ @backendStatsInterval = window.setInterval((() => (@updateBackendStats())), 1000)
+
+ updateBackendStats: () ->
+ connectionStats = window.jamClient.getConnectionDetail('')
+ SessionStatsActions.pushStats(connectionStats)
+
trackChanges: (header, payload) ->
if @currentTrackChanges < payload.track_changes_counter
# we don't have the latest info. try and go get it
@@ -939,6 +1032,11 @@ NotificationActions = @NotificationActions
@currentSession = sessionData
+ if context.jamClient.UpdateSessionInfo?
+ if @currentSession?
+ context.jamClient.UpdateSessionInfo(@currentSession)
+ else
+ context.jamClient.UpdateSessionInfo({})
#logger.debug("session changed")
@issueChange()
@@ -974,13 +1072,48 @@ NotificationActions = @NotificationActions
logger.warn("no location specified in leaveSession action", behavior)
window.location = '/client#/home'
- if gon.global.video_available && gon.global.video_available != "none"
- @webcamViewer.beforeHide()
+ #VideoActions.stopVideo()
+
+
+ if @currentSession?.lesson_session?
+ isTeacher = context.JK.currentUserId == @currentSession.lesson_session.teacher_id
+
+ tempSession = @currentSession
+ rest.ratingDecision({
+ as_student: !isTeacher,
+ teacher_id: @currentSession.lesson_session.teacher_id,
+ student_id: @currentSession.lesson_session.teacher_id
+ }).done(((decision) =>
+ showDialog = !decision.rating || showDialog = decision.lesson_count % 6 == 0
+
+ if showDialog
+ if isTeacher
+ @app.layout.showDialog('rate-user-dialog', {d1: 'student_' + tempSession.lesson_session.student_id})
+ else
+ @app.layout.showDialog('rate-user-dialog', {d1: 'teacher_' + tempSession.lesson_session.teacher_id})
+ else
+ unless @rateSessionDialog?
+ @rateSessionDialog = new context.JK.RateSessionDialog(context.JK.app);
+ @rateSessionDialog.initialize();
+
+ @rateSessionDialog.showDialog();
+ ))
+
+
+
+ else
+ unless @rateSessionDialog?
+ @rateSessionDialog = new context.JK.RateSessionDialog(context.JK.app);
+ @rateSessionDialog.initialize();
+
+ @rateSessionDialog.showDialog();
@leaveSession()
@sessionUtils.SessionPageLeave()
+
+
leaveSession: () ->
if !@joinDeferred? || @joinDeferred?.state() == 'resolved'
@@ -1039,6 +1172,10 @@ NotificationActions = @NotificationActions
@userTracks = null;
@startTime = null;
+ if @backendStatsInterval?
+ window.clearInterval(@backendStatsInterval)
+ @backendStatsInterval = null
+
if @joinDeferred?.state() == 'resolved'
$(document).trigger(EVENTS.SESSION_ENDED, {session: {id: @currentSessionId}})
@@ -1067,5 +1204,22 @@ NotificationActions = @NotificationActions
getCurrentOrLastSession: () ->
@currentOrLastSession
+ handleJoinLeaveRequestCallback: (data) ->
+ op = data["op"]
+ logger.debug("client asks #{op} for #{data["id"]}")
+ if op == "join"
+ sessionId = data["id"]
+ if sessionId != @currentSessionId
+ window.location = "/client#/session/" + sessionId
+ else
+ logger.debug("dropped #{op} because sessionId #{sessionId} matches currentSessionId")
+ else if op == "leave"
+ sessionId = data["id"]
+ if sessionId == @currentSessionId
+ @onLeaveSession({location: '/client#/home'})
+ else
+ logger.debug("dropped #{op} because sessionId #{sessionId} does not match currentSessionId #{@currentSessionId}")
+
+
}
)
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/stores/StripeStore.js.coffee b/web/app/assets/javascripts/react-components/stores/StripeStore.js.coffee
new file mode 100644
index 000000000..7b2cd26c6
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/StripeStore.js.coffee
@@ -0,0 +1,53 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+rest = new context.JK.Rest()
+
+@StripeStore = Reflux.createStore(
+ {
+
+ listenables: @StripeActions
+
+ init: ->
+ this.listenTo(context.AppStore, this.onAppInit)
+
+ onAppInit: (@app) ->
+
+ onConnect: (purpose, user) ->
+ if purpose == 'school'
+ redirect = '/client#/account/school'
+ else if purpose == 'jamclass-home'
+ redirect = '/client#/jamclass'
+ else
+ throw "unknown purpose #{purpose}"
+
+ rest.createRedirectHint({redirect_location:redirect}).done((response) => @onHintDone(response, user)).fail(@app.ajaxError)
+
+ onHintDone: (response, user) ->
+ redirectUri = encodeURIComponent(context.JK.makeAbsolute('/auth/stripe_connect/callback'))
+ teacherProfileUri = encodeURIComponent(context.JK.makeAbsolute("/client#/profile/teacher/#{user.id}"))
+ email = encodeURIComponent(user.email)
+ firstName = encodeURIComponent(user.first_name)
+ lastName = encodeURIComponent(user.last_name)
+
+ url = "/auth/stripe_connect?redirect_uri=#{redirectUri}&scope=read_write&stripe_user[url]=#{teacherProfileUri}&stripe_user[email]=#{email}&stripe_user[first_name]=#{firstName}&stripe_user[last_name]=#{lastName}"
+
+ # product description
+ productDescription = "Online music lessons billed either per lesson or per month"
+ url += "&stripe_user[product_description]=#{productDescription}"
+
+ # business type
+ url += "&stripe_user[business_type]=sole_prop"
+
+ # business name
+ businessName = "#{user.name} Lessons"
+ url += "&stripe_user[business_name]=#{businessName}"
+
+ # country
+ if user.country?
+ url += "&stripe_user[country]=#{encodeURIComponent(user.country)}"
+
+
+ window.location.href = url
+ }
+)
diff --git a/web/app/assets/javascripts/react-components/stores/SubjectStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SubjectStore.js.coffee
new file mode 100644
index 000000000..13d35b89f
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/SubjectStore.js.coffee
@@ -0,0 +1,27 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+
+@SubjectStore = Reflux.createStore(
+ {
+ listenables: @SubjectActions
+ subjects: []
+ subjectLookup: {}
+
+ init: ->
+ # Register with the app store to get @app
+ this.listenTo(context.AppStore, this.onAppInit)
+
+ onAppInit: (@app) ->
+ rest.getSubjects().done (subjects) =>
+ @subjects = subjects
+ for subject in subjects
+ @subjectLookup[subject.id] = subject.description
+
+ @trigger(@subjects)
+
+ display: (id) ->
+ @subjectLookup[id]
+
+ }
+)
diff --git a/web/app/assets/javascripts/react-components/stores/TeacherSearchResultsStore.js.coffee b/web/app/assets/javascripts/react-components/stores/TeacherSearchResultsStore.js.coffee
new file mode 100644
index 000000000..e81153fe6
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/TeacherSearchResultsStore.js.coffee
@@ -0,0 +1,92 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+rest = context.JK.Rest()
+
+TeacherSearchResultsActions = @TeacherSearchResultsActions
+
+@TeacherSearchResultsStore = Reflux.createStore(
+ {
+ listenables: TeacherSearchResultsActions
+ results: []
+ page: 1
+ limit: 20
+ searching: false
+
+ init: ->
+ # Register with the app store to get @app
+ this.listenTo(context.AppStore, this.onAppInit)
+
+ onAppInit: (app) ->
+ @app = app
+
+ onReset: () ->
+ @results = []
+ @page = 1
+ @searching = true
+ @changed()
+
+ query = @createQuery()
+
+ rest.searchTeachers(query)
+ .done((response) =>
+ @next = response.next
+ @searching = false
+ @results.push.apply(@results, response.entries)
+ @changed()
+ )
+ .fail((jqXHR, textStatus, errorMessage) =>
+ @searching = false
+ @changed()
+ @app.ajaxError(jqXHR, textStatus, errorMessage)
+ )
+
+ nextPage: () ->
+ @page += 1
+
+ query = @createQuery()
+
+ @searching = true
+ rest.searchTeachers(query)
+ .done((response) =>
+ @next = response.next
+ @results.push.apply(@results, response.entries)
+ @searching = false
+ @changed()
+ )
+ .fail((jqXHR, textStatus, errorMessage) =>
+ @searching = false
+ @app.ajaxError(jqXHR, textStatus, errorMessage)
+ @changed()
+ )
+
+ getState: () ->
+ ({results: @results, next: @next, currentPage: @page, searching: @searching})
+
+ changed:() ->
+ @trigger(@getState())
+
+ createQuery: () ->
+
+ searchOptions = context.TeacherSearchStore.getState()
+
+ query = {}
+ query.page = @page
+ query.per_page = @limit
+ query.instruments = searchOptions.instruments
+ query.subjects = searchOptions.subjects
+ query.genres = searchOptions.genres
+ query.languages = searchOptions.languages
+ query.teaches_beginner = searchOptions.teaches_beginner
+ query.teaches_intermediate = searchOptions.teaches_intermediate
+ query.teaches_advanced = searchOptions.teaches_advanced
+ query.student_age = searchOptions['ages-taught']
+ query.years_teaching = searchOptions['years-teaching']
+ query.country = searchOptions.location?.country
+ query.region = searchOptions.location?.region
+ query.onlyMySchool = searchOptions.onlyMySchool
+ query
+
+ query
+ }
+)
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/stores/TeacherSearchStore.js.coffee b/web/app/assets/javascripts/react-components/stores/TeacherSearchStore.js.coffee
new file mode 100644
index 000000000..ea67beddb
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/TeacherSearchStore.js.coffee
@@ -0,0 +1,39 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+rest = context.JK.Rest()
+
+TeacherSearchActions = @TeacherSearchActions
+
+@TeacherSearchStore = Reflux.createStore(
+ {
+ listenables: TeacherSearchActions
+ searchOptions: {onlyMySchool: true}
+ viewingTeacher: null
+
+ init: ->
+ # Register with the app store to get @app
+ this.listenTo(context.AppStore, this.onAppInit)
+
+
+ onAppInit: (app) ->
+ @app = app
+
+ onUpdateOptions: (options) ->
+ @searchOptions = options
+
+ onSearch: (searchOptions) ->
+
+ @searchOptions = searchOptions
+
+ @changed()
+
+ window.location = "/client#/teachers/search"
+
+ getState: () ->
+ @searchOptions || {}
+
+ changed:() ->
+ @trigger(@getState())
+ }
+)
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/stores/TeacherStore.js.coffee b/web/app/assets/javascripts/react-components/stores/TeacherStore.js.coffee
new file mode 100644
index 000000000..03a9705a1
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/TeacherStore.js.coffee
@@ -0,0 +1,80 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+rest = context.JK.Rest()
+EVENTS = context.JK.EVENTS
+@teacherActions = window.JK.Actions.Teacher
+
+@TeacherStore = Reflux.createStore({
+ listenables: @teacherActions
+ teacher: null
+
+ init: ->
+ # Register with the app store to get @app
+ this.listenTo(context.AppStore, this.onAppInit)
+ this.listenTo(context.TeacherActions.load, this.onLoadTeacher)
+ this.listenTo(context.TeacherActions.change, this.onSaveTeacher)
+
+ onAppInit: (app) ->
+ @app = app
+
+ defaultTeacher: ->
+ {
+ experiences_teaching: []
+ experiences_education: []
+ experiences_award: []
+ }
+
+ defaults: (teacher) ->
+
+ if teacher.languages.length == 0
+ teacher.languages.push('EN')
+
+ onLoadTeacher: (options) ->
+ logger.debug("onLoadTeacher", options)
+ if !options?
+ throw new Error('@teacher must be specified')
+
+ rest.getTeacher(options)
+ .done((savedTeacher) =>
+ logger.debug("LOADING TEACHER",savedTeacher)
+
+ @defaults(savedTeacher)
+
+ this.trigger({teacher: savedTeacher}))
+ .fail((jqXHR, textStatus, errorMessage) =>
+ logger.debug("FAILED",jqXHR, textStatus, errorMessage)
+ if (jqXHR.status==404)
+ this.trigger({teacher: this.defaultTeacher()})
+ else
+ context.JK.app.ajaxError(jqXHR, textStatus, errorMessage)
+ )
+
+ onSaveTeacher: (teacher, instructions) ->
+ logger.debug("onSaveTeacher", teacher, instructions)
+ rest.updateTeacher(teacher)
+ .done((savedTeacher) =>
+ logger.debug("SAVED TEACHER",savedTeacher)
+ this.trigger({teacher: savedTeacher})
+ if ProfileStore.solo
+ ProfileActions.doneTeacherEdit()
+ else
+ if instructions.navTo?
+ logger.debug("NAVIGATING TO",instructions.navTo)
+ window.location = instructions.navTo
+ ).fail((jqXHR, textStatus, errorMessage) =>
+ logger.debug("FAILED",jqXHR, textStatus, errorMessage)
+ #errors = JSON.parse(jqXHR.responseText)
+
+ if (jqXHR.status==422)
+ logger.debug("FAILED422",jqXHR.responseJSON.errors)
+ this.trigger({errors: jqXHR.responseJSON.errors})
+ if instructions?.instructions?.direction == 'back'
+ if instructions.navTo?
+ logger.debug("NAVIGATING TO",instructions.navTo)
+ window.location = instructions.navTo
+ else
+ context.JK.app.ajaxError(textStatus)
+ )
+ }
+)
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/stores/UserActivityStore.js.coffee b/web/app/assets/javascripts/react-components/stores/UserActivityStore.js.coffee
new file mode 100644
index 000000000..461fdee40
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/UserActivityStore.js.coffee
@@ -0,0 +1,27 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+
+@UserActivityStore = Reflux.createStore(
+ {
+ active: true
+
+ listenables: @UserActivityActions
+
+ init: ->
+ this.listenTo(context.AppStore, this.onAppInit)
+
+ onAppInit: (@app) ->
+
+ onSetActive: (active) ->
+ if active != @active
+ @active = active
+ @changed()
+
+ changed:() ->
+ @trigger(@getState())
+
+ getState:() ->
+ {active: @active}
+ }
+)
diff --git a/web/app/assets/javascripts/react-components/stores/UserStore.js.coffee b/web/app/assets/javascripts/react-components/stores/UserStore.js.coffee
new file mode 100644
index 000000000..0e41ef608
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/UserStore.js.coffee
@@ -0,0 +1,52 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+rest = new context.JK.Rest()
+
+@UserStore = Reflux.createStore(
+ {
+ user: null
+
+ listenables: @UserActions
+
+ init: ->
+ this.listenTo(context.AppStore, this.onAppInit)
+
+ onAppInit: (@app) ->
+ if !@user?
+ @loadAnonymousUser()
+
+ loadAnonymousUser: () ->
+ @user = {id: null, has_redeemable_jamtrack: context.JK.currentUserFreeJamTrack, purchased_jamtracks_count:0, show_free_jamtrack: context.JK.currentUserFreeJamTrack }
+ @changed()
+
+ postProcess: () ->
+ if @user.user_authorizations?
+ for auth in @user.user_authorizations
+ if auth.provider == 'stripe_connect'
+ if !auth.token_expiration || new Date(auth.token_expiration).getTime() > new Date().getTime()
+ @user.stripe_auth = auth
+
+
+ onLoaded:(user) ->
+ @user = user
+ @changed()
+
+ onModify: (changes) ->
+ @user = $.extend({}, @user, changes)
+ @changed()
+
+ onRefresh: () ->
+ rest.getUserDetail().done((response) => @onLoaded(response)).fail((jqXHR) => @onUserFail(jqXHR))
+
+ onUserFail:(jqXHR) ->
+ @app.layout.notify({title: 'Unable to Update User Info', text: "We recommend you refresh the page."})
+
+ changed:() ->
+ @postProcess()
+ @trigger({user: @user})
+
+ getState:() ->
+ {user: @user}
+ }
+)
diff --git a/web/app/assets/javascripts/react-components/stores/VideoStore.js.coffee b/web/app/assets/javascripts/react-components/stores/VideoStore.js.coffee
index 540ca965b..fa1f7282c 100644
--- a/web/app/assets/javascripts/react-components/stores/VideoStore.js.coffee
+++ b/web/app/assets/javascripts/react-components/stores/VideoStore.js.coffee
@@ -6,6 +6,14 @@ NAMED_MESSAGES = context.JK.NAMED_MESSAGES
VideoActions = @VideoActions
+BackendToFrontendFPS = {
+ 0: 30,
+ 1: 24,
+ 2: 20,
+ 3: 15,
+ 4: 10
+}
+
@VideoStore = Reflux.createStore(
{
listenables: VideoActions
@@ -13,40 +21,70 @@ VideoActions = @VideoActions
videoShared: false
videoOpen : false
state : null
+ everDisabled : false
init: ->
- this.listenTo(context.SessionStore, this.onSessionChange)
+ this.listenTo(context.AppStore, this.onAppInit)
+
+ onAppInit: (@app) ->
- # someone has requested us to refresh our config
+ # someone has requested us to refresh our config
onRefresh: ->
- currentDevice = context.jamClient.FTUECurrentSelectedVideoDevice()
- deviceNames = context.jamClient.FTUEGetVideoCaptureDeviceNames()
- #deviceCaps = context.jamClient.FTUEGetVideoCaptureDeviceCapabilities()
- currentResolution = context.jamClient.GetCurrentVideoResolution()
- currentFrameRate = context.jamClient.GetCurrentVideoFrameRate()
- encodeResolutions = context.jamClient.FTUEGetAvailableEncodeVideoResolutions()
- frameRates = context.jamClient.FTUEGetSendFrameRates()
+ # don't do any check if this is a client with no video enabled
+ return unless context.jamClient.FTUECurrentSelectedVideoDevice?
- #deviceCaps: deviceCaps,
+ videoEnabled = context.jamClient.FTUEGetVideoShareEnable()
+
+ @videoEnabled = videoEnabled
+
+ if videoEnabled
+ currentDevice = context.jamClient.FTUECurrentSelectedVideoDevice()
+ deviceNames = context.jamClient.FTUEGetVideoCaptureDeviceNames()
+ #deviceCaps = context.jamClient.FTUEGetVideoCaptureDeviceCapabilities()
+ captureResolutions = context.jamClient.FTUEGetCaptureResolution()
+ currentCaptureResolution = context.jamClient.FTUEGetCurrentCaptureResolution()
+
+ logger.debug("captureResolutions, currentCaptureResolution", captureResolutions, currentCaptureResolution)
+ else
+ @everDisabled = true
+ # don't talk to the backend when video is disabled; avoiding crashes
+ currentDevice = null
+ deviceNames = {}
+ captureResolutions: {}
+ currentCaptureResolution: null
+ frameRates: {}
+
+
+ #deviceCaps: deviceCaps,
@state = {
currentDevice: currentDevice,
deviceNames: deviceNames,
- currentResolution: currentResolution,
- currentFrameRate: currentFrameRate,
- encodeResolutions: encodeResolutions,
- frameRates: frameRates,
+ captureResolutions: captureResolutions,
+ currentCaptureResolution: currentCaptureResolution,
videoShared: @videoShared
- videoOpen: @videoOpen
+ videoOpen: @videoOpen,
+ videoEnabled: videoEnabled,
+ everDisabled: @everDisabled
}
this.trigger(@state)
- onSessionChange: (@session) ->
+ onSetVideoEnabled: (enable) ->
+
+ return unless context.jamClient.FTUESetVideoShareEnable?
+
+ context.jamClient.FTUESetVideoShareEnable(enable)
+
+ # keep state in sync
+ @state.videoEnabled = enable
+ @onRefresh()
onStartVideo: ->
+ return unless context.jamClient.SessStartVideoSharing?
+
if @howtoWindow?
@howtoWindow.close()
@howtoWindow = null
@@ -68,29 +106,56 @@ VideoActions = @VideoActions
@state.videoShared = @videoShared
this.trigger(@state)
+ onBringVideoToFront: ->
+ if @videoShared
+ @logger.debug("BringVideoToFront")
+ context.jamClient.BringVideoWindowToFront();
+
+ onTestVideo: () ->
+
+ return unless context.jamClient.testVideoRender?
+ result = context.jamClient.testVideoRender()
+
+ if !result
+ @app.layout.notify({title: 'Unable to initialize video window', text: "Please contact support@jamkazam.com"})
+
onToggleVideo: () ->
if @videoShared
- @onStopVideo()
+ @onBringVideoToFront()
else
@onStartVideo()
- onSetVideoEncodeResolution: (resolution) ->
- context.jamClient.FTUESetVideoEncodeResolution(resolution)
+ onSetCaptureResolution: (resolution) ->
+ @logger.debug("set capture resolution: #{resolution}")
+ context.jamClient.FTUESetCaptureResolution(resolution)
+ @state.currentCaptureResolution = resolution
+ this.trigger(@state)
onSetSendFrameRate: (frameRates) ->
+ @logger.debug("set capture frame rate: #{frameRates}")
context.jamClient.FTUESetSendFrameRates(frameRates)
+ @state.currentFrameRate = frameRates
+ this.trigger(@state)
onSelectDevice: (device, caps) ->
+
+ # don't do anything if no video capabilities
+ return unless context.jamClient.FTUESelectVideoCaptureDevice?
+
result = context.jamClient.FTUESelectVideoCaptureDevice(device, caps)
if(!result)
@logger.error("onSelectDevice failed with device #{device}")
+ @app.layout.notify({title: 'Unable to select webcam', text: "Please try reconnecting webcam."})
+ else
+ @state.currentDevice = context.jamClient.FTUECurrentSelectedVideoDevice();
+ this.trigger(@state)
onVideoWindowOpened: () ->
@onRefresh() unless @state?
- @logger.debug("in session? #{@session.inSession()}, currentDevice? #{@state?.currentDevice?}, videoShared? #{@videoShared}")
+ @logger.debug("in session? #{context.SessionStore.inSession()}, currentDevice? #{@state?.currentDevice?}, videoShared? #{@videoShared}")
- if @session.inSession() && @state.currentDevice? && Object.keys(@state.currentDevice).length > 0 && !@videoShared
+ if context.SessionStore.inSession() && @state.currentDevice? && Object.keys(@state.currentDevice).length > 0 && !@videoShared
context.JK.ModUtils.shouldShow(NAMED_MESSAGES.HOWTO_USE_VIDEO_NOSHOW).done((shouldShow) =>
@logger.debug("checking if user has 'should show' on video howto: #{shouldShow}")
if shouldShow
@@ -135,17 +200,25 @@ VideoActions = @VideoActions
# if the user passes all the safeguards, let's see if we should get them to configure video
onCheckPromptConfigureVideo: () ->
+ # don't do any check if this is a client with no video enabled
+ return unless context.jamClient.FTUECurrentSelectedVideoDevice?
+
@onRefresh() unless @state?
@logger.debug("checkPromptConfigureVideo", @state.currentDevice, @state.deviceNames)
# if no device configured and this is the native client and if you have at least 1 video
- if (!@state.currentDevice? || Object.keys(@state.currentDevice).length == 0) && gon?.isNativeClient && Object.keys(@state.deviceNames).length > 0
+ # currentDevice, from the backend, is '{'':''}' in the case of no device configured. But we also check for an empty object, or null object.
+ if (!@state.currentDevice? || Object.keys(@state.currentDevice).length == 0 || (Object.keys(@state.currentDevice).length == 1 && @state.currentDevice[''] == '')) && gon?.isNativeClient && Object.keys(@state.deviceNames).length > 0
# and if they haven't said stop bothering me about this
context.JK.ModUtils.shouldShow(NAMED_MESSAGES.CONFIGURE_VIDEO_NOSHOW).done((shouldShow) =>
@logger.debug("checking if user has 'should show' on video config: #{shouldShow}")
if shouldShow
@configureWindow = window.open("/popups/configure-video", 'Configure Video', 'scrollbars=yes,toolbar=no,status=no,height=395,width=444')
)
+
+ isVideoEnabled:() ->
+ return @videoEnabled
+
}
)
diff --git a/web/app/assets/javascripts/react-components/stores/VideoUploaderStore.js.coffee b/web/app/assets/javascripts/react-components/stores/VideoUploaderStore.js.coffee
new file mode 100644
index 000000000..10c726e1a
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/VideoUploaderStore.js.coffee
@@ -0,0 +1,219 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+rest = context.JK.Rest();
+
+VideoUploaderActions = @VideoUploaderActions
+
+
+@VideoUploaderStore = Reflux.createStore(
+ {
+ listenables: VideoUploaderActions
+ logger: context.JK.logger
+ preparingUpload: false
+ uploading: false
+ paused: false
+ recordingId: null
+ videoUrl: null
+ bytesSent: 0
+ bytesTotal: 1
+ errorReason: null
+ errorDetail: null
+ state: null
+ done: false
+
+ init: ->
+ # Register with the app store to get @app
+ this.listenTo(context.AppStore, this.onAppInit)
+ @state = {uploading: @uploading, recordingId: @recordingId, videoUrl: @videoUrl, paused: @paused, preparingUpload: @preparingUpload, bytesSent: @bytesSent, bytesTotal: @bytesTotal, errorReason: @errorReason, errorDetail: @errorDetail, done: @done }
+
+ onAppInit: (app) ->
+ @app = app
+
+ triggerState: () ->
+ @state = {uploading: @uploading, recordingId: @recordingId, videoUrl: @videoUrl, paused: @paused, preparingUpload: @preparingUpload, bytesSent: @bytesSent, bytesTotal: @bytesTotal, errorReason: @errorReason, errorDetail: @errorDetail, done: @done}
+ @trigger(@state)
+
+ getState: () ->
+ @state
+
+ onPause: () ->
+ if @uploading
+ @uploading = false
+ @paused = true
+ context.jamClient.pauseVideoUpload()
+ @triggerState()
+
+ onResume: () ->
+ if @paused && @recordingId?
+ @uploading = true
+ @paused = false
+ context.jamClient.resumeVideoUpload()
+ @triggerState()
+ else
+ if @uploading
+ @app.layout.notify({title: 'Already uploading', text: "A video is already being uploaded."})
+ else
+ @app.layout.notify({title: 'Nothing to resume', text: "No upload to resume."})
+
+ onCancel: () ->
+ if @uploading
+ @uploading = false
+ context.jamClient.cancelVideoUpload()
+ @triggerState()
+
+ onDelete: (recordingId) ->
+ context.jamClient.deleteVideo(recordingId);
+
+ onNewVideo: (recordingId) ->
+
+ @onCancel(recordingId)
+
+ @done = false
+ @paused = false
+ @bytesSent = 0
+ @bytesTotal = 1
+ @errorReason = null
+ @errorDetail = null
+ @videoUrl = null
+ @triggerState()
+
+ onShowUploader: (recordingId) ->
+
+ if @childWindow?
+ logger.debug("showUploader popup being closed automatically")
+ @childWindow.close()
+ @childWindow = null
+
+ @childWindow = window.open("/popups/video/upload/" + recordingId, 'Video Uploader', 'scrollbars=yes,toolbar=no,status=no,height=155,width=350')
+
+ onUploaderClosed: () ->
+
+ if @childWindow?
+ @childWindow = null
+
+ onUploadVideo: (recordingId) ->
+
+ if @uploading || @preparingUpload
+ logger.debug("ignoring upload request")
+ return
+
+ @preparingUpload = true
+ @onNewVideo()
+
+ rest.getRecording({id:recordingId})
+ .done((response) =>
+ claim = response.my
+
+ privateStatus = 'private'
+ privateStatus = 'public' if claim.is_public
+
+ if claim?
+ videoInfo = {
+ "snippet": {
+ "title": claim.name,
+ "description": claim.description,
+ "tags": ["JamKazam"],
+ "categoryId": 10 # music
+ },
+ "status": {
+ "privacyStatus": privateStatus,
+ "embeddable": true,
+ "license": "youtube"
+ }
+ }
+
+ rest.getUserAuthorizations()
+ .done((response) =>
+
+ # http://localhost:3000/popups/video/upload/d25dbe8e-a066-4ea0-841d-16872c713fc9
+ youtube_auth = null
+ for authorization in response.authorizations
+ if authorization.provider == 'google_login'
+ youtube_auth = authorization.token
+ break
+
+ if youtube_auth?
+ logger.debug("calling uploadVideo(#{recordingId}, #{youtube_auth}, #{videoInfo})")
+ result = context.jamClient.uploadVideo(recordingId, youtube_auth, JSON.stringify(videoInfo),
+ "VideoUploaderStore.clientUploadCallback",
+ "VideoUploaderStore.clientDoneCallback",
+ "VideoUploaderStore.clientFailCallback")
+
+ if result.error
+ @preparingUpload = false
+ @triggerState()
+ @app.layout.notify({title: 'Unable to upload video', text: 'Application error: ' + result.error})
+ else
+ @preparingUpload = false
+ @videoUrl = null
+ @uploading = true
+ @recordingId = recordingId
+ @triggerState()
+
+
+ else
+ @preparingUpload = false
+ @triggerState()
+ @app.layout.notify({title: 'No Authorization Yet for YouTube', text: 'Youtube authorization still needed'})
+
+ )
+ .fail((jqXHR) =>
+ @preparingUpload = false
+ @triggerState()
+ @app.layout.notifyServerError(jqXHR, 'Unable to fetch user authorizations')
+ )
+
+ else
+ @preparingUpload = false
+ @triggerState()
+ @app.layout.notify({title: "You do not have a claim to this recording", text: "If this is in error, contact support@jamkazam.com."})
+
+ )
+ .fail((jqXHR) =>
+ @preparingUpload = false
+ @triggerState()
+ @app.layout.notifyServerError(jqXHR, 'Unable to fetch recording information')
+ )
+
+ clientUploadCallback: (bytesSent, bytesTotal) ->
+ logger.debug("bytesSent: #{bytesSent} bytesTotal: #{bytesTotal}")
+
+ # backend will report 0 bytes total sometimes as the upload is failing. just ignore it; we'll get an error message soon
+ return if bytesTotal == 0
+
+ VideoUploaderStore.bytesSent = Number(bytesSent)
+ VideoUploaderStore.bytesTotal = Number(bytesTotal)
+ VideoUploaderStore.triggerState()
+
+ clientDoneCallback: (video_id) ->
+ console.log
+ logger.debug("client uploaded video successfully to #{video_id}")
+ VideoUploaderStore.uploading = false
+
+ VideoUploaderStore.videoUrl = "https://www.youtube.com/watch?v=#{video_id}"
+
+ rest.addRecordingVideoData(VideoUploaderStore.recordingId, {video_id: video_id})
+ .fail(() =>
+ VideoUploaderStore.app.layout.notify({title: 'Sync Error', text:'Unable to notify server about uploaded video'})
+ )
+
+ VideoUploaderStore.recordingId = null
+ VideoUploaderStore.done = true
+ VideoUploaderStore.triggerState()
+
+ clientFailCallback: (reason, detail) =>
+ logger.warn("client failed to video upload #{reason}, #{detail}")
+ VideoUploaderStore.uploading = false
+ VideoUploaderStore.errorReason = reason
+ VideoUploaderStore.errorDetail = detail
+
+ # if reason == "create_video_failed" && errorDetail = "401"
+ # then don't trigger state, instead ask server for a fresh token
+ #
+
+ VideoUploaderStore.triggerState()
+ }
+)
+
+@VideoUploaderStore
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/stores/WebcamViewer.js.jsx.coffee b/web/app/assets/javascripts/react-components/stores/WebcamViewer.js.jsx.coffee
index d9cb7210d..9c87f9e9f 100644
--- a/web/app/assets/javascripts/react-components/stores/WebcamViewer.js.jsx.coffee
+++ b/web/app/assets/javascripts/react-components/stores/WebcamViewer.js.jsx.coffee
@@ -9,20 +9,18 @@ if window.opener?
catch e
reactContext = window
+userAgent = window.navigator.userAgent;
+if /iPhone|iPad|iPod|android/i.test(navigator.userAgent)
+ # iPad or iPhone
+ reactContext = window
+
+
VideoStore = reactContext.VideoStore
VideoActions = reactContext.VideoActions
+PlatformStore = reactContext.PlatformStore
ALERT_NAMES = context.JK.ALERT_NAMES;
-BackendToFrontend = {
- 1 : "CIF (352x288)",
- 2 : "VGA (640x480)",
- 3 : "4CIF (704x576)",
- 4 : "1/2 720p HD (640x360)",
- 5 : "720p HD (1280x720)",
- 6 : "1080p HD (1920x1080)"
-}
-
BackendNumericToBackendString = {
1 : "CIF (352X288)",
2 : "VGA (640X480)",
@@ -34,34 +32,29 @@ BackendNumericToBackendString = {
BackendToFrontendFPS = {
-
1: 30,
2: 24,
3: 20,
4: 15,
5: 10
}
-FrontendToBackend = {}
-for key, value of BackendToFrontend
- FrontendToBackend[value] = key
mixins = []
mixins.push(Reflux.listenTo(VideoStore, 'onVideoStateChanged'))
-
@WebcamViewer = React.createClass({
mixins: mixins
logger: context.JK.logger
+ visible: false
getInitialState: () ->
{
currentDevice: null
deviceNames: {}
deviceCaps: null
- currentResolution: 0
- currentFrameRate: 0
- encodeResolutions: {}
+ currentCaptureResolution: 0
+ captureResolutions: {}
frameRates: {}
rescanning: false
}
@@ -79,60 +72,51 @@ mixins.push(Reflux.listenTo(VideoStore, 'onVideoStateChanged'))
# build list of webcams
webcams = []
+ noneSelected = selectedDevice == null || selectedDevice.length == 0
+
+ # the backend does not allow setting no video camera. So if a webcam is selected, prevent un-selecting
+ if noneSelected
+ webcams.push ``
+
context._.each @state.deviceNames, (deviceName, deviceGuid) ->
- selected = deviceName == selectedDevice
+ selected = deviceGuid == selectedDevice
webcams.push ``
+ noWebcams = Object.keys(@state.deviceNames).length == 0
+
# build list of capture resolutions
captureResolutions = []
# load current settings from backend
- currentResolution = @state.currentResolution
- currentFrameRate = @state.currentFrameRate
+ captureResolution = @state.currentCaptureResolution
- # protect against non-video clients pointed at video-enabled server from getting into a session
- resolutions = @state.encodeResolutions
- frames = @state.frameRates
- @logger.debug 'FOUND THESE RESOLUTIONS', resolutions
- @logger.debug 'FOUND THESE FPS', frames
+ resolutions = @state.captureResolutions
context._.each resolutions, (resolution, resolutionKey, obj) =>
- #{1: "CIF (352X288)", 2: "VGA (640X480)", 3: "4CIF (704X576)", 4: "1/2WHD (640X360)", 5: "WHD (1280X720)", 6: "FHD (1920x1080)"}
- context._.each frames, (frame, key, obj) =>
+ value = resolutionKey
+ text = resolution
- frontendResolution = BackendToFrontend[resolutionKey]
+ selected = captureResolution.toString() == value.toString()
- @logger.error("unknown resolution! #{resolution}", BackendToFrontend) unless frontendResolution
+ captureResolutions.push ``
- value = "#{resolutionKey}|#{frame}"
- text = "#{frontendResolution} at #{frame} fps"
- selected = currentResolution + '|' + currentFrameRate == value
-
- captureResolutions.push ``
-
- autoSelect = false
- if currentResolution == 0
- @logger.warn("current resolution not specified; defaulting to VGA")
- autoSelect = true
- currentResolution = 2
- if currentFrameRate == 0
- autoSelect = true
- @logger.warn("current frame rate not specified; defaulting to 30")
- currentFrameRate = 30
+ testBtnClassNames = {'button-orange' : true, 'webcam-test-btn' : true}
+ if noWebcams
+ if PlatformStore.isWindows()
+ testBtnClassNames.disabled = !@state.videoEnabled
+ testBtnClasses = classNames(testBtnClassNames)
+ testBtn = `TEST VIDEO`
+ else
+ testBtn = null
+ else if @state.videoShared
+ testBtnClassNames.disabled = !@state.videoEnabled
+ testBtnClasses = classNames(testBtnClassNames)
+ testBtn = `STOP WEBCAM`
else
- convertedFrameRate = BackendToFrontendFPS[currentFrameRate]
- @logger.debug("translating FPS: backend numeric #{currentFrameRate} to #{convertedFrameRate}")
- currentFrameRate = convertedFrameRate
-
- # backend needs to be same as frontend
- if autoSelect
- @updateBackend(currentResolution, currentFrameRate)
-
- if @state.videoShared
- toggleText = 'STOP WEBCAM'
- else
- toggleText = 'TEST WEBCAM'
+ testBtnClassNames.disabled = !@state.videoEnabled || noneSelected
+ testBtnClasses = classNames(testBtnClassNames)
+ testBtn = `TEST WEBCAM`
if @state.rescanning
rescanning =
@@ -141,26 +125,66 @@ mixins.push(Reflux.listenTo(VideoStore, 'onVideoStateChanged'))
CHECKING GEAR
`
- ``
+ if @props.show_header
+ if noWebcams
+ if PlatformStore.isWindows()
+ testVideoHelpText = `The TEST VIDEO button will open the JamKazam video window to verify that receiving video works on your system.`
+ header = `
+
video gear:
+
+ JamKazam does not detect any webcams. You will not be able to send video, but you can still receive it from others. {testVideoHelpText}
+
+
`
+ else
+ header =
+ `
+
video gear:
+
+ Select webcam to use for video in sessions. Verify that you see video from webcam in the external application window (it may be behind this window).
+