// 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

const CC = Components.classes;
const CI = Components.interfaces;
const CR = Components.results;
const CU = Components.utils;

CU.import("resource:///modules/FlockXPCOMUtils.jsm");
FlockXPCOMUtils.debug = false;
CU.import("resource:///modules/FlockMediaUtils.jsm");
CU.import("resource:///modules/FlockPhotoUploader.jsm");
CU.import("resource:///modules/FlockPrefsUtils.jsm");
CU.import("resource:///modules/FlockSvcUtils.jsm");
CU.import("resource:///modules/FlockXMLUtils.jsm");
CU.import("resource:///modules/FlockScheduler.jsm");
CU.import("resource://gre/modules/JSON.jsm");
CU.import("resource://gre/modules/ISO8601DateUtils.jsm");


const MODULE_NAME = "Hi5";
const API_CLASS_NAME = "Flock Hi5 API";
const API_CLASS_ID = Components.ID("{e4dba06d-f335-47c0-b42b-9ac8b70d0506}");
const API_CONTRACT_ID = "@flock.com/webservice/api/hi5;1";
const CLASS_NAME = "Flock Hi5 Service";
const CLASS_SHORT_NAME = "hi5";
const CLASS_TITLE = "Hi5";
const CLASS_ID = Components.ID("{b173f245-b532-4690-84f8-a9001d9bcde5}");
const CONTRACT_ID = "@flock.com/webservice/hi5;1";
const FLOCK_ERROR_CONTRACTID = "@flock.com/error;1";
const XMLHTTPREQUEST_CONTRACTID = "@mozilla.org/xmlextras/xmlhttprequest;1";
const HASH_PROPERTY_BAG_CONTRACTID = "@mozilla.org/hash-property-bag;1";
const FLOCK_RDNDS_CONTRACTID = "@flock.com/rich-dnd-service;1";
// From nsIXMLHttpRequest.idl
const XMLHTTPREQUEST_READYSTATE_COMPLETED = 4;
const SERVICE_ENABLED_PREF = "flock.service.hi5.enabled";
const HI5_URL = "http://www.hi5.com/";
const FAVICON = "chrome://flock/content/services/hi5/hi5Favicon.png";
const HI5_PROPERTIES = "chrome://flock/locale/services/hi5.properties";
const FLOCK_PHOTO_ALBUM_CONTRACTID  = "@flock.com/photo-album;1";
const ATOM_NS = "http://www.w3.org/2005/Atom";

const SERVICES_PROPERTIES_FILE = "chrome://flock/locale/services/services.properties";
const SERVICES_SHARE_FLOCK_SUBJECT = "flock.friendShareFlock.subject";
const SERVICES_SHARE_FLOCK_MESSAGE = "flock.friendShareFlock.message";

// The delay between two refreshes when the sidebar is closed (in seconds)
const REFRESH_INTERVAL = 1800; // seconds == 30 minutes
// The delay between two refreshes when the sidebar is open (in seconds)
const SHORT_INTERVAL = 300; // 5 minutes

var gApi = null;

// Override the buildTooptip function in order to use the
// large img for the tooltip image.
var flockMediaItemFormatter = {
  canBuildTooltip: true,
  buildTooltip: function Hi5_buildTooltip(aMediaItem) {
    default xml namespace =
      "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";

    var xml =
      <vbox>
        <hbox>
          <image src={aMediaItem.largeSizePhoto} style="margin-bottom: 2px;" />
          <spacer flex="1" />
        </hbox>
        <hbox>
          <vbox>
            <image src={aMediaItem.icon} />
            <spacer flex="1" />
          </vbox>
          <vbox anonid="ttInfoContainer">
            <label anonid="ttTitleTxt">{aMediaItem.title}</label>
            <label anonid="ttUserTxt" class="user">{aMediaItem.username}</label>
          </vbox>
        </hbox>
      </vbox>;

    return xml;
  }
};

/*************************************************************************
 * Component: Hi5API
 *************************************************************************/
function Hi5API() {
  FlockSvcUtils.getLogger(this);
  this._logger.init("Hi5API");

  this._WebDetective = CC["@flock.com/web-detective;1"]
                       .getService(CI.flockIWebDetective);

  // Convenience variable.
  this._wd = this._WebDetective;

  this._logger.debug("constructor");
  var wsa = FlockSvcUtils.flockIWebServiceAPI;
  wsa.addDefaultMethod(this, "getRequestMethod");
}

Hi5API.prototype = new FlockXPCOMUtils.genericComponent(
  API_CLASS_NAME,
  API_CLASS_ID,
  API_CONTRACT_ID,
  Hi5API,
  0,
  [
    CI.nsISupports,
    CI.nsIClassInfo,
    CI.flockIAuthenticatedAPI,
    CI.flockITokenAPI,
    CI.flockIWebServiceAPI
  ]
);

/*************************************************************************
 * Hi5API: flockIWebServiceAPI Implementation
 *************************************************************************/

/**
 * void call(in AString aApiMethod,
 *           in nsISupports aParams,
 *           in nsISupports aPostVars,
 *           in nsISupports aRequestMethod,
 *           in flockIListener aFlockListener);
 * @see flockIWebServiceAPI#call
 */
Hi5API.prototype.call =
function Hi5API_call(aApiMethod,
                     aParams,
                     aPostVars,
                     aRequestMethod,
                     aFlockListener)
{
  this._logger.debug(arguments.callee.name + "('" + aApiMethod + "', ...)");

  var api = this;

  var req = CC[XMLHTTPREQUEST_CONTRACTID].createInstance(CI.nsIXMLHttpRequest);
  req.onreadystatechange = function Hi5API_call_orsc(aEvent) {
    api._logger.debug(".call() ReadyState = " + req.readyState);
    if (req.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
      api._logger.debug(".call() Status = " + req.status);
      var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
      var result = nsJSON.decode(req.responseText);
      if (Math.floor(req.status / 100) == 2) {
        aFlockListener.onSuccess(result, arguments.callee.name);
      } else {
        // HTTP errors
        api._logger.debug("_doCall() url: " + aApiMethod
                          + " HTTP Error: " + req.status + "\n");
        var error = api.getHttpError(req.status);
        error.serviceErrorString =
          result["hi5.error_response"]["hi5.error_message"];
        error.serviceErrorCode = req.status;
        aFlockListener.onError(error, arguments.callee.name);
      }
    }
  };

  var url = "http://api.hi5.com/json/" + aApiMethod;
  if (aParams) {
    url += "?" + this._getParamString(aParams);
  }

  this._logger.debug(".call() url - " + url);

  var requestMethodStr = this.getRequestMethod(aRequestMethod);
  req.open(requestMethodStr, url, true);
  req.send(null);
};

/**
 * void openSocialCall(in AString aApiMethod,
 *                     in nsISupports aParams,
 *                     in nsISupports aPostVars,
 *                     in nsISupports aRequestMethod,
 *                     in flockIListener aFlockListener);
 * @see flockIWebServiceAPI#call
 */
Hi5API.prototype.openSocialCall =
function Hi5API_call(aApiMethod,
                     aParams,
                     aPostVars,
                     aRequestMethod,
                     aFlockListener)
{
  this._logger.debug(arguments.callee.name + "('" + aApiMethod + "', ...)");

  var api = this;

  var req = CC[XMLHTTPREQUEST_CONTRACTID].createInstance(CI.nsIXMLHttpRequest);
  req.onreadystatechange = function Hi5API_call_orsc(aEvent) {
    api._logger.debug(".openSocialCall() ReadyState = " + req.readyState);
    if (req.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
      api._logger.debug(".openSocialall() Status = " + req.status);
      if (Math.floor(req.status / 100) == 2) {
        var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
        var result = nsJSON.decode(req.responseText);
        aFlockListener.onSuccess(result, arguments.callee.name);
      } else {
        // HTTP errors
      }
    }
  };

  var url = "http://api.hi5.com/social/rest/" + aApiMethod;
  if (aParams) {
    url += "?" + this._getParamString(aParams);
  }

  this._logger.debug(".openSocialCall() url - " + url);

  var requestMethodStr = this.getRequestMethod(aRequestMethod);
  req.open(requestMethodStr, url, true);
  req.send(null);
};


/**
 * flockIError getError(in AString aErrorCode);
 * @see flockIWebServiceAPI#getError
 */
Hi5API.prototype.getError =
function Hi5API_getError(aErrorCode) {
  this._logger.debug(".getError('" + aErrorCode + "')");

  var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
  error.serviceErrorCode = aErrorCode;
  error.serviceName = CLASS_TITLE;

  switch (aErrorCode) {
    default:
      error.errorCode = CI.flockIError.PHOTOSERVICE_UNKNOWN_ERROR;
      break;
  }

  return error;
};

/**
 * flockIError getHttpError(in AString aHttpErrorCode);
 * @see flockIWebServiceAPI#getHttpError
 */
Hi5API.prototype.getHttpError =
function Hi5API_getHttpError(aHttpErrorCode) {
  this._logger.debug(".getHttpError('" + aHttpErrorCode + "')");

  var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
  error.errorCode = CI.flockIError.PHOTOSERVICE_UNKNOWN_ERROR;
  error.serviceErrorCode = aHttpErrorCode;
  error.serviceName = CLASS_TITLE;

  return error;
};


/*************************************************************************
 * Hi5API: flockIAuthenticatedAPI Implementation
 *************************************************************************/

// readonly attribute boolean isAuthenticated;
Hi5API.prototype.isAuthenticated = false;

