// BEGIN FLOCK GPL
//
// Copyright Flock Inc. 2005-2009
// http://flock.com
//
// This file may be used under the terms of the
// GNU General Public License Version 2 or later (the "GPL"),
// http://www.gnu.org/licenses/gpl.html
//
// Software distributed under the License is distributed on an "AS IS" basis,
// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
// for the specific language governing rights and limitations under the
// License.
//
// END FLOCK GPL

Components.utils.import("resource:///modules/FlockMediaUtils.jsm");
Components.utils.import("resource:///modules/FlockPrefsUtils.jsm");
Components.utils.import("resource:///modules/FlockURLUtils.jsm");
Components.utils.import("resource:///modules/FlockXMLUtils.jsm");

const FLOCK_SNOWMAN_URL = "chrome://flock/skin/people/no_avatar.png";
const PEOPLE_PREFERRED_SERVICES_LIST_PREF = "flock.people.preferredServiceList";
const DISABLE_TWITTER_SIDEBAR_REPLY_PREF
  = "flock.myworld.twitter.sidebarReply.disabled";
const TWITTER_CONTRACT_ID = "@flock.com/people/twitter;1";

// These constants correspond to the index in the deck in topfriends.xul
NO_LOGGED_IN_ACCOUNTS = 0;
NO_FRIEND_ACTIVITIES_TO_DISPLAY = 1;
NO_FRIENDS = 2;
SHOWING_FRIEND_ACTIVITIES = 3;

var _logger = Cc["@flock.com/logger;1"].createInstance(Ci.flockILogger);
_logger.init("topfriends");

var _sb = Cc["@mozilla.org/intl/stringbundle;1"]
          .getService(Ci.nsIStringBundleService)
          .createBundle("chrome://flock/locale/start/myworld.properties");

var _coop = Cc["@flock.com/singleton;1"]
            .getService(Ci.flockISingleton)
            .getSingleton("chrome://flock/content/common/load-faves-coop.js")
            .wrappedJSObject;

var pplsvc = Cc["@flock.com/people-service;1"]
             .getService(Ci.flockIPeopleService);
var accsvc = Cc["@flock.com/account-service;1"]
             .getService(Ci.flockIAccountService);
var acUtils = Cc["@flock.com/account-utils;1"]
              .getService(Ci.flockIAccountUtils);

var gFriendsFeedController;

var draggingObserver = {
  onDragStart: function topfriends_onDragStart(aEvent,
                                               aTransferData,
                                               aAction)
  {
    var profileURL = aEvent.target.getAttribute("profileURL");
    var handle = aEvent.target.getAttribute("handle");

    if (!profileURL) {
      return;
    }

    aTransferData.data = new TransferData();
    aTransferData.data.addDataForFlavour("text/x-moz-url",
                                         profileURL + "\n" + handle);
    var link = "<a href='" + profileURL + "'>" + handle + "</a>";
    aTransferData.data.addDataForFlavour("text/html", link);
    aTransferData.data.addDataForFlavour("text/unicode", profileURL);
  },
  onDragOver: function topfriends_onDragOver(aEvent, aTransferData, aAction) {
  },
  onDrop: function topfriends_onDrop(aEvent, aTransferData, aAction) {
  },
  onDragExit: function topfriends_onDragExit(aEvent, aTransferData, aAction) {
  }
}

function FriendsFeedController(aContainerId, aDeckId) {
  this._friendFeedBox = document.getElementById(aContainerId);
  this._deck = document.getElementById(aDeckId);
  this._logger = Cc["@flock.com/logger;1"].createInstance(Ci.flockILogger);
  this._logger.init("topfriends");

  var prefs = Cc["@mozilla.org/preferences-service;1"]
              .getService(Ci.nsIPrefBranch);
  this.mMaxCount = prefs.getIntPref("flock.myworld.topfriends.count");
}

// Auto-remember account when we click a service for discovery
function rememberAccount(aSvcURN) {
  _logger.debug("rememberAccount: " + aSvcURN);
  var wsMgr = Cc["@flock.com/webservice-manager;1"]
              .getService(Ci.flockIWebServiceManager);
  wsMgr.autoKeepAccountForService(aSvcURN);
}

