diff --git a/admin/Gemfile b/admin/Gemfile
index 5ec8a3411..5fea93d71 100644
--- a/admin/Gemfile
+++ b/admin/Gemfile
@@ -65,10 +65,11 @@ gem 'logging-rails', :require => 'logging/rails'
gem 'pg_migrate'
gem 'ruby-protocol-buffers', '1.2.2'
-gem 'sendgrid', '1.1.0'
+gem 'sendgrid', '1.2.0'
gem 'geokit-rails'
gem 'postgres_ext', '1.0.0'
+gem 'resque_mailer'
group :libv8 do
gem 'libv8', "~> 3.11.8"
@@ -106,6 +107,7 @@ group :development, :test do
gem 'factory_girl_rails', '4.1.0'
gem 'database_cleaner', '0.7.0'
gem 'launchy'
+ gem 'faker'
end
group :test do
diff --git a/admin/app/admin/email_batch.rb b/admin/app/admin/email_batch.rb
new file mode 100644
index 000000000..3f15aadd3
--- /dev/null
+++ b/admin/app/admin/email_batch.rb
@@ -0,0 +1,106 @@
+ActiveAdmin.register JamRuby::EmailBatch, :as => 'Batch Emails' do
+
+ menu :label => 'Emails'
+
+ config.sort_order = 'updated_at DESC'
+ config.batch_actions = false
+ # config.clear_action_items!
+ config.filters = false
+
+ form :partial => 'form'
+
+ index do
+ column 'Subject' do |pp| pp.subject end
+ column 'Updated' do |pp| pp.updated_at end
+ column 'From' do |pp| pp.from_email end
+ column 'Status' do |pp| pp.aasm_state end
+ column 'Test Emails' do |pp| pp.test_emails end
+ column 'Email Count' do |pp| pp.qualified_count end
+ column 'Send Count' do |pp| pp.sent_count end
+ column 'Started' do |pp| pp.started_at end
+ column 'Completed' do |pp| pp.completed_at end
+ column 'Send Test' do |pp|
+ link_to("Test Batch (#{pp.test_count})",
+ batch_test_admin_batch_email_path(pp.id),
+ :confirm => "Run test batch with #{pp.test_count} emails?")
+ end
+ column 'Send Live' do |pp|
+ link_to("Live Batch (#{User.email_opt_in.count})",
+ batch_send_admin_batch_email_path(pp.id),
+ :confirm => "Run LIVE batch with #{User.email_opt_in.count} emails?")
+ end
+
+ default_actions
+ end
+
+ action_item :only => :show do
+ link_to("Send Test Batch (#{resource.test_count})",
+ batch_test_admin_batch_email_path(resource.id),
+ :confirm => "Run test batch with #{resource.test_count} emails?")
+ end
+
+ action_item :only => :show do
+ link_to("Send Live Batch (#{User.email_opt_in.count})",
+ batch_send_admin_batch_email_path(resource.id),
+ :confirm => "Run LIVE batch with #{User.email_opt_in.count} emails?")
+ end
+
+ show :title => 'Batch Email' do |obj|
+ panel 'Email Contents' do
+ attributes_table_for obj do
+ row 'From' do |obj| obj.from_email end
+ row 'Test Emails' do |obj| obj.test_emails end
+ row 'Subject' do |obj| obj.subject end
+ row 'Body' do |obj| obj.body end
+ end
+ end
+ columns do
+ column do
+ panel 'Sending Parameters' do
+ attributes_table_for obj do
+ row 'State' do |obj| obj.aasm_state end
+ row 'User Count' do |obj|
+ obj.qualified_count ? obj.qualified_count : User.email_opt_in.count
+ end
+ row 'Sent Count' do |obj| obj.sent_count end
+ row 'Started' do |obj| obj.started_at end
+ row 'Completed' do |obj| obj.completed_at end
+ row 'Updated' do |obj| obj.updated_at end
+ end
+ end
+ end
+ column do
+ panel 'Send Results' do
+ end
+ end
+ end
+ end
+
+ controller do
+
+ def create
+ batch = EmailBatch.create_with_params(params[:jam_ruby_email_batch])
+ redirect_to admin_batch_email_path(batch.id)
+ # redirect_to admin_batch_emails_path
+ end
+
+ def update
+ resource.update_with_conflict_validation(params[:jam_ruby_email_batch])
+ redirect_to admin_batch_email_path(resource.id)
+ end
+
+ end
+
+ member_action :batch_test, :method => :get do
+ batch = EmailBatch.find(params[:id])
+ batch.send_test_batch
+ redirect_to admin_batch_email_path(batch.id)
+ end
+
+ member_action :batch_send, :method => :get do
+ batch = EmailBatch.find(params[:id])
+ batch.deliver_batch
+ redirect_to admin_batch_email_path(batch.id)
+ end
+
+end
diff --git a/admin/app/views/admin/batch_emails/_form.html.erb b/admin/app/views/admin/batch_emails/_form.html.erb
new file mode 100644
index 000000000..a9c83f836
--- /dev/null
+++ b/admin/app/views/admin/batch_emails/_form.html.erb
@@ -0,0 +1,9 @@
+<%= semantic_form_for([:admin, resource], :url => resource.new_record? ? admin_batch_emails_path : "/admin/batch_emails/#{resource.id}") do |f| %>
+ <%= f.inputs do %>
+ <%= f.input(:from_email, :label => "From Email", :input_html => {:maxlength => 64}) %>
+ <%= f.input(:subject, :label => "Subject", :input_html => {:maxlength => 128}) %>
+ <%= f.input(:test_emails, :label => "Test Emails", :input_html => {:maxlength => 1024, :size => '3x3'}) %>
+ <%= f.input(:body, :label => "Body", :input_html => {:maxlength => 3096, :size => '10x20'}) %>
+ <% end %>
+ <%= f.actions %>
+<% end %>
diff --git a/admin/config/application.rb b/admin/config/application.rb
index 0ae29cd8e..36a91e1a5 100644
--- a/admin/config/application.rb
+++ b/admin/config/application.rb
@@ -119,5 +119,7 @@ module JamAdmin
config.twitter_app_secret = ENV['TWITTER_APP_SECRET'] || 'Azcy3QqfzYzn2fsojFPYXcn72yfwa0vG6wWDrZ3KT8'
config.ffmpeg_path = ENV['FFMPEG_PATH'] || (File.exist?('/usr/local/bin/ffmpeg') ? '/usr/local/bin/ffmpeg' : '/usr/bin/ffmpeg')
+
+ config.max_audio_downloads = 100
end
end
diff --git a/db/manifest b/db/manifest
index 9b732d4fe..aba246d9b 100755
--- a/db/manifest
+++ b/db/manifest
@@ -137,4 +137,6 @@ cascading_delete_constraints_for_release.sql
events_social_description.sql
fix_broken_cities.sql
notifications_with_text.sql
-
+notification_seen_at.sql
+order_event_session.sql
+emails.sql
diff --git a/db/up/emails.sql b/db/up/emails.sql
new file mode 100644
index 000000000..86f8b4f6f
--- /dev/null
+++ b/db/up/emails.sql
@@ -0,0 +1,38 @@
+CREATE TABLE email_batches (
+ id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+ subject VARCHAR(256) NOT NULL,
+ body TEXT NOT NULL,
+ from_email VARCHAR(64) NOT NULL default 'support@jamkazam.com',
+
+ aasm_state VARCHAR(32) NOT NULL default 'pending',
+
+ test_emails TEXT NOT NULL default '',
+
+ qualified_count INTEGER NOT NULL default 0,
+ sent_count INTEGER NOT NULL default 0,
+
+ lock_version INTEGER,
+
+ started_at TIMESTAMP,
+ completed_at TIMESTAMP,
+
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+-- CREATE TABLE email_batch_results (
+-- id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
+-- email_batch_id VARCHAR(64) REFERENCES email_batches(id) ON DELETE CASCADE,
+-- user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE,
+
+-- error_type VARCHAR(32),
+-- email_address VARCHAR(256),
+
+-- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+-- updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+-- );
+
+-- ALTER TABLE email_batch_results ADD CONSTRAINT email_batch_uniqkey UNIQUE (email_batch_id);
+-- ALTER TABLE email_batch_results ADD CONSTRAINT email_user_uniqkey UNIQUE (user_id);
+
+ALTER TABLE users ALTER COLUMN subscribe_email SET DEFAULT true;
diff --git a/db/up/notification_seen_at.sql b/db/up/notification_seen_at.sql
new file mode 100644
index 000000000..67b29ddd8
--- /dev/null
+++ b/db/up/notification_seen_at.sql
@@ -0,0 +1 @@
+ALTER TABLE users ADD COLUMN notification_seen_at TIMESTAMP;
\ No newline at end of file
diff --git a/db/up/order_event_session.sql b/db/up/order_event_session.sql
new file mode 100644
index 000000000..ba36f8f94
--- /dev/null
+++ b/db/up/order_event_session.sql
@@ -0,0 +1 @@
+ALTER TABLE event_sessions ADD COLUMN ordinal INTEGER;
\ No newline at end of file
diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto
index 4ae83c193..1df3bebf8 100644
--- a/pb/src/client_container.proto
+++ b/pb/src/client_container.proto
@@ -472,7 +472,8 @@ message TestClientMessage {
// sent from client to server periodically to let server track if the client is truly alive and avoid TCP timeout scenarios
// the server will send a HeartbeatAck in response to this
message Heartbeat {
-
+ optional string notification_seen = 1;
+ optional string notification_seen_at = 2;
}
// target: client
diff --git a/ruby/Gemfile b/ruby/Gemfile
index 7ba18443c..cd4bca5ec 100644
--- a/ruby/Gemfile
+++ b/ruby/Gemfile
@@ -27,7 +27,7 @@ gem 'eventmachine', '1.0.3'
gem 'amqp', '1.0.2'
gem 'will_paginate'
gem 'actionmailer', '3.2.13'
-gem 'sendgrid'
+gem 'sendgrid', '1.2.0'
gem 'aws-sdk', '1.29.1'
gem 'carrierwave'
gem 'aasm', '3.0.16'
@@ -40,6 +40,7 @@ gem 'resque'
gem 'resque-retry'
gem 'resque-failed-job-mailer' #, :path => "/Users/seth/workspace/resque_failed_job_mailer"
gem 'resque-lonely_job', '~> 1.0.0'
+gem 'resque_mailer'
gem 'oj'
gem 'builder'
gem 'fog'
diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb
index da72d911b..b6f17a3a4 100755
--- a/ruby/lib/jam_ruby.rb
+++ b/ruby/lib/jam_ruby.rb
@@ -16,6 +16,7 @@ require "geokit-rails"
require "postgres_ext"
require 'builder'
require 'cgi'
+require 'resque_mailer'
require "jam_ruby/constants/limits"
require "jam_ruby/constants/notification_types"
@@ -132,6 +133,9 @@ require "jam_ruby/models/playable_play"
require "jam_ruby/models/country"
require "jam_ruby/models/region"
require "jam_ruby/models/city"
+require "jam_ruby/models/email_batch"
+require "jam_ruby/app/mailers/async_mailer"
+require "jam_ruby/app/mailers/batch_mailer"
include Jampb
diff --git a/ruby/lib/jam_ruby/app/mailers/async_mailer.rb b/ruby/lib/jam_ruby/app/mailers/async_mailer.rb
new file mode 100644
index 000000000..4cbad60e4
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/mailers/async_mailer.rb
@@ -0,0 +1,8 @@
+require 'resque_mailer'
+
+module JamRuby
+ class AsyncMailer < ActionMailer::Base
+ include SendGrid
+ include Resque::Mailer
+ end
+end
diff --git a/ruby/lib/jam_ruby/app/mailers/batch_mailer.rb b/ruby/lib/jam_ruby/app/mailers/batch_mailer.rb
new file mode 100644
index 000000000..11e21efd2
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/mailers/batch_mailer.rb
@@ -0,0 +1,38 @@
+module JamRuby
+ class BatchMailer < JamRuby::AsyncMailer
+ layout "user_mailer"
+
+ sendgrid_category :use_subject_lines
+ sendgrid_unique_args :env => Environment.mode
+
+ def _send_batch(batch, users)
+ @batch_body = batch.body
+ emails = users.map(&:email)
+
+ sendgrid_recipients(emails)
+ sendgrid_substitute(EmailBatch::VAR_FIRST_NAME, users.map(&:first_name))
+
+ batch.did_send(emails)
+
+ mail(:to => emails,
+ :from => batch.from_email,
+ :subject => batch.subject) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ def send_batch_email(batch_id, user_ids)
+ users = User.find_all_by_id(user_ids)
+ batch = EmailBatch.where(:id => batch_id).limit(1).first
+ self._send_batch(batch, users)
+ end
+
+ def send_batch_email_test(batch_id)
+ batch = EmailBatch.where(:id => batch_id).limit(1).first
+ users = batch.test_users
+ self._send_batch(batch, users)
+ end
+
+ end
+end
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/batch_mailer/send_batch_email.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/batch_mailer/send_batch_email.html.erb
new file mode 100644
index 000000000..31bd20e21
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/batch_mailer/send_batch_email.html.erb
@@ -0,0 +1 @@
+<%= @body %>
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/batch_mailer/send_batch_email.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/batch_mailer/send_batch_email.text.erb
new file mode 100644
index 000000000..31bd20e21
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/batch_mailer/send_batch_email.text.erb
@@ -0,0 +1 @@
+<%= @body %>
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/batch_mailer/send_batch_email_test.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/batch_mailer/send_batch_email_test.html.erb
new file mode 120000
index 000000000..f14e9223c
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/batch_mailer/send_batch_email_test.html.erb
@@ -0,0 +1 @@
+send_batch_email.html.erb
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/batch_mailer/send_batch_email_test.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/batch_mailer/send_batch_email_test.text.erb
new file mode 120000
index 000000000..2a1e564e8
--- /dev/null
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/batch_mailer/send_batch_email_test.text.erb
@@ -0,0 +1 @@
+send_batch_email.text.erb
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/password_changed.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/password_changed.html.erb
index a140d13fe..534066c5a 100644
--- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/password_changed.html.erb
+++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/password_changed.html.erb
@@ -1,3 +1,3 @@
<% provide(:title, 'Jamkazam Password Changed') %>
-You just changed your password at Jamkazam.
+You just changed your password at JamKazam.
diff --git a/ruby/lib/jam_ruby/app/views/layouts/user_mailer.html.erb b/ruby/lib/jam_ruby/app/views/layouts/user_mailer.html.erb
index 6fcd69d53..1198a26f4 100644
--- a/ruby/lib/jam_ruby/app/views/layouts/user_mailer.html.erb
+++ b/ruby/lib/jam_ruby/app/views/layouts/user_mailer.html.erb
@@ -24,13 +24,11 @@
<%= yield(:title) %>
- <%= yield %>
+ <%= @batch_body ? @batch_body.html_safe : yield %>
|
-
-
<% unless @suppress_user_has_account_footer == true %>
|
@@ -39,8 +37,8 @@
|
+ This email was sent to you because you have an account at JamKazam. Click here to unsubscribe and update your profile settings.
- This email was sent to you because you have an account at JamKazam.
|
diff --git a/ruby/lib/jam_ruby/app/views/layouts/user_mailer.text.erb b/ruby/lib/jam_ruby/app/views/layouts/user_mailer.text.erb
index 8bd3c7483..5c8262f63 100644
--- a/ruby/lib/jam_ruby/app/views/layouts/user_mailer.text.erb
+++ b/ruby/lib/jam_ruby/app/views/layouts/user_mailer.text.erb
@@ -1,8 +1,11 @@
-<%= yield %>
-
+<% if @batch_body %>
+ <%= Nokogiri::HTML(@batch_body).text %>
+<% else %>
+ <%= yield %>
+<% end %>
<% unless @suppress_user_has_account_footer == true %>
-This email was sent to you because you have an account at JamKazam / http://www.jamkazam.com.
+This email was sent to you because you have an account at JamKazam / http://www.jamkazam.com. Visit your profile page to unsubscribe: http://www.jamkazam.com/client#/account/profile.
<% end %>
Copyright <%= Time.now.year %> JamKazam, Inc. All rights reserved.
diff --git a/ruby/lib/jam_ruby/models/email_batch.rb b/ruby/lib/jam_ruby/models/email_batch.rb
new file mode 100644
index 000000000..a2fd36387
--- /dev/null
+++ b/ruby/lib/jam_ruby/models/email_batch.rb
@@ -0,0 +1,142 @@
+module JamRuby
+ class EmailBatch < ActiveRecord::Base
+ self.table_name = "email_batches"
+
+ attr_accessible :from_email, :subject, :test_emails, :body
+ attr_accessible :lock_version, :qualified_count, :sent_count, :started_at, :completed_at
+
+ VAR_FIRST_NAME = '@FIRSTNAME'
+ VAR_LAST_NAME = '@LASTNAME'
+
+ DEFAULT_SENDER = "support@jamkazam.com"
+
+ include AASM
+ aasm do
+ state :pending, :initial => true
+ state :testing
+ state :tested
+ state :batching
+ state :batched
+ state :disabled
+
+ event :enable do
+ transitions :from => :disabled, :to => :pending
+ end
+ event :reset do
+ transitions :from => [:disabled, :testing, :tested, :batching, :batched, :pending], :to => :pending
+ end
+ event :do_test_run, :before => :running_tests do
+ transitions :from => [:pending, :tested, :batched], :to => :testing
+ end
+ event :did_test_run, :after => :ran_tests do
+ transitions :from => :testing, :to => :tested
+ end
+ event :do_batch_run, :before => :running_batch do
+ transitions :from => [:tested, :pending, :batched], :to => :batching
+ end
+ event :did_batch_run, :after => :ran_batch do
+ transitions :from => :batching, :to => :batched
+ end
+ event :disable do
+ transitions :from => [:pending, :tested, :batched], :to => :disabled
+ end
+ end
+
+ # has_many :email_batch_results, :class_name => 'JamRuby::EmailBatchResult'
+
+ def self.create_with_params(params)
+ obj = self.new
+ params.each { |kk,vv| vv.strip! }
+ obj.update_with_conflict_validation(params)
+ obj
+ end
+
+ def deliver_batch
+ self.perform_event('do_batch_run!')
+ User.email_opt_in.find_in_batches(batch_size: 1000) do |users|
+ BatchMailer.send_batch_email(self.id, users.map(&:id)).deliver
+ end
+ end
+
+ def test_count
+ self.test_emails.split(',').count
+ end
+
+ def test_users
+ self.test_emails.split(',').collect do |ee|
+ ee.strip!
+ uu = User.new
+ uu.email = ee
+ uu.first_name = ee.match(/^(.*)@/)[1].to_s
+ uu.last_name = 'Test'
+ uu
+ end
+ end
+
+ def send_test_batch
+ self.perform_event('do_test_run!')
+ BatchMailer.send_batch_email_test(self.id).deliver
+ end
+
+ def merged_body(user)
+ body.gsub(VAR_FIRST_NAME, user.first_name).gsub(VAR_LAST_NAME, user.last_name)
+ end
+
+ def did_send(emails)
+ self.update_with_conflict_validation({ :sent_count => self.sent_count + emails.size })
+
+ if self.sent_count >= self.qualified_count
+ if batching?
+ self.perform_event('did_batch_run!')
+ elsif testing?
+ self.perform_event('did_test_run!')
+ end
+ end
+ end
+
+ def perform_event(event_name)
+ num_try = 0
+ self.send(event_name)
+ rescue ActiveRecord::StaleObjectError
+ num_try += 1
+ if 5 > num_try
+ self.reload
+ retry
+ end
+ end
+
+ def update_with_conflict_validation(*args)
+ num_try = 0
+ update_attributes(*args)
+ rescue ActiveRecord::StaleObjectError
+ num_try += 1
+ if 5 > num_try
+ self.reload
+ retry
+ end
+ end
+
+ def running_batch
+ self.update_with_conflict_validation({:qualified_count => User.email_opt_in.count,
+ :sent_count => 0,
+ :started_at => Time.now
+ })
+ end
+
+ def running_tests
+ self.update_with_conflict_validation({:qualified_count => self.test_count,
+ :sent_count => 0,
+ :started_at => Time.now
+ })
+ end
+
+ def ran_tests
+ self.update_with_conflict_validation({ :completed_at => Time.now })
+ end
+
+ def ran_batch
+ self.update_with_conflict_validation({ :completed_at => Time.now })
+ end
+
+ end
+end
diff --git a/ruby/lib/jam_ruby/models/event_session.rb b/ruby/lib/jam_ruby/models/event_session.rb
index 7385de68d..49329712c 100644
--- a/ruby/lib/jam_ruby/models/event_session.rb
+++ b/ruby/lib/jam_ruby/models/event_session.rb
@@ -1,6 +1,6 @@
class JamRuby::EventSession < ActiveRecord::Base
- attr_accessible :event_id, :user_id, :band_id, :starts_at, :ends_at, :pinned_state, :position, :img_url, :img_width, :img_height, as: :admin
+ attr_accessible :event_id, :user_id, :band_id, :starts_at, :ends_at, :pinned_state, :position, :img_url, :img_width, :img_height, :ordinal, as: :admin
belongs_to :user, class_name: 'JamRuby::User'
belongs_to :band, class_name: 'JamRuby::Band'
@@ -13,6 +13,48 @@ class JamRuby::EventSession < ActiveRecord::Base
before_validation :sanitize_active_admin
+ def has_public_mixed_recordings?
+ public_mixed_recordings.length > 0
+ end
+
+ def public_mixed_recordings
+ recordings.select { |recording| recording if recording.has_mix? && recording.is_public? }
+ end
+
+ def recordings
+ recordings=[]
+
+ sessions.each do |session_history|
+ recordings = recordings + session_history.recordings
+ end
+
+ recordings.sort! do |x, y|
+ x.candidate_claimed_recording.name <=> y.candidate_claimed_recording.name
+ end
+
+ recordings
+ end
+
+ # ideally this is based on some proper association with the event, not such a slushy time grab
+ def sessions
+ if ready_display
+ query = MusicSessionHistory.where(fan_access: true).where(created_at: (self.starts_at - 12.hours)..(self.ends_at + 12.hours))
+ if self.user_id
+ query = query.where(user_id: self.user_id)
+ elsif self.band_id
+ query = query.where(band_id: self.band_id)
+ else
+ raise 'invalid state in event_session_button'
+ end
+ query
+ else
+ []
+ end
+ end
+
+ def ready_display
+ self.starts_at && self.ends_at && (self.user_id || self.band_id)
+ end
def sanitize_active_admin
self.img_url = nil if self.img_url == ''
self.user_id = nil if self.user_id == ''
diff --git a/ruby/lib/jam_ruby/models/music_session_history.rb b/ruby/lib/jam_ruby/models/music_session_history.rb
index 089969434..5e3637284 100644
--- a/ruby/lib/jam_ruby/models/music_session_history.rb
+++ b/ruby/lib/jam_ruby/models/music_session_history.rb
@@ -157,6 +157,10 @@ module JamRuby
music_session && music_session.mount
end
+ def recordings
+ Recording.where(music_session_id: self.id)
+ end
+
def end_history
self.update_attribute(:session_removed_at, Time.now)
diff --git a/ruby/lib/jam_ruby/models/recording.rb b/ruby/lib/jam_ruby/models/recording.rb
index 570734180..645ccf6b8 100644
--- a/ruby/lib/jam_ruby/models/recording.rb
+++ b/ruby/lib/jam_ruby/models/recording.rb
@@ -132,7 +132,7 @@ module JamRuby
end
def has_access?(user)
- return users.exists?(user)
+ users.exists?(user)
end
# Start recording a session.
@@ -342,6 +342,9 @@ module JamRuby
save
end
+ def is_public?
+ claimed_recordings.where(is_public: true).length > 0
+ end
# meant to be used as a way to 'pluck' a claimed_recording appropriate for user.
def candidate_claimed_recording
diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb
index 93f73d829..2ac2ac6a8 100644
--- a/ruby/lib/jam_ruby/models/user.rb
+++ b/ruby/lib/jam_ruby/models/user.rb
@@ -5,8 +5,9 @@ module JamRuby
#devise: for later: :trackable
- devise :database_authenticatable,
- :recoverable, :rememberable
+ VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
+
+ devise :database_authenticatable, :recoverable, :rememberable
include Geokit::ActsAsMappable::Glue unless defined?(acts_as_mappable)
acts_as_mappable
@@ -107,8 +108,7 @@ module JamRuby
validates :first_name, presence: true, length: {maximum: 50}, no_profanity: true
validates :last_name, presence: true, length: {maximum: 50}, no_profanity: true
validates :biography, length: {maximum: 4000}, no_profanity: true
- VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
- validates :email, presence: true, format: {with: VALID_EMAIL_REGEX}
+ validates :email, presence: true, format: {with: VALID_EMAIL_REGEX}
validates :update_email, presence: true, format: {with: VALID_EMAIL_REGEX}, :if => :updating_email
validates_length_of :password, minimum: 6, maximum: 100, :if => :should_validate_password?
@@ -132,6 +132,7 @@ module JamRuby
scope :fans, where(:musician => false)
scope :geocoded_users, where(['lat IS NOT NULL AND lng IS NOT NULL'])
scope :musicians_geocoded, musicians.geocoded_users
+ scope :email_opt_in, where(:subscribe_email => true)
def user_progression_fields
@user_progression_fields ||= Set.new ["first_downloaded_client_at", "first_ran_client_at", "first_music_session_at", "first_real_music_session_at", "first_good_music_session_at", "first_certified_gear_at", "first_invited_at", "first_friended_at", "first_recording_at", "first_social_promoted_at" ]
@@ -291,7 +292,33 @@ module JamRuby
recordings.concat(msh)
recordings.sort! {|a,b| b.created_at <=> a.created_at}.first(5)
end
-
+
+ # returns the # of new notifications
+ def new_notifications
+ search = Notification.select('id').where(target_user_id: self.id)
+ search = search.where('created_at > ?', self.notification_seen_at) if self.notification_seen_at
+ search.count
+ end
+
+ # the user can pass in a timestamp string, or the keyword 'LATEST'
+ # if LATEST is specified, we'll use the latest_notification as the timestamp
+ # if not, just use seen as-is
+ def update_notification_seen_at seen
+ new_latest_seen = nil
+ if seen == 'LATEST'
+ latest = self.latest_notification
+ new_latest_seen = latest.created_at if latest
+ else
+ new_latest_seen = seen
+ end
+
+ self.notification_seen_at = new_latest_seen
+ end
+
+ def latest_notification
+ Notification.select('created_at').where(target_user_id: id).limit(1).order('created_at DESC').first
+ end
+
def confirm_email!
self.email_confirmed = true
end
@@ -378,7 +405,7 @@ module JamRuby
def self.reset_password(email, base_uri)
user = User.where("email ILIKE ?", email).first
- raise JamRuby::JamArgumentError if user.nil?
+ raise JamRuby::JamArgumentError.new('unknown email', :email) if user.nil?
user.reset_password_token = SecureRandom.urlsafe_base64
user.reset_password_token_created = Time.now
diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb
index 80d337abc..0c32a5dbb 100644
--- a/ruby/spec/factories.rb
+++ b/ruby/spec/factories.rb
@@ -429,6 +429,12 @@ FactoryGirl.define do
factory :event_session, :class => JamRuby::EventSession do
end
+ factory :email_batch, :class => JamRuby::EmailBatch do
+ subject Faker::Lorem.sentence
+ body "#{JamRuby::EmailBatch::VAR_FIRST_NAME} " + Faker::Lorem.paragraphs(3).join("\n")
+ test_emails 4.times.collect { Faker::Internet.safe_email }.join(',')
+ end
+
factory :notification, :class => JamRuby::Notification do
factory :notification_text_message do
diff --git a/ruby/spec/jam_ruby/models/email_batch_spec.rb b/ruby/spec/jam_ruby/models/email_batch_spec.rb
new file mode 100644
index 000000000..e3b14c32d
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/email_batch_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe EmailBatch do
+ let (:email_batch) { FactoryGirl.create(:email_batch) }
+
+ before(:each) do
+ BatchMailer.deliveries.clear
+ end
+
+ it 'has test emails setup' do
+ expect(email_batch.test_emails.present?).to be true
+ expect(email_batch.pending?).to be true
+
+ users = email_batch.test_users
+ expect(email_batch.test_count).to eq(users.count)
+ end
+
+end
diff --git a/ruby/spec/jam_ruby/models/track_spec.rb b/ruby/spec/jam_ruby/models/track_spec.rb
index 781456720..be63c781b 100644
--- a/ruby/spec/jam_ruby/models/track_spec.rb
+++ b/ruby/spec/jam_ruby/models/track_spec.rb
@@ -116,8 +116,8 @@ describe Track do
tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id}])
tracks.length.should == 1
found = tracks[0]
- found.id.should == track.id
- found.updated_at.should == track.updated_at
+ expect(found.id).to eq track.id
+ expect(found.updated_at.to_i).to eq track.updated_at.to_i
end
end
end
\ No newline at end of file
diff --git a/ruby/spec/jam_ruby/models/user_spec.rb b/ruby/spec/jam_ruby/models/user_spec.rb
index f52876ce0..744aef38d 100644
--- a/ruby/spec/jam_ruby/models/user_spec.rb
+++ b/ruby/spec/jam_ruby/models/user_spec.rb
@@ -195,7 +195,7 @@ describe User do
end
it "fails if the provided email address is unrecognized" do
- expect { User.reset_password("invalidemail@invalid.com", RESET_PASSWORD_URL) }.to raise_error
+ expect { User.reset_password("invalidemail@invalid.com", RESET_PASSWORD_URL) }.to raise_error(JamRuby::JamArgumentError)
end
it "assigns a reset_token and reset_token_created on reset" do
diff --git a/ruby/spec/mailers/batch_mailer_spec.rb b/ruby/spec/mailers/batch_mailer_spec.rb
new file mode 100644
index 000000000..a1a216607
--- /dev/null
+++ b/ruby/spec/mailers/batch_mailer_spec.rb
@@ -0,0 +1,26 @@
+require "spec_helper"
+
+describe BatchMailer do
+
+ describe "should send test emails" do
+ BatchMailer.deliveries.clear
+
+ let (:mail) { BatchMailer.deliveries[0] }
+
+ batch = FactoryGirl.create(:email_batch)
+ batch.update_attribute(:test_emails, "jonathan@jamkazam.com")
+ batch.send_test_batch
+
+ it { BatchMailer.deliveries.length.should == 1 }
+
+ it { mail['from'].to_s.should == EmailBatch::DEFAULT_SENDER }
+ it { mail['to'].to_s.split(',')[0].should == batch.test_emails.split(',')[0] }
+ it { mail.subject.should == batch.subject }
+
+ it { mail.multipart?.should == true } # because we send plain + html
+ it { mail.text_part.decode_body.should match(/#{Regexp.escape(batch.body)}/) }
+
+ it { batch.testing?.should == true }
+ end
+
+end
diff --git a/web/Gemfile b/web/Gemfile
index 6ea68e5fb..f2cdab689 100644
--- a/web/Gemfile
+++ b/web/Gemfile
@@ -45,7 +45,7 @@ gem 'omniauth-twitter'
gem 'omniauth-google-oauth2', '0.2.1'
gem 'twitter'
gem 'fb_graph', '2.5.9'
-gem 'sendgrid', '1.1.0'
+gem 'sendgrid', '1.2.0'
gem 'recaptcha', '0.3.4'
gem 'filepicker-rails', '0.1.0'
gem 'aws-sdk', '1.29.1'
@@ -67,6 +67,8 @@ gem 'resque-retry'
gem 'resque-failed-job-mailer'
gem 'resque-dynamic-queues'
gem 'resque-lonely_job', '~> 1.0.0'
+gem 'resque_mailer'
+
gem 'quiet_assets', :group => :development
gem 'bugsnag'
gem 'multi_json', '1.9.0'
diff --git a/web/app/assets/javascripts/AAB_message_factory.js b/web/app/assets/javascripts/AAB_message_factory.js
index 7f11e603d..04bd51344 100644
--- a/web/app/assets/javascripts/AAB_message_factory.js
+++ b/web/app/assets/javascripts/AAB_message_factory.js
@@ -99,8 +99,10 @@
};
// Heartbeat message
- factory.heartbeat = function() {
+ factory.heartbeat = function(lastNotificationSeen, lastNotificationSeenAt) {
var data = {};
+ data.notification_seen = lastNotificationSeen;
+ data.notification_seen_at = lastNotificationSeenAt;
return client_container(msg.HEARTBEAT, route_to.SERVER, data);
};
diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js
index 68db544ad..ec1dee5c6 100644
--- a/web/app/assets/javascripts/application.js
+++ b/web/app/assets/javascripts/application.js
@@ -28,6 +28,9 @@
//= require jquery.infinitescroll
//= require jquery.hoverIntent
//= require jquery.dotdotdot
+//= require jquery.pulse
+//= require jquery.browser
+//= require jquery.custom-protocol
//= require AAA_Log
//= require globals
//= require AAB_message_factory
diff --git a/web/app/assets/javascripts/createSession.js.erb b/web/app/assets/javascripts/createSession.js.erb
index 494f8f661..b572c2ca7 100644
--- a/web/app/assets/javascripts/createSession.js.erb
+++ b/web/app/assets/javascripts/createSession.js.erb
@@ -21,6 +21,8 @@
function afterShow(data) {
inviteMusiciansUtil.loadFriends();
+
+ context.JK.guardAgainstBrowser(app);
}
function resetForm() {
@@ -117,11 +119,15 @@
function submitForm(evt) {
evt.preventDefault();
+ if(!gon.isNativeClient) {
+ return false;
+ }
+
// If user hasn't completed FTUE - do so now.
if (!(context.JK.hasOneConfiguredDevice())) {
app.afterFtue = function() { submitForm(evt); };
app.layout.startNewFtue();
- return;
+ return false;
}
// if for some reason there are 0 tracks, show FTUE
@@ -131,7 +137,7 @@
// If user hasn't completed FTUE - do so now.
app.afterFtue = function() { submitForm(evt); };
app.layout.startNewFtue();
- return;
+ return false;
}
var isValid = validateForm();
diff --git a/web/app/assets/javascripts/feed_item_recording.js b/web/app/assets/javascripts/feed_item_recording.js
index 0d8793446..834aedae5 100644
--- a/web/app/assets/javascripts/feed_item_recording.js
+++ b/web/app/assets/javascripts/feed_item_recording.js
@@ -7,6 +7,7 @@
var claimedRecordingId = $parentElement.attr('data-claimed-recording-id');
var recordingId = $parentElement.attr('id');
+ var mode = $parentElement.attr('data-mode');
var $feedItem = $parentElement;
var $name = $('.name', $feedItem);
diff --git a/web/app/assets/javascripts/findSession.js b/web/app/assets/javascripts/findSession.js
index 31421dfcd..a684081cb 100644
--- a/web/app/assets/javascripts/findSession.js
+++ b/web/app/assets/javascripts/findSession.js
@@ -346,6 +346,7 @@
buildQuery();
refreshDisplay();
loadSessions();
+ context.JK.guardAgainstBrowser(app);
}
function clearResults() {
diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js
index 2b19b5d44..fbdf74a2a 100644
--- a/web/app/assets/javascripts/jam_rest.js
+++ b/web/app/assets/javascripts/jam_rest.js
@@ -934,6 +934,7 @@
}
function getNotifications(options) {
+ if(!options) options = {};
var id = getId(options);
return $.ajax({
type: "GET",
diff --git a/web/app/assets/javascripts/jamkazam.js b/web/app/assets/javascripts/jamkazam.js
index ea0a39710..f00e9b02d 100644
--- a/web/app/assets/javascripts/jamkazam.js
+++ b/web/app/assets/javascripts/jamkazam.js
@@ -28,6 +28,8 @@
var lastHeartbeatFound = false;
var heartbeatAckCheckInterval = null;
var userDeferred = null;
+ var notificationLastSeenAt = undefined;
+ var notificationLastSeen = undefined;
var opts = {
inClient: true, // specify false if you want the app object but none of the client-oriented features
@@ -91,8 +93,9 @@
function _heartbeat() {
if (app.heartbeatActive) {
-
- var message = context.JK.MessageFactory.heartbeat();
+ var message = context.JK.MessageFactory.heartbeat(notificationLastSeen, notificationLastSeenAt);
+ notificationLastSeenAt = undefined;
+ notificationLastSeen = undefined;
context.JK.JamServer.send(message);
lastHeartbeatFound = false;
}
@@ -384,6 +387,28 @@
return userDeferred;
}
+ this.updateNotificationSeen = function(notificationId, notificationCreatedAt) {
+ var time = new Date(notificationCreatedAt);
+
+ if(!notificationCreatedAt) {
+ throw 'invalid value passed to updateNotificationSeen'
+ }
+
+ if(!notificationLastSeenAt) {
+ notificationLastSeenAt = notificationCreatedAt;
+ notificationLastSeen = notificationId;
+ logger.debug("updated notificationLastSeenAt with: " + notificationCreatedAt);
+ }
+ else if(time.getTime() > new Date(notificationLastSeenAt).getTime()) {
+ notificationLastSeenAt = notificationCreatedAt;
+ notificationLastSeen = notificationId;
+ logger.debug("updated notificationLastSeenAt with: " + notificationCreatedAt);
+ }
+ else {
+ logger.debug("ignored notificationLastSeenAt for: " + notificationCreatedAt);
+ }
+ }
+
this.unloadFunction = function () {
logger.debug("window.unload function called.");
diff --git a/web/app/assets/javascripts/jquery.custom-protocol.js b/web/app/assets/javascripts/jquery.custom-protocol.js
new file mode 100644
index 000000000..3e319fa48
--- /dev/null
+++ b/web/app/assets/javascripts/jquery.custom-protocol.js
@@ -0,0 +1,139 @@
+/*!
+ * jQuery Custom Protocol Launcher v0.0.1
+ * https://github.com/sethcall/jquery-custom-protocol
+ *
+ * Taken and modified from:
+ * https://gist.github.com/rajeshsegu/3716941
+ * http://stackoverflow.com/a/22055638/834644
+ *
+ * Depends on:
+ * https://github.com/gabceb/jquery-browser-plugin
+ *
+ * Modifications Copyright 2014 Seth Call
+ * https://github.com/sethcall
+ *
+ * Released under the MIT license
+ */
+
+(function (jQuery, window, undefined) {
+ "use strict";
+
+ function launchCustomProtocol(elem, url, options) {
+ var myWindow, success = false;
+
+ if (!url) {
+ throw "attribute 'href' must be specified on the element, or specified in options"
+ }
+ if (!options.callback) {
+ throw "Specify 'callback' as an option to $.customProtocol";
+ }
+
+ var settings = $.extend({}, options);
+
+ var callback = settings.callback;
+
+ if ($.browser.msie) {
+ return ieTest(elem, url, callback);
+ }
+ else if ($.browser.mozilla) {
+ return iframeTest(elem, url, callback);
+ }
+ else if ($.browser.chrome) {
+ return blurTest(elem, url, callback);
+ }
+ }
+
+ function blurTest(elem, url, callback) {
+ var timeout = null;
+ // If focus is taken, assume a launch dialog was shown
+ elem.css({"outline": 0});
+ elem.attr("tabindex", "1");
+ elem.focus();
+
+ function cleanup() {
+ elem.off('blur');
+ elem.removeAttr("tabindex");
+ if(timeout) {
+ clearTimeout(timeout)
+ timeout = null;
+ }
+ }
+ elem.blur(function () {
+ cleanup();
+ callback(true);
+ });
+
+ location.replace(url);
+
+ timeout = setTimeout(function () {
+ timeout = null;
+ cleanup();
+ callback(false);
+ }, 1000);
+
+ return false;
+ }
+
+ function iframeTest(elem, url, callback) {
+ var iframe, success = false;
+
+ try {
+ iframe = $("");
+ iframe.css({"display": "none"});
+ iframe.appendTo("body");
+ iframe[0].contentWindow.location.href = url;
+ success = true;
+ } catch (ex) {
+ success = false;
+ }
+
+ if(iframe) iframe.remove();
+
+ callback(success);
+
+ return false;
+ }
+
+ function ieTest(elem, url, callback) {
+ var success = false;
+
+ if (navigator.msLaunchUri) {
+ // use msLaunchUri if available. IE10+
+ navigator.msLaunchUri(
+ url,
+ function () {
+ callback(true);
+ },
+ function () {
+ callback(false);
+ }
+ );
+ }
+
+ return false;
+ }
+
+ function supportedBrowser() {
+ return $.browser.desktop && (navigator.msLaunchUri || $.browser.chrome || $.browser.mozilla);
+ }
+
+ $.fn.customProtocol = function (options) {
+ this.each(function () {
+ var $elem = $(this);
+
+ if(supportedBrowser()) {
+ $elem.click(function (e) {
+ return launchCustomProtocol($elem, options.href || $elem.attr('href'), options);
+ });
+ }
+ else {
+ if(options && options.fallback) {
+ options.fallback.call($elem)
+ return;
+ }
+ }
+ })
+
+ }
+
+})(jQuery, window);
diff --git a/web/app/assets/javascripts/launchAppDialog.js b/web/app/assets/javascripts/launchAppDialog.js
new file mode 100644
index 000000000..fe1d9f76b
--- /dev/null
+++ b/web/app/assets/javascripts/launchAppDialog.js
@@ -0,0 +1,111 @@
+(function(context,$) {
+
+ "use strict";
+ context.JK = context.JK || {};
+ context.JK.LaunchAppDialog = function(app) {
+ var logger = context.JK.logger;
+ var rest = context.JK.Rest();
+ var $dialog = null;
+ var $contentHolder = null;
+ var $templateUnsupportedLaunch = null;
+ var $templateAttemptLaunch = null;
+ var $templateLaunchSuccessful = null;
+ var $templateLaunchUnsuccessful = null;
+
+ function renderAttemptLaunch() {
+ var $template = $(context._.template($templateAttemptLaunch.html(), buildOptions(), { variable: 'data' }));
+ $template.find('.btn-cancel').click(handleBack);
+ $contentHolder.empty().append($template);
+
+
+ var options = {
+ callback:function(success) {
+ if(success) {
+ renderLaunchSuccess();
+ }
+ else {
+ renderLaunchUnsuccessful();
+ }
+ },
+ fallback:function() {
+ renderUnsupportedLaunch();
+ }
+ };
+
+ $template.find('.btn-launch-app').customProtocol(options);
+ }
+
+ function renderLaunchSuccess() {
+ var $template = $(context._.template($templateLaunchSuccessful.html(), buildOptions(), { variable: 'data' }));
+ $template.find('.btn-done').click(handleBack);
+ $template.find('.not-actually-launched').click(function() {
+ $dialog.find('.dont-see-it').show();
+ return false;
+ });
+ $contentHolder.empty().append($template);
+ }
+
+ function renderLaunchUnsuccessful() {
+ var $template = $(context._.template($templateLaunchUnsuccessful.html(), buildOptions(), { variable: 'data' }));
+ $template.find('.btn-done').click(handleBack);
+ $contentHolder.empty().append($template);
+ }
+
+ function renderUnsupportedLaunch() {
+ var $template = $(context._.template($templateUnsupportedLaunch.html(), buildOptions(), { variable: 'data' }));
+ $template.find('.btn-cancel').click(handleBack);
+ $contentHolder.empty().append($template);
+ }
+
+ function handleBack() {
+ app.layout.closeDialog('launch-app-dialog');
+ window.location = '/client#/home';
+ return false;
+ }
+
+ function buildOptions() {
+ var osSpecificSystemTray = 'taskbar';
+ if($.browser.mac) {
+ osSpecificSystemTray = 'dock or status menu';
+ }
+
+ var osSpecificSystemTrayLink = '';
+
+ if($.browser.win) {
+ osSpecificSystemTrayLink = 'http://windows.microsoft.com/en-us/windows/taskbar-overview#1TC=windows-7';
+ }
+ else if($.browser.mac) {
+ osSpecificSystemTrayLink = 'http://support.apple.com/kb/ht3737';
+ }
+
+ return { osSpecificSystemTray: osSpecificSystemTray, osSpecificSystemTrayLink: osSpecificSystemTrayLink, launchUrl: 'jamkazam:'};
+ }
+
+ function reset() {
+ renderAttemptLaunch();
+ }
+
+ function beforeShow() {
+ reset();
+ }
+
+ function initialize() {
+ var dialogBindings = {
+ 'beforeShow' : beforeShow
+ };
+
+ app.bindDialog('launch-app-dialog', dialogBindings);
+
+ $dialog = $('#launch-app-dialog');
+ $contentHolder = $dialog.find('.dialog-inner');
+ $templateAttemptLaunch = $('#template-attempt-launch');
+ $templateUnsupportedLaunch = $('#template-unsupported-launch');
+ $templateLaunchSuccessful = $('#template-launch-successful');
+ $templateLaunchUnsuccessful = $('#template-launch-unsuccessful');
+ }
+
+ this.initialize = initialize;
+ }
+
+ return this;
+})(window,jQuery);
diff --git a/web/app/assets/javascripts/layout.js b/web/app/assets/javascripts/layout.js
index d948945dc..555cc0d6d 100644
--- a/web/app/assets/javascripts/layout.js
+++ b/web/app/assets/javascripts/layout.js
@@ -251,7 +251,8 @@
if (!sidebarVisible) {
return;
}
- var $expandedPanelContents = $('[layout-id="' + expandedPanel + '"] [layout-panel="contents"]');
+ var $expandedPanel = $('[layout-id="' + expandedPanel + '"]');
+ var $expandedPanelContents = $expandedPanel.find('[layout-panel="contents"]');
var combinedHeaderHeight = $('[layout-panel="contents"]').length * opts.panelHeaderHeight;
var searchHeight = $('.sidebar .search').first().height();
var expanderHeight = $('[layout-sidebar-expander]').height();
@@ -259,6 +260,7 @@
$('[layout-panel="contents"]').hide();
$('[layout-panel="contents"]').css({"height": "1px"});
$expandedPanelContents.show();
+ $expandedPanel.triggerHandler('open')
$expandedPanelContents.animate({"height": expandedPanelHeight + "px"}, opts.animationDuration);
}
@@ -425,6 +427,7 @@
unstackDialogs($overlay);
$dialog.hide();
dialogEvent(dialog, 'afterHide');
+ $(me).triggerHandler('dialog_closed', {dialogCount: openDialogs.length})
}
function screenEvent(screen, evtName, data) {
@@ -526,6 +529,29 @@
}
}
+ function isDialogShowing() {
+ return openDialogs.length > 0;
+ }
+
+ function currentDialog() {
+ if(openDialogs.length == 0) return null;
+
+ return openDialogs[openDialogs.length - 1];
+ }
+
+ // payload is a notification event from websocket gateway
+ function dialogObscuredNotification(payload) {
+ var openDialog = currentDialog();
+ if(!openDialog) return false;
+
+ if(typeof openDialog.handledNotification === 'function') {
+ return !openDialog.handledNotification(payload);
+ }
+ else {
+ return true;
+ }
+ }
+
/**
* Responsible for keeping N dialogs in correct stacked order,
* also moves the .dialog-overlay such that it hides/obscures all dialogs except the highest one
@@ -849,6 +875,14 @@
showDialog(dialog, options);
};
+ this.dialogObscuredNotification = function() {
+ return dialogObscuredNotification();
+ }
+
+ this.isDialogShowing = function() {
+ return isDialogShowing();
+ }
+
this.close = function (evt) {
close(evt);
};
diff --git a/web/app/assets/javascripts/notificationPanel.js b/web/app/assets/javascripts/notificationPanel.js
new file mode 100644
index 000000000..90e9d5d5c
--- /dev/null
+++ b/web/app/assets/javascripts/notificationPanel.js
@@ -0,0 +1,905 @@
+(function(context,$) {
+
+ "use strict";
+
+ context.JK = context.JK || {};
+ context.JK.NotificationPanel = function(app) {
+ var logger = context.JK.logger;
+ var friends = [];
+ var rest = context.JK.Rest();
+ var missedNotificationsWhileAway = false;
+ var $panel = null;
+ var $expanded = null;
+ var $contents = null;
+ var $count = null;
+ var $list = null;
+ var $notificationTemplate = null;
+ var sidebar = null;
+ var darkenedColor = '#0D7B89';
+ var highlightedColor = 'white'
+ var textMessageDialog = null;
+ var queuedNotification = null;
+ var queuedNotificationCreatedAt = null;
+
+ function isNotificationsPanelVisible() {
+ return $contents.is(':visible')
+ }
+
+ function incrementNotificationCount() {
+ var count = parseInt($count.text());
+ setCount(count + 1);
+ }
+
+ // set the element to white, and pulse it down to the un-highlighted value 2x, then set
+ function pulseToDark() {
+ logger.debug("pulsing notification badge")
+ lowlightCount();
+ $count.pulse({'background-color' : highlightedColor}, {pulses: 2}, function() {
+ $count.removeAttr('style')
+ setCount(0);
+ })
+ }
+
+ function setCount(count) {
+ $count.text(count);
+ }
+
+ function lowlightCount() {
+ $count.removeClass('highlighted');
+ }
+
+ function highlightCount() {
+ $count.addClass('highlighted');
+ }
+
+ function queueNotificationSeen(notificationId, notificationCreatedAt) {
+
+ var time = new Date(notificationCreatedAt);
+
+ if(!notificationCreatedAt) {
+ throw 'invalid value passed to queuedNotificationCreatedAt'
+ }
+
+ if(!queuedNotificationCreatedAt) {
+ queuedNotification = notificationId;
+ queuedNotificationCreatedAt = notificationCreatedAt;
+ logger.debug("updated queuedNotificationCreatedAt with: " + notificationCreatedAt);
+ }
+ else if(time.getTime() > new Date(queuedNotificationCreatedAt).getTime()) {
+ queuedNotification = notificationId;
+ queuedNotificationCreatedAt = notificationCreatedAt;
+ logger.debug("updated queuedNotificationCreatedAt with: " + notificationCreatedAt);
+ }
+ else {
+ logger.debug("ignored queuedNotificationCreatedAt for: " + notificationCreatedAt);
+ }
+ }
+
+ function onNotificationOccurred(payload) {
+ if(userCanSeeNotifications(payload)) {
+ app.updateNotificationSeen(payload.notification_id, payload.created_at);
+ }
+ else {
+ queueNotificationSeen(payload.notification_id, payload.created_at);
+ highlightCount();
+ incrementNotificationCount();
+ missedNotificationsWhileAway = true;
+ }
+ }
+
+ function userCameBack() {
+ if(isNotificationsPanelVisible()) {
+ if(missedNotificationsWhileAway) {
+ // catch user's eye, then put count to 0
+ pulseToDark();
+ if(queuedNotificationCreatedAt) {
+ app.updateNotificationSeen(queuedNotification, queuedNotificationCreatedAt);
+ }
+ }
+ }
+
+ queuedNotification = null;
+ queuedNotificationCreatedAt = null;
+ missedNotificationsWhileAway = false;
+ }
+
+ function opened() {
+ queuedNotification = null;
+ queuedNotificationCreatedAt = null;
+ rest.updateUser({notification_seen_at: 'LATEST'})
+ .done(function(response) {
+ lowlightCount();
+ setCount(0);
+ })
+ .fail(app.ajaxError)
+ }
+
+ function windowBlurred() {
+
+ }
+
+ function events() {
+ $(app.layout).on('dialog_closed', function(e, data) {if(data.dialogCount == 0) userCameBack(); });
+ $(window).focus(userCameBack);
+ $(window).blur(windowBlurred);
+ app.user()
+ .done(function(user) {
+ setCount(user.new_notifications);
+ if(user.new_notifications > 0) {
+ highlightCount();
+ }
+ });
+
+ $panel.on('open', opened);
+
+ // friend notifications
+ registerFriendRequest();
+ registerFriendRequestAccepted();
+ registerNewUserFollower();
+ registerNewBandFollower();
+
+ // session notifications
+ registerSessionInvitation();
+ registerSessionEnded();
+ registerJoinRequest();
+ registerJoinRequestApproved();
+ registerJoinRequestRejected();
+ registerMusicianSessionJoin();
+ registerBandSessionJoin();
+
+ // recording notifications
+ registerMusicianRecordingSaved();
+ registerBandRecordingSaved();
+ registerRecordingMasterMixComplete();
+
+ // band notifications
+ registerBandInvitation();
+ registerBandInvitationAccepted();
+
+ // register text messages
+ registerTextMessage();
+ }
+
+ function populate() {
+ // retrieve pending notifications for this user
+ rest.getNotifications()
+ .done(function(response) {
+ updateNotificationList(response);
+ })
+ .fail(app.ajaxError)
+ }
+
+ function updateNotificationList(response) {
+ $list.empty();
+
+ $.each(response, function(index, val) {
+
+ if(val.description == 'TEXT_MESSAGE') {
+ val.formatted_msg = textMessageDialog.formatTextMessage(val.message.substring(0, 200), val.source_user_id, val.source_user.name, val.message.length > 200).html();
+ }
+
+ // fill in template for Connect pre-click
+ var template = $notificationTemplate.html();
+ var notificationHtml = context.JK.fillTemplate(template, {
+ notificationId: val.notification_id,
+ sessionId: val.session_id,
+ avatar_url: context.JK.resolveAvatarUrl(val.photo_url),
+ text: val.formatted_msg,
+ date: $.timeago(val.created_at)
+ });
+
+ $list.append(notificationHtml);
+
+ // val.description contains the notification record's description value from the DB (i.e., type)
+ initializeActions(val, val.description);
+ });
+ }
+
+
+ function initializeActions(payload, type) {
+
+ var $notification = $('li[notification-id=' + payload.notification_id + ']');
+ var $btnNotificationAction = '#btn-notification-action';
+
+ // wire up "x" button to delete notification
+ $notification.find('#img-delete-notification').click(deleteNotificationHandler);
+
+ // customize action buttons based on notification type
+ if (type === context.JK.MessageType.FRIEND_REQUEST) {
+ var $action_btn = $notification.find($btnNotificationAction);
+ $action_btn.text('ACCEPT');
+ $action_btn.click(function() {
+ acceptFriendRequest(payload);
+ });
+ }
+
+ else if (type === context.JK.MessageType.FRIEND_REQUEST_ACCEPTED) {
+ $notification.find('#div-actions').hide();
+ }
+
+ else if (type === context.JK.MessageType.NEW_USER_FOLLOWER || type === context.JK.MessageType.NEW_BAND_FOLLOWER) {
+ $notification.find('#div-actions').hide();
+ }
+
+ else if (type === context.JK.MessageType.SESSION_INVITATION) {
+ var $action_btn = $notification.find($btnNotificationAction);
+ $action_btn.text('JOIN');
+ $action_btn.click(function() {
+ openTerms(payload);
+ });
+ }
+
+ else if (type === context.JK.MessageType.JOIN_REQUEST) {
+ var $action_btn = $notification.find($btnNotificationAction);
+ $action_btn.text('APPROVE');
+ $action_btn.click(function() {
+ approveJoinRequest(payload);
+ });
+ }
+
+ else if (type === context.JK.MessageType.JOIN_REQUEST_APPROVED) {
+ var $action_btn = $notification.find($btnNotificationAction);
+ $action_btn.text('JOIN');
+ $action_btn.click(function() {
+ openTerms(payload);
+ });
+ }
+
+ else if (type === context.JK.MessageType.JOIN_REQUEST_REJECTED) {
+ $notification.find('#div-actions').hide();
+ }
+
+ else if (type === context.JK.MessageType.MUSICIAN_SESSION_JOIN || type === context.JK.MessageType.BAND_SESSION_JOIN) {
+
+ var actionText = '';
+ var callback;
+ if (context.JK.currentUserMusician) {
+ // user is MUSICIAN; musician_access = TRUE
+ if (payload.musician_access) {
+ actionText = "JOIN";
+ callback = joinSession;
+ }
+ // user is MUSICIAN; fan_access = TRUE
+ else if (payload.fan_access) {
+ actionText = "LISTEN";
+ callback = listenToSession;
+ }
+ }
+ else {
+ // user is FAN; fan_access = TRUE
+ if (payload.fan_access) {
+ actionText = "LISTEN";
+ callback = listenToSession;
+ }
+ }
+
+ var $action_btn = $notification.find($btnNotificationAction);
+ $action_btn.text(actionText);
+ $action_btn.click(function() {
+ callback(payload);
+ });
+ }
+
+ else if (type === context.JK.MessageType.MUSICIAN_RECORDING_SAVED || type === context.JK.MessageType.BAND_RECORDING_SAVED) {
+ var $action_btn = $notification.find($btnNotificationAction);
+ $action_btn.text('LISTEN');
+ $action_btn.click(function() {
+ listenToRecording(payload);
+ });
+ }
+
+ else if (type === context.JK.MessageType.RECORDING_MASTER_MIX_COMPLETE) {
+ $notification.find('#div-actions').hide();
+ context.jamClient.OnDownloadAvailable(); // poke backend, letting it know a download is available
+ }
+
+ else if (type === context.JK.MessageType.BAND_INVITATION) {
+ var $action_btn = $notification.find($btnNotificationAction);
+ $action_btn.text('ACCEPT');
+ $action_btn.click(function() {
+ acceptBandInvitation(payload);
+ });
+ }
+ else if (type === context.JK.MessageType.BAND_INVITATION_ACCEPTED) {
+ $notification.find('#div-actions').hide();
+ }
+ else if (type === context.JK.MessageType.TEXT_MESSAGE) {
+ var $action_btn = $notification.find($btnNotificationAction);
+ $action_btn.text('REPLY');
+ $action_btn.click(function() {
+ var userId = $notification.find('.more-text-available').attr('data-sender-id');
+ app.layout.showDialog('text-message', { d1: userId });
+ });
+
+ var moreTextLink = $notification.find('.more-text-available');
+ var textMessage = $notification.find('.text-message');
+ var clipped_msg = textMessage.attr('data-is-clipped') === 'true';
+
+ if(clipped_msg) {
+ moreTextLink.text('more').show();
+ moreTextLink.click(function(e) {
+ var userId = $(this).attr('data-sender-id');
+
+ return false;
+ });
+ }
+ else {
+ moreTextLink.hide();
+ }
+ }
+ }
+
+ function acceptBandInvitation(args) {
+ rest.updateBandInvitation(
+ args.band_id,
+ args.band_invitation_id,
+ true
+ ).done(function(response) {
+ deleteNotification(args.notification_id); // delete notification corresponding to this friend request
+ }).error(app.ajaxError);
+ }
+
+
+ function deleteNotification(notificationId) {
+ var url = "/api/users/" + context.JK.currentUserId + "/notifications/" + notificationId;
+ $.ajax({
+ type: "DELETE",
+ dataType: "json",
+ contentType: 'application/json',
+ url: url,
+ processData: false,
+ success: function(response) {
+ $('li[notification-id=' + notificationId + ']').hide();
+ //decrementNotificationCount();
+ },
+ error: app.ajaxError
+ });
+ }
+
+
+ function listenToSession(args) {
+ deleteNotification(args.notification_id);
+ context.JK.popExternalLink('/sessions/' + args.session_id);
+ }
+
+ /*********** TODO: THE NEXT 3 FUNCTIONS ARE COPIED FROM sessionList.js. REFACTOR TO COMMON PLACE. *************/
+ function joinSession(args) {
+ // NOTE: invited musicians get their own notification, so no need to check if user has invitation here
+ // like other places because an invited user would never get this notification
+ if (args.musician_access) {
+ if (args.approval_required) {
+ openAlert(args.session_id);
+ }
+ else {
+ openTerms(args);
+ }
+ }
+ deleteNotification(args.notification_id);
+ }
+
+
+ function registerJoinRequestApproved() {
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.JOIN_REQUEST_APPROVED, function(header, payload) {
+ logger.debug("Handling JOIN_REQUEST_APPROVED msg " + JSON.stringify(payload));
+
+ handleNotification(payload, header.type);
+
+ app.notify({
+ "title": "Join Request Approved",
+ "text": payload.msg,
+ "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
+ }, {
+ "ok_text": "JOIN SESSION",
+ "ok_callback": openTerms,
+ "ok_callback_args": { "session_id": payload.session_id, "notification_id": payload.notification_id }
+ });
+ });
+ }
+
+ function registerJoinRequestRejected() {
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.JOIN_REQUEST_REJECTED, function(header, payload) {
+ logger.debug("Handling JOIN_REQUEST_REJECTED msg " + JSON.stringify(payload));
+
+ handleNotification(payload, header.type);
+
+ app.notify({
+ "title": "Join Request Rejected",
+ "text": payload.msg,
+ "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
+ });
+ });
+ }
+
+
+ function registerJoinRequest() {
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.JOIN_REQUEST, function(header, payload) {
+ logger.debug("Handling JOIN_REQUEST msg " + JSON.stringify(payload));
+
+ handleNotification(payload, header.type);
+
+ app.notify({
+ "title": "New Join Request",
+ "text": payload.msg,
+ "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
+ }, {
+ "ok_text": "APPROVE",
+ "ok_callback": approveJoinRequest,
+ "ok_callback_args": { "join_request_id": payload.join_request_id, "notification_id": payload.notification_id },
+ "cancel_text": "REJECT",
+ "cancel_callback": rejectJoinRequest,
+ "cancel_callback_args": { "join_request_id": payload.join_request_id, "notification_id": payload.notification_id }
+ });
+ });
+ }
+
+
+ function registerFriendRequestAccepted() {
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.FRIEND_REQUEST_ACCEPTED, function(header, payload) {
+ logger.debug("Handling FRIEND_REQUEST_ACCEPTED msg " + JSON.stringify(payload));
+
+ handleNotification(payload, header.type);
+
+ sidebar.refreshFriends();
+
+ app.notify({
+ "title": "Friend Request Accepted",
+ "text": payload.msg,
+ "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
+ });
+ });
+ }
+
+ function registerNewUserFollower() {
+
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.NEW_USER_FOLLOWER, function(header, payload) {
+ logger.debug("Handling NEW_USER_FOLLOWER msg " + JSON.stringify(payload));
+
+ handleNotification(payload, header.type);
+
+ app.notify({
+ "title": "New Follower",
+ "text": payload.msg,
+ "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
+ });
+ });
+ }
+
+
+ function registerNewBandFollower() {
+
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.NEW_BAND_FOLLOWER, function(header, payload) {
+ logger.debug("Handling NEW_BAND_FOLLOWER msg " + JSON.stringify(payload));
+
+ handleNotification(payload, header.type);
+
+ app.notify({
+ "title": "New Band Follower",
+ "text": payload.msg,
+ "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
+ });
+ });
+ }
+
+
+ function registerFriendRequest() {
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.FRIEND_REQUEST, function(header, payload) {
+ logger.debug("Handling FRIEND_REQUEST msg " + JSON.stringify(payload));
+
+ handleNotification(payload, header.type);
+
+ app.notify({
+ "title": "New Friend Request",
+ "text": payload.msg,
+ "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
+ }, {
+ "ok_text": "ACCEPT",
+ "ok_callback": acceptFriendRequest,
+ "ok_callback_args": { "friend_request_id": payload.friend_request_id, "notification_id": payload.notification_id }
+ });
+ });
+ }
+
+ function acceptFriendRequest(args) {
+
+ rest.acceptFriendRequest({
+ status: 'accept',
+ friend_request_id: args.friend_request_id
+ }).done(function(response) {
+ deleteNotification(args.notification_id); // delete notification corresponding to this friend request
+ sidebar.refreshFriends(); // refresh friends panel when request is accepted
+ }).error(app.ajaxError);
+ }
+
+ function registerSessionEnded() {
+ // TODO: this should clean up all notifications related to this session
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_ENDED, function(header, payload) {
+ logger.debug("Handling SESSION_ENDED msg " + JSON.stringify(payload));
+ deleteSessionNotifications(payload.session_id);
+ });
+ }
+
+ // remove all notifications for this session
+ function deleteSessionNotifications(sessionId) {
+ $('li[session-id=' + sessionId + ']').hide();
+ //decrementNotificationCount();
+ }
+
+ function registerSessionInvitation() {
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_INVITATION, function(header, payload) {
+ logger.debug("Handling SESSION_INVITATION msg " + JSON.stringify(payload));
+
+ handleNotification(payload, header.type);
+
+ var participants = [];
+ rest.getSession(payload.session_id).done(function(response) {
+ $.each(response.participants, function(index, val) {
+ participants.push({"photo_url": context.JK.resolveAvatarUrl(val.user.photo_url), "name": val.user.name});
+ });
+
+ var participantHtml = "You have been invited to join a session with:
";
+ participantHtml += "";
+
+ $.each(participants, function(index, val) {
+ if (index < 4) {
+ participantHtml += " + ") | " + val.name + " |
";
+ }
+ });
+
+ participantHtml += "
";
+
+ app.notify({
+ "title": "Session Invitation",
+ "text": participantHtml
+ }, {
+ "ok_text": "JOIN SESSION",
+ "ok_callback": openTerms,
+ "ok_callback_args": { "session_id": payload.session_id, "notification_id": payload.notification_id }
+ });
+ }).error(app.ajaxError);
+
+ });
+ }
+
+
+ function registerMusicianSessionJoin() {
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_JOIN, function(header, payload) {
+ logger.debug("Handling MUSICIAN_SESSION_JOIN msg " + JSON.stringify(payload));
+
+ var okText = '';
+ var showNotification = false;
+ var callback;
+ if (context.JK.currentUserMusician) {
+ // user is MUSICIAN; musician_access = TRUE
+ if (payload.musician_access) {
+ showNotification = true;
+ okText = "JOIN";
+ callback = joinSession;
+ }
+ // user is MUSICIAN; fan_access = TRUE
+ else if (payload.fan_access) {
+ showNotification = true;
+ okText = "LISTEN";
+ callback = listenToSession;
+ }
+ }
+ else {
+ // user is FAN; fan_access = TRUE
+ if (payload.fan_access) {
+ showNotification = true;
+ okText = "LISTEN";
+ callback = listenToSession;
+ }
+ }
+
+ if (showNotification) {
+ handleNotification(payload, header.type);
+
+ app.notify({
+ "title": "Musician Joined Session",
+ "text": payload.msg,
+ "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
+ }, {
+ "ok_text": okText,
+ "ok_callback": callback,
+ "ok_callback_args": {
+ "session_id": payload.session_id,
+ "fan_access": payload.fan_access,
+ "musician_access": payload.musician_access,
+ "approval_required": payload.approval_required,
+ "notification_id": payload.notification_id
+ }
+ }
+ );
+ }
+ });
+ }
+
+
+ function registerBandSessionJoin() {
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_SESSION_JOIN, function(header, payload) {
+ logger.debug("Handling BAND_SESSION_JOIN msg " + JSON.stringify(payload));
+
+ var okText = '';
+ var showNotification = false;
+ var callback;
+ if (context.JK.currentUserMusician) {
+ // user is MUSICIAN; musician_access = TRUE
+ if (payload.musician_access) {
+ showNotification = true;
+ okText = "JOIN";
+ callback = joinSession;
+ }
+ // user is MUSICIAN; fan_access = TRUE
+ else if (payload.fan_access) {
+ showNotification = true;
+ okText = "LISTEN";
+ callback = listenToSession;
+ }
+ }
+ else {
+ // user is FAN; fan_access = TRUE
+ if (payload.fan_access) {
+ showNotification = true;
+ okText = "LISTEN";
+ callback = listenToSession;
+ }
+ }
+
+ if (showNotification) {
+ handleNotification(payload, header.type);
+
+ app.notify({
+ "title": "Band Joined Session",
+ "text": payload.msg,
+ "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
+ }, {
+ "ok_text": "LISTEN",
+ "ok_callback": callback,
+ "ok_callback_args": {
+ "session_id": payload.session_id,
+ "fan_access": payload.fan_access,
+ "musician_access": payload.musician_access,
+ "approval_required": payload.approval_required,
+ "notification_id": payload.notification_id
+ }
+ }
+ );
+ }
+ });
+ }
+
+
+ function registerMusicianRecordingSaved() {
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.MUSICIAN_RECORDING_SAVED, function(header, payload) {
+ logger.debug("Handling MUSICIAN_RECORDING_SAVED msg " + JSON.stringify(payload));
+
+ handleNotification(payload, header.type);
+
+ app.notify({
+ "title": "Musician Recording Saved",
+ "text": payload.msg,
+ "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
+ }, {
+ "ok_text": "LISTEN",
+ "ok_callback": listenToRecording,
+ "ok_callback_args": {
+ "recording_id": payload.recording_id,
+ "notification_id": payload.notification_id
+ }
+ });
+ });
+ }
+
+ function registerBandRecordingSaved() {
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_RECORDING_SAVED, function(header, payload) {
+ logger.debug("Handling BAND_RECORDING_SAVED msg " + JSON.stringify(payload));
+
+ handleNotification(payload, header.type);
+
+ app.notify({
+ "title": "Band Recording Saved",
+ "text": payload.msg,
+ "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
+ }, {
+ "ok_text": "LISTEN",
+ "ok_callback": listenToRecording,
+ "ok_callback_args": {
+ "recording_id": payload.recording_id,
+ "notification_id": payload.notification_id
+ }
+ });
+ });
+ }
+
+
+ function registerRecordingMasterMixComplete() {
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.RECORDING_MASTER_MIX_COMPLETE, function(header, payload) {
+ logger.debug("Handling RECORDING_MASTER_MIX_COMPLETE msg " + JSON.stringify(payload));
+
+ handleNotification(payload, header.type);
+
+ app.notify({
+ "title": "Recording Master Mix Complete",
+ "text": payload.msg,
+ "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
+ }, {
+ "ok_text": "SHARE",
+ "ok_callback": shareRecording,
+ "ok_callback_args": {
+ "recording_id": payload.recording_id
+ }
+ });
+ });
+ }
+
+ function shareRecording(args) {
+ var recordingId = args.recording_id;
+ }
+
+ function registerBandInvitation() {
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_INVITATION, function(header, payload) {
+ logger.debug("Handling BAND_INVITATION msg " + JSON.stringify(payload));
+
+ handleNotification(payload, header.type);
+
+ app.notify({
+ "title": "Band Invitation",
+ "text": payload.msg,
+ "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
+ }, {
+ "ok_text": "ACCEPT",
+ "ok_callback": acceptBandInvitation,
+ "ok_callback_args": {
+ "band_invitation_id": payload.band_invitation_id,
+ "band_id": payload.band_id,
+ "notification_id": payload.notification_id
+ }
+ });
+ });
+ }
+
+
+ function registerTextMessage() {
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.TEXT_MESSAGE, function(header, payload) {
+ logger.debug("Handling TEXT_MESSAGE msg " + JSON.stringify(payload));
+
+ textMessageDialog.messageReceived(payload);
+
+ handleNotification(payload, header.type);
+ });
+ }
+
+ function registerBandInvitationAccepted() {
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_INVITATION_ACCEPTED, function(header, payload) {
+ logger.debug("Handling BAND_INVITATION_ACCEPTED msg " + JSON.stringify(payload));
+
+ handleNotification(payload, header.type);
+
+ app.notify({
+ "title": "Band Invitation Accepted",
+ "text": payload.msg,
+ "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
+ });
+ });
+ }
+
+
+ // one important limitation; if the user is focused on an iframe, this will be false
+ // however, if they are doing something with Facebook or the photo picker, this may actually still be desirable
+ function userCanSeeNotifications(payload) {
+ return document.hasFocus() && !app.layout.dialogObscuredNotification(payload);
+ }
+
+ // default handler for incoming notification
+ function handleNotification(payload, type) {
+
+ // on a load of notifications, it is possible to load a very new notification,
+ // and get a websocket notification right after for that same notification,
+ // so we need to protect against such duplicates
+ if($list.find('li[notification-id="' + payload.notification_id + '"]').length > 0) {
+ return false;
+ }
+
+ // add notification to sidebar
+ var template = $notificationTemplate.html();
+ var notificationHtml = context.JK.fillTemplate(template, {
+ notificationId: payload.notification_id,
+ sessionId: payload.session_id,
+ avatar_url: context.JK.resolveAvatarUrl(payload.photo_url),
+ text: payload.msg instanceof jQuery ? payload.msg.html() : payload.msg ,
+ date: $.timeago(payload.created_at)
+ });
+
+ $list.prepend(notificationHtml);
+
+ onNotificationOccurred(payload);
+
+ initializeActions(payload, type);
+
+ return true;
+ }
+
+
+ function onCreateJoinRequest(sessionId) {
+ var joinRequest = {};
+ joinRequest.music_session = sessionId;
+ joinRequest.user = context.JK.currentUserId;
+ rest.createJoinRequest(joinRequest)
+ .done(function(response) {
+
+ }).error(context.JK.app.ajaxError);
+
+ context.JK.app.layout.closeDialog('alert');
+ }
+
+ function approveJoinRequest(args) {
+ rest.updateJoinRequest(args.join_request_id, true)
+ .done(function(response) {
+ deleteNotification(args.notification_id);
+ }).error(app.ajaxError);
+ }
+
+ function rejectJoinRequest(args) {
+ rest.updateJoinRequest(args.join_request_id, false)
+ .done(function(response) {
+ deleteNotification(args.notification_id);
+ }).error(app.ajaxError);
+ }
+
+ function openTerms(args) {
+ var termsDialog = new context.JK.TermsDialog(app, args, onTermsAccepted);
+ termsDialog.initialize();
+ app.layout.showDialog('terms');
+ }
+
+ function onTermsAccepted(args) {
+ deleteNotification(args.notification_id);
+ context.location = '/client#/session/' + args.session_id;
+ }
+
+
+ function openAlert(sessionId) {
+ var alertDialog = new context.JK.AlertDialog(context.JK.app, "YES",
+ "You must be approved to join this session. Would you like to send a request to join?",
+ sessionId, onCreateJoinRequest);
+
+ alertDialog.initialize();
+ context.JK.app.layout.showDialog('alert');
+ }
+
+ function listenToRecording(args) {
+ deleteNotification(args.notification_id);
+ context.JK.popExternalLink('/recordings/' + args.recording_id);
+ }
+
+ function deleteNotificationHandler(evt) {
+ evt.stopPropagation();
+ var notificationId = $(this).attr('notification-id');
+ deleteNotification(notificationId);
+ }
+
+ function initialize(sidebarInstance, textMessageDialogInstance) {
+ sidebar = sidebarInstance;
+ textMessageDialog = textMessageDialogInstance;
+ $panel = $('[layout-id="panelNotifications"]');
+ $expanded = $panel.find('.panel.expanded');
+ $contents = $panel.find('.panelcontents');
+ $count = $panel.find('#sidebar-notification-count');
+ $list = $panel.find('#sidebar-notification-list');
+ $notificationTemplate = $('#template-notification-panel');
+ if($panel.length == 0) throw "notifications panel not found"
+ if($expanded.length == 0) throw "notifications expanded content not found"
+ if($contents.length == 0) throw "notifications contents not found"
+ if($count.length == 0) throw "notifications count element not found";
+ if($list.length == 0) throw "notification list element not found";
+ if($notificationTemplate.length == 0) throw "notification template not found";
+
+ events();
+
+ populate();
+ };
+
+ this.initialize = initialize;
+ this.onNotificationOccurred = onNotificationOccurred;
+ };
+})(window, jQuery);
diff --git a/web/app/assets/javascripts/sessionModel.js b/web/app/assets/javascripts/sessionModel.js
index 4a546a26a..31a057e14 100644
--- a/web/app/assets/javascripts/sessionModel.js
+++ b/web/app/assets/javascripts/sessionModel.js
@@ -111,9 +111,7 @@
// time, for that entire duration you'll still be sending voice data to the other users.
// this may be bad if someone decides to badmouth others in the left-session during this time
logger.debug("calling jamClient.LeaveSession for clientId=" + clientId);
- console.time('jamClient.LeaveSession');
client.LeaveSession({ sessionID: currentSessionId });
- console.timeEnd('jamClient.LeaveSession');
leaveSessionRest(currentSessionId)
.done(function() {
sessionChanged();
diff --git a/web/app/assets/javascripts/sidebar.js b/web/app/assets/javascripts/sidebar.js
index 00807f517..bac8a80cf 100644
--- a/web/app/assets/javascripts/sidebar.js
+++ b/web/app/assets/javascripts/sidebar.js
@@ -9,6 +9,8 @@
var rest = context.JK.Rest();
var invitationDialog = null;
var textMessageDialog = null;
+ var notificationPanel = null;
+ var me = null;
function initializeSearchPanel() {
$('#search_text_type').change(function() {
@@ -88,204 +90,8 @@
}
function initializeNotificationsPanel() {
- // retrieve pending notifications for this user
- var url = "/api/users/" + context.JK.currentUserId + "/notifications"
- $.ajax({
- type: "GET",
- dataType: "json",
- contentType: 'application/json',
- url: url,
- processData: false,
- success: function(response) {
-
- updateNotificationList(response);
-
- // set notification count
- $('#sidebar-notification-count').html(response.length);
- },
- error: app.ajaxError
- });
- }
-
- function updateNotificationList(response) {
- $('#sidebar-notification-list').empty();
-
- $.each(response, function(index, val) {
-
- if(val.description == 'TEXT_MESSAGE') {
- val.formatted_msg = textMessageDialog.formatTextMessage(val.message.substring(0, 200), val.source_user_id, val.source_user.name, val.message.length > 200).html();
- }
-
- // fill in template for Connect pre-click
- var template = $('#template-notification-panel').html();
- var notificationHtml = context.JK.fillTemplate(template, {
- notificationId: val.notification_id,
- sessionId: val.session_id,
- avatar_url: context.JK.resolveAvatarUrl(val.photo_url),
- text: val.formatted_msg,
- date: $.timeago(val.created_at)
- });
-
- $('#sidebar-notification-list').append(notificationHtml);
-
- // val.description contains the notification record's description value from the DB (i.e., type)
- initializeActions(val, val.description);
- });
- }
-
- function initializeActions(payload, type) {
-
- var $notification = $('li[notification-id=' + payload.notification_id + ']');
- var $btnNotificationAction = '#btn-notification-action';
-
- // wire up "x" button to delete notification
- $notification.find('#img-delete-notification').click(deleteNotificationHandler);
-
- // customize action buttons based on notification type
- if (type === context.JK.MessageType.FRIEND_REQUEST) {
- var $action_btn = $notification.find($btnNotificationAction);
- $action_btn.text('ACCEPT');
- $action_btn.click(function() {
- acceptFriendRequest(payload);
- });
- }
-
- else if (type === context.JK.MessageType.FRIEND_REQUEST_ACCEPTED) {
- $notification.find('#div-actions').hide();
- }
-
- else if (type === context.JK.MessageType.NEW_USER_FOLLOWER || type === context.JK.MessageType.NEW_BAND_FOLLOWER) {
- $notification.find('#div-actions').hide();
- }
-
- else if (type === context.JK.MessageType.SESSION_INVITATION) {
- var $action_btn = $notification.find($btnNotificationAction);
- $action_btn.text('JOIN');
- $action_btn.click(function() {
- openTerms(payload);
- });
- }
-
- else if (type === context.JK.MessageType.JOIN_REQUEST) {
- var $action_btn = $notification.find($btnNotificationAction);
- $action_btn.text('APPROVE');
- $action_btn.click(function() {
- approveJoinRequest(payload);
- });
- }
-
- else if (type === context.JK.MessageType.JOIN_REQUEST_APPROVED) {
- var $action_btn = $notification.find($btnNotificationAction);
- $action_btn.text('JOIN');
- $action_btn.click(function() {
- openTerms(payload);
- });
- }
-
- else if (type === context.JK.MessageType.JOIN_REQUEST_REJECTED) {
- $notification.find('#div-actions').hide();
- }
-
- else if (type === context.JK.MessageType.MUSICIAN_SESSION_JOIN || type === context.JK.MessageType.BAND_SESSION_JOIN) {
-
- var actionText = '';
- var callback;
- if (context.JK.currentUserMusician) {
- // user is MUSICIAN; musician_access = TRUE
- if (payload.musician_access) {
- actionText = "JOIN";
- callback = joinSession;
- }
- // user is MUSICIAN; fan_access = TRUE
- else if (payload.fan_access) {
- actionText = "LISTEN";
- callback = listenToSession;
- }
- }
- else {
- // user is FAN; fan_access = TRUE
- if (payload.fan_access) {
- actionText = "LISTEN";
- callback = listenToSession;
- }
- }
-
- var $action_btn = $notification.find($btnNotificationAction);
- $action_btn.text(actionText);
- $action_btn.click(function() {
- callback(payload);
- });
- }
-
- else if (type === context.JK.MessageType.MUSICIAN_RECORDING_SAVED || type === context.JK.MessageType.BAND_RECORDING_SAVED) {
- var $action_btn = $notification.find($btnNotificationAction);
- $action_btn.text('LISTEN');
- $action_btn.click(function() {
- listenToRecording(payload);
- });
- }
-
- else if (type === context.JK.MessageType.RECORDING_MASTER_MIX_COMPLETE) {
- $notification.find('#div-actions').hide();
- context.jamClient.OnDownloadAvailable(); // poke backend, letting it know a download is available
- }
-
- else if (type === context.JK.MessageType.BAND_INVITATION) {
- var $action_btn = $notification.find($btnNotificationAction);
- $action_btn.text('ACCEPT');
- $action_btn.click(function() {
- acceptBandInvitation(payload);
- });
- }
- else if (type === context.JK.MessageType.BAND_INVITATION_ACCEPTED) {
- $notification.find('#div-actions').hide();
- }
- else if (type === context.JK.MessageType.TEXT_MESSAGE) {
- var $action_btn = $notification.find($btnNotificationAction);
- $action_btn.text('REPLY');
- $action_btn.click(function() {
- var userId = $notification.find('.more-text-available').attr('data-sender-id');
- app.layout.showDialog('text-message', { d1: userId });
- });
-
- var moreTextLink = $notification.find('.more-text-available');
- var textMessage = $notification.find('.text-message');
- var clipped_msg = textMessage.attr('data-is-clipped') === 'true';
-
- if(clipped_msg) {
- moreTextLink.text('more').show();
- moreTextLink.click(function(e) {
- var userId = $(this).attr('data-sender-id');
-
- return false;
- });
- }
- else {
- moreTextLink.hide();
- }
- }
- }
-
- function deleteNotificationHandler(evt) {
- evt.stopPropagation();
- var notificationId = $(this).attr('notification-id');
- deleteNotification(notificationId);
- }
-
- function deleteNotification(notificationId) {
- var url = "/api/users/" + context.JK.currentUserId + "/notifications/" + notificationId;
- $.ajax({
- type: "DELETE",
- dataType: "json",
- contentType: 'application/json',
- url: url,
- processData: false,
- success: function(response) {
- $('li[notification-id=' + notificationId + ']').hide();
- decrementNotificationCount();
- },
- error: app.ajaxError
- });
+ notificationPanel = new context.JK.NotificationPanel(app);
+ notificationPanel.initialize(me, textMessageDialog);
}
function initializeChatPanel() {
@@ -334,51 +140,6 @@
$('#sidebar-search-results').height('0px');
}
- function incrementNotificationCount() {
- var count = parseInt($('#sidebar-notification-count').html());
- $('#sidebar-notification-count').html(count + 1);
- }
-
- function decrementNotificationCount() {
- var count = parseInt($('#sidebar-notification-count').html());
- if (count === 0) {
- $('#sidebar-notification-count').html(0);
- }
- else {
- $('#sidebar-notification-count').html(count - 1);
- }
- }
-
- // default handler for incoming notification
- function handleNotification(payload, type) {
-
- // on a load of notifications, it is possible to load a very new notification,
- // and get a websocket notification right after for that same notification,
- // so we need to protect against such duplicates
- if($('#sidebar-notification-list').find('li[notification-id="' + payload.notification_id + '"]').length > 0) {
- return false;
- }
-
- // increment displayed notification count
- incrementNotificationCount();
-
- // add notification to sidebar
- var template = $("#template-notification-panel").html();
- var notificationHtml = context.JK.fillTemplate(template, {
- notificationId: payload.notification_id,
- sessionId: payload.session_id,
- avatar_url: context.JK.resolveAvatarUrl(payload.photo_url),
- text: payload.msg instanceof jQuery ? payload.msg.html() : payload.msg ,
- date: $.timeago(payload.created_at)
- });
-
- $('#sidebar-notification-list').prepend(notificationHtml);
-
- initializeActions(payload, type);
-
- return true;
- }
-
var delay = (function(){
var timer = 0;
return function(callback, ms) {
@@ -436,32 +197,14 @@
// friend notifications
registerFriendUpdate();
- registerFriendRequest();
- registerFriendRequestAccepted();
- registerNewUserFollower();
- registerNewBandFollower();
// session invitations
- registerSessionInvitation();
- registerSessionEnded();
- registerJoinRequest();
- registerJoinRequestApproved();
- registerJoinRequestRejected();
registerSessionJoin();
registerSessionDepart();
- registerMusicianSessionJoin();
- registerBandSessionJoin();
// recording notifications
- registerMusicianRecordingSaved();
- registerBandRecordingSaved();
registerRecordingStarted();
registerRecordingEnded();
- registerRecordingMasterMixComplete();
-
- // band notifications
- registerBandInvitation();
- registerBandInvitationAccepted();
// broadcast notifications
registerSourceUpRequested();
@@ -469,9 +212,6 @@
registerSourceUp();
registerSourceDown();
- // register text messages
- registerTextMessage();
-
// watch for Invite More Users events
$('#sidebar-div .btn-email-invitation').click(function() {
invitationDialog.showEmailDialog();
@@ -505,210 +245,6 @@
});
}
- function registerFriendRequest() {
- context.JK.JamServer.registerMessageCallback(context.JK.MessageType.FRIEND_REQUEST, function(header, payload) {
- logger.debug("Handling FRIEND_REQUEST msg " + JSON.stringify(payload));
-
- handleNotification(payload, header.type);
-
- app.notify({
- "title": "New Friend Request",
- "text": payload.msg,
- "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
- }, {
- "ok_text": "ACCEPT",
- "ok_callback": acceptFriendRequest,
- "ok_callback_args": { "friend_request_id": payload.friend_request_id, "notification_id": payload.notification_id }
- });
- });
- }
-
- function acceptFriendRequest(args) {
-
- rest.acceptFriendRequest({
- status: 'accept',
- friend_request_id: args.friend_request_id
- }).done(function(response) {
- deleteNotification(args.notification_id); // delete notification corresponding to this friend request
- initializeFriendsPanel(); // refresh friends panel when request is accepted
- }).error(app.ajaxError);
- }
-
- function registerFriendRequestAccepted() {
- context.JK.JamServer.registerMessageCallback(context.JK.MessageType.FRIEND_REQUEST_ACCEPTED, function(header, payload) {
- logger.debug("Handling FRIEND_REQUEST_ACCEPTED msg " + JSON.stringify(payload));
-
- handleNotification(payload, header.type);
-
- initializeFriendsPanel();
-
- app.notify({
- "title": "Friend Request Accepted",
- "text": payload.msg,
- "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
- });
- });
- }
-
- function registerNewUserFollower() {
-
- context.JK.JamServer.registerMessageCallback(context.JK.MessageType.NEW_USER_FOLLOWER, function(header, payload) {
- logger.debug("Handling NEW_USER_FOLLOWER msg " + JSON.stringify(payload));
-
- handleNotification(payload, header.type);
-
- app.notify({
- "title": "New Follower",
- "text": payload.msg,
- "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
- });
- });
- }
-
- function registerNewBandFollower() {
-
- context.JK.JamServer.registerMessageCallback(context.JK.MessageType.NEW_BAND_FOLLOWER, function(header, payload) {
- logger.debug("Handling NEW_BAND_FOLLOWER msg " + JSON.stringify(payload));
-
- handleNotification(payload, header.type);
-
- app.notify({
- "title": "New Band Follower",
- "text": payload.msg,
- "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
- });
- });
-
- }
-
- function registerSessionInvitation() {
- context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_INVITATION, function(header, payload) {
- logger.debug("Handling SESSION_INVITATION msg " + JSON.stringify(payload));
-
- handleNotification(payload, header.type);
-
- var participants = [];
- rest.getSession(payload.session_id).done(function(response) {
- $.each(response.participants, function(index, val) {
- participants.push({"photo_url": context.JK.resolveAvatarUrl(val.user.photo_url), "name": val.user.name});
- });
-
- var participantHtml = "You have been invited to join a session with:
";
- participantHtml += "";
-
- $.each(participants, function(index, val) {
- if (index < 4) {
- participantHtml += " + ") | " + val.name + " |
";
- }
- });
-
- participantHtml += "
";
-
- app.notify({
- "title": "Session Invitation",
- "text": participantHtml
- }, {
- "ok_text": "JOIN SESSION",
- "ok_callback": openTerms,
- "ok_callback_args": { "session_id": payload.session_id, "notification_id": payload.notification_id }
- });
- }).error(app.ajaxError);
-
- });
- }
-
- function openTerms(args) {
- var termsDialog = new context.JK.TermsDialog(app, args, onTermsAccepted);
- termsDialog.initialize();
- app.layout.showDialog('terms');
- }
-
- function onTermsAccepted(args) {
- deleteNotification(args.notification_id);
- context.location = '/client#/session/' + args.session_id;
- }
-
- function registerSessionEnded() {
- // TODO: this should clean up all notifications related to this session
- context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_ENDED, function(header, payload) {
- logger.debug("Handling SESSION_ENDED msg " + JSON.stringify(payload));
- deleteSessionNotifications(payload.session_id);
- });
- }
-
- // remove all notifications for this session
- function deleteSessionNotifications(sessionId) {
- $('li[session-id=' + sessionId + ']').hide();
- decrementNotificationCount();
- }
-
- function registerJoinRequest() {
- context.JK.JamServer.registerMessageCallback(context.JK.MessageType.JOIN_REQUEST, function(header, payload) {
- logger.debug("Handling JOIN_REQUEST msg " + JSON.stringify(payload));
-
- handleNotification(payload, header.type);
-
- app.notify({
- "title": "New Join Request",
- "text": payload.msg,
- "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
- }, {
- "ok_text": "APPROVE",
- "ok_callback": approveJoinRequest,
- "ok_callback_args": { "join_request_id": payload.join_request_id, "notification_id": payload.notification_id },
- "cancel_text": "REJECT",
- "cancel_callback": rejectJoinRequest,
- "cancel_callback_args": { "join_request_id": payload.join_request_id, "notification_id": payload.notification_id }
- });
- });
- }
-
- function approveJoinRequest(args) {
- rest.updateJoinRequest(args.join_request_id, true)
- .done(function(response) {
- deleteNotification(args.notification_id);
- }).error(app.ajaxError);
- }
-
- function rejectJoinRequest(args) {
- rest.updateJoinRequest(args.join_request_id, false)
- .done(function(response) {
- deleteNotification(args.notification_id);
- }).error(app.ajaxError);
- }
-
- function registerJoinRequestApproved() {
- context.JK.JamServer.registerMessageCallback(context.JK.MessageType.JOIN_REQUEST_APPROVED, function(header, payload) {
- logger.debug("Handling JOIN_REQUEST_APPROVED msg " + JSON.stringify(payload));
-
- handleNotification(payload, header.type);
-
- app.notify({
- "title": "Join Request Approved",
- "text": payload.msg,
- "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
- }, {
- "ok_text": "JOIN SESSION",
- "ok_callback": openTerms,
- "ok_callback_args": { "session_id": payload.session_id, "notification_id": payload.notification_id }
- });
- });
- }
-
- function registerJoinRequestRejected() {
- context.JK.JamServer.registerMessageCallback(context.JK.MessageType.JOIN_REQUEST_REJECTED, function(header, payload) {
- logger.debug("Handling JOIN_REQUEST_REJECTED msg " + JSON.stringify(payload));
-
- handleNotification(payload, header.type);
-
- app.notify({
- "title": "Join Request Rejected",
- "text": payload.msg,
- "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
- });
- });
- }
-
function registerSessionJoin() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_JOIN, function(header, payload) {
logger.debug("Handling SESSION_JOIN msg " + JSON.stringify(payload));
@@ -741,201 +277,6 @@
});
}
- function registerMusicianSessionJoin() {
- context.JK.JamServer.registerMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_JOIN, function(header, payload) {
- logger.debug("Handling MUSICIAN_SESSION_JOIN msg " + JSON.stringify(payload));
-
- var okText = '';
- var showNotification = false;
- var callback;
- if (context.JK.currentUserMusician) {
- // user is MUSICIAN; musician_access = TRUE
- if (payload.musician_access) {
- showNotification = true;
- okText = "JOIN";
- callback = joinSession;
- }
- // user is MUSICIAN; fan_access = TRUE
- else if (payload.fan_access) {
- showNotification = true;
- okText = "LISTEN";
- callback = listenToSession;
- }
- }
- else {
- // user is FAN; fan_access = TRUE
- if (payload.fan_access) {
- showNotification = true;
- okText = "LISTEN";
- callback = listenToSession;
- }
- }
-
- if (showNotification) {
- handleNotification(payload, header.type);
-
- app.notify({
- "title": "Musician Joined Session",
- "text": payload.msg,
- "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
- }, {
- "ok_text": okText,
- "ok_callback": callback,
- "ok_callback_args": {
- "session_id": payload.session_id,
- "fan_access": payload.fan_access,
- "musician_access": payload.musician_access,
- "approval_required": payload.approval_required,
- "notification_id": payload.notification_id
- }
- }
- );
- }
- });
- }
-
- function registerBandSessionJoin() {
- context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_SESSION_JOIN, function(header, payload) {
- logger.debug("Handling BAND_SESSION_JOIN msg " + JSON.stringify(payload));
-
- var okText = '';
- var showNotification = false;
- var callback;
- if (context.JK.currentUserMusician) {
- // user is MUSICIAN; musician_access = TRUE
- if (payload.musician_access) {
- showNotification = true;
- okText = "JOIN";
- callback = joinSession;
- }
- // user is MUSICIAN; fan_access = TRUE
- else if (payload.fan_access) {
- showNotification = true;
- okText = "LISTEN";
- callback = listenToSession;
- }
- }
- else {
- // user is FAN; fan_access = TRUE
- if (payload.fan_access) {
- showNotification = true;
- okText = "LISTEN";
- callback = listenToSession;
- }
- }
-
- if (showNotification) {
- handleNotification(payload, header.type);
-
- app.notify({
- "title": "Band Joined Session",
- "text": payload.msg,
- "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
- }, {
- "ok_text": "LISTEN",
- "ok_callback": callback,
- "ok_callback_args": {
- "session_id": payload.session_id,
- "fan_access": payload.fan_access,
- "musician_access": payload.musician_access,
- "approval_required": payload.approval_required,
- "notification_id": payload.notification_id
- }
- }
- );
- }
- });
- }
-
- function listenToSession(args) {
- deleteNotification(args.notification_id);
- context.JK.popExternalLink('/sessions/' + args.session_id);
- }
-
- /*********** TODO: THE NEXT 3 FUNCTIONS ARE COPIED FROM sessionList.js. REFACTOR TO COMMON PLACE. *************/
- function joinSession(args) {
- // NOTE: invited musicians get their own notification, so no need to check if user has invitation here
- // like other places because an invited user would never get this notification
- if (args.musician_access) {
- if (args.approval_required) {
- openAlert(args.session_id);
- }
- else {
- openTerms(args);
- }
- }
- deleteNotification(args.notification_id);
- }
-
- function openAlert(sessionId) {
- var alertDialog = new context.JK.AlertDialog(context.JK.app, "YES",
- "You must be approved to join this session. Would you like to send a request to join?",
- sessionId, onCreateJoinRequest);
-
- alertDialog.initialize();
- context.JK.app.layout.showDialog('alert');
- }
-
- function onCreateJoinRequest(sessionId) {
- var joinRequest = {};
- joinRequest.music_session = sessionId;
- joinRequest.user = context.JK.currentUserId;
- rest.createJoinRequest(joinRequest)
- .done(function(response) {
-
- }).error(context.JK.app.ajaxError);
-
- context.JK.app.layout.closeDialog('alert');
- }
- //////////////////////////////////////////////////////////////////////////////////////////
-
- function registerMusicianRecordingSaved() {
- context.JK.JamServer.registerMessageCallback(context.JK.MessageType.MUSICIAN_RECORDING_SAVED, function(header, payload) {
- logger.debug("Handling MUSICIAN_RECORDING_SAVED msg " + JSON.stringify(payload));
-
- handleNotification(payload, header.type);
-
- app.notify({
- "title": "Musician Recording Saved",
- "text": payload.msg,
- "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
- }, {
- "ok_text": "LISTEN",
- "ok_callback": listenToRecording,
- "ok_callback_args": {
- "recording_id": payload.recording_id,
- "notification_id": payload.notification_id
- }
- });
- });
- }
-
- function registerBandRecordingSaved() {
- context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_RECORDING_SAVED, function(header, payload) {
- logger.debug("Handling BAND_RECORDING_SAVED msg " + JSON.stringify(payload));
-
- handleNotification(payload, header.type);
-
- app.notify({
- "title": "Band Recording Saved",
- "text": payload.msg,
- "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
- }, {
- "ok_text": "LISTEN",
- "ok_callback": listenToRecording,
- "ok_callback_args": {
- "recording_id": payload.recording_id,
- "notification_id": payload.notification_id
- }
- });
- });
- }
-
- function listenToRecording(args) {
- deleteNotification(args.notification_id);
- context.JK.popExternalLink('/recordings/' + args.recording_id);
- }
-
function registerRecordingStarted() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.RECORDING_STARTED, function(header, payload) {
logger.debug("Handling RECORDING_STARTED msg " + JSON.stringify(payload));
@@ -960,86 +301,6 @@
});
}
- function registerRecordingMasterMixComplete() {
- context.JK.JamServer.registerMessageCallback(context.JK.MessageType.RECORDING_MASTER_MIX_COMPLETE, function(header, payload) {
- logger.debug("Handling RECORDING_MASTER_MIX_COMPLETE msg " + JSON.stringify(payload));
-
- handleNotification(payload, header.type);
-
- app.notify({
- "title": "Recording Master Mix Complete",
- "text": payload.msg,
- "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
- }, {
- "ok_text": "SHARE",
- "ok_callback": shareRecording,
- "ok_callback_args": {
- "recording_id": payload.recording_id
- }
- });
- });
- }
-
- function shareRecording(args) {
- var recordingId = args.recording_id;
- }
-
- function registerBandInvitation() {
- context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_INVITATION, function(header, payload) {
- logger.debug("Handling BAND_INVITATION msg " + JSON.stringify(payload));
-
- handleNotification(payload, header.type);
-
- app.notify({
- "title": "Band Invitation",
- "text": payload.msg,
- "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
- }, {
- "ok_text": "ACCEPT",
- "ok_callback": acceptBandInvitation,
- "ok_callback_args": {
- "band_invitation_id": payload.band_invitation_id,
- "band_id": payload.band_id,
- "notification_id": payload.notification_id
- }
- });
- });
- }
-
- function acceptBandInvitation(args) {
- rest.updateBandInvitation(
- args.band_id,
- args.band_invitation_id,
- true
- ).done(function(response) {
- deleteNotification(args.notification_id); // delete notification corresponding to this friend request
- }).error(app.ajaxError);
- }
-
- function registerTextMessage() {
- context.JK.JamServer.registerMessageCallback(context.JK.MessageType.TEXT_MESSAGE, function(header, payload) {
- logger.debug("Handling TEXT_MESSAGE msg " + JSON.stringify(payload));
-
- textMessageDialog.messageReceived(payload);
-
- handleNotification(payload, header.type);
- });
- }
-
- function registerBandInvitationAccepted() {
- context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_INVITATION_ACCEPTED, function(header, payload) {
- logger.debug("Handling BAND_INVITATION_ACCEPTED msg " + JSON.stringify(payload));
-
- handleNotification(payload, header.type);
-
- app.notify({
- "title": "Band Invitation Accepted",
- "text": payload.msg,
- "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
- });
- });
- }
-
function registerSourceUpRequested() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SOURCE_UP_REQUESTED, function(header, payload) {
@@ -1146,13 +407,14 @@
}
this.initialize = function(invitationDialogInstance, textMessageDialogInstance) {
+ me = this;
+ invitationDialog = invitationDialogInstance;
+ textMessageDialog = textMessageDialogInstance;
events();
initializeSearchPanel();
initializeFriendsPanel();
initializeChatPanel();
initializeNotificationsPanel();
- invitationDialog = invitationDialogInstance;
- textMessageDialog = textMessageDialogInstance;
};
this.refreshFriends = refreshFriends;
diff --git a/web/app/assets/javascripts/textMessageDialog.js b/web/app/assets/javascripts/textMessageDialog.js
index e76d413ba..3bcc2c5c2 100644
--- a/web/app/assets/javascripts/textMessageDialog.js
+++ b/web/app/assets/javascripts/textMessageDialog.js
@@ -47,8 +47,7 @@
return message;
}
- function sendMessage(e) {
-
+ function sendMessage() {
var msg = $textBox.val();
if(!msg || msg == '') {
// don't bother the server with empty messages
@@ -124,6 +123,15 @@
return markedUpMsg;
}
+ // we handled the notification, meaning the dialog showed this message as a chat message
+ function handledNotification(payload) {
+ return showing && payload.description == "TEXT_MESSAGE" && payload.sender_id == otherId;
+ }
+
+ function afterShow(args) {
+ $textBox.focus();
+ }
+
function beforeShow(args) {
app.layout.closeDialog('text-message') // ensure no others are showing. this is a singleton dialog
@@ -175,13 +183,38 @@
reset();
}
- function postMessage(e) {
+ function pasteIntoInput(el, text) {
+ el.focus();
+ if (typeof el.selectionStart == "number"
+ && typeof el.selectionEnd == "number") {
+ var val = el.value;
+ var selStart = el.selectionStart;
+ el.value = val.slice(0, selStart) + text + val.slice(el.selectionEnd);
+ el.selectionEnd = el.selectionStart = selStart + text.length;
+ } else if (typeof document.selection != "undefined") {
+ var textRange = document.selection.createRange();
+ textRange.text = text;
+ textRange.collapse(false);
+ textRange.select();
+ }
+ }
- return false;
+ function handleEnter(evt) {
+ if (evt.keyCode == 13 && evt.shiftKey) {
+ pasteIntoInput(this, "\n");
+ evt.preventDefault();
+ }
+ else if(evt.keyCode == 13 && !evt.shiftKey){
+ sendMessage();
+ return false;
+ }
}
function events() {
- $form.submit(postMessage)
+ $form.submit(sendMessage)
+
+ // http://stackoverflow.com/questions/6014702/how-do-i-detect-shiftenter-and-generate-a-new-line-in-textarea
+ $textBox.keydown(handleEnter);
}
@@ -234,6 +267,7 @@
function initialize() {
var dialogBindings = {
'beforeShow' : beforeShow,
+ 'afterShow' : afterShow,
'afterHide': afterHide
};
@@ -253,6 +287,7 @@
this.initialize = initialize;
this.messageReceived = messageReceived;
this.formatTextMessage = formatTextMessage;
+ this.handledNotification = handledNotification;
}
return this;
diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js
index 583881cbe..d335768e8 100644
--- a/web/app/assets/javascripts/utils.js
+++ b/web/app/assets/javascripts/utils.js
@@ -153,9 +153,9 @@
$element.bt(text, options);
}
- context.JK.bindHoverEvents = function ($parent) {
+ context.JK.bindHoverEvents = function ($parent) {
- if(!$parent) {
+ if (!$parent) {
$parent = $('body');
}
@@ -328,9 +328,9 @@
}
// creates an array with entries like [{ id: "drums", description: "Drums"}, ]
- context.JK.listInstruments = function() {
+ context.JK.listInstruments = function () {
var instrumentArray = [];
- $.each(context.JK.server_to_client_instrument_map, function(key, val) {
+ $.each(context.JK.server_to_client_instrument_map, function (key, val) {
instrumentArray.push({"id": context.JK.server_to_client_instrument_map[key].client_id, "description": key});
});
return instrumentArray;
@@ -652,22 +652,22 @@
return hasFlash;
}
- context.JK.hasOneConfiguredDevice = function() {
+ context.JK.hasOneConfiguredDevice = function () {
var result = context.jamClient.FTUEGetGoodConfigurationList();
logger.debug("hasOneConfiguredDevice: ", result);
return result.length > 0;
};
- context.JK.getGoodAudioConfigs = function() {
+ context.JK.getGoodAudioConfigs = function () {
var result = context.jamClient.FTUEGetGoodAudioConfigurations();
logger.debug("goodAudioConfigs=%o", result);
return result;
};
- context.JK.getGoodConfigMap = function() {
+ context.JK.getGoodConfigMap = function () {
var goodConfigMap = [];
var goodConfigs = context.JK.getGoodAudioConfigs();
- $.each(goodConfigs, function(index, profileKey) {
+ $.each(goodConfigs, function (index, profileKey) {
var friendlyName = context.jamClient.FTUEGetConfigurationDevice(profileKey);
goodConfigMap.push({key: profileKey, name: friendlyName});
});
@@ -675,12 +675,12 @@
return goodConfigMap;
}
- context.JK.getBadAudioConfigs = function() {
+ context.JK.getBadAudioConfigs = function () {
var badAudioConfigs = [];
var allAudioConfigs = context.jamClient.FTUEGetAllAudioConfigurations();
var goodAudioConfigs = context.JK.getGoodAudioConfigs();
- for (var i=0; i < allAudioConfigs.length; i++) {
+ for (var i = 0; i < allAudioConfigs.length; i++) {
if ($.inArray(allAudioConfigs[i], goodAudioConfigs) === -1) {
badAudioConfigs.push(allAudioConfigs[i]);
}
@@ -689,10 +689,10 @@
return badAudioConfigs;
};
- context.JK.getBadConfigMap = function() {
+ context.JK.getBadConfigMap = function () {
var badConfigMap = [];
var badConfigs = context.JK.getBadAudioConfigs();
- $.each(badConfigs, function(index, profileKey) {
+ $.each(badConfigs, function (index, profileKey) {
var friendlyName = context.jamClient.FTUEGetConfigurationDevice(profileKey);
badConfigMap.push({key: profileKey, name: friendlyName});
});
@@ -700,7 +700,7 @@
return badConfigMap;
}
- context.JK.getFirstGoodDevice = function(preferredDeviceId) {
+ context.JK.getFirstGoodDevice = function (preferredDeviceId) {
var badConfigs = context.JK.getBadAudioConfigs();
function getGoodDevice() {
@@ -713,7 +713,7 @@
}
return deviceId;
}
-
+
var deviceId = null;
if (preferredDeviceId) {
@@ -724,7 +724,7 @@
}
else {
deviceId = getGoodDevice();
- }
+ }
}
else {
deviceId = getGoodDevice();
@@ -733,15 +733,22 @@
}
// returns /client#/home for http://www.jamkazam.com/client#/home
- context.JK.locationPath = function() {
+ context.JK.locationPath = function () {
var bits = context.location.href.split('/');
return '/' + bits.slice(3).join('/');
}
- context.JK.nowUTC = function() {
+ context.JK.nowUTC = function () {
var d = new Date();
- return new Date( d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() );
+ return new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds());
}
+
+ context.JK.guardAgainstBrowser = function(app) {
+ if(!gon.isNativeClient) {
+ app.layout.showDialog('launch-app-dialog')
+ }
+ }
+
/*
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
* Digest Algorithm, as defined in RFC 1321.
diff --git a/web/app/assets/javascripts/web/downloads.js b/web/app/assets/javascripts/web/downloads.js
index 1c3976f90..82ef0a1da 100644
--- a/web/app/assets/javascripts/web/downloads.js
+++ b/web/app/assets/javascripts/web/downloads.js
@@ -78,8 +78,8 @@
var clicked = $(this);
var href = clicked.attr('href');
if(href != "#") {
+ context.JK.GA.trackDownload(clicked.attr('data-platform'));
rest.userDownloadedClient().always(function() {
- context.JK.GA.trackDownload(clicked.attr('data-platform'));
$('body').append('')
});
}
diff --git a/web/app/assets/javascripts/web/web.js b/web/app/assets/javascripts/web/web.js
index e8abf7d8b..4b0f88bdd 100644
--- a/web/app/assets/javascripts/web/web.js
+++ b/web/app/assets/javascripts/web/web.js
@@ -13,6 +13,8 @@
//= require jquery.dotdotdot
//= require jquery.listenbroadcast
//= require jquery.listenRecording
+//= require jquery.browser
+//= require jquery.custom-protocol
//= require jquery.ba-bbq
//= require AAA_Log
//= require AAC_underscore
diff --git a/web/app/assets/javascripts/web/welcome.js b/web/app/assets/javascripts/web/welcome.js
index 0d72c6a76..2403fa2dc 100644
--- a/web/app/assets/javascripts/web/welcome.js
+++ b/web/app/assets/javascripts/web/welcome.js
@@ -61,17 +61,6 @@
});
$('.carousel').show()
-
- $.each($('.feed-entry'), function (index, feedEntry) {
- var $feedEntry = $(this);
- if ($feedEntry.is('.recording-entry')) {
- new context.JK.FeedItemRecording($feedEntry);
- }
- else {
- new context.JK.FeedItemSession($feedEntry);
- }
- })
-
context.JK.TickDuration('.feed-entry.music-session-history-entry .inprogress .tick-duration');
if ($.QueryString['showVideo']) {
diff --git a/web/app/assets/stylesheets/client/client.css b/web/app/assets/stylesheets/client/client.css
index 8f2bf9049..e69b9848b 100644
--- a/web/app/assets/stylesheets/client/client.css
+++ b/web/app/assets/stylesheets/client/client.css
@@ -43,6 +43,7 @@
*= require ./leaveSessionWarning
*= require ./textMessageDialog
*= require ./acceptFriendRequestDialog
+ *= require ./launchAppDialog
*= require ./terms
*= require ./createSession
*= require ./feed
diff --git a/web/app/assets/stylesheets/client/dialog.css.scss b/web/app/assets/stylesheets/client/dialog.css.scss
index 438725f43..06e8ca2ba 100644
--- a/web/app/assets/stylesheets/client/dialog.css.scss
+++ b/web/app/assets/stylesheets/client/dialog.css.scss
@@ -8,6 +8,11 @@
min-width: 400px;
min-height: 350px;
z-index: 100;
+
+ h2 {
+ font-size:20px;
+ font-weight:300;
+ }
}
.thin-dialog {
diff --git a/web/app/assets/stylesheets/client/ftue.css.scss b/web/app/assets/stylesheets/client/ftue.css.scss
index fa37a754e..a5f6796de 100644
--- a/web/app/assets/stylesheets/client/ftue.css.scss
+++ b/web/app/assets/stylesheets/client/ftue.css.scss
@@ -604,7 +604,7 @@ table.audiogeartable {
left:10px;
width:150px;
height:18px;
- background-image:url(../images/content/bkg_slider_gain_horiz.png);
+ background-image:url(/assets/content/bkg_slider_gain_horiz.png);
background-repeat:repeat-x;
}
diff --git a/web/app/assets/stylesheets/client/hoverBubble.css.scss b/web/app/assets/stylesheets/client/hoverBubble.css.scss
index 61eff474b..5016310ac 100644
--- a/web/app/assets/stylesheets/client/hoverBubble.css.scss
+++ b/web/app/assets/stylesheets/client/hoverBubble.css.scss
@@ -1,76 +1,77 @@
.bubble {
- width:350px;
- min-height:200px;
- background-color:#242323;
- border:solid 1px #ed3618;
- position:absolute;
- z-index:999;
-}
+ width:350px;
+ min-height:200px;
+ background-color:#242323;
+ border:solid 1px #ed3618;
+ position:absolute;
+ z-index:999;
-.bubble.musician-bubble {
- width:410px;
-}
-.bubble h2 {
- padding:6px 0px;
- text-align:center;
- font-size:15px;
- font-weight:200;
- width:100%;
- background-color:#ed3618;
-}
+ &.musician-bubble {
+ width:410px;
+ }
-.bubble h3 {
- font-weight:400;
- font-size:16px;
- color:#fff;
-}
-
-.bubble-inner {
- padding:10px;
- color:#ccc;
-}
-
-.bubble-inner div.mb {
- margin-bottom:5px;
-}
-
-strong {
- font-weight:600 !important;
-}
-
-.musicians {
- margin-top:-3px;
- font-size:11px;
-}
-
-.musicians td {
- border-right:none;
- border-top:none;
- padding:3px;
- vertical-align:middle;
-}
-
-.musicians a {
- color:#fff;
- text-decoration:none;
-}
-
-.avatar-tiny {
- float:left;
- padding:1px;
- width:24px;
- height:24px;
+ h2 {
+ padding:6px 0px;
+ text-align:center;
+ font-size:15px;
+ font-weight:200;
+ width:100%;
background-color:#ed3618;
- -webkit-border-radius:12px;
- -moz-border-radius:12px;
- border-radius:12px;
-}
+ }
-.avatar-tiny img {
- width: 24px;
- height: 24px;
- -webkit-border-radius:12px;
- -moz-border-radius:12px;
- border-radius:12px;
+ h3 {
+ font-weight:400;
+ font-size:16px;
+ color:#fff;
+ }
+
+ .bubble-inner {
+ padding:10px;
+ color:#ccc;
+ }
+
+ .bubble-inner div.mb {
+ margin-bottom:5px;
+ }
+
+ strong {
+ font-weight:600 !important;
+ }
+
+ .musicians {
+ margin-top:-3px;
+ font-size:11px;
+ }
+
+ .musicians td {
+ border-right:none;
+ border-top:none;
+ padding:3px;
+ vertical-align:middle;
+ }
+
+ .musicians a {
+ color:#fff;
+ text-decoration:none;
+ }
+
+ .avatar-tiny {
+ float:left;
+ padding:1px;
+ width:24px;
+ height:24px;
+ background-color:#ed3618;
+ -webkit-border-radius:12px;
+ -moz-border-radius:12px;
+ border-radius:12px;
+ }
+
+ .avatar-tiny img {
+ width: 24px;
+ height: 24px;
+ -webkit-border-radius:12px;
+ -moz-border-radius:12px;
+ border-radius:12px;
+ }
}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/client/launchAppDialog.css.scss b/web/app/assets/stylesheets/client/launchAppDialog.css.scss
new file mode 100644
index 000000000..c05bb2313
--- /dev/null
+++ b/web/app/assets/stylesheets/client/launchAppDialog.css.scss
@@ -0,0 +1,16 @@
+#launch-app-dialog {
+ min-height:150px;
+
+ .dont-see-it {
+ display:none;
+ }
+
+ p {
+ margin:10px 0;
+ line-height:1em;
+ }
+
+ .buttons {
+ margin:20px 0;
+ }
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/client/shareDialog.css.scss b/web/app/assets/stylesheets/client/shareDialog.css.scss
index dee33c55e..74d095e53 100644
--- a/web/app/assets/stylesheets/client/shareDialog.css.scss
+++ b/web/app/assets/stylesheets/client/shareDialog.css.scss
@@ -219,7 +219,7 @@
.share-message {
width: 100%;
- padding:0;
+ padding:4px;
}
.error-msg {
diff --git a/web/app/assets/stylesheets/client/sidebar.css.scss b/web/app/assets/stylesheets/client/sidebar.css.scss
index 4234bc5e3..9134f21b1 100644
--- a/web/app/assets/stylesheets/client/sidebar.css.scss
+++ b/web/app/assets/stylesheets/client/sidebar.css.scss
@@ -42,6 +42,10 @@
-webkit-border-radius:50%;
-moz-border-radius:50%;
border-radius:50%;
+
+ &.highlighted {
+ background-color:white;
+ }
}
.expander {
diff --git a/web/app/assets/stylesheets/client/textMessageDialog.css.scss b/web/app/assets/stylesheets/client/textMessageDialog.css.scss
index 04f837e53..5605adfce 100644
--- a/web/app/assets/stylesheets/client/textMessageDialog.css.scss
+++ b/web/app/assets/stylesheets/client/textMessageDialog.css.scss
@@ -29,6 +29,7 @@
.previous-message-text {
line-height:18px;
+ white-space:pre-line;
}
.previous-message-timestamp {
@@ -52,4 +53,9 @@
width:100%;
height:40px;
}
+
+ .btn-send-text-message {
+ text-align:center;
+ width:50px;
+ }
}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/web/audioWidgets.css.scss b/web/app/assets/stylesheets/web/audioWidgets.css.scss
index ce9b3c251..a21ce84e6 100644
--- a/web/app/assets/stylesheets/web/audioWidgets.css.scss
+++ b/web/app/assets/stylesheets/web/audioWidgets.css.scss
@@ -109,13 +109,43 @@
}
}
-
.feed-entry {
+ position:relative;
+ display:block;
+ white-space:nowrap;
+ min-width:700px;
+ border-bottom:solid 1px #666;
+ max-height:74px;
+ overflow:hidden;
+ margin-top:20px;
+
+ &:nth-child(1) {
+ margin-top:0;
+ }
+
+ &[data-mode="minimal"] {
+ .avatar-small {
+ display:none;
+ }
+ .feed-type-title {
+ display:none;
+ }
+ .recording-controls-holder {
+ width: 65%;
+ float:left;
+ }
+ .name-and-description {
+ margin-left:0;
+ }
+
+ min-width:300px;
+ border-bottom-width:0;
+ }
+
.session-controls, .recording-controls {
display:inline-block;
&.ended {
background-color: #471f18;
-
}
&.inprogress {
@@ -129,6 +159,16 @@
}
}
+ .recording-controls-holder {
+ float:right;
+ width:40%;
+ }
+
+ .name-and-description {
+ float:left;
+ width:30%;
+ margin-left:20px;
+ }
.recording-controls {
.recording-position {
width:70%;
@@ -147,22 +187,6 @@
}
}
}
-}
-
-.feed-entry {
- position:relative;
- display:block;
- white-space:nowrap;
- min-width:700px;
- border-bottom:solid 1px #666;
- max-height:74px;
- overflow:hidden;
- margin-top:20px;
-
- &:nth-child(1) {
- margin-top:0;
- }
-
/**
&.animate-down {
-webkit-transition: max-height height 2s;
diff --git a/web/app/assets/stylesheets/web/welcome.css.scss b/web/app/assets/stylesheets/web/welcome.css.scss
index d16cb7bab..e78d9df34 100644
--- a/web/app/assets/stylesheets/web/welcome.css.scss
+++ b/web/app/assets/stylesheets/web/welcome.css.scss
@@ -242,7 +242,7 @@ Version: 1.1
.carousel .slides .spinner
{
- background : #000 url(loading.gif) no-repeat center; /* video preloader */
+ background : #000 /*url(loading.gif)*/ no-repeat center; /* video preloader */
}
/* _____________________________ *
diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb
index ea3d28c47..5dc55cf9c 100644
--- a/web/app/controllers/api_users_controller.rb
+++ b/web/app/controllers/api_users_controller.rb
@@ -44,6 +44,12 @@ class ApiUsersController < ApiController
@user.show_whats_next = params[:show_whats_next] if params.has_key?(:show_whats_next)
@user.subscribe_email = params[:subscribe_email] if params.has_key?(:subscribe_email)
@user.biography = params[:biography] if params.has_key?(:biography)
+
+ # allow keyword of 'LATEST' to mean set the notification_seen_at to the most recent notification for this user
+ if params.has_key?(:notification_seen_at)
+ @user.update_notification_seen_at params[:notification_seen_at]
+ end
+
@user.save
if @user.errors.any?
diff --git a/web/app/controllers/spikes_controller.rb b/web/app/controllers/spikes_controller.rb
index 682c8dccb..29bc4337b 100644
--- a/web/app/controllers/spikes_controller.rb
+++ b/web/app/controllers/spikes_controller.rb
@@ -21,4 +21,7 @@ class SpikesController < ApplicationController
render :layout => 'web'
end
+ def launch_app
+ render :layout => 'web'
+ end
end
diff --git a/web/app/controllers/users_controller.rb b/web/app/controllers/users_controller.rb
index 348cf7469..9f31e199d 100644
--- a/web/app/controllers/users_controller.rb
+++ b/web/app/controllers/users_controller.rb
@@ -173,12 +173,9 @@ class UsersController < ApplicationController
else
sign_in @user
- if @user.musician
- redirect_to :action => :congratulations_musician, :type => 'Native'
- else
- redirect_to :action => :congratulations_fan, :type => 'Native'
- end
+ destination = @user.musician ? :congratulations_musician : :congratulations_fan
+ redirect_to :action => destination, :type => @user.user_authorization('facebook') ? 'Facebook' : 'Native'
end
end
@@ -215,8 +212,8 @@ class UsersController < ApplicationController
end
# temporary--will go away soon
- @jamfest_2014 = Event.find_by_id('80bb6acf-3ddc-4305-9442-75e6ec047c27')
- @jamfest_2014 = Event.find_by_id('a2dfbd26-9b17-4446-8c61-b67a542ea6ee') unless @jamfest_2014
+ @jamfest_2014 = Event.find_by_id('80bb6acf-3ddc-4305-9442-75e6ec047c27') # production ID
+ @jamfest_2014 = Event.find_by_id('a2dfbd26-9b17-4446-8c61-b67a542ea6ee') unless @jamfest_2014 # development ID
# temporary--end
@welcome_page = true
@@ -271,8 +268,16 @@ class UsersController < ApplicationController
begin
@reset_password_email = params[:jam_ruby_user][:email]
- if @reset_password_email.empty?
- @reset_password_error = "Please enter an email address above"
+ if @reset_password_email.blank?
+ @reset_password_error = "Please enter an email address"
+ render 'request_reset_password', :layout => 'landing'
+ return
+ end
+
+ @reset_password_email.strip!
+
+ unless User::VALID_EMAIL_REGEX.match(@reset_password_email)
+ @reset_password_error = "Please enter a valid email address"
render 'request_reset_password', :layout => 'landing'
return
end
@@ -281,7 +286,6 @@ class UsersController < ApplicationController
render 'sent_reset_password', :layout => 'landing'
rescue JamRuby::JamArgumentError
# Dont tell the user if this error occurred to prevent scraping email addresses.
- #@reset_password_error = "Email address not found"
render 'sent_reset_password', :layout => 'landing'
end
end
diff --git a/web/app/views/api_music_sessions/show.rabl b/web/app/views/api_music_sessions/show.rabl
index c42d940b6..0b1c30540 100644
--- a/web/app/views/api_music_sessions/show.rabl
+++ b/web/app/views/api_music_sessions/show.rabl
@@ -1,4 +1,4 @@
-object @music_session
+ object @music_session
if !current_user
# there should be more data returned, but we need to think very carefully about what data is public for a music session
diff --git a/web/app/views/api_users/show.rabl b/web/app/views/api_users/show.rabl
index 9c1d18422..eff5b3c5c 100644
--- a/web/app/views/api_users/show.rabl
+++ b/web/app/views/api_users/show.rabl
@@ -10,7 +10,8 @@ end
# give back more info if the user being fetched is yourself
if @user == current_user
- attributes :email, :original_fpfile, :cropped_fpfile, :crop_selection, :session_settings, :show_whats_next, :subscribe_email, :auth_twitter
+ attributes :email, :original_fpfile, :cropped_fpfile, :crop_selection, :session_settings, :show_whats_next, :subscribe_email, :auth_twitter, :new_notifications
+
elsif current_user
node :is_friend do |uu|
diff --git a/web/app/views/clients/_createSession.html.erb b/web/app/views/clients/_createSession.html.erb
index 11d39d756..7b6b42b19 100644
--- a/web/app/views/clients/_createSession.html.erb
+++ b/web/app/views/clients/_createSession.html.erb
@@ -139,7 +139,7 @@
diff --git a/web/app/views/clients/_home.html.erb b/web/app/views/clients/_home.html.erb
index 1eba66f1b..c8b834347 100644
--- a/web/app/views/clients/_home.html.erb
+++ b/web/app/views/clients/_home.html.erb
@@ -46,39 +46,50 @@
- <% else %>
-
<% end %>
diff --git a/web/app/views/clients/_launchAppDialog.html.haml b/web/app/views/clients/_launchAppDialog.html.haml
new file mode 100644
index 000000000..92b4482b5
--- /dev/null
+++ b/web/app/views/clients/_launchAppDialog.html.haml
@@ -0,0 +1,47 @@
+.dialog.dialog-overlay-sm{ layout: 'dialog', 'layout-id' => 'launch-app-dialog', id: 'launch-app-dialog'}
+ .content-head
+ = image_tag "content/icon_alert.png", {:width => 24, :height => 24, :class => 'content-icon' }
+ %h1 Application Notice
+ .dialog-inner
+
+%script{type: 'text/template', id: 'template-attempt-launch'}
+ %p To create or find and join a session, you must use the JamKazam application.
+ .right.buttons
+ %a.button-grey.btn-cancel{href:'#', 'layout-action' => 'close'} CANCEL
+ %a.button-orange.btn-launch-app{href:'{{data.launchUrl}}'} LAUNCH APP
+
+%script{type: 'text/template', id: 'template-unsupported-launch'}
+ %p To create or find and join a session, you must use the JamKazam application. Please download and install the application if you have not done so already.
+ .right.buttons
+ %a.button-grey.btn-cancel{href:'#', 'layout-action' => 'close'} CANCEL
+ %a.button-orange.btn-go-to-download-page{href:'/downloads'} GO TO APP DOWNLOAD PAGE
+
+%script{type: 'text/template', id: 'template-launch-successful'}
+ %p
+ The JamKazam application was launched successfully.
+ %a.not-actually-launched{href: '#'} Don't see it?
+ .dont-see-it
+ %p
+ If you do not see the application, it may be minimized. Double-check the
+ %a{href:'{{data.osSpecificSystemTrayLink}}', rel: 'external'} {{data.osSpecificSystemTray}}
+ to see if it's running.
+ %p
+ If the application is not running, then please
+ %a.download-application{href: '/downloads'} download
+ and install the application if you have not done so already, and then start it manually rather than using this web launcher.
+ .right.buttons
+ %a.button-grey.btn-done{href:'#', 'layout-action' => 'close'} DONE
+
+%script{type: 'text/template', id: 'template-launch-unsuccessful'}
+ %h2 The JamKazam application could not be launched.
+ %p
+ If you do not see the application, it may be minimized. Double-check the
+ %a{href:'{{data.osSpecificSystemTrayLink}}', rel: 'external'} {{data.osSpecificSystemTray}}
+ to see if it's running.
+ %p
+ If the application is not running, then please
+ %a.download-application{href: '/downloads'} download
+ and install the application if you have not done so already, and then start it manually rather than using this web launcher.
+ .right.buttons
+ %a.button-grey.btn-done{href:'#', 'layout-action' => 'close'} CLOSE
+ %a.button-orange.btn-go-to-download-page{href:'/downloads'} GO TO APP DOWNLOAD PAGE
diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb
index 68b765856..be7ba434c 100644
--- a/web/app/views/clients/index.html.erb
+++ b/web/app/views/clients/index.html.erb
@@ -51,6 +51,7 @@
<%= render "showServerErrorDialog" %>
<%= render "textMessageDialog" %>
<%= render "acceptFriendRequestDialog" %>
+<%= render "launchAppDialog" %>
<%= render "notify" %>
<%= render "client_update" %>
<%= render "banner" %>
@@ -145,6 +146,9 @@
var friendSelectorDialog = new JK.FriendSelectorDialog(JK.app);
friendSelectorDialog.initialize();
+ var launchAppDialog = new JK.LaunchAppDialog(JK.app);
+ launchAppDialog.initialize();
+
var userDropdown = new JK.UserDropdown(JK.app);
JK.UserDropdown = userDropdown;
userDropdown.initialize(invitationDialog);
diff --git a/web/app/views/events/_event_session.html.haml b/web/app/views/events/_event_session.html.haml
index 3bb2a449f..419dc9bb8 100644
--- a/web/app/views/events/_event_session.html.haml
+++ b/web/app/views/events/_event_session.html.haml
@@ -6,15 +6,19 @@
%span.event-title= event_session_title(event_session)
.landing-details.event
- .left.f20.teal.time
- %strong
- = event_session_start_hour(event_session)
- .right.session-button
- = event_session_button(event_session)
+ - unless event_session.has_public_mixed_recordings?
+ .left.f20.teal.time
+ %strong
+ = event_session_start_hour(event_session)
+ .right.session-button
+ = event_session_button(event_session)
%br{ clear:'all' }
.left.bio
= event_session_description(event_session)
+ .left
+ - event_session.public_mixed_recordings.each do |recording|
+ = render :partial => "users/feed_recording", locals: { feed_item: recording, mode: 'minimal'}
%br
%br
%br{ clear:'all' }
\ No newline at end of file
diff --git a/web/app/views/events/event.html.haml b/web/app/views/events/event.html.haml
index 866dcbdc5..9a9248c6c 100644
--- a/web/app/views/events/event.html.haml
+++ b/web/app/views/events/event.html.haml
@@ -23,7 +23,7 @@
%h2 ARTIST LINEUP
%br
- = render :partial => "event_session", :collection => @event.event_sessions.order('starts_at')
+ = render :partial => "event_session", :collection => @event.event_sessions.order('ordinal, starts_at')
%br{clear:'all'}
diff --git a/web/app/views/layouts/client.html.erb b/web/app/views/layouts/client.html.erb
index ed645fe5b..7797d1dd5 100644
--- a/web/app/views/layouts/client.html.erb
+++ b/web/app/views/layouts/client.html.erb
@@ -29,7 +29,7 @@
<%= render "layouts/social_meta" %>
<% end %>
-
+
<%= yield %>
<%= render "shared/ga" %>
diff --git a/web/app/views/spikes/launch_app.html.haml b/web/app/views/spikes/launch_app.html.haml
new file mode 100644
index 000000000..1542ea3b0
--- /dev/null
+++ b/web/app/views/spikes/launch_app.html.haml
@@ -0,0 +1,28 @@
+- provide(:title, 'Launch App')
+
+
+.content-wrapper
+ %h2 Launch App Test
+
+ %a#try{href:'jamkazam:abc'} Click Here For Mumble
+
+ %br
+
+ %a#try_bad{href:'bumble:abc'} Click Here For Bumble
+
+ %a#hiddenLink{style: 'display:none'} Hidden Link
+
+ #result Result will show here
+
+
+ :javascript
+ $(function () {
+
+ var options = {
+ callback:function(success) {$('#result').text('success=' + success); },
+ fallback:function() {console.log("not supported"); }
+ };
+
+ $('#try').customProtocol(options);
+ $('#try_bad').customProtocol(options);
+ });
\ No newline at end of file
diff --git a/web/app/views/users/_feed_music_session.html.haml b/web/app/views/users/_feed_music_session.html.haml
index c64534191..ff40d20d6 100644
--- a/web/app/views/users/_feed_music_session.html.haml
+++ b/web/app/views/users/_feed_music_session.html.haml
@@ -68,4 +68,8 @@
%br{:clear => "all"}/
- %br/
\ No newline at end of file
+ %br/
+:javascript
+ $(function () {
+ new window.JK.FeedItemSession($('.feed-entry[data-music-session="#{feed_item.id}"]'));
+ })
\ No newline at end of file
diff --git a/web/app/views/users/_feed_recording.html.haml b/web/app/views/users/_feed_recording.html.haml
index 18f1b7b97..dc4051602 100644
--- a/web/app/views/users/_feed_recording.html.haml
+++ b/web/app/views/users/_feed_recording.html.haml
@@ -1,21 +1,24 @@
-.feed-entry.recording-entry{:id => feed_item.id, :'data-claimed-recording-id' => feed_item.candidate_claimed_recording.id}
+/ default values for template
+- mode ||= local_assigns[:mode] = local_assigns.fetch(:mode, 'full')
+
+.feed-entry.recording-entry{:id => feed_item.id, :'data-claimed-recording-id' => feed_item.candidate_claimed_recording.id, :'data-mode' => mode}
/ avatar
.avatar-small.ib
= recording_avatar(feed_item)
/ type and artist
- .left.ml20.w15
+ .left.ml20.w15.feed-type-title
.title{hoveraction: 'recording', :'recording-id' => feed_item.candidate_claimed_recording.id } RECORDING
.artist
= recording_artist_name(feed_item)
= timeago(feed_item.created_at, class: 'small created_at')
/ name and description
- .left.ml20.w30
+ .name-and-description
.name.dotdotdot
= recording_name(feed_item)
.description.dotdotdot
= recording_description(feed_item)
/ timeline and controls
- .right.w40
+ .recording-controls-holder
/ recording play controls
.recording-controls{ class: feed_item.candidate_claimed_recording.has_mix? ? 'has-mix' : 'no-mix'}
/ play button
@@ -83,3 +86,7 @@
%br{:clear => "all"}/
%br/
+:javascript
+ $(function () {
+ new window.JK.FeedItemRecording($('.feed-entry[data-claimed-recording-id="#{feed_item.candidate_claimed_recording.id}"]'));
+ })
diff --git a/web/app/views/users/sent_reset_password.erb b/web/app/views/users/sent_reset_password.erb
index cdc2da0d6..dda0f3f55 100644
--- a/web/app/views/users/sent_reset_password.erb
+++ b/web/app/views/users/sent_reset_password.erb
@@ -10,14 +10,15 @@
- Password successfully reset for email: <%= params[:jam_ruby_user][:email] %>
+ Please check your email at <%= params[:jam_ruby_user][:email] %> and click the link in the email to set a new password.
- Please check your email and click the URL to set a new password.
+ If no email arrives at <%= params[:jam_ruby_user][:email] %>, you may not be entering the email address that you used when signing up with the service.
+
diff --git a/web/app/views/users/welcome.html.haml b/web/app/views/users/welcome.html.haml
index f2d987044..6624039b7 100644
--- a/web/app/views/users/welcome.html.haml
+++ b/web/app/views/users/welcome.html.haml
@@ -9,8 +9,8 @@
- content_for :after_black_bar do
- if @jamfest_2014
.jamfest{style: 'top:-70px;position:relative'}
- %a{ href: event_path(@jamfest_2014.slug), style: 'font-size:24px' }
- Join us online March 12 for Virtual Jam Fest!
+ %a{ href: event_path(@jamfest_2014.slug), style: 'font-size:20px;margin-top:11px' }
+ Listen to the terrific band performances from Virtual Jam Fest 2014!
%div{style: "padding-top:20px;"}
.right
= render :partial => "buzz"
diff --git a/web/config/environments/development.rb b/web/config/environments/development.rb
index 188a87ed9..017f717a1 100644
--- a/web/config/environments/development.rb
+++ b/web/config/environments/development.rb
@@ -68,8 +68,8 @@ SampleApp::Application.configure do
# it's nice to have even admin accounts (which all the default ones are) generate GA data for testing
config.ga_suppress_admin = false
- config.websocket_gateway_connect_time_stale = 12 # 12 matches production
- config.websocket_gateway_connect_time_expire = 20 # 20 matches production
+ config.websocket_gateway_connect_time_stale = 12
+ config.websocket_gateway_connect_time_expire = 20
config.audiomixer_path = ENV['AUDIOMIXER_PATH'] || audiomixer_workspace_path || "/var/lib/audiomixer/audiomixer/audiomixerapp"
diff --git a/web/config/initializers/resque_mailer.rb b/web/config/initializers/resque_mailer.rb
new file mode 100644
index 000000000..5a9e2bcdb
--- /dev/null
+++ b/web/config/initializers/resque_mailer.rb
@@ -0,0 +1 @@
+Resque::Mailer.excluded_environments = [:test, :cucumber]
diff --git a/web/config/routes.rb b/web/config/routes.rb
index eb4d82e4b..9f95134a5 100644
--- a/web/config/routes.rb
+++ b/web/config/routes.rb
@@ -75,6 +75,8 @@ SampleApp::Application.routes.draw do
# route to spike controller (proof-of-concepts)
match '/facebook_invite', to: 'spikes#facebook_invite'
+ match '/launch_app', to: 'spikes#launch_app'
+
# junk pages
match '/help', to: 'static_pages#help'
diff --git a/web/spec/features/event_spec.rb b/web/spec/features/event_spec.rb
index 4ea9dab33..51cd4f599 100644
--- a/web/spec/features/event_spec.rb
+++ b/web/spec/features/event_spec.rb
@@ -115,15 +115,44 @@ describe "Events", :js => true, :type => :feature, :capybara_feature => true, :s
@event_session2.starts_at = 4.hours.ago
@event_session2.save!
visit "/events/so_latency"
- expect(page).to have_css(".landing-band.event[data-event-session=\"#{@event_session2.id}\"]")
+ first(".landing-band.event[data-event-session='#{@event_session2.id}']:nth-child(1)")
# test that it sorts correctly by putting this later event second
@event_session2.starts_at = 4.hours.from_now
@event_session2.save!
visit "/events/so_latency"
- expect(page).to have_css(".landing-band.event[data-event-session=\"#{@event_session.id}\"]")
+ first(".landing-band.event[data-event-session='#{@event_session.id}']:nth-child(1)")
+ # test that it sorts correctly by putting this later event first, because ordinal is specified
+ @event_session2.ordinal = 0
+ @event_session2.save!
+ visit "/events/so_latency"
+ first(".landing-band.event[data-event-session='#{@event_session2.id}']:nth-child(1)")
+ # associate a recording, and verify that the display changes to have no description, but has a recording widget
+ mix = FactoryGirl.create(:mix)
+ mix.recording.music_session_id = music_session_history.id
+ mix.recording.save!
+ visit "/events/so_latency"
+ find(".feed-entry.recording-entry[data-claimed-recording-id='#{mix.recording.candidate_claimed_recording.id}']")
+ # associate a second recording, and verify it's ordered by name
+ mix2 = FactoryGirl.create(:mix)
+ mix2.recording.music_session_id = music_session_history.id
+ mix2.recording.save!
+ mix2.recording.claimed_recordings.length.should == 1
+ mix2.recording.claimed_recordings[0].name = '____AAA'
+ mix2.recording.claimed_recordings[0].save!
+ visit "/events/so_latency"
+ find(".feed-entry.recording-entry[data-claimed-recording-id='#{mix2.recording.candidate_claimed_recording.id}']:nth-child(1)")
+ find(".feed-entry.recording-entry[data-claimed-recording-id='#{mix.recording.candidate_claimed_recording.id}']:nth-child(3)")
+
+ # and do a re-order test
+ mix2.recording.claimed_recordings[0].name = 'zzzzz'
+ mix2.recording.claimed_recordings[0].save!
+ visit "/events/so_latency"
+ find('.feed-entry.recording-entry .name', text:'zzzzz')
+ find(".feed-entry.recording-entry[data-claimed-recording-id='#{mix.recording.candidate_claimed_recording.id}']:nth-child(1)")
+ find(".feed-entry.recording-entry[data-claimed-recording-id='#{mix2.recording.candidate_claimed_recording.id}']:nth-child(3)")
end
end
diff --git a/web/spec/features/home_spec.rb b/web/spec/features/home_spec.rb
index 1585f9c73..b4a529407 100644
--- a/web/spec/features/home_spec.rb
+++ b/web/spec/features/home_spec.rb
@@ -8,6 +8,7 @@ describe "Home Screen", :js => true, :type => :feature, :capybara_feature => tru
Capybara.javascript_driver = :poltergeist
Capybara.current_driver = Capybara.javascript_driver
Capybara.default_wait_time = 10
+ MusicSession.delete_all
end
let(:user) { FactoryGirl.create(:user) }
@@ -137,6 +138,8 @@ describe "Home Screen", :js => true, :type => :feature, :capybara_feature => tru
end
it_behaves_like :has_footer
+ it_behaves_like :create_session_homecard
+ it_behaves_like :find_session_homecard
it_behaves_like :feed_homecard
it_behaves_like :musicians_homecard
it_behaves_like :bands_homecard
diff --git a/web/spec/features/launch_app_spec.rb b/web/spec/features/launch_app_spec.rb
new file mode 100644
index 000000000..e4aaa590c
--- /dev/null
+++ b/web/spec/features/launch_app_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe "Reset Password", :js => true, :type => :feature, :capybara_feature => true do
+
+ subject { page }
+
+ let(:user) { FactoryGirl.create(:user) }
+
+ share_examples_for :launch_not_supported do |options|
+ it "should indicate not supported" do
+ sign_in_poltergeist user
+ visit options[:screen_path]
+ should have_selector('h1', text: 'Application Notice')
+ should have_selector('p', text: 'To create or find and join a session, you must use the JamKazam application. Please download and install the application if you have not done so already.')
+ find('a.btn-go-to-download-page').trigger(:click)
+ find('h3', text: 'SYSTEM REQUIREMENTS:')
+ end
+ end
+
+ share_examples_for :launch_supported do |options|
+ it "should indicate supported" do
+ sign_in_poltergeist user
+ visit options[:screen_path]
+ should have_selector('h1', text: 'Application Notice')
+ should have_selector('p', text: 'To create or find and join a session, you must use the JamKazam application.')
+ should have_selector('.btn-launch-app')
+ find('.btn-launch-app')['href'].start_with?('jamkazam:')
+ end
+ end
+
+ describe "unsupported" do
+ before do
+ # emulate mac safari
+ page.driver.headers = { 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.74.9 (KHTML, like Gecko) Version/7.0.2 Safari/537.74.9'}
+ end
+ it_behaves_like :launch_not_supported, screen_path: '/client#/createSession'
+ it_behaves_like :launch_not_supported, screen_path: '/client#/findSession'
+ end
+
+ describe "supported" do
+ before do
+ # emulate chrome
+ page.driver.headers = { 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36'}
+ end
+ it_behaves_like :launch_supported, screen_path: '/client#/createSession'
+ it_behaves_like :launch_supported, screen_path: '/client#/findSession'
+ end
+end
+
+
diff --git a/web/spec/features/notification_highlighter_spec.rb b/web/spec/features/notification_highlighter_spec.rb
new file mode 100644
index 000000000..d75946605
--- /dev/null
+++ b/web/spec/features/notification_highlighter_spec.rb
@@ -0,0 +1,168 @@
+require 'spec_helper'
+
+describe "Notification Highlighter", :js => true, :type => :feature, :capybara_feature => true do
+
+ subject { page }
+
+ before(:all) do
+ Capybara.javascript_driver = :poltergeist
+ Capybara.current_driver = Capybara.javascript_driver
+ Capybara.default_wait_time = 10
+ end
+
+ let(:user) { FactoryGirl.create(:user) }
+ let(:user2) { FactoryGirl.create(:user) }
+
+
+ shared_examples_for :notification_badge do |options|
+ it "in correct state" do
+ sign_in_poltergeist(user) unless page.has_selector?('h2', 'musicians')
+ badge = find("#{NOTIFICATION_PANEL} .badge", text:options[:count])
+ badge['class'].include?('highlighted').should == options[:highlighted]
+
+ if options[:action] == :click
+ badge.trigger(:click)
+ badge = find("#{NOTIFICATION_PANEL} .badge", text:0)
+ badge['class'].include?('highlighted').should == false
+ end
+
+ end
+ end
+
+
+ describe "user with no notifications" do
+
+ it_behaves_like :notification_badge, highlighted: false, count:0
+
+ describe "and realtime notifications with sidebar closed" do
+ before(:each) do
+ sign_in_poltergeist(user)
+ document_focus
+ user.reload
+ end
+
+ it_behaves_like :notification_badge, highlighted: false, count:0
+
+ describe "sees notification" do
+ before(:each) do
+ notification = Notification.send_text_message("text message", user2, user)
+ notification.errors.any?.should be_false
+ end
+
+ it_behaves_like :notification_badge, highlighted: false, count:0, action: :click
+ end
+
+ describe "document out of focus" do
+ before(:each) do
+ document_blur
+ notification = Notification.send_text_message("text message", user2, user)
+ notification.errors.any?.should be_false
+ end
+
+ it_behaves_like :notification_badge, highlighted: true, count:1, action: :click
+ end
+ end
+
+
+ describe "and realtime notifications with sidebar open" do
+ before(:each) do
+ # generate one message so that count = 1 to start
+ notification = Notification.send_text_message("text message", user2, user)
+ notification.errors.any?.should be_false
+ sign_in_poltergeist(user)
+ document_focus
+ open_notifications
+ badge = find("#{NOTIFICATION_PANEL} .badge", text:'0') # wait for the opening of the sidebar to bring count to 0
+ user.reload
+ end
+
+ it_behaves_like :notification_badge, highlighted: false, count:0
+
+ describe "sees notification" do
+ before(:each) do
+ notification = Notification.send_text_message("text message", user2, user)
+ notification.errors.any?.should be_false
+ find('#notification #ok-button') # wait for notification to show, so that we know the sidebar had a chance to update
+ end
+
+ it_behaves_like :notification_badge, highlighted: false, count:0
+ end
+
+ describe "document out of focus" do
+ before(:each) do
+ document_blur
+ notification = Notification.send_text_message("text message 2", user2, user)
+ notification.errors.any?.should be_false
+ find('#notification #ok-button')
+ end
+
+ it_behaves_like :notification_badge, highlighted: true, count:1
+
+ describe "user comes back" do
+ before(:each) do
+ window_focus
+
+ it_behaves_like :notification_badge, highlighted: false, count:1
+ end
+ end
+ end
+ end
+ end
+
+ describe "user with new notifications" do
+ before(:each) do
+ # create a notification
+ notification = Notification.send_text_message("text message", user2, user)
+ notification.errors.any?.should be_false
+ end
+
+ it_behaves_like :notification_badge, highlighted:true, count:1, action: :click
+
+ describe "user has previously seen notifications" do
+ before(:each) do
+ user.update_notification_seen_at 'LATEST'
+ user.save!
+ end
+
+ it_behaves_like :notification_badge, highlighted: false, count:0, action: :click
+
+ describe "user again has unseen notifications" do
+ before(:each) do
+ # create a notification
+ notification = Notification.send_text_message("text message", user2, user)
+ notification.errors.any?.should be_false
+ end
+
+ it_behaves_like :notification_badge, highlighted:true, count:1, action: :click
+ end
+ end
+ end
+
+
+ describe "user no unseen notifications" do
+ describe "notification occurs in realtime" do
+
+ describe "sidebar is open" do
+ describe "user can see notifications" do
+ it "stays deactivated" do
+
+ end
+ end
+
+ describe "user can not see notifications" do
+ describe "with dialog open" do
+ it "temporarily activates" do
+
+ end
+ end
+
+ describe "with document blurred" do
+ it "temporarily activates" do
+
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/web/spec/features/password_reset_spec.rb b/web/spec/features/password_reset_spec.rb
new file mode 100644
index 000000000..ccf799255
--- /dev/null
+++ b/web/spec/features/password_reset_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe "Reset Password", :js => true, :type => :feature, :capybara_feature => true do
+
+ subject { page }
+
+ before do
+ UserMailer.deliveries.clear
+ visit request_reset_password_path
+ end
+
+ it "shows specific error for empty password" do
+ click_button 'RESET'
+ find('.login-error-msg', text: 'Please enter an email address')
+ UserMailer.deliveries.length.should == 0
+ end
+
+ it "shows specific error for invalid email address" do
+ fill_in "jam_ruby_user_email", with: 'snoozeday'
+ click_button 'RESET'
+ find('.login-error-msg', text: 'Please enter a valid email address')
+ UserMailer.deliveries.length.should == 0
+ end
+
+ it "acts as if success when email of no one in the service" do
+ fill_in "jam_ruby_user_email", with: 'noone_exists_with_this@blah.com'
+ click_button 'RESET'
+ find('span.please-check', text: 'Please check your email at noone_exists_with_this@blah.com and click the link in the email to set a new password.')
+ UserMailer.deliveries.length.should == 0
+ end
+
+ it "acts as if success when email is valid" do
+ user = FactoryGirl.create(:user)
+ fill_in "jam_ruby_user_email", with: user.email
+ click_button 'RESET'
+ find('span.please-check', text: "Please check your email at #{user.email} and click the link in the email to set a new password.")
+ UserMailer.deliveries.length.should == 1
+ end
+
+ it "acts as if success when email is valid (but with extra whitespace)" do
+ user = FactoryGirl.create(:user)
+ fill_in "jam_ruby_user_email", with: user.email + ' '
+ click_button 'RESET'
+ find('span.please-check', text: "Please check your email at #{user.email} and click the link in the email to set a new password.")
+ UserMailer.deliveries.length.should == 1
+ end
+end
diff --git a/web/spec/features/signup_spec.rb b/web/spec/features/signup_spec.rb
index 3b11319a2..f8f223cf4 100644
--- a/web/spec/features/signup_spec.rb
+++ b/web/spec/features/signup_spec.rb
@@ -162,6 +162,30 @@ describe "Signup", :js => true, :type => :feature, :capybara_feature => true do
end
+ describe "signup facebook user" do
+ before do
+ @fb_signup = FactoryGirl.create(:facebook_signup)
+ visit "#{signup_path}?facebook_signup=#{@fb_signup.lookup_id}"
+ find('#jam_ruby_user_first_name')
+ sleep 1 # if I don't do this, first_name and/or last name intermittently fail to fill out
+
+ fill_in "jam_ruby_user[first_name]", with: "Mike"
+ fill_in "jam_ruby_user[last_name]", with: "Jones"
+ fill_in "jam_ruby_user[email]", with: "newuser_fb@jamkazam.com"
+ fill_in "jam_ruby_user[password]", with: "jam123"
+ fill_in "jam_ruby_user[password_confirmation]", with: "jam123"
+ check("jam_ruby_user[instruments][drums][selected]")
+ check("jam_ruby_user[terms_of_service]")
+ click_button "CREATE ACCOUNT"
+ end
+
+ it "success" do
+ page.should have_title("JamKazam")
+ should have_selector('div.tagline', text: "Congratulations!")
+ uri = URI.parse(current_url)
+ "#{uri.path}?#{uri.query}".should == congratulations_musician_path(:type => 'Facebook')
+ end
+ end
def signup_invited_user
visit "#{signup_path}?invitation_code=#{@invited_user.invitation_code}"
find('#jam_ruby_user_first_name')
@@ -239,7 +263,6 @@ describe "Signup", :js => true, :type => :feature, :capybara_feature => true do
end
-
end
end
diff --git a/web/spec/features/text_message_spec.rb b/web/spec/features/text_message_spec.rb
index 601eb7c93..517b135da 100644
--- a/web/spec/features/text_message_spec.rb
+++ b/web/spec/features/text_message_spec.rb
@@ -42,7 +42,7 @@ describe "Text Message", :js => true, :type => :feature, :capybara_feature => tr
notification = Notification.send_text_message("bibbity bobbity boo", @user2, @user1)
notification.errors.any?.should be_false
- open_sidebar
+ open_notifications
# find the notification and click REPLY
find("[layout-id='panelNotifications'] [notification-id='#{notification.id}'] .button-orange", text:'REPLY').trigger(:click)
diff --git a/web/spec/support/client_interactions.rb b/web/spec/support/client_interactions.rb
index 8b4bc2121..a02c12026 100644
--- a/web/spec/support/client_interactions.rb
+++ b/web/spec/support/client_interactions.rb
@@ -1,6 +1,8 @@
# methods here all assume you are in /client
+NOTIFICATION_PANEL = '[layout-id="panelNotifications"]'
+
# enters text into the search sidebar
def site_search(text, options = {})
within('#searchForm') do
@@ -56,11 +58,41 @@ def send_text_message(msg, options={})
end
end
-def open_sidebar
- find('[layout-id="panelNotifications"] .panel-header').trigger(:click)
+def open_notifications
+ find("#{NOTIFICATION_PANEL} .panel-header").trigger(:click)
end
def hover_intent(element)
element.hover
element.hover
+end
+
+# forces document.hasFocus() to return false
+def document_blur
+ page.evaluate_script(%{(function() {
+ // save original
+ if(!window.documentFocus) { window.documentFocus = window.document.hasFocus; }
+
+ window.document.hasFocus = function() {
+ console.log("document.hasFocus() returns false");
+ return false;
+ }
+ })()})
+end
+
+def document_focus
+ page.evaluate_script(%{(function() {
+ // save original
+ if(!window.documentFocus) { window.documentFocus = window.document.hasFocus; }
+
+ window.document.hasFocus = function() {
+ console.log("document.hasFocus() returns true");
+ return true;
+ }
+ })()})
+end
+
+# simulates focus event on window
+def window_focus
+ page.evaluate_script(%{window.jQuery(window).trigger('focus');})
end
\ No newline at end of file
diff --git a/web/spec/support/utilities.rb b/web/spec/support/utilities.rb
index c184dfbb2..8529365e1 100644
--- a/web/spec/support/utilities.rb
+++ b/web/spec/support/utilities.rb
@@ -194,6 +194,7 @@ def create_session(creator = FactoryGirl.create(:user), unique_session_desc = ni
# create session in one client
in_client(creator) do
page.driver.resize(1500, 800) # makes sure all the elements are visible
+ page.driver.headers = { 'User-Agent' => ' JamKazam ' }
sign_in_poltergeist creator
wait_until_curtain_gone
visit "/client#/createSession"
diff --git a/web/vendor/assets/javascripts/jquery.browser.js b/web/vendor/assets/javascripts/jquery.browser.js
new file mode 100644
index 000000000..fdf337586
--- /dev/null
+++ b/web/vendor/assets/javascripts/jquery.browser.js
@@ -0,0 +1,112 @@
+/*!
+ * jQuery Browser Plugin v0.0.6
+ * https://github.com/gabceb/jquery-browser-plugin
+ *
+ * Original jquery-browser code Copyright 2005, 2013 jQuery Foundation, Inc. and other contributors
+ * http://jquery.org/license
+ *
+ * Modifications Copyright 2013 Gabriel Cebrian
+ * https://github.com/gabceb
+ *
+ * Released under the MIT license
+ *
+ * Date: 2013-07-29T17:23:27-07:00
+ */
+
+(function( jQuery, window, undefined ) {
+ "use strict";
+
+ var matched, browser;
+
+ jQuery.uaMatch = function( ua ) {
+ ua = ua.toLowerCase();
+
+ var match = /(opr)[\/]([\w.]+)/.exec( ua ) ||
+ /(chrome)[ \/]([\w.]+)/.exec( ua ) ||
+ /(version)[ \/]([\w.]+).*(safari)[ \/]([\w.]+)/.exec( ua ) ||
+ /(webkit)[ \/]([\w.]+)/.exec( ua ) ||
+ /(opera)(?:.*version|)[ \/]([\w.]+)/.exec( ua ) ||
+ /(msie) ([\w.]+)/.exec( ua ) ||
+ ua.indexOf("trident") >= 0 && /(rv)(?::| )([\w.]+)/.exec( ua ) ||
+ ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( ua ) ||
+ [];
+
+ var platform_match = /(ipad)/.exec( ua ) ||
+ /(iphone)/.exec( ua ) ||
+ /(android)/.exec( ua ) ||
+ /(windows phone)/.exec( ua ) ||
+ /(win)/.exec( ua ) ||
+ /(mac)/.exec( ua ) ||
+ /(linux)/.exec( ua ) ||
+ /(cros)/i.exec( ua ) ||
+ [];
+
+ return {
+ browser: match[ 3 ] || match[ 1 ] || "",
+ version: match[ 2 ] || "0",
+ platform: platform_match[ 0 ] || ""
+ };
+ };
+
+ matched = jQuery.uaMatch( window.navigator.userAgent );
+ browser = {};
+
+ if ( matched.browser ) {
+ browser[ matched.browser ] = true;
+ browser.version = matched.version;
+ browser.versionNumber = parseInt(matched.version);
+ }
+
+ if ( matched.platform ) {
+ browser[ matched.platform ] = true;
+ }
+
+ // These are all considered mobile platforms, meaning they run a mobile browser
+ if ( browser.android || browser.ipad || browser.iphone || browser[ "windows phone" ] ) {
+ browser.mobile = true;
+ }
+
+ // These are all considered desktop platforms, meaning they run a desktop browser
+ if ( browser.cros || browser.mac || browser.linux || browser.win ) {
+ browser.desktop = true;
+ }
+
+ // Chrome, Opera 15+ and Safari are webkit based browsers
+ if ( browser.chrome || browser.opr || browser.safari ) {
+ browser.webkit = true;
+ }
+
+ // IE11 has a new token so we will assign it msie to avoid breaking changes
+ if ( browser.rv )
+ {
+ var ie = "msie";
+
+ matched.browser = ie;
+ browser[ie] = true;
+ }
+
+ // Opera 15+ are identified as opr
+ if ( browser.opr )
+ {
+ var opera = "opera";
+
+ matched.browser = opera;
+ browser[opera] = true;
+ }
+
+ // Stock Android browsers are marked as Safari on Android.
+ if ( browser.safari && browser.android )
+ {
+ var android = "android";
+
+ matched.browser = android;
+ browser[android] = true;
+ }
+
+ // Assign the name and platform variable
+ browser.name = matched.browser;
+ browser.platform = matched.platform;
+
+
+ jQuery.browser = browser;
+})( jQuery, window );
\ No newline at end of file
diff --git a/web/vendor/assets/javascripts/jquery.pulse.js b/web/vendor/assets/javascripts/jquery.pulse.js
new file mode 100644
index 000000000..872c24b31
--- /dev/null
+++ b/web/vendor/assets/javascripts/jquery.pulse.js
@@ -0,0 +1,72 @@
+/*global jQuery*/
+/*jshint curly:false*/
+
+;(function ( $, window) {
+ "use strict";
+
+ var defaults = {
+ pulses : 1,
+ interval : 0,
+ returnDelay : 0,
+ duration : 500
+ };
+
+ $.fn.pulse = function(properties, options, callback) {
+ // $(...).pulse('destroy');
+ var stop = properties === 'destroy';
+
+ if (typeof options === 'function') {
+ callback = options;
+ options = {};
+ }
+
+ options = $.extend({}, defaults, options);
+
+ if (!(options.interval >= 0)) options.interval = 0;
+ if (!(options.returnDelay >= 0)) options.returnDelay = 0;
+ if (!(options.duration >= 0)) options.duration = 500;
+ if (!(options.pulses >= -1)) options.pulses = 1;
+ if (typeof callback !== 'function') callback = function(){};
+
+ return this.each(function () {
+ var el = $(this),
+ property,
+ original = {};
+
+ var data = el.data('pulse') || {};
+ data.stop = stop;
+ el.data('pulse', data);
+
+ for (property in properties) {
+ if (properties.hasOwnProperty(property)) original[property] = el.css(property);
+ }
+
+ var timesPulsed = 0;
+
+ function animate() {
+ if (typeof el.data('pulse') === 'undefined') return;
+ if (el.data('pulse').stop) return;
+ if (options.pulses > -1 && ++timesPulsed > options.pulses) return callback.apply(el);
+ el.animate(
+ properties,
+ {
+ duration : options.duration / 2,
+ complete : function(){
+ window.setTimeout(function(){
+ el.animate(original, {
+ duration : options.duration / 2,
+ complete : function() {
+ window.setTimeout(animate, options.interval);
+ }
+ });
+ },options.returnDelay);
+ }
+ }
+ );
+ }
+
+ animate();
+ });
+ };
+
+})( jQuery, window, document );
diff --git a/websocket-gateway/lib/jam_websockets/router.rb b/websocket-gateway/lib/jam_websockets/router.rb
index 70ee50421..0f3c8b0e0 100644
--- a/websocket-gateway/lib/jam_websockets/router.rb
+++ b/websocket-gateway/lib/jam_websockets/router.rb
@@ -565,9 +565,19 @@ module JamWebsockets
raise SessionError, 'connection state is gone. please reconnect.'
else
Connection.transaction do
- music_session = MusicSession.select(:track_changes_counter).find_by_id(connection.music_session_id) if connection.music_session_id
- track_changes_counter = music_session.track_changes_counter if music_session
+ # send back track_changes_counter if in a session
+ if connection.music_session_id
+ music_session = MusicSession.select(:track_changes_counter).find_by_id(connection.music_session_id)
+ track_changes_counter = music_session.track_changes_counter if music_session
+ end
+
+ # update connection updated_at
connection.touch
+
+ # update user's notification_seen_at field if the heartbeat indicates it saw one
+ # first we try to use the notification id, which should usually exist.
+ # if not, then fallback to notification_seen_at, which is approximately the last time we saw a notification
+ update_notification_seen_at(connection, context, heartbeat)
end
ConnectionManager.active_record_transaction do |connection_manager|
@@ -593,6 +603,34 @@ module JamWebsockets
end
end
+ def update_notification_seen_at(connection, context, heartbeat)
+ notification_id_field = heartbeat.notification_seen if heartbeat.value_for_tag(1)
+ if notification_id_field
+ notification = Notification.find_by_id(notification_id_field)
+ if notification
+ connection.user.notification_seen_at = notification.created_at
+ unless connection.user.save(validate: false)
+ @log.error "unable to update notification_seen_at via id field for client #{context}. errors: #{connection.user.errors.inspect}"
+ end
+ else
+ notification_seen_at_parsed = nil
+ notification_seen_at = heartbeat.notification_seen_at if heartbeat.value_for_tag(2)
+ begin
+ notification_seen_at_parsed = Time.parse(notification_seen_at) if notification_seen_at && notification_seen_at.length > 0
+ rescue Exception => e
+ @log.error "unable to parse notification_seen_at in heartbeat from #{context}. notification_seen_at: #{notification_seen_at}"
+ end
+
+ if notification_seen_at_parsed
+ connection.user.notification_seen_at = notification_seen_at
+ unless connection.user.save(validate: false)
+ @log.error "unable to update notification_seen_at via time field for client #{context}. errors: #{connection.user.errors.inspect}"
+ end
+ end
+ end
+ end
+ end
+
def valid_login(username, password, token, client_id)
if !token.nil? && token != ''