/**
 * void authenticate(in nsILoginInfo aCredentials,
 *                   in flockIListener aFlockListener);
 * @see flockIAuthenticatedAPI#authenticate
 */
Hi5API.prototype.authenticate =
function Hi5API_authenticate(aCredentials, aFlockListener) {
  this._logger.debug(".authenticate("
                     + ((aCredentials) ? aCredentials.username : "")
                     + ", ...)");
  var api = this;

  var getTokenListener = {
    onSuccess: function gtl_onSuccess(aSubject, aTopic) {
      api._logger.debug(arguments.callee.name);
      api.token = aSubject["hi5.authToken"].$;
      aFlockListener.onSuccess(api, "authenticated");
    },
    onError: function gtl_onError(aFlockError, aTopic) {
      api._logger.error(arguments.callee.name);
      aFlockListener.onError(aFlockError, arguments.callee.name);
    }
  };

  var params = {
    username: aCredentials.username,
    password: aCredentials.password,
    api_key: "a31124_488abe7f850ab7071fbf2" // TODO: move to web dic
  };

  this.call("auth/plain",
            params,
            null,
            CI.flockIWebServiceAPI.POST,
            getTokenListener);
};

/**
 * void deauthenticate();
 * @see flockIAuthenticatedAPI#deauthenticate
 */
Hi5API.prototype.deauthenticate =
function Hi5API_deauthenticate() {
  this._logger.debug(".deauthenticate()");
  this.token = "";
  this.tokenSecret = "";
  this.isAuthenticated = false;
};


/*************************************************************************
 * Hi5API: flockITokenAPI Implementation
 *************************************************************************/

// readonly attribute AString token;
Hi5API.prototype.token = "";

// readonly attribute AString token secret;
Hi5API.prototype.tokenSecret = "";


/*************************************************************************
 * Hi5API: Private Data and Functions
 *************************************************************************/

Hi5API.prototype._getParamString =
function Hi5API__getParamString(aParams) {
  var rval = "";

  var count = 0;
  for (var p in aParams) {
    if (count++ != 0) {
      rval += "&";
    }
    rval += encodeURIComponent(p) + "=" + encodeURIComponent(aParams[p]);
  }

  return rval;
};


/*************************************************************************
 * Component: Hi5Service
 *************************************************************************/
function Hi5Service() {
  FlockSvcUtils.getLogger(this);
  this._logger.init("Hi5Service");

  // Determine whether this service has been disabled, and unregister if so.
  if (FlockPrefsUtils.getBoolPref(SERVICE_ENABLED_PREF) === false) {
    this._logger.info(SERVICE_ENABLED_PREF + " is FALSE! Not initializing.");
    this.deleteCategories();
    return;
  }

  var profiler = CC["@flock.com/profiler;1"].getService(CI.flockIProfiler);
  var evtID = profiler.profileEventStart(CLASS_SHORT_NAME + "-init");

  this._ppSvc = CC["@flock.com/people-service;1"]
                .getService(CI.flockIPeopleService);

  this._accountClassCtor = Hi5Account;

  // Used for pseudo-pagination
  this._cache = {
    userName: null,
    userAvatar: null,
    photos: null,
    isAlbum: false
  };

  // Convenience variable.
  this._wd = FlockSvcUtils.getWD(this);
  //FlockSvcUtils.getACUtils(this);
  FlockSvcUtils.initService(this, true);

  // Initialize API
  gApi = new Hi5API();

  // Initialize member that specifies path for localized string bundle
  this._stringBundlePath = HI5_PROPERTIES;

  profiler.profileEventEnd(evtID, "");

  var ws = FlockSvcUtils.flockIWebService;
  ws.addDefaultMethod(this, "getStringBundle");
  ws.addDefaultMethod(this, "isHidden");

  var lws = FlockSvcUtils.flockILoginWebService;
  lws.addDefaultMethod(this, "loginURL");
  lws.addDefaultMethod(this, "tooltip");
  lws.addDefaultMethod(this, "docRepresentsSuccessfulLogin");
  lws.addDefaultMethod(this, "getAccountUrns");
  lws.addDefaultMethod(this, "getAccount");
  lws.addDefaultMethod(this, "getAccountIDFromDocument");
  lws.addDefaultMethod(this, "getAuthenticatedAccount");
  lws.addDefaultMethod(this, "getCredentialsFromForm");
  lws.addDefaultMethod(this, "getSessionValue");
  lws.addDefaultMethod(this, "ownsDocument");
  lws.addDefaultMethod(this, "ownsLoginForm");
  lws.addDefaultMethod(this, "removeAccount");

  var mws = FlockSvcUtils.flockIMediaWebService;
  mws.addDefaultMethod(this, "enumerateChannels");
  mws.addDefaultMethod(this, "getChannel");
  mws.addDefaultMethod(this, "getMediaItemFormatter");
  mws.addDefaultMethod(this, "getIconForQuery");

  var muws = FlockSvcUtils.flockIMediaUploadWebService;
  muws.addDefaultMethod(this, "getAlbumsForUpload");

  var mews = FlockSvcUtils.flockIMediaEmbedWebService;
  mews.addDefaultMethod(this, "getSharingContent");

  var rcdh = FlockSvcUtils.flockIRichContentDropHandler;
  rcdh.addDefaultMethod(this, "_handleTextareaDrop");

  var sws = FlockSvcUtils.flockISocialWebService;
  sws.addDefaultMethod(this, "parseStatus");
}

Hi5Service.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME,
  CLASS_ID,
  CONTRACT_ID,
  Hi5Service,
  CI.nsIClassInfo.SINGLETON,
  [
    CI.flockIWebService,
    CI.flockILoginWebService,
    CI.flockIPollingService,
    CI.flockIMediaWebService,
    CI.flockIMediaUploadWebService,
    CI.flockIMediaEmbedWebService,
    CI.flockISocialWebService,
    CI.flockIRichContentDropHandler
  ]
);

// FlockXPCOMUtils.genericModule() categories
Hi5Service.prototype._xpcom_categories = [
  { category: "wsm-startup" },
  { category: "flockWebService", entry: CLASS_SHORT_NAME },
  { category: "flockMediaProvider", entry: CLASS_SHORT_NAME },
  { category: "flockRichContentHandler", entry: CLASS_SHORT_NAME }
];


/*************************************************************************
 * Hi5Service: Private Data and Functions
 *************************************************************************/

Hi5Service.prototype._processAlbumResults =
function Hi5Service__processAlbumResults(aJSON) {
  var result = [];
  var newAlbum;
  var temp;

  // Return empty array if user has no albums
  if (!aJSON["atom.feed"]["atom.entry"]) {
    return result;
  }
  // Get the default album
  temp = aJSON["atom.feed"]["atom.entry"]["atom.id"].split("/");
  newAlbum = CC[FLOCK_PHOTO_ALBUM_CONTRACTID]
             .createInstance(CI.flockIPhotoAlbum);
  newAlbum.id = temp[temp.length - 1];

  // Construct the name of the default album
  var re = this._wd.getString(CLASS_SHORT_NAME, "albumsFor", "");
  var name = aJSON["atom.feed"]["atom.title"].match(new RegExp(re));
  if (name && name[1]) {
    // Trim whitespace from ends
    var nameStr = name[1].replace(/^\s+/g, "").replace(/\s+$/g, "");
    newAlbum.title = this.getStringBundle()
                         .GetStringFromName("flock.hi5.default.album.name")
                         .replace("%name%", nameStr);
  }
  result.push(newAlbum);

  var albums = [];
  if (aJSON["atom.entry"]) {
    albums =  albums.concat(aJSON["atom.entry"]);
  }
  for (var j = 0; j < albums.length; j++) {
    var album = albums[j];
    temp =  album["atom.id"].split("/");
    newAlbum = CC[FLOCK_PHOTO_ALBUM_CONTRACTID]
               .createInstance(CI.flockIPhotoAlbum);
    newAlbum.id =  temp[temp.length - 1];
    newAlbum.title = album["atom.title"];
    result.push(newAlbum);
  }

  return result;
};

Hi5Service.prototype._addPerson =
function Hi5Service__addPerson(aAccount, aPerson) {
  var person = {};
  person.accountId = aPerson.id;
  person.avatar = aPerson.thumbnailUrl;
  person.name = aPerson.displayName;
  person.statusMessage = aPerson.statusMessage;
  person.lastUpdateType = "status";
  person.URL = this._wd
                   .getString(CLASS_SHORT_NAME, "profileURL", "")
                   .replace("%userid%", aPerson.id);

  var lastUpdate = 0;
  if (aPerson.statusUpdateDate) {
    lastUpdate = Math.floor(aPerson.statusUpdateDate.getTime() / 1000);
  }

  var identityUrn = "urn:flock:identity:hi5:"
                  + aAccount.getParam("accountId") + ":"
                  + person.accountId;
  var cachedPerson = this._coop.get(identityUrn);
  if (cachedPerson) {
    var now = new Date().getTime();
    now = Math.floor(now / 1000);

    if (person.name != cachedPerson.name ||
        person.avatar != cachedPerson.avatar)
    {
      person.lastUpdateType = "profile";
      lastUpdate = now;
    }

    if (cachedPerson.lastUpdate >= lastUpdate) {
      return;
    }
  }

  aAccount.coopObj.addPerson(person, lastUpdate);
};

/**
 * Create a lightweight quasi-nsISimpleEnumerator from a passed in array.
 *
 * Doing it here in JS prevents having to cross XPCOM boundaries, resulting
 * in a performance bump.
 *
 * @param aArray  The array to create the enumerator from.
 * @returns  the enumerator claiming to be a nsISimpleEnumerator.
 */
