* VRFS-871 - user-to-user messaging complete

This commit is contained in:
Seth Call 2014-03-20 11:53:26 +00:00
parent b783d270b1
commit b04c7bcea5
85 changed files with 1412 additions and 128 deletions

View File

@ -136,4 +136,5 @@ events.sql
cascading_delete_constraints_for_release.sql
events_social_description.sql
fix_broken_cities.sql
notifications_with_text.sql

View File

@ -0,0 +1 @@
ALTER TABLE notifications ADD COLUMN message TEXT;

View File

@ -49,6 +49,9 @@ message ClientMessage {
BAND_INVITATION = 225;
BAND_INVITATION_ACCEPTED = 230;
BAND_SESSION_JOIN = 235;
// text message
TEXT_MESSAGE = 236;
MUSICIAN_SESSION_FRESH = 240;
MUSICIAN_SESSION_STALE = 245;
@ -125,6 +128,9 @@ message ClientMessage {
optional BandInvitationAccepted band_invitation_accepted = 230;
optional BandSessionJoin band_session_join = 235;
// text message
optional TextMessage text_message = 236;
optional MusicianSessionFresh musician_session_fresh = 240;
optional MusicianSessionStale musician_session_stale = 245;
@ -381,6 +387,16 @@ message BandSessionJoin {
optional string created_at = 8;
}
message TextMessage {
optional string photo_url = 1;
optional string sender_name = 2;
optional string sender_id = 3;
optional string msg = 4;
optional string notification_id = 5;
optional string created_at = 6;
optional bool clipped_msg = 7;
}
// route_to: client:
// sent by server to let the rest of the participants know a client has become active again after going stale
message MusicianSessionFresh {

View File

@ -233,6 +233,24 @@
end
end
def text_message(email, sender_id, sender_name, sender_photo_url, message)
subject = "Message from #{sender_name}"
unique_args = {:type => "text_message"}
@note = message
@root_url = APP_CONFIG.external_root_url
@sender_id = sender_id
@sender_name = sender_name
@sender_photo_url = sender_photo_url
sendgrid_category "Notification"
sendgrid_unique_args :type => unique_args[:type]
mail(:to => email, :subject => subject) do |format|
format.text
format.html { render :layout => "from_user_mailer" }
end
end
# def send_notification(email, subject, msg, unique_args)
# @body = msg
# sendgrid_category "Notification"

View File

@ -0,0 +1,8 @@
<% provide(:title, "Message from #{@sender_name}") %>
<% provide(:photo_url, @sender_photo_url) %>
<% content_for :note do %>
<%= @note %>
<p>To reply to this message, <a href="<%= @root_url %>/client#/home/text-message/d1=<%= @sender_id %>">click here</a>.</p>
<% end %>

View File

@ -0,0 +1,3 @@
<%= @sender_name %> says: <%= @note %>
To reply to this message, click here: <%= @root_url %>/client#/home/text-message/d1=<%= @sender_id %>

View File

@ -30,4 +30,7 @@ module NotificationTypes
BAND_INVITATION_ACCEPTED = "BAND_INVITATION_ACCEPTED"
BAND_SESSION_JOIN = "BAND_SESSION_JOIN" # cleared using SESSION_ENDED notification
# general purpose text message
TEXT_MESSAGE = "TEXT_MESSAGE"
end

View File

@ -75,6 +75,8 @@ module ValidationMessages
MUST_BE_A_MUSICIAN = "must be a musician"
CLAIMED_RECORDING_ALREADY_IN_PROGRESS = "already started by someone else"
# notification
DIFFERENT_SOURCE_TARGET = 'can\'t be same as the sender'
# takes either a string/string hash, or a string/array-of-strings|symbols hash,
# and creates a ActiveRecord.errors style object

View File

@ -1,7 +1,14 @@
module JamRuby
# if a bad argument is supplied.
# Why not use the default ruby argument error? Using this one allows us to know our API layer threw this, versus us using some core library incorrectly
# So why not use the default ruby argument error? Using this one allows us to know our API layer threw this, versus us using some core library incorrectly
class JamArgumentError < ArgumentError
attr_accessor :field, :field_message
def initialize(message, field = nil)
@message = message
@field_message = message
@field = field
end
end
end

View File

@ -64,6 +64,8 @@ module JamWebEventMachine
def self.run
Thread.new do
#ActiveRecord::Base.connection.disconnect!
ActiveRecord::Base.establish_connection
run_em
end
end

View File

@ -33,7 +33,7 @@ end
class NoProfanityValidator < ActiveModel::EachValidator
# implement the method called during validation
def validate_each(record, attribute, value)
record.errors[attribute] << 'Cannot contain profanity' if Profanity.is_profane?(value)
record.errors[attribute] << 'cannot contain profanity' if Profanity.is_profane?(value)
end
end

View File

@ -560,6 +560,25 @@ module JamRuby
)
end
# creates the general purpose text message
def text_message(receiver_id, sender_photo_url, sender_name, sender_id, msg, clipped_msg, notification_id, created_at)
text_message = Jampb::TextMessage.new(
:photo_url => sender_photo_url,
:sender_name => sender_name,
:sender_id => sender_id,
:msg => msg,
:clipped_msg => clipped_msg,
:notification_id => notification_id,
:created_at => created_at
)
Jampb::ClientMessage.new(
:type => ClientMessage::Type::TEXT_MESSAGE,
:route_to => USER_TARGET_PREFIX + receiver_id,
:text_message => text_message
)
end
# create a musician fresh session message
def musician_session_fresh(session_id, user_id, username, photo_url)
fresh = Jampb::MusicianSessionFresh.new(

View File

@ -21,7 +21,7 @@ module JamRuby
# ensure recipient is a Musician
user = User.find(user_id)
unless user.musician?
raise JamRuby::JamArgumentError, BAND_INVITATION_FAN_RECIPIENT_ERROR
raise JamRuby::JamArgumentError.new(BAND_INVITATION_FAN_RECIPIENT_ERROR, :receiver)
end
band_invitation.band_id = band_id

View File

@ -17,7 +17,7 @@ module JamRuby
validates :email, format: {with: VALID_EMAIL_REGEX}, :if => lambda { |iu| iu.email_required? }
validates :autofriend, :inclusion => {:in => [nil, true, false]}
validates :invitation_code, :presence => true
validates :note, length: {maximum: 400}, no_profanity: true # 400 == arbitrary.
validates :note, length: {maximum: 1000}, no_profanity: true # 1000 == arbitrary.
validate :one_facebook_invite_per_user, :if => lambda { |iu| iu.facebook_invite? }
validate :valid_personalized_invitation

View File

@ -14,6 +14,14 @@ module JamRuby
belongs_to :recording, :class_name => "JamRuby::Recording", :foreign_key => "recording_id"
validates :target_user, :presence => true
validates :message, length: {minimum: 1, maximum: 400}, no_profanity: true, if: :text_message?
validate :different_source_target, if: :text_message?
def different_source_target
unless target_user_id.nil? || source_user_id.nil?
errors.add(:target_user, ValidationMessages::DIFFERENT_SOURCE_TARGET) if target_user_id == source_user_id
end
end
def index(user_id)
Notification.where(:target_user_id => user_id).limit(50)
@ -42,7 +50,7 @@ module JamRuby
band = Band.find(self.band_id)
end
return self.class.format_msg(self.description, source_user, band)
self.class.format_msg(self.description, source_user, band)
end
# TODO: MAKE ALL METHODS BELOW ASYNC SO THE CLIENT DOESN'T BLOCK ON NOTIFICATION LOGIC
@ -719,6 +727,38 @@ module JamRuby
end
end
def send_text_message(message, sender, receiver)
notification = Notification.new
notification.description = NotificationTypes::TEXT_MESSAGE
notification.message = message
notification.source_user_id = sender.id
notification.target_user_id = receiver.id if receiver
if notification.save
if receiver.online
clip_at = 200
msg_is_clipped = message.length > clip_at
truncated_msg = message[0..clip_at - 1]
msg = @@message_factory.text_message(
receiver.id,
sender.photo_url,
sender.name,
sender.id,
truncated_msg,
msg_is_clipped,
notification.id,
notification.created_date)
@@mq_router.publish_to_user(receiver.id, msg)
else
UserMailer.text_message(receiver.email, sender.id, sender.name, sender.resolved_photo_url, message).deliver
end
end
notification
end
def send_band_invitation(band, band_invitation, sender, receiver)
notification = Notification.new
@ -830,5 +870,11 @@ module JamRuby
@@mq_router.server_publish_to_everyone_in_session(music_session, msg)
end
end
private
def text_message?
description == 'TEXT_MESSAGE'
end
end
end

View File

@ -424,4 +424,12 @@ FactoryGirl.define do
factory :event_session, :class => JamRuby::EventSession do
end
factory :notification, :class => JamRuby::Notification do
factory :notification_text_message do
description 'TEXT_MESSAGE'
message Faker::Lorem.characters(10)
end
end
end

View File

@ -0,0 +1,137 @@
require 'spec_helper'
describe Notification do
before(:each) do
UserMailer.deliveries.clear
end
describe "send_text_message" do
it "success when offline" do
receiver = FactoryGirl.create(:user)
sender = FactoryGirl.create(:user)
message = "Just a test message!"
called_count = 0
MQRouter.any_instance.stub(:publish_to_user) do |receiver_id, msg|
called_count += 1
end
notification = Notification.send_text_message(message, sender, receiver)
notification.errors.any?.should be_false
UserMailer.deliveries.length.should == 1
called_count.should == 0
end
it "success when online" do
receiver = FactoryGirl.create(:user)
receiver_connection = FactoryGirl.create(:connection, user: receiver)
sender = FactoryGirl.create(:user)
message = "Just a test message!"
called_count = 0
saved_msg = nil
MQRouter.any_instance.stub(:publish_to_user) do |receiver_id, msg|
saved_msg = msg
called_count += 1
end
notification = Notification.send_text_message(message, sender, receiver)
notification.errors.any?.should be_false
UserMailer.deliveries.length.should == 0
called_count.should == 1
saved_msg.text_message.msg.should == message
saved_msg.text_message.photo_url.should == ''
saved_msg.text_message.sender_name.should == sender.name
saved_msg.text_message.notification_id.should == notification.id
saved_msg.text_message.created_at = notification.created_date
saved_msg.text_message.clipped_msg.should be_false
end
it "success when online with long message" do
receiver = FactoryGirl.create(:user)
receiver_connection = FactoryGirl.create(:connection, user: receiver)
sender = FactoryGirl.create(:user)
message = "0" * 203 # 200 is clip size
called_count = 0
saved_msg = nil
MQRouter.any_instance.stub(:publish_to_user) do |receiver_id, msg|
saved_msg = msg
called_count += 1
end
notification = Notification.send_text_message(message, sender, receiver)
notification.errors.any?.should be_false
UserMailer.deliveries.length.should == 0
called_count.should == 1
saved_msg.text_message.msg.should == "0" * 200
saved_msg.text_message.photo_url.should == ''
saved_msg.text_message.sender_name.should == sender.name
saved_msg.text_message.notification_id.should == notification.id
saved_msg.text_message.created_at = notification.created_date
saved_msg.text_message.clipped_msg.should be_true
end
it "fails with profanity" do
receiver = FactoryGirl.create(:user)
sender = FactoryGirl.create(:user)
message = "ass"
called_count = 0
MQRouter.any_instance.stub(:publish_to_user) do |receiver_id, msg|
called_count += 1
end
notification = Notification.send_text_message(message, sender, receiver)
notification.errors.any?.should be_true
notification.errors[:message].should == ['cannot contain profanity']
UserMailer.deliveries.length.should == 0
called_count.should == 0
end
it "fails when target is same as receiver" do
receiver = FactoryGirl.create(:user)
sender = FactoryGirl.create(:user)
message = "yo"
called_count = 0
MQRouter.any_instance.stub(:publish_to_user) do |receiver_id, msg|
called_count += 1
end
notification = Notification.send_text_message(message, sender, sender)
notification.errors.any?.should be_true
notification.errors[:target_user].should == [ValidationMessages::DIFFERENT_SOURCE_TARGET]
UserMailer.deliveries.length.should == 0
called_count.should == 0
end
it "fails when there is no message" do
receiver = FactoryGirl.create(:user)
sender = FactoryGirl.create(:user)
message = ''
called_count = 0
MQRouter.any_instance.stub(:publish_to_user) do |receiver_id, msg|
called_count += 1
end
notification = Notification.send_text_message(message, sender, receiver)
notification.errors.any?.should be_true
notification.errors[:message].should == ['is too short (minimum is 1 characters)']
UserMailer.deliveries.length.should == 0
called_count.should == 0
end
end
end

View File

@ -31,6 +31,12 @@ describe "RenderMailers", :slow => true do
it { @filename="password_changed"; UserMailer.password_changed(user).deliver }
it { @filename="updated_email"; UserMailer.updated_email(user).deliver }
it { @filename="updating_email"; UserMailer.updating_email(user).deliver }
describe "has sending user" do
let(:user2) { FactoryGirl.create(:user) }
it { @filename="text_message"; UserMailer.text_message(user.email, user2.id, user2.name, user2.resolved_photo_url, 'Get online!!').deliver }
end
end
describe "InvitedUserMailer emails" do

View File

@ -49,6 +49,9 @@
BAND_INVITATION : "BAND_INVITATION",
BAND_INVITATION_ACCEPTED : "BAND_INVITATION_ACCEPTED",
// text message
TEXT_MESSAGE : "TEXT_MESSAGE",
// broadcast notifications
SOURCE_UP_REQUESTED : "SOURCE_UP_REQUESTED",
SOURCE_DOWN_REQUESTED : "SOURCE_DOWN_REQUESTED",

View File

@ -51,7 +51,7 @@
}
function handleDeleteAudioProfile(audioProfileId) {
console.log("deleting audio profile: " + audioProfileId);
logger.debug("deleting audio profile: " + audioProfileId);
context.jamClient.TrackDeleteProfile(audioProfileId);

View File

@ -209,7 +209,7 @@
function regionListFailure(jqXHR, textStatus, errorThrown) {
if(jqXHR.status == 422) {
console.log("no regions found for country: " + recentUserDetail.country);
logger.debug("no regions found for country: " + recentUserDetail.country);
}
else {
app.ajaxError(arguments);
@ -218,7 +218,7 @@
function cityListFailure(jqXHR, textStatus, errorThrown) {
if(jqXHR.status == 422) {
console.log("no cities found for country/region: " + recentUserDetail.country + "/" + recentUserDetail.state);
logger.debug("no cities found for country/region: " + recentUserDetail.country + "/" + recentUserDetail.state);
}
else {
app.ajaxError(arguments);
@ -410,7 +410,7 @@
}
function updateCityList(selectedCountry, selectedRegion, cityElement) {
console.log("updating city list: selectedCountry %o, selectedRegion %o", selectedCountry, selectedRegion);
logger.debug("updating city list: selectedCountry %o, selectedRegion %o", selectedCountry, selectedRegion);
// only update cities
if (selectedCountry && selectedRegion) {

View File

@ -287,7 +287,7 @@
self.updatingAvatar = true;
renderAvatarSpinner();
console.log("Converting...");
logger.debug("Converting...");
// we convert two times; first we crop to the selected region,
// then we scale to 88x88 (targetCropSize X targetCropSize), which is the largest size we use throughout the site.

View File

@ -28,6 +28,7 @@
//= require jquery.infinitescroll
//= require jquery.hoverIntent
//= require jquery.dotdotdot
//= require AAA_Log
//= require globals
//= require AAB_message_factory
//= require AAC_underscore

View File

@ -284,7 +284,7 @@
self.updatingBandPhoto = true;
renderBandPhotoSpinner();
console.log("Converting...");
logger.debug("Converting...");
// we convert two times; first we crop to the selected region,
// then we scale to 88x88 (targetCropSize X targetCropSize), which is the largest size we use throughout the site.

View File

@ -19,7 +19,7 @@
function cancelUpdate(e) {
if ((e.ctrlKey || e.metaKey) && e.keyCode == 78) {
console.log("update canceled!");
logger.debug("update canceled!");
app.layout.closeDialog('client-update');
app.clientUpdating = false;
}

View File

@ -1,5 +1,6 @@
//= require jquery
//= require jquery.queryparams
//= require AAA_Log
//= require AAC_underscore
//= require globals
//= require jamkazam

View File

@ -35,7 +35,7 @@
function handle_fblogin_response(response) {
console.log("facebook login response: status=" + response.status)
logger.debug("facebook login response: status=" + response.status)
if(response.status == "connected") {
connected = true;

View File

@ -11,6 +11,7 @@
var instrument_logo_map = context.JK.getInstrumentIconMap24();
var did_show_musician_page = false;
var page_num=1, page_count=0;
var textMessageDialog = null;
function loadMusicians(queryString) {
// squelch nulls and undefines
@ -86,27 +87,27 @@
var mTemplate = $('#template-find-musician-row').html();
var fTemplate = $('#template-musician-follow-info').html();
var aTemplate = $('#template-musician-action-btns').html();
var mVals, mm, renderings='';
var mVals, musician, renderings='';
var instr_logos, instr;
var follows, followVals, aFollow;
for (ii=0, len=musicians.length; ii < len; ii++) {
mm = musicians[ii];
if (context.JK.currentUserId === mm.id) {
musician = musicians[ii];
if (context.JK.currentUserId === musician.id) {
// VRFS-294.3 (David) => skip if current user is musician
continue;
}
instr_logos = '';
for (var jj=0, ilen=mm['instruments'].length; jj<ilen; jj++) {
if (mm['instruments'][jj].instrument_id in instrument_logo_map) {
instr = instrument_logo_map[mm['instruments'][jj].instrument_id];
for (var jj=0, ilen=musician['instruments'].length; jj<ilen; jj++) {
if (musician['instruments'][jj].instrument_id in instrument_logo_map) {
instr = instrument_logo_map[musician['instruments'][jj].instrument_id];
}
instr_logos += '<img src="' + instr + '"/>';
}
follows = '';
followVals = {};
for (var jj=0, ilen=mm['followings'].length; jj<ilen; jj++) {
aFollow = mm['followings'][jj];
for (var jj=0, ilen=musician['followings'].length; jj<ilen; jj++) {
aFollow = musician['followings'][jj];
followVals = {
user_id: aFollow.user_id,
musician_name: aFollow.name,
@ -117,27 +118,29 @@
if (2 == jj) break;
}
var actionVals = {
profile_url: "/client#/profile/" + mm.id,
friend_class: 'button-' + (mm['is_friend'] ? 'grey' : 'orange'),
friend_caption: (mm.is_friend ? 'DIS':'')+'CONNECT',
follow_class: 'button-' + (mm['is_following'] ? 'grey' : 'orange'),
follow_caption: (mm.is_following ? 'UN':'')+'FOLLOW',
profile_url: "/client#/profile/" + musician.id,
friend_class: 'button-' + (musician['is_friend'] ? 'grey' : 'orange'),
friend_caption: (musician.is_friend ? 'DIS':'')+'CONNECT',
follow_class: 'button-' + (musician['is_following'] ? 'grey' : 'orange'),
follow_caption: (musician.is_following ? 'UN':'')+'FOLLOW',
message_class: 'button-orange',
message_caption: 'MESSAGE',
button_message: 'button-orange'
};
var musician_actions = context.JK.fillTemplate(aTemplate, actionVals);
mVals = {
avatar_url: context.JK.resolveAvatarUrl(mm.photo_url),
profile_url: "/client#/profile/" + mm.id,
musician_name: mm.name,
musician_location: mm.city + ', ' + mm.state,
avatar_url: context.JK.resolveAvatarUrl(musician.photo_url),
profile_url: "/client#/profile/" + musician.id,
musician_name: musician.name,
musician_location: musician.city + ', ' + musician.state,
instruments: instr_logos,
biography: mm['biography'],
follow_count: mm['follow_count'],
friend_count: mm['friend_count'],
recording_count: mm['recording_count'],
session_count: mm['session_count'],
musician_id: mm['id'],
biography: musician['biography'],
follow_count: musician['follow_count'],
friend_count: musician['friend_count'],
recording_count: musician['recording_count'],
session_count: musician['session_count'],
musician_id: musician['id'],
musician_follow_template: follows,
musician_action_template: musician_actions
};
@ -148,6 +151,7 @@
$('.search-m-friend').on('click', friendMusician);
$('.search-m-follow').on('click', followMusician);
$('.search-m-message').on('click', messageMusician);
context.JK.bindHoverEvents();
}
@ -163,7 +167,7 @@
function clearResults() {
musicians = {};
$('#musician-filter-results').empty();
$('#musician-filter-results .musician-list-result').remove();
page_num = 1;
page_count = 0;
}
@ -217,6 +221,12 @@
});
}
function messageMusician() {
var userId = $(this).parent().data('musician-id');
app.layout.showDialog('text-message', { d1: userId });
return false;
}
function events() {
$('#musician_query_distance').change(refreshDisplay);
$('#musician_instrument').change(refreshDisplay);
@ -228,11 +238,17 @@
page_num += 1;
search();
}
else {
$('#end-of-musician-list').show()
}
}
});
}
function initialize() {
function initialize(textMessageDialogInstance) {
textMessageDialog = textMessageDialogInstance;
var screenBindings = {
'beforeShow': beforeShow,
'afterShow': afterShow

View File

@ -692,7 +692,7 @@
}
function newFtueSave(persist) {
console.log("newFtueSave persist(" + persist + ")")
logger.debug("newFtueSave persist(" + persist + ")")
newFtueUpdateLatencyView('loading');
logger.debug("Calling FTUESave(" + persist + ")");
jamClient.FTUESave(persist);

View File

@ -21,16 +21,16 @@
function switchClientMode(e) {
// ctrl + shift + 0
if(e.ctrlKey && e.shiftKey && e.keyCode == 48) {
console.log("switch client mode!");
logger.debug("switch client mode!");
var act_as_native_client = $.cookie('act_as_native_client');
console.log("currently: " + act_as_native_client);
logger.debug("currently: " + act_as_native_client);
if(act_as_native_client == null || act_as_native_client != "true") {
console.log("forcing act as native client!");
logger.debug("forcing act as native client!");
$.cookie('act_as_native_client', 'true', { expires: 120, path: '/' });
}
else {
console.log("remove act as native client!");
logger.debug("remove act as native client!");
$.removeCookie('act_as_native_client');
}
window.location.reload();

View File

@ -108,11 +108,13 @@
function configureActionButtons(user) {
var btnFriendSelector = "#btnFriend";
var btnFollowSelector = "#btnFollow";
var btnMessageSelector = '#btnMessage';
// if unauthenticated or authenticated user is viewing his own profile
if (!context.JK.currentUserId || context.JK.currentUserId === user.id) {
$(btnFriendSelector, hoverSelector).hide();
$(btnFollowSelector, hoverSelector).hide();
$(btnMessageSelector, hoverSelector).hide();
}
else {
if (user.is_friend) {
@ -125,6 +127,7 @@
$(btnFriendSelector, hoverSelector).hide();
}
}
}
this.hideBubble = function() {

View File

@ -201,9 +201,9 @@
description: '',
actions: [{ name: 'Signup', link: signupUrl }]
};
console.log("facebook feed options:", obj);
logger.debug("facebook feed options:", obj);
function fbFeedDialogCallback(response) {
//console.log("feedback dialog closed: " + response['post_id'])
//logger.debug("feedback dialog closed: " + response['post_id'])
if (response && response['post_id']) {
context.JK.GA.trackServiceInvitations(context.JK.GA.InvitationTypes.facebook, 1);
}

View File

@ -570,7 +570,7 @@
/** check if the server is alive */
function serverHealthCheck(options) {
console.log("serverHealthCheck")
logger.debug("serverHealthCheck")
return $.ajax({
type: "GET",
url: "/api/versioncheck"
@ -901,6 +901,27 @@
});
}
function createTextMessage(options) {
var id = getId(options);
return $.ajax({
type: "POST",
url: '/api/users/' + id + '/notifications',
dataType: "json",
contentType: 'application/json',
data: JSON.stringify(options)
});
}
function getNotifications(options) {
var id = getId(options);
return $.ajax({
type: "GET",
url: '/api/users/' + id + '/notifications?' + $.param(options),
dataType: "json",
contentType: 'application/json'
});
}
function initialize() {
return self;
}
@ -979,6 +1000,8 @@
this.getShareRecording = getShareRecording;
this.tweet = tweet;
this.createFbInviteUrl = createFbInviteUrl;
this.createTextMessage = createTextMessage;
this.getNotifications = getNotifications;
return this;
};

View File

@ -19,6 +19,7 @@
var JamKazam = context.JK.JamKazam = function () {
var app;
var logger = context.JK.logger;
var rest = context.JK.Rest();
var heartbeatInterval = null;
var heartbeatMS = null;
var heartbeatMissedMS = 10000; // if 5 seconds go by and we haven't seen a heartbeat ack, get upset
@ -26,6 +27,7 @@
var lastHeartbeatAckTime = null;
var lastHeartbeatFound = false;
var heartbeatAckCheckInterval = null;
var userDeferred = null;
var opts = {
inClient: true, // specify false if you want the app object but none of the client-oriented features
@ -57,6 +59,10 @@
}
rules[target] = {route: '/' + targetUrl + '/:d?', method: target};
routingContext[target] = fn;
// allow dialogs to take an optional argument
rules[target+'opt'] = {route: '/' + targetUrl + '/:d?/d1:', method: target};
routingContext[target + 'opt'] = fn;
});
routes.context(routingContext);
for (rule in rules) if (rules.hasOwnProperty(rule)) routes.add(rules[rule]);
@ -214,7 +220,7 @@
var errorResponse = JSON.parse(jqXHR.responseText)["errors"];
for (var key in errorResponse) {
var errorsForKey = errorResponse[key];
console.log("key: " + key);
logger.debug("key: " + key);
var prettyKey = context.JK.entityToPrintable[key];
if (!prettyKey) {
prettyKey = key;
@ -296,7 +302,7 @@
if (jqXHR.status == 422) {
var errors = JSON.parse(jqXHR.responseText);
var $errors = context.JK.format_all_errors(errors);
console.log("Unprocessable entity sent from server:", errors)
logger.debug("Unprocessable entity sent from server:", errors)
this.notify({title: title, text: $errors, icon_url: "/assets/content/icon_alert_big.png"})
}
else {
@ -311,7 +317,7 @@
if(bodyIndex > -1) {
text = text.substr(bodyIndex);
}
console.log("html", text);
logger.debug("html", text);
$('#server-error-dialog .error-contents').html(text);
app.layout.showDialog('server-error-dialog')
return false;
@ -356,7 +362,7 @@
context.RouteMap.parse(hash);
}
catch (e) {
console.log("ignoring bogus screen name: %o", hash)
logger.debug("ignoring bogus screen name: %o", hash)
hash = null;
}
@ -369,6 +375,10 @@
context.location = url;
}
// call .done/.fail on this to wait for safe user data
this.user = function() {
return userDeferred;
}
this.unloadFunction = function () {
logger.debug("window.unload function called.");
@ -395,6 +405,8 @@
events();
this.layout.handleDialogState();
userDeferred = rest.getUserDetail();
if (opts.inClient) {
registerLoginAck();
registerHeartbeatAck();
@ -404,6 +416,10 @@
registerDownloadAvailable();
context.JK.FaderHelpers.initialize();
context.window.onunload = this.unloadFunction;
userDeferred.fail(function(jqXHR) {
app.notify({title: "Unable to Load User", text: "You should reload the page."})
});
}
};

View File

@ -1,5 +1,6 @@
//= require jquery
//= require jquery.queryparams
//= require AAA_Log
//= require AAC_underscore
//= require globals
//= require jamkazam

View File

@ -418,7 +418,7 @@
}
function closeDialog(dialog) {
console.log("closing dialog: " + dialog);
logger.debug("closing dialog: " + dialog);
var $dialog = $('[layout-id="' + dialog + '"]');
dialogEvent(dialog, 'beforeHide');
var $overlay = $('.dialog-overlay');
@ -468,7 +468,7 @@
var screen = location.page.substring(1); // remove leading slash
var accepted = screenEvent(currentScreen, 'beforeLeave', {screen:screen, hash: context.location.hash});
if(accepted === false) {
console.log("navigation to " + context.location.hash + " rejected by " + currentScreen);
logger.debug("navigation to " + context.location.hash + " rejected by " + currentScreen);
//resettingHash = true;
// reset the hash to where it just was
context.location.hash = currentHash;
@ -497,6 +497,7 @@
logger.debug("Changing screen to " + currentScreen);
logger.debug("data: ", data);
screenEvent(currentScreen, 'beforeShow', data);
// For now -- it seems we want it open always.
@ -522,7 +523,7 @@
// Show any requested dialog
if ("d" in data) {
showDialog(data.d);
showDialog(data.d, data);
}
}
@ -575,7 +576,7 @@
}
function showDialog(dialog, options) {
if (!dialogEvent(dialog, 'beforeShow')) {
if (!dialogEvent(dialog, 'beforeShow', options)) {
return;
}
var $overlay = $('.dialog-overlay')
@ -593,7 +594,7 @@
var $dialog = $('[layout-id="' + dialog + '"]');
stackDialogs($dialog, $overlay);
$dialog.show();
dialogEvent(dialog, 'afterShow');
dialogEvent(dialog, 'afterShow', options);
}
function centerDialog(dialog) {
@ -845,8 +846,8 @@
return onHashChange(e, postFunction);
}
this.showDialog = function (dialog) {
showDialog(dialog);
this.showDialog = function (dialog, options) {
showDialog(dialog, options);
};
this.close = function (evt) {

View File

@ -48,7 +48,7 @@
});
}
else {
console.log("workin fool: %o", working)
}
}

View File

@ -139,7 +139,7 @@
$playmodeButton.on('ifChecked', function(e) {
var playmode = $(this).val();
console.log("set new playmode", playmode);
logger.debug("set new playmode", playmode);
setPlaybackMode(playmode);
});
@ -160,7 +160,7 @@
}
// at the end of the play, the duration sets to 0, as does currentTime. but isPlaying does not reset to
console.log("currentTimeMs, durationTimeMs", currentTimeMs, durationTimeMs);
logger.debug("currentTimeMs, durationTimeMs", currentTimeMs, durationTimeMs);
if(currentTimeMs == 0 && durationTimeMs == 0) {
if(isPlaying) {
isPlaying = false;
@ -168,7 +168,7 @@
currentTimeMs = playbackDurationMs;
stopPlay();
endReached = true;
console.log("end reached");
logger.debug("end reached");
}
else {
return;

View File

@ -12,6 +12,7 @@
var decrementedFriendCountOnce = false;
var sentFriendRequest = false;
var profileScreen = null;
var textMessageDialog = null;
var instrument_logo_map = context.JK.getInstrumentIconMap24();
@ -110,6 +111,7 @@
$('#btn-profile-edit').show();
$('#btn-add-friend').hide();
$('#btn-follow-user').hide();
$('#btn-message-user').hide();
}
else {
configureFriendFollowersControls();
@ -117,6 +119,7 @@
$('#btn-profile-edit').hide();
$('#btn-add-friend').show();
$('#btn-follow-user').show();
$('#btn-message-user').show();
}
}
@ -151,6 +154,7 @@
// this doesn't need deferred because it's only shown when valid
$('#btn-add-friend').click(handleFriendChange);
$('#btn-follow-user').click(handleFollowingChange);
$('#btn-message-user').click(handleMessageMusician);
}
function handleFriendChange(evt) {
@ -160,6 +164,7 @@
else {
sendFriendRequest(evt);
}
return false;
}
function handleFollowingChange(evt) {
@ -169,6 +174,12 @@
else {
addFollowing();
}
return false;
}
function handleMessageMusician(evt) {
app.layout.showDialog('text-message', { d1: userId });
return false;
}
function sendFriendRequest(evt) {
@ -741,7 +752,8 @@
function bindFavorites() {
}
function initialize() {
function initialize(textMessageDialogInstance) {
textMessageDialog = textMessageDialogInstance;
var screenBindings = {
'beforeShow': beforeShow,
'afterShow': afterShow

View File

@ -154,7 +154,7 @@
timeCallback();
console.log("alert callback", type, text);
logger.debug("alert callback", type, text);
if (type === 2) { // BACKEND_MIXER_CHANGE
logger.debug("BACKEND_MIXER_CHANGE alert. reason:" + text);
@ -904,7 +904,7 @@
lookingForMixers[track.id] = participant.client_id;
trackData.noaudio = true;
if (!(lookingForMixersTimer)) {
console.log("waiting for mixer to show up for track: " + track.id)
logger.debug("waiting for mixer to show up for track: " + track.id)
lookingForMixersTimer = context.setInterval(lookForMixers, 500);
}
}

View File

@ -59,10 +59,10 @@
var jsFunction = "JK.Callbacks.clientPingResponse";
var timeoutFunction = "JK.Callbacks.clientPingTimeout";
console.log("jamClient.TestLatency")
console.time('jamClient.TestLatency');
logger.debug("jamClient.TestLatency")
//console.time('jamClient.TestLatency');
jamClient.TestLatency(clientID, jsFunction, timeoutFunction);
console.timeEnd('jamClient.TestLatency');
//console.timeEnd('jamClient.TestLatency');
});
}

View File

@ -463,7 +463,7 @@
})
}
else {
console.log("no copy-to-clipboard capabilities")
logger.debug("no copy-to-clipboard capabilities")
}
}
}

View File

@ -8,6 +8,7 @@
var friends = [];
var rest = context.JK.Rest();
var invitationDialog = null;
var textMessageDialog = null;
function initializeSearchPanel() {
$('#search_text_type').change(function() {
@ -139,13 +140,17 @@
$.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);
}
// 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,
text: val.formatted_msg.html(),
date: $.timeago(val.created_at)
});
@ -263,6 +268,30 @@
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) {
@ -350,8 +379,13 @@
// default handler for incoming notification
function handleNotification(payload, type) {
var sidebarText;
sidebarText = payload.msg;
// 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();
@ -359,16 +393,18 @@
// add notification to sidebar
var template = $("#template-notification-panel").html();
var notificationHtml = context.JK.fillTemplate(template, {
notificationId: payload.notification_id,
notificationId: payload.notification_id,
sessionId: payload.session_id,
avatar_url: context.JK.resolveAvatarUrl(payload.photo_url),
text: sidebarText,
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(){
@ -461,6 +497,9 @@
registerSourceUp();
registerSourceDown();
// register text messages
registerTextMessage();
// watch for Invite More Users events
$('#sidebar-div .btn-email-invitation').click(function() {
invitationDialog.showEmailDialog();
@ -1000,9 +1039,19 @@
args.band_id,
args.band_invitation_id,
true
).done(function(response) {
).done(function(response) {
deleteNotification(args.notification_id); // delete notification corresponding to this friend request
}).error(app.ajaxError);
}).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() {
@ -1124,13 +1173,14 @@
});
}
this.initialize = function(invitationDialogInstance) {
this.initialize = function(invitationDialogInstance, textMessageDialogInstance) {
events();
initializeSearchPanel();
initializeFriendsPanel();
initializeChatPanel();
initializeNotificationsPanel();
invitationDialog = invitationDialogInstance;
textMessageDialog = textMessageDialogInstance;
};
}
})(window,jQuery);

View File

@ -0,0 +1,259 @@
(function(context,$) {
"use strict";
context.JK = context.JK || {};
context.JK.TextMessageDialog = function(app) {
var logger = context.JK.logger;
var rest = context.JK.Rest();
var $dialog = null;
var $previousMessages = null;
var $previousMessagesScroller = null;
var $sendTextMessage = null;
var $form = null;
var $textBox = null;
var userLookup = null;
var otherId = null;
var offset = 0;
var sendingMessage = false;
var LIMIT = 20;
var showing = false;
var fullyInitialized = false;
var user = null;
var renderQueue = []; // to handle race condition between dialog showing and populated using REST, and messages coming in
var remainingCap = 400;
function reset() {
fullyInitialized = false;
renderQueue = [];
sendingMessage = false;
offset = 0;
userLookup = {};
$previousMessages.empty();
$textBox.val('');
$sendTextMessage.unbind('click');
}
function buildParams() {
return { type: 'TEXT_MESSAGE', receiver: otherId, offset: offset, limit: LIMIT};
}
function buildMessage() {
var message = {};
message['message'] = $textBox.val();
message['receiver'] = otherId;
return message;
}
function sendMessage(e) {
var msg = $textBox.val();
if(!msg || msg == '') {
// don't bother the server with empty messages
return;
}
if(!sendingMessage) {
sendingMessage = true;
$sendTextMessage.text('SENDING...')
rest.createTextMessage(buildMessage())
.done(function() {
$textBox.val('');
renderMessage(msg, user.id, user.name, new Date().toISOString(), true);
})
.fail(function(jqXHR) {
app.notifyServerError(jqXHR, 'Unable to Send Message');
})
.always(function() {
sendingMessage = false;
$sendTextMessage.text('SEND');
})
}
return false;
}
function scrollToBottom(instant) {
$previousMessagesScroller.animate({scrollTop: $previousMessagesScroller[0].scrollHeight}, instant ? 0 : 'slow');
}
function renderMessage(msg, senderId, senderName, sent, append) {
var options = {
msg: msg,
sender: senderId == user.id ? 'me' : senderName,
sent: sent
};
var txt = $(context._.template($('#template-previous-message').html(), options, { variable: 'data' }));
txt.find('.timeago').timeago();
if(append) {
$previousMessages.append(txt);
scrollToBottom();
}
else {
$previousMessages.prepend(txt);
}
}
function drainQueue() {
context._.each(renderQueue, function(msg) {
renderMessage(msg.msg, msg.senderId, msg.senderName, msg.sent, true);
});
renderQueue = [];
}
function formatTextMessage(msg, sender_id, sender_name, clipped_msg) {
var markedUpMsg = $('<span><span class="sender-name"></span> says: <span class="text-message"></span><a href="#" class="more-text-available"></a></span>');
markedUpMsg.find('.sender-name').text(sender_name)
markedUpMsg.find('.text-message').text(clipped_msg ? msg + "... " : msg).attr('data-is-clipped', clipped_msg)
var moreTextLink = markedUpMsg.find('.more-text-available').attr('data-sender-id', sender_id);
if(clipped_msg) {
moreTextLink.text('more').show();
moreTextLink.click(function(e) {
app.layout.showDialog('text-message', {d1: $(this).attr('data-sender-id')});
return false;
});
}
else {
moreTextLink.hide();
}
return markedUpMsg;
}
function beforeShow(args) {
app.user()
.done(function(userDetail) {
user = userDetail;
var other = args.d1;
if(!other) throw "other must be specified in TextMessageDialog"
otherId = other;
app.layout.closeDialog('text-message') // ensure no others are showing. this is a singleton dialog
showing = true;
userLookup[user.id] = user;
rest.getUserDetail({id: otherId})
.done(function(otherUser) {
userLookup[otherUser.id] = otherUser;
$dialog.find('.receiver-name').text(otherUser.name);
$dialog.find('textarea').attr('placeholder', 'enter a message to ' + otherUser.name + '...');
$dialog.find('.offline-tip').text('An email will be sent if ' + otherUser.name + ' is offline');
$sendTextMessage.click(sendMessage);
rest.getNotifications(buildParams())
.done(function(response) {
context._.each(response, function(textMessage) {
renderMessage(textMessage.message, textMessage.source_user_id, userLookup[textMessage.source_user_id].name, textMessage.created_at);
})
scrollToBottom(true);
fullyInitialized = true;
drainQueue();
})
.fail(function(jqXHR) {
app.notifyServerError(jqXHR, 'Unable to Load Conversation')
})
})
.fail(function(jqXHR) {
app.notifyServerError(jqXHR, 'Unable to Load Other User')
})
})
}
function afterHide() {
showing = false;
reset();
}
function postMessage(e) {
return false;
}
function events() {
$form.submit(postMessage)
}
function respondTextInvitation(args) {
app.layout.showDialog('text-message', {d1: args.sender_id}) ;
}
// called from sidebar when messages come in
function messageReceived(payload) {
if(showing && otherId == payload.sender_id) {
if(fullyInitialized) {
renderMessage(payload.msg, payload.sender_id, payload.sender_name, payload.created_at, true);
}
else {
// the dialog caught a message as it was initializing... queue it for later once dialog is showing
renderQueue.push({msg: payload.msg, senderId: payload.sender_id, senderName: payload.sender_name, sent: msg.created_at});
}
}
else {
payload.msg = formatTextMessage(payload.msg, payload.sender_id, payload.sender_name, payload.clipped_msg);
app.notify({
"title": "Message from " + payload.sender_name,
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, {
"ok_text": "REPLY",
"ok_callback": respondTextInvitation,
"ok_callback_args": {
"sender_id": payload.sender_id,
"notification_id": payload.notification_id
}
});
}
}
/**
function showDialog(_other) {
app.layout.closeDialog('text-message') // this dialog is implemented as a singleton, so must enforce this
reset();
if(!_other) throw "other must be specified in TextMessageDialog"
otherId = _other;
app.layout.showDialog('text-message')
}*/
function initialize() {
var dialogBindings = {
'beforeShow' : beforeShow,
'afterHide': afterHide
};
app.bindDialog('text-message', dialogBindings);
$dialog = $('#text-message-dialog');
$previousMessagesScroller = $dialog.find('.previous-messages-scroller');
$previousMessages = $dialog.find('.previous-messages');
$sendTextMessage = $dialog.find('.btn-send-text-message');
$form = $dialog.find('form');
$textBox = $form.find('textarea');
events();
}
this.initialize = initialize;
this.messageReceived = messageReceived;
this.formatTextMessage = formatTextMessage;
}
return this;
})(window,jQuery);

View File

@ -62,15 +62,14 @@
return;
}
$.ajax({
url: '/api/users/' + context.JK.currentUserId
}).done(function (r) {
userMe = r;
// TODO - Setting global variable for local user.
context.JK.userMe = r;
updateHeader();
handleWhatsNext(userMe);
}).fail(app.ajaxError);
app.user()
.done(function(r) {
userMe = r;
// TODO - Setting global variable for local user.
context.JK.userMe = r;
updateHeader();
handleWhatsNext(userMe);
});
}
function updateHeader() {

View File

@ -640,13 +640,13 @@
context.JK.hasOneConfiguredDevice = function() {
var result = context.jamClient.FTUEGetGoodConfigurationList();
console.log("hasOneConfiguredDevice: ", result);
logger.debug("hasOneConfiguredDevice: ", result);
return result.length > 0;
};
context.JK.getGoodAudioConfigs = function() {
var result = context.jamClient.FTUEGetGoodAudioConfigurations();
console.log("goodAudioConfigs=%o", result);
logger.debug("goodAudioConfigs=%o", result);
return result;
};
@ -671,7 +671,7 @@
badAudioConfigs.push(allAudioConfigs[i]);
}
}
console.log("badAudioConfigs=%o", badAudioConfigs);
logger.debug("badAudioConfigs=%o", badAudioConfigs);
return badAudioConfigs;
};
@ -718,6 +718,12 @@
return deviceId;
}
// returns /client#/home for http://www.jamkazam.com/client#/home
context.JK.locationPath = function() {
var bits = context.location.href.split('/');
return '/' + bits.slice(3).join('/');
}
context.JK.nowUTC = function() {
var d = new Date();
return new Date( d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() );

View File

@ -25,7 +25,16 @@
rest.login({email: email, password: password, remember_me: rememberMe})
.done(function() {
app.layout.closeDialog('signin-dialog')
window.location = '/client'
var redirectTo = $.QueryString['redirect-to'];
if(redirectTo) {
logger.debug("redirectTo:" + redirectTo);
window.location.href = redirectTo;
}
else {
logger.debug("default post-login path");
window.location.href = '/client'
}
})
.fail(function(jqXHR) {
if(jqXHR.status == 422) {
@ -62,6 +71,7 @@
}
function beforeShow() {
logger.debug("showing login form")
reset();
}
@ -70,7 +80,7 @@
}
function afterHide() {
logger.debug("hiding login form")
}
function initialize(){

View File

@ -22,7 +22,6 @@
}
else {
var videoUrl = $.param.querystring(window.location.href, 'showVideo=' + encodeURIComponent($self.data('video-url')));
console.log("videoUrl: ", videoUrl);
context.jamClient.OpenSystemBrowser(videoUrl);
}
}

View File

@ -24,9 +24,9 @@
//= require web/videoDialog
//= require invitationDialog
//= require tickDuration
//= require hoverMusician
//= require feed_item_recording
//= require feed_item_session
//= require hoverMusician
//= require hoverFan
//= require hoverBand
//= require hoverSession

View File

@ -6,6 +6,7 @@
var welcomeRoot;
var rest = context.JK.Rest();
var logger = context.JK.logger;
function initialize() {
@ -22,7 +23,15 @@
rest.getUserDetail({id:context.JK.currentUserId})
.done(function () {
e.preventDefault();
window.location = '/client';
var redirectTo = $.QueryString['redirect-to'];
if(redirectTo) {
logger.debug("redirectTo:" + redirectTo);
window.location = redirectTo;
}
else {
logger.debug("default post-login path");
window.location = '/client';
}
})
.fail(function () {
context.JK.app.layout.showDialog('signin-dialog');

View File

@ -41,6 +41,7 @@
*= require ./localRecordingsDialog
*= require ./serverErrorDialog
*= require ./leaveSessionWarning
*= require ./textMessageDialog
*= require ./terms
*= require ./createSession
*= require ./feed

View File

@ -7,6 +7,10 @@
z-index:999;
}
.bubble.musician-bubble {
width:400px;
}
.bubble h2 {
padding:6px 0px;
text-align:center;

View File

@ -526,6 +526,26 @@ hr {
margin: 10px 0;
}
::-webkit-input-placeholder {
font-family: Raleway, Arial, Helvetica, sans-serif;
font-size:14px;
}
:-moz-placeholder { /* Firefox 18- */
font-family: Raleway, Arial, Helvetica, sans-serif;
font-size:14px;
}
::-moz-placeholder { /* Firefox 19+ */
font-family: Raleway, Arial, Helvetica, sans-serif;
font-size:14px;
}
:-ms-input-placeholder {
font-family: Raleway, Arial, Helvetica, sans-serif;
font-size:14px;
}
// infinitescroll required element
.btn-next-pager {
display:none;

View File

@ -31,3 +31,8 @@
}
}
#notification p .text-message {
white-space: pre-wrap;
word-wrap: break-word;
}

View File

@ -261,5 +261,15 @@
margin-left:-3px;
}
}
#sidebar-notification-list {
.text-message {
word-wrap:break-word;
}
.more-text-available {
}
}
}

