This commit is contained in:
Seth Call 2015-06-09 17:36:11 -05:00
parent 02a96e93e8
commit ea0c2015d1
26 changed files with 309 additions and 103 deletions

View File

@ -22,12 +22,5 @@ ActiveAdmin.register JamRuby::BroadcastNotification, :as => 'BroadcastNotificati
end end
end end
controller do
def create
resource_class.create(params[:broadcast_notification])
redirect_to(admin_broadcast_notifications_path)
end
end
end end

View File

@ -16,3 +16,6 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
pg_migrate (= 0.1.13) pg_migrate (= 0.1.13)
BUNDLED WITH
1.10.3

View File

@ -14,6 +14,7 @@ CREATE TABLE broadcast_notification_views (
user_id varchar(64) NOT NULL REFERENCES users(id), user_id varchar(64) NOT NULL REFERENCES users(id),
broadcast_notification_id varchar(64) NOT NULL REFERENCES broadcast_notifications(id) ON DELETE CASCADE, broadcast_notification_id varchar(64) NOT NULL REFERENCES broadcast_notifications(id) ON DELETE CASCADE,
view_count INTEGER DEFAULT 0, view_count INTEGER DEFAULT 0,
active_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
); );

View File

@ -2,6 +2,7 @@ module JamRuby
class BroadcastNotification < ActiveRecord::Base class BroadcastNotification < ActiveRecord::Base
attr_accessible :title, :message, :button_label, :frequency, :button_url, as: :admin attr_accessible :title, :message, :button_label, :frequency, :button_url, as: :admin
has_many :user_views, class_name: 'JamRuby::BroadcastNotificationView', dependent: :destroy has_many :user_views, class_name: 'JamRuby::BroadcastNotificationView', dependent: :destroy
validates :button_label, presence: true, length: {maximum: 14} validates :button_label, presence: true, length: {maximum: 14}
@ -16,8 +17,9 @@ module JamRuby
self.select('broadcast_notifications.*, bnv.updated_at AS bnv_updated_at') 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}'))") .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(['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']) .where(['bnv.user_id IS NULL OR broadcast_notifications.frequency > bnv.view_count'])
.order('bnv_updated_at') .order('bnv_updated_at NULLS FIRST')
end end
def did_view(user) def did_view(user)
@ -25,6 +27,13 @@ module JamRuby
.where(broadcast_notification_id: self.id, user_id: user.id) .where(broadcast_notification_id: self.id, user_id: user.id)
.limit(1) .limit(1)
.first .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 = user_views.new(user: user) unless bnv
bnv.view_count += 1 bnv.view_count += 1
bnv.save bnv.save

View File

@ -23,7 +23,7 @@ describe BroadcastNotification do
it 'gets view incremented' do it 'gets view incremented' do
bnv = broadcast1.did_view(user1) bnv = broadcast1.did_view(user1)
bnv = broadcast1.did_view(user1) bnv = broadcast1.did_view(user1)
expect(bnv.view_count).to be eq(2) bnv.view_count.should eq(2)
end end
it 'loads viewable broadcasts for a user' do it 'loads viewable broadcasts for a user' do

View File

@ -1,26 +1,34 @@
# 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
broadcastActions = BroadcastNotificationActions # require('./react-components/actions/BroadcastNotificationActions')
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()
setTimeout(this.checkBroadcastNotification, 3000) context.JK.onBackendEvent(@ALERT_NAMES.WINDOW_OPEN_FOREGROUND_MODE, 'client_init', @watchBroadcast);
checkBroadcastNotification: () => this.watchBroadcast()
checkBroadcast: () =>
broadcastActions.load.triggerPromise()
watchBroadcast: () =>
if context.JK.currentUserId if context.JK.currentUserId
broadcastActions.load.triggerPromise() # 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: () =>

View File