Hi5Service.prototype._createEnum =
function Hi5Service__createEnum(aArray) {
  return {
    QueryInterface: function QueryInterface(aIid) {
      if (aIid.equals(CI.nsISimpleEnumerator)) {
        return this;
      }
      throw CR.NS_ERROR_NO_INTERFACE;
    },
    hasMoreElements: function hasMoreElements() {
      return (aArray.length > 0);
    },
    getNext: function getNext() {
      return aArray.shift();
    }
  };
};

Hi5Service.prototype._refreshAccount =
function Hi5Service_refreshAccount(aAccount, aPollingListener) {
  this._logger.debug("._refreshAccount('" + aAccount.getParam("accountId")
                                          + "')");
  var inst = this;
  var refreshItem = aAccount.coopObj;
  var peopleHash = {};

  var params = {
    "Hi5AuthToken": gApi.token
  };

  var pplRefreshListener = CC["@flock.com/people-refresh-listener;1"]
                           .createInstance(CI.flockIPeopleRefreshListener);
  pplRefreshListener.init(4, refreshItem.id(), aPollingListener, 0);

  var profileGetListener = {
    onSuccess: function fgl_onSuccess(aResult, aStatus) {
      inst._logger.info("Success for profileGetListener");
      // TODO: find a way to get the avatar
      refreshItem.name = aResult.entry.displayName;
      refreshItem.avatar = aResult.entry.thumbnailUrl;
      pplRefreshListener.onSuccess(aAccount, "profileGetListener");
    },
    onError: function fgl_onError(aFlockError, aTopic) {
      inst._logger.error("Error on gettingFriends");
      pplRefreshListener.onError(aFlockError, "profileGetListener");
    }
  };
  var statusGetListener = {
    onSuccess: function fgl_onSuccess(aResult, aStatus) {
      inst._logger.info("Success for statusGetListener");
      var status = aResult["hi5.memberStatus"];
      var statusMessage = status["hi5.content"];
      var statusDate = status["hi5.timeAdded"];
      if (statusDate) {
        var statusEpoch = ISO8601DateUtils.parse(statusDate).getTime();
        refreshItem.lastStatusMessageUpdateDate = statusEpoch / 1000;
      }
      if (statusMessage) {
        refreshItem.statusMessage = flockXMLEncode(statusMessage);
      }
      pplRefreshListener.onSuccess(aAccount, "statusGetListener");
    },
    onError: function fgl_onError(aFlockError, aTopic) {
      inst._logger.error("Error on gettingFriends");
      pplRefreshListener.onError(aFlockError, "statusGetListener");
    }
  };
  var friendStatusesListener = {
    onSuccess: function fgl_onSuccess(aResult, aStatus) {
      inst._logger.info("Success for friendsStatusesListener");

      var statuses = aResult["hi5.memberStatusList"]["hi5.friendStatuses"];
      for each (let status in statuses) {
        var userid = status["hi5.userId"];
        if (!peopleHash[userid]) {
          // We got the status of a person who's not a friend?
          // That shouldn't happen.
          continue;
        }
        peopleHash[userid].statusMessage = status["hi5.content"];
        peopleHash[userid].statusUpdateDate = ISO8601DateUtils.parse(status["hi5.timeAdded"]);
      }

      function myWorker(aShouldYield) {
        if (aAccount.isAuthenticated()) {
          // REMOVE locally people removed on the server
          inst._logger.debug("Checking if friends were removed on the server...");
          var localFriends = aAccount.enumerateFriends();
          while (localFriends.hasMoreElements()) {
            var identity = localFriends.getNext();
            if (!peopleHash[identity.accountId]) {
              inst._logger.info(identity.name
                                 + " removed on the server,"
                                 + " let's remove locally");
              identity.destroy();
            }
          }
          // ADD or update existing people
          for each (var person in peopleHash) {
            inst._addPerson(aAccount, person);
            if (aShouldYield()) {
              yield;
              if (!aAccount.isAuthenticated()) {
                // Account has just been deleted or logged out
                break;
              }
            }
          }
        }
        pplRefreshListener.onSuccess(aAccount, "friendsGetListener");
      }
      FlockScheduler.schedule(null, 0.05, 10, myWorker);
    },
    onError: function fgl_onError(aFlockError, aTopic) {
      inst._logger.error("Error on gettingFriends");
      pplRefreshListener.onError(aFlockError, "friendsGetListener");
    }
  };
  var friendsGetListener = {
    onSuccess: function fgl_onSuccess(aResult, aStatus) {
      var friends = aResult.entry;
      for each (let person in friends) {
        peopleHash[person.id] = person;
      }
      gApi.call("status/getFriendStatuses",
                params,
                null,
                CI.flockIWebServiceAPI.GET,
                friendStatusesListener);
    },
    onError: function fgl_onError(aFlockError, aTopic) {
      inst._logger.error("Error on gettingFriends");
      pplRefreshListener.onError(aFlockError, "friendsGetListener");
    }
  };
  var meNotificationsListener = {
    onSuccess: function fgl_onSuccess(aResult, aStatus) {
      var notis = aResult.entry;
      var messages, friendRequests, profileComments, photoComments;
      for each(let noti in notis) {
        switch (noti.id) {
          case "inbox":
            messages = noti.unread;
            break;
          case "friendRequests":
            friendRequests = noti.total;
            break;
          case "profile":
            profileComments = noti.total;
            break;
          case "photo":
            photoComments = noti.total;
            break;
        }
      }

      // if any of the Me notifications have increased in count,
      // light the people button
      var c_params = aAccount.getAllCustomParams();
      var profileCommentsTotal = c_params.getProperty("profileCommentsTotal");
      var oldProfileComments = c_params.getProperty("profileComments");
      var photoCommentsTotal = c_params.getProperty("photoCommentsTotal");
      var oldPhotoComments = c_params.getProperty("photoComments");

      if ((refreshItem.accountMessages < messages) ||
          (profileCommentsTotal < profileComments) ||
          (photoCommentsTotal < photoComments) ||
          (c_params.getProperty("friendRequests") < friendRequests))
      {
        inst._ppSvc.togglePeopleIcon(true);
      }

      // write notifications to RDF
      refreshItem.accountMessages = messages;
      aAccount.setCustomParam("friendRequests", friendRequests);

      var profileCommentsDiff = profileComments - profileCommentsTotal >= 0
                              ? profileComments - profileCommentsTotal
                              : 0;
      aAccount.setCustomParam("profileComments",
                              profileCommentsDiff + oldProfileComments);
      aAccount.setCustomParam("profileCommentsTotal", profileComments);

      var photoCommentsDiff = photoComments - photoCommentsTotal >= 0
                            ? photoComments - photoCommentsTotal
                            : 0;
      aAccount.setCustomParam("photoComments",
                              photoCommentsDiff + oldPhotoComments);
      aAccount.setCustomParam("photoCommentsTotal", photoComments);

      pplRefreshListener.onSuccess(aAccount, "meNotificationsListener");
    },
    onError: function fgl_onError(aFlockError, aTopic) {
      inst._logger.error("Error on gettingFriends");
      pplRefreshListener.onError(aFlockError, "meNotificationsListener");
    }
  };

  gApi.openSocialCall("people/" + refreshItem.accountId + "/@friends",
                      null,
                      null,
                      CI.flockIWebServiceAPI.GET,
                      friendsGetListener);
  gApi.openSocialCall("people/" + refreshItem.accountId,
                      null,
                      null,
                      CI.flockIWebServiceAPI.GET,
                      profileGetListener);
  params["userId"] = refreshItem.accountId;
  gApi.call("status/getStatus",
            params,
            null,
            CI.flockIWebServiceAPI.GET,
            statusGetListener);

  gApi.openSocialCall("messages/" + refreshItem.accountId,
            { "st": gApi.token },
            null,
            CI.flockIWebServiceAPI.GET,
            meNotificationsListener);
};

/*************************************************************************
 * Hi5Service: flockIWebService Implementation
 *************************************************************************/

// readonly attribute AString urn;
Hi5Service.prototype.urn = "urn:" + CLASS_SHORT_NAME + ":service";

// readonly attribute AString title;
Hi5Service.prototype.title = CLASS_TITLE;

// readonly attribute AString icon;
Hi5Service.prototype.icon = FAVICON;

// readonly attribute AString url;
Hi5Service.prototype.url = HI5_URL;

// readonly attribute AString contractId;
// ALMOST duplicated by nsIClassInfo::contractID
// with different case: contractId != contractID
Hi5Service.prototype.contractId = CONTRACT_ID;

// readonly attribute AString shortName;
Hi5Service.prototype.shortName = CLASS_SHORT_NAME;


/*************************************************************************
 * Hi5Service: flockILoginWebService Implementation
 *************************************************************************/

// readonly attribute AString domains;
Hi5Service.prototype.__defineGetter__("domains",
function Hi5Service_getdomains() {
  this._logger.debug("Getting attribute: domains");

  return this._wd.getString(CLASS_SHORT_NAME, "domains", "hi5.com");
});

// readonly attribute boolean needPassword;
Hi5Service.prototype.needPassword = true;

// DEFAULT: readonly attribute AString loginURL;
// DEFAULT: readonly attribute AString tooltip;

/**
 * @see flockILoginWebService#addAccount
 */