function activityClicked(aNode, aEvent) {
  // Suppress mouse right-click
  if (aEvent.button == 2) {
    return;
  }

  var activityId = aNode.id ? aNode.id
                            : aNode.parentNode.parentNode.parentNode.id;
  var friendActivity = _coop.get(activityId);
  var identity = friendActivity.identity;
  var service = Cc[identity.serviceId].getService(Ci.flockIWebService);

  var win = Cc["@mozilla.org/appshell/window-mediator;1"]
            .getService(Ci.nsIWindowMediator)
            .getMostRecentWindow("navigator:browser");

  switch (friendActivity.updateType) {
    // If the friendActivity item is for a profile or status update then open
    // the service specific profile page for the friend
    case "profile":
    case "status":
      var accountUrn = identity.getAccount().id();
      var account = service.getAccount(accountUrn)
                           .QueryInterface(Ci.flockISocialAccount);
      if (aNode.id) {
        var url = friendActivity.updateURL;
        if (!url) {
          url = account.getProfileURLForFriend(identity.id());
        }
        if (url && win) {
          win.openUILinkIn(url, "tab");
        }

        gMetrics.report("MyWorld-FriendActivityClick",
                        [{service: service.shortName,
                          type: friendActivity.updateType}]);
      } else {
        // Event comes from the Reply button
        if (identity.serviceId == TWITTER_CONTRACT_ID &&
            !FlockPrefsUtils.getBoolPref(DISABLE_TWITTER_SIDEBAR_REPLY_PREF))
        {
          // Do Twitter replies in the sidebar unless power-user has set a pref
          // to do otherwise.
          var tweetId = friendActivity.updateURL.split("/").pop();
          var statusText = "@" + identity.screenName + " ";
          fillTwitterStatusEntry(tweetId,
                                 statusText,
                                 "FriendActivityReplyClicked");
        } else {
          account.friendAction("statusComment", null, activityId);
        }

        gMetrics.report("MyWorld-FriendActivity-ReplyClick", null);
      }
      break;
    // If the friendActivity item is for a media update then display the media
    // bar with the friends media stream
    case "media":
      var query = new QueryHelper();
      query.user = identity.accountId;
      query.username = identity.name;
      if (win) {
        gMetrics.report("MediaBar-Open", "MyWorld-FriendActivity");
        win.FlockTopbar.selectById("flock_MediabarTopbarBroadcaster", true);
        win.mediabar.loadQuery(service.shortName,
                               query.stringVal,
                               query.username);
      }

      gMetrics.report("MyWorld-FriendActivityClick",
                      [{service: service.shortName,
                        type: friendActivity.updateType}]);
      break;
    // If all else fails, see if there's a statusMessageUrl and launch that.
    default:
      var url = friendActivity.updateURL;
      if (!url) {
        url = identity.statusMessageUrl;
      }
      if (url && win) {
        win.openUILinkIn(url, "tab");
      }

      gMetrics.report("MyWorld-FriendActivityClick",
                      [{service: service.shortName,
                        type: friendActivity.updateType}]);
      break;
  }
}

function retweetClicked(aNode, aEvent) {
  // Suppress mouse right-click
  if (aEvent.button == 2) {
    return;
  }
  var friendActivity = _coop.get(document.getBindingParent(aNode).id);
  var tweetId = friendActivity.updateURL.split("/").pop();
  var statusText = friendActivity.updateValue;
  // Check if this is already a re-tweet
  if (!statusText.match(/^rt\W*@/i)) {
    // If not, append re-tweet info
    statusText = "RT @" + friendActivity.identity.screenName + " " + statusText;
  }
  fillTwitterStatusEntry(tweetId, statusText, "FriendActivityRetweetClicked");
 
  gMetrics.report("MyWorld-FriendActivity-RetweetClick", null);
}

function faviconClicked(aNode, aEvent) {
  var friendActivity = _coop.get(document.getBindingParent(aNode).id);
  var identity = friendActivity.identity;
  var service = Cc[identity.serviceId].getService(Ci.flockIWebService);
  loadURI(service.url, aEvent);
}

function avatarClicked(aNode, aEvent) {
  var url = document.getBindingParent(aNode).getAttribute("profileURL");
  loadURI(url, aEvent);
}

// Delete elements from the bottom if it exceeds the MaxCount
FriendsFeedController.prototype.cutToMax =
function FriendsFeedController_cutToMax() {
  while (this._friendFeedBox.childNodes.length > this.mMaxCount) {
    this._friendFeedBox.removeChild(this._friendFeedBox.lastChild);
  }
}

