﻿// Twitter JScript Functions
// NB: to ensure changes to this code are rolled out immediately to all clients, change the ?v= parameter on the calling script tag to the date of the change

        var DEBUG_ALERTS = false;
        var USE_JSONP = true; //configure that the friends stream will make use of JSONP callbacks to obtain tweet data, rather than proxying via the server
        //var tweetcount = 0;
        //var linenum = 0;

        var SCROLL_RATE = 2000; //ms
        //IS THERE A WAY I COULD MAKE TWEET_RATE DYNAMIC BASED ON SERVER LOAD??? SO IT AUTOINCREASES IF SERVER IS GETTING OVERLOADED, HENCE REDUCING DEMAND ON SQLSERVER?
        var TWEET_RATE = 25000; //ms - note that tweets are also cached by the Twitter class, so updates at the rate specified here may read from Cache sometimes.
        var TWEET_HEIGHT = 84; //px
        var TWEETS_PER_PAGE = 25;

        var WAITTWEETS_RATE = 250; //ms - used to prevent event reentry via Timer_WaitForTweets...

        var RATELIMITS_RATE = 10000; //ms
        
        var Timer_Scroll;
        var Timer_Tweets;
        var Timer_WaitForTweets;
        var Timer_RateLimits;
        
        var btnScroll_Items; //have to cache the ClientID of these hidden buttons as we are in a .js file, that cannot easily see clientIDs
        var btnScroll_Items_Init; 

        var FirstTweetID = 0;
        var FirstMessageID = 0;
        var FirstSubscrID = 0;
        var LastTweets = null;
        
        //var Tweets_Page = "1";
        
        var TwitterTimeOffset = 0; //ms : difference between client and twitter server time to correct displayed times on tweets
        
        var RateLimitCommand = '';
        var Scrolling = false;
        var WaitForScroll = false;
        var Tweeting = false;
        var StallTweets = false; //used for example to display a set of search results with the auto refresh temporarily disabled
        var WaitForTweets = false;
        var Initialized = false;
        var LoggedIn = "False"; //to be used to spot changes in login status by comparing with a hidden field that contains real login status
        var LoggedIn_UserID = 0; //used for friends stream in JSONP mode for example
        var Unloading = false;
        var Refreshing = false; //used to prevent any further additions to the tweet panel via timed and callback events as soon as this flag is set to true...
                                //  to be set on the main page in page_loaded, used in conjunction with GetTweetsWithWait
                                // For example, on logging in, the page method will deliver undelayed feeds of tweets, so it is possible that without this Refreshing check 'new tweets' could
                                //   be received and cause the panel scrolling to move up before the lines of code execute that prevent it... in GetTweetsWithWait.

        var ShowRateLimits = "False"; //to be used to control generation of rate limit data from twitter

        var TradeFlags = null;

        //08 Mar 2010\; SJA - revised paging control
        var ShowHistoryDate = null; //null = history mode off/show first; a date = show messages immediately preceding that date; last ???
        var ShowHistoryNewer = false; //Store the direction to get hsitroy in from the above date: false => Older; true => Newer
        var cmdHistoryNewer_id = "";
        var cmdHistoryOlder_id = "";
        var cmdHistoryLast_id = "";
        var cmdHistoryCancel_id = "";
        var imHistoryLoading_id = "";

        function SyncTwitterTime() {
            PageMethods.GetTwitterData("TWITTER_TIME",null,false,false,OnSucceeded_SyncTwitterTime,OnFailed_SyncTwitterTime);
            return false;
        }
        function OnSucceeded_SyncTwitterTime(result) {
            // Get current date and time
            var d = new Date();
            
            // convert to msec
            // add local time zone offset 
            // get UTC time in msec
            var nowutc = d.getTime() + (d.getTimezoneOffset() * 60000);

            try
            {
                var twitterutc = Date.parse(result);//ms
                TwitterTimeOffset = nowutc - twitterutc;
        		//alert('TwitterTimeOffset = ' + TwitterTimeOffset);
            }
            catch(err)
            {
                //alert('error: ' + err.description);
            }
        }
        function OnFailed_SyncTwitterTime(error) {
           // Alert user to the error.
           if (DEBUG_ALERTS) {alert('Sync Time Error:' + error.get_message());}
        }

        function GetRateLimitsTimer() {
            if (ShowRateLimits=="True")
                {
                    if (RateLimitCommand == 'RATE_LIMIT_STATUS_USER') {
                        Timer_RateLimits = setTimeout("GetRateLimits('RATE_LIMIT_STATUS_IP')",RATELIMITS_RATE);
                    } else {
                        Timer_RateLimits = setTimeout("GetRateLimits('RATE_LIMIT_STATUS_USER')",RATELIMITS_RATE);
                    }
                }
        }
        function GetRateLimits(command) {
            RateLimitCommand = command;
            PageMethods.GetTwitterData(command,null,false,false,OnSucceeded_RateLimits,OnFailed_RateLimits);
            return false;
        }
        function OnSucceeded_RateLimits(result) {
            if (RateLimitCommand == 'RATE_LIMIT_STATUS_USER') {
                var RsltElem = $get("Results_RateLimitsUser");
            } else {
                var RsltElem = $get("Results_RateLimitsIP");
            }
            RsltElem.innerHTML = RateLimitCommand + ": " + result;
            GetRateLimitsTimer();
        }
        function OnFailed_RateLimits(error) {
            // Alert user to the error.
            if (DEBUG_ALERTS) {alert('Error:' + error.get_message());}
            GetRateLimitsTimer();
        }

        function GetTweets_StartStop(btn) {
            Tweeting = !Tweeting;
            GetTweets_SetImage(btn,Tweeting);
            if (Tweeting) {
                if (FirstTweetID == 0) { //immediately update the tweets, as there are none, which will also start the timer
                    GetTweets();
                } else { //update via timer
                    GetTweetsTimer();
                }
            } else {
                clearTimeout(Timer_Tweets);
            }
            return false;
        }
        function GetTweets_SetImage(btn,Twting) {
            if (Twting) {
                btn.src = 'images/pause.png';
            } else {
                btn.src = 'images/play.png';
            }
        }
        
        function clearTweets() {
            FirstTweetID = 0;
            FirstMessageID = 0;
            FirstSubscrID = 0;
            LastTweets = null;
        }

        function GetTweetsTimer() {
            Timer_Tweets = setTimeout("GetTweets()",TWEET_RATE);
        }

        function GetTweetsWithWait() { //this can be used to delay a call to GetTweets until the previous call is completed, and combines resetting the tweets as required for use with the pageloaded handler
            if (WaitForTweets) { Timer_WaitForTweets = setTimeout("GetTweetsWithWait()",WAITTWEETS_RATE); }
                else {
                        clearTweets();
                        //tweetcount=0;//for debug
                        Refreshing=false;
                        GetTweets();
                    }
        }

        // This function calls the Web Service method.  cf: http://msdn.microsoft.com/en-us/library/bb515101.aspx and
        // also see http://www.singingeels.com/Articles/Using_Page_Methods_in_ASPNET_AJAX.aspx for implementing as a Page Method vs a Web Service
        function GetTweets() {
            var FreshStream = (FirstTweetID == 0 && FirstMessageID == 0 && FirstSubscrID == 0);
            if ((Tweeting || FreshStream) && !Refreshing) { //added && !Refreshing to ensure cannot get event reentry... even using GetTweetsWithWait am still seeing this if a tweet refresh occurs at the 'wrong' time before GetTweetsWithWait executes
                WaitForTweets=true;
                PageMethods.GetTwitterData('FRIENDS_TIMELINE',ShowHistoryDate,ShowHistoryNewer,FreshStream,OnSucceeded_Tweets,OnFailed_Tweets);
            }
            return false;
        }

        // This is the callback function that
        // processes the Web Service return value.
        function OnSucceeded_Tweets(result) {
            if (Unloading || Refreshing) { WaitForTweets=false; return; }
            //var RsltElem = $get("Results");
            //RsltElem.innerHTML = result;
            var isHistory = false;
            if (result=='[NO RESULTS]'){ //special null result passed from search for example
                var Tweets = null;
                alert('No Results were found matching your selection.');
                var search_none=true;
            } else if (result=='[NONE]') {
                var Tweets = null; //special null result passed from normal tweet select method that had no results
                var search_none=true;
            } else if (result=='H[NONE]'){ //special null result passed from History if no results found
                $get(imHistoryLoading_id).style.display="none"; //hide preloading
                $get(cmdHistoryOlder_id).disabled=false;
                $get(cmdHistoryNewer_id).disabled=false;
//                $get(cmdHistoryLast_id).disabled=false;
                alert("There is no more history available.");
                WaitForTweets=false;
                return;
            } else {
                if (result.indexOf('H')==0) {
                   isHistory=true;
                   result=result.substr(1);
                   clearTweets(); //so the message list is fully refreshed
                }
                var Tweets = eval('(' + result + ')'); //convert the JSON string back to a Javascript Object/code...
                var search_none=false;
            }

            if (!Tweets||result=='[]') {Tweets = LastTweets; LastTweets = null;} //add some immunity to a Twitter response being null due to un untrapped error in the Twitter Class / or bows db lock timeouts

            var args = Tweets;
            if (LastTweets && (Tweets.length==TWEETS_PER_PAGE)) {args=args.concat(LastTweets);} // bolt last tweets onto end of new tweets so we definitely have enough to fill the panel if some are still off the top to be scrolled, but keep copy of new Tweets as is for below

            var x;
            var mytext='<table class="tweet" cellpadding="0" cellspacing="0">';
            mytext += '<tr><td colspan="2" class="tweets_header">&nbsp;</td></tr>';
            
            // Get current date and time
            var d = new Date();
            
            // convert to msec
            // add local time zone offset 
            // get UTC time in msec, and remove calculated offset between client and twitter server time
            var utc_offset = (d.getTimezoneOffset() * 60000) - TwitterTimeOffset;//ms
            var utc = d.getTime() + utc_offset;
            var newTweetCount = 0;
            var newFirstTweetID = 0;
            var newFirstMessageID = 0;
            var newFirstSubscrID = 0;
            
            var loLoggedIn = (LoggedIn == "True");

            var scroll_panel=$get("tomove");
            var scroll_panel_style_top = parseInt(scroll_panel.style.top);
            var TweetsToScroll = Math.round(-scroll_panel_style_top/TWEET_HEIGHT); //calc the no of Tweets still above the panel top, so can work out how many to add to fill the panel, taking into account also new tweets

            if (args) {
                if (ShowHistoryDate){ //Older History Mode
                    if (!isHistory) { //then this instance is the result of a normal messages callback that was in effect as the history older button was clicked...
                        return; //so simply abort the callback
                    }
                    $get(imHistoryLoading_id).style.display="none"; //hide preloading
                    $get(cmdHistoryOlder_id).disabled=false;
                    $get(cmdHistoryNewer_id).disabled=false;
                    $get(cmdHistoryLast_id).disabled=false;
                }
            }

            for (x in args) { if (x > (newTweetCount+TweetsToScroll+(Tweets.length-1))) { break; }
                    
                if (args[x].user) {//want to put the following number of tweets in the panel: DownloadedCount + newTweetCount + TweetsToScroll: as the latter two params amounts will be off the top of the panel
                    var tweet = args[x].text;
                    var tweet_id = args[x].id + '_T';
                    
                    var is_mention = false; //of the current user
                    if (args[x].current_user){
                        var pattn = new RegExp("(\\W|^)" + args[x].current_user + "(\\W|$)","i");
                        if (tweet.match(pattn)) {
                            is_mention = true;
                        }
                    }

                    if (args[x].user.member) { //determine background for tweet
                        switch (args[x].msg_type) {
                            case 0://Tweet
                            case 255://Internal msg from Premium member... visible to all; contains enriched features
                                var can_retweet = true;
                                var can_reply = true;
                                if (is_mention) {
                                    var class_ext = ' flw_Mn';
                                    var reply_text = ' in reply to ';
                                }
                                else {
                                    var class_ext = ' flw';
                                    var reply_text = ' in reply to ';
                                }
                                break;
                            case 1://DM
                            case 254://Internal msg from Premium member... DM like; contains enriched features
                                var can_retweet = true;
                                var can_reply = false; //but still via DM can if followed by sender
                                var class_ext = ' flw_DM';
                                var reply_text = ' to ';
                                break;
                            default://Subscription msg
                                var can_retweet = false;
                                var can_reply = false; //but still via DM can if followed by sender
                                var class_ext = ' D2';
                                var reply_text = ' to ';
                        }
                    } else {
                        switch (args[x].msg_type) {
                            case 0://Tweet
                            case 255://internal public message
                                var can_retweet = true;
                                var can_reply = true;
                                if (is_mention) {
                                    var class_ext = ' Mn';
                                    var reply_text = ' in reply to ';
                                }
                                else {
                                    var class_ext = '';
                                    var reply_text = ' in reply to ';
                                }
                                break;
                            case 1://DM
                            case 254://Internal msg from Premium member... DM like; contains enriched features
                                var can_retweet = true;
                                var can_reply = false; //but still via DM can if followed by sender
                                var class_ext = ' DM';
                                var reply_text = ' to ';
                                break;
                            default://Internal Subscription msg; can contain enriched features
                                var can_retweet = false;
                                var can_reply = false; //but still via DM can if followed by sender
                                var class_ext = ' D2';
                                var reply_text = ' to ';
                        }
                    }

                    mytext += '<tr id="' + tweet_id + '"><td class="tweet_left' + class_ext + '">';
                    mytext += '<div class="tweet_left_top">';
                        mytext += '<div class="tweet_avatar"><a href="/Main.aspx?screen_name=' + args[x].user.screen_name + '"><img src="' + args[x].user.profile_image_url_bigger + '" alt="' + args[x].user.screen_name + '" title="' + args[x].user.screen_name + '"></a></div>';
                        mytext += '<div class="tweet_rating">';
                            if (args[x].user.view_influence) {
                                mytext += '<img src="/RenderInfluence.aspx?v=' + args[x].user.influence + '" alt="BOWS Influence rating: ' + args[x].user.influence + '" title="BOWS Influence rating: ' + args[x].user.influence + '" />';
//                                if (args[x].user.rating) {
//                                    for (r=1; r<=5; r++) {
//                                        if (r>args[x].user.rating){ mytext += '<div><img src="images/EmptyStar.png" /></div>'; }
//                                            else { mytext += '<div><img src="images/FilledStar.png" /></div>'; }
//                                    }
//                                }
                            } else { mytext += '&nbsp;'; }
                        mytext += '</div>';
                    mytext += '</div>';
//                    mytext += '<div class="tweet_left_bottom">';
////                        mytext += '<div class="tweet_follow">';
////                        mytext += '</div>';
////                        mytext += '<div class="tweet_block">';
////                        mytext += '</div>';
//                        mytext += '&nbsp;';
//                    mytext += '</div>';
                    mytext += '</td>';
                    
                    if (loLoggedIn) {
                        mytext += '<td class="tweet_right' + class_ext + '" onmouseout="symbol_mouseout()">';//to make sure the popup chart disappears if the mousout is missed on the symbol itself
                    }
                    else {
                        mytext += '<td class="tweet_right' + class_ext + '">';
                    }
                    mytext += '<div class="tweet_content">';
                    
                    mytext += '<div class="tweet_header">';
                    var displayName = args[x].user.screen_name;
//                    if (args[x].user.real_name) { var displayName = args[x].user.real_name; } //real_name now always to be passed
//                        else { var displayName = args[x].user.name; }
                    mytext += '<span class="tweet_name"><a href="/Main.aspx?screen_name=' + args[x].user.screen_name + '">' + displayName + '</a>'
                    if (args[x].user.followed) {
                        mytext += ' <span style="font-weight:normal;font-style:italic;">(follower)</span>';
                    }
                    mytext +='</span>';

                    if (loLoggedIn) {
                        if (args[x].user.isadmin && !args[x].user.iswhitelisted) {
                            mytext += '&nbsp;<img src="images/block.png" title="Block" alt="Block" style="cursor:pointer;vertical-align:top;" onclick="javascript:return doBlockUser(this,\'' + args[x].user.id + '\',\'' + args[x].user.real_name + '\');">';
                        } else { mytext += '&nbsp;'; }
                        if (!args[x].user.following) {
                            mytext += '&nbsp;<img src="images/follow2.gif" title="Follow" alt="Follow" style="cursor:pointer;vertical-align:top;" onclick="javascript:return doFollowUser(this,\'' + args[x].user.id + '\');">';
                        }
                    }

                    if (args[x].in_reply_to_screen_name) {
                        mytext += '<span class="tweet_from">' + reply_text + '<a href="/Main.aspx?screen_name=' + args[x].in_reply_to_screen_name + '">@' + args[x].in_reply_to_screen_name + '</a></span>';
                    }

                    mytext += '</div>';//header
                    
                    //make any http references hyperlinked: cf: http://www.w3schools.com/jsref/jsref_obj_regexp.asp
                    tweet = tweet.replace(/(http|https):\/\/\S*(?=\s?)/gi,'<a href="$&" target="_blank">$&</a>');

                    //make any twitter @ mentions highlighted and linked to the users twitter page
                    tweet = tweet.replace(/@(\w+)/g,'<span class="tweet_mention"><a href="/Main.aspx?screen_name=$1">$&</a></span>');

                    //make any twitter $ Tickers highlighted and linked to the ticker profile page using regex search and replace
                    //var pattn = /[$]((?:[a-zA-Z]+:)?((?=[a-zA-Z]\b)|(?=\w*[a-zA-Z]\w))\w+(?:\.[a-zA-Z]+)*)/g;
                    var pattn = /[$]((?:[a-zA-Z]+:)?[a-zA-Z]\w*(?:\.[a-zA-Z]+)*)/g; // find $ followed by (optionally just chars a-z and : ), then (alphanumeric) sequence that contains at least one letter before the last character, or just one letter, (followed optionally by a sequence of chars a-z (but no numbers) preceded by .) that can repeat   
                                                                                            // $1 will reference the only captured back ref... which is everything less the $; $& will return the entire match incl the $
                    if (loLoggedIn) {
                        tweet = tweet.replace(pattn,'<span class="tweet_ticker" onmouseover="symbol_mouseover(event,\'$1\')" onmouseout="symbol_mouseout()" onmousemove="symbol_mousemove(event)"><a href="/Main.aspx?ticker=$1">$&</a></span>');
                    }
                    else {
                        tweet = tweet.replace(pattn,'<span class="tweet_ticker"><a href="/Main.aspx?ticker=$1">$&</a></span>');
                    }
                    mytext += '<div class="tweet_body">' + tweet;
                        if (args[x].trade_flags) { //Add Enriched content from trade flags
                            for (t in TradeFlags) { //defined in main.js... this code SHOULD only execute on main.aspx; if not then will need to wrap this is a try catch or define TradeFlags on the page as null
                                if (TradeFlags[t].Value & args[x].trade_flags) {
                                    mytext += '<img src="' + TradeFlags[t].IconURL + '" alt="' + TradeFlags[t].FlagName + '" title="' + TradeFlags[t].FlagName + '" />';
                                }
                            }
                        }
                    mytext += '</div>';//body

                    mytext += '<div class="tweet_body_footer">';
                        mytext += '<div style="float:left;">';
                        if (args[x].created_at) {
                            mytext += '<span class="tweet_time">';
    //                        var sd=args[x].created_at.split(' '); //Example: Sat Jan 24 22:14:29 +0000 2009   seems to always be GMT time zone ???
    //                        var nutc= Date.parse(sd[1] + ' ' + sd[2] + ', ' + sd[5] + ' ' + sd[3]);// + (sd[4] * 3600000);//utc_offset is in secs and is negative for north american time offsets... ie opposite of normal offset sign
                            var nutc= Date.parse(args[x].created_at); //created_at now preformatted before being passed...
                            var datediff = utc - nutc;
                            if (datediff<60000) { //up to 1 minute
                                var units = Math.max(Math.round(datediff/1000),1); //ensure that report no less than 1 sec; could happen due to time diffs between Twitter and Web Server
                                mytext += 'about ' + units + ' second';
                                if (units > 1) { mytext += 's'; }
                                mytext += ' ago';
                                }
                            else if (datediff<3600000) { //up to 1 hour
                                var units = Math.round(datediff/60000);
                                mytext += 'about ' + units + ' minute';
                                if (units > 1) { mytext += 's'; }
                                mytext += ' ago';
                                }
    //                        else if (datediff<86400000*3) { //up to 72 hours
    //                            var units = Math.round(datediff/3600000);
    //                            mytext += 'about ' + units + ' hour';
    //                            if (units > 1) { mytext += 's'; }
    //                            mytext += ' ago';
    //                            }
                            else {
                                var clientdate = new Date(nutc - utc_offset); //convert twitter UTC time to syncronised client time zone
                                mytext += '<span class="tweet_date">' + clientdate.toLocaleDateString() + ' </span>' + clientdate.toLocaleTimeString();
                                }
                            mytext += '</span>';
                        }
                        mytext += '&nbsp;<span class="tweet_from">from ' + args[x].source;
                        mytext += '</span>';
                        mytext += '</div>';
                    mytext += '</div>';//body_footer
                    mytext += '</div>';//content
                    mytext += '<div class="tweet_commands">';
                    if (loLoggedIn) {
//                        if (args[x].bookmarked) {
//                            //mytext += '<div><img src="images/unbookmark.png" title="Remove Bookmark" alt="Remove Bookmark" style="cursor:pointer;" onclick="javascript:return doToggleBookmark(this,\'' + args[x].id + '\', ' + args[x].msg_type + ', ' + args[x].bookmarked + ');"></div>';
//                        } else {
//                            mytext += '<div><img src="images/bookmark.png" title="Bookmark This" alt="Bookmark This" style="cursor:pointer;" onclick="javascript:return doToggleBookmark(this,\'' + args[x].id + '\', ' + args[x].msg_type + ', ' + args[x].bookmarked + ');"></div>';
//                        }
                        if (can_reply || args[x].user.followed) { //need title and alt set so both firefox and IE display tooltip
                            mytext += '<div><img src="images/reply.png" title="Reply" alt="Reply" style="cursor:pointer;" onclick="javascript:return OpenReplyPanel(this,\'' + args[x].user.id + '\', \'' + tweet_id + '\', \'REPLY\', \'' + escape(args[x].text) + '\', ' + args[x].msg_type + ');"></div>';
                        }
                        if (args[x].user.followed) {
                            mytext += '<div><img src="images/dm.png" title="Direct Message" alt="Direct Message" style="cursor:pointer;" onclick="javascript:return OpenReplyPanel(this,\'' + args[x].user.id + '\', \'' + tweet_id + '\', \'NEWDM\', \'' + escape(args[x].text) + '\', ' + args[x].msg_type + ');"></div>';
                        }
                        if (can_retweet) {
                            mytext += '<div><img src="images/retweet.png" title="Retweet" alt="Retweet" style="cursor:pointer;" onclick="javascript:return OpenReplyPanel(this,\'' + args[x].user.id + '\', \'' + tweet_id + '\', \'RETWEET\', \'' + escape(args[x].text) + '\', ' + args[x].msg_type + ');"></div>';
                        }
                    }
//                    if (args[x].user.member) {
//                        if (args[x].user.bull) { mytext += '<img src="images/bull.png" class="tweet_tradeicon" alt="Bull Trader" />'; }
//                        if (args[x].user.bear) { mytext += '<img src="images/bear.png" class="tweet_tradeicon" alt="Bear Trader" />'; }
//                        if (args[x].user.hedgie) { mytext += '<img src="images/hedgie.png" class="tweet_tradeicon" alt="Hedgie Trader" />'; }
//                    }
                    //mytext += '<a href="http://twitter.com/' + args[x].user.screen_name + '" target="_blank"><img src="images/twitter.png" alt="T"></a>';
                    mytext += '</div>';//commands
                    mytext += '</td></tr>';
                    
                    //now calculate necessary shift upwards for new tweets to be scrolled back down
                    switch (args[x].msg_type) {
                        case 0://Tweet
                            if (newFirstTweetID == 0) { newFirstTweetID = args[x].id; }
                            if (args[x].id > FirstTweetID){ newTweetCount++; }
                            break;
                        case 1://DM
                            if (newFirstMessageID == 0) { newFirstMessageID = args[x].id; }
                            if (args[x].id > FirstMessageID){ newTweetCount++; }
                            break;
                        default://Subscription msg
                            if (newFirstSubscrID == 0) { newFirstSubscrID = args[x].id; }
                            if (args[x].id > FirstSubscrID){ newTweetCount++; }
                    }
    
                    mytext += '<tr id="' + tweet_id + '3"><td colspan="2" class="tweet_footer">&nbsp;</td></tr>';
                }
            }

            mytext += '</table>';

            LastTweets = Tweets; // cache the tweets just received to possible use in the next call to this function
           
            if (args||search_none) { //ie: if some tweets present/search has no results... otherwise leave the panel as is for now... nb search_none is passed to eval with result
                var panelTopShift = 0;
                if (FirstTweetID != 0 || FirstMessageID != 0 || FirstSubscrID != 0) { panelTopShift = newTweetCount*TWEET_HEIGHT;}// if (LoggedIn=="True"){alert('tweetcount: '+tweetcount+'  linenum: '+linenum);}
                    else if (btnScroll_Items_Init) { panelTopShift = TWEETS_PER_PAGE*TWEET_HEIGHT; } //this line for initial 'cascade' scroll effect

                //alert(panelTopShift);
                if (panelTopShift != 0) { scroll_panel.style.top = (scroll_panel_style_top - panelTopShift) + 'px'; } //Firefox requires the value to be a string with units
            
                scroll_panel.innerHTML = mytext;
                FirstTweetID = newFirstTweetID;
                FirstMessageID = newFirstMessageID;
                FirstSubscrID = newFirstSubscrID;
            }

            if (Tweeting && !StallTweets && !ShowHistoryDate) { GetTweetsTimer(); }
            
            //alert(result);
            //tweetcount+=1; //for debug
            WaitForTweets=false;
            if (btnScroll_Items_Init){ ScrollTweets(); }
        }
        function OnFailed_Tweets(error) {
            if (Unloading) { WaitForTweets=false; return; }
            // Alert user to the error. NB the timer will stop here...
            if (DEBUG_ALERTS) {alert('Error:' + error.get_message());}
            if (Tweeting) { GetTweetsTimer(); }
            WaitForTweets=false;
        }

        function ScrollTweets_StartStop(scrollbtn) {
            btnScroll_Items = scrollbtn;
            Scrolling = !Scrolling;
            if (Scrolling) {
                if (!btnScroll_Items_Init) { ScrollTweets(); } //if tweets and scrolling have just been started, then the first scroll will be fired once tweets finish loading, using the second animation extended
                //ScrollTweetsTimer();
            } else {
                clearTimeout(Timer_Scroll);
            }
            return false;
        }
        
        function ScrollTweetsTimer() {
            Timer_Scroll = setTimeout("ScrollTweets()",SCROLL_RATE);
            WaitForScroll = false;
        }
        
        function ScrollTweets() {
            if (Scrolling) {
                if (parseInt($get("tomove").style.top)<0) { //if there are atill new tweets to scroll, process with animation
                    WaitForScroll = true; //used for locking updates to the panel while it is actually scrolling; this gets released in ScrollTweetsTimer / at the end of the animation extender
                    if (btnScroll_Items_Init){
                        setTimeout("$get(btnScroll_Items_Init).click();btnScroll_Items_Init = null;",100); //SJAMOD 26 Mar 2010: wrapped in a brief delay timer as sometimes not firing the initial scroll action
                        }
                      else { $get(btnScroll_Items).click(); }
                } else { //no current new tweets, process without animation
                     ScrollTweetsTimer();
                }
            }
        }

        function ScrollTweets_GetTweets_StartStop(btn, scrollbtn, scrollbtninit) { //single function to pause and start scrolling and tweeting simultaneously
            btnScroll_Items_Init = scrollbtninit;
            ScrollTweets_StartStop(scrollbtn);
            GetTweets_StartStop(btn); //this second function will change the btn's image
            return false;
        }

        function action_unload()
        {
            if (Timer_Tweets) {clearTimeout(Timer_Tweets);}
            if (Timer_Tweets_Friends) {clearTimeout(Timer_Tweets_Friends);}
            Unloading=true;
            if (WaitForTweets||WaitForTweets_Friends) {do_nothing();} //call an actual function that uses setTimeout so that the page method has a chance to fire its success/fail event.
        }
        function do_nothing() {
            //alert("Doing Nothing...");
            if (WaitForTweets||WaitForTweets_Friends) {
                var t=setTimeout("do_nothing();",100);
            }
            return;
        }
        function waitfor_ms(delay) { //ms delay
            var d = new Date();
            var t= d.getTime();
            var t2 = t;
            while (t2-t<delay) {
                var d2 = new Date();
                t2 = d2.getTime();
            }
        }
        
        // *** HISTORY METHODS ***

        function ShowHistoryOlderCmd() {
            if (LastTweets && LastTweets.length>=TWEETS_PER_PAGE)
            {
                if (!ShowHistoryDate){ //just entering history mode... 
                    $get(cmdHistoryCancel_id).style.display=""; //display the history cancel button
                    $get(cmdHistoryNewer_id).style.display=""; //display the history Newer button
                }
                $get(imHistoryLoading_id).style.display=""; //show preloading
                $get(cmdHistoryOlder_id).disabled=true; //debounce history button
                
                ShowHistoryNewer = false;//alert(LastTweets[TWEETS_PER_PAGE-1].created_at); 
                ShowHistoryDate = new Date(Date.parse(LastTweets[TWEETS_PER_PAGE-1].created_at+' GMT')); //set the Date seed to get messages older than, need to spec timezone retain as GMT
                if (Timer_Tweets) {clearTimeout(Timer_Tweets);} //stop the timer - though still need code in the callback message handler to abort display of a callback already in effect
                GetTweets(); //immediately get the historical messages
            } else alert("There is no more history available.");
            return false;
        }

        function ShowHistoryNewerCmd() {
            if (LastTweets && LastTweets.length>0)
            {
                $get(imHistoryLoading_id).style.display=""; //show preloading
                $get(cmdHistoryNewer_id).disabled=true; //debounce history button
                
                ShowHistoryNewer = true;
                ShowHistoryDate = new Date(Date.parse(LastTweets[0].created_at+' GMT')); //set the ID seed to get messages newer than
                if (Timer_Tweets) {clearTimeout(Timer_Tweets);} //stop the timer - though still need code in the callback message handler to abort display of a callback already in effect
                GetTweets(); //immediately get the historical messages
            } else alert("There is no more history available.");
            return false;
        }

        function ShowHistoryLastCmd() {
            if (LastTweets && LastTweets.length>=TWEETS_PER_PAGE)
            {
                if (!ShowHistoryDate){ //just entering history mode... 
                    $get(cmdHistoryCancel_id).style.display=""; //display the history cancel button
                    $get(cmdHistoryNewer_id).style.display=""; //display the history Newer button
                }
                $get(imHistoryLoading_id).style.display=""; //show preloading
                $get(cmdHistoryLast_id).disabled=true; //debounce history button
                
                ShowHistoryNewer = true;
                ShowHistoryDate = new Date(0); //a 1970 date to be older than anything in the stream, but not null for flag purposes
                //ShowHistoryDate = null; //true,null implies that messages will be taken from the oldest
                if (Timer_Tweets) {clearTimeout(Timer_Tweets);} //stop the timer - though still need code in the callback message handler to abort display of a callback already in effect
                GetTweets(); //immediately get the historical messages
            } else alert("There is no more history available.");
            return false;
        }
        
        function HistoryCancelCmd(refresh) {
            if (ShowHistoryDate)
            {
                ShowHistoryNewer = false;
                ShowHistoryDate = null; //Cancel History mode
                $get(cmdHistoryCancel_id).style.display="none"; //hide the history cancel button
                $get(cmdHistoryNewer_id).style.display="none"; //hide the history Newer button
                $get(imHistoryLoading_id).style.display="none"; //hide preloading
                if (refresh) {
                    clearTweets(); //so the message list is fully refreshed
                    GetTweets(); //Get the live message list
                }
            }
            return false;
        }