Hi5Service.prototype.addAccount =
function Hi5Service_addAccount(aAccountId, aIsTransient, aListener) {
  this._logger.debug(".addAccount('" + aAccountId + "', "
                                     + aIsTransient + ", aListener)");

  if (!aAccountId) {
    if (aListener) {
      var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
      // XXX See bug 10749 - flockIError cleanup
      error.errorCode = 9999;
      error.errorString = "No Account ID provided";
      aListener.onError(this, arguments.callee.name, error);
    }
    return null;
  }

  var pw = this._acUtils.getPassword(this.urn + ":" + aAccountId);
  var name = (pw) ? pw.username : aAccountId;
  var url = this._wd
                .getString(CLASS_SHORT_NAME, "myProfileURL", "")
                .replace("%userid%", aAccountId);
  var accountUrn = this._acUtils.createAccount(this,
                                               aAccountId,
                                               name,
                                               url,
                                               aIsTransient);

  var account = this.getAccount(accountUrn);
  account.setParam("service", this._coopObj);
  account.setCustomParam("friendRequests", 0);
  account.setCustomParam("profileComments", 0);
  account.setCustomParam("profileCommentsTotal", 0);
  account.setCustomParam("photoComments", 0);
  account.setCustomParam("photoCommentsTotal", 0);
  // Is this service pollable by flockIPollerService?
  account.setParam("refreshInterval", REFRESH_INTERVAL);
  if (aListener) {
    aListener.onSuccess(account, "addAccount");
  }
  return account;
};

// DEFAULT: boolean docRepresentsSuccessfulLogin(in nsIDOMHTMLDocument aDocument);
// DEFAULT: flockIWebServiceAccount getAccount(in AString aAccountUrn);
// DEFAULT: AString getAccountIDFromDocument(in nsIDOMHTMLDocument aDocument);
// DEFAULT: nsIStringEnumerator getAccountUrns();
// DEFAULT: flockIWebServiceAccount getAuthenticatedAccount();
// DEFAULT: nsIPassword getCredentialsFromForm(in nsIDOMHTMLFormElement aForm);
// DEFAULT: AString getSessionValue();

/**
 * @see flockILoginWebService#logout
 */
Hi5Service.prototype.logout =
function Hi5Service_logout() {
  this._logger.debug(".logout()");
  this._acUtils.removeCookies(FlockSvcUtils.getWD(this)
               .getSessionCookies(CLASS_SHORT_NAME));
  this._acUtils.markAllAccountsAsLoggedOut(this);
  gApi.deauthenticate();
};

// DEFAULT: boolean ownsDocument(in nsIDOMHTMLDocument aDocument);
// DEFAULT: boolean ownsLoginForm(in nsIDOMHTMLFormElement aForm);
// DEFAULT: void removeAccount(in AString aAccountUrn);

/**
 * void updateAccountStatusFromDocument(in nsIDOMHTMLDocument aDocument,
 *                                      in flockIWebServiceAccount aAccount,
 *                                      in flockIListener aAuthListener);
 * @see flockILoginWebService#updateAccountStatusFromDocument
 */
Hi5Service.prototype.updateAccountStatusFromDocument =
function Hi5Service_updateAccountStatusFromDocument(aDocument,
                                                    aAccount,
                                                    aAuthListener)
{
  if (aAccount) {
    // We know we're already logged in to this account
    if (!aAccount.isAuthenticated()) {
      aAccount.login(aAuthListener);
    }
  } else if (this._wd.detect(this.shortName, "loggedout", aDocument, null)) {
    this.logout();
  }
};


/*************************************************************************
 * Hi5Service: flockIPollingService Implementation
 *************************************************************************/

/**
 * @see flockIPollingService#refresh
 */
Hi5Service.prototype.refresh =
function Hi5Service_refresh(aUrn, aPollingListener) {
  this._logger.debug(".refresh('" + aUrn + "', ...)");
  var inst = this;
  var account = this.getAccount(aUrn);

  if (!account) {
    this._logger.error("can't refresh " + aUrn + ": not a valid account");
    aPollingListener.onError(null);
  }

  if (account.isAuthenticated()) {
    var pwKey = this.urn + ":" + account.getParam("accountId");
    var loginInfo = this._acUtils.getPassword(pwKey);

    var authListener = {
      onSuccess: function authListener_onSuccess(aSubject, aTopic) {
        inst._refreshAccount(account, aPollingListener);
      },
      onError: function authListener_onError(aFlockError, aTopic) {
        inst._acUtils.markAllAccountsAsLoggedOut(inst);
        aPollingListener.onError(aFlockError, aTopic);
      }
    };

    gApi.authenticate(loginInfo, authListener);
  } else {
    this._logger.debug("account is not logged in - skipping refresh");
    aPollingListener.onResult();
  }
};


/*************************************************************************
 * Hi5Service: flockIMediaWebService Implementation
 *************************************************************************/

// readonly attribute boolean flockIMediaWebService::supportsUsers
Hi5Service.prototype.supportsUsers = true;

/**
 * void decorateForMedia(in nsIDOMHTMLDocument aDocument
                         in flockIWebServiceAccount aAccount);
 * @see flockIMediaWebService#decorateForMedia
 */
Hi5Service.prototype.decorateForMedia =
function Hi5Service_decorateForMedia(aDocument, aAccount) {
  this._logger.debug(".decorateForMedia('" + aDocument.URL + "')");
  var inst = this;
  var mediaArr = [];

  function setupMedia() {
    if (!aDocument._flock_decorations) {
      aDocument._flock_decorations = {};
    }

    if (aDocument._flock_decorations.mediaArr) {
      aDocument._flock_decorations.mediaArr =
      aDocument._flock_decorations.mediaArr.concat(mediaArr);
    } else {
      aDocument._flock_decorations.mediaArr = mediaArr;
    }

    inst._obs.notifyObservers(aDocument, "media", "media:update");
  }

  var userId, userName, albumId, albumName;
  aDocument.QueryInterface(CI.nsIDOMHTMLDocument);
  var results = FlockSvcUtils.newResults();

  if (this._wd.detect(this.shortName, "person", aDocument, results)) {
    userId = results.getPropertyAsAString("userId");

    var getUserNameListener = {
      onSuccess: function gunl_onSuccess(aSubject, aTopic) {
        inst._logger.debug(arguments.callee.name);
        var userName = aSubject.entry.displayName;

        var media = {
          name: userName,
          query: "user:" + userId + "|username:" + userName,
          label: userName,
          favicon: inst.icon,
          service: inst.shortName
        };

        mediaArr.push(media);
        setupMedia();
      },
      onError: function gunl_onError(aFlockError, aTopic) {
        inst._logger.error(arguments.callee.name);
      }
    };

    gApi.openSocialCall("people/" + userId,
                        null,
                        null,
                        CI.flockIWebServiceAPI.GET,
                        getUserNameListener);
  } else if (this._wd.detect(this.shortName, "homepage", aDocument, results)) {
    if (!aAccount) {
      return;
    }
    userId = aAccount.getParam("accountId");
    userName = aAccount.getParam("name");

    var media = {
      name: userName,
      query: "user:" + userId + "|username:" + userName,
      label: userName,
      favicon: inst.icon,
      service: inst.shortName
    };

    mediaArr.push(media);
    setupMedia();
  } else if (this._wd.detect(this.shortName, "album", aDocument, results) ||
             this._wd.detect(this.shortName, "photo", aDocument, results))
  {
    userId = results.getPropertyAsAString("userId");
    var photoId;
    try {
      albumId = results.getPropertyAsAString("albumId");
    } catch (ex) {
      albumId = "";
    }

    try {
      photoId = results.getPropertyAsAString("photoId");
    } catch (ex) {
      photoId = "";
    }

    function getUserNameForAlbum() {
      var getUserNameForAlbumListener = {
        onSuccess: function gunfal_onSuccess(aSubject, aTopic) {
          inst._logger.debug(arguments.callee.name);
          userName = aSubject.entry.displayName;

          var media;
          if (photoId) {
            media = {
              name: userName,
              query: "user:" + userId + "|username:" + userName,
              label: userName,
              favicon: inst.icon,
              service: inst.shortName
            };
            mediaArr.push(media);
          }

          media = {
            name: albumId,
            query: "user:" + userId
                           + "|username:" + userName
                           + "|album:" + albumId
                           + "|albumlabel:" + albumName,
            label: albumName,
            favicon: inst.icon,
            service: inst.shortName
          };
          mediaArr.push(media);

          setupMedia();
        },
        onError: function gunfal_onError(aFlockError, aTopic) {
          inst._logger.error(arguments.callee.name);
        }
      };

      gApi.openSocialCall("people/" + userId,
                          null,
                          null,
                          CI.flockIWebServiceAPI.GET,
                          getUserNameForAlbumListener);
    }

    if (albumId) {
      var getAlbumListener = {
        onSuccess: function gal_onSuccess(aSubject, aTopic) {
          inst._logger.debug(arguments.callee.name);
          albumName = aSubject["atom.feed"]["atom.title"];
          getUserNameForAlbum();
        },
        onError: function gal_onError(aFlockError, aTopic) {
          inst._logger.error(arguments.callee.name);
        }
      }; // getAlbumListener

      gApi.call("feed/photos/user/" + userId + "/albumid/" + albumId,
                null,
                null,
                CI.flockIWebServiceAPI.GET,
                getAlbumListener);
    } else {
      var getAlbumsListener = {
        onSuccess: function gasl_onSuccess(aSubject, aTopic) {
          inst._logger.debug(arguments.callee.name);
          var albumsEnum = aSubject.QueryInterface(CI.nsISimpleEnumerator);
          if (albumsEnum.hasMoreElements()) {
            var defaultAlbum = albumsEnum.getNext()
                                         .QueryInterface(CI.flockIPhotoAlbum);
            albumId = defaultAlbum.id;
            albumName = defaultAlbum.title;
            getUserNameForAlbum();
          }
        },
        onError: function gasl_onError(aFlockError, aTopic) {
          inst._logger.error(arguments.callee.name);
        }
      };
      this.getAlbums(getAlbumsListener, userId);
    } // END else
  } // END else if
};

