Bug 953992 - new buddy list backend.

This commit is contained in:
Florian Quèze 2010-10-25 02:30:53 +02:00
Родитель c94c279962
Коммит 16efff4710
11 изменённых файлов: 1575 добавлений и 296 удалений

Просмотреть файл

@ -354,8 +354,7 @@ Logger.prototype = {
["new-conversation", "new-text",
"conversation-closed", "conversation-left-chat",
"account-connected", "account-disconnected",
"buddy-signed-on", "buddy-signed-off",
"buddy-away", "buddy-idle"].forEach(function(aEvent) {
"account-buddy-status-changed"].forEach(function(aEvent) {
obs.addObserver(this, aEvent, false);
}, this);
break;
@ -381,7 +380,7 @@ Logger.prototype = {
" signed off");
closeLogForAccount(aSubject);
break;
default:
case "account-buddy-status-changed":
let status;
if (!aSubject.online)
status = "Offline";
@ -394,13 +393,15 @@ Logger.prototype = {
else
status = "Unavailable";
let statusText = aSubject.status;
let statusText = aSubject.statusText;
if (statusText)
status += " (\"" + statusText + "\")";
let name = aSubject.buddyName;
let nameText = (aSubject.buddyAlias || name) + " (" + name + ")";
let nameText = aSubject.displayName + " (" + aSubject.userName + ")";
getLogForAccount(aSubject.account).logEvent(nameText + " is now " + status);
break;
default:
throw "Unexpected notification " + aTopic;
}
},

Просмотреть файл

@ -40,8 +40,8 @@ var addBuddy = {
onload: function ab_onload() {
this.pcs = Components.classes["@instantbird.org/purple/core;1"]
.getService(Ci.purpleICoreService);
this.pts = Components.classes["@instantbird.org/purple/tags;1"]
.getService(Ci.purpleITagsService);
this.pts = Components.classes["@instantbird.org/purple/tags-service;1"]
.getService(Ci.imITagsService);
this.buildAccountList();
this.buildTagList();
},

Просмотреть файл

@ -35,8 +35,9 @@
*
* ***** END LICENSE BLOCK ***** */
const events = ["buddy-signed-on",
"buddy-added",
const events = ["contact-availability-changed",
"contact-added",
"contact-moved",
"account-disconnected",
"status-changed",
"purple-quit"];
@ -88,13 +89,14 @@ buddyListContextMenu.prototype = {
Components.classes["@mozilla.org/intl/stringbundle;1"]
.getService(Components.interfaces.nsIStringBundleService)
.createBundle("chrome://instantbird/locale/instantbird.properties");
let buddy = this.target.buddy;
let displayName = buddy.alias || buddy.name;
let contact = this.target.contact;
let displayName = contact.displayName;
let promptTitle = bundle.formatStringFromName("buddy.deletePrompt.title",
[displayName], 1);
if (displayName != buddy.name)
displayName += " (" + buddy.name + ")";
let proto = buddy.getAccount(0).protocol.name;
let userName = contact.preferredBuddy.userName;
if (displayName != userName)
displayName += " (" + userName + ")";
let proto = contact.preferredBuddy.protocol.name; // FIXME build a list
let promptMessage = bundle.formatStringFromName("buddy.deletePrompt.message",
[displayName, proto], 2);
let deleteButton = bundle.GetStringFromName("buddy.deletePrompt.button");
@ -117,8 +119,8 @@ buddyListContextMenu.prototype = {
popup.removeChild(item);
let groupId = this.target.group.groupId;
let pts = Components.classes["@instantbird.org/purple/tags;1"]
.getService(Ci.purpleITagsService);
let pts = Components.classes["@instantbird.org/purple/tags-service;1"]
.getService(Ci.imITagsService);
let sortFunction = function (a, b) {
let [a, b] = [a.name.toLowerCase(), b.name.toLowerCase()];
@ -157,8 +159,8 @@ buddyListContextMenu.prototype = {
{value: false}) || !name.value)
return; // the user canceled
let pts = Components.classes["@instantbird.org/purple/tags;1"]
.getService(Ci.purpleITagsService);
let pts = Components.classes["@instantbird.org/purple/tags-service;1"]
.getService(Ci.imITagsService);
let tag = pts.getTagByName(name.value) || pts.createTag(name.value);
this.target.moveTo(tag.id);
},
@ -207,21 +209,22 @@ var buddyList = {
else
item.removeAttribute("checked");
let pts = Components.classes["@instantbird.org/purple/tags;1"]
.getService(Ci.purpleITagsService);
let pts = Components.classes["@instantbird.org/purple/tags-service;1"]
.getService(Ci.imITagsService);
let blistBox = document.getElementById("buddylistbox");
pts.getTags().forEach(function (aTag) {
let elt = document.getElementById("group" + aTag.id);
if (!elt && showOffline) {
if (elt)
elt.showOffline = showOffline;
else if (showOffline) {
elt = document.createElement("group");
blistBox.appendChild(elt);
elt._showOffline = true;
if (!elt.build(aTag))
blistBox.removeChild(elt);
}
if (elt)
elt.showOffline = showOffline;
});
return;
}
if (aTopic == "status-changed") {
@ -237,17 +240,24 @@ var buddyList = {
return;
}
var pab = aSubject.QueryInterface(Ci.purpleIAccountBuddy);
var group = pab.tag;
var groupId = "group" + group.id;
if ((aTopic == "buddy-signed-on" ||
(aTopic == "buddy-added" && (this._showOffline || pab.online))) &&
!document.getElementById(groupId)) {
let groupElt = document.createElement("group");
document.getElementById("buddylistbox").appendChild(groupElt);
if (this._showOffline)
groupElt._showOffline = true;
groupElt.build(group);
// aSubject is an imIContact
if (aSubject.online || this._showOffline) {
aSubject.getTags().forEach(function (aTag) {
if (!document.getElementById("group" + aTag.id)) {
let groupElt = document.createElement("group");
let blistBox = document.getElementById("buddylistbox");
blistBox.appendChild(groupElt);
if (this._showOffline)
groupElt._showOffline = true;
if (!groupElt.build(aTag)) {
// Broken group or notification?
// This should never happen as there will always be at least
// one contact shown.
// (We test the aSubject.online || this._showOffline to ensure it.)
blistBox.removeChild(groupElt);
}
}
}, this);
}
},
@ -507,8 +517,8 @@ var buddyList = {
.setAttribute("checked", "true");
}
let pts = Components.classes["@instantbird.org/purple/tags;1"]
.getService(Ci.purpleITagsService);
let pts = Components.classes["@instantbird.org/purple/tags-service;1"]
.getService(Ci.imITagsService);
let blistBox = document.getElementById("buddylistbox");
pts.getTags().forEach(function (aTag) {
let groupElt = document.createElement("group");

Просмотреть файл

@ -54,7 +54,16 @@
anonid="displayname" class="buddyDisplayName"
xbl:inherits="value=displayname"/>
</content>
<implementation>
<implementation implements="nsIObserver">
<destructor>
<![CDATA[
if (this.contact) {
this.contact.removeObserver(this);
delete this.contact;
}
]]>
</destructor>
<!-- delay in milliseconds before starting the fade out animation -->
<field name="animationDelay">1000</field>
@ -63,22 +72,16 @@
<field name="animationInterval">20</field>
<method name="build">
<parameter name="aBuddy"/>
<parameter name="aContact"/>
<parameter name="aGroup"/>
<body>
<![CDATA[
this.group = aGroup;
this.buddy = aBuddy;
this.buddyId = aBuddy.id; // used by group.xml
this.name = aBuddy.name;
this.setAttribute("displayname", aBuddy.alias || this.name);
//this.setAttribute("id", "buddy" + this.buddyId);
this.setAttribute("iconPrpl",
aBuddy.getAccount(0).protocol.iconBaseURI + "icon.png");
this.accounts = { };
this.accountsCount = 0;
// We don't call this.update yet because this.accounts is empty
this.contact = aContact;
this.update();
this.contact.addObserver(this);
#ifndef WINCE
// Don't do the animation if inside a closed group
if (this.hasAttribute("collapsed"))
return;
@ -91,76 +94,56 @@
</body>
</method>
<property name="offline">
<getter>
<![CDATA[
for (var id in this.accounts)
if (this.accounts[id].online)
return false;
return true;
]]>
</getter>
</property>
<!-- returns true if no account remain and the buddy should be removed from the list -->
<method name="removeOfflineAccounts">
<!-- nsIObserver implementation -->
<method name="observe">
<parameter name="aSubject"/>
<parameter name="aTopic"/>
<parameter name="aData"/>
<body>
<![CDATA[
let offlineAccounts = [];
for (var id in this.accounts)
if (!this.accounts[id].online)
offlineAccounts.push(id);
if (offlineAccounts.length == this.accountsCount)
return true;
offlineAccounts.forEach(function (aId) { delete this.accounts[aId]; }, this);
this.accountsCount -= offlineAccounts.length;
<![CDATA[
if (aTopic == "contact-preferred-buddy-changed" ||
aTopic == "contact-display-name-changed" ||
aTopic == "contact-availability-changed") {
this.update();
return false;
]]>
return;
}
if (aTopic == "contact-signed-on")
this.cancelRemoveNode();
else if (aTopic == "contact-removed" ||
(aTopic == "contact-moved-out" && aSubject.id == this.group.tag.id) ||
(aTopic == "contact-signed-off" && !this.group.showOffline))
this.removeNode();
]]>
</body>
</method>
<method name="update">
<body>
<![CDATA[
var accounts = [];
for (var id in this.accounts)
accounts.push(this.accounts[id]);
this.setAttribute("displayname", this.contact.displayName);
let status;
if (accounts.every(function(pab) !pab.online))
status = "offline";
else if (accounts.every(function(pab) pab.idle))
status = "idle";
else if (accounts.every(function(pab) pab.mobile))
status = "mobile";
else if (accounts.every(function(pab) !pab.available))
status = "away";
else
status = "available";
this.setAttribute("status", status);
let accountsWithAlias = accounts.filter(function(pab) pab.buddyAlias);
let alias;
if (accountsWithAlias.length)
alias = accountsWithAlias[0].buddyAlias;
this.setAttribute("displayname", alias || this.name);
const imIStatusInfo = Components.interfaces.imIStatusInfo;
let statusNames = {};
statusNames[imIStatusInfo.STATUS_UNKNOWN] = "offline"; // Should not happen
statusNames[imIStatusInfo.STATUS_OFFLINE] = "offline";
statusNames[imIStatusInfo.STATUS_MOBILE] = "mobile";
statusNames[imIStatusInfo.STATUS_IDLE] = "idle";
statusNames[imIStatusInfo.STATUS_AWAY] = "away";
//XXX should UNAVAILABLE (= busy) make a difference?
statusNames[imIStatusInfo.STATUS_UNAVAILABLE] = "away";
statusNames[imIStatusInfo.STATUS_AVAILABLE] = "available";
this.setAttribute("status", statusNames[this.contact.statusType]);
let proto = this.contact.preferredBuddy.protocol;
this.setAttribute("iconPrpl", proto.iconBaseURI + "icon.png");
]]>
</body>
</method>
<method name="addAccount">
<parameter name="aPab"/>
<method name="cancelRemoveNode">
<body>
<![CDATA[
var id = aPab.account.id;
if (id in this.accounts)
return;
this.accounts[id] = aPab;
++this.accountsCount;
this.update();
if (this.hasAttribute("removing")) {
this.removeAttribute("removing");
this.removeAttribute("collapsing");
@ -180,40 +163,31 @@
</body>
</method>
<method name="removeAccount">
<parameter name="aPab"/>
<method name="finishRemoveNode">
<body>
<![CDATA[
var id = aPab.account.id;
if (!(id in this.accounts))
return; // this used to throw, but this seems to be harmless
//throw "This buddy (" + aPab.buddy.name +
") has not been added for account " + aPab.account.name;
this.contact.removeObserver(this);
this.group.removeContact(this);
]]>
</body>
</method>
delete this.accounts[id];
--this.accountsCount;
if (this.accountsCount) {
this.update();
return;
}
// No account left, this node is now useless, remove it
this.removing = true;
<method name="removeNode">
<body>
<![CDATA[
if (this.hasAttribute("expanding")) {
// We are still doing the expand animation!
clearInterval(this.animInterval);
this.setAttribute("collapsing", "true");
this.removeAttribute("expanding", "true");
this.animInterval = setInterval(this._animateCollapse, this.animationInterval, this);
this.group.removeBuddy(this);
this.finishRemoveNode();
}
else if (this.hasAttribute("collapsed"))
// If the buddy is not visible, remove it immediately (without animation)
this.group.removeBuddy(this);
this.finishRemoveNode();
else
this.animTimeout = setTimeout(this._startAnimation, this.animationDelay, this);
this.setAttribute("status", "offline");
this.setAttribute("removing", "true");
]]>
</body>
@ -249,7 +223,7 @@
<body>
<![CDATA[
if (aSave) {
this.buddy.alias =
this.contact.alias =
document.getAnonymousElementByAttribute(this, "anonid", "displayname").value;
}
this.removeAttribute("aliasing");
@ -268,14 +242,9 @@
if (currentGroupId == aGroupId)
return;
let pts = Components.classes["@instantbird.org/purple/tags;1"]
.getService(Ci.purpleITagsService);
let newGroup = pts.getTagById(aGroupId);
for (var id in this.accounts) {
let pab = this.accounts[id];
if (pab.tag.id == currentGroupId)
pab.tag = newGroup;
}
let pts = Components.classes["@instantbird.org/purple/tags-service;1"]
.getService(Ci.imITagsService);
this.contact.move(this.group.tag, pts.getTagById(aGroupId));
]]>
</body>
</method>
@ -283,12 +252,7 @@
<method name="remove">
<body>
<![CDATA[
let currentGroupId = this.group.groupId;
for (var id in this.accounts) {
let pab = this.accounts[id];
if (pab.tag.id == currentGroupId)
pab.remove();
}
this.contact.remove();
]]>
</body>
</method>
@ -318,7 +282,7 @@
aThis.style.height = (aThis.animHeight = aThis.clientHeight) + "px";
aThis.setAttribute("collapsing", "true");
aThis.animInterval = setInterval(aThis._animateCollapse, this.animationInterval, aThis);
aThis.group.removeBuddy(aThis);
aThis.finishRemoveNode();
}
]]>
</body>
@ -363,10 +327,7 @@
<method name="canOpenConversation">
<body>
<![CDATA[
for (var id in this.accounts)
if (this.accounts[id].canSendMessage)
return true;
return false;
return this.contact.canSendMessage;
]]>
</body>
</method>
@ -374,18 +335,7 @@
<method name="openConversation">
<body>
<![CDATA[
var conv;
for (var id in this.accounts) {
if (!this.accounts[id].canSendMessage)
continue;
// this will send a new-conversation notification that will force
// opening a conversation window if there isn't one around
conv = this.accounts[id].createConversation();
break;
}
Conversations.focusConversation(conv);
Conversations.focusConversation(this.contact.createConversation());
]]>
</body>
</method>

Просмотреть файл

@ -97,19 +97,11 @@
if (val == this._buddy)
return val;
let os =
Components.classes["@mozilla.org/observer-service;1"]
.getService(Components.interfaces.nsIObserverService);
const events = [
"buddy-update",
"buddy-idle",
"buddy-away"
];
if (!val)
events.forEach(function(aName) { os.removeObserver(this, aName); }, this);
this._buddy.buddy.removeObserver(this);
else
events.forEach(function(aName) { os.addObserver(this, aName, false); }, this);
val.buddy.addObserver(this);
return (this._buddy = val);
]]>
</setter>
@ -243,9 +235,9 @@
this.elt = aElt;
this.reset();
let name = aBuddy.buddyName;
let alias = aBuddy.buddyAlias;
this.setAttribute("displayname", alias || name);
let name = aBuddy.userName;
let displayName = aBuddy.displayName;
this.setAttribute("displayname", displayName);
let account = aBuddy.account;
this.setAttribute("iconPrpl", account.protocol.iconBaseURI + "icon.png");
if (aElt.hasAttribute("status"))
@ -254,7 +246,7 @@
this.removeAttribute("status");
this.setBuddyIcon(aBuddy.buddyIconFilename);
if (alias != name)
if (displayName != name)
this.addRow(this.bundle.GetStringFromName("buddy.screenname"), name);
this.addRow(this.bundle.GetStringFromName("buddy.account"), account.name);
@ -313,7 +305,10 @@
<parameter name="aData"/>
<body>
<![CDATA[
if (aSubject == this.buddy)
if (aSubject == this.buddy &&
(aTopic == "account-buddy-status-changed" ||
aTopic == "account-display-name-changed" ||
aTopic == "account-buddy-icon-changed"))
this.updateTooltipFromBuddy(this.buddy, this.elt);
]]>
</body>
@ -333,14 +328,7 @@
if (elt.localName != "buddy")
return false;
var buddy;
for (var id in elt.accounts) {
buddy = elt.accounts[id];
break;
}
if (!buddy)
throw "no account in this buddy!";
let buddy = elt.contact.preferredBuddy.preferredAccountBuddy;
return updateTooltipFromBuddy(buddy, elt);
]]>
</handler>

Просмотреть файл

@ -45,7 +45,7 @@
xmlns:xbl="http://www.mozilla.org/xbl">
<binding id="group" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
<content>
<content persist="closed">
<xul:image class="twisty"/>
<xul:label flex="1" crop="end" xbl:inherits="value=name"/>
</content>
@ -67,13 +67,17 @@
<parameter name="aGroup"/>
<body>
<![CDATA[
this.groupId = aGroup.id;
this.tag = aGroup;
this.name = aGroup.name;
this.buddies = [ ];
let contacts = this.tag.getContacts();
if (!this.showOffline)
contacts = contacts.filter((function (c) c.online), this);
if (!contacts.length)
return false;
this.groupId = aGroup.id;
this.contacts = [ ];
this.contactsById = {};
this.setAttribute("id", "group" + this.groupId);
this.setAttribute("name", this.name);
this.setAttribute("persist", "closed");
// restore the potential persisted value
var source = Components.classes["@mozilla.org/rdf/datasource;1?name=local-store"]
@ -82,22 +86,13 @@
.getService(Components.interfaces.nsIRDFService);
var elt = RDF.GetResource(document.location + "#" + this.id);
if (source.HasAssertion(elt, RDF.GetResource("closed"),
RDF.GetLiteral("true"), true)) {
RDF.GetLiteral("true"), true))
this.setAttribute("closed", "true");
this._updateGroupLabel();
this._updateClosedState(true);
}
let empty = true;
this.tag.getBuddies().forEach(function (aBuddy) {
aBuddy.getAccountBuddies()
.filter(function (b) (this.showOffline || b.online) && b.tag.id == this.groupId, this)
.forEach(function(b) { this.addBuddy(b); empty = false; }, this);
}, this);
if (!empty)
this.tag.addObserver(this);
return !empty;
contacts.forEach(this.addContact, this);
this._updateGroupLabel();
this.tag.addObserver(this);
return true;
]]>
</body>
</method>
@ -112,16 +107,16 @@
<![CDATA[
this._showOffline = val;
if (val) {
this.tag.getBuddies().forEach(function (aBuddy) {
aBuddy.getAccountBuddies()
.filter(function (b) !b.online && b.tag.id == this.groupId, this)
.forEach(function(b) { this.addBuddy(b); }, this);
}, this);
this.tag.getContacts().filter(function (c) !c.online)
.forEach(this.addContact, this);
this._updateGroupLabel();
}
else {
this.buddies.filter(function (b) b.removeOfflineAccounts())
.forEach(function (b) { b.parentNode.removeChild(b);
this.removeBuddy(b); }, this);
this.contacts.filter(function (b) !b.contact.online)
.forEach(function (b) {
b.finishRemoveNode();
b.parentNode.removeChild(b);
}, this);
}
return val;
]]>
@ -135,58 +130,49 @@
<parameter name="aData"/>
<body>
<![CDATA[
if (this.showOffline) {
if (aTopic == "buddy-added")
this.addBuddy(aSubject);
else if (aTopic == "buddy-deleted" || aTopic == "buddy-removed-from-group")
this.signedOff(aSubject);
else
this.updateBuddy(aSubject);
if (this.showOffline && (aTopic == "contact-added" ||
aTopic == "contact-moved-in") ||
!this.showOffline && aSubject.online &&
(aTopic == "contact-availability-changed" ||
aTopic == "contact-added" ||
aTopic == "contact-moved-in")) {
this.addContact(aSubject);
this._updateGroupLabel();
return;
}
if (aTopic == "buddy-signed-on" || (aTopic == "buddy-added" && aSubject.online))
this.addBuddy(aSubject);
else if (aTopic == "buddy-signed-off" || aTopic == "buddy-removed")
this.signedOff(aSubject);
else if (aTopic == "buddy-idle" || aTopic == "buddy-away" || aTopic == "buddy-alias")
this.updateBuddy(aSubject);
if (aTopic == "contact-no-longer-dummy") {
let oldId = parseInt(aData);
if (this.contactsById.hasOwnProperty(oldId)) {
let contact = this.contactsById[oldId];
delete this.contactsById[oldId];
this.contactsById[contact.contact.id] = contact;
}
return;
}
]]>
</body>
</method>
<method name="addBuddy">
<parameter name="aPab"/>
<method name="addContact">
<parameter name="aContact"/>
<body>
<![CDATA[
var buddy = aPab.buddy;
if (this.contactsById.hasOwnProperty(aContact.id))
return;
var buddyElt;
for (var i = 0; i < this.buddies.length; ++i) {
if (this.buddies[i].buddyId == buddy.id) {
buddyElt = this.buddies[i];
break;
}
}
var buddyElt = document.createElement("buddy");
if (this.hasAttribute("closed"))
buddyElt.setAttribute("collapsed", "true");
if (!buddyElt) {
buddyElt = document.createElement("buddy");
let groupIsHidden = this.hasAttribute("closed");
if (groupIsHidden)
buddyElt.setAttribute("collapsed", "true");
var last = this;
if (this.contacts.length)
last = this.contacts[this.contacts.length - 1];
var last = this;
if (this.buddies.length)
last = this.buddies[this.buddies.length - 1];
this.parentNode.insertBefore(buddyElt, last.nextSibling);
buddyElt.build(aPab.buddy, this);
this.buddies.push(buddyElt);
if (groupIsHidden)
this._updateGroupLabel();
}
buddyElt.addAccount(aPab);
this.parentNode.insertBefore(buddyElt, last.nextSibling);
buddyElt.build(aContact, this);
this.contacts.push(buddyElt);
this.contactsById[aContact.id] = buddyElt;
if (this.hasAttribute("collapsing")) {
this.removeAttribute("collapsing");
@ -200,35 +186,20 @@
</body>
</method>
<method name="signedOff">
<parameter name="aPab"/>
<method name="removeContact">
<parameter name="aContact"/>
<body>
<![CDATA[
var buddy = aPab.buddy;
for (var i = 0; i < this.buddies.length; ++i) {
if (this.buddies[i].buddyId == buddy.id) {
this.buddies[i].removeAccount(aPab);
return;
}
}
]]>
</body>
</method>
<method name="removeBuddy">
<parameter name="aBuddy"/>
<body>
<![CDATA[
var i = this.buddies.indexOf(aBuddy);
var i = this.contacts.indexOf(aContact);
if (i == -1)
throw "Removing a buddy that doesn't exist?";
throw "Removing a contact that doesn't exist?";
// remove the buddy from the array
this.buddies.splice(i, 1);
// remove the contact from the array
this.contacts.splice(i, 1);
delete this.contactsById[aContact.contact.id];
// Check if some buddy remain in the group, if empty remove it
if (!this.buddies.length) {
// Check if some contacts remain in the group, if empty remove it
if (!this.contacts.length) {
this.style.height = (this.animHeight = this.clientHeight) + "px";
this.setAttribute("collapsing", "true");
this.animInterval = setInterval(this._animateCollapse, this.animationInterval, this);
@ -258,28 +229,12 @@
</body>
</method>
<method name="updateBuddy">
<parameter name="aPab"/>
<body>
<![CDATA[
var buddy = aPab.buddy;
for (var i = 0; i < this.buddies.length; ++i) {
if (this.buddies[i].buddyId == buddy.id) {
this.buddies[i].update();
return;
}
}
]]>
</body>
</method>
<method name="_updateClosedState">
<parameter name="aClosed"/>
<body>
<![CDATA[
for (let i = 0; i < this.buddies.length; ++i)
this.buddies[i].collapsed = aClosed;
for each (let contact in this.contacts)
contact.collapsed = aClosed;
]]>
</body>
</method>
@ -304,9 +259,9 @@
<method name="_updateGroupLabel">
<body>
<![CDATA[
let name = this.name;
let name = this.tag.name;
if (this.hasAttribute("closed"))
name += " (" + this.buddies.length + ")";
name += " (" + this.contacts.length + ")";
this.setAttribute("name", name);
]]>

Просмотреть файл

@ -0,0 +1,312 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* 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.
*
* The Original Code is the Instantbird messenging client, released
* 2010.
*
* The Initial Developer of the Original Code is
* Florian QUEZE <florian@instantbird.org>.
* Portions created by the Initial Developer are Copyright (C) 2010
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
#include "nsISupports.idl"
#include "nsIObserver.idl"
#include "nsISimpleEnumerator.idl"
#include "purpleIConversation.idl"
interface imITag;
interface imIContact;
interface imIBuddy;
interface imIAccountBuddy;
interface purpleIProtocol;
[scriptable, uuid(f1619b49-310b-47aa-ab1c-238aba084c62)]
interface imIContactsService: nsISupports {
void initContacts();
void unInitContacts();
imIContact getContactById(in long aId);
imIBuddy getBuddyById(in long aId);
imIBuddy getBuddyByNameAndProtocol(in AUTF8String aNormalizedName,
in purpleIProtocol aPrpl);
// These 3 functions are called by the protocol plugins when
// synchronizing the buddy list with the server stored list,
// or after user operations have been performed.
void accountBuddyAdded(in imIAccountBuddy aAccountBuddy);
void accountBuddyRemoved(in imIAccountBuddy aAccountBuddy);
void accountBuddyMoved(in imIAccountBuddy aAccountBuddy,
in imITag aOldTag, in imITag aNewTag);
};
[scriptable, uuid(f799a9c2-23f2-4fd1-96fb-515bad238f8c)]
interface imITagsService: nsISupports {
// Create a new tags or return the existing one if it already exists
imITag createTag(in AUTF8String aName);
// Get an existing tag by (numeric) id. Returns null if not found.
imITag getTagById(in long aId);
// Get an existing tag by name (will do an SQL query). Returns null
// if not found.
imITag getTagByName(in AUTF8String aName);
// Get an array of all existing tags.
void getTags([optional] out unsigned long tagCount,
[retval, array, size_is(tagCount)] out imITag tags);
};
[scriptable, uuid(c211e5e2-f0a4-4a86-9e4c-3f6b905628a5)]
interface imITag: nsISupports {
readonly attribute long id;
attribute AUTF8String name;
// Get an array of all the contacts associated with this tag.
// Contacts can either "have the tag" (added by user action) or
// have inherited the tag because it was the server side group for
// one of the AccountBuddy of the contact.
void getContacts([optional] out unsigned long contactCount,
[retval, array, size_is(contactCount)] out imIContact contacts);
void addObserver(in nsIObserver aObserver);
void removeObserver(in nsIObserver aObserver);
/* Observers will be notified of changes related to the contacts
* that have the tag: contact-*, buddy-*, account-buddy-*
* notifications forwarded respectively from the imIContact,
* imIBuddy and imIAccountBuddy instances.
*/
// Exposed for add-on authors. All usage by Instantbird will come from
// the imITag implementation so it wasn't required to expose this.
// This can be used to dispatch custom notifications to the
// observers of the tag.
void notifyObservers(in nsISupports aObj, in string aEvent,
[optional] in wstring aData);
};
[scriptable, uuid(f13dc4fc-5334-45cb-aa58-a92851955e55)]
interface imIStatusInfo: nsISupports {
// Name suitable for display in the UI. Can either be the username,
// the server side alias, or the user set local alias of the contact.
readonly attribute AUTF8String displayName;
readonly attribute AUTF8String buddyIconFilename;
const short STATUS_UNKNOWN = 0;
const short STATUS_OFFLINE = 1;
const short STATUS_INVISIBLE = 2;
const short STATUS_MOBILE = 3;
const short STATUS_IDLE = 4;
const short STATUS_AWAY = 5;
const short STATUS_UNAVAILABLE = 6;
const short STATUS_AVAILABLE = 7;
// numerical value used to compare the availability of two buddies
// based on their current status.
// Use it only for immediate comparisons, do not store the value,
// it can change between versions for a same status of the buddy.
readonly attribute long statusType;
readonly attribute boolean online; // (statusType > STATUS_OFFLINE)
readonly attribute boolean available; // (statusType == STATUS_AVAILABLE)
readonly attribute boolean idle; // (statusType == STATUS_IDLE)
readonly attribute boolean mobile; // (statusType == STATUS_MOBILE)
readonly attribute AUTF8String statusText;
// Gives more detail to compare the availability of two buddies with the same
// status type.
// Example: 2 buddies may have been idle for various amounts of times.
readonly attribute long availabilityDetails;
// True if the buddy is online or if the account supports sending
// offline messages to the buddy.
readonly attribute boolean canSendMessage;
// enumerator of purpleTooltipInfo components
nsISimpleEnumerator getTooltipInfo();
// Will select the buddy automatically based on availability, and
// the account (if needed) based on the account order in the account
// manager.
purpleIConversation createConversation();
};
[scriptable, uuid(f585b0df-f6ad-40d5-9de4-c58b14af13e4)]
interface imIContact: imIStatusInfo {
// The id will be positive if the contact is real (stored in the
// SQLite database) and negative if the instance is a dummy contact
// holding only a single buddy without aliases or additional tags.
readonly attribute long id;
attribute AUTF8String alias;
void getTags([optional] out unsigned long tagCount,
[retval, array, size_is(tagCount)] out imITag tags);
void move(in imITag aOldTag, in imITag aNewtag);
readonly attribute imIBuddy preferredBuddy;
void getBuddies([optional] out unsigned long buddyCount,
[retval, array, size_is(buddyCount)] out imIBuddy buddies);
// remove the contact from the buddy list. Will also remove the
// associated buddies.
void remove();
void addObserver(in nsIObserver aObserver);
void removeObserver(in nsIObserver aObserver);
/* Observers will be notified of changes related to the contact.
* aSubject will point to the imIContact object
* (with some exceptions for contact-moved-* notifications).
*
* Fired notifications:
* contact-availability-changed
* when either statusType or availabilityDetails has changed.
* contact-signed-on
* contact-signed-off
* contact-status-changed
* when either statusType or statusText has changed.
* contact-display-name-changed
* when the alias (or serverAlias of the most available buddy if
* no alias is set) has changed.
* The old display name is provided in aData.
* contact-preferred-buddy-changed
* The buddy that would be favored to start a conversation has changed.
* contact-moved, contact-moved-in, contact-moved-out
* contact-moved is notified through the observer service
* contact-moved-in is notified to
* - the contact observers (aSubject is the new tag)
* - the new tag (aSubject is the contact instance)
* contact-moved-out is notified to
* - the contact observers (aSubject is the old tag)
* - the old tag (aSubject is the contact instance)
* contact-no-longer-dummy
* When a real contact is created to replace a dummy contact.
* The old (negative) id will be given in aData.
* See also the comment above the 'id' attribute.
*
* Observers will also receive all the (forwarded) notifications
* from the linked buddies (imIBuddy instances) and their account
* buddies (imIAccountBuddy instances).
*/
// Exposed for add-on authors. All usage by Instantbird will come from
// the imIContact implementation so it wasn't required to expose this.
// This can be used to dispatch custom notifications to the
// observers of the contact and its tags.
// The notification will also be forwarded to the observer service.
void notifyObservers(in nsISupports aObj, in string aEvent,
[optional] in wstring aData);
};
[scriptable, uuid(fea582a0-3839-4d80-bcab-0ff82ae8f97f)]
interface imIBuddy: imIStatusInfo {
readonly attribute long id;
readonly attribute purpleIProtocol protocol;
readonly attribute AUTF8String userName; // may be formatted
readonly attribute AUTF8String normalizedName; // normalized userName
// The optional server alias is in displayName (inherited from imIStatusInfo)
// displayName = serverAlias || userName.
readonly attribute imIContact contact;
readonly attribute imIAccountBuddy preferredAccountBuddy;
void getAccountBuddies([optional] out unsigned long accountBuddyCount,
[retval, array, size_is(accountBuddyCount)] out imIAccountBuddy accountBuddies);
// remove the buddy from the buddy list. If the contact becomes empty, it will be removed too.
void remove();
void addObserver(in nsIObserver aObserver);
void removeObserver(in nsIObserver aObserver);
/* Observers will be notified of changes related to the buddy.
* aSubject will point to the imIBuddy object.
* Fired notifications:
* buddy-availability-changed
* when either statusType or availabilityDetails has changed.
* buddy-signed-on
* buddy-signed-off
* buddy-status-changed
* when either statusType or statusText has changed.
* buddy-display-name-changed
* when the serverAlias has changed.
* The old display name is provided in aData.
* buddy-preferred-account-changed
* The account that would be favored to start a conversation has changed.
*
* Observers will also receive all the (forwarded) notifications
* from the linked account buddies (imIAccountBuddy instances).
*/
// Exposed for add-on authors. All usage by Instantbird will come from
// the imIBuddy implementation so it wasn't required to expose this.
// This can be used to dispatch custom notifications to the
// observers of the buddy, its contact and its tags.
// The contact will forward the notifications to the observer service.
void notifyObservers(in nsISupports aObj, in string aEvent,
[optional] in wstring aData);
// observe should only be called by the imIAccountBuddy
// implementations to report changes.
void observe(in nsISupports aObj, in string aEvent,
[optional] in wstring aData);
};
/* imIAccountBuddy implementations can send notifications to their buddy:
*
* For all of them, aSubject points to the imIAccountBuddy object.
*
* Supported notifications:
* account-buddy-availability-changed
* when either statusType or availabilityDetails has changed.
* account-buddy-signed-on
* account-buddy-signed-off
* account-buddy-status-changed
* when either statusType or statusText has changed.
* account-buddy-display-name-changed
* when the serverAlias has changed.
* The old display name is provided in aData.
*
* All notifications (even unsupported ones) will be forwarded to the contact,
* its tags and nsObserverService.
*/
[scriptable, uuid(114d24ff-56a1-4fd6-9822-4992efb6e036)]
interface imIAccountBuddy: imIStatusInfo {
// The setter is for internal use only. buddy will be set by the
// Contacts service when accountBuddyAdded is called on this
// instance of imIAccountBuddy.
attribute imIBuddy buddy;
readonly attribute purpleIAccount account;
// Setting the tag will move the buddy to a different group on the
// server-stored buddy list.
attribute imITag tag;
readonly attribute AUTF8String userName;
readonly attribute AUTF8String normalizedName;
attribute AUTF8String serverAlias;
// remove the buddy from the buddy list of this account.
void remove();
};

Просмотреть файл

@ -40,7 +40,7 @@
#include "nsISimpleEnumerator.idl"
#include "nsIObserver.idl"
interface purpleIAccountBuddy;
interface imIAccountBuddy;
interface purpleIAccount;
interface nsIURI;
interface nsIDOMDocument;
@ -102,7 +102,7 @@ interface purpleIConversation: nsISupports {
interface purpleIConvIM: purpleIConversation {
/* The buddy at the remote end of the conversation */
readonly attribute purpleIAccountBuddy buddy;
readonly attribute imIAccountBuddy buddy;
/* The remote buddy is not currently typing */
const short NOT_TYPING = 0;

Просмотреть файл

@ -0,0 +1,924 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* 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.
*
* The Original Code is the Instantbird messenging client, released
* 2010.
*
* The Initial Developer of the Original Code is
* Florian QUEZE <florian@instantbird.org>.
* Portions created by the Initial Developer are Copyright (C) 2010
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
const Cc = Components.classes;
const Ci = Components.interfaces;
XPCOMUtils.defineLazyServiceGetter(this, "obs",
"@mozilla.org/observer-service;1",
"nsIObserverService");
XPCOMUtils.defineLazyServiceGetter(this, "prefs",
"@mozilla.org/preferences-service;1",
"nsIPrefBranch");
XPCOMUtils.defineLazyServiceGetter(this, "pcs",
"@instantbird.org/purple/core;1",
"purpleICoreService");
XPCOMUtils.defineLazyGetter(this, "DBConn", function() pcs.storageConnection);
var AccountsById = { };
function getAccountById(aId) {
if (AccountsById.hasOwnProperty(aId))
return AccountsById[aId];
let account = null;
try {
account = pcs.getAccountByNumericId(aId);
} catch (x) { /* Not found */ }
AccountsById[aId] = account;
return account;
}
function TagsService() { }
TagsService.prototype = {
get wrappedJSObject() this,
createTag: function(aName) {
// If the tag already exists, we don't want to create a duplicate.
let tag = this.getTagByName(aName);
if (tag)
return tag;
let statement = DBConn.createStatement("INSERT INTO tags (name, position) VALUES(:name, 0)");
statement.params.name = aName;
statement.executeStep();
tag = new Tag(DBConn.lastInsertRowID, aName);
Tags.push(tag);
return tag;
},
// Get an existing tag by (numeric) id. Returns null if not found.
getTagById: function(aId) TagsById[aId],
// Get an existing tag by name (will do an SQL query). Returns null
// if not found.
getTagByName: function(aName) {
let statement = DBConn.createStatement("SELECT id FROM tags where name = :name");
statement.params.name = aName;
if (!statement.executeStep())
return null;
return this.getTagById(statement.row.id);
},
// Get an array of all existing tags.
getTags: function(aTagCount) {
if (aTagCount)
aTagCount.value = Tags.length;
return Tags;
},
QueryInterface: XPCOMUtils.generateQI([Ci.imITagsService]),
classDescription: "Tags",
classID: Components.ID("{1fa92237-4303-4384-b8ac-4e65b50810a5}"),
contractID: "@instantbird.org/purple/tags-service;1"
};
// TODO move into the tagsService
var Tags = [];
var TagsById = { };
function Tag(aId, aName) {
this._id = aId;
this._name = aName;
this._contacts = [];
this._observers = [];
TagsById[this.id] = this;
}
Tag.prototype = {
get id() this._id,
get name() this._name,
set name(aNewName) {
var statement = DBConn.createStatement("UPDATE tags SET name = :name WHERE id = :id");
statement.params.name = aNewName;
statement.params.id = this._id;
statement.execute();
//FIXME move the account buddies if some use this tag as their group
return aNewName;
},
getContacts: function(aContactCount) {
let contacts = this._contacts.filter(function(c) !c._empty);
if (aContactCount)
aContactCount.value = contacts.length;
return contacts;
},
_addContact: function (aContact) {
this._contacts.push(aContact);
},
_removeContact: function (aContact) {
let index = this._contacts.indexOf(aContact);
if (index != -1)
this._contacts.splice(index, 1);
},
addObserver: function(aObserver) {
if (this._observers.indexOf(aObserver) == -1)
this._observers.push(aObserver);
},
removeObserver: function(aObserver) {
let index = this._observers.indexOf(aObserver);
if (index != -1)
this._observers.splice(index, 1);
},
notifyObservers: function(aSubject, aTopic, aData) {
for each (let observer in this._observers)
observer.observe(aSubject, aTopic, aData);
},
getInterfaces: function(countRef) {
var interfaces = [Ci.nsIClassInfo, Ci.nsISupports, Ci.imITag];
countRef.value = interfaces.length;
return interfaces;
},
getHelperForLanguage: function(language) null,
implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,
flags: 0,
QueryInterface: XPCOMUtils.generateQI([Ci.imITag, Ci.nsIClassInfo])
};
var ContactsById = { };
var LastDummyContactId = 0;
function Contact(aId, aAlias) {
// Assign a negative id to dummy contacts that have a single buddy
this._id = aId || --LastDummyContactId;
this._alias = aAlias;
this._tags = [];
this._buddies = [];
this._observers = [];
ContactsById[this._id] = this;
}
Contact.prototype = {
_id: 0,
get id() this._id,
get alias() this._alias,
set alias(aNewAlias) {
if (this._id < 0) {
// Create a real contact for this dummy contact
let statement = DBConn.createStatement("INSERT INTO contacts (alias) VALUES(:alias)");
statement.params.alias = aNewAlias;
statement.execute();
delete ContactsById[this._id];
let oldId = this._id;
this._id = DBConn.lastInsertRowID;
ContactsById[this._id] = this;
this._notifyObservers("no-longer-dummy", oldId.toString());
// Update the contact_id for the single existing buddy of this contact
statement = DBConn.createStatement("UPDATE buddies SET contact_id = :id WHERE id = :buddy_id");
statement.params.id = this._id;
statement.params.buddy_id = this._buddies[0].id;
statement.execute();
}
else {
let statement = DBConn.createStatement("UPDATE contacts SET alias = :alias WHERE id = :id");
statement.params.alias = aNewAlias;
statement.params.id = this._id;
statement.execute();
}
let oldDisplayName = this.displayName;
this._alias = aNewAlias;
this._notifyObservers("display-name-changed", oldDisplayName);
for each (let buddy in this._buddies)
for each (let accountBuddy in buddy._accounts)
accountBuddy.serverAlias = aNewAlias;
return aNewAlias;
},
getTags: function(aTagCount) {
if (aTagCount)
aTagCount.value = this._tags.length;
return this._tags;
},
hasTag: function(aTag) this._tags.some(function (t) t.id == aTag.id),
_massMove: false,
move: function(aOldTag, aNewtag) {
if (!this.hasTag(aOldTag))
throw "Attempting to remove a tag that the contact doesn't have";
// FIXME: If the old tag is part of the local tags, just change that tag.
// Doesn't matter as long as there's no UI to add a tag to a contact.
// Later we will want to keep 2 arrays (one for local tags and one
// for inherited tags) and concatenate them during getTags calls.
this._massMove = true;
let moved = false;
this._buddies.forEach(function (aBuddy) {
aBuddy._accounts.forEach(function (aAccountBuddy) {
if (aAccountBuddy.tag.id == aOldTag.id) {
if (aBuddy._accounts.some(function(ab)
ab.account.numericId == aAccountBuddy.account.numericId &&
ab.tag.id == aNewtag.id)) {
aAccountBuddy.remove();
moved = true;
}
else {
try {
aAccountBuddy.tag = aNewtag;
moved = true;
} catch (e) {
// Ignore failures. Some protocol plugins may not implement this.
}
}
}
});
});
this._massMove = false;
if (moved)
this._moved(aOldTag, aNewtag);
},
_isTagInherited: function(aTag) {
for each (let buddy in this._buddies)
for each (let accountBuddy in buddy._accounts)
if (accountBuddy.tag.id == aTag.id)
return true;
return false;
},
_moved: function(aOldTag, aNewTag) {
if (this._massMove)
return;
// Avoid xpconnect wrappers.
aNewTag = aNewTag && TagsById[aNewTag.id];
aOldTag = aOldTag && TagsById[aOldTag.id];
// Decide what we need to do. Return early if nothing to do.
let shouldRemove =
aOldTag && this.hasTag(aOldTag) && !this._isTagInherited(aOldTag);
let shouldAdd =
aNewTag && !this.hasTag(aNewTag) && this._isTagInherited(aNewTag);
if (!shouldRemove && !shouldAdd)
return;
// Apply the changes.
let tags = this._tags;
if (shouldRemove) {
tags = tags.filter(function(aTag) aTag.id != aOldTag.id);
aOldTag._removeContact(this);
}
if (shouldAdd) {
tags.push(aNewTag);
aNewTag._addContact(this);
}
this._tags = tags;
// Finally, notify of the changes.
if (shouldRemove) {
aOldTag.notifyObservers(this, "contact-moved-out");
for each (let observer in this._observers)
observer.observe(aOldTag, "contact-moved-out");
}
if (shouldAdd) {
aNewTag.notifyObservers(this, "contact-moved-in");
for each (let observer in this._observers)
observer.observe(aNewTag, "contact-moved-in");
}
obs.notifyObservers(this, "contact-moved", null);
},
getBuddies: function(aBuddyCount) {
if (aBuddyCount)
aBuddyCount.value = this._buddies.length;
return this._buddies;
},
get _empty() this._buddies.length == 0 ||
this._buddies.every(function(b) b._empty),
remove: function() {
for each (let buddy in this._buddies)
buddy.remove();
},
// imIStatusInfo implementation
_preferredBuddy: null,
get preferredBuddy() {
if (!this._preferredBuddy)
this._updatePreferredBuddy();
return this._preferredBuddy;
},
set preferredBuddy(aBuddy) {
let shouldNotify = this._preferredBuddy != null;
let oldDisplayName =
this._preferredBuddy && this._preferredBuddy.displayName;
this._preferredBuddy = aBuddy;
if (shouldNotify)
this._notifyObservers("preferred-buddy-changed");
if (oldDisplayName && this._preferredBuddy.displayName != oldDisplayName)
this._notifyObservers("display-name-changed", oldDisplayName);
this._updateStatus();
},
// aBuddy indicate which buddy's availability has changed.
_updatePreferredBuddy: function(aBuddy) {
if (aBuddy) {
if (!this._preferredBuddy) {
this.preferredBuddy = aBuddy;
return;
}
if (aBuddy.id == this._preferredBuddy.id) {
// The suggested buddy is already preferred, check if its
// availability has changed.
if (aBuddy.statusType > this._statusType ||
(aBuddy.statusType == this._statusType &&
aBuddy.availabilityDetails >= this._availabilityDetails)) {
// keep the currently preferred buddy, only update the status.
this._updateStatus();
return;
}
// We aren't sure that the currently preferred buddy should
// still be preferred. Let's go through the list!
}
else {
// The suggested buddy is not currently preferred. If it is
// more available, prefer it!
if (aBuddy.statusType > this._statusType ||
(aBuddy.statusType == this._statusType &&
aBuddy.availabilityDetails > this._availabilityDetails))
this.preferredBuddy = aBuddy;
return;
}
}
let preferred;
//TODO take into account the order of the buddies in the contact.
for each (let buddy in this._buddies) {
if (!preferred || preferred.statusType < buddy.statusType ||
(preferred.statusType == buddy.statusType &&
preferred.availabilityDetails < buddy.availabilityDetails))
preferred = buddy;
}
if (preferred && (!this._preferredBuddy ||
preferred.id != this._preferredBuddy.id))
this.preferredBuddy = preferred;
},
_updateStatus: function() {
let buddy = this._preferredBuddy; // for convenience
// Decide which notifications should be fired.
let notifications = [];
if (this._statusType != buddy.statusType ||
this._availabilityDetails != buddy.availabilityDetails)
notifications.push("availability-changed");
if (this._statusType != buddy.statusType ||
this._statusText != buddy.statusText) {
notifications.push("status-changed");
if (this.online && buddy.statusType <= Ci.imIStatusInfo.STATUS_OFFLINE)
notifications.push("signed-off");
if (!this.online && buddy.statusType > Ci.imIStatusInfo.STATUS_OFFLINE)
notifications.push("signed-on");
}
// Actually change the stored status.
[this._statusType, this._statusText, this._availabilityDetails] =
[buddy.statusType, buddy.statusText, buddy.availabilityDetails];
// Fire the notifications.
notifications.forEach(function(aTopic) {
this._notifyObservers(aTopic);
}, this);
},
get displayName() this._alias || this.preferredBuddy.displayName,
get buddyIconFilename() this.preferredBuddy.buddyIconFileName,
_statusType: 0,
get statusType() this._statusType,
get online() this.statusType > Ci.imIStatusInfo.STATUS_OFFLINE,
get available() this.statusType == Ci.imIStatusInfo.STATUS_AVAILABLE,
get idle() this.statusType == Ci.imIStatusInfo.STATUS_IDLE,
get mobile() this.statusType == Ci.imIStatusInfo.STATUS_MOBILE,
_statusText: "",
get statusText() this._statusText,
_availabilityDetails: 0,
get availabilityDetails() this._availabilityDetails,
get canSendMessage() this.preferredBuddy.canSendMessage,
//XXX should we list the buddies in the tooltip?
getTooltipInfo: function() this.preferredBuddy.getTooltipInfo(),
createConversation: function() this.preferredBuddy.createConversation(),
addObserver: function(aObserver) {
if (this._observers.indexOf(aObserver) == -1)
this._observers.push(aObserver);
},
removeObserver: function(aObserver) {
if (!this.hasOwnProperty("_observers"))
return;
let index = this._observers.indexOf(aObserver);
if (index != -1)
this._observers.splice(index, 1);
},
// internal calls + calls from add-ons
notifyObservers: function(aSubject, aTopic, aData) {
for each (let observer in this._observers)
observer.observe(aSubject, aTopic, aData);
for each (let tag in this._tags)
tag.notifyObservers(aSubject, aTopic, aData);
obs.notifyObservers(aSubject, aTopic, aData);
},
_notifyObservers: function(aTopic, aData) {
this.notifyObservers(this, "contact-" + aTopic, aData);
},
// This is called by the imIBuddy implementations.
_observe: function(aSubject, aTopic, aData) {
// Forward the notification.
this.notifyObservers(aSubject, aTopic, aData);
let isPreferredBuddy =
aSubject instanceof Buddy && aSubject.id == this.preferredBuddy.id;
switch (aTopic) {
case "buddy-availability-changed":
this._updatePreferredBuddy(aSubject);
break;
case "buddy-status-changed":
if (isPreferredBuddy)
this._updateStatus();
break;
case "buddy-display-name-changed":
if (isPreferredBuddy && !this._alias)
this._notifyObservers("display-name-changed", aData);
break;
case "buddy-added":
// Currently buddies are always added in dummy empty contacts,
// later we may want to check this._buddies.length == 1.
this._notifyObservers("added");
break;
case "buddy-removed":
if (this._buddies.length == 1) {
if (this._id > 0) {
let statement =
DBConn.createStatement("DELETE FROM contacts WHERE id = :id");
statement.params.id = this._id;
statement.execute();
}
this._notifyObservers("removed");
delete ContactsById[this._id];
for each (let tag in this._tags)
tag._removeContact(this);
delete this._tags;
delete this._buddies;
delete this._observers;
}
else {
let index = this._buddies.indexOf(aSubject);
if (index != -1)
this._buddies.splice(index, 1);
else
throw "Removing an unknown buddy from contact " + this._id;
if (this._preferredBuddy.id == aSubject.id) {
this._preferredBuddy = null;
this._updatePreferredBuddy();
}
}
}
},
getInterfaces: function(countRef) {
var interfaces = [Ci.nsIClassInfo, Ci.nsISupports, Ci.imIContact];
countRef.value = interfaces.length;
return interfaces;
},
getHelperForLanguage: function(language) null,
implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,
flags: 0,
QueryInterface: XPCOMUtils.generateQI([Ci.imIContact, Ci.nsIClassInfo])
};
var BuddiesById = { };
function Buddy(aId, aKey, aName, aSrvAlias, aContactId) {
this._id = aId;
this._key = aKey;
this._name = aName;
if (aSrvAlias)
this._srvAlias = aSrvAlias;
this._accounts = [];
this._observers = [];
if (aContactId)
this._contact = ContactsById[aContactId];
else
this._contact = new Contact(null, null);
this._contact._buddies.push(this);
BuddiesById[this._id] = this;
}
Buddy.prototype = {
get id() this._id,
destroy: function() {
delete this._accounts;
delete this._observers;
delete this._preferredAccount;
},
get protocol() this._accounts[0].account.protocol,
get userName() this._name,
get normalizedName() this._key,
_srvAlias: "",
_contact: null,
get contact() this._contact,
getAccountBuddies: function(aAccountBuddyCount) {
if (aAccountBuddyCount)
aAccountBuddyCount.value = this._accounts.length;
return this._accounts;
},
_addAccount: function(aAccountBuddy, aTag) {
this._accounts.push(aAccountBuddy);
let contact = this._contact;
if (this._contact._tags.indexOf(aTag) == -1) {
this._contact._tags.push(aTag);
aTag._addContact(contact);
}
if (!this._preferredAccount)
this._preferredAccount = aAccountBuddy;
},
get _empty() this._accounts.length == 0,
remove: function() {
for each (let account in this._accounts)
account.remove();
},
// imIStatusInfo implementation
_preferredAccount: null,
get preferredAccountBuddy() this._preferredAccount,
_isPreferredAccount: function(aAccountBuddy)
aAccountBuddy.account.numericId == this._preferredAccount.account.numericId,
set preferredAccount(aAccount) {
let oldDisplayName =
this._preferredAccount && this._preferredAccount.displayName;
this._preferredAccount = aAccount;
this._notifyObservers("preferred-account-changed");
if (oldDisplayName && this._preferredAccount.displayName != oldDisplayName)
this._notifyObservers("display-name-changed", oldDisplayName);
this._updateStatus();
},
// aAccount indicate which account's availability has changed.
_updatePreferredAccount: function(aAccount) {
if (aAccount) {
if (aAccount.account.numericId == this._preferredAccount.account.numericId) {
// The suggested account is already preferred, check if its
// availability has changed.
if (aAccount.statusType > this._statusType ||
(aAccount.statusType == this._statusType &&
aAccount.availabilityDetails >= this._availabilityDetails)) {
// keep the currently preferred account, only update the status.
this._updateStatus();
return;
}
// We aren't sure that the currently preferred account should
// still be preferred. Let's go through the list!
}
else {
// The suggested account is not currently preferred. If it is
// more available, prefer it!
if (aAccount.statusType > this._statusType ||
(aAccount.statusType == this._statusType &&
aAccount.availabilityDetails > this._availabilityDetails))
this.preferredAccount = aAccount;
return;
}
}
let preferred;
//TODO take into account the order of the account-manager list.
for each (let account in this._accounts) {
if (!preferred || preferred.statusType < account.statusType ||
(preferred.statusType == account.statusType &&
preferred.availabilityDetails < account.availabilityDetails))
preferred = account;
}
if (!this._preferredAccount) {
if (preferred)
this.preferredAccount = preferred;
return;
}
if (preferred.account.numericId != this._preferredAccount.account.numericId)
this.preferredAccount = preferred;
else
this._updateStatus();
},
_updateStatus: function() {
let account = this._preferredAccount; // for convenience
// Decide which notifications should be fired.
let notifications = [];
if (this._statusType != account.statusType ||
this._availabilityDetails != account.availabilityDetails)
notifications.push("availability-changed");
if (this._statusType != account.statusType ||
this._statusText != account.statusText) {
notifications.push("status-changed");
if (this.online && account.statusType <= Ci.imIStatusInfo.STATUS_OFFLINE)
notifications.push("signed-off");
if (!this.online && account.statusType > Ci.imIStatusInfo.STATUS_OFFLINE)
notifications.push("signed-on");
}
// Actually change the stored status.
[this._statusType, this._statusText, this._availabilityDetails] =
[account.statusType, account.statusText, account.availabilityDetails];
// Fire the notifications.
notifications.forEach(function(aTopic) {
this._notifyObservers(aTopic);
}, this);
},
get displayName() this._preferredAccount && this._preferredAccount.displayName ||
this._srvAlias || this._name,
get buddyIconFilename() this._preferredAccount.buddyIconFileName,
_statusType: 0,
get statusType() this._statusType,
get online() this.statusType > Ci.imIStatusInfo.STATUS_OFFLINE,
get available() this.statusType == Ci.imIStatusInfo.STATUS_AVAILABLE,
get idle() this.statusType == Ci.imIStatusInfo.STATUS_IDLE,
get mobile() this.statusType == Ci.imIStatusInfo.STATUS_MOBILE,
_statusText: "",
get statusText() this._statusText,
_availabilityDetails: 0,
get availabilityDetails() this._availabilityDetails,
get canSendMessage() this._preferredAccount.canSendMessage,
//XXX should we list the accounts in the tooltip?
getTooltipInfo: function() this._preferredAccount.getTooltipInfo(),
createConversation: function() this._preferredAccount.createConversation(),
addObserver: function(aObserver) {
if (this._observers.indexOf(aObserver) == -1)
this._observers.push(aObserver);
},
removeObserver: function(aObserver) {
let index = this._observers.indexOf(aObserver);
if (index != -1)
this._observers.splice(index, 1);
},
// internal calls + calls from add-ons
notifyObservers: function(aSubject, aTopic, aData) {
for each (let observer in this._observers)
observer.observe(aSubject, aTopic, aData);
this._contact._observe(aSubject, aTopic, aData);
},
_notifyObservers: function(aTopic, aData) {
this.notifyObservers(this, "buddy-" + aTopic, aData);
},
// This is called by the imIAccountBuddy implementations.
observe: function(aSubject, aTopic, aData) {
// Forward the notification.
this.notifyObservers(aSubject, aTopic, aData);
switch (aTopic) {
case "account-buddy-availability-changed":
this._updatePreferredAccount(aSubject);
break;
case "account-buddy-status-changed":
if (this._isPreferredAccount(aSubject))
this._updateStatus();
break;
case "account-buddy-display-name-changed":
if (this._isPreferredAccount(aSubject)) {
this._srvAlias =
this.displayName != this.userName ? this.displayName : "";
let statement =
DBConn.createStatement("UPDATE buddies SET srv_alias = :srvAlias " +
"WHERE id = :buddyId");
statement.params.buddyId = this.id;
statement.params.srvAlias = this._srvAlias;
statement.executeAsync();
this._notifyObservers("display-name-changed", aData);
}
break;
case "account-buddy-added":
if (this._accounts.length == 0) {
// Add the new account in the empty buddy instance.
// The TagsById hack is to bypass the xpconnect wrapper.
this._addAccount(aSubject, TagsById[aSubject.tag.id]);
this._notifyObservers("added");
}
else {
this._accounts.push(aSubject);
this.contact._moved(null, aSubject.tag);
this._updatePreferredAccount(aSubject);
}
break;
case "account-buddy-removed":
if (this._accounts.length == 1) {
let statement =
DBConn.createStatement("DELETE FROM buddies WHERE id = :id");
statement.params.id = this.id;
statement.execute();
this._notifyObservers("removed");
delete BuddiesById[this._id];
this.destroy();
}
else {
this._accounts = this._accounts.filter(function (ab) {
return (ab.account.numericId != aSubject.account.numericId ||
ab.tag.id != aSubject.tag.id);
});
if (this._preferredAccount.account.numericId == aSubject.account.numericId &&
this._preferredAccount.tag.id == aSubject.tag.id) {
this._preferredAccount = null;
this._updatePreferredAccount();
}
this.contact._moved(aSubject.tag);
}
break;
}
},
getInterfaces: function(countRef) {
var interfaces = [Ci.nsIClassInfo, Ci.nsISupports, Ci.imIBuddy];
countRef.value = interfaces.length;
return interfaces;
},
getHelperForLanguage: function(language) null,
implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,
flags: 0,
QueryInterface: XPCOMUtils.generateQI([Ci.imIBuddy, Ci.nsIClassInfo])
};
function ContactsService() { }
ContactsService.prototype = {
initContacts: function() {
var statement = DBConn.createStatement("SELECT id, name FROM tags");
while (statement.executeStep())
Tags.push(new Tag(statement.getInt32(0), statement.getUTF8String(1)));
statement = DBConn.createStatement("SELECT id, alias FROM contacts");
while (statement.executeStep())
new Contact(statement.getInt32(0), statement.getUTF8String(1));
statement =
DBConn.createStatement("SELECT contact_id, tag_id FROM contact_tag");
while (statement.executeStep()) {
let contact = ContactsById[statement.getInt32(0)];
let tag = TagsById(statement.getInt32(1));
contact._tags.push(tag);
tag._addContact(contact);
}
statement = DBConn.createStatement("SELECT id, key, name, srv_alias, contact_id FROM buddies");
while (statement.executeStep())
new Buddy(statement.getInt32(0), statement.getUTF8String(1),
statement.getUTF8String(2), statement.getUTF8String(3),
statement.getInt32(4));
// FIXME is there a way to enforce that all AccountBuddies of a Buddy have the same protocol?
statement = DBConn.createStatement("SELECT account_id, buddy_id, tag_id FROM account_buddy");
while (statement.executeStep()) {
let account = getAccountById(statement.getInt32(0));
let buddy = BuddiesById[statement.getInt32(1)];
let tag = TagsById[statement.getInt32(2)];
try {
let ab = account.loadBuddy(buddy, tag);
if (ab)
buddy._addAccount(ab, tag);
} catch (e) {
// FIXME ab shouldn't be NULL (once purpleAccount is finished)
// It currently doesn't work write with unknown protocols.
Components.utils.reportError(e);
dump(e + "\n");
}
}
},
unInitContacts: function() {
AccountsById = { };
Tags = [];
TagsById = { };
// Avoid shutdown leaks caused by references to native components
// implementing imIAccountBuddy.
for each (let buddy in BuddiesById)
buddy.destroy();
BuddiesById = { };
ContactsById = { };
},
getContactById: function(aId) ContactsById[aId],
getBuddyById: function(aId) BuddiesById[aId],
getBuddyByNameAndProtocol: function (aNormalizedName, aPrpl) {
let statement =
DBConn.createStatement("SELECT b.id FROM buddies b " +
"JOIN account_buddy ab ON buddy_id = b.id " +
"JOIN accounts a ON account_id = a.id " +
"WHERE b.key = :buddyName and a.prpl = :prplId");
statement.params.buddyName = aNormalizedName;
statement.params.prplId = aPrpl.id;
if (!statement.executeStep())
return null;
return BuddiesById[statement.row.id];
},
accountBuddyAdded: function(aAccountBuddy) {
let account = aAccountBuddy.account;
let normalizedName = aAccountBuddy.normalizedName;
let buddy = this.getBuddyByNameAndProtocol(normalizedName, account.protocol);
if (!buddy) {
let statement =
DBConn.createStatement("INSERT INTO buddies " +
"(key, name, srv_alias, position) " +
"VALUES(:key, :name, :srvAlias, 0)");
let name = aAccountBuddy.userName;
let srvAlias = aAccountBuddy.serverAlias;
statement.params.key = normalizedName;
statement.params.name = name;
statement.params.srvAlias = srvAlias;
statement.execute();
buddy =
new Buddy(DBConn.lastInsertRowID, normalizedName, name, srvAlias, 0);
}
// Initialize the 'buddy' field of the imIAccountBuddy instance.
aAccountBuddy.buddy = buddy;
// Store the new account buddy.
let statement =
DBConn.createStatement("INSERT INTO account_buddy " +
"(account_id, buddy_id, tag_id) " +
"VALUES(:accountId, :buddyId, :tagId)");
statement.params.accountId = account.numericId;
statement.params.buddyId = buddy.id;
statement.params.tagId = aAccountBuddy.tag.id;
statement.execute();
// Fire the notifications.
buddy.observe(aAccountBuddy, "account-buddy-added");
},
accountBuddyRemoved: function(aAccountBuddy) {
let buddy = aAccountBuddy.buddy;
let statement =
DBConn.createStatement("DELETE FROM account_buddy " +
"WHERE account_id = :accountId AND " +
"buddy_id = :buddyId AND " +
"tag_id = :tagId");
statement.params.accountId = aAccountBuddy.account.numericId;
statement.params.buddyId = buddy.id;
statement.params.tagId = aAccountBuddy.tag.id;
statement.execute();
buddy.observe(aAccountBuddy, "account-buddy-removed");
},
accountBuddyMoved: function(aAccountBuddy, aOldTag, aNewTag) {
let buddy = aAccountBuddy.buddy;
let statement =
DBConn.createStatement("UPDATE account_buddy " +
"SET tag_id = :newTagId " +
"WHERE account_id = :accountId AND " +
"buddy_id = :buddyId AND " +
"tag_id = :oldTagId");
statement.params.accountId = aAccountBuddy.account.numericId;
statement.params.buddyId = buddy.id;
statement.params.oldTagId = aOldTag.id;
statement.params.newTagId = aNewTag.id;
statement.execute();
buddy.observe(aAccountBuddy, "account-buddy-moved");
ContactsById[buddy.contact.id]._moved(aOldTag, aNewTag);
},
QueryInterface: XPCOMUtils.generateQI([Ci.imIContactsService]),
classDescription: "Contacts",
classID: Components.ID("{8c3725dd-ee26-489d-8135-736015af8c7f}"),
contractID: "@instantbird.org/purple/contacts-service;1"
};
const NSGetFactory = XPCOMUtils.generateNSGetFactory([ContactsService,
TagsService]);

Просмотреть файл

@ -0,0 +1,4 @@
component {8c3725dd-ee26-489d-8135-736015af8c7f} imContacts.js
contract @instantbird.org/purple/contacts-service;1 {8c3725dd-ee26-489d-8135-736015af8c7f}
component {1fa92237-4303-4384-b8ac-4e65b50810a5} imContacts.js
contract @instantbird.org/purple/tags-service;1 {1fa92237-4303-4384-b8ac-4e65b50810a5}

Просмотреть файл

@ -41,6 +41,7 @@ var EXPORTED_SYMBOLS = [
"nsSimpleEnumerator",
"EmptyEnumerator",
"GenericAccountPrototype",
"GenericAccountBuddyPrototype",
"GenericConversationPrototype",
"GenericProtocolPrototype",
"ForwardProtocolPrototype",
@ -133,8 +134,14 @@ const GenericAccountPrototype = {
cancelReconnection: function() this._base.cancelReconnection(),
createConversation: function(aName) this._base.createConversation(aName),
addBuddy: function(aTag, aName) this._base.addBuddy(aTag, aName),
loadBuddy: function(aBuddy, aName, aAlias, aServerAlias, aTag)
this._base.loadBuddy(aBuddy, aName, aAlias, aServerAlias, aTag),
loadBuddy: function(aBuddy, aTag) {
try {
return new AccountBuddy(this, aBuddy, aTag) ;
} catch (x) {
dump(x + "\n");
return null;
}
},
getChatRoomFields: function() this._base.getChatRoomFields(),
getChatRoomDefaultFieldValues: function(aDefaultChatName)
this._base.getChatRoomDefaultFieldValues(aDefaultChatName),
@ -196,6 +203,133 @@ const GenericAccountPrototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.purpleIAccount, Ci.nsIClassInfo])
};
var GenericAccountBuddyPrototype = {
_init: function(aAccount, aBuddy, aTag) {
this._tag = aTag;
this._account = aAccount;
this._buddy = aBuddy;
},
get account() this._account,
set buddy(aBuddy) {
if (this._buddy)
throw Components.results.NS_ERROR_ALREADY_INITIALIZED;
this._buddy = aBuddy;
},
get buddy() this._buddy,
get tag() this._tag,
set tag(aNewTag) {
let oldTag = this._tag;
this._tag = aNewTag;
Components.classes["@instantbird.org/purple/contacts-service;1"]
.getService(Ci.imIContactsService)
.accountBuddyMoved(this, oldTag, aNewTag);
},
_notifyObservers: function(aTopic, aData) {
this._buddy.observe(this, "account-buddy-" + aTopic, aData);
},
get userName() this._buddy.userName, // FIXME
get normalizedName() this._buddy.normalizedName, //FIXME
_serverAlias: "",
get serverAlias() this._serverAlias,
set serverAlias(aNewAlias) {
let old = this.displayName;
this._serverAlias = aNewAlias;
this._notifyObservers("display-name-changed", old);
},
remove: function() {
Components.classes["@instantbird.org/purple/contacts-service;1"]
.getService(Ci.imIContactsService)
.accountBuddyRemoved(this);
},
// imIStatusInfo implementation
get displayName() this.serverAlias || this.userName,
_buddyIconFileName: "",
get buddyIconFilename() this._buddyIconFileName,
set buddyIconFilename(aNewFileName) {
this._buddyIconFileName = aNewFileName;
this._notifyObservers("icon-changed");
},
_statusType: 0,
get statusType() this._statusType,
get online() this._statusType > Ci.imIStatusInfo.STATUS_OFFLINE,
get available() this._statusType == Ci.imIStatusInfo.STATUS_AVAILABLE,
get idle() this._statusType == Ci.imIStatusInfo.STATUS_IDLE,
get mobile() this._statusType == Ci.imIStatusInfo.STATUS_MOBILE,
_statusText: "",
get statusText() this._statusText,
// This is for use by the protocol plugin, it's not exposed in the
// imIStatusInfo interface.
// All parameters are optional and will be ignored if they are null
// or undefined.
setStatus: function(aStatusType, aStatusText, aAvailabilityDetails) {
// Ignore omitted parameters.
if (aStatusType === undefined || aStatusType === null)
aStatusType = this._statusType;
if (aStatusText === undefined || aStatusText === null)
aStatusText = this._statusText;
if (aAvailabilityDetails === undefined || aAvailabilityDetails === null)
aAvailabilityDetails = this._availabilityDetails;
// Decide which notifications should be fired.
let notifications = [];
if (this._statusType != aStatusType ||
this._availabilityDetails != aAvailabilityDetails)
notifications.push("availability-changed");
if (this._statusType != aStatusType ||
this._statusText != aStatusText) {
notifications.push("status-changed");
if (this.online && aStatusType <= Ci.imIStatusInfo.STATUS_OFFLINE)
notifications.push("signed-off");
if (!this.online && aStatusType > Ci.imIStatusInfo.STATUS_OFFLINE)
notifications.push("signed-on");
}
// Actually change the stored status.
[this._statusType, this._statusText, this._availabilityDetails] =
[aStatusType, aStatusText, aAvailabilityDetails];
// Fire the notifications.
notifications.forEach(function(aTopic) {
this._notifyObservers(aTopic);
}, this);
},
_availabilityDetails: 0,
get availabilityDetails() this._availabilityDetails,
get canSendMessage() this.online /*|| this.account.canSendOfflineMessage(this) */,
getTooltipInfo: function() {
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
},
createConversation: function() {
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
},
getInterfaces: function(countRef) {
var interfaces = [Ci.nsIClassInfo, Ci.nsISupports, Ci.imIAccountBuddy];
countRef.value = interfaces.length;
return interfaces;
},
getHelperForLanguage: function(language) null,
implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,
flags: 0,
QueryInterface: XPCOMUtils.generateQI([Ci.imIAccountBuddy, Ci.nsIClassInfo])
};
function AccountBuddy(aAccount, aBuddy, aTag) {
this._init(aAccount, aBuddy, aTag);
}
AccountBuddy.prototype = GenericAccountBuddyPrototype;
function Message(aWho, aMessage, aObject)
{
this.id = ++Message.prototype._lastId;
@ -372,6 +506,7 @@ const ForwardProtocolPrototype = {
let proto = this;
let account = {
_base: this.base.getAccount(aKey, aName),
loadBuddy: function(aBuddy, aTag) this._base.loadBuddy(aBuddy, aTag),
get normalizedName() this._base.normalizedName,
get protocol() proto
};