From eb50ea03ab7fc41610a8945002fe19dd30ffb5d7 Mon Sep 17 00:00:00 2001 From: Kevin Smith Date: Tue, 31 Aug 2010 20:10:57 +0100 Subject: 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 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 me } chatStateNotifier_->receivedMessageFromContact(message->getPayload()); chatStateTracker_->handleMessageReceived(message); + lastWasPresence_ = false; } void ChatController::preSendMessageRequest(boost::shared_ptr message) { @@ -80,6 +82,7 @@ void ChatController::preSendMessageRequest(boost::shared_ptr message) { void ChatController::postSendMessage(const String& body) { addMessage(body, "me", true, labelsEnabled_ ? chatWindow_->getSelectedSecurityLabel() : boost::optional(), 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 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) { } void MUCController::preHandleIncomingMessage(boost::shared_ptr messageEvent) { + clearPresenceQueue(); boost::shared_ptr message = messageEvent->getStanza(); if (joined_ && messageTargetsMe(message) && !message->getPayload()) { 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 MUCController::getMessageTimestamp(boo return message->getTimestampFrom(toJID_); } +void MUCController::updateJoinParts() { + chatWindow_->replaceLastMessage(generateJoinPartString(joinParts_)); +} + +void MUCController::appendToJoinParts(std::vector& joinParts, const NickJoinPart& newEvent) { + std::vector::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 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 onUserLeft; virtual void setEnabled(bool enabled); void rejoin(); + static void appendToJoinParts(std::vector& joinParts, const NickJoinPart& newEvent); + static String generateJoinPartString(std::vector joinParts); protected: void preSendMessageRequest(boost::shared_ptr message); @@ -47,6 +57,8 @@ namespace Swift { void preHandleIncomingMessage(boost::shared_ptr); 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); + 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 loginCheckTimer_; std::set currentOccupants_; + std::vector 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 +#include + +#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& expected, const std::vector& 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 list; + std::vector 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 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 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 onClosed; boost::signal 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 onClosed; boost::signal 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 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 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(); -- cgit v0.10.2-6-g49f6