// DEFAULT: nsISimpleEnumerator enumerateChannels();

/**
 * void findByUsername(in flockIListener aListener, in AString aUsername);
 * @see flockIMediaWebService#findByUsername
 */
Hi5Service.prototype.findByUsername =
function Hi5Service_findByUsername(aListener, aUsername) {
  this._logger.debug(".findByUsername(..., '" + aUsername + "')");
  aListener.onError(null, aUsername, null);
};

/**
 * void getAlbums(in flockIListener aFlockListener, in AString aUsername);
 * @see flockIMediaWebService#getAlbums
 */
Hi5Service.prototype.getAlbums =
function Hi5Service_getAlbums(aFlockListener, aUsername) {
  this._logger.debug(".getAlbums(..., '" + aUsername + "')");
  var inst = this;

  var getAlbumsListener = {
    onSuccess: function gal_onSuccess(aSubject, aTopic) {
      inst._logger.debug(arguments.callee.name);
      var albums = inst._processAlbumResults(aSubject);
      aFlockListener.onSuccess(inst._createEnum(albums), "getAlbums");
    },
    onError: function gal_onError(aFlockError, aTopic) {
      inst._logger.error(arguments.callee.name);
      aFlockListener.onError(aFlockError, aTopic);
    }
  };

  gApi.call("feed/albumlist/" + aUsername,
            null,
            null,
            CI.flockIWebServiceAPI.GET,
            getAlbumsListener);
};

// DEFAULT: flockIMediaChannel getChannel(in AString aChannelId);
// DEFAULT: flockIMediaItemFormatter getMediaItemFormatter();

/**
 * void migrateAccount(in AString aId, in AString aUsername);
 * @see flockIMediaWebService#migrateAccount
 */
Hi5Service.prototype.migrateAccount =
function Hi5Service_migrateAccount(aId, aUsername) {
  this._logger.debug(".migrateAccount('" + aId + "', '" + aUsername + "')");
};

/**
 * void search(in flockIListener aListener, in AString aQuery,
 *             in long aCount, in AString aRequestId);
 * @see flockIMediaWebService#search
 */
Hi5Service.prototype.search =
function Hi5Service_search(aFlockListener, aQuery, aCount, aPage, aRequestId) {
  this._logger.debug(".search(..., '" + aQuery + "', " + aCount + ", "
                                      + aPage + ")");

  var inst = this;
  var query = new QueryHelper(aQuery);

  function _loadFromCache(aCount, aPage) {
    var cache = inst._cache.photos;
    var result = [];

    for (var i = (aPage - 1) * aCount; i < aPage * aCount; i++ ) {
      if (i == 0) {
        if (!cache["atom.link"]) {
          break;
        }

        // Get the first photo
        var first = CC["@flock.com/photo;1"].createInstance(CI.flockIMediaItem);
        first.init(CLASS_SHORT_NAME, flockMediaItemFormatter);
        first.thumbnail = cache["atom.link"][1]["@href"];
        first.midSizePhoto = cache["atom.link"][1]["@href"];
        first.largeSizePhoto = cache["atom.feed"]["atom.link"][1]["@href"];
        first.webPageUrl = cache["atom.link"][0]["@href"];
        first.uploadDate = ISO8601DateUtils.parse(cache["atom.published"])
                                           .getTime();
        first.id = first.uploadDate;
        first.title = cache["atom.summary"] ? cache["atom.summary"]
                                            : cache["atom.title"];
        first.userid = query.user;
        first.username = inst._cache.userName;
        first.icon = inst._cache.userAvatar;
        first.is_public = true;
        first.is_video = false;
        result.push(first);
      } else {
        if (!cache["atom.entry"]) {
          break;
        }

        var photos = cache["atom.entry"];
        var k = i - 1;
        if (k >= photos.length) {
          break;
        }
        var photo = photos[k];
        var media = CC["@flock.com/photo;1"].createInstance(CI.flockIMediaItem);
        media.init(CLASS_SHORT_NAME, flockMediaItemFormatter);
        media.thumbnail = photo["atom.link"][2]["@href"];
        media.midSizePhoto = photo["atom.link"][2]["@href"];
        media.largeSizePhoto = photo["atom.link"][0]["@href"];
        media.webPageUrl = photo["atom.link"][1]["@href"];
        media.uploadDate = ISO8601DateUtils.parse(photo["atom.published"])
                                           .getTime();
        // Since Hi5 doesn't return an ID for the photo, we will just use the
        // upload date.
        media.id = media.uploadDate;
        media.title = photo["atom.summary"] ? photo["atom.summary"]
                                            : photo["atom.title"];
        media.userid = query.user;
        media.username = inst._cache.userName;
        media.icon = inst._cache.userAvatar;
        media.is_public = true;
        media.is_video = false;
        if (inst._cache.isAlbum) {
          result.unshift(media);
        } else {
          result.push(media);
        }
      }
    } // END for

    aFlockListener.onSuccess(inst._createEnum(result), aRequestId);
  }

  aPage = aPage ? aPage : 1;
  if (aPage > 1) {
    _loadFromCache(aCount, aPage);
    return;
  }

  var getPhotosListener = {
    onSuccess: function gpl_onSuccess(aSubject, aTopic) {
      inst._logger.debug(arguments.callee.name);
      inst._cache.photos = aSubject;
      _loadFromCache(aCount, 1);
    },
    onError: function gpl_onError(aFlockError, aTopic) {
      inst._logger.error(arguments.callee.name);
      aFlockListener.onError(aFlockError, aTopic);
    }
  };

  var getUserListener = {
    onSuccess: function gul_onSuccess(aSubject, aTopic) {
      inst._logger.debug(arguments.callee.name);
      inst._cache.userName = aSubject.entry.displayName;
      inst._cache.userAvatar = aSubject.entry.thumbnailUrl;
      var actionUrl;
      if (query.album) {
        inst._cache.isAlbum = true;
        actionUrl = "feed/photos/user/" + query.user + "/albumid/" + query.album;
      } else {
        inst._cache.isAlbum = false;
        actionUrl = "feed/photos/user/" + query.user + "/albumid/@all";
      }
      gApi.call(actionUrl,
                null,
                null,
                CI.flockIWebServiceAPI.GET,
                getPhotosListener);
    },
    onError: function gul_onError(aFlockError, aTopic) {
      inst._logger.error(arguments.callee.name);
      aFlockListener.onError(aFlockError, aTopic);
    }
  };

  if (query.user) {
    gApi.openSocialCall("people/" + query.user,
                        null,
                        null,
                        CI.flockIWebServiceAPI.GET,
                        getUserListener);
  }
};

/**
 * boolean supportsSearch(in AString aQuery);
 * @see flockIMediaWebService#supportsSearch
 */
Hi5Service.prototype.supportsSearch =
function Hi5Service_supportsSearch(aQuery) {
  return false;
};


/*************************************************************************
 * Hi5Service: flockIMediaUploadWebService Implementation
 *************************************************************************/

/**
 * void getAccountStatus(in flockIListener aListener);
 *
 * @see flockIMediaWebService#getAccountStatus
 */
Hi5Service.prototype.getAccountStatus =
function Hi5Service_getAccountStatus(aFlockListener) {
  this._logger.debug(".getAccountStatus(aListener)");

  var result = CC["@mozilla.org/hash-property-bag;1"]
               .createInstance(CI.nsIWritablePropertyBag2);

  result.setPropertyAsAString("maxSpace", 100 * 5 * 1024 * 1024);
  result.setPropertyAsAString("usedSpace", 0);
  result.setPropertyAsAString("maxFileSize", 5 * 1024 * 1024);
  // This is a key, not something displayed to the user. Don't l10n this!
  result.setPropertyAsAString("usageUnits", "bytes");
  result.setPropertyAsBool("isPremium", true); // Hi5 has no "pro" level.

  aFlockListener.onSuccess(result, "");
};

// DEFAULT: void getAlbumsForUpload(in flockIListener aListener,
//                                  in AString aUsername);

/**
 * void createAlbum(in flockIListener aFlockListener,
                    in AString aAccountId,
                    in AString aAlbumName);
 * @see flockIMediaUploadWebService#createAlbum
 */
Hi5Service.prototype.createAlbum =
function Hi5Service_createAlbum(aFlockListener, aAccountId, aAlbumName) {
  this._logger.debug(".createAlbum(aListener, '" + aAlbumName + "')");
  var inst = this;

  var createAlbumListener = {
    onSuccess: function createAlbum_onSuccess(aSubject, aTopic) {
      var albums = inst._processAlbumResults(aSubject);
      for each (var album in albums) {
        if (album.title == aAlbumName) {
          aFlockListener.onSuccess(album, "success");
          break;
        }
      }
    },
    onError: function createAlbum_onError(aFlockError, aTopic) {
      inst._logger.debug(".createAlbum() error: " + aFlockError.errorString);
      aFlockListener.onError(aFlockError, null);
    }
  };

  // Create the required POST data
  var params = {
    "Hi5AuthToken": gApi.token,
    "name": aAlbumName
  };
  gApi.call("feed/albumlist/" + aAccountId,
            params,
            null,
            CI.flockIWebServiceAPI.POST,
            createAlbumListener);
};

