Hi i have implemented action cable using rails 5. its working fine locally but its not working on heroku. on heroku whenever i send any message it sends four messages, two duplicates and 2 blank messages. here is my code :-
conversation.js
App.conversation = App.cable.subscriptions.create("ConversationChannel", {
connected: function() {},
disconnected: function() {},
received: function(data) {
var conversation = $('#conversations-list').find("[data-conversation-id='" + data['conversation_id'].$oid + "']");
if (data['window'] !== undefined) {
var conversation_visible = conversation.is(':visible');
if (conversation_visible) {
var messages_visible = (conversation).find('.panel-body').is(':visible');
if (!messages_visible) {
conversation.removeClass('panel-default').addClass('panel-success');
conversation.find('.panel-body').toggle();
}
conversation.find('.messages-list').find('ul').append(data['message']);
}
else {
$('#conversations-list').append(data['window']);
conversation = $('#conversations-list').find("[data-conversation-id='" + data['conversation_id'].$oid + "']");
conversation.find('.panel-body').toggle();
}
}
else {
conversation.find('ul').append(data['message']);
}
var messages_list = conversation.find('.messages-list');
var height = messages_list[0].scrollHeight;
messages_list.scrollTop(height);
},
speak: function(message) {
return this.perform('speak', {
message: message
});
},
});
$(document).on('submit', '.new_message', function(e) {
e.preventDefault();
var values = $(this).serializeArray();
App.conversation.speak(values);
$(this).trigger('reset');
});
Application.js
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's
// vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require rails-ujs
// require turbolinks
//= require jquery-3.2.1.min
// require_tree .
(function() {
$(document).on('click', '.toggle-window', function(e) {
e.preventDefault();
var panel = $(this).parent().parent();
var messages_list = panel.find('.messages-list');
panel.find('.panel-body').toggle();
panel.attr('class', 'panel panel-default');
if (panel.find('.panel-body').is(':visible')) {
var height = messages_list[0].scrollHeight;
messages_list.scrollTop(height);
}
});
})();
Cable.js
//= require action_cable
//= require_self
//= require_tree ./channels
(function() {
this.App || (this.App = {});
App.cable = ActionCable.createConsumer();
}).call(this);
create.js
var conversations = $('#conversations-list');
var conversation = conversations.find("[data-conversation-id='" + "<%= #conversation.id %>" + "']");
if (conversation.length !== 1) {
conversations.append("<%= j(render 'conversations/conversation', conversation: #conversation, user: current_user) %>");
conversation = conversations.find("[data-conversation-id='" + "<%= #conversation.id %>" + "']");
}
conversation.find('.panel-body').show();
var messages_list = conversation.find('.messages-list');
var height = messages_list[0].scrollHeight;
messages_list.scrollTop(height);
Chat Screenshot
enter image description here
Please let me know how i can fix it. i am using rails 5 with ruby-2.4.0. i am also using redis server for jobs.
You set a Javascript event listener that has nothing to do with ActionCable.
Every time you trigger the submit bottom you call the App.conversation.speak() function that append the message on the page
$(document).on('submit', '.new_message', function(e) {
e.preventDefault();
var values = $(this).serializeArray();
App.conversation.speak(values);
$(this).trigger('reset');
});
this is your speak function
speak: function(message) {
return this.perform('speak', {
message: message
});
I quote Defining The Channel's Subscriber
We add our new subscription to our consumer with App.cable.subscriptions.create. We give this function an argument of the name of the channel to which we want to subscribe, ConversationChannel.
When this subscriptions.create function is invoked, it will invoke the ConversationChannel#subscribed method, which is in fact a callback method.
So what is a callback method? I can't answer clearly this question.
This method is responsible for subscribing to and streaming messages that are broadcast to this channel.
app/channels/conversation_channel.rb
class ConversationChannel < ApplicationCable::Channel
def subscribed
stream_from 'conversation'
end
end
that
ConversationChannel#subscribed streams from our messages broadcast, sending along any new messages as JSON to the client-side subscription function.
This is how I implement ActionCable, after the Message is saved to the db I trigger the following action in my MessagesController as in Sophie Debenedetto guide (I don't know if you save a Conversation to the DB or a Message)
app/controllers/messages_controller.rb
class MessagesController < ApplicationController
def create
message = Message.new(message_params)
message.user = current_user
if message.save
ActionCable.server.broadcast 'messages',
message: message.content,
user: message.user.username
head :ok
end
end
...
end
ActionCable.server.broadcast 'messages', sends a call to the received function inside App.cable.subscription and that function is responsible for updating the page with the new message.
This call will be performed only for the user that are subscribed to this event. The subscriptions are managed in the subscribed method of the ConversationChannel
App.conversation = App.cable.subscriptions.create('ConversationChannel', {
received: function(data) {
$('#messages').append(this.speak(data));
},
speak: function(data) {
return "<br><p> <strong>" + data.user + ": </strong>" + data.message + "</p>";
};
}
});
It passed the following data from your rails controller in the json format message: message.content, user: message.user.username
Some of this code is taken from my app https://sprachspiel.xyz that is an action cable app you can test, the app should be still working. This is the github repository
I believe you are calling your js function twice, or doing some workaround to make actioncable work that causes the div to be appended twice. I believe your are executing 2 different time js to run action cable.
Remember that action cable is a websocket meant to update that message on 2 different users/browsers
Related
I've created an html file with embedded Watson Virtual Agent chat bot, code similar below, with WVA strictly using the building core capabilities:
IBMChat.init({
el: 'ibm_chat_root',
baseURL: 'https://api.ibm.com/virtualagent/run/api/v1',
botID: '',
XIBMClientID: '',
XIBMClientSecret: ''
});
What I noticed is if I run the WVA in Preview mode, and have input "pay bill", the WVA can come back with two piece response, with first:
Accessing your account information...
and second the make payment:
Your account balance is $42.01 due on 5/17/2017. What would you like to do? (More options coming soon!)
However, if I enter the same in my HTML chatbot, the response only comes back with the first part:
Accessing your account information...
and second part never comes out.
Does anyone else experience the same problem?
The version in the "Preview" mode has some mock "action" handlers setup. Obviously, not every one of you users would owe $42! In the sample code on the github, the mock action handlers are not setup. There are examples on how to subscribe to those action events with handlers here: https://github.com/watson-virtual-agents/chat-widget/tree/master/examples/basic-actions-example
As of 5/31/17 you can cover all the built in actions using the code snippet below...
const config = { instance: null };
const getUserProfileVariablesMap = {
'bill_amount': '42.01',
'payment_due_date': (() => {
const currentDate = new Date(new Date().getTime() + 24 * 60 * 60 * 1000);
return `${currentDate.getMonth() + 1}/${currentDate.getDate()}/${currentDate.getFullYear()}`;
})(),
'authorized_users': 'Bob Everyman and Jane Doe'
};
const getUserProfileVariables = (data) => {
const variables = data.message.action.args.variables;
variables.forEach(v => {
const value = getUserProfileVariablesMap[v];
(value) ? config.instance.profile.set(v, value) : config.instance.profile.set(v, '[sample data]');
});
config.instance.sendSilently('success');
};
const success = () => config.instance.sendSilently('success');
const agent = () => config.instance.receive('On your own site you would run code to connect to an agent now.');
const accountSettings = () => config.instance.receive('On your own site you would run code to open the Account Settings page now.');
function registerActions(instance) {
config.instance = instance;
instance.subscribe('action:getUserProfileVariables', getUserProfileVariables);
instance.subscribe('action:updateAddress', success);
instance.subscribe('action:updateUserName', success);
instance.subscribe('action:updatePhoneNumber', success);
instance.subscribe('action:updateEmail', success);
instance.subscribe('action:payBill', success);
instance.subscribe('action:sendPaymentReceipt', success);
instance.subscribe('action:agent', agent);
instance.subscribe('action:openAccountSettingsPage', accountSettings);
};
window.IBMChatActions = {
registerActions: registerActions
};
// window.IBMChatActions.registerActions(window.IBMChat);
On the Administrative Preview, you are getting fake code stubs that handle action requests from the agent.
When one of these actions are invoked, the widget will print the "Processing..." message and then invoke all registered subscribers for that action. It is up to these registered subscribers to continue the conversation flow by silently sending "success", "failure", or "cancel" back to the server.
For example, the agent might pass down the "payBill" action. You would want to call your payment gateway, determine if it was successful, and then notify the agent of the result:
IBMChat.init(/* Settings */);
IBMChat.subscribe('action:payBill', function() {
var data = {
amount: IBMChat.profile.get('amount'),
card: {
number: IBMChat.profile.get('cc_number'),
// ... other private card data
}
};
$.post('https://www.myserver.com/payment-gateway', data)
.done( function() {
IBMChat.sendSilently('success');
})
.fail( function() {
IBMChat.sendSilently('failure');
});
});
Actions Documentation
https://github.com/watson-virtual-agents/chat-widget/blob/master/docs/DOCS.md#actions
I created a simple Rails 4 cms. There is an article list with disqus comment count on the homepage and also on the category and archive pages.
The disqus code responsible for displaying comment count is just before the closing body tag.
When I enable Turbolinks the comment count shows up only on initial page load. If I visit an article then go back to the article list, there is no comment count. If I reload the page the comment count is there though.
I tried to add jquery.turbolinks gem.
I tried to edit the disqus code so it executes on page:change or page:load.
I tried to put it inside the head.
I tried to add disqus identifiers to the code.
UPDATE:
Here is what I am trying right now (edited my shortname):
var disqus_shortname = 'my_shortname'; // required: replace example with your forum shortname
/* * * DON'T EDIT BELOW THIS LINE * * */
($(document).on('page:change',function () {
var s = document.createElement('script'); s.async = true;
s.type = 'text/javascript';
s.src = '//' + disqus_shortname + '.disqus.com/count.js';
(document.getElementsByTagName('HEAD')[0] || document.getElementsByTagName('BODY')[0]).appendChild(s);
})());
This way the comment count shows up on initial page load and also when I visit the post list pages the first time. But when I visit them the second time the comment count doesn't show up. Tried this with page:load and page:update also without results.
Anyone encountered this problem?
I couldn't make this work with the default disqus comment count code, so I used the disqus api instead which works like a charm:
$(document).on('page:change', function () {
var disqusPublicKey = "MY_PUBLIC_KEY"; // Replace with your own public key
var disqusShortname = "my_shortname"; // Replace with your own shortname
var urlArray = [];
$('.comment-link-marker').each(function () {
var url = $(this).attr('data-disqus-url');
urlArray.push('link:' + url);
});
$.ajax({
type: 'GET',
url: "https://disqus.com/api/3.0/threads/set.jsonp",
data: { api_key: disqusPublicKey, forum : disqusShortname, thread : urlArray },
cache: false,
dataType: 'jsonp',
success: function (result) {
for (var i in result.response) {
var countText = " comments";
var count = result.response[i].posts;
if (count == 1)
countText = " comments";
$('a[data-disqus-url="' + result.response[i].link + '"]').html(count + countText);
}
}
});
});
This is a Turbolinks issue. With Turbolinks pages will change without a full reload, so you can't rely on DOMContentLoaded or jQuery.ready() to trigger your code.
Try this: add jquery.turbolinks gem, update application.js and restart the server.
I've looked at multiple solutions to this problem but nothing's working to fix my problem.
I'm using ASP.NET MVC 4.5.
Here are my steps:
Use ajax call in page to upload file.
Within same function that generates ajax call run an ajax call to refresh the page to include the uploaded file, after ajax call is finished.
I'm using this as the first call (to upload) (compliments of another Stack Overflow user):
function uploadFiles() {
document.getElementById('fileupload').onsubmit = function () {
var formdata = new FormData(); //FormData object
var fileInput = document.getElementById('uploadfilenames');
//Iterating through each files selected in fileInput
for (i = 0; i < fileInput.files.length; i++) {
//Appending each file to FormData object
formdata.append(fileInput.files[i].name, fileInput.files[i]);
}
//Creating an XMLHttpRequest and sending
var xhr = new XMLHttpRequest();
xhr.open('POST', '/Dashboard/UploadFiles');
xhr.send(formdata);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
//alert(xhr.responseText);
}
}
return false;
}
reloadMain();
}
The reloadMain() function is:
function reloadMain() {
$.ajax({
url: '/Dashboard/ThumbList/' + currentPath,
type: "GET",
timeout: 5000,
success: function (msg) {
$("#thumb-list").html(msg)
},
error: displayError("Unable to get file listing")
});
}
I have noticed this:
The 'refresh' doesn't include the uploaded file information in the response
IE11 and Chrome act differently.
It seems that the problem is that the controller/system doesn't complete the file operations soon enough (I saw a "denied access...file in use" error when using Chrome.
So, it would seem that the refresh ajax call needs to wait until the file system completes its work.
Would you agree? If so, how can I make this work?
You can either set your XMLHttpRequest async to false:
xhr.open('POST', '/Dashboard/UploadFiles', false);
Or you can call your refresh function in callback:
var xhr = new XMLHttpRequest();
xhr.open('POST', '/Dashboard/UploadFiles');
xhr.send(formdata);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
reloadMain(); //Only refresh after the file post get a 200 response
}
}
I'm developing an application in Rails wich acts as a network and apps monitor. I'm using Active Admin Dashboard as a main page, showing the status of every server and some apps in my network. I'd like to configure the dashboard page to autorefresh every x minutes, but I don't know where to configure this setting, because I don't have full control of the html rendered by the dashboard. Have anyone managed to do it?
Thanks
In config/initializers/active_admin.rb you can register javascripts:
config.register_javascript "/javascripts/admin-auto-refresh.js"
Then create a admin-auto-refresh.js that does exactly that.
You'll also want to register admin-auto-refresh.js in your config/environments/production.rb
config.assets.precompile += "admin-auto-refresh.js"
UPDATE:
Added some code to refresh the page after 5 seconds. Add this to /javascripts/admin-auto-refresh.js
$(function() {
setTimeout(location.reload(true), 5000);
})
Here's the final code, thanks very much to #JesseWolgamott.
$(function() {
var sPath = window.location.pathname;
var sPage = sPath.substring(sPath.lastIndexOf('/') + 1);
if (sPage == 'admin'){
setTimeout("location.reload(true);", 10000);
}
})
Below is code that will refresh without the page flashing. It is enabled by having at least one element in the page with a class tag of needs_updating. Include this code snippet in any javascript that is loaded and then add the tag any where on the page.
The only downside is this ONLY updates the html body of the page.
For example
show do |my_model|
...
if my_model.processing?
row :status, class: 'needs_updating' do
'we are working on it...'
end
else
row :status do
'ready'
end
end
....
end
so if the model is still processing then you get the class tag 'needs_updating' which will cause the below javascript to be invoked every 10 seconds
jQuery(document).ready(function($) {
if ($('.needs_updating').length > 0) {
console.log("we need some updating soon");
var timer = setTimeout(function () {
console.log("re-loading now");
$.ajax({
url: "",
context: document.body,
success: function(s,x) {
$(this).html(s);
if ($('.needs_updating').length == 0) {
clearInterval(timer);
}
}
});
}, 10000)
}
})
I would like to use an autocomplete with ajax. So my goal is to have:
When the user types something in the text field, some suggestions provided by the server appear (I have to find suggestions in a database)
When the user presses "enter", clicks somewhere else than in the autocomplete box, or when he/she selects a suggestion, the string in the textfield is sent to the server.
I first tried to use the autocomplete widget provided by lift but I faced three problems:
it is meant to be an extended select, that is to say you can originally only submit suggested values.
it is not meant to be used with ajax.
it gets buggy when combined with WiringUI.
So, my question is: How can I combine jquery autocomplete and interact with the server in lift. I think I should use some callbacks but I don't master them.
Thanks in advance.
UPDATE Here is a first implementation I tried but the callback doesn't work:
private def update_source(current: String, limit: Int) = {
val results = if (current.length == 0) Nil else /* generate list of results */
new JsCmd{def toJsCmd = if(results.nonEmpty) results.mkString("[\"", "\", \"", "\"]") else "[]" }
}
def render = {
val id = "my-autocomplete"
val cb = SHtml.ajaxCall(JsRaw("request"), update_source(_, 4))
val script = Script(new JsCmd{
def toJsCmd = "$(function() {"+
"$(\"#"+id+"\").autocomplete({ "+
"autocomplete: on, "+
"source: function(request, response) {"+
"response("+cb._2.toJsCmd + ");" +
"}"+
"})});"
})
<head><script charset="utf-8"> {script} </script></head> ++
<span id={id}> {SHtml.ajaxText(init, s=>{ /*set cell to value s*/; Noop}) } </span>
}
So my idea was:
to get the selected result via an SHtml.ajaxText field which would be wraped into an autocomplete field
to update the autocomplete suggestions using a javascript function
Here's what you need to do.
1) Make sure you are using Lift 2.5-SNAPSHOT (this is doable in earlier versions, but it's more difficult)
2) In the snippet you use to render the page, use SHtml.ajaxCall (in particular, you probably want this version: https://github.com/lift/framework/blob/master/web/webkit/src/main/scala/net/liftweb/http/SHtml.scala#L170) which will allow you to register a server side function that accepts your search term and return a JSON response containing the completions. You will also register some action to occur on the JSON response with the JsContext.
3) The ajaxCall above will return a JsExp object which will result in the ajax request when it's invoked. Embed it within a javascript function on the page using your snippet.
4) Wire them up with some client side JS.
Update - Some code to help you out. It can definitely be done more succinctly with Lift 2.5, but due to some inconsistencies in 2.4 I ended up rolling my own ajaxCall like function. S.fmapFunc registers the function on the server side and the function body makes a Lift ajax call from the client, then invokes the res function (which comes from jQuery autocomplete) on the JSON response.
My jQuery plugin to "activate" the text input
(function($) {
$.fn.initAssignment = function() {
return this.autocomplete({
autoFocus: true,
source: function(req, res) {
search(req.term, res);
},
select: function(event, ui) {
assign(ui.item.value, function(data){
eval(data);
});
event.preventDefault();
$(this).val("");
},
focus: function(event, ui) {
event.preventDefault();
}
});
}
})(jQuery);
My Scala code that results in the javascript search function:
def autoCompleteJs = JsRaw("""
function search(term, res) {
""" +
(S.fmapFunc(S.contextFuncBuilder(SFuncHolder({ terms: String =>
val _candidates =
if(terms != null && terms.trim() != "")
assigneeCandidates(terms)
else
Nil
JsonResponse(JArray(_candidates map { c => c.toJson }))
})))
({ name =>
"liftAjax.lift_ajaxHandler('" + name
})) +
"=' + encodeURIComponent(term), " +
"function(data){ res(data); }" +
", null, 'json');" +
"""
}
""")
Update 2 - To add the function above to your page, use a CssSelector transform similar to the one below. The >* means append to anything that already exists within the matched script element. I've got other functions I've defined on that page, and this adds the search function to them.
"script >*" #> autoCompleteJs
You can view source to verify that it exists on the page and can be called just like any other JS function.
With the help of Dave Whittaker, here is the solution I came with.
I had to change some behaviors to get:
the desired text (from autocomplete or not) in an ajaxText element
the possibility to have multiple autocomplete forms on same page
submit answer on ajaxText before blurring when something is selected in autocomplete suggestions.
Scala part
private def getSugggestions(current: String, limit: Int):List[String] = {
/* returns list of suggestions */
}
private def autoCompleteJs = AnonFunc("term, res",JsRaw(
(S.fmapFunc(S.contextFuncBuilder(SFuncHolder({ terms: String =>
val _candidates =
if(terms != null && terms.trim() != "")
getSugggestions(terms, 5)
else
Nil
JsonResponse(JArray(_candidates map { c => JString(c)/*.toJson*/ }))
})))
({ name =>
"liftAjax.lift_ajaxHandler('" + name
})) +
"=' + encodeURIComponent(term), " +
"function(data){ res(data); }" +
", null, 'json');"))
def xml = {
val id = "myId" //possibility to have multiple autocomplete fields on same page
Script(OnLoad(JsRaw("jQuery('#"+id+"').createAutocompleteField("+autoCompleteJs.toJsCmd+")"))) ++
SHtml.ajaxText(cell.get, s=>{ cell.set(s); SearchMenu.recomputeResults; Noop}, "id" -> id)
}
Script to insert into page header:
(function($) {
$.fn.createAutocompleteField = function(search) {
return this.autocomplete({
autoFocus: true,
source: function(req, res) {
search(req.term, res);
},
select: function(event, ui) {
$(this).val(ui.item.value);
$(this).blur();
},
focus: function(event, ui) {
event.preventDefault();
}
});
}
})(jQuery);
Note: I accepted Dave's answer, mine is just to provide a complete answer for my purpose