Bug 983347 - Need different paths for displaying to the screen and sending over the wire. r=florian,clokep a=jcranmer
authorArlo Breault <arlolra@gmail.com>
Wed, 27 Aug 2014 23:28:11 -0700
changeset 16798 d2a0c6d324fa77a2cf9f258b4c64693bfc9eb2b0
parent 16797 383642a78e4053d4dbe0448efb7b8b05dd2fdf16
child 16799 a27468ae784665edbba48f15a76969ee950f3023
push id10450
push userclokep@gmail.com
push dateSun, 14 Sep 2014 20:51:16 +0000
treeherdercomm-central@d2a0c6d324fa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersflorian, clokep, jcranmer
bugs983347
Bug 983347 - Need different paths for displaying to the screen and sending over the wire. r=florian,clokep a=jcranmer
chat/components/public/imIConversationsService.idl
chat/components/public/imILogger.idl
chat/components/public/prplIConversation.idl
chat/components/src/imConversations.js
chat/components/src/logger.js
chat/content/convbrowser.xml
chat/modules/imThemes.jsm
chat/modules/jsProtoHelper.jsm
chat/protocols/irc/irc.js
chat/protocols/xmpp/xmpp.jsm
--- a/chat/components/public/imIConversationsService.idl
+++ b/chat/components/public/imIConversationsService.idl
@@ -1,17 +1,18 @@
 /* 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/. */
 
 #include "nsISupports.idl"
 #include "prplIConversation.idl"
+#include "prplIMessage.idl"
 #include "imIContactsService.idl"
 
-interface prplIMessage;
+interface imIMessage;
 
 [scriptable, uuid(81b8d9a9-4715-4109-b522-84b9d31493a3)]
 interface imIConversation: prplIConversation {
   // Will be null for MUCs and IMs from people not in the contacts list.
   readonly attribute imIContact contact;
 
   // Write a system message into the conversation.
   // Note: this will not be logged.
@@ -38,17 +39,17 @@ interface imIConversation: prplIConversa
   // 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 prplIMessage messages);
+                   [retval, array, size_is(messageCount)] out imIMessage messages);
 };
 
 [scriptable, uuid(984e182c-d395-4fba-ba6e-cc80c71f57bf)]
 interface imIConversationsService: nsISupports {
   void initConversations();
   void unInitConversations();
 
   // Register a conversation. This will create a unique id for the
@@ -62,8 +63,29 @@ interface imIConversationsService: nsISu
   imIConversation getUIConversationByContactId(in long aId);
 
   nsISimpleEnumerator getConversations();
   prplIConversation getConversationById(in unsigned long aId);
   prplIConversation getConversationByNameAndAccount(in AUTF8String aName,
                                                     in imIAccount aAccount,
                                                     in boolean aIsChat);
 };
+
+// A cancellable outgoing message. Before handing a message off to a protocol,
+// the conversation service notifies observers (typically add-ons) of an
+// outgoing message, which can be transformed or cancelled.
+[scriptable, uuid(4391ba5c-9566-41a9-bb9b-fd0a0a490c2c)]
+interface imIOutgoingMessage: nsISupports {
+           attribute AUTF8String message;
+           attribute boolean cancelled;
+  readonly attribute prplIConversation conversation;
+};
+
+// A cancellable message to be displayed. When the conversation service is
+// notified of a new-text (ie. an incoming or outgoing message to be displayed),
+// it in turn notifies observers (again, typically add-ons), which have the
+// opportunity to swap or cancel the message.
+[scriptable, uuid(bd2f77d4-1fad-432d-a914-6bb5ed5c13d0)]
+interface imIMessage: prplIMessage {
+           attribute boolean cancelled;
+           // What eventually gets shown to the user.
+           attribute AUTF8String displayMessage;
+};
--- a/chat/components/public/imILogger.idl
+++ b/chat/components/public/imILogger.idl
@@ -5,18 +5,18 @@
 #include "nsISupports.idl"
 #include "nsISimpleEnumerator.idl"
 #include "nsIFile.idl"
 
 interface imIAccount;
 interface imIAccountBuddy;
 interface imIBuddy;
 interface imIContact;
+interface imIMessage;
 interface prplIConversation;
-interface prplIMessage;
 
 [scriptable, uuid(5bc06f3b-33a1-412b-a4d8-4fc7ba4c962b)]
 interface imILogConversation: nsISupports {
   readonly attribute AUTF8String title;
   readonly attribute AUTF8String name;
   // Value in microseconds.
   readonly attribute PRTime startDate;
 
@@ -27,17 +27,17 @@ interface imILogConversation: nsISupport
   //  - protocol will only contain a "name" attribute, with the prpl's normalized name.
   // Other methods/attributes aren't implemented.
   readonly attribute imIAccount account;
 
   readonly attribute boolean isChat; // always false (compatibility with prplIConversation).
   readonly attribute imIAccountBuddy buddy; // always null (compatibility with prplIConvIM).
 
   void getMessages([optional] out unsigned long messageCount,
-                   [retval, array, size_is(messageCount)] out prplIMessage messages);
+                   [retval, array, size_is(messageCount)] out imIMessage messages);
 
   // Callers that process the messages asynchronously should use the enumerator
   // instead of the array version of the getMessages* methods to avoid paying
   // up front the cost of xpconnect wrapping all message objects.
   nsISimpleEnumerator getMessagesEnumerator([optional] out unsigned long messageCount);
 };
 
 [scriptable, uuid(27712ece-ad2c-4504-87d5-9e2c16d40fef)]
--- a/chat/components/public/prplIConversation.idl
+++ b/chat/components/public/prplIConversation.idl
@@ -4,16 +4,17 @@
 
 
 #include "nsISupports.idl"
 #include "nsISimpleEnumerator.idl"
 #include "nsIObserver.idl"
 
 interface imIAccountBuddy;
 interface imIAccount;
+interface imIOutgoingMessage;
 interface nsIURI;
 interface nsIDOMDocument;
 interface prplIChatRoomFieldValues;
 
 /*
  * This is the XPCOM purple conversation component, a proxy for PurpleConversation.
  */
 
@@ -40,16 +41,22 @@ interface prplIConversation: nsISupports
   readonly attribute PRTime startDate;
   /* Unique identifier of the conversation */
   /* Setable only once by purpleCoreService while calling addConversation. */
            attribute unsigned long id;
 
   /* Send a message in the conversation */
   void sendMsg(in AUTF8String aMsg);
 