View File

@ -0,0 +1,55 @@
#text-message-dialog {
width:500px;
.previous-message-sender {
font-weight:bold;
margin-right:10px;
margin-left:-10px;
color:#ED3618;
&:after {
content:':'
}
}
.previous-messages-scroller {
height:250px;
position:relative;
display:block;
overflow:auto;
}
.previous-messages {
padding:10px;
color:white;
}
.previous-message {
margin:5px 0;
}
.previous-message-text {
line-height:18px;
}
.previous-message-timestamp {
margin-top:4px;
color:#AAA;
display:block;
font-size:12px;
}
.text-message-scroller {
width: 100%;
height: 4em;
margin-top:10px;
}
.text-message-box {
}
#new-text-message {
width:100%;
height:40px;
}
}

View File

@ -9,7 +9,7 @@ class ApiController < ApplicationController
end
rescue_from 'JamRuby::JamArgumentError' do |exception|
@exception = exception
render "errors/jam_argument_error", :status => 400
render "errors/jam_argument_error", :status => 422
end
rescue_from 'JamRuby::PermissionError' do |exception|
@exception = exception

View File

@ -302,7 +302,24 @@ class ApiUsersController < ApiController
###################### NOTIFICATIONS ####################
def notification_index
@notifications = @user.notifications
if params[:type] == 'TEXT_MESSAGE'
# you can ask for just text_message notifications
raise JamArgumentError.new('can\'t be blank', 'receiver') if params[:receiver].blank?
raise JamArgumentError.new('can\'t be blank', 'limit') if params[:limit].blank?
raise JamArgumentError.new('can\'t be blank', 'offset') if params[:offset].blank?
receiver_id = params[:receiver]
limit = params[:limit]
limit = limit.to_i
offset = params[:offset]
offset = offset.to_i
@notifications = Notification.where(description: 'TEXT_MESSAGE').where('(source_user_id = (?) AND target_user_id = (?)) OR (source_user_id = (?) AND target_user_id = (?))', @user.id, receiver_id, receiver_id, @user.id).offset(offset).limit(limit).order('created_at DESC')
else
# you can ask for all notifications
@notifications = @user.notifications
end
respond_with @notifications, responder: ApiResponder, :status => 200
end
@ -311,6 +328,12 @@ class ApiUsersController < ApiController
respond_with responder: ApiResponder, :status => 204
end
def notification_create
@notification = Notification.send_text_message(params[:message], current_user, User.find_by_id(params[:receiver]))
respond_with_model(@notification, new: true)
end
##################### BAND INVITATIONS ##################
def band_invitation_index
@invitations = @user.received_band_invitations

