Hi,

I've quickly worked up a node.js implementation based heavily on the Privatemsg Nodejs (https://drupal.org/project/privatemsg_nodejs) module's methods. I wasn't sure if the current sub-module was working but I proceeded to write a new version anyway (independent of Rules). I haven't created an administrative interface to store settings.

Since Privatemsg Nodejs module added audio alerts I kept it as a feature. It requires the audiojs library in sites/all/libraries with audio.min.js (and, as always, remembering to fix the file permissions is necessary...). The alert file must be called mnc.mp3 and stored in mnc/mnc_nodejs/sounds directory.

You would want to set all cache lifetimes and refresh intervals to 0 in mnc admin settings once this module is active (since AJAX refreshes become irrelevant [after the one initiated on page load]).

mnc_nodejs.module


/**
 * @file
 * Module file for mnc with node.js.
 */

/**
 * Implements hook_page_build().
 */
function mnc_nodejs_page_build(&$page) {
                global $user;


		mnc_nodejs_initialization($page['content']);
		nodejs_send_content_channel_token('mnc_nodejs_alert_' . $user->uid);

}

/**
 * Init js, css, libraries and the addition of user's object to js settings.
 */
function mnc_nodejs_initialization(&$page) {
  global $user;
  static $enabled = FALSE;
  if (!$enabled) {
    $folder = drupal_get_path('module', 'mnc_nodejs');

    $page['#attached']['js'][] = $folder . '/nodejs.mnc_nodejs.js';
    $page['#attached']['js'][] = array(
      'data' => array(
        'mncNodeJS' => array(
          'user' => $user->uid,
          'folder' => $folder,
        ),
      ),
      'type' => 'setting',
    );

    if ($audio_js_path = _mnc_nodejs_audio_library_path()) {
      $page['#attached']['js'][] = $audio_js_path;

      // Add sound settings for alert message if it is enabled.
      //if (variable_get('mnc_nodejs_sound', TRUE)) {
		//TO DO: Add variable in admin settings page to set condition for adding audio - default always on for now	
		//}
  
        $path = 'default';
        
        //TODO: In admin can offer alternate destination for audio
        //if ($fid = variable_get('mnc_nodejs_sound_file', 0)) {
          //$path = file_create_url(file_load($fid)->uri);
        //}
        
        $page['#attached']['js'][] = array(
          'data' => array('mncNodeJS' => array('sounds' => array('filepath' => $path))),
          'type' => 'setting',
        );
      }

    }

    $enabled = TRUE;
  }


/**
 * This hook is executed after the message has been inserted (transaction is not finalised however - could it cause confusing errors upon failures?).
 *
 * Implements hook_message_insert(). (i.e. hook_entity_insert()).
 */
function mnc_nodejs_message_insert($message) {
	mnc_nodejs_send($message);
}

/**
 * Send messages to node js channels.
 */
function mnc_nodejs_send($message) {
  // Send updated unread count to node.js for all recipients.
	 if (isset($message->field_mnc_recipients[LANGUAGE_NONE])) {
		foreach ($message->field_mnc_recipients[LANGUAGE_NONE] as $field) {
		  if (isset($field['target_id']) && is_numeric($field['target_id'])) {
			  $uid = $field['target_id'];
			  $count = 0;
			  
			  if ($account = user_load($uid)) {
			      $count = mnc_count_unread_notifications($account);
                           }
			  
			  $nodejs_count_update = (object) array(
				'channel' => 'mnc_nodejs_alert_' . $uid,
				'count' => $count, 
				'callback' => 'mncNodejsUnreadCount',
			  );

			  // One can change some settings (or add new) to those which
			  // are going to node.js channel.
			  drupal_alter('mnc_nodejs_channel', $nodejs_count_update, $message);
			  nodejs_send_content_channel_message($nodejs_count_update);
				}
			 }
		}

}

function _mnc_nodejs_audio_library_path() {

  $js_path = FALSE;
  if (function_exists('libraries_get_path')) {
    $audiojs_path = libraries_get_path('audiojs');
    if (!empty($audiojs_path)) {
      if (file_exists($audiojs_path . '/audio.min.js')) {
        $js_path = $audiojs_path . '/audio.min.js';
      }
    }
  }

  return $js_path;
}

nodejs.mnc_nodejs.js