FriendsFeedController.prototype.refreshDiscoveryPanel =
function FriendsFeedController_refreshDiscoveryPanel() {
  _logger.debug("refreshDiscoveryPanel");
  // refresh available services for discovery
  var catMgr = Cc["@mozilla.org/categorymanager;1"]
               .getService(Ci.nsICategoryManager);
  var e = catMgr.enumerateCategory("flockWebService");
  var serviceBox = document.getElementById("available-services-content");
  var isRemainingUnconfiguredServices = false;
  var svcChild = serviceBox.firstChild;
  while (svcChild) {
    serviceBox.removeChild(svcChild);
    svcChild = serviceBox.firstChild;
  }

  // discoveryLinkServiceList will contain the list of services for which we
  // will build a service discovery link in the unactivated Friend Activity
  // column in MyWorld
  var discoveryLinkServiceList = [];

  // Get the pref that governs the set of preferred services.
  var preferredServicesPref =
    FlockPrefsUtils.getCharPref(PEOPLE_PREFERRED_SERVICES_LIST_PREF);
  if (preferredServicesPref) {
    discoveryLinkServiceList = preferredServicesPref.split("|");
  }

  while (e.hasMoreElements()) {
    var entry = e.getNext().QueryInterface(Ci.nsISupportsCString);
    if (!entry) {
      continue;
    }

    var contractID = catMgr.getCategoryEntry("flockWebService", entry.data);
    var svc = Cc[contractID].getService(Ci.flockIWebService);

    if (svc instanceof Ci.flockISocialWebService) {
      _logger.debug("refreshDiscoveryPanel(): Found a service - "
                    + svc.shortName);

      // Check if the service is already in our list
      var idx = discoveryLinkServiceList.indexOf(svc.shortName);

      // Skip services which already have accounts configured.
      var accountUrns = svc.getAccountUrns();
      if (accountUrns.hasMore()) {
        if (idx > -1) {
          // Found a configured account so remove the service from the discovery
          // list
          discoveryLinkServiceList.splice(idx, 1);
        }
        continue;
      }

      if (idx < 0) {
        // Add the service to our discovery list if it's not already there
        discoveryLinkServiceList.push(svc.shortName);
      }
    }
  }

  if (discoveryLinkServiceList.length > 0) {
    isRemainingUnconfiguredServices = true;
  }

  // Create the discovery links
  for each (var s in discoveryLinkServiceList) {
    _logger.debug("svcShortName = " + s);
    var svc = acUtils.getServiceFromShortname(s);
    if (!(svc instanceof Ci.flockIWebService)) {
      continue;
    }
    var favicon = document.createElement("image");
    favicon.setAttribute("src", svc.icon);

    var label = document.createElement("label");
    label.setAttribute("id", "discovery-service-link");
    label.setAttribute("value", svc.title
                                + " - "
                                + _sb.GetStringFromName("flock.myworld.services.clickMessage"));
    label.setAttribute("onclick",
                       "Components.classes['@flock.com/metrics-service;1']"
                       + ".getService(Components.interfaces.flockIMetricsService)"
                       + ".report('Myworld-DiscoverService', '" + svc.shortName + "');"
                       + "rememberAccount('" + svc.urn + "');"
                       + "openURLMyWorld('" + svc.url + "', event)");
    label.setAttribute("crop", "end");

    var hbox = document.createElement("hbox");
    hbox.setAttribute("id", "discovery-service-entry");
    hbox.appendChild(favicon);
    hbox.appendChild(label);
    serviceBox.appendChild(hbox);
  }

  // Suppress the available services content box when the user has a configured
  // account for all services supported in the people sidebar
  // (i.e. flockISocialWebService)
  document.getElementById("content-box-middle")
          .setAttribute("hidden",
                        (isRemainingUnconfiguredServices ? "false" : "true"));

}

