From 4cc96003c6702168da2faa955e3c771272211e32 Mon Sep 17 00:00:00 2001
From: Kevin Smith <git@kismith.co.uk>
Date: Fri, 23 Jul 2010 15:47:48 +0100
Subject: Recognise when leaving a MUC (disconnect or kick).

Also cleans up some outstanding MUC issues.

Resolves: #288
Resolves: #392
Resolves: #279
Resolves: #114

diff --git a/Swift/Controllers/Chat/ChatControllerBase.h b/Swift/Controllers/Chat/ChatControllerBase.h
index f0d4b7a..61d0ab7 100644
--- a/Swift/Controllers/Chat/ChatControllerBase.h
+++ b/Swift/Controllers/Chat/ChatControllerBase.h
@@ -41,7 +41,7 @@ namespace Swift {
 			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);
-			void setEnabled(bool enabled);
+			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);
diff --git a/Swift/Controllers/Chat/ChatsManager.cpp b/Swift/Controllers/Chat/ChatsManager.cpp
index 2ed7051..921cc6f 100644
--- a/Swift/Controllers/Chat/ChatsManager.cpp
+++ b/Swift/Controllers/Chat/ChatsManager.cpp
@@ -139,12 +139,18 @@ void ChatsManager::setServerDiscoInfo(boost::shared_ptr<DiscoInfo> info) {
 	}
 }
 
+/**
+ * This is to be called on connect/disconnect.
+ */ 
 void ChatsManager::setEnabled(bool enabled) {
 	foreach (JIDChatControllerPair controllerPair, chatControllers_) {
 		controllerPair.second->setEnabled(enabled);
 	}
 	foreach (JIDMUCControllerPair controllerPair, mucControllers_) {
 		controllerPair.second->setEnabled(enabled);
+		if (enabled) {
+			controllerPair.second->rejoin();
+		}
 	}
 
 }