(function ($) {

/**
 * Drupal.Nodejs.callback on message receive in chat.
 */
Drupal.Nodejs.callbacks.mncNodejsUnreadCount = {
  callback: function (message) {
	  if(typeof Drupal.mnc.setCount !== undefined) {
		Drupal.mnc.setCount(message.count);
		Drupal.mncNodejs.soundAlert();
	}

    // Attach behavior.
    Drupal.behaviors.mncNodejs.attach();
  }
};

/**
 * Provide functions for mnc manipulations, etc.
 */
Drupal.mncNodejs = {

  /**
   * Sound alert.
   *
   * 
   * @todo Change this in future.
   */
  soundAlert: function () {
    var settings = Drupal.settings,
        mncNodeJS = settings.mncNodeJS,
        audioSel = 'audio[id*="mnc-alert"]',
        html;

    if (mncNodeJS.sounds) {
      var sounds = mncNodeJS.sounds;
      if (sounds.filepath) {
        soundPath = settings.basePath + mncNodeJS.folder + '/sounds/mnc.mp3';
        if (sounds.filepath !== 'default') {
          soundPath = sounds.filepath;
        }
        html = '<audio id="mnc-alert-1" class="mnc-alert" src="' + soundPath + '"></audio>';
      }
      $('body').append(html);
      if ($(audioSel).length) {
        $(audioSel).parent('.audiojs').find('.pause').click();
        $(audioSel).parent('.audiojs').remove();
      }
      $audionInstance = audiojs.create($(audioSel));
      if ($audionInstance[0].settings.hasFlash && $audionInstance[0].settings.useFlash) {
        $audionInstance[0].settings.autoplay = true;
      }
      $(audioSel).parent('.audiojs').find('.play').click();
      $(audioSel).parent('.audiojs').addClass('mnc-nj-audiojs').hide();
    }
  }

};

}(jQuery));

I haven't had a chance to carefully test the code just yet but it's designed to plug in with the mnc module with no modifications except the admin refresh/cache settings. If the audiojs folder is missing hopefully it works without audio as expected.

Comments

Andre-B’s picture

Thanks for the work, I guess you put some effort in this. From my current point I wont include this at the current state for multiple reasons:

"I wasn't sure if the current sub-module was working but I proceeded to write a new version anyway (independent of Rules)."

The current submodule is working as far as I can tell, and it's independent of Rules already, Rules integration is optional and you can always just use these functions directly:

mnc_nodejs_send_counter_add($account, $message);
mnc_nodejs_send_counter_sub($account, $message);

One thing that was planned for future was adding the possibility to add further possibilities of displaying the new messages on the client (not only play sounds). E.g. show a small box somewhere on the page once a message comes in. But that will require quiet a lot of work (and resources), so I did not include that in the fist version.

I also believe that it should be as modular as possible, so I don't want to anything that could be counted as a "feature" to be needed for the core.

First let me explain why there are two methods rather than one that just pushes the total count: performance. Think of a relationship that will update 50-100 recipients, for each recipient your solution would call mnc_count_unread_notifications() - which is by design a view and might cause huge load on your system. So, instead of pushing the actual counter I only push the state change (if the client is connected, otherwise there's no queue to send that update to a later state). If for whatever reason the unread counter is invalid without knowledge of the client, there's an internal cache lifetime, once that cache expires, the client will request a new counter no matter what. (This request is handled also if the client opens the mnc view). This is suitable in almost all cases and a client should not have any issues with that.

In order to extend on what is already built in the submodule I would suggest implementing a new hook, that is called right before the message will be send to the channel -

/**
 * Sends a counter update: +1.
 *
 * @param object $account
 *   A Drupal user object e.g. from user_load(1)
 * @param object $message
 *   The message entitiy that is new
 */
function mnc_nodejs_send_counter_add($account, $message) {
  $nodejs_message = new stdClass();
  $nodejs_message->channel = 'nodejs_user_' . $account->uid;
  $nodejs_message->broadcast = FALSE;
  $nodejs_message->callback = 'mnc_nodejs_counter_add';
  // $nodejs_message->data = 'some data here? maybe to provide a full counter?';
  drupal_alter('mnc_nodejs_send_counter_add', $nodejs_message);
  nodejs_send_message($nodejs_message);
}

(same will be needed for the sub method).

Once you implement the hook, you could add further output, even the full counter - if you took your preparations that this won't impact your infrastructure. Even changing the nodejs callback will be possible at that time (so no monkey patching).

What I did now:

  • verified current mnc_nodejs and fixed an issue with calling message_notification_center attribute rather than mnc.
  • added two hooks for hook_mnc_nodejs_send_counter_add_alter and hook_mnc_nodejs_send_counter_sub_alter

What you can do now:

  1. create a new submodule called something like mnc_nodejs_audio (dependency is mnc_nodejs)
  2. implement hook_nodejs_handlers_info() and add a custom callback for your audio playback (and total message counter).
  3. recommendation: make the audio playback configurable for a) all messages b) messages with notification attribute = 1 only
  4. implement hook_mnc_nodejs_send_counter_add_alter and hook_mnc_nodejs_send_counter_sub_alter to target that callback, change the message data as needed.
  5. in order to have me submit your patch:

  • Commit 0e17e59 on 7.x-1.x by Andre-B:
    added a hook before message is send to nodejs channel #2263099
    
