Updates to Facebook client:
authorMichael J. Rubinsky <mrubinsk@horde.org>
Mon, 5 Jul 2010 21:45:19 +0000 (17:45 -0400)
committerMichael J. Rubinsky <mrubinsk@horde.org>
Mon, 5 Jul 2010 21:46:48 +0000 (17:46 -0400)
    be less obtrusive where we can, fix Like action.
    Use Horde_View for the Facebook stream.
    add Get More Entries action
    use the start and end parameters
    seperate out facebook js, retrieve info via ajax

horde/js/facebookclient.js [new file with mode: 0644]
horde/lib/Block/fb_stream.php
horde/services/facebook.php
horde/templates/block/facebook_story.html.php [new file with mode: 0644]

diff --git a/horde/js/facebookclient.js b/horde/js/facebookclient.js
new file mode 100644 (file)
index 0000000..5193869
--- /dev/null
@@ -0,0 +1,145 @@
+/**
+ * Facebook client javascript.
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde
+ */
+
+var Horde_Facebook = Class.create({
+
+    oldest: '',
+    newest: '',
+    opts: {},
+
+    /**
+     * opts.spinner
+     * opts.input
+     * opts.refreshrate
+     * opts.content
+     * opts.endpoint,
+     * opts.notifications
+     * opts.getmore
+     * opts.button
+     * opts.instance
+     *
+     * 
+     */
+    initialize: function(opts)
+    {
+        this.opts = Object.extend({
+            refreshrate: 300
+        }, opts);
+
+        this.getNewEntries();
+        $(this.opts.getmore).observe('click', function() { this.getOlderEntries(); return false; }.bind(this));
+        $(this.opts.button).observe('click', function() { this.updateStatus(); return false; }.bind(this));
+    },
+    
+    /**
+     * Update FB status.
+     *
+     * @param string statusText  The new status text.
+     * @param string inputNode   The DOM Element for the input box.
+     *
+     * @return void
+     */
+    updateStatus: function()
+    {
+        $(this.opts.spinner).toggle();
+        params = new Object();
+        params.actionID = 'updateStatus';
+        params.statusText = $F(this.opts.input);
+        new Ajax.Updater({success:'currentStatus'},
+             this.opts.endpoint,
+             {
+                 method: 'post',
+                 parameters: params,
+                 onComplete: function() {
+                     $(this.opts.input).value = '';
+                     $(this.opts.spinner).toggle()
+                 },
+                 onFailure: function() {$(this.opts.spinner).toggle()}
+             }
+       );
+    },
+
+    addLike: function(post_id)
+    {
+        $(this.opts.spinner).toggle();
+        var params = {
+          actionID: 'addLike',
+          post_id: post_id
+        };
+        new Ajax.Updater(
+             {success:'fb' + post_id},
+             this.opts.endpoint,
+             {
+                 method: 'post',
+                 parameters: params,
+                 onComplete: function() {$(this.opts.spinner).toggle()}.bind(this),
+                 onFailure: function() {$(this.opts.spinner).toggle()}.bind(this)
+             }
+       );
+
+       return false;
+    },
+
+    getOlderEntries: function() {
+        var params = {
+            'actionID': 'getStream',
+            'newest': this.oldest,
+            'instance': this.opts.instance
+        };
+        new Ajax.Request(this.opts.endpoint, {
+            method: 'post',
+            parameters: params,
+            onSuccess: this._getOlderEntriesCallback.bind(this),
+            onFailure: function() {
+                $(this.opts.spinner).toggle();
+            }
+        });
+    },
+
+    _getOlderEntriesCallback: function(response)
+    {
+        var content = response.responseJSON.c;
+        this.oldest = response.responseJSON.o;
+        var h = $(this.opts.content).scrollHeight
+        $(this.opts.content).insert(content);
+        $(this.opts.content).scrollTop = h;
+    },
+
+    getNewEntries: function()
+    {
+        var params = { 
+            'actionID': 'getStream',
+            'notifications': this.opts.notifications,
+            'oldest': this.oldest,
+            'newest': this.newest,
+            'instance': this.opts.instance
+         };
+        new Ajax.Request(this.opts.endpoint, {
+            method: 'post',
+            parameters: params,
+            onSuccess: this._getNewEntriesCallback.bind(this),
+            onFailure: function() {
+                $(this.opts.spinner).toggle();
+            }
+        });
+    },
+
+    _getNewEntriesCallback: function(response)
+    {
+        $(this.opts.content).insert({ 'top': response.responseJSON.c });
+        $(this.opts.notifications).update(response.responseJSON.nt);
+
+        this.newest = response.responseJSON.n;
+        if (!this.oldest) {
+            this.oldest = response.responseJSON.o;
+        }
+    }
+    
+});
index 365454c..b13dd93 100644 (file)
@@ -17,8 +17,10 @@ class Horde_Block_Horde_fb_stream extends Horde_Block {
 
     /**
      * Whether this block has changing content.
+     *
+     * Set this to false, since we handle the updates via AJAX on our own.
      */
-    var $updateable = true;
+    var $updateable = false;
 
     /**
      * @var string
@@ -42,30 +44,31 @@ class Horde_Block_Horde_fb_stream extends Horde_Block {
      */
     public function __construct($params = array(), $row = null, $col = null)
     {
-        $GLOBALS['injector']->addBinder('Facebook', new Horde_Core_Binder_Facebook());
         try {
-            $this->_facebook = $GLOBALS['injector']->getInstance('Facebook');
+            $this->_facebook = $GLOBALS['injector']->getInstance('Horde_Service_Facebook');
         } catch (Horde_Exception $e) {
             return $e->getMessage();
         }
-        $this->_fbp = unserialize($GLOBALS['prefs']->getValue('facebook'));
 
+        /* Authenticate the client */
+        $this->_fbp = unserialize($GLOBALS['prefs']->getValue('facebook'));
         if (!empty($this->_fbp['sid'])) {
             $this->_facebook->auth->setUser($this->_fbp['uid'], $this->_fbp['sid'], 0);
         }
         parent::__construct($params, $row, $col);
     }
 
+    /**
+     *
+     * @return array
+     */
     function _params()
     {
         $filters = array();
 
         if (!empty($this->_fbp['sid'])) {
-            // Use a raw fql query to reduce the amount of data that is
-            // returned.
             $fql = 'SELECT filter_key, name FROM stream_filter WHERE uid="'
                 . $this->_fbp['uid'] . '"';
-
             try {
                 $stream_filters = $this->_facebook->fql->run($fql);
                 foreach ($stream_filters as $filter) {
@@ -109,95 +112,53 @@ class Horde_Block_Horde_fb_stream extends Horde_Block {
      */
     function _content()
     {
+        $instance = md5(mt_rand());
         $csslink = $GLOBALS['registry']->get('themesuri', 'horde') . '/facebook.css';
         $endpoint = Horde::url('services/facebook.php', true);
-        $spinner = '$(\'loading\')';
-        $html = <<<EOF
-        <script type="text/javascript">
-        function updateStatus(statusText, inputNode)
-        {
-            {$spinner}.toggle();
-            params = new Object();
-            params.actionID = 'updateStatus';
-            params.statusText = statusText;
-            new Ajax.Updater({success:'currentStatus'},
-                 '$endpoint',
-                 {
-                     method: 'post',
-                     parameters: params,
-                     onComplete: function() {inputNode.value = '';{$spinner}.toggle()},
-                     onFailure: function() {{$spinner}.toggle()}
-                 }
-           );
-        }
-        function addLike(post_id)
-        {
-            {$spinner}.toggle();
-            params = new Object();
-            params.actionID = 'addLike';
-            params.post_id = post_id;
-            new Ajax.Updater({success:'fb' + post_id},
-                 '$endpoint',
-                 {
-                     method: 'post',
-                     parameters: params,
-                     onComplete: function() {{$spinner}.toggle()},
-                     onFailure: function() {{$spinner}.toggle()}
-                 }
-           );
-
-           return false;
-        }
-
-        </script>
-EOF;
-
+        $html = '';
+
+        /* Add the client javascript / initialize it */
+        Horde::addScriptFile('facebookclient.js');
+        $script = <<<EOT
+            var Horde = window.Horde || {};
+            Horde['{$instance}_facebook'] = new Horde_Facebook({
+               spinner: '{$instance}_loading',
+               endpoint: '{$endpoint}',
+               content: '{$instance}_fbcontent',
+               status: '{$instance}_currentStatus',
+               notifications: '{$instance}_fbnotifications',
+               getmore: '{$instance}_getmore',
+               'input': '{$instance}_newStatus',
+               'button': '{$instance}_button',
+               instance: '{$instance}'
+            });
+EOT;
+        Horde::addInlineScript($script, 'dom');
+
+        /* Init facebook driver, exit early if no prefs exist */
         $facebook = $this->_facebook;
         $fbp = $this->_fbp;
-
-        // If no prefs exist -------
         if (empty($fbp['sid'])) {
             return sprintf(_("You have not properly connected your Facebook account with Horde. You should check your Facebook settings in your %s."), Horde::getServiceLink('options', 'horde')->add('group', 'facebook')->link() . _("preferences") . '</a>');
         }
 
-        // Get stream
-        try {
-            $stream = $facebook->streams->get('', array(), '', '', $this->_params['count'], $this->_params['filter']);
-        } catch (Horde_Service_Facebook_Exception $e) {
-            $html .= sprintf(_("There was an error making the request: %s"), $e->getMessage());
-            $html .= sprintf(_("You can also check your Facebook settings in your %s."), Horde::getServiceLink('options', 'horde')->add('group', 'facebook')->link() . _("preferences") . '</a>');
-            return $html;
-        }
+        /* Build the UI */
+        $html .= '<link href="' . $csslink . '" rel="stylesheet" type="text/css" />';
+        $html .= '<div style="padding-left: 8px;padding-right:8px;">';
 
-        //Do we want notifications too?
+        /* Build the Notification Section */
         if (!empty($this->_params['notifications'])) {
-            try {
-                $notifications = $facebook->notifications->get();
-            } catch (Horde_Service_Facebook_Exception $e) {
-                $html .= sprintf(_("There was an error making the request: %s"), $e->getMessage());
-                $html .= sprintf(_("You can also check your Facebook settings in your %s."), Horde::getServiceLink('options', 'horde')->add('group', 'facebook')->link() . _("preferences") . '</a>');
-                return $html;
-            }
-        }
-
-        $posts = $stream['posts'];
-        $profiles = array();
-        // Sure would be nice if fb returned these keyed properly...
-        foreach ($stream['profiles'] as $profile) {
-            $profiles[(string)$profile['id']] = $profile;
+            $html .= '<div class="fbinfobox" id="' . $instance . '_fbnotifications"></div>';
         }
 
-        // Bring in the Facebook CSS
-        $html .= '<link href="' . $csslink . '" rel="stylesheet" type="text/css" />';
-        $html .= '<div style="float:left;padding-left: 8px;padding-right:8px;">';
-
-        // User's current status and input box to change it.
+        /* User's current status and input box to change it. */
         $fql = 'SELECT first_name, last_name, status, pic_square_with_logo from user where uid=' . $fbp['uid'] . ' LIMIT 1';
         try {
             $status = $facebook->fql->run($fql);
         } catch (Horde_Service_Facebook_Exception $e) {
-            $html .= sprintf(_("There was an error making the request: %s"), $e->getMessage());
+            $html = sprintf(_("There was an error making the request: %s"), $e->getMessage());
             $html .= sprintf(_("You can also check your Facebook settings in your %s."), Horde::getServiceLink('options', 'horde')->add('group', 'facebook')->link() . _("preferences") . '</a>');
+
             return $html;
         }
         $status = array_pop($status);
@@ -207,22 +168,17 @@ EOF;
         } else {
             $class = '';
         }
-
-        if (!empty($notifications)) {
-            $html .= '<div class="fbinfobox">' . _("New Messages:")
-                . ' ' . $notifications['messages']['unread']
-                . ' ' . _("Pokes:") . ' ' . $notifications['pokes']['unread']
-                . ' ' . _("Friend Requests:") . ' ' . count($notifications['friend_requests'])
-                . ' ' . _("Event Invites:") . ' ' . count($notifications['event_invites']) . '</div>';
-        }
-
-        $html .= '<div class="fbgreybox fbboxfont"><img style="float:left;" src="' . $status['pic_square_with_logo'] . '" /><div id="currentStatus" class="' . $class . '" style="margin-left:55px;">' . $status['status']['message'] . '</div>';
+        $html .= '<div class="fbgreybox fbboxfont">'
+            . '<img style="float:left;" src="' . $status['pic_square_with_logo'] . '" />'
+            . '<div id="' . $instance . '_currentStatus" class="' . $class . '" style="margin-left:55px;">'
+            . $status['status']['message']
+            . '</div>';
+        
         try {
-            //TODO: We could probably cache this perm somehow - maybe in the session?
             if ($facebook->users->hasAppPermission(Horde_Service_Facebook_Auth::EXTEND_PERMS_PUBLISHSTREAM)) {
-                $html .= '<input style="width:100%;margin-top:4px;margin-bottom:4px;" type="text" class="fbinput" id="newStatus" name="newStatus" />'
-                . '<div><a class="button" onclick="updateStatus($F(\'newStatus\'), $(\'newStatus\'));" href="#">' . _("Update") . '</a></div>'
-                . Horde::img('loading.gif', '', array('id' => 'loading', 'style' => 'display:none;'));
+                $html .= '<input style="width:100%;margin-top:4px;margin-bottom:4px;" type="text" class="fbinput" id="' . $instance . '_newStatus" name="newStatus" />'
+                    . '<div><a class="button" href="#" id="' . $instance . '_button">' . _("Update") . '</a></div>'
+                    . Horde::img('loading.gif', '', array('id' => $instance. '_loading', 'style' => 'display:none;'));
             }
         } catch (Horde_Service_Facebook_Exception $e) {
             $html .= sprintf(_("There was an error making the request: %s"), $e->getMessage());
@@ -230,78 +186,12 @@ EOF;
             return $html;
         }
         $html .= '</div>'; // Close the fbgreybox node that wraps the status
-        // Build the stream feed.
-        $html .= '<div style="height:' . (empty($this->_params['height']) ? 300 : $this->_params['height']) . 'px;overflow-y:auto;">';
-        foreach ($posts as $post) {
-            $html .= '<div class="fbstreamstory">';
-            $html .= '<div class="fbstreampic"><img style="float:left;" src="' . $profiles[(string)$post['actor_id']]['pic_square'] . '" /></div>';
 
-            // fbstreambody wraps all content except the actor's image. This
-            // displays the actor's name and any message he/she added.
-            $html .= ' <div class="fbstreambody">'
-                . Horde::externalUrl($profiles[(string)$post['actor_id']]['url'], true)
-                . $profiles[(string)$post['actor_id']]['name'] . '</a> '
-                . (empty($post['message']) ? '' : $post['message']);
 
-            // Parse any attachments
-            if (!empty($post['attachment'])) {
-                $html .= '<div class="fbattachment">';
-                if (!empty($post['attachment']['media']) && count($post['attachment']['media'])) {
-                    $html .= '<div class="fbmedia' . (count($post['attachment']['media']) > 1 ? ' fbmediawide' : '') . '">';
-                    // Decide what mediaitem css class to use for padding and
-                    // display the media items.
-                    $multiple = false;
-                    $single = count($post['attachment']['media']) == 1;
-                    foreach ($post['attachment']['media'] as $item) {
-                        $link = Horde::externalUrl($item['href'], true);
-                        $img = '<img src="' . htmlspecialchars($item['src']) . '" />';
-                        if ($single) {
-                            $html .= '<div class="fbmediaitem fbmediaitemsingle">' . $link . $img . '</a></div>';
-                        } else {
-                            $html .= '<div class="fbmediaitem' . ($multiple ? ' fbmediaitemmultiple' : '') . '">' . $link . $img . '</a></div>';
-                            $multiple = true;
-                        }
-                    }
-                    $html .= '</div>';  // Close the fbmedia node
-                }
-
-                // Attachment properties.
-                if (!empty($post['attachment']['name'])) {
-                    $link = Horde::externalUrl($post['attachment']['href'], true);
-                    $html .= '<div class="fbattachmenttitle">' . $link . $post['attachment']['name'] . '</a></div>';
-                }
-                if (!empty($post['attachment']['caption'])) {
-                    $html .= '<div class="fbattachmentcaption">' . $post['attachment']['caption'] . '</div>';
-                }
-                if (!empty($post['attachment']['description'])) {
-                    $html .= '<div class="fbattachmentcopy">' . $post['attachment']['description'] . '</div>';
-                }
-
-                $html .= '</div>'; // Close the fbattachemnt node.
-            }
-
-            // Build the likes string to display.
-            if (empty($post['likes']['user_likes']) && !empty($post['likes']['can_like'])) {
-                $like = '<a href="#" onclick="return addLike(\'' . $post['post_id'] . '\');">' . _("Like") . '</a>';
-            } else {
-                $like = '';
-            }
-            $html .= '<div class="fbstreaminfo">' . sprintf(_("Posted %s"), Horde_Date_Utils::relativeDateTime($post['created_time'], $GLOBALS['prefs']->getValue('date_format'), $GLOBALS['prefs']->getValue('twentyFour') ? "%H:%M %P" : "%I %M %P")) . ' ' . sprintf(_("Comments: %d"), $post['comments']['count']) . '</div>';
-            $html .= '<div class="fbstreaminfo" id="fb' . $post['post_id'] . '">';
-            if (!empty($post['likes']['user_likes']) && !empty($post['likes']['count'])) {
-                $html .= sprintf(ngettext("You and %d other person likes this", "You and %d other people like this", $post['likes']['count'] - 1), $post['likes']['count'] - 1);
-            } elseif (!empty($post['likes']['user_likes'])) {
-                $html .= _("You like this");
-            } elseif (!empty($post['likes']['count'])) {
-                $html .= sprintf(ngettext("%d person likes this", "%d persons like this", $post['likes']['count']), $post['likes']['count']) . (!empty($like) ? ' ' . $like : '');
-            } elseif (!empty($like)) {
-                $html .= $like;
-            }
-            $html .= '</div>'; // Close the fbstreaminfo node that wraps the like
-            $html .= '</div></div>'; // Close the fbstreambody, fbstreamstory nodes
-            $html .= '<div class="fbcontentdivider">&nbsp;</div>';
-        }
-        $html .= '</div></div>'; // fbbody end
+       // Build the stream feed.
+        $html .= '<div id="' . $instance . '_fbcontent" style="height:' . (empty($this->_params['height']) ? 300 : $this->_params['height']) . 'px;overflow-y:auto;"></div>';
+ $html .= '<div class="control fbgetmore"><a href="#" id="' . $instance . '_getmore">' . _("Get More") . '</a></div>';
+        $html .= '</div>'; // fbbody end
 
         return $html;
     }
index 87d73bf..c3cb59e 100644 (file)
@@ -20,8 +20,9 @@ try {
     header('Location: ' . $horde_url);
 }
 
-// See why we are here.
+/* See why we are here. */
 if ($token = Horde_Util::getFormData('auth_token')) {
+    /* Is this an authentication sequence? */
     // Assume we are here for a successful authentication if we have a
     // auth_token. It *must* be allowed to be in GET since that's how FB
     // sends it. This is the *only* time we will be able to capture these values.
@@ -39,11 +40,97 @@ if ($token = Horde_Util::getFormData('auth_token')) {
         $url = Horde::url('services/prefs.php', true)->add(array('group' => 'facebook', 'app'  => 'horde'));
         header('Location: ' . $url);
     }
+
 } else {
-    // Require the rest of the actions to be POST only since following them
-    // could change the user's state.
+
+    /* We are here for an Action request */
     $action = Horde_Util::getPost('actionID');
     switch ($action) {
+    case 'getStream':
+        $fbp = unserialize($prefs->getValue('facebook'));
+        $facebook->auth->setUser($fbp['uid'], $fbp['sid']);
+        try {
+            $count = Horde_Util::getPost('count');
+            $filter = Horde_Util::getPost('filter');
+            $stream = $facebook->streams->get('', array(), Horde_Util::getPost('oldest'), Horde_Util::getPost('newest'), $count, $filter);
+        } catch (Horde_Service_Facebook_Exception $e) {
+            $html .= sprintf(_("There was an error making the request: %s"), $e->getMessage());
+            $html .= sprintf(_("You can also check your Facebook settings in your %s."), Horde::getServiceLink('options', 'horde')->add('group', 'facebook')->link() . _("preferences") . '</a>');
+
+            return $html;
+        }
+
+        /* Do we want notifications too? */
+        $n_html = '';
+        if (Horde_Util::getPost('notifications')) {
+            try {
+                $notifications = $facebook->notifications->get();
+                $n_html =  _("New Messages:")
+                . ' ' . $notifications['messages']['unread']
+                . ' ' . _("Pokes:") . ' ' . $notifications['pokes']['unread']
+                . ' ' . _("Friend Requests:") . ' ' . count($notifications['friend_requests'])
+                . ' ' . _("Event Invites:") . ' ' . count($notifications['event_invites']);
+            } catch (Horde_Service_Facebook_Exception $e) {
+                $html .= sprintf(_("There was an error making the request: %s"), $e->getMessage());
+                $html .= sprintf(_("You can also check your Facebook settings in your %s."), Horde::getServiceLink('options', 'horde')->add('group', 'facebook')->link() . _("preferences") . '</a>');
+
+                return $html;
+            }
+        }
+
+        /* Start parsing the posts */
+        $posts = $stream['posts'];
+        $profiles = array();
+        $newest = $posts[0]['created_time'];
+        $oldest = $posts[count($posts) -1]['created_time'];
+        $instance = Horde_Util::getPost('instance');
+
+        /* Sure would be nice if fb returned these keyed properly... */
+        foreach ($stream['profiles'] as $profile) {
+            $profiles[(string)$profile['id']] = $profile;
+        }
+
+        /* Build Horde_View for each story */
+        $html = '';
+        foreach ($posts as $post) {
+                $postView = new Horde_View(array('templatePath' => HORDE_TEMPLATES . '/block'));
+                $postView->actorImgUrl = $profiles[(string)$post['actor_id']]['pic_square'];
+                $postView->actorProfileLink =  Horde::externalUrl($profiles[(string)$post['actor_id']]['url'], true) . $profiles[(string)$post['actor_id']]['name'] . '</a>';
+                $postView->message = empty($post['message']) ? '' : $post['message'];
+                $postView->attachment = empty($post['attachment']) ? null : $post['attachment'];
+                $postView->likes = $post['likes'];
+                $postView->postId = $post['post_id'];
+                $postView->postInfo = sprintf(_("Posted %s"), Horde_Date_Utils::relativeDateTime($post['created_time'], $GLOBALS['prefs']->getValue('date_format'), $GLOBALS['prefs']->getValue('twentyFour') ? "%H:%M %P" : "%I %M %P")) . ' ' . sprintf(_("Comments: %d"), $post['comments']['count']);
+
+                /* Build the 'Likes' string. */
+                if (empty($post['likes']['user_likes']) && !empty($post['likes']['can_like'])) {
+                    $like = '<a href="#" onclick="Horde[\'' . $instance . '_facebook\'].addLike(\'' . $post['post_id'] . '\');return false;">' . _("Like") . '</a>';
+                } else {
+                    $like = '';
+                }
+                if (!empty($post['likes']['user_likes']) && !empty($post['likes']['count'])) {
+                    $likes = sprintf(ngettext("You and %d other person likes this", "You and %d other people like this", $post['likes']['count'] - 1), $post['likes']['count'] - 1);
+                } elseif (!empty($post['likes']['user_likes'])) {
+                    $likes = _("You like this");
+                } elseif (!empty($post['likes']['count'])) {
+                    $likes = sprintf(ngettext("%d person likes this", "%d persons like this", $post['likes']['count']), $post['likes']['count']) . (!empty($like) ? ' ' . $like : '');
+                } else {
+                    $likes = $like;
+                }
+                $postView->likesInfo = $likes;
+                $html .= $postView->render('facebook_story');
+        }
+
+        /* Build response structure */
+        $result = array(
+            'o' => $oldest,
+            'n' => $newest,
+            'c' => $html,
+            'nt' => $n_html
+        );
+        header('Content-Type: application/json');
+        echo Horde_Serialize::serialize($result, Horde_Serialize::JSON);
+        exit;
     case 'updateStatus':
         // Set the user's status
         $fbp = unserialize($prefs->getValue('facebook'));
diff --git a/horde/templates/block/facebook_story.html.php b/horde/templates/block/facebook_story.html.php
new file mode 100644 (file)
index 0000000..72ce3ec
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+/**
+ * Template for a Facebook story entry. Expects the following to be set:
+ *
+ * $this->actorImgUrl        URI to the actor's pic.
+ * $this->actorProfileLink   Full link <a ...>Text</a> to FB Profile
+ * $this->message            The body of the post
+ * $this->attachement        Attachement array
+ * $this->postId             The postId
+ * $this->postInfo           Text to display for post info (post time etc...)
+ * $this->likesInfo          Text to display for the Like info (You and one other person etc...)
+ */
+?>
+<div class="fbstreamstory">
+ <div class="fbstreampic"><img style="float:left;" src="<?php echo $this->actorImgUrl ?>" /></div>
+ <div class="fbstreambody">
+  <?php echo $this->actorProfileLink ?><br />
+  <?php echo empty($this->message) ? '' : $this->message;?>
+  <?php if(!empty($this->attachment)):?>
+    <div class="fbattachment">
+      <?php if (!empty($this->attachment['media']) && count($this->attachment['media'])):?>
+        <div class="fbmedia<?php echo count($this->attachment['media']) > 1 ? ' fbmediawide' : ''?>">
+          <?php foreach($this->attachment['media'] as $item): ?>
+            <div class="fbmediaitem<?php echo (count($this->attachement['media']) > 1) ? ' fbmediaitemmultiple' : ' fbmediaitemsingle'?>">
+              <?php echo Horde::externalUrl($item['href'], true) ?><img alt="[image]"src="<?php echo htmlspecialchars($item['src'])?>" /></a>
+            </div>
+          <?php endforeach;?>
+        </div>
+      <?php endif;?>
+      <?php if (!empty($this->attachment['name'])):?>
+        <div class="fbattachmenttitle">
+          <?php echo Horde::externalUrl($this->attachment['href'], true) . $this->attachment['name']?></a>
+        </div>
+      <?php endif;?>
+      <?php if (!empty($this->attachment['caption'])):?>
+        <div class="fbattachmentcaption"><?php echo $this->attachment['caption']?></div>
+      <?php endif;?>
+      <?php if (!empty($this->attachment['description'])):?>
+        <div class="fbattachmentcopy"><?php echo $this->attachment['description']?></div>
+      <?php endif;?>
+    </div>
+  <?php endif;?>
+  <div class="fbstreaminfo"><?php echo $this->postInfo?></div>
+  <div class="fbstreaminfo" id="fb<?php echo $this->postId?>"><?php echo $this->likesInfo?></div>
+ </div>
+</div>
+<div class="fbcontentdivider">&nbsp;</div>
\ No newline at end of file