diff options
| author | Kevin Smith <git@kismith.co.uk> | 2010-08-31 19:10:57 (GMT) | 
|---|---|---|
| committer | Kevin Smith <git@kismith.co.uk> | 2010-09-03 10:02:35 (GMT) | 
| commit | eb50ea03ab7fc41610a8945002fe19dd30ffb5d7 (patch) | |
| tree | d26d378e2996b163f13562488eb2bbc31d89db04 | |
| parent | 276d7f82ba42cdbc65ec5c9f35873a265a69bd73 (diff) | |
| download | swift-contrib-eb50ea03ab7fc41610a8945002fe19dd30ffb5d7.zip swift-contrib-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
| -rw-r--r-- | Swift/Controllers/Chat/ChatController.cpp | 10 | ||||
| -rw-r--r-- | Swift/Controllers/Chat/ChatController.h | 1 | ||||
| -rw-r--r-- | Swift/Controllers/Chat/MUCController.cpp | 86 | ||||
| -rw-r--r-- | Swift/Controllers/Chat/MUCController.h | 16 | ||||
| -rw-r--r-- | Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp | 101 | ||||
| -rw-r--r-- | Swift/Controllers/SConscript | 1 | ||||
| -rw-r--r-- | Swift/Controllers/UIInterfaces/ChatWindow.h | 1 | ||||
| -rw-r--r-- | Swift/Controllers/UnitTest/MockChatWindow.h | 1 | ||||
| -rw-r--r-- | Swift/QtUI/QtChatView.cpp | 21 | ||||
| -rw-r--r-- | Swift/QtUI/QtChatView.h | 4 | ||||
| -rw-r--r-- | Swift/QtUI/QtChatWindow.cpp | 4 | ||||
| -rw-r--r-- | Swift/QtUI/QtChatWindow.h | 1 | 
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(); | 
 Swift
 Swift