Merge branch 'develop' into hotfix/feed_icon_fix

This commit is contained in:
Brian Smith 2014-04-01 21:31:07 -04:00
commit b17dafd94f
91 changed files with 2733 additions and 969 deletions

View File

@ -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

View File

@ -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

View File

@ -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 %>

View File

@ -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

View File

@ -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

38
db/up/emails.sql Normal file
View File

@ -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;

View File

@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN notification_seen_at TIMESTAMP;

View File

@ -0,0 +1 @@
ALTER TABLE event_sessions ADD COLUMN ordinal INTEGER;

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -0,0 +1,8 @@
require 'resque_mailer'
module JamRuby
class AsyncMailer < ActionMailer::Base
include SendGrid
include Resque::Mailer
end
end

View File

@ -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

View File

@ -0,0 +1 @@
<%= @body %>

View File

@ -0,0 +1 @@
<%= @body %>

View File

@ -0,0 +1 @@
send_batch_email.html.erb

View File

@ -0,0 +1 @@
send_batch_email.text.erb

View File

@ -1,3 +1,3 @@
<% provide(:title, 'Jamkazam Password Changed') %>
You just changed your password at Jamkazam.
You just changed your password at JamKazam.

View File

@ -24,13 +24,11 @@
<tr>
<td align="left"><h1 style="font-size:22px;font-weight:normal;margin-top:0px"><font color="#F34E1C" face="Arial, Helvetica, sans-serif"><%= yield(:title) %></font></h1>
<p><font size="3" color="#AAAAAA" face="Arial, Helvetica, sans-serif"><%= yield %></font></p>
<p><font size="3" color="#AAAAAA" face="Arial, Helvetica, sans-serif"><%= @batch_body ? @batch_body.html_safe : yield %></font></p>
<br>
</td>
</tr>
<% unless @suppress_user_has_account_footer == true %>
<tr>
<td>
@ -39,8 +37,8 @@
<td align="left">
<!-- CALL OUT BOX -->
<p style="margin-top:0px"><font size="2" color="#7FACBA" face="Arial, Helvetica, sans-serif">This email was sent to you because you have an account at <a style="color: #588C98;" href="http://www.jamkazam.com">JamKazam</a>.&nbsp;&nbsp;Click <a style="color: #588C98;" href="http://www.jamkazam.com/client#/account/profile">here to unsubscribe</a> and update your profile settings.
</font></p>
<p style="margin-top:0px"><font size="2" color="#7FACBA" face="Arial, Helvetica, sans-serif">This email was sent to you because you have an account at <a style="color: #588C98;" href="http://www.jamkazam.com">JamKazam</a>.
</td></tr></table>
</td>

View File

@ -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.

View File

@ -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

View File

@ -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 == ''

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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);
};

View File

@ -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

View File

@ -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();

View File

@ -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);

View File

@ -346,6 +346,7 @@
buildQuery();
refreshDisplay();
loadSessions();
context.JK.guardAgainstBrowser(app);
}
function clearResults() {

View File

@ -934,6 +934,7 @@
}
function getNotifications(options) {
if(!options) options = {};
var id = getId(options);
return $.ajax({
type: "GET",

View File

@ -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.");

View File

@ -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 />");
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);

View File

@ -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);

View File

@ -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);
};

View File

@ -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: <br/><br/>";
participantHtml += "<table><tbody>";
$.each(participants, function(index, val) {
if (index < 4) {
participantHtml += "<tr><td><img class='avatar-small' src='" + context.JK.resolveAvatarUrl(val.photo_url) + "' /></td><td>" + val.name + "</td></tr>";
}
});
participantHtml += "</tbody></table>";
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);

View File

@ -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();

View File

@ -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: <br/><br/>";
participantHtml += "<table><tbody>";
$.each(participants, function(index, val) {
if (index < 4) {
participantHtml += "<tr><td><img class='avatar-small' src='" + context.JK.resolveAvatarUrl(val.photo_url) + "' /></td><td>" + val.name + "</td></tr>";
}
});
participantHtml += "</tbody></table>";
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;

View File

@ -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;

View File

@ -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.

View File

@ -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('<iframe class="downloading" src="' + clicked.attr('href') + '" style="display:none"/>')
});
}

View File

@ -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

View File

@ -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']) {

View File

@ -43,6 +43,7 @@
*= require ./leaveSessionWarning
*= require ./textMessageDialog
*= require ./acceptFriendRequestDialog
*= require ./launchAppDialog
*= require ./terms
*= require ./createSession
*= require ./feed

View File