/**
 * boolean supportsFeature(in AString aFeature);
 * @see flockIMediaUploadWebService#supportsFeature
 */
Hi5Service.prototype.supportsFeature =
function Hi5Service_supportsFeature(aFeature) {
  this._logger.debug(".supportsFeature('" + aFeature + "')");

  var supports = {
    albumCreation: true,
    contacts: false,
    description: true,
    filename: false,
    notes: false,
    privacy: false,
    tags: false,
    title: false
  };
  return supports[aFeature];
};

/**
 * void upload(in flockIPhotoUploadAPIListener aUploadListener,
 *             in flockIPhotoUpload aUpload,
 *             in AString aFilename);
 * @see flockIMediaUploadWebService#upload
 */
Hi5Service.prototype.upload =
function Hi5Service_upload(aUploadListener, aUpload, aFilename) {
  this._logger.debug(".upload(aUploadListener, aUpload, '" + aFilename + "')");

  var inst = this;
  var params = {};

  this._photoUploader = new FlockPhotoUploader();
  this._photoUploader.photoParam = "uploadfile";

  var userId = this.getAccount(this.getAuthenticatedAccount())
                   .getParam("accountId");
  var endPoint = "http://api.hi5.com/rest/feed/photos"
               + "/user/" + userId
               + "/albumid/" + aUpload.album
               + "?Hi5AuthToken=" + gApi.token;
  this._photoUploader.setEndpoint(endPoint);

  var uploadListener = {
    onResult: function upload_onResult(aResponseText) {
      var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
                   .createInstance(Components.interfaces.nsIDOMParser);
      var xml = parser.parseFromString(aResponseText, "text/xml");
      var photo = xml.getElementsByTagNameNS(ATOM_NS, "entry")[0];
      if (photo) {
        var media = CC["@flock.com/photo;1"].createInstance(CI.flockIMediaItem);
        media.init(CLASS_SHORT_NAME, flockMediaItemFormatter);
        var links = photo.getElementsByTagNameNS(ATOM_NS, "link");
        media.thumbnail = links[2].getAttribute("href");
        media.midSizePhoto = links[2].getAttribute("href");
        media.largeSizePhoto = links[0].getAttribute("href");
        media.webPageUrl = links[1].getAttribute("href");
        var published = photo.getElementsByTagNameNS(ATOM_NS, "published")[0]
                             .firstChild.nodeValue;
        media.uploadDate = ISO8601DateUtils.parse(published)
                                           .getTime();

        // Get the photoId
        var photoUrl = photo.getElementsByTagNameNS(ATOM_NS, "id")[0]
                            .firstChild.nodeValue;
        var tmp = photoUrl.split("/");
        var photoId = tmp[tmp.length - 1];
        media.id = photoId;

        media.title =  photo.getElementsByTagNameNS(ATOM_NS, "title")[0]
                            .firstChild.nodeValue;
        media.is_public = true;
        media.is_video = false;

        var addPhotoDescListener = {
          onSuccess: function addPhotoDesc_onSuccess(aSubject, aTopic) {
            aUploadListener.onUploadComplete(aUpload);
            aUploadListener.onUploadFinalized(aUpload, media);
          },
          onError: function addPhotoDesc_onError(aFlockError, aTopic) {
            inst._logger.debug(".addPhotoDesc() error: " + aFlockError.errorString);
            aUploadListener.onError(aFlockError);
          }
        };

        // The description is set with a separate API call.
        var params = {
          "Hi5AuthToken": gApi.token,
          "description": aUpload.description
        };
        var addPhotoDescUrl = "feed/photos/user/" + userId
                            + "/albumid/" + aUpload.album
                            + "/photoid/" + photoId;
        gApi.call(addPhotoDescUrl,
                  params,
                  null,
                  CI.flockIWebServiceAPI.POST,
                  addPhotoDescListener);
      } else {
        aUploadListener.onError(gApi.getError(null, null));
      }
    },
    onError: function upload_onError(aErrorCode) {
      this._logger.debug("uploadListener_onError");
      aUploadListener.onError(gApi.getError(aErrorCode, null));
    },
    onProgress: function upload_onProgress(aCurrentProgress) {
      aUploadListener.onProgress(aCurrentProgress);
    }
  };
  this._photoUploader.upload(uploadListener, aFilename, params, aUpload);
};


/*************************************************************************
 * Hi5Service: flockISocialWebService Implementation
 *************************************************************************/

// readonly attribute long maxStatusLength;
Hi5Service.prototype.maxStatusLength = 160;

// void markAllMediaSeen(in AString aPersonUrn);
Hi5Service.prototype.markAllMediaSeen =
function Hi5Service_markAllMediaSeen(aPersonUrn) {
  this._logger.debug(".markAllMediaSeen('" + aPersonUrn + "')");
};


/*************************************************************************
 * Hi5Service: flockIMediaEmbedWebService Implementation
 *************************************************************************/

Hi5Service.prototype.checkIsStreamUrl =
function Hi5Service_checkIsStreamUrl(aUrl) {
  if (this._wd.detectNoDOM(CLASS_SHORT_NAME, "isStreamUrl", "", aUrl, null)) {
    this._logger.debug("Checking if url is hi5 stream: YES: " + aUrl);
    return true;
  }
  this._logger.debug("Checking if url is hi5 stream: NO: " + aUrl);
  return false;
};

Hi5Service.prototype.getMediaQueryFromURL =
function Hi5Service_getMediaQueryFromURL(aUrl, aListener) {
  return false;
};


/**************************************************************************
 * Hi5Service: flockIRichContentDropHandler Implementation
 **************************************************************************/

/**
 * boolean handleDrop(in nsITransferable aFlavours,
 *                    in nsIDOMHTMLElement aTargetElement);
 * @see flockIRichContentDropHandler#handleDrop
 */
Hi5Service.prototype.handleDrop =
function Hi5Service_handleDrop(aFlavours, aTargetElement) {
  this._logger.debug(".handleDrop()");

  // Handle textarea drops
  if (aTargetElement instanceof CI.nsIDOMHTMLTextAreaElement) {
    var dropCallback = function flickr_dropCallback(aFlav) {
      var data = {};
      var len = {};
      aFlavours.getTransferData(aFlav, data, len);
      var caretPos = aTargetElement.selectionEnd;
      var currentValue = aTargetElement.value;

      // Add a trailing space so that we don't mangle the url
      var nextChar = currentValue.charAt(caretPos);
      var trailingSpace = ((nextChar == "") ||
                           (nextChar == " ") ||
                           (nextChar == "\n"))
                        ? ""
                        : " ";

      aTargetElement.focus();

      // We'll only add a breadcrumb if there isn't one already present
      var breadcrumb = "";
      var richDnDSvc = CC[FLOCK_RDNDS_CONTRACTID]
                       .getService(CI.flockIRichDNDService);
      if (!richDnDSvc.hasBreadcrumb(aTargetElement)) {
        breadcrumb = richDnDSvc.getBreadcrumb("plain");
      }

      aTargetElement.value = currentValue.substring(0, caretPos)
                           + data.value.QueryInterface(CI.nsISupportsString)
                                 .data.replace(/: /, ":\n")
                           + trailingSpace
                           + currentValue.substring(caretPos)
                           + breadcrumb;
    };

    return this._handleTextareaDrop(CLASS_SHORT_NAME,
                                    this._coopObj.domains,
                                    aTargetElement,
                                    dropCallback);
  }

  // Handle drops onto a rich text editor
  var richDnDSvc = CC[FLOCK_RDNDS_CONTRACTID]
                   .getService(CI.flockIRichDNDService);
  if (richDnDSvc.isInRichTextEditor(aTargetElement)) {
    // The rich text editor will most likely be in an iFrame, so there's no way
    // to get a direct link to the top document from the target element. We get
    // the document of the current tab instead. We've just dropped onto it, so
    // we know it exists!
    var win = CC["@mozilla.org/appshell/window-mediator;1"]
              .getService(CI.nsIWindowMediator)
              .getMostRecentWindow("navigator:browser");
    var doc = win.getBrowser().contentDocument;

    // Get the plain text data object
    var dataObj = {};
    var len = {};
    aFlavours.getTransferData("text/unicode", dataObj, len);

    // Get plain content from it. We'll also put a line break between the URL
    // and the title.
    var content = dataObj.value.QueryInterface(CI.nsISupportsString)
                         .data.replace(/: /, ":<br />");
    // Add breadcrumb to message body
    var breadcrumb = CC[FLOCK_RDNDS_CONTRACTID]
                     .getService(CI.flockIRichDNDService)
                     .getBreadcrumb("plain");
    if (breadcrumb) {
      content += breadcrumb.replace(/\x0A/g, "<br />");
    }

    // Add the content to the editor
    var xpathquery = this._wd.getString(CLASS_SHORT_NAME, "scrapbookCommentXPath", "");
    var formItems = doc.evaluate(xpathquery, doc, null,
                                 CI.nsIDOMXPathResult.ANY_TYPE, null);
    if (formItems) {
      var formItem = formItems.iterateNext();
      var iFrameBody = formItem.contentDocument.body;
      iFrameBody.innerHTML = content;
    }
    return true;
  }

  // Default handling otherwise
  return false;
};