@@ -201,9 +207,9 @@ void ChatsManager::rebindControllerJID(const JID& from, const JID& to) {
 void ChatsManager::handleJoinMUCRequest(const JID &muc, const boost::optional<String>& nickMaybe) {
 	std::map<JID, MUCController*>::iterator it = mucControllers_.find(muc);
 	if (it != mucControllers_.end()) {
-		//FIXME: What's correct behaviour here?
+		it->second->rejoin();
 	} else {
-		String nick = nickMaybe ? nickMaybe.get() : "Swift user";
+		String nick = nickMaybe ? nickMaybe.get() : jid_.getNode();
 		MUCController* controller = new MUCController(jid_, muc, nick, stanzaChannel_, presenceSender_, iqRouter_, chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory_, eventController_);
 		mucControllers_[muc] = controller;
 		controller->setAvailableServerFeatures(serverDiscoInfo_);
diff --git a/Swift/Controllers/Chat/MUCController.cpp b/Swift/Controllers/Chat/MUCController.cpp
index 1e790b7..650a915 100644
--- a/Swift/Controllers/Chat/MUCController.cpp
+++ b/Swift/Controllers/Chat/MUCController.cpp
@@ -46,10 +46,9 @@ MUCController::MUCController (
 		bool useDelayForLatency,
 		TimerFactory* timerFactory,
 		EventController* eventController) :
-	ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, muc, presenceOracle, avatarManager, useDelayForLatency, uiEventStream, eventController),
-			muc_(new MUC(stanzaChannel, presenceSender, muc)), 
-	nick_(nick) {
-	parting_ = false;
+			ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, muc, presenceOracle, avatarManager, useDelayForLatency, uiEventStream, eventController), muc_(new MUC(stanzaChannel, presenceSender, muc)), nick_(nick) {
+	parting_ = true;
+	joined_ = false;
 	events_ = uiEventStream;
 	
 	roster_ = new Roster(true);
@@ -69,11 +68,9 @@ MUCController::MUCController (
 		loginCheckTimer_->onTick.connect(boost::bind(&MUCController::handleJoinTimeoutTick, this));
 		loginCheckTimer_->start();
 	}
-
-	muc_->joinAs(nick);
 	chatWindow_->convertToMUC();
 	chatWindow_->addSystemMessage("Trying to join room " + toJID_.toString());
-	joined_ = false;
+	rejoin();
 	if (avatarManager_ != NULL) {
 		avatarChangedConnection_ = (avatarManager_->onAvatarChanged.connect(boost::bind(&MUCController::handleAvatarChanged, this, _1, _2)));
 	} 
@@ -90,6 +87,17 @@ MUCController::~MUCController() {
 	delete completer_;
 }
 
+/**
+ * Join the MUC if not already in it.
+ */
+void MUCController::rejoin() {
+	if (parting_) {
+		joined_ = false;
+		parting_ = false;
+		muc_->joinAs(nick_);
+	}
+}
+
 void MUCController::handleJoinTimeoutTick() {
 	receivedActivity();
 	chatWindow_->addSystemMessage("Room " + toJID_.toString() + " is not responding. This operation may never complete");
@@ -122,7 +130,7 @@ void MUCController::handleJoinFailed(boost::shared_ptr<ErrorPayload> error) {
 	chatWindow_->addErrorMessage(errorMessage);
 	if (!rejoinNick.isEmpty()) {
 		nick_ = rejoinNick;
-		muc_->joinAs(rejoinNick);
+		rejoin();
 	}
 }
 
@@ -132,6 +140,7 @@ void MUCController::handleJoinComplete(const String& nick) {
 	String joinMessage = "You have joined room " + toJID_.toString() + " as " + nick;
 	nick_ = nick;
 	chatWindow_->addSystemMessage(joinMessage);
+	setEnabled(true);
 }
 
 void MUCController::handleAvatarChanged(const JID& jid, const String&) {
@@ -194,7 +203,7 @@ bool MUCController::messageTargetsMe(boost::shared_ptr<Message> message) {
 
 void MUCController::preHandleIncomingMessage(boost::shared_ptr<MessageEvent> messageEvent) {
 	boost::shared_ptr<Message> message = messageEvent->getStanza();
-	if (messageTargetsMe(message)) {
+	if (joined_ && messageTargetsMe(message)) {
 		eventController_->handleIncomingEvent(messageEvent);
 	}
 	String nick = message->getFrom().getResource();
@@ -235,15 +244,32 @@ String MUCController::roleToGroupName(MUCOccupant::Role role) {
 	return result;
 }
 
+void MUCController::setEnabled(bool enabled) {
+	ChatControllerBase::setEnabled(enabled);
+	if (!enabled) {
+		roster_->removeAll();
+		/* handleUserLeft won't throw a part back up unless this is called
+		   when it doesn't yet know we've left - which only happens on
+		   disconnect, so call with disconnect here so if the signal does
+		   bubble back up, it'll be with the right type.*/
+		muc_->handleUserLeft(MUC::Disconnect);
+	}
+}
+
 void MUCController::handleOccupantLeft(const MUCOccupant& occupant, MUC::LeavingType, const String& reason) {
 	completer_->removeWord(occupant.getNick());
-	String partMessage = occupant.getNick() + " has left the room";
+	String partMessage = (occupant.getNick() != nick_) ? occupant.getNick() + " has left the room" : "You have left the room";
 	if (!reason.isEmpty()) {
 		partMessage += " (" + reason + ")";
 	}
 	partMessage += ".";
 	chatWindow_->addSystemMessage(partMessage);
-	roster_->removeContact(JID(toJID_.getNode(), toJID_.getDomain(), occupant.getNick()));
+	if (occupant.getNick() != nick_) {
+		roster_->removeContact(JID(toJID_.getNode(), toJID_.getDomain(), occupant.getNick()));
+	} else {
+		parting_ = true;
+		setEnabled(false);
+	}
 }
 
 void MUCController::handleOccupantPresenceChange(boost::shared_ptr<Presence> presence) {
diff --git a/Swift/Controllers/Chat/MUCController.h b/Swift/Controllers/Chat/MUCController.h
index 65ef84c..7d40ec2 100644
--- a/Swift/Controllers/Chat/MUCController.h
+++ b/Swift/Controllers/Chat/MUCController.h
@@ -35,6 +35,8 @@ namespace Swift {
 			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);
 			~MUCController();
 			boost::signal<void ()> onUserLeft;
+			virtual void setEnabled(bool enabled);
+			void rejoin();
 		
 		protected:
 			void preSendMessageRequest(boost::shared_ptr<Message> message);
diff --git a/Swift/QtUI/Roster/RosterModel.cpp b/Swift/QtUI/Roster/RosterModel.cpp
index d705d34..c2d4147 100644
--- a/Swift/QtUI/Roster/RosterModel.cpp
+++ b/Swift/QtUI/Roster/RosterModel.cpp
@@ -177,7 +177,7 @@ QModelIndex RosterModel::index(RosterItem* item) const {
 	/* Recursive check that it's ok to create such an item 
 		Assuming there are more contacts in a group than groups in a 
 		group, this could save a decent chunk of search time at startup.*/
-	if (parent != roster_->getRoot() && !index(parent).isValid()) {
+	if (parent == NULL || (parent != roster_->getRoot() && !index(parent).isValid())) {
 		return QModelIndex();
 	}
 	for (size_t i = 0; i < parent->getDisplayedChildren().size(); i++) {
diff --git a/Swiften/MUC/MUC.cpp b/Swiften/MUC/MUC.cpp
index b4265b5..91ba043 100644
--- a/Swiften/MUC/MUC.cpp
+++ b/Swiften/MUC/MUC.cpp
@@ -44,6 +44,18 @@ void MUC::part() {
 	presenceSender->removeDirectedPresenceReceiver(ownMUCJID);
 }
 
+void MUC::handleUserLeft(LeavingType type) {
+	std::map<String,MUCOccupant>::iterator i = occupants.find(ownMUCJID.getResource());
+	if (i != occupants.end()) {
+		MUCOccupant me = i->second;
+		occupants.erase(i);
+		onOccupantLeft(me, type, "");
+	}
+	occupants.clear();
+	joinComplete_ = false;
+	presenceSender->removeDirectedPresenceReceiver(ownMUCJID);
+}
+
 void MUC::handleIncomingPresence(boost::shared_ptr<Presence> presence) {
 	if (!isFromMUC(presence->getFrom())) {
 		return;
@@ -79,14 +91,18 @@ void MUC::handleIncomingPresence(boost::shared_ptr<Presence> presence) {
 	//170 is room logging to http
 	//TODO: Nick changes
 	if (presence->getType() == Presence::Unavailable) {
-		std::map<String,MUCOccupant>::iterator i = occupants.find(nick);
-		if (i != occupants.end()) {
-			//TODO: part type
-			onOccupantLeft(i->second, Part, "");
-			occupants.erase(i);
+		if (presence->getFrom() == ownMUCJID) {
+			handleUserLeft(Part);
+			return;
+		} else {
+			std::map<String,MUCOccupant>::iterator i = occupants.find(nick);
+			if (i != occupants.end()) {
+				//TODO: part type
+				onOccupantLeft(i->second, Part, "");
+				occupants.erase(i);
+			}
 		}
-	}
-	else if (presence->getType() == Presence::Available) {
+	} else if (presence->getType() == Presence::Available) {
 		std::map<String, MUCOccupant>::iterator it = occupants.find(nick);
 		MUCOccupant occupant(nick, role, affiliation);
 		if (it != occupants.end()) {
diff --git a/Swiften/MUC/MUC.h b/Swiften/MUC/MUC.h
index 40fc2f6..bccc26c 100644
--- a/Swiften/MUC/MUC.h
+++ b/Swiften/MUC/MUC.h
@@ -25,7 +25,7 @@ namespace Swift {
 	class MUC {
 		public:
 			enum JoinResult { JoinSucceeded, JoinFailed };
-			enum LeavingType { Part };			
+			enum LeavingType { Part, Disconnect };			
 
 		public:
 			MUC(StanzaChannel* stanzaChannel, PresenceSender* presenceSender, const JID &muc);
@@ -36,6 +36,8 @@ namespace Swift {
 			String getCurrentNick();
 			void part();
 			void handleIncomingMessage(boost::shared_ptr<Message> message);
+			/** Expose public so it can be called when e.g. user goes offline */
+			void handleUserLeft(LeavingType);
 
 		public:
 			boost::signal<void (const String& /*nick*/)> onJoinComplete;
diff --git a/Swiften/Roster/GroupRosterItem.cpp b/Swiften/Roster/GroupRosterItem.cpp
index e5490e2..8e4be35 100644
--- a/Swiften/Roster/GroupRosterItem.cpp
+++ b/Swiften/Roster/GroupRosterItem.cpp
@@ -55,6 +55,27 @@ void GroupRosterItem::addChild(RosterItem* item) {
 }
 
 /**
+ * Does not emit a changed signal.
+ */
+void GroupRosterItem::removeAll() {
+	std::vector<RosterItem*>::iterator it = children_.begin();
+	displayedChildren_.clear();
+	while (it != children_.end()) {
+		ContactRosterItem* contact = dynamic_cast<ContactRosterItem*>(*it);
+		if (contact) {
+			delete contact;
+		} 
+		GroupRosterItem* group = dynamic_cast<GroupRosterItem*>(*it);
+		if (group) {
+			group->removeAll();
+			delete group;
+		}
+		it++;
+	}
+	children_.clear();
+}
+
+/**
  * Returns the removed item - but only if it's the only one, otherwise
  * the return result is undefined.
  */
diff --git a/Swiften/Roster/GroupRosterItem.h b/Swiften/Roster/GroupRosterItem.h
index aca2b05..096d053 100644
--- a/Swiften/Roster/GroupRosterItem.h
+++ b/Swiften/Roster/GroupRosterItem.h
@@ -22,6 +22,7 @@ class GroupRosterItem : public RosterItem {
 		const std::vector<RosterItem*>& getDisplayedChildren() const;
 		void addChild(RosterItem* item);
 		ContactRosterItem* removeChild(const JID& jid);
+		void removeAll();
 		void setDisplayed(RosterItem* item, bool displayed);
 		boost::signal<void ()> onChildrenChanged;
 		static bool itemLessThan(const RosterItem* left, const RosterItem* right);
diff --git a/Swiften/Roster/Roster.cpp b/Swiften/Roster/Roster.cpp
index b7ed6c4..e91b843 100644
--- a/Swiften/Roster/Roster.cpp
+++ b/Swiften/Roster/Roster.cpp
@@ -82,6 +82,13 @@ struct JIDEqualsTo {
 	JID jid;
 };
 
+void Roster::removeAll() {
+	root_->removeAll();
+	itemMap_.clear();
+	onChildrenChanged(root_);
+	onDataChanged(root_);
+}
+
 void Roster::removeContact(const JID& jid) {
 	std::vector<ContactRosterItem*>* items = &itemMap_[fullJIDMapping_ ? jid : jid.toBare()];
 	items->erase(std::remove_if(items->begin(), items->end(), JIDEqualsTo(jid)), items->end());
diff --git a/Swiften/Roster/Roster.h b/Swiften/Roster/Roster.h
index 346157e..d54a12e 100644
--- a/Swiften/Roster/Roster.h
+++ b/Swiften/Roster/Roster.h
@@ -31,6 +31,7 @@ class Roster {
 		void addContact(const JID& jid, const JID& displayJID, const String& name, const String& group);
 		void removeContact(const JID& jid);
 		void removeContactFromGroup(const JID& jid, const String& group);
+		void removeAll();
 		void applyOnItems(const RosterItemOperation& operation);
 		void applyOnAllItems(const RosterItemOperation& operation);
 		void applyOnItem(const RosterItemOperation& operation, const JID& jid);
-- 
cgit v0.10.2-6-g49f6