@ -8,6 +8,11 @@
min-width: 400px;
min-height: 350px;
z-index: 100;
h2 {
font-size:20px;
font-weight:300;
}
}
.thin-dialog {

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -219,7 +219,7 @@
.share-message {
width: 100%;
padding:0;
padding:4px;
}
.error-msg {

View File

@ -42,6 +42,10 @@
-webkit-border-radius:50%;
-moz-border-radius:50%;
border-radius:50%;
&.highlighted {
background-color:white;
}
}
.expander {

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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 */
}
/* _____________________________ *

View File

@ -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?

View File

@ -21,4 +21,7 @@ class SpikesController < ApplicationController
render :layout => 'web'
end
def launch_app
render :layout => 'web'
end
end

View File

@ -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

View File

@ -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

View File

@ -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|

View File

@ -139,7 +139,7 @@
<div class="right mt10">
<div>
<a href="#" layout-link="home" class="button-grey">CANCEL</a>
<a href="#" id="btn-create-session" class="button-orange">JAM!</a>
<a href="#" id="btn-create-session" class="button-orange <%= @nativeClient ? '' : 'button-disabled' %>">JAM!</a>
</div>
</div>
</div>

View File

@ -46,39 +46,50 @@
<div class="homebox-info"><!-- free service level --></div>
</div>
</div>
<% else %>
<div layout-grid="2x4" class="grid">
<div layout-grid-position="0,0" layout-grid-rows="1" layout-grid-columns="1"
layout-link="feed" class="homecard feed">
<h2>feed</h2>
<% else %><div layout-grid="2x12" class="grid">
<div type="createSession" layout-grid-position="0,0" layout-grid-rows="1" layout-grid-columns="4"
layout-link="createSession" class="homecard createsession">
<h2>create session</h2>
<div class="homebox-info"><!-- 4 friends online, 2 currently in sessions --></div>
</div>
<div layout-grid-position="1,0" layout-grid-rows="1" layout-grid-columns="2"
layout-link="bands" class="homecard bands">
<h2>bands</h2>
<div class="homebox-info"><!-- 4 friends online, 2 currently in sessions --></div>
</div>
<div type="findSession" layout-grid-position="4,0" layout-grid-rows="1" layout-grid-columns="4"
layout-link="findSession" class="homecard findsession">
<h2>find session</h2>
<div class="homebox-info"><!-- 1 session invitation, 19 public sessions active --></div>
</div>
<div layout-grid-position="3,0" layout-grid-rows="1" layout-grid-columns="1"
layout-link="musicians" class="homecard musicians">
<h2>musicians</h2>
<div class="homebox-info"><!-- 1 session invitation, 19 public sessions active --></div>
</div>
<div layout-grid-position="8,0" layout-grid-rows="1" layout-grid-columns="4"
layout-link="feed" class="homecard feed">
<h2>feed</h2>
<div class="homebox-info"><!-- 5 followers, 3 following --></div>
</div>
<div layout-grid-position="0,1" layout-grid-rows="1" layout-grid-columns="2"
class="homecard profile">
<h2>profile</h2>
<div class="homebox-info"><!-- 4 friends online, 2 currently in sessions --></div>
</div>
<div layout-grid-position="0,1" layout-grid-rows="1" layout-grid-columns="3"
layout-link="musicians" class="homecard musicians">
<h2>musicians</h2>
<div class="homebox-info"><!-- 38 new recordings, 19 sessions open to fans --></div>
</div>
<div layout-grid-position="2,1" layout-grid-rows="1" layout-grid-columns="2"
layout-link="account" class="homecard account">
<h2>account</h2>
<div class="homebox-info"><!-- 5 followers, 3 following --></div>
</div>
<div layout-grid-position="3,1" layout-grid-rows="1" layout-grid-columns="3"
layout-link="bands" class="homecard bands">
<h2>bands</h2>
<div class="homebox-info"><!-- free service level --></div>
</div>
</div>
<div class="homebox-info"><!-- 1 session invitation, 19 public sessions active --></div>
</div>
<div layout-grid-position="6,1" layout-grid-rows="1" layout-grid-columns="3"
class="homecard profile">
<h2>profile</h2>
<div class="homebox-info"><!-- 5 followers, 3 following --></div>
</div>
<div layout-grid-position="9,1" layout-grid-rows="1" layout-grid-columns="3"
layout-link="account" class="homecard account">
<h2>account</h2>
<div class="homebox-info"><!-- free service level --></div>
</div>
</div>
<% end %>
</div>

View File

@ -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

View File

@ -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);

View File

@ -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' }

View File

@ -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'}

View File

@ -29,7 +29,7 @@
<%= render "layouts/social_meta" %>
<% end %>
</head>
<body class="jam">
<body class="jam" data-client-type="<%= @nativeClient ? 'client' : 'browser' %>">
<%= yield %>
<%= render "shared/ga" %>
</body>

View File

@ -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);
});

View File

@ -68,4 +68,8 @@
%br{:clear => "all"}/
%br/
%br/
:javascript
$(function () {
new window.JK.FeedItemSession($('.feed-entry[data-music-session="#{feed_item.id}"]'));
})

View File

@ -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}"]'));
})

View File

@ -10,14 +10,15 @@
<!-- inner wrapper -->
<div class="ftue-inner">
Password successfully reset for email: <%= params[:jam_ruby_user][:email] %>
<span class="please-check">Please check your email at <b><%= params[:jam_ruby_user][:email] %></b> and click the link in the email to set a new password.</span>
<br clear="all" />
<br/>
Please check your email and click the URL to set a new password.
<span class="wrong-email-notice">If no email arrives at <b><%= params[:jam_ruby_user][:email] %></b>, you may not be entering the email address that you used when signing up with the service.</span>
<br clear="all" />
</div>
</div>

View File

@ -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"

View File

@ -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"

View File

@ -0,0 +1 @@
Resque::Mailer.excluded_environments = [:test, :cucumber]

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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"

View File

@ -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 );

View File

@ -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 );

View File

@ -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 != ''