From a0185934b0c929622c5526b84235b86cd44aad1d Mon Sep 17 00:00:00 2001
From: Kevin Smith <git@kismith.co.uk>
Date: Fri, 3 Sep 2010 16:07:47 +0100
Subject: XEP-0198 Ack support in the UI

Resolves: #7

diff --git a/Swift/Controllers/Chat/ChatController.cpp b/Swift/Controllers/Chat/ChatController.cpp
index 52288c2..9154b9a 100644
--- a/Swift/Controllers/Chat/ChatController.cpp
+++ b/Swift/Controllers/Chat/ChatController.cpp
@@ -12,6 +12,7 @@
 #include "Swiften/Chat/ChatStateNotifier.h"
 #include "Swiften/Chat/ChatStateMessageSender.h"
 #include "Swiften/Chat/ChatStateTracker.h"
+#include "Swiften/Client/StanzaChannel.h"
 #include "Swift/Controllers/UIInterfaces/ChatWindow.h"
 #include "Swift/Controllers/UIInterfaces/ChatWindowFactory.h"
 #include "Swift/Controllers/NickResolver.h"
@@ -32,6 +33,7 @@ ChatController::ChatController(const JID& self, StanzaChannel* stanzaChannel, IQ
 	nickResolver_ = nickResolver;
 	presenceOracle_->onPresenceChange.connect(boost::bind(&ChatController::handlePresenceChange, this, _1, _2));
 	chatStateTracker_->onChatStateChange.connect(boost::bind(&ChatWindow::setContactChatState, chatWindow_, _1));
+	stanzaChannel_->onStanzaAcked.connect(boost::bind(&ChatController::handleStanzaAcked, this, _1));
 	String nick = nickResolver_->jidToNick(toJID_);
 	chatWindow_->setName(nick);
 	String startMessage("Starting chat with " + nick);
@@ -43,6 +45,7 @@ ChatController::ChatController(const JID& self, StanzaChannel* stanzaChannel, IQ
 	chatWindow_->addSystemMessage(startMessage);
 	chatWindow_->onUserTyping.connect(boost::bind(&ChatStateNotifier::setUserIsTyping, chatStateNotifier_));
 	chatWindow_->onUserCancelsTyping.connect(boost::bind(&ChatStateNotifier::userCancelledNewMessage, chatStateNotifier_));
+
 }
 
 ChatController::~ChatController() {
@@ -80,12 +83,35 @@ void ChatController::preSendMessageRequest(boost::shared_ptr<Message> message) {
 	}
 }
 
-void ChatController::postSendMessage(const String& body) {
-	addMessage(body, "me", true, labelsEnabled_ ? chatWindow_->getSelectedSecurityLabel() : boost::optional<SecurityLabel>(), String(avatarManager_->getAvatarPath(selfJID_).string()), boost::posix_time::microsec_clock::universal_time());
+void ChatController::postSendMessage(const String& body, boost::shared_ptr<Stanza> sentStanza) {
+	String id = addMessage(body, "me", true, labelsEnabled_ ? chatWindow_->getSelectedSecurityLabel() : boost::optional<SecurityLabel>(), String(avatarManager_->getAvatarPath(selfJID_).string()), boost::posix_time::microsec_clock::universal_time());
+	if (stanzaChannel_->getStreamManagementEnabled()) {
+		chatWindow_->setAckState(id, ChatWindow::Pending);
+		unackedStanzas_[sentStanza] = id;
+	}
 	lastWasPresence_ = false;
 	chatStateNotifier_->userSentMessage();
 }
 
+void ChatController::handleStanzaAcked(boost::shared_ptr<Stanza> stanza) {
+	String id = unackedStanzas_[stanza];
+	if (id != "") {
+		chatWindow_->setAckState(id, ChatWindow::Received);
+	}
+	unackedStanzas_.erase(unackedStanzas_.find(stanza));
+}
+
+void ChatController::setEnabled(bool enabled) {
+	if (!enabled) {
+		std::map<boost::shared_ptr<Stanza>, String>::iterator it = unackedStanzas_.begin();
+		for ( ; it != unackedStanzas_.end(); it++) {
+			chatWindow_->setAckState(it->second, ChatWindow::Failed);
+		}
+		unackedStanzas_.clear();
+	}
+	ChatControllerBase::setEnabled(enabled);
+}
+
 String ChatController::senderDisplayNameFromMessage(const JID& from) {
 	return nickResolver_->jidToNick(from);
 }
diff --git a/Swift/Controllers/Chat/ChatController.h b/Swift/Controllers/Chat/ChatController.h
index d833094..971fca9 100644
--- a/Swift/Controllers/Chat/ChatController.h
+++ b/Swift/Controllers/Chat/ChatController.h
@@ -20,16 +20,18 @@ namespace Swift {
 			ChatController(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &contact, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool isInMUC, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController);
 			virtual ~ChatController();
 			virtual void setToJID(const JID& jid);
+			virtual void setEnabled(bool enabled);
 
 		private:
 			void handlePresenceChange(boost::shared_ptr<Presence> newPresence, boost::shared_ptr<Presence> previousPresence);
 			String getStatusChangeString(boost::shared_ptr<Presence> presence);
 			bool isIncomingMessageFromMe(boost::shared_ptr<Message> message);
-			void postSendMessage(const String &body);
+			void postSendMessage(const String &body, boost::shared_ptr<Stanza> sentStanza);
 			void preHandleIncomingMessage(boost::shared_ptr<MessageEvent> messageEvent);
 			void preSendMessageRequest(boost::shared_ptr<Message>);
 			String senderDisplayNameFromMessage(const JID& from);
 			virtual boost::optional<boost::posix_time::ptime> getMessageTimestamp(boost::shared_ptr<Message>) const;
+			void handleStanzaAcked(boost::shared_ptr<Stanza> stanza);
 
 		private:
 			NickResolver* nickResolver_;
@@ -39,6 +41,7 @@ namespace Swift {
 			ChatStateTracker* chatStateTracker_;
 			bool isInMUC_;
 			bool lastWasPresence_;
+			std::map<boost::shared_ptr<Stanza>, String> unackedStanzas_;
 	};
 }
 #endif
diff --git a/Swift/Controllers/Chat/ChatControllerBase.cpp b/Swift/Controllers/Chat/ChatControllerBase.cpp
index e38d12d..b7dd1c0 100644
--- a/Swift/Controllers/Chat/ChatControllerBase.cpp
+++ b/Swift/Controllers/Chat/ChatControllerBase.cpp
@@ -79,7 +79,7 @@ void ChatControllerBase::handleSendMessageRequest(const String &body) {
 		message->addPayload(boost::shared_ptr<Delay>(new Delay(now, selfJID_)));
 	}
 	stanzaChannel_->sendMessage(message);
-	postSendMessage(message->getBody());
+	postSendMessage(message->getBody(), boost::dynamic_pointer_cast<Stanza>(message));
 }
 
 void ChatControllerBase::handleSecurityLabelsCatalogResponse(boost::shared_ptr<SecurityLabelsCatalog> catalog, const boost::optional<ErrorPayload>& error) {
@@ -106,11 +106,11 @@ void ChatControllerBase::activateChatWindow() {
 	chatWindow_->activate();
 }
 
-void ChatControllerBase::addMessage(const String& message, const String& senderName, bool senderIsSelf, const boost::optional<SecurityLabel>& label, const String& avatarPath, const boost::posix_time::ptime& time) {
+String ChatControllerBase::addMessage(const String& message, const String& senderName, bool senderIsSelf, const boost::optional<SecurityLabel>& label, const String& avatarPath, const boost::posix_time::ptime& time) {
 	if (message.beginsWith("/me ")) {
-		chatWindow_->addAction(message.getSplittedAtFirst(' ').second, senderName, senderIsSelf, label, avatarPath, time);
+		return chatWindow_->addAction(message.getSplittedAtFirst(' ').second, senderName, senderIsSelf, label, avatarPath, time);
 	} else {
-		chatWindow_->addMessage(message, senderName, senderIsSelf, label, avatarPath, time);
+		return chatWindow_->addMessage(message, senderName, senderIsSelf, label, avatarPath, time);
 	}
 }
 
diff --git a/Swift/Controllers/Chat/ChatControllerBase.h b/Swift/Controllers/Chat/ChatControllerBase.h
index 61d0ab7..2466690 100644
--- a/Swift/Controllers/Chat/ChatControllerBase.h
+++ b/Swift/Controllers/Chat/ChatControllerBase.h
@@ -15,6 +15,7 @@
 #include <boost/optional.hpp>
 #include <boost/date_time/posix_time/posix_time.hpp>
 
+#include "Swiften/Elements/Stanza.h"
 #include "Swiften/Base/String.h"
 #include "Swiften/Elements/DiscoInfo.h"
 #include "Swiften/Events/MessageEvent.h"
@@ -40,13 +41,16 @@ namespace Swift {
 			void activateChatWindow();
 			void setAvailableServerFeatures(boost::shared_ptr<DiscoInfo> info);
 			void handleIncomingMessage(boost::shared_ptr<MessageEvent> message);
-			void addMessage(const String& message, const String& senderName, bool senderIsSelf, const boost::optional<SecurityLabel>& label, const String& avatarPath, const boost::posix_time::ptime& time);
+			String addMessage(const String& message, const String& senderName, bool senderIsSelf, const boost::optional<SecurityLabel>& label, const String& avatarPath, const boost::posix_time::ptime& time);
 			virtual void setEnabled(bool enabled);
 			virtual void setToJID(const JID& jid) {toJID_ = jid;};
 		protected:
 			ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController);
 
-			virtual void postSendMessage(const String&) {};
+			/**
+			 * Pass the Message appended, and the stanza used to send it.
+			 */
+			virtual void postSendMessage(const String&, boost::shared_ptr<Stanza>) {};
 			virtual String senderDisplayNameFromMessage(const JID& from) = 0;
 			virtual bool isIncomingMessageFromMe(boost::shared_ptr<Message>) = 0;
 			virtual void preHandleIncomingMessage(boost::shared_ptr<MessageEvent>) {};
diff --git a/Swift/Controllers/Chat/ChatsManager.cpp b/Swift/Controllers/Chat/ChatsManager.cpp
index dfe75d6..acc5e1d 100644
--- a/Swift/Controllers/Chat/ChatsManager.cpp
+++ b/Swift/Controllers/Chat/ChatsManager.cpp
@@ -8,8 +8,6 @@
 
 #include <boost/bind.hpp>
 
-#include "Swiften/Client/Client.h"
-
 #include "Swift/Controllers/Chat/ChatController.h"
 #include "Swift/Controllers/EventController.h"
 #include "Swift/Controllers/Chat/MUCController.h"
diff --git a/Swift/Controllers/Chat/ChatsManager.h b/Swift/Controllers/Chat/ChatsManager.h
index d7a6275..3efd507 100644
--- a/Swift/Controllers/Chat/ChatsManager.h
+++ b/Swift/Controllers/Chat/ChatsManager.h
@@ -61,7 +61,7 @@ namespace Swift {
 			EventController* eventController_;
 			JID jid_;
 			StanzaChannel* stanzaChannel_;
-			IQRouter* iqRouter_;;
+			IQRouter* iqRouter_;
 			ChatWindowFactory* chatWindowFactory_;
 			NickResolver* nickResolver_;
 			PresenceOracle* presenceOracle_;
diff --git a/Swift/Controllers/UIInterfaces/ChatWindow.h b/Swift/Controllers/UIInterfaces/ChatWindow.h
index 4d00dca..1ee632c 100644
--- a/Swift/Controllers/UIInterfaces/ChatWindow.h
+++ b/Swift/Controllers/UIInterfaces/ChatWindow.h
@@ -25,11 +25,18 @@ namespace Swift {
 
 	class ChatWindow {
 		public:
+			enum AckState {Pending, Received, Failed};
 			ChatWindow() {}
 			virtual ~ChatWindow() {};
 
-			virtual void addMessage(const String& message, const String& senderName, bool senderIsSelf, const boost::optional<SecurityLabel>& label, const String& avatarPath, const boost::posix_time::ptime& time) = 0;
-			virtual void addAction(const String& message, const String& senderName, bool senderIsSelf, const boost::optional<SecurityLabel>& label, const String& avatarPath, const boost::posix_time::ptime& time) = 0;
+			/** Add message to window.
+			 * @return id of added message (for acks).
+			 */
+			virtual String addMessage(const String& message, const String& senderName, bool senderIsSelf, const boost::optional<SecurityLabel>& label, const String& avatarPath, const boost::posix_time::ptime& time) = 0;
+			/** Adds action to window.
+			 * @return id of added message (for acks);
+			 */
+			virtual String addAction(const String& message, const String& senderName, bool senderIsSelf, const boost::optional<SecurityLabel>& label, const String& avatarPath, const boost::posix_time::ptime& time) = 0;
 			virtual void addSystemMessage(const String& message) = 0;
 			virtual void addPresenceMessage(const String& message) = 0;
 			virtual void addErrorMessage(const String& message) = 0;
@@ -49,6 +56,7 @@ namespace Swift {
 			virtual void setRosterModel(Roster* model) = 0;
 			virtual void setTabComplete(TabComplete* completer) = 0;
 			virtual void replaceLastMessage(const String& message) = 0;
+			virtual void setAckState(const String& id, AckState state) = 0;
 
 			boost::signal<void ()> onClosed;
 			boost::signal<void ()> onAllMessagesRead;
diff --git a/Swift/Controllers/UnitTest/MockChatWindow.h b/Swift/Controllers/UnitTest/MockChatWindow.h
index 822a128..4e7a117 100644
--- a/Swift/Controllers/UnitTest/MockChatWindow.h
+++ b/Swift/Controllers/UnitTest/MockChatWindow.h
@@ -14,8 +14,8 @@ namespace Swift {
 			MockChatWindow() {};
 			virtual ~MockChatWindow();
 
-			virtual void addMessage(const String& message, const String& /*senderName*/, bool /*senderIsSelf*/, const boost::optional<SecurityLabel>& /*label*/, const String& /*avatarPath*/, const boost::posix_time::ptime&) {lastMessageBody_ = message;};
-			virtual void addAction(const String& message, const String& /*senderName*/, bool /*senderIsSelf*/, const boost::optional<SecurityLabel>& /*label*/, const String& /*avatarPath*/, const boost::posix_time::ptime&) {lastMessageBody_ = message;};
+			virtual String addMessage(const String& message, const String& /*senderName*/, bool /*senderIsSelf*/, const boost::optional<SecurityLabel>& /*label*/, const String& /*avatarPath*/, const boost::posix_time::ptime&) {lastMessageBody_ = message; return "";};
+			virtual String addAction(const String& message, const String& /*senderName*/, bool /*senderIsSelf*/, const boost::optional<SecurityLabel>& /*label*/, const String& /*avatarPath*/, const boost::posix_time::ptime&) {lastMessageBody_ = message; return "";};
 			virtual void addSystemMessage(const String& /*message*/) {};
 			virtual void addErrorMessage(const String& /*message*/) {};
 			virtual void addPresenceMessage(const String& /*message*/) {};
@@ -34,6 +34,7 @@ namespace Swift {
 			virtual void setRosterModel(Roster* /*roster*/) {};
 			virtual void setTabComplete(TabComplete*) {};
 			virtual void replaceLastMessage(const Swift::String&) {};
+			void setAckState(const String& /*id*/, AckState /*state*/) {};
 
 			boost::signal<void ()> onClosed;
 			boost::signal<void ()> onAllMessagesRead;
diff --git a/Swift/QtUI/MessageSnippet.cpp b/Swift/QtUI/MessageSnippet.cpp
index 0159386..1c8ddca 100644
--- a/Swift/QtUI/MessageSnippet.cpp
+++ b/Swift/QtUI/MessageSnippet.cpp
@@ -11,7 +11,7 @@
 
 namespace Swift {
 
-MessageSnippet::MessageSnippet(const QString& message, const QString& sender, const QDateTime& time, const QString& iconURI, bool isIncoming, bool appendToPrevious, QtChatTheme* theme) : ChatSnippet(appendToPrevious) {
+MessageSnippet::MessageSnippet(const QString& message, const QString& sender, const QDateTime& time, const QString& iconURI, bool isIncoming, bool appendToPrevious, QtChatTheme* theme, const QString& id) : ChatSnippet(appendToPrevious) {
 	if (isIncoming) {
 		if (appendToPrevious) {
 			content_ = theme->getIncomingNextContent();
@@ -29,10 +29,11 @@ MessageSnippet::MessageSnippet(const QString& message, const QString& sender, co
 		}
 	}
 
-	content_.replace("%message%", "<span class='swift_message'>" + escape(message) + "</span>");
+	content_.replace("%message%", "<span class='swift_ack'></span><span class='swift_message'>" + escape(message) + "</span>");
 	content_.replace("%sender%", escape(sender));
 	content_.replace("%time%", "<span class='swift_time'>" + escape(time.toString("h:mm")) + "</span>");
 	content_.replace("%userIconPath%", escape(iconURI));
+	content_ = "<div id='" + id + "'>" + content_ + "</div>";
 }
 
 MessageSnippet::~MessageSnippet() {
diff --git a/Swift/QtUI/MessageSnippet.h b/Swift/QtUI/MessageSnippet.h
index 4918c19..c7425e9 100644
--- a/Swift/QtUI/MessageSnippet.h
+++ b/Swift/QtUI/MessageSnippet.h
@@ -15,7 +15,7 @@ class QDateTime;
 namespace Swift {
 	class MessageSnippet : public ChatSnippet {
 		public:
-			MessageSnippet(const QString& message, const QString& sender, const QDateTime& time, const QString& iconURI, bool isIncoming, bool appendToPrevious, QtChatTheme* theme);
+			MessageSnippet(const QString& message, const QString& sender, const QDateTime& time, const QString& iconURI, bool isIncoming, bool appendToPrevious, QtChatTheme* theme, const QString& id);
 			virtual ~MessageSnippet();
 			const QString& getContent() const {
 				return content_;
diff --git a/Swift/QtUI/QtChatView.cpp b/Swift/QtUI/QtChatView.cpp
index d48365b..145371f 100644
--- a/Swift/QtUI/QtChatView.cpp
+++ b/Swift/QtUI/QtChatView.cpp
@@ -129,6 +129,15 @@ void QtChatView::copySelectionToClipboard() {
 	}
 }
 
+void QtChatView::setAckXML(const QString& id, const QString& xml) {
+	QWebElement message = document_.findFirst("#" + id);
+	/* Deliberately not asserting here, so that when we start expiring old messages it won't hit us */
+	if (message.isNull()) return;
+	QWebElement ackElement = message.findFirst("span.swift_ack");
+	assert(!ackElement.isNull());
+	ackElement.setInnerXml(xml);
+}
+
 bool QtChatView::isScrolledToBottom() const {
 	return webPage_->mainFrame()->scrollBarValue(Qt::Vertical) == webPage_->mainFrame()->scrollBarMaximum(Qt::Vertical);
 }
diff --git a/Swift/QtUI/QtChatView.h b/Swift/QtUI/QtChatView.h
index ce1f8bc..e60c92f 100644
--- a/Swift/QtUI/QtChatView.h
+++ b/Swift/QtUI/QtChatView.h
@@ -31,6 +31,7 @@ namespace Swift {
 			void replaceLastMessage(const QString& newMessage);
 			void replaceLastMessage(const QString& newMessage, const QString& note);
 			bool isScrolledToBottom() const;
+			void setAckXML(const QString& id, const QString& xml);
 
 		signals:
 			void gotFocus();
diff --git a/Swift/QtUI/QtChatWindow.cpp b/Swift/QtUI/QtChatWindow.cpp
index 70bde4b..2955ee4 100644
--- a/Swift/QtUI/QtChatWindow.cpp
+++ b/Swift/QtUI/QtChatWindow.cpp
@@ -245,11 +245,11 @@ void QtChatWindow::updateTitleWithUnreadCount() {
 	emit titleUpdated();
 }
 
-void QtChatWindow::addMessage(const String &message, const String &senderName, bool senderIsSelf, const boost::optional<SecurityLabel>& label, const String& avatarPath, const boost::posix_time::ptime& time) {
-	addMessage(message, senderName, senderIsSelf, label, avatarPath, "", time);
+String QtChatWindow::addMessage(const String &message, const String &senderName, bool senderIsSelf, const boost::optional<SecurityLabel>& label, const String& avatarPath, const boost::posix_time::ptime& time) {
+	return addMessage(message, senderName, senderIsSelf, label, avatarPath, "", time);
 }
 
-void QtChatWindow::addMessage(const String &message, const String &senderName, bool senderIsSelf, const boost::optional<SecurityLabel>& label, const String& avatarPath, const QString& style, const boost::posix_time::ptime& time) {
+String QtChatWindow::addMessage(const String &message, const String &senderName, bool senderIsSelf, const boost::optional<SecurityLabel>& label, const String& avatarPath, const QString& style, const boost::posix_time::ptime& time) {
 	if (isWidgetSelected()) {
 		onAllMessagesRead();
 	}
@@ -268,20 +268,32 @@ void QtChatWindow::addMessage(const String &message, const String &senderName, b
 
 	bool appendToPrevious = !previousMessageWasSystem_ && !previousMessageWasPresence_ && ((senderIsSelf && previousMessageWasSelf_) || (!senderIsSelf && !previousMessageWasSelf_ && previousSenderName_ == P2QSTRING(senderName)));
 	QString qAvatarPath =  avatarPath.isEmpty() ? "qrc:/icons/avatar.png" : QUrl::fromLocalFile(P2QSTRING(avatarPath)).toEncoded();
-	messageLog_->addMessage(boost::shared_ptr<ChatSnippet>(new MessageSnippet(htmlString, Qt::escape(P2QSTRING(senderName)), B2QDATE(time), qAvatarPath, senderIsSelf, appendToPrevious, theme_)));
+	String id = id_.generateID();
+	messageLog_->addMessage(boost::shared_ptr<ChatSnippet>(new MessageSnippet(htmlString, Qt::escape(P2QSTRING(senderName)), B2QDATE(time), qAvatarPath, senderIsSelf, appendToPrevious, theme_, P2QSTRING(id))));
 
 	previousMessageWasSelf_ = senderIsSelf;
 	previousSenderName_ = P2QSTRING(senderName);
 	previousMessageWasSystem_ = false;
 	previousMessageWasPresence_ = false;
+	return id;
+}
+
+void QtChatWindow::setAckState(String const& id, ChatWindow::AckState state) {
+	QString xml;
+	switch (state) {
+		case ChatWindow::Pending: xml = "<img src='qrc:/icons/throbber.gif' alt='This message has not been received by your server yet.'/>"; break;
+		case ChatWindow::Received: xml = ""; break;
+		case ChatWindow::Failed: xml = "<img src='qrc:/icons/error.png' alt='This message may not have been transmitted.'/>"; break;
+	}
+	messageLog_->setAckXML(P2QSTRING(id), xml);
 }
 
 int QtChatWindow::getCount() {
 	return unreadCount_;
 }
 
-void QtChatWindow::addAction(const String &message, const String &senderName, bool senderIsSelf, const boost::optional<SecurityLabel>& label, const String& avatarPath, const boost::posix_time::ptime& time) {
-	addMessage(" *" + message + "*", senderName, senderIsSelf, label, avatarPath, "font-style:italic ", time);
+String QtChatWindow::addAction(const String &message, const String &senderName, bool senderIsSelf, const boost::optional<SecurityLabel>& label, const String& avatarPath, const boost::posix_time::ptime& time) {
+	return addMessage(" *" + message + "*", senderName, senderIsSelf, label, avatarPath, "font-style:italic ", time);
 }
 
 void QtChatWindow::addErrorMessage(const String& errorMessage) {
diff --git a/Swift/QtUI/QtChatWindow.h b/Swift/QtUI/QtChatWindow.h
index a51b866..333aa44 100644
--- a/Swift/QtUI/QtChatWindow.h
+++ b/Swift/QtUI/QtChatWindow.h
@@ -11,6 +11,8 @@
 
 #include "QtTabbable.h"
 
+#include "Swiften/Base/IDGenerator.h"
+
 class QTextEdit;
 class QLineEdit;
 class QComboBox;
@@ -28,8 +30,8 @@ namespace Swift {
 		public:
 			QtChatWindow(const QString &contact, QtChatTheme* theme, UIEventStream* eventStream);
 			~QtChatWindow();
-			void addMessage(const String &message, const String &senderName, bool senderIsSelf, const boost::optional<SecurityLabel>& label, const String& avatarPath, const boost::posix_time::ptime& time);
-			void addAction(const String &message, const String &senderName, bool senderIsSelf, const boost::optional<SecurityLabel>& label, const String& avatarPath, const boost::posix_time::ptime& time);
+			String addMessage(const String &message, const String &senderName, bool senderIsSelf, const boost::optional<SecurityLabel>& label, const String& avatarPath, const boost::posix_time::ptime& time);
+			String addAction(const String &message, const String &senderName, bool senderIsSelf, const boost::optional<SecurityLabel>& label, const String& avatarPath, const boost::posix_time::ptime& time);
 			void addSystemMessage(const String& message);
 			void addPresenceMessage(const String& message);
 			void addErrorMessage(const String& errorMessage);
@@ -50,6 +52,7 @@ namespace Swift {
 			void setTabComplete(TabComplete* completer);
 			int getCount();
 			void replaceLastMessage(const String& message);
+			void setAckState(const String& id, AckState state);
 
 		signals:
 			void geometryChanged();
@@ -71,7 +74,7 @@ namespace Swift {
 		private:
 			void updateTitleWithUnreadCount();
 			void tabComplete();
-			void addMessage(const String &message, const String &senderName, bool senderIsSelf, const boost::optional<SecurityLabel>& label, const String& avatarPath, const QString& style, const boost::posix_time::ptime& time);
+			String addMessage(const String &message, const String &senderName, bool senderIsSelf, const boost::optional<SecurityLabel>& label, const String& avatarPath, const QString& style, const boost::posix_time::ptime& time);
 
 			int unreadCount_;
 			bool contactIsTyping_;
@@ -90,6 +93,7 @@ namespace Swift {
 			bool inputClearing_;
 			UIEventStream* eventStream_;
 			bool inputEnabled_;
+			IDGenerator id_;
 	};
 }
 
diff --git a/Swift/QtUI/Swift.qrc b/Swift/QtUI/Swift.qrc
index 4da687c..6c8e027 100644
--- a/Swift/QtUI/Swift.qrc
+++ b/Swift/QtUI/Swift.qrc
@@ -10,6 +10,7 @@
 		<file alias="icons/offline.png">../resources/icons/offline.png</file>
 		<file alias="icons/certificate.png">../resources/icons/certificate.png</file>
 		<file alias="icons/error.png">../resources/icons/error.png</file>
+		<file alias="icons/throbber.gif">../resources/icons/throbber.gif</file>
 		<file alias="icons/avatar.png">../resources/icons/avatar.png</file>
 		<file alias="icons/tray-standard.png">../resources/icons/tray-standard.png</file>
 		<file alias="icons/new-chat.png">../resources/icons/new-chat.png</file>
diff --git a/Swift/resources/icons/throbber.gif b/Swift/resources/icons/throbber.gif
new file mode 100644
index 0000000..d0bce15
Binary files /dev/null and b/Swift/resources/icons/throbber.gif differ
diff --git a/Swiften/Client/Client.h b/Swiften/Client/Client.h
index 10e7c38..57228ef 100644
--- a/Swiften/Client/Client.h
+++ b/Swiften/Client/Client.h
@@ -55,7 +55,6 @@ namespace Swift {
 			boost::signal<void ()> onConnected;
 			boost::signal<void (const String&)> onDataRead;
 			boost::signal<void (const String&)> onDataWritten;
-			boost::signal<void (boost::shared_ptr<Stanza>)> onStanzaAcked;
 
 		private:
 			void handleConnectorFinished(boost::shared_ptr<Connection>, Connector::ref);
diff --git a/Swiften/Client/DummyStanzaChannel.h b/Swiften/Client/DummyStanzaChannel.h
index b35ed92..d43d68a 100644
--- a/Swiften/Client/DummyStanzaChannel.h
+++ b/Swiften/Client/DummyStanzaChannel.h
@@ -39,6 +39,10 @@ namespace Swift {
 				return true;
 			}
 
+			virtual bool getStreamManagementEnabled() const {
+				return false;
+			}
+
 			template<typename T> bool isRequestAtIndex(int index, const JID& jid, IQ::Type type) {
 				boost::shared_ptr<IQ> iqStanza = boost::dynamic_pointer_cast<IQ>(sentStanzas[index]);
 				return iqStanza && iqStanza->getType() == type && iqStanza->getTo() == jid && iqStanza->getPayload<T>();
diff --git a/Swiften/Client/StanzaChannel.h b/Swiften/Client/StanzaChannel.h
index bfde05c..09a6db3 100644
--- a/Swiften/Client/StanzaChannel.h
+++ b/Swiften/Client/StanzaChannel.h
@@ -19,8 +19,10 @@ namespace Swift {
 			virtual void sendMessage(boost::shared_ptr<Message>) = 0;
 			virtual void sendPresence(boost::shared_ptr<Presence>) = 0;
 			virtual bool isAvailable() = 0;
+			virtual bool getStreamManagementEnabled() const = 0;
 
 			boost::signal<void (boost::shared_ptr<Message>)> onMessageReceived;
 			boost::signal<void (boost::shared_ptr<Presence>) > onPresenceReceived;
+			boost::signal<void (boost::shared_ptr<Stanza>)> onStanzaAcked;
 	};
 }
-- 
cgit v0.10.2-6-g49f6