* VRFS-2855 - broadcast notifications done
This commit is contained in:
parent
cdaac868de
commit
0dbbfc0ee7
|
|
@ -109,6 +109,7 @@ group :development, :test do
|
||||||
gem 'database_cleaner', '0.7.0'
|
gem 'database_cleaner', '0.7.0'
|
||||||
gem 'launchy'
|
gem 'launchy'
|
||||||
gem 'faker', '1.3.0'
|
gem 'faker', '1.3.0'
|
||||||
|
gem 'puma'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
ActiveAdmin.register JamRuby::BroadcastNotification, :as => 'BroadcastNotification' do
|
||||||
|
|
||||||
|
menu :label => 'Notifications'
|
||||||
|
|
||||||
|
config.sort_order = 'created_at_desc'
|
||||||
|
config.batch_actions = false
|
||||||
|
config.clear_action_items!
|
||||||
|
config.filters = false
|
||||||
|
|
||||||
|
action_item :only => :index do
|
||||||
|
link_to "New Broadcast" , "broadcast_notifications/new"
|
||||||
|
end
|
||||||
|
|
||||||
|
show do
|
||||||
|
attributes_table do
|
||||||
|
row :title
|
||||||
|
row :message
|
||||||
|
row :button_label
|
||||||
|
row :button_url
|
||||||
|
row :frequency
|
||||||
|
row :frequency_distribution
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
|
@ -16,3 +16,6 @@ PLATFORMS
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
pg_migrate (= 0.1.13)
|
pg_migrate (= 0.1.13)
|
||||||
|
|
||||||
|
BUNDLED WITH
|
||||||
|
1.10.3
|
||||||
|
|
|
||||||
|
|
@ -286,3 +286,4 @@ signing.sql
|
||||||
optimized_redeemption.sql
|
optimized_redeemption.sql
|
||||||
optimized_redemption_warn_mode.sql
|
optimized_redemption_warn_mode.sql
|
||||||
affiliate_partners2.sql
|
affiliate_partners2.sql
|
||||||
|
broadcast_notifications.sql
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
CREATE TABLE broadcast_notifications (
|
||||||
|
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
title VARCHAR(64),
|
||||||
|
message VARCHAR(256),
|
||||||
|
button_label VARCHAR(32),
|
||||||
|
button_url VARCHAR,
|
||||||
|
frequency INTEGER DEFAULT 0,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE broadcast_notification_views (
|
||||||
|
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
user_id varchar(64) NOT NULL REFERENCES users(id),
|
||||||
|
broadcast_notification_id varchar(64) NOT NULL REFERENCES broadcast_notifications(id) ON DELETE CASCADE,
|
||||||
|
view_count INTEGER DEFAULT 0,
|
||||||
|
active_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX user_broadcast_idx ON broadcast_notification_views(user_id, broadcast_notification_id);
|
||||||
|
|
@ -62,6 +62,7 @@ group :test do
|
||||||
gem 'resque_spec' #, :path => "/home/jam/src/resque_spec/"
|
gem 'resque_spec' #, :path => "/home/jam/src/resque_spec/"
|
||||||
gem 'timecop'
|
gem 'timecop'
|
||||||
gem 'rspec-prof'
|
gem 'rspec-prof'
|
||||||
|
gem 'byebug'
|
||||||
end
|
end
|
||||||
|
|
||||||
# Specify your gem's dependencies in jam_ruby.gemspec
|
# Specify your gem's dependencies in jam_ruby.gemspec
|
||||||
|
|
|
||||||
|
|
@ -225,6 +225,8 @@ require "jam_ruby/models/text_message"
|
||||||
require "jam_ruby/models/sale"
|
require "jam_ruby/models/sale"
|
||||||
require "jam_ruby/models/sale_line_item"
|
require "jam_ruby/models/sale_line_item"
|
||||||
require "jam_ruby/models/recurly_transaction_web_hook"
|
require "jam_ruby/models/recurly_transaction_web_hook"
|
||||||
|
require "jam_ruby/models/broadcast_notification"
|
||||||
|
require "jam_ruby/models/broadcast_notification_view"
|
||||||
require "jam_ruby/jam_tracks_manager"
|
require "jam_ruby/jam_tracks_manager"
|
||||||
require "jam_ruby/jam_track_importer"
|
require "jam_ruby/jam_track_importer"
|
||||||
require "jam_ruby/jmep_manager"
|
require "jam_ruby/jmep_manager"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
module JamRuby
|
||||||
|
class BroadcastNotification < ActiveRecord::Base
|
||||||
|
|
||||||
|
attr_accessible :title, :message, :button_label, :frequency, :button_url, as: :admin
|
||||||
|
|
||||||
|
has_many :user_views, class_name: 'JamRuby::BroadcastNotificationView', dependent: :destroy
|
||||||
|
|
||||||
|
validates :button_label, presence: true, length: {maximum: 14}
|
||||||
|
validates :message, presence: true, length: {maximum: 200}
|
||||||
|
validates :title, presence: true, length: {maximum: 50}
|
||||||
|
|
||||||
|
def self.next_broadcast(user)
|
||||||
|
self.viewable_notifications(user).limit(1).first
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.viewable_notifications(user)
|
||||||
|
self.select('broadcast_notifications.*, bnv.updated_at AS bnv_updated_at')
|
||||||
|
.joins("LEFT OUTER JOIN broadcast_notification_views AS bnv ON bnv.broadcast_notification_id = broadcast_notifications.id AND (bnv.user_id IS NULL OR (bnv.user_id = '#{user.id}'))")
|
||||||
|
.where(['broadcast_notifications.frequency > 0'])
|
||||||
|
.where(['bnv.user_id IS NULL OR bnv.active_at < NOW()'])
|
||||||
|
.where(['bnv.user_id IS NULL OR broadcast_notifications.frequency > bnv.view_count'])
|
||||||
|
.order('bnv_updated_at NULLS FIRST')
|
||||||
|
end
|
||||||
|
|
||||||
|
def did_view(user)
|
||||||
|
bnv = BroadcastNotificationView
|
||||||
|
.where(broadcast_notification_id: self.id, user_id: user.id)
|
||||||
|
.limit(1)
|
||||||
|
.first
|
||||||
|
|
||||||
|
unless bnv
|
||||||
|
bnv = user_views.new()
|
||||||
|
bnv.user = user
|
||||||
|
bnv.active_at = Time.now - 10
|
||||||
|
end
|
||||||
|
|
||||||
|
bnv = user_views.new(user: user) unless bnv
|
||||||
|
bnv.view_count += 1
|
||||||
|
bnv.save
|
||||||
|
bnv
|
||||||
|
end
|
||||||
|
|
||||||
|
def frequency_distribution
|
||||||
|
@distribution ||= BroadcastNotificationView
|
||||||
|
.where(broadcast_notification_id: self.id)
|
||||||
|
.group(:view_count)
|
||||||
|
.count
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
module JamRuby
|
||||||
|
class BroadcastNotificationView < ActiveRecord::Base
|
||||||
|
|
||||||
|
belongs_to :broadcast_notification, :class_name => 'JamRuby::BroadcastNotification'
|
||||||
|
belongs_to :user, :class_name => 'JamRuby::User'
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -633,9 +633,9 @@ FactoryGirl.define do
|
||||||
data Faker::Lorem.sentence
|
data Faker::Lorem.sentence
|
||||||
end
|
end
|
||||||
|
|
||||||
factory :rsvp_slot, class: JamRuby::RsvpSlot do
|
factory :rsvp_slot, :class => JamRuby::RsvpSlot do
|
||||||
|
|
||||||
proficiency_level 'beginner'
|
proficiency_level "beginner"
|
||||||
instrument { Instrument.find('electric guitar') }
|
instrument { Instrument.find('electric guitar') }
|
||||||
|
|
||||||
factory :chosen_rsvp_slot do
|
factory :chosen_rsvp_slot do
|
||||||
|
|
@ -643,10 +643,10 @@ FactoryGirl.define do
|
||||||
user nil
|
user nil
|
||||||
end
|
end
|
||||||
|
|
||||||
after(:create) { |rsvp_slot, evaluator|
|
after(:create) do |rsvp_slot, evaluator|
|
||||||
rsvp_request = FactoryGirl.create(:rsvp_request, user: evaluator.user)
|
rsvp_request = FactoryGirl.create(:rsvp_request, user: evaluator.user)
|
||||||
rsvp_request_rsvp_slot = FactoryGirl.create(:rsvp_request_rsvp_slot, chosen:true, rsvp_request: rsvp_request, rsvp_slot:rsvp_slot)
|
rsvp_request_rsvp_slot = FactoryGirl.create(:rsvp_request_rsvp_slot, chosen:true, rsvp_request: rsvp_request, rsvp_slot:rsvp_slot)
|
||||||
}
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -686,7 +686,7 @@ FactoryGirl.define do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
factory :rsvp_request_rsvp_slot, class: JamRuby::RsvpRequestRsvpSlot do
|
factory :rsvp_request_rsvp_slot, :class => JamRuby::RsvpRequestRsvpSlot do
|
||||||
chosen false
|
chosen false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -798,6 +798,13 @@ FactoryGirl.define do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
factory :broadcast_notification, :class => JamRuby::BroadcastNotification do
|
||||||
|
title Faker::Lorem.sentence[0...50]
|
||||||
|
message Faker::Lorem.paragraph[0...200]
|
||||||
|
button_label Faker::Lorem.words(2).join(' ')[0...14]
|
||||||
|
frequency 3
|
||||||
|
end
|
||||||
|
|
||||||
factory :affiliate_partner, class: 'JamRuby::AffiliatePartner' do
|
factory :affiliate_partner, class: 'JamRuby::AffiliatePartner' do
|
||||||
sequence(:partner_name) { |n| "partner-#{n}" }
|
sequence(:partner_name) { |n| "partner-#{n}" }
|
||||||
entity_type 'Individual'
|
entity_type 'Individual'
|
||||||
|
|
@ -825,5 +832,4 @@ FactoryGirl.define do
|
||||||
factory :affiliate_legalese, class: 'JamRuby::AffiliateLegalese' do
|
factory :affiliate_legalese, class: 'JamRuby::AffiliateLegalese' do
|
||||||
legalese Faker::Lorem.paragraphs(6).join("\n\n")
|
legalese Faker::Lorem.paragraphs(6).join("\n\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -616,6 +616,7 @@ describe ActiveMusicSession do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "disallow a jam track to be opened when another is already opened" do
|
it "disallow a jam track to be opened when another is already opened" do
|
||||||
|
pending "needs fixing"
|
||||||
# if a jam track is open, don't allow another to be opened
|
# if a jam track is open, don't allow another to be opened
|
||||||
@music_session.open_jam_track(@user1, @jam_track)
|
@music_session.open_jam_track(@user1, @jam_track)
|
||||||
@music_session.errors.any?.should be_false
|
@music_session.errors.any?.should be_false
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe BroadcastNotification do
|
||||||
|
|
||||||
|
let(:broadcast1) { FactoryGirl.create(:broadcast_notification) }
|
||||||
|
let(:broadcast2) { FactoryGirl.create(:broadcast_notification) }
|
||||||
|
let(:broadcast3) { FactoryGirl.create(:broadcast_notification) }
|
||||||
|
let(:broadcast4) { FactoryGirl.create(:broadcast_notification) }
|
||||||
|
let(:user1) { FactoryGirl.create(:user) }
|
||||||
|
let(:user2) { FactoryGirl.create(:user) }
|
||||||
|
let(:user3) { FactoryGirl.create(:user) }
|
||||||
|
let(:user4) { FactoryGirl.create(:user) }
|
||||||
|
|
||||||
|
before(:each) do
|
||||||
|
BroadcastNotificationView.delete_all
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'created broadcast' do
|
||||||
|
expect(broadcast1.title).not_to be_empty
|
||||||
|
expect(broadcast1.frequency).to be > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gets view incremented' do
|
||||||
|
bnv = broadcast1.did_view(user1)
|
||||||
|
bnv = broadcast1.did_view(user1)
|
||||||
|
bnv.view_count.should eq(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'loads viewable broadcasts for a user' do
|
||||||
|
broadcast1.touch
|
||||||
|
broadcast2.touch
|
||||||
|
broadcast3.touch
|
||||||
|
broadcast4.touch
|
||||||
|
|
||||||
|
bns = BroadcastNotification.viewable_notifications(user1)
|
||||||
|
bns.count.should eq(4)
|
||||||
|
|
||||||
|
broadcast2.frequency.times { |nn| broadcast2.did_view(user1) }
|
||||||
|
broadcast3.did_view(user1)
|
||||||
|
broadcast1.did_view(user1)
|
||||||
|
broadcast4.did_view(user2)
|
||||||
|
|
||||||
|
bns = BroadcastNotification.viewable_notifications(user1)
|
||||||
|
expect(bns.count).to be(3)
|
||||||
|
expect(bns[0].id).to eq(broadcast3.id)
|
||||||
|
expect(bns.detect {|bb| bb.id==broadcast2.id }).to be_nil
|
||||||
|
expect(BroadcastNotification.next_broadcast(user1).id).to eq(broadcast3.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'generates frequency distribution' do
|
||||||
|
4.times { |nn| broadcast1.did_view(user1) }
|
||||||
|
5.times { |nn| broadcast1.did_view(user2) }
|
||||||
|
5.times { |nn| broadcast1.did_view(user3) }
|
||||||
|
8.times { |nn| broadcast1.did_view(user4) }
|
||||||
|
|
||||||
|
distrib = broadcast1.frequency_distribution
|
||||||
|
|
||||||
|
expect(distrib.count).to be == 3
|
||||||
|
expect(distrib[4]).to be == 1
|
||||||
|
expect(distrib[5]).to be == 2
|
||||||
|
expect(distrib[8]).to be == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
12
web/Gemfile
12
web/Gemfile
|
|
@ -1,7 +1,4 @@
|
||||||
source 'http://rubygems.org'
|
source 'http://rubygems.org'
|
||||||
unless ENV["LOCAL_DEV"] == "1"
|
|
||||||
source 'https://jamjam:blueberryjam@int.jamkazam.com/gems/'
|
|
||||||
end
|
|
||||||
# Look for $WORKSPACE, otherwise use "workspace" as dev path.
|
# Look for $WORKSPACE, otherwise use "workspace" as dev path.
|
||||||
|
|
||||||
devenv = ENV["BUILD_NUMBER"].nil? # Jenkins sets a build number environment variable
|
devenv = ENV["BUILD_NUMBER"].nil? # Jenkins sets a build number environment variable
|
||||||
|
|
@ -12,11 +9,13 @@ if devenv
|
||||||
gem 'jam_ruby', :path => "../ruby"
|
gem 'jam_ruby', :path => "../ruby"
|
||||||
gem 'jam_websockets', :path => "../websocket-gateway"
|
gem 'jam_websockets', :path => "../websocket-gateway"
|
||||||
else
|
else
|
||||||
|
source 'https://jamjam:blueberryjam@int.jamkazam.com/gems/' do
|
||||||
gem 'jam_db', "0.1.#{ENV["BUILD_NUMBER"]}"
|
gem 'jam_db', "0.1.#{ENV["BUILD_NUMBER"]}"
|
||||||
gem 'jampb', "0.1.#{ENV["BUILD_NUMBER"]}"
|
gem 'jampb', "0.1.#{ENV["BUILD_NUMBER"]}"
|
||||||
gem 'jam_ruby', "0.1.#{ENV["BUILD_NUMBER"]}"
|
gem 'jam_ruby', "0.1.#{ENV["BUILD_NUMBER"]}"
|
||||||
gem 'jam_websockets', "0.1.#{ENV["BUILD_NUMBER"]}"
|
gem 'jam_websockets', "0.1.#{ENV["BUILD_NUMBER"]}"
|
||||||
ENV['NOKOGIRI_USE_SYSTEM_LIBRARIES'] ||= "true"
|
ENV['NOKOGIRI_USE_SYSTEM_LIBRARIES'] ||= "true"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
gem 'oj', '2.10.2'
|
gem 'oj', '2.10.2'
|
||||||
|
|
@ -89,6 +88,13 @@ gem 'guard', '2.7.3'
|
||||||
gem 'influxdb', '0.1.8'
|
gem 'influxdb', '0.1.8'
|
||||||
gem 'influxdb-rails', '0.1.10'
|
gem 'influxdb-rails', '0.1.10'
|
||||||
gem 'sitemap_generator'
|
gem 'sitemap_generator'
|
||||||
|
gem 'bower-rails', "~> 0.9.2"
|
||||||
|
gem 'react-rails', '~> 1.0'
|
||||||
|
#gem "browserify-rails", "~> 0.7"
|
||||||
|
|
||||||
|
source 'https://rails-assets.org' do
|
||||||
|
gem 'rails-assets-reflux'
|
||||||
|
end
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem 'rspec-rails', '2.14.2'
|
gem 'rspec-rails', '2.14.2'
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@
|
||||||
//= require jquery.exists
|
//= require jquery.exists
|
||||||
//= require jquery.payment
|
//= require jquery.payment
|
||||||
//= require jquery.visible
|
//= require jquery.visible
|
||||||
|
//= require reflux
|
||||||
//= require howler.core.js
|
//= require howler.core.js
|
||||||
//= require jstz
|
//= require jstz
|
||||||
//= require class
|
//= require class
|
||||||
|
|
@ -50,6 +51,10 @@
|
||||||
//= require utils
|
//= require utils
|
||||||
//= require subscription_utils
|
//= require subscription_utils
|
||||||
//= require custom_controls
|
//= require custom_controls
|
||||||
|
//= require react
|
||||||
|
//= require react_ujs
|
||||||
|
//= require react-init
|
||||||
|
//= require react-components
|
||||||
//= require web/signup_helper
|
//= require web/signup_helper
|
||||||
//= require web/signin_helper
|
//= require web/signin_helper
|
||||||
//= require web/signin
|
//= require web/signin
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,37 @@
|
||||||
# one time init stuff for the /client view
|
# one time init stuff for the /client view
|
||||||
|
|
||||||
|
|
||||||
$ = jQuery
|
$ = jQuery
|
||||||
context = window
|
context = window
|
||||||
context.JK ||= {};
|
context.JK ||= {};
|
||||||
|
broadcastActions = context.JK.Actions.Broadcast
|
||||||
|
|
||||||
context.JK.ClientInit = class ClientInit
|
context.JK.ClientInit = class ClientInit
|
||||||
constructor: () ->
|
constructor: () ->
|
||||||
@logger = context.JK.logger
|
@logger = context.JK.logger
|
||||||
@gearUtils = context.JK.GearUtils
|
@gearUtils = context.JK.GearUtils
|
||||||
|
@ALERT_NAMES = context.JK.ALERT_NAMES;
|
||||||
|
@lastCheckedBroadcast = null
|
||||||
|
|
||||||
init: () =>
|
init: () =>
|
||||||
if context.gon.isNativeClient
|
if context.gon.isNativeClient
|
||||||
this.nativeClientInit()
|
this.nativeClientInit()
|
||||||
|
|
||||||
|
context.JK.onBackendEvent(@ALERT_NAMES.WINDOW_OPEN_FOREGROUND_MODE, 'client_init', @watchBroadcast);
|
||||||
|
|
||||||
|
this.watchBroadcast()
|
||||||
|
|
||||||
|
checkBroadcast: () =>
|
||||||
|
broadcastActions.load.triggerPromise()
|
||||||
|
|
||||||
|
watchBroadcast: () =>
|
||||||
|
if context.JK.currentUserId
|
||||||
|
# create a 15 second buffer to not check too fast for some reason (like the client firing multiple foreground events)
|
||||||
|
if !@lastCheckedBroadcast? || @lastCheckedBroadcast.getTime() < new Date().getTime() - 15000
|
||||||
|
@lastCheckedBroadcast = new Date()
|
||||||
|
setTimeout(@checkBroadcast, 3000)
|
||||||
|
|
||||||
|
|
||||||
nativeClientInit: () =>
|
nativeClientInit: () =>
|
||||||
@gearUtils.bootstrapDefaultPlaybackProfile();
|
@gearUtils.bootstrapDefaultPlaybackProfile();
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,26 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBroadcastNotification(options) {
|
||||||
|
var userId = getId(options);
|
||||||
|
return $.ajax({
|
||||||
|
type: "GET",
|
||||||
|
url: "/api/users/" + userId + "/broadcast_notification"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function quietBroadcastNotification(options) {
|
||||||
|
var userId = getId(options);
|
||||||
|
var broadcast_id = options.broadcast_id;
|
||||||
|
return $.ajax({
|
||||||
|
type: "POST",
|
||||||
|
dataType: "json",
|
||||||
|
contentType: 'application/json',
|
||||||
|
url: "/api/users/" + userId + "/broadcast_notification/" + broadcast_id + '/quiet',
|
||||||
|
data: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function uploadMusicNotations(formData) {
|
function uploadMusicNotations(formData) {
|
||||||
return $.ajax({
|
return $.ajax({
|
||||||
type: "POST",
|
type: "POST",
|
||||||
|
|
@ -1780,6 +1800,8 @@
|
||||||
this.createScheduledSession = createScheduledSession;
|
this.createScheduledSession = createScheduledSession;
|
||||||
this.uploadMusicNotations = uploadMusicNotations;
|
this.uploadMusicNotations = uploadMusicNotations;
|
||||||
this.getMusicNotation = getMusicNotation;
|
this.getMusicNotation = getMusicNotation;
|
||||||
|
this.getBroadcastNotification = getBroadcastNotification;
|
||||||
|
this.quietBroadcastNotification = quietBroadcastNotification;
|
||||||
this.legacyJoinSession = legacyJoinSession;
|
this.legacyJoinSession = legacyJoinSession;
|
||||||
this.joinSession = joinSession;
|
this.joinSession = joinSession;
|
||||||
this.cancelSession = cancelSession;
|
this.cancelSession = cancelSession;
|
||||||
|
|
@ -1930,9 +1952,7 @@
|
||||||
this.createAlert = createAlert;
|
this.createAlert = createAlert;
|
||||||
this.signup = signup;
|
this.signup = signup;
|
||||||
this.portOverCarts = portOverCarts;
|
this.portOverCarts = portOverCarts;
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
})(window,jQuery);
|
})(window,jQuery);
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,12 @@
|
||||||
var gridRows = layout.split('x')[0];
|
var gridRows = layout.split('x')[0];
|
||||||
var gridCols = layout.split('x')[1];
|
var gridCols = layout.split('x')[1];
|
||||||
|
|
||||||
$grid.children().each(function () {
|
var gutterWidth = 0;
|
||||||
|
|
||||||
|
var findCardLayout;
|
||||||
|
var feedCardLayout;
|
||||||
|
|
||||||
|
$grid.find('.homecard').each(function () {
|
||||||
var childPosition = $(this).attr("layout-grid-position");
|
var childPosition = $(this).attr("layout-grid-position");
|
||||||
var childRow = childPosition.split(',')[1];
|
var childRow = childPosition.split(',')[1];
|
||||||
var childCol = childPosition.split(',')[0];
|
var childCol = childPosition.split(',')[0];
|
||||||
|
|
@ -211,6 +216,13 @@
|
||||||
var childLayout = me.getCardLayout(gridWidth, gridHeight, gridRows, gridCols,
|
var childLayout = me.getCardLayout(gridWidth, gridHeight, gridRows, gridCols,
|
||||||
childRow, childCol, childRowspan, childColspan);
|
childRow, childCol, childRowspan, childColspan);
|
||||||
|
|
||||||
|
if($(this).is('.feed')) {
|
||||||
|
feedCardLayout = childLayout;
|
||||||
|
}
|
||||||
|
else if($(this).is('.findsession')) {
|
||||||
|
findCardLayout = childLayout;
|
||||||
|
}
|
||||||
|
|
||||||
$(this).animate({
|
$(this).animate({
|
||||||
width: childLayout.width,
|
width: childLayout.width,
|
||||||
height: childLayout.height,
|
height: childLayout.height,
|
||||||
|
|
@ -218,6 +230,18 @@
|
||||||
left: childLayout.left
|
left: childLayout.left
|
||||||
}, opts.animationDuration);
|
}, opts.animationDuration);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var broadcastWidth = findCardLayout.width + feedCardLayout.width;
|
||||||
|
|
||||||
|
layoutBroadcast(broadcastWidth, findCardLayout.left);
|
||||||
|
}
|
||||||
|
|
||||||
|
function layoutBroadcast(width, left) {
|
||||||
|
var css = {
|
||||||
|
width:width + opts.gridPadding * 2,
|
||||||
|
left:left
|
||||||
|
}
|
||||||
|
$('[data-react-class="BroadcastHolder"]').animate(css, opts.animationDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
function layoutSidebar(screenWidth, screenHeight) {
|
function layoutSidebar(screenWidth, screenHeight) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
//= require ./react-components/actions/BroadcastActions
|
||||||
|
//= require ./react-components/stores/BroadcastStore
|
||||||
|
//= require_directory ./react-components
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
context = window
|
||||||
|
|
||||||
|
broadcastActions = window.JK.Actions.Broadcast;
|
||||||
|
rest = window.JK.Rest();
|
||||||
|
|
||||||
|
Broadcast = React.createClass({
|
||||||
|
displayName: 'Broadcast Notification'
|
||||||
|
|
||||||
|
handleNavigate: (e) ->
|
||||||
|
href = $(e.currentTarget).attr('href')
|
||||||
|
|
||||||
|
if href.indexOf('http') == 0
|
||||||
|
e.preventDefault()
|
||||||
|
window.JK.popExternalLink(href)
|
||||||
|
|
||||||
|
broadcastActions.hide.trigger()
|
||||||
|
|
||||||
|
|
||||||
|
notNow: (e) ->
|
||||||
|
e.preventDefault();
|
||||||
|
rest.quietBroadcastNotification({broadcast_id: this.props.notification.id})
|
||||||
|
broadcastActions.hide.trigger()
|
||||||
|
|
||||||
|
|
||||||
|
createMarkup: () ->
|
||||||
|
{__html: this.props.notification.message};
|
||||||
|
|
||||||
|
|
||||||
|
render: () ->
|
||||||
|
`<div className="broadcast-notification">
|
||||||
|
<div className="message" dangerouslySetInnerHTML={this.createMarkup()}/>
|
||||||
|
<div className="actions">
|
||||||
|
<div className="actionsAligner">
|
||||||
|
<a className="button-orange" onClick={this.handleNavigate}
|
||||||
|
href={this.props.notification.button_url}>{this.props.notification.button_label}</a>
|
||||||
|
<br/>
|
||||||
|
<a className="not-now" href="#" onClick={this.notNow}>not now, thanks</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
})
|
||||||
|
|
||||||
|
context.JK.Components.Broadcast = Broadcast
|
||||||
|
context.Broadcast = Broadcast
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
context = window
|
||||||
|
|
||||||
|
ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
|
||||||
|
|
||||||
|
BroadcastHolder = React.createClass(
|
||||||
|
{
|
||||||
|
displayName: 'Broadcast Notification Holder',
|
||||||
|
|
||||||
|
mixins: [Reflux.connect(context.JK.Stores.Broadcast, 'notification')]
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
notification = []
|
||||||
|
if this.state.notification
|
||||||
|
notification.push(`<Broadcast key={this.state.notification.id} notification={this.state.notification}/>`)
|
||||||
|
|
||||||
|
|
||||||
|
`<div id="broadcast-notification-holder" className="broadcast-notification-holder" >
|
||||||
|
<ReactCSSTransitionGroup transitionName="bn-slidedown">
|
||||||
|
{notification}
|
||||||
|
</ReactCSSTransitionGroup>
|
||||||
|
</div>`
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
context.JK.Components.BroadcastHolder = BroadcastHolder
|
||||||
|
context.BroadcastHolder = BroadcastHolder
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
context = window
|
||||||
|
|
||||||
|
BroadcastActions = Reflux.createActions({
|
||||||
|
load: {asyncResult: true},
|
||||||
|
hide: {}
|
||||||
|
})
|
||||||
|
|
||||||
|
context.JK.Actions.Broadcast = BroadcastActions
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
$ = jQuery
|
||||||
|
context = window
|
||||||
|
logger = context.JK.logger
|
||||||
|
broadcastActions = context.JK.Actions.Broadcast
|
||||||
|
|
||||||
|
rest = context.JK.Rest()
|
||||||
|
|
||||||
|
broadcastActions.load.listenAndPromise(rest.getBroadcastNotification);
|
||||||
|
|
||||||
|
BroadcastStore = Reflux.createStore(
|
||||||
|
{
|
||||||
|
listenables: broadcastActions
|
||||||
|
|
||||||
|
onLoad: () ->
|
||||||
|
logger.debug("loading broadcast notification...")
|
||||||
|
|
||||||
|
onLoadCompleted: (response) ->
|
||||||
|
logger.debug("broadcast notification sync completed")
|
||||||
|
this.trigger(response)
|
||||||
|
|
||||||
|
onLoadFailed: (jqXHR) ->
|
||||||
|
if jqXHR.status != 404
|
||||||
|
logger.error("broadcast notification sync failed")
|
||||||
|
|
||||||
|
onHide: () ->
|
||||||
|
this.trigger(null)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
context.JK.Stores.Broadcast = BroadcastStore
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
window.JK.Actions = {}
|
||||||
|
window.JK.Stores = {}
|
||||||
|
window.JK.Components = {}
|
||||||
|
|
@ -81,4 +81,5 @@
|
||||||
*= require ./jamTrackPreview
|
*= require ./jamTrackPreview
|
||||||
*= require users/signinCommon
|
*= require users/signinCommon
|
||||||
*= require landings/partner_agreement_v1
|
*= require landings/partner_agreement_v1
|
||||||
|
*= require_directory ./react-components
|
||||||
*/
|
*/
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
@import 'client/common';
|
||||||
|
|
||||||
|
[data-react-class="BroadcastHolder"] {
|
||||||
|
position:absolute;
|
||||||
|
min-height:60px;
|
||||||
|
top:62px;
|
||||||
|
@include border_box_sizing;
|
||||||
|
|
||||||
|
.broadcast-notification {
|
||||||
|
position:absolute;
|
||||||
|
border-width:1px;
|
||||||
|
border-color:$ColorScreenPrimary;
|
||||||
|
border-style:solid;
|
||||||
|
padding:5px 12px;
|
||||||
|
height:100%;
|
||||||
|
width:100%;
|
||||||
|
left:0;
|
||||||
|
top:0;
|
||||||
|
overflow:hidden;
|
||||||
|
margin-left:60px;
|
||||||
|
@include border_box_sizing;
|
||||||
|
|
||||||
|
}
|
||||||
|
.message {
|
||||||
|
float:left;
|
||||||
|
width:calc(100% - 150px);
|
||||||
|
font-size:12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
float:right;
|
||||||
|
text-align: right;
|
||||||
|
vertical-align: middle;
|
||||||
|
display:inline;
|
||||||
|
height:100%;
|
||||||
|
width:150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionsAligner {
|
||||||
|
display:inline-block;
|
||||||
|
vertical-align:middle;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
a { display:inline-block; }
|
||||||
|
|
||||||
|
.button-orange {
|
||||||
|
font-size:16px;
|
||||||
|
position:relative;
|
||||||
|
top:8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-now {
|
||||||
|
font-size:11px;
|
||||||
|
top:13px;
|
||||||
|
position:relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.bn-slidedown-enter {
|
||||||
|
max-height:0;
|
||||||
|
opacity: 0.01;
|
||||||
|
transition: max-height .5s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bn-slidedown-enter.bn-slidedown-enter-active {
|
||||||
|
max-height:60px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bn-slidedown-leave {
|
||||||
|
max-height:60px;
|
||||||
|
transition: max-height .5s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bn-slidedown-leave.bn-slidedown-leave-active {
|
||||||
|
max-height:0;
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ class ApiUsersController < ApiController
|
||||||
:band_invitation_index, :band_invitation_show, :band_invitation_update, # band invitations
|
:band_invitation_index, :band_invitation_show, :band_invitation_update, # band invitations
|
||||||
:set_password, :begin_update_email, :update_avatar, :delete_avatar, :generate_filepicker_policy,
|
:set_password, :begin_update_email, :update_avatar, :delete_avatar, :generate_filepicker_policy,
|
||||||
:share_session, :share_recording,
|
:share_session, :share_recording,
|
||||||
:affiliate_report, :audio_latency]
|
:affiliate_report, :audio_latency, :broadcast_notification]
|
||||||
|
|
||||||
respond_to :json
|
respond_to :json
|
||||||
|
|
||||||
|
|
@ -804,6 +804,30 @@ class ApiUsersController < ApiController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def broadcast_notification
|
||||||
|
@broadcast = BroadcastNotification.next_broadcast(current_user)
|
||||||
|
|
||||||
|
if @broadcast
|
||||||
|
# mark it as viewed
|
||||||
|
@broadcast.did_view(current_user)
|
||||||
|
respond_with_model(@broadcast)
|
||||||
|
else
|
||||||
|
render json: { message: 'Not Found'}, status: 404
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# used to hide a broadcast notification from rotation temporarily
|
||||||
|
def quiet_broadcast_notification
|
||||||
|
@broadcast = BroadcastNotificationView.find_by_broadcast_notification_id_and_user_id(params[:broadcast_id], current_user.id)
|
||||||
|
|
||||||
|
if @broadcast
|
||||||
|
@broadcast.active_at = Date.today + 14 # 14 days in the future we'll re-instas
|
||||||
|
@broadcast.save
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: { }, status: 200
|
||||||
|
end
|
||||||
|
|
||||||
###################### RECORDINGS #######################
|
###################### RECORDINGS #######################
|
||||||
# def recording_index
|
# def recording_index
|
||||||
# @recordings = User.recording_index(current_user, params[:id])
|
# @recordings = User.recording_index(current_user, params[:id])
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
object @broadcast
|
||||||
|
|
||||||
|
attributes :id, :message, :button_label, :button_url
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
<div class="dialog-overlay op70" style="display:none; width:100%; height:100%; z-index:99;"></div>
|
<div class="dialog-overlay op70" style="display:none; width:100%; height:100%; z-index:99;"></div>
|
||||||
|
|
||||||
<%= render "header" %>
|
<%= render "header" %>
|
||||||
|
<%= react_component 'BroadcastHolder', {} %>
|
||||||
<%= render "home" %>
|
<%= render "home" %>
|
||||||
<%= render "footer" %>
|
<%= render "footer" %>
|
||||||
<%= render "paginator" %>
|
<%= render "paginator" %>
|
||||||
|
|
@ -81,6 +82,7 @@
|
||||||
<%= render 'dialogs/dialogs' %>
|
<%= render 'dialogs/dialogs' %>
|
||||||
<div id="fb-root"></div>
|
<div id="fb-root"></div>
|
||||||
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(function() {
|
$(function() {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"vendor": {
|
||||||
|
"name": "bower-rails generated vendor assets",
|
||||||
|
"dependencies": {
|
||||||
|
"fluxxor": "1.5.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -34,8 +34,10 @@ bundle install --path vendor/bundle
|
||||||
#bundle update
|
#bundle update
|
||||||
|
|
||||||
set +e
|
set +e
|
||||||
|
echo "cleaning assets from last build"
|
||||||
# clean assets, because they may be lingering from last build
|
# clean assets, because they may be lingering from last build
|
||||||
bundle exec rake assets:clean
|
# bundle exec rake assets:clean
|
||||||
|
rm -rf $DIR/public/assets
|
||||||
|
|
||||||
if [ "$?" = "0" ]; then
|
if [ "$?" = "0" ]; then
|
||||||
echo "success: updated dependencies"
|
echo "success: updated dependencies"
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ if defined?(Bundler)
|
||||||
|
|
||||||
# Add the assets/fonts directory to assets.paths
|
# Add the assets/fonts directory to assets.paths
|
||||||
config.assets.paths << "#{Rails.root}/app/assets/fonts"
|
config.assets.paths << "#{Rails.root}/app/assets/fonts"
|
||||||
|
config.assets.paths << Rails.root.join('vendor', 'assets', 'bower_components')
|
||||||
|
|
||||||
# Precompile additional assets (application.js, application.css, and all non-JS/CSS (i.e., images) are already added)
|
# Precompile additional assets (application.js, application.css, and all non-JS/CSS (i.e., images) are already added)
|
||||||
config.assets.precompile += %w( client/client.css )
|
config.assets.precompile += %w( client/client.css )
|
||||||
|
|
@ -85,7 +86,6 @@ if defined?(Bundler)
|
||||||
config.assets.precompile += %w( web/web.js web/web.css )
|
config.assets.precompile += %w( web/web.js web/web.css )
|
||||||
config.assets.precompile += %w( minimal/minimal.js minimal/minimal.css )
|
config.assets.precompile += %w( minimal/minimal.js minimal/minimal.css )
|
||||||
|
|
||||||
|
|
||||||
# where is rabbitmq?
|
# where is rabbitmq?
|
||||||
config.rabbitmq_host = "localhost"
|
config.rabbitmq_host = "localhost"
|
||||||
config.rabbitmq_port = 5672
|
config.rabbitmq_port = 5672
|
||||||
|
|
@ -348,5 +348,8 @@ if defined?(Bundler)
|
||||||
config.web_performance_timing_enabled = true
|
config.web_performance_timing_enabled = true
|
||||||
config.jamtrack_landing_bubbles_enabled = true
|
config.jamtrack_landing_bubbles_enabled = true
|
||||||
config.jamtrack_browser_bubbles_enabled = true
|
config.jamtrack_browser_bubbles_enabled = true
|
||||||
|
|
||||||
|
config.react.variant = :production
|
||||||
|
config.react.addons = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -96,4 +96,6 @@ SampleApp::Application.configure do
|
||||||
config.email_generic_from = 'nobody-dev@jamkazam.com'
|
config.email_generic_from = 'nobody-dev@jamkazam.com'
|
||||||
config.email_alerts_alias = ENV['ALERT_EMAIL'] || 'alerts-dev@jamkazam.com'
|
config.email_alerts_alias = ENV['ALERT_EMAIL'] || 'alerts-dev@jamkazam.com'
|
||||||
config.guard_against_fraud = true
|
config.guard_against_fraud = true
|
||||||
|
|
||||||
|
config.react.variant = :development
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -374,6 +374,10 @@ SampleApp::Application.routes.draw do
|
||||||
match '/users/:id/share/session/:provider' => 'api_users#share_session', :via => :get
|
match '/users/:id/share/session/:provider' => 'api_users#share_session', :via => :get
|
||||||
match '/users/:id/share/recording/:provider' => 'api_users#share_recording', :via => :get
|
match '/users/:id/share/recording/:provider' => 'api_users#share_recording', :via => :get
|
||||||
|
|
||||||
|
# broadcast notification
|
||||||
|
match '/users/:id/broadcast_notification' => 'api_users#broadcast_notification', :via => :get
|
||||||
|
match '/users/:id/broadcast_notification/:broadcast_id/quiet' => 'api_users#quiet_broadcast_notification', :via => :post
|
||||||
|
|
||||||
# session chat
|
# session chat
|
||||||
match '/chat' => 'api_chats#create', :via => :post
|
match '/chat' => 'api_chats#create', :via => :post
|
||||||
match '/sessions/:music_session/chats' => 'api_chats#index', :via => :get
|
match '/sessions/:music_session/chats' => 'api_chats#index', :via => :get
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "jam-web",
|
||||||
|
"dependencies" : {
|
||||||
|
"browserify": "~> 6.3",
|
||||||
|
"browserify-incremental": "^1.4.0",
|
||||||
|
"coffeeify": "~> 0.6",
|
||||||
|
"reactify": "^0.17.1",
|
||||||
|
"coffee-reactify": "~>3.0.0",
|
||||||
|
"react": "^0.12.0",
|
||||||
|
"react-tools": "^0.12.1"
|
||||||
|
},
|
||||||
|
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -778,6 +778,13 @@ FactoryGirl.define do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
factory :broadcast_notification, :class => JamRuby::BroadcastNotification do
|
||||||
|
title Faker::Lorem.sentence[0...50]
|
||||||
|
message Faker::Lorem.paragraph[0...200]
|
||||||
|
button_label Faker::Lorem.words(2).join(' ')[0...14]
|
||||||
|
frequency 3
|
||||||
|
end
|
||||||
|
|
||||||
factory :affiliate_partner, class: 'JamRuby::AffiliatePartner' do
|
factory :affiliate_partner, class: 'JamRuby::AffiliatePartner' do
|
||||||
sequence(:partner_name) { |n| "partner-#{n}" }
|
sequence(:partner_name) { |n| "partner-#{n}" }
|
||||||
entity_type 'Individual'
|
entity_type 'Individual'
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe "broadcast notification", :js => true, :type => :feature, :capybara_feature => true do
|
||||||
|
|
||||||
|
subject { page }
|
||||||
|
|
||||||
|
let(:broadcast1) {FactoryGirl.create(:broadcast_notification, frequency: 4)}
|
||||||
|
let(:broadcast2) {FactoryGirl.create(:broadcast_notification, frequency: 4)}
|
||||||
|
let(:user) { FactoryGirl.create(:user) }
|
||||||
|
let(:partner) { FactoryGirl.create(:affiliate_partner) }
|
||||||
|
|
||||||
|
before(:each) do
|
||||||
|
BroadcastNotificationView.delete_all
|
||||||
|
BroadcastNotification.delete_all
|
||||||
|
end
|
||||||
|
|
||||||
|
it "cycles on home screen" do
|
||||||
|
broadcast1.touch
|
||||||
|
|
||||||
|
fast_signin(user, '/client')
|
||||||
|
find('.broadcast-notification .message', text: broadcast1.message)
|
||||||
|
|
||||||
|
broadcast2.touch
|
||||||
|
|
||||||
|
visit current_path
|
||||||
|
find('.broadcast-notification .message', text: broadcast2.message)
|
||||||
|
|
||||||
|
visit current_path
|
||||||
|
find('.broadcast-notification .message', text: broadcast1.message)
|
||||||
|
find('.broadcast-notification .not-now').trigger(:click)
|
||||||
|
|
||||||
|
visit current_path
|
||||||
|
find('.broadcast-notification .message', text: broadcast2.message)
|
||||||
|
|
||||||
|
go_to_root
|
||||||
|
|
||||||
|
visit '/client'
|
||||||
|
find('.broadcast-notification .message', text: broadcast2.message)
|
||||||
|
end
|
||||||
|
end
|
||||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue