Skip to content

Instantly share code, notes, and snippets.

@arlolra
Created July 3, 2014 10:30
Show Gist options
  • Save arlolra/533c2abff56ccdd85cde to your computer and use it in GitHub Desktop.
Save arlolra/533c2abff56ccdd85cde to your computer and use it in GitHub Desktop.
diff --git a/chat/components/public/imIConversationsService.idl b/chat/components/public/imIConversationsService.idl
--- a/chat/components/public/imIConversationsService.idl
+++ b/chat/components/public/imIConversationsService.idl
@@ -39,16 +39,21 @@ interface imIConversation: prplIConversa
// 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);
+
+ // void displayMsg(in prplIConversation aConv,
+ // in AUTF8String aWho,
+ // in AUTF8String aText,
+ // in prplIMessage aProperties);
};
[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 +67,26 @@ 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);
};
+
+[scriptable, uuid(4391ba5c-9566-41a9-bb9b-fd0a0a490c2c)]
+interface imIOutgoingMessage: nsISupports {
+ attribute AUTF8String message;
+ attribute boolean cancelled;
+ readonly attribute imIConversation conversation;
+};
+
+[scriptable, uuid(0684ed1e-97b7-4eba-bf58-dd9dd81c668c)]
+interface imISendableMessage: nsISupports {
+ void getSendableMessages([optional] out unsigned long messageCount,
+ [retval, array, size_is(messageCount)] out AUTF8String messages);
+ void setSendableMessages(in unsigned long messageCount,
+ [array, size_is(messageCount)] in AUTF8String messages);
+ readonly attribute AUTF8String originalMessage;
+ attribute boolean cancelled;
+ readonly attribute prplIConversation target;
+};
diff --git a/chat/components/public/prplIConversation.idl b/chat/components/public/prplIConversation.idl
--- a/chat/components/public/prplIConversation.idl
+++ b/chat/components/public/prplIConversation.idl
@@ -6,16 +6,17 @@
#include "nsISupports.idl"
#include "nsISimpleEnumerator.idl"
#include "nsIObserver.idl"
interface imIAccountBuddy;
interface imIAccount;
interface nsIURI;
interface nsIDOMDocument;
+interface cancellableOutgoingMessage;
/*
* This is the XPCOM purple conversation component, a proxy for PurpleConversation.
*/
[scriptable, uuid(e40dc3e5-c9ff-457b-a6cc-655cce81042c)]
interface prplIConversation: nsISupports {
@@ -39,16 +40,18 @@ 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);
+ void prepareForSending(in cancellableOutgoingMessage aOm);
+
/* 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
diff --git a/chat/components/src/imConversations.js b/chat/components/src/imConversations.js
--- a/chat/components/src/imConversations.js
+++ b/chat/components/src/imConversations.js
@@ -284,17 +284,42 @@ UIConversation.prototype = {
// 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", null);
+ if (om.cancelled)
+ return;
+
+ // prpls 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.
+ // prpls can return null if they don't need to make any change
+ let messages = this.target.prepareForSending(om) || [om.message];
+
+ for (let msg of messages) {
+ // add-ons (eg. OTR) have an opportunity to tweak or cancel the message
+ // point.
+ let sm = new SendableMessage(msg, this.target);
+ this.notifyObservers(sm, "sending-message", null);
+ if (sm.cancelled)
+ continue;
+ sm.getSendableMessages().forEach((m) => this.target.sendMsg(m));
+ }
+ },
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.
diff --git a/chat/modules/jsProtoHelper.jsm b/chat/modules/jsProtoHelper.jsm
--- a/chat/modules/jsProtoHelper.jsm
+++ b/chat/modules/jsProtoHelper.jsm
@@ -6,16 +6,17 @@ const EXPORTED_SYMBOLS = [
"GenericAccountPrototype",
"GenericAccountBuddyPrototype",
"GenericConvIMPrototype",
"GenericConvChatPrototype",
"GenericConvChatBuddyPrototype",
"GenericMessagePrototype",
"GenericProtocolPrototype",
"Message",
+ "CancellableOutgoingMessage",
"TooltipInfo"
];
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
Cu.import("resource:///modules/imXPCOMUtils.jsm");
Cu.import("resource:///modules/imServices.jsm");
@@ -424,16 +425,31 @@ const GenericMessagePrototype = {
}
};
function Message(aWho, aMessage, aObject) {
this._init(aWho, aMessage, aObject);
}
Message.prototype = GenericMessagePrototype;
+function CancellableOutgoingMessage(aMsg, aTarget) {
+ this.displayableMessage = aMsg;
+ this.nextDisplayableMessage = null;
+ this.target = aTarget;
+
+ let sendableMessages = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+ let msg = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+ msg.data = aMsg;
+ sendableMessages.appendElement(msg, false);
+ this.sendableMessages = sendableMessages;
+}
+CancellableOutgoingMessage.prototype = {
+ constructor: CancellableOutgoingMessage,
+ cancelled: false
+};
const GenericConversationPrototype = {
__proto__: ClassInfo("prplIConversation", "generic conversation object"),
get wrappedJSObject() this,
get DEBUG() this._account.DEBUG,
get LOG() this._account.LOG,
get WARN() this._account.WARN,
@@ -467,31 +483,46 @@ const GenericConversationPrototype = {
try {
observer.observe(aSubject, aTopic, aData);
} catch(e) {
this.ERROR(e);
}
}
},
- sendMsg: function (aMsg) {
+ prepareForSending: function(aOm) {},
+ 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);
},
unInit: function() {
delete this._account;
delete this._observers;
},
writeMessage: function(aWho, aText, aProperties) {
+ if (!aProperties.system) {
+ let om = new CancellableOutgoingMessage(aText, this);
+
+ if (aProperties.outgoing) {
+ Services.obs.notifyObservers(om, "displaying-message", null);
+ } else if (aProperties.incoming) {
+ Services.obs.notifyObservers(om, "receiving-message", null);
+ }
+
+ if (om.cancelled)
+ return;
+
+ aText = om.displayableMessage;
+ }
(new Message(aWho, aText, aProperties)).conversation = this;
},
get account() this._account.imAccount,
get name() this._name,
get normalizedName() this._account.normalize(this.name),
get title() this.name,
get startDate() this._date
diff --git a/chat/protocols/irc/irc.js b/chat/protocols/irc/irc.js
--- a/chat/protocols/irc/irc.js
+++ b/chat/protocols/irc/irc.js
@@ -129,59 +129,73 @@ 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);
},
+ prepareForSending: function(aOm) {
+ let sendableMessages = [];
+ let count = aOm.sendableMessages.length;
+ for (let i = 0; i < count; ++i) {
+ let aMessage = aOm.sendableMessages.queryElementAt(i, Ci.nsISupportsString).toString();
+
+ // Split the message by line breaks and send each one individually.
+ let messages = aMessage.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);
+ // The message is short enough.
+ if (length <= maxLength)
+ continue;
+
+ // Find the location of a space before the maximum length.
+ let index = message.lastIndexOf(" ", maxLength);
+
+ // 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));
+ }
+ sendableMessages = sendableMessages.concat(messages);
+ }
+ let sMs = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+ for (let msg of sendableMessages) {
+ let nm = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+ nm.data = msg;
+ sMs.appendElement(nm, false);
+ }
+ aOm.sendableMessages = sMs;
+ if (aOm.nextDisplayableMessage)
+ this.prepareForSending(aOm.nextDisplayableMessage);
+ },
sendMsg: function(aMessage) {
- // Split the message by line breaks and send each one individually.
- let messages = aMessage.split(/[\r\n]+/);
+ if (!aMessage.length)
+ return;
- 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);
- // The message is short enough.
- if (length <= maxLength)
- continue;
-
- // Find the location of a space before the maximum length.
- let index = message.lastIndexOf(" ", maxLength);
-
- // 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));
+ if (!this._account.sendMessage("PRIVMSG", [this.name, aMessage])) {
+ this.writeMessage(this._account._currentServerName,
+ _("error.sendMessageFailed"),
+ {error: true, system: true});
+ return;
}
- // Send each message and display it in the conversation.
- for (let message of messages) {
- if (!message.length)
- 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, aMessage, {outgoing: true});
- if (!this._account.sendMessage("PRIVMSG", [this.name, message])) {
- this.writeMessage(this._account._currentServerName,
- _("error.sendMessageFailed"),
- {error: true, system: true});
- break;
- }
-
- // 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});
-
- 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;
@@ -601,16 +615,30 @@ function ircConversation(aAccount, aName
// Always request the info as it may be out of date.
this._waitingForNick = true;
this.requestBuddyInfo(aName);
}
ircConversation.prototype = {
__proto__: GenericConvIMPrototype,
get buddy() this._account.buddies.get(this.name),
+ // prepareForDisplaying: function(aOm) {
+ // let sMs = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+ // let count = aOm.sendableMessages.length;
+ // for (let i = 0; i < count; ++i) {
+ // let aMessage = aOm.sendableMessages.queryElementAt(i, Ci.nsISupportsString).toString();
+ // let nm = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+ // nm.data = ctcpFormatToHTML(aMessage);
+ // sMs.appendElement(nm, false);
+ // }
+ // aOm.sendableMessages = sMs;
+ // if (aOm.nextDisplayableMessage)
+ // this.prepareForSending(aOm.nextDisplayableMessage);
+ // },
+
// Overwrite the writeMessage function to apply CTCP formatting before
// display.
writeMessage: function(aWho, aText, aProperties) {
GenericConvIMPrototype.writeMessage.call(this, aWho,
ctcpFormatToHTML(aText),
aProperties);
},
diff --git a/chat/protocols/xmpp/xmpp.jsm b/chat/protocols/xmpp/xmpp.jsm
--- a/chat/protocols/xmpp/xmpp.jsm
+++ b/chat/protocols/xmpp/xmpp.jsm
@@ -28,16 +28,21 @@ XPCOMUtils.defineLazyModuleGetter(this,
XPCOMUtils.defineLazyServiceGetter(this, "imgTools",
"@mozilla.org/image/tools;1",
"imgITools");
XPCOMUtils.defineLazyGetter(this, "_", function()
l10nHelper("chrome://chat/locale/xmpp.properties")
);
+XPCOMUtils.defineLazyGetter(this, "TXTToHTML", function() {
+ let cs = Cc["@mozilla.org/txttohtmlconv;1"].getService(Ci.mozITXTToHTMLConv);
+ return function(aTXT) cs.scanTXT(aTXT, cs.kEntities);
+});
+
/* This is an ordered list, used to determine chat buddy flags:
* index < member -> noFlags
* index = member -> voiced
* moderator -> halfOp
* admin -> op
* owner -> founder
*/
const kRoles = ["outcast", "visitor", "participant", "member", "moderator",
@@ -233,32 +238,43 @@ const XMPPConversationPrototype = {
_targetResource: "",
get to() {
let to = this.buddy.userName;
if (this._targetResource)
to += "/" + this._targetResource;
return to;
},
+ prepareForSending: function(aOm) {
+ let sMs = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+ let count = aOm.sendableMessages.length;
+ for (let i = 0; i < count; ++i) {
+ let aMessage = aOm.sendableMessages.queryElementAt(i, Ci.nsISupportsString).toString();
+ let nm = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+ nm.data = TXTToHTML(aMessage);
+ sMs.appendElement(nm, false);
+ }
+ aOm.sendableMessages = sMs;
+ if (aOm.nextDisplayableMessage)
+ this.prepareForSending(aOm.nextDisplayableMessage);
+ },
+
/* 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 = {};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment