Put multi-user chats in the 'Contacts' window instead of really closing them when the conversation window is closed.

This commit is contained in:
Florian Quèze 2011-08-24 02:58:24 +02:00
Родитель 0120d44513
Коммит 14ac369d1d
11 изменённых файлов: 355 добавлений и 59 удалений

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

@ -46,6 +46,7 @@ pref("messenger.buddies.hideTagPrompt", true);
pref("messenger.conversations.openInTabs", true);
pref("messenger.conversations.useSeparateWindowsForMUCs", false);
pref("messenger.conversations.alwaysClose", true);
pref("messenger.conversations.selections.magicCopyEnabled", true);
pref("messenger.conversations.selections.ellipsis", "chrome://instantbird/locale/instantbird.properties");

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

@ -147,3 +147,16 @@ contact[cansend]:hover .startChatBubble,
contact[cansend][selected] .startChatBubble {
display: -moz-box;
}
conv {
-moz-binding: url("chrome://instantbird/content/conv.xml#conv");
-moz-box-align: center;
}
.convUnreadCount[value="0"] {
display: none;
}
#buddyListMsg[listedConvCount="0"] > .listboxHeader {
display: none;
}

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

@ -40,9 +40,11 @@ Components.utils.import("resource:///modules/imStatusUtils.jsm");
const events = ["contact-availability-changed",
"contact-added",
"contact-tag-added",
"showing-ui-conversation",
"status-changed",
"tag-hidden",
"tag-shown",
"ui-conversation-hidden",
"user-display-name-changed",
"user-icon-changed",
"purple-quit"];
@ -350,6 +352,20 @@ var buddyList = {
return;
}
if (aTopic == "ui-conversation-hidden") {
let convElt = document.createElement("conv");
this.convBox.appendChild(convElt);
convElt.build(aSubject);
this._updateListConvCount();
return;
}
else if (aTopic == "showing-ui-conversation") {
if (this.convBox.listedConvs.hasOwnProperty(aSubject.id))
this.convBox.listedConvs[aSubject.id].removeNode();
this._updateListConvCount();
return;
}
// aSubject is an imIContact
if (aSubject.online || this._showOffline) {
aSubject.getTags().forEach(function (aTag) {
@ -361,6 +377,11 @@ var buddyList = {
}
},
_updateListConvCount: function() {
let count = Object.keys(this.convBox.listedConvs).length;
this.convBox.parentNode.setAttribute("listedConvCount", count);
},
displayUserIcon: function bl_displayUserIcon() {
let icon = Services.core.getUserIcon();
document.getElementById("userIcon").src = icon ? icon.spec : "";
@ -641,6 +662,21 @@ var buddyList = {
buddyList.showOtherContacts();
blistBox.focus();
buddyList.convBox = document.getElementById("convlistbox");
buddyList.convBox.listedConvs = {};
let convs = Services.conversations.getUIConversations();
if (convs.hasMoreElements()) {
if (!("Conversations" in window))
Components.utils.import("resource:///modules/imWindows.jsm");
for (let conv in getIter(convs)) {
if (Conversations.isUIConversationDisplayed(conv)) {
let convElt = document.createElement("conv");
buddyList.convBox.appendChild(convElt);
convElt.build(conv);
}
}
buddyList._updateListConvCount();
}
prefBranch.addObserver(showOfflineBuddiesPref, buddyList, false);
addObservers(buddyList, events);

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

@ -168,7 +168,10 @@
</toolbar>
</toolbox>
<notificationbox id="buddyListMsg" flex="1">
<notificationbox id="buddyListMsg" flex="1" listedConvCount="0">
<label value="Hidden Conversations" class="listboxHeader"/>
<richlistbox id="convlistbox"/>
<label value="Contacts" class="listboxHeader"/>
<richlistbox id="buddylistbox" flex="1"
onkeypress="buddyList.keyPress(event);"
context="buddyListContextMenu"

178
im/content/conv.xml Normal file
Просмотреть файл

@ -0,0 +1,178 @@
<?xml version="1.0"?>
<!-- ***** 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
- 2007.
-
- The Initial Developer of the Original Code is
- Florian QUEZE <florian@instantbird.org>.
- Portions created by the Initial Developer are Copyright (C) 2007
- 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 ***** -->
<!DOCTYPE bindings [
<!ENTITY % instantbirdDTD SYSTEM "chrome://instantbird/locale/instantbird.dtd" >
%instantbirdDTD;
]>
<bindings id="convBindings"
xmlns="http://www.mozilla.org/xbl"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:xbl="http://www.mozilla.org/xbl"
xmlns:html="http://www.w3.org/1999/xhtml">
<binding id="conv" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
<content>
<xul:stack class="prplBuddyIcon" mousethrough="always">
<xul:image xbl:inherits="src=iconPrpl" class="protoIcon"/>
<xul:image class="statusIcon"/>
</xul:stack>
<xul:hbox flex="1" class="conv-hbox" mousethrough="always">
<xul:label crop="end" flex="1" mousethrough="always"
anonid="displayname" class="convDisplayName"
xbl:inherits="value=displayname"/>
<xul:label crop="end" mousethrough="always"
anonid="unreadCount" class="convUnreadCount"
xbl:inherits="value=unreadCount"/>
</xul:hbox>
</content>
<implementation implements="nsIObserver">
<destructor>
<![CDATA[
if (this.conv) {
this.conv.removeObserver(this);
delete this.conv;
}
]]>
</destructor>
<method name="build">
<parameter name="aConv"/>
<body>
<![CDATA[
this.conv = aConv;
this.parentNode.listedConvs[this.conv.id] = this;
this.conv.addObserver(this);
this.update();
]]>
</body>
</method>
<property name="displayName"
onget="return this.conv.title;"/>
<!-- nsIObserver implementation -->
<method name="observe">
<parameter name="aSubject"/>
<parameter name="aTopic"/>
<parameter name="aData"/>
<body>
<![CDATA[
if (aTopic == "target-purple-conversation-changed" ||
aTopic == "unread-message-count-changed" ||
aTopic == "update-conv-title" ||
aTopic == "update-buddy-status" ||
aTopic == "update-buddy-status" ||
aTopic == "update-conv-chatleft")
this.update();
else if (aTopic == "ui-conversation-closed")
this.removeNode();
]]>
</body>
</method>
<method name="update">
<body>
<![CDATA[
this.setAttribute("displayname", this.displayName);
this.setAttribute("unreadCount", this.conv.unreadMessageCount);
this.setAttribute("iconPrpl",
this.conv.account.protocol.iconBaseURI + "icon.png");
]]>
</body>
</method>
<method name="removeNode">
<body>
<![CDATA[
this.conv.removeObserver(this);
delete this.parentNode.listedConvs[this.conv.id];
this.parentNode.removeChild(this);
delete this.conv;
]]>
</body>
</method>
<method name="openConversation">
<body>
<![CDATA[
if (!("Conversations" in window))
Components.utils.import("resource:///modules/imWindows.jsm");
Conversations.showConversation(this.conv);
]]>
</body>
</method>
<method name="keyPress">
<parameter name="aEvent"/>
<body>this._keyPress(aEvent);</body>
</method>
<method name="_keyPress">
<parameter name="aEvent"/>
<body>
<![CDATA[
switch (aEvent.keyCode) {
// If Enter or Return is pressed, open a new conversation
case aEvent.DOM_VK_RETURN:
case aEvent.DOM_VK_ENTER:
this.openConversation();
break;
}
]]>
</body>
</method>
</implementation>
<handlers>
<handler event="mousedown">
<![CDATA[
if (event.originalTarget.getAttribute("anonid") == "unreadCount")
openConversation();
]]>
</handler>
<handler event="click">
<![CDATA[
if (event.detail == 2)
openConversation();
]]>
</handler>
</handlers>
</binding>
</bindings>

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

@ -125,10 +125,8 @@
<method name="destroy">
<body>
<![CDATA[
if (this._conv) {
this._conv.close();
if (this._conv)
this._forgetConv();
}
if ("MessageFormat" in window) {
let textbox = this.editor;
@ -158,21 +156,19 @@
this.editor.value = aConversation.editor.value;
this.browser.browserResize();
this.updateTyping();
this.loaded = true;
this.observe(this.browser, "conversation-loaded", null);
]]>
</body>
</method>
<field name="loaded">false</field>
<field name="messageQueue">[]</field>
<method name="_emptyMessageQueue">
<method name="_showFirstMessages">
<body>
<![CDATA[
this.loaded = true;
for each(var m in this.messageQueue)
this._addMsg(m);
this.messageQueue = null;
this._conv.getMessages().forEach(this.addMsg.bind(this));
]]>
</body>
</method>
@ -181,36 +177,13 @@
<field name="_statusTextEnd">""</field>
<field name="_statusTextEndIsError">false</field>
<!-- This is used when we want to remove close the conversation binding
without closing the associated PurpleConversation.
For example when quitting the application, we don't want to close the
conversation. -->
<method name="unInit">
<body>
<![CDATA[
this._conv.removeObserver(this);
Conversations.unregisterConversation(this);
this._conv = null;
]]>
</body>
</method>
<method name="addMsg">
<parameter name="aMsg"/>
<body>
<![CDATA[
if (this.loaded)
this._addMsg(aMsg);
else
this.messageQueue.push(aMsg);
]]>
</body>
</method>
if (!this.loaded)
throw "Calling addMsg before the browser is ready?";
<method name="_addMsg">
<parameter name="aMsg"/>
<body>
<![CDATA[
var conv = aMsg.conversation;
if (!conv) {
// The conversation has already been destroyed,
@ -236,12 +209,15 @@
window.getAttention();
this.browser.appendMessage(aMsg);
if (this.tab && aMsg.incoming && !aMsg.system &&
(!this.tab.selected || !document.hasFocus())) {
if (conv.isChat && aMsg.containsNick)
this.tab.setAttribute("attention", "true");
else
this.tab.setAttribute("unread", "true");
if (this.tab && aMsg.incoming && !aMsg.system) {
if (this.tab.selected && document.hasFocus())
this._conv.markAsRead();
else {
if (conv.isChat && aMsg.containsNick)
this.tab.setAttribute("attention", "true");
else
this.tab.setAttribute("unread", "true");
}
}
]]>
</body>
@ -881,6 +857,7 @@
this.editor.focus();
this.tab.removeAttribute("unread");
this.tab.removeAttribute("attention");
this._conv.markAsRead();
this.displayStatusText();
]]>
</body>
@ -1072,7 +1049,8 @@
// Display all queued messages. Use a timeout so that message text
// modifiers can be added with observers for this notification.
setTimeout(function(aSelf) { aSelf._emptyMessageQueue(); }, 0, this);
if (!this.loaded)
setTimeout(this._showFirstMessages.bind(this), 0);
Services.obs.removeObserver(this, "conversation-loaded");
return;
@ -1080,7 +1058,8 @@
switch(aTopic) {
case "new-text":
this.addMsg(aSubject);
if (this.loaded)
this.addMsg(aSubject);
break;
case "update-typing":

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

@ -23,6 +23,7 @@ instantbird.jar:
* content/instantbird/conversation.xml
content/instantbird/convbrowser.xml
content/instantbird/conv.html
content/instantbird/conv.xml
* content/instantbird/credits.xhtml
* content/instantbird/engineManager.js
* content/instantbird/engineManager.xul

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

@ -83,12 +83,17 @@ var Conversations = {
if (index != -1)
this._conversations.splice(index, 1);
if (this._uiConv[aConversation.conv.id] == aConversation)
delete this._uiConv[aConversation.conv.id];
let uiConv = aConversation.conv;
if (this._uiConv[uiConv.id] == aConversation) {
delete this._uiConv[uiConv.id];
if (!uiConv.checkClose())
Services.obs.notifyObservers(uiConv, "ui-conversation-hidden", null);
}
},
isConversationWindowFocused: function()
this._windows.length > 0 && this._windows[0].document.hasFocus(),
isUIConversationDisplayed: function(aUIConv) aUIConv in this._uiConv,
focusConversation: function(aConv) {
let uiConv = Services.conversations.getUIConversation(aConv);
uiConv.target = aConv;
@ -146,25 +151,32 @@ var Conversations = {
if (aTopic != "new-ui-conversation")
return;
let conv = aSubject;
if (!(aSubject.id in this._uiConv)) {
// TODO: let addons customize this behavior.
this.showConversation(aSubject);
},
showConversation: function(aConv) {
if (!(aConv.id in this._uiConv)) {
Services.obs.notifyObservers(aConv, "showing-ui-conversation", null);
// The conversation is not displayed anywhere yet.
// First, check if an existing conversation window can accept it.
for each (let win in this._windows)
if (win.document.getElementById("conversations").addConversation(aSubject))
if (win.document.getElementById("conversations").addConversation(aConv))
return;
// At this point, no existing registered window can accept the conversation.
if (this._pendingConversations) {
// If we are already creating a window, append the notification.
this._pendingConversations.push(aSubject);
this._pendingConversations.push(aConv);
}
else {
// We need to create a new window.
this._pendingConversations = [aSubject];
this._pendingConversations = [aConv];
Services.ww.openWindow(null, CONVERSATION_WINDOW_URI, "_blank",
"chrome,toolbar,resizable", null);
}
}
else
this.focusConversation(aConv.target);
}
};

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

@ -41,6 +41,22 @@ richlistbox {
-moz-appearance: none;
}
.listboxHeader {
-moz-appearance: treeheadercell;
margin: 0 0;
%ifdef XP_MACOSX
margin-right: -1px;
%endif
}
.convUnreadCount {
background-color:red;
padding: 0 7px;
border-radius: 50px;
color: white;
font-weight:bold;
}
contact,
group {
padding: 0 2px;
@ -89,6 +105,7 @@ contact[droptarget] > buddy[dummy] {
.contactDisplayName,
.contactStatusText,
.convDisplayName,
.buddyDisplayName,
.buddyStatusText {
margin: 0;
@ -98,7 +115,8 @@ contact[droptarget] > buddy[dummy] {
-moz-margin-start: 2px;
}
.contact-hbox {
.contact-hbox,
.conv-hbox {
margin: 2px 0;
-moz-margin-start: 2px;
min-height: 16px;

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

@ -38,6 +38,8 @@
#include "purpleIConversation.idl"
#include "imIContactsService.idl"
interface purpleIMessage;
[scriptable, uuid(a09faf46-bb9d-402f-b460-89f8d7827ff1)]
interface imIConversation: purpleIConversation {
// Will be null for MUCs and IMs from people not in the contacts list.
@ -48,6 +50,22 @@ interface imIConversation: purpleIConversation {
void systemMessage(in AUTF8String aMessage);
attribute purpleIConversation target;
// Number of unread incoming messages.
readonly attribute PRUint32 unreadMessageCount;
// Reset unreadMessageCount.
void markAsRead();
// Call this to give the core an opportunity to close an inactive
// conversation. If the conversation is a left MUC or an IM
// conversation without unread message, the implementation will call
// close().
// The returned value indicates if the conversation was closed.
boolean checkClose();
// Get an array of all messages of the conversation.
void getMessages([optional] out unsigned long messageCount,
[retval, array, size_is(messageCount)] out purpleIMessage messages);
};
[scriptable, uuid(984e182c-d395-4fba-ba6e-cc80c71f57bf)]
@ -60,6 +78,7 @@ interface imIConversationsService: nsISupports {
void addConversation(in purpleIConversation aConversation);
void removeConversation(in purpleIConversation aConversation);
nsISimpleEnumerator getUIConversations();
imIConversation getUIConversation(in purpleIConversation aConversation);
imIConversation getUIConversationByContactId(in long aId);

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

@ -53,7 +53,7 @@ function UIConversation(aPurpleConversation)
this._purpleConv = {};
this.id = ++gLastUIConvId;
this._observers = [];
this._pendingMessages = [];
this._messages = [];
this.changeTargetTo(aPurpleConversation);
let iface = Ci["purpleIConv" + (aPurpleConversation.isChat ? "Chat" : "IM")];
this._interfaces = this._interfaces.concat(iface);
@ -137,6 +137,37 @@ UIConversation.prototype = {
return true;
},
_unreadMessageCount: 0,
get unreadMessageCount() this._unreadMessageCount,
markAsRead: function() {
delete this._unreadMessageCount;
this._notifyUnreadCountChanged();
},
_lastNotifiedUnreadCount: 0,
_notifyUnreadCountChanged: function() {
if (this._unreadMessageCount == this._lastNotifiedUnreadCount)
return;
for each (let observer in this._observers)
observer.observe(this, "unread-message-count-changed",
this._unreadMessageCount.toString());
this._lastNotifiedUnreadCount = this._unreadMessageCount;
},
getMessages: function(aMessageCount) {
if (aMessageCount)
aMessageCount.value = this._messages.length;
return this._messages;
},
checkClose: function() {
if (!Services.prefs.getBoolPref("messenger.conversations.alwaysClose") &&
(this.isChat && !this.left ||
!this.isChat && this.unreadMessageCount != 0))
return false;
this.close();
return true;
},
observe: function(aSubject, aTopic, aData) {
if (aTopic == "contact-no-longer-dummy") {
let oldId = parseInt(aData);
@ -248,23 +279,25 @@ UIConversation.prototype = {
close: function() {
for each (let conv in this._purpleConv)
conv.close();
this.notifyObservers(this, "ui-conversation-closed");
Services.obs.notifyObservers(this, "ui-conversation-closed", null);
},
addObserver: function(aObserver) {
if (this._observers.indexOf(aObserver) == -1) {
if (this._observers.indexOf(aObserver) == -1)
this._observers.push(aObserver);
if (this._observers.length == 1)
while (this._pendingMessages.length)
this.notifyObservers(this._pendingMessages.shift(), "new-text");
}
},
removeObserver: function(aObserver) {
this._observers = this._observers.filter(function(o) o !== aObserver);
},
notifyObservers: function(aSubject, aTopic, aData) {
if (aTopic == "new-text") {
this._messages.push(aSubject);
if (aSubject.incoming && !aSubject.system)
++this._unreadMessageCount;
}
for each (let observer in this._observers)
observer.observe(aSubject, aTopic, aData);
if (!this._observers.length && aTopic == "new-text")
this._pendingMessages.push(aSubject);
this._notifyUnreadCountChanged();
},
// purpleIConvIM
@ -367,6 +400,9 @@ ConversationsService.prototype = {
this._purpleConversations.filter(function(c) c !== aPurpleConversation);
},
getUIConversations: function()
new nsSimpleEnumerator(Object.keys(this._uiConv)
.map(function (k) this._uiConv[k])),
getUIConversation: function(aPurpleConversation) {
let id = aPurpleConversation.id;
if (id in this._uiConv)
@ -376,7 +412,7 @@ ConversationsService.prototype = {
getUIConversationByContactId: function(aId)
(aId in this._uiConvByContactId) ? this._uiConvByContactId[aId] : null,
getConversations: function() nsSimpleEnumerator(this._purpleConversations),
getConversations: function() new nsSimpleEnumerator(this._purpleConversations),
getConversationById: function(aId) {
for (let i = 0; i < this._purpleConversations.length; ++i)
if (this._purpleConversations[i].id == aId)