Andre-B’s picture

another small node:
don't use mnc_count_unread_notifications() directly in your code but be aware that the callback for unread notifications is configurable and should be called like

$callback = variable_get_value('mnc_count_callback');
$unread_count = call_user_func($callback, $user);

examples are in mnc_ajax_unread_count() and mnc_add_js_setting_unread_count()

  • Commit d26733d on 7.x-1.x by Andre-B:
    updated readme part about unread count #2263099
    
Andre-B’s picture

and another note regarding sending the full unread count:
instead of assuming that every recipient is online and should receive a full count update, assume that there might be quiet a lot of offline clients and try to limit the calls you are doing. I took a quick look in the nodejs module to see if there's a way to tell if a client/ user is connected and found this partial in nodejs_buddylist module:

/**
 * Filter the given list of uids based on who is online.
 */
function nodejs_buddylist_get_online_uids($uids) {
  return db_select('nodejs_presence', 'njp')
    ->condition('njp.uid', $uids, 'IN')
    ->fields('njp', array('uid'))
    ->execute()
    ->fetchCol();
}

I did not investigate further - so I dont know wether that nodejs_presence table is created by default or for buddylist only.

Keep up the good work :)

cmonnow’s picture

Thanks for your reply. I agree with all of your suggestions - I've been concerned with the number of queries and user_loads all this live activity requires when not optimised. It was hard imagining all the potential scenarios where adding +1 could fail and I haven't played around enough with node.js/sockets to know how it behaves under various scenarios. Thanks for fixes and additional hooks - if I get the time I'll try to extend the audio module.

The hard-coded 'mnc_count_callback' (rather than the call_user_func) I entered was a bad stuff up during testing - I wasn't even using the notifications callback (and I haven't tried notifications yet) - but I did have the 'messages unread' option overriding the $view->build (i.e. 'the display used for unread notification messages' in the admin page) so it worked during testing (but with my stuff up I would have missed the performance advantage of the mnc cache table).

That 'nodejs_presence' table is great and I can confirm it functions normally on my setup (I haven't double checked yet to see if it relies on sub-modules but probably not).

Regarding the js file I wasn't too sure how it would have worked but I haven't tested it yet or maybe not thought it through enough. After getCount() is called, if the client's cache says the count is expired (or doesn't exist etc) then it would perform an AJAX call to fetch the unread count (if notifications are activated or cache expiry set to 0 it would fetch the same slow view that I was sending, defeating the performance benefit) and then add 1 to it. So would the count end up being be 1 too high (and was this getCount() a mistake)? Even if this worked, if I set the client-side cache life time to say, 30 seconds (instead of 0 as I have it now), and I receive 3 messages within 30 seconds, would that mean that the count won't ever be updated despite the node.js alerts (since I've turned off AJAX polling/refresh since that was the intent of node.js)?

Drupal.Nodejs.callbacks.mnc_nodejs_counter_add = {
  callback: function (message) {
    if (message.callback == 'mnc_nodejs_counter_add') {
      var count = Drupal.message_notification_center.getCount();
      count = count + 1;
      if (count >= 0) {
        Drupal.message_notification_center.setCount(count);
      }
    }
  }
};

To take advantage of the cache table and avoid redundancy, originally I was going to simply add a hook into the existing mnc_message_insert function and send the cached count and uid directly to the client's channel (if messages_unread is set), but this requires the cache to already be set. It appears the view query will be run eventually regardless if the cache is empty - the only thing we can potentially control or defer is when it happens (which could be if present in nodejs_presence table (which should already be cached on initial page load), queuing cron jobs if benefiting the server/database setup etc?).

