Support hiding some tags in the buddy list, and add an 'Other Contacts' special group for contacts which aren't in any visible group.

This commit is contained in:
Florian Quèze 2011-04-02 01:36:51 +02:00
Родитель fc75462d58
Коммит f94552df58
10 изменённых файлов: 397 добавлений и 34 удалений

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

@ -41,6 +41,8 @@ pref("messenger.accounts.promptOnDelete", true);
pref("messenger.accounts.reconnectTimer", "1,5,30,60,90,300,600,1200,3600");
pref("messenger.buddies.showOffline", false);
pref("messenger.buddies.hiddenTags", "");
pref("messenger.buddies.hideTagPrompt", true);
pref("messenger.conversations.openInTabs", true);
pref("messenger.conversations.useSeparateWindowsForMUCs", false);

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

@ -56,6 +56,7 @@ buddy {
group {
-moz-binding: url("chrome://instantbird/content/group.xml#group");
-moz-box-align: center;
}
/* The height is required for the animation to work. The skin can
@ -125,16 +126,20 @@ tooltip[type="buddy"] {
-moz-binding: url('chrome://global/content/bindings/textbox.xml#textbox');
}
.hideGroupButton,
.startChatBubble,
.expander-up,
.expander-down {
cursor: pointer;
}
.startChatBubble {
.hideGroupButton,
.startChatBubble
{
display: none;
}
group:not([id="group-1"]):hover .hideGroupButton,
contact[cansend]:hover .startChatBubble,
contact[cansend][selected] .startChatBubble {
display: -moz-box;

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

@ -41,6 +41,8 @@ const events = ["contact-availability-changed",
"contact-added",
"contact-moved",
"status-changed",
"tag-hidden",
"tag-shown",
"purple-quit"];
const showOfflineBuddiesPref = "messenger.buddies.showOffline";
@ -69,8 +71,22 @@ function buddyListContextMenu(aXulMenu) {
"context-show-offline-buddies-separator"
].forEach(function (aId) {
document.getElementById(aId).hidden = hide;
});
[ "context-hide-tag",
"context-visible-tags"
].forEach(function (aId) {
document.getElementById(aId).hidden = !this.onGroup;
}, this);
if (this.onGroup) {
document.getElementById("context-hide-tag").disabled =
this.target.tag.id == -1;
}
document.getElementById("context-show-offline-buddies-separator").hidden =
hide && !this.onGroup;
let detach = document.getElementById("context-detach");
detach.hidden = !this.onBuddy;
if (this.onBuddy)
@ -192,6 +208,62 @@ buddyListContextMenu.prototype = {
"Logs", "chrome,resizable", {logs: logs},
this.target.displayName);
},
hideTag: function blcm_hideTag() {
if (!this.onGroup || this.target.tag.id == -1)
return;
this.target.hide();
},
visibleTagsPopupShowing: function blcm_visibleTagsPopupShowing() {
if (!this.onGroup)
return;
let popup = document.getElementById("context-visible-tags-popup");
let item;
while ((item = popup.firstChild) && item.localName != "menuseparator")
popup.removeChild(item);
let sortFunction = function (a, b) {
let [a, b] = [a.name.toLowerCase(), b.name.toLowerCase()];
return a < b ? 1 : a > b ? -1 : 0;
};
Services.tags.getTags()
.sort(sortFunction)
.forEach(function (aTag) {
item = document.createElement("menuitem");
item.setAttribute("label", aTag.name);
item.setAttribute("type", "checkbox");
let id = aTag.id;
item.groupId = id;
if (!Services.tags.isTagHidden(aTag))
item.setAttribute("checked", "true");
popup.insertBefore(item, popup.firstChild);
});
let otherContactsTag = document.getElementById("group-1");
[ "context-other-contacts-tag-separator",
"context-other-contacts-tag"
].forEach(function (aId) {
document.getElementById(aId).hidden = !otherContactsTag;
});
if (otherContactsTag) {
// This avoids having the localizable "Other Contacts" string in
// both a .dtd and .properties file.
document.getElementById("context-other-contacts-tag").label =
otherContactsTag.displayName;
}
},
visibleTags: function blcm_visibleTags(aEvent) {
let id = aEvent.originalTarget.groupId;
if (!id)
return;
let tags = Services.tags;
let tag = tags.getTagById(id);
if (tags.isTagHidden(tag))
tags.showTag(tag);
else
tags.hideTag(tag);
},
toggleShowOfflineBuddies: function blcm_toggleShowOfflineBuddies() {
let newValue =
!!document.getElementById("context-show-offline-buddies")
@ -216,19 +288,20 @@ var buddyList = {
else
item.removeAttribute("checked");
let blistBox = document.getElementById("buddylistbox");
Services.tags.getTags().forEach(function (aTag) {
let elt = document.getElementById("group" + aTag.id);
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 (Services.tags.isTagHidden(aTag))
this.showOtherContacts();
else
this.displayGroup(aTag);
}
});
}, this);
let elt = document.getElementById("group-1"); // "Other contacts""
if (elt)
elt.showOffline = showOffline;
return;
}
@ -237,23 +310,24 @@ var buddyList = {
return;
}
if (aTopic == "tag-hidden") {
this.showOtherContacts();
return;
}
if (aTopic == "tag-shown") {
if (!document.getElementById("group" + aSubject.id))
this.displayGroup(aSubject);
return;
}
// 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);
}
}
if (Services.tags.isTagHidden(aTag))
this.showOtherContacts();
else if (!document.getElementById("group" + aTag.id))
this.displayGroup(aTag);
}, this);
}
},
@ -420,22 +494,35 @@ var buddyList = {
.setAttribute("checked", "true");
}
let blistBox = document.getElementById("buddylistbox");
let showOtherContacts = false;
Services.tags.getTags().forEach(function (aTag) {
let groupElt = document.createElement("group");
blistBox.appendChild(groupElt);
if (buddyList._showOffline)
groupElt._showOffline = true;
if (!groupElt.build(aTag))
blistBox.removeChild(groupElt);
if (Services.tags.isTagHidden(aTag))
showOtherContacts = true;
else
buddyList.displayGroup(aTag);
});
blistBox.focus();
if (showOtherContacts)
buddyList.showOtherContacts();
document.getElementById("buddylistbox").focus();
prefBranch.addObserver(showOfflineBuddiesPref, buddyList, false);
addObservers(buddyList, events);
this.addEventListener("unload", buddyList.unload, false);
},
displayGroup: function(aTag) {
let blistBox = document.getElementById("buddylistbox");
let groupElt = document.createElement("group");
blistBox.insertBefore(groupElt, document.getElementById("group-1"));
if (this._showOffline)
groupElt._showOffline = true;
if (!groupElt.build(aTag))
blistBox.removeChild(groupElt);
},
showOtherContacts: function bl_showOtherContacts() {
if (!document.getElementById("group-1"))
this.displayGroup(Services.tags.otherContactsTag);
},
unload: function bl_unload() {
removeObservers(buddyList, events);
Services.prefs.removeObserver(showOfflineBuddiesPref, buddyList);

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

@ -117,6 +117,21 @@
oncommand="gBuddyListContextMenu.moveToNewTag();"/>
</menupopup>
</menu>
<menuitem id="context-hide-tag"
label="&hideTagCmd.label;"
accesskey="&hideTagCmd.accesskey;"
oncommand="gBuddyListContextMenu.hideTag();"/>
<menu id="context-visible-tags"
label="&visibleTagsCmd.label;"
accesskey="&visibleTagsCmd.accesskey;">
<menupopup id="context-visible-tags-popup"
oncommand="gBuddyListContextMenu.visibleTags(event);"
onpopupshowing="gBuddyListContextMenu.visibleTagsPopupShowing();">
<menuseparator id="context-other-contacts-tag-separator"/>
<menuitem id="context-other-contacts-tag"
checked="true" disabled="true"/>
</menupopup>
</menu>
<menuseparator id="context-show-offline-buddies-separator"/>
<menuitem id="context-show-offline-buddies"
label="&showOfflineBuddiesCmd.label;"

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

@ -36,8 +36,10 @@
-
- ***** END LICENSE BLOCK ***** -->
<!DOCTYPE bindings>
<!DOCTYPE bindings [
<!ENTITY % instantbirdDTD SYSTEM "chrome://instantbird/locale/instantbird.dtd" >
%instantbirdDTD;
]>
<bindings id="groupBindings"
xmlns="http://www.mozilla.org/xbl"
@ -48,6 +50,8 @@
<content persist="closed">
<xul:image class="twisty"/>
<xul:label flex="1" crop="end" xbl:inherits="value=name"/>
<xul:button anonid="hideGroupButton" class="hideGroupButton"
tooltiptext="&hideGroupTooltip;"/>
</content>
<implementation implements="nsIObserver">
@ -90,6 +94,15 @@
this.setAttribute("closed", "true");
contacts.forEach(this.addContact, this);
let name;
if (this.tag.id != -1)
name = this.tag.name;
else {
name = Services.strings.createBundle("chrome://instantbird/locale/instantbird.properties")
.GetStringFromName("group.otherContacts.name");
}
this.displayName = name;
this._updateGroupLabel();
this.tag.addObserver(this);
return true;
@ -150,6 +163,16 @@
}
return;
}
if (aTopic == "tag-hidden") {
this.setAttribute("collapsing", "true");
this.addEventListener("transitionend", this._transitionEnd, true);
for each (let contact in this.contacts) {
contact.state = "collapsing";
contact.finishRemoveNode();
}
return;
}
]]>
</body>
</method>
@ -214,6 +237,38 @@
</body>
</method>
<method name="hide">
<body>
<![CDATA[
const promptPrefName = "messenger.buddies.hideTagPrompt";
if (Services.prefs.getBoolPref(promptPrefName)) {
let bundle =
Services.strings.createBundle("chrome://instantbird/locale/instantbird.properties");
let name = this.displayName;
let promptTitle =
bundle.formatStringFromName("group.hidePrompt.title", [name], 1);
let promptMessage =
bundle.formatStringFromName("group.hidePrompt.message", [name], 1);
let hideButton = bundle.GetStringFromName("group.hidePrompt.button");
let promptCheckbox =
bundle.GetStringFromName("group.hidePrompt.checkbox");
let prompts = Services.prompt;
let checkbox = {};
let flags = prompts.BUTTON_TITLE_IS_STRING * prompts.BUTTON_POS_0 +
prompts.BUTTON_TITLE_CANCEL * prompts.BUTTON_POS_1 +
prompts.BUTTON_POS_0_DEFAULT;
if (prompts.confirmEx(window, promptTitle, promptMessage, flags,
hideButton, null, null, promptCheckbox, checkbox))
return;
if (!checkbox.value)
Services.prefs.setBoolPref(promptPrefName, false);
}
Services.tags.hideTag(this.tag);
]]>
</body>
</method>
<method name="_updateClosedState">
<parameter name="aClosed"/>
<body>
@ -241,10 +296,11 @@
</body>
</method>
<field name="displayName"></field>
<method name="_updateGroupLabel">
<body>
<![CDATA[
let name = this.tag.name;
let name = this.displayName;
if (this.hasAttribute("closed"))
name += " (" + this.contacts.length + ")";
@ -284,6 +340,9 @@
if ((event.detail == 1 && event.originalTarget.localName == "image") ||
(event.detail == 2 && event.originalTarget.localName == "label"))
this.close();
if (event.originalTarget.localName == "button")
this.hide();
]]>
</handler>
</handlers>

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

@ -90,12 +90,18 @@
<!ENTITY moveNewTagCmd.accesskey "N">
<!ENTITY showLogsCmd.label "Show Logs">
<!ENTITY showLogsCmd.accesskey "L">
<!ENTITY hideTagCmd.label "Hide Tag">
<!ENTITY hideTagCmd.accesskey "H">
<!ENTITY visibleTagsCmd.label "Visible Tags…">
<!ENTITY visibleTagsCmd.accesskey "V">
<!ENTITY showOfflineBuddiesCmd.label "Show offline buddies">
<!ENTITY showOfflineBuddiesCmd.accesskey "o">
<!ENTITY expandContactTooltip "Expand">
<!ENTITY collapseContactTooltip "Collapse">
<!ENTITY hideGroupTooltip "Hide">
<!-- Copied from mozilla/browser/locales/en-US/chrome/browser/baseMenuOverlay.dtd -->
<!-- Mac OS X Window Menu -->
<!ENTITY windowMenu.label "Window">

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

@ -34,6 +34,24 @@ buddy.deletePrompt.message=%S will be permanently removed from your %S buddy lis
# used as the accesskey for this button.
buddy.deletePrompt.button=&Delete
group.otherContacts.name=Other Contacts
#LOCALIZATION NOTE
# %S here will be replaced by the tag name
group.hidePrompt.title=Hide %S?
#LOCALIZATION NOTE %S here will be replaced by the tag name.
# The translation for 'Visible Tags…' here should match the translation of
# visibleTagsCmd.label in instantbird.dtd
# The translation for 'Other Contacts' should match group.otherContacts.name
group.hidePrompt.message=The tag '%S' will no longer be visible. Use the 'Visible Tags…' context menu item to show it again.\n\nContacts that have no visible tag will be displayed in the 'Other Contacts' special group at the bottom of the list.
#LOCALIZATION NOTE
# the & symbol indicates the position of the character that should be
# used as the accesskey for this button.
group.hidePrompt.button=&Hide
group.hidePrompt.checkbox=Show next time
availableStatusType=Available
awayStatusType=Away
unavailableStatusType=Unavailable

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

@ -99,6 +99,7 @@ buddy[droptarget] {
min-height: 40px;
}
.hideGroupButton,
.startChatBubble,
.expander-up,
.expander-down {
@ -110,6 +111,29 @@ buddy[droptarget] {
-moz-appearance: none;
}
%ifdef XP_UNIX
%ifndef XP_MACOSX
%define UNIX_BUT_NOT_MAC
%endif
%endif
.hideGroupButton {
-moz-margin-end: 0;
-moz-stack-sizing: ignore;
%ifdef UNIX_BUT_NOT_MAC
list-style-image: url("moz-icon://stock/gtk-close?size=menu");
%else
list-style-image: url("chrome://global/skin/icons/close.png");
-moz-image-region: rect(0, 16px, 16px, 0);
}
.hideGroupButton:hover {
-moz-image-region: rect(0, 32px, 16px, 16px);
}
.hideGroupButton:hover:active {
-moz-image-region: rect(0, 48px, 16px, 32px);
%endif
}
.startChatBubble {
list-style-image: url('chrome://instantbird/skin/prpl-generic/icon.png');
}
@ -135,7 +159,9 @@ buddy[droptarget] {
max-height: 16px;
}
.protoIcon, .statusIcon, .startChatBubble, .expander-up, .expander-down {
.protoIcon, .statusIcon,
.hideGroupButton, .startChatBubble,
.expander-up, .expander-down {
width: 16px;
height: 16px;
min-height: 16px;
@ -206,6 +232,7 @@ group[closed] .twisty {
%ifdef XP_MACOSX
group .twisty {
width: 9px;
height: 9px;
-moz-margin-end: 3px;
-moz-margin-start: 3px;
background: url("chrome://global/skin/arrow/arrow-dn-sharp.gif") no-repeat center;

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

@ -77,6 +77,12 @@ interface imITagsService: nsISupports {
// Get an array of all existing tags.
void getTags([optional] out unsigned long tagCount,
[retval, array, size_is(tagCount)] out imITag tags);
boolean isTagHidden(in imITag aTag);
void hideTag(in imITag aTag);
void showTag(in imITag aTag);
readonly attribute imITag otherContactsTag;
};
[scriptable, uuid(c211e5e2-f0a4-4a86-9e4c-3f6b905628a5)]

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

@ -91,6 +91,14 @@ TagsService.prototype = {
return Tags;
},
isTagHidden: function(aTag) aTag.id in otherContactsTag._hiddenTags,
hideTag: function(aTag) { otherContactsTag.hideTag(aTag); },
showTag: function(aTag) { otherContactsTag.showTag(aTag); },
get otherContactsTag() {
otherContactsTag._initContacts();
return otherContactsTag;
},
QueryInterface: XPCOMUtils.generateQI([Ci.imITagsService]),
classDescription: "Tags",
classID: Components.ID("{1fa92237-4303-4384-b8ac-4e65b50810a5}"),
@ -159,6 +167,134 @@ Tag.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.imITag, Ci.nsIClassInfo])
};
var otherContactsTag = {
hiddenTagsPref: "messenger.buddies.hiddenTags",
_hiddenTags: {},
_contactsInitialized: false,
_saveHiddenTagsPref: function() {
Services.prefs.setCharPref(this.hiddenTagsPref,
[id for (id in this._hiddenTags)].join(","));
},
showTag: function(aTag) {
aTag.removeObserver(this);
let id = aTag.id;
delete this._hiddenTags[id];
for each (let contact in this._contacts)
if (contact.getTags().some(function(t) t.id == id))
this._removeContact(contact);
aTag.notifyObservers(aTag, "tag-shown", null);
Services.obs.notifyObservers(aTag, "tag-shown", null);
this._saveHiddenTagsPref();
},
hideTag: function(aTag) {
if (aTag.id < 0 || aTag.id in otherContactsTag._hiddenTags)
return;
this._hiddenTags[aTag.id] = aTag;
if (this._contactsInitialized)
this._hideTag(aTag);
aTag.notifyObservers(aTag, "tag-hidden", null);
Services.obs.notifyObservers(aTag, "tag-hidden", null);
this._saveHiddenTagsPref();
},
_hideTag: function(aTag) {
for each (let contact in aTag.getContacts())
if (!(contact.id in this._contacts) &&
contact.getTags().every(function(t) t.id in this._hiddenTags, this))
this._addContact(contact);
aTag.addObserver(this);
},
observe: function(aSubject, aTopic, aData) {
if (aTopic != "contact-moved-in" && aTopic != "contact-moved-out")
return;
try {
aSubject.QueryInterface(Ci.imIContact);
} catch (e) {
// TODO Most likely, aSubject is a tag. If a tag was added to a
// contact we currently track, we should check that the new tag
// is hidden, and if it is not, remove the contact from the
// 'other contacts' group.
}
if (aTopic == "contact-moved-in" && !(aSubject.id in this._contacts) &&
aSubject.getTags().every(function(t) t.id in this._hiddenTags, this))
this._addContact(aSubject);
else if (aTopic == "contact-moved-out" && aSubject.id in this._contacts &&
aSubject.getTags().some(function(t) !(t.id in this._hiddenTags)))
this._removeContact(aSubject);
},
_initHiddenTags: function() {
let pref = Services.prefs.getCharPref(this.hiddenTagsPref);
if (!pref)
return;
for each (let tagId in pref.split(","))
this._hiddenTags[tagId] = TagsById[tagId];
},
_initContacts: function() {
if (this._contactsInitialized)
return;
this._observers = [];
this._observer = {observe: this.notifyObservers.bind(this)};
this._contacts = {};
this._contactsInitialized = true;
for each (let tag in this._hiddenTags)
this._hideTag(tag);
},
// imITag implementation
get id() -1,
get name() "__others__",
set name(aNewName) { throw Cr.NS_ERROR_NOT_AVAILABLE; },
getContacts: function(aContactCount) {
let contacts = [contact for each (contact in this._contacts)];
if (aContactCount)
aContactCount.value = contacts.length;
return contacts;
},
_addContact: function (aContact) {
this._contacts[aContact.id] = aContact;
this.notifyObservers(aContact, "contact-moved-in");
for each (let observer in ContactsById[aContact.id]._observers)
observer.observe(this, "contact-moved-in", null);
aContact.addObserver(this._observer);
},
_removeContact: function (aContact) {
delete this._contacts[aContact.id];
aContact.removeObserver(this._observer);
this.notifyObservers(aContact, "contact-moved-out");
for each (let observer in ContactsById[aContact.id]._observers)
observer.observe(this, "contact-moved-out", null);
},
addObserver: function(aObserver) {
if (this._observers.indexOf(aObserver) == -1)
this._observers.push(aObserver);
},
removeObserver: function(aObserver) {
this._observers = this._observers.filter(function(o) o !== aObserver);
},
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.nsIObserver, Ci.imITag];
countRef.value = interfaces.length;
return interfaces;
},
getHelperForLanguage: function(language) null,
implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,
flags: 0,
QueryInterface: XPCOMUtils.generateQI([Ci.imITag, Ci.nsIObserver, Ci.nsIClassInfo])
};
var ContactsById = { };
var LastDummyContactId = 0;
function Contact(aId, aAlias) {
@ -991,6 +1127,8 @@ ContactsService.prototype = {
dump(e + "\n");
}
}
otherContactsTag._initHiddenTags();
},
unInitContacts: function() {
AccountsById = { };