FriendsFeedController.prototype.doLayout =
function FriendsFeedController_doLayout() {
  // Used to test the all accounts logged out scenario.
  var allAccountsLoggedOut = true;

  // Used to test the no friends to display scenario.
  var accountsStillRefreshing = false;
  var friendsExist = false;

  // We only need to look at flockISocialAccount type acounts.
  var accounts = acUtils.getAccountsByInterface("flockISocialWebService");

  while (accounts.hasMoreElements()) {
    var account = accounts.getNext();
    account = account.QueryInterface(Ci.flockIWebServiceAccount);

    // We only care about authenticated accounts.
    if (!account.isAuthenticated()) {
      continue;
    }

    // We have at least one account that is logged in.
    allAccountsLoggedOut = false;

    _logger.debug("Checking account: " + account.urn);

    // Is this account refreshing? (important because if an account is
    // refreshing we still can't confirm whether it has friends or not
    // even if currently it has none)
    if (account.getParam("isRefreshing")) {
      _logger.debug("Account still refreshing");
      accountsStillRefreshing = true;
    }

    var friendsEnum = account.QueryInterface(Ci.flockISocialAccount)
                             .enumerateFriends();

    if (friendsEnum.hasMoreElements()) {
      // We know now that at least one account has friends, so we wont be
      // displaying the no friends panel.
      friendsExist = true;
    } else {
      _logger.debug("Found account with no friends: " + account.urn);
    }
  }

  if (allAccountsLoggedOut) {
    // All accounts are logged out so show the discovery panel.
    _logger.debug("Showing discovery panel");
    this._deck.selectedIndex = NO_LOGGED_IN_ACCOUNTS;

    // Show services not already configured.
    this.refreshDiscoveryPanel();

    return;
  }

  _logger.debug("accountsStillRefreshing = " + accountsStillRefreshing);
  _logger.debug("friendsExist = " + friendsExist);
  if (!accountsStillRefreshing && !friendsExist) {
    // All accounts have finished refreshing and confirmed to not have
    // friends so we will show the no friends message panel.
    _logger.debug("Showing no friends panel");
    this._deck.selectedIndex = NO_FRIENDS;
    return;
  }

  // Show the appropriate panel based on whether there are friend activities
  // being displayed in the feed box
  if (this._friendFeedBox.firstChild) {
    _logger.debug("Showing friend activities");
    this._deck.selectedIndex = SHOWING_FRIEND_ACTIVITIES;
  } else {
    _logger.debug("Showing no friend activities");
    this._deck.selectedIndex = NO_FRIEND_ACTIVITIES_TO_DISPLAY
  }
}

FriendsFeedController.prototype.insert =
function FriendsFeedController_insert(aFriendActivity, aAccount, aService) {

  // check to see whether already in the list
  var nodeList = this._friendFeedBox.childNodes;
  for (var c = 0; c < nodeList.length; c++) {
    var item = nodeList.item(c);
    if (item.getAttribute("id") == aFriendActivity.id()) {
      return;
    }
  }

  // Create the new DOM node
  var friend = aFriendActivity.identity;

  if (!friend) {
    return;
  }

  var feedItem = document.createElement("friendfeeditem");
  feedItem.setAttribute("id", aFriendActivity.id());
  feedItem.setAttribute("lastUpdateType", aFriendActivity.updateType);
  feedItem.setAttribute("lastupdate", Number(aFriendActivity.updateDate));

  var dt = aFriendActivity.updateDate;
  if (!dt) {
    dt = new Date();
  }
  feedItem.setAttribute("timeInfo", FlockDateFormatter.getFriendlyLastDate(dt));
  feedItem.handle = flockXMLDecode((friend.screenName) ? friend.screenName
                                                       : friend.name);
  var avatarUrl = (friend.avatar && friend.avatar != "") ? friend.avatar
                                                         : FLOCK_SNOWMAN_URL;
  feedItem.setAttribute("avatarUrl", avatarUrl);
  feedItem.setAttribute("serviceId", friend.serviceId);
  feedItem.setAttribute("onclick", "event.stopPropagation(); return false;");
  feedItem.setAttribute("onmouseup",
                        "activityClicked(this, event); return false;");
  feedItem.setAttribute("faviconOnClick", "faviconClicked(this, event);");
  feedItem.setAttribute("avatarOnClick", "avatarClicked(this, event);");

  var formatted = aAccount.formatFriendActivityForDisplay(aFriendActivity.id());
  var description;
  if (aFriendActivity.updateType == "status") {
    description = aService.parseStatus(document, formatted);
    flockBreakLongText(description, 40);
    // Do not add a Reply button for Digg's stories
    if (aFriendActivity.updateURL && aAccount.isStatusEditable) {
      feedItem.setAttribute("reply_enabled", "true");
      // Include the Re-tweet button for Twitter
      if (aService.contractId == TWITTER_CONTRACT_ID) {
        feedItem.setAttribute("retweet_enabled", "true");
      }
    }
  } else {
    description = document.createTextNode(formatted);
  }

  feedItem.setAttribute("profileURL",
                        aAccount.getProfileURLForFriend(friend.id()));
  feedItem.setAttribute("ondraggesture",
                        "nsDragAndDrop.startDrag(event, draggingObserver, true);");
  feedItem.description = description;
  feedItem.setAttribute("serviceName", aService.title);
  feedItem.setAttribute("faviconUrl", aService.icon);
  feedItem.latestDate = dt.getTime();
  feedItem.accountUrn = aAccount.urn;

  // Insert (sorted) in the container
  var inserted = false;
  var children = this._friendFeedBox.childNodes;
  for (let i = 0; (i < children.length && !inserted); i++) {
    if (feedItem.latestDate > children[i].latestDate) {
      this._friendFeedBox.insertBefore(feedItem, children[i]);
      inserted = true;
    }
  }
  if (!inserted) {
    this._friendFeedBox.appendChild(feedItem);
  }
  var dateLbl = document.getAnonymousElementByAttribute(feedItem,
                                                        "anonid",
                                                        "person-lastupdate");
  dateLbl.setAttribute("tooltiptext", FlockDateFormatter.getDateTimeString(dt));

  this._deck.selectedIndex = SHOWING_FRIEND_ACTIVITIES;

  var os = Cc["@mozilla.org/observer-service;1"]
           .getService(Ci.nsIObserverService);
  os.notifyObservers(null, "refresh-myworld-size", false);
}