// *******************************************************************************************        
// ********************* Client Side 'Friends' Stream Methods and Events *********************
// *******************************************************************************************        

        var Tweets_per_Page_Friends=8; //updated via config
        var Tweets_Pages_Friends=4; //updated via config

        var Timer_Scroll_Friends;
        var Timer_Tweets_Friends;
        var Timer_WaitForTweets_Friends;

        var btnScroll_Items_Friends; //have to cache the ClientID of these hidden buttons as we are in a .js file, that cannot easily see clientIDs

        var TWEET_RATE_FRIENDS = 60000; //ms 
        var TWEET_HEIGHT_FRIENDS = 59; //px
        var REQUEST_TIMEOUT = 10000; //ms 
       
        var Tweets_Page_Friends = 1; //myFriends is paged differently to the main stream; X pages of Y tweets are preloaded in one go, and just dynamically paged on display...
        var FirstTweetID_Friends = 0;
        var Scrolling_Friends = false;
        var WaitForScroll_Friends = false;
        var Tweeting_Friends = false;
        var WaitForTweets_Friends = false;
        var Refreshing_Friends = false;
        
        //var C_FRIENDSSTREAM = 'FRIENDS_STREAM'; Can't cache something so large to cookies.... won't fit, and also goes to server and back with each request... not other solution known to do this between page loads client-side...
        var C_FRIENDSSTREAM_USERID = 'FRIENDS_STREAM_USERID';
        var C_FRIENDSSTREAM_LAST_DATE = 'FRIENDS_STREAM_LASTDATE';

        //cf: http://www.position-absolute.com/articles/display-your-tweet-in-real-time-a-basic-twitter-api-use/ 
        function createJSONcallback(url){
            //ensure the url passed looks like a proper twitter url... as a precaution
            if (url.indexOf('http://api.twitter.com/1/statuses/')==0){
                var getTweet = document.createElement("script");
                //url='http://api.twitter.com/1/statuses/public_timeline.json?count=10' + '&' + 'callback=' + callbackfn_name;
                getTweet.src=url;
                getTweet.type='text/javascript';
                document.getElementById('twitter_script').appendChild(getTweet) 
            }
        }

        function GetTweets_StartStop_Friends(btn) {
            Tweeting_Friends = !Tweeting_Friends;
            GetTweets_SetImage(btn,Tweeting_Friends);
            if (Tweeting_Friends) {
                GetTweets_Friends(); //due to this being on a 30 sec timer, when stream restarted, check immediately for update; nb session object caches the checks on a 28 sec basis
//                if (FirstTweetID_Friends == 0) { //immediately update the tweets, as there are none, which will also start the timer
//                    GetTweets_Friends();
//                } else { //update via timer
//                    GetTweetsTimer_Friends();
//                }
            } else {
                clearTimeout(Timer_Tweets_Friends);
            }
            return false;
        }

        function GetTweetsTimer_Friends() {
            Timer_Tweets_Friends = setTimeout("GetTweets_Friends()",TWEET_RATE_FRIENDS);
        }

        function GetTweetsWithWait_Friends() { //this can be used to delay a call to GetTweets_Friends until the previous call is completed, and combines resetting the tweets as required for use with the pageloaded handler
            if (WaitForTweets_Friends) { Timer_WaitForTweets_Friends = setTimeout("GetTweetsWithWait_Friends()",WAITTWEETS_RATE); }
                else {
                        FirstTweetID_Friends = 0;
                        Refreshing_Friends=false;
                        GetTweets_Friends();
                    }
        }

        // This function calls the Web Service method.  cf: http://msdn.microsoft.com/en-us/library/bb515101.aspx and
        // also see http://www.singingeels.com/Articles/Using_Page_Methods_in_ASPNET_AJAX.aspx for implementing as a Page Method vs a Web Service
        function GetTweets_Friends() {
            if ((Tweeting_Friends || FirstTweetID_Friends == 0) && !Refreshing_Friends) { 
                WaitForTweets_Friends=true;
                if (USE_JSONP) {
                    //use client side JSONP request to get the stream - still don't have a solution to pure client side caching of this data... cookie solution below no good.
//                    var _now = new Date();
//                    var _nowUTC = _now.getTime();
//                    
//                    var _lastUserID = getCookie(C_FRIENDSSTREAM_USERID);
//                    if (!_lastUserID) {
//                        _lastUserID=LoggedIn_UserID;
//                        setCookie(C_FRIENDSSTREAM_USERID,LoggedIn_UserID,1);
//                    }
//                    
//                    var _lastDate = getCookie(C_FRIENDSSTREAM_LAST_DATE);
//                    if (!_lastDate) { _lastDate=0; }
//                    
//                    alert(_lastUserID + ' ' + LoggedIn_UserID);
//                    if (_lastUserID==LoggedIn_UserID) {//ensure cached stream caches only for current user
//                        var _lastStream = getCookie(C_FRIENDSSTREAM);
//                        alert(_lastStream);
//                    } else {
//                        var _lastStream = "";
//                        setCookie(C_FRIENDSSTREAM,"",-1); //clear the cookie also
//                        alert('reset');
//                    }
//                    //alert(_nowUTC - _lastDate);
//                    if ((Tweeting_Friends && (_nowUTC >= _lastDate+28000)) || !_lastStream) { //if 28 secs or more from last stream update, or if cache is empty...
//                        alert('using updated friends stream');
//                        PageMethods.GetTwitterData('FRIENDS_SIGNED_URL',null,false,false,OnSucceeded_url_Friends,OnFailed_url_Friends); //try to get updated stream
//                    } else {
//                        alert('using cached friends stream');
//                        OnSucceeded_Tweets_Friends(_lastStream,true); //used cached copy
//                    }

                    PageMethods.GetTwitterData('FRIENDS_SIGNED_URL',null,false,false,OnSucceeded_url_Friends,OnFailed_url_Friends); //try to get updated stream
                    
                } else {
                    //use server as proxy for the stream
                    if (Tweeting_Friends) {
                        var strCmd = 'MYFRIENDS_STREAM';
                    } else {
                        var strCmd = 'MYFRIENDS_STREAM_REDISP'; //just display tweets from session cache if possible... stream not running
                    }
                    PageMethods.GetTwitterData(strCmd,null,false,false,OnSucceeded_Tweets_Friends,OnFailed_url_Friends); //note that the page passed here is the twitter page of tweetsperpage*pages to pass back... not to confuse with displayed page within those results locally
                }
            }
            return false;
        }

        // This is the callback function that
        // processes the Web Service return value.
        function OnSucceeded_url_Friends(url) {
            if (url) {//alert(url);
                createJSONcallback(url); //callback_fn gets added by the page method into this url, as has to be presigned
            } else {
                if (Unloading) { WaitForTweets_Friends=false; return; }
                if (DEBUG_ALERTS) {alert('Error:' + error.get_message());}
                if (Tweeting_Friends) { GetTweetsTimer_Friends(); }
                WaitForTweets_Friends=false;
            }
        }
        
        function OnTimeout_Friends() {
            if (Unloading) { WaitForTweets_Friends=false; return; }
            if (Tweeting_Friends) { GetTweetsTimer_Friends(); }
            WaitForTweets_Friends=false;
        }

        function OnError_Friends(e) { //e will hold 0 for a generic comms error, or the http status error code....
            if (Unloading) { WaitForTweets_Friends=false; return; }
            if (DEBUG_ALERTS) {alert('OnError_Friends: ' + e);}
            if (Tweeting_Friends) { GetTweetsTimer_Friends(); }
            WaitForTweets_Friends=false;
        }

        // This is the callback function that
        // processes the AJAX request return value.
        function OnSucceeded_Tweets_Friends(result,fromCache) {
            if (Unloading || Refreshing_Friends) { WaitForTweets_Friends=false; return; }
            //var RsltElem = $get("Results");
            //RsltElem.innerHTML = result;
            
            if (fromCache==null) {var fromCache = false;} //ensure value is valid as callbacks omit this param

            var retry = true;
            while (retry) { retry = false;

                try {
                    if (USE_JSONP) {
                        //for JSONP responses, check the results look like tweets by testing for the first tweets, user screen_name
//                        if (fromCache) {
//                            var Tweets = JSON.parse(result); //deserialise the JSON string back to a Javascript Object, using safe parser, as data came from a cookie
//                        } else {
                            var Tweets = result; //JSONP callback returns the actual object, no need to deserialise
//                        }
                        var test=Tweets[0].user.screen_name;
                        if (!test){
//                            if (!fromCache){ //redo the test, but this time with the cached stream...
//                                var result=getCookie(C_FRIENDSSTREAM);
//                                fromCache = true;
//                                retry = true;
//                            } else {
                                WaitForTweets_Friends=false;
                                if (Tweeting_Friends) { GetTweetsTimer_Friends(); }
                                return;
//                            }
                        }
                    } else {
                        //for Server Proxied tweets, returned as JSON strings, prevalidated
                        if (!result) {
                            WaitForTweets_Friends=false;
                            if (Tweeting_Friends) { GetTweetsTimer_Friends(); }
                            return;
                        }
                        var Tweets = eval('(' + result + ')'); //convert the JSON string back to a Javascript Object
                    }
                    
                }
                catch(e){
//                    if (USE_JSONP && !fromCache) {
//                        var result=getCookie(C_FRIENDSSTREAM);
//                        fromCache = true;
//                        retry = true;
//                    } else {
                        WaitForTweets_Friends=false;
                        if (Tweeting_Friends) { GetTweetsTimer_Friends(); }
                        return;
//                    }
                }
            }//while

            // Get current date and time
            var d = new Date();
            
//            if (USE_JSONP && !fromCache) { //cache the updated stream
//                alert('caching friends stream... ');
//                setCookie(C_FRIENDSSTREAM,JSON.stringify(Tweets),1);
//                setCookie(C_FRIENDSSTREAM_USERID,LoggedIn_UserID,1);
//                setCookie(C_FRIENDSSTREAM_LAST_DATE,d.getTime(),1);
//            }

            var args = Tweets;
            
            var class_ext = ' friends';

            var x;
            var mytext='<table class="tweet' + class_ext + '" cellpadding="0" cellspacing="0">';
            mytext += '<tr><td colspan="2" class="tweets_header">&nbsp;</td></tr>';
            
            // convert to msec
            // add local time zone offset 
            // get UTC time in msec, and remove calculated offset between client and twitter server time
            var utc_offset = (d.getTimezoneOffset() * 60000) - TwitterTimeOffset;//ms
            var utc = d.getTime() + utc_offset;
            var newTweetCount = 0;
            var newFirstTweetID = 0;
            
            var loLoggedIn = (LoggedIn == "True");

            var scroll_panel=$get("tomove_Friends");
            var scroll_panel_style_top = parseInt(scroll_panel.style.top);
            var TweetsToScroll = Math.round(-scroll_panel_style_top/TWEET_HEIGHT_FRIENDS); //calc the no of Tweets still above the panel top, so can work out how many to add to fill the panel, taking into account also new tweets
            
            var page_offset=(Tweets_Page_Friends-1)*Tweets_per_Page_Friends;
            var page_upperbound=Math.min(args.length,1*page_offset+1*Tweets_per_Page_Friends); //alert(page_offset + ' ' + page_upperbound+ ' ' + args.length);

            for (x=page_offset; x<page_upperbound; x++) { if (x-page_offset > (newTweetCount+TweetsToScroll+(Tweets.length-1))) { break; }
                    
                if (args[x].user) {//want to put the following number of tweets in the panel: DownloadedCount + newTweetCount + TweetsToScroll: as the latter two params amounts will be off the top of the panel
                    var tweet = args[x].text;
                    var tweet_id = args[x].id + '_T';
                    var reply_text = ' in reply to ';
                    
                    mytext += '<tr id="' + tweet_id + '"><td class="tweet_left' + class_ext + '">';
                    mytext += '<div class="tweet_left_top' + class_ext + '">';
                        mytext += '<div class="tweet_avatar' + class_ext + '"><a href="/Main.aspx?screen_name=' + args[x].user.screen_name + '"><img src="' + args[x].user.profile_image_url + '" alt="' + args[x].user.screen_name + '" title="' + args[x].user.screen_name + '"></a></div>';
                    mytext += '</div>';
                    mytext += '</td>';
                    
                    if (loLoggedIn) {
                        mytext += '<td class="tweet_right' + class_ext + '" onmouseout="symbol_mouseout()">';//to make sure the popup chart disappears if the mousout is missed on the symbol itself
                    }
                    else {
                        mytext += '<td class="tweet_right' + class_ext + '">';
                    }
                    mytext += '<div class="tweet_content' + class_ext + '">';
                    
                    mytext += '<div class="tweet_header' + class_ext + '">';
                    var displayName = args[x].user.screen_name;

                    mytext += '<span class="tweet_name' + class_ext + '"><a href="/Main.aspx?screen_name=' + args[x].user.screen_name + '">' + displayName + '</a>'
                    mytext +='</span><br>&nbsp;&nbsp;';

                    if (args[x].created_at) {
                        mytext += '&nbsp;<span class="tweet_time">';

                        var sd=args[x].created_at.split(' '); //Example: Sat Jan 24 22:14:29 +0000 2009   seems to always be GMT time zone
                        var nutc= Date.parse(sd[1] + ' ' + sd[2] + ', ' + sd[5] + ' ' + sd[3]);
                        //var nutc= Date.parse(args[x].created_at); //created_at now preformatted before being passed...
                        var datediff = utc - nutc;
                        if (datediff<60000) { //up to 1 minute
                            var units = Math.max(Math.round(datediff/1000),1); //ensure that report no less than 1 sec; could happen due to time diffs between Twitter and Web Server
                            mytext += 'about ' + units + ' second';
                            if (units > 1) { mytext += 's'; }
                            mytext += ' ago';
                            }
                        else if (datediff<3600000) { //up to 1 hour
                            var units = Math.round(datediff/60000);
                            mytext += 'about ' + units + ' minute';
                            if (units > 1) { mytext += 's'; }
                            mytext += ' ago';
                            }
                        else {
                            var clientdate = new Date(nutc - utc_offset); //convert twitter UTC time to syncronised client time zone
                            mytext += '<span class="tweet_date">' + clientdate.toLocaleDateString() + ' </span>' + clientdate.toLocaleTimeString();
                            }
                        mytext += '</span>';
                    }
                    mytext += '&nbsp;<span class="tweet_from">from ' + args[x].source;
                    if (args[x].in_reply_to_screen_name) {
                        mytext += reply_text + '<a href="/Main.aspx?screen_name=' + args[x].in_reply_to_screen_name + '">@' + args[x].in_reply_to_screen_name + '</a>';
                    }
                    mytext += '</span>';
                    mytext += '</div>';//header
                    
                    //make any http references hyperlinked: cf: http://www.w3schools.com/jsref/jsref_obj_regexp.asp
                    tweet = tweet.replace(/(http|https):\/\/\S*(?=\s?)/gi,'<a href="$&" target="_blank">$&</a>');

                    //make any twitter @ mentions highlighted and linked to the users twitter page
                    tweet = tweet.replace(/@(\w+)/g,'<span class="tweet_mention"><a href="/Main.aspx?screen_name=$1">$&</a></span>');

                    //make any twitter $ Tickers highlighted and linked to the ticker profile page using regex search and replace
                    //var pattn = /[$]((?:[a-zA-Z]+:)?((?=[a-zA-Z]\b)|(?=\w*[a-zA-Z]\w))\w+(?:\.[a-zA-Z]+)*)/g;
                    var pattn = /[$]((?:[a-zA-Z]+:)?[a-zA-Z]\w*(?:\.[a-zA-Z]+)*)/g; // find $ followed by (optionally just chars a-z and : ), then (alphanumeric) sequence that contains at least one letter before the last character, or just one letter, (followed optionally by a sequence of chars a-z (but no numbers) preceded by .) that can repeat   
                                                                                            // $1 will reference the only captured back ref... which is everything less the $; $& will return the entire match incl the $
                    if (loLoggedIn) {
                        tweet = tweet.replace(pattn,'<span class="tweet_ticker" onmouseover="symbol_mouseover(event,\'$1\')" onmouseout="symbol_mouseout()" onmousemove="symbol_mousemove(event)"><a href="/Main.aspx?ticker=$1">$&</a></span>');
                    }
                    else {
                        tweet = tweet.replace(pattn,'<span class="tweet_ticker"><a href="/Main.aspx?ticker=$1">$&</a></span>');
                    }
                    mytext += '<div class="tweet_body' + class_ext + '">' + tweet + '</div>';
                    mytext += '</div>';//content
                    mytext += '<div class="tweet_commands' + class_ext + '">';
                    if (loLoggedIn) {
                        mytext += '<div><img src="images/reply.png" title="Reply" alt="Reply" style="cursor:pointer;" onclick="javascript:return OpenReplyPanel(this,\'' + args[x].user.id + '\', \'' + tweet_id + '\', \'REPLY\', \'' + escape(args[x].text) + '\', 0);"></div>';
                        mytext += '<div><img src="images/retweet.png" title="Retweet" alt="Retweet" style="cursor:pointer;" onclick="javascript:return OpenReplyPanel(this,\'' + args[x].user.id + '\', \'' + tweet_id + '\', \'RETWEET\', \'' + escape(args[x].text) + '\', 0);"></div>';
                    }
                    mytext += '</div>';//commands
                    mytext += '</td></tr>';
                    
                    //now calculate necessary shift upwards for new tweets to be scrolled back down
                    if (newFirstTweetID == 0) { newFirstTweetID = args[x].id; }
                    if (args[x].id > FirstTweetID_Friends){ newTweetCount++; }
    
                    mytext += '<tr id="' + tweet_id + '3"><td colspan="2" class="tweet_footer">&nbsp;</td></tr>';
                }
            }

            mytext += '</table>';

            var panelTopShift = 0;
            if (FirstTweetID_Friends != 0) { panelTopShift = newTweetCount*TWEET_HEIGHT_FRIENDS;}

            //alert(panelTopShift);
            if (panelTopShift != 0) { scroll_panel.style.top = (scroll_panel_style_top - panelTopShift) + 'px'; } //Firefox requires the value to be a string with units
            
            scroll_panel.innerHTML = mytext;
            FirstTweetID_Friends = newFirstTweetID;
            if (Tweeting_Friends) { GetTweetsTimer_Friends(); }
            
            //alert(result);
            WaitForTweets_Friends=false;
        }
        function OnFailed_url_Friends(error) {
            if (Unloading) { WaitForTweets_Friends=false; return; }
            // Alert user to the error. NB the timer will stop here...
            if (DEBUG_ALERTS) {alert('Error:' + error.get_message());}
            if (Tweeting_Friends) { GetTweetsTimer_Friends(); }
            WaitForTweets_Friends=false;
        }

        function ScrollTweets_StartStop_Friends(scrollbtn) {
            btnScroll_Items_Friends = scrollbtn;
            Scrolling_Friends = !Scrolling_Friends;
            if (Scrolling) {
                ScrollTweetsTimer_Friends();
            } else {
                clearTimeout(Timer_Scroll_Friends);
            }
            return false;
        }
        
        function ScrollTweetsTimer_Friends() {
            Timer_Scroll_Friends = setTimeout("ScrollTweets_Friends()",SCROLL_RATE);
            WaitForScroll_Friends = false;
        }
        
        function ScrollTweets_Friends() {
            if (Scrolling_Friends) {
                if (parseInt($get("tomove_Friends").style.top)<0) { //if there are atill new tweets to scroll, process with animation
                    WaitForScroll_Friends = true; //used for locking updates to the panel while it is actually scrolling; this gets released in ScrollTweetsTimer / at the end of the animation extender
                    $get(btnScroll_Items_Friends).click();
                } else { //no current new tweets, process without animation
                     ScrollTweetsTimer_Friends();
                }
            }
        }

        function ScrollTweets_GetTweets_StartStop_Friends(btn, scrollbtn, hfMyFriendsTweetingID) { //single function to pause and start scrolling and tweeting simultaneously
            ScrollTweets_StartStop_Friends(scrollbtn);
            GetTweets_StartStop_Friends(btn);
            $get(hfMyFriendsTweetingID).value=Tweeting_Friends; //pass a copy of current tweeting state to server so can store it etc
            return true;
        }
        
        function myFriends_PageSelect(page){
            if (Tweets_Page_Friends!=page){
                Refreshing_Friends=true;
                Tweets_Page_Friends = page; //Set the page
                for (i=1; i<=Tweets_Pages_Friends; i++){ //Update styling to show selected page
                    if (i==page){
                        $get('myFriends_paging' + i).className="selected";
                    } else {
                        $get('myFriends_paging' + i).className="";
                    }
                }
                if (Timer_Tweets_Friends) {clearTimeout(Timer_Tweets_Friends);}
                GetTweetsWithWait_Friends(); //refresh the stream
            }
        }

