Bug 953891 - Participants Need Context Menu, r=florian.

This commit is contained in:
aleth 2013-08-29 01:12:25 +02:00
Родитель ff0044d321
Коммит 6bc43a6ffc
11 изменённых файлов: 254 добавлений и 81 удалений

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

@ -141,6 +141,11 @@ TagsService.prototype = {
},
// Get an array of all existing tags.
getTags: function(aTagCount) {
if (Tags.length)
Tags.sort(function(a, b) a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
else
this.defaultTag;
if (aTagCount)
aTagCount.value = Tags.length;
return Tags;

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

@ -408,7 +408,8 @@ const messageReplacements = {
return (aMsg.incoming ? "Incoming" : "Outgoing") + "/buddy_icon.png";
},
senderScreenName: function(aMsg) TXTToHTML(aMsg.who),
sender: function(aMsg) TXTToHTML(aMsg.alias || aMsg.who),
sender: function(aMsg)
"<span class=\"ib-sender\">" + TXTToHTML(aMsg.alias || aMsg.who) + "</span>",
senderColor: function(aMsg) aMsg.color,
senderStatusIcon: function(aMsg)
getStatusIconFromBuddy(getBuddyFromMessage(aMsg)),

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

@ -28,13 +28,7 @@ var addBuddy = {
buildTagList: function ab_buildTagList() {
var tagList = document.getElementById("taglist");
let tags = Services.tags.getTags();
if (!tags.length) {
let bundle = document.getElementById("instantbirdBundle");
tags.push(Services.tags.defaultTag);
}
tags.forEach(function (tag) {
Services.tags.getTags().forEach(function(tag) {
tagList.appendItem(tag.name, tag.id);
});
tagList.selectedIndex = 0;

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

@ -86,6 +86,10 @@ function buddyListContextMenu(aXulMenu) {
document.getElementById("context-openconversation").disabled =
!hide && !this.target.canOpenConversation();
Components.utils.import("resource:///modules/ibTagMenu.jsm");
this.tagMenu = new TagMenu(this, window,
this.onBuddy ? this.target.contact : this.target);
}
// Prototype for buddyListContextMenu "class."
@ -141,64 +145,16 @@ buddyListContextMenu.prototype = {
this.target.remove();
},
tagsPopupShowing: function blcm_tagsPopupShowing() {
if (!this.onContact && !this.onBuddy)
return;
let popup = document.getElementById("context-tags-popup");
let item;
while ((item = popup.firstChild) && item.localName != "menuseparator")
popup.removeChild(item);
let contact = (this.onBuddy ? this.target.contact : this.target).contact;
let tags = contact.getTags();
let groupId =
(this.onBuddy ? this.target.contact : this.target).group.groupId;
let sortFunction = function (a, b) {
[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 (tags.some(function (t) t.id == id)) {
item.setAttribute("checked", "true");
if (tags.length == 1)
item.setAttribute("disabled", "true"); // can't remove the last tag.
}
popup.insertBefore(item, popup.firstChild);
});
addTag: function blcm_addTag(aTag) {
// If the contact already has the tag, addTag will return early.
this.tagMenu.target.contact.addTag(aTag);
},
tag: function blcm_tag(aEvent) {
let id = aEvent.originalTarget.groupId;
if (!id)
return;
let tag = Services.tags.getTagById(id);
let contact = (this.onBuddy ? this.target.contact : this.target).contact;
if (contact.getTags().some(function (t) t.id == id))
contact.removeTag(tag);
toggleTag: function blcm_toggleTag(aTag) {
let contact = this.tagMenu.target.contact;
if (contact.getTags().some(function(t) t.id == aTag.id))
contact.removeTag(aTag);
else
contact.addTag(tag);
},
addNewTag: function blcm_addNewTag() {
let bundle = document.getElementById("instantbirdBundle").stringBundle;
let title = bundle.GetStringFromName("newTagPromptTitle");
let message = bundle.GetStringFromName("newTagPromptMessage");
let name = {};
if (!Services.prompt.prompt(window, title, message, name, null,
{value: false}) || !name.value)
return; // the user canceled
let contact = (this.onBuddy ? this.target.contact : this.target).contact;
// If the tag already exists, createTag will return it, and if the
// contact already has it, addTag will return early.
contact.addTag(Services.tags.createTag(name.value));
contact.addTag(aTag);
},
_getLogs: function blcm_getLogs() {
if (this.onContact)

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

@ -85,13 +85,13 @@
label="&tagsCmd.label;"
accesskey="&tagsCmd.accesskey;">
<menupopup id="context-tags-popup"
oncommand="gBuddyListContextMenu.tag(event);"
onpopupshowing="gBuddyListContextMenu.tagsPopupShowing();">
oncommand="gBuddyListContextMenu.tagMenu.tag(event, gBuddyListContextMenu.toggleTag.bind(gBuddyListContextMenu));"
onpopupshowing="gBuddyListContextMenu.tagMenu.tagsPopupShowing();">
<menuseparator id="context-create-tag-separator"/>
<menuitem id="context-create-tag"
label="&addNewTagCmd.label;"
accesskey="&addNewTagCmd.accesskey;"
oncommand="gBuddyListContextMenu.addNewTag();"/>
oncommand="gBuddyListContextMenu.tagMenu.addNewTag(gBuddyListContextMenu.addTag.bind(gBuddyListContextMenu));"/>
</menupopup>
</menu>
<menuitem id="context-hide-tag"

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

@ -42,6 +42,7 @@
</xul:hbox>
<xul:listbox anonid="nicklist" class="conv-nicklist"
flex="1" seltype="multiple"
xbl:inherits="contextmenu=contentcontextmenu"
tooltip="buddyTooltip"
onclick="onNickClick(event);"
onkeypress="onNicklistKeyPress(event);"/>

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

@ -126,8 +126,25 @@
<tooltip id="buddyTooltip" type="buddy"/>
<menupopup id="contentAreaContextMenu"
onpopupshowing="if (event.target != this) return true; gContextMenu = new nsContextMenu(this, window.getTabBrowser()); return gContextMenu.shouldDisplay;"
onpopupshowing="if (event.target != this) return true; gContextMenu = new nsContextMenu(this, window.getBrowser()); return gContextMenu.shouldDisplay;"
onpopuphiding="if (event.target == this &amp;&amp; gContextMenu) { gContextMenu.cleanup(); gContextMenu = null; }">
<menuitem id="context-nick-openconv"
oncommand="gContextMenu.nickOpenConv();"/>
<menuitem id="context-nick-showlogs"
oncommand="gContextMenu.nickShowLogs();"/>
<menu id="context-nick-addcontact">
<menupopup id="context-tags-popup"
oncommand="gContextMenu.tagMenu.tag(event, gContextMenu.nickAddContact.bind(gContextMenu));"
onpopupshowing="gContextMenu.tagMenu.tagsPopupShowing();">
<menuseparator id="context-create-tag-separator"/>
<menuitem id="context-create-tag"
label="&addNewTagCmd.label;"
accesskey="&addNewTagCmd.accesskey;"
oncommand="gContextMenu.tagMenu.addNewTag(gContextMenu.nickAddContact.bind(gContextMenu));"/>
</menupopup>
</menu>
<menuseparator id="context-sep-nick"/>
<menuitem id="context-openlink"
label="&openLinkCmd.label;"
accesskey="&openLinkCmd.accesskey;"

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

@ -2,12 +2,16 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
Components.utils.import("resource:///modules/ibTagMenu.jsm");
var gContextMenu = null;
function nsContextMenu(aXulMenu, aBrowser) {
this.target = null;
this.browser = null;
this.conv = null;
this.menu = null;
this.tagMenu = null;
this.onLink = false;
this.onMailtoLink = false;
this.onSaveableLink = false;
@ -15,16 +19,23 @@ function nsContextMenu(aXulMenu, aBrowser) {
this.linkURL = "";
this.linkURI = null;
this.linkProtocol = null;
this.onNick = false;
this.nick = "";
this.buddy = null;
this.isNickOpenConv = false;
this.isNickShowLogs = false;
this.isNickAddContact = false;
this.isTextSelected = false;
this.isContentSelected = false;
this.shouldDisplay = true;
this.ellipsis = "\u2026";
try {
this.ellipsis =
Services.prefs.getComplexValue("intl.ellipsis",
Ci.nsIPrefLocalizedString).data;
} catch (e) { }
} catch (e) {
this.ellipsis = "\u2026";
}
// Initialize new menu.
this.initMenu(aXulMenu, aBrowser);
@ -46,11 +57,26 @@ nsContextMenu.prototype = {
initMenu: function CM_initMenu(aPopup, aBrowser) {
this.menu = aPopup;
this.browser = aBrowser;
this.conv = this.browser._conv;
// Get contextual info.
let node = document.popupNode;
if (node.localName == "listbox") {
// Clicked the participant list, but not a listitem.
this.shouldDisplay = false;
return;
}
this.setTarget(node);
let isParticipantList = node.localName == "listitem";
let nickActions = this.getNickActions(isParticipantList);
this.onNick = nickActions.some(function(action) action.visible);
if (isParticipantList && !this.onNick) {
// If we're in the participant list, there will be no other entries.
this.shouldDisplay = false;
return;
}
let actions = [];
while (node) {
if (node._originalMsg) {
@ -66,7 +92,7 @@ nsContextMenu.prototype = {
// Initialize (disable/remove) menu items.
// Open/Save/Send link depends on whether we're in a link.
var shouldShow = this.onSaveableLink;
let shouldShow = this.onSaveableLink;
this.showItem("context-openlink", shouldShow);
this.showItem("context-sep-open", shouldShow);
this.showItem("context-savelink", shouldShow);
@ -80,7 +106,8 @@ nsContextMenu.prototype = {
goUpdateGlobalEditMenuItems();
this.showItem("context-copy", this.isContentSelected);
this.showItem("context-selectall", !this.onLink || this.isContentSelected);
this.showItem("context-selectall", (!this.onNick && !this.onLink) ||
this.isContentSelected);
this.showItem("context-sep-selectall", actions.length);
this.showItem("context-sep-messageactions", this.isTextSelected);
@ -91,6 +118,13 @@ nsContextMenu.prototype = {
this.showItem("context-copylink", this.onLink && !this.onMailtoLink);
this.showItem("context-sep-copylink", this.onLink && this.isContentSelected);
// Display nick menu items.
let isNonNickItems = this.isContentSelected || this.isTextSelected ||
this.onLink || actions.length;
this.showItem("context-sep-nick", this.onNick && isNonNickItems);
for (let action of nickActions)
this.showItem(action.id, action.visible);
// Display action menu items.
let before = document.getElementById("context-sep-messageactions");
for each (let action in actions) {
@ -102,18 +136,75 @@ nsContextMenu.prototype = {
}
},
getNormalizedName: function(aNick) {
// Unfortunately there is currently no way to obtain the normalizedName
// corresponding to the nick (bug 2115).
// Therefore we may sometimes not find existing logs for a nick,
// and offer "add contact" despite a buddy already existing.
return aNick;
},
getLogsForNick: function(aNick) {
return Services.logs.getLogsForAccountAndName(this.conv.account,
this.getNormalizedName(aNick),
true);
},
getNickActions: function(aIsParticipantList) {
let bundle = document.getElementById("bundle_instantbird");
let nick = this.nick;
let actions = [];
let addAction = function(aId, aVisible) {
let domId = "context-nick-" + aId.toLowerCase();
let stringId = "contextmenu.nick" + aId;
if (!aIsParticipantList)
stringId += ".withNick";
document.getElementById(domId).label =
bundle.getFormattedString(stringId, [nick]);
actions.push({id: domId, visible: aVisible});
};
// Special-case twitter. XXX Drop this when twitter DMs work.
let isTwitter = this.conv.account.protocol.id == "prpl-twitter";
addAction("OpenConv", this.onNick && !isTwitter);
addAction("ShowLogs", this.onNick && this.getLogsForNick(nick).hasMoreElements());
this.buddy = Services.contacts
.getBuddyByNameAndProtocol(this.getNormalizedName(nick),
this.conv.account.protocol);
let isAddContact = this.onNick && !isTwitter && !this.buddy;
if (isAddContact)
this.tagMenu = new TagMenu(this, window);
addAction("AddContact", isAddContact);
return actions;
},
nickOpenConv: function() {
let name = this.conv.target.getNormalizedChatBuddyName(this.nick);
let newConv = this.conv.account.createConversation(name);
Conversations.focusConversation(newConv);
},
nickAddContact: function(aTag)
this.conv.account.addBuddy(aTag, this.nick),
nickShowLogs: function() {
let nick = this.nick;
let enumerator = this.getLogsForNick(nick);
if (!enumerator.hasMoreElements())
return;
window.openDialog("chrome://instantbird/content/viewlog.xul",
"Logs", "chrome,resizable", {logs: enumerator}, nick);
},
// Set various context menu attributes based on the state of the world.
setTarget: function (aNode) {
// Initialize contextual info.
this.onLink = false;
this.linkURL = "";
this.linkURI = null;
this.linkProtocol = "";
setTarget: function(aNode) {
// Remember the node that was clicked.
this.target = aNode;
// Check if we are in the participant list.
if (this.target.localName == "listitem") {
this.onNick = true;
this.nick = this.target.label;
return;
}
// First, do checks for nodes that never have children.
// Second, bubble out, looking for items of interest that can have childen.
// Always pick the innermost link, background image, etc.
@ -155,6 +246,13 @@ nsContextMenu.prototype = {
this.onMailtoLink = (this.linkProtocol == "mailto");
this.onSaveableLink = this.isLinkSaveable(this.link);
}
// Nick?
if (!this.onNick && this.conv.isChat &&
(elem.classList.contains("ib-nick") || elem.classList.contains("ib-sender"))) {
this.nick = elem.textContent;
this.onNick = true;
}
}
elem = elem.parentNode;
@ -223,7 +321,7 @@ nsContextMenu.prototype = {
},
// Open linked-to URL in a new window.
openLink: function (aURI) {
openLink: function(aURI) {
Cc["@mozilla.org/uriloader/external-protocol-service;1"].
getService(Ci.nsIExternalProtocolService).
loadURI(aURI || this.linkURI, window);

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

@ -137,3 +137,17 @@ statusOffline=Your account is disconnected because your status is currently set
# This is shown when the user attempts to send a message to a disconnected account.
# %1$S is the name of the protocol of the account, %2$S the name of the account.
accountDisconnected=Your %1$S account %2$S is disconnected.
#LOCALIZATION NOTE
# These appear in the context menu of the chat participants in the
# participant list.
contextmenu.nickOpenConv=Private Conversation
contextmenu.nickShowLogs=Show Logs
contextmenu.nickAddContact=Add Contact…
# These appear in the context menu for the nicks of chat participants
# highlighted in messages, and extend those for the participant list
# by mentioning the nick.
# %S is the nick of the chat participant.
contextmenu.nickOpenConv.withNick=Private Conversation with %S
contextmenu.nickShowLogs.withNick=Show Logs for %S
contextmenu.nickAddContact.withNick=Add %S to Contacts…

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

@ -13,6 +13,7 @@ EXTRA_JS_MODULES = \
ibInterruptions.jsm \
ibNotifications.jsm \
ibSounds.jsm \
ibTagMenu.jsm \
$(NULL)
EXTRA_PP_JS_MODULES = \

86
im/modules/ibTagMenu.jsm Normal file
Просмотреть файл

@ -0,0 +1,86 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const EXPORTED_SYMBOLS = ["TagMenu"];
const Cu = Components.utils;
Cu.import("resource:///modules/imServices.jsm");
Cu.import("resource:///modules/imXPCOMUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "_", function()
l10nHelper("chrome://instantbird/locale/instantbird.properties")
);
// If a contact binding is given in aTarget, the menu checkmarks the existing
// tags on this contact.
function TagMenu(aParent, aWindow, aTarget = null) {
this.parent = aParent;
this.window = aWindow;
if (aWindow)
this.document = aWindow.document;
this.target = aTarget;
}
TagMenu.prototype = {
document: null,
window: null,
target: null,
tagsPopupShowing: function() {
if (!this.parent.onContact && !this.parent.onBuddy && !this.parent.onNick)
return;
let popup = this.document.getElementById("context-tags-popup");
let item;
while ((item = popup.firstChild) && item.localName != "menuseparator")
popup.removeChild(item);
if (this.target) {
var tags = this.target.contact.getTags();
var groupId = this.target.group.groupId;
}
let allTags = Services.tags.getTags().reverse();
for (let tag of allTags) {
item = this.document.createElement("menuitem");
item.setAttribute("label", tag.name);
let id = tag.id;
item.groupId = id;
if (this.target) {
item.setAttribute("type", "checkbox");
if (tags.some(function(t) t.id == id)) {
item.setAttribute("checked", "true");
if (tags.length == 1)
item.setAttribute("disabled", "true"); // can't remove the last tag.
}
}
popup.insertBefore(item, popup.firstChild);
}
},
tag: function(aEvent, aCallback) {
let id = aEvent.originalTarget.groupId;
if (!id)
return false;
try {
return aCallback(Services.tags.getTagById(id));
} catch(e) {
Cu.reportError(e);
return false;
}
},
addNewTag: function(aCallback) {
let name = {};
if (!Services.prompt.prompt(this.window, _("newTagPromptTitle"),
_("newTagPromptMessage"), name, null,
{value: false}) || !name.value)
return false; // the user canceled
try {
// If the tag already exists, createTag will return it.
return aCallback(Services.tags.createTag(name.value));
} catch(e) {
Cu.reportError(e);
return false;
}
}
};