FriendsFeedController.prototype.removeFromAccount =
function FriendsFeedController_removeFromAccount(accountUrn) {
  var nodeList = this._friendFeedBox.childNodes;
  for (var c = 0; c < nodeList.length; c++) {
    var item = nodeList.item(c);
    if (item.accountUrn == accountUrn) {
      this._friendFeedBox.removeChild(item);
      c--;
    }
  }
}

FriendsFeedController.prototype.insertFromAccount =
function FriendsFeedController_insertFromAccount(aAccount, aService) {

  var friendActivityLog = aAccount.getParam("friendActivityLog");
  if (!friendActivityLog) {
    return;
  }

  var allActivityLogs = [];
  var activityEnum = friendActivityLog.children.enumerateBackwards();
  if (activityEnum.hasMoreElements()) {
    allActivityLogs.push(activityEnum);
  }

  // No activity log!
  if (allActivityLogs.length == 0) {
    return;
  }

  // You might think this should be initialized to the value of
  // this._friendFeedBox.childNodes.length, but that'd mean that
  // not every service would get the chance to add its stuff if
  // the maximum has already been reached. Since we sort by date,
  // that would be bad.
  var count = 0;

  // Only one activity log: do it the simple way
  if (allActivityLogs.length == 1) {
    var log = allActivityLogs[0];
    while (count < this.mMaxCount && log.hasMoreElements()) {
      var activity = log.getNext();
      if (activity) {
        this.insert(activity, aAccount, aService);
      }
    }
    return;
  }

  // Multiple logs: aggregate them, cap to this.mMaxCount
  var allLogs = allActivityLogs.map(
    function (x) {
      // We made sure earlier than hasMoreElements() is true
      var head = x.getNext();
      while (!head && x.hasMoreElements()) {
        head = x.getNext();
      }
      return {
        head: head,
        enumerator: x
      };
    }
  );

  while (count < this.mMaxCount && allLogs.length > 0) {
    var pos = 0;
    for (var i = 1; i < allLogs.length; i++) {
      if (allLogs[i].head.updateDate.getTime() >
          allLogs[pos].head.updateDate.getTime())
      {
        pos = i;
      }
    }
    this.insert(allLogs[pos].head, aAccount, aService);

    if (allLogs[pos].enumerator.hasMoreElements()) {
      allLogs[pos].head = allLogs[pos].enumerator.getNext();
    } else {
      allLogs.splice(pos, 1);
    }

    ++count;
  }
}