View File

@ -28,11 +28,11 @@ class ClientsController < ApplicationController
gon.use_cached_session_scores = Rails.application.config.use_cached_session_scores
gon.allow_both_find_algos = Rails.application.config.allow_both_find_algos
if current_user
#if current_user
render :layout => 'client'
else
redirect_to root_url
end
#else
# redirect_to root_url
#end
end
AUTHED = %W{friend}

View File

@ -8,15 +8,20 @@ module SessionsHelper
def set_remember_token(user)
if @session_only_cookie
cookies.delete(:remember_token)
cookies[:remember_token] = user.remember_token
cookies[:remember_token] = {
value: user.remember_token,
domain: Rails.application.config.session_cookie_domain
}
else
cookies[:remember_token] = {
:value => user.remember_token,
:expires => 20.years.from_now.utc
value: user.remember_token,
expires: 20.years.from_now.utc,
domain: Rails.application.config.session_cookie_domain
}
end
end
def signed_in?
!current_user.nil?
end

View File

@ -0,0 +1,3 @@
object @notification
attributes :id

View File

@ -1,6 +1,14 @@
collection @notifications
attributes :description, :source_user_id, :target_user_id, :session_id, :recording_id, :invitation_id, :join_request_id, :friend_request_id, :band_id, :band_invitation_id, :formatted_msg, :created_at
attributes :description, :source_user_id, :target_user_id, :session_id, :recording_id, :invitation_id, :join_request_id, :friend_request_id, :band_id, :band_invitation_id, :formatted_msg, :message, :created_at
node :source_user do |n|
source_user_data = {}
if n.source_user
source_user_data[:name] = n.source_user.name
end
source_user_data
end
node :notification_id do |n|
n.id

