This is again a slighlty bigger thread.
I will describe a problem with the current PM System, which is mainly basing on pnMessages now.
I also have an working kind of generic interface for all kind of message system, which will
remove the module checks and direct references in the current postnuke code for pnmessages.
I also fixed function_pncountnewmessages.php which is ATM broken (because asking for the
.7xx message system).
Let me first explain that with performance i mean *not* pnMessages itself - i mean how its
called from Postnuke to get the numbers of unread messages and how its attached to the
postnuke theme & core system.
Both is very bad. When you look in the code of the online block code or other code parts
presenting the unreaded messages, you will see something like this:
Code
// check if pnMessages is available and add the necessary info
$pnmessages = pnModAvailable('pnMessages');
$pnr->assign('pnmessages', $pnmessages);
if ($pnmessages) {
pnModLangLoad('pnMessages', 'user');
$pnr->assign('messages', pnModAPIFunc('pnMessages', 'user', 'getmessagecount'));
$pnmessages = pnModAvailable('pnMessages');
$pnr->assign('pnmessages', $pnmessages);
if ($pnmessages) {
pnModLangLoad('pnMessages', 'user');
$pnr->assign('messages', pnModAPIFunc('pnMessages', 'user', 'getmessagecount'));
PnMessages is called here as a kind of "semi-core" module. That is not as clean as it should.
But the real problem is what this invokes: 3 SQL queries. The first query is easy - it will
check for the module. But the real bad point is the call of 'getmessagecount'. That is not
a count - its a COMPLETE status check of the whole message box - inclusive status of outbox,
archive and all. The called SQL query is this:
Code
$sql_1 = "SELECT sum( $messagescolumn[msg_stored] ) msg_stored,
sum( $messagescolumn[msg_inbox] ) msg_inbox,
sum(CASE WHEN $messagescolumn[msg_inbox] = 1 THEN $messagescolumn[msg_read] ELSE 0 END) read_msg,
sum(CASE WHEN $messagescolumn[msg_inbox] = 1 THEN $messagescolumn[msg_popup] ELSE 0 END) msg_popup
FROM $messagestable
WHERE $messagescolumn[to_userid] = $uid";
$sql_2 = "SELECT count($messagescolumn[msg_id])
FROM $messagestable
WHERE $messagescolumn[msg_outbox] = '1'
AND $messagescolumn[from_userid] = $uid";
sum( $messagescolumn[msg_inbox] ) msg_inbox,
sum(CASE WHEN $messagescolumn[msg_inbox] = 1 THEN $messagescolumn[msg_read] ELSE 0 END) read_msg,
sum(CASE WHEN $messagescolumn[msg_inbox] = 1 THEN $messagescolumn[msg_popup] ELSE 0 END) msg_popup
FROM $messagestable
WHERE $messagescolumn[to_userid] = $uid";
$sql_2 = "SELECT count($messagescolumn[msg_id])
FROM $messagestable
WHERE $messagescolumn[msg_outbox] = '1'
AND $messagescolumn[from_userid] = $uid";
To point it out - that is every time called when the online block is shown for a loged in user!
Now lets think about what this does to a userbase of 100k+ and a pretty big message table.
Thats really a totally unneeded performance waste.
We can now add a custom SQL call - but still. Its not needed.
The inbox status is something what can and should be cached somewhere - and i have a good solution.
My solution will do this:
- adding a implicit private messages interface by defintion to the postnuke core
- doing tricky but clean status caching inside the user profile table
- removing every SQL query needed by adding the cache data to already called queries
- removing every need of a pnMessages check or other message system checks from code by moving the work away from postnuke to the message system itself
- advancing the private messages not only to show unread messsages but also new one without adding any new code
Let me first explain whats the point is between "unread" and "new" messages.
Unread vs. new messages
-----------------------
The current message interface from smarty/pnRender to block in general only checks for
unread messages - but names it new. Thats a problem, because this 2 things are different.
An example: You get 3 PMs. One important, one to a topic which is relevant next week
and one spam. PM will now show you "3 new mails". Now you read the important one.
And then you logout fast.
Now there are still 2 messages in your inbox, including one important email - and the system
should remember you about it. In the way to tell you "2 unread messages are there".
But they are not new.
NOW the same sender which wrote the first important messages wrotes an important update.
When you now login next time you see in the current system: "3 new messages".
Now lets hope the receiver have a good memory. Because he MUST remember that there
was 2 emails which was not important and he has to check the inbox now again!
That of course not right and a greater problem with more unread messages in the inbox.
The right system is to mark the 2 unread messages as unread and really new messages as "new".
In that way the user can always see - there are messages i don't have checked.
It must be then "You have 1 new messages and 2 unread in your inbox".
If not, then the users will handle the "new messages" note after some time in the way
they think "oh, its perhaps nothing important".
For daimonin i want use the PM system also for shop infos and other important notes, so
its VERY important that the user really get noticed from the system that there is something
he should or must read.
Solution
--------
The code changes for the following can be downloaded here:
http://static.88-198-46-59.clients.your-server.de/pmpatch01.zip
*You need the SVN snapshot of postnuke & pnmessages to test*
Simply copy this over your test install.
I will put it on my testsite, but this kind of changes should be experienced from admin view - so its a bit senseless.
So, how it works? Its pretty simple:
- we add 2 entries to the user Account Panel Property List. _PRIVMSG_NEW and _PRIVMSG_READ.
Then we call this file in fix positions i the pnmessages code. Example:
Code
<?php
// check if we get called directly via URL, we do not like this
if (eregi(basename(__file__), $_SERVER['PHP_SELF'])) {
die ("You can't access this file directly...");
}
/* we set the "_PRIVMSG_NEW" and "_PRIVMSG_READ" entries of the user data table
* this is mainly a cache system for fast msg count access without accessing
* pnmessages itself. It also adds the "new message" feature to pnmessages without
* in a smart way.
* modes:
* uid, true, false: *default* one or more msg was read, recalc read counter and clear new msg
* uid, false, true: msg was send to uid, increase new msg counter of uid
* uid, false, false: inbox access, clear new msg of uid *not used ATM *
* uid, true,true: *undefined*
* this is an internal function called from various functions of pnuser and pnuserapi
*/
function pnmessage_set_msgcounter($uid, $readmsg = true, $newmsg = false)
{
// sanity check
if($uid <= 0)
return false;
// we need the userinfo of our target user
// this will implicit checkout our counters if not already done
$pnuser = pnUserGetVars($uid);
if (!isset($pnuser))
return false;
// init the counter - have in mind they can already exists or not
$read_count = 0;
$new_count = 0;
if(isset($pnuser['_PRIVMSG_READ']))
$read_count = $pnuser['_PRIVMSG_READ'];
if(isset($pnuser['_PRIVMSG_NEW']))
$new_count = $pnuser['_PRIVMSG_NEW'];
// for $readmsg = true we have to recalc the unreaded messages for the user
if ($readmsg == true)
{
$pntable = pnDBGetTables();
$pmmsgcolumn = $pntable['pnmessages_column'];
$where = "WHERE $pmmsgcolumn[to_userid] = $pnuser[uid] AND $pmmsgcolumn[msg_inbox] = 1 AND $pmmsgcolumn[msg_read] = 0";
$read_count = DBUtil::selectObjectCount('pnmessages', $where);
}
// true will increase the msg counter, false will set it to zero
if($newmsg == true)
$new_count = $new_count + 1;
else
$new_count = 0;
// easy going with pnUser API - but lets ensure we have something to store
if (($new_count && !isset($pnuser['_PRIVMSG_NEW'])) || $news_count != $pnuser['_PRIVMSG_NEW'])
pnUserSetVar(_PRIVMSG_NEW, $new_count, $pnuser['uid']);
if (($read_count && !isset($pnuser['_PRIVMSG_READ'])) || $read_count != $pnuser['_PRIVMSG_READ'])
pnUserSetVar(_PRIVMSG_READ, $read_count, $pnuser['uid']);
// we should refresh the counters!
if ($read_count != $pnuser['_PRIVMSG_NEW'] || $read_count != $pnuser['_PRIVMSG_READ'])
pnUserGetVars($pnuser['uid'], true);
return true;
}
?>
// check if we get called directly via URL, we do not like this
if (eregi(basename(__file__), $_SERVER['PHP_SELF'])) {
die ("You can't access this file directly...");
}
/* we set the "_PRIVMSG_NEW" and "_PRIVMSG_READ" entries of the user data table
* this is mainly a cache system for fast msg count access without accessing
* pnmessages itself. It also adds the "new message" feature to pnmessages without
* in a smart way.
* modes:
* uid, true, false: *default* one or more msg was read, recalc read counter and clear new msg
* uid, false, true: msg was send to uid, increase new msg counter of uid
* uid, false, false: inbox access, clear new msg of uid *not used ATM *
* uid, true,true: *undefined*
* this is an internal function called from various functions of pnuser and pnuserapi
*/
function pnmessage_set_msgcounter($uid, $readmsg = true, $newmsg = false)
{
// sanity check
if($uid <= 0)
return false;
// we need the userinfo of our target user
// this will implicit checkout our counters if not already done
$pnuser = pnUserGetVars($uid);
if (!isset($pnuser))
return false;
// init the counter - have in mind they can already exists or not
$read_count = 0;
$new_count = 0;
if(isset($pnuser['_PRIVMSG_READ']))
$read_count = $pnuser['_PRIVMSG_READ'];
if(isset($pnuser['_PRIVMSG_NEW']))
$new_count = $pnuser['_PRIVMSG_NEW'];
// for $readmsg = true we have to recalc the unreaded messages for the user
if ($readmsg == true)
{
$pntable = pnDBGetTables();
$pmmsgcolumn = $pntable['pnmessages_column'];
$where = "WHERE $pmmsgcolumn[to_userid] = $pnuser[uid] AND $pmmsgcolumn[msg_inbox] = 1 AND $pmmsgcolumn[msg_read] = 0";
$read_count = DBUtil::selectObjectCount('pnmessages', $where);
}
// true will increase the msg counter, false will set it to zero
if($newmsg == true)
$new_count = $new_count + 1;
else
$new_count = 0;
// easy going with pnUser API - but lets ensure we have something to store
if (($new_count && !isset($pnuser['_PRIVMSG_NEW'])) || $news_count != $pnuser['_PRIVMSG_NEW'])
pnUserSetVar(_PRIVMSG_NEW, $new_count, $pnuser['uid']);
if (($read_count && !isset($pnuser['_PRIVMSG_READ'])) || $read_count != $pnuser['_PRIVMSG_READ'])
pnUserSetVar(_PRIVMSG_READ, $read_count, $pnuser['uid']);
// we should refresh the counters!
if ($read_count != $pnuser['_PRIVMSG_NEW'] || $read_count != $pnuser['_PRIVMSG_READ'])
pnUserGetVars($pnuser['uid'], true);
return true;
}
?>
And we call it from pnmessages for example when you send there a PM:
Code
// update the counters for the user!
include_once('user/modules/updateCounters.php');
pnmessage_set_msgcounter($to_uid, false, true);
include_once('user/modules/updateCounters.php');
pnmessage_set_msgcounter($to_uid, false, true);
There are a handful positions you have to add this 2 lines in the current code of pnmesssages.
*Thats all*.
How to call
-----------
I have in the download package a fixed function_pncountnewmessages.php using this interface.
When you look at it:
Code
function smarty_function_pncountnewmessages ($params, &$smarty)
{
extract($params);
unset($params);
$newmessages = 0;
$readmessages = 0;
if (pnUserLoggedIn()) {
$pnuser = pnUserGetVars(pnUserGetVar('uid'));
if(isset($pnuser['_PRIVMSG_NEW']))
$newmessages = $pnuser['_PRIVMSG_NEW'];
if(isset($pnuser['_PRIVMSG_READ']))
$readmessages = $pnuser['_PRIVMSG_READ'];
}
if (isset($assign)) {
$smarty->assign($assign, $newmessages);
if (isset($read))
$smarty->assign($read, $readmessages);
} else {
return $newmessages;
}
}
{
extract($params);
unset($params);
$newmessages = 0;
$readmessages = 0;
if (pnUserLoggedIn()) {
$pnuser = pnUserGetVars(pnUserGetVar('uid'));
if(isset($pnuser['_PRIVMSG_NEW']))
$newmessages = $pnuser['_PRIVMSG_NEW'];
if(isset($pnuser['_PRIVMSG_READ']))
$readmessages = $pnuser['_PRIVMSG_READ'];
}
if (isset($assign)) {
$smarty->assign($assign, $newmessages);
if (isset($read))
$smarty->assign($read, $readmessages);
} else {
return $newmessages;
}
}
It will now deliver new and unread messages but the important point is the way the data
is called. pnUserGetVars() is for a loged user always filled with all user & profile data
from the session handler inside pnInit(). That means our message cache data are added there to. No extra call is needed. There is no overhead. Everytime now we call this for example from blocks,
pnUserGetVars() will give us its internal cache, not invoking any SQL query.
Its a perfect solution - for groups or user don't using the pnmessage system the cache is even not there.
The Interface
-------------
This use of _PRIVMSG_xxx inside the user extended information table is really an interface - it can be moved totally to the message module.
- the message module can check the table for already defined _PRIVMSG_xxx field entry.
If it is there it knows a different message system is installed.
- The field entries can be added from the pninit.php of the message module. No handwork is needed.
- It will clean up the postnuke core from the checks against pnmessage or other message modules which is pretty unclean
Only the profile and profile modify templates needs an default exclusion of this fields - better would be to disallow and remove then in the handler function for that kind of data.
The trick is that we don't need *any* extra information except this 2 fields - no date saving
inside the message module, no status check from postnuke.
The new/unread status will be set automatically and implicit by the logic the message system
calls its functions and what the user does.
"New" messages are moved to "unread" every time when the user accessed the inbox - then we know
he has noticed the messages. We still tell him that there are unreaded.
Well, i will try to update my demo site to show how it work but it should be tested local.
I need this patch in any way, because its really an unneeded performance waste when it comes to bigger sites.
Well... suggestions?