FriendsFeedController.prototype.insertAll =
function FriendsFeedController_insertAll() {
  // Get enumerators for each friend activity log
  var acctRoot = _coop.get("http://flock.com/rdf#AccountsRoot");
  var coopAccounts = acctRoot.children.enumerate();
  while (coopAccounts.hasMoreElements()) {
    var coopAccount = coopAccounts.getNext();
    if (coopAccount && coopAccount.isAuthenticated
                    && coopAccount.friendActivityLog)
    {
      var svcClass = Cc[coopAccount.serviceId];
      if (svcClass) {
        var service = svcClass.getService(Ci.flockIWebService);
        this.insertFromAccount(service.getAccount(coopAccount.id()), service);
      }
    }
  }
  this.cutToMax();
}

FriendsFeedController.prototype.init =
function FriendsFeedController_init() {
  this.insertAll();

  gObsrvr = {};
  gObsrvr.observe = function date_observe(aSubject, aTopic, aString) {
    if (aTopic == "refresh-myworld-date") {
      var nodeList = gFriendsFeedController._friendFeedBox.childNodes;
      for (var c = 0; c < nodeList.length; c++) {
        nodeList.item(c).updateTime();
      }
      _logger.debug("FriendListController: updated dates");
    }
  }

  // Add observer for time change
  var os = Cc["@mozilla.org/observer-service;1"]
           .getService(Ci.nsIObserverService);
  os.addObserver(gObsrvr, "refresh-myworld-date", false);

  this.peopleObserver = {
    onFriendRemove: function peepObs_onFriendRemove(aFriendUrn) {
    },
    onFriendUpdate: function peepObs_onFriendUpdate(aFriendUrn) {
    },
    onInsertFriend: function peepObs_onInsertFriend(aFriendUrn) {
    },
    onNewActivity: function peepObs_onNewActivity(aActivityUrn) {
      var activity = _coop.get(aActivityUrn);
      var friend = activity.identity;
      var svc = Cc[friend.serviceId].getService(Ci.flockIWebService);
      var acct = svc.getAccount(friend.getAccount().id());
      gFriendsFeedController.insert(activity, acct, svc);
      gFriendsFeedController.cutToMax();
    }
  };
  pplsvc.addObserver(this.peopleObserver);

  this.accountsObserver = {
    onAccountAuthenticated:
    function accountsObs_onAccountAuthenticated(aAccount) {
      gFriendsFeedController.insertFromAccount(aAccount, aAccount.getService());
      gFriendsFeedController.cutToMax();
      gFriendsFeedController.doLayout();
    },
    onAccountRemoved:
    function accountsObs_onAccountRemoved(aAccount) {
      gFriendsFeedController.doLayout();
    },
    onAccountUnauthenticated:
    function accountsObs_onAccountUnauthenticated(aAccount) {
      gFriendsFeedController.removeFromAccount(aAccount.urn);
      gFriendsFeedController.insertAll();
      gFriendsFeedController.doLayout();
    },
    onAccountRefreshing:
    function accountsObs_onAccountRefreshing(aAccountUrn) {
    },
    onAccountNoLongerRefreshing:
    function accountsObs_onAccountNoLongerRefreshing(aAccountUrn) {
      gFriendsFeedController.doLayout();
    }
  }
  accsvc.addObserver(this.accountsObserver);

  // We need an initial layout event.
  gFriendsFeedController.doLayout();
}

FriendsFeedController.prototype.uninit =
function FriendsFeedController_uninit() {
  var os = Cc["@mozilla.org/observer-service;1"]
           .getService(Ci.nsIObserverService);
  os.removeObserver(gObsrvr, "refresh-myworld-date");
  pplsvc.removeObserver(this.peopleObserver);
  accsvc.removeObserver(this.accountsObserver);
}

function loadURI(aURL, aEvent) {
  openURLMyWorld(aURL, aEvent);
  aEvent.stopPropagation();
}

function hoverURI(aLinkNode, aEvent) {
  function cb_setStatus(aStatus) {
    window.status = aStatus;
  }
  if (aLinkNode) {
    FlockURLUtils.hoverLink(aLinkNode, cb_setStatus);
  } else {
    cb_setStatus("");
  }
}

function onLoad() {
  gFriendsFeedController = new FriendsFeedController("friendsstreambox",
                                                     "panelSwitcher");
  gFriendsFeedController.init();
}

function onUnload() {
  if (gFriendsFeedController) {
    gFriendsFeedController.uninit();
    gFriendsFeedController = null;
  }
}