View File

@ -1,4 +1,4 @@
<div id="band-hover" class="hidden bubble">
<div id="band-hover" class="hidden bubble band-bubble">
</div>

View File

@ -1,4 +1,4 @@
<div id="fan-hover" class="hidden bubble">
<div id="fan-hover" class="hidden bubble fan-bubble">
</div>

View File

@ -1,4 +1,4 @@
<div id="musician-hover" class="hidden bubble">
<div id="musician-hover" class="hidden bubble musician-bubble">
</div>
@ -146,6 +146,23 @@
});
}
function messageMusician(userId) {
if(JK.TextMessageDialogInstance) {
JK.app.layout.showDialog('text-message', {d1: userId})
}
else {
var goto = $('<span>We are sorry, but you can\'t message from this page.</span>');
JK.app.notify(
{
title: "Unable to Message From Here",
text: goto
},
{ no_cancel: true });
}
return false;
}
function adjustMusicianFollowingCount(value) {
$("#spnFollowCount", "#musician-hover").html(parseInt($("#spnFollowCount", "#musician-hover").html()) + value);
}
@ -200,6 +217,7 @@
<div class="left" style="display:none;"><a id="btnLike" onclick="addLike('{userId}');" class="button-orange">LIKE</a></div>
<div class="left"><a id="btnFriend" onclick="{friendAction}('{userId}');" class="button-orange">CONNECT</a></div>
<div class="left"><a id="btnFollow" onclick="{followAction}('{userId}');" class="button-orange">FOLLOW</a></div>
<div class="left"><a id="btnMessage" onclick="messageMusician('{userId}');" class="button-orange">MESSAGE</a></div>
</div>
<br /><br />
</div>