/*************************************************************************
 * Component: Hi5Account
 *************************************************************************/
function Hi5Account() {
  FlockSvcUtils.getLogger(this);
  this._logger.init("Hi5Account");

  FlockSvcUtils.getACUtils(this);
  FlockSvcUtils.getCoop(this);

  this._WebDetective = CC["@flock.com/web-detective;1"]
                       .getService(CI.flockIWebDetective);

  // Convenience variable.
  this._wd = this._WebDetective;

  var wsa = FlockSvcUtils.flockIWebServiceAccount;
  wsa.addDefaultMethod(this, "getAllCustomParams");
  wsa.addDefaultMethod(this, "getCustomParam");
  wsa.addDefaultMethod(this, "getParam");
  wsa.addDefaultMethod(this, "getService");
  wsa.addDefaultMethod(this, "isAuthenticated");
  wsa.addDefaultMethod(this, "logout");
  wsa.addDefaultMethod(this, "setCustomParam");
  wsa.addDefaultMethod(this, "setParam");

  var sa = FlockSvcUtils.flockISocialAccount;
  sa.addDefaultMethod(this, "enumerateFriends");
  sa.addDefaultMethod(this, "formatFriendActivityForDisplay");
  sa.addDefaultMethod(this, "getFriendCount");
  sa.addDefaultMethod(this, "getInviteFriendsURL");
  sa.addDefaultMethod(this, "getFormattedFriendUpdateType");
}

// Use genericComponent() for the goodness it provides (QI, nsIClassInfo, etc),
// but do NOT add this component to the list of constructors passed
// to FlockXPCOMUtils.genericModule().
Hi5Account.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME + " Account",
  "",
  "",
  Hi5Account,
  0,
  [
    CI.flockIWebServiceAccount,
    CI.flockIMediaAccount,
    CI.flockIMediaUploadAccount,
    CI.flockISocialAccount
  ]
);


/*************************************************************************
 * Hi5Account: flockIWebServiceAccount Implementation
 *************************************************************************/

// readonly attribute AString urn;
Hi5Account.prototype.urn = "";

// DEFAULT: nsIPropertyBag getAllCustomParams();
// DEFAULT: nsIVariant getCustomParam(in AString aParamName);
// DEFAULT: nsIVariant getParam(in AString aParamName);
// DEFAULT: flockILoginWebService getService();
// DEFAULT: boolean isAuthenticated();

/**
 * void keep();
 * @see flockILoginWebService#keep
 */
// TODO: should we be using the default implementation?
Hi5Account.prototype.keep =
function Hi5Account_keep() {
  this._logger.debug(".keep()");
  this._coop.get(this.urn).isTransient = false;
  this._acUtils.makeTempPasswordPermanent(this.urn.replace("account:", ""));
};

/**
 * void login(in flockIListener aListener);
 * @see flockIWebServiceAccount#login
 */
Hi5Account.prototype.login =
function Hi5Account_login(aFlockListener) {
  this._logger.debug(".login()");
  this._acUtils.ensureOnlyAuthenticatedAccount(this);
  // Force refresh on login is done by the people sidebar
  if (aFlockListener) {
    aFlockListener.onSuccess(this, "login");
  }
};

// DEFAULT: void logout(in flockIListener aListener);
// DEFAULT: void setCustomParam(in AString aParamName, in nsIVariant aValue);
// DEFAULT: void setParam(in AString aParamName, in nsIVariant aValue);


/*************************************************************************
 * Hi5Account: flockISocialAccount Implementation
 *************************************************************************/

// readonly attribute boolean hasFriendActions;
Hi5Account.prototype.hasFriendActions = true;

// readonly attribute boolean isMeStatusSupported;
Hi5Account.prototype.isMeStatusSupported = true;

// readonly attribute boolean isFriendStatusSupported;
Hi5Account.prototype.isFriendStatusSupported = true;

// readonly attribute boolean isStatusEditable;
Hi5Account.prototype.isStatusEditable = true;

// readonly attribute boolean isPostLinkSupported;
Hi5Account.prototype.isPostLinkSupported = true;

// readonly attribute boolean isMyMediaFavoritesSupported;
Hi5Account.prototype.isMyMediaFavoritesSupported = false;

// DEFAULT: nsISimpleEnumerator enumerateFriends();
// DEFAULT: AString formatFriendActivityForDisplay(in AString aFriendActivityUrn);

/**
 * AString formatStatusForDisplay(in AString aStatusMessage);
 * @see flockISocialAccount#formatStatusForDisplay
 */
Hi5Account.prototype.formatStatusForDisplay =
function Hi5Account_formatStatusForDisplay(aStatusMessage) {
  return aStatusMessage;
};

/**
 * void setStatus(in AString aStatusMessage, in flockIListener aFlockListener);
 * @see flockISocialAccount#setStatus
 */

Hi5Account.prototype.setStatus =
function Hi5Account_setStatus(aStatusMessage, aFlockListener) {
  this._logger.debug(".setStatus('" + aStatusMessage + "', ...)");

  var params = {
    "Hi5AuthToken": gApi.token,
    "userId": this.getParam("accountId"),
    "status": aStatusMessage
  };

  var inst = this;
  var setStatusListener = {
    onSuccess: function ssl_onSuccess(aSubject, aTopic) {
      inst._logger.debug("phan5: ssl_onSuccess()");
      inst.setParam("statusMessage", aStatusMessage);

      // Save the current time in seconds
      var now = new Date().getTime();
      now = Math.round(now / 1000);
      inst.setParam("lastStatusMessageUpdateDate", now);

      aFlockListener.onSuccess(inst, "setStatus");
    },   
    onError: function ssl_onError(aFlockError, aTopic) {
      inst._logger.debug("phan5: ssl_onError()");
      aFlockListener.onError(aFlockError, "setStatus");
    }    
  };

  gApi.call("status/setStatus",
            params,
            null,
            CI.flockIWebServiceAPI.POST,
            setStatusListener);
};

/**
 * AString getEditableStatus();
 * @see flockISocialAccount#getEditableStatus
 */
Hi5Account.prototype.getEditableStatus =
function Hi5Account_getEditableStatus() {
  this._logger.debug(".getEditableStatus()");

  var message = this._coop.get(this.urn).statusMessage;
  return this.formatStatusForDisplay(message);
};

/**
 * AString getFriendActions(in AString aFriendUrn);
 * @see flockISocialAccount#getFriendActions
 */
Hi5Account.prototype.getFriendActions =
function Hi5Account_getFriendActions(aFriendUrn) {
  var actionNames = ["friendMessage",
                     "friendComment",
                     "friendGiveFive",
                     "friendViewProfile",
                     "friendShareFlock"];

  var bundle = this.getService().getStringBundle();

  var actions = [];
  var coopFriend = this._coop.get(aFriendUrn);
  if (coopFriend) {
    for each (var actionName in actionNames) {
      actions.push({
        label: bundle.GetStringFromName("flock." + CLASS_SHORT_NAME
                                        + ".actions." + actionName),
        class: actionName,
        spec: this._wd.getString(CLASS_SHORT_NAME, actionName, "")
                  .replace(/%friendid%/g, coopFriend.accountId)
      });
    }
  }
  return JSON.toString(actions);
};

Hi5Account.prototype.shareFlock =
function Hi5Account_shareFlock(aFriendURN) {
  this._logger.debug(".shareFlock()");

  // Get friend
  var friend = this._coop.get(aFriendURN);
  if (friend) {
    // Get "Share Flock" message text
    var sbs = CC["@mozilla.org/intl/stringbundle;1"]
              .getService(CI.nsIStringBundleService);
    var bundle = sbs.createBundle(SERVICES_PROPERTIES_FILE);
    var subject = bundle.GetStringFromName(SERVICES_SHARE_FLOCK_SUBJECT);
    var body = "";
    try {
      body = bundle.GetStringFromName(SERVICES_SHARE_FLOCK_MESSAGE + "0");
      for (var i = 1; ; i++) {
        body += "\n";
        body += bundle.GetStringFromName(SERVICES_SHARE_FLOCK_MESSAGE + i);
      }
    } catch (ex) {
      // Ignore -- we've hit the end of our message lines
    }
    // Build the URL and send it to the friend
    var message = {
      subject: subject,
      body: body.replace("%SERVICE", CLASS_TITLE)
    };
    this._shareMessageWithFriend(friend.accountId, message, false);
  }
};

// void friendAction(in AString aAction, in AString aFriendURN, in AString..
Hi5Account.prototype.friendAction =
function Hi5Account_friendAction(aAction, aFriendURN, aActivityURN) {
  this._logger.error(".friendAction(): NS_ERROR_NOT_IMPLEMENTED");
  throw CR.NS_ERROR_NOT_IMPLEMENTED;
};

// DEFAULT: long getFriendCount();

/**
 * AString getMeNotifications();
 * @see flockISocialAccount#getMeNotifications
 */