//Cookie handling - cookies are used to cache the user's friends stream for JSONP mode
//  nb two cookies are set: the stream, and the userid for the stream so that if a client logs out and logs in as another user, the old stream can be discarded

        function setCookie(c_name,value,expirehours){
            var exdate=new Date();
            exdate.setHours(exdate.getHours()+expirehours);
            document.cookie=c_name+ "=" + escape(value) + ((expirehours==null) ? "" : ";expires="+exdate.toGMTString());
        }

        function getCookie(c_name){
        if (document.cookie.length>0)
            {
                c_start=document.cookie.indexOf(c_name + "=");
                if (c_start!=-1)
                {
                    c_start=c_start + c_name.length+1;
                    c_end=document.cookie.indexOf(";",c_start);
                    if (c_end==-1) c_end=document.cookie.length;
                    return unescape(document.cookie.substring(c_start,c_end));
                }
            }
            return "";
        }        
        
//Client Side notes:
//  Page Methods CAN be called off a specific page - very useful to user web controls and master pages.
//  Toolkit components provide the ServicePath attribute that handles this... just put the name of the aspx page in there (with a / if its in root for best practice, so virtual paths do not break it)
//  Can use the JScript command: PageMethods.set_path('pagename.aspx') with possible leading / again just before calls to a pagemethod client side to specify a path to a generic page method
//  This is great in script libraries that might be used on multiple pages, where the page method is only on one of them - saves duplicating the page method to all pages!