View File

@ -18,7 +18,7 @@
<%= select_tag('musician_query_distance', options_for_select(Search::M_DISTANCE_OPTS, Search::M_MILES_DEFAULT)) %>
<% end -%>
<%= content_tag(:div, :class => 'filter-element') do -%>
miles of <%= content_tag(:span, current_user.current_city(request.remote_ip), :id => 'musician-filter-city') %>
miles of <%= content_tag(:span, current_user ? current_user.current_city(request.remote_ip) : '', :id => 'musician-filter-city') %>
<% end -%>
<% end -%>
<%= content_tag(:div,

View File

@ -11,7 +11,10 @@
<%= render(:partial => "web_filter", :locals => {:search_type => Search::PARAM_MUSICIAN}) %>
<%= content_tag(:div, :class => 'filter-body') do %>
<%= content_tag(:div, :class => 'content-body-scroller') do -%>
<%= content_tag(:div, content_tag(:div, '', :id => 'musician-filter-results', :class => 'filter-results'), :class => 'content-wrapper musician-wrapper') %>
<%= content_tag(:div, :class => 'content-wrapper musician-wrapper') do -%>
<%= content_tag(:div, '', :id => 'musician-filter-results', :class => 'filter-results') %>
<%= content_tag(:div, 'No more results.', :class => 'end-of-list', :id => 'end-of-musician-list') %>
<% end -%>
<% end -%>
<% end -%>
<% end -%>
@ -63,10 +66,11 @@
<script type="text/template" id="template-musician-action-btns">
<a href="{profile_url}" class="button-orange smallbutton">PROFILE</a>
<% if current_user.musician? %>
<% if current_user && current_user.musician? %>
<a href="#" class="{friend_class} smallbutton search-m-friend">{friend_caption}</a>
<% end %>
<a href="#" class="{follow_class} smallbutton search-m-follow">{follow_caption}</a>
<a href="#" class="{message_class} smallbutton search-m-message">{message_caption}</a>
<!--<a href="#" class="{button_message} smallbutton search-m-like">MESSAGE</a>-->
<div class="clearall"></div>
</script>

View File

@ -24,6 +24,7 @@
<div class="right">
<a id="btn-add-friend" class="button-orange">ADD FRIEND</a>
<a id="btn-follow-user" class="button-orange">FOLLOW</a>
<a id="btn-message-user" class="button-orange">MESSAGE</a>
<%= link_to("EDIT PROFILE", '/client#/account/profile', :id => "btn-profile-edit", :class => "button-orange") %>
</div>
<br clear="all" /><br />

View File

@ -217,6 +217,7 @@
</li>
</script>
<!-- Chat panel template -->
<script type="text/template" id="template-chat-panel">
<li>

View File

@ -0,0 +1,24 @@
.dialog.textMessage-overlay.ftue-overlay.tall{ layout: 'dialog', 'layout-id' => 'text-message', id: 'text-message-dialog'}
.content-head
= image_tag "content/icon_comment.png", {:width => 12, :height => 12, :class => 'content-icon' }
%h1
= 'conversation with'
%span.receiver-name
.dialog-inner
.previous-messages-scroller
.previous-messages
.text-message-scroller
%form.text-message-box
%textarea{ name: 'new-text-message', id: 'new-text-message' }
.left
%span.small.offline-tip An email will be sent if recipient is offline
.right
%a.button-grey.btn-close-dialog{href:'#', 'layout-action' => 'close'} CLOSE
%a.button-orange.btn-send-text-message{href:'#'} SEND
%script{type: 'text/template', id: 'template-previous-message'}
.previous-message
%span.previous-message-sender= '{{data.sender}}'
%span.previous-message-text= '{{data.msg}}'
%time.previous-message-timestamp.timeago{datetime: '{{data.sent}}'}= '{{data.sent}}'

View File

@ -55,7 +55,7 @@
<%= select_tag("#{filter_label}_query_distance", options_for_select(Search::DISTANCE_OPTS, default_distance), {:class => 'easydropdown'}) %>
<% end -%>
<%= content_tag(:div, :class => 'filter-element desc') do -%>
miles of <%= content_tag(:span, current_user.current_city(request.remote_ip), :id => "#{filter_label}-filter-city") %>
miles of <%= content_tag(:span, current_user ? current_user.current_city(request.remote_ip) : '', :id => "#{filter_label}-filter-city") %>
<% end -%>
<!-- @end distance filter -->
<% end %>

View File

@ -49,6 +49,7 @@
<%= render "recordingFinishedDialog" %>
<%= render "localRecordingsDialog" %>
<%= render "showServerErrorDialog" %>
<%= render "textMessageDialog" %>
<%= render "notify" %>
<%= render "client_update" %>
<%= render "banner" %>
@ -107,6 +108,11 @@
JK.currentUserName = null;
JK.currentUserMusician = null;
JK.currentUserAdmin = false;
// you need to be logged in to use this part of the interface.
// save original URL, and redirect to the home page
logger.debug("redirecting back to / because not logged in")
window.location.href = '/?redirect-to=' + encodeURIComponent(JK.locationPath());
<% end %>
// Some things can't be initialized until we're connected. Put them here.
@ -123,6 +129,10 @@
var invitationDialog = new JK.InvitationDialog(JK.app);
invitationDialog.initialize(facebookHelper);
var textMessageDialog = new JK.TextMessageDialog(JK.app);
JK.TextMessageDialogInstance = textMessageDialog;
textMessageDialog.initialize();
var localRecordingsDialog = new JK.LocalRecordingsDialog(JK.app);
localRecordingsDialog.initialize();
@ -144,13 +154,13 @@
header.initialize();
var sidebar = new JK.Sidebar(JK.app);
sidebar.initialize(invitationDialog);
sidebar.initialize(invitationDialog, textMessageDialog);
var homeScreen = new JK.HomeScreen(JK.app);
homeScreen.initialize();
var profileScreen = new JK.ProfileScreen(JK.app);
profileScreen.initialize();
profileScreen.initialize(textMessageDialog);
var bandProfileScreen = new JK.BandProfileScreen(JK.app);
bandProfileScreen.initialize();
@ -197,7 +207,7 @@
findSessionScreen.initialize(sessionLatency);
var findMusicianScreen = new JK.FindMusicianScreen(JK.app);
findMusicianScreen.initialize();
findMusicianScreen.initialize(textMessageDialog);
var findBandScreen = new JK.FindBandScreen(JK.app);
findBandScreen.initialize();

View File

@ -1,6 +1,13 @@
object @exception
attributes :message
node do |exception|
field = exception.field ? exception.field : 'unknown'
errors = {}
errors[field] = [exception.field_message]
{
errors: errors
}
end
node "type" do
"ArgumentError"

View File

@ -218,5 +218,7 @@ if defined?(Bundler)
# should we use the new FindSessions API that has server-side scores
config.use_cached_session_scores = true
config.allow_both_find_algos = false
config.session_cookie_domain = nil
end
end

View File

@ -1,4 +1,5 @@
unless $rails_rake_task
JamWebEventMachine.start

View File

@ -219,6 +219,7 @@ SampleApp::Application.routes.draw do
# notifications
match '/users/:id/notifications' => 'api_users#notification_index', :via => :get
match '/users/:id/notifications/:notification_id' => 'api_users#notification_destroy', :via => :delete
match '/users/:id/notifications' => 'api_users#notification_create', :via => :post
# user band invitations
match '/users/:id/band_invitations' => 'api_users#band_invitation_index', :via => :get

View File

@ -59,6 +59,11 @@ namespace :db do
make_recording
end
# takes command line args: http://davidlesches.com/blog/passing-arguments-to-a-rails-rake-task
task :populate_conversation, [:target_email] => :environment do |task, args|
populate_conversation(args.target_email)
end
desc "Fill database with music session sample data"
task populate_music_sessions: :environment do
make_users(10) if 14 > User.count
@ -272,4 +277,19 @@ def make_recording
mix.completed = true
recording.mixes << mix
recording.save!(validate:false)
end
def populate_conversation(target_email)
all_users = User.all
target_users = target_email ? User.where(email: target_email) : all_users
target_users.each do |target_user|
all_users.each do |other_user|
next if target_user == other_user
20.times do
FactoryGirl.create(:notification_text_message, target_user: target_user, source_user: other_user, message: Faker::Lorem.characters(rand(400)))
end
end
end
end

View File

@ -400,4 +400,12 @@ FactoryGirl.define do
factory :event_session, :class => JamRuby::EventSession do
end
factory :notification, :class => JamRuby::Notification do
factory :notification_text_message do
description 'TEXT_MESSAGE'
message Faker::Lorem.characters(10)
end
end
end

View File

@ -0,0 +1,141 @@
require 'spec_helper'
describe "Text Message", :js => true, :type => :feature, :capybara_feature => true do
before(:all) do
User.delete_all # we delete all users due to the use of find_musician() helper method, which scrolls through all users
end
before(:each) do
@user1 = FactoryGirl.create(:user)
@user2 = FactoryGirl.create(:user, first_name: 'bone_crusher')
sign_in_poltergeist(@user1)
end
describe "burn em up" do
in_client "one" do
end
in_client "two" do
end
end
# what are all the ways to launch the dialog?
describe "launches" do
it "on hover bubble" do
site_search(@user2.first_name, expand: true)
find("#search-results a[user-id=\"#{@user2.id}\"][hoveraction=\"musician\"]", text: @user2.name).hover_intent
find('#musician-hover #btnMessage').trigger(:click)
find('h1', text: 'conversation with ' + @user2.name)
end
it "on find musician in musician's tile" do
musician = find_musician(@user2)
find(".result-list-button-wrapper[data-musician-id='#{@user2.id}'] .search-m-message").trigger(:click)
find('h1', text: 'conversation with ' + @user2.name)
end
it "on musician profile" do
visit "/client#/profile/#{@user2.id}"
find('#btn-message-user').trigger(:click)
find('h1', text: 'conversation with ' + @user2.name)
end
it "on reply of notification in sidebar" do
# create a notification
notification = Notification.send_text_message("bibbity bobbity boo", @user2, @user1)
notification.errors.any?.should be_false
open_sidebar
# find the notification and click REPLY
find("[layout-id='panelNotifications'] [notification-id='#{notification.id}'] .button-orange", text:'REPLY').trigger(:click)
find('h1', text: 'conversation with ' + @user2.name)
end
it "on hit reply in message notification" do
in_client(@user1) do
sign_in_poltergeist(@user1)
end
in_client(@user2) do
sign_in_poltergeist(@user2)
site_search(@user1.name, expand: true)
find("#search-results a[user-id=\"#{@user1.id}\"][hoveraction=\"musician\"]", text: @user1.name).hover_intent
find('#musician-hover #btnMessage').trigger(:click)
find('h1', text: 'conversation with ' + @user1.name)
send_text_message("Hello to user id #{@user1.id}", close_on_send: true)
end
in_client(@user1) do
find('#notification #ok-button').trigger(:click)
find('h1', text: 'conversation with ' + @user2.name)
end
end
it "can load directly into chat session from url" do
sign_in_poltergeist(@user1)
visit "/"
find('h1', text: 'Play music together over the Internet as if in the same room')
visit "/client#/home/text-message/d1=#{@user2.id}"
find('h1', text: 'conversation with ' + @user2.name)
end
end
describe "chat dialog behavior" do
it "send a message to someone" do
in_client(@user1) do
sign_in_poltergeist(@user1)
end
in_client(@user2) do
sign_in_poltergeist(@user2)
site_search(@user1.name, expand: true)
find("#search-results a[user-id=\"#{@user1.id}\"][hoveraction=\"musician\"]", text: @user1.name).hover_intent
find('#musician-hover #btnMessage').trigger(:click)
find('h1', text: 'conversation with ' + @user1.name)
send_text_message("Oh hai to user id #{@user1.id}")
end
in_client(@user1) do
find('#notification #ok-button').trigger(:click)
find('h1', text: 'conversation with ' + @user2.name)
find('.previous-message-text', text: "Oh hai to user id #{@user1.id}")
send_text_message('hey there yourself')
end
in_client(@user2) do
find('.previous-message-text', text: "hey there yourself")
send_text_message('ok bye', close_on_send: true)
end
in_client(@user1) do
find('.previous-message-text', text: "ok bye")
send_text_message('bye now', close_on_send: true)
end
end
it "shows error with a notify" do
sign_in_poltergeist(@user1)
visit '/'
find('h1', text: 'Play music together over the Internet as if in the same room')
visit "/client#/home/text-message/d1=#{@user2.id}"
find('h1', text: 'conversation with ' + @user2.name)
send_text_message('ass', should_fail:'profanity')
end
end
end

View File

@ -86,6 +86,37 @@ describe "Welcome", :js => true, :type => :feature, :capybara_feature => true d
end
end
describe "redirect-to" do
it "redirect on login" do
visit "/client#/account"
find('.curtain')
find('h1', text: 'Play music together over the Internet as if in the same room')
find('#signin').trigger(:click)
within('#signin-form') do
fill_in "email", with: user.email
fill_in "password", with: user.password
click_button "SIGN IN"
end
wait_until_curtain_gone
find('h2', text: 'identity:')
end
it "redirect if already logged in" do
# this is a rare case
sign_in_poltergeist(user)
visit "/?redirect-to=" + ERB::Util.url_encode("/client#/account")
find('h1', text: 'Play music together over the Internet as if in the same room')
find('#signin').trigger(:click)
wait_until_curtain_gone
find('h2', text: 'identity:')
end
end
describe "signin with facebook" do
before(:each) do

View File

@ -250,9 +250,9 @@ describe "Band API", :type => :api do
it "should not allow user to create invitation to a Fan for band A" do
recipient = FactoryGirl.create(:fan)
last_response = create_band_invitation(band.id, recipient.id)
last_response.status.should == 400
last_response.status.should == 422
error_msg = JSON.parse(last_response.body)
error_msg["message"].should == BandInvitation::BAND_INVITATION_FAN_RECIPIENT_ERROR
error_msg["errors"]['receiver'].should == [BandInvitation::BAND_INVITATION_FAN_RECIPIENT_ERROR]
# test receiver relationships
recipient.received_band_invitations.size.should == 0

View File

@ -983,7 +983,6 @@ describe "User API", :type => :api do
let(:track) { FactoryGirl.create(:track, :connection => connection, :instrument => instrument) }
let(:music_session) { ms = FactoryGirl.create(:music_session, :creator => user, :musician_access => true); ms.connections << connection; ms.save!; ms }
it "fetches facebook successfully" do
login(user.email, user.password, 200, true)
get "/api/users/#{user.id}/share/session/facebook.json?music_session=#{music_session.id}", nil, "CONTENT_TYPE" => 'application/json'
@ -997,6 +996,118 @@ describe "User API", :type => :api do
end
end
describe "notifications" do
let(:other) { FactoryGirl.create(:user) }
before(:each) do
login(user.email, user.password, 200, true)
end
it "create text notification" do
post "/api/users/#{user.id}/notifications.json", {message: 'bibbity bobbity boo', receiver:other.id }.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should == 201
response = JSON.parse(last_response.body)
response['id'].should_not be_nil
# verify that it can be found
get "/api/users/#{user.id}/notifications.json", {type: 'TEXT_MESSAGE', receiver: other.id, limit:20, offset:0}, "CONTENT_TYPE" => 'application/json'
last_response.status.should == 200
response = JSON.parse(last_response.body)
response.length.should == 1
end
it "bad language causes 422" do
post "/api/users/#{user.id}/notifications.json", {message: 'ass', receiver:other.id }.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should == 422
response = JSON.parse(last_response.body)
response['errors']['message'].should == ['cannot contain profanity']
end
it "bad receiver causes 422" do
post "/api/users/#{user.id}/notifications.json", {message: 'ass' }.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should == 422
response = JSON.parse(last_response.body)
response['errors']['target_user'].should == ['can\'t be blank']
end
describe "index" do
describe "text message index" do
it "requires receiver id" do
# verify that it can be found
get "/api/users/#{user.id}/notifications.json", {type: 'TEXT_MESSAGE'}, "CONTENT_TYPE" => 'application/json'
response = JSON.parse(last_response.body)
response['errors']['receiver'].should == ['can\'t be blank']
last_response.status.should == 422
end
it "requires limit" do
# verify that it can be found
get "/api/users/#{user.id}/notifications.json", {type: 'TEXT_MESSAGE', receiver: other.id, offset:0}, "CONTENT_TYPE" => 'application/json'
response = JSON.parse(last_response.body)
response['errors']['limit'].should == ['can\'t be blank']
last_response.status.should == 422
end
it "requires offset" do
# verify that it can be found
get "/api/users/#{user.id}/notifications.json", {type: 'TEXT_MESSAGE', receiver: other.id, limit:20}, "CONTENT_TYPE" => 'application/json'
response = JSON.parse(last_response.body)
response['errors']['offset'].should == ['can\'t be blank']
last_response.status.should == 422
end
it "returns no results" do
# verify that it can be found
get "/api/users/#{user.id}/notifications.json", {type: 'TEXT_MESSAGE', receiver: other.id, offset:0, limit:20}, "CONTENT_TYPE" => 'application/json'
response = JSON.parse(last_response.body)
response.length.should == 0
last_response.status.should == 200
end
it "returns one results" do
msg1 = FactoryGirl.create(:notification_text_message, source_user: user, target_user: other)
# verify that it can be found
get "/api/users/#{user.id}/notifications.json", {type: 'TEXT_MESSAGE', receiver: other.id, offset:0, limit:20}, "CONTENT_TYPE" => 'application/json'
response = JSON.parse(last_response.body)
response.length.should == 1
response[0]['notification_id'].should == msg1.id
response[0]['description'].should == msg1.description
response[0]['message'].should == msg1.message
response[0]['source_user_id'].should == msg1.source_user_id
response[0]['target_user_id'].should == msg1.target_user_id
last_response.status.should == 200
login(other.email, other.password, 200, true)
get "/api/users/#{other.id}/notifications.json", {type: 'TEXT_MESSAGE', receiver: user.id, offset:0, limit:20}, "CONTENT_TYPE" => 'application/json'
response = JSON.parse(last_response.body)
response.length.should == 1
response[0]['notification_id'].should == msg1.id
response[0]['description'].should == msg1.description
response[0]['message'].should == msg1.message
response[0]['source_user_id'].should == msg1.source_user_id
response[0]['target_user_id'].should == msg1.target_user_id
last_response.status.should == 200
end
it "returns sorted results" do
msg1 = FactoryGirl.create(:notification_text_message, source_user: user, target_user: other)
msg2 = FactoryGirl.create(:notification_text_message, source_user: user, target_user: other, created_at: 1.days.ago)
# verify that it can be found
get "/api/users/#{user.id}/notifications.json", {type: 'TEXT_MESSAGE', receiver: other.id, offset:0, limit:20}, "CONTENT_TYPE" => 'application/json'
response = JSON.parse(last_response.body)
last_response.status.should == 200
response.length.should == 2
response[0]['notification_id'].should == msg1.id
response[1]['notification_id'].should == msg2.id
end
end
end
end
describe "share_recording" do
before(:each) do

View File

@ -22,7 +22,7 @@ ActiveRecord::Base.establish_connection(YAML::load(File.open('config/database.ym
require 'jam_ruby'
# uncomment this to see active record logs
# ActiveRecord::Base.logger = Logger.new(STDOUT) if defined?(ActiveRecord::Base)
ActiveRecord::Base.logger = Logger.new(STDOUT) if defined?(ActiveRecord::Base)
include JamRuby
# put ActionMailer into test mode
@ -36,7 +36,7 @@ Thread.new {
sleep 30
unless tests_started
puts "tests are hung. exiting..."
exit! 20
#exit! 20
end
}

View File

@ -0,0 +1,66 @@
# methods here all assume you are in /client
# enters text into the search sidebar
def site_search(text, options = {})
within('#searchForm') do
fill_in "search-input", with: text
end
find('#sidebar-search-expand').trigger(:click) if options[:expand]
end
# goes to the musician tile, and tries to find a musician
def find_musician(user)
visit "/client#/musicians"
timeout = 30
start = Time.now
# scroll by 100px until we find a user with the right id
while page.all('#end-of-musician-list').length == 0
page.execute_script('jQuery("#musician-filter-results").scrollTo("+=100px", 0, {axis:"y"})')
found = page.all(".result-list-button-wrapper[data-musician-id='#{user.id}']")
if found.length == 1
return found[0]
elsif found.length > 1
raise "ambiguous results in musician list"
end
if Time.now - start > timeout
raise "unable to find musician #{user} within #{timeout} seconds"
end
end
raise "unable to find musician #{user}"
end
# sends a text message in the chat interface.
def send_text_message(msg, options={})
find('#text-message-dialog') # assert that the dialog is showing already
within('#text-message-dialog form.text-message-box') do
fill_in 'new-text-message', with: msg
end
find('#text-message-dialog .btn-send-text-message').trigger(:click)
find('#text-message-dialog .previous-message-text', text: msg) unless options[:should_fail]
# close the dialog if caller specified close_on_send
if options[:close_on_send]
find('#text-message-dialog .btn-close-dialog', text: 'CLOSE').trigger(:click) if options[:close_on_send]
page.should have_no_selector('#text-message-dialog')
end
if options[:should_fail]
find('#notification').should have_text(options[:should_fail])
end
end
def open_sidebar
find('[layout-id="panelNotifications"] .panel-header').trigger(:click)
end
def hover_intent(element)
element.hover
element.hover
end

View File

@ -1,5 +1,20 @@
include ApplicationHelper
# add a hover_intent method to element, so that you can do find(selector).hover_intent
module Capybara
module Node
class Element
def hover_intent
hover
hover
hover
end
end
end
end
# holds a single test's session name's, mapped to pooled session names
$capybara_session_mapper = {}
@ -11,7 +26,7 @@ end
# manages the mapped session name
def mapped_session_name(session_name)
return :default if session_name == :default # special treatment for the built-in session
$capybara_session_mapper[session_name] ||= $capybara_session_mapper.length
$capybara_session_mapper[session_name] ||= 'session_' + $capybara_session_mapper.length.to_s
end
# in place of ever using Capybara.session_name directly,
@ -56,15 +71,11 @@ end
def sign_in_poltergeist(user)
visit signin_path
fill_in "session_email", with: user.email
fill_in "session_password", with: user.password
fill_in "Email Address:", with: user.email
fill_in "Password:", with: user.password
click_button "SIGN IN"
if Capybara.javascript_driver == :poltergeist
page.driver.set_cookie(:remember_token, user.remember_token)
else
page.driver.browser.manage.add_cookie :name => :remember_token, :value => user.remember_token
end
wait_until_curtain_gone
end
def sign_out()
@ -109,7 +120,7 @@ def wait_until_user(wait=Capybara.default_wait_time)
end
def wait_until_curtain_gone
should have_no_selector('.curtain')
page.should have_no_selector('.curtain')
end
def wait_to_see_my_track
@ -353,4 +364,11 @@ end
# wait for the easydropdown version of the specified select element to become visible
def wait_for_easydropdown(select)
find(select, :visible => false).find(:xpath, 'ancestor::div[contains(@class, "dropdown easydropdown")]')
end
# defaults to enter key (13)
def send_key(keycode = 13)
keypress_script = "var e = $.Event('keydown', { keyCode: #{keycode} }); $('#search-input').trigger(e);"
page.driver.execute_script(keypress_script)
end