[Note that I may be confused since I'm not considering or experienced with all the scenarios mnc intends to support, or when the hooks are made].

One other thing in the .module is that $nodejs_message->channel = 'nodejs_user_' . $account->uid; doesn't exist on my setup. Unless I can't find it, variable "nodejs_enable_userchannel" that is looked for in nodejs_nodejs_user_channels($account) in nodejs.module is never even created by my installed version of the nodejs module (and at least doesn't exist in my 'variable' table).

/**
 * Implements hook_nodejs_user_channels().
 */
function nodejs_nodejs_user_channels($account) {
  if (variable_get('nodejs_enable_userchannel', TRUE) && $account->uid) {
    return array('nodejs_user_' . $account->uid);
  }
  return array();
}

Thanks again!

cmonnow’s picture

Hmm...regarding the nodejs_presence table I take that back. It doesn't appear to update consistently on my setup. I'll have to look into it.

Andre-B’s picture

regarding getCount():

take a look at the function definition; Drupal.mnc.getCount = function(forceRefresh) has an internal cache, and outputs the cached count (if any), otherwise it will call the backend.

...and I receive 3 messages within 30 seconds, would that mean that the count won't ever be updated despite the node.js alerts (since I've turned off AJAX polling/refresh since that was the intent of node.js)?

Ajax polling/refresh should still be turned on, since we don't send the current counter but only a +1/ -1 the advantages of nodejs is that we have realtime information about new messges, instead of lowering the client side polling time (which will add lots of backend calls otherwise).

Caching logic for that part is almost completly at the client and it's localstorage, but there's still a serverside cache to prevent from DOS. As you already mentioned for the serverside part: you can't assume that a cache exists, and filling that cache is a waste of resources.

So if you define a cache lifetime for 2-5 Minutes (whatever applies to your website usage and how frequently / important it is for your users to have a updated counter in any case) - this is more of a business/ money question, if you can afford a lot of users hitting your backend, set the cache lifetime shorter, if you want to save resources increase the cache lifetime. To make this clear: we have to retrieve a fresh counter from the backend because the client might be disconnnected from the server while a message with him as a recipient was send.

Regarding the node js user channel: it looks like there's a setting to be enabled in the nodejs module to enable 'nodejs_enable_userchannel' which by default should be true - so if you disable that it might introduce problems with mnc. (going to add a part in the readme about that). I am not sure why that channel doesnt exist in your setup, and I can not reproduce that.

Did I cover everything? Do you have further questions?

cmonnow’s picture

Hi,

Sorry I couldn't get back sooner. I believe I solved the bug regarding the nodejs_presence table not being updated but I don't know if it's specific to only some setups (https://drupal.org/node/1823254#comment-8770211). I can also confirm my user channels were loading normally after all, although I couldn't find the configurable setting. In fact, re-reading variable_get('nodejs_enable_userchannel', TRUE) my absent variable defaults to TRUE anyway (but I missed it since unlike content token channels these channels (and broadcasts) don't log to the node.js console unless debug mode is on).

Thanks for the elaboration. I understand your intention with the count + 1 and maintaining AJAX refresh as a disconnection backup but I thought I could consider some scenarios with the current code and maybe an alternative.

To make sure I have things right, this is how I understand the progression of events flow:

(1) Client connects. The mnc attach function will run Drupal.mnc.setCount(settings.mnc.unread_count) if the typeof settings.mnc.unread_count != "undefined" and the anonymous function upon page load will load Drupal.mnc.updateCountInterval at the designated refresh interval (if > 0, via one of two methods - exploiting localstorage or fixed polling), which with Modernizr will set the expiry timestamp for the unread_count at every interval.

Just a long side note: It may be better to load the initial unread_count through the theme to avoid depending on javascript to update the number on screen. We can separate the js settings unread count function fetching in the backend into 2 functions, where the new reuseable function exclusively fetches and caches in memory the initial unread count. For example,

function mnc_add_js_setting_unread_count($ajax = FALSE) {
  $setting['mnc']['unread_count'] = mnc_intial_page_unread_count();
  
  if ($ajax) {
    $commands[] = ajax_command_settings($setting, TRUE);
    return $commands;
  }
  else {
    drupal_add_js($setting, 'setting');
  }
}

/**
 * Obtains the unread count upon initial page load functions
 
 * @return string
 *   Unread count
 */
function mnc_intial_page_unread_count() {
  global $user;
  $callback = variable_get_value('mnc_count_callback');
  $initial_unread_count = &drupal_static(__FUNCTION__);
  if (!isset($initial_unread_count)) {
		$initial_unread_count = call_user_func($callback, $user);
	}
	return $initial_unread_count;
}

This new function can be called by the mnc_theme variables array to print an $initial_unread_count variable to the relevant in the mnc.tpl.php template.

(2) Let's say we want a 20 minute expiry and we're using Modernizr.localstorage (with a 1 minute refresh or whatever less than 20). The initial unread count was 0.

At 5 minutes into the interval, node.js sends a counter update via callback mnc_nodejs_counter_add.

Firstly, it checks the count via getCount:

Among the expectedly true requirements in getCount mnc_valid_timestamp is not < new Date().getTime(), since 20 minutes hasn't been reached, so the cached value is returned. Since the requestUnreadCounter is never fired, the expiry date has not been updated (localStorage.setItem("mnc_notifications_cache_valid_timestamp", new Date().getTime() + expire_time); has never been run).

Then our callback will add 1 to the cached count and setCount() to count + 1. The DOM and Drupal.settings.mnc.unread_count are updated to (0 +1 = 1) , but not the timestamp.

Everything is working nicely.

(3) At 20 minutes the interval is up and the expiry is reached. The server will be contacted for a fresh value which also equals 1, and the +20 minutes expiry is started again. At the same time the last 20 minutes expired co-incidentally a new message was received so the count is updated prematurely to 2, but the asynchronous delayed AJAX callback would override this with the same value of 2. Everything still should work fine.

(4) During the next 20 minutes the client is disconnected on the train for 5 minutes where they receive another message count that isn't added. So instead of 3 they still have 2 showing. Within a minute of reconnection they receive another message so rather than showing 4 messages it will say 3 (though not catastrophic for our purposes and the value will be refreshed in ~10 minutes anyway => if they really cared about the messages they would press the MNC and get the new messages via AJAX anyway).

I realise the use of localstorage is a great solution where a user has 6 tabs open of the one website such that if the timestamp expiry were 5 minutes we would have ~12 client requests instead of 72. So the current implementation would work fine in most circumstances for the module's intended purpose.

However, I was wondering if taking advantage of socket.io's own functions could relegate AJAX refreshing as a backup function only (when MNC node.js is enabled of course by the site developer).

socket.on('disconnect', function(){}); // wait for reconnect, initiate AJAX refreshing on a setInterval (which can be set shorter, but can drain battery and/or processing speed of user) for when node.js server only fails (not client). Right now the count will reset to 0 should JSON requests fail

socket.on('reconnect', function(){}); // connection restored (client restored or node.js restored), terminate interval then requestCount once more, ready to take on plus counts again from node.js

This could remove 100s of Apache requests from idle browsers I suppose.

But I haven't tested this yet nor have I done enough research to confirm this is best practice.

Cheers

Andre-B’s picture

(1):
"Just a long side note: It may be better to load the initial unread_count through the theme to avoid depending on javascript to update the number on screen."
That was in fact my first solution, but there are multiple flaws coming with it:
1. caching: in order to add the settings variable you will probably have to bypass drupal's page cache which will also make it hard to cache pages for logged in (and anonymous user).
2. performance: even if you have some sort of cache going on, you will still hit your cache backend at every client call, rather than hit the cache backend/ or retrieve a fresh cache if the client needs it. HTTP/ PHP has no state information, everything about that has to be bootstraped again by a simple session cookie, you can't fix that in a drupal page, so I'd rather push that logic to the client - saves a lot of performance. (how much? I don't know, you might want to measure).
3. after all, you could simply override the function calls and/ or add a preprocessor to provide that variable in your custom setup. I might add a piece of documentation but I really dont see any benefits in this (there might be one if localstorage is not available - I don't know how many clients that might affect, but that probably varies as well).
(2)(3)(4): I guess you got that correctly, I don't see any question here?
(4) regarding socket.io: I didn't use that so far, but sounds great. Test it, provide a patch and I'll get it in. After a quick read on socket.io: sounds like there will be at least two additions:

  1. socket.io library for drupal/ mnc
  2. socket.io package on node.js server

These are two external dependencies (client side library should come from an external source(libraries api) or module and additional feature in the node.js server), since I do not know how well both are supported I do not want to have it has a hard dependency - there might be node.js servers and hosting providers that do not offer socket.io for instance. So - the whole feature will have to be optional and be toggled by setting/ auto detection (probably auto detection is possible for the client side library, for the server part I'd currently rely on a config variable - or is there a way to test that feature for availability on the node.js server), auto discovery will have to cache it's test and save them to a config variable or cache table.

Please do a discovery on the issues and provide informations :)

And again, I hope I answered your questions.

cmonnow’s picture

Hi,

I can see the potential benefits in AJAXifying the count display. In the long-term I might have to consider a reduced serving from my full bootstrap. My personal scenario involves a tonne of instantaneously relevant dynamically-generated content on most pages (another thing I might have to reconsider, but that's the nature of the application) so I haven't fully thought out the feasibility of full page-caching yet and pushing dynamic content to AJAX. I'd probably need to hack some modules I've installed.

Since I'm serving dynamic content via full page reloads I probably can't take advantage of a localstorage count between pages since node.js "+1" updates will fail between page loads.

My only requirement is that on slow mobile connections the first thing to show (i.e. as part of my page skeleton) is the number of private messages and notifications - like Facebook does now. This is so when mobile push notifications aren't exploited, the client doesn't need to worry about the whole page loading to know if they've been replied to.

Regarding the socket functions, I was under the impression that this module depended specifically on node.js integration module, which if I'm not mistaken relies on socket.io (https://drupal.org/node/1849552)?. I blindly installed the module as suggested in the docs so I never really considered alternate configurations.

From preliminary testing if a node.js server is turned off and reconnects to a client that kept their window open the token channel message will fail to send but a broadcast will work. I still have to try the standard node_send_message so hopefully that works.

Another issue with an alternate module is that currently if a client connects when node.js is disconnected the client will never connect via socket.io even once node.js is turned back on, so a backup is a good idea for anything essential (though to a much reduced scale since it probably would not be scalable without node.js).

Cheers

Andre-B’s picture

Component: Code » mnc_nodejs
cmonnow’s picture

Hi,

I have a few observations I've made that I'll present in no particular order.

(1) Right now the function mnc_preprocess_views_view will flag messages as read by the person requesting the view of messages (and not the person to whom these messages were intended). If the admin decides to view the messages for a user in the backend a flag will be set in the flagging table for the admin (user 1). Since the unread messages view assumes the flagging.uid IS NULL, this will cause corruption in the counts for the viewed messages (that can only be fixed by manually clearing the flagging database. An alternative is proposed below where the logged in user id but be the same as the recipient's id to invoke flagging.

function mnc_preprocess_views_view(&$variables) {
  if ($variables['view']->name == 'mnc' && isset($variables['view']->args[0]) && preg_match('/^\d+$/', $variables['view']->args[0]) && $variables['view']->args[0] > 0) {
		$user = user_load($variables['view']->args[0]);
		if($variables['user']->uid == $variables['view']->args[0]) {
			foreach ($variables['view']->result as $message) {
			  //mnc_mark_read($variables['user'], $message->mid);
			  mnc_mark_read($user, $message->mid);
		}
	}
  }
}

(2) This might need further investigation but I believe any time a message is read or unread it is necessary to generate a new count cache on the server to avoid subsequent miscounts (which could involve simple subtraction/addition to the cache, or forcing a new view query count). If the server db cache is set anywhere from a few seconds (to a few days) there will always be a chance for subsequent miscalculations.

On this note, I believe the function mnc_message_insert has an error. It currently will only update the cache if time() > $cache->expire, but this appears incorrect - an expired cache will never be called by the mnc_count_unread function. So it should be time() < $cache->expire.

Using this same mechanism we can update mnc_mark_all_read, mnc_mark_all_unread, mnc_mark_read and mnc_mark_unread to adjust the counts in the cache directly (rather than await a view refresh).


function mnc_mark_read($user, $message_id) {
  $flag = flag_get_flag(variable_get_value('mnc_flag'))
    or die('no "' . variable_get_value('mnc_flag') . '" flag defined');
//!$flag->is_flagged was added since $flag->flag will return true even if already flagged
  if (!$flag->is_flagged($message_id, $user->uid) && $flag->flag('flag', $message_id, $user)) {
  
  	$cache_key = 'mnc_count_unread:' . $user->uid;
	$cache = cache_get($cache_key, 'cache_entity_mnc');
	//Only update count if not expired (otherwise not necessary since value will be replaced)
	if (
	  variable_get_value('mnc_cache_lifetime_count_unread') > 0 &&
	  $cache &&
	  (time() < $cache->expire)
	) {
	  $count = $cache->data;
	  $count = $count - 1;
	  if($count < 1) {
			$count = 0; //shouldn't be needed if every change accounted for but added here in case something went wrong to make it negative
	  }
	  cache_set($cache_key, $count, 'cache_entity_mnc', $cache->expire);
	}
  
	rules_invoke_event('mnc_mark_read', $user, $message_id);
	}
}

function mnc_mark_unread($user, $message_id) {
  $flag = flag_get_flag(variable_get_value('mnc_flag'))
    or die('no "' . variable_get_value('mnc_flag') . '" flag defined');
   if($flag->is_flagged($message_id, $user->uid) && $flag->flag('unflag', $message_id, $user)) {
  
    $cache_key = 'mnc_count_unread:' . $user->uid;
	$cache = cache_get($cache_key, 'cache_entity_mnc');
	if (
	  variable_get_value('mnc_cache_lifetime_count_unread') > 0 &&
	  $cache &&
	  (time() < $cache->expire)
	) {
	  $count = $cache->data;
	  $count = $count - 1;
	  cache_set($cache_key, $count, 'cache_entity_mnc', $cache->expire);
	}
  }
  
  rules_invoke_event('mnc_mark_read', $user, $message_id);
}

function mnc_mark_all_read($user, $first_page_only = FALSE) {
  $view = views_get_view(variable_get_value('mnc_view'));
  if (!$first_page_only) {
    // Mark all notifications for this user, not just the first page of results.
    $view->items_per_page = 0;
  }
  $view->set_display(variable_get_value('mnc_view_display_unread'));
  $view->set_arguments(array($user->uid));
  $view->pre_execute();
  $view->execute();
  $flag = flag_get_flag(variable_get_value('mnc_flag'))
    or die('no "' . variable_get_value('mnc_flag') . '" flag defined');
    //Update count changes for updating the database cache
    $count_delta = 0;
  foreach ($view->result as $message) {
    if (!$flag->is_flagged($message->id, $user->uid) && $flag->flag('flag', $message->mid, $user)) {
		$count_delta += -1;
	}
  }
  
	$cache_key = 'mnc_count_unread:' . $user->uid;
	$cache = cache_get($cache_key, 'cache_entity_mnc');
	//Only update count if not expired (otherwise not necessary since value will be replaced)
	if (
	  variable_get_value('mnc_cache_lifetime_count_unread') > 0 &&
	  $cache &&
	  (time() < $cache->expire)
	) {
	  $count = $cache->data;
	  //$count = $count + $count_delta;
	  //$count should be 0 anyway if no inconcsistencies - but risky to assume
	  $count = 0;
	  cache_set($cache_key, $count, 'cache_entity_mnc', $cache->expire);
	}
  
  
  rules_invoke_event('mnc_mark_all_read', $user);
  return $view;
}

function mnc_mark_all_unread($user, $first_page_only = FALSE) {
  $view = views_get_view(variable_get_value('mnc_view'));
  if (!$first_page_only) {
    // Mark all notifications for this user, not just the first page of results.
    $view->items_per_page = 0;
  }
  $view->set_display(variable_get_value('mnc_view_display'));
  $view->set_arguments(array($user->uid));
  $view->pre_execute();
  $view->execute();
  $flag = flag_get_flag(variable_get_value('mnc_flag'))
    or die('no "' . variable_get_value('mnc_flag') . '" flag defined');
     $count_delta = 0;
  foreach ($view->result as $message) {
    //We'll flag all message ids (flagged and unflagged) in the count
    if ($flag->flag('unflag', $message->mid, $user)) {
		$count_delta += 1;
		}
  }
  
  	$cache_key = 'mnc_count_unread:' . $user->uid;
	$cache = cache_get($cache_key, 'cache_entity_mnc');
	//Only update count if not expired (otherwise not necessary since value will be replaced)
	if (
	  variable_get_value('mnc_cache_lifetime_count_unread') > 0 &&
	  $cache &&
	  (time() < $cache->expire)
	) {
	  $count = $count_delta;
	  cache_set($cache_key, $count, 'cache_entity_mnc', $cache->expire);
	}

  rules_invoke_event('mnc_mark_all_unread', $user);
  return $view;
}

(3) Although not important since the module is loading the unread count via javascript to permit page caching, I'll add that I had problems with pre-caching of the mnc block (caches first user's count) when it was added to the user menu using the Menu Attach Block module (I haven't checked to see if the block was cached due to this module but I can confirm that the menu does change counts for the Private message module).

(4) Reminder to self to investigate the extended viewport plugins for the qTip module.

(5) Although only receiving potential errors in the console and not on screen from sending multiple requests, the re-attachment of Drupal.behaviors.mnc in mnc.js causes the mark all read/unread links to be tied to the same click events 3 or 4 times. This could not be overridden with the $.once() or Drupal context methods since the click events would fail when the block was re-fetched. I haven't had a chance to look heavily into it but for now I've simply added an unbinding of all click events on those elements.

			$('.mnc-inner-container .markAllRead').unbind('click').click(function(event) {
			  event.preventDefault();
			  Drupal.mnc.markAllRead();
        });

			$('.mnc-inner-container .markAllUnread').unbind('click').click(function(event) {
			  event.preventDefault();
			  Drupal.mnc.markAllUnread();
			});	

(6) Views Load More has not updated the dev version since November last year so none of the new changes are present.

(7) I can confirm that nodejs_send_message reconnects fine with the client following disconnection and node.js messages continue to work.

(8) Here's a working example of code that will fetch a fresh count from the server should the client be reconnected to node.js. This can be extended by shortening the count's expiry upon Drupal.Nodejs.socket.on('disconnect', ...), perhaps using another stored customisable variable, and restoring the original expiry upon 'reconnect'. Using this method and the above mentioned changes can allow much longer expiry times for the client and server side as defaults.

Drupal.behaviors.mncSocketReconnect = {
		attach: function(context, settings) {
			if(typeof(Drupal.Nodejs.socket.on) === "function") {
				mnc_same_page = true;
				    $(window).on('beforeunload', function(){
						mnc_same_page = false; //don't try to update count when page leaving and socket.io reconnects - I can force disconnection beforeunload too?
				    });
				Drupal.Nodejs.socket.on('reconnect', function(){
					if(typeof(Drupal.mnc.getCount) != undefined && typeof(Drupal.mnc.setCount) != undefined && mnc_same_page) {
						var count = Drupal.mnc.getCount();
						Drupal.mnc.setCount(count);
						console.log('Socket reconnected - updating Notification count');
	  			    		} 
					});	
				

				} else {
					console.log('Socket.io could not be setup for notifications');
				}
		}
};

(9) Drupal.Nodejs.callbacks.mnc_nodejs_counter_add can cause 1 extra to be added to the client side count. It adds 1 regardless of whether the count was obtained from the client side or not. This is even possible if the Modernizr refresh is set to <5 seconds as the window of opportunity is still there and the situation won't be rectified until the next requestUnreadCounter() (that isn't initiated by a node.js call). One solution is to not call getCount if the unread_count variable has already been set and simply add 1 to the existing unread_count:

Drupal.Nodejs.callbacks.mnc_nodejs_counter_add = {
  callback: function (message) {
    if (message.callback == 'mnc_nodejs_counter_add' && typeof Drupal.mnc.getCount === "function" && typeof Drupal.mnc.setCount === "function") {
		if (typeof Drupal.settings.mnc.unread_count != undefined) {
			var count = Drupal.settings.mnc.unread_count;
			count = count + 1;
		} else {
			var count = Drupal.mnc.getCount();
		}
      if (count >= 0) {
        Drupal.mnc.setCount(count);
      }
    }
  }
};

By the way, eventually the callback function above will have to be camelCased to conform with Drupal JS coding standards. I've fixed a few of my mistakes like typeof parentheses that don't conform but I'm not generally familiar with all the standards in Drupal JS (https://drupal.org/node/172169) and PHP (https://drupal.org/coding-standards).

Hopefully I didn't forget anything or make any coding mistakes (other than performance mistakes :)).

Cheers

JordanMagnuson’s picture

Version: » 7.x-1.x-dev
Status: Active » Closed (outdated)