Hi5Account.prototype.getMeNotifications =
function Hi5Account_getMeNotifications() {
  this._logger.info(".getMeNotifications()");

  var sbs = CC["@mozilla.org/intl/stringbundle;1"]
            .getService(CI.nsIStringBundleService);
  var bundle = sbs.createBundle(HI5_PROPERTIES);

  var noties = [];
  var inst = this;
  function _addNotie(aType, aCount) {
    var stringName = "flock.hi5.notification."
                   + aType + "."
                   + ((parseInt(aCount, 10) <= 0) ? "none" : "some");
    noties.push({
      class: aType,
      tooltip: bundle.GetStringFromName(stringName),
      metricsName: aType,
      count: (aCount) ? aCount : 0,
      URL: inst._WebDetective
               .getString("hi5", aType + "_URL", "")
               .replace("%userid%", c_acct.accountId)
    });
  }

  var c_acct = this._coop.get(this.urn);
  var c_params = this.getAllCustomParams();
  _addNotie("meMessages", c_acct.accountMessages);
  _addNotie("meFriendRequests", c_params.getProperty("friendRequests"));
  _addNotie("meProfileComments", c_params.getProperty("profileComments"));
  _addNotie("mePhotoComments", c_params.getProperty("photoComments"));
  var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
  return nsJSON.encode(noties);
};

/**
 * AString shareWithFriend(in AString aFriendUrn, in nsITransferable aTransferable);
 * @see flockISocialAccount#shareWithFriend
 */
Hi5Account.prototype.shareWithFriend =
function Hi5Account_shareWithFriend(aFriendUrn, aTransferable) {
  // Get friend ID
  var friendId = null;
  var friend = this._coop.get(aFriendUrn);
  if (friend) {
    friendId = friend.accountId;
  }

  // Get the message to share
  var message = {};
  if (aTransferable) {
    // Flavours we want to support, in order of preference
    var flavours = ["text/x-flock-media",
                    "text/x-moz-url",
                    "text/unicode"];
    // Build the message from the transferable
    message = CC["@flock.com/rich-dnd-service;1"]
              .getService(CI.flockIRichDNDService)
              .getMessageFromTransferable(aTransferable,
                                          flavours.length,
                                          flavours);
  } else {
    // Share the current page
    var win = CC["@mozilla.org/appshell/window-mediator;1"]
              .getService(CI.nsIWindowMediator)
              .getMostRecentWindow("navigator:browser");
    if (win) {
      message.body = win.gBrowser.currentURI.spec;
      message.subject = win.gBrowser.contentTitle;
    }
  }

  // Did we end up with a valid friend and message to share?
  if (friendId && message.body) {
    this._shareMessageWithFriend(friendId, message, true);
  }
};

/**
 * AString getProfileURLForFriend(in AString aFriendUrn);
 * @see flockISocialAccount#getProfileURLForFriend
 */
Hi5Account.prototype.getProfileURLForFriend =
function Hi5Account_getProfileURLForFriend(aFriendUrn) {
  this._logger.debug(".getProfileURLForFriend('" + aFriendUrn + "')");

  var url = "";
  var c_friend = this._coop.get(aFriendUrn);
  if (c_friend) {
    var webDetective = CC["@flock.com/web-detective;1"]
                       .getService(CI.flockIWebDetective);
    url = webDetective.getString(CLASS_SHORT_NAME, "profileURL", "")
                      .replace("%userid%", c_friend.accountId);
  }

  return url;
};

/**
 * AString postlink(in nsITransferable aTransferable);
 * @see flockISocialAccount#postlink
 */
Hi5Account.prototype.postLink =
function Hi5Account_postlink(aTransferable) {
  this._logger.debug(arguments.callee.name);

  var plContent;
  if (aTransferable) {
    var flavors = ["text/x-flock-media",
                   "text/x-moz-url",
                   "text/unicode"];
    // Build the message from the transferable
    var message = CC["@flock.com/rich-dnd-service;1"]
                  .getService(CI.flockIRichDNDService)
                  .getMessageFromTransferable(aTransferable,
                                              flavors.length,
                                              flavors);
    plContent = message.subject
              ? message.subject + "<br />" + message.body
              : message.body;
  } else {
    // Share the current page
    var win = CC["@mozilla.org/appshell/window-mediator;1"]
              .getService(CI.nsIWindowMediator)
              .getMostRecentWindow("navigator:browser");
    if (win) {
      var url = win.gBrowser.currentURI.spec;
      var title = win.gBrowser.contentTitle ? win.gBrowser.contentTitle : url;
      plContent = title ? title + "<br />" + url : url;
    }
  }

  this._doPostLink(plContent);
};

/**
 * void markAllMeNotificationsSeen(in AString aType);
 * @see flockISocialAccount#markAllMediaNotificationsSeen
 */
Hi5Account.prototype.markAllMeNotificationsSeen =
function Hi5Account_markAllMeNotificationsSeen(aType) {
  this._logger.debug(".markAllMeNotificationsSeen('" + aType + "')");

  switch (aType) {
    case "meMessages":
      this.setParam("accountMessages", 0);
      break;
    case "meFriendRequests":
      this.setCustomParam("friendRequests", 0);
      break;
    case "meProfileComments":
      this.setCustomParam("profileComments", 0);
      break;
    case "mePhotoComments":
      this.setCustomParam("photoComments", 0);
      break;
    default:
      break;
  }
};


/*************************************************************************
 * Hi5Account: Private Data and Functions
 *************************************************************************/

Hi5Account.prototype._shareMessageWithFriend =
function Hi5Account__shareMessageWithFriend(aFriendId, aMessage, aBreadCrumb) {
  var inst = this;

  // Get message URL
  var url = this._wd.getString(CLASS_SHORT_NAME, "sendMessageURL", "");
  url = url.replace(/%friendid%/g, aFriendId);

  // Open it in a new tab
  var win = CC["@mozilla.org/appshell/window-mediator;1"]
            .getService(CI.nsIWindowMediator)
            .getMostRecentWindow("navigator:browser");
  if (win) {
    var browser = win.getBrowser();
    var newTab = browser.loadOneTab(url, null, null, null, false, false);
    var contentWindow = newTab.linkedBrowser.docShell
                              .QueryInterface(CI.nsIInterfaceRequestor)
                              .getInterface(CI.nsIDOMWindow);

    function insertContent(aWebDicString, aMessageContent) {
      var xpathquery = inst._wd.getString(CLASS_SHORT_NAME, aWebDicString, "");
      var doc = contentWindow.document;
      var formItems = doc.evaluate(xpathquery, doc, null,
                                   CI.nsIDOMXPathResult.ANY_TYPE, null);
      if (formItems) {
        var formItem = formItems.iterateNext();
        formItem.value = aMessageContent;
      }
    }

    // Wait for the tab to finish loading before executing our callback to fill
    // in the message information
    var obs = CC["@mozilla.org/observer-service;1"]
              .getService(CI.nsIObserverService);
    var observer = {
      observe: function ma__smwf_observer(aContent, aTopic, aOwnsWeak) {
        if (aContent == contentWindow) {
          obs.removeObserver(this, aTopic);
          insertContent("messageSubjectXPath", aMessage.subject);
          // Add breadcrumb to message body
          var breadcrumb = CC[FLOCK_RDNDS_CONTRACTID]
                           .getService(CI.flockIRichDNDService)
                           .getBreadcrumb("plain");
          var body = aMessage.body;
          if (aBreadCrumb && breadcrumb) {
            body += breadcrumb;
          }
          insertContent("messageBodyXPath", body);
        }
      }
    };

    obs.addObserver(observer, "EndDocumentLoad", false);
  }
};

Hi5Account.prototype._doPostLink =
function Hi5Account__doPostLink(aMessage) {
  var inst = this;

  // Get message URL
  var url = this._wd.getString(CLASS_SHORT_NAME, "postLinkURL", "");
  url = url.replace(/%userid%/, this.getParam("accountId"));

  // Open it in a new tab
  var win = CC["@mozilla.org/appshell/window-mediator;1"]
            .getService(CI.nsIWindowMediator)
            .getMostRecentWindow("navigator:browser");
  if (win) {
    var browser = win.getBrowser();
    var newTab = browser.loadOneTab(url, null, null, null, false, false);
    var contentWindow = newTab.linkedBrowser.docShell
                              .QueryInterface(CI.nsIInterfaceRequestor)
                              .getInterface(CI.nsIDOMWindow);

    function insertContent(aWebDicString, aMessageContent) {
      var xpathquery = inst._wd.getString(CLASS_SHORT_NAME, aWebDicString, "");
      var doc = contentWindow.document;
      var formItems = doc.evaluate(xpathquery, doc, null,
                                   CI.nsIDOMXPathResult.ANY_TYPE, null);
      if (formItems) {
        var formItem = formItems.iterateNext();
        var iFrameBody = formItem.contentDocument.body;
        iFrameBody.innerHTML = aMessageContent;
      }
    }

    // Wait for the tab to finish loading before executing our callback to fill
    // in the message information
    var obs = CC["@mozilla.org/observer-service;1"]
              .getService(CI.nsIObserverService);
    var observer = {
      observe: function ma__smwf_observer(aContent, aTopic, aOwnsWeak) {
        if (aContent == contentWindow) {
          obs.removeObserver(this, aTopic);

          // Add breadcrumb to message body
          var breadcrumb = CC[FLOCK_RDNDS_CONTRACTID]
                           .getService(CI.flockIRichDNDService)
                           .getBreadcrumb("plain");
          var body = aMessage;
          if (breadcrumb) {
            body += breadcrumb.replace(/\x0A/g, "<br />");
          }

          insertContent("scrapbookCommentXPath", body);
        }
      }
    };

    obs.addObserver(observer, "EndDocumentLoad", false);
  }
};

/*************************************************************************
 * XPCOM Support - Module Construction
 *************************************************************************/

// Create array of components.
var componentsArray = [Hi5Service];

// Generate a module for XPCOM to find.
var NSGetModule = FlockXPCOMUtils.generateNSGetModule(MODULE_NAME,
                                                      componentsArray);