+  /* Preprocess messages before they are sent (eg. split long messages).
+     Can return null if no changes are to be made. */
+  void prepareForSending(in imIOutgoingMessage message,
+                         [optional] out unsigned long messageCount,
+                         [retval, array, size_is(messageCount)] out wstring messages);
+
   /* Send information about the current typing state to the server.
      aString should contain the content currently in the text field. The
      protocol should return the number of characters that can still be typed. */
   long sendTyping(in AUTF8String aString);
   const long NO_TYPING_LIMIT = 2147483647; // max int = 2 ^ 31 - 1
 
   /* Un-initialize the conversation. Will be called by
      purpleCoreService::RemoveConversation when the conversation is
--- a/chat/components/src/imConversations.js
+++ b/chat/components/src/imConversations.js
@@ -10,16 +10,65 @@ Cu.import("resource:///modules/jsProtoHe
 
 var gLastUIConvId = 0;
 var gLastPrplConvId = 0;
 
 XPCOMUtils.defineLazyGetter(this, "bundle", function()
   Services.strings.createBundle("chrome://chat/locale/conversations.properties")
 );
 
+function OutgoingMessage(aMsg, aConversation) {
+  this.message = aMsg;
+  this.conversation = aConversation;
+}
+OutgoingMessage.prototype = {
+  __proto__: ClassInfo("imIOutgoingMessage", "Outgoing Message"),
+  cancelled: false
+};
+
+function imMessage(aPrplMessage) {
+  this.prplMessage = aPrplMessage;
+}
+imMessage.prototype = {
+  __proto__: ClassInfo(["imIMessage", "prplIMessage"], "IM Message"),
+  cancelled: false,
+  _displayMessage: null,
+
+  get displayMessage() {
+    return this._displayMessage || this.prplMessage.originalMessage;
+  },
+  set displayMessage(aMsg) { this._displayMessage = aMsg; },
+
+  get message() this.prplMessage.message,
+  set message(aMsg) { this.prplMessage.message = aMsg; },
+
+  // from prplIMessage
+  get who() this.prplMessage.who,
+  get time() this.prplMessage.time,
+  get id() this.prplMessage.id,
+  get alias() this.prplMessage.alias,
+  get iconURL() this.prplMessage.iconURL,
+  get conversation() this.prplMessage.conversation,
+  set conversation(aConv) { this.prplMessage.conversation = aConv; },
+  get color() this.prplMessage.color,
+  get outgoing() this.prplMessage.outgoing,
+  get incoming() this.prplMessage.incoming,
+  get system() this.prplMessage.system,
+  get autoResponse() this.prplMessage.autoResponse,
+  get containsNick() this.prplMessage.containsNick,
+  get noLog() this.prplMessage.noLog,
+  get error() this.prplMessage.error,
+  get delayed() this.prplMessage.delayed,
+  get noFormat() this.prplMessage.noFormat,
+  get containsImages() this.prplMessage.containsImages,
+  get notification() this.prplMessage.notification,
+  get noLinkification() this.prplMessage.noLinkification,
+  getActions: function(aCount) this.prplMessage.getActions(aCount)
+};
+
 function UIConversation(aPrplConversation)
 {
   this._prplConv = {};
   this.id = ++gLastUIConvId;
   this._observers = [];
   this._messages = [];
   this.changeTargetTo(aPrplConversation);
   let iface = Ci["prplIConv" + (aPrplConversation.isChat ? "Chat" : "IM")];
@@ -271,40 +320,63 @@ UIConversation.prototype = {
   },
 
   observeConv: function(aTargetId, aSubject, aTopic, aData) {
     if (aTargetId != this._currentTargetId &&
         (aTopic == "new-text" ||
          (aTopic == "update-typing" &&
           this._prplConv[aTargetId].typingState == Ci.prplIConvIM.TYPING)))
       this.target = this._prplConv[aTargetId];
+
     this.notifyObservers(aSubject, aTopic, aData);
-    if (aTopic == "new-text") {
-      Services.obs.notifyObservers(aSubject, aTopic, aData);
-      if (aSubject.incoming && !aSubject.system &&
-          (!this.isChat || aSubject.containsNick)) {
-        this.notifyObservers(aSubject, "new-directed-incoming-message", aData);
-        Services.obs.notifyObservers(aSubject, "new-directed-incoming-message", aData);
-      }
-    }
   },
 
   systemMessage: function(aText, aIsError) {
     let flags = {system: true, noLog: true, error: !!aIsError};
     (new Message("system", aText, flags)).conversation = this;
   },
 
   // prplIConversation
   get isChat() this.target.isChat,
   get account() this.target.account,
   get name() this.target.name,
   get normalizedName() this.target.normalizedName,
   get title() this.target.title,
   get startDate() this.target.startDate,
-  sendMsg: function (aMsg) { this.target.sendMsg(aMsg); },
+  sendMsg: function(aMsg) {
+    // Add-ons (eg. pastebin) have an opportunity to cancel the message at this
+    // point, or change the text content of the message.
+    // If an add-on wants to split a message, it should truncate the first
+    // message, and insert new messages using the conversation's sendMsg method.
+    let om = new OutgoingMessage(aMsg, this);
+    this.notifyObservers(om, "preparing-message");
+    if (om.cancelled)
+      return;
+
+    // Protocols have an opportunity here to preprocess messages before they are
+    // sent (eg. split long messages). If a message is split here, the split
+    // will be visible in the UI.
+    let messages = this.target.prepareForSending(om);
+
+    // Protocols can return null if they don't need to make any changes.
+    // (nb. passing null with retval array results in an empty array)
+    if (messages.length == 0) {
+      messages = [om.message];
+    }
+
+    for (let msg of messages) {
+      // Add-ons (eg. OTR) have an opportunity to tweak or cancel the message
+      // at this point.
+      om = new OutgoingMessage(msg, this.target);
+      this.notifyObservers(om, "sending-message");
+      if (om.cancelled)
+        continue;
+      this.target.sendMsg(om.message);
+    }
+  },
   unInit: function() {
     for each (let conv in this._prplConv)
       gConversationsService.forgetConversation(conv);
     if (this._observedContact) {
       this._observedContact.removeObserver(this);
       delete this._observedContact;
     }
     this._prplConv = {}; // Prevent .close from failing.
@@ -324,30 +396,45 @@ UIConversation.prototype = {
     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) {
     if (aTopic == "new-text") {
+      aSubject = new imMessage(aSubject);
+      this.notifyObservers(aSubject, "received-message");
+      if (aSubject.cancelled)
+        return;
+
       this._messages.push(aSubject);
       ++this._unreadMessageCount;
       if (aSubject.incoming && !aSubject.system) {
         ++this._unreadIncomingMessageCount;
         if (!this.isChat || aSubject.containsNick)
           ++this._unreadTargetedMessageCount;
       }
     }
+
     for each (let observer in this._observers) {
       if (!observer.observe && this._observers.indexOf(observer) == -1)
         continue; // observer removed by a previous call to another observer.
       observer.observe(aSubject, aTopic, aData);
     }
     this._notifyUnreadCountChanged();
+
+    if (aTopic == "new-text") {
+      Services.obs.notifyObservers(aSubject, aTopic, aData);
+      if (aSubject.incoming && !aSubject.system &&
+          (!this.isChat || aSubject.containsNick)) {
+        this.notifyObservers(aSubject, "new-directed-incoming-message", aData);
+        Services.obs.notifyObservers(aSubject, "new-directed-incoming-message", aData);
+      }
+    }
   },
 
   // prplIConvIM
   get buddy() this.target.buddy,
   get typingState() this.target.typingState,
   sendTyping: function(aString) this.target.sendTyping(aString),
 
   // Chat only
--- a/chat/components/src/logger.js
+++ b/chat/components/src/logger.js
@@ -204,32 +204,32 @@ LogWriter.prototype = {
     return encoder.encodeToString();
   },
   logMessage: function cl_logMessage(aMessage) {
     let lineToWrite;
     if (this.format == "json") {
       let msg = {
         date: new Date(aMessage.time * 1000),
         who: aMessage.who,
-        text: aMessage.originalMessage,
+        text: aMessage.displayMessage,
         flags: ["outgoing", "incoming", "system", "autoResponse",
                 "containsNick", "error", "delayed",
                 "noFormat", "containsImages", "notification",
                 "noLinkification"].filter(function(f) aMessage[f])
       };
       let alias = aMessage.alias;
       if (alias && alias != msg.who)
         msg.alias = alias;
       lineToWrite = JSON.stringify(msg) + "\n";
     }
     else {
       // Text log.
       let date = new Date(aMessage.time * 1000);
       let line = "(" + date.toLocaleTimeString() + ") ";
-      let msg = this._serialize(aMessage.originalMessage);
+      let msg = this._serialize(aMessage.displayMessage);
       if (aMessage.system)
         line += msg;
       else {
         let sender = aMessage.alias || aMessage.who;
         if (aMessage.autoResponse)
           line += sender + " <AUTO-REPLY>: " + msg;
         else {
           if (msg.startsWith("/me "))
@@ -367,17 +367,21 @@ function LogMessage(aData, aConversation
   this._init(aData.who, aData.text);
   this._conversation = aConversation;
   this.time = Math.round(new Date(aData.date) / 1000);
   if ("alias" in aData)
     this._alias = aData.alias;
   for (let flag of aData.flags)
     this[flag] = true;
 }
-LogMessage.prototype = GenericMessagePrototype;
+LogMessage.prototype = {
+  __proto__: GenericMessagePrototype,
+  _interfaces: [Ci.imIMessage, Ci.prplIMessage],
+  get displayMessage() this.originalMessage
+};
 
 
 function LogConversation(aMessages, aProperties) {
   this._messages = aMessages;
   for (let property in aProperties)
     this[property] = aProperties[property];
 }
 LogConversation.prototype = {
--- a/chat/content/convbrowser.xml
+++ b/chat/content/convbrowser.xml
@@ -464,17 +464,17 @@
             let csFlags = cs.kStructPhrase;
             // Automatically find and link freetext URLs
             if (!aMsg.noLinkification)
               csFlags |= cs.kURLs;
 
             if (aFirstUnread)
               this.setUnreadRuler();
 
-            let msg = aMsg.originalMessage;
+            let msg = aMsg.displayMessage;
 
             // The slash of a leading '/me' should not be used to
             // format as italic, so we remove the '/me' text before
             // scanning the HTML, and we add it back later.
             let meRegExp = /^((<[^>]+>)*)\/me /;
             let me = false;
             if (meRegExp.test(msg)) {
               me = true;
--- a/chat/modules/imThemes.jsm
+++ b/chat/modules/imThemes.jsm
@@ -361,17 +361,17 @@ const statusMessageReplacements = {
     else {
       msgClass.push("message");
 
       if (aMsg.incoming)
         msgClass.push("incoming");
       else if (aMsg.outgoing)
         msgClass.push("outgoing");
 
-      if (/^(<[^>]+>)*\/me /.test(aMsg.originalMessage))
+      if (/^(<[^>]+>)*\/me /.test(aMsg.displayMessage))
         msgClass.push("action");
 
       if (aMsg.autoResponse)
         msgClass.push("autoreply");
     }
 
     if (aMsg.containsNick)
       msgClass.push("nick");
@@ -486,21 +486,21 @@ function getHTMLForMessage(aMsg, aTheme,
   else {
     html = aMsg.incoming ? "incoming" : "outgoing";
     if (aIsNext)
       html += "Next";
     html += aIsContext ? "Context" : "Content";
     html = aTheme.html[html];
     replacements = messageReplacements;
     let meRegExp = /^((<[^>]+>)*)\/me /;
-    // We must test originalMessage here as aMsg.message loses its /me
+    // We must test displayMessage here as aMsg.message loses its /me
     // in the following, so if getHTMLForMessage is called a second time for
     // the same aMsg (e.g. because it follows the unread ruler), the test
     // would fail otherwise.
-    if (meRegExp.test(aMsg.originalMessage)) {
+    if (meRegExp.test(aMsg.displayMessage)) {
       aMsg.message = aMsg.message.replace(meRegExp, "$1");
       let actionMessageTemplate = "* %message% *";
       if (hasMetadataKey(aTheme, "ActionMessageTemplate"))
         actionMessageTemplate = getMetadata(aTheme, "ActionMessageTemplate");
       html = html.replace(/%message%/g, actionMessageTemplate);
     }
   }
 
@@ -890,17 +890,17 @@ SelectedMessage.prototype = {
     let html, replacements;
     if (msg.system) {
       replacements = statusReplacements;
       html = getLocalizedPrefWithDefault("systemMessagesTemplate",
                                          "%time% - %message%");
     }
     else {
       replacements = messageReplacements;
-      if (/^(<[^>]+>)*\/me /.test(msg.originalMessage)) {
+      if (/^(<[^>]+>)*\/me /.test(msg.displayMessage)) {
         html = getLocalizedPrefWithDefault("actionMessagesTemplate",
                                            "%time% * %sender% %message%");
       }
       else {
         html = getLocalizedPrefWithDefault("contentMessagesTemplate",
                                            "%time% - %sender%: %message%");
       }
     }
--- a/chat/modules/jsProtoHelper.jsm
+++ b/chat/modules/jsProtoHelper.jsm
@@ -478,17 +478,18 @@ const GenericConversationPrototype = {
       try {
         observer.observe(aSubject, aTopic, aData);
       } catch(e) {
         this.ERROR(e);
       }
     }
   },
 
-  sendMsg: function (aMsg) {
+  prepareForSending: function(aOutgoingMessage, aCount) null,
+  sendMsg: function(aMsg) {
     throw Cr.NS_ERROR_NOT_IMPLEMENTED;
   },
   sendTyping: function(aString) Ci.prplIConversation.NO_TYPING_LIMIT,
 
   close: function() {
     Services.obs.notifyObservers(this, "closing-conversation", null);
     Services.conversations.removeConversation(this);
   },
--- a/chat/protocols/irc/irc.js
+++ b/chat/protocols/irc/irc.js
@@ -129,19 +129,19 @@ const GenericIRCConversation = {
   getMaxMessageLength: function() {
     // Build the shortest possible message that could be sent to other users.
     let baseMessage = ":" + this._account._nickname + this._account.prefix +
                       " " + this._account.buildMessage("PRIVMSG", this.name) +
                       " :\r\n";
     return this._account.maxMessageLength -
            this._account.countBytes(baseMessage);
   },
-  sendMsg: function(aMessage) {
+  prepareForSending: function(aOutgoingMessage, aCount) {
     // Split the message by line breaks and send each one individually.
-    let messages = aMessage.split(/[\r\n]+/);
+    let messages = aOutgoingMessage.message.split(/[\r\n]+/);
 
     let maxLength = this.getMaxMessageLength();
 
     // Attempt to smartly split a string into multiple lines (based on the
     // maximum number of characters the message can contain).
     for (let i = 0; i < messages.length; ++i) {
       let message = messages[i];
       let length = this._account.countBytes(message);
@@ -154,34 +154,37 @@ const GenericIRCConversation = {
 
       // Remove the current message and insert the two new ones. If no space was
       // found, cut the first message to the maximum length and start the second
       // message one character after that. If a space was found, exclude it.
       messages.splice(i, 1, message.substr(0, index == -1 ? maxLength : index),
                       message.substr((index + 1) || maxLength));
     }
 
-    // Send each message and display it in the conversation.
-    for (let message of messages) {
-      if (!message.length)
-        return;
+    if (aCount)
+      aCount.value = messages.length;
+
+    return messages;
+  },
+  sendMsg: function(aMessage) {
+    if (!aMessage.length)
+      return;
 
-      if (!this._account.sendMessage("PRIVMSG", [this.name, message])) {
-        this.writeMessage(this._account._currentServerName,
-                          _("error.sendMessageFailed"),
-                          {error: true, system: true});
-        break;
-      }
+    if (!this._account.sendMessage("PRIVMSG", [this.name, aMessage])) {
+      this.writeMessage(this._account._currentServerName,
+                        _("error.sendMessageFailed"),
+                        {error: true, system: true});
+      return;
+    }
 
-      // Since the server doesn't send us a message back, just assume the
-      // message was received and immediately show it.
-      this.writeMessage(this._account._nickname, message, {outgoing: true});
+    // Since the server doesn't send us a message back, just assume the
+    // message was received and immediately show it.
+    this.writeMessage(this._account._nickname, aMessage, {outgoing: true});
 
-      this._pendingMessage = true;
-    }
+    this._pendingMessage = true;
   },
   // IRC doesn't support typing notifications, but it does have a maximum
   // message length.
   sendTyping: function(aString) {
     let longestLineLength =
       Math.max.apply(null, aString.split("\n").map(this._account.countBytes,
                                                    this._account));
     return this.getMaxMessageLength() - longestLineLength;
--- a/chat/protocols/xmpp/xmpp.jsm
+++ b/chat/protocols/xmpp/xmpp.jsm
@@ -240,31 +240,28 @@ const XMPPConversationPrototype = {
   get to() {
     let to = this.buddy.userName;
     if (this._targetResource)
       to += "/" + this._targetResource;
     return to;
   },
 
   /* Called when the user enters a chat message */
-  sendMsg: function (aMsg) {
+  sendMsg: function(aMsg) {
     this._cancelTypingTimer();
     let cs = this.shouldSendTypingNotifications ? "active" : null;
     let s = Stanza.message(this.to, aMsg, cs);
     this._account.sendStanza(s);
     let who;
     if (this._account._connection)
       who = this._account._connection._jid.jid;
     if (!who)
       who = this._account.name;
     let alias = this.account.alias || this.account.statusInfo.displayName;
-    let msg = Cc["@mozilla.org/txttohtmlconv;1"]
-                .getService(Ci.mozITXTToHTMLConv)
-                .scanTXT(aMsg, Ci.mozITXTToHTMLConv.kEntities);
-    this.writeMessage(who, msg, {outgoing: true, _alias: alias});
+    this.writeMessage(who, aMsg, {outgoing: true, _alias: alias});
     delete this._typingState;
   },
 
   /* Called by the account when a messsage is received from the buddy */
   incomingMessage: function(aMsg, aStanza, aDate) {
     let from = aStanza.attributes["from"];
     this._targetResource = this._account._parseJID(from).resource;
     let flags = {};