summaryrefslogtreecommitdiffstats
path: root/Swift
diff options
context:
space:
mode:
authorKevin Smith <git@kismith.co.uk>2010-08-31 19:10:57 (GMT)
committerKevin Smith <git@kismith.co.uk>2010-09-03 10:02:35 (GMT)
commiteb50ea03ab7fc41610a8945002fe19dd30ffb5d7 (patch)
treed26d378e2996b163f13562488eb2bbc31d89db04 /Swift
parent276d7f82ba42cdbc65ec5c9f35873a265a69bd73 (diff)
downloadswift-eb50ea03ab7fc41610a8945002fe19dd30ffb5d7.zip
swift-eb50ea03ab7fc41610a8945002fe19dd30ffb5d7.tar.bz2
Squash presence in chat and MUC windows.
Join/Parts will be shown in one block if they're uninterrupted, and only the last presence change in a row will be shown for chats. Resolves: #230 Resolves: #430
Diffstat (limited to 'Swift')
-rw-r--r--Swift/Controllers/Chat/ChatController.cpp10
-rw-r--r--Swift/Controllers/Chat/ChatController.h1
-rw-r--r--Swift/Controllers/Chat/MUCController.cpp86
-rw-r--r--Swift/Controllers/Chat/MUCController.h16
-rw-r--r--Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp101
-rw-r--r--Swift/Controllers/SConscript1
-rw-r--r--Swift/Controllers/UIInterfaces/ChatWindow.h1
-rw-r--r--Swift/Controllers/UnitTest/MockChatWindow.h1
-rw-r--r--Swift/QtUI/QtChatView.cpp21
-rw-r--r--Swift/QtUI/QtChatView.h4
-rw-r--r--Swift/QtUI/QtChatWindow.cpp4
-rw-r--r--Swift/QtUI/QtChatWindow.h1
12 files changed, 233 insertions, 14 deletions
diff --git a/Swift/Controllers/Chat/ChatController.cpp b/Swift/Controllers/Chat/ChatController.cpp
index d9524da..52288c2 100644
--- a/Swift/Controllers/Chat/ChatController.cpp
+++ b/Swift/Controllers/Chat/ChatController.cpp
@@ -25,6 +25,7 @@ namespace Swift {
ChatController::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)
: ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, contact, presenceOracle, avatarManager, useDelayForLatency, eventStream, eventController) {
isInMUC_ = isInMUC;
+ lastWasPresence_ = false;
chatStateNotifier_ = new ChatStateNotifier();
chatStateMessageSender_ = new ChatStateMessageSender(chatStateNotifier_, stanzaChannel, contact);
chatStateTracker_ = new ChatStateTracker();
@@ -70,6 +71,7 @@ void ChatController::preHandleIncomingMessage(boost::shared_ptr<MessageEvent> me
}
chatStateNotifier_->receivedMessageFromContact(message->getPayload<ChatState>());
chatStateTracker_->handleMessageReceived(message);
+ lastWasPresence_ = false;
}
void ChatController::preSendMessageRequest(boost::shared_ptr<Message> message) {
@@ -80,6 +82,7 @@ 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());
+ lastWasPresence_ = false;
chatStateNotifier_->userSentMessage();
}
@@ -113,7 +116,12 @@ void ChatController::handlePresenceChange(boost::shared_ptr<Presence> newPresenc
chatStateTracker_->handlePresenceChange(newPresence, previousPresence);
String newStatusChangeString = getStatusChangeString(newPresence);
if (!previousPresence || newStatusChangeString != getStatusChangeString(previousPresence)) {
- chatWindow_->addPresenceMessage(newStatusChangeString);
+ if (lastWasPresence_) {
+ chatWindow_->replaceLastMessage(newStatusChangeString);
+ } else {
+ chatWindow_->addPresenceMessage(newStatusChangeString);
+ }
+ lastWasPresence_ = true;
}
}
diff --git a/Swift/Controllers/Chat/ChatController.h b/Swift/Controllers/Chat/ChatController.h
index 291a0d0..d833094 100644
--- a/Swift/Controllers/Chat/ChatController.h
+++ b/Swift/Controllers/Chat/ChatController.h
@@ -38,6 +38,7 @@ namespace Swift {
ChatStateMessageSender* chatStateMessageSender_;
ChatStateTracker* chatStateTracker_;
bool isInMUC_;
+ bool lastWasPresence_;
};
}
#endif
diff --git a/Swift/Controllers/Chat/MUCController.cpp b/Swift/Controllers/Chat/MUCController.cpp
index 5858127..3b37179 100644
--- a/Swift/Controllers/Chat/MUCController.cpp
+++ b/Swift/Controllers/Chat/MUCController.cpp
@@ -10,6 +10,7 @@
#include "Swiften/Network/Timer.h"
#include "Swiften/Network/TimerFactory.h"
+#include "Swiften/Base/foreach.h"
#include "SwifTools/TabComplete.h"
#include "Swiften/Base/foreach.h"
#include "Swift/Controllers/EventController.h"
@@ -50,6 +51,7 @@ MUCController::MUCController (
ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, muc, presenceOracle, avatarManager, useDelayForLatency, uiEventStream, eventController), muc_(new MUC(stanzaChannel, presenceSender, muc)), nick_(nick) {
parting_ = true;
joined_ = false;
+ lastWasPresence_ = false;
events_ = uiEventStream;
roster_ = new Roster(false, true);
@@ -142,6 +144,7 @@ void MUCController::handleJoinComplete(const String& nick) {
String joinMessage = "You have joined room " + toJID_.toString() + " as " + nick;
nick_ = nick;
chatWindow_->addSystemMessage(joinMessage);
+ clearPresenceQueue();
setEnabled(true);
}
@@ -170,6 +173,8 @@ void MUCController::handleOccupantJoined(const MUCOccupant& occupant) {
realJID = occupant.getRealJID().get();
}
currentOccupants_.insert(occupant.getNick());
+ NickJoinPart event(occupant.getNick(), Join);
+ appendToJoinParts(joinParts_, event);
roster_->addContact(jid, realJID, occupant.getNick(), roleToGroupName(occupant.getRole()));
if (joined_) {
String joinString = occupant.getNick() + " has joined the room";
@@ -179,13 +184,28 @@ void MUCController::handleOccupantJoined(const MUCOccupant& occupant) {
}
joinString += ".";
- chatWindow_->addPresenceMessage(joinString);
+ if (shouldUpdateJoinParts()) {
+ updateJoinParts();
+ } else {
+ addPresenceMessage(joinString);
+
+ }
}
if (avatarManager_ != NULL) {
handleAvatarChanged(jid, "dummy");
}
}
+void MUCController::addPresenceMessage(const String& message) {
+ lastWasPresence_ = true;
+ chatWindow_->addPresenceMessage(message);
+}
+
+void MUCController::clearPresenceQueue() {
+ lastWasPresence_ = false;
+ joinParts_.clear();
+}
+
String MUCController::roleToFriendlyName(MUCOccupant::Role role) {
switch (role) {
case MUCOccupant::Moderator: return "moderator";
@@ -205,6 +225,7 @@ bool MUCController::messageTargetsMe(boost::shared_ptr<Message> message) {
}
void MUCController::preHandleIncomingMessage(boost::shared_ptr<MessageEvent> messageEvent) {
+ clearPresenceQueue();
boost::shared_ptr<Message> message = messageEvent->getStanza();
if (joined_ && messageTargetsMe(message) && !message->getPayload<Delay>()) {
eventController_->handleIncomingEvent(messageEvent);
@@ -260,7 +281,13 @@ void MUCController::setEnabled(bool enabled) {
}
}
+bool MUCController::shouldUpdateJoinParts() {
+ return lastWasPresence_;
+}
+
void MUCController::handleOccupantLeft(const MUCOccupant& occupant, MUC::LeavingType, const String& reason) {
+ NickJoinPart event(occupant.getNick(), Part);
+ appendToJoinParts(joinParts_, event);
currentOccupants_.erase(occupant.getNick());
completer_->removeWord(occupant.getNick());
String partMessage = (occupant.getNick() != nick_) ? occupant.getNick() + " has left the room" : "You have left the room";
@@ -268,10 +295,16 @@ void MUCController::handleOccupantLeft(const MUCOccupant& occupant, MUC::Leaving
partMessage += " (" + reason + ")";
}
partMessage += ".";
- chatWindow_->addPresenceMessage(partMessage);
+
if (occupant.getNick() != nick_) {
+ if (shouldUpdateJoinParts()) {
+ updateJoinParts();
+ } else {
+ addPresenceMessage(partMessage);
+ }
roster_->removeContact(JID(toJID_.getNode(), toJID_.getDomain(), occupant.getNick()));
} else {
+ addPresenceMessage(partMessage);
parting_ = true;
setEnabled(false);
}
@@ -299,4 +332,53 @@ boost::optional<boost::posix_time::ptime> MUCController::getMessageTimestamp(boo
return message->getTimestampFrom(toJID_);
}
+void MUCController::updateJoinParts() {
+ chatWindow_->replaceLastMessage(generateJoinPartString(joinParts_));
+}
+
+void MUCController::appendToJoinParts(std::vector<NickJoinPart>& joinParts, const NickJoinPart& newEvent) {
+ std::vector<NickJoinPart>::iterator it = joinParts.begin();
+ bool matched = false;
+ for (; it != joinParts.end(); it++) {
+ if ((*it).nick == newEvent.nick) {
+ matched = true;
+ JoinPart type = (*it).type;
+ switch (newEvent.type) {
+ case Join: type = (type == Part) ? PartThenJoin : Join; break;
+ case Part: type = (type == Join) ? JoinThenPart : Part; break;
+ default: /*Nothing to see here */;break;
+ }
+ (*it).type = type;
+ break;
+ }
+ }
+ if (!matched) {
+ joinParts.push_back(newEvent);
+ }
+}
+
+String MUCController::generateJoinPartString(std::vector<NickJoinPart> joinParts) {
+ String result;
+ for (size_t i = 0; i < joinParts.size(); i++) {
+ if (i > 0) {
+ if (i < joinParts.size() - 1) {
+ result += ", ";
+ } else {
+ result += " and ";
+ }
+ }
+ NickJoinPart event = joinParts[i];
+ result += event.nick;
+ switch (event.type) {
+ case Join: result += " has joined";break;
+ case Part: result += " has left";break;
+ case JoinThenPart: result += " joined then left";break;
+ case PartThenJoin: result += " left then rejoined";break;
+ }
+ result += " the room";
+ }
+ result += ".";
+ return result;
+}
+
}
diff --git a/Swift/Controllers/Chat/MUCController.h b/Swift/Controllers/Chat/MUCController.h
index 31e3d48..4601386 100644
--- a/Swift/Controllers/Chat/MUCController.h
+++ b/Swift/Controllers/Chat/MUCController.h
@@ -31,6 +31,14 @@ namespace Swift {
class TimerFactory;
class TabComplete;
+ enum JoinPart {Join, Part, JoinThenPart, PartThenJoin};
+
+ struct NickJoinPart {
+ NickJoinPart(const String& nick, JoinPart type) : nick(nick), type(type) {};
+ String nick;
+ JoinPart type;
+ };
+
class MUCController : public ChatControllerBase {
public:
MUCController(const JID& self, const JID &muc, const String &nick, StanzaChannel* stanzaChannel, PresenceSender* presenceSender, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, PresenceOracle* presenceOracle, AvatarManager* avatarManager, UIEventStream* events, bool useDelayForLatency, TimerFactory* timerFactory, EventController* eventController);
@@ -38,6 +46,8 @@ namespace Swift {
boost::signal<void ()> onUserLeft;
virtual void setEnabled(bool enabled);
void rejoin();
+ static void appendToJoinParts(std::vector<NickJoinPart>& joinParts, const NickJoinPart& newEvent);
+ static String generateJoinPartString(std::vector<NickJoinPart> joinParts);
protected:
void preSendMessageRequest(boost::shared_ptr<Message> message);
@@ -47,6 +57,8 @@ namespace Swift {
void preHandleIncomingMessage(boost::shared_ptr<MessageEvent>);
private:
+ void clearPresenceQueue();
+ void addPresenceMessage(const String& message);
void handleWindowClosed();
void handleAvatarChanged(const JID& jid, const String&);
void handleOccupantJoined(const MUCOccupant& occupant);
@@ -61,6 +73,8 @@ namespace Swift {
String roleToFriendlyName(MUCOccupant::Role role);
void receivedActivity();
bool messageTargetsMe(boost::shared_ptr<Message> message);
+ void updateJoinParts();
+ bool shouldUpdateJoinParts();
private:
MUC* muc_;
UIEventStream* events_;
@@ -69,9 +83,11 @@ namespace Swift {
TabComplete* completer_;
bool parting_;
bool joined_;
+ bool lastWasPresence_;
boost::bsignals::scoped_connection avatarChangedConnection_;
boost::shared_ptr<Timer> loginCheckTimer_;
std::set<String> currentOccupants_;
+ std::vector<NickJoinPart> joinParts_;
};
}
diff --git a/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp b/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp
new file mode 100644
index 0000000..fbc6901
--- /dev/null
+++ b/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2010 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include <cppunit/extensions/HelperMacros.h>
+#include <cppunit/extensions/TestFactoryRegistry.h>
+
+#include "Swift/Controllers/Chat/MUCController.h"
+
+using namespace Swift;
+
+class MUCControllerTest : public CppUnit::TestFixture
+{
+ CPPUNIT_TEST_SUITE(MUCControllerTest);
+ CPPUNIT_TEST(testJoinPartStringContructionSimple);
+ CPPUNIT_TEST(testJoinPartStringContructionMixed);
+ CPPUNIT_TEST(testAppendToJoinParts);
+ CPPUNIT_TEST_SUITE_END();
+
+public:
+ MUCControllerTest() {};
+
+ void setUp() {
+ };
+
+ void tearDown() {
+ }
+
+ void checkEqual(const std::vector<NickJoinPart>& expected, const std::vector<NickJoinPart>& actual) {
+ CPPUNIT_ASSERT_EQUAL(expected.size(), actual.size());
+ for (size_t i = 0; i < expected.size(); i++) {
+ CPPUNIT_ASSERT_EQUAL(expected[i].nick, actual[i].nick);
+ CPPUNIT_ASSERT_EQUAL(expected[i].type, actual[i].type);
+ }
+ }
+
+ void testAppendToJoinParts() {
+ std::vector<NickJoinPart> list;
+ std::vector<NickJoinPart> gold;
+ MUCController::appendToJoinParts(list, NickJoinPart("Kev", Join));
+ gold.push_back(NickJoinPart("Kev", Join));
+ checkEqual(gold, list);
+ MUCController::appendToJoinParts(list, NickJoinPart("Remko", Join));
+ gold.push_back(NickJoinPart("Remko", Join));
+ checkEqual(gold, list);
+ MUCController::appendToJoinParts(list, NickJoinPart("Bert", Join));
+ gold.push_back(NickJoinPart("Bert", Join));
+ checkEqual(gold, list);
+ MUCController::appendToJoinParts(list, NickJoinPart("Bert", Part));
+ gold[2].type = JoinThenPart;
+ checkEqual(gold, list);
+ MUCController::appendToJoinParts(list, NickJoinPart("Kev", Part));
+ gold[0].type = JoinThenPart;
+ checkEqual(gold, list);
+ MUCController::appendToJoinParts(list, NickJoinPart("Remko", Part));
+ gold[1].type = JoinThenPart;
+ checkEqual(gold, list);
+ MUCController::appendToJoinParts(list, NickJoinPart("Ernie", Part));
+ gold.push_back(NickJoinPart("Ernie", Part));
+ checkEqual(gold, list);
+ MUCController::appendToJoinParts(list, NickJoinPart("Ernie", Join));
+ gold[3].type = PartThenJoin;
+ checkEqual(gold, list);
+ MUCController::appendToJoinParts(list, NickJoinPart("Kev", Join));
+ gold[0].type = Join;
+ checkEqual(gold, list);
+ MUCController::appendToJoinParts(list, NickJoinPart("Ernie", Part));
+ gold[3].type = Part;
+ checkEqual(gold, list);
+
+ }
+
+ void testJoinPartStringContructionSimple() {
+ std::vector<NickJoinPart> list;
+ list.push_back(NickJoinPart("Kev", Join));
+ CPPUNIT_ASSERT_EQUAL(String("Kev has joined the room."), MUCController::generateJoinPartString(list));
+ list.push_back(NickJoinPart("Remko", Part));
+ CPPUNIT_ASSERT_EQUAL(String("Kev has joined the room and Remko has left the room."), MUCController::generateJoinPartString(list));
+ list.push_back(NickJoinPart("Bert", Join));
+ CPPUNIT_ASSERT_EQUAL(String("Kev has joined the room, Remko has left the room and Bert has joined the room."), MUCController::generateJoinPartString(list));
+ list.push_back(NickJoinPart("Ernie", Join));
+ CPPUNIT_ASSERT_EQUAL(String("Kev has joined the room, Remko has left the room, Bert has joined the room and Ernie has joined the room."), MUCController::generateJoinPartString(list));
+ }
+
+ void testJoinPartStringContructionMixed() {
+ std::vector<NickJoinPart> list;
+ list.push_back(NickJoinPart("Kev", JoinThenPart));
+ CPPUNIT_ASSERT_EQUAL(String("Kev joined then left the room."), MUCController::generateJoinPartString(list));
+ list.push_back(NickJoinPart("Remko", Part));
+ CPPUNIT_ASSERT_EQUAL(String("Kev joined then left the room and Remko has left the room."), MUCController::generateJoinPartString(list));
+ list.push_back(NickJoinPart("Bert", PartThenJoin));
+ CPPUNIT_ASSERT_EQUAL(String("Kev joined then left the room, Remko has left the room and Bert left then rejoined the room."), MUCController::generateJoinPartString(list));
+ list.push_back(NickJoinPart("Ernie", JoinThenPart));
+ CPPUNIT_ASSERT_EQUAL(String("Kev joined then left the room, Remko has left the room, Bert left then rejoined the room and Ernie joined then left the room."), MUCController::generateJoinPartString(list));
+ }
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(MUCControllerTest);
+
diff --git a/Swift/Controllers/SConscript b/Swift/Controllers/SConscript
index ea0dc3a..1ccee64 100644
--- a/Swift/Controllers/SConscript
+++ b/Swift/Controllers/SConscript
@@ -48,5 +48,6 @@ if env["SCONS_STAGE"] == "build" :
File("UnitTest/XMPPRosterControllerTest.cpp"),
File("UnitTest/PreviousStatusStoreTest.cpp"),
File("Chat/UnitTest/ChatsManagerTest.cpp"),
+ File("Chat/UnitTest/MUCControllerTest.cpp"),
File("UnitTest/MockChatWindow.cpp"),
])
diff --git a/Swift/Controllers/UIInterfaces/ChatWindow.h b/Swift/Controllers/UIInterfaces/ChatWindow.h
index 79c7d65..4d00dca 100644
--- a/Swift/Controllers/UIInterfaces/ChatWindow.h
+++ b/Swift/Controllers/UIInterfaces/ChatWindow.h
@@ -48,6 +48,7 @@ namespace Swift {
virtual void setInputEnabled(bool enabled) = 0;
virtual void setRosterModel(Roster* model) = 0;
virtual void setTabComplete(TabComplete* completer) = 0;
+ virtual void replaceLastMessage(const String& message) = 0;
boost::signal<void ()> onClosed;
boost::signal<void ()> onAllMessagesRead;
diff --git a/Swift/Controllers/UnitTest/MockChatWindow.h b/Swift/Controllers/UnitTest/MockChatWindow.h
index 4985cbb..822a128 100644
--- a/Swift/Controllers/UnitTest/MockChatWindow.h
+++ b/Swift/Controllers/UnitTest/MockChatWindow.h
@@ -33,6 +33,7 @@ namespace Swift {
virtual void setInputEnabled(bool /*enabled*/) {};
virtual void setRosterModel(Roster* /*roster*/) {};
virtual void setTabComplete(TabComplete*) {};
+ virtual void replaceLastMessage(const Swift::String&) {};
boost::signal<void ()> onClosed;
boost::signal<void ()> onAllMessagesRead;
diff --git a/Swift/QtUI/QtChatView.cpp b/Swift/QtUI/QtChatView.cpp
index ef558b7..d48365b 100644
--- a/Swift/QtUI/QtChatView.cpp
+++ b/Swift/QtUI/QtChatView.cpp
@@ -106,16 +106,21 @@ void QtChatView::addToDOM(boost::shared_ptr<ChatSnippet> snippet) {
}
}
-void QtChatView::correctLastMessage(const QString& newMessage) {
+void QtChatView::replaceLastMessage(const QString& newMessage) {
/* FIXME: must be queued */
- lastElement_.findFirst("swift_message");
- lastElement_.setPlainText(ChatSnippet::escape(newMessage));
+ QWebElement replace = lastElement_.findFirst("span.swift_message");
+ assert(!replace.isNull());
+ QString old = lastElement_.toOuterXml();
+ replace.setInnerXml(ChatSnippet::escape(newMessage));
+ qDebug() << "Replacing old: " << old;
+ qDebug() << "With new: " << lastElement_.toOuterXml();
}
-void QtChatView::correctLastMessage(const QString& newMessage, const QString& note) {
- correctLastMessage(newMessage);
- lastElement_.findFirst("swift_time");
- lastElement_.setPlainText(ChatSnippet::escape(note));
+void QtChatView::replaceLastMessage(const QString& newMessage, const QString& note) {
+ replaceLastMessage(newMessage);
+ QWebElement replace = lastElement_.findFirst("span.swift_time");
+ assert(!replace.isNull());
+ replace.setInnerXml(ChatSnippet::escape(note));
}
void QtChatView::copySelectionToClipboard() {
@@ -147,8 +152,6 @@ void QtChatView::handleViewLoadFinished(bool ok) {
Q_ASSERT(ok);
viewReady_ = true;
addQueuedSnippets();
-// webPage_->mainFrame()->evaluateJavaScript(queuedMessages_);
-// queuedMessages_.clear();
}
}
diff --git a/Swift/QtUI/QtChatView.h b/Swift/QtUI/QtChatView.h
index 01c1ad7..ce1f8bc 100644
--- a/Swift/QtUI/QtChatView.h
+++ b/Swift/QtUI/QtChatView.h
@@ -28,8 +28,8 @@ namespace Swift {
QtChatView(QtChatTheme* theme, QWidget* parent);
void addMessage(boost::shared_ptr<ChatSnippet> snippet);
- void correctLastMessage(const QString& newMessage);
- void correctLastMessage(const QString& newMessage, const QString& note);
+ void replaceLastMessage(const QString& newMessage);
+ void replaceLastMessage(const QString& newMessage, const QString& note);
bool isScrolledToBottom() const;
signals:
diff --git a/Swift/QtUI/QtChatWindow.cpp b/Swift/QtUI/QtChatWindow.cpp
index 53b0dde..70bde4b 100644
--- a/Swift/QtUI/QtChatWindow.cpp
+++ b/Swift/QtUI/QtChatWindow.cpp
@@ -370,4 +370,8 @@ void QtChatWindow::moveEvent(QMoveEvent*) {
emit geometryChanged();
}
+void QtChatWindow::replaceLastMessage(const String& message) {
+ messageLog_->replaceLastMessage(P2QSTRING(message));
+}
+
}
diff --git a/Swift/QtUI/QtChatWindow.h b/Swift/QtUI/QtChatWindow.h
index 2b006d9..a51b866 100644
--- a/Swift/QtUI/QtChatWindow.h
+++ b/Swift/QtUI/QtChatWindow.h
@@ -49,6 +49,7 @@ namespace Swift {
void setRosterModel(Roster* roster);
void setTabComplete(TabComplete* completer);
int getCount();
+ void replaceLastMessage(const String& message);
signals:
void geometryChanged();