@ -64,7 +64,19 @@
}); });
} }
function uploadMusicNotations(formData) { 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) {
return $.ajax({ return $.ajax({
type: "POST", type: "POST",
processData: false, processData: false,
@ -1789,6 +1801,7 @@
this.uploadMusicNotations = uploadMusicNotations; this.uploadMusicNotations = uploadMusicNotations;
this.getMusicNotation = getMusicNotation; this.getMusicNotation = getMusicNotation;
this.getBroadcastNotification = getBroadcastNotification; this.getBroadcastNotification = getBroadcastNotification;
this.quietBroadcastNotification = quietBroadcastNotification;
this.legacyJoinSession = legacyJoinSession; this.legacyJoinSession = legacyJoinSession;
this.joinSession = joinSession; this.joinSession = joinSession;
this.cancelSession = cancelSession; this.cancelSession = cancelSession;

View File

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

View File

@ -1,7 +1,30 @@
//= require_self //= require_self
//= require react_ujs //= require react_ujs
React = require('react'); // this pulls in react + addons (like CSS transitions)
React = require('react/addons');
context = window
var actions = {}
var stores = {}
var components = {}
// create globally available references to all actions, stores, and components
context.JK.Actions = actions
context.JK.Stores = stores
context.JK.Components = components
// FLUX ACTIONS
actions.broadcast = require('./react-components/actions/BroadcastActions')
// FLUX STORES
stores.broadcast = require('./react-components/stores/BroadcastStore');
// REACT COMPONENTS
// NOTE: be sure to give each component a global name so that you can use the <%= react_component "ComponentName" %> directive or in JSX
components.broadcastHolder = BroadcastHolder = require('./react-components/BroadcastHolder')
components.broadcast = Broadcast = require('./react-components/Broadcast')
BroadcastNotificationActions = require('./react-components/actions/BroadcastNotificationActions')
BroadcastNotification = require('./react-components/BroadcastNotification')

View File

@ -0,0 +1,51 @@
var React = require('react');
var broadcastActions = window.JK.Actions.broadcast;
var rest = window.JK.Rest();
var Broadcast = React.createClass({
displayName: 'Broadcast Notification',
handleNavigate: function (e) {
var href = $(e.currentTarget).attr('href')
if (href.indexOf('http') == 0) {
e.preventDefault()
window.JK.popExternalLink(href)
}
broadcastActions.hide.trigger()
},
notNow: function (e) {
e.preventDefault();
rest.quietBroadcastNotification({broadcast_id: this.props.notification.id})
broadcastActions.hide.trigger()
},
createMarkup: function() {
return {__html: this.props.notification.message};
},
render: function () {
return <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>
}
});
// each file will export exactly one component
module.exports = Broadcast;

View File

@ -0,0 +1,27 @@
var React = require('react');
var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
var broadcastStore = window.JK.Stores.broadcast;
var BroadcastHolder = React.createClass({displayName: 'Broadcast Notification Holder',
mixins: [Reflux.connect(broadcastStore, 'notification')],
render: function() {
var notification = []
if(this.state.notification) {
notification.push(<Broadcast key={this.state.notification.id} notification={this.state.notification}/>)
}
return (
<div id="broadcast-notification-holder" className="broadcast-notification-holder" >
<ReactCSSTransitionGroup transitionName="bn-slidedown">
{notification}
</ReactCSSTransitionGroup>
</div>
)
}
});
// each file will export exactly one component
module.exports = BroadcastHolder;

View File

@ -1,25 +0,0 @@
var React = require('react');
var BroadcastNotificationStore = require('./stores/BroadcastNotificationStore');
var BroadcastNotification = React.createClass({displayName: 'Broadcast Notification',
mixins: [Reflux.connect(BroadcastNotificationStore, 'notification')],
render: function() {
if(!this.state.notification) {
return <div>HAHAHAAH</div>
}
return <div className="broadcast-notification">
<div className="message" dangerouslySetInnerHTML={this.state.notification.message}/>
<div className="actions">
<a className="button-orange"
href={this.state.notification.button_url}>{this.state.notification.button_label}</a>
<a className="not-now" href="#">not now, thanks</a>
</div>
</div>
}
});
// each file will export exactly one component
module.exports = BroadcastNotification;

View File

@ -1,10 +0,0 @@
var React = require('react');
var DemoComponent = React.createClass({displayName: 'Demo Component',
render: function() {
return <div>Demo Component</div>;
}
});
// each file will export exactly one component
module.exports = DemoComponent;

View File

@ -0,0 +1,8 @@
BroadcastActions = Reflux.createActions({
load: {asyncResult: true},
hide: {}
})
module.exports = BroadcastActions

View File

@ -1,8 +0,0 @@
//var rest = context.JK.Rest()
var BroadcastNotificationActions = Reflux.createActions({
load: {asyncResult: true}
})
module.exports = BroadcastNotificationActions

View File

@ -1,35 +0,0 @@
$ = jQuery
context = window
logger = context.JK.logger
broadcastActions = BroadcastNotificationActions # require('../actions/BroadcastNotificationActions')
rest = context.JK.Rest()
# see if this shows up elsewhere
broadcastActions.blah = 'hahah'
broadcastActions.load.listenAndPromise(rest.getBroadcastNotification);
console.log("broadcastActions!!", broadcastActions)
BroadcastNotificationStore = Reflux.createStore(
{
listenables: broadcastActions
init: () =>
logger.debug("broadcast notification store init")
#this.listenTo(broadcastActions.load, 'onSync')
onLoad: () =>
logger.debug("loading broadcast notification...")
onLoadCompleted: () =>
logger.debug("broadcast notification sync completed")
onLoadFailed: (jqXHR) =>
if jqXHR.status != 404
logger.error("broadcast notification sync failed")
}
)
module.exports = BroadcastNotificationStore

View File

@ -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)
}
)
module.exports = BroadcastStore

View File

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

View File

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

View File

@ -814,7 +814,18 @@ class ApiUsersController < ApiController
else else
render json: { message: 'Not Found'}, status: 404 render json: { message: 'Not Found'}, status: 404
end 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 end
###################### RECORDINGS ####################### ###################### RECORDINGS #######################

View File

@ -1,3 +1,3 @@
object @broadcast object @broadcast
attributes :message, :button_label, :button_url attributes :id, :message, :button_label, :button_url

View File

@ -10,7 +10,7 @@
/ Grid is the count of the smallest spaces, then / Grid is the count of the smallest spaces, then
/ individual spells span those spaces / individual spells span those spaces
-if @nativeClient -if @nativeClient
.grid layout-grid="2x12" .grid layout-grid="2x12"
.homecard.createsession layout-grid-columns="4" layout-grid-position="0,0" layout-grid-rows="1" layout-link="createSession" type="createSession" class="#{logged_in_not_logged_in_class}" .homecard.createsession layout-grid-columns="4" layout-grid-position="0,0" layout-grid-rows="1" layout-link="createSession" type="createSession" class="#{logged_in_not_logged_in_class}"
h2 create session h2 create session
.homebox-info .homebox-info
@ -45,7 +45,7 @@
.homebox-info .homebox-info
/! free service level /! free service level
-else -else
.grid layout-grid="2x12" .grid layout-grid="2x12"
.homecard.createsession layout-grid-columns="4" layout-grid-position="0,0" layout-grid-rows="1" layout-link="createSession" type="createSession" class="#{logged_in_not_logged_in_class}" .homecard.createsession layout-grid-columns="4" layout-grid-position="0,0" layout-grid-rows="1" layout-link="createSession" type="createSession" class="#{logged_in_not_logged_in_class}"
h2 create session h2 create session
.homebox-info .homebox-info

View File

@ -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,7 +82,6 @@
<%= render 'dialogs/dialogs' %> <%= render 'dialogs/dialogs' %>
<div id="fb-root"></div> <div id="fb-root"></div>
<%= react_component 'BroadcastNotification', {} %>
<script type="text/javascript"> <script type="text/javascript">
$(function() { $(function() {

View File

@ -397,6 +397,6 @@ if defined?(Bundler)
config.react.variant = :production config.react.variant = :production
config.react.addons = true config.react.addons = true
config.browserify_rails.commandline_options = "-t coffeeify --extension=\".js.coffee\" --transform reactify --extension=\".jsx\"" config.browserify_rails.commandline_options = " --transform reactify --extension=\".jsx\" "
end end
end end

View File

@ -376,6 +376,7 @@ SampleApp::Application.routes.draw do
# broadcast notification # broadcast notification
match '/users/:id/broadcast_notification' => 'api_users#broadcast_notification', :via => :get 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

View File

@ -5,6 +5,7 @@
"browserify-incremental": "^1.4.0", "browserify-incremental": "^1.4.0",
"coffeeify": "~> 0.6", "coffeeify": "~> 0.6",
"reactify": "^0.17.1", "reactify": "^0.17.1",
"coffee-reactify": "~>3.0.0",
"react": "^0.12.0", "react": "^0.12.0",
"react-tools": "^0.12.1" "react-tools": "^0.12.1"
}, },